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:

An image of 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.

An image of Compound Finance 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

20 Common Solidity Beginner Mistakes

20 Common Solidity Beginner Mistakes Our intent is not to be patronizing towards developers early in their journey with this article. Having reviewed code from numerous Solidity developers, we’ve seen some mistakes occur more frequently and we list those here. By no means is this an exhaustive list of mistakes a Solidity developer can make. […]

Smart Contract Foundry Upgrades with the OpenZeppelin Plugin

Smart Contract Foundry Upgrades with the OpenZeppelin Plugin Upgrading a smart contract is a multistep and error-prone process, so to minimize the chances of human error, it is desirable to use a tool that automates the procedure as much as possible. Therefore, the OpenZeppelin Upgrade Plugin streamlines deploying, upgrading and managing smart contracts built with Foundry or […]

UUPS: Universal Upgradeable Proxy Standard (ERC-1822)

UUPS: Universal Upgradeable Proxy Standard (ERC-1822) The UUPS pattern is a proxy pattern where the upgrade function resides in the implementation contract, but changes the implementation address stored in the proxy contract via a delegatecall from the proxy. The high level mechanism is shown in the animation below: Similar to the Transparent Upgradeable Proxy, the […]

Try Catch and all the ways Solidity can revert

Try Catch and all the ways Solidity can revert This article describes all the kinds of errors that can happen when a smart contract is called, and how the Solidity Try / Catch block responds (or fails to respond) to each of them. To understand how Try / Catch works in Solidity, we must understand […]