EIP 1967 Storage Slots for Proxies
EIP 1967 is a standard for where to store information that proxy contracts need to execute. Both the UUPS (Universal Upgradeable Proxy Standard) and the Transparent Upgradeable Proxy Pattern use it.
Remember: EIP 1967 only states where certain storage variables go and what logs get emitted when they change, nothing more. It does not state how those variables are updated or who can manage them. It does not define any public functions to implement. The specification for updating these variables is given in the Transparent Upgradeable Proxy Pattern or the UUPS spec.
There are two critical variables a proxy needs to operate: the implementation address and the admin. The implementation address is where the proxy is delegating calls to. During an upgrade, the implementation address is changed to the upgraded contract; only calls from the admin will be accepted to make changes.
Prerequisites
This article assumes the reader has a basic idea of how proxies and delegatecall work, what storage slots are, what function selectors are, and what function selector clashing in the context of a proxy is.
The wrong way to design proxy slots
The following is a bad proxy design:
First, there is a non-negligible probability that the function selector for changeAdmin()
will clash with a function in the implementation. The EIP 1967 spec doesn’t say how to handle this — the proper way to prevent this problem is handled in the Transparent Upgradeable Proxy spec or the UUPS spec. EIP 1967 has nothing to do with clashing function selectors.
The problem ERC 1967 solves is that the implementation
and admin
variables are very likely to clash with a storage variable defined in the implementation contract. Specifically, they use storage slots 0 and 1, which implementation contracts will likely use.
Preventing collisions
Because the admin and implementation addresses can change, these need to be in storage variables, they cannot be immutable. But they must be in storage slots that won’t collide with storage variables in the implementation contract.
Here is the key idea: the space of possible storage slots is extremely large: 2**256 – 1.
If we chose a storage slot at random, it’s essentially impossible for the implementation contract to pick the same slot. The chances that an implementation contract would pick the same slot is about the same as a hash function collision, so the risk is essentially non-existent.
The storage slots for implementation and address
The implementation address is stored in slot
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
The admin address is stored in slot
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
These slots were derived pseudorandomly from
bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
and
bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)
respectively.
In decimal, the storage slot for the implementation and admin are
24440054405305269366569402256811496959409073762505157381672968839269610695612
and
81955473079516046949633743016697847541294818689821282749996681496272635257091
respectively.
No contract could have that many variables, so collision from storage variables negligible. Dynamic mappings and arrays take the hash of the slot number and key value and thus use a pseudorandom storage slot. Again, collision by pseudorandom numbers is negligible.
Deriving the storage slots
If we take the keccak256 hash of a string, the output is essentially a pseudorandom number. By subtracting 1 from the result, we produce a random number which has no known hash preimage, so there is no way a contract can plug something into keccak256 to derive a storage slot that clashes with them.
Assumptions about storage slot use
Of course, devs writing implementation contracts could deliberately write to those storage slots with the following code
assembly {
// implementation slot
sstore(
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc, 0x00
)
}
This would break the proxy by pointing it to the zero address! The assumption is that the devs will not do this.
EIP 1967 makes it easy for Etherscan to tell if it is looking at a proxy contract
Here is an example of Compound Finance’s proxy contract.
By seeing if a contract has a non-zero value in the aforementioned slots, block explorers can tell if a contract is a proxy contract or not. A couple of observations about the screenshot above:2
- In the purple circle, we see Etherscan has identified the contract as following the EIP-1967 pattern.
- In the orange circle, we see a note about where the implementation contract is and where the previous one was. The block explorer just looks at the current implementation slot and also remembers past values for it.
- In the red circle, we see we have the options of writing and reading from the proxy or the implementation. Generally, we want to read or write to the proxy since that holds the state of the contract.
What is a beacon slot?
If you read the original EIP 1967, you’ll see a reference to a beacon slot. Beacons are very rarely used in practice, so we deferred their discussion to the end of the article.
Beacons are a topic for another article, but essentially, they are a mechanism to update multiple proxies at the same time. For example, we can point several proxies to the same implementation contract. Since the storage is kept in each proxy individually, the proxies will not interfere with each other.
The beacon contract is very simple: it just returns the address of the implementation contract:
interface IBeacon {
function implementation() external view returns (address);
}
Each of the proxies asks the beacon what the current implementation contract address is before doing a delegatecall. By changing the return value of the implementation()
function in the beacon, all the proxies can be updated at once.
The beacon storage slot is 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
and is derived from bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)
.
The value address(0) can be stored here for proxies that do not use beacons (or the slot can be left empty).
OpenZeppelin and Solady Implementation
OpenZeppelin’s Transparent Upgradeable Proxy and UUPS contracts both use ERC 1967 for defining where to store the variables discussed in this article.
The gas-efficient library Solady also provides a UUPS proxy implementation that utilizes ERC 1967.
Conclusion
ERC 1967 is a standard for where to place storage variables for the implementation contract, the admin, and the beacon. It enables block explorers to easily identify if a contract is a proxy and eliminates the possibility of storage clashes between the proxy and the implementation.
Learn More with RareSkills
This article is part of our advanced solidity bootcamp. Please see the program to learn more.
Originally Published December 20, 2023