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)
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
fedcbaStorage 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
xslot 1
zyStorage 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
baslot 1 (Struct S)
zyxStorage 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
baslot 1
s.zs.ys.xslot 2
cStorage 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
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.cB.bA.aslot 1
D.dStorage 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 = 3Main storage layout:
aarr.length = 3bArray elements at keccak256(1):
arr[0] = 10arr[1] = 20arr[2] = 30Storage 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...] = 200Main storage layout:
am)bMapping values at hashed locations:
m[0xABC...] = 100m[0xDEF...] = 200Storage Layout for contract F