✅OpenZeppelin - Proxy Upgrade Pattern
Last updated
Last updated
Smart contracts are immutable by design. If we want to upgrade the logic of a smart contract, we have to use a proxy contract (like a stub) that points to the actual logic contract. When a new logic contract is needed, we abandon the old one and set the "pointer" (will be explained later) to the new logic contract's address. In a diagram:
The proxy contract should have a fallback function that "forwards" incoming calls to the actual logic contract via delegatecall
:
The assembly code is pretty straightforward.
The proxy contract and the logic contract must have exactly the same storage layout, otherwise there will be storage collision. For example:
One way to overcome this is to use the "unstructured storage" approach of OpenZeppelin Upgrades. Instead of storing the _implementation
address at the proxy’s first storage slot, it chooses a pseudo random slot instead:
An example of how the randomized storage is achieved, following EIP 1967:
Note that storage collisions between different versions of the logic contract can occur.
Incorrect storage preservation:
Correct storage preservation:
In Solidity, code that is inside a constructor or part of a global variable declaration is not part of a deployed contract’s runtime bytecode. This code is executed only once, when the contract instance is deployed. As a consequence of this, the code within a logic contract’s constructor will never be executed in the context of the proxy’s state. To rephrase, proxies are completely oblivious to the existence of constructors. It’s simply as if they weren’t there for the proxy.
The problem is easily solved though. Logic contracts should move the code within the constructor to a regular "initializer" function, and have this function be called whenever the proxy links to this logic contract. Special care needs to be taken with this initializer function so that it can only be called once, which is one of the properties of constructors in general programming.
This is why when we create a proxy using OpenZeppelin Upgrades, you can provide the name of the initializer function and pass parameters.
To ensure that the initialize
function can only be called once, a simple modifier is used. OpenZeppelin Upgrades provides this functionality via a contract that can be extended:
Notice how the contract extends Initializable
and implements the initializer
provided by it.
Here is the implementation of initializer
:
This code is similar to nonReentrant lock. It ensures that the initializing code is only called once, just like constructor.
As described in the previous sections, upgradeable contract instances (or proxies) work by delegating all calls to a logic contract. However, the proxies need some functions of their own, such as upgradeTo(address)
to upgrade to a new implementation. This begs the question of how to proceed if the logic contract also has a function named upgradeTo(address)
: upon a call to that function, did the caller intend to call the proxy or the logic contract?
The way OpenZeppelin Upgrades deals with this problem is via the transparent proxy pattern. A transparent proxy will decide which calls are delegated to the underlying logic contract based on the caller address (i.e., the msg.sender
):
If the caller is the admin of the proxy (the address with rights to upgrade the proxy), then the proxy will not delegate any calls, and only answer any messages it understands.
If the caller is any other address, the proxy will always delegate a call, no matter if it matches one of the proxy’s functions.
Assuming a proxy with an owner()
and an upgradeTo()
function, that delegates calls to an ERC20 contract with an owner()
and a transfer()
function, the following table covers all scenarios:
msg.sender | owner() | upgradeto() | transfer() |
---|---|---|---|
Owner | returns proxy.owner() | returns proxy.upgradeTo() | fails |
Other | returns erc20.owner() | fails | returns erc20.transfer() |
Fortunately, OpenZeppelin Upgrades accounts for this situation, and creates an intermediary ProxyAdmin
contract that is in charge of all the proxies you create via the Upgrades plugins. Even if you call the deploy
command from your node's default account, the ProxyAdmin
contract will be the actual admin of all your proxies. This means that you will be able to interact with the proxies from any of your node’s accounts, without having to worry about the nuances of the transparent proxy pattern. Only advanced users that create proxies from Solidity need to be aware of the transparent proxies pattern.
The following challenges from Ethernaut expose potential security issues of the proxy upgrade pattern:
Preservation
Puzzle Wallet
Motorbike
Walkthroughs: