Building Upgradeable Smart Contracts

Andrea Di Nenno
clearmatics
Published in
9 min readJun 7, 2019

We’ve recently gone through the process of updating one of our contracts to support upgrades without a hard fork and we thought it would be useful to share how we did this.

Introduction

Last year, we open-sourced our Asset Token smart contract which was designed to represent a fungible asset as an ERC223 token. As we continue to develop Autonity, we know the importance of being able to update smart contracts such as the Asset Token without requiring a hard fork for every new update. We have included a example upgradeable smart contract for you to interact with, which can be found here.

After a brief high-level explanation of the architecture, we cover some of the security issues encountered. The last section is dedicated to a step-by-step guide for creation, deployment, upgrading and testing of an upgradeable contract.

The solution presented here has been formalized within the ZeppelinOS project, forked from the well-known OpenZeppelin. With this huge and exciting project, they’ve provided a set of improvements to the smart contract development experience, such as on-chain packages and libraries, state channels, trusted oracles, other than ZepKit — a toolkit that encapsulates Truffle, Infura, OpenZeppelin, and ZeppelinOS.

Proxy Patterns

If a smart contract has already deployed it cannot be modified, so how can we update it? We use a proxy contract that receives all message calls from the outside world and redirects then to a contract containing the application logic. We can then deploy a new logic contract and update the proxy contract to reference that new contract.

In Ethereum this is possible thanks to the delegatecall opcode: by putting it into the contract’s fallback function, all calls will be proxied no matter what. This doesn’t mean that the application contract will be in responsible for executing the transaction, as the proxy actually call function code from the logic contract, executing that code in proxy contracts own context. Our application state will then naturally be inside the proxy storage, and context variables such as msg.sender or msg.value will still be related to the account which sent the transaction towards the proxy.

Fortunately the basic proxy functionality is quite intuitive and a standard code has been provided as part of ZeppelinOS, benefiting from months of testing and research by the community. This is explored further in one of their blog posts, which contains an overview of the different proxy patterns that have been explored as well as related storage management.

The storage management is the most delicate part when upgrading a smart contract, as the proxy needs to at a minimum to keep track of the latest logic contract address, as well as having a function to update it, with related governance mechanisms. Before diving into the code, it’s better to understand the few caveats related to the proxy pattern.

Security Issues

As previously mentioned, having logic contracts updating the proxy’s state instead of theirs, means that one could unwittingly overwrite variables. This in turn would cause us to lose some of the application state or, even worse, critical proxy variables. Therefore, other considerations must be taken into account such as storage collision both with the proxy and between implementations, function clashes as well as the constructor caveat, which we delve into below.

Storage Collision With Proxy

The first concern is how can we make sure that proxy variables (i.e. proxy owner, current logic address) don’t get overwritten by the application state.

Imagine the address _implementation variable in the proxy contract, and address _owner is instead the first variable of the logic contract. Both variables are 32 byte size, so they would be stored in the first storage slot by the EVM. The consequence is that when the logic contract writes to _owner, it would do that within the proxy’s state, thus overwriting _implementation.

The solution adopted by ZeppelinOS is unstructured storage. Proxy variables are assigned to a pseudo-random storage slot, so that the probability of a logic contract declaring a variable at the same slot is negligible. Hashing some strings is a possible solution to get a pseudo-random (and very high) number that we can use as a slot position, like in the example they provide:

bytes32 private constant implementationPosition = keccak256(“org.zeppelinos.proxy.implementation”);

As a result, the logic contract does not need to care about the proxy’s variables location.

Storage Collision Between Implementations

The aforementioned approach avoids storage collision between logic and proxy, but cannot be done between subsequent updates of logic contract. Storage in Ethereum is set sequentially in order of appearance in the contract code, starting from the 0 position, new versions of a logic contract should either extend previous versions or only append new variables to the storage hierarchy. Fortunately, ZeppelinOS CLI detects when such collision could occur and warns.

Constructor Caveat

In Solidity, code inside a constructor is executed only once during its deployment and as such isn’t included in the contract’s bytecode. This means that code within the constructor will never be executed in the context of the proxy’s state, and thus of our application.

Constructor code of logic contract then must be moved inside an initializer function, as shown in the example below:

import "zos-lib/contracts/Initializable.sol";contract AssetToken is Initializable, ERC223Interface, IERC20 {  function initialize(string memory symbol, string memory name,     address owner) public initializer {
_symbol = symbol;
_name = name;
_owner = owner;
//constructor code
}
}

Like for regular constructors, we need the function to be called only once, and the initializer modifier from the Initializable library covers that. However, in this case we need to manually invoke the initializer of all parent contracts if any are inherited.

Also related to this is the value initialization in field declaration. As setting a value within its declaration is like setting it in the constructor, and as such it will be ignored by the proxy. It is important to make sure that all initial values are set in an initializer function.

Function clashes

Another thing to consider when writing and testing upgradeable contracts is the possibility of function clashes. This happens when proxy and logic contracts have functions with the same name, or more precisely with the same 4-byte signature.

