DEV Community

bnabi.eth
bnabi.eth

Posted on

Playing with OpenZeppelin proxy contracts

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.

war

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:

  1. Open the gist in Remix(Click here to open these contracts and others we'll be using in Remix, if you haven't already)
  2. 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. Deploy our implementation
  3. 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 Upgradable proxy
  4. Deploy the Test.sol contract and pass it the address of the UpgradedProxy we just deployed. deploy Test contract
  5. 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. verify result

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:

  1. Follow the steps above to deploy the Logic contract.
  2. 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)
  3. Deploy the Test.sol contract and pass it the address of the TransparentUpgradableProxy we just deployed.
  4. Test that the proxy works. Also verify the event that is triggered.
  5. Deploy UpgradedLogic and pass its address to the upgradeTo method of the TransparentUpgradableProxy(with the admin account, of course)
  6. 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. AddedSafely

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.

Top comments (2)

Collapse
 
chewhg78 profile image
chewhg78 • Edited

Hi, I tried your code in Remix it works if I deploy them to JVM, where result is 0x2A.

But it won't work if I deploy them to the Testnet. There's no input & output shown from the log in the Testnet. What else have I missed?

Here is the log from remix
dev-to-uploads.s3.amazonaws.com/up...

Collapse
 
chewhg78 profile image
chewhg78

Oh, it works actually, the logs show the contract is upgraded!

dev-to-uploads.s3.amazonaws.com/up...