by Manuel Araoz
The foundational layer of ZeppelinOS is the Kernel: an on-chain set of libraries offering common functionality and services that developers can call from within their smart contracts. We designed the Kernel to be fully decentralized while serving both the needs of contributors and users.
Eventually, there will be a governance mechanism that allows developers to propose upgrades to the Kernel. While the Kernel is still some time away from being ready (see our development roadmap), we wanted to get the community involved in the development of ZeppelinOS as early as possible.
Today, we’re announcing ZeppelinOS labs, a space for the community to interact and exchange ideas. We want development for ZeppelinOS to happen transparently with a high level of community involvement, just as we’ve done for OpenZeppelin. We want to know what you think of the direction we’re going in and how it could be better.
The first component that we’d like to share is our prototype implementation of the upgrading mechanism that will be a cornerstone of the Kernel. You can take a look at the code in the ZeppelinOS labs Github repository.
To implement the upgradeability functionality, we created a Proxy contract which delegates all of its logic to an Implementation contract via delegatecall. This allows a Proxy’s Implementation contract to be upgraded through a central registry of versions.
Here’s a look at why we chose this method.
Having a swappable implementation means that the contract’s data can’t be stored alongside the implementation. There are two common solutions to this problem:
We’ve opted for the second option largely because it’s more ergonomic: coding against a Storage contract using getters and setters would be very uncomfortable, and by looking a lot different than Solidity, it wouldn’t be possible to use some of the patterns that the community has developed so far. Additionally, unless the Storage contract itself is upgradeable, you would be stuck with a fixed set of state variables. Also, each call to a getter or setter would incur a high cost.
By directly using the storage of the Proxy contract, we can code the implementation almost like we would a normal (non-upgradeable) contract. State variables are mapped by the Solidity compiler to storage locations, and these are read from and written to the Proxy contract instead of the Implementation contract due to the use of delegatecall. This results in simpler code because the complexity of reading and writing to storage is left to the compiler.
When a new version of the contract is implemented, we must make sure that the same state variables are mapped to the same locations. A clever use of inheritance allows this to be accomplished in a very simple way: if contract X inherits from contract Y (before any other contract), all of Y’s state variables will be the first state variables in X, and so they will share the same storage locations. Thus, a Proxy running Y’s logic can be upgraded to use X’s logic.
By letting the compiler handle this transparently, we’re relying on behavior that may change between compiler releases. We plan to provide tooling to ensure such changes don’t affect the correctness of an upgrade and to prevent other kinds of human error.
We’d love to hear your thoughts, ideas, and proposals in the form of pull requests and Github issues.