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).
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.- 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
TokenVersion1
by 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
mint
function onTokenVersion1
contract 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 adeletagecall
to the address of theTokenVersion1
contract saved in theimplementation
variable. - The proxy contract will load the code of the
mint
function (thanks to delegatecall) fromtokenVersion1
and execute it. Thebalances
mapping andTransfer
event of themint
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 :) - If the
mint
function was successfully executed, callingbalanceOf(luckyAddress)
through the proxy will return you the correct balance (well multiplied by two). However, callingbalanceOf(luckyAddress)
directly to theTokenVersion1
contract will return you 0 (yes zero). Remember,token
contract did not executed any code… - At this point you noticed that your
mint
function has a bug :(luckyAddress
got twice the amount you intended to mint. So you create aTokenVersion2
contract that inherits (inherited storage) fromTokenVersion1
and you correct themint
function. - Then you deploy your awesome and bug free
TokenVersion2
contract. - 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
luckyAddress
still 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