Understanding Proxy Forwarding & delegatecall

Hi,

I'm reading this part about the Proxy forward.

A very important thing to note is that the code makes use of the EVM’s delegatecall opcode which executes the callee’s code in the context of the caller’s state. That is, the logic contract controls the proxy’s state and the logic contract’s state is meaningless. Thus, the proxy doesn’t only forward transactions to and from the logic contract, but also represents the pair’s state. The state is in the proxy and the logic is in the particular implementation that the proxy points to.

Based on my understanding of the upgrade mechanism and the assembly code listed in the page, wanna confirm my below thinking is correct.

When are the memory slots of variables declared impl contract allocated on proxy contract?
Only when we call some initialize functions, it will allocate corresponding memory slot for those variables in proxy contract.

Example impl contract

contract MyToken is
    Initializable,
    ERC20Upgradeable,
    AccessControlUpgradeable,
    UUPSUpgradeable
{
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    int128 public x;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}

    function initialize() public initializer {
        x = 100;
        __ERC20_init("MyToken", "MTK");
        __AccessControl_init();
        __UUPSUpgradeable_init();

        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(UPGRADER_ROLE, msg.sender);
    }

    function getX() public view returns (int128) {
        return x;
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        override
        onlyRole(UPGRADER_ROLE)
    {}
}

e.g. the x = 100 behavior.

What if we didn't initialize a field at all and then trying to get its value?
Example impl contract:

contract MyToken is
    Initializable,
    ERC20Upgradeable,
    AccessControlUpgradeable,
    UUPSUpgradeable
{
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    int128 public x;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}

    function initialize() public initializer {
        // didn't initialize 
        // x = 100;
        __ERC20_init("MyToken", "MTK");
        __AccessControl_init();
        __UUPSUpgradeable_init();

        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(UPGRADER_ROLE, msg.sender);
    }

    function getX() public view returns (int128) {
        return x;
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        override
        onlyRole(UPGRADER_ROLE)
    {}
}

Let's say we didnt initalize the x anywhere. The proxy will not declared the memory slot in its realm right? And if we call getX(), what will happen?

From my experiment, it will return 0 which is the default value of this type.

  • How I understand the return value here? Is it meaningful. Like we when I call getX(), the compiled assembly will check if there's a corresponding slot for x, if not, declare new slot on the way.
  • I think above question is more like how should I understand the delegatecall works. Like, whenever a delegatecall happens, the whole impl's context (memory slots etc.) will be copied into proxy's domain?

Sorry for many details asking and thanks in advance.

Storage is not "allocated", but simply read/written.

The layout of the state variables in your implementation contract and its parents determines which slots are used for those variables, but nothing gets copied into the proxy. Through the delegatecall, the proxy's storage slots will simply be accessed instead of the implementation address's storage slots. The implementation address's storage slots are not used in this case.

If you declare a state variable in your implementation but never read/write to it, the proxy's corresponding storage slot is simply not used (but its position is reserved in terms of storage layout). Reading from a slot that was not initialized will return 0 since storage is zero by default.

Let me know if you have further questions.

1 Like

Ah~~~that's super clear.

So the trick is that both Poxy and impl follow the same storage management mechanism. As long as the new impl doesn't break the consistency, there will always be a slot there in proxy for impl' fields.

1 Like