Storage in Solidity

December 23, 2025

In Solidity, storage is divided into 32-byte "slots". Solidity contracts pack variables into storage slots according to a number of rules. Since gas is limited, making efficient use of storage is important. So, it is a fun exercise to explore how the Solidity compiler allocates storage.

slot 0
byte31
…
byte2
byte1
byte0
byte0: bytes 0
← high end (MSB) low end (LSB) →

Packing Rules

The first packed value in a slot goes at the end of the slot (the right side), i.e. at byte offset 0 from the low end. (byte0 is the least-significant byte / the rightmost byte in hex)

As a concrete example, in the following code:

Solidity

contract A {
    uint128 a; // 16 bytes
    uint64  b; // 8 bytes
    uint32  c; // 4 bytes
    uint16  d; // 2 bytes
    uint8   e; // 1 byte
    bool    f; // 1 byte
}

a is lower-order aligned (bytes 0-15). Then b is stored right above it, i.e. bytes 16-23 in this case, c is stored 24-27, d 28-29 and e and f take up bytes 30 and 31 respectively.

slot 0
f
31 f: bool, bytes 31
e
30 e: uint8, bytes 30
d
29–28 d: uint16, bytes 29–28
c
27–24 c: uint32, bytes 27–24
b
23–16 b: uint64, bytes 23–16
a
15–0 a: uint128, bytes 15–0

Storage layout for contract A

Fixed types don't reserve extra bytes. So types like uint64 use 8 bytes and bool uses 1 byte. They don't reserve 32 bytes unless the type itself is 32 bytes (uint256, bytes32). If a value type is bigger than the remaining part of a storage slot, it is stored in the next storage slot.

Solidity

contract B {
    uint160 x; // 20 bytes
    uint128 y; // 16 bytes
    uint96  z; // 12 bytes
}

In the following example, x takes slot 0 (bytes 0-19), leaving 12 bytes unused. y (16 bytes) can't fit in the remaining 12 bytes, so it starts fresh in slot 1. z (12 bytes) fits above y in slot 1.

slot 0
31–20 unused: 12 bytes
x
19–0 x: uint160, bytes 19–0
slot 1
31–28 unused: 4 bytes
z
27–16 z: uint96, bytes 27–16
y
15–0 y: uint128, bytes 15–0

Storage layout for contract B

Structs and Fixed-Size Arrays

Structs and fixed-size arrays have different behaviour since they are contiguous values. They always occupy whole 32-byte slots. So, even if a slot has free space left, a struct or a fixed-size array won't start in it. Instead, Solidity moves to the next 32-byte slot, then lays out the struct/array contents contiguously.

Solidity

contract C1 {
    uint128 a;   // 16 bytes
    uint64  b;   // 8 bytes  -> still in slot 0, total used = 24 bytes
    // 8 bytes still free in slot 0 here

    struct S {
        uint128 x; // 16
        uint64  y; // 8  (total 24)
        bool    z; // 1  (total 25)
        // (7 bytes free inside struct's slot)
    }

    S s;         // <-- MUST start at a NEW slot (slot 1)
}

So this ends up looking like the following:

slot 0
31–24 unused: 8 bytes
b
23–16 b: uint64, bytes 23–16
a
15–0 a: uint128, bytes 15–0
slot 1
31–25 unused: 7 bytes
s.z
24 s.z: bool, bytes 24
s.y
23–16 s.y: uint64, bytes 23–16
s.x
15–0 s.x: uint128, bytes 15–0

Storage layout for contract C1

Even if the last slot used by the struct/fixed-size arrays has leftover bytes, the next state variable starts at the next slot, not in the leftover space.

Solidity

contract C2 {
    uint128 a;
    uint64  b;

    struct S { uint128 x; uint64 y; bool z; }
    S s;          // uses slot 1, leaves 7 bytes unused in slot 1

    uint64 c;     // <-- starts in slot 2 (NOT packed into slot 1)
}
slot 0
31–24 unused: 8 bytes
b
23–16 b: uint64, bytes 23–16
a
15–0 a: uint128, bytes 15–0
slot 1
31–25 unused: 7 bytes
s.z
24 s.z: bool, bytes 24
s.y
23–16 s.y: uint64, bytes 23–16
s.x
15–0 s.x: uint128, bytes 15–0
slot 2
31–8 unused: 24 bytes
c
7–0 c: uint64, bytes 7–0

Storage layout for contract C2

Inheritance

