Upgradeable Smart Contracts Made Easy

in #ethereum6 years ago

Upgradeable smart contracts are a desirable design practice given today’s extremely important and ever-changing landscape for smart contract security. In this article, we deploy a plain ERC20 contract, and later upgrade it to a mintable token contract to allow for the functionality of minting new tokens.

However, it is important to keep in mind that there are some key design principles to be followed while coding such contracts. On the surface, smart contract upgradeability may seem like a pretty cool feature to have. However, it violates a fundamental property of Blockchains — immutability. I will be creating a follow-up article which addresses this topic. But for now, let’s focus on the implementation alone.

We use ZeppelinOS (https://zeppelinos.org/) to achieve our task.

Refer to the diagram below for a simple reference on how ZeppelinOS achieves upgradeability. I’m not going to delve deeper into the technical aspects as they already have pretty good documentation (https://docs.zeppelinos.org/docs/start.html) in place.

Let’s get into writing code. For this deployment, I’m going to use Ganache as my local Ethereum node. I’m also going to assume you already have node.js installed on your system.

1 Install ZeppelinOS using the command below

npm install --global zos

If you are on Windows, and you’re running into issues with node-gyp, check out this guide (https://gist.github.com/jtrefry/fd0ea70a89e2c3b7779c).

2 Let’s say we’re creating the project demo-token. Create a folder and initialize the project.

mkdir demo-token && cd demo-token

npm init 

zos init demo-token

This will create a truffle-config.js file along with a zos.json.

Install the ZeppelinOS libraries and link the standard libraries with the following commands

npm install zos-lib

zos link openzeppelin-zos

3 Now let’s write that upgradeable smart contract. In the contracts, folder create a new file DemoToken.sol with the contents provided below

pragma solidity ^0.4.21;
import "zos-lib/contracts/migrations/Migratable.sol";
import "openzeppelin-zos/contracts/token/ERC20/DetailedERC20.sol";
import "openzeppelin-zos/contracts/token/ERC20/StandardToken.sol";
import "openzeppelin-zos/contracts/ownership/Ownable.sol";
contract DemoToken is Migratable, StandardToken, DetailedERC20, Ownable  {
  function initialize(uint256 initialSupply, address InitialAccount, string _name, string _symbol, uint8 _decimals) isInitializer("DemoToken", "0.1") public{
   totalSupply_ = initialSupply;
   balances[InitialAccount] = initialSupply;
   DetailedERC20.initialize(_name, _symbol, _decimals);
   Ownable.initialize(InitialAccount);
  }
}

Note that in ZeppelinOS, your smart contract should not specify a constructor. Initialization for the parameters can be defined inside initialize() function. The isInitializer() modifier accepts the name you wish to give for this version of your contract along with the version number.

4 Now let’s add the token into ZeppelinOS

zos add DemoToken

If all goes well, you should get the above output

5 Time to deploy. Setup your truffle-config.js correctly to point to your local ganache node and execute the command below

zos push --deploy-stdlib --network local

Note: In the above command we are also deploying the Zeppelin standard libraries in your local ganache instance. This need not be done when deploying to any of the test networks or the mainnet.

Note: In my experience Ganache runs on port 7545 by default. You might have to change your truffle-config.js file accordingly. You can refer to my truffle-config.js file below

'use strict';
module.exports = {
  networks: {
    local: {
      host: 'localhost',
      port: 7545,
      gas: 5000000,
      network_id: '*'
    }
  }
};

If correctly done, you should get the below output

6 Now we have to initialize the DemoToken smart contract with the parameters provided in the initialize method.

zos create DemoToken --init initialize --args 100,"<Your Ganache Address>","Demo","DMO",18 --network local

The above command creates 100 DemoTokens at the specified Ganache account. Success would look like this

7 Let’s try out our newly created tokens. Open a new truffle window with the command below

npx truffle console --network local

truffle(local)> demo = DemoToken.at("<ContractAddress>")

Enter the proxy address as the contract address. It is the last line in the above screenshot. In my case, it was 0x0caac69787f1db70308d4fdd1c416d6cf23284b5. If successful the demo object should appear in full.

8 Try out two simple transactions to see if everything works well

truffle(local)> demo.balanceOf(web3.eth.accounts[0])
>BigNumber { s: 1, e: 2, c: [ 100 ] }

truffle(local)> demo.transfer(web3.eth.accounts[1],20)

truffle(local)> demo.balanceOf(web3.eth.accounts[0])
>BigNumber { s: 1, e: 1, c: [ 80 ] }

That’s about it. You’ve just created your very first upgradeable smart contract!

9 Time for the upgrade. Let’s revisit DemoToken.sol, and make it a Mintable ERC20 contract.

pragma solidity ^0.4.21;
import "zos-lib/contracts/migrations/Migratable.sol";
import "openzeppelin-zos/contracts/token/ERC20/DetailedERC20.sol";
import "openzeppelin-zos/contracts/token/ERC20/StandardToken.sol";
import "openzeppelin-zos/contracts/ownership/Ownable.sol";
contract DemoToken is Migratable, StandardToken, DetailedERC20, Ownable  {
  function initialize(uint256 initialSupply, address InitialAccount, string _name, string _symbol, uint8 _decimals) isInitializer("DemoToken", "0") public{
   totalSupply_ = initialSupply;
   balances[InitialAccount] = initialSupply;
   DetailedERC20.initialize(_name, _symbol, _decimals);
   Ownable.initialize(InitialAccount);
  }
  
  event Mint(address indexed to, uint256 amount);
  event MintFinished();
bool public mintingFinished = false;
  
  modifier canMint() {
    require(!mintingFinished);
    _;
  }
  
  function mint(address _to, uint256 _amount) onlyOwner canMint public returns (bool) {
    totalSupply_ = totalSupply_.add(_amount);
    balances[_to] = balances[_to].add(_amount);
    emit Mint(_to, _amount);
    emit Transfer(address(0), _to, _amount);
    return true;
  }
  
  function finishMinting() onlyOwner canMint public returns (bool) {
    mintingFinished = true;
    emit MintFinished();
    return true;
  }
  
}

10 Exit the truffle console and repeat the command in step 5

zos push --network local

Note: There is no need to redeploy stdlib files again in your local ethereum node.

Note: In the file, zos.local.json see how the object under the “contracts” key updated to your new DemoToken contract address, but the key under “proxies” still points to your old contract address. We fix this in the next step.

11 Update ZeppelinOS with the new contract

zos update DemoToken --network local
  1. Now let’s check out the upgraded features. On a new command window, try out the following
npx truffle console --network local

truffle(local)> demo = DemoToken.at("<your-proxy-address>")

In my case, it’s again going to be 0x0caac69787f1db70308d4fdd1c416d6cf23284b5

truffle(local)> demo.balanceOf(web3.eth.accounts[0])
>BigNumber { s: 1, e: 1, c: [ 80 ] }

truffle(local)> demo.mintingFinished()
>false

truffle(local)> demo.mint(web3.eth.accounts[0],100)

truffle(local)> demo.balanceOf(web3.eth.accounts[0])
>BigNumber { s: 1, e: 2, c: [ 180 ] }

Pretty cool eh? Created a plain ERC20 contract. Later upgraded it to a Mintable contract. Then minted a few tokens to myself. As unethical as it can get!

In my next article, I will be writing a follow-up to provide a few best practice guides while creating such upgradeable smart contracts. Please do share and follow my page if you like such guides. Also, feel free to hit me up on Linkedin (https://www.linkedin.com/in/adilharis/) if you have queries.

Cheers.