Before you go, check out these stories!

0
Hackernoon logo Smart Contract Versioning by@vasa

Smart Contract Versioning

Author profile picture

@vasaVaibhav Saini

Entrepreneur | Co-founder @tbc_inc, an MIT CIC incubated startup | Speaker |https://vaibhavsaini.com

Book a call
with @vasa

How to Write Upgradable Smart Contracts

Smart contracts areย immutable, by design. On the other hand, software quality heavily depends on the ability to upgrade and patch source code to produce iterative releases. Even though blockchain-based software profits significantly from the technologyโ€™s immutability, still a certain degree of mutability is needed for bug fixing and potential product improvements.

  1. In this post, we are going to learn:
  2. Why do we need to upgrade smart contracts?
  3. Understanding how upgrades work under the hood?
  4. Usingย OpenZeppelin CLIย to easily write/manage "upgradable" smart contracts.
  5. Upgrading Contracts Programmatically using theย OpenZeppelin Upgradesย library.
  6. A few Limitations & Workarounds of Upgradable Contracts

If you are just looking for a way to write upgradable contracts and don't want to go through "how this all works", then just head over to theย 3rd section.

In case you get stuck somewhere or have any doubts, let us knowย here.

Why do we need to upgrade?

Smart contracts in Ethereum areย immutableย by default. Once you create them there is no way to alter them, effectively acting as an unbreakable contract among participants.

However, there are several scenarios where we wish if there was a way to upgrade the contracts. There are a lot of examples in whichย millions of dollars worth of Ether were stolen/hacked, which could be saved if we could update the smart contracts.

How do upgrades work under the hood?

There can be several ways we can upgrade our contracts.

The most obvious way will be something like this:

  1. Create & deploy a new version of the contract.
  2. Manually migrate all states from the old contract to the new contract.
  3. This seems to work, but has several problems.

  1. Migrating the contract state can be expensive.
  2. As we create & deploy a new contract, theย contract addressย will change. So you would need to update all contracts that interacted with the old contract to use the address of the new version.
  3. You would also have to reach out to all your users and convince them to start using the new contract and handle both contracts being used simultaneously, as users are slow to migrate.

A better way is to use aย proxyย contract with anย interfaceย where each methodย delegatesย to theย implementationย contract (which contains all the logic).

Aย delegate callย is similar to a regular call, except that all code is executed in the context of the caller (proxy),ย notย of the callee (implementation). Because of this, aย transferย in the implementation contractโ€™s code will transfer the proxyโ€™s balance, and any reads or writes to the contract storage will read or write from the proxyโ€™s storage.

This approach is better because the users only interact with theย proxyย contract and we can change theย implementationย contract while keeping the sameย proxyย contract.

This seems better than the previous approach, but if we need to make any changes to theย implementationย contract methods, we would need to update theย proxyย contract's methods too (as the proxy contract has interface methods). Hence, users will need to change the proxy address.

To solve this problem, we can use aย fallbackย function in the proxy contract. Theย fallbackย function will execute on any request, redirecting the request to theย implementationย and returning the resulting value (usingย opcodes). This is similar to the previous approach, but here the proxy contract doesnโ€™t have interface methods, only a fallback function, so there is no need to change the proxy address if contract methods are changed.

This was a basic explanation which is enough for us to work with upgradable contracts. In case, you want to dig deep into proxy contract code and different proxy patterns, then check out this post.

How Upgradable Smart Contracts Work Under the Hood

OpenZeppelin Upgrades

As we saw above, there are a lot of things you need to manage while writing upgradeable contracts.

Fortunately, projects likeย OpenZeppelinย have builtย CLI tools & libraries, which provide an easy to use, simple, robust, and opt-in upgrade mechanism for smart contracts thatย can be controlled by any type of governance, be it a multi-sig wallet, a simple address or a complex DAO.

Let's first build a basic upgradeable contract using the OpenZeppelin CLI tool. You can find theย code for the below implementationย here.

In case you get stuck somewhere or have any doubts, let us knowย here.

OpenZeppelin Upgrades CLI