For contracts with inheritance, Solidity lays out state variables in storage following the C3-linearized order of base contracts (starting from the most base-ward contract). After that, Solidity applies the normal packing rules; variables from different contracts share the same 32-byte storage slot when their sizes allow.

Solidity

contract A {
    uint128 a; // 16 bytes
}

contract B is A {
    uint64 b;  // 8 bytes
}

contract C is A {
    uint64 c;  // 8 bytes
}

contract D is B, C {
    uint32 d;  // 4 bytes
}

As you can see, contract D has a diamond inheritance pattern:

ABCD

D inherits from B and C, which both inherit from A

The C3 linearization for D is D → C → B → A, since Solidity treats the last base listed (C) as the most derived. But storage variables are assigned starting from the most base-ward contract, then packed normally. So the storage layout order is: A → B → C → D.

slot 0
C.c
31–24 C.c: uint64, bytes 31–24
B.b
23–16 B.b: uint64, bytes 23–16
A.a
15–0 A.a: uint128, bytes 15–0
slot 1
31–4 unused: 28 bytes
D.d
3–0 D.d: uint32, bytes 3–0

Storage layout for contract D

Variables from A, B, and C pack into slot 0 (16 + 8 + 8 = 32 bytes). D.d starts in slot 1.

Dynamic Arrays and Mappings

Last are mappings and dynamic arrays, which don't have a fixed size at compile-time. So Solidity cannot place their contents "inline" between neighboring variables, because it would have no idea how much space to reserve. Instead, Solidity gives the mapping/array one 32-byte slot p in the linear layout, and then stores the actual contents somewhere else, at locations derived from keccak256.

Dynamic arrays

If the dynamic array variable itself is assigned slot p, slot p stores the length of the array.

Here keccak256(p) means hashing the 32-byte encoding of p, i.e. keccak256(abi.encode(uint256(p))).

Element data begins at base = keccak256(p). After that, elements are laid out like a normal array (and can pack if the element type is small enough). The array elements are stored far away from the main layout at a hash-derived location, preventing collisions with other variables.

Solidity

contract E {
    uint128 a;           // slot 0
    uint256[] arr;       // slot 1 (stores length)
    uint64 b;            // slot 2
}
// If arr = [10, 20, 30], length = 3
slot 0
31–16 unused: 16 bytes
a
15–0 a: uint128, bytes 15–0
slot 1
arr.length = 3
31–0 arr.length: uint256, bytes 31–0
slot 2
31–8 unused: 24 bytes
b
7–0 b: uint64, bytes 7–0
Hashed slots
keccak256(1)
arr[0] = 10
31–0 arr[0]: uint256, bytes 31–0
keccak256(1) + 1
arr[1] = 20
31–0 arr[1]: uint256, bytes 31–0
keccak256(1) + 2
arr[2] = 30
31–0 arr[2]: uint256, bytes 31–0

Storage layout for contract E

string and bytes are a special case of dynamic arrays with an optimization. Assuming the variable itself is at slot p: for short values (≤ 31 bytes), the bytes are stored directly in slot p (left-aligned in the higher-order bytes) and the lowest-order byte stores length * 2. For longer values (≥ 32 bytes), slot p stores length * 2 + 1, and data starts at keccak256(p) like normal dynamic arrays. A quick trick: check the lowest bit — 0 means "short", 1 means "long".

Mappings

If the mapping variable itself is assigned slot p, slot p is left empty (no length stored). But p still matters as a namespace salt so two mappings don't collide. For a mapping(k => v) at slot p, the slot for key k is: keccak256(abi.encode(k, p)). Each key hashes to its own slot (collisions are practically impossible), so Solidity just computes the slot on demand.

Solidity

contract F {
    uint128 a;                      // slot 0
    mapping(address => uint256) m;  // slot 1 (empty)
    uint64 b;                       // slot 2
}
// If m[0xABC...] = 100, m[0xDEF...] = 200
slot 0
31–16 unused: 16 bytes
a
15–0 a: uint128, bytes 15–0
slot 1
m · empty
31–0 m: mapping(address => uint256), empty
slot 2
31–8 unused: 24 bytes
b
7–0 b: uint64, bytes 7–0
Hashed slots
keccak256(abi.encode(0xABC…, 1))
m[0xABC…] = 100
31–0 m[0xABC…]: uint256, bytes 31–0
keccak256(abi.encode(0xDEF…, 1))
m[0xDEF…] = 200
31–0 m[0xDEF…]: uint256, bytes 31–0

Storage layout for contract F