How to make upgradeable Smart Contracts with ZeppelinOS
Could you imagine a world where apps never update? No more bug fixes, no more new features. Need to change something in your mobile app? Well, you can always create another one! Looks scary, right?
It’s hard to imagine, but Ethereum world looked exactly like that not so long ago. All published contracts were closed for updates. If you want to upgrade a contract you need to publish another one and yes, data migration was your pain.
Fortunately, everything changed a few years ago when it became possible to use delegatecall to implement the proxy pattern. It was a door to the world of amazing opportunities for upgradability. However, this approach has been adopted rather slowly and at this moment upgradability is still perceived as a nice feature instead of something every blockchain project must have.
In this article, I’ll show you that it’s surprisingly easy to make your Ethereum project upgradeable. Interested? Let’s go.
Several strategies to make contracts upgradeable
- Separate logic and data contracts. In this case, the logic can be replaced while keeping the data unchanged. That’s what you need for an upgradeable system. However in this case you need to force users to switch to the new logic contract.
- Use proxy contract. A proxy uses the delegatecall to forward method calls to another contract which can be replaced while proxy address remains the same. There is an interesting article describing different approaches to implement this strategy. However in any case it’s quite complicated task and you need to avoid security vulnerabilities.
- Use an existing framework or platform with built-in upgradability support (for example, Evoluchain or ZeppelinOS).
After some research our team chose ZeppelinOS and here is why
After some research our team chose ZeppelinOS and here is why
- It was developed by Zeppelin, the team who is well known for OpenZeppelin framework of reusable smart contracts.
- It’s a platform to develop and deploy smart contracts on Ethereum. So besides upgradability, it has many powerful packages which you can use, publish and vouch.
- There is already a good infrastructure including a command line tool, integration with truffle, and support of several major EVM networks.
- It continues to evolve. The new 2.0 version was released in October 2018 and introduced EVM packages, which make ZOS not only a useful framework of upgradeable contracts but an excellent ecosystem for developing and sharing contracts.
There is a detailed tutorial which can help you to create your first ZOS project: https://docs.zeppelinos.org/docs/deploying.html. If you’re already familiar with ZOS or an experienced blockchain engineer, you can check out the following script to quickly set up a ZOS project:
Here is a simple example to illustrate all changes you need to make in one place:
Let’s go through all of them:
- Inherit zos-lib/contracts/Initializable contract.
- Replace constructor with initialize function because EVM restricts ZOS from using constructors.
- Add initializer modifier to make sure that initialization is called once.
- Add explicit initialization of all parent contracts, since they are not called automatically, as opposed to initialization of constructors.
- Replace usage of msg.sender with a corresponding argument to be able to initialize contracts from ZOS.
- Move fields initialization to an initializer.
- Use upgradeable packages instead of libraries. This one needs more clarification. If you are familiar with OpenZeppelin library then you may notice there are three versions available:
- openzeppelin-solidity: it contains not-upgradable contracts and can be used without ZOS;
- openzeppelin-zos: it was used by ZeppelinOS 1.x and now deprecated;
- openzeppelin-eth: it is the latest version of OpenZeppelin adapted for use in ZeppelinOS.
You may consider EVM package as a usual npm package already deployed to major networks (kovan, rinkeby, ropsten and mainnet), that you don’t need to redeploy. Instead, you just need to link such a package to your project:
zos link openzeppelin-eth
It is automatically connected once you deploy your contracts. However, during testing, you still have to deploy these packages to a local network. Fortunately, it’s pretty simple:
zos push — deploy-dependencies
I’m not going to discuss the module system in detail in this article. If necessary, you can find more information about the topic here.
First of all, an upgradeable contract in ZOS is presented with logic and proxy contracts. The Proxy has a reference to the logic, and a “delegate” calls it. Once you push your contract to the network you can see the addresses of both contracts:
In practice, you only need a proxy address. Once you upload a new version of the contract, it uploads a new logic contract and updates the reference in the proxy (the proxy address remains the same). So when you reference an upgradeable contract from another contract, you refer to a proxy address. Moreover, if you make a call from your contract to another one, msg.sender becomes a proxy address as well. In other words, all other contracts working with upgradeable contract know only the other party’s proxy address and work with it.
Another important thing is that all data is stored in the proxy contract. So you can think about it as the following:
- Proxy keeps storage
- Logic keeps API
The above said means that you don’t have to migrate your data. It is enough to update contract API to make it work with the same data storage capacity. Besides, you can store more data in the new version by providing your contract with new storage members, in this way all new data becomes associated with the proxy contract.
At first, this upgradeable proxy pattern may seem to be magic. Still this concept is built on top of delegatecall feature, and you can find full proxy implementation in the AdminUpgradeabilityProxy contract. You can actually create and upgrade a proxy by yourself because it’s quite simple:
This is a perfectly working sample which you can use in your tests or solidity code to create a proxy from another contract. As an option, you can use already existing BaseApp contract to create and upgrade a proxy contract.
There are a few limitations you need to keep in mind when developing ZOS contracts:
- You have to use dedicated admin role for an upgradeable contract. Usually in Ethereum world the owner of a contract has exclusive access to its functionality, but you cannot take roles both of an owner and admin simultaneously. Only the admin was intended to have access to upgradeable functions (like upgradeTo or changeAdmin); hence any call from the admin to other proxy methods was supposed to fail. So remember that you need a separate account to deploy and upgrade your contracts.
- You cannot change state variables you had in the previous version of your contract, i.e., you should leave all fields as they were and only add new fields or add/change/remove methods. It’s not that hard to keep the original structure. If it is still necessary to reorganize your store, you can use the following trick:
- keep old fields unchanged
- add new fields you want to use
- add migration method to move existing data to the new structure
- update existing methods to work with the new fields
You can find more details about this topic in the ZeppelinOS documentation: https://docs.zeppelinos.org/docs/writing_contracts.html#modifying-your-contracts
ZeppelinOS is a promising platform to improve development on Ethereum. Upgradability is a must-have feature for the future of smart contracts, and together with the module system, it appears to be a great basis for an ecosystem of secure decentralized applications. It’s still relatively new, and there are not that many articles about this topic, which makes it sometimes hard to find answers to important questions. However, it has already been improved a lot since version 2.x release, and I believe that it will be even better adjusted in the future.
Overall, using ZOS in production was quite a positive experience, and we’ll keep working with this platform and help it to grow!