Skip to content

Conversation

yash-atreya
Copy link
Member

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

Motivation

Closes #11327

Solution

  • Record MappingSlots entries in the Cheatcodes inspector when recording state diffs.
  • Lookup the storage slot in the MappingSlot entries.
  • Retrieve it's original key and parent
  • Decode the key and value type
  • Handles nested mappings as well

Example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "forge-std/Test.sol";
import "forge-std/Vm.sol";

contract MappingStorage {
    mapping(address => uint256) public balances; // Slot 0
    mapping(uint256 => address) public owners; // Slot 1
    mapping(bytes32 => bool) public flags; // Slot 2
    mapping(address => mapping(address => uint256)) public allowances; // Slot 3

    function setBalance(address account, uint256 amount) public {
        balances[account] = amount;
    }

    function setOwner(uint256 tokenId, address owner) public {
        owners[tokenId] = owner;
    }

    function setFlag(bytes32 key, bool value) public {
        flags[key] = value;
    }

    function setAllowance(address owner, address spender, uint256 amount) public {
        allowances[owner][spender] = amount;
    }
}

contract StateDiffMappingsTest is Test {
    MappingStorage public mappingStorage;

    function setUp() public {
        mappingStorage = new MappingStorage();
    }

    function testSimpleMappingStateDiff() public {
        vm.startStateDiffRecording();
        address testAccount = address(0x1234);
        mappingStorage.setBalance(testAccount, 1000 ether);
       
        string memory stateDiffText = vm.getStateDiff();
        emit log_string("State diff text format:");
        emit log_string(stateDiffText);

        string memory json = vm.getStateDiffJson();
        emit log_string("State diff JSON (simple mapping):");
        emit log_string(json);
    }

    function testMappingWithDifferentKeyTypes() public {
        // Start recording state diffs
        vm.startStateDiffRecording();
        mappingStorage.setOwner(12345, address(0x7777));
        bytes32 flagKey = keccak256("test_flag");
        mappingStorage.setFlag(flagKey, true);

        string memory stateDiffText = vm.getStateDiff();
        emit log_string("State diff text format:");
        emit log_string(stateDiffText);

        string memory json = vm.getStateDiffJson();
        emit log_string("State diff JSON:");
        emit log_string(json);
    }

    function testNestedMappingStateDiff() public {
        vm.startStateDiffRecording();

        // Test case 1: owner1 -> spender1
        address owner1 = address(0x1111);
        address spender1 = address(0x2222);
        mappingStorage.setAllowance(owner1, spender1, 500 ether);

        // Test case 2: same owner (owner1) -> different spender (spender2)
        address spender2 = address(0x3333);
        mappingStorage.setAllowance(owner1, spender2, 750 ether);

        // Test case 3: different owner (owner2) -> different spender (spender3)
        address owner2 = address(0x4444);
        address spender3 = address(0x5555);
        mappingStorage.setAllowance(owner2, spender3, 1000 ether);

        string memory stateDiffText = vm.getStateDiff();
        emit log_string("State diff text format (nested mappings):");
        emit log_string(stateDiffText);
        string memory json = vm.getStateDiffJson();
        emit log_string("State diff JSON (nested mapping - multiple entries):");
        emit log_string(json);
    }
}
testSimpleMappingStateDiff output
{
  "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f": {
    "label": null,
    "contract": "test/StateDiffMappings.t.sol:MappingStorage",
    "balanceDiff": null,
    "nonceDiff": null,
    "stateDiff": {
      "0xf3f00ab703b87ba8b65e1fbf147cda608927ab8fd3d225c2a15738b8edfbecc9": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x00000000000000000000000000000000000000000000003635c9adc5dea00000",
        "decoded": {
          "previousValue": "0",
          "newValue": "1000000000000000000000"
        },
        "label": "balances[0x0000000000000000000000000000000000001234]",
        "type": "mapping(address => uint256)",
        "offset": 0,
        "slot": "110336139452806081405069343840860127994419505884627922841905626668596794748105",
        "key": "0x0000000000000000000000000000000000001234"
      }
    }
  }
}
State diff text format:
  0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
contract: test/StateDiffMappings.t.sol:MappingStorage
- state diff:
@ 0xf3f00ab703b87ba8b65e1fbf147cda608927ab8fd3d225c2a15738b8edfbecc9 (balances[0x0000000000000000000000000000000000001234], uint256): 0 → 1000000000000000000000
testMappingWithDifferentKeyTypes output
{
  "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f": {
    "label": null,
    "contract": "test/StateDiffMappings.t.sol:MappingStorage",
    "balanceDiff": null,
    "nonceDiff": null,
    "stateDiff": {
      "0x24689f9b6ba9bad3c49d2b1293bf33fa38d0c418c093b2b4bc23f5d18e11355e": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x0000000000000000000000000000000000000000000000000000000000007777",
        "decoded": {
          "previousValue": "0x0000000000000000000000000000000000000000",
          "newValue": "0x0000000000000000000000000000000000007777"
        },
        "label": "owners[12345]",
        "type": "mapping(uint256 => address)",
        "offset": 0,
        "slot": "16468116211533653055555801021243322305305084552073344281793577370590318245214",
        "key": "12345"
      },
      "0x832960419c6c108ebf65aae23152d3ea265ed2efafa2a2b2c3c9157d6debc215": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001",
        "decoded": {
          "previousValue": "false",
          "newValue": "true"
        },
        "label": "flags[0x56013d42fae894fbf754c7df67af9e5aa2aeef2f974124431633eadec754520f]",
        "type": "mapping(bytes32 => bool)",
        "offset": 0,
        "slot": "59326088230582808623726353015250294500802678803454843012846072954368025149973",
        "key": "0x56013d42fae894fbf754c7df67af9e5aa2aeef2f974124431633eadec754520f"
      }
    }
  }
}
State diff text format (different key types):
  0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
