Thank you for your interest in this post! We’re undergoing a rebranding process, so please excuse us if some names are out of date. Also have in mind that this post might not reference the latest version of our products. For up-to-date guides, please check our documentation site.
The examples showed in this post are just for learning purposes, do not use them in production.
During the last months we’ve been designing and developing a prototype of the zeppelin_os Kernel. Part of that work was to explore and compare different upgradeability mechanisms to decide which to use in zeppelin_os.
All of the approaches we’ve been researching use a proxy contract that delegates calls to a behavior contract. The address of the behavior contract is the configurable part of the proxy, providing a way to upgrade it 1.
However, the main problem of all these patterns is that they require the developer to be very careful when handling storage, since it is shared both by the proxy and the behavior. Let’s see how this looks like:
[contract OwnedUpgradeabilityProxy is Proxy, UpgradeabilityStorage { function upgradeTo(address newImplementation) public onlyProxyOwner { address currentImplementation = implementation(); setImplementation(newImplementation); } ... } contract UpgradeabilityStorage { address internal _proxyOwner; address internal _implementation; function proxyOwner() public view returns(address) { return _proxyOwner; } function implementation() public view returns(address) { return _implementation; } ... } contract BasicTokenis UpgradeabilityStorage { uint256 internal _totalSupply; mapping(address => uint256) internal _balances; function totalSupply() public view returns(uint256) { return _totalSupply; } ... }
As you can see in BasicToken
inheriting from UpgradeabilityStorage
, upgradeable contracts have to be aware of the storage structure declared in the proxy contracts. Therefore, with this approach, every contract requires a considerable modification to be upgradeable.
While building our initial implementation of the kernel, this was a problem. In order to provide an upgradeable version of the OpenZeppelin contracts, this approach would have required many modifications to the code.
The new approach
Fortunately, we’re happy to announce a new upgradeability approach that solves large part of the problems mentioned above. The idea is to change the way upgradeability-required data gets stored, i.e. the implementation address and the proxy owner. To do this, we use fixed storage slots that can be accessed, either for writing or reading, through inline assembly. Let’s take a look at the next example:
[contract OwnedUpgradeabilityProxy is Proxy { bytes32 private constant ownerPosition = keccak256("org.zeppelinos.proxy.owner"); bytes32 private constant implementationPosition = keccak256("org.zeppelinos.proxy.implementation"); function upgradeTo(address newImplementation) public onlyProxyOwner { address currentImplementation = implementation(); setImplementation(newImplementation); } function implementation() public view returns(address impl) { bytes32 position = implementationPosition; assembly { impl: = sload(position) } } function setImplementation(address newImplementation) internal { bytes32 position = implementationPosition; assembly { sstore(position, newImplementation) } } function proxyOwner() public view returns(address owner) { bytes32 position = proxyOwnerPosition; assembly { owner: = sload(position) } } function setUpgradeabilityOwner(address newProxyOwner) internal { bytes32 position = proxyOwnerPosition; assembly { sstore(position, newProxyOwner) } } ... }
As the code snippet shows, we can now handle the storage of the upgradeability-required variables without actually defining them in Solidity. The sload
and sstore
opcodes let us manage the storage to read and write from fixed slots referenced by custom pointers (in our example, keccak256
of org.zeppelinos.proxy.owner
and org.zeppelinos.proxy.implementation
). Thus, the Solidity compiler won’t take them into account when assigning storage slots for the contract. This means there is no need to track them in every implementation contract.
Security Analysis
Now, this approach could be an issue if Solidity attempted to use one of these storage slots for a state variable of the behavior contract. For instance, if we fix the implementationPosition
to 0x4
, then a contract with a few uint
state variables would step over it, causing undesired overwriting of the proxy implementation address.
However, based on how Solidity maps state variables to storage, there is no possibility of a state variable accidentally being mapped to one of these positions. You can take a look at the full implementation in our labs repo. We invite the community to audit our approach and we’re happy to talk if you find any problems.
Example
If you want to make your contract upgradeable you just need to deploy a proxy, and then ask the proxy to point to an initial version of your contract code as is. The only thing you will have to care about is that further versions of your contract have to follow the storage structure of the previous ones.
For example, suppose you have the following contracts:
[contract BasicToken { uint256 internal _totalSupply; mapping(address => uint256) internal _balances; function totalSupply() public view returns(uint256) { return _totalSupply; } function balanceOf(address owner) public view returns(uint256) { return _balances[owner]; }... } contract StandardToken is BasicToken { mapping(address => mapping(address => uint256)) internal _allowances; function transferFrom(address _from, address _to, uint256 _value) public returns(bool) { ... } function approve(address _spender, uint256 _value) public returns(bool) { ... }... }
You can now have an upgradeable token as follows:
> owner = web3.eth.accounts[1] > proxy = OwnedUpgradeabilityProxy.new({ from: owner }) > basicTokenBehavior = BasicToken.new() > proxy.upgradeTo(basicTokenBehavior.address, { from: owner }) > basicToken = BasicToken.at(proxy.address) > basicToken.balanceOf(owner) // 0 > user = web3.eth.accounts[2] > basicToken.approve(user, 100) // Revert, no approve method in BasicToken > standardTokenBehavior = StandardToken.new() > proxy.upgradeTo(standardTokenBehavior.address, { from: owner }) > standardToken = StandardToken.at(proxy.address) > standardToken.approve(user, 100) // true, approve method added in upgrade
Conclusion
Given the huge advantages of this approach, where we almost don’t need to modify our contracts to make them upgradeable, we’re using it in the upcoming release of the zeppelin_os Kernel.
Stay tuned for an upcoming post explaining the different approaches we are exploring for zeppelin_os. In the meantime, you can check the source code out in our labs repo.
1 To better understand how upgradeability works using proxy contracts, you can read more here.