Slot basics
EVM storage is a 2^256 key-value map of 32-byte words. State variables occupy sequential slots starting at 0 in declaration order within a contract, subject to packing. Structs and arrays inline or reference derived slots. Mappings and dynamic arrays do not consume a slot for data — they hash to locations.
- Slot 0 is the first state variable of the outermost contract after linearization.
- Constants live in code, not storage. Immutables are in code at runtime after deploy.
- bytes and string use separate slot rules for short vs long content.
Packing rules
Successive small types pack into one 32-byte slot until full. A uint128, uint128 pair shares slot 0. A uint256 followed by uint8 uses two slots — the uint8 cannot pack backward. Order declarations to minimize slots on hot storage.
- bool packs with smaller ints if space remains in the current slot.
- Structs pack internally like flat variables.
- Storage packing changes do not break layout if slot indices stay identical.
// Slot 0: uint128 a + uint128 b packed
// Slot 1: uint256 c
struct Packed {
uint128 a;
uint128 b;
uint256 c;
}Mappings and dynamic arrays
mapping(K=>V) lives at slot p; entry k is at keccak256(abi.encode(k, p)). Dynamic array at slot p stores length at p; elements start at keccak256(p). Nested mappings hash recursively. Collision with other variables is cryptographically unlikely if base slots differ.
- Deleting mapping entries is not iterable — need key tracking off-chain.
- Array length changes are storage writes — costly for unbounded growth.
- Upgradeable contracts must not change mapping types or slot positions.
Never reorder — upgrade rule #1
In upgradeable contracts, only append new variables at the end of your contract's layout (respecting inherited gaps). Reordering shifts every downstream slot — balances become allowances, owners become random addresses. Incidents have lost millions from single-line reorder bugs.
- Renaming is safe if type and order unchanged.
- Changing uint128 to uint256 at same position corrupts neighbor packing.
- Parent contract changes require re-auditing entire inheritance tree layout.
Storage gaps
OpenZeppelin upgradeable base contracts include uint256[50] private __gap; (or similar) reserved slots. Subclasses consume gap slots when adding variables without shifting parent fields. If you inherit OZ upgradeable, do not remove or shrink gaps in your own bases.
- Each uint256 in __gap occupies one future slot you can rename later into a real variable.
- Copy gap pattern when writing your own upgradeable abstract contracts.
- oz-upgrades validateStorageLayout compares versions before deploy.
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
abstract contract BaseUpgradeable is Initializable {
uint256[49] private __gap;
}ERC-7201 namespaced storage
Solidity 0.8.20+ ecosystems adopt ERC-7201: struct Storage { ... } at a fixed namespace slot computed from a id string. Reduces collision risk between mixins and makes layout explicit. OpenZeppelin is adopting namespaced storage in newer upgradeable releases.
- Namespace slot = keccak256(abi.encode(uint256(keccak256('example.storage')) - 1)) & ~bytes32(uint256(0xff))
- Each module reads/writes its struct at its namespace — fewer inheritance collisions.
- Still require discipline — namespaces do not fix reckless type changes inside structs.
Verification workflow
Before any upgrade: run storage layout diff, test migration on forked mainnet state, assert critical invariants (totalSupply, owner, balances sample). Automate layout checks in CI. Document slot assignments in repo for auditors.
- Hardhat upgrades plugin prints layout tables per contract.
- Foundry fork tests can assert storage slots before/after upgrade.
- Post-upgrade smoke test: read balances, roles, paused flag, implementation pointer.
