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.
As mentioned in our Development Roadmap Part Two, the Kernel is the foundational layer of zeppelin_os, and we will soon be launching a prototype of it, starting with the functionality currently found in the OpenZeppelin framework.
We’re happy to announce another step towards launching the zeppelin_os Kernel. We’re now investigating different upgradeability mechanisms. As you may have read, there are several approaches to implementing contract upgradeability, like proxy libraries or eternal storage, among others. We’ve been working on a solution that combines some of these patterns.
The approach consists in having a Proxy
that delegates calls to specific implementations which can be upgraded, assuming that the storage structure won’t ever change. This scenario is referred to as “Eternal Storage”. Let’s see how the Proxy
contract looks like:
[contract Proxy { function implementation() public view returns (address); function () payable public { address _impl = implementation(); require(_impl != address(0)); bytes memory data = msg.data; assembly { let result := delegatecall(gas, _impl, add(data, 0x20), mload(data), 0, 0) let size := returndatasize let ptr := mload(0x40) returndatacopy(ptr, 0, size) switch result case 0 { revert(ptr, size) } default { return(ptr, size) } } } }
As you can see, given the proxy uses delegatecall
to resolve the requested behaviors, the upgradeable contract’s state will be stored in the proxy contract itself.
Let’s analyze how this would work by building an upgradeable ERC20 token contract. To do this, we will need to manage two really different kinds of data, one related to the upgradeability mechanism and another strictly related to the token contract domain. Naming plays a really important role here if we want to express correctly what’s going on. This is the proposed model:
Upgradable token model using proxies and eternal storageProxy
, UpgradeabilityProxy
and UpgradeabilityStorage
are generic contracts that can be used to implement upgradeability through proxies. In this example we use them to implement an upgradeable ERC20 token:
[contract UpgradeabilityProxy is Proxy, UpgradeabilityStorage { event Upgraded(string version, address indexed implementation); function upgradeTo(string version, address implementation) public { require(_implementation != implementation); _version = version; _implementation = implementation; Upgraded(version, implementation); } } contract UpgradeabilityStorage { string internal _version; address internal _implementation; function version() public view returns (string) { return _version; } function implementation() public view returns (address) { return _implementation; } }
The UpgradeabilityStorage
contract holds data needed for upgradeability, while the TokenStorage
contract holds token-related information. It defines the token-specific storage:
[contract TokenStorage { uint256 internal _totalSupply; mapping (address => uint256) internal _balances; mapping (address => mapping (address => uint256)) internal _allowances; }
On the other hand, TokenProxy
extends from UpgradeabilityProxy
(which in turn extends from UpgradeabilityStorage
), and then from the TokenStorage
contract:
[contract TokenProxy is UpgradeabilityProxy, TokenStorage {}
TokenProxy
is the contract that will delegate calls to specific implementations of the ERC20 token behavior. These behaviors are the code that can be upgraded by the token developer, for example Token_V0
and Token_V1
:
[contract Token_V0 is UpgradeableTokenStorage { using SafeMath for uint256; event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); function totalSupply() public view returns (uint256) {...} function balanceOf(address owner) public view returns (uint256) {...} function transfer(address to, uint256 value) public {...} function transferFrom(address from, address to, uint256 value) public {...} function approve(address spender, uint256 value) public {...} } contract Token_V1 is UpgradeableTokenStorage { using SafeMath for uint256; event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); function mint(address to, uint256 value) public {...} function burn(uint256 value) public {...} function totalSupply() public view returns (uint256) {...} function balanceOf(address owner) public view returns (uint256) {...} function transfer(address to, uint256 value) public {...} function transferFrom(address from, address to, uint256 value) public {...} function approve(address spender, uint256 value) public {...} }
Example of a token behavior upgrade to provide mint and burn functionalities
As you can see, token behavior implementations extends from UpgradeableTokenStorage
, which derives from UpgradeableTokenStorage
and TokenStorage
. This is what every implementation of the upgradeable behavior needs:
[contract UpgradeableTokenStorage is UpgradeabilityStorage, TokenStorage {}
Notice that inheritance order in TokenProxy
needs to be the same as the one in UpgradeableTokenStorage
, to respect storage structure (given we are using delegatecall
). Moreover, we are not defining any new state variables in the token behavior implementation contracts. This is a requirement of the proposed approach to ensure the proxy storage is not messed up.
Thanks to the Winding Tree and Aragon teams for their collaboration on this research and development.