Flexible Upgradability for Smart Contracts

The immutable nature of Ethereum smart contracts is incredibly powerful. It allows us to build applications that are completely tamper-proof, untouchable by any individual, corporation, or government. Every participant is subject to the same set of rules, and these rules will never change.

But ultimately, these rules are created by humans. As humans, we are prone to the occasional mistake. It is impossible for us to see the whole picture from day one and build flawless systems that have no need for adaptability or improvement.

In order to balance immutability and flexibility, we need mechanisms for upgrading decentralized apps (dapps) after they are deployed. In this article, we will describe how this can be done today using some simple but powerful patterns.

While we will be describing the mechanisms for upgradability, we will not discuss the process for how an upgrade is triggered. An assumption can be made that upgrades will be executed by an “owner”. This owner could be an individually held address, a multi-sig contract, or a complex decentralized autonomous organization (DAO).

Existing Patterns

The Zeppelin Solutions and Aragon teams have designed some very useful patterns for upgradability. We’ve borrowed code from Proxy Libraries in Solidity and Smart Contract Upgradability Using Eternal Storage.

Upgradable Dapp Toolkit

At Level K, we are applying these patterns to our (work in progress) Upgradable Dapp Toolkit. This toolkit contains some core contracts that help enable upgradability for any dapp.

Example Code

If you want to stop reading and just look at code, go for it! All code for this post is here: github.com/levelkdev/upgradability-blog-post

How to Make an Upgradable Token

We’ll assume you have some knowledge of ERC20 tokens and the code that makes them work. If not, take a look at Zeppelin’s ERC20 contract code to get a better understanding.

Let’s say we want to deploy a new token called ShrimpCoin. One can only guess what this token might be used for.

Here’s an architecture diagram to show what upgrading ShrimpCoin from a standard token to a “mintable” token might look like:

All of this will be explained in detail, so read on!

Proxy and Delegate Contracts

You’ll notice that ShrimpCoin is a Proxy contract. This means that when a transaction (transfer() for example) is sent and ShrimpCoin does not know about the function specified in the transaction, it will proxy the transaction to what we’ll refer to as a “delegate” contract.

This is done using the native EVM code, delegatecall. From the solidity docs, a contract using delegatecall

…can dynamically load code from a different address at runtime. Storage, current address and balance still refer to the calling contract, only the code is taken from the called address.

Put simply, this means that ShrimpCoin takes on all the functionality of our delegate contract, TokenDelegate. In order to upgrade the functionality of ShrimpCoin, we just need to tell the proxy to use a new delegate (MintableTokenDelegate in our example).

The code for the Proxy contract is slightly intimidating (there’s some EVM assembly code in here):

from https://blog.zeppelinos.org/smart-contract-upgradeability-using-eternal-storage/

