Skip to content

Conversation

yash-atreya
Copy link
Member

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

Motivation

ref #9504

stacked on #11214

Solution

This features enabled when the compiled artifacts include the storageLayout. This can be achieved by setting the extra_output = ["storageLayout"] in foundry.toml or via the cli flag --extra-output storageLayout

  • Include SlotInfo in the SlotStateDiff
  • Identify 0+n slots of static arrays and include the slot info
  • Decode the storage values

Note: This does not handle types that are non-contiguous in the storage such as dynamic arrays or mappings. This shall be addressed in a followup: #11326 and #11327

Consider the following as an example:

import {Test} from "forge-std/Test.sol";

contract SimpleStorage {
    uint256 public value; // Slot 0
    address public owner; // Slot 1
    uint256[3] public values; // Slots 2, 3, 4

    constructor() {
        owner = msg.sender;
    }

    function setValue(uint256 _value) public {
        value = _value;
    }

    function setOwner(address _owner) public {
        owner = _owner;
    }

    function setValues(uint256 a, uint256 b, uint256 c) public {
        values[0] = a;
        values[1] = b;
        values[2] = c;
    }
}

contract StateDiffStorageLayoutTest is Test {
    SimpleStorage simpleStorage;
    function setUp() public {
        simpleStorage = new SimpleStorage();
    }

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

        simpleStorage.setValue(42); // Modifies slot 0
        simpleStorage.setOwner(address(this)); // Modifies slot 1
        simpleStorage.setValues(100, 200, 300); // Modifies slots 2, 3, 4

        string memory stateDiffStr = vm.getStateDiff();
        string memory stateDiffJson = vm.getStateDiffJson();

        emit log_string("State Diff JSON:");
        emit log_string(stateDiffJson);
        emit log_string("State Diff String:");
        emit log_string(stateDiffStr);
    }
}

The above test will emit the following output:

Plaintext String
0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
contract: src/SimpleStorage.sol:SimpleStorage
- state diff:
@ 0x0000000000000000000000000000000000000000000000000000000000000000 (value, uint256): 0 → 42
@ 0x0000000000000000000000000000000000000000000000000000000000000001 (owner, address): 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 → 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496
@ 0x0000000000000000000000000000000000000000000000000000000000000002 (values[0], uint256[3]): 0 → 100
@ 0x0000000000000000000000000000000000000000000000000000000000000003 (values[1], uint256[3]): 0 → 200
@ 0x0000000000000000000000000000000000000000000000000000000000000004 (values[2], uint256[3]): 0 → 300
JSON Output
{
  "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f": {
    "label": null,
    "contract": "src/SimpleStorage.sol:SimpleStorage",
    "balanceDiff": null,
    "stateDiff": {
      "0x0000000000000000000000000000000000000000000000000000000000000000": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x000000000000000000000000000000000000000000000000000000000000002a",
        "decoded": {
          "previousValue": "0",
          "newValue": "42"
        },
        "label": "value",
        "type": "uint256",
        "offset": 0,
        "slot": "0"
      },
      "0x0000000000000000000000000000000000000000000000000000000000000001": {
        "previousValue": "0x0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496",
        "newValue": "0x0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496",
        "decoded": {
          "previousValue": "0x7fa9385be102ac3eac297483dd6233d62b3e1496",
          "newValue": "0x7fa9385be102ac3eac297483dd6233d62b3e1496"
        },
        "label": "owner",
        "type": "address",
        "offset": 0,
        "slot": "1"
      },
      "0x0000000000000000000000000000000000000000000000000000000000000002": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x0000000000000000000000000000000000000000000000000000000000000064",
        "decoded": {
          "previousValue": "0",
          "newValue": "100"
        },
        "label": "values[0]",
        "type": "uint256[3]",
        "offset": 0,
        "slot": "2"
      },
      "0x0000000000000000000000000000000000000000000000000000000000000003": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x00000000000000000000000000000000000000000000000000000000000000c8",
        "decoded": {
          "previousValue": "0",
          "newValue": "200"
        },
        "label": "values[1]",
        "type": "uint256[3]",
        "offset": 0,
        "slot": "3"
      },
      "0x0000000000000000000000000000000000000000000000000000000000000004": {
        "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "newValue": "0x000000000000000000000000000000000000000000000000000000000000012c",
        "decoded": {
          "previousValue": "0",
          "newValue": "300"
        },
        "label": "values[2]",
        "type": "uint256[3]",
        "offset": 0,
        "slot": "4"
      }
    }
  }
}

