Skip to content

Conversation

yash-atreya
Copy link
Member

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

Motivation

Closes #8116

Solution

  • Adds an optional StorageLayout to ContractData and TargetedContracts
  • Upon insertion of state_changeset, the StorageLayout is looked up to decide whether to insert it as a typed sample value OR raw value.
  • This requires enabling storageLayout using extra_output = ["storageLayout"] in foundry.toml OR use the cli flag --extra-output storageLayout
  • This is especially useful in cases where the function call does not return a value OR emit an event but storage changes are made. We are now able to decode these storage changes and add them to the subsequent fuzz inputs.

PR Checklist

  • Added Tests
  • Added Documentation
  • Breaking changes

@yash-atreya yash-atreya changed the title feat(forge): sample typed storage values feat(invariants): sample typed storage values Aug 4, 2025
@grandizzy
Copy link
Collaborator

grandizzy commented Aug 5, 2025

🚀 this is huge improvement, in same line maybe you could look after into adding same for state diff decoding #9504 should be quite similar CC @sakulstra

@yash-atreya
Copy link
Member Author

🚀 this is huge improvement, in same line maybe you could look after into adding same for state diff decoding #9504 should be quite similar CC @sakulstra

Yeah, I'll look into that next

@yash-atreya yash-atreya marked this pull request as ready for review August 5, 2025 15:02
@grandizzy grandizzy self-assigned this Aug 5, 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.

looks good so far, spotted one issue with matching storage layout based on contract identifier. Unfortunately this doesn't catch ds chief bug below because it cannot handle mappings (left a comment re this too, maybe we could think at a way for complex types?). Also note that for this to be taken into account during invariant campaigns one should set extra output in project configs

extra_output = ["storageLayout"]

source contract src/SimpleDSChief.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract SimpleDSChief {
    mapping(bytes32 => address) public slates;
    mapping(address => bytes32) public votes;
    mapping(address => uint256) public approvals;
    mapping(address => uint256) public deposits;

    bool public hacked = false;

    function lock(uint256 wad) public {
        deposits[msg.sender] = add(deposits[msg.sender], wad);
        addWeight(wad, votes[msg.sender]);
    }

    function free(uint256 wad) public {
        deposits[msg.sender] = sub(deposits[msg.sender], wad);
        subWeight(wad, votes[msg.sender]);
    }

    function voteYays(address yay) public {
        bytes32 slate = keccak256(abi.encodePacked(yay));
        slates[slate] = yay;
        voteSlate(slate);
    }

    function etch(address yay) public {
        bytes32 hash = keccak256(abi.encodePacked(yay));
        slates[hash] = yay;
    }

    function voteSlate(bytes32 slate) public {
        uint256 weight = deposits[msg.sender];
        subWeight(weight, votes[msg.sender]);
        votes[msg.sender] = slate;
        addWeight(weight, votes[msg.sender]);
    }

    function addWeight(uint256 weight, bytes32 slate) internal {
        address yay = slates[slate];
        approvals[yay] = add(approvals[yay], weight);
    }

    function subWeight(uint256 weight, bytes32 slate) internal {
        address yay = slates[slate];
        approvals[yay] = sub(approvals[yay], weight);
    }

    function add(uint256 x, uint256 y) internal pure returns (uint256 z) {
        require((z = x + y) >= x);
    }

    function sub(uint256 x, uint256 y) internal pure returns (uint256 z) {
        require((z = x - y) <= x);
    }

    function checkInvariant() public {
        bytes32 senderSlate = votes[msg.sender];
        address option = slates[senderSlate];
        uint256 senderDeposit = deposits[msg.sender];

        if (approvals[option] < senderDeposit) {
            hacked = true;
        }
    }
}

test contract test/SimpleDSChiefTest.t.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

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

contract SimpleDSChiefTest is Test {
    SimpleDSChief dsChief;

    function setUp() public {
        dsChief = new SimpleDSChief();
        targetContract(address(dsChief));
        targetSender(address(0x10000));
        targetSender(address(0x20000));
        targetSender(address(0x30000));
    }

    /// forge-config: default.invariant.runs = 1000
    function invariant_check_dschief() public view {
        assertFalse(dsChief.hacked());
    }
}

