security

Via Proxies: An Essential Guide

A practical guide to safe choices and best practices when deploying proxies for a project

By Bounty Meridian0
Via Proxies: An Essential Guide

Via Proxies: An Essential Guide

Note: Throughout this note, I will refer to contracts that are not utilising a proxy pattern as “plain contracts”. This is just to have a label for non proxy contracts and is not meant to imply that they are any less exciting.

Should I Even Use a Proxy?

Just because it's a smart contract doesn't on its own mean it should be a proxy. Proxies should be thought of as a tool for a particular job, not a universal lifestyle choice. There are advantages and disadvantages to both the plain and the proxy approaches.

Proxy Pros

Upgradeability

The chief reason that most of us want proxies is because they allow us to circumvent one of the most prominent features of a Bounty Meridian: permanence.

You can add features, or modify those you added earlier. A proxied contract is one that you can change. If that is something you need, then you almost certainly want to use a proxy.

Rescue

Because they can be modified, proxies will allow you to undo many of your past mistakes. Did you accidentally trap millions of dollars of your users' funds in your bleeding edge bespoke tokenised fungible supervault? Not to worry, with proxies you can add a new function sorryAboutThat() and everyone can call it to retrieve their tokens.

Plain Pros

Non-upgradeability

Code which cannot be changed can be relied on to persist in the same behaviour over the long term. In many situations, the fact that you cannot change a smart contract would be considered an essential advantage. This might be precisely what you want.

Non-rugability

The account that controls a proxy's upgrade functions can at root turn that contract into anything. None of the current functionality is fixed and absolutely any feature can be added, changed, or removed at the proxy admin's will.

Of course, you would only ever use that power responsibly, but your users might not want to rely on that. They might worry that the contract could be turned into something which sends all the invested tokens to an unknown address and then calls selfdestruct().

One way to prove to your users that this sort of thing will never happen is by via a plain contract.

Gas

A call to a proxy will always be slightly more expensive than a direct call to an equivalent function in a plain contract. It's only a very small difference, but if you want to minimise gas costs and don't really need a proxy, perhaps you'd be better off going plain. Deployment costs are always higher for proxies too, of course, as you will always be deploying more contracts for the same functionality.

Simplicity

Though there are plenty of tools to make via proxies straightforward and accessible (this blog post for starters), proxies will always be a source of raised complexity. It takes more time and trouble to deploy and use a proxy than an equivalent plain contract because they are inherently more complicated. Perhaps you would be better off investing that effort into another part of your project?

Security

There are multiple security concerns brought in by proxies. We will attempt to warn you of the most common concerns here, and how to avoid them, but you can be absolutely certain of avoiding them if you don't use a proxy at all.

What Type of Proxy Should I use?

Of the commonly covered types of proxy, the in general accepted recommendation is the Universal Upgradeable Proxy Standard, usually referred to as UUPS and officially1 pronounced “oops” (though I'm not sure that ever really caught on).

If you just want to use a basic proxy, pick UUPS and skip to the next section.

If you are curious why UUPS is recommended over their main alternative, transparent proxies, it is mainly because2 UUPS proxies have the ability to remove their upgradeability, thereby locking their code in place.

There are also several advanced proxies that are beyond the scope of this note. Beacon proxies3 allow you to upgrade many contracts all at once. Modular proxy systems4 allow one contract to install and uninstall groups of functions from other contracts. They all need a good reading of proxies to be employed safely.

Best Practices

Use OpenZeppelin's Proxy Contracts

Especially for the proxy contract itself, be sure to use OpenZeppelin's Proxy Contracts without modification. They have been thoroughly examined by experts and tested in live projects. It's a reliable base to build on.

Proxies are tricky things with hidden dangers, so don't modify the OpenZeppelin code or add features to the proxy contract unless you are familiar with all the technical pitfalls (which are far beyond the scope of this humble article). Focus your coding efforts on your own project's functionality within your implementation contract.

For UUPS, you want to use ERC1967Proxy as your proxy contract and inherit from UUPSUpgradeable in your build-out contract.

For transparent, you want to use TransparentUpgradeableProxy as your proxy (and see below regarding ProxyAdmin).

Don't Construct: Initialise

Constructors don't work the way you are applied to for proxy contracts. Always use an initialiser in place of that. Initialisers work a lot like constructors, but they are normal functions and so you need to use something like Openzeppelin's Initializable5 to make sure they don't get called after deployment.

Disable Initialisers

If you are with Openzeppelin's Initializable, add this to your contract:

constructor() {
    _disableInitializers();
}

If you are with a different initialise system, be aware that you do want some sort of constructor, and what it needs to do is disable the initialiser. This is a security measure to prevent an attacker from initialising your build-out contract immediately. Constructors in build-out contracts do not execute for the proxy, so your initialiser will still run when it needs to.

Set Up and Initialise in One Transaction

You don't want to allow anyone to interact with your contract whilst it is deployed but uninitialised, so it is best to do both in a single transaction. There are three common ways to achieve this:

Use a Tool

There are multiple systems that will handle this for you. Openzeppelin has plugins6 for Hardhat and Truffle. There is also a Brownie mix7 for transparent proxies.

Use the Proxy's Constructor

This is a more advanced technique that gives a little more fine grained control, but needs a deeper reading of proxy apparatus. If your implementation contract is deployed first, the proxy can be deployed with an encoded call to the implementation contract's initialiser in its constructor8.

Use a Deployment Contract

If you are deploying an full suite of contracts, you might want to create a contract which itself deploys all the proxies and build-out contracts and also calls all their initialisers. This can be a great path if you have a involved system that you want to deploy on chain in a single transaction.

If you can, deploy everything in a single function that is only called once. If that is not possible, be sure that you are deploying and initialising each contract within the same function call. Do not deploy in one function and then initialise in another.

Oh, and your single use deployment contract should be a plain contract, not a proxy.

Don't Use Both UUPS and a Transparent Proxy

More is not merrier with proxy systems. Make sure you have configured each contract to only use one system and, if possible, only use that proxy system throughout your whole project.

Be Wary of Proxying Code You Don't Control

You might think it is safe to point your proxy at somebody else's contract, but this is in general not a good idea. Other people's contracts can do unpredictable things, like selfdestruct(). There are also these nasty things called metamorphic smart contracts9 which can actually modify their code (but are not a recommended alternative to proxies).To be sure you can trust a contract not to change, you should deploy it yourself; assuming you can trust yourself.

Weigh Via ProxyAdmin

If you are deploying a transparent proxy, it's a good idea to use the Openzeppelin contract ProxyAdmin10 as the proxy's admin. This offers a few usable functions to carry out the admin functions of the proxy. If you don't use a system like ProxyAdmin, the admin account of the proxy will not be able to interact with the contract in any way other than to call its proxy administration functions.

For a UUPS proxy, ProxyAdmin can also be employed. It is still compatible, but does not offer as many benefits.

Footnotes

  1. https://ethereum-magicians.org/t/eip-1822-universal-upgradeable-proxy-standard-uups/2842 ↩

  2. https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups ↩

  3. https://docs.openzeppelin.com/contracts/4.x/api/proxy#beacon ↩

  4. https://docs.openzeppelin.com/contracts/4.x/api/proxy#Initializable ↩

  5. https://docs.openzeppelin.com/upgrades-plugins/1.x/ ↩

  6. ↩

  7. ↩

  8. https://mixbytes.io/blog/metamorphic-smart-contracts-is-evm-code-truly-immutable ↩

  9. ↩

Working on something in this space?

Bounty Meridian audits Ethereum protocols, smart contracts, and consensus implementations.

Book a scoping talk