Skip to content

The First Proxy Contract

The first proxy that was ever proposed (to my best knowledge), came from Nick Johnson. If you don't know him, he's founder and lead dev at the ENS (Ethereum Name Service). Also, make sure to checkout his twitter, he's quite active. And he's always ahead of time, literally: he's from New Zealand - GMT+13.

The proxy looks like this. I believe it was written for Sol 0.4.0 (or alike), since later Solidity version would require function visibility specifiers and an actual pragma line.

So, here is a copy of the same Smart Contract ported to Solidity 0.8.1 and stripped of any comments and the replace-method made public so that we can actually replace Smart Contracts. Again, it's a simplified version without any governance or control, simply showing the upgrade architecture:

//SPDX-License-Identifier: No-Idea!

pragma solidity 0.8.1;

abstract contract Upgradeable {
    mapping(bytes4 => uint32) _sizes;
    address _dest;

    function initialize() virtual public ;

    function replace(address target) public {
        _dest = target;
        target.delegatecall(abi.encodeWithSelector(bytes4(keccak256("initialize()"))));
    }
}

contract Dispatcher is Upgradeable {

    constructor(address target) {
        replace(target);
    }

    function initialize() override public{
        // Should only be called by on target contracts, not on the dispatcher
        assert(false);
    }

    fallback() external {
        bytes4 sig;
        assembly { sig := calldataload(0) }
        uint len = _sizes[sig];
        address target = _dest;

        assembly {
            // return _dest.delegatecall(msg.data)
            calldatacopy(0x0, 0x0, calldatasize())
            let result := delegatecall(sub(gas(), 10000), target, 0x0, calldatasize(), 0, len)
            return(0, len) //we throw away any return data
        }
    }
}

contract Example is Upgradeable {
    uint _value;

    function initialize() override public {
        _sizes[bytes4(keccak256("getUint()"))] = 32;
    }

    function getUint() public view returns (uint) {
        return _value;
    }

    function setUint(uint value) public {
        _value = value;
    }
}

So, what's going on here? Before we try the contract, let me quickly explain the assembly in the fallback function.

What happens is basically a delegatecall to the Example Smart Contract. What's a delegate call anyways?

Delegatecall from the Solidity Docs

There exists a special variant of a message call, named delegatecall which is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values.

If that doesn't tell you much: Instead of running the code of the target contract on the target contracts address, we're running the code of the target contract on the contract that called the target. WOOH! Complicated sentence.

Let's play around and you see where this is going:

  1. Deploy Example
  2. Deploy the Dispatcher using the Example address as the Dispatchers constructor argument.
  3. Tell Remix that the Example Contract is now running on the Dispatcher address.

then deploy the dispatcher:

then use the Example on the Dispatchers address:

Storage Pointer

Attention: This implementation only works, because the Upgradeable contract has the target address on storage slot 0. If you're interested why the other implementations use mload(0x40) and what happens here with the storage pointers, then checkout the following guide from OpenZeppelin, which explains this quite elegantly.

In the Example-via-Dispatcher Contract, set a uint and get a uint. Voilà, variables are stored correctly, although our Dispatcher doesn't know any setUint or getUint functions. It also doesn't inherit from Example.

Pretty cool!

This will essentially use the Dispatcher as a storage, but use the logic stored on the Example contract to control what happens. Instead of the Dispatcher "talking to" the Example contract, we're now moving the code of the Example contract into the scope of the Dispatcher and executing it there - changing the Dispatchers storage. That is a huge difference to before with the EternalStorage pattern.

The op-code delegatecall will "move" the Example contract into the Dispatcher and use the Dispatchers storage.

It's a great example of a first proxy implementation. Especially, considering it was early days for Solidity development, that was quite forward thinking!

Let's say we want to upgrade our Smart Contract returning 2* the uint value from getUint():

//... more code
contract Example is Upgradeable {
    uint _value;

    function initialize() override public {
        _sizes[bytes4(keccak256("getUint()"))] = 32;
    }

    function getUint() public view returns (uint) {
        return _value*2;
    }

    function setUint(uint value) public {
        _value = value;
    }
}

That's how you can upgrade your logic contract using the replace method:

  1. Update the Example Contract, for example return 2* the value in getUint()
  2. Deploy the Example Contract
  3. Copy the Example Contract address
  4. Call replace in the Dispatcher with the new Example Contract address

then call replace:

You can still use the old instance, it will return now 2* the value.

Obviously there's a lot going on under the hood. And this is not the end of the whole story, but it's the beginning of how Proxies work internally.

It has a great dis-advantage though: You need to extend from the Upgradeable Smart Contract in all Contracts that are using the Dispatcher, otherwise you will get Storage collisions.

But what are Storage Collisions anyways?


Last update: March 28, 2022