Skip to content

feat(cheatcodes): decode structs in state diff output #11331

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 59 commits into
base: master
Choose a base branch
from

Conversation

yash-atreya
Copy link
Member

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

Motivation

Ref #9504
Closes #11326

Solution

  • Implements struct decoding for state diff output
  • handle_struct_recursive 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

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

Base automatically changed from yash/decode-label-state-diffs to master August 19, 2025 13:02
@yash-atreya yash-atreya marked this pull request as ready for review August 20, 2025 14:08
Comment on lines +279 to +307
// Helper function to check if a string contains a substring
function assertContains(string memory haystack, string memory needle, string memory message) internal pure {
bytes memory haystackBytes = bytes(haystack);
bytes memory needleBytes = bytes(needle);

if (needleBytes.length > haystackBytes.length) {
revert(message);
}

bool found = false;
for (uint256 i = 0; i <= haystackBytes.length - needleBytes.length; i++) {
bool isMatch = true;
for (uint256 j = 0; j < needleBytes.length; j++) {
if (haystackBytes[i + j] != needleBytes[j]) {
isMatch = false;
break;
}
}
if (isMatch) {
found = true;
break;
}
}

if (!found) {
revert(message);
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

@zerosnacks zerosnacks self-requested a review August 22, 2025 11:02
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

feat(cheatcodes): decode structs in state diff output
6 participants