Working with OpenZeppelin CLI requiresย Node.jsย for development. If you don't have it already, install node using whatever package manager you prefer orย using the official installer.

Project Setup

Create a folder namedย upgradable-smart-contractsย and go inside the folder.

$ mkdir upgradable-smart-contracts && cd upgradable-smart-contracts 
Throughout this tutorial, we use theย $ย character to indicate your terminalโ€™s shell prompt. When following along, donโ€™t type theย $ย character, or youโ€™ll get some weird errors.

We will use a local blockchain for this tutorial. The most popular local blockchain isย Ganache. To install and run it on your project, run:

$ npm install --save-dev ganache-cli && npx ganache-cli --deterministic

Now, start a new shell/terminal in the same folder run the following command to install the CLI tool:

$ npm install --save-dev @openzeppelin/cli

To manage your deployed contracts, you need to create a new CLI project. Run the following command, and provide it with a name and version number for your project when prompted:

$ npx openzeppelin init

Two things will happen during initialization. First, anย `.openzeppelin`ย directory will be created, holding project-specific information. This directory will be managed by the CLI: you wonโ€™t need to manually edit anything. You should, however, commitย some of these filesย to Git.

Second, the CLI will store network configuration in a file calledย networks.js. For convenience, it is already populated with an entry calledย development, with configuration matchingย Ganache's default.

You can see all the unlocked accounts by running the following command:

$ npx openzeppelin accounts

Writing & deploying contracts

Now, let's create a contract namedย TodoListย in theย contractsย folder.

// contracts/TodoList.sol
pragma solidity ^0.6.3;

contract TodoList {
    string[] private list;

    // Emitted when the storeda new item is added to the list
    event ItemAdded(string item);

    // Adds a new item in the list
    function addItem(string memory newItem) public {
        list.push(newItem);
        emit ItemAdded(newItem);
    }

    // Gets the item from the list according to index
    function getListItem(uint256 index)
        public
        view
        returns (string memory item)
    {
        return list[index];
    }
}

Now, let's deploy this contract on the local blockchain.

$ npx openzeppelin create

As we can see our contract is deployed atย 0xD833215cBcc3f914bD1C9ece3EE7BF8B14f841bb.

Let's add an item ("responding to emails") to theย listย array using theย addItem()ย function by runningย npx openzeppelin send-tx.

Now, let's suppose we need to add a new function namedย getListSize()ย to get the size of the list. Just add a new function inside theย TodoListย contract.

// contracts/TodoList.sol
pragma solidity ^0.6.3;

contract TodoList {
	// ...


    // Gets the size of the list
    function getListSize() public view returns (uint256 size) {
        return list.length;
    }
}

After changing the Solidity file, we can now just upgrade the instance we had deployed earlier by running theย openzeppelin upgradeย command.

Done! Ourย TodoListย instance has been upgraded to the latest version of the codeย while keeping its state and the same address as before. We didnโ€™t need to create & deploy theย proxyย contract or link theย proxyย to theย TodoList. All that is done under the hood!

Letโ€™s try it out by invoking the newย getListSize()ย function, and checking the size of the list in the new contract:

Thatโ€™s it! Notice how theย sizeย of theย listย was preserved throughout the upgrade, as well as its address. And this process is the same regardless of whether you are working on a local blockchain, a testnet, or the main network.

Upgrading Contracts Programmatically

If you want to create and upgrade contracts from your JavaScript code instead of via the command line, you can use theย OpenZeppelin Upgradesย library instead of the CLI.

The CLI does not just manage contract upgrades, but also compilation, interaction, and source code verification. The Upgrades libraryย onlyย takes care of creating and upgrading. The library also doesย notย keep track of the contracts you have already deployed, nor runs any initializer or storage layout validations, as the CLI does. Nevertheless, these capabilities may be added to the Upgrades library in the near future.

You can find theย code for the below implementationย here.

In case you get stuck somewhere or have any doubts, let us knowย here.

In case you have not followed the above OpenZeppelin CLI part, you need toย install NodeJs & Ganache as instructed here.

