diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 99665df2e5aa6..11be0b72adc96 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -30,7 +30,6 @@ use parking_lot::RwLock; use proptest::{strategy::Strategy, test_runner::TestRunner}; use result::{assert_after_invariant, assert_invariants, can_continue}; use revm::state::Account; -use shrink::shrink_sequence; use std::{ collections::{HashMap as Map, btree_map::Entry}, sync::Arc, @@ -323,6 +322,10 @@ impl<'a> InvariantExecutor<'a> { } } + pub fn config(self) -> InvariantConfig { + self.config + } + /// Fuzzes any deployed contract and checks any broken invariant at `invariant_address`. pub fn invariant_fuzz( &mut self, diff --git a/crates/evm/evm/src/executors/invariant/replay.rs b/crates/evm/evm/src/executors/invariant/replay.rs index a9d28bfff9ced..50276ca10cf48 100644 --- a/crates/evm/evm/src/executors/invariant/replay.rs +++ b/crates/evm/evm/src/executors/invariant/replay.rs @@ -1,18 +1,15 @@ -use super::{ - call_after_invariant_function, call_invariant_function, error::FailedInvariantCaseData, - shrink_sequence, -}; -use crate::executors::{EarlyExit, Executor}; +use super::{call_after_invariant_function, call_invariant_function}; +use crate::executors::{EarlyExit, Executor, invariant::shrink::shrink_sequence}; use alloy_dyn_abi::JsonAbiExt; use alloy_primitives::{Log, U256, map::HashMap}; use eyre::Result; use foundry_common::{ContractsByAddress, ContractsByArtifact}; +use foundry_config::InvariantConfig; use foundry_evm_coverage::HitMaps; use foundry_evm_fuzz::{BaseCounterExample, BasicTxDetails, invariant::InvariantContract}; use foundry_evm_traces::{TraceKind, TraceMode, Traces, load_contracts}; use indicatif::ProgressBar; use parking_lot::RwLock; -use proptest::test_runner::TestError; use std::sync::Arc; /// Replays a call sequence for collecting logs and traces. @@ -98,9 +95,11 @@ pub fn replay_run( /// Replays the error case, shrinks the failing sequence and collects all necessary traces. #[expect(clippy::too_many_arguments)] pub fn replay_error( - failed_case: &FailedInvariantCaseData, - invariant_contract: &InvariantContract<'_>, + config: InvariantConfig, mut executor: Executor, + calls: &[BasicTxDetails], + inner_sequence: Option>>, + invariant_contract: &InvariantContract<'_>, known_contracts: &ContractsByArtifact, ided_contracts: ContractsByAddress, logs: &mut Vec, @@ -108,40 +107,29 @@ pub fn replay_error( line_coverage: &mut Option, deprecated_cheatcodes: &mut HashMap<&'static str, Option<&'static str>>, progress: Option<&ProgressBar>, - show_solidity: bool, early_exit: &EarlyExit, ) -> Result> { - match failed_case.test_error { - // Don't use at the moment. - TestError::Abort(_) => Ok(vec![]), - TestError::Fail(_, ref calls) => { - // Shrink sequence of failed calls. - let calls = shrink_sequence( - failed_case, - calls, - &executor, - invariant_contract.call_after_invariant, - progress, - early_exit, - )?; + // Shrink sequence of failed calls. + let calls = + shrink_sequence(&config, invariant_contract, calls, &executor, progress, early_exit)?; - set_up_inner_replay(&mut executor, &failed_case.inner_sequence); - - // Replay calls to get the counterexample and to collect logs, traces and coverage. - replay_run( - invariant_contract, - executor, - known_contracts, - ided_contracts, - logs, - traces, - line_coverage, - deprecated_cheatcodes, - &calls, - show_solidity, - ) - } + if let Some(sequence) = inner_sequence { + set_up_inner_replay(&mut executor, &sequence); } + + // Replay calls to get the counterexample and to collect logs, traces and coverage. + replay_run( + invariant_contract, + executor, + known_contracts, + ided_contracts, + logs, + traces, + line_coverage, + deprecated_cheatcodes, + &calls, + config.show_solidity, + ) } /// Sets up the calls generated by the internal fuzzer, if they exist. diff --git a/crates/evm/evm/src/executors/invariant/shrink.rs b/crates/evm/evm/src/executors/invariant/shrink.rs index de112c1b9b831..2788348b06193 100644 --- a/crates/evm/evm/src/executors/invariant/shrink.rs +++ b/crates/evm/evm/src/executors/invariant/shrink.rs @@ -1,12 +1,11 @@ use crate::executors::{ EarlyExit, Executor, - invariant::{ - call_after_invariant_function, call_invariant_function, error::FailedInvariantCaseData, - }, + invariant::{call_after_invariant_function, call_invariant_function}, }; use alloy_primitives::{Address, Bytes, U256}; +use foundry_config::InvariantConfig; use foundry_evm_core::constants::MAGIC_ASSUME; -use foundry_evm_fuzz::BasicTxDetails; +use foundry_evm_fuzz::{BasicTxDetails, invariant::InvariantContract}; use indicatif::ProgressBar; use proptest::bits::{BitSetLike, VarBitSet}; @@ -33,15 +32,11 @@ impl CallSequenceShrinker { } } -/// Shrinks the failure case to its smallest sequence of calls. -/// -/// The shrunk call sequence always respect the order failure is reproduced as it is tested -/// top-down. pub(crate) fn shrink_sequence( - failed_case: &FailedInvariantCaseData, + config: &InvariantConfig, + invariant_contract: &InvariantContract<'_>, calls: &[BasicTxDetails], executor: &Executor, - call_after_invariant: bool, progress: Option<&ProgressBar>, early_exit: &EarlyExit, ) -> eyre::Result> { @@ -49,15 +44,16 @@ pub(crate) fn shrink_sequence( // Reset run count and display shrinking message. if let Some(progress) = progress { - progress.set_length(failed_case.shrink_run_limit as usize as u64); + progress.set_length(config.shrink_run_limit as u64); progress.reset(); progress.set_message(" Shrink"); } + let target_address = invariant_contract.address; + let calldata: Bytes = invariant_contract.invariant_function.selector().to_vec().into(); // Special case test: the invariant is *unsatisfiable* - it took 0 calls to // break the invariant -- consider emitting a warning. - let (_, success) = - call_invariant_function(executor, failed_case.addr, failed_case.calldata.clone())?; + let (_, success) = call_invariant_function(executor, target_address, calldata.clone())?; if !success { return Ok(vec![]); } @@ -65,7 +61,7 @@ pub(crate) fn shrink_sequence( let mut call_idx = 0; let mut shrinker = CallSequenceShrinker::new(calls.len()); - for _ in 0..failed_case.shrink_run_limit { + for _ in 0..config.shrink_run_limit { if early_exit.should_stop() { break; } @@ -77,10 +73,10 @@ pub(crate) fn shrink_sequence( executor.clone(), calls, shrinker.current().collect(), - failed_case.addr, - failed_case.calldata.clone(), - failed_case.fail_on_revert, - call_after_invariant, + target_address, + calldata.clone(), + config.fail_on_revert, + invariant_contract.call_after_invariant, ) { // If candidate sequence still fails, shrink until shortest possible. Ok((false, _)) if shrinker.included_calls.count() == 1 => break, diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 157e8f35f7502..721291ecb3e34 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -762,6 +762,14 @@ impl<'a> FunctionRunner<'a> { }; let show_solidity = invariant_config.show_solidity; + let progress = start_fuzz_progress( + self.cr.progress, + self.cr.name, + &func.name, + invariant_config.timeout, + invariant_config.runs, + ); + // Try to replay recorded failure if any. if let Some(mut call_sequence) = persisted_call_sequence(failure_file.as_path(), test_bytecode) @@ -790,26 +798,51 @@ impl<'a> FunctionRunner<'a> { invariant_contract.call_after_invariant, ) && !success { - let _ = sh_warn!( - "\ - Replayed invariant failure from {:?} file. \ - Run `forge clean` or remove file to ignore failure and to continue invariant test campaign.", + let warn = format!( + "Replayed invariant failure from {:?} file. \nRun `forge clean` or remove file to ignore failure and to continue invariant test campaign.", failure_file.as_path() ); - // If sequence still fails then replay error to collect traces and - // exit without executing new runs. - let _ = replay_run( - &invariant_contract, + + if let Some(ref progress) = progress { + progress.set_prefix(format!("{}\n{warn}\n", &func.name)); + } else { + let _ = sh_warn!("{warn}"); + } + + // If sequence still fails then replay error to collect traces and exit without + // executing new runs. + match replay_error( + evm.config(), self.clone_executor(), + &txes, + None, + &invariant_contract, &self.cr.mcr.known_contracts, identified_contracts.clone(), &mut self.result.logs, &mut self.result.traces, &mut self.result.line_coverage, &mut self.result.deprecated_cheatcodes, - &txes, - show_solidity, - ); + progress.as_ref(), + &self.tcfg.early_exit, + ) { + Ok(replayed_call_sequence) => { + if !replayed_call_sequence.is_empty() { + call_sequence = replayed_call_sequence; + // Persist error in invariant failure dir. + record_invariant_failure( + failure_dir.as_path(), + failure_file.as_path(), + &call_sequence, + test_bytecode, + ); + } + } + Err(err) => { + error!(%err, "Failed to replay invariant error"); + } + } + self.result.invariant_replay_fail( replayed_entirely, &invariant_contract.invariant_function.name, @@ -819,13 +852,6 @@ impl<'a> FunctionRunner<'a> { } } - let progress = start_fuzz_progress( - self.cr.progress, - self.cr.name, - &func.name, - invariant_config.timeout, - invariant_config.runs, - ); let invariant_result = match evm.invariant_fuzz( invariant_contract.clone(), &self.setup.fuzz_fixtures, @@ -853,49 +879,53 @@ impl<'a> FunctionRunner<'a> { | InvariantFuzzError::Revert(case_data) => { // Replay error to create counterexample and to collect logs, traces and // coverage. - match replay_error( - &case_data, - &invariant_contract, - self.clone_executor(), - &self.cr.mcr.known_contracts, - identified_contracts.clone(), - &mut self.result.logs, - &mut self.result.traces, - &mut self.result.line_coverage, - &mut self.result.deprecated_cheatcodes, - progress.as_ref(), - show_solidity, - &self.tcfg.early_exit, - ) { - Ok(call_sequence) => { - if !call_sequence.is_empty() { - // Persist error in invariant failure dir. - if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) { - error!(%err, "Failed to create invariant failure dir"); - } else if let Err(err) = foundry_common::fs::write_json_file( - failure_file.as_path(), - &InvariantPersistedFailure { - call_sequence: call_sequence.clone(), - driver_bytecode: Some(test_bytecode.clone()), - }, - ) { - error!(%err, "Failed to record call sequence"); + match case_data.test_error { + TestError::Abort(_) => {} + TestError::Fail(_, ref calls) => { + match replay_error( + evm.config(), + self.clone_executor(), + calls, + Some(case_data.inner_sequence), + &invariant_contract, + &self.cr.mcr.known_contracts, + identified_contracts.clone(), + &mut self.result.logs, + &mut self.result.traces, + &mut self.result.line_coverage, + &mut self.result.deprecated_cheatcodes, + progress.as_ref(), + &self.tcfg.early_exit, + ) { + Ok(call_sequence) => { + if !call_sequence.is_empty() { + // Persist error in invariant failure dir. + record_invariant_failure( + failure_dir.as_path(), + failure_file.as_path(), + &call_sequence, + test_bytecode, + ); + + let original_seq_len = if let TestError::Fail(_, calls) = + &case_data.test_error + { + calls.len() + } else { + call_sequence.len() + }; + + counterexample = Some(CounterExample::Sequence( + original_seq_len, + call_sequence, + )) + } + } + Err(err) => { + error!(%err, "Failed to replay invariant error"); } - - let original_seq_len = - if let TestError::Fail(_, calls) = &case_data.test_error { - calls.len() - } else { - call_sequence.len() - }; - - counterexample = - Some(CounterExample::Sequence(original_seq_len, call_sequence)) } } - Err(err) => { - error!(%err, "Failed to replay invariant error"); - } }; } InvariantFuzzError::MaxAssumeRejects(_) => {} @@ -1135,3 +1165,26 @@ fn test_paths( let failure_file = canonicalized(failures_dir.join(test_name)); (failures_dir, failure_file) } + +/// Helper function to persist invariant failure. +fn record_invariant_failure( + failure_dir: &Path, + failure_file: &Path, + call_sequence: &[BaseCounterExample], + test_bytecode: &Bytes, +) { + if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) { + error!(%err, "Failed to create invariant failure dir"); + return; + } + + if let Err(err) = foundry_common::fs::write_json_file( + failure_file, + &InvariantPersistedFailure { + call_sequence: call_sequence.to_owned(), + driver_bytecode: Some(test_bytecode.clone()), + }, + ) { + error!(%err, "Failed to record call sequence"); + } +}