Building Upgradable Solidity Smart Contracts

Zeppelin OS Introduction

Zeppelin is all about Smart Contract development. The team has helped many developers build Smart Contracts with openzeppelin-solidity, one most downloaded web3 libraries on NPM (with over 10k downloads every week!). According to Zeppelin’s website:

“ZeppelinOS is a development platform designed specifically for smart contract projects. It allows for seamless upgrades and provides economic incentives to create a healthy ecosystem of secure applications.”

What problem does it solve?

ZeppelinOS helps developers build upgradable smart contracts using libraries that anyone can use on the Ethereum blockchain. Unlike traditional contracts (which can remain frozen forever on the blockchain, with mistakes, limited functionalities, etc...), ZeppelinOS allows users to opt-in & allow upgrades of contracts, opening the doors to a more sustainable process for developing web3 blockchain projects. With upgrades, we can make iterative releases, quickly add small pieces of functionalities (that we can adjust according to the always changing goals of our users), and of course, we can implement fixes for bugs we had introduced on previous iterations.

Building on ZeppelinOS

Let’s build an upgradable CrudApp contract! We will deploy CrudApp.sol and then update it while preserving its data… so let’s start:

1- Create a project and install dependencies

mkdir crud
cd crud
npm init
npm install --global zos

2- Initialize your project

zos init crud

This command will create a zos.json file, which contains all the information about the project. For details about this file, see the configuration files page.

The command will also initialize Truffle. So by now, inside the crud directory you should have a package.json file (created by npm), two empty directories named contracts and migrations, a truffle-config.js file (created by zos for Truffle), and a zos.json file (created by zos for ZeppelinOS).

3- Add CrudApp.sol contract

Let’s add our CurdApp.sol now. We will make some changes in our previous Smart Contract, so let’s understand them:

pragma solidity ^0.4.23;

contract CrudApp {
    
   struct country{
      string name;
      string leader;
      uint256 population;
   }
  
   country[] public countries; 

   uint256 public totalCountries;
  
  
    constructor() public {
       totalCountries = 0;
   }

   event CountryEvent(string countryName , string leader, uint256 population);
   
   event LeaderUpdated(string countryName , string leader);

   event CountryDelete(string countryName);

    
   function insert( string countryName , string leader , uint256 population) public returns (uint256 totalCountries){
        country memory newCountry = country(countryName , leader, population);
        countries.push(newCountry);
        totalCountries++;
        //emit event
        emit CountryEvent (countryName, leader, population);
        return totalCountries;
   }
   
   function updateLeader(string countryName, string newLeader) public returns (bool success){
       //This has a problem we need loop
       for(uint256 i =0; i< totalCountries; i++){
           if(compareStrings(countries[i].name ,countryName)){
              countries[i].leader = newLeader;
              emit LeaderUpdated(countryName, newLeader);
              return true;
           }
       }
       return false;
   }
   
   function deleteCountry(string countryName) public returns(bool success){
        require(totalCountries > 0);
        for(uint256 i =0; i< totalCountries; i++){
           if(compareStrings(countries[i].name , countryName)){
              countries[i] = countries[totalCountries-1]; // pushing last into current arrray index which we gonna delete
              delete countries[totalCountries-1]; // now deleteing last index
              totalCountries--; //total count decrease
              countries.length--; // array length decrease
              //emit event
              emit CountryDelete(countryName);
              return true;
           }
       }
       return false;
   }
   
     
   function getCountry(string countryName) public view returns(string name , string leader , uint256 population){
        for(uint256 i =0; i< totalCountries; i++){
           if(compareStrings(countries[i].name, countryName)){
              //emit event
              return (countries[i].name , countries[i].leader , countries[i].population);
           }
       }
       revert('country not found');
   }     
   
  function compareStrings (string a, string b)  internal pure returns (bool){
       return keccak256(a) == keccak256(b);
   }
   
   
   function getTotalCountries() public view returns (uint256 length){
      return countries.length;
   }
}

Removing Constructor and adding an Initializer

“You can use your Solidity contracts in ZeppelinOS without any modifications, except for their constructors. Due to a requirement of the proxy-based upgradeability system, no constructors can be used in upgradeable contracts. You can read in-depth about the reasons behind this restriction in the ZeppelinOS Upgrades Pattern page.”

