PoCo Series #6 — Smart Contract Upgradeability and Governance
As a researcher engineer at iExec, part of my work is keeping track of all the hot topics in the Ethereum community and evaluating what impact they have on our project. For the most relevant issues, proofs-of-concept are made, before full integration in iExec. Unsurprisingly, Smart Contract upgradability is one of these hot topics. After several months of working on this topic, we think it is time to share our vision on the matter.
Our contributions to smart contract upgradeability were also recently recognized and awarded by Binance Labs:
Why would we need smart contract upgradeability?
If you are reading this, you should be familiar with the notion of smart contracts. They are on-chain pieces of software that can process assets in a trustless manner. Their execution is part of the blockchain transition function, meaning its completely decentralized and auditable. Their memory state is also part of the blockchain, which guarantees a secure and predictable execution. The drawback with this design is that once a contract is deployed there is no way (except doing a hard fork) to change the code they run.
iExec is based on multiple smart contracts. They are the beating heart of our platform, ensuring the security of our users’ assets and the auditability of all transactions. Tools like Etherscan allow you check the status of these contracts memory. You will also find the solidity source code of the contracts, which has been verified to produce the actual bytecode deployed on the blockchain.
While these contracts have been extensively tested and audited, it is possible that bugs in the source code or in the compiler could be discovered down the line. Even if a contract is bug-free, it is likely that time ends up pointing out missing features that were not initially envisioned by the developer. In both cases, upgradeability can solve the problem by adding, removing, or modifying functions in the smart contract’s code.
Initially, the idea of the upgradeable smart contract can seem contradictory with the immutability of smart contract. It is however not necessarily the case as Openzeppelin discussed in this blog article.
How does smart contract upgradeability work?
Each smart contract contains 2 parts, the bytecode that contains the execution logic and the memory that can be accessed and modified by transactions. This distinction is important, particularly when considering the different calling mechanisms proposed by the EVM. Similarly to transactions sent by simple wallet, which can trigger the execution of a smart contract, smart contracts can also send transaction trigger further execution. In addition to calls, that are similar to the one performed by wallets, smart contracts can also perform delegatecall and staticcall. Understanding the difference between call, delegatecall, and staticcall is essential to understanding how upgradeability works.
- Call: When a smart contract performs a call, the execution context changed to the targeted address. The bytecode of the targeted address is executed using the memory of a targeted address. This call can read from and write to the memory of the targeted address using the logic of the targeted address. It is a read-write call.
- Staticcall: A staticcall is similar to a call, with the only difference being that a staticcall internal transaction cannot modify the memory of the targeted address. It is a read-only call.
- Delegatecall: When a smart contract performs a delegate call, the bytecode of the targeted address is executed using the memory of the caller. This allows a contract to authorize another contract’s logic to act on its own memory space (thus the name delegatecall). The memory of the targeted address cannot be accessed/modified by this kind of call. it is an on place read-write call that uses an external logic.
You might have guessed it, delegatecall are what make the upgradeable contract a thing. A core contract can store all the memory under its wing, and perform delegatecalls to contracts hosting the logic (but not the memory). Changing the address of the target from a contract to another allows the core to keep its memory intact while updating the logic (bytecode of the targeted address).
In addition to the upgradability, these patterns also save memory (and money) by sharing logic between instances. For example, many core contracts can point to the same ERC20 logic. The core will all have their own memory space, describing their token details and balance but will not have to redeploy the ERC20 logic. A single (audited) deployment is needed, which anyone can then benefit from. ZeppelinOS proposes tools to build this kind of smartcontract easily.
ERC1538 proposes to go further by using spreading the logic between multiple delegate contracts. This solution also has the advantage of solving the contract size limits introduced by EIP170. In the context of iExec, using the ERC1538 would have allowed implementing all the features of the iExecHub and iExecClerk in multiples delegates that could be pointed to by a single entrypoint. Transaction cost would have been reduced, and upgradeability would have been possible.
How far did you go implementing it?
All the way!
We have a running testing environment that relies on ERC1538. This allowed us to work on the deployment and upgrade process and do some gas consumption measurements. Code of our ERC1538 implementation (made compatible with AbiEncoderV2) is available on the iexec-solidity repository.
Then why isn’t the V3 deployment upgradeable?
Using ERC1538 in iExec v3 would have added undeniable advantages, but it would also come with some drawbacks.
Auditability is the first issue.
While contracts using ERC1538 are as safe and public as regular contracts, it is more difficult for non-technical individuals to check the code or read the data. If we wanted to certify the code, a user would only see the code of the proxy, and would not simply be able to read the memory as the accessors are hidden in delegates.
For example, here is the code of a verified proxy used by Kitsune-wallet (that we will discuss later in the article). Code is visible to everyone, but no abi is defined in this code. Looking at the events, you can see that the code is hosted at this address, but you still cannot use it to easily read the memory of the proxy.
Governance is the second issue.
While trusting a non-upgradeable smart contract is equivalent to trusting both the (audited) source code and the blockchain, trusting an upgradeable smart contract adds the issue of trusting the owner. A smart contract that is safe to use at a point in time could very well steal all your assets after an upgrade. As discussed by OpenZeppelin, one should make sure that an upgrade can only happen if the parties potentially affected agree on the upgrade.
This was a dealbreaker for iExec. We want our platform to be trustless, and our community uses their tokens buy and sell IT asset (data, application, computing power) while knowing that no-one, even the iExec team, can prevent them from doing so. We could imagine a DAO mechanism that would control the upgrade process, and that would rely on the RLC token. However having our utility token also acts as a governance token could have an unexpected effect, and the decision was taken to not do that in V3, and further evaluate that for future versions of iExec.
Did you stop there?
While we didn’t include any upgradeability features in the iExec V3’s contracts, we learned a lot from this experiment, from both technical and philosophical point of view.
Upgradeable smart contract has very specific subtleties. Memory mapping, in particular, is tricky. You have to make sure that the different versions of the bytecode access the memory space of the proxy contract in the same way. This means that all version of the logic (all delegates in the context of an ERC1538 implementation) have to agree on a common memory pattern. If you try performing a delegate call to a contract that doesn’t share this memory pattern, it could overwrite fields and break your proxy. For example, deleting the address of the contract that your proxy delegates to permanently destroys your proxy, and all assets owned by the proxy are locked.
Moreover, we came to the same conclusion as OpenZeppelin that upgradability and governance issues are linked. However, while OpenZeppelin continues the moves on to using multisig to control the upgradability process, we followed a different direction:
If governance is needed to upgrade a smart contract, why not focus on governance smart contracts and give them the ability to control their own upgrade?
As I told you previously, part of my job is to keep track of all innovations in the Ethereum ecosystem, and I naturally focussed on on-chain identity, multisig wallets, meta-transactions and so one. If you are not familiar with all that, have a look at ERC725v2 and UniversalLogin. We have been experimenting with this kind of proxy for a long time. The main difference between an identity contract (smart contract based) and a classical wallet (private key based) is that the first one cannot sign a message (ERC191 & ERC712), which is essential to iExec’s Open Decentralized Brokering. ERC1271 proposes a way to solve this problem, by having the identity contract verify the signature independently, but most multisigs do not implement ERC1271.
So for a multisig to work with iExec’s V3 signatures (ERC712) it has to implement ERC1271. On the other hand UniversalLogin relies on ERC1077 which does not support ERC1271. That is just one missing features in ERC1077, and I’m ready to bet we will find others later.
(fun fact, a few months later someone added ERC721 safe transfer support to UniversalLogin’s contract, which was a missing feature up until this point)
So why not make UniversalLogin contracts upgradeable? They would be cheaper to deploy as the complex logic will be deployed once and all proxy will be able to use it. They would also be upgradeable so if you have a UniversalLogin account you are able to upgrade it later on, adding new features while keeping all your assets at the same address. You would also keep your reputation. On iExec each worker has a score that is linked to its address and is not transferable. If you have to change your address you are losing some things that are not transferable, but if you upgrade your account you keep these non-transferable claims.
What is next?
This project, making identity contracts upgradeable, was initially developed during EthParis with the support of iExec. It won two prizes. One from the ENS for the improvement to on-chain, ENS-registered, wallets. The other one from BinanceLabs for the improvement to asset security through safe and persistent onchain wallets.
After EthParis, I got approval from iExec to continue working on, and to seek other partners for, this project which is not essential to iExec’s core but which will help to improve the UX and security of users. The project was renamed KitsuneWallet and was awarded a grant from the BinanceLab Fellowship.
KitsuneWallet proposes a toolkit to build an upgradable proxy contract with self-sovereign upgradeability mechanisms. This approach is being integrated into UniversalLogin and Shipl as I’m writing these lines.
Even if smart contracts upgradeability is not currently used for the iExec smart contracts, we are exploiting our expertize on the matter to help building tools that will benefit the entire community. Understanding this trend will help us in integrating iExec with other actors in order to build the best UX possible.
Our contribution to Smart Contract Upgradability was recently recognized by Binance Labs:
More from iExec:
iExec recently launched the much anticipated iExec V3. See below for more information on the new feature and adoption announcements, including the fact that iExec V4 (High-performance computing and GPU) will be coming much earlier than planned! Learn more: