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 memory.

slot 0 (32 bytes)

byte31
...
byte2
byte1
byte0
← 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:

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 byte 30 and 31 respectively.

slot 0

f
e
d
c
b
a
31
30
28-29
24-27
16-23
0-15

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.

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

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

(unused)
x
31-20
19-0

slot 1

(unused)
z
y
31-28
27-16
15-0

Storage Layout for contract B

Structs and Fixed-Sized Arrays

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

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 would end up looking like the following,

slot 0

(unused)
b
a
31-24
23-16
15-0

slot 1 (Struct S)

(unused)
z
y
x
31-25
24
23-16
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.

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

(unused)
b
a
31-24
23-16
15-0

slot 1

(unused)
s.z
s.y
s.x
31-25
24
23-16
15-0

slot 2

(unused)
c
31-8
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.

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 the contract D has a diamond inheritance pattern

A
B
C
D

The C3 linearization for D is commonly shown as: D → B → C → A. 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 (fully packed!)

C.c
B.b
A.a
31-24
23-16
15-0

slot 1

(unused)
D.d
31-4
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

Assume 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.

contract E {
    uint128 a;           // slot 0
    uint256[] arr;       // slot 1 (stores length)
    uint64 b;            // slot 2
}
// If arr = [10, 20, 30], length = 3

Main storage layout:

slot 0
(unused)
a
slot 1
arr.length = 3
slot 2
(unused)
b

Array elements at keccak256(1):

keccak256(1)
arr[0] = 10
keccak256(1)+1
arr[1] = 20
keccak256(1)+2
arr[2] = 30

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 a unique slot. Solidity just computes the slot on demand.

contract F {
    uint128 a;                      // slot 0
    mapping(address => uint256) m;  // slot 1 (empty)
    uint64 b;                       // slot 2
}
// If m[0xABC...] = 100, m[0xDEF...] = 200

Main storage layout:

slot 0
(unused)
a
slot 1
(empty - reserved for m)
slot 2
(unused)
b

Mapping values at hashed locations:

keccak256(abi.encode(0xABC..., 1))
m[0xABC...] = 100
keccak256(abi.encode(0xDEF..., 1))
m[0xDEF...] = 200

Storage Layout for contract F