Upgradable Contracts in Solidity
Solving the riddle of how to improve immutable Ethereum code
At Cardstack we are building an application framework that will enable developers to create and host dApps that put user experience first. As part of this effort we’re creating an extensible and future-proof token to power our framework. Enter: Upgradable Contracts.
So why are upgradable contracts important? Requirements change, the market changes, the world becomes more complex, entropy increases — software, much like biology, is in a constant state of flux. The most successful software is the software that adapts best to a changing world. A smart contract isn’t so smart if it can’t evolve over time.
We’ve found a way to solve this crucial problem.
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.
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 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:
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:
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:
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.
After the storage contract is deployed we add it to the registry and give the storage contract a name by calling
registry.addStorage(). From this point forward, when contracts are registered with the registry they can declare their intent to use the storage by name. The registry is then responsible for resolving the storage name to a specific address, as well as granting the contract “permissions” to use the storage (more on this later).
For contracts that participate in our ecosystem that are not storage contracts, e.g. our token contract, we use
registry.registerContract() to add them to our registry. Our contracts implement a
storable.sol interface, which has the following declaration:
As you may see, the ledger name and storage name used by the token contract are specified in the constructor of our token contract and then just returned in the
storable.sol functions. Additionally, our token contract implements a
configurable.sol interface, which declares a
configureFromStorage() function. In this function we the registry to resolve the storage names to actual addresses:
And at this point we have a token contract that is bound to the storage contract(s) for which it is has declared an interest.
So a quick recap:
- We add storage to the registry and assign the storage a name.
- We create contracts (in this case a token contract) that declare an interest in using storage with a specific name.
- We register a contract with the registry, which then resolves the names of the storage to specific Ethereum addresses within the contract being registered.
The ability to upgrade a contract is exposed in the
upgradeContract() function. This function’s parameters are the name of the contract and the address of the “successor” contract (where the current contract becomes the predecessor contract). The registry will use the same mechanism that was leveraged in the registration of the predecessor contract to discover the storage that the successor contract has declared it is interested in using. The registry resolves the address for the storage in the successor contract and binds the storage within the successor contract using
During the upgrade operation, the permissions of the predecessor contract upon the storage is revoked, so that the predecessor contract can no longer manipulate the storage. Likewise, the permissions of the successor contract upon the storage is granted, so that the successor contract can manipulate the storage. Additionally, the modifier
unlessUpgraded will prevent the predecessor contract’s functions from executing, and instead cause them to revert. The predecessor contract also acquires a property that points to the successor contract as result of the registry upgrading the contract, so the clients of the contract can discover the new address of the upgraded contract (the successor contract). Finally a
ContractUpgraded event is emitted when the contract upgrades (as well as an
AddrChanged event for EIP-137 ENS resolver support — more on that later).
But what about “permissions”?
So I mentioned the word “permissions” a few times in this post — what do I mean by that? The Cardstack contracts have the ability to designate addresses from which
msg.send calls can invoke privileged functions. In addition, we have devised two different levels (as of the time of this writing) of privileged access (most likely there will be many more, and we’ll probably want to further generalize this solution as time goes on). The lower level role of privileged access is what I’m calling an “admin”, and the higher level role of privileged access is what I’m calling a “super admin”. Generally admins have the ability to modify storage, while super admins have the ability to create admins, register contracts, and upgrade contracts.
We use a
administratable.sol base contract that supplies the ability to add and remove admins and super admins as well as modifiers to designate that functions are only able to be invoked by admins and super admins, or more restrictively, just super admins. Additionally, we use iterate-able maps so that we can easily introspect our administrative addresses.
For reference the full
Registry.sol appears below which shows how storage is added, how contracts are registered, and how contracts are upgraded:
Ethereum Name Service (ENS)
The capstone to this whole process is the Ethereum Naming Service (ENS). ENS allows contracts to be addressed using a human friendly name instead of a non-human friendly string of hexadecimal characters https://ens.domains/. This allows users to interact with a token contract at the address
cardstack.eth instead of the address
0x66406f50624a645f36faa517c39049200d55c56e; much like being able to use the a domain name like https://google.com instead of https://188.8.131.52. The Cardstack registry adheres to EIP-137, which is the standard governing ENS, such that it can act as a “namehash” resolver (which is how ENS internally represents contract names). Because the registry is already keeping track of the address of the latest version of any contract, using it as a resolver for ENS is a very natural fit.
As part of the registration of a contract with the the Cardstack registry, the “namehash” of the contract name is supplied to the
register() function. The namehash can be derived off-chain using a script (check out https://github.com/danfinlay/eth-ens-namehash) that converts the human-friendly contract name into a namehash. The registry contract’s address can then be used as the resolver ENS, such that it resolves the address of the latest version of a token contract for the namehashes that are presented to it.
Putting It All Together
The approach that Cardstack is taking to realize upgradable contracts in Solidity includes these 3 important features:
- Sequestering state into its own contract(s)
- Using a registry contract to manage contract addresses and to bind contracts to their state
- Resolving ENS namehash lookups to contract addresses
By embracing these approaches for contract development, Cardstack can create contracts that can evolve over time to satisfy the ever-changing needs of the people that use our applications.
This is the first of hopefully many updates we’ll be sharing on our experience in Solidity at Cardstack — stay tuned.
- How to Get One Billion Users on Blockchain by Chris Tse
- Growing a Healthy Software Ecosystem by Lead Developer Ed Faulkner
- Building on Blockchain the Right Way
- The Cardstack White Paper
Join our community
To learn more about Cardstack, visit https://cardstack.com.
Join the Cardstack community channel on Telegram at https://t.me/cardstack
Hassan (@habdelra) has been designing and building a system for digital scarcity and digital rights on the bitcoin blockchain since 2014. He has been working on open-source Cardstack codebase since 2015 focusing on bringing external data sources into the Cardstack framework. Hassan is leading the design and architecture efforts in Solidity for Cardstack, and is an expert on the trade-offs of on-chain and off-chain computation and storage. Hassan has a Bachelor’s degree in Computer Science and Mathematics from the Colorado School of Mines.