ERC-1967 Proxy storage slot standard (Proxy Storage Slots)

Final · Other · created 2019-04-24

A standard that fixes where an upgradeable proxy contract stores the address of the logic contract holding the actual code, the beacon address, and the admin address. Tools such as block explorers can read any proxy in the same way.

Why it is needed

Apps that want to upgrade a contract or reduce deployment cost put a proxy contract in front and keep the actual code in a logic contract. The proxy borrows the logic contract's code with an instruction called delegatecall while keeping its own storage and balance. Many projects, including OpenZeppelin, Gnosis and AragonOS, use this approach. The problem was that each project stored the logic contract address in a different place. With no common rule, block explorers could not tell which address was a proxy or what real code sat behind it, so they showed only the proxy's own function list (ABI). Users had difficulty using the actual functionality. Even when a logic contract tried to update code by taking advantage of the fact that it runs behind a proxy, it had to be adapted separately for each proxy implementation. This standard fixes that location to three slots so that everyone can read it in the same way.

What to implement

In depth

The three standard slots and how they are computed

This standard defines three slots for storing proxy-specific information. The logic contract address is at 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc, the beacon contract address is at 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50, and the admin address is at 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103. Each value is the keccak256 hash of the strings eip1967.proxy.implementation, eip1967.proxy.beacon and eip1967.proxy.admin with 1 subtracted, as in bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1). The text states that if more information is needed, later ERCs may define additional slots.

The logic contract slot and the Upgraded event

The logic contract slot holds the address of the logic contract to which this proxy delegates code through delegatecall. A proxy that uses a beacon instead should leave this slot empty (SHOULD). When the value of this slot changes, it should emit event Upgraded(address indexed implementation) (SHOULD). In the reference implementation, _setImplementation checks with Address.isContract that the new address is a contract and then stores it in StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value. _upgradeTo emits the Upgraded event after storing. If data is not empty or forceCall is true, _upgradeToAndCall sends one more delegatecall to the new implementation to handle the initialization call.

The beacon slot and the implementation() function

A beacon is a contract that keeps in one place the logic address used by many proxies. Changing a single storage slot of the beacon upgrades every proxy connected to that beacon at once. A beacon contract must implement function implementation() returns (address) (MUST). Its result should not depend on the caller msg.sender (SHOULD NOT). A beacon-based proxy does not use the logic slot and instead stores the address of the beacon it is attached to in the beacon slot. A client reads the beacon slot and calls implementation() on that address to find the logic contract. The beacon slot should be considered only when the logic slot is empty, and a proxy that uses a logic address directly should leave the beacon slot empty. When the value changes, it should emit event BeaconUpgraded(address indexed beacon).

The admin slot and the AdminChanged event

The admin slot holds the address that has the authority to change this proxy's logic contract address. This slot is optional, so some proxies do not use it. When the value changes, it should emit event AdminChanged(address previousAdmin, address newAdmin) (SHOULD). Unlike the other two events, it carries both the previous and new values, and its arguments are not marked indexed. In the reference implementation, _changeAdmin first emits the previous admin, read with _getAdmin(), and the new admin in the event, and then calls _setAdmin. _setAdmin checks with require that the new admin is not the zero address and stores it in _ADMIN_SLOT.

Event emission recommendation and initialization

The original text regards proxy monitoring as essential to the security of many apps. However, changes to storage slots are hard to track from the outside. It therefore recommends that any function that changes one of these three slots also emit the corresponding event (SHOULD). This includes initialization, where a slot changes from 0x0 to its first non-zero value. In other words, the Upgraded event must be emitted even at the moment the first logic address is set while deploying the proxy. The constructor of ERC1967Proxy in the reference implementation calls _upgradeToAndCall(_logic, _data, false), so Upgraded is emitted at deployment time.

Why storage slots and not public functions

The reason for standardizing storage slots instead of giving the proxy a public function that returns the logic address is function selector collisions. The ABI uses only a 4-byte selector to tell functions apart, so functions with different names can have the same selector. If a function on the proxy collides with a function on the logic contract, the call is not forwarded to the logic contract and the proxy intercepts it and returns its own value. Citing the Nomic Labs article Malicious backdoors in Ethereum proxies, the original text explains that the compiler catches collisions within one contract but not between contracts, so collisions can be abused to hide a backdoor in a contract that looks normal. For this reason a proxy should not expose to end users any function that could collide with the logic contract, and the logic address was standardized as a storage slot rather than a function.

Why it does not collide with the compiler's storage layout

The key condition is that the chosen slot must be one the compiler never picks when storing state variables. Otherwise a logic contract could overwrite proxy information while writing to its own variables. Solidity linearizes the inheritance chain and then places the first variable in the first slot and the next in the following slot, in order of declaration. Only the values of dynamic arrays and mappings are stored at a hash location formed by concatenating the key and the slot number. The Solidity team confirmed that this storage layout is part of the language's external interface, so changing it would be treated as a breaking change. Vyper follows the same approach. The standard slots are hashes of strings that do not start with a storage index, so they do not overlap with slots the compiler assigns. Subtracting 1 makes the hash preimage unknown, which lowers the chance of an attack further. However, collisions can occur in contracts written directly in other languages or in assembly.

Structure of the reference implementation

The reference implementation is split into four parts: Proxy, ERC1967Upgrade, ERC1967Proxy and StorageSlot. Proxy calls _delegate from fallback and receive, copies the calldata as is, sends a delegatecall, and then returns the result to the caller or reverts. ERC1967Upgrade provides getters for the three slots and setters that emit events. _setBeacon checks both that the new beacon is a contract and that the beacon's implementation() is a contract. For UUPS, _upgradeToAndCallSecure switches to the new implementation, turns on the rollback flag slot _ROLLBACK_SLOT (the keccak256 of eip1967.proxy.rollback minus 1, 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143), and tests whether the new implementation's upgradeTo(address) can return to the old implementation. If it cannot, it reverts with the error upgrade breaks further upgrades, and if it can, it upgrades to the new implementation again and emits Upgraded. StorageSlot is a library that uses inline assembly to set r.slot := slot and reads and writes an arbitrary slot as address, bool, bytes32 or uint256.

Things to watch out for

Original ERC · ← ERCs