This leads to a discovered vulnerability that can be exploited to create backdoors inside proxy contracts.

The solidity compiler tracks such collisions only within the same contract and does not do so among different ones, such as in this case. So when a function is called and is present in both contracts, which one should be executed?

ZeppelinOS deals with this problem through the so called transparent proxy pattern, that decides which calls to delegate based on the caller address, msg.sender:

  • If the caller is proxy admin, the proxy will not delegate any calls, thus responding only to messages it understands.
  • Otherwise the call will be always delegated, whether it matches one of the logic contract functions or not.

Setting Up A Project

Lastly, here’s the how-to guide.

  1. In order to set ZeppelinOS up in our project, assuming that this is already a Truffle project, run:

npm install zos

2. To make use of the ZeppelinOS Javascript Library, for example needed to import the Initializable contract, run:

npm install zos-lib

3. Finally, initialize a ZeppelinOS project by:

npx zos init project-name

This will create a zos.json file, containing all the information related to the ZeppelinOS project. Here a more detailed explanation of this file.

Add a contract

Few changes are needed to give a contract the power of upgradeability, namely:

  • Make use of initializer function instead of constructor, as mentioned here
  • Import upgradeable libraries. For example contracts previously belonging to openzeppelin-solidity must be imported from openzeppelin-eth

ZeppelinOS allows us to link packages that has already been deployed to the blockchain, instead of wasting resources deploying them again every time we need them in a project. If your contract imports some of them, make sure that you link these dependencies to the project by calling:

npx zos link openzeppelin-eth

This command will install all openzeppelin-eth contracts locally, which are needed at compile time and will also update the zos.json file with a reference to the linked package, to make sure that when you deploy the project it will use the EVM package that already exists on the blockchain.

When the contract code is ready, we can add it to the project with:

npx zos add ContractName

It will compile the contract with Truffle, add it to the project and update the zos.json configuration file.

Deploy the Project

Assuming that a blockchain network is running, run:

npx zos session --network <network_name> --from <account_address> --expires 3600

This command starts a session to work with in the network you specify (retrieved from truffle.json), setting a default address for future transactions you run, as well as other configuration parameters.

Now you can deploy the whole project with the command:

npx zos push

If you are using a test blockchain, the package dependencies (e.g. openzeppelin-eth) need to be deployed. This is done by adding --deploy-dependencies flag to the push command. Otherwise, those who are already working with Ethereum testnets or mainnet can skip that flag.

This command deploys ContractName and all the other contracts in the zos.json to the specified network, returning their addresses. Additionally the zos.dev-<network_id>.json file is created with all the information of the project in this specific network. More info about this file can be found here.

Deploying a Proxy

Once we have deployed the logic we can create the proxy (which we will always interact with) using the following command:

npx zos create ContractName --init <initialize_function_name> --args <list_of_arguments>

As you can see, we need to specify the initializer function name we used in our logic contract as constructor (default is just initialize) and the argument it takes, as a comma-separated list with no spaces in between. The actual initialization of logic variables takes place here.

The command will update the zos.dev-<network_id>.json file with the proxy contract address.

Upgrading the Logic

On we go with the contract update! After having modified the logic contract (minding the few caveats), push it to the network with:

npx zos push

And finally update the proxy’s pointer to the newest logic with:

npx zos update ContractName

As it should be clear at this point, we would keep interacting with the address returned by the create command above, i.e. the proxy’s one.

If our modification has also affected the initializer function in the contract, because of a new variable which needs to be set, we could specify the modification inside the function. This would only be fine only for newly deployed instances of it, not with one that is instead being updated.

In lieu, we would need to specify only the new initialization values within a new function, let’s say initializeV2, and specify it in the update command as such:

npx zos update ContractName --init initializeV2 --args <list>

Testing

In terms of testing, ZeppelinOS provides specifically designed tools for testing your contracts with the exact set of conditions that they would have in production. The bare bone structure of a test file should contain the following:

const { TestHelper } = require("zos");const { Contracts, ZWeb3 } = require("zos-lib");ZWeb3.initialize(web3.currentProvider);const AssetToken = Contracts.getFromLocal("AssetToken");
  • TestHelper facilitates the creation of a project object that will set up the entire ZeppelinOS project within a test environment
  • Contracts and ZWeb3 help us retrieve compiled contract artifacts and set up our Web3 provider to ZeppelinOS

The beforeEach clause should contain at least:

beforeEach(async () => {  PROJECT = await TestHelper();  PROXY = await PROJECT.createProxy(AssetToken, {    initMethod: "initialize",    initArgs: ["CLR", "Asset Token", addrOwner]  });});

Here we retrieve the project structure and create a proxy instance to our logic. Finally we interact with the logic contract again always through the PROXY, by using their methods object, like in a regular Web3 1.0 Contract interface, for example:

const name = await PROXY.methods.name().call();

That’s all folks!

Cheers,

Andrea

Andrea Di Nenno, Junior Blockchain Engineer, Clearmatics

Tweet us @Clearmatics

References

https://docs.zeppelinos.org/docs/pattern.html

--

--