contract: test/StateDiffMappings.t.sol:MappingStorage
- state diff:
@ 0x24689f9b6ba9bad3c49d2b1293bf33fa38d0c418c093b2b4bc23f5d18e11355e (owners[12345], address): 0x0000000000000000000000000000000000000000 → 0x0000000000000000000000000000000000007777
@ 0x832960419c6c108ebf65aae23152d3ea265ed2efafa2a2b2c3c9157d6debc215 (flags[0x56013d42fae894fbf754c7df67af9e5aa2aeef2f974124431633eadec754520f], bool): false → true
testNestedMappingStateDiff output
{
  "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f": {
    "label": null,
    "contract": "test/StateDiffMappings.t.sol:MappingStorage",
    "balanceDiff": null,
    "nonceDiff": null,
    "stateDiff": {
      "0x1edec543ec7a45eacce61a199b6d7353db54444474bcd1c65d022cdeaae0e653": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x000000000000000000000000000000000000000000000028a857425466f80000",
        "decoded": {
          "previousValue": "0",
          "newValue": "750000000000000000000"
        },
        "label": "allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000003333]",
        "type": "mapping(address => mapping(address => uint256))",
        "offset": 0,
        "slot": "13962986981129538487894965370238095533706618037379660143525103853800567924307",
        "keys": [
          "0x0000000000000000000000000000000000001111",
          "0x0000000000000000000000000000000000003333"
        ]
      },
      "0xc1f75be308f1c0bda55c579d7c2cbbcd837b57736a1e62675a32787f3768c573": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x00000000000000000000000000000000000000000000003635c9adc5dea00000",
        "decoded": {
          "previousValue": "0",
          "newValue": "1000000000000000000000"
        },
        "label": "allowances[0x0000000000000000000000000000000000004444][0x0000000000000000000000000000000000005555]",
        "type": "mapping(address => mapping(address => uint256))",
        "offset": 0,
        "slot": "87733425181338074975416946081399321992714587419438947263525218859528802059635",
        "keys": [
          "0x0000000000000000000000000000000000004444",
          "0x0000000000000000000000000000000000005555"
        ]
      },
      "0xfda545752d61949df603d43aa00046f1e88c8a78031b4a81f029f6e2a2cd8c19": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x00000000000000000000000000000000000000000000001b1ae4d6e2ef500000",
        "decoded": {
          "previousValue": "0",
          "newValue": "500000000000000000000"
        },
        "label": "allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000002222]",
        "type": "mapping(address => mapping(address => uint256))",
        "offset": 0,
        "slot": "114727159836845713707326350220192871457453743652886522949556982491062989589529",
        "keys": [
          "0x0000000000000000000000000000000000001111",
          "0x0000000000000000000000000000000000002222"
        ]
      }
    }
  }
}
State diff text format (nested mappings):
  0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
contract: test/StateDiffMappings.t.sol:MappingStorage
- state diff:
@ 0x1edec543ec7a45eacce61a199b6d7353db54444474bcd1c65d022cdeaae0e653 (allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000003333], uint256): 0 → 750000000000000000000
@ 0xc1f75be308f1c0bda55c579d7c2cbbcd837b57736a1e62675a32787f3768c573 (allowances[0x0000000000000000000000000000000000004444][0x0000000000000000000000000000000000005555], uint256): 0 → 1000000000000000000000
@ 0xfda545752d61949df603d43aa00046f1e88c8a78031b4a81f029f6e2a2cd8c19 (allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000002222], uint256): 0 → 500000000000000000000

PR Checklist

  • Added Tests
  • Added Documentation
  • Breaking changes

@yash-atreya yash-atreya added A-cheatcodes Area: cheatcodes C-forge Command: forge labels Aug 21, 2025
@yash-atreya yash-atreya marked this pull request as ready for review August 22, 2025 10:37
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 👍 !

* 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 merged commit c9d1cef into yash/decode-structs-in-state-diffs Aug 25, 2025
22 checks passed
@yash-atreya yash-atreya deleted the yash/decode-mappings-state-diffs branch August 25, 2025 12:00
@github-project-automation github-project-automation bot moved this to Done in Foundry Aug 25, 2025
yash-atreya added a commit that referenced this pull request Aug 28, 2025
…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` (#11343)

* add @onbjerg

* add @0xrusowsky

* resolve conflicts

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

* chore(deps): bump all dependencies (#11349)

* chore: use get_or_calculate_hash better (#11350)

* resolve more conflicts

* fix(lint): 'unwrapped-modifier-logic' incorrectly marked with `Severity::Gas` (#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 (#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 (#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>
yash-atreya added a commit that referenced this pull request Sep 1, 2025
…s and sampling them (#11450)

* 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` (#11343)

* add @onbjerg

* add @0xrusowsky

* resolve conflicts

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

* chore(deps): bump all dependencies (#11349)

* chore: use get_or_calculate_hash better (#11350)

* resolve more conflicts

* fix(lint): 'unwrapped-modifier-logic' incorrectly marked with `Severity::Gas` (#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 (#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 (#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

* identify slot types using `SlotIdentifier`

* clippy

* feat(`invariants`): record mapping keys and slots to identify their types for sampling

* fix

* only insert if value decodes

* tracing::info logs

* remove logs

* nit

* rm

---------

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
A-cheatcodes Area: cheatcodes C-forge Command: forge
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

2 participants