Crypto Training

Solidity Storage Layout: Slots, Packing, and the Bugs It Enables

Storage is where Solidity hides complexity. If you can reason about slots, you can reason about upgrade safety, state corruption, and exploit primitives.

Crypto Training2026-01-145 min read

Solidity storage layout illustration

Most Solidity bugs are obvious when you stay in the source layer: missing access control, unchecked external calls, etc.

The nastier ones show up when you cross a boundary:

  • an upgrade changes layout
  • inline assembly writes the wrong slot
  • a dynamic structure grows until it becomes un-callable
  • a proxy stores admin state where the implementation expects user balances

If you can map “variable” -> “slot and offset”, you can explain whole classes of incidents.

The mental model that survives the optimizer#

The EVM gives you a simple primitive:

  • key: uint256 slot
  • value: bytes32

SLOAD(slot) reads one bytes32.

SSTORE(slot, value) writes one bytes32.

Everything else is convention.

Cheat sheet#

Solidity typeWhere the data livesWhat attackers exploit
uint256, addressdirect slotupgrades that reorder variables
small values (bool, uint8, uint16)packed into a slotassembly that overwrites neighbors
fixed array T[n]sequential slotsincorrect assumptions about packing
dynamic array T[]slot stores length; elements at keccak256(slot)+iunbounded growth DoS; wrong slot math
mapping(K=>V)entry at keccak256(abi.encode(key, slot))iteration via index arrays (DoS); collisions
structsequential slots (with packing), nested dynamics hash off basebase slot drift across upgrades

If you can explain those six rows without looking them up, you're already ahead of most “Solidity devs”.

Packing: free gas, expensive mistakes#

Solidity packs multiple small variables into one slot when it can.

SOLIDITY
contract Packed {
    // slot 0: a (1 byte) + b (1 byte) + padding
    uint8 a;
    uint8 b;

    // slot 1
    uint256 c;
}

Packing becomes dangerous when you touch storage manually.

A classic footgun:

SOLIDITY
// WRONG if slot contains packed neighbors.
assembly {
  sstore(0, newValue)
}

That overwrites all 32 bytes, not just a.

If you must do this, you need masks.

Masking (the correct idea)#

If a is stored in the lowest byte:

  • read the slot
  • clear the lowest byte
  • OR in the new value
SOLIDITY
assembly {
  let slot := sload(0)
  // clear lowest byte
  slot := and(slot, not(0xff))
  // set new a
  slot := or(slot, and(newA, 0xff))
  sstore(0, slot)
}

This is the kind of code that passes unit tests and then causes an incident six months later when someone adds another packed field.

Dynamic arrays: length is not the data#

For a dynamic array T[] arr stored at slot p:

  • storage slot p stores the length
  • element arr[i] is stored at keccak256(p) + i

Two security consequences:

  1. Any loop over arr is a potential liveness bug if arr can grow without bounds.
  2. If you do slot math incorrectly in assembly, you write to some other random place.

If you see “we store all user addresses in an array so we can iterate later”, you should immediately ask: what is the worst-case length and what is the bounded-work escape hatch?

Mappings: hashed addressing, no native iteration#

For mapping(K => V) m at slot p:

  • m[key] lives at keccak256(abi.encode(key, p))

Mappings are great because they don’t require contiguous slots, but they come with two realities:

  1. You can’t iterate without a separate index.
  2. That index becomes a DoS surface.

I treat “mapping + parallel array” as a red flag until proven safe.

Structs: composition amplifies risk#

Struct layout is “just variables in order”, but nested dynamics (mapping/arrays inside structs) are where people lose track.

If you put a mapping inside a struct, its base slot depends on the struct’s location.

So an innocent refactor like “add a field to the top of the struct” can shift the base and effectively orphan state.

This is why upgradeable projects often keep structs stable and append-only.

Upgradeability: where layout becomes existential#

Upgrades are the most common reason storage layout becomes security-critical.

Two rules keep you alive:

  • append-only layouts
  • never change the meaning of an existing slot

A realistic failure mode:

v1 stores bool paused; address owner; packed. v2 inserts uint256 feeBps; above them. packing shifts. paused reads as true and owner becomes garbage.

Nothing reverts. The system just behaves wrong.

Proxy collisions (why EIP-1967 exists)#

In proxy setups, the proxy stores state and delegates execution to the implementation.

If the proxy uses slot 0 for admin state and the implementation uses slot 0 for owner, you collide.

EIP-1967 avoids this by placing proxy admin/implementation pointers at special “unguessable” slots derived from hashes.

The takeaway for auditors: if you see a custom proxy, verify its storage slots are well-separated from implementation state.

How I audit storage layout in practice#

I do this by hand before I trust tooling:

  1. Write out state variables in order.
  2. Mark packing groups (anything < 32 bytes is suspicious).
  3. For each dynamic type, write down the base slot p.
  4. For mappings/arrays inside structs, note the struct base.

Then for upgrades:

  • diff v1 vs v2
  • verify every existing slot is preserved

If you want to go deeper, compute slots and inspect them.

Slot computation snippets#

Compute a mapping slot:

SOLIDITY
function slotOf(address user, uint256 p) internal pure returns (bytes32) {
    return keccak256(abi.encode(user, p));
}

Compute a dynamic array element slot:

SOLIDITY
function elemSlot(uint256 p, uint256 i) internal pure returns (bytes32) {
    return bytes32(uint256(keccak256(abi.encode(p))) + i);
}

Those two functions are enough to debug half of “mysterious storage corruption” incidents.

Mini-lab (do this once; it pays forever)#

Open Remix and deploy a toy contract that has:

  • packed variables
  • a dynamic array
  • a mapping

Then:

  • set values
  • read raw storage slots
  • modify source order and redeploy

You’ll build the intuition that makes upgrade reviews and incident response dramatically easier.

Further reading#