Skip to content

Commit a510447

Browse files
authored
fix(invariant): honor targetContract setting, don't update targets if any (#7595)
* fix(invariant): respect targetContract setup * Fix test fmt * Check identified contracts after collecting `targetInterfaces`
1 parent bbdb034 commit a510447

File tree

7 files changed

+150
-31
lines changed

7 files changed

+150
-31
lines changed

crates/evm/evm/src/executors/invariant/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ impl FailedInvariantCaseData {
118118
};
119119

120120
// Collect abis of fuzzed and invariant contracts to decode custom error.
121-
let targets = targeted_contracts.lock();
121+
let targets = targeted_contracts.targets.lock();
122122
let abis = targets
123123
.iter()
124124
.map(|contract| &contract.1 .1)

crates/evm/evm/src/executors/invariant/mod.rs

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use foundry_evm_fuzz::{
2323
FuzzCase, FuzzedCases,
2424
};
2525
use foundry_evm_traces::CallTraceArena;
26-
use parking_lot::{Mutex, RwLock};
26+
use parking_lot::RwLock;
2727
use proptest::{
2828
strategy::{BoxedStrategy, Strategy, ValueTree},
2929
test_runner::{TestCaseError, TestRunner},
@@ -249,17 +249,20 @@ impl<'a> InvariantExecutor<'a> {
249249

250250
collect_data(&mut state_changeset, sender, &call_result, &fuzz_state);
251251

252-
if let Err(error) = collect_created_contracts(
253-
&state_changeset,
254-
self.project_contracts,
255-
self.setup_contracts,
256-
&self.artifact_filters,
257-
targeted_contracts.clone(),
258-
&mut created_contracts,
259-
) {
260-
warn!(target: "forge::test", "{error}");
252+
// Collect created contracts and add to fuzz targets only if targeted contracts
253+
// are updatable.
254+
if targeted_contracts.is_updatable {
255+
if let Err(error) = collect_created_contracts(
256+
&state_changeset,
257+
self.project_contracts,
258+
self.setup_contracts,
259+
&self.artifact_filters,
260+
&targeted_contracts,
261+
&mut created_contracts,
262+
) {
263+
warn!(target: "forge::test", "{error}");
264+
}
261265
}
262-
263266
// Commit changes to the database.
264267
executor.backend.commit(state_changeset.clone());
265268

@@ -309,7 +312,7 @@ impl<'a> InvariantExecutor<'a> {
309312

310313
// We clear all the targeted contracts created during this run.
311314
if !created_contracts.is_empty() {
312-
let mut writable_targeted = targeted_contracts.lock();
315+
let mut writable_targeted = targeted_contracts.targets.lock();
313316
for addr in created_contracts.iter() {
314317
writable_targeted.remove(addr);
315318
}
@@ -353,19 +356,10 @@ impl<'a> InvariantExecutor<'a> {
353356
let (targeted_senders, targeted_contracts) =
354357
self.select_contracts_and_senders(invariant_contract.address)?;
355358

356-
if targeted_contracts.is_empty() {
357-
eyre::bail!("No contracts to fuzz.");
358-
}
359-
360359
// Stores fuzz state for use with [fuzz_calldata_from_state].
361360
let fuzz_state: EvmFuzzState =
362361
build_initial_state(self.executor.backend.mem_db(), self.config.dictionary);
363362

364-
// During execution, any newly created contract is added here and used through the rest of
365-
// the fuzz run.
366-
let targeted_contracts: FuzzRunIdentifiedContracts =
367-
Arc::new(Mutex::new(targeted_contracts));
368-
369363
let calldata_fuzz_config =
370364
CalldataFuzzDictionary::new(&self.config.dictionary, &fuzz_state);
371365

@@ -500,7 +494,7 @@ impl<'a> InvariantExecutor<'a> {
500494
pub fn select_contracts_and_senders(
501495
&self,
502496
to: Address,
503-
) -> eyre::Result<(SenderFilters, TargetedContracts)> {
497+
) -> eyre::Result<(SenderFilters, FuzzRunIdentifiedContracts)> {
504498
let targeted_senders =
505499
self.call_sol_default(to, &IInvariantTest::targetSendersCall {}).targetedSenders;
506500
let excluded_senders =
@@ -532,7 +526,15 @@ impl<'a> InvariantExecutor<'a> {
532526

533527
self.select_selectors(to, &mut contracts)?;
534528

535-
Ok((SenderFilters::new(targeted_senders, excluded_senders), contracts))
529+
// There should be at least one contract identified as target for fuzz runs.
530+
if contracts.is_empty() {
531+
eyre::bail!("No contracts to fuzz.");
532+
}
533+
534+
Ok((
535+
SenderFilters::new(targeted_senders, excluded_senders),
536+
FuzzRunIdentifiedContracts::new(contracts, selected.is_empty()),
537+
))
536538
}
537539

538540
/// Extends the contracts and selectors to fuzz with the addresses and ABIs specified in
@@ -708,7 +710,7 @@ fn can_continue(
708710
let mut call_results = None;
709711

710712
// Detect handler assertion failures first.
711-
let handlers_failed = targeted_contracts.lock().iter().any(|contract| {
713+
let handlers_failed = targeted_contracts.targets.lock().iter().any(|contract| {
712714
!executor.is_success(*contract.0, false, Cow::Borrowed(state_changeset), false)
713715
});
714716

crates/evm/fuzz/src/invariant/mod.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,23 @@ mod filters;
1010
pub use filters::{ArtifactFilters, SenderFilters};
1111

1212
pub type TargetedContracts = BTreeMap<Address, (String, JsonAbi, Vec<Function>)>;
13-
pub type FuzzRunIdentifiedContracts = Arc<Mutex<TargetedContracts>>;
13+
14+
/// Contracts identified as targets during a fuzz run.
15+
/// During execution, any newly created contract is added as target and used through the rest of
16+
/// the fuzz run if the collection is updatable (no `targetContract` specified in `setUp`).
17+
#[derive(Clone, Debug)]
18+
pub struct FuzzRunIdentifiedContracts {
19+
/// Contracts identified as targets during a fuzz run.
20+
pub targets: Arc<Mutex<TargetedContracts>>,
21+
/// Whether target contracts are updatable or not.
22+
pub is_updatable: bool,
23+
}
24+
25+
impl FuzzRunIdentifiedContracts {
26+
pub fn new(targets: TargetedContracts, is_updatable: bool) -> Self {
27+
Self { targets: Arc::new(Mutex::new(targets)), is_updatable }
28+
}
29+
}
1430

1531
/// (Sender, (TargetContract, Calldata))
1632
pub type BasicTxDetails = (Address, (Address, Bytes));

crates/evm/fuzz/src/strategies/invariants.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub fn override_call_strat(
1616
target: Arc<RwLock<Address>>,
1717
calldata_fuzz_config: CalldataFuzzDictionary,
1818
) -> SBoxedStrategy<(Address, Bytes)> {
19-
let contracts_ref = contracts.clone();
19+
let contracts_ref = contracts.targets.clone();
2020
proptest::prop_oneof![
2121
80 => proptest::strategy::LazyJust::new(move || *target.read()),
2222
20 => any::<prop::sample::Selector>()
@@ -27,7 +27,7 @@ pub fn override_call_strat(
2727
let calldata_fuzz_config = calldata_fuzz_config.clone();
2828

2929
let func = {
30-
let contracts = contracts.lock();
30+
let contracts = contracts.targets.lock();
3131
let (_, abi, functions) = contracts.get(&target_address).unwrap_or_else(|| {
3232
// Choose a random contract if target selected by lazy strategy is not in fuzz run
3333
// identified contracts. This can happen when contract is created in `setUp` call
@@ -81,7 +81,7 @@ fn generate_call(
8181
any::<prop::sample::Selector>()
8282
.prop_flat_map(move |selector| {
8383
let (contract, func) = {
84-
let contracts = contracts.lock();
84+
let contracts = contracts.targets.lock();
8585
let contracts =
8686
contracts.iter().filter(|(_, (_, abi, _))| !abi.functions.is_empty());
8787
let (&contract, (_, abi, functions)) = selector.select(contracts);

crates/evm/fuzz/src/strategies/state.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,10 +304,10 @@ pub fn collect_created_contracts(
304304
project_contracts: &ContractsByArtifact,
305305
setup_contracts: &ContractsByAddress,
306306
artifact_filters: &ArtifactFilters,
307-
targeted_contracts: FuzzRunIdentifiedContracts,
307+
targeted_contracts: &FuzzRunIdentifiedContracts,
308308
created_contracts: &mut Vec<Address>,
309309
) -> eyre::Result<()> {
310-
let mut writable_targeted = targeted_contracts.lock();
310+
let mut writable_targeted = targeted_contracts.targets.lock();
311311
for (address, account) in state_changeset {
312312
if !setup_contracts.contains_key(address) {
313313
if let (true, Some(code)) = (&account.is_touched(), &account.info.code) {

crates/forge/tests/it/invariant.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ async fn test_invariant() {
149149
"default/fuzz/invariant/common/InvariantCustomError.t.sol:InvariantCustomError",
150150
vec![("invariant_decode_error()", true, None, None, None)],
151151
),
152+
(
153+
"default/fuzz/invariant/target/FuzzedTargetContracts.t.sol:ExplicitTargetContract",
154+
vec![("invariant_explicit_target()", true, None, None, None)],
155+
),
156+
(
157+
"default/fuzz/invariant/target/FuzzedTargetContracts.t.sol:DynamicTargetContract",
158+
vec![("invariant_dynamic_targets()", true, None, None, None)],
159+
),
152160
]),
153161
);
154162
}
@@ -436,3 +444,30 @@ async fn test_invariant_decode_custom_error() {
436444
)]),
437445
);
438446
}
447+
448+
#[tokio::test(flavor = "multi_thread")]
449+
async fn test_invariant_fuzzed_selected_targets() {
450+
let filter = Filter::new(".*", ".*", ".*fuzz/invariant/target/FuzzedTargetContracts.t.sol");
451+
let mut runner = TEST_DATA_DEFAULT.runner();
452+
runner.test_options.invariant.fail_on_revert = true;
453+
let results = runner.test_collect(&filter);
454+
assert_multiple(
455+
&results,
456+
BTreeMap::from([
457+
(
458+
"default/fuzz/invariant/target/FuzzedTargetContracts.t.sol:ExplicitTargetContract",
459+
vec![("invariant_explicit_target()", true, None, None, None)],
460+
),
461+
(
462+
"default/fuzz/invariant/target/FuzzedTargetContracts.t.sol:DynamicTargetContract",
463+
vec![(
464+
"invariant_dynamic_targets()",
465+
false,
466+
Some("revert: wrong target selector called".into()),
467+
None,
468+
None,
469+
)],
470+
),
471+
]),
472+
);
473+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity 0.8.18;
3+
4+
import "ds-test/test.sol";
5+
6+
interface Vm {
7+
function etch(address target, bytes calldata newRuntimeBytecode) external;
8+
}
9+
10+
// https://github.com/foundry-rs/foundry/issues/5625
11+
// https://github.com/foundry-rs/foundry/issues/6166
12+
// `Target.wrongSelector` is not called when handler added as `targetContract`
13+
// `Target.wrongSelector` is called (and test fails) when no `targetContract` set
14+
contract Target {
15+
uint256 count;
16+
17+
function wrongSelector() external {
18+
revert("wrong target selector called");
19+
}
20+
21+
function goodSelector() external {
22+
count++;
23+
}
24+
}
25+
26+
contract Handler is DSTest {
27+
function increment() public {
28+
Target(0x6B175474E89094C44Da98b954EedeAC495271d0F).goodSelector();
29+
}
30+
}
31+
32+
contract ExplicitTargetContract is DSTest {
33+
Vm vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
34+
Handler handler;
35+
36+
function setUp() public {
37+
Target target = new Target();
38+
bytes memory targetCode = address(target).code;
39+
vm.etch(address(0x6B175474E89094C44Da98b954EedeAC495271d0F), targetCode);
40+
41+
handler = new Handler();
42+
}
43+
44+
function targetContracts() public returns (address[] memory) {
45+
address[] memory addrs = new address[](1);
46+
addrs[0] = address(handler);
47+
return addrs;
48+
}
49+
50+
function invariant_explicit_target() public {}
51+
}
52+
53+
contract DynamicTargetContract is DSTest {
54+
Vm vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
55+
Handler handler;
56+
57+
function setUp() public {
58+
Target target = new Target();
59+
bytes memory targetCode = address(target).code;
60+
vm.etch(address(0x6B175474E89094C44Da98b954EedeAC495271d0F), targetCode);
61+
62+
handler = new Handler();
63+
}
64+
65+
function invariant_dynamic_targets() public {}
66+
}

0 commit comments

Comments
 (0)