Please drop your comments on the output structure, especially the plaintext string output which looks a bit weird.

PR Checklist

  • Added Tests
  • Added Documentation
  • Breaking changes

@yash-atreya yash-atreya changed the base branch from master to yash/state-diff-storage-labels August 12, 2025 07:14
@yash-atreya yash-atreya changed the title Yash/decode label state diffs feat(cheatcodes): include slot type and decode values in state diffs Aug 12, 2025
@yash-atreya yash-atreya added A-cheatcodes Area: cheatcodes C-forge Command: forge T-likely-breaking Type: requires changes that can be breaking labels Aug 12, 2025
@yash-atreya yash-atreya self-assigned this Aug 12, 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, would be great to have more reviews - we can have more iterations in case a better format suggested. Left one nit re tests, think we could remove extra output for all the tests?

@@ -10,7 +10,7 @@ bytecode_hash = "ipfs"
cache = true
cache_path = "cache"
evm_version = "paris"
extra_output = []
extra_output = ["storageLayout"]
Copy link
Collaborator

Choose a reason for hiding this comment

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

@sakulstra
Copy link
Contributor

sakulstra commented Aug 19, 2025

Since, all of this is a breaking change, should we also go ahead and reduce the length of storage slots in the above output i.e only keep the significant bits? So 0x0000000000000000000000000000000000000000000000000000000000000000 becomes 0x00 and 0x0000000000000000000000000000000000000000000000000000000000000001 becomes 0x01?

