Published on

June 13, 2025

Solidity Storage Slots — A Core Concept

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.

Storage Slots
Storage Slots

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:

SlotContentsNotes
0Your valuea (uint256)
1Your valueb (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:

SlotContentsNotes
0a (32 bytes)Full slot
1b + c + d packed into 32 bytesPacked 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)
}
SlotContentsNotes
0a (4 bytes) + b (20 bytes)8 bytes unused
1c (32 bytes)Full slot
2d (8 bytes)24 bytes unused

Common Sizes

Data TypeSize (Bytes)
uint81
uint324
uint12816
uint25632
bool1
address20
bytes11
bytes3232

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:

SlotContentsNotes
00x00mapping base slot (unused/pointer)
keccak256(abi.encodePacked(key, 0))Your valuebalances[key] value
  • baseSlot = slot assigned to the mapping variable itself as a pointer
  • key 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:

  1. firstHash = keccak256(abi.encodePacked(outerKey, baseSlot))
  2. 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:

SlotContentsNotes
00x00nested mapping base slot (unused/pointer)
keccak256(abi.encodePacked(outerKey, baseSlot))0x00inner mapping pointer
keccak256(abi.encodePacked(innerKey, firstHash))Your valuenested[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:

SlotContentsNotes
00x00users mapping base slot (unused)
keccak256(abi.encodePacked(user, 0))Your valueusers[user].balance
keccak256(abi.encodePacked(user, 0)) + 1Your valueusers[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:

SlotContentsNotes
00x00Base slot of the flags mapping (used as the seed for keccak256, but not otherwise used directly)
keccak256(abi.encodePacked(user, uint256(0)))0x00...00efbe4201flags[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 N
  • fixedArray[1] → slot N + 1
  • fixedArray[2] → slot N + 2

Storage Slots:

SlotValueNotes
0your valuefixedArray[0]
1your valuefixedArray[1]
2your valuefixedArray[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:

SlotValue
0dynamicArray.length
keccak256(abi.encodePacked(uint256(0)))dynamicArray[0]
keccak256(abi.encodePacked(uint256(0))) + 1dynamicArray[1]
keccak256(abi.encodePacked(uint256(0))) + 2dynamicArray[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 for arr.
  • The array is stored as a flat sequence of 6 uint256 elements.
  • Slot for element arr[i][j] = 0 + i * 2 + j

So:

ElementSlot
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:

SlotValueNotes
00x64arr[0][0] = 100
10x65arr[0][1] = 101
20xc8arr[1][0] = 200
30xc9arr[1][1] = 201
40x12carr[2][0] = 300
50x12darr[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 array i.
  • 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):

  1. Read outer array length from slot 0.

  2. Compute outer base slot:

    outerBase = keccak256(abi.encode(uint256(0)))
    
  3. Read inner array 1 pointer from slot outerBase + 1.

  4. Read inner array 1 length from that pointer slot.

  5. The elements start at pointer slot + 1, so

    elementSlot = pointerSlot + 1 + 2  
    // 2 = index of element in inner array
    

Summary:

StepHow to Calculate
Outer array lengthSlot 0
Outer array basekeccak256(0)
Inner array i pointerSlot keccak256(0) + i
Inner array lengthAt slot pointer
Inner array element jAt slot pointer + 1 + j

Storage Slots:

SlotValueNotes
00x02arr.length = 2
keccak(0)+00xA0pointer to arr[0]
keccak(0)+10xB0pointer to arr[1]
A00x02arr[0].length = 2
A10x0aarr[0][0] = 10
A20x0barr[0][1] = 11
B00x03arr[1].length = 3
B10x14arr[1][0] = 20
B20x15arr[1][1] = 21
B30x16arr[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 and person.age fit together packed into slot N+1 (because 1 + 4 bytes < 32 bytes)

Storage Slots:

SlotValue
0person.id
1person.isRegistered + person.age (packed)

How Variable Packing Works Here

  • id is a full uint256, 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:

MemberSlot
idslot
isRegistered + ageslot + 1

So if key = 0x1234..., then:

  • people[0x1234].id → at slot keccak256(key, 0)
  • people[0x1234].isRegistered + people[0x1234].age → packed at slot keccak256(key, 0) + 1

Storage Slots:

SlotValue
0people 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:

  1. Calculate base data slot:

    base = keccak256(0)
    
  2. Each struct uses consecutive slots starting at:

    structSlot = base + (i * number_of_slots_per_struct)
    
  3. For our struct, number_of_slots_per_struct = 2 (one full slot for id, one packed slot for bool + uint32).

  4. So for element i, slots:

MemberSlot
idbase + (i * 2)
isRegistered + agebase + (i * 2) + 1

Storage Slots:

SlotValue
0people.length
keccak256(0)people[0].id
keccak256(0) + 1people[0].isRegistered + people[0].age (packed)
keccak256(0) + 2people[1].id
keccak256(0) + 3people[1].isRegistered + people[1].age (packed)

Summary Table

ScenarioSlot calculation formulaNotes
Simple struct variableStarts at declared slot N, members stored sequentiallyPacks members when possible
Struct in mappingkeccak256(abi.encodePacked(key, baseSlot)) + memberOffsetmemberOffset = 0,1,... slots
Struct in dynamic arraykeccak256(baseSlot) + i * slotsPerStruct + memberOffseti = index in array

Visual Example for StructMapping

Key (address)Slot (keccak256(key, 0))MemberOffset
0xabc...0xdeadbeef...id0
0xabc...0xdeadbeef... + 1isRegistered + 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
    
    PartMeaning
    0x68656c6c6fASCII for "hello"
    ZerosPadding to fill 32 bytes
    0x0b (last byte)5 * 2 + 1 = 11 (odd → short string flag)

Storage Slots:

SlotValueNotes
00x68656c6c6f0000000000000000...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:

SlotValueNotes
00x40pointer 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:

This post was updated on

June 14, 2025.

July 15, 2024

Exploiting Smart Contracts: Strict Equality

In this tutorial, we demonstrate how to create a strict equality attack in Solidity, including detailed setup, code examples, and execution steps, followed by essential mitigation strategies.