Universal Upgradeable Proxy Standard (UUPS), pronounced oops, 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:
EIP-1822 Universal Upgradeable Proxy Standard
The Ethereum Improvement Proposal repository.
Why use a Proxy?
Proxy contracts enable smart contract version control, 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.
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.
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.
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’ 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.
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)
Ahoy captain, we’re ready to launch your new UUPS-enabled ship. In case of emergency, you can activate the escape hatch feature and transfer to a new ship, without worrying about losing your cargo.
- Navigate to the Remix link below. In the left sidebar, expand “gist”.
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
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.
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.
- 1 Logic Contract (i.e., your dapp’s contract)
- 1 Proxiable Contract
- 1 Proxy Contract
- Add the Proxiable Contract to your Logic Contract, and convert your constructor to a function by adding
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: