Writing Upgradeable Contracts with ZeppelinOS

Hands-On Example of Upgrading a Contract after Deployment

Introduction

In the world of decentralization, Ethereum was the first to bring Turing-complete smart contracts that provide a platform to build applications where the code does not have to run on a central server when it has the world computer to run on. The code, once deployed, is immutable — meaning once a code is deployed it can’t change, update to add new features or even fix a bug. There are plenty of examples available in which minor bugs have resulted in the loss of thousands of ethers, worth millions of dollars.

Regardless of the well-known DAO and parity wallet hacks, recently a research paper published as “Finding The Greedy, Prodigal, and Suicidal Contracts at Scale” claims that over 34,000 Ethereum smart contracts containing $4.4 million in ETH may be vulnerable to exploitation.

That’s where OpenZeppelin comes in to save the day with Zeppelin Operating System (ZOS), which offers a platform for developing, managing, and operating smart contract applications in Ethereum. With ZeppelinOS, fixing bugs and upgrading smart contracts will no longer be a problem.

How it works

As mentioned earlier, once code is deployed over Ethereum, it is immutable. So how can we write an upgradeable contact? The answer is Proxy Patterns — the code deployed is still unchanged but we can now set up a mechanism by using proxy contracts that will allow to replace the previous contracts with new contracts, hence, updating the functionality.

How a Proxy Contract works

User makes a plain old request to a contract, which in this case is the Proxy Contract. This contract forwards the request to MyContract, which contains the actual functionality, to get the request fulfilled. Let us assume the Proxy Contract address is 0x123 and actual contract i.e MyContract in this scenario is 0x125, so user will request at 0x123 which will be forwarded to 0x125.

This implies that the user will always communicate with the Proxy Contract and the contract behind that, which holds the functionality may be changed. To update the contract would simply mean that the Proxy Contract will be updated to forward the request to a new address instead. To update MyContract, we’ll just deploy the new contact and map the Proxy Contract to the new address of MyContract. Ordinarily, when the contract is changed, that means the state of the previous contract and all the data that goes with it will be lost. To make the contracts upgradeable without losing data, we’ve to separate the business logic and storage. ZeppelinOS uses Unstructured Storage to achieve this.

Writing your first upgradeable contract

Using the following instructions, you can write and deploy your first upgradeable contract. Note that these instructions assume that you have already installed Node.js and you are using a linux system.

Installation

Note: The -g option with all the commands installs that particular package globally.

Install the latest version of truffle using: npm install -g truffle

Install the command-line interface of ZeppelinOS: npm install -g zos

Install the command-line interface of Ganache: npm install -g zos

Setting up

Create a new directory for your application: mkdir ZOSProject && cd ZOSProject

Create a new package.json file usingnpm init

Initialize your ZOS application with your application name zos init ZOSProject, that will create a zos.json file which contains all the information about your application.

After you’ve initialized your application, you’ll see these files and folders in your project directory:

  • migrations
  • package.json
  • truffle-config.js
  • zos.json

Building an upgradeable application

Now, you are ready to build your first upgradable application. We’ll create an ERC20 Token Contract and make it Burnable, using ZOS.

Install ZeppelinOS lib, which you will need to make your contract upgradeable: npm install zos-lib --save

A zos-lib folder will be created in your project’s node_modules folder, which maintains the packages required to run a node project.

Get Migratable.sol from the zos-lib folder in node_modules:

contract Migratable {
/**
* @dev Emitted when the contract applies a migration.
* @param contractName Name of the Contract.
* @param migrationId Identifier of the migration applied.
*/
event Migrated(string contractName, string migrationId);
/**
* @dev Mapping of the already applied migrations.
* (contractName => (migrationId => bool))
*/
mapping (string => mapping (string => bool)) internal migrated;
/**
* @dev Modifier to use in the initialization function of a contract.
* @param contractName Name of the contract.
* @param migrationId Identifier of the migration.
*/
modifier isInitializer(string contractName, string migrationId) {
require(!isMigrated(contractName, migrationId));
_;
emit Migrated(contractName, migrationId);
migrated[contractName][migrationId] = true;
}
/**
* @dev Modifier to use in the migration of a contract.
* @param contractName Name of the contract.
* @param requiredMigrationId Identifier of the previous migration, required
* to apply new one.
* @param newMigrationId Identifier of the new migration to be applied.
*/
modifier isMigration(string contractName, string requiredMigrationId, string newMigrationId) {
require(isMigrated(contractName, requiredMigrationId) && !isMigrated(contractName, newMigrationId));
_;
emit Migrated(contractName, newMigrationId);
migrated[contractName][newMigrationId] = true;
}
/**
* @dev Returns true if the contract migration was applied.
* @param contractName Name of the contract.
* @param migrationId Identifier of the migration.
* @return true if the contract migration was applied, false otherwise.
*/
function isMigrated(string contractName, string migrationId) public view returns(bool) {
return migrated[contractName][migrationId];
}
}

Get openzeppelin SimpleContract.sol and inherit it with Migratable and replace the constructor with function initialize. The constructor can only be called once at the time of initialization that why it is replaced with “function initialize”

contract SimpleToken is StandardToken, Migratable {
string public constant name = "SimpleToken"; // solium-disable-line uppercase
string public constant symbol = "SIM"; // solium-disable-line uppercase
uint8 public constant decimals = 18; // solium-disable-line uppercase
address public owner;
uint256 public constant INITIAL_SUPPLY = 10000 * (10 ** uint256(decimals));
/**
* @dev Constructor that gives msg.sender all of exist
* ing tokens.
*/
function initialize(address _owner) public isInitializer("TokenContract", "1.0.0") {
totalSupply_ = INITIAL_SUPPLY;
balances[_owner] = INITIAL_SUPPLY;
owner = _owner;
emit Transfer(address(0), msg.sender, INITIAL_SUPPLY);
}
}

Compile the contracts using zos add SimpleToken. That’ll compile your contracts and create a JSON file in build folder.

Fire up ganache ganache-cli --port 9545 --deterministic. You have to update configurations in truffle-config.js to reflect this port as follows:

module.exports = {
networks: {
development: {
host: 'localhost',
port: 9545,
network_id: '*', // eslint-disable-line camelcase
},
coverage: {
host: 'localhost',
network_id: '*', // eslint-disable-line camelcase
port: 8555,
gas: 0xfffffffffff,
gasPrice: 0x01,
},
},
compilers: {
solc: {
version: '0.5.2',
},
},
};

zos push --network local

This creates a zos.local.json file with all the information about your app in this specific network.

zos create SimpleToken --init initialize --args 0x3c296fdeceed7384442b985f241eface12b3247d --network local

The --init parameter is optional to call the initialization function after creating the contract. You can also pass the --args option to pass arguments into the initialize function like we’ve passed the owner of the contract. After executing the above command, you will see an update in zos.local.json that will now have a proxies object having SimpleToken address and its version.

Interacting with the Contract

Run npx truffle console

Create an object of SimpleToken

mycon = SimpleToken.at(“0xa0a533e78611065a78d64fe9bc61673f855d547c”)

Check the InitalSupply of Contract using mycon.INITIAL_SUPPLY()

Transfer some tokens to another address for testing the persistence of data when we upgrade.

Upgrading SimpleToken

Now open SimpleToken.sol and update it so that it inherits BurnableToken.sol

contract SimpleToken is StandardToken, BurnableToken, Migratable {
string public constant name = “SimpleToken”; // solium-disable-line uppercase
string public constant symbol = “SIM”; // solium-disable-line uppercase
uint8 public constant decimals = 18; // solium-disable-line uppercase
address public owner;
uint256 public constant INITIAL_SUPPLY = 10000 * (10 ** uint256(decimals));
/**
* @dev Constructor that gives msg.sender all of exist
* ing tokens.
*/
function initialize(address _owner) public isInitializer(“TokenContract”, “2.0.0”) {
totalSupply_ = INITIAL_SUPPLY;
balances[_owner] = INITIAL_SUPPLY;
owner = _owner;
emit Transfer(address(0), msg.sender, INITIAL_SUPPLY);
}
}

After upgradation, execute zos push --network local to push these changes to the network.

Finally run zos upgrade SimpleToken --network local to upgrade the already deployed code.

Run npx truffle console --network local

Instantiate an object of the contract again. Notice that we are not changing the address here, even though we have effectively changed the contract, because this address belongs to the Proxy Contract.

mycon = SimpleToken.at(“0xa0a533e78611065a78d64fe9bc61673f855d547c”)

Now burn the half of the tokens mycon.burn(“5000000000000000000000”). Where earlier, the tokens couldn’t have been burned because the contract didn’t support this function, now it will.

Check the totalSupply. It should have halved now!

Now check the balance of the address we transferred 1000 tokens to, with the previous contract.

That is intact, despite the fact that we have upgraded the contract, which means that even though the contract was changed, the data is not lost.

Conclusion

We have successfully created and used an upgradable contract, updating the Simple ERC20 token contract to a Burnable ERC20 token contract. We were also able to retain the state of the previous contract, as can be seen in the demo. Proxy Contracts are a powerful tool which should be used when upgrades are possible, or if a contract is security-critical and you need to have a patching mechanism.