Using Proxies: An Essential Guide
Note: Throughout this article, 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 automatically 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 primary reason that most of us want proxies is because they allow us to circumvent one of the most prominent features of a blockchain: permanence.
A proxied contract is one that you can change. You can add features, or modify those you added previously. 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
In many situations, the fact that you cannot change a smart contract would be considered an essential advantage. Code which cannot be changed can be relied on to persist in the same behaviour over the long term. This might be precisely what you want.
Non-rugability
The account that controls a proxy's upgrade functions can essentially 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 using 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
Although there are plenty of tools to make using proxies simple and accessible (this blog post for starters), proxies will always be a source of increased 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 several security concerns introduced 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 discussed types of proxy, the generally accepted recommendation is the Universal Upgradeable Proxy Standard, usually referred to as UUPS and officially1 pronounced “oops” (although 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 various advanced proxies that are beyond the scope of this article. 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 require a good understanding of proxies in order to be used 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
implementation 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 used to for proxy contracts. Always use an initialiser
instead. Initialisers work a lot like constructors, but they are normal functions and so you need to
use something like Openzeppelin's Initializable
5 to make sure they don't get called after
deployment.
Disable Initialisers
If you are using Openzeppelin's Initializable
, add this to your contract:
constructor() {
_disableInitializers();
}
If you are using 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 implementation contract directly. Constructors in implementation 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 several 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 requires a deeper understanding of proxy mechanisms. 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 entire suite of contracts, you might want to create a contract which itself deploys all the proxies and implementation contracts and also calls all their initialisers. This can be a great approach if you have a complex 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 entire 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 generally 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.
Consider Using ProxyAdmin
If you are deploying a transparent proxy, it's a good idea to use the Openzeppelin contract
ProxyAdmin
10 as the proxy's admin. This provides a few usable functions to perform 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 used. It is still compatible, but does not offer as many
benefits.
-
https://ethereum-magicians.org/t/eip-1822-universal-upgradeable-proxy-standard-uups/2842 ↩
-
https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups ↩
-
https://docs.openzeppelin.com/contracts/4.x/api/proxy#beacon ↩
-
https://archive.devcon.org/archive/watch/6/unlimited-size-contracts-using-solidity/?tab=YouTube ↩
-
https://docs.openzeppelin.com/contracts/4.x/api/proxy#Initializable ↩
-
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/d00acef4059807535af0bd0dd0ddf619747a044b/contracts/proxy/ERC1967/ERC1967Proxy.sol#L22-L24 ↩
-
https://mixbytes.io/blog/metamorphic-smart-contracts-is-evm-code-truly-immutable ↩
-
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/transparent/ProxyAdmin.sol ↩