Skip to content

Conversation

yash-atreya
Copy link
Member

@yash-atreya yash-atreya commented Aug 18, 2025

Motivation

Ref #9504
Closes #11326 + Closes #11327

Solution

  1. Decode Structs:
  • Implements struct decoding for state diff output
  • handle_struct processes both single-slot and multi-slot structs, recursing through nested
    structures.
  • Displays all members with their decoded values; for multi-slot structs, shows the specific member at
    the accessed slot
  1. Decode Mappings: feat(cheatcodes): decode mappings in state diffs #11381

  2. Refactor PR to extract slot identification and decoding logic to crates/common: refactor: moves state diff decoding to common #11413 which will aid in resolving feat(invariants): improve storage decoding for subsequent fuzz runs #11334 and Cannot find the storage slot for a public string variable #3869

Example:

contract DiffTest {
    // slot 0
    struct TestStruct {
        uint128 a;
        uint128 b;
    }

    // Multi-slot struct (spans 3 slots)
    struct MultiSlotStruct {
        uint256 value1; // slot 1
        address addr; // slot 2 (takes 20 bytes, but uses full slot)
        uint256 value2; // slot 3
    }

    // Nested struct with MultiSlotStruct as inner
    struct NestedStruct {
        MultiSlotStruct inner; // slots 4-6 (spans 3 slots)
        uint256 value; // slot 7
        address owner; // slot 8
    }

    TestStruct internal testStruct;
    MultiSlotStruct internal multiSlotStruct;
    NestedStruct internal nestedStruct;

    function setStruct(uint128 a, uint128 b) public {
        testStruct.a = a;
        testStruct.b = b;
    }

    function setMultiSlotStruct(uint256 v1, address a, uint256 v2) public {
        multiSlotStruct.value1 = v1;
        multiSlotStruct.addr = a;
        multiSlotStruct.value2 = v2;
    }

    function setNestedStruct(uint256 v1, address a, uint256 v2, uint256 v, address o) public {
        nestedStruct.inner.value1 = v1;
        nestedStruct.inner.addr = a;
        nestedStruct.inner.value2 = v2;
        nestedStruct.value = v;
        nestedStruct.owner = o;
    }
}

