Storage Layout & Packing

Storage is a map of 2^256 slots, each 32 bytes.

26.1 Slots

Storage is a map of 2^256 slots, each 32 bytes. State variables are laid out in declaration order starting at slot 0 (including inherited variables, in linearization order, most-base first).

Value types < 32 bytes packed into the same slot if they fit, right-aligned (lower-order) Structs and fixed arrays always start a new slot; members packed by the same rules Item after a struct/array starts a new slot Dynamic array at slot p length at p; element i at keccak256(p) + i (packed if small) Mapping at slot p slot p unused; value for key k at keccak256(abi.encode(k, p)) bytes/string < 32 bytes data and length*2 in the same slot bytes/string ≥ 32 bytes slot holds length*2+1; data at keccak256(p) constant / immutable no storage slot separate transient address space with its own layout

26.2 Packing example

Packing saves gas when variables are read or written together; reading a packed small type alone costs extra masking operations. Order struct members and state variables by size and access pattern.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Unpacked {        // 3 slots
    uint128 a;             // slot 0
    uint256 b;             // slot 1
    uint128 c;             // slot 2
}

contract Packed {          // 2 slots
    uint128 a;             // slot 0 (bytes 0-15)
    uint128 c;             // slot 0 (bytes 16-31)
    uint256 b;             // slot 1
}

contract PackedStruct {
    struct Position {      // fits in ONE slot: 20 + 8 + 4 = 32 bytes
        address owner;     // 20 bytes
        uint64 openedAt;   // 8 bytes
        uint32 leverage;   // 4 bytes
    }
    mapping(uint256 => Position) public positions;
}

26.3 Inspecting storage

forge inspect Packed storageLayout          # slot, offset, type of each variable
cast storage <address> 0 --rpc-url $RPC     # raw value of slot 0 on a live contract
cast index address <key> <mappingSlot>      # compute mapping slot for key
// Reading/writing arbitrary slots in assembly
function readSlot(uint256 slot) external view returns (bytes32 v) { assembly { v := sload(slot) } }

26.4 Namespaced storage (ERC-7201) and custom layouts

To avoid collisions in upgradeable and modular contracts, group state in a struct stored at a pseudo-random slot:

OpenZeppelin Contracts 5.x upgradeable contracts use ERC-7201 namespaces. Since 0.8.29 you can also move a contract's whole storage base: contract C layout at 0x1234 { ... } (the expression must be a compile-time constant).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract NamespacedStorage {
    /// @custom:storage-location erc7201:example.main
    struct MainStorage {
        uint256 value;
        mapping(address => uint256) balances;
    }

    // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant MAIN_STORAGE_LOCATION =
        keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff));

    function _getMainStorage() private pure returns (MainStorage storage $) {
        bytes32 slot = MAIN_STORAGE_LOCATION;
        assembly { $.slot := slot }
    }

    function setValue(uint256 v) external { _getMainStorage().value = v; }
    function value() external view returns (uint256) { return _getMainStorage().value; }
}