This means that, when using a contract within ZeppelinOS, you need to change its constructor into a regular function, typically named initialize, where you run all the setup logic:

function initialize(uint256 countryLimit) initializer public {
   totalCountries = 0;
   limit = countryLimit;
}

You need to import Initializable.sol too for this to work:

import "zos-lib/contracts/Initializable.sol";

Install Ganache and deploy your project

We will use Ganache (local blockchain) to deploy our Smart Contract. You can change network settings in truffle-config.js if you want to use a different network. Open a different terminal and run below commands:

npm install -g ganache-cliganache-cli --port 9545 --deterministic

Now go back to the previous terminal and run below commands to start a session. We are using --network option for local network and --from [address] to provide an Ethereum address (which we are going use to deploy the smart contract). We are using a default Ganache address (you can see it on the Ganache terminal) and we are also using an additional expires flag to define the number of seconds this session will be valid (1 hour in this case).

zos session --network local --from 0x1df62f291b2e969fb0849d99d9ce41e2f137006e --expires 3600

Now let’s deploy our project:

zos push

This command deploys CrudApp to the local network and prints its address. You can deploy more contracts using add command and they will also get deployed after this command.

The push command also creates a zos.dev-<network_id>.json file with all the information about your project in this specific network, including the addresses of the deployed contract implementations in contracts["MyContract"].address. You can read more about this file format in the configuration files section.

Upgrade the Project

push command deploys logic contracts which are not intended to be used directly, so instead we need an upgradable instance (we will cover logic contracts and ZeppelinOS Upgrades Pattern page in our future blogs).

Now let’s create an upgradeable instance of our CrudApp.sol:

zos create CrudApp --init initialize --args 200

The zos create command uses optional --init [function-name] with the function name (which is initialize in our case). You can also pass arguments for this function (which is 200, limitation of countries) in our case. This will also print a Smart Contract address, which we will use while interacting with it.

Interacting with the Contract

Now let’s interact with our contract and insert some countries. To get to the Truffle console, run below command:

npx truffle console --network local

You will now see the Truffle console. Let’s run some commands and insert some countries:

truffle(local)> crud = CrudApp.at('<your-contract-address>') 
truffle(local)> crud.limit()
truffle(local)> crud.insert("USA", "Elizabeth Warren", 33000000)
truffle(local)> crud.getCountry("USA")
truffle(local)> crud.getTotalCountries()

We have inserted one entry in our Smart Contract. Let’s update our CrudApp!

Adding a function

We will add an extra function at the end of our Smart Contract and update it. So, for now, we already have a getCountry() function… we will add one more getLeader() function in our Smart Contract.

function getLeader(string leaderName) public view returns(string name , string leader , uint256 population){
        for(uint256 i =0; i< totalCountries; i++){
           if(compareStrings(countries[i].leader, leaderName)){
              //emit event
              return (countries[i].name , countries[i].leader , countries[i].population);
           }
       }
       revert('Leader not found');
   }
   

Now let’s update our Smart Contract. We need to run below commands for that:

zos push
zos update CrudApp

This will print a new Smart Contract address which we’ll use for testing our updated contract.

Testing updated Smart Contract

Now let’s test our Smart Contract. It should preserve our previous data and we can access our new getLeader() function. So let’s interact with our Smart Contract again:

truffle(local)> crud = CrudApp.at('<your-contract-address>') 
truffle(local)> crud.limit()
truffle(local)> crud.getTotalCountries()
truffle(local)> crud.getLeader("Elizabeth Warren")

You will see able to see the results, that the Smart Contract was updated just like any other piece of code. 🍰 — We will build an upgradable ERC-20 token in our next article!

Conclusion

Though it seems pretty easy, we need to be pretty careful while using ZeppelingOS because of Solidity storage management. You can check here about the thing you need to pay extra attention to while updating your Smart Contract. ZeppelinOS is a great value addition to the Ethereum ecosystem!

Let us know what you want to learn about in the comment section.👇

About QuikNode

QuikNode is building infrastructure to support the future of Web3. Since 2017, we’ve worked with hundreds of developers & companies, helping scale dApps and providing high-performance Ethereum nodes. We’re working on something interesting from the past few months and will be launching soon, so subscribe our newsletter for more updates!! 😃