contract StateDiffStructTest is DSTest {
    Vm constant vm = Vm(HEVM_ADDRESS);
    DiffTest internal test;

    function setUp() public {
        test = new DiffTest();
    }

    function testDiffs() public {
        // Start recording state diffs
        vm.startStateDiffRecording();

        // Set some values to trigger state changes
        test.setStruct(1, 2);
        // Get the state diff as JSON
        string memory td = vm.getStateDiffJson();

        // Debug: log the JSON for inspection
        emit log_string("TestStruct Diff:");
        emit log_string(td);

        test.setMultiSlotStruct(123456789, address(0xdEaDbEeF), 987654321);

        // Get the state diff as JSON
        string memory md = vm.getStateDiffJson();

        // Debug: log the JSON for inspection
        emit log_string("MultiSlotStruct Diff:");
        emit log_string(md);

        test.setNestedStruct(
            111111111,
            address(0xCAFE),
            222222222,
            333333333,
            address(0xBEEF)
        );

        // Get the state diff as JSON
        string memory nd = vm.getStateDiffJson();

        // Debug: log the JSON for inspection
        emit log_string("NestedStruct Diff:");
        emit log_string(nd);
    }
}
TestStruct Diff
{
  "0xce71065d4017f316ec606fe4422e11eb2c47c246": {
    "label": null,
    "contract": "default/cheats/StateDiffStructTest.t.sol:DiffTest",
    "balanceDiff": null,
    "nonceDiff": null,
    "stateDiff": {
      "0x0000000000000000000000000000000000000000000000000000000000000000": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x0000000000000000000000000000000200000000000000000000000000000001",
        "label": "testStruct",
        "type": "struct DiffTest.TestStruct",
        "offset": 0,
        "slot": "0",
        "members": [
          {
            "label": "a",
            "type": "uint128",
            "offset": 0,
            "slot": "0",
            "decoded": {
              "previousValue": "0",
              "newValue": "1"
            }
          },
          {
            "label": "b",
            "type": "uint128",
            "offset": 16,
            "slot": "0",
            "decoded": {
              "previousValue": "0",
              "newValue": "2"
            }
          }
        ]
      }
    }
  }
}
MultiSlotStruct Diff
{
  "0xce71065d4017f316ec606fe4422e11eb2c47c246": {
    "label": null,
    "contract": "default/cheats/StateDiffStructTest.t.sol:DiffTest",
    "balanceDiff": null,
    "nonceDiff": null,
    "stateDiff": {
      "0x0000000000000000000000000000000000000000000000000000000000000000": { .. },
      "0x0000000000000000000000000000000000000000000000000000000000000001": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x00000000000000000000000000000000000000000000000000000000075bcd15",
        "decoded": {
          "previousValue": "0",
          "newValue": "123456789"
        },
        "label": "multiSlotStruct.value1",
        "type": "uint256",
        "offset": 0,
        "slot": "1"
      },
      "0x0000000000000000000000000000000000000000000000000000000000000002": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x00000000000000000000000000000000000000000000000000000000deadbeef",
        "decoded": {
          "previousValue": "0x0000000000000000000000000000000000000000",
          "newValue": "0x00000000000000000000000000000000DeaDBeef"
        },
        "label": "multiSlotStruct.addr",
        "type": "address",
        "offset": 0,
        "slot": "2"
      },
      "0x0000000000000000000000000000000000000000000000000000000000000003": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x000000000000000000000000000000000000000000000000000000003ade68b1",
        "decoded": {
          "previousValue": "0",
          "newValue": "987654321"
        },
        "label": "multiSlotStruct.value2",
        "type": "uint256",
        "offset": 0,
        "slot": "3"
      }
    }
  }
}
NestedStruct Diff
{
  "0xce71065d4017f316ec606fe4422e11eb2c47c246": {
    "label": null,
    "contract": "default/cheats/StateDiffStructTest.t.sol:DiffTest",
    "balanceDiff": null,
    "nonceDiff": null,
    "stateDiff": {
      "0x0000000000000000000000000000000000000000000000000000000000000000": { .. },
      "0x0000000000000000000000000000000000000000000000000000000000000001": { .. },
      "0x0000000000000000000000000000000000000000000000000000000000000002": { .. },
      "0x0000000000000000000000000000000000000000000000000000000000000003": { .. },
      "0x0000000000000000000000000000000000000000000000000000000000000004": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x00000000000000000000000000000000000000000000000000000000069f6bc7",
        "decoded": {
          "previousValue": "0",
          "newValue": "111111111"
        },
        "label": "nestedStruct.inner.value1",
        "type": "uint256",
        "offset": 0,
        "slot": "4"
      },
      "0x0000000000000000000000000000000000000000000000000000000000000005": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x000000000000000000000000000000000000000000000000000000000000cafe",
        "decoded": {
          "previousValue": "0x0000000000000000000000000000000000000000",
          "newValue": "0x000000000000000000000000000000000000cafE"
        },
        "label": "nestedStruct.inner.addr",
        "type": "address",
        "offset": 0,
        "slot": "5"
      },
      "0x0000000000000000000000000000000000000000000000000000000000000006": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x000000000000000000000000000000000000000000000000000000000d3ed78e",
        "decoded": {
          "previousValue": "0",
          "newValue": "222222222"
        },
        "label": "nestedStruct.inner.value2",
        "type": "uint256",
        "offset": 0,
        "slot": "6"
      },
      "0x0000000000000000000000000000000000000000000000000000000000000007": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x0000000000000000000000000000000000000000000000000000000013de4355",
        "decoded": {
          "previousValue": "0",
          "newValue": "333333333"
        },
        "label": "nestedStruct.value",
        "type": "uint256",
        "offset": 0,
        "slot": "7"
      },
      "0x0000000000000000000000000000000000000000000000000000000000000008": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x000000000000000000000000000000000000000000000000000000000000beef",
        "decoded": {
          "previousValue": "0x0000000000000000000000000000000000000000",
          "newValue": "0x000000000000000000000000000000000000bEEF"
        },
        "label": "nestedStruct.owner",
        "type": "address",
        "offset": 0,
        "slot": "8"
      }
    }
  }
}

PR Checklist

  • Added Tests
  • Added Documentation
  • Breaking changes

* feat(cheatcodes): decode mappings in state diffs

* feat: decode nested mappings

* assert vm.getStateDiff output

* feat: add `keys` fields to `SlotInfo` in case of mappings

* remove wrapper

