DEV Community

Paul Berg
Paul Berg

Posted on • Edited on

How to Write Upgradeable Smart Contracts with Truffle ^5.0 and ZeppelinOS ^2.0

Context

In this post, we'll learn how to write upgradeable smart contracts with the latest versions of Truffle and ZeppelinOS. In particular, version ^5.0 of Truffle introduces a plethora of updates, with the most prominent one being the integration with web3 ^1.0. Let's unpack these updates and introduce upgradeable smart contracts with the state-of-the-art ZeppelinOS.

This is not an introductory article to Ethereum development, if you want that, take a look at the following resources:

Do note that the blockchain world moves at a ridiculous pace, giving little to no time for standards to sink in. This means that a lot of code snippets from the articles above may not work as expected, but don't panic and return here when that happens.

Prerequisites

Make sure you are equipped with the following:

  • node.js and npm
  • ganache-cli or the Ganache desktop app
  • curiosity to learn more

Truffle ^5.0

Truffle Logo

To install truffle globally, go to your terminal and write:

$ npm install truffle@^5.0.0 --global
Enter fullscreen mode Exit fullscreen mode

And let's create our project:

$ mkdir MyProject
$ cd myProject
$ npm init -y
$ truffle init
Enter fullscreen mode Exit fullscreen mode

This will initialise a bunch of files. If you used older versions of Truffle, you may notice that "truffle-config.js" is more verbose now and has many more configurable options. Let's go through the important changes.

Web3 ^1.0

This is a major API change, but it's a good one, because the new library is much more elegant and intuitive to use. Of course, there are transition costs if you had already written your Truffle tests using older versions, so bookmark the web3 ^1.0 documentation just in case.

HD Wallet Provider

If you were previously using the "truffle-hdwallet-provider" npm module, you must upgrade to "truffle-hdwallet-provider@web3-one" to make it work with Truffle ^5.0:

$ npm install truffle-hdwallet-provider@web3-one --save-dev
Enter fullscreen mode Exit fullscreen mode

To load your mnemonic, you can either use the "fs" module natively provided by node or you can install "dotenv". Never hardcode it in JavaScript!

Bring Your Own Compiler

It used to be a huge pain in the neck to change the Solidity compiler when compiling with Truffle, but fear not any more! You can define whatever (remote) version you want by doing this:

compilers: {
  solc: {
    optimizer: {
      enabled: true,
      runs: 200,
    },
    version: "0.4.25",
  },
}
Enter fullscreen mode Exit fullscreen mode

In this particular example, we set the compiler version to "0.4.25", but you can choose "0.4.24" or "0.4.18" if you want to. To list all the available versions:

$ truffle compile --list
Enter fullscreen mode Exit fullscreen mode

Support for Async, Await

If you're a fan of JavaScript ES8's super duper cool "async" and "await", you can now make good use of them when running $ truffle develop or $ truffle console.

Others

There a few other new features, such as the added support for plugins, but they are outside the scope of this tutorial. Check out the changelog if you're looking for an exhaustive list.

Deploy a Smart Contract

Smart Contract

This is the mock-up Solidity contract we're going to use:

// Note.sol
pragma solidity ^0.4.25;

contract Note  {
  uint256 private number;

  constructor(uint256 _number) public {
    number = _number;
  }

  function getNumber() public view returns (uint256 _number) {
    return number;
  }
}
Enter fullscreen mode Exit fullscreen mode

Create a new file called "Note.sol" in your "contracts" folder and copy and paste the code above in there. Now, make sure you've done all the steps correctly by compiling the contract:

$ truffle compile
Enter fullscreen mode Exit fullscreen mode

You should get no error and the following logs:

Compiling ./contracts/Migrations.sol...

Compiling ./contracts/Note.sol...

Writing artifacts to ./build/contracts

Now, let's write the migration file.

// 2_migrate_note.js

var Note = artifacts.require("./Note.sol");

module.exports = function (deployer) {
  deployer.deploy(Note, 64);
};
Enter fullscreen mode Exit fullscreen mode

In the "migrations" folder, create a new file called "2_migrate_note.js" and copy and paste the code above. To verify the set up, you have to first spin up an Ethereum node, but don't worry, for local development there are tools like Ganache. You can run it either by opening a new terminal window and executing $ ganache-cli or by launching the desktop app. Then:

$ truffle migrate
Enter fullscreen mode Exit fullscreen mode

If you encounter problems, make sure that your "truffle-config.js" file looks like this (I stripped the comments for brevity):

// truffle-config.js

module.exports = {
  compilers: {
    solc: {
      version: "0.4.25",
      settings: {
        optimizer: {
          enabled: false,
          runs: 200
        }
      }
    }
  },
  networks: {
    development: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*",
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

If it works, you should get a beautiful report like the one here. Awesome! Now you know how to compile and deploy a contract with Truffle ^5.0, but you haven't got your hands dirty with the cooler part yet: making the smart contracts upgradeable.

ZeppelinOS

ZeppelinOS Logo

Set Up

If you're completely unfamiliar with upgradeable smart contracts, go watch Elena Nadolinski's presentation on the topic. It's totally worth it and it can do a much better job than someone can on a blog.

The gist of it is that we're aiming to reconcile traditional development practices with smart contracts:

  1. When running A/B testing with real users and there's a bug, we should patch the code and fix it asap.
  2. The upgradeability processes should remain fully transparent and not grant total power to the developer(s).

Point no. 2 is currently under research with many people actively proposing transparent governance models. However, we won't mind decentralised autonomous organisations (DAOs) for now and focus on the technical part of the story.

Let's install ZeppelinOS and its library dependency:

npm install zos --global
npm install zos-lib --save-dev
Enter fullscreen mode Exit fullscreen mode

And then initialise it within our Truffle project:

zos init MyProject
Enter fullscreen mode Exit fullscreen mode

A file called "zos.json" should've been initialised:

{
  "zosversion": "2",
  "name": "MyProject",
  "version": "0.1.0",
  "contracts": {}
}
Enter fullscreen mode Exit fullscreen mode

Importantly, make sure to update the Note contract as follows:

// Note.sol
pragma solidity ^0.4.25;

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

contract Note is Initializable {
  uint256 private number;

  function initialize(uint256 _number) public initializer {
    number = _number;
  }

  function getNumber() public view returns (uint256 _number) {
    return number;
  }
}
Enter fullscreen mode Exit fullscreen mode

ZeppelinOS contracts don't use normal Solidity constructors but instead rely on an "Initializable" base contract which needs to have its "initialize" function overridden. Let's continue by adding the edited contract to ZeppelinOS:

$ zos add Note
Enter fullscreen mode Exit fullscreen mode

The following JavaScript object should've been added to "zos.json":

"contracts": {
  "Note": "Note"
}
Enter fullscreen mode Exit fullscreen mode

Before deployment, ZeppelinOS requires you to create a session:

$ zos session --network development --from YOUR_DEPLOYMENT_ACCOUNT --expires 7200
Enter fullscreen mode Exit fullscreen mode

Explaining each parameter:

  • network: one of the networks defined under "truffle-config.js"
  • from: because of a known transparent proxy issue, the deployment account mustn't be the default address designated by truffle or ganache
  • expires: the time-to-live of the session, measured in seconds

Now, to clarify a few things:

  1. The session is sort of a "behavioural sugar" - while deploying, you don't have to specify the "network" and the "from" parameters.
  2. You might wonder what "from" parameter to set. Simply put, any account except the first one. Find a picture below for an example on the Ganache desktop app:

Ganache Screenshot

Ready for prime time?

$ zos push
Enter fullscreen mode Exit fullscreen mode

If it worked, you should've got something like this:

Compiling contracts

Compiling ./contracts/Migrations.sol...

Compiling ./contracts/Note.sol...

Compiling zos-lib/contracts/Initializable.sol...

Writing artifacts to ./build/contracts

Validating contract Note

Uploading Note contract as Note

Deploying logic contract for Note

Created zos.dev-7923.json

This command finally deploys your smart contract to the blockchain and it also creates a file called "zos.dev-<network_id>.json", where "<network_id>" is the id of your Ethereum network. This is where you can find important information about your project, such as the addresses of your deployed contracts.

Upgradeability

It is highly important to understand that ZeppelinOS, under the hood, works by creating two contracts:

  1. Proxy
  2. Logic

The catch is that the Proxy redirects the end user to the Logic, but the Proxy "retains" the storage. That is, even if you update the Logic, the state of your smart contracts stays the same.

Previously, you created and deployed a Logic contract. Let's create an instance of a Proxy:

$ zos create Note --init initialize --args 64
Enter fullscreen mode Exit fullscreen mode

This deploys and logs the address of the new Proxy contract. Open a new terminal window and initialise a Truffle console:

truffle console --network development
let abi = require("./build/contracts/Note.json").abi
let contract = new web3.eth.Contract(abi, "your-proxy-address")
contract.methods.getNumber().call();
Enter fullscreen mode Exit fullscreen mode

It should print "64".

Head to the web3 ^1.0 docs for more information on how to work with the new Contract API.

What if you want to change the number? Fortunately, you can make an upgrade to the logic of the contract, while preserving the storage.

Firstly, update the contract by adding a new function:

// Note.sol
pragma solidity ^0.4.25;

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

contract Note is Initializable {
  uint256 private number;

  function initialize(uint256 _number) public initializer {
    number = _number;
  }

  function getNumber() public view returns (uint256 _number) {
    return number;
  }

  function setNumber(uint256 _number) public {
    number = _number;
  }
}
Enter fullscreen mode Exit fullscreen mode

And then:

$ zos push
$ zos update Note
Enter fullscreen mode Exit fullscreen mode

Logs should look like this:

Using session with network development, sender address 0xd833B5fa468A5a430a890390D8Ec652142836019

Upgrading proxy to logic contract 0xe6d6d1cd339f129275e5b5e7ab39d85bc5642010

Instance at 0x746bb4a872bdfd861c4af5dd9391b3fb5b22fb7a upgraded

0x746bb4a872bdfd861c4af5dd9391b3fb5b22fb7a

Updated zos.dev-7923.json

Now, start a new Truffle console and call your new "setNumber" function:

$ truffle console --network development
let abi = require("./build/contracts/Note.json").abi;
let contract = new web3.eth.Contract(abi, "your-proxy-address");
contract.methods.getNumber().call();
contract.methods.setNumber(65).send({ from: YOUR_OTHER_ACCOUNT });
contract.methods.getNumber().call();
Enter fullscreen mode Exit fullscreen mode

It should print "65".

Voilà! You just wrote, deployed and upgraded an Ethereum smart contract.

Caveats

  • "YOUR_DEPLOYMENT_ACCOUNT" must only be used when setting up a session, for any other interaction with the contract, you have to use other account.
  • If you got a A network name must be provided to execute the requested action error, just start a new session, the old one expired.
  • You might've noticed that we didn't use TruffleContract to interact with the deployed contracts and instead relied on the web3 ^1.0 implementation. This is because I find the former confusing and, on many occasions[1][2], not even functional.
  • There is more to upgradeable smart contracts than what we discussed here. Head to Zeppelin's awesome documentation to get the whole picture. In particular, I recommend looking over the limitations to upgrading storage variables.
  • If you're planning to migrate to Solidity ^0.5.0, I'm sorry to disappoint you that ZeppelinOS doesn't support it yet, as of Dec 2018. There is an option called "--skip-compile", but I felt that it added too much complexity to be worth it. Santiago Palladino from Zeppelin reached out on Twitter and announced that they now do support the ^0.5.0 versions of Solidity. If you want to test it out, just do npm install zos-lib@2.1.0-rc.0 --global.

Wrap-Up

I hope you enjoyed this tutorial and you're as excited about blockchain development as I am, despite the occasional versioning and coordination issues.

I compiled the code used throughout this article in this GitHub repo. There are three branches (master, zos, zos-upgraded) with the correspondent code of the Note contract for every stage we've been through.

Find me on Twitter or Keybase if you want to chat.

Top comments (3)

Collapse
 
abedshouman profile image
abedshouman

Hello Paul,
I am very pleased with your work and would like some clarification on an issue:
When compiling on Truffle , zos and on Ganache it worked flawlessly.
However, when working on my Geth server, Truffle worked but the "zos push" command is displaying the insufficient funds error: gas * price + value .
Any insight would be helpful. Best regards.

Collapse
 
prberg profile image
Paul Berg • Edited

Hey abedshouman, thanks for reading my article!

Unfortunately, I've never used zos with a geth server, so I don't know how to solve your problem. But I can recommend asking this question on the Ethereum StackExchange forum.

Finally, note that OpenZeppelin has recently been through a rebranding, which changed "zos" to "openzeppelin (installabable via npm install -g @openzeppelin/cli). See this repo.

Collapse
 
abedshouman profile image
abedshouman

Hello Paul,

Thank you for your fast reply, i will make sure to use the latest version you mentioned.
Regarding this issue i solved it by transferring balance "more than 7000000000" to the second wallet i used in the session and it worked perfectly.

Thank You