We recently read some articles about neat tricks and hacks one can do in Solidity. Specifically, Jorge Izquierdo’s article on Library Driven Development and Simon de la Rouviere’s article on ThrowProxy.
That got us thinking about using those ideas to turn Zeppelin into an upgradeable blockchain-deployed library. Currently, Zeppelin is a collection of secure and community-vetted contracts for anyone to use. This is work towards creating security standards and industry best practices. But in order to use them, developers have to download our code and deploy parallel copies of it alongside their application-specific contracts.
This has several disadvantages:
- Deployment gas costs.
- Code repetition in the blockchain.
- Bug fixes and updates need to be deployed independently on each project (or, even worse, Ethereum has to hard fork to fix a contract’s problems).
What if we could have a deployed version of a library in the blockchain, which all projects using Zeppelin could link directly? That’s the idea behind Solidity’s libraries. The problem is, once a library code is deployed, it’s immutable. For Zeppelin, being able to fix security vulnerabilities and add more reusable modules is key for our users. Thus, we’d need some mechanism to upgrade the contract’s code.
Our idea: Use a normal contract, but call it as a library (use
delegatecall instead of
Result: It works!
Check it out and please give us feedback! We’ll be evaluating how to use this new technique to turn Zeppelin into a deployed upgradable library in the next few weeks.
On a more technical level, our solution flow looks like this:
Instead of linking the main, user facing contract directly with the address of the deployed library, it is linked to a ‘Dispatcher’. At compile and deploy time this is just fine, even though the Dispatcher doesn’t implement any of the methods of the library, if the bytecode has a valid address the deploy will be successful.
When a transaction comes in, the main contract thinks it is making a
delegatecall to the library it is linked with. But this
delegatecall will instead be made to the dispatcher.
Here is where things get interesting. Once the dispatcher catches the
delegatecall in its fallback function it figures out what the correct version of the library code is, and redirects the call once again with a
delegatecall. Once the library returns, the return will go all the way back to the main contract.
- The dispatcher needs to know what the memory size for the return of that library call is. Right now we solved it by having a mapping for function signatures to their return type size. This was intentionally kept out of the drawing for the sake of simplicity.
- Given the way
delegatecall’s work on the EVM level, you can only use it from one contract to another that have the same storage footprint. As libraries have no storage, we kept Dispatcher with no storage. That’s why a separate DispatcherStorage to keep all the data it needs. Also, the address of the DispatcherStorage needs to be hardcoded in the contract bytecode.
Note that for the user contract nothing special is needed, only that instead of being linked with the concrete version of the library, it has to be linked with the dispatcher.
- Allow data migration of user contracts when library is upgraded (data structures may change). Golem’s GTN Token has a migration mechanism that could serve as inspiration.
- Storage layout sanity checks in the proxy.
- Arbitrary code execution on contracts for emergencies.
We’ll be writing about these topics in the following weeks.
Thanks Christian Reitwiessner for reviewing and commenting on early drafts.