Skip to content
Learn · Advanced

Storage layout and collision safety

How Solidity assigns slots, why upgrades corrupt state when layout drifts, and how OpenZeppelin gaps and ERC-7201 namespaced storage help.

Advanced
16 min read

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.
Packing.solsolidity
// 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.
Gap.solsolidity
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.