🎓 A1Learner
🔍
0 of 48 lessons completed

Solidity Upgrade Patterns

An upgrade pattern is a plan for replacing a proxy logic contract with a new version. The proxy keeps its state, while the logic contract changes underneath it.

A value set before an upgrade survives it

The first logic version sets a value through the proxy. After the upgrade, the new version reads the same value.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract V1 {
    uint256 public value;
    function setValue(uint256 v) public {
        value = v;
    }
}


contract V2 {
    uint256 public value;
    function setValue(uint256 v) public {
        value = v;
    }
}


contract Proxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
    constructor(address impl) {
        _setImplementation(impl);
        _setOwner(msg.sender);
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    function owner() public view returns (address admin) {
        bytes32 slot = ADMIN_SLOT;
        assembly { admin := sload(slot) }
    }
    function _setImplementation(address impl) internal {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
    }
    function _setOwner(address admin) internal {
        bytes32 slot = ADMIN_SLOT;
        assembly { sstore(slot, admin) }
    }
    function upgrade(address impl) public {
        require(msg.sender == owner(), "only owner");
        _setImplementation(impl);
    }
    fallback() external payable {
        address impl = implementation();
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Only the owner can upgrade the proxy

A stranger trying to upgrade is refused. The owner upgrade succeeds and points the proxy at the new logic.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract V1 {
    uint256 public value;
    function setValue(uint256 v) public {
        value = v;
    }
}


contract V2 {
    uint256 public value;
    function setValue(uint256 v) public {
        value = v;
    }
}


contract Proxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
    constructor(address impl) {
        _setImplementation(impl);
        _setOwner(msg.sender);
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    function owner() public view returns (address admin) {
        bytes32 slot = ADMIN_SLOT;
        assembly { admin := sload(slot) }
    }
    function _setImplementation(address impl) internal {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
    }
    function _setOwner(address admin) internal {
        bytes32 slot = ADMIN_SLOT;
        assembly { sstore(slot, admin) }
    }
    function upgrade(address impl) public {
        require(msg.sender == owner(), "only owner");
        _setImplementation(impl);
    }
    fallback() external payable {
        address impl = implementation();
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

An upgrade can add a new function

The new logic version adds an increment function. It works on the value the old version stored.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract V1 {
    uint256 public value;
    function setValue(uint256 v) public {
        value = v;
    }
}


contract V2 {
    uint256 public value;
    function setValue(uint256 v) public {
        value = v;
    }
    function increment() public {
        value += 1;
    }
}


contract Proxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
    constructor(address impl) {
        _setImplementation(impl);
        _setOwner(msg.sender);
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    function owner() public view returns (address admin) {
        bytes32 slot = ADMIN_SLOT;
        assembly { admin := sload(slot) }
    }
    function _setImplementation(address impl) internal {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
    }
    function _setOwner(address admin) internal {
        bytes32 slot = ADMIN_SLOT;
        assembly { sstore(slot, admin) }
    }
    function upgrade(address impl) public {
        require(msg.sender == owner(), "only owner");
        _setImplementation(impl);
    }
    fallback() external payable {
        address impl = implementation();
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract V1Score {
    uint256 public score;
    function setScore(uint256 v) public {
        score = v;
    }
}


contract V2Score {
    uint256 public score;
    function setScore(uint256 v) public {
        score = v;
    }
    function doubleScore() public {
        score *= 2;
    }
}


contract Proxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
    constructor(address impl) {
        _setImplementation(impl);
        _setOwner(msg.sender);
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    function owner() public view returns (address admin) {
        bytes32 slot = ADMIN_SLOT;
        assembly { admin := sload(slot) }
    }
    function _setImplementation(address impl) internal {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
    }
    function _setOwner(address admin) internal {
        bytes32 slot = ADMIN_SLOT;
        assembly { sstore(slot, admin) }
    }
    function upgrade(address impl) public {
        require(msg.sender == owner(), "only owner");
        _setImplementation(impl);
    }
    fallback() external payable {
        address impl = implementation();
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Key points

  • Upgrade: replacing the logic contract a proxy points at.
  • State kept: proxy values survive an upgrade because they live in the proxy.
  • Transparent proxy: admin calls are handled by the proxy itself, while user calls are forwarded.
  • UUPS proxy: the upgrade function lives in the logic contract instead of the proxy.
  • Risk warning: an upgrade is never free of risk, since a layout mistake corrupts stored values.

In this example, a score contract sets a score of 10 through a proxy and then upgrades to a version with a doubling function. The upgraded version doubles the kept score to 20.

Note

compare the patterns before choosing one, and test every upgrade on a copy first.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

In a UUPS proxy, where does the upgrade function live?