// Try to determine the type of this storage slot
let storage_type = storage_layout.and_then(|layout| {
// Find the storage entry for this slot
layout.storage.iter().find(|s| s.slot == storage_slot.to_string()).and_then(|storage| {
Copy link
Collaborator

@grandizzy grandizzy Aug 6, 2025

Choose a reason for hiding this comment

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

would be great instead s.slot == storage_slot.to_string() to also figure out if it falls into a mapping base slot and what's the type (this logic could then be reused for decoding in #9504 as well) though not trivial and not sure exactly how achievable...

Copy link
Member Author

@yash-atreya yash-atreya Aug 6, 2025

Choose a reason for hiding this comment

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

Yep, looking into how we can make this work for mappings with simpler types first such as mapping(address => bytes32). We can later look into complex mappings with custom structs such as mapping(address => CustomStruct)

Copy link
Collaborator

Choose a reason for hiding this comment

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

awesome, yep, simple mappings support would be great

Copy link
Member Author

@yash-atreya yash-atreya Aug 6, 2025

Choose a reason for hiding this comment

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

Still some cleanup and optimizations to do but pls check: c7029d5.

Found the violation using the above:

[FAIL: assertion failed]
        [Sequence] (original: 333, shrunk: 6)
                sender=0x0000000000000000000000000000000000020000 addr=[src/SimpleDSChief.sol:SimpleDSChief]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=lock(uint256) args=[48459496635163284513498257876386846003409259726038080939842009107832010600766 [4.845e76]]
                sender=0x0000000000000000000000000000000000030000 addr=[src/SimpleDSChief.sol:SimpleDSChief]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=voteSlate(bytes32) args=[0xe169a59fb923f91b8fb1d1fc9258a999c5a19ad050199cd59889d6fa30d99753]
                sender=0x0000000000000000000000000000000000030000 addr=[src/SimpleDSChief.sol:SimpleDSChief]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=lock(uint256) args=[3065]
                sender=0x0000000000000000000000000000000000020000 addr=[src/SimpleDSChief.sol:SimpleDSChief]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=voteYays(address) args=[0x0000000000000000000000000000000000001474]
                sender=0x0000000000000000000000000000000000030000 addr=[src/SimpleDSChief.sol:SimpleDSChief]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=voteYays(address) args=[0x078B931Fa9ccaAF6580B28Eec0E2fA94D9E0cB0F]
                sender=0x0000000000000000000000000000000000020000 addr=[src/SimpleDSChief.sol:SimpleDSChief]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=checkInvariant() args=[]
 invariant_check_dschief() (runs: 577, calls: 288500, reverts: 21306)
Failure call sequence
function testViotationSequence2() public {
        dsChief = new SimpleDSChief();
        address sender200 = address(0x20000);
        address sender300 = address(0x30000);
        vm.prank(sender200);
        dsChief.lock(
            48459496635163284513498257876386846003409259726038080939842009107832010600766
        );

        vm.startPrank(sender300);
        dsChief.voteSlate(
            0xe169a59fb923f91b8fb1d1fc9258a999c5a19ad050199cd59889d6fa30d99753
        );
        dsChief.lock(3065);
        vm.stopPrank();

        vm.prank(sender200);
        dsChief.voteYays(0x0000000000000000000000000000000000001474);

        vm.prank(sender300);
        dsChief.voteYays(0x078B931Fa9ccaAF6580B28Eec0E2fA94D9E0cB0F);

        vm.prank(sender200);
        dsChief.checkInvariant();

        // Invariant broken.
        assertTrue(dsChief.hacked());
    }
"call_sequence": [
        {
            "sender": "0x0000000000000000000000000000000000020000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0xdd4670646b231a65c89caadb908c2e1634448f2b534d25b8a37ded97e5e06bfec772713e",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "lock",
            "signature": "lock(uint256)",
            "args": "48459496635163284513498257876386846003409259726038080939842009107832010600766 \u001b[2m[4.845e76]\u001b[0m",
            "raw_args": "48459496635163284513498257876386846003409259726038080939842009107832010600766"
        },
        {
            "sender": "0x0000000000000000000000000000000000030000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0xed337208e169a59fb923f91b8fb1d1fc9258a999c5a19ad050199cd59889d6fa30d99753",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "voteSlate",
            "signature": "voteSlate(bytes32)",
            "args": "0xe169a59fb923f91b8fb1d1fc9258a999c5a19ad050199cd59889d6fa30d99753",
            "raw_args": "0xe169a59fb923f91b8fb1d1fc9258a999c5a19ad050199cd59889d6fa30d99753"
        },
        {
            "sender": "0x0000000000000000000000000000000000030000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0xdd4670640000000000000000000000000000000000000000000000000000000000000bf9",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "lock",
            "signature": "lock(uint256)",
            "args": "3065",
            "raw_args": "3065"
        },
        {
            "sender": "0x0000000000000000000000000000000000020000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0x30d6c5750000000000000000000000000000000000000000000000000000000000001474",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "voteYays",
            "signature": "voteYays(address)",
            "args": "0x0000000000000000000000000000000000001474",
            "raw_args": "0x0000000000000000000000000000000000001474"
        },
        {
            "sender": "0x0000000000000000000000000000000000030000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0x30d6c575000000000000000000000000078b931fa9ccaaf6580b28eec0e2fa94d9e0cb0f",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "voteYays",
            "signature": "voteYays(address)",
            "args": "0x078B931Fa9ccaAF6580B28Eec0E2fA94D9E0cB0F",
            "raw_args": "0x078B931Fa9ccaAF6580B28Eec0E2fA94D9E0cB0F"
        },
        {
            "sender": "0x0000000000000000000000000000000000020000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0xe79487da",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "checkInvariant",
            "signature": "checkInvariant()",
            "args": "",
            "raw_args": ""
        }
    ]
Another failure sequence
function testViolationSequence() public {
        dsChief = new SimpleDSChief();
        address sender = address(0x20000);
        vm.startPrank(sender);
        uint256 lockAmt = 97755186804595591533339148550949742259098835763;
        dsChief.lock(lockAmt);
        bytes32 slate = 0xcc53d03fda58d9a098fd16c62dc974b0bc1c6e942fad55724f240f085e67485c;
        dsChief.voteSlate(slate);
        address etchAddr = 0x000000000000000000000000000000000000178c;
        dsChief.etch(etchAddr);
        dsChief.checkInvariant();
        // Invariant broken.
        assertTrue(dsChief.hacked());
    }
"call_sequence": [
        {
            "sender": "0x0000000000000000000000000000000000020000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0xdd467064000000000000000000000000111f7e82a185dd2d1ee7350ff4b5ceaece1b5b33",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "lock",
            "signature": "lock(uint256)",
            "args": "97755186804595591533339148550949742259098835763 \u001b[2m[9.775e46]\u001b[0m",
            "raw_args": "97755186804595591533339148550949742259098835763"
        },
        {
            "sender": "0x0000000000000000000000000000000000020000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0xed337208cc53d03fda58d9a098fd16c62dc974b0bc1c6e942fad55724f240f085e67485c",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "voteSlate",
            "signature": "voteSlate(bytes32)",
            "args": "0xcc53d03fda58d9a098fd16c62dc974b0bc1c6e942fad55724f240f085e67485c",
            "raw_args": "0xcc53d03fda58d9a098fd16c62dc974b0bc1c6e942fad55724f240f085e67485c"
        },
        {
            "sender": "0x0000000000000000000000000000000000020000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0x77c243eb000000000000000000000000000000000000000000000000000000000000178c",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "etch",
            "signature": "etch(address)",
            "args": "0x000000000000000000000000000000000000178c",
            "raw_args": "0x000000000000000000000000000000000000178c"
        },
        {
            "sender": "0x0000000000000000000000000000000000020000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0xe79487da",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "checkInvariant",
            "signature": "checkInvariant()",
            "args": "",
            "raw_args": ""
        }
    ]

Copy link
Member Author

@yash-atreya yash-atreya Aug 6, 2025

Choose a reason for hiding this comment

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

Unable to reproduce the violations reliably likely because we're still not checking whether the storage_slot belongs to the mapping or not but instead optmistically inserting all storage_values that can be decoded into any of the mapping value types. This is still better than not handling mapping storage values but isn't ideal

Copy link
Collaborator

@grandizzy grandizzy Aug 7, 2025

Choose a reason for hiding this comment

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

nice, definitely better, maybe we insert only values for the known mapping here
https://github.com/foundry-rs/foundry/pull/11204/files#diff-c5b4765d032eafa6ae80b980dbf25e2ad41b34d65c33fff247b5bd9bbd333db5R369-R374
so if we don't have a mapping with bool for example then we don't insert (in DS chief sample we only have

    mapping(bytes32 => address) public slates;
    mapping(address => bytes32) public votes;
    mapping(address => uint256) public approvals;
    mapping(address => uint256) public deposits;

so we insert only as address, bytes32 and uint256).

For tests - you could also set smth like timeout = 3600 in invariant config section so you won't be bounded by the number of runs but rather let it run until 3600 seconds expires

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

@yash-atreya yash-atreya Aug 8, 2025

Choose a reason for hiding this comment

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

Ran with timeout, found another sequence. With this 937c504 change we are handling mappings with custom structs as well; as long as the storage_value can be decoded into that type. See:

for sol_type in mapping_types {
match sol_type.abi_decode(storage_value.as_le_slice()) {
Ok(_) => {
self.sample_values
.entry(sol_type)
.or_default()
.insert(B256::from(*storage_value));

    "call_sequence": [
        {
            "sender": "0x0000000000000000000000000000000000010000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0xed337208e169a59fb923f91b8fb1d1fc9258a999c5a19ad050199cd59889d6fa30d99753",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "voteSlate",
            "signature": "voteSlate(bytes32)",
            "args": "0xe169a59fb923f91b8fb1d1fc9258a999c5a19ad050199cd59889d6fa30d99753",
            "raw_args": "0xe169a59fb923f91b8fb1d1fc9258a999c5a19ad050199cd59889d6fa30d99753"
        },
        {
            "sender": "0x0000000000000000000000000000000000010000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0xdd4670640001bb5012f07628cf040345bb01e9d354eac85ce93561e02e5fac9de5df1531",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "lock",
            "signature": "lock(uint256)",
            "args": "3059632421902990131620449197297599091381462006012196859469650801012446513 \u001b[2m[3.059e72]\u001b[0m",
            "raw_args": "3059632421902990131620449197297599091381462006012196859469650801012446513"
        },
        {
            "sender": "0x0000000000000000000000000000000000010000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0x77c243eb0000000000000000000000000000000000000000000000000000000000001474",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "etch",
            "signature": "etch(address)",
            "args": "0x0000000000000000000000000000000000001474",
            "raw_args": "0x0000000000000000000000000000000000001474"
        },
        {
            "sender": "0x0000000000000000000000000000000000010000",
            "addr": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f",
            "calldata": "0xe79487da",
            "contract_name": "src/SimpleDSChief.sol:SimpleDSChief",
            "func_name": "checkInvariant",
            "signature": "checkInvariant()",
            "args": "",
            "raw_args": ""
        }
    ]

@yash-atreya yash-atreya marked this pull request as draft August 6, 2025 20:39
@yash-atreya yash-atreya marked this pull request as ready for review August 8, 2025 18:09
@grandizzy grandizzy self-requested a review August 11, 2025 04:46
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.

thank you, even if not fully accurate I think we can move forward with this approach and improve further (it only applies if storageLayout is enabled in project config). I added a nit a570318 to reuse logic of extracting storage layout from project contracts, pls have a look. (additionally we should update the docs to make people aware of this new feature)

@yash-atreya
Copy link
Member Author

Docs PR: foundry-rs/book#1631

@yash-atreya yash-atreya merged commit ff732fc into master Aug 11, 2025
22 checks passed
@yash-atreya yash-atreya deleted the yash/fuzz-storage-values-by-type branch August 11, 2025 09:18
@github-project-automation github-project-automation bot moved this to Done in Foundry Aug 11, 2025
@grandizzy grandizzy moved this from Done to Completed in Foundry Aug 18, 2025
MerkleBoy pushed a commit to MerkleBoy/foundry that referenced this pull request Sep 17, 2025
* feat(`forge`): sample typed storage values

* arc it

* nit

* clippy

* nit

* strip file prefixes

* fmt

* don't add adjacent values to sample

* correctly match artifact identifier and name

* clippy

* nit

* handle simple mappings while inserting storage values

* cleanup + insert only DynSolType's found in mappings

* Nit: with_project_contracts

---------

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-testing Area: testing C-forge Command: forge
Projects
Status: Completed
Development

Successfully merging this pull request may close these issues.

feat(invariant): use storage layout to fuzz values from state by type
2 participants