If I asked you to map out how Solidity assigns storage slots for primitive and complex data types, could you? Let's find out!
What is a Storage Slot?
Every smart contract has a fixed array of storage slots indexed from 0
to 2^256 - 1
. Each slot holds 32 bytes (256 bits) and acts like a unique address for storing data.

In Solidity, variables are stored sequentially in these slots based on their order of declaration in the contract — always predictable.
contract SimpleStorage {
uint256 public a; // slot 0
uint256 public b; // slot 1
}
Storage Slots:
Slot | Contents | Notes |
---|---|---|
0 | Your value | a (uint256) |
1 | Your value | b (uint256) |
Variable Packing
In the example above we dealt with uint256
, which take a full 32-byte slot each. But smaller types like uint8
, uint32
, address
, and bool
use less space. Solidity can pack these smaller types together into the same 32-byte slot — if they're declared next to each other and the total size stays within 32 bytes.
contract GoodPacking {
uint256 a; // slot 0 (32 bytes)
address b; // slot 1 (20 bytes)
uint64 c; // slot 1 (8 bytes)
uint32 d; // slot 1 (4 bytes)
}
Storage Slots:
Slot | Contents | Notes |
---|---|---|
0 | a (32 bytes) | Full slot |
1 | b + c + d packed into 32 bytes | Packed from right to left (LSB to MSB) |
If packing isn't possible (wrong order or alignment), unused space is wasted:
contract BadPacking {
uint32 a; // slot 0 (4 bytes, 28 bytes unused)
address b; // slot 1 (20 bytes, 12 bytes unused)
uint256 c; // slot 2 (32 bytes)
uint64 d; // slot 3 (8 bytes, 24 bytes unused)
}
Slot | Contents | Notes |
---|---|---|
0 | a (4 bytes) + b (20 bytes) | 8 bytes unused |
1 | c (32 bytes) | Full slot |
2 | d (8 bytes) | 24 bytes unused |
Common Sizes
Data Type | Size (Bytes) |
---|---|
uint8 | 1 |
uint32 | 4 |
uint128 | 16 |
uint256 | 32 |
bool | 1 |
address | 20 |
bytes1 | 1 |
bytes32 | 32 |
Reading Storage Slots with Foundry's cast
You can inspect contract storage on-chain using:
cast storage <contract-address> <slot-index> --rpc-url <your-rpc-url>
This returns the raw 32-byte slot value in hex. Here is the link to the cast-storage documentation.
Why Hex?
- Ethereum stores data in 32-byte words, which are naturally represented in hex.
- Hex is compact and easy to interpret for developers debugging low-level storage.
- Here is a link to a UI hex converter.
- Here is the link to the cast-conversion documentation.
Convert Hex to Decimal
cast to-dec 0x2a
# 42
Or use Unix shell:
echo $((0x2a))
# 42
Storage Slots for Mappings
Mappings don't use sequential slots like primitives. Instead, each key-value pair is stored in a slot derived by hashing:
mapping(address => uint256) public balances; // base slot N
Each balances[key]
value is at:
bytes32 slot = keccak256(abi.encodePacked(
bytes32(uint256(uint160(key))), // 32-byte key
bytes32(uint256(0)) // base slot 0
));
Storage Slots:
Slot | Contents | Notes |
---|---|---|
0 | 0x00 | mapping base slot (unused/pointer) |
keccak256(abi.encodePacked(key, 0)) | Your value | balances[key] value |
baseSlot
= slot assigned to the mapping variable itself as a pointerkey
is padded to 32 bytes- The result is a unique slot for each key.
Nested Mappings
For nested mappings like:
mapping(address outerKey => mapping(uint256 innerKey => uint256)) public nested;
The slot for nested[outerKey][innerKey]
is:
firstHash = keccak256(abi.encodePacked(outerKey, baseSlot))
finalSlot = keccak256(abi.encodePacked(innerKey, firstHash))
function getSlot(address outerKey, uint256 innerKey)
external pure returns (bytes32) {
// Step 1: keccak256(abi.encodePacked(padded outerKey, padded base slot))
bytes32 first = keccak256(abi.encodePacked(
bytes32(uint256(uint160(outerKey))), // pad outerKey to 32 bytes
bytes32(uint256(0)) // base slot of `nested`
));
// Step 2: keccak256(abi.encodePacked(padded innerKey, first hash))
bytes32 finalSlot = keccak256(abi.encodePacked(
bytes32(innerKey),
first
));
return finalSlot;
}
Storage Slots:
Slot | Contents | Notes |
---|---|---|
0 | 0x00 | nested mapping base slot (unused/pointer) |
keccak256(abi.encodePacked(outerKey, baseSlot)) | 0x00 | inner mapping pointer |
keccak256(abi.encodePacked(innerKey, firstHash)) | Your value | nested[outerKey][innerKey] value |
Mappings with Structs
When mappings store structs, the struct's fields are stored sequentially starting at the computed slot.
Example:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StructInMappingStorage {
struct User {
uint256 balance;
bool isActive;
}
mapping(address => User) public users; // base slot = 0
constructor() {
address user = 0x1234567890123456789012345678901234567890;
uint256 baseSlot = 0;
// Step 1: mapping base slot
bytes32 mappingSlot = keccak256(abi.encodePacked(
bytes32(uint256(uint160(user))), // pad address
bytes32(baseSlot) // base slot of `users`
));
// Step 2: field offsets
bytes32 balanceSlot = mappingSlot; // offset 0
bytes32 isActiveSlot = bytes32(uint256(mappingSlot) + 1); // offset 1
// Store values for demonstration
assembly {
sstore(balanceSlot, 1000) // users[user].balance = 1000
sstore(isActiveSlot, 1) // users[user].isActive = true
}
}
}
Storage Slots:
Slot | Contents | Notes |
---|---|---|
0 | 0x00 | users mapping base slot (unused) |
keccak256(abi.encodePacked(user, 0)) | Your value | users[user].balance |
keccak256(abi.encodePacked(user, 0)) + 1 | Your value | users[user].isActive |
users[key].balance
→ slot:keccak256(abi.encodePacked(key, N))
users[key].isActive
→ slot:keccak256(abi.encodePacked(key, N)) + 1
Each struct field gets its own slot incrementally after the base slot for that key.
Packed Struct in Mapping
Let's show a real packing case:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract PackedStructInMapping {
struct Flags {
bool a; // 1 byte
uint8 b; // 1 byte
uint16 c; // 2 bytes
// total: 4 bytes → all packed into one 32-byte slot
}
mapping(address => Flags) public flags; // base slot = 0
constructor() {
address user = 0x1234567890123456789012345678901234567890;
// Compute base slot of mapping
bytes32 baseSlot = bytes32(uint256(0));
bytes32 slot = keccak256(abi.encodePacked(
bytes32(uint256(uint160(user))),
baseSlot
));
// All fields are packed into `slot`
// Let's set the raw storage manually
// Pack: [a (1 byte) | b (1 byte) | c (2 bytes) | padding (28 bytes)]
uint256 packedValue = uint256(
bytes32(
abi.encodePacked(
uint8(1), // a = true
uint8(0x42), // b
uint16(0xBEEF) // c
)
)
);
assembly {
sstore(slot, packedValue)
}
}
}
Storage Slots:
Slot | Contents | Notes |
---|---|---|
0 | 0x00 | Base slot of the flags mapping (used as the seed for keccak256, but not otherwise used directly) |
keccak256(abi.encodePacked(user, uint256(0))) | 0x00...00efbe4201 | flags[user] struct packed into a single 32-byte storage slot: a = true (0x01, byte 0), b = 0x42 (byte 1), c = 0xBEEF (stored as 0xEFBE in little-endian, bytes 2–3). Packed in declaration order, starting from the least-significant byte |
Accessing Packed Fields
To read the values, you'd have to mask and shift from vm.load()
or inline assembly. Check back for a future article on how to do this in detail.
// read from slot
uint256 raw = uint256(vm.load(addr, slot));
// unpack values
bool a = (raw & 0xFF) != 0;
uint8 b = uint8((raw >> 8) & 0xFF);
uint16 c = uint16((raw >> 16) & 0xFFFF);
Storage Slots for Arrays
Fixed-Size Arrays
uint256[3] public fixedArray; // slot N
Stored sequentially:
fixedArray[0]
→ slot NfixedArray[1]
→ slot N + 1fixedArray[2]
→ slot N + 2
Storage Slots:
Slot | Value | Notes |
---|---|---|
0 | your value | fixedArray[0] |
1 | your value | fixedArray[1] |
2 | your value | fixedArray[2] |
Dynamic Arrays
uint256[] public dynamicArray; // slot N
-
Slot N holds the length of the array.
-
Data elements start at:
keccak256(abi.encodePacked(uint256(N)))
-
Element
i
is at:keccak256(...) + i
Storage Slots:
Slot | Value |
---|---|
0 | dynamicArray.length |
keccak256(abi.encodePacked(uint256(0))) | dynamicArray[0] |
keccak256(abi.encodePacked(uint256(0))) + 1 | dynamicArray[1] |
keccak256(abi.encodePacked(uint256(0))) + 2 | dynamicArray[2] |
Nested Arrays
1. Fixed Nested Arrays — Flattened Sequential Storage
Example:
pragma solidity ^0.8.0;
contract FixedNestedArray {
uint256[2][3] public arr; // stored at slot 0
function setValues() public {
arr[0][0] = 100;
arr[0][1] = 101;
arr[1][0] = 200;
arr[1][1] = 201;
arr[2][0] = 300;
arr[2][1] = 301;
}
}
- Slots start at
0
forarr
. - The array is stored as a flat sequence of 6 uint256 elements.
- Slot for element
arr[i][j]
=0 + i * 2 + j
So:
Element | Slot |
---|---|
arr[0][0] | 0 |
arr[0][1] | 1 |
arr[1][0] | 2 |
arr[1][1] | 3 |
arr[2][0] | 4 |
arr[2][1] | 5 |
Storage Slots:
Slot | Value | Notes |
---|---|---|
0 | 0x64 | arr[0][0] = 100 |
1 | 0x65 | arr[0][1] = 101 |
2 | 0xc8 | arr[1][0] = 200 |
3 | 0xc9 | arr[1][1] = 201 |
4 | 0x12c | arr[2][0] = 300 |
5 | 0x12d | arr[2][1] = 301 |
2. Dynamic Nested Arrays — Pointers and Hashing
Example:
contract DynamicNestedArray {
uint256[][] public arr; // stored at slot 0
function addValues() public {
arr.push([10, 11]);
arr.push([20, 21, 22]);
}
}
Storage:
- Slot
0
stores length of outer array (arr.length
). - Outer array data slots start at
keccak256(0)
. - Each slot at
keccak256(0) + i
stores the starting slot of inner arrayi
. - Each inner array stores its length at its starting slot, and its elements follow sequentially.
How to read arr[1][2]
(3rd element of inner array 1):
-
Read outer array length from slot
0
. -
Compute outer base slot:
outerBase = keccak256(abi.encode(uint256(0)))
-
Read inner array 1 pointer from slot
outerBase + 1
. -
Read inner array 1 length from that pointer slot.
-
The elements start at pointer slot + 1, so
elementSlot = pointerSlot + 1 + 2 // 2 = index of element in inner array
Summary:
Step | How to Calculate |
---|---|
Outer array length | Slot 0 |
Outer array base | keccak256(0) |
Inner array i pointer | Slot keccak256(0) + i |
Inner array length | At slot pointer |
Inner array element j | At slot pointer + 1 + j |
Storage Slots:
Slot | Value | Notes |
---|---|---|
0 | 0x02 | arr.length = 2 |
keccak(0)+0 | 0xA0 | pointer to arr[0] |
keccak(0)+1 | 0xB0 | pointer to arr[1] |
A0 | 0x02 | arr[0].length = 2 |
A1 | 0x0a | arr[0][0] = 10 |
A2 | 0x0b | arr[0][1] = 11 |
B0 | 0x03 | arr[1].length = 3 |
B1 | 0x14 | arr[1][0] = 20 |
B2 | 0x15 | arr[1][1] = 21 |
B3 | 0x16 | arr[1][2] = 22 |
Structs
Structs in Solidity are essentially groups of variables packed together in storage like fixed arrays, but with the benefit of variable packing to save space where possible.
Basic Struct Storage Layout
Consider this struct:
struct Person {
uint256 id; // 32 bytes
bool isRegistered; // 1 byte
uint32 age; // 4 bytes
}
If you declare a variable:
Person public person; // stored starting at slot N (e.g. 0)
person.id
occupies slot N (full 32 bytes)person.isRegistered
andperson.age
fit together packed into slot N+1 (because 1 + 4 bytes < 32 bytes)
Storage Slots:
Slot | Value |
---|---|
0 | person.id |
1 | person.isRegistered + person.age (packed) |
How Variable Packing Works Here
id
is a fulluint256
, so it takes one entire slot.- The smaller types (
bool
,uint32
) are packed tightly into the next slot if declared consecutively. - If there were larger gaps or non-consecutive smaller variables, unused space would remain.
Structs Inside Mappings or Arrays
When a struct is stored inside a mapping or an array, its base storage slot is calculated first (via mapping hashing or array slot calculation), then the struct members are stored sequentially starting at that slot.
Example: Struct in a Mapping
contract StructMapping {
struct Person {
uint256 id;
bool isRegistered;
uint32 age;
}
mapping(address => Person) public people; // base slot 0
}
Storage slot for people[key]
is:
slot = keccak256(abi.encodePacked(key, baseSlot))
Where baseSlot
= 0 here.
Then, members of Person
are stored as:
Member | Slot |
---|---|
id | slot |
isRegistered + age | slot + 1 |
So if key = 0x1234...
, then:
people[0x1234].id
→ at slotkeccak256(key, 0)
people[0x1234].isRegistered
+people[0x1234].age
→ packed at slotkeccak256(key, 0) + 1
Storage Slots:
Slot | Value |
---|---|
0 | people mapping base slot (unused) |
keccak256(abi.encodePacked(key, 0)) | people[key].id |
keccak256(abi.encodePacked(key, 0)) + 1 people[key].isRegistered + people[key].age (packed) |
Example: Struct in a Dynamic Array
contract StructArray {
struct Person {
uint256 id;
bool isRegistered;
uint32 age;
}
Person[] public people; // slot 0
}
people.length
stored at slot 0- Data stored starting at
keccak256(0)
sequentially per struct
To read the i-th Person
:
-
Calculate base data slot:
base = keccak256(0)
-
Each struct uses consecutive slots starting at:
structSlot = base + (i * number_of_slots_per_struct)
-
For our struct, number_of_slots_per_struct = 2 (one full slot for id, one packed slot for bool + uint32).
-
So for element
i
, slots:
Member | Slot |
---|---|
id | base + (i * 2) |
isRegistered + age | base + (i * 2) + 1 |
Storage Slots:
Slot | Value |
---|---|
0 | people.length |
keccak256(0) | people[0].id |
keccak256(0) + 1 | people[0].isRegistered + people[0].age (packed) |
keccak256(0) + 2 | people[1].id |
keccak256(0) + 3 | people[1].isRegistered + people[1].age (packed) |
Summary Table
Scenario | Slot calculation formula | Notes |
---|---|---|
Simple struct variable | Starts at declared slot N, members stored sequentially | Packs members when possible |
Struct in mapping | keccak256(abi.encodePacked(key, baseSlot)) + memberOffset | memberOffset = 0,1,... slots |
Struct in dynamic array | keccak256(baseSlot) + i * slotsPerStruct + memberOffset | i = index in array |
Visual Example for StructMapping
Key (address ) | Slot (keccak256(key, 0)) | Member | Offset |
---|---|---|---|
0xabc... | 0xdeadbeef... | id | 0 |
0xabc... | 0xdeadbeef... + 1 | isRegistered + age (packed) | 1 |
Solidity String Storage — Two Cases
Solidity stores strings in two different ways, depending on their length.
Case 1: Short Strings (≤ 31 bytes)
- Stored inline in a single storage slot.
- The last byte stores
length * 2 + 1
(odd number indicates short string). - The actual string is left-aligned, with padding.
Example
string public s = "hello"; // 5 bytes
-
Stored in one slot:
0x68656c6c6f0000000000000000...0000000000000000000000000b
Part Meaning 0x68656c6c6f
ASCII for "hello"
Zeros Padding to fill 32 bytes 0x0b
(last byte)5 * 2 + 1 = 11
(odd → short string flag)
Storage Slots:
Slot | Value | Notes |
---|---|---|
0 | 0x68656c6c6f0000000000000000...0000000000000000000000000b | "hello" (5 bytes) + length flag |
If the last byte is odd, it's a short string and the content is inline.
Case 2: Long Strings (≥ 32 bytes)
-
Slot stores a pointer:
length * 2
(even number). -
Actual data is stored in a separate location:
keccak256(slot)
Example
string public s = "abcdefghijklmnopqrstuvwxyz123456"; // 32 bytes
-
Slot contains:
0x00000000000000000000...00000000000000000000000000040
(because
32 * 2 = 64 = 0x40
→ even → long string) -
Actual string is stored at:
slot = keccak256(slot_number)
-
If it's more than 32 bytes, it spills into
slot + 1
,slot + 2
, etc.
Storage Slots:
Slot | Value | Notes |
---|---|---|
0 | 0x40 | pointer to string data (32 * 2 = 64 = 0x40) |
keccak256(0) | 0x6162636465666768696a6b6c6d6e6f707172737475767778797a313233343536 | "abcdefghijklmnopqrstuvwxyz123456" |
Summary
Congratulations on reaching this point!
Getting how Solidity stores different data types might seem tricky at first, but once you've got it down, you'll write smarter, cheaper, and more reliable contracts. Knowing where and how your data lives on-chain helps you avoid surprises and optimize gas costs. With this knowledge, you're ready to build solid Ethereum apps that run smoothly and efficiently.
Resources
Connect with me on social media: