Just like with every new material, understanding smart contract upgradeability requires to spent quality time on it. Let’s try to shorten this learning curve :)
Lately great articles and resources have been published on the topic and the Zeppelin team (OpenZeppelin and zeppelinOS) really pushed forward the concept of upgradeable smart contracts.
However I am feeling that a dead simple example is missing in those discussions and this is what I would like to share with you.
I am not going to summarizes or give you an overview about upgradeability patterns here. As I said I think there is already enough amazing ressources on the internet and you will need to spend time learning and researching anyway (this is a great starting post). Nevertheless I would like to provide you with an easy start instead. A dummy dead simple upgradeable smart contract.
But before that let’s recapitulate some key points:
- upgradeability relies on the solidity method
delegatecall. - The most basic solution relies on 2 contracts. A proxy contract (for storage) that delegates calls to a logic contract (logic contract can be upgraded. Not the proxy).
delegatecallwill load code from the contract receiving the call. Storage is done on the calling contract. Again, storage is done on the calling contract ! Therefore the proxy contract will hold the state of our upgradeable contract.- Calling a smart contract function that does not exist will trigger its fallback function (if the fallback function is implemented of course). This mechanism is used by the proxy contract.
note: I used the inherited storage pattern from zeppelinos for this example. Code was adapted from their repo.
1. Overview
let’s consider the following smart contracts and this scenario: TokenVersion1 is deployed but suddenly you realize that it contains a bug… too bad. The bug is in the mint function.
pragma solidity ^0.4.21;
contract TokenVersion1 {mapping (address => uint) balances;
event Transfer(address \_from, address \_to, uint256 \_value);
function balanceOf(address \_address) public view returns (uint) {
return balances\[\_address\];
}
function transfer(address \_to, uint256 \_value) public {
require(balances\[msg.sender\] >= \_value);
balances\[msg.sender\] -= \_value;
balances\[\_to\] += \_value;
emit Transfer(msg.sender, \_to, \_value);
}
// there is a bug in this function: value should not
// be multiplied by 2
function mint(address \_to, uint256 \_value) public {
balances\[\_to\] += \_value \* 2;
emit Transfer(0x0, \_to, \_value);
}
}
contract TokenVersion2 is TokenVersion1 {
// bug corrected here: multiplication by 2 removed
function mint(address \_to, uint256 \_value) public {
balances\[\_to\] += \_value;
emit Transfer(0x0, \_to, \_value);
}
}
You do not want to mint double right ? (or maybe you do ^^). If your system is designed to support upgradeable smart contracts the bugged contract TokenVersion1 can be fixed by deploying the contract TokenVersion2 . But for this to work you need what is call a proxy contract for delegating calls to your Token contracts:
pragma solidity ^0.4.21;
/*** @title Proxy* @dev Gives the possibility to delegate any call to a foreign implementation.*/contract Proxy {
address public implementation;
function upgradeTo(address \_address) public {
implementation = \_address;
}
/\*\*
\* @dev Fallback function allowing to perform a delegatecall to the given implementation.
\* This function will return whatever the implementation call returns
\*/
function () payable public {
address \_impl = implementation;
require(\_impl != address(0));
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
let result := delegatecall(gas, \_impl, ptr, calldatasize, 0, 0)
let size := returndatasize
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}
The transaction flow is the following:
transaction flow
2. Hands-On
Let’s walk through the transaction flow (figure just above) and how upgradeability is used:
- You start by deploying your proxy contract
Proxy. - Then deploy your logic contract. Here it is
TokenVersion1 - You tell your proxy contract to point to the
TokenVersion1by calling its functionupgradeTo(address of TokenVersion1) - Here comes the tricky part: let’s try to mint some tokens. But we are not going to call the
mintfunction onTokenVersion1contract directly (we would be bypassing the proxy by doing that, and breaking the upgradeability pattern). Instead we are going the callmint(address, value)directly on the proxy and because this function does not exist, it will trigger the fallback function, firing adeletagecallto the address of theTokenVersion1contract saved in theimplementationvariable. - The proxy contract will load the code of the
mintfunction (thanks to delegatecall) fromtokenVersion1and execute it. Thebalancesmapping andTransferevent of themintfunction are executed and stored in the proxy contract.TokenVersion1WILL NOT store any data and WILL NOT fire any event ! Proxy contract does. And this is exactly why you can upgrade this contract :) - If the
mintfunction was successfully executed, callingbalanceOf(luckyAddress)through the proxy will return you the correct balance (well multiplied by two). However, callingbalanceOf(luckyAddress)directly to theTokenVersion1contract will return you 0 (yes zero). Remember,tokencontract did not executed any code… - At this point you noticed that your
mintfunction has a bug :(luckyAddressgot twice the amount you intended to mint. So you create aTokenVersion2contract that inherits (inherited storage) fromTokenVersion1and you correct themintfunction. - Then you deploy your awesome and bug free
TokenVersion2contract. - You tell the proxy to point to this new contract by calling
upgradeTo(address of TokenVersion2) - Et voilà! :) Your proxy is now delegating calls to the new version of your Token contract. And because the state is stored in the proxy contract, no data was lost ! (yes
luckyAddressstill has double the coins from the bug intokenVersion1— state is persistance across your updates, and that’s the point.)
3. Code
You’ll find a complete working example in my github repo:
salanfe/ethereum_contract_upgradeablitiy_simple_example_ethereum_contract_upgradeablitiy_simple_example - dead simple example of smart contract upgradeability mechanism…_github.com
There is a standalone python script in /python folder. See the file header on how to run it. In a nut shell start ganache and run it :).
If you prefer javascript, you’ll find a test file in /test . Same, instructions are in its header.
4. Ressources
Don’t stop here :) This “dummy” example is just to get you started. Inherited storage is one pattern among a few. The community has come up with at least 2 other patterns: eternal storage and unstructured storage. Here are great ressources (kind of sorted):
- https://blog.zeppelinos.org/proxy-patterns/
- https://github.com/zeppelinos/labs
- https://blog.zeppelin.solutions/proxy-libraries-in-solidity-79fbe4b970fd
- https://blog.colony.io/writing-upgradeable-contracts-in-solidity-6743f0eecc88
- https://vomtom.at/upgrade-smart-contracts-on-chain/
- https://medium.com/aigang-network/upgradable-smart-contracts-what-weve-learned-at-aigang-b181d3d4b668
- https://blog.indorse.io/ethereum-upgradeable-smart-contract-strategies-456350d0557c
