ERC-7201 Namespaced storage layout
Final · Other · created 2023-06-20
A rule that groups state variables used together into a struct, places it at an independent storage location determined by a hash, and marks that location with a source comment. It avoids storage collisions in upgradeable and modular contracts and lets analysis tools read where data is located.
Why it is needed
Smart contract languages such as Solidity and Vyper use a tree-shaped storage layout that places state variables in order starting from slot 0. This is sufficient for most contracts, but it causes problems in some design patterns. In modular designs that execute the code of several contracts with DELEGATECALL while sharing the same storage, each piece of code must carefully coordinate how it uses storage. In upgradeable contracts, adding a state variable during an upgrade can affect the storage locations of existing variables, which makes adding variables difficult. For this reason, a method of placing a group of variables at a pseudorandom location obtained by hashing is used, but values placed this way are not declared as Solidity state variables and are therefore invisible to the compiler. Tools such as static analyzers and block explorers need to know where a contract's data is, and if the way locations are determined is not standardized, they cannot interpret such contracts correctly.
In depth
Namespaces and namespace ids
A namespace is an ordered set of variables. It can contain dynamic arrays or mappings, and its values are laid out under the same rules as the default storage layout, except that the root may not be slot 0. This pseudorandom root is constructed so that it does not belong to the default tree, so it becomes the starting point of a new storage tree and forms an independent space that does not overlap with others. A contract that organizes its storage using namespaces is said to use namespaced storage. A namespace id is a string that identifies a namespace within a contract and must not contain whitespace characters.
@custom:storage-location comment
A namespace inside a contract must be implemented as a struct type. The struct is annotated with the NatSpec tag `@custom:storage-location <FORMULA_ID>:<NAMESPACE_ID>`. `<FORMULA_ID>` refers to the formula that computes the root storage location from the namespace id. The Solidity compiler includes this annotation in the AST starting with v0.8.20, so that version is recommended as the minimum compiler version when using this pattern. If a struct declared outside a contract carries this annotation, it is not treated as the namespace of any contract in the source code.
The erc7201 formula
The formula referred to by the identifier `erc7201` is `erc7201(id: string) = keccak256(keccak256(id) - 1) & ~0xff`. As a Solidity expression, it is `keccak256(abi.encode(uint256(keccak256(bytes(id))) - 1)) & ~bytes32(uint256(0xff))`. When this formula is used, the annotation becomes `@custom:storage-location erc7201:<NAMESPACE_ID>`. For example, `@custom:storage-location erc7201:foobar` denotes a namespace with the id "foobar" whose root is placed at `erc7201("foobar")`. Other EIPs may define new formulas with unique identifiers in the future, and they are encouraged to follow this EIP's convention and use identifiers of the form `erc1234`.
Why the formula avoids collisions
The tree-shaped storage of Solidity and Vyper starts at root 0 and builds locations of the form `root + n`, `keccak256(root)`, `keccak256(H(k) ⊕ root)` and `keccak256(root ⊕ H(k))`. A namespace root must belong neither to this default tree nor to a tree derived from another namespace. That way, multiple namespaces can be used together with the default layout, intentionally or not, without collisions. `keccak256(id) - 1` is a location that Solidity does not use, but once a namespace grows beyond one slot it extends to `keccak256(id) + n`, which can overlap with the region Solidity may use, so it is not used as the final location. The hash is therefore applied once more, so that the namespace is fully separated from the default storage under the assumptions of keccak256 collision resistance and no abnormally large arrays. The trailing `& ~0xff` aligns the location to a multiple of 256. This is an optimization anticipating that, after the transition to the Verkle state tree, the gas schedule will change and a group of 256 slots may become warm all at once.
Reference implementation
The reference implementation uses `pragma solidity ^0.8.20` and attaches the annotation `/// @custom:storage-location erc7201:example.main` to a `MainStorage` struct holding `uint256 x` and `uint256 y`. The value `0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500`, computed as `keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff))`, is stored in the `MAIN_STORAGE_LOCATION` constant. The `_getMainStorage()` function assigns this constant to the `.slot` of the storage pointer `$` to be returned in assembly and returns the struct, and `_getXTimesY()` returns `x * y` of that struct.
Naming and backwards compatibility
This pattern has sometimes been called diamond storage. That name makes it easy to mistake it for the same thing as the diamond proxy pattern, but the two can be used independently. To distinguish it clearly from the proxy pattern, this EIP chose a different name, namespaced storage layout. On backwards compatibility, the original text states that no issues were found. Its status is Final, so it is a finalized standard.
Things to watch out for
- Avoiding collisions with other namespaces and the default layout: A namespace must not collide with other namespaces or with the standard Solidity and Vyper storage layout. Assuming keccak256 collision resistance, the formula in this ERC guarantees this property for any namespace id.
- The annotation is not enforced by the compiler: `@custom:storage-location` is only a NatSpec annotation, and current compilers apply no rules to it and give it no meaning. The contract developer is responsible for implementing the pattern and using the namespace as the annotation states.