diff --git a/crates/evm/evm/src/executors/invariant/error.rs b/crates/evm/evm/src/executors/invariant/error.rs index 98977b539fd5..0695ad6c4eff 100644 --- a/crates/evm/evm/src/executors/invariant/error.rs +++ b/crates/evm/evm/src/executors/invariant/error.rs @@ -118,7 +118,7 @@ impl FailedInvariantCaseData { }; // Collect abis of fuzzed and invariant contracts to decode custom error. - let targets = targeted_contracts.lock(); + let targets = targeted_contracts.targets.lock(); let abis = targets .iter() .map(|contract| &contract.1 .1) diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 8f1d3ca57e38..eb4cecb3851e 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -23,7 +23,7 @@ use foundry_evm_fuzz::{ FuzzCase, FuzzedCases, }; use foundry_evm_traces::CallTraceArena; -use parking_lot::{Mutex, RwLock}; +use parking_lot::RwLock; use proptest::{ strategy::{BoxedStrategy, Strategy, ValueTree}, test_runner::{TestCaseError, TestRunner}, @@ -249,17 +249,20 @@ impl<'a> InvariantExecutor<'a> { collect_data(&mut state_changeset, sender, &call_result, &fuzz_state); - if let Err(error) = collect_created_contracts( - &state_changeset, - self.project_contracts, - self.setup_contracts, - &self.artifact_filters, - targeted_contracts.clone(), - &mut created_contracts, - ) { - warn!(target: "forge::test", "{error}"); + // Collect created contracts and add to fuzz targets only if targeted contracts + // are updatable. + if targeted_contracts.is_updatable { + if let Err(error) = collect_created_contracts( + &state_changeset, + self.project_contracts, + self.setup_contracts, + &self.artifact_filters, + &targeted_contracts, + &mut created_contracts, + ) { + warn!(target: "forge::test", "{error}"); + } } - // Commit changes to the database. executor.backend.commit(state_changeset.clone()); @@ -309,7 +312,7 @@ impl<'a> InvariantExecutor<'a> { // We clear all the targeted contracts created during this run. if !created_contracts.is_empty() { - let mut writable_targeted = targeted_contracts.lock(); + let mut writable_targeted = targeted_contracts.targets.lock(); for addr in created_contracts.iter() { writable_targeted.remove(addr); } @@ -353,19 +356,10 @@ impl<'a> InvariantExecutor<'a> { let (targeted_senders, targeted_contracts) = self.select_contracts_and_senders(invariant_contract.address)?; - if targeted_contracts.is_empty() { - eyre::bail!("No contracts to fuzz."); - } - // Stores fuzz state for use with [fuzz_calldata_from_state]. let fuzz_state: EvmFuzzState = build_initial_state(self.executor.backend.mem_db(), self.config.dictionary); - // During execution, any newly created contract is added here and used through the rest of - // the fuzz run. - let targeted_contracts: FuzzRunIdentifiedContracts = - Arc::new(Mutex::new(targeted_contracts)); - let calldata_fuzz_config = CalldataFuzzDictionary::new(&self.config.dictionary, &fuzz_state); @@ -500,7 +494,7 @@ impl<'a> InvariantExecutor<'a> { pub fn select_contracts_and_senders( &self, to: Address, - ) -> eyre::Result<(SenderFilters, TargetedContracts)> { + ) -> eyre::Result<(SenderFilters, FuzzRunIdentifiedContracts)> { let targeted_senders = self.call_sol_default(to, &IInvariantTest::targetSendersCall {}).targetedSenders; let excluded_senders = @@ -532,7 +526,15 @@ impl<'a> InvariantExecutor<'a> { self.select_selectors(to, &mut contracts)?; - Ok((SenderFilters::new(targeted_senders, excluded_senders), contracts)) + // There should be at least one contract identified as target for fuzz runs. + if contracts.is_empty() { + eyre::bail!("No contracts to fuzz."); + } + + Ok(( + SenderFilters::new(targeted_senders, excluded_senders), + FuzzRunIdentifiedContracts::new(contracts, selected.is_empty()), + )) } /// Extends the contracts and selectors to fuzz with the addresses and ABIs specified in @@ -708,7 +710,7 @@ fn can_continue( let mut call_results = None; // Detect handler assertion failures first. - let handlers_failed = targeted_contracts.lock().iter().any(|contract| { + let handlers_failed = targeted_contracts.targets.lock().iter().any(|contract| { !executor.is_success(*contract.0, false, Cow::Borrowed(state_changeset), false) }); diff --git a/crates/evm/fuzz/src/invariant/mod.rs b/crates/evm/fuzz/src/invariant/mod.rs index 6dfcd8248739..d682041e9706 100644 --- a/crates/evm/fuzz/src/invariant/mod.rs +++ b/crates/evm/fuzz/src/invariant/mod.rs @@ -10,7 +10,23 @@ mod filters; pub use filters::{ArtifactFilters, SenderFilters}; pub type TargetedContracts = BTreeMap)>; -pub type FuzzRunIdentifiedContracts = Arc>; + +/// Contracts identified as targets during a fuzz run. +/// During execution, any newly created contract is added as target and used through the rest of +/// the fuzz run if the collection is updatable (no `targetContract` specified in `setUp`). +#[derive(Clone, Debug)] +pub struct FuzzRunIdentifiedContracts { + /// Contracts identified as targets during a fuzz run. + pub targets: Arc>, + /// Whether target contracts are updatable or not. + pub is_updatable: bool, +} + +impl FuzzRunIdentifiedContracts { + pub fn new(targets: TargetedContracts, is_updatable: bool) -> Self { + Self { targets: Arc::new(Mutex::new(targets)), is_updatable } + } +} /// (Sender, (TargetContract, Calldata)) pub type BasicTxDetails = (Address, (Address, Bytes)); diff --git a/crates/evm/fuzz/src/strategies/invariants.rs b/crates/evm/fuzz/src/strategies/invariants.rs index c98e84598d1b..137e70852366 100644 --- a/crates/evm/fuzz/src/strategies/invariants.rs +++ b/crates/evm/fuzz/src/strategies/invariants.rs @@ -16,7 +16,7 @@ pub fn override_call_strat( target: Arc>, calldata_fuzz_config: CalldataFuzzDictionary, ) -> SBoxedStrategy<(Address, Bytes)> { - let contracts_ref = contracts.clone(); + let contracts_ref = contracts.targets.clone(); proptest::prop_oneof![ 80 => proptest::strategy::LazyJust::new(move || *target.read()), 20 => any::() @@ -27,7 +27,7 @@ pub fn override_call_strat( let calldata_fuzz_config = calldata_fuzz_config.clone(); let func = { - let contracts = contracts.lock(); + let contracts = contracts.targets.lock(); let (_, abi, functions) = contracts.get(&target_address).unwrap_or_else(|| { // Choose a random contract if target selected by lazy strategy is not in fuzz run // identified contracts. This can happen when contract is created in `setUp` call @@ -81,7 +81,7 @@ fn generate_call( any::() .prop_flat_map(move |selector| { let (contract, func) = { - let contracts = contracts.lock(); + let contracts = contracts.targets.lock(); let contracts = contracts.iter().filter(|(_, (_, abi, _))| !abi.functions.is_empty()); let (&contract, (_, abi, functions)) = selector.select(contracts); diff --git a/crates/evm/fuzz/src/strategies/state.rs b/crates/evm/fuzz/src/strategies/state.rs index 3c8f490f3a1e..3a3b17ed641e 100644 --- a/crates/evm/fuzz/src/strategies/state.rs +++ b/crates/evm/fuzz/src/strategies/state.rs @@ -304,10 +304,10 @@ pub fn collect_created_contracts( project_contracts: &ContractsByArtifact, setup_contracts: &ContractsByAddress, artifact_filters: &ArtifactFilters, - targeted_contracts: FuzzRunIdentifiedContracts, + targeted_contracts: &FuzzRunIdentifiedContracts, created_contracts: &mut Vec
, ) -> eyre::Result<()> { - let mut writable_targeted = targeted_contracts.lock(); + let mut writable_targeted = targeted_contracts.targets.lock(); for (address, account) in state_changeset { if !setup_contracts.contains_key(address) { if let (true, Some(code)) = (&account.is_touched(), &account.info.code) { diff --git a/crates/forge/tests/it/invariant.rs b/crates/forge/tests/it/invariant.rs index 4de3d3dcc301..49cbce5db7f2 100644 --- a/crates/forge/tests/it/invariant.rs +++ b/crates/forge/tests/it/invariant.rs @@ -149,6 +149,14 @@ async fn test_invariant() { "default/fuzz/invariant/common/InvariantCustomError.t.sol:InvariantCustomError", vec![("invariant_decode_error()", true, None, None, None)], ), + ( + "default/fuzz/invariant/target/FuzzedTargetContracts.t.sol:ExplicitTargetContract", + vec![("invariant_explicit_target()", true, None, None, None)], + ), + ( + "default/fuzz/invariant/target/FuzzedTargetContracts.t.sol:DynamicTargetContract", + vec![("invariant_dynamic_targets()", true, None, None, None)], + ), ]), ); } @@ -436,3 +444,30 @@ async fn test_invariant_decode_custom_error() { )]), ); } + +#[tokio::test(flavor = "multi_thread")] +async fn test_invariant_fuzzed_selected_targets() { + let filter = Filter::new(".*", ".*", ".*fuzz/invariant/target/FuzzedTargetContracts.t.sol"); + let mut runner = TEST_DATA_DEFAULT.runner(); + runner.test_options.invariant.fail_on_revert = true; + let results = runner.test_collect(&filter); + assert_multiple( + &results, + BTreeMap::from([ + ( + "default/fuzz/invariant/target/FuzzedTargetContracts.t.sol:ExplicitTargetContract", + vec![("invariant_explicit_target()", true, None, None, None)], + ), + ( + "default/fuzz/invariant/target/FuzzedTargetContracts.t.sol:DynamicTargetContract", + vec![( + "invariant_dynamic_targets()", + false, + Some("revert: wrong target selector called".into()), + None, + None, + )], + ), + ]), + ); +} diff --git a/testdata/default/fuzz/invariant/target/FuzzedTargetContracts.t.sol b/testdata/default/fuzz/invariant/target/FuzzedTargetContracts.t.sol new file mode 100644 index 000000000000..7988d5c8a2c3 --- /dev/null +++ b/testdata/default/fuzz/invariant/target/FuzzedTargetContracts.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity 0.8.18; + +import "ds-test/test.sol"; + +interface Vm { + function etch(address target, bytes calldata newRuntimeBytecode) external; +} + +// https://github.com/foundry-rs/foundry/issues/5625 +// https://github.com/foundry-rs/foundry/issues/6166 +// `Target.wrongSelector` is not called when handler added as `targetContract` +// `Target.wrongSelector` is called (and test fails) when no `targetContract` set +contract Target { + uint256 count; + + function wrongSelector() external { + revert("wrong target selector called"); + } + + function goodSelector() external { + count++; + } +} + +contract Handler is DSTest { + function increment() public { + Target(0x6B175474E89094C44Da98b954EedeAC495271d0F).goodSelector(); + } +} + +contract ExplicitTargetContract is DSTest { + Vm vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + Handler handler; + + function setUp() public { + Target target = new Target(); + bytes memory targetCode = address(target).code; + vm.etch(address(0x6B175474E89094C44Da98b954EedeAC495271d0F), targetCode); + + handler = new Handler(); + } + + function targetContracts() public returns (address[] memory) { + address[] memory addrs = new address[](1); + addrs[0] = address(handler); + return addrs; + } + + function invariant_explicit_target() public {} +} + +contract DynamicTargetContract is DSTest { + Vm vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + Handler handler; + + function setUp() public { + Target target = new Target(); + bytes memory targetCode = address(target).code; + vm.etch(address(0x6B175474E89094C44Da98b954EedeAC495271d0F), targetCode); + + handler = new Handler(); + } + + function invariant_dynamic_targets() public {} +}