bnabi.eth
Posted on November 2, 2021
The code in this post is based on the popular OpenZeppelin contract implementation for a proxy pattern. The original code can be found here. This post will help to understand a bit more about:
- proxy contract implementation
- how to deploy with a proxy pattern
- how to change implementation with proxy patterns
Before we begin there are some things that might help to grasp the subject matter better. This article is a great starting point. Learning about the layout of storage is also a good idea. Finally an understanding of delegatecall is essential to understand what is going on. Armed with an idea of these, we can go ahead and try to tackle the implementation itself.
Overview
We are going to deploy a simple contract, Logic
, that adds two numbers. Then we are going to upgrade it to UpgradedLogic
, an upgraded implementation that does the same thing, except that it uses the SafeMath library.
Click here to open the contracts we'll be using in Remix.
The code that we will be using is based on this gist. It is almost a carbon copy of the OpenZeppelin implementation but leaves out a few bits to makes things simpler.
It consists of the following contracts:
- Proxy, UpgradableProxy, TransparentUpgradableProxy and ProxyAdmin : These are slightly modified versions of OpenZeppelin's implementation of these contracts.
- Logic, UpgradedLogic: Contracts that hold the logic of our application
- Test: Contract that interacts with the proxy. Think of it like a user interacting with our application.
Proxy and UpgradableProxy contracts
Overview:
-
fallback
function that calls_fallback()
internally which in turn calls_delegate()
. This is the code that delegates our call to the implementation. - Proxy is an abstract contract which does not have the implementation logic. That's where UpgradableProxy comes in.
Overview:
- constructor take in an address which is the contract that houses our logic
- implementation/logic contract address is stored at a random storage location called
IMPLEMENTATION_SLOT
. This is done to avoid clashes with default storage slots while managing the proxy. Understanding how storage works in Solidity will help drive this point home. - methods to read and write to
IMPLEMENTATION_SLOT
. - public method to
getImplementation()
This is a simple contract which takes the address of a proxy and makes a low level call to add two numbers.
At this point we can deploy our basic proxy contract.
Steps to Deploy:
- Open the gist in Remix(Click here to open these contracts and others we'll be using in Remix, if you haven't already)
- Compile and deploy this
Logic.sol
contract. Test it by passing in two integers and verifying that the sum is correct in the Remix console. - To deploy the UpgradableProxy contract we need the address of our implementation/logic. Copy the address of the Logic contract deployed above and pass it into the constructor.
- Deploy the
Test.sol
contract and pass it the address of the UpgradedProxy we just deployed. - Call the
test
function in the contract. It makes a low level call to the UpgradedProxy which in turn calls our Logic contract. If all goes well, you should see 0x2a as the output of the test call.
Wait.. how do we upgrade?
You might have noticed that the _upgradeTo
function is internal, which means that we can't really call is from external contracts or accounts.
This is where the TransparentUpgradableProxy
comes in which handles the admin logic for us. This contract exposes functions for upgrading implementations, as well as owners, who handle those changes. See the OpenZeppelin contract implementation of TransparentUpgradableProxy for more details. They have some great documentation.
Overview:
- Admin slot to store the admin who can deploy and upgrade proxies. Again, to avoid collision with default storage.
- Note that it inherits from the UpgradableProxy contract which already has the proxy logic baked in.
- Methods for changing implementation and admin.
Deploying and testing:
- Follow the steps above to deploy the Logic contract.
- Just like we deployed the Upgradable proxy in the step 3 above, we can deploy
TransparentUpgradableProxy
with the address of the implementation(Logic contract) and admin(use Remix account address) - Deploy the
Test.sol
contract and pass it the address of the TransparentUpgradableProxy we just deployed. - Test that the proxy works. Also verify the event that is triggered.
- Deploy UpgradedLogic and pass its address to the
upgradeTo
method of the TransparentUpgradableProxy(with the admin account, of course) - Use test contract again to verify the result. You should see a AddedSafely event in the console reflecting that the implementation was upgraded in the background, while the address you used to interact with remained the same.
That's it for this post. I hope you have a better understanding of proxy contracts after reading and playing with the attached Remix gist.
A good exercise to understand these contracts might be to go through the OZ contract implementation and the related comments in the code. One could also learn about ProxyAdmin and the use cases that addresses.
Might post more about storage and its layout or OpenZeppelin's upgrade plugins in the future, both of which would build on top of this post.
Posted on November 2, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024