No to strong opinion(especially as we're primarily working with the json), but I think i liked it better before.
Feels a bit weird to have different lengths on sequential and non sequential(mappings / arrays) slots

@zerosnacks
Copy link
Member

should we also go ahead and reduce the length of storage slots in the above output i.e only keep the significant bits? So 0x0000000000000000000000000000000000000000000000000000000000000000 becomes 0x00 and 0x0000000000000000000000000000000000000000000000000000000000000001 becomes 0x01?

I would prefer to keep the entire output as in 0x0000000000000000000000000000000000000000000000000000000000000001 as the mental model for the EVM are 256-bit slots. It is also nicer to read as you can scan from the back and don't have different lengths for 0x123 and 0x5678.

@yash-atreya
Copy link
Member Author

yash-atreya commented Aug 19, 2025

I would prefer to keep the entire output as in 0x0000000000000000000000000000000000000000000000000000000000000001 as the mental model for the EVM are 256-bit slots. It is also nicer to read as you can scan from the back and don't have different lengths for 0x123 and 0x5678.

@zerosnacks @grandizzy reverted the change f366667

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.

👍 needs docs fix

@grandizzy grandizzy self-requested a review August 19, 2025 12:18
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 👍 !

@yash-atreya yash-atreya merged commit a7e5d64 into master Aug 19, 2025
22 checks passed
@yash-atreya yash-atreya deleted the yash/decode-label-state-diffs branch August 19, 2025 13:02
@github-project-automation github-project-automation bot moved this from Ready For Review to Done in Foundry Aug 19, 2025
@sakulstra
Copy link
Contributor

@yash-atreya I just tried with some real world projects, and for the majority of state diff i get non decoded states & "contract": null. Even for contracts that are properly named in the trace.

@yash-atreya
Copy link
Member Author

@yash-atreya I just tried with some real world projects, and for the majority of state diff i get non decoded states & "contract": null. Even for contracts that are properly named in the trace.

hey, can you share the projects you tried, a minimal repro and state diff output would be great as well.

@sakulstra
Copy link
Contributor

sakulstra commented Aug 20, 2025

@yash-atreya created a very minimal reproduction here:

Traces:
  [65923] AaveV3Ethereum_TestVoteOnEthereum_20250530_Test::test_defaultProposalExecution()
    ├─ [0] VM::startStateDiffRecording()
    │   └─ ← [Return]
    ├─ [3685] TransparentUpgradeableProxy::fallback() [staticcall]
    │   ├─ [369] PayloadsController::getPayloadsCount() [delegatecall]
    │   │   └─ ← [Return] 296
    │   └─ ← [Return] 296
    ├─ [0] VM::load(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0x0000000000000000000000000000000000000000000000000000000000000001) [staticcall]
    │   └─ ← [Return] 0x000000000000000001280001ce52ab41c40575b072a18c9700091ccbe4a06710
    ├─ [0] VM::store(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0x0000000000000000000000000000000000000000000000000000000000000001, 0x000000000000000001290001ce52ab41c40575b072a18c9700091ccbe4a06710)
    │   └─ ← [Return]
    ├─ [0] VM::load(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed5) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ [0] VM::store(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed5, 0x0000000000000000000001000000000000000000000000000000000000000000)
    │   └─ ← [Return]
    ├─ [1088] TransparentUpgradeableProxy::fallback() [staticcall]
    │   ├─ [272] PayloadsController::EXPIRATION_DELAY() [delegatecall]
    │   │   └─ ← [Return] 3024000 [3.024e6]
    │   └─ ← [Return] 3024000 [3.024e6]
    ├─ [0] VM::load(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed6) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ [0] VM::store(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed6, 0x0000000000000000000000000000000000006867ddb700000000000000000000)
    │   └─ ← [Return]
    ├─ [1131] TransparentUpgradeableProxy::fallback() [staticcall]
    │   ├─ [315] PayloadsController::GRACE_PERIOD() [delegatecall]
    │   │   └─ ← [Return] 604800 [6.048e5]
    │   └─ ← [Return] 604800 [6.048e5]
    ├─ [0] VM::load(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed6) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000006867ddb700000000000000000000
    ├─ [0] VM::store(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed6, 0x000000000000000000093a800000000000006867ddb700000000000000000000)
    │   └─ ← [Return]
    ├─ [0] VM::store(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed7, 0x0000000000000000000000000000000000000000000000000000000000000001)
    │   └─ ← [Return]
    ├─ [0] VM::load(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0xaf7c4cdf6a6f378bb622e12c29c89d21f776af5e52e4ad7016126d0e2f8846b9) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ [0] VM::store(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0xaf7c4cdf6a6f378bb622e12c29c89d21f776af5e52e4ad7016126d0e2f8846b9, 0x0000000000000000000001015615deb798bb3e4dfa0139dfa1b3d433cc23b72f)
    │   └─ ← [Return]
    ├─ [0] VM::store(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0xaf7c4cdf6a6f378bb622e12c29c89d21f776af5e52e4ad7016126d0e2f8846bb, 0x6578656375746528290000000000000000000000000000000000000000000012)
    │   └─ ← [Return]
    ├─ [6954] TransparentUpgradeableProxy::fallback(296) [staticcall]
    │   ├─ [6008] PayloadsController::getPayloadById(296) [delegatecall]
    │   │   └─ ← [Return] Payload({ creator: 0x0000000000000000000000000000000000000000, maximumAccessLevelRequired: 0, state: 1, createdAt: 0, queuedAt: 0, executedAt: 0, cancelledAt: 0, expirationTime: 1751637431 [1.751e9], delay: 0, gracePeriod: 604800 [6.048e5], actions: [ExecutionAction({ target: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, withDelegateCall: true, accessLevel: 1, value: 0, signature: "execute()", callData: 0x })] })
    │   └─ ← [Return] Payload({ creator: 0x0000000000000000000000000000000000000000, maximumAccessLevelRequired: 0, state: 1, createdAt: 0, queuedAt: 0, executedAt: 0, cancelledAt: 0, expirationTime: 1751637431 [1.751e9], delay: 0, gracePeriod: 604800 [6.048e5], actions: [ExecutionAction({ target: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, withDelegateCall: true, accessLevel: 1, value: 0, signature: "execute()", callData: 0x })] })
    ├─ [0] VM::load(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed5) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000001000000000000000000000000000000000000000000
    ├─ [0] VM::store(TransparentUpgradeableProxy: [0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5], 0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed5, 0x006839b936000000000002000000000000000000000000000000000000000000)
    │   └─ ← [Return]
    ├─ [19921] TransparentUpgradeableProxy::fallback(296)
    │   ├─ [19105] PayloadsController::executePayload(296) [delegatecall]
    │   │   ├─ [7337] Executor::executeTransaction(AaveV3Ethereum_TestVoteOnEthereum_20250530: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 0, "execute()", 0x, true)
    │   │   │   ├─ [97] AaveV3Ethereum_TestVoteOnEthereum_20250530::execute() [delegatecall]
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ emit ExecutedAction(target: AaveV3Ethereum_TestVoteOnEthereum_20250530: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], value: 0, signature: "execute()", data: 0x, executionTime: 1748613431 [1.748e9], withDelegatecall: true, resultData: 0x)
    │   │   │   └─ ← [Return] 0x
    │   │   ├─ emit PayloadExecuted(payloadId: 296)
    │   │   └─ ← [Stop]
    │   └─ ← [Return]
    ├─ [0] VM::getStateDiffJson() [staticcall]
    │   └─ ← [Return] "{\"0xdabad81af85554e9ae636395611c58f7ec1aaec5\":{\"label\":null,\"contract\":null,\"balanceDiff\":null,\"nonceDiff\":null,\"stateDiff\":{\"0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed5\":{\"previousValue\":\"0x006839b936000000000002000000000000000000000000000000000000000000\",\"newValue\":\"0x006839b936000000000003000000000000000000000000000000000000000000\"},\"0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed6\":{\"previousValue\":\"0x000000000000000000093a800000000000006867ddb700000000000000000000\",\"newValue\":\"0x000000000000000000093a800000000000006867ddb70000000000006839b937\"}}}}"
    ├─ emit log_string(val: "{\"0xdabad81af85554e9ae636395611c58f7ec1aaec5\":{\"label\":null,\"contract\":null,\"balanceDiff\":null,\"nonceDiff\":null,\"stateDiff\":{\"0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed5\":{\"previousValue\":\"0x006839b936000000000002000000000000000000000000000000000000000000\",\"newValue\":\"0x006839b936000000000003000000000000000000000000000000000000000000\"},\"0xf3e9bdb17e809d40894019e71fc3cd1caab638876c2f7e53420abc45924bfed6\":{\"previousValue\":\"0x000000000000000000093a800000000000006867ddb700000000000000000000\",\"newValue\":\"0x000000000000000000093a800000000000006867ddb70000000000006839b937\"}}}}")
    └─ ← [Stop]

In the trace 0xdabad81af85554e9ae636395611c58f7ec1aaec5 is named TransparentUpgradeableProxy while on the stateDiff it's "contract\":null ... generally proxies are an interesting topic in that context of state decoding.

@yash-atreya
Copy link
Member Author

@sakulstra, thanks. Can you also share the repo and branch you're running this on? Would like to debug.

@sakulstra
Copy link
Contributor

@yash-atreya , lol sorry - wrote herebut forgot to put the link 😅

here

@grandizzy grandizzy moved this from Done to Completed in Foundry Aug 25, 2025
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 T-likely-breaking Type: requires changes that can be breaking
Projects
Status: Completed
Development

Successfully merging this pull request may close these issues.

4 participants