The core of the approach that we have taken at Cardstack was greatly influenced by Elena Dimitrova’s blog post on how Colony builds upgradable contracts. Specifically, the most important thing to consider when upgrading contracts is how to preserve the state of the original contract in the upgraded contract. In a classic software engineering approach, Elena describes how the state of the contract can be separated from the functionality of the contract. This approach allows multiple contracts to share the same state.

When we built upon this approach, we added an additional component to the solution: a “registry contract”. The registry contract performs the bookkeeping necessary to keep track of the shared contract state, keep track of the different versions of each contract, and to bind and unbind the shared state to contracts as new versions of the contracts are added to the network. Furthermore, the registry contract acts as a bridge between the Ethereum Name Service (ENS) and the latest version of our contracts.

Storage

First, let’s talk a little more about how we are handling the contract state. As in the approach described in Elena’s post, we have created a “storage contract” that basically acts a bucket, carrying the state from one version of our contract to the next. In this storage contract, we can accommodate many different types of data in a name-value store. We have also created a “ledger contract”, as in the Colony approach, a specialized form of a storage contract designed specifically for ledgers. However, in our storage contract we’ve created a map of ledgers you can use in place of the “ledger contract”, which we’ll most likely end up doing in the future.

ExternalStorage.sol

The ExternalStorage.sol is our “storage contract”. In it you can see we provide slots for storing all types of data, including more complex structures like actual ledgers, and even “multi-ledgers” (a word I just invented), which is a map of ledgers. This is what we use to store the allowances for the transferFrom() ERC20 function of our contract. For each type, we have a record parameter which is the “key” in our storage contract’s key value store. In our token’s library contract you can see how we set and get these values from our storage:

snippet from CstLibrary.sol

Another interesting thing to note: we’ve done some extra work in our storage contract to allow our ledgers (and multi-ledgers) to be iterate-able so that our storage is more easily introspected. It does cost additional gas to maintain such structures; but for our specific use-cases, we felt the additional gas fees were worth the trade-off.

For completeness, the specialized form of our ledger storage appears below:

CstLedger.sol

You can see how we use our ledger storage in our main ERC20 token contract. Below are an example of a few of the ERC20 functions that leverage the ledger-specific storage:

Snippet from CardstackToken.sol

Registry

The next component of our upgradable approach is the registry. The registry creates a nice abstraction over the actual Ethereum addresses for the contracts that participate in our ecosystem. The idea is that we only need to know the “name” of a contract, and query the registry for the most recent version of the contract that has a particular name. In addition, contracts that need to use the shared storage do not need to know the address of the shared storage; rather, they can declare to the registry their interest in using a particular bucket of storage (which also has a name), and the registry will take care of binding the storage to the contract that declared interest in using it. So at a real high-level, the registry is doing the work of keeping track of the addresses of all the contracts in our system and mapping the names of contracts to the addresses of contracts.

registry.addStorage()