Ethereum Smart Contract Upgradeability: Hands-On

Written by ethervolution | Published 2018/04/26
Tech Story Tags: ethereum | upgradeability | smart-contracts | solidity | proxy

TLDRvia the TL;DR App

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:

  1. upgradeability relies on the solidity methoddelegatecall .
  2. 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).
  3. 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.
  4. 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:

  1. You start by deploying your proxy contract Proxy.
  2. Then deploy your logic contract. Here it is TokenVersion1
  3. You tell your proxy contract to point to the TokenVersion1 by calling its function upgradeTo(address of TokenVersion1)
  4. Here comes the tricky part: let’s try to mint some tokens. But we are not going to call the 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.
  5. The proxy contract will load the code of the 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 :)
  6. If the 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…
  7. At this point you noticed that your 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.
  8. Then you deploy your awesome and bug free TokenVersion2 contract.
  9. You tell the proxy to point to this new contract by calling upgradeTo(address of TokenVersion2)
  10. 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 in tokenVersion1 — 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):

  1. https://blog.zeppelinos.org/proxy-patterns/
  2. https://github.com/zeppelinos/labs
  3. https://blog.zeppelin.solutions/proxy-libraries-in-solidity-79fbe4b970fd
  4. https://blog.colony.io/writing-upgradeable-contracts-in-solidity-6743f0eecc88
  5. https://vomtom.at/upgrade-smart-contracts-on-chain/
  6. https://medium.com/aigang-network/upgradable-smart-contracts-what-weve-learned-at-aigang-b181d3d4b668
  7. https://blog.indorse.io/ethereum-upgradeable-smart-contract-strategies-456350d0557c

Published by HackerNoon on 2018/04/26