Designing an upgradeable Ethereum contract

Mira Belenkiy
6 min readSep 26, 2018

--

CENTRE will be a governed network powered by price-stable crypto assets (see whitepaper). Our first contribution is the USD Coin (USDC): an Ethereum ERC-20 token that is redeemable into USD. You can trade the USDC token on Poloniex and other digital asset exchanges. The total supply will be determined by CENTRE approved Issuers who maintain reserves in escrowed accounts and then get permission to mint tokens. You can look at our contract on Etherscan, as well as the source code and design overview on GitHub. The main contract is FiatTokenProxy that forwards all function calls to FiatTokenV1.

Circle (and any future issuer) has an obligation to its customers that extends beyond the written code of the smart contracts CENTRE deploys. We chose to write a Ricardian smart contract: CENTRE will use the USDC smart contract to implement controls relating to Circle’s obligations to its USDC-holding customers and we have created a mechanism that will let us change the code if we determine that Circle is not fulfilling its part.

But how do you upgrade an Ethereum contract?

Upgrading Smart Contracts

Ethereum lets anyone put code on the blockchain. Ethereum assigns the code an address, and anyone can call functions on the code stored at that address. No-one can ever change the code at a particular address, not even the owner. Software upgrades have to be done using address pointers and redirection techniques.

There are several challenges:

  1. We needed a persistent data storage contract for our data. The USDC contract needs to store balances for millions of users, and migrating the data to a new contract would be expensive.
  2. We needed a mechanism to change which smart contract had permission to write to the persistent storage. This would be our upgradeable logic contract.
  3. Users and large ERC-20 token exchanges needed to be oblivious to a USDC logic contract upgrade. CENTRE had to publish a single USDC contract address that would be valid even after an upgrade.
  4. The upgrade mechanism had to be as simple as possible to enable through testing.

Eternal unstructured storage

We first looked at Rocket Pool’s eternal unstructured storage model. In this model, the USDC token would consist of two contracts: a FiatToken contract that writes to an EternalStorage contract. In case of a problem, we would deploy FiatTokenV2 and tell EternalStorage about the upgrade.

EternalStorage would contain mappings to store data of every basic Ethereum data type. For example, to store a uint256, EternalStorage would use the state variable:

mapping(bytes32 => uint256 private uIntStorage;

Each mapping would have corresponding getter and setter functions:

function setUint(bytes32 key, value) onlyLogicContract {     uIntStorage[key] = value;}

FiatToken would use the sha3 hash of the variable name as the key to the mapping. To store totalSupply=5000, FiatToken would call:

setUint(sha3(“totalSupply”), 5000).

The mapping between user accounts and their balance would have to be stored as

setUint(sha3(“balance”, accountAddress), value);

We discovered that this simple design is very dangerous! Ethereum tightly packs input to the sha3 hash and then pads it at the end with 00 to get an input length that is a multiple of 32 bytes. This means sha3(A,B) can equal sha3(C,D) because of how EVM parses the input.

The chief mitigation is using length-encoded input. However, it still does not guarantee collision free keys. We wanted a more robust approach.

Eternal structured storage

Our next thought was to replace the unstructured mappings with data specific variables:

uint256 totalSupply;mapping(address => uint256) balance;mapping(address => bool) blacklisted;bool paused;

We wrote specific getters and setters for each variable inside the EternalStorage contract. Then we wrote a parallel set of getters and setters inside the FiatToken contract to call the EternalStorage contract.

Our codebase rapidly increased in size. The list of variables above is only a small subset of the variables in our actual USDC contract. We needed a simpler approach that was easier to analyze and reason about.

More problems with EternalStorage

The EternalStorage model had a bigger problem. The main USDC contract would be FiatToken. After an upgrade, even though EternalStorage would know about FiatTokenV2, the token owners and token exchanges would not. Every FiatToken function had to check whether EternalStorage was using a different logic contract and redirect the call. Our codebase grew even more.

Proxy Model

ConsenSys Diligence strongly encouraged us to switch to the Zeppelin OS proxy model.

In the proxy model, the main contract is a proxy with almost no logic of its own. Every call to the proxy contract gets sent to the fallback function and then forwarded via a DELEGATECALL to the current implementation — our FiatToken contract. Since this is a DELEGATECALL, the FiatToken reads and writes directly to the storage of the proxy contract.

The beauty of the proxy model is that we can switch the implementation contract without affecting our users. Individuals and exchanges would continue to call ERC-20 functions on the proxy.

One of our smart contract developers rewrote our entire contract to create a new prototype. He replaced our EternalStorage with a proxy and deleted about 75% of the code. What remained was a simple contract with no function calls or if-then statements. It was much easier to understand and we could be confident we gave it full unit test coverage (more in another blog post). We had a winner!

The proxy approach solved all our problems. We deployed the AdminUpgradeabilityProxy as our main USDC contract. It is an advanced version of the basic proxy with special guards against malicious backdoors.

Contract migration

Before settling on the upgradable proxy approach, we considered several other alternatives. To begin with, the very concept of an upgradeable smart-contract is controversial. One of reviewers, Trail of Bits, recommends contract migration instead of contract upgrades because upgradeability increases code complexity. Contract migration requires notifying all users of a new contract address. We were concerned about the operational challenges of coordinating the migration of a widely traded ERC-20 token.

Trail of Bits also identifies interesting issues with where the Solidity compiler stores variables in proxy and implementation contracts. We avoid these problems by using a simple proxy that does not share any common variables with the implementation. We also tested several upgraded implementation contracts to ensure consistent variable layout.

Upon weighing all concerns required to meet our obligations to our customers, we chose to use the upgradable proxy pattern. We did so based on the strength of the extensive testing and community confidence in Open Zeppelin libraries, operational simplicity, and our own comprehensive design and review process.

Advice on using proxied contracts

We would like to offer some general advice to developers who want to use the proxied contract design pattern.

  1. The implementation contract writes to the proxy contract, but it stores data based on the order in which the variables are declared in the implementation. To upgrade to a new implementation, developers must declare the same variables in the same order. The easiest way to ensure this is to have the new implementation inherit from the original. We created several test versions of FiatTokenV2 to ensure we can upgrade successfully.
  2. The ReadContracts tab on Etherscan does not work with proxied contracts. Users can still see the ERC-20 token properties such as balances, total supply, etc. This is because data is stored in the proxy contract, but the proxy has no getter functions to notify Etherscan that they exist. Reading all the contract data requires custom tooling.
  3. The AdminUpgradeabilityProxy we used as a base class does not have getters for the admin and implementation address. Developers need to either create getters in the implementation contract or manually retrieve their value from the blockchain using the web3js getStorageAt() function.

We are very grateful to our external reviewers, Trail of Bits and ConsenSys Diligence. They gave us invaluable design and testing advice, and caught several subtle (and not so subtle) issues.

--

--

Mira Belenkiy

Cryptographer specializing in zero-knowledge, crypto-currency, and privacy preserving technology.