Skip to content

Commit cb109b1

Browse files
authored
feat(cheatcodes): add vm.assumeNoRevert for fuzz tests (#8780)
1 parent 91c0782 commit cb109b1

File tree

7 files changed

+160
-17
lines changed

7 files changed

+160
-17
lines changed

crates/cheatcodes/assets/cheatcodes.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/spec/src/vm.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,10 @@ interface Vm {
678678
#[cheatcode(group = Testing, safety = Safe)]
679679
function assume(bool condition) external pure;
680680

681+
/// Discard this run's fuzz inputs and generate new ones if next call reverted.
682+
#[cheatcode(group = Testing, safety = Safe)]
683+
function assumeNoRevert() external pure;
684+
681685
/// Writes a breakpoint to jump to in the debugger.
682686
#[cheatcode(group = Testing, safety = Safe)]
683687
function breakpoint(string calldata char) external;

crates/cheatcodes/src/inspector.rs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ use crate::{
99
},
1010
inspector::utils::CommonCreateInput,
1111
script::{Broadcast, ScriptWallets},
12-
test::expect::{
13-
self, ExpectedCallData, ExpectedCallTracker, ExpectedCallType, ExpectedEmit,
14-
ExpectedRevert, ExpectedRevertKind,
12+
test::{
13+
assume::AssumeNoRevert,
14+
expect::{
15+
self, ExpectedCallData, ExpectedCallTracker, ExpectedCallType, ExpectedEmit,
16+
ExpectedRevert, ExpectedRevertKind,
17+
},
1518
},
1619
utils::IgnoredTraces,
1720
CheatsConfig, CheatsCtxt, DynCheatcode, Error, Result, Vm,
@@ -25,7 +28,7 @@ use foundry_config::Config;
2528
use foundry_evm_core::{
2629
abi::Vm::stopExpectSafeMemoryCall,
2730
backend::{DatabaseExt, RevertDiagnostic},
28-
constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS},
31+
constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS, MAGIC_ASSUME},
2932
utils::new_evm_with_existing_context,
3033
InspectorExt,
3134
};
@@ -294,6 +297,9 @@ pub struct Cheatcodes {
294297
/// Expected revert information
295298
pub expected_revert: Option<ExpectedRevert>,
296299

300+
/// Assume next call can revert and discard fuzz run if it does.
301+
pub assume_no_revert: Option<AssumeNoRevert>,
302+
297303
/// Additional diagnostic for reverts
298304
pub fork_revert_diagnostic: Option<RevertDiagnostic>,
299305

@@ -384,6 +390,7 @@ impl Cheatcodes {
384390
gas_price: Default::default(),
385391
prank: Default::default(),
386392
expected_revert: Default::default(),
393+
assume_no_revert: Default::default(),
387394
fork_revert_diagnostic: Default::default(),
388395
accesses: Default::default(),
389396
recorded_account_diffs_stack: Default::default(),
@@ -1106,6 +1113,19 @@ impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes {
11061113
}
11071114
}
11081115

1116+
// Handle assume not revert cheatcode.
1117+
if let Some(assume_no_revert) = &self.assume_no_revert {
1118+
if ecx.journaled_state.depth() == assume_no_revert.depth && !cheatcode_call {
1119+
// Discard run if we're at the same depth as cheatcode and call reverted.
1120+
if outcome.result.is_revert() {
1121+
outcome.result.output = Error::from(MAGIC_ASSUME).abi_encode().into();
1122+
return outcome;
1123+
}
1124+
// Call didn't revert, reset `assume_no_revert` state.
1125+
self.assume_no_revert = None;
1126+
}
1127+
}
1128+
11091129
// Handle expected reverts
11101130
if let Some(expected_revert) = &self.expected_revert {
11111131
if ecx.journaled_state.depth() <= expected_revert.depth {

crates/cheatcodes/src/test.rs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,15 @@
33
use chrono::DateTime;
44
use std::env;
55

6-
use crate::{Cheatcode, Cheatcodes, CheatsCtxt, DatabaseExt, Error, Result, Vm::*};
6+
use crate::{Cheatcode, Cheatcodes, CheatsCtxt, DatabaseExt, Result, Vm::*};
77
use alloy_primitives::Address;
88
use alloy_sol_types::SolValue;
9-
use foundry_evm_core::constants::{MAGIC_ASSUME, MAGIC_SKIP};
9+
use foundry_evm_core::constants::MAGIC_SKIP;
1010

1111
pub(crate) mod assert;
12+
pub(crate) mod assume;
1213
pub(crate) mod expect;
1314

14-
impl Cheatcode for assumeCall {
15-
fn apply(&self, _state: &mut Cheatcodes) -> Result {
16-
let Self { condition } = self;
17-
if *condition {
18-
Ok(Default::default())
19-
} else {
20-
Err(Error::from(MAGIC_ASSUME))
21-
}
22-
}
23-
}
24-
2515
impl Cheatcode for breakpoint_0Call {
2616
fn apply_stateful<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
2717
let Self { char } = self;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Error, Result};
2+
use foundry_evm_core::{backend::DatabaseExt, constants::MAGIC_ASSUME};
3+
use spec::Vm::{assumeCall, assumeNoRevertCall};
4+
use std::fmt::Debug;
5+
6+
#[derive(Clone, Debug)]
7+
pub struct AssumeNoRevert {
8+
/// The call depth at which the cheatcode was added.
9+
pub depth: u64,
10+
}
11+
12+
impl Cheatcode for assumeCall {
13+
fn apply(&self, _state: &mut Cheatcodes) -> Result {
14+
let Self { condition } = self;
15+
if *condition {
16+
Ok(Default::default())
17+
} else {
18+
Err(Error::from(MAGIC_ASSUME))
19+
}
20+
}
21+
}
22+
23+
impl Cheatcode for assumeNoRevertCall {
24+
fn apply_stateful<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
25+
ccx.state.assume_no_revert =
26+
Some(AssumeNoRevert { depth: ccx.ecx.journaled_state.depth() });
27+
Ok(Default::default())
28+
}
29+
}

crates/forge/tests/cli/test_cmd.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1834,3 +1834,82 @@ contract CounterTest is DSTest {
18341834
...
18351835
"#]]);
18361836
});
1837+
1838+
forgetest_init!(test_assume_no_revert, |prj, cmd| {
1839+
prj.wipe_contracts();
1840+
prj.insert_ds_test();
1841+
prj.insert_vm();
1842+
prj.clear();
1843+
1844+
prj.add_source(
1845+
"Counter.t.sol",
1846+
r#"pragma solidity 0.8.24;
1847+
import {Vm} from "./Vm.sol";
1848+
import {DSTest} from "./test.sol";
1849+
contract CounterWithRevert {
1850+
error CountError();
1851+
error CheckError();
1852+
1853+
function count(uint256 a) public pure returns (uint256) {
1854+
if (a > 1000 || a < 10) {
1855+
revert CountError();
1856+
}
1857+
return 99999999;
1858+
}
1859+
function check(uint256 a) public pure {
1860+
if (a == 99999999) {
1861+
revert CheckError();
1862+
}
1863+
}
1864+
function dummy() public pure {}
1865+
}
1866+
1867+
contract CounterRevertTest is DSTest {
1868+
Vm vm = Vm(HEVM_ADDRESS);
1869+
1870+
function test_assume_no_revert_pass(uint256 a) public {
1871+
CounterWithRevert counter = new CounterWithRevert();
1872+
vm.assumeNoRevert();
1873+
a = counter.count(a);
1874+
assertEq(a, 99999999);
1875+
}
1876+
function test_assume_no_revert_fail_assert(uint256 a) public {
1877+
CounterWithRevert counter = new CounterWithRevert();
1878+
vm.assumeNoRevert();
1879+
a = counter.count(a);
1880+
// Test should fail on next assertion.
1881+
assertEq(a, 1);
1882+
}
1883+
function test_assume_no_revert_fail_in_2nd_call(uint256 a) public {
1884+
CounterWithRevert counter = new CounterWithRevert();
1885+
vm.assumeNoRevert();
1886+
a = counter.count(a);
1887+
// Test should revert here (not in scope of `assumeNoRevert` cheatcode).
1888+
counter.check(a);
1889+
assertEq(a, 99999999);
1890+
}
1891+
function test_assume_no_revert_fail_in_3rd_call(uint256 a) public {
1892+
CounterWithRevert counter = new CounterWithRevert();
1893+
vm.assumeNoRevert();
1894+
a = counter.count(a);
1895+
// Test `assumeNoRevert` applied to non reverting call should not be available for next reverting call.
1896+
vm.assumeNoRevert();
1897+
counter.dummy();
1898+
// Test will revert here (not in scope of `assumeNoRevert` cheatcode).
1899+
counter.check(a);
1900+
assertEq(a, 99999999);
1901+
}
1902+
}
1903+
"#,
1904+
)
1905+
.unwrap();
1906+
1907+
cmd.args(["test"]).with_no_redact().assert_failure().stdout_eq(str![[r#"
1908+
...
1909+
[FAIL. Reason: assertion failed; counterexample: [..]] test_assume_no_revert_fail_assert(uint256) [..]
1910+
[FAIL. Reason: CheckError(); counterexample: [..]] test_assume_no_revert_fail_in_2nd_call(uint256) [..]
1911+
[FAIL. Reason: CheckError(); counterexample: [..]] test_assume_no_revert_fail_in_3rd_call(uint256) [..]
1912+
[PASS] test_assume_no_revert_pass(uint256) (runs: 256, [..])
1913+
...
1914+
"#]]);
1915+
});

testdata/cheats/Vm.sol

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)