Smart contracts: Upgrading and user versioning
Everything that doesn't update becomes rapidly obsolete. This is natural, but smart contracts provide value to society in large part because they are unchangeable. The main challenges to smart-contract upgradeability is to keep up with rapidly evolving digital world, without betraying the promise of immutability offered by it's own code.
In order to avoid giving the contract developer supreme power over upgradeability we need a mechanism for the users to decide which version they would like to use. Voting mechanisms can offer a path for upgradeability, where each user can vote on when and which contract to upgrade to, but the minority are unwillingly forced to follow along. What if we let each user choose their own preferred version and run the contract versions in parallel. This method was introduced to me by Ali Azam at Devcon IV in Prague.
This method of upgrading contracts leaves previous versions of the contract intact, while offering updated alternatives. Caution needs to be taken, as the different versions will be sharing a storage state, and thus the upgraded version must take into account that users will changing the contract state in different ways. For more information on ' proxy patterns' and alternative methods, check out Jack Tanner who overviews the different methods for upgrading contracts. ZeppelinOS uses proxy patterns for upgradeability as well. The unstructured storage pattern revolves around a 'Proxy contract', which is the 'front-door' of the smart contract system. You can view an example implementation I made here.
The Proxy
The Proxy contract has little functionality, and most transactions will end up in it's fallback function, which will copy the incoming calldata and use DelegateCall to run a function from whichever contract the user prefers. If the implementation contract reverts, the proxy contract will receive 0 as a result and will also revert.
https://gist.github.com/kyledewy/557253586ed3f109c94f3593b4ad43c9
Variables in Solidity get lined up in storage one after another, so when your updating contracts you have to make sure to maintain this storage alignment. For this reason we don't want the Proxy contract to store any variables, since the implementation contract will overwrite these storage spots. To avoid taking up storage spots in the Proxy, we set constant variables which are stored somewhere within an impossibly large hash map. In this case they are stored at location sha3("implementation.address") and sha3("resolver.address")
bytes32 private constant IMPLEMENTATION_SLOT = keccak256(abi.encodePacked("implementation.address"));
bytes32 private constant RESOLVER_SLOT = keccak256(abi.encodePacked("resolver.address"));
The resolver slot is where we will store the resolver contract which manages user preferences and the implementation slot is where we are storing the default implementation contract.
So if your implementation contract is an ERC20 token, then you would call transfer() on the Proxy contract. Since the proxy contract doesn't have a method called 'transfer()' the transaction ends up in the fallback function of our Proxy contract. The fallback function dissects the calldata and looks to see if the implementing contract contains the function called transfer(). If so, then it uses 'DelegateCall' on the implementation contract to execute the transfer() function as if the code were contained within the Proxy contract itself.
Remember that all the storage variables are actually lined up one above another in the Proxy contract, and so the implementation contract can very easily overwrite other variables if variables are not stacked in the same order. For this reason when deploying an upgrade, you should inherit the previous implementation contract to avoid overlapping storage. When you inherit, the storage layout is maintained and any additional variables you add, will be laid out underneath the previous declarations. I made an example contract, which stores the largest bytes32 value it can hash from a string. I extend the contract "Example with Example2" , which overrides setHighestHash() to add a nonce variable, keeping track of how many times a new highest hash is discovered.
The Resolver
The resolver contract is made up to two mappings.
1.) Which contracts are valid
2.) Which contract the user prefers to use
mapping (address => address) public userVersion;
mapping (address => bool) public validImplementation;
When the fallback function of the Proxy contract receive a transaction it checks the resolver contract if the msg.sender has a preferred version to use. If not it uses the latest version defined by the owner.
https://gist.github.com/kyledewy/9475bbaae1d146122b6f2d5b054be6e3
Note that this code is not heavily tested and is only written as an example. Use at your own risk.
Congratulations @kyledewhurst! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :
You can view your badges on your Steem Board and compare to others on the Steem Ranking
If you no longer want to receive notifications, reply to this comment with the word
STOP
To support your work, I also upvoted your post!
Vote for @Steemitboard as a witness to get one more award and increased upvotes!
Congratulations @kyledewhurst! You received a personal award!
You can view your badges on your Steem Board and compare to others on the Steem Ranking
Do not miss the last post from @steemitboard:
Vote for @Steemitboard as a witness to get one more award and increased upvotes!