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?
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:
- Deploy Example
- Deploy the Dispatcher using the Example address as the Dispatchers constructor argument.
- 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:
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:
- Update the Example Contract, for example return 2* the value in getUint()
- Deploy the Example Contract
- Copy the Example Contract address
- 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?