Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions slither/tools/read_storage/read_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,49 @@ def _all_struct_slots(
)
if info:
data[elem.name] = info
elem_type = elem.type
# Recurse into nested structs so their members get their own slots.
if isinstance(elem_type, UserDefinedType) and isinstance(elem_type.type, Structure):
info.elems = self._all_nested_struct_slots(elem_type.type, info.slot, info.name)

return data

def _all_nested_struct_slots(
self,
st: Structure,
base_slot: int,
name_prefix: str,
) -> Elem:
"""Recursively resolves the members of a struct nested within another struct.

Unlike `_all_struct_slots`, the nested struct does not start at a state
variable's slot, so the offsets are computed relative to `base_slot`.

Args:
st (Structure): The nested struct definition.
base_slot (int): The slot at which the nested struct begins.
name_prefix (str): The dotted name of the nested struct (e.g. `outer.middle`).
Returns:
(Elem): A mapping of each member's name to its `SlotInfo`.
"""
slot_as_bytes = int.to_bytes(base_slot, 32, byteorder="big")
data: Elem = {}
for elem in st.elems_ordered:
_, type_to, member_slot_bytes, size, offset = self._find_struct_var_slot(
st.elems_ordered, slot_as_bytes, elem.name
)
member_slot = int.from_bytes(member_slot_bytes, byteorder="big")
info = SlotInfo(
name=f"{name_prefix}.{elem.name}",
type_string=type_to,
slot=member_slot,
size=size,
offset=offset,
)
data[elem.name] = info
elem_type = elem.type
if isinstance(elem_type, UserDefinedType) and isinstance(elem_type.type, Structure):
info.elems = self._all_nested_struct_slots(elem_type.type, member_slot, info.name)

return data

Expand Down
108 changes: 108 additions & 0 deletions tests/unit/tools/read_storage/test_nested_struct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""
Tests for nested struct support in slither-read-storage.

A struct member that is itself a struct must have its own members resolved
recursively, following Solidity's storage layout rules (a nested struct occupies
consecutive slots starting at the parent member's slot).

Related to issue #2077.
"""

import os
import tempfile

from slither import Slither
from slither.tools.read_storage.read_storage import SlitherReadStorage


def test_nested_struct_members(solc_binary_path) -> None:
"""Members of a struct nested inside another struct get their own slots."""
solc_path = solc_binary_path("0.8.10")

test_content = """
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract TestNestedStruct {
struct Inner {
uint128 a; // slot base+0, offset 0
uint128 b; // slot base+0, offset 128 (packed with a)
uint256 c; // slot base+1
}

struct Middle {
uint256 x; // slot base+0
Inner inner; // slot base+1 .. base+2
uint256 y; // slot base+3
}

struct Outer {
uint256 p; // slot 0
Middle middle; // slot 1 .. 4
uint256 q; // slot 5
}

Outer private outer;
}
"""

with tempfile.TemporaryDirectory() as tmpdir:
test_file = os.path.join(tmpdir, "test_nested_struct.sol")
with open(test_file, "w") as f:
f.write(test_content)

slither = Slither(test_file, solc=solc_path)
contracts = slither.contracts

srs = SlitherReadStorage(contracts, 20)
srs.get_all_storage_variables()
srs.get_storage_layout()

assert "outer" in srs.slot_info, "Expected outer in slot_info"
outer = srs.slot_info["outer"]
assert outer.slot == 0, f"Expected outer at slot 0, got {outer.slot}"

# Top-level members
assert outer.elems["p"].slot == 0, f"Expected p at slot 0, got {outer.elems['p'].slot}"
assert outer.elems["middle"].slot == 1, (
f"Expected middle at slot 1, got {outer.elems['middle'].slot}"
)
assert outer.elems["q"].slot == 5, f"Expected q at slot 5, got {outer.elems['q'].slot}"

# The nested struct's members must be resolved recursively.
middle = outer.elems["middle"]
assert middle.elems, "Expected nested struct 'middle' members to be resolved"
assert middle.elems["x"].slot == 1, (
f"Expected middle.x at slot 1, got {middle.elems['x'].slot}"
)
assert middle.elems["inner"].slot == 2, (
f"Expected middle.inner at slot 2, got {middle.elems['inner'].slot}"
)
assert middle.elems["y"].slot == 4, (
f"Expected middle.y at slot 4, got {middle.elems['y'].slot}"
)

# The doubly-nested struct's members must also be resolved, with correct
# packing offsets relative to their own base slot.
inner = middle.elems["inner"]
assert inner.elems, "Expected doubly-nested struct 'inner' members to be resolved"
assert inner.elems["a"].slot == 2, (
f"Expected inner.a at slot 2, got {inner.elems['a'].slot}"
)
assert inner.elems["a"].offset == 0, (
f"Expected inner.a offset 0, got {inner.elems['a'].offset}"
)
assert inner.elems["b"].slot == 2, (
f"Expected inner.b at slot 2, got {inner.elems['b'].slot}"
)
assert inner.elems["b"].offset == 128, (
f"Expected inner.b offset 128, got {inner.elems['b'].offset}"
)
assert inner.elems["c"].slot == 3, (
f"Expected inner.c at slot 3, got {inner.elems['c'].slot}"
)

# Fully-qualified names should reflect the nesting path.
assert inner.elems["a"].name == "outer.middle.inner.a", (
f"Expected name 'outer.middle.inner.a', got {inner.elems['a'].name}"
)