Skip to main content

forge/
runner.rs

1//! The Forge test runner.
2
3use crate::{
4    MultiContractRunner, TestFilter,
5    coverage::HitMaps,
6    fuzz::{BaseCounterExample, FuzzTestResult},
7    multi_runner::{TestContract, TestRunnerConfig},
8    progress::{TestsProgress, start_fuzz_progress},
9    result::{InvariantFailure, SuiteResult, TestResult, TestSetup},
10};
11use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
12use alloy_json_abi::Function;
13use alloy_primitives::{Address, Bytes, Selector, U256, address, map::HashMap};
14use eyre::Result;
15use foundry_common::{TestFunctionExt, TestFunctionKind, contracts::ContractsByAddress};
16use foundry_compilers::utils::canonicalized;
17use foundry_config::{Config, FuzzCorpusConfig, InvariantConfig};
18use foundry_evm::{
19    constants::CALLER,
20    core::evm::FoundryEvmNetwork,
21    decode::RevertDecoder,
22    executors::{
23        CallResult, EvmError, Executor, ITest, RawCallResult,
24        fuzz::FuzzedExecutor,
25        invariant::{
26            CheckSequenceOptions, HandlerAssertionFailure, InvariantExecutor, InvariantFuzzError,
27            check_sequence, replay_error, replay_handler_failure_sequence, replay_run,
28        },
29    },
30    fuzz::{
31        BasicTxDetails, CallDetails, CounterExample, FuzzFixtures, fixture_name,
32        invariant::{InvariantContract, InvariantSettings, is_optimization_invariant},
33        strategies::EvmFuzzState,
34    },
35    revm::primitives::hardfork::SpecId,
36    traces::{TraceKind, TraceMode, load_contracts},
37};
38use itertools::Itertools;
39use proptest::test_runner::{RngAlgorithm, TestError, TestRng, TestRunner};
40use rayon::prelude::*;
41use serde::{Deserialize, Serialize};
42use std::{
43    borrow::Cow,
44    cmp::min,
45    collections::BTreeMap,
46    ops::Deref,
47    path::{Path, PathBuf},
48    sync::Arc,
49    time::Instant,
50};
51use tokio::signal;
52use tracing::Span;
53
54/// When running tests, we deploy all external libraries present in the project. To avoid additional
55/// libraries affecting nonces of senders used in tests, we are using separate address to
56/// predeploy libraries.
57///
58/// `address(uint160(uint256(keccak256("foundry library deployer"))))`
59pub const LIBRARY_DEPLOYER: Address = address!("0x1F95D37F27EA0dEA9C252FC09D5A6eaA97647353");
60
61/// A type that executes all tests of a contract
62pub struct ContractRunner<'a, FEN: FoundryEvmNetwork> {
63    /// The name of the contract.
64    name: &'a str,
65    /// The data of the contract.
66    contract: &'a TestContract,
67    /// The EVM executor.
68    executor: Executor<FEN>,
69    /// Overall test run progress.
70    progress: Option<&'a TestsProgress>,
71    /// The handle to the tokio runtime.
72    tokio_handle: &'a tokio::runtime::Handle,
73    /// The span of the contract.
74    span: tracing::Span,
75    /// The contract-level configuration.
76    tcfg: Cow<'a, TestRunnerConfig<FEN>>,
77    /// The parent runner.
78    mcr: &'a MultiContractRunner<FEN>,
79}
80
81impl<'a, FEN: FoundryEvmNetwork> Deref for ContractRunner<'a, FEN> {
82    type Target = Cow<'a, TestRunnerConfig<FEN>>;
83
84    #[inline(always)]
85    fn deref(&self) -> &Self::Target {
86        &self.tcfg
87    }
88}
89
90impl<'a, FEN: FoundryEvmNetwork> ContractRunner<'a, FEN> {
91    pub const fn new(
92        name: &'a str,
93        contract: &'a TestContract,
94        executor: Executor<FEN>,
95        progress: Option<&'a TestsProgress>,
96        tokio_handle: &'a tokio::runtime::Handle,
97        span: Span,
98        mcr: &'a MultiContractRunner<FEN>,
99    ) -> Self {
100        Self {
101            name,
102            contract,
103            executor,
104            progress,
105            tokio_handle,
106            span,
107            tcfg: Cow::Borrowed(&mcr.tcfg),
108            mcr,
109        }
110    }
111
112    /// Returns `true` if `func` should run in the current multi-network pass.
113    ///
114    /// In single-pass mode (no inline network overrides) every function passes.
115    /// In multi-pass mode:
116    /// - Default pass (`pass_network = None`): includes functions *without* an override annotation.
117    /// - Override pass (`pass_network = Some(v)`): includes only functions annotated with `v`.
118    fn function_matches_network_pass(&self, func: &Function) -> bool {
119        let multi = &self.mcr.tcfg.multi_network;
120        if multi.all_override_networks.is_empty() {
121            return true;
122        }
123        let profile = &self.tcfg.config.profile;
124        let func_network = self.mcr.inline_config.network_for(profile, self.name, &func.name);
125        match &multi.pass_network {
126            None => func_network.is_none_or(|n| !multi.all_override_networks.contains(&n)),
127            Some(target) => func_network.as_ref() == Some(target),
128        }
129    }
130
131    /// Deploys the test contract inside the runner from the sending account, and optionally runs
132    /// the `setUp` function on the test contract.
133    pub fn setup(&mut self, call_setup: bool) -> TestSetup {
134        self._setup(call_setup).unwrap_or_else(|err| {
135            if err.to_string().contains("skipped") {
136                TestSetup::skipped(err.to_string())
137            } else {
138                TestSetup::failed(err.to_string())
139            }
140        })
141    }
142
143    fn _setup(&mut self, call_setup: bool) -> Result<TestSetup> {
144        trace!(call_setup, "setting up");
145
146        self.apply_contract_inline_config()?;
147
148        // We max out their balance so that they can deploy and make calls.
149        self.executor.set_balance(self.sender, U256::MAX)?;
150        self.executor.set_balance(CALLER, U256::MAX)?;
151
152        // We set the nonce of the deployer accounts to 1 to get the same addresses as DappTools.
153        self.executor.set_nonce(self.sender, 1)?;
154
155        // Deploy libraries.
156        self.executor.set_balance(LIBRARY_DEPLOYER, U256::MAX)?;
157
158        let mut result = TestSetup::default();
159        for code in &self.mcr.libs_to_deploy {
160            let deploy_result = self.executor.deploy(
161                LIBRARY_DEPLOYER,
162                code.clone(),
163                U256::ZERO,
164                Some(&self.mcr.revert_decoder),
165            );
166
167            // Record deployed library address.
168            if let Ok(deployed) = &deploy_result {
169                result.deployed_libs.push(deployed.address);
170            }
171
172            let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
173            result.extend(raw, TraceKind::Deployment);
174            if reason.is_some() {
175                debug!(?reason, "deployment of library failed");
176                result.reason = reason;
177                return Ok(result);
178            }
179        }
180
181        let address = self.sender.create(self.executor.get_nonce(self.sender)?);
182        result.address = address;
183
184        // Set the contracts initial balance before deployment, so it is available during
185        // construction
186        self.executor.set_balance(address, self.initial_balance())?;
187
188        // Deploy the test contract
189        let deploy_result = self.executor.deploy(
190            self.sender,
191            self.contract.bytecode.clone(),
192            U256::ZERO,
193            Some(&self.mcr.revert_decoder),
194        );
195
196        result.deployment_failure = deploy_result.is_err();
197
198        if let Ok(dr) = &deploy_result {
199            debug_assert_eq!(dr.address, address);
200        }
201        let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
202        result.extend(raw, TraceKind::Deployment);
203        if reason.is_some() {
204            debug!(?reason, "deployment of test contract failed");
205            result.reason = reason;
206            return Ok(result);
207        }
208
209        // Reset `self.sender`s, `CALLER`s and `LIBRARY_DEPLOYER`'s balance to the initial balance.
210        self.executor.set_balance(self.sender, self.initial_balance())?;
211        self.executor.set_balance(CALLER, self.initial_balance())?;
212        self.executor.set_balance(LIBRARY_DEPLOYER, self.initial_balance())?;
213
214        self.executor.deploy_create2_deployer()?;
215
216        // Optionally call the `setUp` function
217        if call_setup {
218            trace!("calling setUp");
219            let res = self.executor.setup(None, address, Some(&self.mcr.revert_decoder));
220            let (raw, reason) = RawCallResult::from_evm_result(res)?;
221            result.extend(raw, TraceKind::Setup);
222            result.reason = reason;
223        }
224
225        result.fuzz_fixtures = self.fuzz_fixtures(address);
226
227        Ok(result)
228    }
229
230    fn initial_balance(&self) -> U256 {
231        self.evm_opts.initial_balance
232    }
233
234    /// Configures this runner with the inline configuration for the contract.
235    fn apply_contract_inline_config(&mut self) -> Result<()> {
236        if self.inline_config.contains_contract(self.name) {
237            let new_config = Arc::new(self.inline_config(None)?);
238            self.tcfg.to_mut().reconfigure_with(new_config);
239            let prev_tracer = self.executor.inspector_mut().tracer.take();
240            self.tcfg.configure_executor(&mut self.executor);
241            // Don't set tracer here.
242            self.executor.inspector_mut().tracer = prev_tracer;
243        }
244        Ok(())
245    }
246
247    /// Returns the configuration for a contract or function.
248    fn inline_config(&self, func: Option<&Function>) -> Result<Config> {
249        let function = func.map(|f| f.name.as_str()).unwrap_or("");
250        let config = self
251            .config
252            .merge_inline_provider(self.mcr.inline_config.provide(self.name, function))?;
253        Ok(config)
254    }
255
256    /// Collect fixtures from test contract.
257    ///
258    /// Fixtures can be defined:
259    /// - as storage arrays in test contract, prefixed with `fixture`
260    /// - as functions prefixed with `fixture` and followed by parameter name to be fuzzed
261    ///
262    /// Storage array fixtures:
263    /// `uint256[] public fixture_amount = [1, 2, 3];`
264    /// define an array of uint256 values to be used for fuzzing `amount` named parameter in scope
265    /// of the current test.
266    ///
267    /// Function fixtures:
268    /// `function fixture_owner() public returns (address[] memory){}`
269    /// returns an array of addresses to be used for fuzzing `owner` named parameter in scope of the
270    /// current test.
271    fn fuzz_fixtures(&mut self, address: Address) -> FuzzFixtures {
272        let mut fixtures = HashMap::default();
273        let fixture_functions = self.contract.abi.functions().filter(|func| func.is_fixture());
274        for func in fixture_functions {
275            if func.inputs.is_empty() {
276                // Read fixtures declared as functions.
277                if let Ok(CallResult { raw: _, decoded_result }) =
278                    self.executor.call(CALLER, address, func, &[], U256::ZERO, None)
279                {
280                    fixtures.insert(fixture_name(func.name.clone()), decoded_result);
281                }
282            } else {
283                // For reading fixtures from storage arrays we collect values by calling the
284                // function with incremented indexes until there's an error.
285                let mut vals = Vec::new();
286                let mut index = 0;
287                loop {
288                    if let Ok(CallResult { raw: _, decoded_result }) = self.executor.call(
289                        CALLER,
290                        address,
291                        func,
292                        &[DynSolValue::Uint(U256::from(index), 256)],
293                        U256::ZERO,
294                        None,
295                    ) {
296                        vals.push(decoded_result);
297                    } else {
298                        // No result returned for this index, we reached the end of storage
299                        // array or the function is not a valid fixture.
300                        break;
301                    }
302                    index += 1;
303                }
304                fixtures.insert(fixture_name(func.name.clone()), DynSolValue::Array(vals));
305            };
306        }
307        FuzzFixtures::new(fixtures)
308    }
309
310    /// Runs all tests for a contract whose names match the provided regular expression
311    pub fn run_tests(mut self, filter: &dyn TestFilter) -> SuiteResult {
312        let start = Instant::now();
313        let mut warnings = Vec::new();
314
315        // Check if `setUp` function with valid signature declared.
316        let setup_fns: Vec<_> =
317            self.contract.abi.functions().filter(|func| func.name.is_setup()).collect();
318        let call_setup = setup_fns.len() == 1 && setup_fns[0].name == "setUp";
319        // There is a single miss-cased `setUp` function, so we add a warning
320        for &setup_fn in &setup_fns {
321            if setup_fn.name != "setUp" {
322                warnings.push(format!(
323                    "Found invalid setup function \"{}\" did you mean \"setUp()\"?",
324                    setup_fn.signature()
325                ));
326            }
327        }
328
329        // There are multiple setUp function, so we return a single test result for `setUp`
330        if setup_fns.len() > 1 {
331            return SuiteResult::new(
332                start.elapsed(),
333                [("setUp()".to_string(), TestResult::fail("multiple setUp functions".to_string()))]
334                    .into(),
335                warnings,
336            );
337        }
338
339        // Check if `afterInvariant` function with valid signature declared.
340        let after_invariant_fns: Vec<_> =
341            self.contract.abi.functions().filter(|func| func.name.is_after_invariant()).collect();
342        if after_invariant_fns.len() > 1 {
343            // Return a single test result failure if multiple functions declared.
344            return SuiteResult::new(
345                start.elapsed(),
346                [(
347                    "afterInvariant()".to_string(),
348                    TestResult::fail("multiple afterInvariant functions".to_string()),
349                )]
350                .into(),
351                warnings,
352            );
353        }
354        let call_after_invariant = after_invariant_fns.first().is_some_and(|after_invariant_fn| {
355            let match_sig = after_invariant_fn.name == "afterInvariant";
356            if !match_sig {
357                warnings.push(format!(
358                    "Found invalid afterInvariant function \"{}\" did you mean \"afterInvariant()\"?",
359                    after_invariant_fn.signature()
360                ));
361            }
362            match_sig
363        });
364
365        // Invariant testing requires tracing to figure out what contracts were created.
366        // We also want to disable `debug` for setup since we won't be using those traces.
367        let invariant_fns: Vec<_> =
368            self.contract.abi.functions().filter(|func| func.is_invariant_test()).collect();
369
370        // Validate signatures up front: invariant functions must take no parameters. Without
371        // this, parameterized `invariant_*` functions would slip into `assert_all` secondaries
372        // and fail with a confusing "selector not found" / decode error mid-campaign. Reject
373        // here with a per-function result so the failure is obvious to the user.
374        let invalid_invariants: Vec<_> = invariant_fns
375            .iter()
376            .filter(|f| !f.inputs.is_empty())
377            .map(|f| {
378                (
379                    f.signature(),
380                    TestResult::fail(format!(
381                        "invariant `{}` must take no parameters",
382                        f.signature()
383                    )),
384                )
385            })
386            .collect();
387        if !invalid_invariants.is_empty() {
388            return SuiteResult::new(
389                start.elapsed(),
390                invalid_invariants.into_iter().collect(),
391                warnings,
392            );
393        }
394
395        let has_invariants = !invariant_fns.is_empty();
396
397        let prev_tracer = self.executor.inspector_mut().tracer.take();
398        if prev_tracer.is_some() || has_invariants {
399            self.executor.set_tracing(TraceMode::Call);
400        }
401
402        let setup_time = Instant::now();
403        let setup = self.setup(call_setup);
404        debug!("finished setting up in {:?}", setup_time.elapsed());
405
406        self.executor.inspector_mut().tracer = prev_tracer;
407
408        if setup.reason.is_some() {
409            // The setup failed, so we return a single test result for `setUp`
410            let fail_msg = if setup.deployment_failure {
411                "constructor()".to_string()
412            } else {
413                "setUp()".to_string()
414            };
415            return SuiteResult::new(
416                start.elapsed(),
417                [(fail_msg, TestResult::setup_result(setup))].into(),
418                warnings,
419            );
420        }
421
422        // Filter out functions sequentially since it's very fast and there is no need to do it
423        // in parallel.
424        let find_timer = Instant::now();
425        let functions = self
426            .contract
427            .abi
428            .functions()
429            .filter(|func| filter.matches_test_function(func))
430            .filter(|func| self.function_matches_network_pass(func))
431            .collect::<Vec<_>>();
432        debug!(
433            "Found {} test functions out of {} in {:?}",
434            functions.len(),
435            self.contract.abi.functions().count(),
436            find_timer.elapsed(),
437        );
438
439        let identified_contracts = has_invariants.then(|| {
440            load_contracts(setup.traces.iter().map(|(_, t)| &t.arena), &self.mcr.known_contracts)
441        });
442
443        let test_fail_functions =
444            functions.iter().filter(|func| func.test_function_kind().is_any_test_fail());
445        if test_fail_functions.clone().next().is_some() {
446            let fail = || {
447                TestResult::fail("`testFail*` has been removed. Consider changing to test_Revert[If|When]_Condition and expecting a revert".to_string())
448            };
449            let test_results = test_fail_functions.map(|func| (func.signature(), fail())).collect();
450            return SuiteResult::new(start.elapsed(), test_results, warnings);
451        }
452
453        let early_exit = &self.tcfg.early_exit;
454
455        if self.progress.is_some() {
456            let interrupt = early_exit.clone();
457            self.tokio_handle.spawn(async move {
458                signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
459                interrupt.record_ctrl_c();
460            });
461        }
462
463        let test_results = functions
464            .par_iter()
465            .filter_map(|&func| {
466                // Early exit if we're running with fail-fast and a test already failed.
467                if early_exit.should_stop() {
468                    return None;
469                }
470
471                let start = Instant::now();
472
473                let _guard = self.tokio_handle.enter();
474
475                let _guard;
476                let current_span = tracing::Span::current();
477                if current_span.is_none() || current_span.id() != self.span.id() {
478                    _guard = self.span.enter();
479                }
480
481                let sig = func.signature();
482                let kind = func.test_function_kind();
483
484                let _guard = debug_span!(
485                    "test",
486                    %kind,
487                    name = %if enabled!(tracing::Level::TRACE) { &sig } else { &func.name },
488                )
489                .entered();
490
491                let mut res = FunctionRunner::new(&self, &setup).run(
492                    func,
493                    invariant_fns.clone(),
494                    kind,
495                    call_after_invariant,
496                    identified_contracts.as_ref(),
497                );
498                res.duration = start.elapsed();
499
500                // Record test failure for early exit (only triggers if fail-fast is enabled).
501                if res.status.is_failure() {
502                    early_exit.record_failure();
503                }
504
505                Some((sig, res))
506            })
507            .collect::<BTreeMap<_, _>>();
508
509        let duration = start.elapsed();
510        SuiteResult::new(duration, test_results, warnings)
511    }
512}
513
514/// Executes a single test function, returning a [`TestResult`].
515struct FunctionRunner<'a, FEN: FoundryEvmNetwork> {
516    /// The function-level configuration.
517    tcfg: Cow<'a, TestRunnerConfig<FEN>>,
518    /// The EVM executor.
519    executor: Cow<'a, Executor<FEN>>,
520    /// The parent runner.
521    cr: &'a ContractRunner<'a, FEN>,
522    /// The address of the test contract.
523    address: Address,
524    /// The test setup result.
525    setup: &'a TestSetup,
526    /// The test result. Returned after running the test.
527    result: TestResult,
528}
529
530impl<'a, FEN: FoundryEvmNetwork> Deref for FunctionRunner<'a, FEN> {
531    type Target = Cow<'a, TestRunnerConfig<FEN>>;
532
533    #[inline(always)]
534    fn deref(&self) -> &Self::Target {
535        &self.tcfg
536    }
537}
538
539impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> {
540    fn new(cr: &'a ContractRunner<'a, FEN>, setup: &'a TestSetup) -> Self {
541        Self {
542            tcfg: match &cr.tcfg {
543                Cow::Borrowed(tcfg) => Cow::Borrowed(tcfg),
544                Cow::Owned(tcfg) => Cow::Owned(tcfg.clone()),
545            },
546            executor: Cow::Borrowed(&cr.executor),
547            cr,
548            address: setup.address,
549            setup,
550            result: TestResult::new(setup),
551        }
552    }
553
554    const fn revert_decoder(&self) -> &'a RevertDecoder {
555        &self.cr.mcr.revert_decoder
556    }
557
558    /// Configures this runner with the inline configuration for the contract.
559    fn apply_function_inline_config(&mut self, func: &Function) -> Result<()> {
560        if self.inline_config.contains_function(self.cr.name, &func.name) {
561            let new_config = Arc::new(self.cr.inline_config(Some(func))?);
562            self.tcfg.to_mut().reconfigure_with(new_config);
563            self.tcfg.configure_executor(self.executor.to_mut());
564        }
565        Ok(())
566    }
567
568    fn run(
569        mut self,
570        func: &Function,
571        invariants: Vec<&Function>,
572        kind: TestFunctionKind,
573        call_after_invariant: bool,
574        identified_contracts: Option<&ContractsByAddress>,
575    ) -> TestResult {
576        let fail_on_revert_for = |f: &Function| {
577            if self.inline_config.contains_function(self.cr.name, &f.name)
578                && let Ok(config) = self.cr.inline_config(Some(f))
579            {
580                return config.invariant.fail_on_revert;
581            }
582            self.config.invariant.fail_on_revert
583        };
584        let invariant_fns: Vec<_> =
585            invariants.into_iter().map(|f| (f, fail_on_revert_for(f))).collect();
586
587        if let Err(e) = self.apply_function_inline_config(func) {
588            self.result.single_fail(Some(e.to_string()));
589            return self.result;
590        }
591
592        match kind {
593            TestFunctionKind::UnitTest { .. } => self.run_unit_test(func),
594            TestFunctionKind::FuzzTest { .. } => self.run_fuzz_test(func),
595            TestFunctionKind::TableTest => self.run_table_test(func),
596            TestFunctionKind::InvariantTest => self.run_invariant_test(
597                func,
598                invariant_fns,
599                call_after_invariant,
600                identified_contracts.unwrap(),
601            ),
602            _ => unreachable!(),
603        }
604    }
605
606    /// Runs a single unit test.
607    ///
608    /// Applies before test txes (if any), runs current test and returns the `TestResult`.
609    ///
610    /// Before test txes are applied in order and state modifications committed to the EVM database
611    /// (therefore the unit test call will be made on modified state).
612    /// State modifications of before test txes and unit test function call are discarded after
613    /// test ends, similar to `eth_call`.
614    fn run_unit_test(mut self, func: &Function) -> TestResult {
615        // Prepare unit test execution.
616        if self.prepare_test(func).is_err() {
617            return self.result;
618        }
619
620        // Run current unit test.
621        let (mut raw_call_result, reason) = match self.executor.call(
622            self.sender,
623            self.address,
624            func,
625            &[],
626            U256::ZERO,
627            Some(self.revert_decoder()),
628        ) {
629            Ok(res) => (res.raw, None),
630            Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
631            Err(EvmError::Skip(reason)) => {
632                self.result.single_skip(reason);
633                return self.result;
634            }
635            Err(err) => {
636                self.result.single_fail(Some(err.to_string()));
637                return self.result;
638            }
639        };
640
641        let success =
642            self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, false);
643        self.result.single_result(success, reason, raw_call_result);
644        self.result
645    }
646
647    /// Runs a table test.
648    /// The parameters dataset (table) is created from defined parameter fixtures, therefore each
649    /// test table parameter should have the same number of fixtures defined.
650    /// E.g. for table test
651    /// - `table_test(uint256 amount, bool swap)` fixtures are defined as
652    /// - `uint256[] public fixtureAmount = [2, 5]`
653    /// - `bool[] public fixtureSwap = [true, false]` The `table_test` is then called with the pair
654    ///   of args `(2, true)` and `(5, false)`.
655    fn run_table_test(mut self, func: &Function) -> TestResult {
656        // Prepare unit test execution.
657        if self.prepare_test(func).is_err() {
658            return self.result;
659        }
660
661        // Extract and validate fixtures for the first table test parameter.
662        let Some(first_param) = func.inputs.first() else {
663            self.result.single_fail(Some("Table test should have at least one parameter".into()));
664            return self.result;
665        };
666
667        let Some(first_param_fixtures) =
668            &self.setup.fuzz_fixtures.param_fixtures(first_param.name())
669        else {
670            self.result.single_fail(Some("Table test should have fixtures defined".into()));
671            return self.result;
672        };
673
674        if first_param_fixtures.is_empty() {
675            self.result.single_fail(Some("Table test should have at least one fixture".into()));
676            return self.result;
677        }
678
679        let fixtures_len = first_param_fixtures.len();
680        let mut table_fixtures = vec![&first_param_fixtures[..]];
681
682        // Collect fixtures for remaining parameters.
683        for param in &func.inputs[1..] {
684            let param_name = param.name();
685            let Some(fixtures) = &self.setup.fuzz_fixtures.param_fixtures(param.name()) else {
686                self.result.single_fail(Some(format!("No fixture defined for param {param_name}")));
687                return self.result;
688            };
689
690            if fixtures.len() != fixtures_len {
691                self.result.single_fail(Some(format!(
692                    "{} fixtures defined for {param_name} (expected {})",
693                    fixtures.len(),
694                    fixtures_len
695                )));
696                return self.result;
697            }
698
699            table_fixtures.push(&fixtures[..]);
700        }
701
702        let progress = start_fuzz_progress(
703            self.cr.progress,
704            self.cr.name,
705            &func.name,
706            None,
707            fixtures_len as u32,
708        );
709
710        let mut result = FuzzTestResult::default();
711
712        for i in 0..fixtures_len {
713            if self.tcfg.early_exit.should_stop() {
714                return self.result;
715            }
716
717            // Increment progress bar.
718            if let Some(progress) = progress.as_ref() {
719                progress.inc(1);
720            }
721
722            let args = table_fixtures.iter().map(|row| row[i].clone()).collect_vec();
723            let (mut raw_call_result, reason) = match self.executor.call(
724                self.sender,
725                self.address,
726                func,
727                &args,
728                U256::ZERO,
729                Some(self.revert_decoder()),
730            ) {
731                Ok(res) => (res.raw, None),
732                Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
733                Err(EvmError::Skip(reason)) => {
734                    self.result.single_skip(reason);
735                    return self.result;
736                }
737                Err(err) => {
738                    self.result.single_fail(Some(err.to_string()));
739                    return self.result;
740                }
741            };
742
743            result.gas_by_case.push((raw_call_result.gas_used, raw_call_result.stipend));
744            result.logs.extend(raw_call_result.logs.clone());
745            result.labels.extend(raw_call_result.labels.clone());
746            HitMaps::merge_opt(&mut result.line_coverage, raw_call_result.line_coverage.clone());
747
748            let is_success =
749                self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, false);
750            // Record counterexample if test fails.
751            if !is_success {
752                result.counterexample =
753                    Some(CounterExample::Single(BaseCounterExample::from_fuzz_call(
754                        Bytes::from(func.abi_encode_input(&args).unwrap()),
755                        args,
756                        raw_call_result.traces.clone(),
757                    )));
758                result.reason = reason;
759                result.traces = raw_call_result.traces;
760                self.result.table_result(result);
761                return self.result;
762            }
763
764            // If it's the last iteration and all other runs succeeded, then use last call result
765            // for logs and traces.
766            if i == fixtures_len - 1 {
767                result.success = true;
768                result.traces = raw_call_result.traces;
769                self.result.table_result(result);
770                return self.result;
771            }
772        }
773
774        self.result
775    }
776
777    fn run_invariant_test(
778        mut self,
779        func: &Function,
780        invariants: Vec<(&Function, bool)>,
781        call_after_invariant: bool,
782        identified_contracts: &ContractsByAddress,
783    ) -> TestResult {
784        // First, run the test normally to see if it needs to be skipped.
785        if let Err(EvmError::Skip(reason)) = self.executor.call(
786            self.sender,
787            self.address,
788            func,
789            &[],
790            U256::ZERO,
791            Some(self.revert_decoder()),
792        ) {
793            self.result.invariant_skip(reason);
794            return self.result;
795        };
796
797        let runner = self.invariant_runner();
798        let invariant_config = self.config.invariant.clone();
799        let invariant_config = &invariant_config;
800
801        let mut executor = self.clone_executor();
802        // Enable edge coverage if running with coverage guided fuzzing or with edge coverage
803        // metrics (useful for benchmarking the fuzzer).
804        executor
805            .inspector_mut()
806            .collect_edge_coverage(invariant_config.corpus.collect_evm_edge_coverage());
807        executor.inspector_mut().collect_evm_cmp_log(invariant_config.corpus.collect_evm_cmp_log());
808        executor
809            .inspector_mut()
810            .collect_sancov_edges(invariant_config.corpus.collect_sancov_edges());
811        executor
812            .inspector_mut()
813            .collect_sancov_trace_cmp(invariant_config.corpus.collect_sancov_trace_cmp());
814        let mut config = invariant_config.clone();
815        let (failure_dir, failure_file) = test_paths(
816            &mut config.corpus,
817            invariant_config.failure_persist_dir.clone().unwrap(),
818            self.cr.name,
819            &func.name,
820        );
821
822        let mut evm = InvariantExecutor::new(
823            executor,
824            runner,
825            config,
826            identified_contracts,
827            &self.cr.mcr.known_contracts,
828        );
829        // Filter out additional invariants to test if we already have a persisted failure.
830        // Optimization mode only tracks the primary invariant's return value, so secondary
831        // boolean invariants are excluded to avoid silently skipping them.
832        let is_optimization = is_optimization_invariant(func);
833        // When the primary is an optimization invariant and the user has assert_all on, warn
834        // them once that secondary boolean invariants in the same contract are being skipped
835        // — assert_all has no effect under optimization mode, so silently dropping them would
836        // be a footgun.
837        if is_optimization && invariant_config.assert_all {
838            let dropped: Vec<&str> = invariants
839                .iter()
840                .filter(|(invariant_fn, _)| *invariant_fn != func)
841                .map(|(invariant_fn, _)| invariant_fn.name.as_str())
842                .collect();
843            if !dropped.is_empty() {
844                let _ = sh_warn!(
845                    "{}: assert_all is on but {} is an optimization invariant; \
846                     {} boolean invariant(s) skipped: {}. \
847                     Move them to a separate contract to run them.",
848                    self.cr.name,
849                    func.name,
850                    dropped.len(),
851                    dropped.join(", "),
852                );
853            }
854        }
855        // Compute current invariant settings up front so secondary persisted-failure handling
856        // can use the same compatibility check as the primary replay path below.
857        let current_settings = match evm.compute_settings(self.address) {
858            Ok(s) => s,
859            Err(e) => {
860                self.result.invariant_setup_fail(e);
861                return self.result;
862            }
863        };
864        // A secondary's persisted failure is only honored when its embedded settings still
865        // match the current run; stale caches fall back to a fresh campaign.
866        let secondary_has_compatible_persisted = |invariant_fn: &Function| {
867            persisted_call_sequence(
868                canonicalized(failure_dir.join(invariant_fn.name.clone())).as_path(),
869                &current_settings,
870            )
871            .is_some()
872        };
873        // Warn when secondaries are dropped because they already have persisted failures from a
874        // previous campaign. Symmetric with the primary's persisted-replay warning so users
875        // aren't surprised when fewer invariants appear in the report than their contract
876        // defines (Echidna/Medusa never skip properties between runs).
877        if !is_optimization && invariant_config.assert_all {
878            let persisted_skipped: Vec<&str> = invariants
879                .iter()
880                .filter(|(invariant_fn, _)| {
881                    *invariant_fn != func && secondary_has_compatible_persisted(invariant_fn)
882                })
883                .map(|(invariant_fn, _)| invariant_fn.name.as_str())
884                .collect();
885            if !persisted_skipped.is_empty() {
886                let _ = sh_warn!(
887                    "{}: {} invariant(s) skipped due to persisted failures: {}. \
888                     Run `forge clean` or delete files in {} to re-include.",
889                    self.cr.name,
890                    persisted_skipped.len(),
891                    persisted_skipped.join(", "),
892                    failure_dir.display(),
893                );
894            }
895        }
896        // Build the invariant list in source declaration order, retaining the anchor (`func`)
897        // and — under `assert_all` — every other invariant that doesn't already have a
898        // compatible persisted failure. Track the anchor's index so downstream consumers can
899        // resolve the campaign-anchor without searching by name.
900        let invariant_fns: Vec<(&Function, bool)> = invariants
901            .into_iter()
902            .filter(|(invariant_fn, _)| {
903                *invariant_fn == func
904                    || (!is_optimization
905                        && invariant_config.assert_all
906                        && !secondary_has_compatible_persisted(invariant_fn))
907            })
908            .collect();
909        let anchor_idx = invariant_fns
910            .iter()
911            .position(|(invariant_fn, _)| *invariant_fn == func)
912            .expect("anchor must be present in invariant_fns");
913        let invariant_contract = InvariantContract::new(
914            self.address,
915            self.cr.name,
916            invariant_fns,
917            anchor_idx,
918            call_after_invariant,
919            &self.cr.contract.abi,
920        );
921        let show_solidity = invariant_config.show_solidity;
922
923        let progress = start_fuzz_progress(
924            self.cr.progress,
925            self.cr.name,
926            &func.name,
927            invariant_config.timeout,
928            invariant_config.runs,
929        );
930
931        let replay_ctx = ReplayContext {
932            invariant_contract: &invariant_contract,
933            invariant_config,
934            revert_decoder: self.revert_decoder(),
935            show_solidity,
936        };
937
938        // Try to replay recorded failure if any.
939        if let Some(InvariantPersistedFailure { mut call_sequence, assertion_failure, .. }) =
940            persisted_call_sequence(failure_file.as_path(), &current_settings)
941        {
942            let (txes, replay) = replay_persisted_call_sequence(
943                &replay_ctx,
944                self.clone_executor(),
945                &mut call_sequence,
946                assertion_failure,
947            );
948            if let Ok((success, replayed_entirely, replay_reason)) = replay
949                && !success
950            {
951                let warn = format!(
952                    "Replayed invariant failure from {:?} file. \nRun `forge clean` or remove file to ignore failure and to continue invariant test campaign.",
953                    failure_file.as_path()
954                );
955
956                if let Some(ref progress) = progress {
957                    progress.set_prefix(format!("{}\n{warn}\n", func.name));
958                } else {
959                    let _ = sh_warn!("{warn}");
960                }
961
962                // If sequence still fails then replay error to collect traces and exit without
963                // executing new runs.
964                match replay_error(
965                    evm.config(),
966                    self.clone_executor(),
967                    &txes,
968                    None,
969                    assertion_failure,
970                    None, // check mode
971                    &invariant_contract,
972                    invariant_contract.anchor(),
973                    &self.cr.mcr.known_contracts,
974                    identified_contracts.clone(),
975                    &mut self.result.logs,
976                    &mut self.result.traces,
977                    &mut self.result.line_coverage,
978                    &mut self.result.deprecated_cheatcodes,
979                    progress.as_ref(),
980                    &self.tcfg.early_exit,
981                    None, // single-invariant replay path; no [i/N] counter
982                ) {
983                    Ok(replayed_call_sequence) if !replayed_call_sequence.is_empty() => {
984                        call_sequence = replayed_call_sequence;
985                        // Persist error in invariant failure dir.
986                        record_invariant_failure(
987                            failure_dir.as_path(),
988                            failure_file.as_path(),
989                            &call_sequence,
990                            &current_settings,
991                            assertion_failure,
992                        );
993                    }
994                    Ok(_) => {}
995                    Err(err) => {
996                        error!(%err, "Failed to replay invariant error");
997                    }
998                }
999
1000                self.result.invariant_replay_fail(
1001                    replayed_entirely,
1002                    &invariant_contract.anchor().name,
1003                    replay_reason,
1004                    call_sequence,
1005                );
1006                return self.result;
1007            }
1008        }
1009
1010        // Replay persisted handler bugs; feed still-reproducing ones into the campaign,
1011        // delete stale files in place.
1012        let persisted_handler_failures = replay_persisted_handler_failures(
1013            &failure_dir.join("handlers"),
1014            &current_settings,
1015            self.clone_executor(),
1016            &replay_ctx,
1017        );
1018
1019        let invariant_result = match evm.invariant_fuzz(
1020            invariant_contract.clone(),
1021            &self.setup.fuzz_fixtures,
1022            self.build_fuzz_state(true),
1023            progress.as_ref(),
1024            &self.tcfg.early_exit,
1025            persisted_handler_failures,
1026        ) {
1027            Ok(x) => x,
1028            Err(e) => {
1029                self.result.invariant_setup_fail(e);
1030                return self.result;
1031            }
1032        };
1033        // Merge coverage collected during invariant run with test setup coverage.
1034        self.result.merge_coverages(invariant_result.line_coverage);
1035
1036        let mut counterexample = None;
1037        // Success requires zero predicate breaks *and* zero handler-side assertion bugs.
1038        let success =
1039            invariant_result.errors.is_empty() && invariant_result.handler_errors.is_empty();
1040        let mut invariant_failures: Vec<InvariantFailure> = vec![];
1041        let mut any_failure_persisted = false;
1042
1043        if success {
1044            if let Some(best_value) = invariant_result.optimization_best_value {
1045                // Optimization mode: replay and shrink to find shortest best sequence.
1046                match replay_error(
1047                    evm.config(),
1048                    self.clone_executor(),
1049                    &invariant_result.optimization_best_sequence,
1050                    None,
1051                    false,
1052                    Some(best_value),
1053                    &invariant_contract,
1054                    invariant_contract.anchor(),
1055                    &self.cr.mcr.known_contracts,
1056                    identified_contracts.clone(),
1057                    &mut self.result.logs,
1058                    &mut self.result.traces,
1059                    &mut self.result.line_coverage,
1060                    &mut self.result.deprecated_cheatcodes,
1061                    progress.as_ref(),
1062                    &self.tcfg.early_exit,
1063                    None, // optimization mode is single-invariant; no [i/N] counter
1064                ) {
1065                    Ok(best_sequence) if !best_sequence.is_empty() => {
1066                        counterexample = Some(CounterExample::Sequence(
1067                            invariant_result.optimization_best_sequence.len(),
1068                            best_sequence,
1069                        ));
1070                    }
1071                    Err(err) => {
1072                        error!(%err, "Failed to replay optimization best sequence");
1073                    }
1074                    _ => {}
1075                }
1076            } else {
1077                // Standard check mode: replay last run for traces.
1078                if let Err(err) = replay_run(
1079                    &invariant_contract,
1080                    invariant_contract.anchor(),
1081                    self.clone_executor(),
1082                    &self.cr.mcr.known_contracts,
1083                    identified_contracts.clone(),
1084                    &mut self.result.logs,
1085                    &mut self.result.traces,
1086                    &mut self.result.line_coverage,
1087                    &mut self.result.deprecated_cheatcodes,
1088                    &invariant_result.last_run_inputs,
1089                    show_solidity,
1090                ) {
1091                    error!(%err, "Failed to replay last invariant run");
1092                }
1093            }
1094        } else {
1095            // Total broken invariants in this campaign — used to decorate the shrink progress
1096            // bar with `[i/N]` so users see how many shrinkers are queued behind the current
1097            // one. `errors` keys cover both the anchor and any broken secondaries.
1098            let total_broken = invariant_result.errors.len();
1099            // Replay-and-shrink the anchor's failure first (gets [1/N] on the progress bar),
1100            // then push it into `invariant_failures` as the first entry. Non-replayable error
1101            // variants (e.g. `MaxAssumeRejects`) still get an entry — without a counterexample
1102            // — so the reason is rendered.
1103            if let Some(error) = invariant_result.errors.get(&invariant_contract.anchor().name) {
1104                let anchor_counterexample = match error {
1105                    InvariantFuzzError::BrokenInvariant(case_data)
1106                    | InvariantFuzzError::Revert(case_data) => {
1107                        let TestError::Fail(_, ref calls) = case_data.test_error else {
1108                            unreachable!("FailedInvariantCaseData::new always sets TestError::Fail")
1109                        };
1110                        match replay_error(
1111                            evm.config(),
1112                            self.clone_executor(),
1113                            calls,
1114                            Some(case_data.inner_sequence.clone()),
1115                            case_data.assertion_failure,
1116                            None, // check mode
1117                            &invariant_contract,
1118                            invariant_contract.anchor(),
1119                            &self.cr.mcr.known_contracts,
1120                            identified_contracts.clone(),
1121                            &mut self.result.logs,
1122                            &mut self.result.traces,
1123                            &mut self.result.line_coverage,
1124                            &mut self.result.deprecated_cheatcodes,
1125                            progress.as_ref(),
1126                            &self.tcfg.early_exit,
1127                            Some((1, total_broken)),
1128                        ) {
1129                            Ok(call_sequence) if !call_sequence.is_empty() => {
1130                                record_invariant_failure(
1131                                    failure_dir.as_path(),
1132                                    failure_file.as_path(),
1133                                    &call_sequence,
1134                                    &current_settings,
1135                                    case_data.assertion_failure,
1136                                );
1137                                any_failure_persisted = true;
1138                                Some(CounterExample::Sequence(calls.len(), call_sequence))
1139                            }
1140                            Ok(_) => None,
1141                            Err(err) => {
1142                                error!(%err, "Failed to replay invariant error");
1143                                None
1144                            }
1145                        }
1146                    }
1147                    InvariantFuzzError::MaxAssumeRejects(_) => None,
1148                    // Handler bugs live in `handler_errors`; defensive None here.
1149                    InvariantFuzzError::HandlerAssertion(_) => None,
1150                };
1151                invariant_failures.push(InvariantFailure::Predicate {
1152                    name: invariant_contract.anchor().name.clone(),
1153                    reason: error.revert_reason().unwrap_or_default(),
1154                    counterexample: anchor_counterexample,
1155                    persisted_path: failure_file,
1156                    is_anchor: true,
1157                });
1158            }
1159
1160            // Shrink each broken non-primary invariant in turn so users get a ready-to-debug
1161            // counterexample for every failure in a single run. Loop is serial; on Ctrl+C we
1162            // still record every known secondary failure (without shrinking or persisting), so
1163            // the final report matches what the live progress bar showed.
1164            //
1165            // `next_position` tracks where this invariant sits in the broken queue (primary is
1166            // 1, secondaries follow). Only incremented when a secondary is actually shrunk so
1167            // the bar's `[i/N]` counter matches user-visible progress.
1168            let mut next_position = 2usize;
1169            // Iterate every invariant; skip the anchor (handled in the primary path above).
1170            for (idx, (invariant, _)) in invariant_contract.invariant_fns.iter().enumerate() {
1171                if idx == invariant_contract.anchor_idx {
1172                    continue;
1173                }
1174
1175                // Skip invariants whose counterexample is already persisted from a prior run
1176                // (those were filtered out of the live campaign earlier; `errors` won't contain
1177                // them, but the dir check is a belt-and-braces safety net). Use the same
1178                // settings-aware compatibility check as the filter so a stale persisted cache
1179                // doesn't suppress a freshly-broken secondary.
1180                let persisted_failure = canonicalized(failure_dir.join(invariant.name.clone()));
1181                if !secondary_has_compatible_persisted(invariant)
1182                    && let Some(error) = invariant_result.errors.get(&invariant.name)
1183                    && let InvariantFuzzError::BrokenInvariant(case_data)
1184                    | InvariantFuzzError::Revert(case_data) = error
1185                    && let TestError::Fail(_, ref calls) = case_data.test_error
1186                {
1187                    let original_seq_len = calls.len();
1188                    // On Ctrl+C: skip the (potentially long) replay+shrink, but still persist
1189                    // the un-shrunk sequence so the next run targeting this invariant picks it
1190                    // up and shrinks from the saved counterexample. The current run's output
1191                    // still gets a terse `name: reason` line via the no-counterexample path.
1192                    let secondary_counterexample = if self.tcfg.early_exit.should_stop() {
1193                        let unshrunk_sequence = calls
1194                            .iter()
1195                            .map(|tx| {
1196                                BaseCounterExample::from_invariant_call(
1197                                    tx,
1198                                    identified_contracts,
1199                                    None,
1200                                    invariant_config.show_solidity,
1201                                )
1202                            })
1203                            .collect::<Vec<_>>();
1204                        record_invariant_failure(
1205                            failure_dir.as_path(),
1206                            persisted_failure.as_path(),
1207                            &unshrunk_sequence,
1208                            &current_settings,
1209                            case_data.assertion_failure,
1210                        );
1211                        any_failure_persisted = true;
1212                        None
1213                    } else {
1214                        let position = next_position;
1215                        next_position += 1;
1216                        match replay_error(
1217                            invariant_config.clone(),
1218                            self.clone_executor(),
1219                            calls,
1220                            Some(case_data.inner_sequence.clone()),
1221                            case_data.assertion_failure,
1222                            None, // check mode
1223                            &invariant_contract,
1224                            invariant,
1225                            &self.cr.mcr.known_contracts,
1226                            identified_contracts.clone(),
1227                            &mut self.result.logs,
1228                            &mut self.result.traces,
1229                            &mut self.result.line_coverage,
1230                            &mut self.result.deprecated_cheatcodes,
1231                            progress.as_ref(),
1232                            &self.tcfg.early_exit,
1233                            Some((position, total_broken)),
1234                        ) {
1235                            Ok(call_sequence) if !call_sequence.is_empty() => {
1236                                record_invariant_failure(
1237                                    failure_dir.as_path(),
1238                                    persisted_failure.as_path(),
1239                                    &call_sequence,
1240                                    &current_settings,
1241                                    case_data.assertion_failure,
1242                                );
1243                                any_failure_persisted = true;
1244                                Some(CounterExample::Sequence(original_seq_len, call_sequence))
1245                            }
1246                            Ok(_) => None,
1247                            Err(err) => {
1248                                error!(%err, "Failed to replay invariant error");
1249                                None
1250                            }
1251                        }
1252                    };
1253                    invariant_failures.push(InvariantFailure::Predicate {
1254                        name: invariant.name.clone(),
1255                        reason: error.revert_reason().unwrap_or_default(),
1256                        counterexample: secondary_counterexample,
1257                        persisted_path: persisted_failure.clone(),
1258                        is_anchor: false,
1259                    });
1260                }
1261            }
1262        }
1263
1264        let invariant_failure_dir = any_failure_persisted.then(|| failure_dir.clone());
1265        // Only attach a suite-level roll-up when `assert_all` actually exercised >1 invariant.
1266        // Single-invariant runs (no assert_all, or filter-narrowed to one) get the legacy
1267        // single-block render with no roll-up line.
1268        let assert_all_invariant_count = (invariant_config.assert_all
1269            && invariant_contract.invariant_fns.len() > 1)
1270            .then_some(invariant_contract.invariant_fns.len());
1271
1272        // Convert handler-side assertion bugs into render-ready entries. The name is a
1273        // best-effort `Contract::function` from `identified_contracts`, falling back to
1274        // `0xreverter::0xselector`. Map is keyed by `(reverter, selector)` site so multiple
1275        // code paths through the same function collapse to one entry, rendered in the
1276        // dedicated `Suite handlers:` section.
1277        let identified_contracts_ro = identified_contracts;
1278        let invariant_handler_failures = invariant_result
1279            .handler_errors
1280            .iter()
1281            .sorted_by(|(ka, _), (kb, _)| {
1282                // Stable order across runs: sort by `(reverter, selector)` site directly.
1283                ka.cmp(kb)
1284            })
1285            .filter_map(|(site, err)| err.as_handler_assertion().map(|f| (site, f)))
1286            .map(|(_site, failure)| {
1287                let reverter = failure.reverter;
1288                let selector = failure.selector;
1289                // Resolve `Contract::function` from identified contracts when possible.
1290                let resolved_name = identified_contracts_ro
1291                    .get(&reverter)
1292                    .and_then(|(contract_name, abi)| {
1293                        abi.functions()
1294                            .find(|f| f.selector() == selector)
1295                            .map(|f| format!("{contract_name}::{}", f.name))
1296                    })
1297                    .unwrap_or_else(|| format!("{reverter}::{selector}"));
1298
1299                let counterexample_calls = failure
1300                    .call_sequence
1301                    .iter()
1302                    .map(|tx| {
1303                        BaseCounterExample::from_invariant_call(
1304                            tx,
1305                            identified_contracts_ro,
1306                            None,
1307                            invariant_config.show_solidity,
1308                        )
1309                    })
1310                    .collect::<Vec<_>>();
1311
1312                // Persist for next-run replay (skip if nothing to record).
1313                if !counterexample_calls.is_empty() {
1314                    record_handler_failure(
1315                        failure_dir.as_path(),
1316                        reverter,
1317                        selector,
1318                        &counterexample_calls,
1319                        &current_settings,
1320                    );
1321                }
1322
1323                let counterexample = if counterexample_calls.is_empty() {
1324                    None
1325                } else {
1326                    // Preserve pre-shrink length for `(original: N, shrunk: M)` rendering.
1327                    Some(CounterExample::Sequence(
1328                        failure.original_sequence_len,
1329                        counterexample_calls,
1330                    ))
1331                };
1332
1333                InvariantFailure::Handler {
1334                    name: resolved_name,
1335                    reverter,
1336                    selector,
1337                    reason: failure.revert_reason.clone(),
1338                    counterexample,
1339                }
1340            })
1341            .collect::<Vec<_>>();
1342
1343        self.result.invariant_result(
1344            invariant_result.gas_report_traces,
1345            success,
1346            invariant_failures,
1347            invariant_failure_dir,
1348            assert_all_invariant_count,
1349            invariant_handler_failures,
1350            counterexample,
1351            invariant_result.cases,
1352            invariant_result.reverts,
1353            invariant_result.metrics,
1354            invariant_result.failed_corpus_replays,
1355            invariant_result.optimization_best_value,
1356        );
1357        self.result
1358    }
1359
1360    /// Runs a fuzzed test.
1361    ///
1362    /// Applies the before test txes (if any), fuzzes the current function and returns the
1363    /// `TestResult`.
1364    ///
1365    /// Before test txes are applied in order and state modifications committed to the EVM database
1366    /// (therefore the fuzz test will use the modified state).
1367    /// State modifications of before test txes and fuzz test are discarded after test ends,
1368    /// similar to `eth_call`.
1369    fn run_fuzz_test(mut self, func: &Function) -> TestResult {
1370        // Prepare fuzz test execution.
1371        if self.prepare_test(func).is_err() {
1372            return self.result;
1373        }
1374
1375        let runner = self.fuzz_runner();
1376        let mut fuzz_config = self.config.fuzz.clone();
1377        let (failure_dir, failure_file) = test_paths(
1378            &mut fuzz_config.corpus,
1379            fuzz_config.failure_persist_dir.clone().unwrap(),
1380            self.cr.name,
1381            &func.name,
1382        );
1383
1384        let progress = start_fuzz_progress(
1385            self.cr.progress,
1386            self.cr.name,
1387            &func.name,
1388            fuzz_config.timeout,
1389            if fuzz_config.run.is_some() { 1 } else { fuzz_config.runs },
1390        );
1391
1392        let state = self.build_fuzz_state(false);
1393        let mut executor = self.executor.into_owned();
1394        // Enable edge coverage if running with coverage guided fuzzing or with edge coverage
1395        // metrics (useful for benchmarking the fuzzer).
1396        executor
1397            .inspector_mut()
1398            .collect_edge_coverage(fuzz_config.corpus.collect_evm_edge_coverage());
1399        executor.inspector_mut().collect_evm_cmp_log(fuzz_config.corpus.collect_evm_cmp_log());
1400        executor.inspector_mut().collect_sancov_edges(fuzz_config.corpus.collect_sancov_edges());
1401        executor
1402            .inspector_mut()
1403            .collect_sancov_trace_cmp(fuzz_config.corpus.collect_sancov_trace_cmp());
1404        // Load persisted counterexample, if any.
1405        let persisted_failure =
1406            foundry_common::fs::read_json_file::<BaseCounterExample>(failure_file.as_path()).ok();
1407        // Run fuzz test.
1408        let mut fuzzed_executor =
1409            FuzzedExecutor::new(executor, runner, self.tcfg.sender, fuzz_config, persisted_failure);
1410        let result = match fuzzed_executor.fuzz(
1411            func,
1412            &self.setup.fuzz_fixtures,
1413            state,
1414            self.address,
1415            &self.cr.mcr.revert_decoder,
1416            progress.as_ref(),
1417            &self.tcfg.early_exit,
1418            self.cr.tokio_handle,
1419        ) {
1420            Ok(x) => x,
1421            Err(e) => {
1422                self.result.fuzz_setup_fail(e);
1423                return self.result;
1424            }
1425        };
1426
1427        // Record counterexample.
1428        if let Some(CounterExample::Single(counterexample)) = &result.counterexample {
1429            if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) {
1430                error!(%err, "Failed to create fuzz failure dir");
1431            } else if let Err(err) =
1432                foundry_common::fs::write_json_file(failure_file.as_path(), counterexample)
1433            {
1434                error!(%err, "Failed to record call sequence");
1435            }
1436        }
1437
1438        self.result.fuzz_result(result);
1439        self.result
1440    }
1441
1442    /// Prepares single unit test and fuzz test execution:
1443    /// - set up the test result and executor
1444    /// - check if before test txes are configured and apply them in order
1445    ///
1446    /// Before test txes are arrays of arbitrary calldata obtained by calling the `beforeTest`
1447    /// function with test selector as a parameter.
1448    ///
1449    /// Unit tests within same contract (or even current test) are valid options for before test tx
1450    /// configuration. Test execution stops if any of before test txes fails.
1451    fn prepare_test(&mut self, func: &Function) -> Result<(), ()> {
1452        let address = self.setup.address;
1453
1454        // Apply before test configured functions (if any).
1455        if self.cr.contract.abi.functions().any(|func| func.name.is_before_test_setup()) {
1456            for calldata in self.executor.call_sol_default(
1457                address,
1458                &ITest::beforeTestSetupCall { testSelector: func.selector() },
1459            ) {
1460                let spec_id: SpecId = self.executor.spec_id().into();
1461                debug!(?calldata, spec=%spec_id, "applying before_test_setup");
1462                // Apply before test configured calldata.
1463                match self.executor.to_mut().transact_raw(
1464                    self.tcfg.sender,
1465                    address,
1466                    calldata,
1467                    U256::ZERO,
1468                ) {
1469                    Ok(call_result) => {
1470                        let reverted = call_result.reverted;
1471
1472                        // Merge tx result traces in unit test result.
1473                        self.result.extend(call_result);
1474
1475                        // To continue unit test execution the call should not revert.
1476                        if reverted {
1477                            self.result.single_fail(None);
1478                            return Err(());
1479                        }
1480                    }
1481                    Err(_) => {
1482                        self.result.single_fail(None);
1483                        return Err(());
1484                    }
1485                }
1486            }
1487        }
1488        Ok(())
1489    }
1490
1491    fn fuzz_runner(&self) -> TestRunner {
1492        let config = &self.config.fuzz;
1493        fuzzer_with_cases(config.seed, config.runs, config.max_test_rejects)
1494    }
1495
1496    fn invariant_runner(&self) -> TestRunner {
1497        let config = &self.config.invariant;
1498        fuzzer_with_cases(self.config.fuzz.seed, config.runs, config.max_assume_rejects)
1499    }
1500
1501    fn clone_executor(&self) -> Executor<FEN> {
1502        self.executor.clone().into_owned()
1503    }
1504
1505    fn build_fuzz_state(&self, invariant: bool) -> EvmFuzzState {
1506        let config =
1507            if invariant { self.config.invariant.dictionary } else { self.config.fuzz.dictionary };
1508        if let Some(db) = self.executor.backend().active_fork_db() {
1509            EvmFuzzState::new(
1510                &self.setup.deployed_libs,
1511                db,
1512                config,
1513                Some(&self.cr.mcr.fuzz_literals),
1514            )
1515        } else {
1516            let db = self.executor.backend().mem_db();
1517            EvmFuzzState::new(
1518                &self.setup.deployed_libs,
1519                db,
1520                config,
1521                Some(&self.cr.mcr.fuzz_literals),
1522            )
1523        }
1524    }
1525}
1526
1527fn fuzzer_with_cases(seed: Option<U256>, cases: u32, max_global_rejects: u32) -> TestRunner {
1528    let config = proptest::test_runner::Config {
1529        cases,
1530        max_global_rejects,
1531        // Disable proptest shrink: for fuzz tests we provide single counterexample,
1532        // for invariant tests we shrink outside proptest.
1533        max_shrink_iters: 0,
1534        ..Default::default()
1535    };
1536
1537    if let Some(seed) = seed {
1538        trace!(target: "forge::test", %seed, "building deterministic fuzzer");
1539        let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
1540        TestRunner::new_with_rng(config, rng)
1541    } else {
1542        trace!(target: "forge::test", "building stochastic fuzzer");
1543        TestRunner::new(config)
1544    }
1545}
1546
1547/// Holds data about a persisted invariant failure.
1548#[derive(Serialize, Deserialize)]
1549struct InvariantPersistedFailure {
1550    /// Recorded counterexample.
1551    call_sequence: Vec<BaseCounterExample>,
1552    /// Invariant settings when the counterexample was generated.
1553    /// Used to determine if the counterexample is still valid.
1554    settings: InvariantSettings,
1555    /// Whether the persisted failure came from a handler assertion instead of the invariant body.
1556    #[serde(default)]
1557    assertion_failure: bool,
1558}
1559
1560/// Mirrors `check_sequence`'s return: `(success, replayed_entirely, optional_reason)`.
1561type CheckSequenceResult = eyre::Result<(bool, bool, Option<String>)>;
1562
1563/// Borrowed context shared by primary-invariant and handler-side replay helpers.
1564struct ReplayContext<'a> {
1565    invariant_contract: &'a InvariantContract<'a>,
1566    invariant_config: &'a InvariantConfig,
1567    revert_decoder: &'a RevertDecoder,
1568    show_solidity: bool,
1569}
1570
1571/// Helper function to load failed call sequence from file.
1572/// Ignores failure if generated with different invariant settings than the current ones.
1573fn persisted_call_sequence(
1574    path: &Path,
1575    current_settings: &InvariantSettings,
1576) -> Option<InvariantPersistedFailure> {
1577    foundry_common::fs::read_json_file::<InvariantPersistedFailure>(path).ok().and_then(
1578        |persisted_failure| {
1579            if let Some(diff) = persisted_failure.settings.diff(current_settings) {
1580                let _ = sh_warn!(
1581                    "Failure from {:?} file was ignored because invariant test settings have changed: {}",
1582                    path,
1583                    diff
1584                );
1585                return None;
1586            }
1587            Some(persisted_failure)
1588        },
1589    )
1590}
1591
1592/// Converts a persisted counterexample to `BasicTxDetails`, setting `show_solidity` in place.
1593fn base_counterexamples_to_txes(
1594    ctx: &ReplayContext<'_>,
1595    call_sequence: &mut [BaseCounterExample],
1596) -> Vec<BasicTxDetails> {
1597    call_sequence
1598        .iter_mut()
1599        .map(|seq| {
1600            seq.show_solidity = ctx.show_solidity;
1601            BasicTxDetails {
1602                warp: seq.warp,
1603                roll: seq.roll,
1604                sender: seq.sender.unwrap_or_default(),
1605                call_details: CallDetails {
1606                    target: seq.addr.unwrap_or_default(),
1607                    calldata: seq.calldata.clone(),
1608                    value: seq.value,
1609                },
1610            }
1611        })
1612        .collect()
1613}
1614
1615/// Converts a persisted `BaseCounterExample` sequence into `BasicTxDetails` (applying
1616/// `ctx.show_solidity` in place) and replays it via `check_sequence`.
1617fn replay_persisted_call_sequence<FEN: FoundryEvmNetwork>(
1618    ctx: &ReplayContext<'_>,
1619    executor: Executor<FEN>,
1620    call_sequence: &mut [BaseCounterExample],
1621    expect_assertion_failure: bool,
1622) -> (Vec<BasicTxDetails>, CheckSequenceResult) {
1623    let txes = base_counterexamples_to_txes(ctx, call_sequence);
1624    let result = check_sequence(
1625        executor,
1626        &txes,
1627        (0..min(txes.len(), ctx.invariant_config.depth as usize)).collect(),
1628        ctx.invariant_contract.address,
1629        ctx.invariant_contract.anchor().selector().to_vec().into(),
1630        CheckSequenceOptions {
1631            accumulate_warp_roll: ctx.invariant_config.has_delay(),
1632            fail_on_revert: ctx.invariant_config.fail_on_revert,
1633            expect_assertion_failure,
1634            call_after_invariant: ctx.invariant_contract.call_after_invariant,
1635            rd: Some(ctx.revert_decoder),
1636        },
1637    );
1638    (txes, result)
1639}
1640
1641/// Helper function to set test corpus dir and to compose persisted failure paths.
1642fn test_paths(
1643    corpus_config: &mut FuzzCorpusConfig,
1644    persist_dir: PathBuf,
1645    contract_name: &str,
1646    test_name: &str,
1647) -> (PathBuf, PathBuf) {
1648    let contract = contract_name.split(':').next_back().unwrap();
1649    // Update config with corpus dir for current test.
1650    corpus_config.with_test(contract, test_name);
1651
1652    let failures_dir = canonicalized(persist_dir.join("failures").join(contract));
1653    let failure_file = canonicalized(failures_dir.join(test_name));
1654    (failures_dir, failure_file)
1655}
1656
1657/// Helper function to persist invariant failure.
1658fn record_invariant_failure(
1659    failure_dir: &Path,
1660    failure_file: &Path,
1661    call_sequence: &[BaseCounterExample],
1662    settings: &InvariantSettings,
1663    assertion_failure: bool,
1664) {
1665    if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) {
1666        error!(%err, "Failed to create invariant failure dir");
1667        return;
1668    }
1669
1670    if let Err(err) = foundry_common::fs::write_json_file(
1671        failure_file,
1672        &InvariantPersistedFailure {
1673            call_sequence: call_sequence.to_owned(),
1674            settings: settings.clone(),
1675            assertion_failure,
1676        },
1677    ) {
1678        error!(%err, "Failed to record call sequence");
1679    }
1680}
1681
1682/// Persists a handler-side assertion bug under `<failure_dir>/handlers/<site>.json`,
1683/// where `<site>` is `keccak256(reverter || selector)`.
1684fn record_handler_failure(
1685    failure_dir: &Path,
1686    reverter: Address,
1687    selector: Selector,
1688    call_sequence: &[BaseCounterExample],
1689    settings: &InvariantSettings,
1690) {
1691    let handlers_dir = failure_dir.join("handlers");
1692    if let Err(err) = foundry_common::fs::create_dir_all(&handlers_dir) {
1693        error!(%err, "Failed to create handler failure dir");
1694        return;
1695    }
1696    let mut buf = [0u8; 24];
1697    buf[..20].copy_from_slice(reverter.as_slice());
1698    buf[20..].copy_from_slice(selector.as_slice());
1699    let site_hash = alloy_primitives::keccak256(buf);
1700    let file = handlers_dir.join(format!("{site_hash:x}.json"));
1701    record_invariant_failure(&handlers_dir, &file, call_sequence, settings, true);
1702}
1703
1704/// Replays persisted handler-side assertion bugs. A file is kept only if the anchor still
1705/// asserts at the same `(reverter, selector)` site; stale files (anchor no longer asserts,
1706/// asserts at a different site, or earlier call asserts) are deleted in place.
1707fn replay_persisted_handler_failures<FEN: FoundryEvmNetwork>(
1708    handlers_dir: &Path,
1709    current_settings: &InvariantSettings,
1710    executor: Executor<FEN>,
1711    ctx: &ReplayContext<'_>,
1712) -> std::collections::HashMap<(Address, Selector), InvariantFuzzError> {
1713    let mut replayed: std::collections::HashMap<(Address, Selector), InvariantFuzzError> =
1714        std::collections::HashMap::new();
1715    let entries = match std::fs::read_dir(handlers_dir) {
1716        Ok(e) => e,
1717        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return replayed,
1718        Err(err) => {
1719            error!(%err, "Failed to read handler failure dir");
1720            return replayed;
1721        }
1722    };
1723    for entry in entries.flatten() {
1724        let path = entry.path();
1725        if path.extension().and_then(|s| s.to_str()) != Some("json") {
1726            continue;
1727        }
1728        let Some(persisted) = persisted_call_sequence(&path, current_settings) else {
1729            continue;
1730        };
1731        let mut call_sequence = persisted.call_sequence;
1732        if call_sequence.is_empty() {
1733            let _ = std::fs::remove_file(&path);
1734            continue;
1735        }
1736        let txes = base_counterexamples_to_txes(ctx, &mut call_sequence);
1737        // Expected site = (target, selector) of the persisted reproducer's last call.
1738        let Some(last) = txes.last() else {
1739            let _ = std::fs::remove_file(&path);
1740            continue;
1741        };
1742        let expected_target = last.call_details.target;
1743        let expected_selector_bytes: [u8; 4] =
1744            last.call_details.calldata.get(..4).and_then(|s| s.try_into().ok()).unwrap_or_default();
1745        let expected_site = (expected_target, Selector::from(expected_selector_bytes));
1746        let sequence: Vec<usize> =
1747            (0..min(txes.len(), ctx.invariant_config.depth as usize)).collect();
1748        let outcome = replay_handler_failure_sequence(
1749            executor.clone(),
1750            &txes,
1751            sequence,
1752            ctx.invariant_config.has_delay(),
1753            Some(ctx.revert_decoder),
1754        );
1755        match outcome {
1756            Ok(outcome) if outcome.anchor_asserted => {
1757                let _ = sh_warn!(
1758                    "Replayed handler-side assertion bug from {path:?}. \nRun `forge clean` or remove file to ignore."
1759                );
1760                let failure = HandlerAssertionFailure::from_replayed_sequence(
1761                    txes,
1762                    outcome.anchor_fingerprint,
1763                    outcome.revert_reason.unwrap_or_default(),
1764                );
1765                // On collision keep the shorter reproducer. Inlined: `replayed` uses the legacy
1766                // `(reverter, selector)` key, not the unified `FailureKey`.
1767                let already_shorter = replayed
1768                    .get(&expected_site)
1769                    .and_then(InvariantFuzzError::as_handler_assertion)
1770                    .is_some_and(|existing| {
1771                        existing.call_sequence.len() <= failure.call_sequence.len()
1772                    });
1773                if !already_shorter {
1774                    replayed.insert(expected_site, InvariantFuzzError::HandlerAssertion(failure));
1775                }
1776            }
1777            // Stale: anchor doesn't assert or earlier call asserts.
1778            Ok(_) => {
1779                let _ = std::fs::remove_file(&path);
1780            }
1781            Err(err) => {
1782                error!(%err, "Failed to replay handler-side assertion bug");
1783            }
1784        }
1785    }
1786    replayed
1787}