Post originally written by @paulinablaszk on medium (Apr 23, 2019)
In this tutorial we will use ZeppelinOS to fix smart contract vulnerable to underflow/overflow attacks. Firstly we will prepare our environment and create the upgradeable smart contract with some bug and test it. Then we will update the code, upgrade the smart contract and finally, we will check if it is safe.
Upgradeability in smart contracts
In principle smart contracts are immutable. Once deployed to the network, they cannot be changed. Unfortunately in smart contracts, like in every software, mistakes frequently happens. Sometimes they may cost millions of dollars… But don’t worry! Now we have ZeppelinOS — powerful tool to upgrade our smart contracts and fix mistakes. ZeppelinOS is a platform to develop, deploy and operate smart contract projects on Ethereum and every other EVM and eWASM-powered blockchain as its documentation says. And one of its most important features is smart contracts upgradeability.
Environment preparation
You need to have installed essential software for creating and upgrading the smart contracts. You can find the instructions for installing Node.js and NPM, Truffle, Ganache in my previous article.
ZeppelinOS installation
To install ZeppelinOS globally we run a command:
npm install -g zos
To check if you have already installed ZeppelinOs or to make sure that the installation process was successful you can verify the version of your software:
zos --version
I am currently using zos v2.2.3
Project setup
We have to create a new directory for our project and then navigate to it:
mkdir hello-zos
cd hello-zos
Project initiation
In the beginning, we need to create the package.json file:
npm init -y
We run this command with -y
flag to generate package.json without having it ask any questions - it will take default values. Of course, if you want to personalize it, you can just use npm init
and answer given questions.
Next, we can finally init our first ZeppelinOs project:
npx zos init hello-zos
If it asks us for version — we can accept 1.0.0 by pressing enter. A command zos init
creates file zos.json
in our directory. It stores the general configuration of our zos project (ZeppelinOS version, name of the initialized project, its version and information about our contracts that our project contains).
What is more, zos init
also initializes Truffle. This means that the standard structure of truffle project has been created in our hello-zos
directory. Now there are two new directories (contracts and migrations) and truffle-config.js file. Remember to check if truffle-config.js is compatible with your Ganache configuration.
Creating sample smart contract
The problem with smart contracts immutability may especially arise when we find a bug in our code. Without upgradeability, we can do nothing with such a smart contract and it can be a great target of hacker attacks. For our example, we will create the smart contract vulnerable to underflow/overflow attack (you can read more about it here)
In the zos.json
file, we can see that object storing our contracts is empty, so it is time to create our smart contract. First we add new file in contracts directory. We can call it HelloZos.sol
. Our smart contract will contains:
- string state variable name
- uint256 state variable MaxNumber
- uint256 state variable inc
- uint256 state variable dec
- function decrement (that subtracts 1 from the choosen number)
- function increment (that adds 1 to the choosen number)
pragma solidity ^0.5.0;
contract HelloZos {
string public name;
uint256 public maxNumber;
uint256 public dec;
uint256 public inc;
function decrement(uint256 x) public returns (uint256) {
return dec = x - 1;
}
function increment(uint256 x) public returns (uint256) {
return inc = x + 1;
}
}
In the upgradeable smart contract we use init function instead of constructor. Why? The constructor is executed when the smart contract instance is deployed. In the proxy-based upgradeability system, it would never happen. That is why we use initializer. To do so, at the beginning of HelloZos.sol, just after pragma
, we have to import contract Initializable.sol
from ZeppelinOS Library:
import "zos-lib/contracts/Initializable.sol";
Of course, to use ZeppelinOS library we have to install it in our project directory, so we have to run a command:
npm install zos-lib --save
Now we can define our smart contract as Initializable and add init function. The whole contract code is:
pragma solidity ^0.5.0;
import "zos-lib/contracts/Initializable.sol";
contract HelloZos is Initializable {
string public name;
uint256 public maxNumber;
uint256 public dec;
uint256 public inc;
function initialize(string memory _name) initializer public {
maxNumber = 2**256-1;
name = _name;
}
function decrement(uint256 x) public returns (uint256) {
return dec = x - 1;
}
function increment(uint256 x) public returns (uint256) {
return inc = x + 1;
}
}
It is time for the best fun — let’s deploy our smart contract to the network, test it and fix the bug!
Registering smart contract in zos project
First, we have to register the smart contract in our ZeppelinOS project. To do this we run a command:
npx zos add HelloZos
If we have more than one smart contract, we can add all their names at once (separated by spaces) or use --all
flag.
Now our smart contracts (both: HelloZos and Initializable) are compiled (their artifacts are written to build/contracts
directory). And we can find HelloZos contract under contracts filed in our zos.json
{
“zosversion”: “2.2”,
“name”: “hello-zos”,
“version”: “1.0.0”,
“contracts”: {
“HelloZos”: “HelloZos”
}
}
Deploying smart contract to the network
Then we deploy our smart contract to a specified network. In our case it is Ganache local network (make sure that it is running) and use a command:
npx zos session --network local --from 0x9ea26a91A3eF090498827989d17FC0E7eEA0987f --expires 3600
Remember to replace the address after --from
flag with one of your Ganache accounts, but do not use the first one (account[0]
). I usually choose the second (accounts[1]
) or the last one (accounts[9]). You should also use network specified in your truffle-config.js (in our case it is local).
After these preparations we can push HelloZos smart contract to our local network by running a command:
npx zos push
It creates file zos.dev-.json
Creating an upgradeable instance of smart contract
We have just deployed HelloZos contract to the network, but it only implements the logic. If we want to interact with our smart contract we have to create its upgradeable instance by:
npx zos create HelloZos --init initialize --args 'Paulina'
After “create” we add the name of our smart contract, then we call initialize function and we pass the arguments after --args flag (if there is more than one argument they should be separated with commas, without spaces)
The result is:
We can also find our smart contract in the bottom part of zos.dev-.json
"proxies": {
"hello-zos/HelloZos": [
{
"address": "0xfe3d58BdB14d44d1759c1Eb04c91Ac93567dA4Ba",
"version": "1.0.0",
"implementation": "0xf1208D91287aCFBf7c1d7b9c886218a1761f4E20"
}
]
}
The address of your HelloZos proxy is the white address that we can see in the console and we will interact with it later. The “implementation” is the address of current smart contract version and it will change when we upgrade our smart contract.
Interacting with our smart contract
Now we can test how our smart contract works. To communicate with it we will use the truffle console. We open it with the command:
npx truffle console --network local
In the beginning, we will save the instance of HelloZos contract in the myContract
variable:
let abi = require("./build/contracts/HelloZos.json").abi
let myContract = new web3.eth.Contract(abi, "0xd6e0095002f3B287A468203B87D6408f172e1CbF")
Remember to replace the address above with address of your HelloZos proxy!
Now we can check values of our state variables:
myContract.methods.name().call()
let max = await myContract.methods.maxNumber().call()
max
Now we can check our functions with a simple number:
myContract.methods.decrement(5).call()
myContract.methods.increment(5).call()
On the face of it, everything looks fine:
Then we call our function with 0 and max number and check the results:
myContract.methods.decrement(0).call()
myContract.methods.increment(max).call()
Now we know that our smart contract has a bug and is vulnerable for attacks. So what to do? Don’t worry, we use ZeppelinOS so we can upgrade our code in few simple steps.
Upgrading contract
How can we fix it? We should use SafeMath library in our smart contract for all arithmetic operations.
We will use OpenZeppelin contract:
import "openzeppelin-eth/contracts/math/SafeMath.sol";
Please notice that we import openzeppelin-eth
not openzeppelin-solidity
as in usual. What is the difference? Openzeppelin-eth
is the library of EVM packages, that have been already deployed to the blockchain (you can read more about differences between openzeppelin-eth
and openzeppelin-solidity
in this article).
To use it, we have to link the EVM package to our project:
npx zos link openzeppelin-eth
Our zos.json
file is updated and contains a new object with dependencies:
“dependencies”: {
“openzeppelin-eth”: “2.1.3”
}
Now we have to update the code of our HelloZos smart contract. We need to change the add and subtraction characters to the SafeMath functions.
function decrement(uint256 x) public returns (uint256) {
return dec = x.sub(1);
}
function increment(uint256 x) public returns (uint256) {
return inc = x.add(1);
}
We also have to add the statement below just under the state variables:
using SafeMath for uint256;
The code of our whole updated HelloZos:
pragma solidity ^0.5.0;
import "zos-lib/contracts/Initializable.sol";
import "openzeppelin-eth/contracts/math/SafeMath.sol";
contract HelloZos is Initializable {
string public name;
uint256 public maxNumber;
uint256 public dec;
uint256 public inc;
using SafeMath for uint256;
function initialize(string memory _name) initializer public {
maxNumber = 2**256-1;
name = _name;
}
function decrement(uint256 x) public returns (uint256) {
return dec = x.sub(1);
}
function increment(uint256 x) public returns (uint256) {
return inc = x.add(1);
}
}
Important!
When upgrading smart contract we cannot:
- change the type of existing variables,
- change the order in which variables are declared,
- remove the existing variable,
- introduce a new variable before the existing one.
You can read more about upgrades pattern at ZeppelinOS Documentation .
But let’s go back to our brand new HelloZos. Now we can run the command:
npx zos push --deploy-dependencies
We use --deploy-dependencies to deploy EVM package to our local network (we don’t have to do it when we use the mainnet, ropsten, rinkeby or kovan because those packages are already deployed).
Finally we can update our project by using the command:
npx zos update HelloZos
Now we can test our smart contract again in the truffle console.
npx truffle console --network local
let abi = require("./build/contracts/HelloZos.json").abi
let myContract = new web3.eth.Contract(abi, "0xd6e0095002f3B287A468203B87D6408f172e1CbF")
Remember to replace the address above with address of your HelloZos proxy!
Now we can check values of our state variables:
let max = await myContract.methods.maxNumber().call()
myContract.methods.decrement(0).call()
myContract.methods.increment(max).call()
When we try to overflow/underflow we will see
Error: Returned error: VM Exception while processing transaction: revert
Thanks to ZeppelinOS we have updated the code of our smart contract and we are protected from overflow/underflow attacks!