Your first step will be to install the library in your project, and you will also probably want to installย web3ย to interact with our contracts using JavaScript, andย @openzeppelin/contract-loaderย to load the contracts from the JSON artifacts.

$ npm install web3 @openzeppelin/upgrades @openzeppelin/contract-loader

Now, create a fileย index.jsย insideย upgradable-smart-contractsย folder and paste this boilerplate code.

// index.js
const Web3 = require("web3");
const {
  ZWeb3,
  Contracts,
  ProxyAdminProject
} = require("@openzeppelin/upgrades");

async function main() {
  // Set up web3 object, connected to the local development network, initialize the Upgrades library
  const web3 = new Web3("http://localhost:8545");
  ZWeb3.initialize(web3.currentProvider);
  const loader = setupLoader({ provider: web3 }).web3;
}

main();

Here we set upย web3ย object, connected to the local development network, initialize theย Upgradesย library viaย ZWeb3.initialize, and initialize the contractย loader.

Now, add this following snippet in theย main()ย to create a newย project, to manage our upgradeable contracts.

async function main() {
  // ...
    
  //Fetch the default account
  const from = await ZWeb3.defaultAccount();

  //creating a new project, to manage our upgradeable contracts.
  const project = new ProxyAdminProject("MyProject", null, null, {
    from,
    gas: 1e6,
    gasPrice: 1e9
  });
}

Now, using thisย project, we can create an instance of any contract. Theย projectย will take care of deploying it in such a way it can be upgraded later.

Let's create 2 contracts,ย TodoList1ย and its updated versionย TodoList2ย insideย upgradable-smart-contracts/contractsย folder.

// contracts/TodoList1.sol
pragma solidity ^0.6.3;

contract TodoList1 {
    string[] private list;

    // Emitted when the storeda new item is added to the list
    event ItemAdded(string item);

    // Adds a new item in the list
    function addItem(string memory newItem) public {
        list.push(newItem);
        emit ItemAdded(newItem);
    }

    // Gets the item from the list according to index
    function getListItem(uint256 index)
        public
        view
        returns (string memory item)
    {
        return list[index];
    }
}

To createย TodoList2, just add a newย getListSize()ย function in the above contract.

// contracts/TodoList2.sol
pragma solidity ^0.6.3;

contract TodoList2 {
    string[] private list;

    // Emitted when the storeda new item is added to the list
    event ItemAdded(string item);

    // Adds a new item in the list
    function addItem(string memory newItem) public {
        list.push(newItem);
        emit ItemAdded(newItem);
    }

    // Gets the item from the list according to index
    function getListItem(uint256 index)
        public
        view
        returns (string memory item)
    {
        return list[index];
    }

    // Gets the size of the list
    function getListSize() public view returns (uint256 size) {
        return list.length;
    }
}

Now, we need to compile these 2 contracts using:

$ npx openzeppelin compile

This will create JSON contract artifacts in theย build/contractsย folder. These artifacts files contain all the information about the contracts that we would need to deploy and interact with the contracts.

Now, let's create an instance ofย TodoList1ย using theย projectย we created above.

async function main() {
//...
    
    
//Using this project, we can now create an instance of any contract.
  //The project will take care of deploying it in such a way it can be upgraded later.
  const TodoList1 = Contracts.getFromLocal("TodoList1");
  const instance = await project.createProxy(TodoList1);
  const address = instance.options.address;
  console.log("Proxy Contract Address 1: ", address);
}

Here we get theย TodoList1ย contract details from the contract artifacts we created above usingย Contracts.getFromLocal. Then we create & deploy a pair ofย proxyย &ย implementationย (TodoList1)ย contracts and link the proxy contract to theย TodoList1ย viaย project.createProxyย method. Finally, we print out the address of our proxy contract.

Now, let's add an item to theย listย usingย addItem()ย method and then fetch the added item usingย getListItem().

async function main() {
//...
    
  // Send a transaction to add a new item in the TodoList1
  await todoList1.methods
    .addItem("go to class")
    .send({ from: from, gas: 100000, gasPrice: 1e6 });

  // Call the getListItem() function to fetch the added item from TodoList1
  var item = await todoList1.methods.getListItem(0).call();
  console.log("TodoList1: List Item 0: ", item);
}

