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:
delegatecall
.delegatecall
will 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.note: I used the inherited storage pattern from zeppelinos for this example. Code was adapted from their repo.
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
Let’s walk through the transaction flow (figure just above) and how upgradeability is used:
Proxy
.TokenVersion1
TokenVersion1
by calling its function upgradeTo(address of TokenVersion1)
mint
function on TokenVersion1
contract directly (we would be bypassing the proxy by doing that, and breaking the upgradeability pattern). Instead we are going the call mint(address, value)
directly on the proxy and because this function does not exist, it will trigger the fallback function, firing a deletagecall
to the address of the TokenVersion1
contract saved in the implementation
variable.mint
function (thanks to delegatecall) from tokenVersion1
and execute it. The balances
mapping and Transfer
event of the mint
function are executed and stored in the proxy contract. TokenVersion1
WILL NOT store any data and WILL NOT fire any event ! Proxy contract does. And this is exactly why you can upgrade this contract :)mint
function was successfully executed, calling balanceOf(luckyAddress)
through the proxy will return you the correct balance (well multiplied by two). However, calling balanceOf(luckyAddress)
directly to the TokenVersion1
contract will return you 0 (yes zero). Remember,token
contract did not executed any code…mint
function has a bug :( luckyAddress
got twice the amount you intended to mint. So you create a TokenVersion2
contract that inherits (inherited storage) from TokenVersion1
and you correct the mint
function.TokenVersion2
contract.upgradeTo(address of TokenVersion2)
luckyAddress
still has double the coins from the bug in tokenVersion1
— state is persistance across your updates, and that’s the point.)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.
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):