Skip to main content

Pointer playground

A pointer is a recipe for finding data in the EVM: where bytes live, and how to compute that location from the current machine state. The examples below resolve real Solidity storage layouts against concrete state — click "Try it" to open the playground and watch each pointer select its bytes.

They build up from a single packed variable to the full short/long string layout, where the format really earns its keep.

A packed storage variable

Solidity packs small values so several share a slot. This pointer reads a 20-byte address stored at byte 12 of slot 0 — a sub-slot region, offset and length inside a single word.

Packed address (sub-slot)
Reads a 20-byte address packed into the low bytes of slot 0
{
"location": "storage",
"slot": 0,
"offset": 12,
"length": 20
}

A dynamic array element

A uint256[] at slot 5 stores its elements starting at keccak256(5), so element i lives at keccak256(5) + i. This pointer computes the slot for element 2.

Dynamic array element
Computes keccak256(5) + 2 to find element index 2
{
"define": {
"element-index": 2
},
"in": {
"name": "element",
"location": "storage",
"slot": {
"$sum": [
{
"$keccak256": [
{
"$wordsized": 5
}
]
},
"element-index"
]
}
}
}

A mapping value

A mapping(address => uint256) at slot 3 stores each value at keccak256(key, 3). This pointer reads the key from the stack, hashes it with the slot, and resolves the value's location.

Mapping value
Computes keccak256(key, 3) — the standard Solidity mapping slot
{
"group": [
{
"name": "key-region",
"location": "stack",
"slot": 0
},
{
"name": "value",
"location": "storage",
"slot": {
"$keccak256": [
{
"$read": "key-region"
},
{
"$wordsized": 3
}
]
}
}
]
}

A Solidity storage string

Strings are the real test. Solidity packs a short string (31 bytes or fewer) directly into its slot, with 2 × length in the slot's last byte. A long string instead stores 2 × length + 1 in the slot and puts the data at keccak256(slot), spanning as many following slots as it needs.

One pointer handles both: it reads the last byte, and the parity of that flag selects the short branch or the long branch. It's the same pointer in both examples below — only the state changes.

Short string

Slot 0 packs the bytes of "Hello" with 0x0a (that's 2 × 5) in its last byte. The flag is even, so the pointer reads the string straight out of the slot.

Short string (in-slot)
Resolves "Hello" — a 5-byte string packed into slot 0
{
"define": {
"slot": 0
},
"in": {
"group": [
{
"name": "length-flag",
"location": "storage",
"slot": "slot",
"offset": 31,
"length": 1
},
{
"if": {
"$remainder": [
{
"$sum": [
{
"$read": "length-flag"
},
1
]
},
2
]
},
"then": {
"define": {
"string-length": {
"$quotient": [
{
"$read": "length-flag"
},
2
]
}
},
"in": {
"name": "string",
"location": "storage",
"slot": "slot",
"offset": 0,
"length": "string-length"
}
},
"else": {
"group": [
{
"name": "long-string-length-data",
"location": "storage",
"slot": "slot",
"offset": 0,
"length": 32
},
{
"define": {
"string-length": {
"$quotient": [
{
"$difference": [
{
"$read": "long-string-length-data"
},
1
]
},
2
]
},
"start-slot": {
"$keccak256": [
{
"$wordsized": "slot"
}
]
},
"total-slots": {
"$quotient": [
{
"$sum": [
"string-length",
{
"$difference": [
32,
1
]
}
]
},
32
]
}
},
"in": {
"list": {
"count": "total-slots",
"each": "i",
"is": {
"define": {
"current-slot": {
"$sum": [
"start-slot",
"i"
]
},
"previous-length": {
"$product": [
"i",
32
]
}
},
"in": {
"if": {
"$difference": [
"string-length",
{
"$sum": [
"previous-length",
32
]
}
]
},
"then": {
"name": "string",
"location": "storage",
"slot": "current-slot"
},
"else": {
"name": "string",
"location": "storage",
"slot": "current-slot",
"offset": 0,
"length": {
"$difference": [
"string-length",
"previous-length"
]
}
}
}
}
}
}
}
]
}
}
]
}
}

Long string

Here the flag is 0x51 (2 × 40 + 1), so the pointer takes the long branch: it computes the length as (0x51 − 1) / 2 = 40, finds the data at keccak256(0), and reads 40 bytes across that slot and the next.

Long string (keccak256 data)
Resolves a 40-byte string across keccak256(0) and the following slot
{
"define": {
"slot": 0
},
"in": {
"group": [
{
"name": "length-flag",
"location": "storage",
"slot": "slot",
"offset": 31,
"length": 1
},
{
"if": {
"$remainder": [
{
"$sum": [
{
"$read": "length-flag"
},
1
]
},
2
]
},
"then": {
"define": {
"string-length": {
"$quotient": [
{
"$read": "length-flag"
},
2
]
}
},
"in": {
"name": "string",
"location": "storage",
"slot": "slot",
"offset": 0,
"length": "string-length"
}
},
"else": {
"group": [
{
"name": "long-string-length-data",
"location": "storage",
"slot": "slot",
"offset": 0,
"length": 32
},
{
"define": {
"string-length": {
"$quotient": [
{
"$difference": [
{
"$read": "long-string-length-data"
},
1
]
},
2
]
},
"start-slot": {
"$keccak256": [
{
"$wordsized": "slot"
}
]
},
"total-slots": {
"$quotient": [
{
"$sum": [
"string-length",
{
"$difference": [
32,
1
]
}
]
},
32
]
}
},
"in": {
"list": {
"count": "total-slots",
"each": "i",
"is": {
"define": {
"current-slot": {
"$sum": [
"start-slot",
"i"
]
},
"previous-length": {
"$product": [
"i",
32
]
}
},
"in": {
"if": {
"$difference": [
"string-length",
{
"$sum": [
"previous-length",
32
]
}
]
},
"then": {
"name": "string",
"location": "storage",
"slot": "current-slot"
},
"else": {
"name": "string",
"location": "storage",
"slot": "current-slot",
"offset": 0,
"length": {
"$difference": [
"string-length",
"previous-length"
]
}
}
}
}
}
}
}
]
}
}
]
}
}

Learn more