* refactor: moves state diff decoding to common (#11413)

* refactor: storage decoder

* cleanup

* dedup MappingSlots by moving it to common

* move decoding logic into SlotInfo

* rename to SlotIndentifier

* docs

* fix: delegate identification according to encoding types

* clippy + fmt

* docs fix

* fix

* merge match arms

* merge ifs

* recurse handle_struct
@yash-atreya yash-atreya changed the title feat(cheatcodes): decode structs in state diff output feat(cheatcodes): decode structs and mappings in state diff output Aug 25, 2025
@zerosnacks zerosnacks self-requested a review August 25, 2025 13:48
Copy link
Member

@zerosnacks zerosnacks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Overall lgtm! Few small nits

I think users will really enjoy this feature

}

/// Formats a [`DynSolValue`] as a raw string without type information and only the value itself.
pub fn format_value(value: &DynSolValue) -> String {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is common::fmt

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reused in 8be596d

grandizzy and others added 2 commits August 26, 2025 10:56
Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
grandizzy
grandizzy previously approved these changes Aug 26, 2025
Copy link
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm! pending @zerosnacks @DaniPopes , thank you

@DaniPopes
Copy link
Member

lgtm, nitz, merge when done

zerosnacks
zerosnacks previously approved these changes Aug 28, 2025
Copy link
Member

@zerosnacks zerosnacks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm 👍 , pending Dani's nits

@yash-atreya yash-atreya dismissed stale reviews from zerosnacks and grandizzy via fbcd265 August 28, 2025 10:54
@yash-atreya yash-atreya enabled auto-merge (squash) August 28, 2025 10:54
@yash-atreya yash-atreya merged commit 2dad626 into master Aug 28, 2025
22 checks passed
@yash-atreya yash-atreya deleted the yash/decode-structs-in-state-diffs branch August 28, 2025 11:12
@github-project-automation github-project-automation bot moved this to Done in Foundry Aug 28, 2025
MerkleBoy pushed a commit to MerkleBoy/foundry that referenced this pull request Sep 17, 2025
…oundry-rs#11331)

* feat(`forge`): sample typed storage values

* arc it

* nit

* clippy

* nit

* strip file prefixes

* fmt

* don't add adjacent values to sample

* feat(cheatcodes): add contract identifier to AccountStateDiffs

* forge fmt

* doc nits

* fix tests

* feat(`cheatcodes`): include `SlotInfo` in SlotStateDiff

* cleanup + identify slots of static arrays

* nits

* nit

* nits

* test + nits

* docs

* handle 2d arrays

* use DynSolType

* feat: decode storage values

* doc nit

* skip decoded serialization if none

* nit

* fmt

* fix

* fix

* fix

* feat(cheatcodes): decode structs in state diff output

* fix

* while decode

* fix: show only decoded in plaintext / display output + test

* feat: format slots to only significant bits in vm.getStateDiff output

* encode_prefixed

* nit

* chore: add @onbjerg to `CODEOWNERS` (foundry-rs#11343)

* add @onbjerg

* add @0xrusowsky

* resolve conflicts

* fix: disable tx gas limit cap (foundry-rs#11347)

* chore(deps): bump all dependencies (foundry-rs#11349)

* chore: use get_or_calculate_hash better (foundry-rs#11350)

* resolve more conflicts

* fix(lint): 'unwrapped-modifier-logic' incorrectly marked with `Severity::Gas` (foundry-rs#11358)

fix(lint): 'unwrapped-modifier-logic' incorrectly marked with Severity::Gas

* feat: identify and decode nested structs

* cleanup

* decode structs and members recursively

* cleanup

* doc fix

* feat(cheatcodes): decode mappings in state diffs (foundry-rs#11381)

* feat(cheatcodes): decode mappings in state diffs

* feat: decode nested mappings

* assert vm.getStateDiff output

* feat: add `keys` fields to `SlotInfo` in case of mappings

* remove wrapper

* refactor: moves state diff decoding to common (foundry-rs#11413)

* refactor: storage decoder

* cleanup

* dedup MappingSlots by moving it to common

* move decoding logic into SlotInfo

* rename to SlotIndentifier

* docs

* fix: delegate identification according to encoding types

* clippy + fmt

* docs fix

* fix

* merge match arms

* merge ifs

* recurse handle_struct

* dedup assertContains test util

* fix

* Update crates/common/src/slot_identifier.rs

Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>

* Review changes: simplify get or insert, use common fmt

* alloy-dyn-abi.workspace

* nits

---------

Co-authored-by: Yash Atreya <yash@Yashs-Laptop.local>
Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
Co-authored-by: srdtrk <59252793+srdtrk@users.noreply.github.com>
Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com>
Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com>
Co-authored-by: grandizzy <grandizzy.the.egg@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

feat(cheatcodes): identify mapping slots and decode its values in state diffs feat(cheatcodes): decode structs in state diff output
7 participants