Now, let's update ourย TodoList1ย contract toย TodoList2.

async function main() {
//...
    
    
//After deploying the contract, you can upgrade it to a new version of
  //the code using the upgradeProxy method, and providing the instance address.
  const TodoList2 = Contracts.getFromLocal("TodoList2");
  const updatedInstance = await project.upgradeProxy(address, TodoList2);
  console.log("Proxy Contract Address 2: ", updatedInstance.options.address);
}

Here we get theย TodoList2ย contract details from the contract artifacts. Then we update our contract viaย project.upgradeProxyย method which takes 2 parameters, theย addressย of the proxy contract that we deployed in the previous step, and theย TodoList2ย contract object. We then print out the address of the proxy contract after the update.

Now, let's add a new item to theย TodoList2ย and fetch the items.

async function main() {
//...
    
    
  // Send a transaction to add a new item in the TodoList2
  await todoList2.methods
    .addItem("code")
    .send({ from: from, gas: 100000, gasPrice: 1e6 });

  // Call the getListItem() function to fetch the added items from TodoList2
  var item0 = await todoList2.methods.getListItem(0).call();
  var item1 = await todoList2.methods.getListItem(1).call();
  console.log("TodoList2: List Item 0: ", item0);
  console.log("TodoList2: List Item 1: ", item1);
}

Now, let's run theย index.jsย usingย node index.js.

Here we can observe 2 things:

  1. The address of theย proxyย contract did not change even after we updatedย TodoList1ย toย TodoList2.
  2. As we got 2 items from theย TodoList2, this shows that the state was preserved throughout the update.

Hence we can say thatย TodoList1ย instance has been upgraded to the latest version of the code (TodoList2),ย while keeping its state and the same address as before.

In case you get stuck somewhere or have any doubts, let us knowย here.

Now, as we have seen how to upgrade contracts, let's see a few limitations & workarounds that you need to know about when writing more complex contracts.

A few things to keep in mind: limitations & workarounds

When working with upgradeable contracts using OpenZeppelin Upgrades, there are a few minor caveats to keep in mind when writing your Solidity code.

Itโ€™s worth mentioning that these restrictions have their roots in how theย Ethereum VMย works, and apply to all projects that work with upgradeable contracts, not just OpenZeppelin Upgrades.

If your contract is not compatible for upgrades, the CLI will warn you when you try to upgrade. This feature is not there yet in the OpenZeppelin Upgrades libraries.

In order to understand the limitations & workarounds, let's take anย Exampleย contract, explore the limitations in the contract and add some workarounds to make the contract upgradable.

// contracts/Example.sol

pragma solidity ^0.6.0;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";

