I’ve prepared updated version of my previous article - using OpenZeppelin SDK v.2.5.2 (originally published here )
How to create an upgradeable smart contract using OpenZeppelin SDK — example of fixing smart contract vulnerable to underflow/overflow attacks
In this tutorial we will use OpenZeppelin SDK 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 the smart contracts, like in every software, mistakes frequently happens. Sometimes they may cost millions of dollars… But don’t worry! Now we have OpenZeppelin SDK (former ZeppelinOS) — powerful tool to upgrade our smart contracts and fix mistakes. OpenZeppelin SDK 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 and Ganache in my previous article.
OpenZeppelin SDK installation
To install OpenZeppelin SDK globally we run a command:
npm install -g @openzeppelin/cli
To check if you have already installed OpenZeppelin SDK or to make sure that the installation process was successful you can verify the version of your software:
oz --version
In all OpenZeppelin SDK command I use oz
which is shorthand from openzeppelin
.
I am currently using OpenZeppelin SDK v2.5.1.
Project setup
We have to create a new directory for our project and then navigate to it:
mkdir hello-oz
cd hello-oz
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 OpenZeppelin SDK project:
oz init
We will be asked for project name and version — we can use hello-oz name, pick version 1.0.0 and accept by pressing enter.
A command oz init
creates file project.json
in .openzeppelin
directory. It stores the general configuration of our project (manifest version, name of the initialized project, its version and information about our contracts that our project contains).
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 project.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 HelloOz.sol
. Our smart contract will contains:
- string state variable name (just for testing)
- uint256 state variable inc
- uint256 state variable dec
- function decrement (that subtracts 1 from the chosen number)
- function increment (that adds 1 to the chosen number)
pragma solidity ^0.5.0;
contract HelloOz {
string public name;
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 HelloOz.sol, just after pragma
, we have to import contract Initializable.sol
from OpenZeppelin Upgrades:
import "@openzeppelin/upgrades/contracts/Initializable.sol"
Of course, to use OpenZeppelin upgrades (former ZeppelinOS library zos-lib
) we have to install it in our project directory, so we have to run a command:
npm install @openzeppelin/upgrades --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 "@openzeppelin/upgrades/contracts/Initializable.sol";
contract HelloOz is Initializable {
string public name;
uint256 public dec;
uint256 public inc;
function initialize(string memory _name) initializer public {
name = _name;
}
function decrement(uint256 x) public returns (uint256) {
return dec = x - 1;
}
function increment(uint256 x) public returns (uint256) {
return inc = x + 1;
}
}
We can check if our smart contract compiles properly using
oz compile
It is time for the best fun — let’s deploy our smart contract to the network, test it and fix the bug!
Creating an upgradeable instance of smart contract
We have to run our development network (Ganache). Now we can deploy an upgradeable instance of our smart contract by:
oz create
After running this command we have to pick our contract name and network. The answer for question Do you want to call a function on the instance after creating it? is y
and we can choose initialize
function and enter the parameter. The result is:
The “address” of your HelloOz proxy is the white address that we can see in the console and we will interact with it later.
We can also find our smart contract in the bottom part of dev-<number>.json
in .openzepplin
directory
"proxies": {
"hello-oz/HelloOz": [
{
"address": "0x1161e67eFf76cb6F7Fb18d52F674D3e9C59dc950",
"version": "1.0.0",
"implementation": "0x64786cCD5C1C2572CEe97B09ed1F12fa0857a561",
"admin": "0x1aA7817c55ACee80426e10eAB66320F686539880",
"kind": "Upgradeable"
}
]
}
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 openzeppelin send-tx
and openzeppelin call
. After running these commands we have to pick the network, instance, function and (when needed).
In the beginning, we can check the values of our state variables:
oz call
We can see that name was properly initialized and dec and inc returns 0.
Now we can check our decrement function with a simple number (for example with 5) by running :
oz send-tx
We can do the same with increment
On the face of it, our results look fine.
Now it is time to decrement function with 0:
and increment with 115792089237316195423570985008687907853269984665640564039457584007913129639935
(which is maximum uint256 you can pass in Solidity — 2^256–1) :
The results are incorrect!
Now we know that our smart contract has a bug and is vulnerable for attacks. So what to do? Don’t worry, we use OpenZeppelin SDK 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 Contracts:
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
To use it, we have to link the EVM package to our project:
oz link @openzeppelin/contracts-ethereum-package
Please notice that we import openzeppelin/contracts-ethereum-package
not openzeppelin-solidity
as is usual. What is the difference? contracts-ethereum-package
is the library of EVM packages, that have been already deployed to the blockchain ( you can read more about differences between contracts-ethereum-package
and openzeppelin-solidity
in this article).
Our project.json
file is updated and contains a new object with dependencies:
"dependencies": {
"@openzeppelin/contracts-ethereum-package": "^2.2.0"
},
Now we have to update the code of our HelloOz
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 HelloOz
:
pragma solidity ^0.5.0;
import "@openzeppelin/upgrades/contracts/Initializable.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
contract HelloOz is Initializable {
string public name;
uint256 public dec;
uint256 public inc;
using SafeMath for uint256;
function initialize(string memory _name) initializer public {
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 OpenZeppelin SDK Documentation .
We can update our project by using the command:
oz upgrade
Now we can test our smart contract again with openzeppelin send-tx
( decrement with 0 and increment with 115792089237316195423570985008687907853269984665640564039457584007913129639935
)
Both transactions have reverted!
Thanks to OpenZeppelin SDK we have updated the code of our smart contract and we are protected from overflow/underflow attacks!