Smart contracts: Upgrading and user versioning

in #ethereum5 years ago (edited)

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.

Github
Twitter

Sort:  

Congratulations @kyledewhurst! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

You distributed more than 10 upvotes. Your next target is to reach 50 upvotes.

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!

Happy Birthday! - You are on the Steem blockchain for 2 years!

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:

SteemFest⁴ commemorative badge refactored
Vote for @Steemitboard as a witness to get one more award and increased upvotes!

Coin Marketplace

STEEM 0.19
TRX 0.13
JST 0.030
BTC 62907.89
ETH 3379.73
USDT 1.00
SBD 2.50