Proxy Upgrade pattern
- The first contract is a simple wrapper or "proxy" which users interact with directly and is in charge of forwarding transactions to and from the second contract, which contains the logic.
User ---- tx ---> Proxy ----------> Implementation_v0
|
------------> Implementation_v1
|
------------> Implementation_v2
- The proxy contract has a fallback function. This function gets triggered if a tx calls a function that does not exist on the contract.
- The fallback function delegate calls the tx to the implmentation contract. (1) the
calldata
is copied to memory, (2) the call is forwarded to the logic contract, (3) the return data from the call to the logic contract is retrieved, and (4) the returned data is forwarded back to the caller.
Storage in Proxies
- If the proxy stores the implementation address is the 0th slot and the implementation has some variable stored in it’s 0th slot, there will be storage collision.
|Proxy |Implementation |
|--------------------------|-------------------------|
|address _implementation |address _owner | <=== Storage collision!
|... |mapping _balances |
| |uint256 _supply |
| |... |
- Instead of storing the
_implementation
address at the proxy’s first storage slot, it chooses a pseudo random slot instead.
bytes32 private constant implementationPosition = bytes32(uint256(
keccak256('eip1967.proxy.implementation')) - 1
));
- imagine that the first implementation of the logic contract stores
address public _owner
at the first storage slot and an upgraded logic contract storesaddress public _lastContributor
at the same first slot. When the updated logic contract attempts to write to the_lastContributor
variable, it will be using the same storage position where the previous value for_owner
was being stored, and overwrite it!
- Incorrect
|Implementation_v0 |Implementation_v1 |
|--------------------|-------------------------|
|address _owner |address _lastContributor | <=== Storage collision!
|mapping _balances |address _owner |
|uint256 _supply |mapping _balances |
|... |uint256 _supply |
| |... |
- Correct
|Implementation_v0 |Implementation_v1 |
|--------------------|-------------------------|
|address _owner |address _owner |
|mapping _balances |mapping _balances |
|uint256 _supply |uint256 _supply |
|... |address _lastContributor | <=== Storage extension.
| |... |
- New variables in different implementation should always be stored after the old variables.
Constructor
- 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.
- proxies are completely oblivious to the storage trie changes that are performed by the constructor.
- This is solved using the
initialize
function
- 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:
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract MyContract is Initializable {
function initialize(
address arg1,
uint256 arg2,
bytes memory arg3
) public payable initializer {
// "constructor" code...
}
}
Function clashes
- 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 namedupgradeTo(address)
: upon a call to that function, did the caller intend to call the proxy or the logic contract?
- 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.
msg.sender | owner() | upgradeto() | transfer() |
Owner | returns proxy.owner() | returns proxy.upgradeTo() | fails |
Other | returns erc20.owner() | fails | returns erc20.transfer() |
- OpenZeppelin Upgrades uses an intermediary ProxyAdmin contract for each transparent proxy. Even if you call the
deploy
command from your node’s default account, the ProxyAdmin contracts will be the actual admins of your transparent 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.