Blocksism Labs Logo

3 minutes read

Solidity: Upgradable Contracts, Tokens

Implementing upgradable smart contracts, tokens in Solidity programming language dedicated for Ethereum blockchain.

Michał Mirończuk
03/01/2021 12:00 AM

Introduction 💬

We are able to compare Blockchain Developer to a sapper who only makes a mistake once. Once smart contracts are deployed in the blockchain, they are immutable from the nature. Although we are only human and anyone can make a mistake, especially when creating a complex system. What if the implemented smart contract turns out to be unrealible 😬, with a bug 😲 or we need to provide new functionality 🙄? Is there any solution to fix the above problems 🤔? The answer is YES 🥳🥳🥳! In this blog post, I write about the possibility of updating smart contracts (tokens), which will allow you to keep up-to-date with smart contracts and enable their expansion. The knowledge we gathered in the previous Solidity: Delegatecall vs Call vs Library blog post will help us understand how upgradeability works in smart contracts underneath.

Proxy Patterns ✏️

Proxy contract architecture predominantly relies on separation of responsibilities to two different contracts. First one, Proxy contract (immutable) which stores all data and the second Logic contract (mutable) which contains the whole logic. For the purposes of this post, we will often refer to Logic contract as Implementation contract. When we initialize transaction, all message calls go through Proxy contract which invokes Implementation contract via delegatecall. In the result, Implementation Contract works in Proxy Contract context. Upgradability while retaining data relies on Implementation Contract replacement by different implementation contract.

Let me present initial status before upgrade on the example below. Note that we are going to want to manipulate value variable with two functions in the contract where the logic resides.

Upgradable smart contracts

After the upgrade, relationship between contract will look like:

Upgradable smart contracts version 2

To update our smart contract, we need to change the address that points to the ImplementationContract (_implAddr variable on the diagram) in ProxyContract to delegate calls to the new contract (ImplementationContractV2 on the diagram). Finally, we will keep our existing data and provide a new implementation.

Let's write some code 🤓😃

Remember: Each version of the implementation contract must follow the same memory layout. What does it exactly mean 🤔? If we take example of the contracts below, there is one contract Proxy and the second contract Implementation. Notice that both contract have the same variables in the same order. By fallback function in Proxy contract we are able to delegate every call to Implementation contract where changes will take place in Proxy contract context.

Proxy Contract

pragma solidity ^0.7.0;

contract ProxyContract {
   uint public value = 0;
   address implAddr;

   constructor(address implementationAddr){
       implAddr = implementationAddr;
   }

   function upgradeContract(address newImplementationContractAddress) public {
       implAddr = newImplementationContractAddress;
   }

    fallback() external payable {
        address implementationAddr = implAddr;
        require(implementationAddr != address(0));

      assembly {
        let ptr := mload(0x40)
        calldatacopy(ptr, 0, calldatasize())
        let result := delegatecall(gas(), implementationAddr, ptr, calldatasize(), 0, 0)
        let size := returndatasize()
        returndatacopy(ptr, 0, size)
        switch result
        case 0 {revert(ptr, size)}
        default {return (ptr, size)}
      }
     }
}

ImplementationContract

pragma solidity ^0.7.0;

contract ImplementationContract {
    uint value;

    function incrementValue() public payable {
        value++; //increment by 1
    }

    function decrementValue() public payable {
        value--; //decrement by 1
    }
}

ImplementationContractV2

pragma solidity ^0.7.0;

contract ImplementationContractV2 {
    uint value;

    function incrementValue() public payable {
        value+=10; //increment by 10
    }

    function decrementValue() public payable {
        value-=10; //decrement by 10
    }
}

Our task is to update the logic of our functionality from ImplementationContract to ImplementationContractV2 while keeping our previous data. First of all we need to deploy ImplementationContract and put it's address on ProxyContract creation and invoke incrementValue(), which should change value variable from 0 to 1. Let us assume that this action will simulate the performance of the system to date. Deploying the ImplementationContractV2 contract and using the upgradeContract(address newImplementationContractAddress) function in ProxyContract we are able to change the address of the logic contract to the new one we want to refer to. (ImplementationContractV2). From now all calls to ProxyContract will be delegated to new logic contract. Since the upgraded contract has a different implementation of the incrementValue() function, which increments value by 10 instead of 1, when it executes, we get 11. This shows that the number in value variable remained the same after the contract was updated.

Note that ALL references to the ProxyContract except upgradeContract() will be delegated to the logic contract. We can only achieve this by using the fallback() function by giving the first 4 bytes of the hash name of the function with parameters in the data. For a better understanding, I will not go into the web3 library. We'll only use one of its functions to get the hash, and I'll do the rest in the Remix IDE.

Remix IDE 🛠️ Working in Remix IDE, we are able to easily check the hash of the function we want to run using console with loaded web3 library.

web3.utils.sha3('incrementValue()');

Where in the value of the sha3 function we give the name of the function we want to refer to.

In the case of the function named incrementValue() we get following hash:

0x3bfd7fd3ec639bc48184934b65e95ac1cf6a287d453af5392bddef068bf12c31;

As I mentioned before we only need 4 first bytes to indicate a function. So the 0x3bfd7fd3 part is enough.

The next step is to call fallback() function and pass those 4 bytes in hexadecimal form in the CALLDATA input of 'Low level interactions' section of ProxyContract. If you do not know where the above-described fields are located, go to the screenshot below.

If you followed my actions in this post and worked as I did in the Remix IDE you should get a similar result as in the screenshot below.

Screenshot

Conclusion ✏️

You just learned how to create upgradable smart contracts. This knowledge will certainly be useful in your projects. It will leave you room for more maneuver and will also make you believe that smart contracts can be more flexible. However, flexibility comes with some risk. As you can see in the example above, we used low-level functions that are inherently risky. That is why I think that it is worth rely on the already existing libraries created by experts who have been working on them for many years, and it is them that we should base on when creating smart contracts.

Tags:
Development

Michał Mirończuk

Blockchain Developer

Experienced Blockchain/Full Stack Developer with a 6-year tenure in the crypto realm.

Member since Mar 15, 2019

Latest Posts

30 Oct 20203 minutes read
Solidity: Delegatecall vs Call vs Library
Michał Mirończuk
Michał Mirończuk
Michał Mirończuk
View All

Get Free
Quote

hire@blocksism.com

Sign Up For Newsletter

Receive 50% discount on first project