contract Example {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;

    constructor(uint8 cap) public {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Limitation 1: No Constructors

Due to a requirement of the proxy-based upgradeability system, no constructors can be used in upgradeable contracts. To learn about the reasons behind this restriction, head to thisย post.

Workaround: Initializer

A workaround is to replace the constructor with a function, typically namedย initialize, where you run constructor logic.

// contracts/Example.sol
pragma solidity ^0.6.0;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";

contract Example {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;

    function initialize(uint8 cap) public {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Now, as theย constructorย is called only once when the contract is initialized, we need to add a check to ensure that theย initializeย function gets called only once.

// contracts/Example.sol
pragma solidity ^0.6.0;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";

contract Example {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;
    
    bool private _initialized = false;

    function initialize(uint8 cap) public {
        require(!_initialized);
        _initialized = true;
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Since this will be a common thing to do when writing upgradeable contracts, OpenZeppelin Upgrades provides anย Initializableย base contract that has anย initializerย modifier that takes care of this:

// contracts/Example.sol
pragma solidity ^0.6.0;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";
import "@openzeppelin/upgrades/contracts/Initializable.sol";

contract Example is Initializable {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;
    
    function initialize(uint8 cap) public initializer {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Another difference between aย constructorย and a regular function is that Solidity takes care of automatically invoking the constructors of all ancestors of a contract. When writing an initializer, you need to take special care to manually call the initializers of all parent contracts:

// contracts/Example.sol
pragma solidity ^0.6.0;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";
import "@openzeppelin/upgrades/contracts/Initializable.sol";


contract BaseExample is Initializable {
    uint256 public createdAt;
    
    function initialize() initializer public {
        createdAt = block.timestamp;
    }
    
}

contract Example is BaseExample {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;

    function initialize(uint8 cap) initializer public {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Keep in mind that this restriction affects not only your contracts, but also the contracts you import from a library. Consider for exampleย ERC20Cappedย from OpenZeppelin Contracts: the contract initializes the tokenโ€™sย capย in its constructor.

pragma solidity ^0.6.0;

import "./ERC20.sol";

/**
 * @dev Extension of {ERC20} that adds a cap to the supply of tokens.
 */
contract ERC20Capped is ERC20 {
    uint256 private _cap;

    /**
     * @dev Sets the value of the `cap`. This value is immutable, it can only be
     * set once during construction.
     */
    constructor (uint256 cap) public {
        require(cap > 0, "ERC20Capped: cap is 0");
        _cap = cap;
    }
    
    //...
}

This means you should not be using these contracts in your OpenZeppelin Upgrades project. Instead, make sure to useย @openzeppelin/contracts-ethereum-package, which is an official fork of OpenZeppelin Contracts that has been modified to use initializers instead of constructors. Take a look at howย ERC20Cappedย looks inย @openzeppelin/contracts-ethereum-package:

pragma solidity ^0.5.0;

import "@openzeppelin/upgrades/contracts/Initializable.sol";
import "./ERC20Mintable.sol";

/**
 * @dev Extension of {ERC20Mintable} that adds a cap to the supply of tokens.
 */
contract ERC20Capped is Initializable, ERC20Mintable {
    uint256 private _cap;

    /**
     * @dev Sets the value of the `cap`. This value is immutable, it can only be
     * set once during construction.
     */
    function initialize(uint256 cap, address sender) public initializer {
        ERC20Mintable.initialize(sender);

        require(cap > 0, "ERC20Capped: cap is 0");
        _cap = cap;
    }

	//...
}

Whether using OpenZeppelin Contracts or another Ethereum Package, always make sure that the package is set up to handle upgradeable contracts.

// contracts/Example.sol
pragma solidity ^0.6.0;

import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20Capped.sol";
import "@openzeppelin/upgrades/contracts/Initializable.sol";


contract BaseExample is Initializable {
    uint256 public createdAt;
    
    function initialize() initializer public {
        createdAt = block.timestamp;
    }
    
}

contract Example is BaseExample {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;
 
    function initialize(uint8 cap) initializer public {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Limitation 2: Initial Values in Field Declarations

Solidity allows defining initial values for fields when declaring them in a contract.

contract Example is BaseExample {
    uint256 private _cap = 1000000000000000000;
	
    //...
}

This is equivalent to setting these values in the constructor, and as such, will not work for upgradeable contracts.

Workaround: Initializer

Make sure that all initial values are set in an initializer function as shown below; otherwise, any upgradeable instances will not have these fields set.

//...

contract Example is BaseExample {
    uint256 private _cap;
    ERC20Capped public token;

    function initialize(uint8 cap) initializer public {
        _cap = 1000000000000000000;
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Note that it still is fine to set constants here, because the compilerย does not reserve a storage slot for these variables, and every occurrence is replaced by the respective constant expression. So the following still works with OpenZeppelin Upgrades:

//...

contract Example is BaseExample {
    uint256 constant private _cap = 1000000000000000000;
    
    //...
}

Limitation: Creating New Instances From Your Contract Code

When creating a new instance of a contract from your contractโ€™s code, these creations are handled directly by Solidity and not by OpenZeppelin Upgrades, which means thatย these contracts will not be upgradeable.

For instance, in the following example, even ifย Exampleย is upgradeable (if created viaย openzeppelin create Example), theย tokenย contract created is not:

//...

contract Example is BaseExample {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;

    function initialize(uint8 cap) initializer public {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Workaround: Inject a pre-deployed contract from CLI

The easiest way around this issue is to avoid creating contracts on your own altogether: instead of creating a contract in anย initializeย function, simply accept an instance of that contract as a parameter, and inject it after creating it from the OpenZeppelin CLI:

//...

contract Example is BaseExample {
    ERC20Capped public token;
    
    function initialize(ERC20Capped _token) initializer public {
        token = _token;
    }
}
$ TOKEN=$(npx openzeppelin create TokenContract)
$ npx oz create Example --init --args $TOKEN

Workaround: OpenZeppelin App Contract

An advanced alternative, if you need to create upgradeable contracts on the fly, is to keep an instance of your OpenZeppelin projectโ€™sย Appย in your contracts. Theย Appย is a contract that acts as the entrypoint for your OpenZeppelin project, which has references to your logic implementations, and can create new contract instances:

// contracts/Example.sol
pragma solidity ^0.6.0;

import "@openzeppelin/upgrades/contracts/Initializable.sol";
import "@openzeppelin/upgrades/contracts/application/App.sol";

contract BaseExample is Initializable {
    //...
}

contract Example is BaseExample {
    
  App private app;

  function initialize(App _app) initializer public {
    app = _app;
  }

  function createNewToken() public returns(address) {
    return app.create("@openzeppelin/contracts-ethereum-package", "ERC20Capped");
  }
}

Potentially Unsafe Operations

When working with upgradeable smart contracts, you will always interact with the proxy contract instance, and never with the underlying logic (implementation) contract. However, nothing prevents a malicious actor from sending transactions to the logic contract directly. This does not pose a threat, since any changes to the state of the logic contracts do not affect your proxy contract instances, as the storage of the logic contracts is never used in your project.

There is, however, an exception. If the direct call to the logic contract triggers aย selfdestructย operation, then the logic contract will be destroyed, and all your contract instances will end up delegating all calls to an address without any code. This would effectively break all contract instances in your project.

A similar effect can be achieved if the logic contract contains aย delegatecallย operation. If the contract can be made toย delegatecallย into a malicious contract that contains aย selfdestruct, then the calling contract will be destroyed.

pragma solidity ^0.6.0;

// The Exmaple contract makes a `delegatecall` to the Malicious contract. Thus, even if the Malicious contract runs the `selfdestruct` function, it is run in the context of the Example contract, thus killing the Example contract.  

contract Example {
    function testFunc(address malicious) public {
        malicious.delegatecall(abi.encodeWithSignature("kill()"));
    }
}

contract Malicious {
    function kill() public {
        address payable addr = address(uint160(address(0x4Bf8c809c898ee52Eb7fc6e1FdbB067423326B2A)));
        selfdestruct(addr);
    }
}

As such, it is strongly recommended to avoid any usage of eitherย selfdestructย orย delegatecallย in your contracts. If you need to include them, make absolutely sure they cannot be called by an attacker on an uninitialized logic contract.

Modifying Your Contracts

When writing new versions of your contracts, either due to new features or bugfixing, there is an additional restriction to observe: you cannot change the order in which the contract state variables are declared, nor their type. You can read more about the reasons behind this restriction by learning aboutย Proxies.

Violating any of these storage layout restrictions will cause the upgraded version of the contract to have its storage values mixed up, and can lead to critical errors in your application.

This means that if you have an initial contract that looks like this:

pragma solidity ^0.6.3;

contract Example {
    string public tokenName;
    uint8 public decimals;
}

Then you cannot change the type of a variable:

pragma solidity ^0.6.3;

contract Example {
    string public tokenName;
    uint public decimals;
}

Or change the order in which they are declared:

pragma solidity ^0.6.3;

contract Example {
    uint public decimals;
    string public tokenName;
}

Or introduce a new variable before existing ones:

pragma solidity ^0.6.3;

contract Example {
	string public tokenSymbol;
    string public tokenName;
    uint public decimals;
}

Or remove an existing variable:

pragma solidity ^0.6.3;

contract Example {
    string public tokenName;
}

If you need to introduce a new variable, make sure you always do so at the end:

pragma solidity ^0.6.3;

contract Example {
    string public tokenName;
    uint public decimals;
	string public tokenSymbol;
}

Keep in mind that if you rename a variable, then it will keep the same value as before after upgrading. This may be the desired behaviour if the new variable is semantically the same as the old one:

pragma solidity ^0.6.3;

contract Example {
    string public tokenName;
    uint public decimalCount;	// starts with the value of `decimals`
}

And if you remove a variable from the end of the contract, note that the storage will not be cleared. A subsequent update that adds a new variable will cause that variable to read the leftover value from the deleted one.

pragma solidity ^0.6.3;

contract Example1 {
    string public tokenName;
    uint public decimals;
}

// Updating Example1 --> Example2

contract Example2 {
    string public tokenName;
}

// Updating Example2 --> Example3

contract Example3 {
    string public tokenName;
    uint public decimalCount;	// starts with the value of `decimals`
}

Note that you may also be inadvertently changing the storage variables of your contract by changing its parent (base) contracts. For instance, if you have the following contracts:

pragma solidity ^0.6.3;

contract BaseExample1 {
	uint256 createdAt;
}

contract BaseExample2 {
	string version;
}

contract Example is BaseExample1, BaseExample2 {}

Then modifyingย Exampleย by swapping the order in which the base contracts are declared, or adding new base contracts or removing base contracts, will change how the variables are actually stored:

pragma solidity ^0.6.3;

contract BaseExample1 {
	uint256 createdAt;
}

contract BaseExample2 {
	string version;
}

//swapping the order in which the base contracts are declared
contract Example is BaseExample2, BaseExample1 {}

//Or...

//removing base contract(s)
contract Example is BaseExample1 {}

//Or...

contract BaseExample3 {} 

//adding new base contract
contract Example is BaseExample1, BaseExample2, BaseExample3 {}

You also cannot add new variables to base contracts, if the child has any variables of its own. Given the following scenario:

pragma solidity ^0.6.3;

contract BaseExample {}

contract Example is BaseExample {
	string tokenName;
}

//Now, if the BaseExample is updated to the following

contract BaseExample {
	string version;		// takes the value of `tokenName` 
}

contract Example is BaseExample {
	string tokenName;
}

Then the variableย versionย would be assigned the slot thatย tokenNameย had in the previous version.

You also remove a variable from the base contract, if the child has any variables of its own. For example:

pragma solidity ^0.6.3;

contract BaseExample {
	uint256 createdAt;
    string version;
}

contract Example is BaseExample {
	string tokenName;
}

//Now, if the BaseExample is updated to the following

contract BaseExample {
	uint256 createdAt; 
}

contract Example is BaseExample {
	string tokenName;	//takes the value of `version`
}

Here, as we remove theย versionย variable from theย BaseExample, the memory slot forย versionย (before update) will now be used byย tokenNameย (after update).

A workaround for this is to declare unused variables on base contracts that you may want to extend in the future, as a means of "reserving" those slots. So, basically, keeping the number and order of the variables in the parent and child contracts same for all the updates.

pragma solidity ^0.6.3;

contract BaseExample {
	string someVar1;
    string someVar2;
    string someVar3;
    
    //...
}

Note that this trick doesย notย involve increased gas usage.

In case you get stuck somewhere or have any doubts, let us knowย here.

Credits

We have copied some of the text from these amazing docs fromย OpenZeppelinย &ย NuCypher.

  1. NuCypherโ€™s Approaches to Upgradeable Contracts
  2. Upgrading Smart Contracts
  3. Writing Upgradeable Contracts

This article was first published on our open-source platform, SimpleAsWater.com. If you are interested in IPFS, Libp2p, Ethereum, Zero-knowledge Proofs, Defi, CryptoEconomics, IPLD, Multi formats, and other Web 3.0 projects, concepts and interactive tutorials, then be sure to check outย SimpleAsWater.

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.