@@ -2055,3 +2055,138 @@ contract InvariantOptimizeWarpTest is Test {
20552055...
20562056"# ] ] ) ;
20572057} ) ;
2058+
2059+ // Test that without max_deal, payable calls with value fail when sender has no balance.
2060+ // The fuzzer should fall back to value=0, so the invariant should pass.
2061+ forgetest_init ! ( invariant_no_max_deal_fallback, |prj, cmd| {
2062+ prj. update_config( |config| {
2063+ config. fuzz. seed = Some ( U256 :: from( 42u32 ) ) ;
2064+ config. invariant. runs = 50 ;
2065+ config. invariant. depth = 10 ;
2066+ // No max_deal configured - sender has no balance so value falls back to 0
2067+ } ) ;
2068+
2069+ prj. add_test(
2070+ "InvariantNoMaxDeal.t.sol" ,
2071+ r#"
2072+ import "forge-std/Test.sol";
2073+
2074+ contract ValueTarget {
2075+ bool public valueReceived;
2076+
2077+ function deposit() external payable {
2078+ if (msg.value > 0) {
2079+ valueReceived = true;
2080+ }
2081+ }
2082+ }
2083+
2084+ contract InvariantNoMaxDeal is Test {
2085+ ValueTarget target;
2086+
2087+ function setUp() public {
2088+ target = new ValueTarget();
2089+ // Target only the ValueTarget contract
2090+ targetContract(address(target));
2091+ // Use a custom sender with no initial balance
2092+ targetSender(makeAddr("sender1"));
2093+ // NOTE: No vm.deal() for sender1 - it has 0 balance
2094+ // Without max_deal config, value should fall back to 0
2095+ }
2096+
2097+ // This invariant should PASS because without max_deal, value falls back to 0
2098+ function invariant_value_never_received() public view {
2099+ require(!target.valueReceived(), "Value was received");
2100+ }
2101+ }
2102+ "# ,
2103+ ) ;
2104+
2105+ // The test should pass because without max_deal and no sender balance,
2106+ // value falls back to 0
2107+ cmd. args( [ "test" , "--mt" , "invariant_value_never_received" ] ) . assert_success( ) . stdout_eq( str ![
2108+ [ r#"
2109+ ...
2110+ [PASS] invariant_value_never_received() (runs: 50, calls: 500, reverts: 0)
2111+ ...
2112+ "# ]
2113+ ] ) ;
2114+ } ) ;
2115+
2116+ // Test that with max_deal configured, the fuzzer deals balance to sender before tx,
2117+ // enabling payable calls with msg.value > 0 to succeed.
2118+ forgetest_init ! ( invariant_with_max_deal, |prj, cmd| {
2119+ prj. update_config( |config| {
2120+ config. fuzz. seed = Some ( U256 :: from( 42u32 ) ) ;
2121+ config. invariant. runs = 100 ;
2122+ config. invariant. depth = 20 ;
2123+ // Configure max_deal to fund senders before each tx
2124+ config. invariant. max_deal = Some ( 1_000_000_000_000_000_000 ) ; // 1 ETH in wei
2125+ } ) ;
2126+
2127+ prj. add_test(
2128+ "InvariantWithMaxDeal.t.sol" ,
2129+ r#"
2130+ import "forge-std/Test.sol";
2131+
2132+ contract ValueTarget {
2133+ bool public valueReceived;
2134+
2135+ function deposit() external payable {
2136+ if (msg.value > 0) {
2137+ valueReceived = true;
2138+ }
2139+ }
2140+ }
2141+
2142+ contract InvariantWithMaxDeal is Test {
2143+ ValueTarget target;
2144+
2145+ function setUp() public {
2146+ target = new ValueTarget();
2147+ // Target only the ValueTarget contract
2148+ targetContract(address(target));
2149+ // Use a custom sender with no initial balance
2150+ targetSender(makeAddr("sender1"));
2151+ // NOTE: No vm.deal() in setUp - max_deal config will fund sender1
2152+ }
2153+
2154+ // This invariant should FAIL because max_deal funds senders,
2155+ // allowing payable calls with msg.value > 0
2156+ function invariant_value_never_received() public view {
2157+ require(!target.valueReceived(), "Value was received");
2158+ }
2159+ }
2160+ "# ,
2161+ ) ;
2162+
2163+ // The test should fail because max_deal funds senders before each tx,
2164+ // enabling value > 0 to be sent
2165+ cmd. args( [ "test" , "--mt" , "invariant_value_never_received" ] )
2166+ . assert_failure( )
2167+ . stdout_eq( str ![ [ r#"
2168+ ...
2169+ [FAIL: Value was received]
2170+ [Sequence] (original: [..], shrunk: 1)
2171+ sender=[..] addr=[test/InvariantWithMaxDeal.t.sol:ValueTarget][..] deal=[..] value=[..] calldata=deposit() args=[]
2172+ ...
2173+ "# ] ] ) ;
2174+
2175+ // Check solidity output format shows vm.deal()
2176+ cmd. forge_fuse( ) . arg( "clean" ) . assert_success( ) ;
2177+ prj. update_config( |config| {
2178+ config. invariant. show_solidity = true ;
2179+ } ) ;
2180+ cmd. forge_fuse( )
2181+ . args( [ "test" , "--mt" , "invariant_value_never_received" ] )
2182+ . assert_failure( )
2183+ . stdout_eq( str ![ [ r#"
2184+ ...
2185+ [FAIL: Value was received]
2186+ [Sequence] (original: [..], shrunk: 1)
2187+ vm.deal([..], [..].balance + [..]);
2188+ vm.prank([..]);
2189+ ValueTarget([..]).deposit{value: [..]}();
2190+ ...
2191+ "# ] ] ) ;
2192+ } ) ;
0 commit comments