Solidity's layout at and erc7201: custom storage layouts explained
How Solidity's layout at specifier and the new erc7201 builtin move contract storage, how that differs from ERC-7201 namespaced structs, and the pitfalls.
· 6 min read
- Solidity
- Storage layout
- Upgradeable contracts
For most of Solidity’s history, a contract’s state variables started at storage slot 0, and the only way to put state anywhere else was inline assembly. Two recent releases changed that. Solidity 0.8.29 added the layout at specifier, which moves a contract’s storage to a base slot you choose. Solidity 0.8.35 added an erc7201 builtin, which computes an ERC-7201 namespace slot at compile time. Together, they let a contract place its storage at a namespaced location in one line.
This article covers what the two features do, how they differ from the ERC-7201 struct pattern most upgradeable code already uses, and the mistakes to avoid. The rules below are as of Solidity 0.8.37.
The layout specifier
layout at goes in the contract header and takes a base slot:
contract C layout at 0xAAAA + 0x11 {
uint[3] x; // occupies slots 0xAABB..0xAABD
}
Every state variable is then laid out exactly as before, but shifted by the base slot, including variables inherited from base contracts. Packing and ordering are unchanged. Mappings and dynamic arrays still keep their data at hashed locations, but those hashes are computed from the shifted slots, so they move too.
The rules:
- The specifier can go before or after the
islist, and only once. - It can only be declared on a non-abstract contract. It applies to that contract’s whole inheritance tree. Interfaces, libraries and abstract contracts can’t declare it, though variables they define are shifted along with everything else.
- A contract with a custom layout can’t be inherited from. The compiler rejects
contract B is AwhenAdeclareslayout at. - The base slot must be a compile-time constant expression. Since 0.8.31 it can refer to
constantvariables, and since 0.8.35 it can callerc7201. - Transient storage is unaffected. Constants and immutables don’t use storage at all.
- The compiler rejects a base slot that would push static variables past the end of storage, and it warns when they fall within the last 2^64 slots.
The erc7201 builtin
ERC-7201 defines a formula that turns a namespace id into a storage slot that won’t collide with Solidity’s default layout. In the EIP’s notation:
keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff))
Until 0.8.35 you computed this off-chain and pasted the result into your source as a hex constant. Now the compiler computes it:
uint256 constant VAULT_STORAGE = erc7201("acme.vault.main");
contract Vault is VaultLogic layout at VAULT_STORAGE {}
erc7201 takes a string and returns a uint256. Unlike keccak256, its result is a compile-time constant, so it can appear in a layout specifier or an array length. As a check, erc7201("openzeppelin.storage.ERC20") returns 0x52c6…ce00, the same value OpenZeppelin hardcodes in ERC20Upgradeable.
The builtin also works with the struct-based namespaces described below, and it is worth adopting there too. We cover that in one storage namespace per module.
One base slot, not one namespace per module
layout at gives a contract and all its bases one base slot. The whole linear layout moves together, in the same order as before.
The ERC-7201 pattern used by OpenZeppelin Contracts 5 is different. Each module keeps its state in its own struct, at its own namespace, and reads it through a storage pointer. The two approaches solve different problems:
layout at |
Namespaced structs | |
|---|---|---|
| Granularity | Whole contract tree | Per module |
| Inheritance order matters | Yes, as with the default layout | No |
| Syntax | Ordinary state variables | Struct reached through a pointer |
| Can be declared in | Non-abstract contracts only | Any contract or library |
| Contract can be inherited | No | Yes |
| In the compiler’s storage layout output | Yes | No |
They combine well. OpenZeppelin’s upgradeable contracts declare no ordinary state variables, so a layout at on your contract moves only your own.
Where it helps
EIP-7702 delegated accounts. This was the case the feature was designed for. An EOA that delegates to smart account code keeps that code’s storage in its own account. If the user switches to a different implementation, both would use slot 0 onwards and read each other’s leftovers. Giving each implementation its own base slot keeps them apart. The old values are still there, though, and an implementation the user delegates to again later will read them. Namespaced structs give the same separation.
Separating implementations behind one proxy. More generally, layout at protects a contract’s state from code that isn’t part of its inheritance tree, such as a previous implementation or raw sstore calls in assembly that assume slot 0. It does not protect against a base contract added in a later version. That base is shifted along with everything else, and collides by order exactly as it would without the specifier.
Readability. Ordinary state variables are easier to read and audit than a struct reached through assembly, and they appear by name in the compiler’s storage layout output and in debuggers.
Pitfalls
Don’t add it to a deployed contract. For a contract already live behind a proxy, adding layout at or changing its value moves every variable in the new implementation. The old values stay at their old slots and the upgraded contract reads zeros. Treat the base slot like any other part of the storage layout: fixed once deployed. Our comparison of proxy patterns covers the other layout rules upgrades must keep.
Keep logic in an abstract base. Since a contract with layout at can’t be inherited, contract VaultV2 is Vault, test harnesses that expose internals, and mocks all stop compiling. Put the logic in an abstract contract and make the deployable contract a thin shell, as in the Vault is VaultLogic example above. Tests and later versions inherit VaultLogic. Give harnesses and new shells the same layout at VAULT_STORAGE, so their layout matches what’s deployed.
Inheritance order still matters. Inside the shifted region the usual rules apply. Inserting a base contract, or a variable in a base, still moves every variable after it.
Use a current compiler. erc7201 needs 0.8.35 or later. Compile with 0.8.37, which has no known bugs at the time of writing, and pin it in foundry.toml. Versions 0.8.29 to 0.8.35 had a bug (SOL-2026-3) affecting base slots near the end of storage.
Use current upgrade tooling. OpenZeppelin’s @openzeppelin/upgrades-core validates layout at from version 1.44.0. It accepts erc7201(...) in the specifier only from 1.46.0, which also fixed a bug in its ERC-7201 slot calculation. Use 1.46.0 or later.
Reserved words. layout and at will become keywords in a future breaking release, and 0.8.35 already warns when they are used as names. The same release also warns about error, super, transient and this, so expect a few warnings from dependencies.
Check what you get. Run forge inspect <Contract> storage-layout (or solc --storage-layout) and confirm the first slot is the value you expect.
Our recommendation
For new code compiled with 0.8.37:
- Use
erc7201(...)instead of hardcoded hex slots everywhere. - Keep namespaced structs for reusable modules and abstract base contracts, where each module needs its own namespace.
- Put a deployable contract’s own variables either in a namespaced struct or behind
layout at erc7201(...)in a thin shell. Don’t leave them at slot 0.layout atis a good fit for EIP-7702 delegation code. - Leave the layout of already-deployed contracts alone.
For how these choices fit into a larger codebase, see structuring a Solidity protocol codebase. If you’re planning an upgradeable protocol and want a second opinion on its storage design, see our Ethereum smart contract development service.