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