🎓 A1Learner
🔍
0 of 48 lessons completed

Solidity Proxy Concepts

A proxy is a contract that holds state and forwards calls to a separate logic contract. The logic contract holds the code, while the proxy holds the values, so one logic contract can serve many proxies.

A value set through the proxy reads back through the proxy

The logic contract sets a value. The call goes through the proxy, so the proxy stores the value.

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

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


contract Proxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    constructor(address impl) {
        _setImplementation(impl);
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    function _setImplementation(address impl) internal {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, 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()) }
        }
    }
}

The logic contract keeps its own value untouched

Setting a value through the proxy changes proxy storage only. The logic contract still reads its own starting value.

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

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


contract Proxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    constructor(address impl) {
        _setImplementation(impl);
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    function _setImplementation(address impl) internal {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, 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()) }
        }
    }
}

Two proxies share one logic contract with separate values

Both proxies point at the same logic contract. Each proxy still stores its own value.

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

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


contract Proxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    constructor(address impl) {
        _setImplementation(impl);
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    function _setImplementation(address impl) internal {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, 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 Counter {
    uint256 public value;
    function setValue(uint256 v) public {
        value = v;
    }
    function increment() public {
        value += 1;
    }
}


contract Proxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    constructor(address impl) {
        _setImplementation(impl);
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    function _setImplementation(address impl) internal {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, 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

  • Proxy: a contract that stores state and forwards calls to a logic contract.
  • Logic contract: a contract that holds the code the proxy runs.
  • Delegatecall: the forwarding call that runs logic code in the proxy storage.
  • Shared logic: many proxies can point at one logic contract and keep separate values.
  • State stays put: a value set through the proxy lives in the proxy, not in the logic contract.

In this example, a counter logic contract sets and raises a value, and a proxy forwards both calls. The proxy ends at 7 while the logic contract itself stays at 0, which shows the state lives in the proxy.

Note

the proxy keeps its logic address in a separate slot, so it never collides with logic values.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

Where does a value set through a proxy live?