Introducing the “Escape-hatch” Proxy

TERMINAL
TERMINAL
Mar 8 · 7 min read

Universal Upgradeable Proxy Standard (UUPS), pronounced , is similar to existing proxy contracts. Proxy contracts create an escape hatch for upgrading to a new smart contract when a bug or vulnerability is found. Here we introduce an improvement upon proxy contracts which can be used as a holistic life-cycle management tool for smart contracts.

View Ethereum Improvement Proposal #1822 here:

Why use a Proxy?

Proxy contracts enable smart contract, so developers can update the business logic of a contract, without needing to worry about transferring storage. This is called contract forwarding, and you may have encountered this phenomena if you’ve interacted with Gnosis contracts. In addition, proxy contracts can prevent chain-bloat by preventing deployment of many copies of the same code, by way of contract recycling.

The fundamental behavior of a proxy contract is to use delegatecall() to allow an external contract to modify its own storage. This streamlines the development cycle when writing upgradeable smart contracts, since the contract can be written in the same manner as a normal non-upgradeable contract.

delegatecall() enables external contract to modify a contract’s storage directly.

In addition, proxy contracts are an improvement upon the use of library contracts. Using library contracts requires additional consideration for the coordination of storage, which isn’t necessary when using a proxy contract.

As shown in the diagram above, the proxy contract (contract A) is lightweight in both code and storage. This contract performs a delegatecall() to the logic contract (contract B), which contains the desired business-logic. The resulting code from the logic contract then runs as if it were native to the proxy contract.

Contract forwarding

In some instances, it makes sense to have the ability to add new features or implement security patches in your smart contract. With a static smart contract, the process is lengthy and requires coordination of all the involved stakeholders. As an example, I’ve summarized the steps that were taken during the Augur Deployment in July of 2018.

  • Freeze the contract and take a snapshot of the contract storage.
  • Force users to take specific actions like withdrawing tokens from exchanges.
  • Coordinate with exchanges and third-party dapps to freeze all user interactions, and update to the new contract address.
  • Spend hours deploying the new contracts and updating storage manually.

Consider that the above process becomes increasingly difficult for library contracts and contracts with complex data.

Contract forwarding eliminates all the above steps, by allowing the developer to upgrade a contract’s business logic with a single function call.

Proxy contracts provide the ability to simply “push changes” as you would in a normal web-app.

Contract recycling

Proxy contracts are very useful for conserving gas in situations where many contracts are deployed, and each contract has the same business logic. One example of this is when creating unique identities or wallets for each user.

Rather than redeploying the entire logic for a user’s identity, you are only required to deploy the identity logic contract(s) once. Then, for each new user you can simply deploy the lightweight proxy contract. This is our initial use-case for the Terminal platform, and primary motivation for developing UUPS.

Problems with current state of proxy contracts

UUPS is an improvement upon two well-known approaches to proxy contracts used today.

Gnosis (example)

Gnosis’ implementation uses a specific variable within the proxy contract for storing the logic contract address. This address variable must be kept in the first storage slot for the proxy contract, and the logic contract must be designed to not interfere with this storage. For instance, the logic contract cannot occupy or modify the first storage slot, else it can irreparably damage the proxy contract. Also, the inheritance order of the proxyData contract must be maintained to prevent this storage collision issue.

Open Zeppelin (example)

Zeppelin’s implementation takes a similar approach, except that the logic contract address is stored in an external reference contract. This reference contract also contains the functions needed to perform the upgrade, rather than placing them in the proxy contract. The drawback here is that the reference contract is permanently hard-coded in the proxy, therefore the rules governing the upgrade process are frozen and can never be changed.

The UUPS

UUPS eliminates these concerns, since the governing logic is never frozen, and there is no issue in terms of compatibility between the storage of the logic and proxy contracts.

Avoiding storage collision

Storage collision can occur when a logic contract and proxy contract attempt to store a variable at the same storage slot, as described in the Gnosis example above. Like overwriting a saved game, if the logic contract is upgraded, and care is not taken to avoid storage collision, the results can be catastrophic. With UUPS, rather than forcing use of a variable, the address of the Logic Contract is stored at the defined storage position keccak256(“PROXIABLE”). This eliminates the possibility of collision between variables in the Proxy and Logic Contracts, thus providing “universal” compatibility with any Logic Contract.

Upgrade governance never freezes

With UUPS, the governing logic for upgrading can be itself upgraded. This prevents the frozen upgrade logic described in the Gnosis example above. For instance, you might start with a contract which requires onlyOwner to perform an upgrade, but in the future you may want to switch to a multi-signature approach or maybe introduce voting from the community. Using UUPS, changing the governance model for performing upgrades is as easy as performing a logic contract upgrade.

Try UUPS now (no-code)

  1. Navigate to the Remix link below. In the left sidebar, expand “gist”.

2. Open Example.sol and in the toolbar on the right hit “Start to compile.” Then in the “Run” tab, click “Deploy”. This will deploy a wrapper contract to help facilitate the tutorial.

3. Click to expand the deployed Example contract (shown below), and enter a name for your ship. When ready, launch your ship by pressing step1_launchShip

This deploys the logic contract ShipA.sol and uses the resulting contract address to deploy and initialize the proxy contract with the name you selected.

Now we will access our ship via the proxy contract.

4. Click lastDeployedShipProxy and copy the address. This is the address of the deployed proxy contract.

5. In the contract dropdown, select ShipA and paste the address you just copied in the “At Address” box. Push the button to access the functions of ShipA via your proxy contract.

6. You will now see a new contract ShipA created below the Example contract. You are now ready to sail the ocean with your ship! Be sure to check fuelSupply and to travel at least 4 times. You will notice that ShipA runs out of fuel fairly quickly, and you suddenly become stranded in the middle of the sea.

It’s time to abandon ship! Use the UUPS escape hatch.

7. Over in the Example contract, click step2_upgradeShip which deploys the logic for ShipB and upgrades your proxy to use this new ship logic. Notice that the address for lastDeployedShipProxy has not changed, since the proxy contract never changes.

8. In the contract dropdown, select ShipB and paste the address you copied earlier from lastDeployedShipProxy into the “At Address” box. Push the button to access the newly deployed logic of ShipB via your proxy contract.

9. You have now successfully escaped and boarded a better ship, without losing any of your crew or freight. In your new ShipB, you have the added function refuel . Call this function to refuel your ship and you can continue to sail wherever your heart desires!

Integrate UUPS with your contract (5 min)

The UUPS can be implemented in under 5 minutes, with very minor modifications to your existing business logic.

Ingredients:

  • 1 Logic Contract (i.e., your dapp’s contract)
  • 1 Proxiable Contract
The Proxiable Contract, which is added to your business-logic contract
  • 1 Proxy Contract
The lightweight UUPS proxy contract.

Steps:

  1. Add the Proxiable Contract to your Logic Contract, and convert your constructor to a function by adding function and deploy.

2. Encode your constructor arguments using your Logic Contract ABI.

3. Deploy the Proxy Contract with your encoded constructor arguments as constructData and the address contractLogicof the Logic Contract.

To view more detailed instructions and potential pitfalls, view EIP-1822 UUPS.

Join the discussion!

Join us in the ETH Magicians forum:

Contact the authors

terminaldotco

Terminal is building the access point to Web3.

TERMINAL

Written by

TERMINAL

terminaldotco

Terminal is building the access point to Web3.