Take a look at the fallback function function() payable public {…, which is a catch-all for transactions with unknown functional signatures. Within this function, the assembly code is where thedelegatecall happens. This could be done in plain old solidity for functions that don’t have a return value. However, delegatecall only returns a single value to indicate whether it succeeded or failed. This block of assembly gets the actual return value of the proxied transaction and returns it.

The Proxy contract is an Ownable, allowing some predetermined owner to execute upgradeTo() and upgrade the contract to use any delegate contract.

Proxy Delegate State

When a proxy contract uses a delegate contract’s functionality, state modifications will happen on the proxy contract. This means that the two contracts need to define the same storage memory. The order that storage is defined in memory needs to match between the two contracts.

Here’s an example to illustrate this concept. Assume that Thing is set up to use functionality from ThingDelegate:

A few interesting things going on here…

Although the storage memory matches (both contracts define a uint256), the variable names (num and n) do not match. Even though these variables have different names, they will still compile to bytecode with matching storage memory. As a result, when Thing proxies a call to incrementNum() on ThingDelegate, this will still increment num on Thing state.

Also, additional storage and state is defined here (string name = "Thing";). This storage won’t be manipulated by ThingDelegate. The order of memory matters here. If name was defined above num, then incrementNum() would try to add 1 to a string.

Something that we love about this pattern is that ThingDelegate does not need to know about Thing. Once ThingDelegate is deployed, any contract can use it as a delegate, thus ThingDelegate is publicly consumable. In fact, any deployed contract can be used as a delegate. It does not need to be defined as such.

ShrimpCoin and TokenDelegate

Let’s look at the slightly more complex ShrimpCoin and TokenDelegate, along with some storage helpers, StorageConsumer and StorageStateful:

This follows the same pattern as the Thing example. But the common state here is the address of a KeyValueStorage contract (explained in the next section).

It’s important to note that ShrimpCoin extends StorageConsumer before extending DetailedToken. If these were swapped, TokenDelegate would try to use string name; instead of KeyValueStorage _storage; for that getUint() operation. This would cause the transaction to revert.

Key Value Storage

The proxy delegate pattern is super useful for upgrading functionality, but what if we want to add something to state that wasn’t defined on the original contract? That’s where the “Eternal Storage” pattern comes in. This pattern was originally described in Writing Updatable Contracts in Solidity.

Here is a simplified KeyValueStorage contract:

This contract defines three mappings of mappings. These will be used to store values of type uint256, bool, and address. The highest level key for these mappings will be msg.sender, which is the address of a smart contract that is executing a write or read using the set/get functions.

Logically, this key/value storage is structured like this:

"totalSupply": 1000
"totalSupply": 2000
"isPaused": true
"isPaused": false

In our example, the ShrimpCoin contract address will be msg.sender, while the key could be something like "totalSupply".

Since we’re keying off of msg.sender, all key/value pairs are scoped to the sender contract. A contract cannot manipulate another contract’s storage values. This means that after KeyValueStorage is deployed, it is open for any contract to use.

Getting and Setting Key/Value Pairs

We can use the getters and setters provided by KeyValueStorage to read or manipulate state values.

This code would set totalSupply to 1000 for the calling contract:

_storage.setUint("totalSupply", 1000);

We can also set more complex values, such as mappings. We can create a hashed key value using keccak256() in order to set a balance of amount for balanceHolder within a balances mapping:

_storage.setUint(keccak256("balances", balanceHolder), amount);

These low-level storage functions are much more verbose than the usual balances[address] = amount; syntax, so it will make sense to wrap them in some higher level functions. Look at how this is done in TokenDelegate:

Internal functions like getBalance() make the balance storage easier to work with. This functionality could be further refactored into a library to be shared across multiple delegate contracts.

Upgrading to a Mintable Token

Let’s say we deploy ShrimpCoin with a proxy pointer to TokenDelegate (we’ll call this V1). The V1 implementation is limited because TokenDelegate doesn’t provide a mechanism for initial creation, or “minting” of tokens.

The owner address of ShrimpCoin can call upgradeTo() to point the proxy at an instance of MintableTokenDelegate (we’ll call this V2).

The V2 MintableTokenDelegate contract provides some additional functions for minting, which act on a totally new set of storage keys:

It also takes on all the functionality of the V1 TokenDelegate, so contracts like ShrimpCoin that are proxying to it won’t lose any of their original functionality.

Future Projections

We’ve presented a simple example of a token upgrade, but this approach can also be applied to more complex scenarios. One use case that we’re excited about at Level K is an upgradable Token Curated Registry.

These patterns open up some really cool opportunities to make and deploy re-usable delegates for common functionality, which could be leveraged by a potentially massive and diverse set of dapps.

A few advantages of using the combined proxy delegate and key/value storage upgradability pattern:

  • Gives total flexibility to upgrade both functionality and storage
  • Encourages the creation and deployment of standard contracts that encapsulate common functionality
  • Leveraging pre-deployed smart contracts as delegates (which will someday be thoroughly battle tested) is less error prone.
  • Re-use of pre-deployed contracts means simpler audits and reduced deployment gas costs.

And some disadvantages:

  • The upgrade “owner” has full control, meaning full trust. In order to design a truly trustless contract that is also upgradable, the “owner” must itself be a trustless contract.
  • Syntax for interacting with key/value storage is more verbose than standard solidity state variable operations.
  • A flaw in a standardized and shared contract could lead to widespread damage across all dapps that consume the contract.

We’re excited to hear how other developers are using these patterns. The Rocket Pool project is doing some awesome things with upgradability. We’d love to hear about others!

If you found this post helpful please let us know! Send some love to contact@levelk.io

Thanks for reading :-)