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::{
8        FuzzMinimizeConfig, FuzzMinimizeMode, FuzzMinimizeObservation, LibraryDeployment,
9        TestContract, TestFunctionMatcher, TestRunnerConfig,
10        is_generated_symbolic_regression_contract,
11    },
12    progress::TestsProgress,
13    result::{
14        InvariantFailure, InvariantOutcome, InvariantPredicateResult, SuiteResult,
15        SymbolicArtifactRef, SymbolicCallTrace, SymbolicCorpusSeedMetadata, SymbolicCorpusSeedRef,
16        SymbolicCounterexample, SymbolicCounterexampleArtifact, SymbolicCounterexampleArtifactKind,
17        SymbolicCounterexampleCall, SymbolicCounterexampleMinimization,
18        SymbolicCounterexampleReplaySemantics, SymbolicCounterexampleTestIdentity,
19        SymbolicInvariantArtifactFailure, SymbolicInvariantFailureSite, SymbolicReplayMetadata,
20        SymbolicReplayStatus, SymbolicResult, TestKind, TestResult, TestSetup, TestStatus,
21        invariant_campaign_display_name, invariant_kind,
22    },
23    symbolic_minimizer::{
24        MinimizedSequence, minimize_sequence_counterexample, minimize_single_call_counterexample,
25    },
26};
27use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
28use alloy_json_abi::{Function, JsonAbi, StateMutability};
29use alloy_primitives::{
30    Address, B256, Bytes, I256, Selector, U256, address, hex, keccak256,
31    map::{Entry, HashMap, HashSet},
32};
33use eyre::Result;
34use foundry_common::{
35    LIBRARY_DEPLOYER, TestFunctionExt, TestFunctionKind, contracts::ContractsByAddress,
36};
37use foundry_compilers::utils::canonicalized;
38use foundry_config::{
39    Config, FuzzConfig, FuzzCorpusConfig, FuzzDictionaryConfig, InlineConfig, InvariantConfig,
40    InvariantDepthMode, SymbolicConfig,
41};
42use foundry_evm::{
43    constants::{CALLER, MAGIC_ASSUME},
44    core::{backend::DatabaseExt, evm::FoundryEvmNetwork},
45    decode::{RevertDecoder, SkipReason},
46    executors::{
47        CallResult, DynamicTargetCtx, EvmError, Executor, ITest, InvariantReplayOptions,
48        MinimizationReplayInput, RawCallResult, ShowmapOpts, ShowmapReplayTarget,
49        StatelessReplayTarget, canonical_replay_dirs,
50        fuzz::FuzzedExecutor,
51        invariant::{
52            CheckSequenceFailureSite, CheckSequenceOptions, CheckSequenceOutcome,
53            HandlerAssertionFailure, InvariantExecutor, InvariantFuzzError, ReplayErrorResult,
54            check_sequence, did_fail_on_assert, execute_tx, execute_tx_and_register_created,
55            handler_edge_fingerprint, replay_error, replay_handler_failure_sequence, replay_run,
56        },
57        persist_corpus_seed, read_corpus_dir, replay_corpus_to_showmap,
58        replay_sequence_for_minimization, should_ignore_revert,
59    },
60    fuzz::{
61        BasicTxDetails, CallDetails, CounterExample, FuzzFixtures, fixture_name,
62        invariant::{
63            FuzzRunIdentifiedContracts, InvariantContract, InvariantSettings, SenderFilters,
64            is_optimization_invariant,
65        },
66        strategies::EvmFuzzState,
67    },
68    inspectors::{CmpOperands, EdgeCovConfig, EdgeCovKind, cheatcodes::Vm::AccountAccess},
69    revm::{bytecode::opcode, primitives::hardfork::SpecId},
70    traces::{TraceKind, TraceRequirements, load_contracts},
71};
72use foundry_evm_networks::NetworkVariant;
73use foundry_evm_symbolic::{
74    ExecutorView, SymbolicBranchTarget, SymbolicConcreteInput, SymbolicExecutor,
75    SymbolicInvariantCandidateInput, SymbolicInvariantCounterexampleKind,
76    SymbolicInvariantRunInput, SymbolicInvariantRunResult, SymbolicInvariantStep,
77    SymbolicInvariantTarget, SymbolicRunInput, SymbolicRunResult, SymbolicStats,
78    SymbolicStopReason, SymbolicStorageAssignment,
79};
80use itertools::Itertools;
81use proptest::test_runner::{RngAlgorithm, TestError, TestRng, TestRunner};
82use rayon::prelude::*;
83use serde::{Deserialize, Serialize};
84use std::{
85    borrow::Cow,
86    collections::BTreeMap,
87    ops::Deref,
88    path::{Path, PathBuf},
89    sync::{Arc, Mutex},
90    time::Instant,
91};
92use tokio::signal;
93use tracing::Span;
94
95const FUZZ_BRANCH_FRONTIER_SCHEMA: &str = "foundry:fuzz.branch-frontiers@v1";
96const STATEFUL_FUZZ_BRANCH_FRONTIER_SCHEMA: &str = "foundry:fuzz.branch-frontiers@v2";
97const FUZZ_BRANCH_FRONTIER_FILE: &str = "branch-frontiers.json";
98
99#[derive(Deserialize)]
100struct FuzzBranchFrontierArtifact {
101    schema: String,
102    version: u32,
103    test: String,
104    #[serde(default)]
105    sequences: Vec<Vec<BasicTxDetails>>,
106    frontiers: Vec<FuzzBranchFrontierRecord>,
107}
108
109#[derive(Deserialize)]
110struct FuzzBranchFrontierRecord {
111    id: u64,
112    #[serde(skip)]
113    both_results_retained: bool,
114    call_index: usize,
115    #[serde(default)]
116    sequence: Vec<BasicTxDetails>,
117    sequence_index: Option<usize>,
118    site: FuzzBranchFrontierSite,
119    operands: FuzzBranchFrontierOperands,
120}
121
122#[derive(Clone, Copy, Deserialize)]
123struct FuzzBranchFrontierSite {
124    address: Address,
125    pc: usize,
126    opcode: u8,
127}
128
129#[derive(Deserialize)]
130struct FuzzBranchFrontierOperands {
131    result: bool,
132}
133
134fn select_stateful_frontiers(
135    frontiers: Vec<FuzzBranchFrontierRecord>,
136    limit: usize,
137    explicit_selection: bool,
138) -> Vec<FuzzBranchFrontierRecord> {
139    if limit == 0 {
140        return Vec::new();
141    }
142
143    if explicit_selection {
144        return sample_stateful_frontiers(frontiers, limit);
145    }
146
147    let mut candidates = Vec::with_capacity(frontiers.len());
148    let mut lower_priority = Vec::new();
149    for frontier in frontiers {
150        if frontier.both_results_retained {
151            lower_priority.push(frontier);
152        } else {
153            candidates.push(frontier);
154        }
155    }
156
157    let deep_context_index = if !candidates.is_empty() && limit > 1 {
158        lower_priority
159            .iter()
160            .enumerate()
161            .max_by_key(|(_, frontier)| (frontier.call_index, frontier.id))
162            .map(|(index, _)| index)
163    } else {
164        None
165    };
166    let deep_context = deep_context_index.map(|index| lower_priority.swap_remove(index));
167    let mut selected = select_context_diverse_frontiers(candidates, limit);
168    selected.extend(select_context_diverse_frontiers(
169        lower_priority,
170        limit.saturating_sub(selected.len()),
171    ));
172    if let Some(deep_context) = deep_context {
173        if selected.len() == limit {
174            let mut context_counts = HashMap::<(Option<usize>, usize), usize>::default();
175            for frontier in &selected {
176                *context_counts
177                    .entry((frontier.sequence_index, frontier.call_index))
178                    .or_default() += 1;
179            }
180            let repeated_context = selected
181                .iter()
182                .enumerate()
183                .filter(|(_, frontier)| {
184                    context_counts
185                        .get(&(frontier.sequence_index, frontier.call_index))
186                        .is_some_and(|count| *count > 1)
187                })
188                .min_by_key(|(_, frontier)| frontier.id)
189                .map(|(index, _)| index);
190            if let Some(index) = repeated_context {
191                selected.remove(index);
192            } else {
193                selected.pop();
194            }
195        }
196        selected.push(deep_context);
197    }
198    selected
199}
200
201fn select_context_diverse_frontiers(
202    mut frontiers: Vec<FuzzBranchFrontierRecord>,
203    limit: usize,
204) -> Vec<FuzzBranchFrontierRecord> {
205    if limit == 0 {
206        return Vec::new();
207    }
208    if frontiers.len() <= limit {
209        return frontiers;
210    }
211
212    frontiers.sort_unstable_by_key(|frontier| {
213        (frontier.sequence_index, frontier.call_index, frontier.id)
214    });
215    let mut representatives = Vec::<FuzzBranchFrontierRecord>::new();
216    let mut remaining = Vec::new();
217    for frontier in frontiers {
218        let context = (frontier.sequence_index, frontier.call_index);
219        if let Some(previous) = representatives.last_mut()
220            && (previous.sequence_index, previous.call_index) == context
221        {
222            remaining.push(std::mem::replace(previous, frontier));
223        } else {
224            representatives.push(frontier);
225        }
226    }
227
228    let mut selected = sample_stateful_frontiers(representatives, limit);
229    selected.extend(sample_stateful_frontiers(remaining, limit - selected.len()));
230    selected
231}
232
233fn sample_stateful_frontiers(
234    mut frontiers: Vec<FuzzBranchFrontierRecord>,
235    limit: usize,
236) -> Vec<FuzzBranchFrontierRecord> {
237    if limit == 0 {
238        return Vec::new();
239    }
240    frontiers.sort_unstable_by_key(|frontier| (frontier.call_index, frontier.id));
241    if frontiers.len() <= limit {
242        return frontiers;
243    }
244
245    let total = frontiers.len();
246    let denominator = 2 * limit as u128;
247    let mut indexes =
248        (0..limit).map(|index| (((2 * index + 1) as u128 * total as u128) / denominator) as usize);
249    let mut next = indexes.next();
250    frontiers
251        .into_iter()
252        .enumerate()
253        .filter_map(|(index, frontier)| {
254            (Some(index) == next).then(|| {
255                next = indexes.next();
256                frontier
257            })
258        })
259        .collect()
260}
261
262fn comparison_result(opcode: u8, lhs: U256, rhs: U256) -> Option<bool> {
263    match opcode {
264        opcode::EQ => Some(lhs == rhs),
265        opcode::LT => Some(lhs < rhs),
266        opcode::GT => Some(lhs > rhs),
267        opcode::SLT => Some(I256::from_raw(lhs) < I256::from_raw(rhs)),
268        opcode::SGT => Some(I256::from_raw(lhs) > I256::from_raw(rhs)),
269        opcode::ISZERO => Some(lhs.is_zero()),
270        _ => None,
271    }
272}
273
274fn frontier_comparison_flipped(
275    site: FuzzBranchFrontierSite,
276    observed_result: bool,
277    comparisons: &[CmpOperands],
278) -> bool {
279    comparisons.iter().any(|comparison| {
280        comparison.address == site.address
281            && comparison.pc == site.pc
282            && comparison.opcode == site.opcode
283            && comparison_result(comparison.opcode, comparison.op1, comparison.op2)
284                == Some(!observed_result)
285    })
286}
287
288pub(crate) struct InvariantCampaignScope<'a> {
289    pub config: &'a Config,
290    pub inline_config: &'a InlineConfig,
291    pub contract_name: &'a str,
292    pub all_override_networks: &'a [NetworkVariant],
293    pub pass_network: Option<&'a NetworkVariant>,
294}
295
296struct InvariantCampaignSelection<'a> {
297    matched_boolean_invariant_fns: Vec<&'a Function>,
298    merge_boolean_suite: bool,
299    shared_boolean_namespace: bool,
300    boolean_suite_anchor: Option<&'a Function>,
301    optimization_anchors: usize,
302}
303
304impl InvariantCampaignSelection<'_> {
305    const fn anchor_count(&self) -> usize {
306        self.optimization_anchors
307            + if self.matched_boolean_invariant_fns.is_empty() {
308                0
309            } else if self.merge_boolean_suite {
310                1
311            } else {
312                self.matched_boolean_invariant_fns.len()
313            }
314    }
315}
316
317pub(crate) fn count_runnable_invariant_campaign_anchors(
318    abi: &JsonAbi,
319    filter: &dyn TestFilter,
320    scope: InvariantCampaignScope<'_>,
321) -> usize {
322    let invariant_fns = abi.functions().filter(|func| func.is_invariant_test()).collect::<Vec<_>>();
323    if invariant_fns.iter().any(|func| !func.inputs.is_empty()) {
324        return 0;
325    }
326
327    let functions = abi
328        .functions()
329        .filter(|func| filter.matches_test_function(func))
330        .filter(|func| {
331            function_matches_network_pass(
332                scope.all_override_networks,
333                scope.pass_network,
334                scope.inline_config.network_for(
335                    &scope.config.profile,
336                    scope.contract_name,
337                    &func.name,
338                ),
339            )
340        })
341        .collect::<Vec<_>>();
342
343    select_invariant_campaigns(
344        &invariant_fns,
345        &functions,
346        scope.config,
347        scope.inline_config,
348        scope.contract_name,
349    )
350    .anchor_count()
351}
352
353pub(crate) fn function_matches_network_pass(
354    all_override_networks: &[NetworkVariant],
355    pass_network: Option<&NetworkVariant>,
356    func_network: Option<NetworkVariant>,
357) -> bool {
358    if all_override_networks.is_empty() {
359        return true;
360    }
361    match pass_network {
362        None => func_network.is_none_or(|network| !all_override_networks.contains(&network)),
363        Some(target) => func_network.as_ref() == Some(target),
364    }
365}
366
367pub(crate) fn inline_config_for(
368    config: &Config,
369    inline_config: &InlineConfig,
370    contract_name: &str,
371    func: Option<&Function>,
372) -> Result<Config> {
373    let function = func.map(|f| f.name.as_str()).unwrap_or("");
374    Ok(config.merge_inline_provider(inline_config.provide(contract_name, function))?)
375}
376
377fn invariant_suite_configs_match(
378    config: &Config,
379    inline_config: &InlineConfig,
380    contract_name: &str,
381    funcs: &[&Function],
382) -> bool {
383    let Some((anchor, rest)) = funcs.split_first() else {
384        return true;
385    };
386    let anchor_config = match inline_config_for(config, inline_config, contract_name, Some(anchor))
387    {
388        Ok(config) => config.invariant,
389        Err(_) => return false,
390    };
391    rest.iter().all(|func| {
392        inline_config_for(config, inline_config, contract_name, Some(func))
393            .map(|config| config.invariant == anchor_config)
394            .unwrap_or(false)
395    })
396}
397
398fn select_invariant_campaigns<'a>(
399    invariant_fns: &[&'a Function],
400    functions: &[&'a Function],
401    config: &Config,
402    inline_config: &InlineConfig,
403    contract_name: &str,
404) -> InvariantCampaignSelection<'a> {
405    let boolean_invariant_fns = invariant_fns
406        .iter()
407        .copied()
408        .filter(|func| !is_optimization_invariant(func))
409        .collect::<Vec<_>>();
410    let matched_boolean_invariant_fns = functions
411        .iter()
412        .copied()
413        .filter(|func| func.is_invariant_test() && !is_optimization_invariant(func))
414        .collect::<Vec<_>>();
415    let optimization_anchors = functions
416        .iter()
417        .filter(|func| func.is_invariant_test() && is_optimization_invariant(func))
418        .count();
419
420    // Merge compatible selected predicates even when an excluded predicate has different config.
421    // Decide the corpus/frontier namespace separately from the full suite so filtering cannot
422    // move an isolated campaign into the contract-level namespace.
423    let canonical_boolean_anchor = boolean_invariant_fns.first().copied();
424    let merge_boolean_suite = !matched_boolean_invariant_fns.is_empty()
425        && invariant_suite_configs_match(
426            config,
427            inline_config,
428            contract_name,
429            &matched_boolean_invariant_fns,
430        );
431    let shared_boolean_namespace = merge_boolean_suite
432        && invariant_suite_configs_match(
433            config,
434            inline_config,
435            contract_name,
436            &boolean_invariant_fns,
437        );
438    let boolean_suite_anchor = merge_boolean_suite
439        .then(|| {
440            canonical_boolean_anchor
441                .filter(|anchor| matched_boolean_invariant_fns.contains(anchor))
442                .or_else(|| matched_boolean_invariant_fns.first().copied())
443        })
444        .flatten();
445
446    InvariantCampaignSelection {
447        matched_boolean_invariant_fns,
448        merge_boolean_suite,
449        shared_boolean_namespace,
450        boolean_suite_anchor,
451        optimization_anchors,
452    }
453}
454
455/// A type that executes all tests of a contract
456pub struct ContractRunner<'a, FEN: FoundryEvmNetwork> {
457    /// The name of the contract.
458    name: &'a str,
459    /// The data of the contract.
460    contract: &'a TestContract,
461    /// The EVM executor.
462    executor: Executor<FEN>,
463    /// Overall test run progress.
464    progress: Option<&'a TestsProgress>,
465    /// The handle to the tokio runtime.
466    tokio_handle: tokio::runtime::Handle,
467    /// The span of the contract.
468    span: tracing::Span,
469    /// The contract-level configuration.
470    tcfg: Cow<'a, TestRunnerConfig<FEN>>,
471    /// The parent runner.
472    mcr: &'a MultiContractRunner<FEN>,
473    /// Number of matching invariant campaign anchors in the current test pass.
474    num_invariant_campaign_anchors: usize,
475}
476
477pub(crate) struct ContractRunnerContext<'a> {
478    pub(crate) progress: Option<&'a TestsProgress>,
479    pub(crate) tokio_handle: tokio::runtime::Handle,
480    pub(crate) num_invariant_campaign_anchors: usize,
481}
482
483impl<'a, FEN: FoundryEvmNetwork> Deref for ContractRunner<'a, FEN> {
484    type Target = Cow<'a, TestRunnerConfig<FEN>>;
485
486    #[inline(always)]
487    fn deref(&self) -> &Self::Target {
488        &self.tcfg
489    }
490}
491
492impl<'a, FEN: FoundryEvmNetwork> ContractRunner<'a, FEN> {
493    pub(crate) fn new(
494        name: &'a str,
495        contract: &'a TestContract,
496        executor: Executor<FEN>,
497        span: Span,
498        mcr: &'a MultiContractRunner<FEN>,
499        context: ContractRunnerContext<'a>,
500    ) -> Self {
501        Self {
502            name,
503            contract,
504            executor,
505            progress: context.progress,
506            tokio_handle: context.tokio_handle,
507            span,
508            tcfg: Cow::Borrowed(&mcr.tcfg),
509            mcr,
510            num_invariant_campaign_anchors: context.num_invariant_campaign_anchors,
511        }
512    }
513
514    /// Returns `true` if `func` should run in the current multi-network pass.
515    ///
516    /// In single-pass mode (no inline network overrides) every function passes.
517    /// In multi-pass mode:
518    /// - Default pass (`pass_network = None`): includes functions *without* an override annotation.
519    /// - Override pass (`pass_network = Some(v)`): includes only functions annotated with `v`.
520    fn function_matches_network_pass(&self, func: &Function) -> bool {
521        function_matches_network_pass(
522            &self.mcr.tcfg.multi_network.all_override_networks,
523            self.mcr.tcfg.multi_network.pass_network.as_ref(),
524            self.mcr.inline_config.network_for(&self.tcfg.config.profile, self.name, &func.name),
525        )
526    }
527
528    /// Deploys the test contract inside the runner from the sending account, and optionally runs
529    /// the `setUp` function on the test contract.
530    pub fn setup(&mut self, call_setup: bool) -> TestSetup {
531        self._setup(call_setup).unwrap_or_else(|err| {
532            if matches!(err.downcast_ref::<EvmError<FEN>>(), Some(EvmError::Skip(_))) {
533                TestSetup::skipped(err.to_string())
534            } else {
535                TestSetup::failed(err.to_string())
536            }
537        })
538    }
539
540    fn _setup(&mut self, call_setup: bool) -> Result<TestSetup> {
541        trace!(call_setup, "setting up");
542
543        self.apply_contract_inline_config()?;
544
545        // We max out their balance so that they can deploy and make calls.
546        self.executor.set_balance(self.sender, U256::MAX)?;
547        self.executor.set_balance(CALLER, U256::MAX)?;
548
549        // We set the nonce of the deployer accounts to 1 to get the same addresses as DappTools.
550        self.executor.set_nonce(self.sender, 1)?;
551
552        // Deploy libraries.
553        self.executor.set_balance(LIBRARY_DEPLOYER, U256::MAX)?;
554
555        let rd = &self.mcr.revert_decoder;
556        let mut result = TestSetup::default();
557        let mut pending_account_diffs = Vec::new();
558        match self.mcr.library_deployment {
559            LibraryDeployment::Nonce => {
560                // Fork state may carry a nonzero deployer nonce.
561                if !self.mcr.libs_to_deploy.is_empty() {
562                    self.executor.set_account_nonce(LIBRARY_DEPLOYER, 0)?;
563                }
564                for (nonce, code) in self.mcr.libs_to_deploy.iter().enumerate() {
565                    // Libraries are linked from nonce zero in the same order they are deployed.
566                    let expected_address = LIBRARY_DEPLOYER.create(nonce as u64);
567                    let (deploy_result, recorded_account_diffs) =
568                        self.deploy_library(expected_address, |executor| {
569                            executor.deploy(LIBRARY_DEPLOYER, code.clone(), U256::ZERO, Some(rd))
570                        });
571
572                    if let Ok(deployed) = &deploy_result {
573                        result.deployed_libs.push(deployed.address);
574                        if self.contract.library_addresses.contains(&deployed.address) {
575                            pending_account_diffs.extend(recorded_account_diffs);
576                        }
577                    }
578
579                    let (raw, reason) =
580                        RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
581                    result.extend(raw, TraceKind::Deployment);
582                    if reason.is_some() {
583                        debug!(?reason, "deployment of library failed");
584                        result.reason = reason;
585                        return Ok(result);
586                    }
587                }
588            }
589            LibraryDeployment::Create2 { deployer, salt } => {
590                // Foundry only knows how to install the canonical factory locally. A custom
591                // factory is usable only when it already exists in fork state. Tempo also
592                // provides the factory as a predeploy, which must not be deployed again.
593                if deployer == foundry_evm::constants::DEFAULT_CREATE2_DEPLOYER
594                    && !self.evm_opts.networks.is_tempo()
595                {
596                    self.executor.deploy_create2_deployer()?;
597                }
598                for code in &self.mcr.libs_to_deploy {
599                    let address = deployer.create2_from_code(salt, code);
600                    if self.executor.is_empty_code(address)? {
601                        let calldata = [salt.as_slice(), code.as_ref()].concat().into();
602                        let (raw, recorded_account_diffs) =
603                            self.deploy_library(address, |executor| {
604                                executor.transact_raw(
605                                    LIBRARY_DEPLOYER,
606                                    deployer,
607                                    calldata,
608                                    U256::ZERO,
609                                )
610                            });
611                        let raw = raw?;
612                        let (raw, reason) = if raw.reverted {
613                            RawCallResult::from_evm_result(Err(raw.into_evm_error(Some(rd))))?
614                        } else {
615                            (raw, None)
616                        };
617                        result.extend(raw, TraceKind::Deployment);
618                        if reason.is_some() {
619                            debug!(?reason, "CREATE2 deployment of library failed");
620                            result.reason = reason;
621                            return Ok(result);
622                        }
623                        if self.executor.is_empty_code(address)? {
624                            result.reason = Some(format!(
625                                "CREATE2 library deployment succeeded but no code was found at {address}"
626                            ));
627                            return Ok(result);
628                        }
629                        pending_account_diffs.extend(recorded_account_diffs);
630                    }
631                    self.executor.backend_mut().add_persistent_account(address);
632                    result.deployed_libs.push(address);
633                }
634
635                // Factory calls are test harness setup and must not be observable through the
636                // last-call gas cheatcodes.
637                if let Some(cheats) = self.executor.inspector_mut().cheatcodes.as_mut() {
638                    cheats.gas_metering.last_call_gas = None;
639                    cheats.gas_metering.last_frame_gas = None;
640                }
641            }
642        }
643        if !pending_account_diffs.is_empty()
644            && let Some(cheats) = self.executor.inspector_mut().cheatcodes.as_deref_mut()
645        {
646            cheats.set_pending_account_diffs(pending_account_diffs);
647        }
648
649        // Configured libraries may already exist and are not present in `libs_to_deploy`.
650        for &address in &self.mcr.library_addresses {
651            if !self.executor.is_empty_code(address)? {
652                result.deployed_libs.push(address);
653            }
654        }
655        result.deployed_libs.sort_unstable();
656        result.deployed_libs.dedup();
657
658        let address = self.sender.create(self.executor.get_nonce(self.sender)?);
659        result.address = address;
660
661        // Set the contracts initial balance before deployment, so it is available during
662        // construction
663        self.executor.set_balance(address, self.initial_balance())?;
664
665        // Deploy the test contract
666        let deploy_result =
667            self.executor.deploy(self.sender, self.contract.bytecode.clone(), U256::ZERO, Some(rd));
668
669        result.deployment_failure = deploy_result.is_err();
670
671        if let Ok(dr) = &deploy_result {
672            debug_assert_eq!(dr.address, address);
673        }
674        let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
675        result.extend(raw, TraceKind::Deployment);
676        if reason.is_some() {
677            debug!(?reason, "deployment of test contract failed");
678            result.reason = reason;
679            return Ok(result);
680        }
681
682        // Reset `self.sender`s, `CALLER`s and `LIBRARY_DEPLOYER`'s balance to the initial balance.
683        self.executor.set_balance(self.sender, self.initial_balance())?;
684        self.executor.set_balance(CALLER, self.initial_balance())?;
685        self.executor.set_balance(LIBRARY_DEPLOYER, self.initial_balance())?;
686
687        if matches!(self.mcr.library_deployment, LibraryDeployment::Nonce)
688            && !self.evm_opts.networks.is_tempo()
689        {
690            self.executor.deploy_create2_deployer()?;
691        }
692
693        // Optionally call the `setUp` function
694        if call_setup {
695            trace!("calling setUp");
696            let res = self.executor.setup(None, address, Some(rd));
697            let (raw, reason) = RawCallResult::from_evm_result(res)?;
698            result.extend(raw, TraceKind::Setup);
699            result.reason = reason;
700        }
701
702        Ok(result)
703    }
704
705    fn initial_balance(&self) -> U256 {
706        self.evm_opts.initial_balance
707    }
708
709    /// Runs `deploy`, recording the account diffs of linked library deployments so cheatcodes
710    /// can attribute them to the library.
711    fn deploy_library<T>(
712        &mut self,
713        address: Address,
714        deploy: impl FnOnce(&mut Executor<FEN>) -> T,
715    ) -> (T, Vec<AccountAccess>) {
716        let recording = self.contract.library_addresses.contains(&address)
717            && self
718                .executor
719                .inspector_mut()
720                .cheatcodes
721                .as_deref_mut()
722                .is_some_and(|cheats| cheats.start_internal_state_diff_recording());
723        let result = deploy(&mut self.executor);
724        let diffs = if recording {
725            self.executor
726                .inspector_mut()
727                .cheatcodes
728                .as_deref_mut()
729                .map(|cheats| cheats.stop_internal_state_diff_recording())
730                .unwrap_or_default()
731        } else {
732            Vec::new()
733        };
734        (result, diffs)
735    }
736
737    /// Configures this runner with the inline configuration for the contract.
738    fn apply_contract_inline_config(&mut self) -> Result<()> {
739        if self.inline_config.contains_contract(self.name) {
740            let new_config = Arc::new(self.inline_config(None)?);
741            self.tcfg.to_mut().reconfigure_with(new_config);
742            let prev_tracer = self.executor.inspector_mut().tracer.take();
743            self.tcfg.configure_executor(&mut self.executor);
744            // Don't set tracer here.
745            self.executor.inspector_mut().tracer = prev_tracer;
746        }
747        Ok(())
748    }
749
750    /// Returns the configuration for a contract or function.
751    fn inline_config(&self, func: Option<&Function>) -> Result<Config> {
752        let mut config = inline_config_for(&self.config, &self.mcr.inline_config, self.name, func)?;
753        config.networks = config.networks.with_execution_profile(self.tcfg.evm_opts.networks);
754        Ok(config)
755    }
756
757    /// Collect fixtures from test contract.
758    ///
759    /// Fixtures can be defined:
760    /// - as storage arrays in test contract, prefixed with `fixture`
761    /// - as functions prefixed with `fixture` and followed by parameter name to be fuzzed
762    ///
763    /// Storage array fixtures:
764    /// `uint256[] public fixture_amount = [1, 2, 3];`
765    /// define an array of uint256 values to be used for fuzzing `amount` named parameter in scope
766    /// of the current test.
767    ///
768    /// Function fixtures:
769    /// `function fixture_owner() public returns (address[] memory){}`
770    /// returns an array of addresses to be used for fuzzing `owner` named parameter in scope of the
771    /// current test.
772    fn fuzz_fixtures(&mut self, address: Address) -> FuzzFixtures {
773        let mut fixtures = HashMap::default();
774        let fixture_functions = self.contract.abi.functions().filter(|func| func.is_fixture());
775        for func in fixture_functions {
776            if func.inputs.is_empty() {
777                // Read fixtures declared as functions.
778                if let Ok(CallResult { raw: _, decoded_result }) =
779                    self.executor.call(CALLER, address, func, &[], U256::ZERO, None)
780                {
781                    fixtures.insert(fixture_name(func.name.clone()), decoded_result);
782                }
783            } else {
784                // For reading fixtures from storage arrays we collect values by calling the
785                // function with incremented indexes until there's an error.
786                let mut vals = Vec::new();
787                let mut index = 0;
788                loop {
789                    if let Ok(CallResult { raw: _, decoded_result }) = self.executor.call(
790                        CALLER,
791                        address,
792                        func,
793                        &[DynSolValue::Uint(U256::from(index), 256)],
794                        U256::ZERO,
795                        None,
796                    ) {
797                        vals.push(decoded_result);
798                    } else {
799                        // No result returned for this index, we reached the end of storage
800                        // array or the function is not a valid fixture.
801                        break;
802                    }
803                    index += 1;
804                }
805                fixtures.insert(fixture_name(func.name.clone()), DynSolValue::Array(vals));
806            };
807        }
808        FuzzFixtures::new(fixtures).with_enum_bounds(self.mcr.enum_bounds.clone())
809    }
810
811    /// Classifies test functions with the current contract-level configuration.
812    fn test_matcher(&self) -> TestFunctionMatcher<'_> {
813        TestFunctionMatcher::new(
814            &self.config,
815            &self.mcr.inline_config,
816            self.mcr.tcfg.symbolic_artifact_replay.as_ref(),
817        )
818    }
819
820    /// Returns the test functions selected by `filter` that run in the current network pass.
821    fn matching_test_functions(
822        &self,
823        filter: &dyn TestFilter,
824        test_matcher: &TestFunctionMatcher<'_>,
825    ) -> Vec<&'a Function> {
826        test_matcher
827            .test_functions(self.name.to_string(), &self.contract.abi, |contract_id, func, kind| {
828                filter.matches_test_function_kind_in_contract(contract_id, func, kind)
829                    && self.function_matches_network_pass(func)
830            })
831            .collect()
832    }
833
834    /// Runs all tests for a contract whose names match the provided regular expression
835    pub fn run_tests(mut self, filter: &dyn TestFilter) -> SuiteResult {
836        let start = Instant::now();
837        let mut warnings = Vec::new();
838        let generated_symbolic_regression =
839            is_generated_symbolic_regression_contract(&self.contract.abi);
840        // Classified before `setUp`; the full function list is built after setup so
841        // contract-level inline config can still affect symbolic entrypoint discovery.
842        let test_matcher = self.test_matcher();
843        // Skip suites with no selected tests in this network pass before deploying the
844        // contract or executing `setUp`. Fuzz-only mode also excludes unit and symbolic tests.
845        if !self.matching_test_functions(filter, &test_matcher).into_iter().any(|func| {
846            !self.mcr.tcfg.fuzz_only
847                || matches!(
848                    test_matcher.test_function_kind(self.name, func, generated_symbolic_regression),
849                    TestFunctionKind::FuzzTest { .. } | TestFunctionKind::InvariantTest
850                )
851        }) {
852            return SuiteResult::new(start.elapsed(), BTreeMap::new(), warnings);
853        }
854
855        // Check if `setUp` function with valid signature declared.
856        let setup_fns: Vec<_> =
857            self.contract.abi.functions().filter(|func| func.name.is_setup()).collect();
858        let call_setup = setup_fns.len() == 1 && setup_fns[0].name == "setUp";
859        // There is a single miss-cased `setUp` function, so we add a warning
860        for &setup_fn in &setup_fns {
861            if setup_fn.name != "setUp" {
862                warnings.push(format!(
863                    "Found invalid setup function \"{}\" did you mean \"setUp()\"?",
864                    setup_fn.signature()
865                ));
866            }
867        }
868
869        // There are multiple setUp function, so we return a single test result for `setUp`
870        if setup_fns.len() > 1 {
871            return self.failed_suite(
872                start,
873                warnings,
874                [("setUp()".to_string(), TestResult::fail("multiple setUp functions".to_string()))],
875            );
876        }
877
878        // Check if `afterInvariant` function with valid signature declared.
879        let after_invariant_fns: Vec<_> =
880            self.contract.abi.functions().filter(|func| func.name.is_after_invariant()).collect();
881        if after_invariant_fns.len() > 1 {
882            return self.failed_suite(
883                start,
884                warnings,
885                [(
886                    "afterInvariant()".to_string(),
887                    TestResult::fail("multiple afterInvariant functions".to_string()),
888                )],
889            );
890        }
891        let call_after_invariant = after_invariant_fns.first().is_some_and(|after_invariant_fn| {
892            let match_sig = after_invariant_fn.name == "afterInvariant";
893            if !match_sig {
894                warnings.push(format!(
895                    "Found invalid afterInvariant function \"{}\" did you mean \"afterInvariant()\"?",
896                    after_invariant_fn.signature()
897                ));
898            }
899            match_sig
900        });
901
902        let invariant_fns = self
903            .contract
904            .abi
905            .functions()
906            .filter(|func| {
907                test_matcher
908                    .test_function_kind(self.name, func, generated_symbolic_regression)
909                    .is_invariant_test()
910            })
911            .collect::<Vec<_>>();
912
913        // Validate signatures up front: invariant functions must take no parameters. Without
914        // this, parameterized `invariant_*` functions would slip into contract-level campaigns
915        // and fail with a confusing "selector not found" / decode error mid-campaign. Reject
916        // here with a per-function result so the failure is obvious to the user.
917        let invalid_invariants = invariant_fns
918            .iter()
919            .filter(|f| !f.inputs.is_empty())
920            .map(|f| {
921                let signature = f.signature();
922                let reason = format!("invariant `{signature}` must take no parameters");
923                (signature, TestResult::fail(reason))
924            })
925            .collect::<Vec<_>>();
926        if !invalid_invariants.is_empty() {
927            return self.failed_suite(start, warnings, invalid_invariants);
928        }
929
930        for invariant in &invariant_fns {
931            if invariant.outputs.len() == 1 && invariant.outputs[0].ty == "bool" {
932                warnings.push(format!(
933                    "Invariant function `{}` returns `bool`, but its return value is ignored; use assertions or revert to indicate failure.",
934                    invariant.signature()
935                ));
936            }
937        }
938
939        // Invariant testing requires tracing to figure out what contracts were created.
940        // For regular test runs we disable debug-level setup traces as an optimization.
941        // In `forge test --debug`, keep setup traces in debug mode so setup failures are
942        // inspectable in the debugger.
943        let has_invariants = !invariant_fns.is_empty();
944
945        let should_override_setup_tracing =
946            !self.tcfg.debug && (self.executor.inspector().tracer.is_some() || has_invariants);
947
948        let prev_tracer = should_override_setup_tracing.then(|| {
949            let prev_tracer = self.executor.inspector_mut().tracer.take();
950            self.executor.set_trace_requirements(TraceRequirements::none().with_calls(true));
951            prev_tracer
952        });
953
954        let setup_time = Instant::now();
955        let mut setup = self.setup(call_setup);
956        debug!("finished setting up in {:?}", setup_time.elapsed());
957
958        if let Some(prev_tracer) = prev_tracer {
959            self.executor.inspector_mut().tracer = prev_tracer;
960        }
961
962        if setup.reason.is_some() {
963            // The setup failed, so we return a single test result for `setUp`
964            let name = if setup.deployment_failure { "constructor()" } else { "setUp()" };
965            return self.failed_suite(
966                start,
967                warnings,
968                [(name.to_string(), TestResult::setup_result(setup))],
969            );
970        }
971
972        // Filter out functions sequentially since it's very fast and there is no need to do it
973        // in parallel.
974        let find_timer = Instant::now();
975        let functions = self.matching_test_functions(filter, &self.test_matcher());
976        debug!(
977            "Found {} test functions out of {} in {:?}",
978            functions.len(),
979            self.contract.abi.functions().count(),
980            find_timer.elapsed(),
981        );
982
983        let identified_contracts = has_invariants.then(|| {
984            load_contracts(setup.traces.iter().map(|(_, t)| &t.arena), &self.mcr.known_contracts)
985        });
986
987        if let Some(replay) = &self.mcr.tcfg.symbolic_artifact_replay {
988            let artifact = &replay.artifact;
989            let target = &artifact.test;
990            let replay_functions =
991                functions.iter().filter(|func| func.signature() == target.test).collect::<Vec<_>>();
992            let func = match replay_functions[..] {
993                [] if !self.mcr.tcfg.multi_network.all_override_networks.is_empty() => {
994                    return SuiteResult::new(start.elapsed(), BTreeMap::new(), warnings);
995                }
996                [] => {
997                    let reason = format!(
998                        "symbolic artifact target `{}` was not found in `{}`",
999                        target.test, target.contract
1000                    );
1001                    let results = [(target.test.clone(), TestResult::fail(reason))];
1002                    return SuiteResult::new(start.elapsed(), results.into(), warnings);
1003                }
1004                [func] => *func,
1005                _ => {
1006                    let reason = format!(
1007                        "symbolic artifact target `{}` matched {} functions in `{}`",
1008                        target.test,
1009                        replay_functions.len(),
1010                        target.contract
1011                    );
1012                    let results = [(target.test.clone(), TestResult::fail(reason))];
1013                    return SuiteResult::new(start.elapsed(), results.into(), warnings);
1014                }
1015            };
1016
1017            let is_sequence = artifact.kind == SymbolicCounterexampleArtifactKind::Sequence;
1018            let kind = if is_sequence {
1019                func.test_function_kind()
1020            } else {
1021                TestFunctionKind::SymbolicTest
1022            };
1023            let test_start = Instant::now();
1024            let mut res = if is_sequence && !kind.is_invariant_test() {
1025                TestResult::fail(format!(
1026                    "sequence symbolic artifact must target an invariant test, but matched {} function `{}`",
1027                    kind.name(),
1028                    func.signature(),
1029                ))
1030            } else {
1031                let invariants = if is_sequence { std::slice::from_ref(&func) } else { &[][..] };
1032                FunctionRunner::new(&self, &setup).run_symbolic_artifact_replay(
1033                    func,
1034                    invariants,
1035                    call_after_invariant,
1036                )
1037            };
1038            res.duration = test_start.elapsed();
1039            debug!(%kind, path = %replay.path.display(), "replayed symbolic artifact");
1040            return SuiteResult::new(start.elapsed(), [(func.signature(), res)].into(), warnings);
1041        }
1042
1043        let test_fail_results = functions
1044            .iter()
1045            .filter(|func| func.test_function_kind().is_any_test_fail())
1046            .map(|func| {
1047                let reason = "`testFail*` has been removed. Consider changing to test_Revert[If|When]_Condition and expecting a revert";
1048                (func.signature(), TestResult::fail(reason.to_string()))
1049            })
1050            .collect::<Vec<_>>();
1051        if !test_fail_results.is_empty() {
1052            return self.failed_suite(start, warnings, test_fail_results);
1053        }
1054
1055        if functions.iter().any(|func| {
1056            matches!(
1057                func.test_function_kind(),
1058                TestFunctionKind::FuzzTest { .. }
1059                    | TestFunctionKind::TableTest
1060                    | TestFunctionKind::InvariantTest
1061            )
1062        }) {
1063            setup.fuzz_fixtures = self.fuzz_fixtures(setup.address);
1064        }
1065
1066        let early_exit = &self.tcfg.early_exit;
1067        let test_matcher = self.test_matcher();
1068        if self.progress.is_some() {
1069            let interrupt = early_exit.clone();
1070            self.tokio_handle.spawn(async move {
1071                signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
1072                interrupt.record_ctrl_c();
1073            });
1074        }
1075
1076        let InvariantCampaignSelection {
1077            matched_boolean_invariant_fns,
1078            merge_boolean_suite: merge_invariant_suite,
1079            shared_boolean_namespace,
1080            boolean_suite_anchor: invariant_suite_anchor,
1081            optimization_anchors: _,
1082        } = select_invariant_campaigns(
1083            &invariant_fns,
1084            &functions,
1085            &self.config,
1086            &self.mcr.inline_config,
1087            self.name,
1088        );
1089
1090        let test_results = functions
1091            .par_iter()
1092            .filter_map(|&func| {
1093                // Early exit if we're running with fail-fast and a test already failed.
1094                if early_exit.should_stop() {
1095                    return None;
1096                }
1097                // Invariant tests run either as a shared boolean suite or as a single
1098                // optimization campaign; other test kinds keep their original invariant set.
1099                let invariants: &[&Function] = if func.is_invariant_test() {
1100                    if is_optimization_invariant(func) {
1101                        std::slice::from_ref(&func)
1102                    } else if merge_invariant_suite {
1103                        // Only the suite anchor runs the merged boolean campaign.
1104                        if invariant_suite_anchor != Some(func) {
1105                            return None;
1106                        }
1107                        matched_boolean_invariant_fns.as_slice()
1108                    } else {
1109                        std::slice::from_ref(&func)
1110                    }
1111                } else {
1112                    invariant_fns.as_slice()
1113                };
1114
1115                // Skip invariant anchors that have no predicates to execute.
1116                if func.is_invariant_test() && invariants.is_empty() {
1117                    return None;
1118                }
1119
1120                let start = Instant::now();
1121
1122                let _guard = self.tokio_handle.enter();
1123
1124                let _guard;
1125                let current_span = tracing::Span::current();
1126                if current_span.is_none() || current_span.id() != self.span.id() {
1127                    _guard = self.span.enter();
1128                }
1129
1130                let sig = func.signature();
1131                let kind =
1132                    test_matcher.test_function_kind(self.name, func, generated_symbolic_regression);
1133
1134                let _guard = debug_span!(
1135                    "test",
1136                    %kind,
1137                    name = %if enabled!(tracing::Level::TRACE) { &sig } else { &func.name },
1138                )
1139                .entered();
1140
1141                let mut res = FunctionRunner::new(&self, &setup).run(
1142                    func,
1143                    invariants,
1144                    shared_boolean_namespace,
1145                    kind,
1146                    call_after_invariant,
1147                    identified_contracts.as_ref(),
1148                );
1149                res.duration = start.elapsed();
1150
1151                // Record test failure for early exit (only triggers if fail-fast is enabled).
1152                if res.status.is_failure() {
1153                    early_exit.record_failure();
1154                }
1155
1156                Some((sig, res))
1157            })
1158            .collect::<BTreeMap<_, _>>();
1159
1160        SuiteResult::new(start.elapsed(), test_results, warnings)
1161    }
1162
1163    /// Returns a suite that failed before its tests could run, tripping the global fail-fast
1164    /// flag so sibling parallel suites (notably long-running invariant campaigns) observe
1165    /// `should_stop()` and exit at their next run boundary instead of running to their timeout.
1166    fn failed_suite(
1167        &self,
1168        start: Instant,
1169        warnings: Vec<String>,
1170        results: impl IntoIterator<Item = (String, TestResult)>,
1171    ) -> SuiteResult {
1172        self.tcfg.early_exit.record_failure();
1173        SuiteResult::new(start.elapsed(), results.into_iter().collect(), warnings)
1174    }
1175}
1176
1177/// Executes a single test function, returning a [`TestResult`].
1178struct FunctionRunner<'a, FEN: FoundryEvmNetwork> {
1179    /// The function-level configuration.
1180    tcfg: Cow<'a, TestRunnerConfig<FEN>>,
1181    /// The EVM executor.
1182    executor: Cow<'a, Executor<FEN>>,
1183    /// The parent runner.
1184    cr: &'a ContractRunner<'a, FEN>,
1185    /// The address of the test contract.
1186    address: Address,
1187    /// The test setup result.
1188    setup: &'a TestSetup,
1189    /// The test result. Returned after running the test.
1190    result: TestResult,
1191}
1192
1193/// A replayed and shrunk invariant counterexample.
1194struct ReplayedInvariantSequence {
1195    call_sequence: Vec<BaseCounterExample>,
1196    artifact: Option<SymbolicArtifactRef>,
1197    minimization: Option<SymbolicCounterexampleMinimization>,
1198    fork_block_number: Option<u64>,
1199}
1200
1201/// An invariant failure confirmed while solving a captured fuzz frontier.
1202struct ConfirmedFrontierInvariantFailure {
1203    invariant_idx: usize,
1204    call_sequence: Vec<BasicTxDetails>,
1205    replay: CheckSequenceOutcome,
1206}
1207
1208/// A stateful call sequence replay target shared by symbolic minimization and failure checks.
1209#[derive(Clone, Copy)]
1210struct SequenceReplay<'a> {
1211    invariant_config: &'a InvariantConfig,
1212    invariant_contract: &'a InvariantContract<'a>,
1213    target_invariant: &'a Function,
1214    assertion_failure: bool,
1215    storage: &'a [SymbolicStorageAssignment],
1216}
1217
1218/// Metadata for the symbolic artifact persisted with a replayed invariant sequence.
1219struct SequenceArtifactSpec<'a> {
1220    file_name: &'a str,
1221    fail_on_revert: bool,
1222    failure: Option<SymbolicInvariantArtifactFailure>,
1223}
1224
1225/// Returns `true` if two sequence replays failed in the same way at the same site.
1226fn same_sequence_failure(actual: &CheckSequenceOutcome, expected: &CheckSequenceOutcome) -> bool {
1227    actual.replayed_entirely == expected.replayed_entirely
1228        && actual.reason == expected.reason
1229        && actual.failure_site == expected.failure_site
1230        && actual.sequence_assertion_failure == expected.sequence_assertion_failure
1231}
1232
1233impl<'a, FEN: FoundryEvmNetwork> Deref for FunctionRunner<'a, FEN> {
1234    type Target = Cow<'a, TestRunnerConfig<FEN>>;
1235
1236    #[inline(always)]
1237    fn deref(&self) -> &Self::Target {
1238        &self.tcfg
1239    }
1240}
1241
1242impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> {
1243    fn new(cr: &'a ContractRunner<'a, FEN>, setup: &'a TestSetup) -> Self {
1244        Self {
1245            tcfg: Cow::Borrowed(cr.tcfg.as_ref()),
1246            executor: Cow::Borrowed(&cr.executor),
1247            cr,
1248            address: setup.address,
1249            setup,
1250            result: TestResult::new(setup),
1251        }
1252    }
1253
1254    const fn revert_decoder(&self) -> &'a RevertDecoder {
1255        &self.cr.mcr.revert_decoder
1256    }
1257
1258    /// Creates the progress bar for a fuzz or invariant campaign, if progress is shown.
1259    fn fuzz_progress(
1260        &self,
1261        test_name: &str,
1262        timeout: Option<u32>,
1263        runs: u32,
1264    ) -> Option<indicatif::ProgressBar> {
1265        self.cr.progress?.inner.lock().start_fuzz_progress(self.cr.name, test_name, timeout, runs)
1266    }
1267
1268    fn fuzz_minimize_target_id(&self, test_name: &str) -> String {
1269        let network = self
1270            .cr
1271            .mcr
1272            .tcfg
1273            .multi_network
1274            .pass_network
1275            .as_ref()
1276            .map(|network| format!("{network:?}"))
1277            .unwrap_or_else(|| "default".to_string());
1278        format!("{network}:{}::{test_name}", self.cr.name)
1279    }
1280
1281    /// Builds a symbolic counterexample artifact for this test.
1282    fn symbolic_artifact(
1283        &self,
1284        test_name: &str,
1285        kind: SymbolicCounterexampleArtifactKind,
1286        symbolic: &SymbolicResult,
1287        fail_on_revert: bool,
1288        calls: Vec<SymbolicCounterexampleCall>,
1289    ) -> SymbolicCounterexampleArtifact {
1290        SymbolicCounterexampleArtifact::new(
1291            kind,
1292            SymbolicCounterexampleTestIdentity {
1293                contract: self.cr.name.to_string(),
1294                test: test_name.to_string(),
1295            },
1296            symbolic,
1297            SymbolicCounterexampleReplaySemantics { fail_on_revert },
1298            calls,
1299        )
1300    }
1301
1302    /// Writes `artifact` to the stable per-test path, so the latest counterexample replaces
1303    /// older ones, and returns a reference to it.
1304    fn write_symbolic_artifact(
1305        &self,
1306        file_name: &str,
1307        artifact: &SymbolicCounterexampleArtifact,
1308    ) -> Option<SymbolicArtifactRef> {
1309        let dir = self
1310            .config
1311            .cache_path
1312            .join("symbolic")
1313            .join(sanitize_symbolic_artifact_component(self.cr.name));
1314        let path = dir.join(symbolic_artifact_file_name(self.cr.name, file_name, artifact.kind));
1315        if let Err(err) = foundry_common::fs::create_dir_all(&dir) {
1316            tracing::error!(%err, path = %dir.display(), "Failed to create symbolic artifact dir");
1317            return None;
1318        }
1319        if let Err(err) = foundry_common::fs::write_json_file(&path, artifact) {
1320            tracing::error!(%err, path = %path.display(), "Failed to write symbolic artifact");
1321            return None;
1322        }
1323        Some(SymbolicArtifactRef::new(path))
1324    }
1325
1326    /// Persists a replay-confirmed stateful counterexample as a sequence artifact.
1327    fn persist_sequence_artifact(
1328        &self,
1329        test_name: &str,
1330        file_name: &str,
1331        calls: Vec<SymbolicCounterexampleCall>,
1332        fail_on_revert: bool,
1333        storage: &[SymbolicStorageAssignment],
1334        failure: Option<SymbolicInvariantArtifactFailure>,
1335    ) -> Option<SymbolicArtifactRef> {
1336        if calls.is_empty() || !self.config.symbolic.enabled {
1337            return None;
1338        }
1339        let symbolic = SymbolicResult::incomplete(
1340            &self.config.symbolic,
1341            SymbolicStopReason::Error,
1342            "concrete replay confirmed stateful counterexample",
1343            SymbolicStats::default(),
1344            SymbolicReplayMetadata::confirmed(),
1345            SymbolicCallTrace::none(),
1346            None,
1347        );
1348        let mut artifact = self.symbolic_artifact(
1349            test_name,
1350            SymbolicCounterexampleArtifactKind::Sequence,
1351            &symbolic,
1352            fail_on_revert,
1353            calls,
1354        );
1355        if !storage.is_empty() {
1356            artifact = artifact.with_storage(storage.to_vec());
1357        }
1358        if let Some(failure) = failure {
1359            artifact = artifact.with_invariant_failure(failure);
1360        }
1361        self.write_symbolic_artifact(file_name, &artifact)
1362    }
1363
1364    /// Converts a counterexample sequence into artifact calls.
1365    fn sequence_calls(
1366        &self,
1367        call_sequence: &[BaseCounterExample],
1368    ) -> Vec<SymbolicCounterexampleCall> {
1369        call_sequence
1370            .iter()
1371            .map(|counterexample| {
1372                SymbolicCounterexampleCall::from_base_counterexample(
1373                    counterexample,
1374                    CALLER,
1375                    self.address,
1376                )
1377            })
1378            .collect()
1379    }
1380
1381    /// Replays a single-call minimization candidate and checks it fails for `expected_reason`.
1382    fn replay_confirmed_symbolic_single_call(
1383        &self,
1384        call: &SymbolicCounterexampleCall,
1385        expected_reason: Option<&str>,
1386    ) -> Result<(RawCallResult<FEN>, Option<String>), String> {
1387        let Some(expected_reason) = expected_reason else {
1388            return Err("candidate replay has no stable failure reason to compare".to_string());
1389        };
1390
1391        let mut executor = self.clone_executor();
1392        let raw = execute_tx(&mut executor, &call.to_basic_tx_details())
1393            .map_err(|err| err.to_string())?;
1394        if executor.is_raw_call_success(self.address, Cow::Borrowed(&raw.state_changeset), &raw) {
1395            return Err("candidate replay succeeded".to_string());
1396        }
1397        if let Some(reason) = raw.skip_reason() {
1398            return Err(format!("vm.skip during concrete replay: {reason}"));
1399        }
1400
1401        let reason = (raw.reverted || raw.exit_reason.is_some_and(|reason| !reason.is_ok()))
1402            .then(|| self.revert_decoder().decode(&raw.result, raw.exit_reason));
1403        if reason.as_deref() != Some(expected_reason) {
1404            return Err(format!(
1405                "candidate replay failed with different reason: expected `{expected_reason}`, got `{}`",
1406                reason.as_deref().unwrap_or("")
1407            ));
1408        }
1409        Ok((raw, reason))
1410    }
1411
1412    /// Shrinks and replays a failing invariant call sequence, persisting the confirmed
1413    /// counterexample as a symbolic artifact.
1414    #[expect(clippy::too_many_arguments)]
1415    fn replay_invariant_error_sequence(
1416        &mut self,
1417        replay: SequenceReplay<'_>,
1418        original_calls: &[BasicTxDetails],
1419        inner_sequence: Option<Vec<Option<BasicTxDetails>>>,
1420        identified_contracts: &ContractsByAddress,
1421        current_settings: &InvariantSettings,
1422        artifact: SequenceArtifactSpec<'_>,
1423        progress: Option<&indicatif::ProgressBar>,
1424        position: Option<(usize, usize)>,
1425    ) -> Result<ReplayedInvariantSequence> {
1426        let minimization = self.minimize_symbolic_invariant_sequence(
1427            replay,
1428            original_calls,
1429            identified_contracts,
1430            current_settings,
1431        );
1432
1433        let mut replay_config = replay.invariant_config.clone();
1434        let minimized_txes;
1435        let replay_calls = if let Some(minimization) = &minimization {
1436            minimized_txes = minimization
1437                .minimized_calls
1438                .iter()
1439                .map(SymbolicCounterexampleCall::to_basic_tx_details)
1440                .collect::<Vec<_>>();
1441            replay_config.shrink_run_limit = 0;
1442            minimized_txes.as_slice()
1443        } else {
1444            original_calls
1445        };
1446
1447        let ReplayErrorResult { counterexample_sequence: call_sequence, fork_block_number, .. } =
1448            self.replay_error(
1449                replay_config,
1450                self.clone_executor_with_symbolic_storage(replay.storage)?,
1451                replay_calls,
1452                inner_sequence,
1453                replay.assertion_failure,
1454                None,
1455                &[],
1456                replay.invariant_contract,
1457                replay.target_invariant,
1458                identified_contracts,
1459                progress,
1460                position,
1461            )?;
1462
1463        let test_name = replay.target_invariant.signature();
1464        let calls = self.sequence_calls(&call_sequence);
1465        let (artifact_ref, minimization) = match minimization {
1466            None => (
1467                self.persist_sequence_artifact(
1468                    &test_name,
1469                    artifact.file_name,
1470                    calls,
1471                    artifact.fail_on_revert,
1472                    replay.storage,
1473                    artifact.failure,
1474                ),
1475                None,
1476            ),
1477            Some(minimization) => {
1478                let original = self.persist_sequence_artifact(
1479                    &test_name,
1480                    &format!("original__{}", artifact.file_name),
1481                    minimization.original_calls.clone(),
1482                    artifact.fail_on_revert,
1483                    replay.storage,
1484                    artifact.failure.clone(),
1485                );
1486                let minimized = self.persist_sequence_artifact(
1487                    &test_name,
1488                    artifact.file_name,
1489                    calls,
1490                    artifact.fail_on_revert,
1491                    replay.storage,
1492                    artifact.failure,
1493                );
1494                // Schema v1 cannot persist an empty sequence; retain the confirmed original
1495                // artifact.
1496                let primary = if call_sequence.is_empty() { &original } else { &minimized };
1497                let primary = primary.clone();
1498                let metadata = original.zip(minimized).map(|(original, minimized)| {
1499                    SymbolicCounterexampleMinimization::new(
1500                        original,
1501                        minimized,
1502                        minimization.attempts,
1503                        minimization.accepted,
1504                        minimization.original_calldata_bytes(),
1505                        minimization.minimized_calldata_bytes(),
1506                    )
1507                    .with_sequence_lengths(
1508                        minimization.original_calls.len(),
1509                        minimization.minimized_calls.len(),
1510                    )
1511                });
1512                (primary, metadata)
1513            }
1514        };
1515
1516        Ok(ReplayedInvariantSequence {
1517            call_sequence,
1518            artifact: artifact_ref,
1519            minimization,
1520            fork_block_number,
1521        })
1522    }
1523
1524    /// Shrinks and replays a failing call sequence, collecting logs, traces and coverage into
1525    /// the test result. Returns the counterexample, the terminal check outcome when shrinking
1526    /// re-checked the sequence, and the fork block number.
1527    #[expect(clippy::too_many_arguments)]
1528    fn replay_error(
1529        &mut self,
1530        config: InvariantConfig,
1531        executor: Executor<FEN>,
1532        calls: &[BasicTxDetails],
1533        inner_sequence: Option<Vec<Option<BasicTxDetails>>>,
1534        expect_assertion_failure: bool,
1535        target_value: Option<I256>,
1536        targets: &[Address],
1537        invariant_contract: &InvariantContract<'_>,
1538        target_invariant: &Function,
1539        identified_contracts: &ContractsByAddress,
1540        progress: Option<&indicatif::ProgressBar>,
1541        position: Option<(usize, usize)>,
1542    ) -> Result<ReplayErrorResult> {
1543        replay_error(
1544            config,
1545            executor,
1546            calls,
1547            inner_sequence,
1548            expect_assertion_failure,
1549            target_value.is_none().then(|| self.revert_decoder()),
1550            target_value,
1551            targets,
1552            invariant_contract,
1553            target_invariant,
1554            &self.cr.mcr.known_contracts,
1555            identified_contracts.clone(),
1556            &mut self.result.logs,
1557            &mut self.result.traces,
1558            &mut self.result.debug_bytecodes,
1559            &mut self.result.line_coverage,
1560            &mut self.result.deprecated_cheatcodes,
1561            progress,
1562            &self.tcfg.early_exit,
1563            position,
1564        )
1565    }
1566
1567    fn minimize_symbolic_invariant_sequence(
1568        &self,
1569        replay: SequenceReplay<'_>,
1570        calls: &[BasicTxDetails],
1571        identified_contracts: &ContractsByAddress,
1572        current_settings: &InvariantSettings,
1573    ) -> Option<MinimizedSequence> {
1574        if !self.config.symbolic.enabled || calls.is_empty() {
1575            return None;
1576        }
1577
1578        let original_calls = self.sequence_calls(&base_counterexamples(
1579            calls,
1580            identified_contracts,
1581            replay.invariant_config.show_solidity,
1582        ));
1583        let expected = self.symbolic_sequence_failure(replay, &original_calls)?;
1584        let preserves = |candidate: &[SymbolicCounterexampleCall]| {
1585            self.symbolic_sequence_failure(replay, candidate)
1586                .is_some_and(|actual| same_sequence_failure(&actual, &expected))
1587        };
1588
1589        let minimization = minimize_sequence_counterexample(
1590            &original_calls,
1591            &self.symbolic_sequence_sender_candidates(current_settings),
1592            replay.invariant_config.shrink_run_limit as usize,
1593            preserves,
1594        )?;
1595        preserves(&minimization.minimized_calls).then_some(minimization)
1596    }
1597
1598    fn symbolic_sequence_sender_candidates(
1599        &self,
1600        current_settings: &InvariantSettings,
1601    ) -> Vec<Address> {
1602        let mut candidates = if current_settings.target_senders.is_empty() {
1603            vec![self.sender, CALLER, address!("0x0000000000000000000000000000000000000100")]
1604        } else {
1605            current_settings.target_senders.clone()
1606        };
1607
1608        candidates.retain(|sender| {
1609            !current_settings.excluded_senders.contains(sender)
1610                && (current_settings.target_senders.is_empty()
1611                    || current_settings.target_senders.contains(sender))
1612        });
1613        candidates.sort_unstable();
1614        candidates.dedup();
1615        candidates
1616    }
1617
1618    /// Replays `calls` concretely and returns the outcome if the sequence still fails.
1619    fn symbolic_sequence_failure(
1620        &self,
1621        replay: SequenceReplay<'_>,
1622        calls: &[SymbolicCounterexampleCall],
1623    ) -> Option<CheckSequenceOutcome> {
1624        let txes =
1625            calls.iter().map(SymbolicCounterexampleCall::to_basic_tx_details).collect::<Vec<_>>();
1626        let sequence = (0..txes.len()).collect::<Vec<_>>();
1627        let outcome = check_sequence(
1628            self.clone_executor_with_symbolic_storage(replay.storage).ok()?,
1629            &txes,
1630            &sequence,
1631            replay.invariant_contract.address,
1632            replay.target_invariant.selector().into(),
1633            CheckSequenceOptions {
1634                accumulate_warp_roll: false,
1635                fail_on_revert: replay.invariant_config.fail_on_revert,
1636                expect_assertion_failure: replay.assertion_failure,
1637                call_after_invariant: replay.invariant_contract.call_after_invariant,
1638                rd: Some(self.revert_decoder()),
1639            },
1640        )
1641        .ok()?;
1642        (!outcome.success).then_some(outcome)
1643    }
1644
1645    /// Converts a persisted counterexample into transactions (applying `show_solidity` in
1646    /// place) and replays it through `check_sequence`.
1647    fn replay_persisted_call_sequence(
1648        &self,
1649        invariant_contract: &InvariantContract<'_>,
1650        call_sequence: &mut [BaseCounterExample],
1651        expect_assertion_failure: bool,
1652        storage: &[SymbolicStorageAssignment],
1653    ) -> Result<(Vec<BasicTxDetails>, CheckSequenceOutcome)> {
1654        let config = &self.config.invariant;
1655        let txes = base_counterexamples_to_txes(call_sequence, config.show_solidity);
1656        // Replay the whole persisted sequence: it was produced under the depth of an earlier run,
1657        // and cutting it to the current depth would turn a still-failing sequence into a pass.
1658        let sequence = (0..txes.len()).collect::<Vec<_>>();
1659        let outcome = check_sequence(
1660            self.clone_executor_with_symbolic_storage(storage)?,
1661            &txes,
1662            &sequence,
1663            invariant_contract.address,
1664            invariant_contract.anchor().selector().into(),
1665            CheckSequenceOptions {
1666                accumulate_warp_roll: config.has_delay(),
1667                fail_on_revert: config.fail_on_revert,
1668                expect_assertion_failure,
1669                call_after_invariant: invariant_contract.call_after_invariant,
1670                rd: Some(self.revert_decoder()),
1671            },
1672        )?;
1673        Ok((txes, outcome))
1674    }
1675
1676    /// Replays persisted handler-side assertion bugs. A file is kept only if the anchor still
1677    /// asserts at the same `(reverter, selector)` site; stale files (anchor no longer asserts,
1678    /// asserts at a different site, or earlier call asserts) are deleted in place.
1679    fn replay_persisted_handler_failures(
1680        &self,
1681        handlers_dir: &Path,
1682        current_settings: &InvariantSettings,
1683    ) -> (HandlerFailureMap, SymbolicHandlerStorageMap) {
1684        let mut replayed = HandlerFailureMap::new();
1685        let mut replayed_storage = SymbolicHandlerStorageMap::default();
1686        let entries = match std::fs::read_dir(handlers_dir) {
1687            Ok(entries) => entries,
1688            Err(err) => {
1689                if err.kind() != std::io::ErrorKind::NotFound {
1690                    error!(%err, "Failed to read handler failure dir");
1691                }
1692                return (replayed, replayed_storage);
1693            }
1694        };
1695        let config = &self.config.invariant;
1696        let mut replayed_canonical_files = std::collections::HashSet::<PathBuf>::new();
1697        let mut legacy_files = Vec::new();
1698        for entry in entries.flatten() {
1699            let path = entry.path();
1700            if path.extension().and_then(|s| s.to_str()) != Some("json") {
1701                continue;
1702            }
1703            let Some(InvariantPersistedFailure {
1704                mut call_sequence,
1705                storage,
1706                failure_site,
1707                fingerprint_provenance,
1708                fork_block_number,
1709                ..
1710            }) = persisted_call_sequence(&path, current_settings)
1711            else {
1712                continue;
1713            };
1714            if call_sequence.is_empty() {
1715                let _ = std::fs::remove_file(&path);
1716                continue;
1717            }
1718            // Keep files that `forge fuzz replay` cannot judge because the fork block changed.
1719            let keep_stale = self.cr.mcr.tcfg.fuzz_failure_replay
1720                && warn_fork_block_mismatch(
1721                    path.display(),
1722                    fork_block_number,
1723                    self.setup.fork_block_number,
1724                );
1725            let expected_site = failure_site.and_then(|site| match site {
1726                SymbolicInvariantFailureSite::SequenceCall { target, selector, fingerprint } => {
1727                    Some((target, selector, fingerprint))
1728                }
1729                _ => None,
1730            });
1731            let edge_fingerprinted =
1732                expected_site.is_some_and(|(target, selector, fingerprint)| {
1733                    fingerprint != handler_edge_fingerprint(None, target, selector)
1734                });
1735            let txes = base_counterexamples_to_txes(&mut call_sequence, config.show_solidity);
1736            let sequence = (0..txes.len()).collect::<Vec<_>>();
1737            let mut replay_executor = match self.clone_executor_with_symbolic_storage(&storage) {
1738                Ok(executor) => executor,
1739                Err(err) => {
1740                    error!(%err, "Failed to apply symbolic storage for handler-side assertion replay");
1741                    continue;
1742                }
1743            };
1744            if let Some(provenance) = fingerprint_provenance {
1745                replay_executor
1746                    .inspector_mut()
1747                    .collect_edge_coverage_with_edge_config(provenance.edge_config());
1748            }
1749            match replay_handler_failure_sequence(
1750                replay_executor,
1751                &txes,
1752                &sequence,
1753                config.has_delay(),
1754                Some(self.revert_decoder()),
1755            ) {
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                    if let Some((target, selector, fingerprint)) = expected_site {
1761                        let canonical_handler = target == outcome.handler_target;
1762                        let legacy_handler = target == outcome.reverter;
1763                        let different_handler =
1764                            (!canonical_handler && !legacy_handler) || selector != outcome.selector;
1765                        let verified_fingerprint_mismatch = fingerprint_provenance.is_some()
1766                            && fingerprint != outcome.anchor_fingerprint;
1767                        if different_handler || verified_fingerprint_mismatch {
1768                            if !keep_stale {
1769                                let _ = std::fs::remove_file(&path);
1770                            }
1771                            continue;
1772                        }
1773                        if canonical_handler {
1774                            replayed_canonical_files.insert(path);
1775                        } else {
1776                            legacy_files.push((
1777                                path,
1778                                handler_failure_file(
1779                                    handlers_dir,
1780                                    outcome.handler_target,
1781                                    outcome.selector,
1782                                ),
1783                            ));
1784                        }
1785                    }
1786                    // Legacy edge fingerprints have no reproducible provenance. Retain their
1787                    // identity after the handler site reproduces instead of replacing or deleting
1788                    // them based on an unverifiable fingerprint.
1789                    let fingerprint = if edge_fingerprinted && fingerprint_provenance.is_none() {
1790                        expected_site.expect("edge fingerprint has a site").2
1791                    } else {
1792                        outcome.anchor_fingerprint
1793                    };
1794                    let failure = HandlerAssertionFailure::from_replayed_sequence(
1795                        txes,
1796                        outcome.handler_target,
1797                        outcome.selector,
1798                        fingerprint,
1799                        outcome.revert_reason.unwrap_or_default(),
1800                    );
1801                    let site = (failure.reverter, failure.selector);
1802                    // On collision keep the shorter reproducer.
1803                    let already_shorter = replayed
1804                        .get(&site)
1805                        .and_then(InvariantFuzzError::as_handler_assertion)
1806                        .is_some_and(|existing| {
1807                            existing.call_sequence.len() <= failure.call_sequence.len()
1808                        });
1809                    if !already_shorter {
1810                        replayed_storage.insert(
1811                            (failure.reverter, failure.selector, failure.edge_fingerprint),
1812                            SymbolicHandlerReplayStorage {
1813                                call_sequence: failure.call_sequence.clone(),
1814                                assignments: storage,
1815                                fingerprint_provenance,
1816                            },
1817                        );
1818                        replayed.insert(site, InvariantFuzzError::HandlerAssertion(failure));
1819                    }
1820                }
1821                // Stale: anchor doesn't assert or earlier call asserts.
1822                Ok(_) => {
1823                    if !keep_stale {
1824                        let _ = std::fs::remove_file(&path);
1825                    }
1826                }
1827                Err(err) => {
1828                    error!(%err, "Failed to replay handler-side assertion bug");
1829                }
1830            }
1831        }
1832        // A legacy symbolic handler identity is removed only after its canonical replacement has
1833        // itself replayed successfully. This keeps migration safe across interrupted/failed writes
1834        // and independent of directory iteration order.
1835        for (legacy, canonical) in legacy_files {
1836            if replayed_canonical_files.contains(&canonical) {
1837                let _ = std::fs::remove_file(legacy);
1838            }
1839        }
1840        (replayed, replayed_storage)
1841    }
1842
1843    /// Configures this runner with the inline configuration for the contract.
1844    fn apply_function_inline_config(&mut self, func: &Function) -> Result<()> {
1845        if self.inline_config.contains_function(self.cr.name, &func.name) {
1846            let new_config = Arc::new(self.cr.inline_config(Some(func))?);
1847            self.tcfg.to_mut().reconfigure_with(new_config);
1848            self.tcfg.configure_executor(self.executor.to_mut());
1849        }
1850        Ok(())
1851    }
1852
1853    fn run(
1854        mut self,
1855        func: &Function,
1856        invariants: &[&Function],
1857        shared_invariant_namespace: bool,
1858        kind: TestFunctionKind,
1859        call_after_invariant: bool,
1860        identified_contracts: Option<&ContractsByAddress>,
1861    ) -> TestResult {
1862        if let Err(e) = self.apply_function_inline_config(func) {
1863            self.result.single_fail(Some(e.to_string()));
1864            return self.result;
1865        }
1866        let kind = effective_test_function_kind(kind, &self.config, func);
1867
1868        // In showmap replay mode and `forge fuzz`, only fuzz/invariant tests are runnable.
1869        if (self.cr.mcr.tcfg.showmap.is_some() || self.cr.mcr.tcfg.fuzz_only)
1870            && matches!(
1871                kind,
1872                TestFunctionKind::UnitTest { .. }
1873                    | TestFunctionKind::TableTest
1874                    | TestFunctionKind::SymbolicTest
1875            )
1876        {
1877            if let Some(showmap) = self.cr.mcr.tcfg.showmap.as_ref() {
1878                let mode = if showmap.emit_files { "showmap" } else { "replay" };
1879                self.result.replay_skip(format!("not runnable in {mode} mode"));
1880            } else if self.cr.mcr.tcfg.fuzz_failure_replay {
1881                self.result
1882                    .single_skip(SkipReason(Some("not runnable in replay mode".to_string())));
1883            } else {
1884                self.result.single_skip(SkipReason(Some("not runnable in fuzz mode".to_string())));
1885            }
1886            return self.result;
1887        }
1888
1889        match kind {
1890            TestFunctionKind::UnitTest { .. } => self.run_unit_test(func),
1891            TestFunctionKind::FuzzTest { .. } => self.run_fuzz_test(func),
1892            TestFunctionKind::TableTest => self.run_table_test(func),
1893            TestFunctionKind::SymbolicTest => self.run_symbolic_test(func),
1894            TestFunctionKind::InvariantTest => {
1895                let fail_on_revert_for = |f: &Function| {
1896                    if self.inline_config.contains_function(self.cr.name, &f.name)
1897                        && let Ok(config) = self.cr.inline_config(Some(f))
1898                    {
1899                        return config.invariant.fail_on_revert;
1900                    }
1901                    self.config.invariant.fail_on_revert
1902                };
1903                let invariant_fns: Vec<_> =
1904                    invariants.iter().copied().map(|f| (f, fail_on_revert_for(f))).collect();
1905                self.run_invariant_test(
1906                    func,
1907                    invariant_fns,
1908                    shared_invariant_namespace,
1909                    call_after_invariant,
1910                    identified_contracts.unwrap(),
1911                )
1912            }
1913            _ => unreachable!(),
1914        }
1915    }
1916
1917    /// Runs a single unit test.
1918    ///
1919    /// Applies before test txes (if any), runs current test and returns the `TestResult`.
1920    ///
1921    /// Before test txes are applied in order and state modifications committed to the EVM database
1922    /// (therefore the unit test call will be made on modified state).
1923    /// State modifications of before test txes and unit test function call are discarded after
1924    /// test ends, similar to `eth_call`.
1925    fn run_unit_test(mut self, func: &Function) -> TestResult {
1926        // Prepare unit test execution.
1927        if self.prepare_test(func).is_err() {
1928            return self.result;
1929        }
1930
1931        // Run current unit test.
1932        let Ok((mut raw_call_result, reason)) = self.call_test(func, &[]) else {
1933            return self.result;
1934        };
1935        let success = self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result);
1936        self.result.single_result(success, reason, raw_call_result);
1937        self.result
1938    }
1939
1940    /// Calls `func` on the test contract, returning the raw result and revert reason. Skipped
1941    /// and failed calls are recorded in the test result and returned as `Err`.
1942    fn call_test(
1943        &mut self,
1944        func: &Function,
1945        args: &[DynSolValue],
1946    ) -> Result<(RawCallResult<FEN>, Option<String>), ()> {
1947        match self.executor.call(
1948            self.sender,
1949            self.address,
1950            func,
1951            args,
1952            U256::ZERO,
1953            Some(self.revert_decoder()),
1954        ) {
1955            Ok(res) => Ok((res.raw, None)),
1956            Err(EvmError::Execution(err)) => Ok((err.raw, Some(err.reason))),
1957            Err(EvmError::Skip(reason)) => {
1958                self.result.single_skip(reason);
1959                Err(())
1960            }
1961            Err(err) => {
1962                self.result.single_fail(Some(err.to_string()));
1963                Err(())
1964            }
1965        }
1966    }
1967
1968    /// Builds the symbolic executor input for one run of `func`.
1969    fn symbolic_run_input<'f>(
1970        &'f self,
1971        func: &'f Function,
1972        sender: Address,
1973        collect_success_input: bool,
1974        corpus_seeds: Vec<SymbolicConcreteInput>,
1975        branch_target: Option<SymbolicBranchTarget>,
1976    ) -> SymbolicRunInput<'f> {
1977        SymbolicRunInput {
1978            executor: ExecutorView::new(self.executor.as_ref()),
1979            target: self.address,
1980            sender,
1981            function: func,
1982            value: U256::ZERO,
1983            ffi_enabled: self.config.ffi,
1984            collect_success_input,
1985            corpus_seeds,
1986            branch_target,
1987        }
1988    }
1989
1990    /// Sets the per-test corpus directory in `fuzz_config` and returns the test path name, the
1991    /// legacy corpus directory and the persisted failure `(dir, file)` paths.
1992    fn fuzz_test_paths<'f>(
1993        &self,
1994        func: &'f Function,
1995        fuzz_config: &mut FuzzConfig,
1996    ) -> (Cow<'f, str>, Option<PathBuf>, (PathBuf, PathBuf)) {
1997        let test_name = fuzz_test_path_name(&self.cr.contract.abi, func, fuzz_config, self.cr.name);
1998        let legacy_corpus_dir = legacy_fuzz_corpus_dir(
1999            fuzz_config.corpus.corpus_dir.as_deref(),
2000            self.cr.name,
2001            func,
2002            &test_name,
2003        );
2004        let failure_paths = test_paths(
2005            &mut fuzz_config.corpus,
2006            fuzz_config.failure_persist_dir.clone().unwrap(),
2007            self.cr.name,
2008            &test_name,
2009        );
2010        (test_name, legacy_corpus_dir, failure_paths)
2011    }
2012
2013    /// Imports persisted fuzz corpus entries as symbolic path-priority hints.
2014    fn import_symbolic_fuzz_corpus(
2015        &self,
2016        func: &Function,
2017    ) -> (Vec<SymbolicConcreteInput>, Option<SymbolicCorpusSeedMetadata>) {
2018        let mut inputs = Vec::new();
2019        if !should_symbolically_import_fuzz_corpus(&self.config, func) {
2020            return (inputs, None);
2021        }
2022
2023        let mut fuzz_config = self.config.fuzz.clone();
2024        let (_, legacy_corpus_dir, _) = self.fuzz_test_paths(func, &mut fuzz_config);
2025        let corpus_dir = legacy_corpus_dir.or(fuzz_config.corpus.corpus_dir);
2026        let limit = self.config.symbolic.corpus_seed_limit;
2027        let mut metadata = SymbolicCorpusSeedMetadata {
2028            corpus_dir: corpus_dir.clone(),
2029            limit,
2030            loaded: 0,
2031            skipped: 0,
2032            used: Vec::new(),
2033        };
2034        let Some(corpus_dir) = corpus_dir else {
2035            let _ = sh_warn!(
2036                "`--symbolic-use-fuzz-corpus` requires `--fuzz-corpus-dir` or `fuzz.corpus_dir`; \
2037                 running without imported corpus seeds"
2038            );
2039            return (inputs, Some(metadata));
2040        };
2041        if limit == 0 {
2042            return (inputs, Some(metadata));
2043        }
2044
2045        'dirs: for replay_dir in canonical_replay_dirs(&corpus_dir) {
2046            let mut entries = read_corpus_dir(&replay_dir).collect::<Vec<_>>();
2047            entries.sort_by(|left, right| left.path.cmp(&right.path));
2048            for entry in entries {
2049                if inputs.len() >= limit {
2050                    break 'dirs;
2051                }
2052                metadata.loaded += 1;
2053                let input = match entry.read_tx_seq() {
2054                    Ok(tx_seq) => self.symbolic_corpus_seed_input(func, &tx_seq),
2055                    Err(err) => {
2056                        debug!(%err, path = %entry.path.display(), "failed to read symbolic corpus seed");
2057                        None
2058                    }
2059                };
2060                let Some(input) = input else {
2061                    metadata.skipped += 1;
2062                    continue;
2063                };
2064                metadata.used.push(SymbolicCorpusSeedRef {
2065                    path: entry.path,
2066                    calldata: input.calldata.clone(),
2067                });
2068                inputs.push(input);
2069            }
2070        }
2071
2072        debug!(
2073            test = %func.signature(),
2074            corpus_dir = %corpus_dir.display(),
2075            loaded = metadata.loaded,
2076            skipped = metadata.skipped,
2077            imported = inputs.len(),
2078            "imported symbolic fuzz corpus seeds"
2079        );
2080        (inputs, Some(metadata))
2081    }
2082
2083    /// Imports persisted fuzz branch frontiers as `(id, sender, branch target, input)` seeds.
2084    fn import_symbolic_fuzz_frontiers(
2085        &self,
2086        func: &Function,
2087        fuzz_config: &FuzzConfig,
2088    ) -> Vec<(u64, Address, SymbolicBranchTarget, SymbolicConcreteInput)> {
2089        let limit = self.config.symbolic.frontier_limit;
2090        if limit == 0 {
2091            return Vec::new();
2092        }
2093
2094        let Some(frontier_dir) = fuzz_config.corpus.frontier_dir.as_ref() else {
2095            let _ = sh_warn!(
2096                "`--symbolic-use-fuzz-frontiers` requires `--fuzz-frontier-dir` or \
2097                 `fuzz.frontier_dir`; running without targeted frontier seeds"
2098            );
2099            return Vec::new();
2100        };
2101
2102        let frontier_path = frontier_dir.join(FUZZ_BRANCH_FRONTIER_FILE);
2103        let artifact = match foundry_common::fs::read_json_file::<FuzzBranchFrontierArtifact>(
2104            &frontier_path,
2105        ) {
2106            Ok(artifact) => artifact,
2107            Err(err) => {
2108                debug!(
2109                    %err,
2110                    path = %frontier_path.display(),
2111                    "failed to read fuzz branch frontier artifact"
2112                );
2113                return Vec::new();
2114            }
2115        };
2116
2117        if artifact.schema != FUZZ_BRANCH_FRONTIER_SCHEMA || artifact.version != 1 {
2118            warn!(
2119                schema = %artifact.schema,
2120                version = artifact.version,
2121                path = %frontier_path.display(),
2122                "unsupported fuzz branch frontier artifact"
2123            );
2124            return Vec::new();
2125        }
2126        let signature = func.signature();
2127        if artifact.test != signature {
2128            warn!(
2129                artifact_test = %artifact.test,
2130                test = %signature,
2131                path = %frontier_path.display(),
2132                "fuzz branch frontier artifact does not match symbolic target"
2133            );
2134            return Vec::new();
2135        }
2136
2137        let requested_ids = &self.config.symbolic.frontier_ids;
2138        let requested_pcs = &self.config.symbolic.frontier_pcs;
2139        let requested_selectors = &self.config.symbolic.frontier_selectors;
2140        let parsed_selectors = parse_frontier_selectors(requested_selectors, &signature);
2141        let selection_active = !requested_ids.is_empty()
2142            || !requested_pcs.is_empty()
2143            || !requested_selectors.is_empty();
2144        let mut skipped_by_selection = 0usize;
2145        let mut imported_ids = Vec::new();
2146        let mut imported_pcs = Vec::new();
2147        let mut imported_selectors = Vec::new();
2148        let mut imported = Vec::with_capacity(limit.min(artifact.frontiers.len()));
2149        for frontier in artifact.frontiers {
2150            let selector = frontier_selector(&frontier);
2151            if (!requested_ids.is_empty() && !requested_ids.contains(&frontier.id))
2152                || (!requested_pcs.is_empty() && !requested_pcs.contains(&frontier.site.pc))
2153                || (!requested_selectors.is_empty()
2154                    && selector.is_none_or(|selector| !parsed_selectors.contains(&selector)))
2155            {
2156                skipped_by_selection += 1;
2157                continue;
2158            }
2159            if imported.len() == limit {
2160                if selection_active {
2161                    continue;
2162                }
2163                break;
2164            }
2165            if !matches!(
2166                frontier.site.opcode,
2167                opcode::EQ | opcode::LT | opcode::GT | opcode::SLT | opcode::SGT | opcode::ISZERO
2168            ) {
2169                debug!(
2170                    opcode = frontier.site.opcode,
2171                    id = frontier.id,
2172                    "skipping unsupported fuzz branch frontier opcode"
2173                );
2174                continue;
2175            }
2176            let ([call], 0) = (frontier.sequence.as_slice(), frontier.call_index) else {
2177                debug!(
2178                    id = frontier.id,
2179                    sequence_len = frontier.sequence.len(),
2180                    call_index = frontier.call_index,
2181                    "skipping non-stateless fuzz branch frontier"
2182                );
2183                continue;
2184            };
2185            let Some(input) = self.symbolic_corpus_seed_input(func, std::slice::from_ref(call))
2186            else {
2187                debug!(id = frontier.id, "skipping fuzz branch frontier with incompatible call");
2188                continue;
2189            };
2190            let target = SymbolicBranchTarget::new(
2191                frontier.site.address,
2192                frontier.site.pc,
2193                frontier.site.opcode,
2194                frontier.operands.result,
2195            );
2196            imported.push((frontier.id, call.sender, target, input));
2197            imported_ids.push(frontier.id);
2198            imported_pcs.push(frontier.site.pc);
2199            imported_selectors.extend(selector);
2200        }
2201
2202        warn_unimported_frontiers("id", requested_ids, &imported_ids, &signature, &frontier_path);
2203        warn_unimported_frontiers("pc", requested_pcs, &imported_pcs, &signature, &frontier_path);
2204        warn_unimported_frontiers(
2205            "selector",
2206            &parsed_selectors,
2207            &imported_selectors,
2208            &signature,
2209            &frontier_path,
2210        );
2211        if selection_active {
2212            let _ = sh_status!(
2213                "Symbolic frontier selection for {signature}: imported {}, skipped {} by target \
2214                 filters (ids: {}; pcs: {}; selectors: {}; limit: {limit})",
2215                imported.len(),
2216                skipped_by_selection,
2217                frontier_filter_display(requested_ids),
2218                frontier_filter_display(requested_pcs),
2219                frontier_filter_display(requested_selectors),
2220            );
2221        }
2222
2223        debug!(
2224            test = %signature,
2225            path = %frontier_path.display(),
2226            imported = imported.len(),
2227            limit,
2228            skipped_by_selection,
2229            requested_ids = ?requested_ids,
2230            requested_pcs = ?requested_pcs,
2231            requested_selectors = ?requested_selectors,
2232            "imported fuzz branch frontiers for targeted symbolic seeding"
2233        );
2234        imported
2235    }
2236
2237    fn import_symbolic_invariant_frontiers(
2238        &self,
2239        invariant_contract: &InvariantContract<'_>,
2240        invariant_config: &InvariantConfig,
2241    ) -> Vec<(FuzzBranchFrontierRecord, Arc<[BasicTxDetails]>)> {
2242        let limit = self.config.symbolic.frontier_limit;
2243        if limit == 0 {
2244            return Vec::new();
2245        }
2246
2247        let Some(frontier_dir) = invariant_config.corpus.frontier_dir.as_ref() else {
2248            let _ = sh_warn!(
2249                "Symbolic invariant frontier seeding requires `--invariant-frontier-dir` or \
2250                 `invariant.frontier_dir`; running without targeted frontier seeds"
2251            );
2252            return Vec::new();
2253        };
2254        let frontier_path = frontier_dir.join(FUZZ_BRANCH_FRONTIER_FILE);
2255        let artifact = match foundry_common::fs::read_json_file::<FuzzBranchFrontierArtifact>(
2256            &frontier_path,
2257        ) {
2258            Ok(artifact) => artifact,
2259            Err(err) => {
2260                debug!(
2261                    %err,
2262                    path = %frontier_path.display(),
2263                    "failed to read invariant branch frontier artifact"
2264                );
2265                return Vec::new();
2266            }
2267        };
2268        if artifact.schema != STATEFUL_FUZZ_BRANCH_FRONTIER_SCHEMA || artifact.version != 2 {
2269            warn!(
2270                schema = %artifact.schema,
2271                version = artifact.version,
2272                path = %frontier_path.display(),
2273                "unsupported invariant branch frontier artifact"
2274            );
2275            return Vec::new();
2276        }
2277        let signature = invariant_contract.anchor().signature();
2278        // Boolean frontiers describe handler execution, so their recorded predicate anchor is
2279        // provenance; candidates are replayed against the currently selected predicates.
2280        if invariant_contract.is_optimization() && artifact.test != signature {
2281            warn!(
2282                artifact_test = %artifact.test,
2283                test = %signature,
2284                path = %frontier_path.display(),
2285                "invariant branch frontier artifact does not match campaign anchor"
2286            );
2287            return Vec::new();
2288        }
2289
2290        let requested_ids = &self.config.symbolic.frontier_ids;
2291        let requested_pcs = &self.config.symbolic.frontier_pcs;
2292        let requested_selectors = &self.config.symbolic.frontier_selectors;
2293        let parsed_selectors = parse_frontier_selectors(requested_selectors, &signature);
2294        let select_frontier_ids = !requested_ids.is_empty();
2295        let select_frontier_pcs = !requested_pcs.is_empty();
2296        let select_frontier_selectors = !requested_selectors.is_empty();
2297
2298        let FuzzBranchFrontierArtifact { sequences, mut frontiers, .. } = artifact;
2299        let sequences =
2300            sequences.into_iter().map(Arc::<[BasicTxDetails]>::from).collect::<Vec<_>>();
2301        let mut observed_results = HashMap::<(Address, usize, u8), u8>::default();
2302        for frontier in &frontiers {
2303            let key = (frontier.site.address, frontier.site.pc, frontier.site.opcode);
2304            let result_bit = if frontier.operands.result { 2 } else { 1 };
2305            *observed_results.entry(key).or_default() |= result_bit;
2306        }
2307        for frontier in &mut frontiers {
2308            frontier.both_results_retained = observed_results
2309                .get(&(frontier.site.address, frontier.site.pc, frontier.site.opcode))
2310                .is_some_and(|results| *results == 3);
2311        }
2312        frontiers.retain(|frontier| {
2313            if select_frontier_ids && !requested_ids.contains(&frontier.id) {
2314                return false;
2315            }
2316            if select_frontier_pcs && !requested_pcs.contains(&frontier.site.pc) {
2317                return false;
2318            }
2319            if !matches!(
2320                frontier.site.opcode,
2321                opcode::EQ | opcode::LT | opcode::GT | opcode::SLT | opcode::SGT | opcode::ISZERO
2322            ) {
2323                return false;
2324            }
2325            let Some(sequence) = frontier.sequence_index.and_then(|index| sequences.get(index))
2326            else {
2327                debug!(id = frontier.id, "skipping invariant frontier with missing sequence");
2328                return false;
2329            };
2330            let Some(call) = sequence.get(frontier.call_index) else {
2331                debug!(
2332                    id = frontier.id,
2333                    call_index = frontier.call_index,
2334                    sequence_len = sequence.len(),
2335                    "skipping invariant frontier with invalid call index"
2336                );
2337                return false;
2338            };
2339            if call.warp.is_some_and(|warp| !warp.is_zero())
2340                || call.roll.is_some_and(|roll| !roll.is_zero())
2341                || call.call_details.value.is_some_and(|value| !value.is_zero())
2342            {
2343                return false;
2344            }
2345            let selector = call
2346                .call_details
2347                .calldata
2348                .get(..4)
2349                .and_then(|selector| <[u8; 4]>::try_from(selector).ok())
2350                .map(Selector::from);
2351            !(select_frontier_selectors
2352                && selector.is_none_or(|selector| !parsed_selectors.contains(&selector)))
2353        });
2354        let explicit_selection =
2355            select_frontier_ids || select_frontier_pcs || select_frontier_selectors;
2356        let frontiers = select_stateful_frontiers(frontiers, limit, explicit_selection);
2357
2358        let mut imported = Vec::with_capacity(frontiers.len());
2359        for frontier in frontiers {
2360            let sequence = sequences
2361                .get(frontier.sequence_index.expect("frontier sequence index was validated"))
2362                .expect("frontier sequence was validated");
2363            imported.push((frontier, Arc::clone(sequence)));
2364        }
2365
2366        debug!(
2367            test = %signature,
2368            path = %frontier_path.display(),
2369            imported = imported.len(),
2370            limit,
2371            "imported invariant branch frontiers for targeted symbolic seeding"
2372        );
2373        imported
2374    }
2375
2376    fn symbolic_corpus_seed_input(
2377        &self,
2378        func: &Function,
2379        tx_seq: &[BasicTxDetails],
2380    ) -> Option<SymbolicConcreteInput> {
2381        let [tx] = tx_seq else {
2382            return None;
2383        };
2384        if tx.call_details.target != self.address
2385            || !tx.call_details.value.unwrap_or_default().is_zero()
2386        {
2387            return None;
2388        }
2389        let calldata = &tx.call_details.calldata;
2390        if calldata.get(..4) != Some(func.selector().as_slice()) {
2391            return None;
2392        }
2393        let args = func.abi_decode_input(&calldata[4..]).ok()?;
2394        Some(SymbolicConcreteInput { args, calldata: calldata.clone() })
2395    }
2396
2397    /// Runs a symbolic test and replays any discovered counterexample concretely.
2398    fn run_symbolic_test(mut self, func: &Function) -> TestResult {
2399        if self.prepare_test(func).is_err() {
2400            return self.result;
2401        }
2402
2403        let (corpus_seeds, mut corpus_seed_metadata) = self.import_symbolic_fuzz_corpus(func);
2404        if let Some(metadata) = corpus_seed_metadata.as_mut() {
2405            match SymbolicExecutor::modeled_corpus_seed_indexes(
2406                &self.config.symbolic,
2407                func,
2408                &corpus_seeds,
2409            ) {
2410                Ok(indexes) => {
2411                    let mut indexes = indexes.into_iter().peekable();
2412                    metadata.used = std::mem::take(&mut metadata.used)
2413                        .into_iter()
2414                        .enumerate()
2415                        .filter_map(|(idx, seed)| indexes.next_if_eq(&idx).map(|_| seed))
2416                        .collect();
2417                }
2418                Err(err) => {
2419                    debug!(
2420                        %err,
2421                        test = %func.signature(),
2422                        "failed to model imported symbolic corpus seeds"
2423                    );
2424                }
2425            }
2426        }
2427        let symbolic_config = self.config.symbolic.clone();
2428        let mut symbolic = SymbolicExecutor::new(symbolic_config.clone());
2429        let result =
2430            symbolic.run(self.symbolic_run_input(func, self.sender, false, corpus_seeds, None));
2431
2432        let (status, reason, counterexample, symbolic_result) = match result {
2433            SymbolicRunResult::Safe { stats, .. } => {
2434                (TestStatus::Success, None, None, SymbolicResult::pass(&symbolic_config, stats))
2435            }
2436            SymbolicRunResult::Incomplete { kind, reason, stats } => (
2437                TestStatus::Failure,
2438                Some(format!("incomplete symbolic execution ({kind:?}): {reason}")),
2439                None,
2440                SymbolicResult::incomplete(
2441                    &symbolic_config,
2442                    kind,
2443                    reason,
2444                    stats,
2445                    SymbolicReplayMetadata::not_required(),
2446                    SymbolicCallTrace::none(),
2447                    None,
2448                ),
2449            ),
2450            SymbolicRunResult::Counterexample { args, calldata, stats } => {
2451                self.replay_symbolic_counterexample(func, args, calldata, stats, &symbolic_config)
2452            }
2453        };
2454        let symbolic_result = match corpus_seed_metadata {
2455            Some(metadata) => symbolic_result.with_corpus_seeds(metadata),
2456            None => symbolic_result,
2457        };
2458        self.result.symbolic_result(status, reason, counterexample, symbolic_result);
2459        self.result
2460    }
2461
2462    /// Replays a symbolic counterexample concretely, minimizing and persisting it when it
2463    /// reproduces.
2464    fn replay_symbolic_counterexample(
2465        &mut self,
2466        func: &Function,
2467        args: Vec<DynSolValue>,
2468        calldata: Bytes,
2469        stats: SymbolicStats,
2470        symbolic_config: &SymbolicConfig,
2471    ) -> (TestStatus, Option<String>, Option<CounterExample>, SymbolicResult) {
2472        let symbolic_counterexample = SymbolicCounterexample::from(
2473            &BaseCounterExample::from_fuzz_call(calldata.clone(), args.clone(), None),
2474        );
2475        let incomplete = |reason: String, replay, call_trace| {
2476            SymbolicResult::incomplete(
2477                symbolic_config,
2478                SymbolicStopReason::Error,
2479                reason,
2480                stats,
2481                replay,
2482                call_trace,
2483                Some(symbolic_counterexample.clone()),
2484            )
2485        };
2486
2487        let (raw, reason) = match self.executor.call(
2488            self.sender,
2489            self.address,
2490            func,
2491            &args,
2492            U256::ZERO,
2493            Some(self.revert_decoder()),
2494        ) {
2495            Ok(res) => (res.raw, None),
2496            Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
2497            Err(EvmError::Skip(reason)) => {
2498                let replay_reason = format!("vm.skip during concrete replay: {reason}");
2499                let symbolic_result = incomplete(
2500                    "concrete replay skipped the symbolic counterexample".to_string(),
2501                    SymbolicReplayMetadata::skipped(replay_reason),
2502                    SymbolicCallTrace::none(),
2503                );
2504                return (TestStatus::Skipped, reason.0, None, symbolic_result);
2505            }
2506            Err(err) => {
2507                let reason = err.to_string();
2508                let symbolic_result = incomplete(
2509                    reason.clone(),
2510                    SymbolicReplayMetadata::error(reason.clone()),
2511                    SymbolicCallTrace::none(),
2512                );
2513                return (TestStatus::Failure, Some(reason), None, symbolic_result);
2514            }
2515        };
2516
2517        let base_counterexample =
2518            BaseCounterExample::from_fuzz_call(calldata, args, raw.traces.clone());
2519        // Concrete replay rejected the input as an assumption, as fuzzing does.
2520        let rejected_by_assume = raw.result.as_ref() == MAGIC_ASSUME;
2521        if rejected_by_assume
2522            || self.executor.is_raw_call_success(
2523                self.address,
2524                Cow::Borrowed(&raw.state_changeset),
2525                &raw,
2526            )
2527        {
2528            // The solver model is not a user-facing counterexample until replay confirms it, so
2529            // report the mismatch as an incomplete run instead.
2530            let call_trace = SymbolicCallTrace::test_result_traces(raw.traces.is_some());
2531            self.result.extend(raw);
2532            let reason = if rejected_by_assume {
2533                "symbolic counterexample did not replay: rejected by `vm.assume`"
2534            } else {
2535                "symbolic counterexample did not replay"
2536            }
2537            .to_string();
2538            let display_reason = format!(
2539                "incomplete symbolic execution ({:?}): {reason}",
2540                SymbolicStopReason::Error
2541            );
2542            let symbolic_result =
2543                incomplete(reason.clone(), SymbolicReplayMetadata::mismatch(reason), call_trace);
2544            return (TestStatus::Failure, Some(display_reason), None, symbolic_result);
2545        }
2546
2547        let original_call = SymbolicCounterexampleCall::from_base_counterexample(
2548            &base_counterexample,
2549            self.sender,
2550            self.address,
2551        );
2552        let mut final_call = original_call.clone();
2553        let mut final_raw = raw;
2554        let mut final_reason = reason;
2555        let mut minimization = None;
2556        if final_reason.is_some()
2557            && let Some(candidate) = minimize_single_call_counterexample(
2558                func,
2559                &original_call,
2560                self.tcfg.config.invariant.shrink_run_limit as usize,
2561                |candidate| {
2562                    self.replay_confirmed_symbolic_single_call(candidate, final_reason.as_deref())
2563                        .is_ok()
2564                },
2565            )
2566        {
2567            if candidate.changed() {
2568                match self.replay_confirmed_symbolic_single_call(
2569                    &candidate.minimized_call,
2570                    final_reason.as_deref(),
2571                ) {
2572                    Ok((raw, reason)) => {
2573                        final_call = candidate.minimized_call.clone();
2574                        final_raw = raw;
2575                        final_reason = reason;
2576                        minimization = Some(candidate);
2577                    }
2578                    Err(err) => {
2579                        warn!(
2580                            %err,
2581                            "discarding symbolic counterexample minimization result that no longer replays"
2582                        );
2583                    }
2584                }
2585            } else {
2586                minimization = Some(candidate);
2587            }
2588        }
2589
2590        let call_trace = SymbolicCallTrace::test_result_traces(final_raw.traces.is_some());
2591        let mut base_counterexample = final_call.to_base_counterexample();
2592        base_counterexample.traces = final_raw.traces.clone();
2593        self.result.extend(final_raw);
2594
2595        let signature = func.signature();
2596        let fail_on_revert = self.config.invariant.fail_on_revert;
2597        let kind = SymbolicCounterexampleArtifactKind::SingleCall;
2598        let mut symbolic_result = SymbolicResult::fail_counterexample(
2599            symbolic_config,
2600            stats,
2601            call_trace,
2602            SymbolicCounterexample::from(&base_counterexample),
2603        );
2604        let minimized_artifact = self.write_symbolic_artifact(
2605            &signature,
2606            &self.symbolic_artifact(
2607                &signature,
2608                kind,
2609                &symbolic_result,
2610                fail_on_revert,
2611                vec![final_call],
2612            ),
2613        );
2614        if let Some(artifact) = minimized_artifact.clone() {
2615            symbolic_result = symbolic_result.with_artifact(artifact);
2616        }
2617        if let Some(minimization) = minimization {
2618            let original_result = SymbolicResult::fail_counterexample(
2619                symbolic_config,
2620                stats,
2621                SymbolicCallTrace::none(),
2622                symbolic_counterexample,
2623            );
2624            let original_artifact = self.write_symbolic_artifact(
2625                &format!("original__{signature}"),
2626                &self.symbolic_artifact(
2627                    &signature,
2628                    kind,
2629                    &original_result,
2630                    fail_on_revert,
2631                    vec![minimization.original_call.clone()],
2632                ),
2633            );
2634            if let Some((original, minimized)) = original_artifact.zip(minimized_artifact) {
2635                symbolic_result =
2636                    symbolic_result.with_minimization(SymbolicCounterexampleMinimization::new(
2637                        original,
2638                        minimized,
2639                        minimization.attempts,
2640                        minimization.accepted,
2641                        minimization.original_call.calldata.len(),
2642                        minimization.minimized_call.calldata.len(),
2643                    ));
2644            }
2645        }
2646        (
2647            TestStatus::Failure,
2648            final_reason,
2649            Some(CounterExample::Single(base_counterexample)),
2650            symbolic_result,
2651        )
2652    }
2653
2654    /// Replays a durable symbolic counterexample artifact against this freshly set up test.
2655    fn run_symbolic_artifact_replay(
2656        mut self,
2657        func: &Function,
2658        invariants: &[&Function],
2659        call_after_invariant: bool,
2660    ) -> TestResult {
2661        if let Err(reason) = self.replay_symbolic_artifact(func, invariants, call_after_invariant) {
2662            self.result.single_fail(Some(reason));
2663        }
2664        self.result
2665    }
2666
2667    /// Replays a persisted symbolic counterexample artifact against `func`, failing with the
2668    /// mismatch reason when the recorded outcome does not reproduce.
2669    fn replay_symbolic_artifact(
2670        &mut self,
2671        func: &Function,
2672        invariants: &[&Function],
2673        call_after_invariant: bool,
2674    ) -> Result<(), String> {
2675        let Some(replay) = &self.cr.mcr.tcfg.symbolic_artifact_replay else {
2676            return Err("missing symbolic artifact replay config".to_string());
2677        };
2678        let artifact = &replay.artifact;
2679        self.apply_function_inline_config(func).map_err(|e| e.to_string())?;
2680
2681        match artifact.kind {
2682            SymbolicCounterexampleArtifactKind::SingleCall => {
2683                if artifact.replay.status != SymbolicReplayStatus::Confirmed {
2684                    return Err(format!(
2685                        "single-call symbolic artifact replay status must be confirmed, got {:?}",
2686                        artifact.replay.status
2687                    ));
2688                }
2689                let Some(call) = artifact.calls.first() else {
2690                    return Err("symbolic artifact has no calls".to_string());
2691                };
2692                if artifact.calls.len() != 1 {
2693                    return Err(
2694                        "single-call symbolic artifact must contain exactly one call".to_string()
2695                    );
2696                }
2697                // Single-call artifacts are concrete replay inputs: sender, value, warp, and roll
2698                // are intentionally taken from the artifact. Validation only checks that the call
2699                // still targets this test function.
2700                if call.target != self.address {
2701                    return Err(format!(
2702                        "single-call symbolic artifact target {} does not match test contract {}",
2703                        call.target, self.address
2704                    ));
2705                }
2706                if call.calldata.get(..4).is_none_or(|selector| func.selector() != selector) {
2707                    return Err(format!(
2708                        "single-call symbolic artifact calldata does not match `{}` selector",
2709                        func.signature()
2710                    ));
2711                }
2712
2713                if self.prepare_test(func).is_err() {
2714                    return Ok(());
2715                }
2716
2717                let counterexample = || CounterExample::Single(call.to_base_counterexample());
2718                let mut executor = self.clone_executor();
2719                let raw = match execute_tx(&mut executor, &call.to_basic_tx_details()) {
2720                    Ok(raw) => raw,
2721                    Err(err) => {
2722                        self.result.counterexample = Some(counterexample());
2723                        return Err(err.to_string());
2724                    }
2725                };
2726                if executor.is_raw_call_success(
2727                    self.address,
2728                    Cow::Borrowed(&raw.state_changeset),
2729                    &raw,
2730                ) {
2731                    self.result.single_result(true, None, raw);
2732                    return Ok(());
2733                }
2734                match raw.into_evm_error(Some(self.revert_decoder())) {
2735                    EvmError::Execution(err) => {
2736                        let reason = if err.reason.is_empty() {
2737                            artifact.replay.reason.clone()
2738                        } else {
2739                            Some(err.reason.clone())
2740                        };
2741                        self.result.single_result(false, reason, err.raw);
2742                        self.result.counterexample = Some(counterexample());
2743                    }
2744                    EvmError::Skip(reason) => self.result.single_skip(reason),
2745                    err => {
2746                        self.result.counterexample = Some(counterexample());
2747                        return Err(err.to_string());
2748                    }
2749                }
2750            }
2751            SymbolicCounterexampleArtifactKind::Sequence => {
2752                let Some(invariant) = invariants.first() else {
2753                    return Err(
2754                        "sequence symbolic artifact must target an invariant test".to_string()
2755                    );
2756                };
2757                if artifact.calls.is_empty() {
2758                    return Err("symbolic artifact has no calls".to_string());
2759                }
2760
2761                let calls = artifact
2762                    .calls
2763                    .iter()
2764                    .map(SymbolicCounterexampleCall::to_base_counterexample)
2765                    .collect::<Vec<_>>();
2766                let txes = artifact
2767                    .calls
2768                    .iter()
2769                    .map(SymbolicCounterexampleCall::to_basic_tx_details)
2770                    .collect::<Vec<_>>();
2771                let setup_contracts = load_contracts(
2772                    self.setup.traces.iter().map(|(_, trace)| &trace.arena),
2773                    &self.cr.mcr.known_contracts,
2774                );
2775                let mut evm = InvariantExecutor::new_with_fuzz_seed(
2776                    self.clone_executor(),
2777                    self.invariant_runner(),
2778                    self.config.fuzz.seed,
2779                    self.config.invariant.clone(),
2780                    &setup_contracts,
2781                    &self.cr.mcr.known_contracts,
2782                    self.cr.num_invariant_campaign_anchors,
2783                );
2784                if let Err(err) = evm.select_contract_artifacts(self.address) {
2785                    self.result.invariant_setup_fail(err);
2786                    return Ok(());
2787                }
2788                let (sender_filters, targeted) =
2789                    match evm.select_contracts_and_senders(self.address) {
2790                        Ok(selected) => selected,
2791                        Err(err) => {
2792                            self.result.invariant_setup_fail(err);
2793                            return Ok(());
2794                        }
2795                    };
2796                let artifact_executor =
2797                    match self.clone_executor_with_symbolic_storage(&artifact.storage) {
2798                        Ok(executor) => executor,
2799                        Err(err) => {
2800                            self.result.counterexample =
2801                                Some(CounterExample::Sequence(calls.len(), calls));
2802                            return Err(err.to_string());
2803                        }
2804                    };
2805
2806                let dynamic_target_ctx = evm.dynamic_target_ctx();
2807                let mut validation_executor =
2808                    targeted.is_updatable.then(|| artifact_executor.clone());
2809                let mut validation_created_contracts = Vec::new();
2810                for (idx, tx) in txes.iter().enumerate() {
2811                    let Some(selector) = tx.call_details.calldata.get(..4) else {
2812                        return Err(format!(
2813                            "sequence symbolic artifact call {} has calldata shorter than a selector",
2814                            idx + 1
2815                        ));
2816                    };
2817                    if !targeted.targets().can_replay(tx) {
2818                        return Err(format!(
2819                            "sequence symbolic artifact call {} targets unknown function {} on {}",
2820                            idx + 1,
2821                            hex::encode_prefixed(selector),
2822                            tx.call_details.target
2823                        ));
2824                    }
2825                    if !sender_filters.allows(tx.sender) {
2826                        return Err(format!(
2827                            "sequence symbolic artifact call {} uses forbidden sender {}",
2828                            idx + 1,
2829                            tx.sender
2830                        ));
2831                    }
2832                    if let Some(validation_executor) = validation_executor.as_mut() {
2833                        execute_tx_and_register_created(
2834                            validation_executor,
2835                            tx,
2836                            &targeted,
2837                            &dynamic_target_ctx,
2838                            &mut validation_created_contracts,
2839                        )
2840                        .map_err(|err| {
2841                            format!(
2842                                "sequence symbolic artifact call {} failed during target validation: {err}",
2843                                idx + 1
2844                            )
2845                        })?;
2846                    }
2847                }
2848
2849                let artifact_failure = artifact.invariant_failure.as_ref();
2850                if matches!(
2851                    artifact_failure,
2852                    Some(SymbolicInvariantArtifactFailure::Predicate { site: None, .. })
2853                ) {
2854                    return Err(
2855                        "sequence symbolic artifact does not identify an exact predicate failure site"
2856                            .to_string(),
2857                    );
2858                }
2859                let is_handler_artifact = matches!(
2860                    artifact_failure,
2861                    Some(SymbolicInvariantArtifactFailure::Handler { .. })
2862                );
2863                let sequence = (0..txes.len()).collect::<Vec<_>>();
2864                let outcome = match check_sequence(
2865                    artifact_executor,
2866                    &txes,
2867                    &sequence,
2868                    self.setup.address,
2869                    invariant.selector().into(),
2870                    CheckSequenceOptions {
2871                        // Artifact replay executes every stored call in order, so each call's
2872                        // warp/roll delta is applied directly. Accumulation is only needed when a
2873                        // shrink candidate skips calls and must fold removed delays forward.
2874                        accumulate_warp_roll: false,
2875                        fail_on_revert: is_handler_artifact
2876                            || artifact.replay_semantics.fail_on_revert,
2877                        expect_assertion_failure: is_handler_artifact,
2878                        call_after_invariant,
2879                        rd: Some(self.revert_decoder()),
2880                    },
2881                ) {
2882                    Ok(outcome) => outcome,
2883                    Err(err) => {
2884                        self.result.counterexample =
2885                            Some(CounterExample::Sequence(calls.len(), calls));
2886                        return Err(err.to_string());
2887                    }
2888                };
2889                if outcome.success {
2890                    self.result.invariant_replay_success(outcome.calls_count, outcome.reverts);
2891                    return Ok(());
2892                }
2893                match artifact_failure {
2894                    Some(SymbolicInvariantArtifactFailure::Handler {
2895                        name,
2896                        reverter,
2897                        selector,
2898                        fingerprint,
2899                    }) => {
2900                        let Some(CheckSequenceFailureSite::SequenceCall {
2901                            target: actual_target,
2902                            selector: actual_selector,
2903                            fingerprint: actual_fingerprint,
2904                        }) = outcome.failure_site
2905                        else {
2906                            return Err(format!(
2907                                "sequence symbolic artifact replayed a non-handler failure site: \
2908                                 {:?}",
2909                                outcome.failure_site
2910                            ));
2911                        };
2912                        let canonical_handler = *reverter == actual_target
2913                            && *selector == actual_selector
2914                            && *fingerprint == actual_fingerprint;
2915                        let legacy_handler = outcome.sequence_reverter == Some(*reverter)
2916                            && *reverter != actual_target
2917                            && *selector == actual_selector
2918                            && (*fingerprint == actual_fingerprint
2919                                || *fingerprint
2920                                    == handler_edge_fingerprint(None, *reverter, *selector));
2921                        if !canonical_handler && !legacy_handler {
2922                            return Err(format!(
2923                                "sequence symbolic artifact replayed a different handler \
2924                                 failure site than the stored artifact: expected \
2925                                 {reverter}::{selector} at {fingerprint}, got {:?}",
2926                                outcome.failure_site
2927                            ));
2928                        }
2929                        let handler_name = if legacy_handler {
2930                            invariant_handler_failure_name(
2931                                &setup_contracts,
2932                                actual_target,
2933                                actual_selector,
2934                            )
2935                        } else {
2936                            name.clone().unwrap_or_else(|| {
2937                                invariant_handler_failure_name(
2938                                    &setup_contracts,
2939                                    actual_target,
2940                                    actual_selector,
2941                                )
2942                            })
2943                        };
2944                        self.result.invariant_result(
2945                            invariant_kind(1, outcome.calls_count, outcome.reverts),
2946                            InvariantOutcome {
2947                                handler_failures: vec![InvariantFailure::Handler {
2948                                    name: handler_name,
2949                                    reverter: actual_target,
2950                                    selector: actual_selector,
2951                                    reason: outcome
2952                                        .reason
2953                                        .or_else(|| artifact.replay.reason.clone())
2954                                        .unwrap_or_else(|| {
2955                                            "symbolic handler counterexample".to_string()
2956                                        }),
2957                                    counterexample: Some(CounterExample::Sequence(
2958                                        calls.len(),
2959                                        calls,
2960                                    )),
2961                                    artifact: Some(SymbolicArtifactRef::new(replay.path.clone())),
2962                                }],
2963                                ..Default::default()
2964                            },
2965                        );
2966                    }
2967                    _ => {
2968                        if let Some(SymbolicInvariantArtifactFailure::Predicate { site, .. }) =
2969                            artifact_failure
2970                            && outcome.failure_site.map(SymbolicInvariantFailureSite::from) != *site
2971                        {
2972                            return Err(format!(
2973                                "sequence symbolic artifact replayed a different failure \
2974                                 origin than the stored predicate: got {:?}",
2975                                outcome.failure_site
2976                            ));
2977                        }
2978                        let signature = invariant.signature();
2979                        let invariant_name = match artifact_failure {
2980                            Some(SymbolicInvariantArtifactFailure::Predicate { name, .. }) => {
2981                                name.as_str()
2982                            }
2983                            _ => signature.as_str(),
2984                        };
2985                        self.result.invariant_replay_fail(
2986                            outcome,
2987                            invariant_name,
2988                            artifact.replay.reason.clone(),
2989                            calls,
2990                        );
2991                    }
2992                }
2993            }
2994        }
2995        Ok(())
2996    }
2997
2998    fn try_seed_fuzz_corpus_from_frontiers(&self, func: &Function, fuzz_config: &FuzzConfig) {
2999        if !self.config.symbolic.use_fuzz_frontiers || !func.test_function_kind().is_fuzz_test() {
3000            return;
3001        }
3002        if fuzz_config.corpus.corpus_dir.is_none() {
3003            let _ = sh_warn!(
3004                "`--symbolic-use-fuzz-frontiers` requires `--fuzz-corpus-dir` or \
3005                 `fuzz.corpus_dir`; skipping targeted frontier seeding"
3006            );
3007            return;
3008        }
3009
3010        for (id, sender, target, input) in self.import_symbolic_fuzz_frontiers(func, fuzz_config) {
3011            let mut symbolic = SymbolicExecutor::new(self.config.symbolic.clone());
3012            let result = symbolic.run(self.symbolic_run_input(
3013                func,
3014                sender,
3015                true,
3016                vec![input],
3017                Some(target),
3018            ));
3019
3020            let (input, expect_failure) = match result {
3021                SymbolicRunResult::Safe { success_input: Some(input), .. } => (input, false),
3022                SymbolicRunResult::Safe { success_input: None, .. } => {
3023                    warn!(
3024                        id,
3025                        test = %func.signature(),
3026                        "targeted symbolic frontier produced no branch-flipping input"
3027                    );
3028                    continue;
3029                }
3030                SymbolicRunResult::Incomplete { kind, reason, .. } => {
3031                    warn!(
3032                        id,
3033                        ?kind,
3034                        %reason,
3035                        test = %func.signature(),
3036                        "targeted symbolic frontier incomplete"
3037                    );
3038                    continue;
3039                }
3040                SymbolicRunResult::Counterexample { args, calldata, .. } => {
3041                    (SymbolicConcreteInput { args, calldata }, true)
3042                }
3043            };
3044
3045            let replay = self.symbolic_fuzz_seed_replay(sender, &input, fuzz_config);
3046            if replay != Some(!expect_failure) {
3047                warn!(
3048                    id,
3049                    ?replay,
3050                    test = %func.signature(),
3051                    "targeted symbolic frontier seed did not replay with the expected outcome"
3052                );
3053                continue;
3054            }
3055
3056            match self.persist_symbolic_fuzz_seed(&fuzz_config.corpus, sender, input.calldata) {
3057                Ok(Some(path)) => {
3058                    debug!(
3059                        id,
3060                        path = %path.display(),
3061                        test = %func.signature(),
3062                        "persisted targeted symbolic frontier seed"
3063                    );
3064                }
3065                Ok(None) => {}
3066                Err(err) => {
3067                    warn!(
3068                        %err,
3069                        id,
3070                        test = %func.signature(),
3071                        "failed to persist targeted symbolic frontier seed"
3072                    );
3073                }
3074            }
3075        }
3076    }
3077
3078    fn replay_invariant_sequence(
3079        &self,
3080        invariant_contract: &InvariantContract<'_>,
3081        invariant_idx: usize,
3082        sequence: &[BasicTxDetails],
3083        replay_order: &[usize],
3084        call_after_invariant: bool,
3085    ) -> Option<CheckSequenceOutcome> {
3086        let policy = invariant_contract.invariant_fns[invariant_idx].1;
3087        let outcome = check_sequence(
3088            self.clone_executor(),
3089            sequence,
3090            replay_order,
3091            invariant_contract.address,
3092            invariant_contract.invariant_calldata(invariant_idx),
3093            CheckSequenceOptions {
3094                accumulate_warp_roll: false,
3095                fail_on_revert: policy,
3096                expect_assertion_failure: false,
3097                call_after_invariant,
3098                rd: Some(self.revert_decoder()),
3099            },
3100        )
3101        .ok()?;
3102        (!outcome.success && outcome.replayed_entirely).then_some(outcome)
3103    }
3104
3105    fn replay_invariant_checkpoints(
3106        &self,
3107        invariant_contract: &InvariantContract<'_>,
3108        invariant_config: &InvariantConfig,
3109        sequence: &[BasicTxDetails],
3110        terminal_checkpoint: bool,
3111    ) -> (Vec<(usize, CheckSequenceOutcome)>, Option<CheckSequenceOutcome>) {
3112        let mut broken_invariants = Vec::new();
3113        let mut remaining_invariants = vec![true; invariant_contract.invariant_fns.len()];
3114        let replay_order = (0..sequence.len()).collect::<Vec<_>>();
3115
3116        if !invariant_contract.is_optimization() {
3117            for accepted_calls in 1..=sequence.len() {
3118                let should_check = invariant_config.check_interval == 1
3119                    || (invariant_config.check_interval > 1
3120                        && accepted_calls.is_multiple_of(invariant_config.check_interval as usize))
3121                    || (terminal_checkpoint && accepted_calls == sequence.len());
3122                if !should_check {
3123                    continue;
3124                }
3125
3126                for (invariant_idx, pending) in remaining_invariants.iter_mut().enumerate() {
3127                    if !*pending {
3128                        continue;
3129                    }
3130                    let Some(replay) = self.replay_invariant_sequence(
3131                        invariant_contract,
3132                        invariant_idx,
3133                        &sequence[..accepted_calls],
3134                        &replay_order[..accepted_calls],
3135                        false,
3136                    ) else {
3137                        continue;
3138                    };
3139                    if matches!(
3140                        replay.failure_site,
3141                        Some(CheckSequenceFailureSite::Invariant { selector, .. })
3142                            if selector == invariant_contract.invariant_fns[invariant_idx].0.selector()
3143                    ) {
3144                        *pending = false;
3145                        broken_invariants.push((invariant_idx, replay));
3146                    }
3147                }
3148            }
3149        }
3150
3151        let after_invariant_failure = if terminal_checkpoint
3152            && invariant_contract.call_after_invariant
3153            && broken_invariants.is_empty()
3154        {
3155            invariant_contract
3156                .abi
3157                .functions()
3158                .find(|function| {
3159                    function.name == "afterInvariant" && function.inputs.is_empty()
3160                })
3161                .and_then(|after_invariant| {
3162                    let calldata = after_invariant.abi_encode_input(&[]).ok()?.into();
3163                    let mut replay = check_sequence(
3164                        self.clone_executor(),
3165                        sequence,
3166                        &replay_order,
3167                        invariant_contract.address,
3168                        calldata,
3169                        CheckSequenceOptions {
3170                            accumulate_warp_roll: false,
3171                            fail_on_revert: false,
3172                            expect_assertion_failure: false,
3173                            call_after_invariant: false,
3174                            rd: Some(self.revert_decoder()),
3175                        },
3176                    )
3177                    .ok()?;
3178                    if replay.success || !replay.replayed_entirely {
3179                        return None;
3180                    }
3181                    let Some(CheckSequenceFailureSite::Invariant {
3182                        target,
3183                        selector,
3184                        fingerprint,
3185                    }) = replay.failure_site
3186                    else {
3187                        return None;
3188                    };
3189                    if selector != after_invariant.selector() {
3190                        return None;
3191                    }
3192                    replay.failure_site = Some(CheckSequenceFailureSite::AfterInvariant {
3193                        target,
3194                        selector,
3195                        fingerprint,
3196                    });
3197                    Some(replay)
3198                })
3199        } else {
3200            None
3201        };
3202
3203        (broken_invariants, after_invariant_failure)
3204    }
3205
3206    fn solve_invariants_from_frontier_prefix(
3207        &self,
3208        invariant_contract: &InvariantContract<'_>,
3209        invariant_indexes: &[usize],
3210        prefix_executor: &Executor<FEN>,
3211        target: &SymbolicInvariantTarget,
3212        sender: Address,
3213        prefix: &[BasicTxDetails],
3214    ) -> Vec<(usize, CheckSequenceOutcome, Vec<BasicTxDetails>)> {
3215        let after_invariant = invariant_contract
3216            .call_after_invariant
3217            .then(|| {
3218                invariant_contract.abi.functions().find(|function| {
3219                    function.name == "afterInvariant" && function.inputs.is_empty()
3220                })
3221            })
3222            .flatten();
3223
3224        let invariants = invariant_indexes
3225            .iter()
3226            .map(|&idx| invariant_contract.invariant_fns[idx].0)
3227            .collect::<Vec<_>>();
3228        let mut symbolic = SymbolicExecutor::new(self.config.symbolic.clone());
3229        let result = symbolic.search_invariant_candidates(SymbolicInvariantCandidateInput {
3230            executor: ExecutorView::new(prefix_executor),
3231            invariant_address: invariant_contract.address,
3232            invariants: &invariants,
3233            after_invariant,
3234            target,
3235            handler_sender: sender,
3236            ffi_enabled: self.config.ffi,
3237        });
3238        if let Some(limitation) = &result.limitation {
3239            debug!(
3240                ?limitation.kind,
3241                reason = %limitation.reason,
3242                candidates = result.candidates.len(),
3243                "symbolic invariant frontier candidate search incomplete"
3244            );
3245        }
3246
3247        result
3248            .candidates
3249            .into_iter()
3250            .filter_map(|candidate| {
3251                if !candidate.storage.is_empty() {
3252                    return None;
3253                }
3254                let invariant_idx = invariant_indexes[candidate.invariant_idx];
3255                let call = BasicTxDetails {
3256                    warp: None,
3257                    roll: None,
3258                    sender: candidate.step.sender,
3259                    call_details: CallDetails {
3260                        target: candidate.step.address,
3261                        calldata: candidate.step.calldata,
3262                        value: None,
3263                    },
3264                };
3265                let mut sequence = Vec::with_capacity(prefix.len() + 1);
3266                sequence.extend_from_slice(prefix);
3267                sequence.push(call);
3268                let replay_order = (0..sequence.len()).collect::<Vec<_>>();
3269                let replay = self.replay_invariant_sequence(
3270                    invariant_contract,
3271                    invariant_idx,
3272                    &sequence,
3273                    &replay_order,
3274                    after_invariant.is_some(),
3275                )?;
3276                let exact_failure = match replay.failure_site? {
3277                    CheckSequenceFailureSite::Invariant { selector, .. } => {
3278                        selector == invariant_contract.invariant_fns[invariant_idx].0.selector()
3279                    }
3280                    CheckSequenceFailureSite::AfterInvariant { .. } => true,
3281                    CheckSequenceFailureSite::SequenceCall { .. } => false,
3282                };
3283                exact_failure.then_some((invariant_idx, replay, sequence))
3284            })
3285            .collect()
3286    }
3287
3288    fn try_seed_invariant_corpus_from_frontiers(
3289        &self,
3290        invariant_contract: &InvariantContract<'_>,
3291        invariant_config: &InvariantConfig,
3292        sender_filters: &SenderFilters,
3293        targeted_contracts: &FuzzRunIdentifiedContracts,
3294        dynamic_target_ctx: &DynamicTargetCtx<'_>,
3295        confirmed_invariants: &HashSet<usize>,
3296    ) -> Vec<ConfirmedFrontierInvariantFailure> {
3297        if !self.config.symbolic.use_fuzz_frontiers
3298            && !self.config.symbolic.check_invariant_frontiers
3299        {
3300            return Vec::new();
3301        }
3302        if invariant_config.corpus.corpus_dir.is_none() {
3303            let _ = sh_warn!(
3304                "Symbolic invariant frontier seeding requires `--invariant-corpus-dir` or \
3305                 `invariant.corpus_dir`; skipping targeted invariant frontier seeding"
3306            );
3307            return Vec::new();
3308        }
3309
3310        let mut checked_property_calls = HashSet::<(usize, usize)>::default();
3311        let mut reported_invariants = confirmed_invariants.clone();
3312        let mut after_invariant_reported = false;
3313        let mut confirmed_failures = Vec::new();
3314        let is_optimization = invariant_contract.is_optimization();
3315        let fail_on_revert = invariant_contract.invariant_fns.iter().any(|(_, policy)| *policy);
3316        for (frontier, sequence) in
3317            self.import_symbolic_invariant_frontiers(invariant_contract, invariant_config)
3318        {
3319            let id = frontier.id;
3320            let call_index = frontier.call_index;
3321            let max_depth = match invariant_config.depth_mode {
3322                InvariantDepthMode::Fixed => invariant_config.depth,
3323                InvariantDepthMode::Random => invariant_config.depth.max(1),
3324            };
3325            if call_index >= max_depth as usize {
3326                debug!(
3327                    id,
3328                    call_index,
3329                    depth = max_depth,
3330                    "skipping invariant frontier beyond campaign depth"
3331                );
3332                continue;
3333            }
3334            let Some(call) = sequence.get(call_index) else {
3335                continue;
3336            };
3337            let accepted_calls = call_index + 1;
3338            let terminal_checkpoint = match invariant_config.depth_mode {
3339                InvariantDepthMode::Fixed => accepted_calls == invariant_config.depth as usize,
3340                InvariantDepthMode::Random => {
3341                    let min_depth = invariant_config.min_depth.max(1);
3342                    let max_depth = invariant_config.depth.max(1);
3343                    let accepted_calls = accepted_calls as u32;
3344                    if invariant_config.depth <= min_depth {
3345                        accepted_calls == max_depth
3346                    } else {
3347                        (min_depth..=max_depth).contains(&accepted_calls)
3348                    }
3349                }
3350            };
3351            let Some(selector) = call
3352                .call_details
3353                .calldata
3354                .get(..4)
3355                .and_then(|selector| <[u8; 4]>::try_from(selector).ok())
3356                .map(Selector::from)
3357            else {
3358                continue;
3359            };
3360            let mut prefix_executor = self.clone_executor();
3361            let mut created_contracts = Vec::new();
3362            let prefix_targets = FuzzRunIdentifiedContracts::new(
3363                targeted_contracts.targets().clone(),
3364                targeted_contracts.is_updatable,
3365            );
3366            let prefix_result = sequence[..call_index].iter().try_for_each(|prefix_call| {
3367                if !prefix_targets.targets().can_replay(prefix_call)
3368                    || !sender_filters.allows(prefix_call.sender)
3369                {
3370                    return Err(eyre::eyre!(
3371                        "frontier prefix call is not eligible for this campaign"
3372                    ));
3373                }
3374                execute_tx_and_register_created(
3375                    &mut prefix_executor,
3376                    prefix_call,
3377                    &prefix_targets,
3378                    dynamic_target_ctx,
3379                    &mut created_contracts,
3380                )
3381            });
3382            if let Err(err) = prefix_result {
3383                debug!(%err, id, "failed to replay invariant frontier prefix");
3384                continue;
3385            }
3386            if !sender_filters.allows(call.sender) {
3387                debug!(id, sender = %call.sender, "skipping invariant frontier with forbidden sender");
3388                continue;
3389            }
3390            let invariant_target = {
3391                let targets = prefix_targets.targets();
3392                targets.get(&call.call_details.target).and_then(|contract| {
3393                    contract.fuzzed_function_by_selector(selector).map(|function| {
3394                        SymbolicInvariantTarget {
3395                            address: call.call_details.target,
3396                            contract_name: Some(contract.identifier.clone()),
3397                            function: function.clone(),
3398                        }
3399                    })
3400                })
3401            };
3402            let Some(invariant_target) = invariant_target else {
3403                debug!(id, selector = %selector, "skipping invariant frontier with unknown target function");
3404                continue;
3405            };
3406            let function = &invariant_target.function;
3407            let Ok(args) = function.abi_decode_input(&call.call_details.calldata[4..]) else {
3408                debug!(id, selector = %selector, "skipping invariant frontier with invalid calldata");
3409                continue;
3410            };
3411
3412            let input =
3413                SymbolicConcreteInput { args, calldata: call.call_details.calldata.clone() };
3414            let property_target_reported = if invariant_contract.call_after_invariant {
3415                reported_invariants.contains(&invariant_contract.anchor_idx)
3416                    || after_invariant_reported
3417            } else {
3418                reported_invariants.len() == invariant_contract.invariant_fns.len()
3419            };
3420            if !is_optimization
3421                && self.config.symbolic.check_invariant_frontiers
3422                && !property_target_reported
3423                && checked_property_calls.insert((
3424                    frontier.sequence_index.expect("frontier sequence index was validated"),
3425                    call_index,
3426                ))
3427            {
3428                let mut invariant_indexes = if invariant_contract.call_after_invariant {
3429                    vec![invariant_contract.anchor_idx]
3430                } else {
3431                    (0..invariant_contract.invariant_fns.len())
3432                        .filter(|idx| !reported_invariants.contains(idx))
3433                        .collect::<Vec<_>>()
3434                };
3435                if invariant_indexes.len() > 1 {
3436                    let rotation = (checked_property_calls.len() - 1) % invariant_indexes.len();
3437                    invariant_indexes.rotate_left(rotation);
3438                }
3439                for (_, _, solved_sequence) in self.solve_invariants_from_frontier_prefix(
3440                    invariant_contract,
3441                    &invariant_indexes,
3442                    &prefix_executor,
3443                    &invariant_target,
3444                    call.sender,
3445                    &sequence[..call_index],
3446                ) {
3447                    let (broken_invariants, after_invariant_failure) = self
3448                        .replay_invariant_checkpoints(
3449                            invariant_contract,
3450                            invariant_config,
3451                            &solved_sequence,
3452                            terminal_checkpoint,
3453                        );
3454                    for (invariant_idx, replay) in broken_invariants {
3455                        if reported_invariants.insert(invariant_idx) {
3456                            confirmed_failures.push(ConfirmedFrontierInvariantFailure {
3457                                invariant_idx,
3458                                call_sequence: solved_sequence.clone(),
3459                                replay,
3460                            });
3461                        }
3462                    }
3463                    if let Some(replay) = after_invariant_failure
3464                        && !after_invariant_reported
3465                    {
3466                        after_invariant_reported = true;
3467                        confirmed_failures.push(ConfirmedFrontierInvariantFailure {
3468                            invariant_idx: invariant_contract.anchor_idx,
3469                            call_sequence: solved_sequence.clone(),
3470                            replay,
3471                        });
3472                    }
3473                    match persist_corpus_seed(&invariant_config.corpus, solved_sequence) {
3474                        Ok(path) => {
3475                            if let Some(path) = path {
3476                                debug!(id, path = %path.display(), "persisted property-directed invariant frontier seed");
3477                            }
3478                        }
3479                        Err(err) => {
3480                            warn!(%err, id, "failed to persist property-directed invariant frontier seed");
3481                        }
3482                    }
3483                }
3484            }
3485
3486            let mut symbolic = SymbolicExecutor::new(self.config.symbolic.clone());
3487            let target = SymbolicBranchTarget::new(
3488                frontier.site.address,
3489                frontier.site.pc,
3490                frontier.site.opcode,
3491                frontier.operands.result,
3492            );
3493            let search = symbolic.search_branch_target(SymbolicRunInput {
3494                executor: ExecutorView::new(&prefix_executor),
3495                target: call.call_details.target,
3496                sender: call.sender,
3497                function,
3498                value: U256::ZERO,
3499                ffi_enabled: self.config.ffi,
3500                collect_success_input: false,
3501                corpus_seeds: vec![input],
3502                branch_target: Some(target),
3503            });
3504            if let SymbolicRunResult::Incomplete { kind, reason, .. } = &search.execution {
3505                debug!(
3506                    id,
3507                    ?kind,
3508                    %reason,
3509                    candidates = search.candidates.len(),
3510                    "targeted invariant frontier incomplete"
3511                );
3512            } else if search.candidates.is_empty() {
3513                debug!(id, "targeted invariant frontier produced no branch-flipping input");
3514            }
3515
3516            let mut selected_branch_seed = None;
3517            let mut selected_failure_seed = false;
3518            for solved_input in search.candidates {
3519                let mut solved_sequence = sequence[..=call_index].to_vec();
3520                solved_sequence[call_index].call_details.calldata = solved_input.calldata;
3521                let mut replay_executor = prefix_executor.clone();
3522                replay_executor.inspector_mut().collect_evm_cmp_log(true);
3523                let replay_result =
3524                    match execute_tx(&mut replay_executor, &solved_sequence[call_index]) {
3525                        Ok(result) => result,
3526                        Err(err) => {
3527                            debug!(%err, id, "failed to replay solved invariant frontier");
3528                            continue;
3529                        }
3530                    };
3531                let comparisons = replay_result.evm_cmp_values.as_deref().unwrap_or_default();
3532                let branch_flipped = frontier_comparison_flipped(
3533                    frontier.site,
3534                    frontier.operands.result,
3535                    comparisons,
3536                );
3537                let assertion_failure =
3538                    did_fail_on_assert(&replay_result, &replay_result.state_changeset);
3539                let accepted = replay_result.result.as_ref() != MAGIC_ASSUME
3540                    && (!replay_result.reverted || fail_on_revert || assertion_failure);
3541                if !branch_flipped || !accepted {
3542                    debug!(
3543                        id,
3544                        branch_flipped,
3545                        reverted = replay_result.reverted,
3546                        fail_on_revert,
3547                        "solved invariant frontier was not eligible during concrete replay"
3548                    );
3549                    continue;
3550                }
3551                let (broken_invariants, after_invariant_failure) = self
3552                    .replay_invariant_checkpoints(
3553                        invariant_contract,
3554                        invariant_config,
3555                        &solved_sequence,
3556                        terminal_checkpoint,
3557                    );
3558                let newly_broken_invariants = broken_invariants
3559                    .iter()
3560                    .filter(|(idx, _)| reported_invariants.insert(*idx))
3561                    .collect::<Vec<_>>();
3562                let after_invariant_failure =
3563                    after_invariant_failure.filter(|_| !after_invariant_reported);
3564                if !newly_broken_invariants.is_empty() || after_invariant_failure.is_some() {
3565                    for (invariant_idx, replay) in newly_broken_invariants {
3566                        confirmed_failures.push(ConfirmedFrontierInvariantFailure {
3567                            invariant_idx: *invariant_idx,
3568                            call_sequence: solved_sequence.clone(),
3569                            replay: replay.clone(),
3570                        });
3571                    }
3572                    if let Some(replay) = after_invariant_failure.clone() {
3573                        after_invariant_reported = true;
3574                        confirmed_failures.push(ConfirmedFrontierInvariantFailure {
3575                            invariant_idx: invariant_contract.anchor_idx,
3576                            call_sequence: solved_sequence.clone(),
3577                            replay,
3578                        });
3579                    }
3580                    match persist_corpus_seed(&invariant_config.corpus, solved_sequence.clone()) {
3581                        Ok(path) => {
3582                            if let Some(path) = path {
3583                                debug!(
3584                                    id,
3585                                    ?broken_invariants,
3586                                    ?after_invariant_failure,
3587                                    path = %path.display(),
3588                                    "persisted property-breaking branch frontier seed"
3589                                );
3590                            }
3591                        }
3592                        Err(err) => {
3593                            warn!(%err, id, "failed to persist property-breaking branch frontier seed");
3594                        }
3595                    }
3596                }
3597
3598                if assertion_failure || replay_result.reverted {
3599                    if !selected_failure_seed {
3600                        selected_branch_seed = Some(solved_sequence);
3601                        selected_failure_seed = true;
3602                    }
3603                } else {
3604                    selected_branch_seed.get_or_insert(solved_sequence);
3605                }
3606            }
3607            if let Some(sequence) = selected_branch_seed {
3608                match persist_corpus_seed(&invariant_config.corpus, sequence) {
3609                    Ok(Some(path)) => {
3610                        debug!(id, path = %path.display(), "persisted targeted invariant frontier seed");
3611                    }
3612                    Ok(None) => {}
3613                    Err(err) => {
3614                        warn!(%err, id, "failed to persist targeted invariant frontier seed");
3615                    }
3616                }
3617            }
3618        }
3619        confirmed_failures
3620    }
3621
3622    fn try_seed_fuzz_corpus_symbolically(&self, func: &Function, fuzz_config: &FuzzConfig) {
3623        if !self.config.symbolic.seed_corpus || !func.test_function_kind().is_fuzz_test() {
3624            return;
3625        }
3626        if fuzz_config.corpus.corpus_dir.is_none() {
3627            let _ = sh_warn!(
3628                "`--symbolic-seed-corpus` requires `--fuzz-corpus-dir` or `fuzz.corpus_dir`; \
3629                 skipping symbolic corpus seeding"
3630            );
3631            return;
3632        }
3633
3634        let mut symbolic = SymbolicExecutor::new(self.config.symbolic.clone());
3635        let result =
3636            symbolic.run(self.symbolic_run_input(func, self.sender, true, Vec::new(), None));
3637
3638        let input = match result {
3639            SymbolicRunResult::Safe { success_input: Some(input), .. } => input,
3640            SymbolicRunResult::Safe { success_input: None, .. } => {
3641                warn!(test = %func.signature(), "symbolic fuzz corpus seeding found no successful input");
3642                return;
3643            }
3644            SymbolicRunResult::Incomplete { kind, reason, .. } => {
3645                warn!(?kind, %reason, test = %func.signature(), "symbolic fuzz corpus seeding incomplete");
3646                return;
3647            }
3648            SymbolicRunResult::Counterexample { .. } => {
3649                warn!(test = %func.signature(), "symbolic fuzz corpus seeding found a counterexample");
3650                return;
3651            }
3652        };
3653
3654        if self.symbolic_fuzz_seed_replay(self.sender, &input, fuzz_config) != Some(true) {
3655            warn!(test = %func.signature(), "symbolic fuzz corpus seed did not pass concrete replay");
3656            return;
3657        }
3658
3659        if let Err(err) =
3660            self.persist_symbolic_fuzz_seed(&fuzz_config.corpus, self.sender, input.calldata)
3661        {
3662            warn!(%err, test = %func.signature(), "failed to persist symbolic fuzz corpus seed");
3663        }
3664    }
3665
3666    /// Persists a concretely confirmed symbolic input as a fuzz corpus seed.
3667    fn persist_symbolic_fuzz_seed(
3668        &self,
3669        corpus: &FuzzCorpusConfig,
3670        sender: Address,
3671        calldata: Bytes,
3672    ) -> foundry_common::fs::Result<Option<PathBuf>> {
3673        persist_corpus_seed(
3674            corpus,
3675            vec![BasicTxDetails {
3676                warp: None,
3677                roll: None,
3678                sender,
3679                call_details: CallDetails {
3680                    target: self.address,
3681                    calldata,
3682                    value: Some(U256::ZERO),
3683                },
3684            }],
3685        )
3686    }
3687
3688    /// Replays a symbolic seed concretely: `Some(success)`, or `None` if the input was rejected.
3689    fn symbolic_fuzz_seed_replay(
3690        &self,
3691        sender: Address,
3692        input: &SymbolicConcreteInput,
3693        fuzz_config: &FuzzConfig,
3694    ) -> Option<bool> {
3695        let raw = self
3696            .clone_executor()
3697            .call_raw(sender, self.address, input.calldata.clone(), U256::ZERO)
3698            .ok()?;
3699        if raw.result.as_ref() == MAGIC_ASSUME {
3700            return None;
3701        }
3702        Some(
3703            should_ignore_revert(
3704                fuzz_config.fail_on_revert,
3705                self.address,
3706                raw.reverter,
3707                self.executor.inspector().extra_cheatcode_addresses(),
3708            ) || self.executor.is_raw_call_success(
3709                self.address,
3710                Cow::Borrowed(&raw.state_changeset),
3711                &raw,
3712            ),
3713        )
3714    }
3715
3716    /// Runs a table test.
3717    /// The parameters dataset (table) is created from defined parameter fixtures, therefore each
3718    /// test table parameter should have the same number of fixtures defined.
3719    /// E.g. for table test
3720    /// - `table_test(uint256 amount, bool swap)` fixtures are defined as
3721    /// - `uint256[] public fixtureAmount = [2, 5]`
3722    /// - `bool[] public fixtureSwap = [true, false]` The `table_test` is then called with the pair
3723    ///   of args `(2, true)` and `(5, false)`.
3724    fn run_table_test(mut self, func: &Function) -> TestResult {
3725        // Prepare unit test execution.
3726        if self.prepare_test(func).is_err() {
3727            return self.result;
3728        }
3729
3730        // Extract and validate fixtures for the first table test parameter.
3731        let Some(first_param) = func.inputs.first() else {
3732            self.result.single_fail(Some("Table test should have at least one parameter".into()));
3733            return self.result;
3734        };
3735
3736        let Some(first_param_fixtures) =
3737            &self.setup.fuzz_fixtures.param_fixtures(first_param.name())
3738        else {
3739            self.result.single_fail(Some("Table test should have fixtures defined".into()));
3740            return self.result;
3741        };
3742
3743        if first_param_fixtures.is_empty() {
3744            self.result.single_fail(Some("Table test should have at least one fixture".into()));
3745            return self.result;
3746        }
3747
3748        let fixtures_len = first_param_fixtures.len();
3749        let mut table_fixtures = vec![&first_param_fixtures[..]];
3750
3751        // Collect fixtures for remaining parameters.
3752        for param in &func.inputs[1..] {
3753            let param_name = param.name();
3754            let Some(fixtures) = &self.setup.fuzz_fixtures.param_fixtures(param.name()) else {
3755                self.result.single_fail(Some(format!("No fixture defined for param {param_name}")));
3756                return self.result;
3757            };
3758
3759            if fixtures.len() != fixtures_len {
3760                self.result.single_fail(Some(format!(
3761                    "{} fixtures defined for {param_name} (expected {})",
3762                    fixtures.len(),
3763                    fixtures_len
3764                )));
3765                return self.result;
3766            }
3767
3768            table_fixtures.push(&fixtures[..]);
3769        }
3770
3771        let progress = self.fuzz_progress(&func.name, None, fixtures_len as u32);
3772
3773        let mut result = FuzzTestResult::default();
3774
3775        for i in 0..fixtures_len {
3776            if self.tcfg.early_exit.should_stop() {
3777                self.result.table_result(result);
3778                self.result.interrupt();
3779                return self.result;
3780            }
3781
3782            // Increment progress bar.
3783            if let Some(progress) = progress.as_ref() {
3784                progress.inc(1);
3785            }
3786
3787            let args = table_fixtures.iter().map(|row| row[i].clone()).collect_vec();
3788            let Ok((mut raw_call_result, reason)) = self.call_test(func, &args) else {
3789                return self.result;
3790            };
3791
3792            result.gas_by_case.push((raw_call_result.gas_used, raw_call_result.stipend));
3793            result.logs.extend(raw_call_result.logs.clone());
3794            result.labels.extend(raw_call_result.labels.clone());
3795            HitMaps::merge_opt(&mut result.line_coverage, raw_call_result.line_coverage.clone());
3796
3797            let is_success =
3798                self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result);
3799            // Record counterexample if test fails.
3800            if !is_success {
3801                result.counterexample =
3802                    Some(CounterExample::Single(BaseCounterExample::from_fuzz_call(
3803                        Bytes::from(func.abi_encode_input(&args).unwrap()),
3804                        args,
3805                        raw_call_result.traces.clone(),
3806                    )));
3807                result.reason = reason;
3808            }
3809            // Stop on the first failure, or after the last row using its call result for logs
3810            // and traces.
3811            if !is_success || i == fixtures_len - 1 {
3812                result.success = is_success;
3813                result.traces = raw_call_result.traces;
3814                result.debug_bytecodes = raw_call_result.debug_bytecodes;
3815                self.result.table_result(result);
3816                return self.result;
3817            }
3818        }
3819
3820        self.result
3821    }
3822
3823    fn run_invariant_test(
3824        mut self,
3825        func: &Function,
3826        invariants: Vec<(&Function, bool)>,
3827        shared_invariant_namespace: bool,
3828        call_after_invariant: bool,
3829        identified_contracts: &ContractsByAddress,
3830    ) -> TestResult {
3831        let fuzz_failure_replay = self.cr.mcr.tcfg.fuzz_failure_replay;
3832        let mut invariant_config = self.config.invariant.clone();
3833        if fuzz_failure_replay {
3834            invariant_config.runs = 0;
3835        }
3836        let invariant_config = &invariant_config;
3837        let is_optimization = is_optimization_invariant(func);
3838        let isolated_campaign =
3839            (is_optimization || !shared_invariant_namespace).then_some(func.name.as_str());
3840
3841        let mut live_invariants = Vec::new();
3842        let mut skipped_predicate_results = Vec::new();
3843        for (invariant, fail_on_revert) in invariants {
3844            if let Some(reason) = self.invariant_skip_reason(invariant) {
3845                skipped_predicate_results.push(InvariantPredicateResult {
3846                    name: invariant.name.clone(),
3847                    status: TestStatus::Skipped,
3848                    reason: reason.0,
3849                });
3850            } else {
3851                live_invariants.push((invariant, fail_on_revert));
3852            }
3853        }
3854
3855        if live_invariants.is_empty() {
3856            let skip_reason = skipped_predicate_results
3857                .iter()
3858                .find(|predicate| predicate.name == func.name)
3859                .and_then(|predicate| predicate.reason.clone());
3860            self.result
3861                .invariant_skip_with_predicates(SkipReason(skip_reason), skipped_predicate_results);
3862            return self.result;
3863        }
3864        // Predicates stay in source declaration order; `func` anchors the campaign when it is
3865        // live.
3866        let anchor_idx =
3867            live_invariants.iter().position(|(invariant, _)| *invariant == func).unwrap_or(0);
3868
3869        let mut executor = self.clone_executor();
3870        // Every campaign call would append its steps to the shared step table. The replays that
3871        // the debugger shows run on clones of the test executor, which keeps its recorder.
3872        executor.inspector_mut().debug_recorder = None;
3873        // Enable edge coverage if running with coverage guided fuzzing or with edge coverage
3874        // metrics (useful for benchmarking the fuzzer).
3875        executor.inspector_mut().collect_edge_coverage_with_config(&invariant_config.corpus);
3876        executor
3877            .inspector_mut()
3878            .collect_sancov_edges(invariant_config.corpus.collect_sancov_edges());
3879        executor
3880            .inspector_mut()
3881            .collect_sancov_trace_cmp(invariant_config.corpus.collect_sancov_trace_cmp());
3882        let mut config = invariant_config.clone();
3883        if config.call_override && config.corpus.capture_branch_frontiers() {
3884            let _ = sh_warn!(
3885                "Invariant frontier capture does not support `invariant.call_override`; running \
3886                 the campaign without writing frontier artifacts."
3887            );
3888            config.corpus.frontier_dir = None;
3889        }
3890        let execution_profile = self.tcfg.evm_opts.networks.execution_profile_name();
3891        let execution_pass = if self.cr.mcr.tcfg.multi_network.all_override_networks.is_empty() {
3892            "single"
3893        } else if self.cr.mcr.tcfg.multi_network.pass_network.is_some() {
3894            "override"
3895        } else {
3896            "default"
3897        };
3898        let failure_dir = invariant_suite_paths(
3899            &mut config.corpus,
3900            invariant_config.failure_persist_dir.clone().unwrap(),
3901            self.cr.name,
3902            isolated_campaign,
3903            execution_profile,
3904            execution_pass,
3905        );
3906        // Snapshot the per-test corpus dir before `config` is moved into `InvariantExecutor`.
3907        let resolved_corpus_dir = config.corpus.corpus_dir.clone();
3908
3909        let mut evm = InvariantExecutor::new_with_fuzz_seed(
3910            executor,
3911            self.invariant_runner(),
3912            self.config.fuzz.seed,
3913            config,
3914            identified_contracts,
3915            &self.cr.mcr.known_contracts,
3916            self.cr.num_invariant_campaign_anchors,
3917        );
3918
3919        let predicate_count = live_invariants.len() + skipped_predicate_results.len();
3920        let invariant_contract = InvariantContract::new(
3921            self.address,
3922            self.cr.name,
3923            live_invariants,
3924            anchor_idx,
3925            call_after_invariant,
3926            &self.cr.contract.abi,
3927        );
3928        let anchor = invariant_contract.anchor();
3929        let is_campaign = predicate_count > 1;
3930        let invariant_count = is_campaign.then_some(predicate_count);
3931        let invariant_display_name = if is_campaign {
3932            Cow::Owned(invariant_campaign_display_name(self.cr.name))
3933        } else {
3934            Cow::Borrowed(func.name.as_str())
3935        };
3936
3937        // Select the per-test targets once; the campaign, replay and symbolic paths all need the
3938        // same selection and settings.
3939        if let Err(e) = evm.select_contract_artifacts(self.address) {
3940            self.result.invariant_setup_fail(e);
3941            return self.result;
3942        }
3943        let (sender_filters, targeted) = match evm.select_contracts_and_senders(self.address) {
3944            Ok(selected) => selected,
3945            Err(e) => {
3946                self.result.invariant_setup_fail(e);
3947                return self.result;
3948            }
3949        };
3950        let current_settings = InvariantSettings::new(
3951            &targeted.targets(),
3952            &sender_filters,
3953            invariant_config.fail_on_revert,
3954        );
3955        let setup_fork_block = self.setup.fork_block_number;
3956
3957        let showmap = self.cr.mcr.tcfg.showmap.as_ref();
3958        let minimize = self.cr.mcr.tcfg.fuzz_minimize.as_ref();
3959        if showmap.is_some() || minimize.is_some() {
3960            let dynamic = evm.dynamic_target_ctx();
3961            let replay_target = ShowmapReplayTarget {
3962                stateless: None,
3963                fuzz_fail_on_revert: false,
3964                fuzzed_contracts: Some(&targeted),
3965                invariant_address: Some(self.address),
3966                invariant_fns: &invariant_contract.invariant_fns,
3967                invariant_replay: InvariantReplayOptions {
3968                    check_interval: invariant_config.check_interval,
3969                    call_after_invariant,
3970                    is_optimization,
3971                },
3972                dynamic: Some(&dynamic),
3973            };
3974            // Showmap replay mode: replay the persisted corpus and emit coverage files instead
3975            // of running the invariant campaign.
3976            if let Some(showmap) = showmap {
3977                let corpus_dir = showmap
3978                    .corpus_dir
3979                    .clone()
3980                    .map(|corpus_dir| {
3981                        let target_dir =
3982                            invariant_corpus_dir(&corpus_dir, self.cr.name, isolated_campaign);
3983                        narrow_generated_corpus_root(corpus_dir, target_dir)
3984                    })
3985                    .or(resolved_corpus_dir);
3986                return self.run_showmap(func, &func.name, corpus_dir, showmap, replay_target);
3987            }
3988            if let Some(minimize) = minimize {
3989                let target = self.fuzz_minimize_target_id(&invariant_display_name);
3990                replay_fuzz_minimize(
3991                    &mut self.result,
3992                    minimize,
3993                    target,
3994                    &evm.executor,
3995                    &invariant_config.corpus,
3996                    replay_target,
3997                );
3998                return self.result;
3999            }
4000        }
4001
4002        let progress = self.fuzz_progress(
4003            &invariant_display_name,
4004            invariant_config.timeout,
4005            invariant_config.runs,
4006        );
4007        let primary_failure_file = invariant_failure_file(&failure_dir, anchor);
4008
4009        // Try to replay recorded failure if any. `forge fuzz replay` checks each selected
4010        // predicate as the replay anchor because merged invariant suites persist failures per
4011        // predicate, while campaign runs use a stable suite anchor.
4012        let mut replayed_persisted_invariant = false;
4013        let mut replayed_predicate_failures = Vec::new();
4014        let mut confirmed_persisted_invariants = HashSet::default();
4015        // Normal campaigns must collect secondaries before the anchor's early exit.
4016        let replay_candidates = invariant_contract
4017            .invariant_fns
4018            .iter()
4019            .copied()
4020            .sorted_by_key(|(invariant, _)| (*invariant == anchor) != fuzz_failure_replay)
4021            .collect::<Vec<_>>();
4022        for (replay_invariant, fail_on_revert) in replay_candidates {
4023            let Some(InvariantPersistedFailure {
4024                mut call_sequence,
4025                assertion_failure,
4026                storage,
4027                failure_site,
4028                fork_block_number,
4029                ..
4030            }) = persisted_invariant_failure(&failure_dir, replay_invariant, &current_settings)
4031            else {
4032                continue;
4033            };
4034            if fuzz_failure_replay {
4035                warn_fork_block_mismatch(
4036                    &replay_invariant.name,
4037                    fork_block_number,
4038                    setup_fork_block,
4039                );
4040            }
4041            replayed_persisted_invariant = true;
4042            let replay_anchor_idx = invariant_contract
4043                .invariant_fns
4044                .iter()
4045                .position(|(invariant, _)| *invariant == replay_invariant)
4046                .expect("replay anchor must be present in invariant_fns");
4047            let replay_contract = InvariantContract::new(
4048                self.address,
4049                self.cr.name,
4050                invariant_contract.invariant_fns.clone(),
4051                replay_anchor_idx,
4052                call_after_invariant,
4053                &self.cr.contract.abi,
4054            );
4055            let Ok((txes, mut replay)) = self.replay_persisted_call_sequence(
4056                &replay_contract,
4057                &mut call_sequence,
4058                assertion_failure,
4059                &storage,
4060            ) else {
4061                continue;
4062            };
4063            if replay.success {
4064                continue;
4065            }
4066            let Some(confirmed_failure_site) =
4067                replay.failure_site.map(SymbolicInvariantFailureSite::from)
4068            else {
4069                continue;
4070            };
4071            if failure_site.is_some_and(|expected| expected != confirmed_failure_site) {
4072                continue;
4073            }
4074            if replay_invariant != anchor && !fuzz_failure_replay {
4075                let is_revert = match confirmed_failure_site {
4076                    SymbolicInvariantFailureSite::Invariant { selector, .. }
4077                        if selector == replay_invariant.selector() =>
4078                    {
4079                        false
4080                    }
4081                    SymbolicInvariantFailureSite::SequenceCall { .. }
4082                        if fail_on_revert && !replay.sequence_assertion_failure =>
4083                    {
4084                        true
4085                    }
4086                    _ => continue,
4087                };
4088                confirmed_persisted_invariants.insert(replay_anchor_idx);
4089                replayed_predicate_failures.push((
4090                    replay_invariant.name.clone(),
4091                    InvariantFuzzError::from_replayed_invariant(
4092                        self.address,
4093                        replay_invariant,
4094                        txes,
4095                        replay.reason,
4096                        invariant_config,
4097                        fail_on_revert,
4098                        assertion_failure,
4099                        is_revert,
4100                    ),
4101                    storage,
4102                    confirmed_failure_site,
4103                ));
4104                continue;
4105            }
4106            if is_campaign && !fuzz_failure_replay {
4107                let is_revert = matches!(
4108                    confirmed_failure_site,
4109                    SymbolicInvariantFailureSite::SequenceCall { .. }
4110                ) && fail_on_revert
4111                    && !replay.sequence_assertion_failure;
4112                confirmed_persisted_invariants.insert(replay_anchor_idx);
4113                replayed_predicate_failures.push((
4114                    replay_invariant.name.clone(),
4115                    InvariantFuzzError::from_replayed_invariant(
4116                        self.address,
4117                        replay_invariant,
4118                        txes,
4119                        replay.reason,
4120                        invariant_config,
4121                        fail_on_revert,
4122                        assertion_failure,
4123                        is_revert,
4124                    ),
4125                    storage,
4126                    confirmed_failure_site,
4127                ));
4128                evm.skip_fresh_runs();
4129                break;
4130            }
4131            let warn = "Replayed invariant failure from persisted file. \nRun `forge clean` or remove file to ignore failure and to continue invariant test campaign.";
4132            if let Some(progress) = &progress {
4133                progress.set_prefix(format!("{invariant_display_name}\n{warn}\n"));
4134            } else {
4135                let _ = sh_warn!("{warn}");
4136            }
4137
4138            // If sequence still fails then replay error to collect traces and exit without
4139            // executing new runs.
4140            let trace_executor = match self.clone_executor_with_symbolic_storage(&storage) {
4141                Ok(executor) => executor,
4142                Err(err) => {
4143                    error!(%err, "Failed to apply symbolic storage for invariant error replay");
4144                    self.result.single_fail(Some(err.to_string()));
4145                    return self.result;
4146                }
4147            };
4148            let mut replay_config = invariant_config.clone();
4149            // The persisted replay was confirmed at `confirmed_failure_site`; generic shrinking
4150            // only preserves failure and could append diagnostics from a different failure before
4151            // the later site check rejects it. This also applies to legacy entries without a site.
4152            replay_config.shrink_run_limit = 0;
4153            match self.replay_error(
4154                replay_config,
4155                trace_executor,
4156                &txes,
4157                None,
4158                assertion_failure,
4159                None,
4160                &[],
4161                &replay_contract,
4162                replay_invariant,
4163                identified_contracts,
4164                progress.as_ref(),
4165                None,
4166            ) {
4167                Ok(ReplayErrorResult {
4168                    counterexample_sequence: sequence, check_result, ..
4169                }) if !sequence.is_empty() => {
4170                    call_sequence = sequence;
4171                    if let Some(updated) = check_result {
4172                        if updated.failure_site.map(SymbolicInvariantFailureSite::from)
4173                            != Some(confirmed_failure_site)
4174                        {
4175                            continue;
4176                        }
4177                        replay = updated;
4178                    }
4179                    record_invariant_failure(
4180                        &invariant_failure_file(&failure_dir, replay_invariant),
4181                        &call_sequence,
4182                        &current_settings,
4183                        assertion_failure,
4184                        &storage,
4185                        Some(confirmed_failure_site),
4186                        None,
4187                        setup_fork_block,
4188                    );
4189                }
4190                Ok(_) => {}
4191                Err(err) => {
4192                    error!(%err, "Failed to replay invariant error");
4193                }
4194            }
4195
4196            self.result.invariant_replay_fail(
4197                replay,
4198                &replay_invariant.name,
4199                None,
4200                call_sequence.clone(),
4201            );
4202            let signature = replay_invariant.signature();
4203            if let Some(artifact) = self.persist_sequence_artifact(
4204                &signature,
4205                &format!("{signature}-replay"),
4206                self.sequence_calls(&call_sequence),
4207                self.config.invariant.fail_on_revert,
4208                &storage,
4209                Some(SymbolicInvariantArtifactFailure::Predicate {
4210                    name: replay_invariant.name.clone(),
4211                    site: Some(confirmed_failure_site),
4212                }),
4213            ) {
4214                self.result.add_counterexample_artifact(artifact);
4215            }
4216            return self.result;
4217        }
4218
4219        if (self.config.symbolic.use_fuzz_frontiers
4220            || self.config.symbolic.check_invariant_frontiers)
4221            && !fuzz_failure_replay
4222        {
4223            let seeding_config = evm.config();
4224            let dynamic_target_ctx = evm.dynamic_target_ctx();
4225            let fresh_failures = self.try_seed_invariant_corpus_from_frontiers(
4226                &invariant_contract,
4227                &seeding_config,
4228                &sender_filters,
4229                &targeted,
4230                &dynamic_target_ctx,
4231                &confirmed_persisted_invariants,
4232            );
4233            let mut reported_fresh_invariants = confirmed_persisted_invariants.clone();
4234            for ConfirmedFrontierInvariantFailure { invariant_idx, call_sequence: txes, replay } in
4235                fresh_failures
4236            {
4237                let replay_invariant = invariant_contract.invariant_fns[invariant_idx].0;
4238                let fail_on_revert = invariant_contract.invariant_fns[invariant_idx].1;
4239                let call_sequence = base_counterexamples(
4240                    &txes,
4241                    identified_contracts,
4242                    invariant_config.show_solidity,
4243                );
4244                let failure_site = replay
4245                    .failure_site
4246                    .map(SymbolicInvariantFailureSite::from)
4247                    .expect("confirmed frontier failure has a failure site");
4248                // The result map keeps the first failure per predicate; persist that same one.
4249                if !reported_fresh_invariants.insert(invariant_idx) {
4250                    continue;
4251                }
4252
4253                record_invariant_failure(
4254                    &invariant_failure_file(&failure_dir, replay_invariant),
4255                    &call_sequence,
4256                    &current_settings,
4257                    false,
4258                    &[],
4259                    Some(failure_site),
4260                    None,
4261                    setup_fork_block,
4262                );
4263                replayed_predicate_failures.push((
4264                    replay_invariant.name.clone(),
4265                    InvariantFuzzError::from_replayed_invariant(
4266                        self.address,
4267                        replay_invariant,
4268                        txes,
4269                        replay.reason,
4270                        invariant_config,
4271                        fail_on_revert,
4272                        false,
4273                        false,
4274                    ),
4275                    Vec::new(),
4276                    failure_site,
4277                ));
4278            }
4279        }
4280
4281        // Replay persisted handler bugs; feed still-reproducing ones into the campaign,
4282        // delete stale files in place.
4283        let (mut persisted_handler_failures, mut symbolic_handler_storage) = self
4284            .replay_persisted_handler_failures(&failure_dir.join("handlers"), &current_settings);
4285
4286        // `forge fuzz replay` (without `--corpus-dir`) only replays persisted failures and
4287        // must never start a fresh campaign. If handler bugs still reproduce, surface them
4288        // through the normal invariant result path below; otherwise report a skip.
4289        if fuzz_failure_replay && persisted_handler_failures.is_empty() {
4290            let reason = if replayed_persisted_invariant {
4291                "no persisted invariant failure reproduced for selected invariants".to_string()
4292            } else {
4293                format!("no persisted invariant failure reproduced for {}", anchor.name)
4294            };
4295            self.result.single_skip(SkipReason(Some(reason)));
4296            return self.result;
4297        }
4298
4299        let confirmed_anchor_failure =
4300            replayed_predicate_failures.iter().any(|(name, ..)| name == &anchor.name);
4301        // Do not record a symbolic PASS for an anchor already confirmed to fail concretely.
4302        if self.config.symbolic.enabled && !is_optimization && !confirmed_anchor_failure {
4303            let anchor_fail_on_revert = invariant_contract.invariant_fns[anchor_idx].1;
4304            let after_invariant = call_after_invariant
4305                .then(|| {
4306                    self.cr
4307                        .contract
4308                        .abi
4309                        .functions()
4310                        .find(|func| func.name == "afterInvariant" && func.inputs.is_empty())
4311                })
4312                .flatten();
4313            let symbolic_targets = targeted
4314                .targets()
4315                .iter()
4316                .flat_map(|(address, contract)| {
4317                    let contract_name = Some(contract.identifier.clone());
4318                    contract.abi_fuzzed_functions().map(move |function| SymbolicInvariantTarget {
4319                        address: *address,
4320                        contract_name: contract_name.clone(),
4321                        function: function.clone(),
4322                    })
4323                })
4324                .collect::<Vec<_>>();
4325            let unsupported_domain_reason = symbolic_invariant_unsupported_domain_reason(
4326                invariant_config,
4327                &sender_filters,
4328                &targeted,
4329                &symbolic_targets,
4330            );
4331
4332            let mut symbolic_invariant_config = invariant_config.clone();
4333            symbolic_invariant_config.fail_on_revert = anchor_fail_on_revert;
4334            let symbolic_config = self.config.symbolic.clone();
4335            let incomplete = |kind, reason: &str, stats, replay| {
4336                SymbolicResult::incomplete(
4337                    &symbolic_config,
4338                    kind,
4339                    reason,
4340                    stats,
4341                    replay,
4342                    SymbolicCallTrace::none(),
4343                    None,
4344                )
4345            };
4346            let mut symbolic = SymbolicExecutor::new(symbolic_config.clone());
4347            match symbolic.run_invariant(SymbolicInvariantRunInput {
4348                executor: ExecutorView::new(&evm.executor),
4349                invariant_address: self.address,
4350                sender: self.sender,
4351                invariant: anchor,
4352                after_invariant,
4353                targets: symbolic_targets,
4354                senders: sender_filters.targeted,
4355                excluded_senders: sender_filters.excluded,
4356                depth: symbolic_config.invariant_depth as usize,
4357                check_interval: invariant_config.check_interval,
4358                fail_on_revert: anchor_fail_on_revert,
4359                ffi_enabled: self.config.ffi,
4360            }) {
4361                SymbolicInvariantRunResult::Safe(stats) => {
4362                    self.result.record_symbolic(match unsupported_domain_reason {
4363                        Some(reason) => incomplete(
4364                            SymbolicStopReason::Stuck,
4365                            reason,
4366                            stats,
4367                            SymbolicReplayMetadata::not_required(),
4368                        ),
4369                        None => SymbolicResult::pass(&symbolic_config, stats),
4370                    });
4371                }
4372                SymbolicInvariantRunResult::Incomplete { kind, reason, stats } => {
4373                    self.result.record_symbolic(incomplete(
4374                        kind,
4375                        &reason,
4376                        stats,
4377                        SymbolicReplayMetadata::not_required(),
4378                    ));
4379                }
4380                SymbolicInvariantRunResult::Counterexample {
4381                    kind,
4382                    sequence,
4383                    storage,
4384                    stats,
4385                } => 'counterexample: {
4386                    let is_handler = matches!(kind, SymbolicInvariantCounterexampleKind::Handler);
4387                    let symbolic_calls = symbolic_invariant_counterexample_calls(
4388                        &sequence,
4389                        identified_contracts,
4390                        invariant_config.show_solidity,
4391                    );
4392                    let check = SequenceReplay {
4393                        invariant_config: &symbolic_invariant_config,
4394                        invariant_contract: &invariant_contract,
4395                        target_invariant: anchor,
4396                        assertion_failure: false,
4397                        storage: &storage,
4398                    };
4399                    let replayed = self
4400                        .symbolic_sequence_failure(check, &symbolic_calls)
4401                        .ok_or("symbolic invariant counterexample did not replay")
4402                        .and_then(|failure| {
4403                            let handler_site = match failure.failure_site {
4404                                Some(CheckSequenceFailureSite::SequenceCall {
4405                                    target,
4406                                    selector,
4407                                    fingerprint,
4408                                }) if is_handler => Some((target, selector, fingerprint)),
4409                                _ => None,
4410                            };
4411                            if is_handler && handler_site.is_none() {
4412                                return Err("symbolic handler counterexample replayed at a \
4413                                            non-handler failure site");
4414                            }
4415                            Ok((failure, handler_site))
4416                        });
4417                    let (failure, handler_site) = match replayed {
4418                        Ok(replayed) => replayed,
4419                        Err(reason) => {
4420                            self.result.record_symbolic(incomplete(
4421                                SymbolicStopReason::Error,
4422                                reason,
4423                                stats,
4424                                SymbolicReplayMetadata::mismatch(reason.to_string()),
4425                            ));
4426                            break 'counterexample;
4427                        }
4428                    };
4429
4430                    let txes = symbolic_calls
4431                        .iter()
4432                        .map(SymbolicCounterexampleCall::to_basic_tx_details)
4433                        .collect::<Vec<_>>();
4434                    let original_sequence_len = txes.len();
4435                    let failure_site = failure.failure_site.map(SymbolicInvariantFailureSite::from);
4436                    let (artifact_file_name, artifact_failure) = match handler_site {
4437                        Some((reverter, selector, fingerprint)) => (
4438                            format!("handler-{reverter}-{selector}"),
4439                            SymbolicInvariantArtifactFailure::Handler {
4440                                name: Some(invariant_handler_failure_name(
4441                                    identified_contracts,
4442                                    reverter,
4443                                    selector,
4444                                )),
4445                                reverter,
4446                                selector,
4447                                fingerprint,
4448                            },
4449                        ),
4450                        None => (
4451                            anchor.signature(),
4452                            SymbolicInvariantArtifactFailure::Predicate {
4453                                name: anchor.name.clone(),
4454                                site: failure_site,
4455                            },
4456                        ),
4457                    };
4458                    let replayed = match self.replay_invariant_error_sequence(
4459                        SequenceReplay { assertion_failure: is_handler, ..check },
4460                        &txes,
4461                        None,
4462                        identified_contracts,
4463                        &current_settings,
4464                        SequenceArtifactSpec {
4465                            file_name: &artifact_file_name,
4466                            fail_on_revert: is_handler || anchor_fail_on_revert,
4467                            failure: Some(artifact_failure),
4468                        },
4469                        progress.as_ref(),
4470                        Some((1, 1)),
4471                    ) {
4472                        Ok(replayed) => replayed,
4473                        Err(err) => {
4474                            let reason = format!("symbolic invariant replay failed: {err}");
4475                            self.result.record_symbolic(incomplete(
4476                                SymbolicStopReason::Error,
4477                                &reason,
4478                                stats,
4479                                SymbolicReplayMetadata::error(reason.clone()),
4480                            ));
4481                            break 'counterexample;
4482                        }
4483                    };
4484                    let ReplayedInvariantSequence {
4485                        call_sequence,
4486                        artifact,
4487                        minimization,
4488                        fork_block_number,
4489                    } = replayed;
4490                    let mut symbolic_result = SymbolicResult::fail_counterexample_sequence(
4491                        &symbolic_config,
4492                        stats,
4493                        SymbolicCallTrace::test_result_traces(!self.result.traces.is_empty()),
4494                    );
4495                    if let Some(artifact) = artifact.clone() {
4496                        symbolic_result = symbolic_result.with_artifact(artifact);
4497                    }
4498                    if let Some(minimization) = minimization.clone() {
4499                        symbolic_result = symbolic_result.with_minimization(minimization);
4500                    }
4501                    let reason = failure.reason.unwrap_or_else(|| {
4502                        if is_handler {
4503                            "symbolic handler counterexample".to_string()
4504                        } else {
4505                            "symbolic invariant counterexample".to_string()
4506                        }
4507                    });
4508
4509                    if let Some((reverter, selector, fingerprint)) = handler_site {
4510                        let call_sequence =
4511                            call_sequence.iter().map(base_counterexample_to_tx).collect::<Vec<_>>();
4512                        symbolic_handler_storage.insert(
4513                            (reverter, selector, fingerprint),
4514                            SymbolicHandlerReplayStorage {
4515                                call_sequence: call_sequence.clone(),
4516                                assignments: storage,
4517                                fingerprint_provenance: None,
4518                            },
4519                        );
4520                        persisted_handler_failures.insert(
4521                            (reverter, selector),
4522                            InvariantFuzzError::HandlerAssertion(HandlerAssertionFailure {
4523                                reverter,
4524                                selector,
4525                                call_sequence,
4526                                original_sequence_len,
4527                                revert_reason: reason,
4528                                fork_block_number: None,
4529                                edge_fingerprint: fingerprint,
4530                            }),
4531                        );
4532                        self.result.record_symbolic(symbolic_result);
4533                        break 'counterexample;
4534                    }
4535
4536                    record_invariant_failure(
4537                        &primary_failure_file,
4538                        &call_sequence,
4539                        &current_settings,
4540                        false,
4541                        &storage,
4542                        failure_site,
4543                        None,
4544                        setup_fork_block,
4545                    );
4546                    let mut invariant_failures = vec![InvariantFailure::Predicate {
4547                        name: anchor.name.clone(),
4548                        reason,
4549                        counterexample: Some(CounterExample::Sequence(
4550                            original_sequence_len,
4551                            call_sequence,
4552                        )),
4553                        artifact,
4554                        minimization,
4555                        persisted_path: primary_failure_file,
4556                        is_anchor: true,
4557                    }];
4558                    for (invariant, _) in &invariant_contract.invariant_fns {
4559                        if let Some((_, error, _, _)) = replayed_predicate_failures
4560                            .iter()
4561                            .find(|(name, ..)| name == &invariant.name)
4562                            && let Some(calls) = failed_invariant_calls(error)
4563                        {
4564                            invariant_failures.push(InvariantFailure::Predicate {
4565                                name: invariant.name.clone(),
4566                                reason: error.revert_reason().unwrap_or_default(),
4567                                counterexample: Some(CounterExample::Sequence(
4568                                    calls.len(),
4569                                    base_counterexamples(
4570                                        calls,
4571                                        identified_contracts,
4572                                        invariant_config.show_solidity,
4573                                    ),
4574                                )),
4575                                artifact: None,
4576                                minimization: None,
4577                                persisted_path: invariant_failure_file(&failure_dir, invariant),
4578                                is_anchor: false,
4579                            });
4580                        }
4581                    }
4582                    let invariant_predicate_results = if is_campaign {
4583                        self.sort_predicate_results(
4584                            invariant_failures
4585                                .iter()
4586                                .map(|failure| InvariantPredicateResult {
4587                                    name: failure.name().to_string(),
4588                                    status: TestStatus::Failure,
4589                                    reason: Some(failure.reason().to_string()),
4590                                })
4591                                .chain(skipped_predicate_results),
4592                        )
4593                    } else {
4594                        Vec::new()
4595                    };
4596                    self.result.invariant_result(
4597                        invariant_kind(1, failure.calls_count, failure.reverts),
4598                        InvariantOutcome {
4599                            fork_block_number,
4600                            failures: invariant_failures,
4601                            predicate_results: invariant_predicate_results,
4602                            failure_dir: Some(failure_dir),
4603                            invariant_count,
4604                            ..Default::default()
4605                        },
4606                    );
4607                    self.result.record_symbolic(symbolic_result);
4608                    return self.result;
4609                }
4610            }
4611        }
4612
4613        let mut invariant_result = match evm.invariant_fuzz(
4614            invariant_contract.clone(),
4615            &self.setup.fuzz_fixtures,
4616            self.build_fuzz_state(true, None),
4617            progress.as_ref(),
4618            &self.tcfg.early_exit,
4619            persisted_handler_failures,
4620        ) {
4621            Ok(x) => x,
4622            Err(e) if replayed_predicate_failures.is_empty() => {
4623                self.result.invariant_setup_fail(e);
4624                return self.result;
4625            }
4626            Err(e) => {
4627                warn!(%e, "invariant campaign setup failed after replay-confirmed frontier failure");
4628                Default::default()
4629            }
4630        };
4631        let mut replayed_predicate_metadata = BTreeMap::new();
4632        for (name, failure, storage, failure_site) in replayed_predicate_failures {
4633            if let Entry::Vacant(entry) = invariant_result.errors.entry(name) {
4634                replayed_predicate_metadata.insert(entry.key().clone(), (storage, failure_site));
4635                entry.insert(failure);
4636            }
4637        }
4638        // Merge coverage collected during invariant run with test setup coverage.
4639        self.result.merge_coverages(invariant_result.line_coverage);
4640
4641        let mut counterexample = None;
4642        // Success requires zero predicate breaks *and* zero handler-side assertion bugs.
4643        let success =
4644            invariant_result.errors.is_empty() && invariant_result.handler_errors.is_empty();
4645        let single_failure =
4646            invariant_result.errors.len() + invariant_result.handler_errors.len() == 1;
4647        let mut fork_block_number = invariant_result.fork_block_number;
4648        let mut invariant_failures = Vec::new();
4649        let mut any_failure_persisted = false;
4650
4651        if success {
4652            if let Some(best_value) = invariant_result.optimization_best_value {
4653                // Optimization mode: replay and shrink to find shortest best sequence.
4654                let targets = evm
4655                    .select_contracts_and_senders(invariant_contract.address)
4656                    .map(|(_, targets)| targets.targets().keys().copied().collect::<Vec<_>>())
4657                    .unwrap_or_default();
4658                match self.replay_error(
4659                    evm.config(),
4660                    self.clone_executor(),
4661                    &invariant_result.optimization_best_sequence,
4662                    None,
4663                    false,
4664                    Some(best_value),
4665                    &targets,
4666                    &invariant_contract,
4667                    anchor,
4668                    identified_contracts,
4669                    progress.as_ref(),
4670                    None,
4671                ) {
4672                    Ok(ReplayErrorResult {
4673                        counterexample_sequence: sequence,
4674                        optimization_value,
4675                        ..
4676                    }) if !sequence.is_empty() => {
4677                        counterexample = Some(CounterExample::Sequence(
4678                            invariant_result.optimization_best_sequence.len(),
4679                            sequence,
4680                        ));
4681                        // Shrinking can reach a higher value than the campaign did.
4682                        if let Some(value) = optimization_value
4683                            && value > best_value
4684                        {
4685                            invariant_result.optimization_best_value = Some(value);
4686                        }
4687                    }
4688                    Err(err) => {
4689                        error!(%err, "Failed to replay optimization best sequence");
4690                    }
4691                    _ => {}
4692                }
4693            } else if let Err(err) = replay_run(
4694                // Standard check mode: replay last run for traces.
4695                &invariant_contract,
4696                anchor,
4697                self.clone_executor(),
4698                &self.cr.mcr.known_contracts,
4699                identified_contracts.clone(),
4700                &mut self.result.logs,
4701                &mut self.result.traces,
4702                &mut self.result.debug_bytecodes,
4703                &mut self.result.line_coverage,
4704                &mut self.result.deprecated_cheatcodes,
4705                &invariant_result.last_run_inputs,
4706                invariant_config.show_solidity,
4707            ) {
4708                error!(%err, "Failed to replay last invariant run");
4709            }
4710        } else {
4711            // Total broken invariants in this campaign, used to decorate the shrink progress bar
4712            // with `[i/N]`. `errors` keys cover both the anchor and any broken secondaries.
4713            let total_broken = invariant_result.errors.len();
4714            // The anchor is shrunk first (as `[1/N]`); secondaries follow and only advance the
4715            // counter when they are actually shrunk so it matches user-visible progress.
4716            let mut next_position = 2usize;
4717            let order = std::iter::once(anchor_idx).chain(
4718                (0..invariant_contract.invariant_fns.len()).filter(|idx| *idx != anchor_idx),
4719            );
4720            for idx in order {
4721                let is_anchor = idx == anchor_idx;
4722                let invariant = invariant_contract.invariant_fns[idx].0;
4723                let Some(error) = invariant_result.errors.get(&invariant.name) else {
4724                    continue;
4725                };
4726                let persisted_path = invariant_failure_file(&failure_dir, invariant);
4727                let (case_data, calls) = match error {
4728                    InvariantFuzzError::BrokenInvariant(case_data)
4729                    | InvariantFuzzError::Revert(case_data) => {
4730                        (case_data, failed_invariant_calls(error).unwrap_or_default())
4731                    }
4732                    // Non-replayable anchor errors (e.g. `MaxAssumeRejects`) still get an entry,
4733                    // without a counterexample, so the reason is rendered.
4734                    _ if is_anchor => {
4735                        invariant_failures.push(InvariantFailure::Predicate {
4736                            name: invariant.name.clone(),
4737                            reason: error.revert_reason().unwrap_or_default(),
4738                            counterexample: None,
4739                            artifact: None,
4740                            minimization: None,
4741                            persisted_path,
4742                            is_anchor,
4743                        });
4744                        continue;
4745                    }
4746                    _ => continue,
4747                };
4748                let replayed_metadata = replayed_predicate_metadata.get(&invariant.name);
4749
4750                // On Ctrl+C: skip the (potentially long) secondary replay+shrink, but still
4751                // persist the un-shrunk sequence so the next run targeting this invariant picks
4752                // it up and shrinks from the saved counterexample. The current run's output
4753                // still gets a terse `name: reason` line via the no-counterexample path.
4754                let replayed = if !is_anchor && self.tcfg.early_exit.should_stop() {
4755                    if replayed_metadata.is_none() {
4756                        record_invariant_failure(
4757                            &persisted_path,
4758                            &base_counterexamples(
4759                                calls,
4760                                identified_contracts,
4761                                invariant_config.show_solidity,
4762                            ),
4763                            &current_settings,
4764                            case_data.assertion_failure,
4765                            &[],
4766                            None,
4767                            None,
4768                            setup_fork_block,
4769                        );
4770                    }
4771                    any_failure_persisted = true;
4772                    None
4773                } else {
4774                    let position = if is_anchor {
4775                        1
4776                    } else {
4777                        next_position += 1;
4778                        next_position - 1
4779                    };
4780                    let mut replay_config = invariant_config.clone();
4781                    if replayed_metadata.is_some() {
4782                        // The persisted failure site was validated before entering the
4783                        // campaign. The generic shrinker only preserves failure, not its site,
4784                        // so shrinking here could misattribute a different failure to this
4785                        // predicate.
4786                        replay_config.shrink_run_limit = 0;
4787                    }
4788                    let (storage, artifact_failure) = match replayed_metadata {
4789                        Some((storage, site)) => (
4790                            storage.as_slice(),
4791                            Some(SymbolicInvariantArtifactFailure::Predicate {
4792                                name: invariant.name.clone(),
4793                                site: Some(*site),
4794                            }),
4795                        ),
4796                        None => (&[][..], None),
4797                    };
4798                    let signature = invariant.signature();
4799                    match self.replay_invariant_error_sequence(
4800                        SequenceReplay {
4801                            invariant_config: &replay_config,
4802                            invariant_contract: &invariant_contract,
4803                            target_invariant: invariant,
4804                            assertion_failure: case_data.assertion_failure,
4805                            storage,
4806                        },
4807                        calls,
4808                        Some(case_data.inner_sequence.clone()),
4809                        identified_contracts,
4810                        &current_settings,
4811                        SequenceArtifactSpec {
4812                            file_name: &signature,
4813                            fail_on_revert: self.config.invariant.fail_on_revert,
4814                            failure: artifact_failure,
4815                        },
4816                        progress.as_ref(),
4817                        Some((position, total_broken)),
4818                    ) {
4819                        Ok(replayed) if !replayed.call_sequence.is_empty() => {
4820                            if single_failure {
4821                                fork_block_number =
4822                                    replayed.fork_block_number.or(fork_block_number);
4823                            }
4824                            // Keep all replay metadata for a seeded persisted failure. A fresh
4825                            // campaign error takes precedence and is persisted normally.
4826                            if replayed_metadata.is_none() {
4827                                record_invariant_failure(
4828                                    &persisted_path,
4829                                    &replayed.call_sequence,
4830                                    &current_settings,
4831                                    case_data.assertion_failure,
4832                                    &[],
4833                                    None,
4834                                    None,
4835                                    setup_fork_block,
4836                                );
4837                            }
4838                            any_failure_persisted = true;
4839                            Some(replayed)
4840                        }
4841                        Ok(_) => None,
4842                        Err(err) => {
4843                            error!(%err, "Failed to replay invariant error");
4844                            None
4845                        }
4846                    }
4847                };
4848                let (counterexample, artifact, minimization) = match replayed {
4849                    Some(replayed) => (
4850                        Some(CounterExample::Sequence(calls.len(), replayed.call_sequence)),
4851                        replayed.artifact,
4852                        replayed.minimization,
4853                    ),
4854                    None => (None, None, None),
4855                };
4856                invariant_failures.push(InvariantFailure::Predicate {
4857                    name: invariant.name.clone(),
4858                    reason: error.revert_reason().unwrap_or_default(),
4859                    counterexample,
4860                    artifact,
4861                    minimization,
4862                    persisted_path,
4863                    is_anchor,
4864                });
4865            }
4866        }
4867
4868        let invariant_failure_dir = any_failure_persisted.then(|| failure_dir.clone());
4869        let invariant_predicate_results = if is_campaign {
4870            let failures_by_name = invariant_failures
4871                .iter()
4872                .map(|failure| (failure.name(), failure))
4873                .collect::<BTreeMap<_, _>>();
4874            self.sort_predicate_results(
4875                invariant_contract
4876                    .invariant_fns
4877                    .iter()
4878                    .map(|(invariant, _)| {
4879                        let failure = failures_by_name.get(invariant.name.as_str());
4880                        InvariantPredicateResult {
4881                            name: invariant.name.clone(),
4882                            status: if failure.is_some() {
4883                                TestStatus::Failure
4884                            } else {
4885                                TestStatus::Success
4886                            },
4887                            reason: failure.map(|failure| failure.reason().to_string()),
4888                        }
4889                    })
4890                    .chain(skipped_predicate_results),
4891            )
4892        } else {
4893            Vec::new()
4894        };
4895
4896        // Convert handler-side assertion bugs into render-ready entries. The name is a
4897        // best-effort `Contract::function` from `identified_contracts`, falling back to
4898        // `0xreverter::0xselector`. Map is keyed by `(reverter, selector)` site so multiple
4899        // code paths through the same function collapse to one entry, rendered in the
4900        // dedicated handler assertions section.
4901        let invariant_handler_failures = invariant_result
4902            .handler_errors
4903            .iter()
4904            // Stable order across runs: sort by `(reverter, selector)` site directly.
4905            .sorted_by(|(a, _), (b, _)| a.cmp(b))
4906            .filter_map(|(_, err)| err.as_handler_assertion())
4907            .map(|failure| {
4908                let (reverter, selector) = (failure.reverter, failure.selector);
4909                let name = invariant_handler_failure_name(identified_contracts, reverter, selector);
4910                let symbolic_replay = symbolic_handler_storage
4911                    .get(&(reverter, selector, failure.edge_fingerprint))
4912                    .filter(|storage| storage.call_sequence == failure.call_sequence);
4913                let symbolic_storage =
4914                    symbolic_replay.map_or(&[][..], |storage| &storage.assignments);
4915                let fingerprint_provenance = if let Some(replay) = symbolic_replay {
4916                    replay.fingerprint_provenance
4917                } else if failure.edge_fingerprint
4918                    != handler_edge_fingerprint(None, reverter, selector)
4919                {
4920                    PersistedFingerprintProvenance::from_corpus(&invariant_config.corpus)
4921                } else {
4922                    None
4923                };
4924                let calls = base_counterexamples(
4925                    &failure.call_sequence,
4926                    identified_contracts,
4927                    invariant_config.show_solidity,
4928                );
4929
4930                // Persist for next-run replay (skip if nothing to record).
4931                if !calls.is_empty() {
4932                    record_handler_failure(
4933                        &failure_dir,
4934                        reverter,
4935                        selector,
4936                        failure.edge_fingerprint,
4937                        &calls,
4938                        &current_settings,
4939                        symbolic_storage,
4940                        fingerprint_provenance,
4941                        setup_fork_block,
4942                    );
4943                }
4944                let artifact = self.persist_sequence_artifact(
4945                    &anchor.signature(),
4946                    &format!("handler-{reverter}-{selector}"),
4947                    self.sequence_calls(&calls),
4948                    true,
4949                    symbolic_storage,
4950                    Some(SymbolicInvariantArtifactFailure::Handler {
4951                        name: Some(name.clone()),
4952                        reverter,
4953                        selector,
4954                        fingerprint: failure.edge_fingerprint,
4955                    }),
4956                );
4957                // Preserve pre-shrink length for `(original: N, shrunk: M)` rendering.
4958                let counterexample = (!calls.is_empty())
4959                    .then(|| CounterExample::Sequence(failure.original_sequence_len, calls));
4960
4961                InvariantFailure::Handler {
4962                    name,
4963                    reverter,
4964                    selector,
4965                    reason: failure.revert_reason.clone(),
4966                    counterexample,
4967                    artifact,
4968                }
4969            })
4970            .collect::<Vec<_>>();
4971
4972        let interrupted = success && invariant_result.interrupted;
4973        self.result.invariant_result(
4974            TestKind::Invariant {
4975                runs: invariant_result.runs,
4976                calls: invariant_result.calls,
4977                reverts: invariant_result.reverts,
4978                workers: invariant_result.workers.max(1),
4979                metrics: invariant_result.metrics,
4980                failed_corpus_replays: invariant_result.failed_corpus_replays,
4981                optimization_best_value: invariant_result.optimization_best_value,
4982            },
4983            InvariantOutcome {
4984                success,
4985                fork_block_number,
4986                failures: invariant_failures,
4987                handler_failures: invariant_handler_failures,
4988                predicate_results: invariant_predicate_results,
4989                failure_dir: invariant_failure_dir,
4990                invariant_count,
4991                counterexample,
4992                gas_report_traces: invariant_result.gas_report_traces,
4993            },
4994        );
4995        if interrupted {
4996            self.result.interrupt();
4997        }
4998        self.result
4999    }
5000
5001    /// Orders predicate results by their declaration position in the test contract ABI.
5002    fn sort_predicate_results(
5003        &self,
5004        results: impl Iterator<Item = InvariantPredicateResult>,
5005    ) -> Vec<InvariantPredicateResult> {
5006        results
5007            .sorted_by_key(|predicate| {
5008                self.cr
5009                    .contract
5010                    .abi
5011                    .functions()
5012                    .position(|func| func.name == predicate.name)
5013                    .unwrap_or(usize::MAX)
5014            })
5015            .collect()
5016    }
5017
5018    fn invariant_skip_reason(&self, func: &Function) -> Option<SkipReason> {
5019        match self.executor.call(
5020            self.sender,
5021            self.address,
5022            func,
5023            &[],
5024            U256::ZERO,
5025            Some(self.revert_decoder()),
5026        ) {
5027            Err(EvmError::Skip(reason)) => Some(reason),
5028            _ => None,
5029        }
5030    }
5031
5032    /// Runs a fuzzed test.
5033    ///
5034    /// Applies the before test txes (if any), fuzzes the current function and returns the
5035    /// `TestResult`.
5036    ///
5037    /// Before test txes are applied in order and state modifications committed to the EVM database
5038    /// (therefore the fuzz test will use the modified state).
5039    /// State modifications of before test txes and fuzz test are discarded after test ends,
5040    /// similar to `eth_call`.
5041    fn run_fuzz_test(mut self, func: &Function) -> TestResult {
5042        // Prepare fuzz test execution.
5043        if self.prepare_test(func).is_err() {
5044            return self.result;
5045        }
5046        // Includes forks selected or rolled by `beforeTestSetup` calls.
5047        let fork_block_number = self.result.fork_block_number;
5048
5049        let runner = self.fuzz_runner();
5050        let mut fuzz_config = self.config.fuzz.clone();
5051        let (test_name, legacy_corpus_dir, (failure_dir, failure_file)) =
5052            self.fuzz_test_paths(func, &mut fuzz_config);
5053        let fuzz_input = self.cr.mcr.tcfg.fuzz_input.as_ref();
5054        let is_explicit_target = fuzz_input
5055            .is_some_and(|input| input.contract == self.cr.name && input.test == func.signature());
5056        if is_explicit_target && fuzz_config.run.is_some() {
5057            self.result.fuzz_setup_fail(eyre::eyre!(
5058                "`--fuzz-input-file` cannot be combined with `fuzz.run`"
5059            ));
5060            return self.result;
5061        }
5062
5063        let replay_target = ShowmapReplayTarget {
5064            stateless: Some(StatelessReplayTarget { function: func, address: self.address }),
5065            fuzz_fail_on_revert: fuzz_config.fail_on_revert,
5066            fuzzed_contracts: None,
5067            invariant_address: None,
5068            invariant_fns: &[],
5069            invariant_replay: InvariantReplayOptions::default(),
5070            dynamic: None,
5071        };
5072        // Showmap replay mode: replay the persisted corpus and emit coverage
5073        // files instead of running the fuzz campaign.
5074        if let Some(showmap) = self.cr.mcr.tcfg.showmap.as_ref() {
5075            let corpus_dir = showmap
5076                .corpus_dir
5077                .clone()
5078                .map(|corpus_dir| {
5079                    legacy_fuzz_corpus_dir(Some(&corpus_dir), self.cr.name, func, &test_name)
5080                        .unwrap_or_else(|| {
5081                            let target_dir = corpus_dir
5082                                .join(contract_short_name(self.cr.name))
5083                                .join(&*test_name);
5084                            narrow_generated_corpus_root(corpus_dir, target_dir)
5085                        })
5086                })
5087                .or(legacy_corpus_dir)
5088                .or_else(|| fuzz_config.corpus.corpus_dir.clone());
5089            return self.run_showmap(func, &test_name, corpus_dir, showmap, replay_target);
5090        }
5091        if let Some(minimize) = self.cr.mcr.tcfg.fuzz_minimize.as_ref() {
5092            let target = self.fuzz_minimize_target_id(&func.signature());
5093            replay_fuzz_minimize(
5094                &mut self.result,
5095                minimize,
5096                target,
5097                &self.executor,
5098                &fuzz_config.corpus,
5099                replay_target,
5100            );
5101            return self.result;
5102        }
5103
5104        // Load the validated explicit input for its unique target, or fall back to this test's
5105        // canonical cache.
5106        let persisted_failure = if is_explicit_target {
5107            fuzz_input.map(|input| input.failure.as_ref().clone())
5108        } else {
5109            foundry_common::fs::read_json_file::<BaseCounterExample>(&failure_file).ok().or_else(
5110                || {
5111                    if test_name == func.name {
5112                        return None;
5113                    }
5114                    let legacy_file = canonicalized(failure_dir.join(&func.name));
5115                    let failure =
5116                        foundry_common::fs::read_json_file::<BaseCounterExample>(&legacy_file)
5117                            .ok()?;
5118                    failure
5119                        .calldata
5120                        .get(..4)
5121                        .is_some_and(|selector| func.selector() == selector)
5122                        .then_some(failure)
5123                },
5124            )
5125        };
5126        if self.cr.mcr.tcfg.fuzz_failure_replay {
5127            let skip_reason = match &persisted_failure {
5128                None => {
5129                    Some(format!("no persisted fuzz failure found at {}", failure_file.display()))
5130                }
5131                Some(failure)
5132                    if failure
5133                        .calldata
5134                        .get(..4)
5135                        .is_none_or(|selector| func.selector() != selector) =>
5136                {
5137                    Some(format!("persisted fuzz failure selector does not match {}", func.name))
5138                }
5139                Some(failure) => {
5140                    warn_fork_block_mismatch(
5141                        &func.name,
5142                        failure.fork_block_number,
5143                        fork_block_number,
5144                    );
5145                    None
5146                }
5147            };
5148            if let Some(reason) = skip_reason {
5149                self.result.fuzz_result(FuzzTestResult {
5150                    skipped: true,
5151                    reason: Some(reason),
5152                    ..Default::default()
5153                });
5154                return self.result;
5155            }
5156            fuzz_config.corpus.corpus_dir = None;
5157        }
5158
5159        self.try_seed_fuzz_corpus_from_frontiers(func, &fuzz_config);
5160        self.try_seed_fuzz_corpus_symbolically(func, &fuzz_config);
5161
5162        let progress = self.fuzz_progress(
5163            &func.name,
5164            fuzz_config.timeout,
5165            if fuzz_config.run.is_some() { 1 } else { fuzz_config.runs },
5166        );
5167
5168        let state = self.build_fuzz_state(false, Some(func));
5169        let mut executor = self.executor.into_owned();
5170        // Enable edge coverage if running with coverage guided fuzzing or with edge coverage
5171        // metrics (useful for benchmarking the fuzzer).
5172        executor.inspector_mut().collect_edge_coverage_with_config(&fuzz_config.corpus);
5173        executor.inspector_mut().collect_evm_cmp_log(fuzz_config.corpus.collect_evm_cmp_log());
5174        executor.inspector_mut().collect_sancov_edges(fuzz_config.corpus.collect_sancov_edges());
5175        executor
5176            .inspector_mut()
5177            .collect_sancov_trace_cmp(fuzz_config.corpus.collect_sancov_trace_cmp());
5178        let mut fuzzed_executor = FuzzedExecutor::new(
5179            executor,
5180            runner,
5181            self.tcfg.sender,
5182            fuzz_config,
5183            persisted_failure,
5184            legacy_corpus_dir,
5185        );
5186        let result = if self.cr.mcr.tcfg.fuzz_failure_replay {
5187            fuzzed_executor.replay_persisted_failure(
5188                func,
5189                self.address,
5190                &self.cr.mcr.revert_decoder,
5191            )
5192        } else {
5193            fuzzed_executor.fuzz(
5194                func,
5195                &self.setup.fuzz_fixtures,
5196                state,
5197                self.address,
5198                &self.cr.mcr.revert_decoder,
5199                progress.as_ref(),
5200                &self.tcfg.early_exit,
5201                &self.cr.tokio_handle,
5202            )
5203        };
5204        let result = match result {
5205            Ok(result) => result,
5206            Err(e) => {
5207                self.result.fuzz_setup_fail(e);
5208                return self.result;
5209            }
5210        };
5211
5212        // Record counterexample.
5213        if !self.cr.mcr.tcfg.fuzz_failure_replay
5214            && let Some(CounterExample::Single(counterexample)) = &result.counterexample
5215        {
5216            let counterexample = BaseCounterExample { fork_block_number, ..counterexample.clone() };
5217            if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) {
5218                error!(%err, "Failed to create fuzz failure dir");
5219            } else if let Err(err) =
5220                foundry_common::fs::write_json_file(&failure_file, &counterexample)
5221            {
5222                error!(%err, "Failed to record call sequence");
5223            }
5224        }
5225
5226        self.result.fuzz_result(result);
5227        self.result
5228    }
5229
5230    fn prepare_test(&mut self, func: &Function) -> Result<(), ()> {
5231        let address = self.setup.address;
5232
5233        // Apply before test configured functions (if any).
5234        if self.cr.contract.abi.functions().any(|func| func.name.is_before_test_setup()) {
5235            for calldata in self.executor.call_sol_default(
5236                address,
5237                &ITest::beforeTestSetupCall { testSelector: func.selector() },
5238            ) {
5239                let spec_id: SpecId = self.executor.spec_id().into();
5240                debug!(?calldata, spec=%spec_id, "applying before_test_setup");
5241                // Apply before test configured calldata.
5242                let Ok(call_result) = self.executor.to_mut().transact_raw(
5243                    self.tcfg.sender,
5244                    address,
5245                    calldata,
5246                    U256::ZERO,
5247                ) else {
5248                    self.result.single_fail(None);
5249                    return Err(());
5250                };
5251                let reverted = call_result.reverted;
5252                // Merge tx result traces in unit test result.
5253                self.result.extend_setup(call_result);
5254                // To continue unit test execution the call should not revert.
5255                if reverted {
5256                    self.result.single_fail(None);
5257                    return Err(());
5258                }
5259            }
5260        }
5261        Ok(())
5262    }
5263
5264    fn fuzz_runner(&self) -> TestRunner {
5265        let config = &self.config.fuzz;
5266        fuzzer_with_cases(config.seed, config.runs, config.max_test_rejects)
5267    }
5268
5269    /// Replays the persisted corpus and writes AFL-`afl-showmap`-style files.
5270    fn run_showmap(
5271        mut self,
5272        func: &Function,
5273        test_name: &str,
5274        corpus_dir: Option<PathBuf>,
5275        showmap: &crate::multi_runner::ShowmapConfig,
5276        target: ShowmapReplayTarget<'_>,
5277    ) -> TestResult {
5278        let Some(corpus_dir) = corpus_dir else {
5279            self.result.replay_skip("no corpus_dir configured for this test");
5280            return self.result;
5281        };
5282
5283        // Configure executor with the requested coverage collectors. Showmap
5284        // ignores fuzz config defaults: the CLI domain is the source of truth.
5285        // For EVM we enable line coverage rather than edge coverage so the IDs
5286        // (bytecode_hash, pc) are deterministic across forge processes —
5287        // `EdgeCovInspector` uses a per-process random hash and would yield
5288        // non-comparable IDs across approaches.
5289        let mut executor = self.clone_executor();
5290        let domain = showmap.domain;
5291        executor.inspector_mut().collect_line_coverage(domain.includes_evm());
5292        executor.inspector_mut().collect_sancov_edges(domain.includes_sancov());
5293
5294        // Fold test identity into the approach dir so each `<approach>/` contains
5295        // trials of a single test — what `differential-coverage` expects. The
5296        // (anchor) function name is included for invariant tests too so contracts
5297        // with multiple invariant campaigns don't collide on the same approach dir
5298        // (which `File::create_new` would reject). Distinct anchors sharing one
5299        // corpus simply produce equivalent, separately-named approach dirs.
5300        let safe_id = self.cr.name.replace(['/', '\\', ':'], "_");
5301        let safe_fn = test_name.replace(['/', '\\', ':', '(', ')', ',', ' '], "_");
5302        let approach = format!("{}__{safe_id}__{safe_fn}", showmap.approach);
5303        let opts = ShowmapOpts {
5304            out_dir: showmap.out_dir.clone(),
5305            approach,
5306            trial: showmap.trial.clone(),
5307            per_input: showmap.per_input,
5308            domain,
5309            emit_files: showmap.emit_files,
5310        };
5311
5312        let start = std::time::Instant::now();
5313        let result = replay_corpus_to_showmap(&executor, &corpus_dir, target, &opts);
5314        let duration = start.elapsed();
5315        match result {
5316            Ok(stats) => {
5317                if stats.sancov_requested && !stats.sancov_observed && stats.corpus_entries > 0 {
5318                    let _ = sh_warn!(
5319                        "{}::{}: sancov coverage requested but no hits observed (build is likely not sancov-instrumented)",
5320                        self.cr.name,
5321                        func.name,
5322                    );
5323                }
5324                if stats.unreadable_entries > 0 {
5325                    self.result.single_fail(Some(format!(
5326                        "failed to read {} corpus entries from {}",
5327                        stats.unreadable_entries,
5328                        corpus_dir.display()
5329                    )));
5330                } else if !showmap.emit_files && stats.corpus_entries == 0 {
5331                    self.result.replay_skip(format!(
5332                        "replayed 0 corpus entries from {}",
5333                        corpus_dir.display()
5334                    ));
5335                } else {
5336                    self.result.replay_result(
5337                        stats.corpus_entries,
5338                        stats.showmap_files,
5339                        stats.skipped_entries,
5340                        duration,
5341                    );
5342                }
5343            }
5344            Err(e) => {
5345                self.result.single_fail(Some(e.to_string()));
5346            }
5347        }
5348        self.result
5349    }
5350
5351    fn invariant_runner(&self) -> TestRunner {
5352        let config = &self.config.invariant;
5353        fuzzer_with_cases(self.config.fuzz.seed, config.runs, config.max_assume_rejects)
5354    }
5355
5356    fn clone_executor(&self) -> Executor<FEN> {
5357        self.executor.clone().into_owned()
5358    }
5359
5360    fn clone_executor_with_symbolic_storage(
5361        &self,
5362        storage: &[SymbolicStorageAssignment],
5363    ) -> Result<Executor<FEN>> {
5364        let mut executor = self.clone_executor();
5365        for assignment in storage {
5366            executor.set_storage_slot(assignment.address, assignment.slot, assignment.value)?;
5367            if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut() {
5368                cheats.cache_arbitrary_storage_value(
5369                    assignment.address,
5370                    assignment.slot,
5371                    assignment.value,
5372                );
5373            }
5374        }
5375        Ok(executor)
5376    }
5377
5378    fn build_fuzz_state(&self, invariant: bool, func: Option<&Function>) -> EvmFuzzState {
5379        let config =
5380            if invariant { self.config.invariant.dictionary } else { self.config.fuzz.dictionary };
5381        let has_function_inline_config =
5382            func.is_some_and(|func| self.inline_config.contains_function(self.cr.name, &func.name));
5383        let can_reuse_setup_state = !invariant
5384            && config == self.cr.config.fuzz.dictionary
5385            && !has_function_inline_config
5386            && !self.cr.contract.abi.functions().any(|func| func.name.is_before_test_setup());
5387        if can_reuse_setup_state {
5388            return self
5389                .setup
5390                .fuzz_state
5391                .get_or_init(|| self.build_fuzz_state_uncached(false, config))
5392                .fork();
5393        }
5394
5395        self.build_fuzz_state_uncached(invariant, config)
5396    }
5397
5398    fn build_fuzz_state_uncached(
5399        &self,
5400        invariant: bool,
5401        config: FuzzDictionaryConfig,
5402    ) -> EvmFuzzState {
5403        let literals =
5404            if invariant { &self.cr.mcr.invariant_literals } else { &self.cr.mcr.fuzz_literals };
5405        if let Some(db) = self.executor.backend().active_fork_db() {
5406            EvmFuzzState::new(&self.setup.deployed_libs, db, config, Some(literals))
5407        } else {
5408            let db = self.executor.backend().mem_db();
5409            EvmFuzzState::new(&self.setup.deployed_libs, db, config, Some(literals))
5410        }
5411    }
5412}
5413
5414fn fuzzer_with_cases(seed: Option<U256>, cases: u32, max_global_rejects: u32) -> TestRunner {
5415    let config = proptest::test_runner::Config {
5416        cases,
5417        max_global_rejects,
5418        // Disable proptest shrink: for fuzz tests we provide single counterexample,
5419        // for invariant tests we shrink outside proptest.
5420        max_shrink_iters: 0,
5421        ..Default::default()
5422    };
5423
5424    if let Some(seed) = seed {
5425        trace!(target: "forge::test", %seed, "building deterministic fuzzer");
5426        let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
5427        TestRunner::new_with_rng(config, rng)
5428    } else {
5429        trace!(target: "forge::test", "building stochastic fuzzer");
5430        TestRunner::new(config)
5431    }
5432}
5433
5434/// Holds data about a persisted invariant failure.
5435#[derive(Serialize, Deserialize)]
5436struct InvariantPersistedFailure {
5437    /// Recorded counterexample.
5438    call_sequence: Vec<BaseCounterExample>,
5439    /// Invariant settings when the counterexample was generated.
5440    /// Used to determine if the counterexample is still valid.
5441    settings: InvariantSettings,
5442    /// Whether the persisted failure came from a handler assertion instead of the invariant body.
5443    #[serde(default)]
5444    assertion_failure: bool,
5445    /// Concrete setup-storage assignments required before replaying this failure.
5446    #[serde(default, skip_serializing_if = "Vec::is_empty")]
5447    storage: Vec<SymbolicStorageAssignment>,
5448    /// Exact failure site required to accept a persisted symbolic handler rerun.
5449    #[serde(default, skip_serializing_if = "Option::is_none")]
5450    failure_site: Option<SymbolicInvariantFailureSite>,
5451    /// Versioned configuration used to produce a reproducible edge fingerprint.
5452    #[serde(default, skip_serializing_if = "Option::is_none")]
5453    fingerprint_provenance: Option<PersistedFingerprintProvenance>,
5454    /// Active fork block after setup when the counterexample was recorded.
5455    #[serde(default, skip_serializing_if = "Option::is_none")]
5456    fork_block_number: Option<u64>,
5457}
5458
5459/// Reproducible edge-fingerprint algorithms and their capture configuration.
5460#[derive(Clone, Copy, Serialize, Deserialize)]
5461#[serde(tag = "algorithm", rename_all = "snake_case")]
5462enum PersistedFingerprintProvenance {
5463    CollisionFreeV1 { include_call_depth: bool },
5464    HashV1 { include_call_depth: bool },
5465}
5466
5467impl PersistedFingerprintProvenance {
5468    fn from_corpus(config: &FuzzCorpusConfig) -> Option<Self> {
5469        config.collect_evm_edge_coverage().then(|| {
5470            let include_call_depth = config.evm_edge_coverage_include_call_depth();
5471            if config.evm_edge_coverage_collision_free() {
5472                Self::CollisionFreeV1 { include_call_depth }
5473            } else {
5474                Self::HashV1 { include_call_depth }
5475            }
5476        })
5477    }
5478
5479    const fn edge_config(self) -> EdgeCovConfig {
5480        match self {
5481            Self::CollisionFreeV1 { include_call_depth } => {
5482                EdgeCovConfig::new(EdgeCovKind::CollisionFree, include_call_depth)
5483            }
5484            Self::HashV1 { include_call_depth } => {
5485                EdgeCovConfig::new(EdgeCovKind::Hash, include_call_depth)
5486            }
5487        }
5488    }
5489}
5490
5491/// Persisted handler-side assertion bugs keyed by `(reverter, selector)`.
5492type HandlerFailureMap = std::collections::HashMap<(Address, Selector), InvariantFuzzError>;
5493/// Symbolic replay storage for handler bugs keyed by `(reverter, selector, fingerprint)`.
5494type SymbolicHandlerStorageMap = HashMap<(Address, Selector, B256), SymbolicHandlerReplayStorage>;
5495
5496/// Symbolic storage assignments that only apply when replaying the exact recorded sequence.
5497struct SymbolicHandlerReplayStorage {
5498    call_sequence: Vec<BasicTxDetails>,
5499    assignments: Vec<SymbolicStorageAssignment>,
5500    fingerprint_provenance: Option<PersistedFingerprintProvenance>,
5501}
5502
5503/// Helper function to load failed call sequence from file.
5504/// Ignores failure if generated with different invariant settings than the current ones.
5505fn persisted_call_sequence(
5506    path: &Path,
5507    current_settings: &InvariantSettings,
5508) -> Option<InvariantPersistedFailure> {
5509    let persisted = foundry_common::fs::read_json_file::<InvariantPersistedFailure>(path).ok()?;
5510    if let Some(diff) = persisted.settings.diff(current_settings) {
5511        let _ = sh_warn!(
5512            "Failure from {path:?} file was ignored because invariant test settings have changed: {diff}"
5513        );
5514        return None;
5515    }
5516    Some(persisted)
5517}
5518
5519/// Warns when a persisted failure is replayed on a different fork block than it was recorded on.
5520/// Returns whether the blocks differ.
5521fn warn_fork_block_mismatch(
5522    label: impl std::fmt::Display,
5523    recorded: Option<u64>,
5524    current: Option<u64>,
5525) -> bool {
5526    let Some(recorded) = recorded else { return false };
5527    if current == Some(recorded) {
5528        return false;
5529    }
5530    let current = current.map_or_else(|| "without a fork".to_string(), |b| format!("at block {b}"));
5531    let _ = sh_warn!(
5532        "Persisted failure for {label} was recorded at fork block {recorded} but is replayed \
5533         {current}; pin the fork block (e.g. `--fork-block-number {recorded}` or a block in \
5534         `vm.createSelectFork`) to replay it against the recorded state."
5535    );
5536    true
5537}
5538
5539/// Returns the current invariant failure cache path.
5540fn invariant_failure_file(failure_dir: &Path, invariant: &Function) -> PathBuf {
5541    canonicalized(failure_dir.join("invariants").join(&invariant.name))
5542}
5543
5544/// Loads a persisted invariant failure from the new cache path, falling back to the legacy path.
5545fn persisted_invariant_failure(
5546    failure_dir: &Path,
5547    invariant: &Function,
5548    current_settings: &InvariantSettings,
5549) -> Option<InvariantPersistedFailure> {
5550    persisted_call_sequence(&invariant_failure_file(failure_dir, invariant), current_settings)
5551        .or_else(|| {
5552            // Older Foundry versions stored invariant failures directly under the failure root.
5553            let legacy_path = canonicalized(failure_dir.join(&invariant.name));
5554            let persisted = persisted_call_sequence(&legacy_path, current_settings)?;
5555            let _ = sh_warn!(
5556                "Using legacy invariant failure cache at {}; new failures will be persisted under {}/invariants.",
5557                legacy_path.display(),
5558                failure_dir.display(),
5559            );
5560            Some(persisted)
5561        })
5562}
5563
5564/// Converts a persisted counterexample to `BasicTxDetails`, setting `show_solidity` in place.
5565fn base_counterexamples_to_txes(
5566    call_sequence: &mut [BaseCounterExample],
5567    show_solidity: bool,
5568) -> Vec<BasicTxDetails> {
5569    call_sequence
5570        .iter_mut()
5571        .map(|seq| {
5572            seq.show_solidity = show_solidity;
5573            base_counterexample_to_tx(seq)
5574        })
5575        .collect()
5576}
5577
5578/// Converts campaign transactions into displayable counterexample calls.
5579fn base_counterexamples(
5580    calls: &[BasicTxDetails],
5581    identified_contracts: &ContractsByAddress,
5582    show_solidity: bool,
5583) -> Vec<BaseCounterExample> {
5584    calls
5585        .iter()
5586        .map(|tx| {
5587            BaseCounterExample::from_invariant_call(tx, identified_contracts, None, show_solidity)
5588        })
5589        .collect()
5590}
5591
5592/// Returns the failing call sequence of a replayable invariant error.
5593fn failed_invariant_calls(error: &InvariantFuzzError) -> Option<&[BasicTxDetails]> {
5594    match error {
5595        InvariantFuzzError::BrokenInvariant(case_data) | InvariantFuzzError::Revert(case_data) => {
5596            let TestError::Fail(_, calls) = &case_data.test_error else {
5597                unreachable!("FailedInvariantCaseData::new always sets TestError::Fail")
5598            };
5599            Some(calls)
5600        }
5601        _ => None,
5602    }
5603}
5604
5605fn base_counterexample_to_tx(seq: &BaseCounterExample) -> BasicTxDetails {
5606    BasicTxDetails {
5607        warp: seq.warp,
5608        roll: seq.roll,
5609        sender: seq.sender.unwrap_or_default(),
5610        call_details: CallDetails {
5611            target: seq.addr.unwrap_or_default(),
5612            calldata: seq.calldata.clone(),
5613            value: seq.value,
5614        },
5615    }
5616}
5617
5618fn symbolic_invariant_counterexample_calls(
5619    steps: &[SymbolicInvariantStep],
5620    identified_contracts: &ContractsByAddress,
5621    show_solidity: bool,
5622) -> Vec<SymbolicCounterexampleCall> {
5623    steps
5624        .iter()
5625        .map(|step| {
5626            let tx = BasicTxDetails {
5627                warp: None,
5628                roll: None,
5629                sender: step.sender,
5630                call_details: CallDetails {
5631                    target: step.address,
5632                    calldata: step.calldata.clone(),
5633                    value: None,
5634                },
5635            };
5636            let counterexample = BaseCounterExample::from_invariant_call(
5637                &tx,
5638                identified_contracts,
5639                None,
5640                show_solidity,
5641            );
5642            SymbolicCounterexampleCall::from_base_counterexample(
5643                &counterexample,
5644                step.sender,
5645                step.address,
5646            )
5647        })
5648        .collect()
5649}
5650
5651fn frontier_selector(frontier: &FuzzBranchFrontierRecord) -> Option<Selector> {
5652    frontier
5653        .sequence
5654        .get(frontier.call_index)
5655        .and_then(|call| call.call_details.calldata.get(..4))
5656        .map(Selector::from_slice)
5657}
5658
5659fn parse_frontier_selectors(selectors: &[String], signature: &str) -> Vec<Selector> {
5660    selectors
5661        .iter()
5662        .filter_map(|selector| {
5663            let parsed = hex::decode(selector)
5664                .ok()
5665                .filter(|bytes| bytes.len() == 4)
5666                .map(|bytes| Selector::from_slice(&bytes));
5667            if parsed.is_none() {
5668                let _ = sh_warn!(
5669                    "invalid symbolic frontier selector `{selector}` for {signature}; expected \
5670                     a 4-byte hex selector like 0x12345678"
5671                );
5672            }
5673            parsed
5674        })
5675        .collect()
5676}
5677
5678/// Warns about requested frontier `label`s that the frontier file at `path` did not provide.
5679fn warn_unimported_frontiers<T: std::fmt::Display + PartialEq>(
5680    label: &str,
5681    requested: &[T],
5682    imported: &[T],
5683    signature: &str,
5684    path: &Path,
5685) {
5686    for value in requested.iter().filter(|value| !imported.contains(value)) {
5687        warn!(
5688            %value,
5689            label,
5690            test = %signature,
5691            path = %path.display(),
5692            "requested fuzz branch frontier was not imported"
5693        );
5694        let _ = sh_warn!(
5695            "requested fuzz branch frontier {label} {value} was not imported for {signature}"
5696        );
5697    }
5698}
5699
5700fn frontier_filter_display<T: std::fmt::Display>(values: &[T]) -> String {
5701    if values.is_empty() { "any".to_string() } else { values.iter().format(", ").to_string() }
5702}
5703
5704/// Returns the contract name without the file path prefix.
5705fn contract_short_name(contract_name: &str) -> &str {
5706    contract_name.split(':').next_back().unwrap()
5707}
5708
5709/// Returns a stable path component that distinguishes overloaded fuzz tests.
5710fn fuzz_test_path_name<'a>(
5711    abi: &JsonAbi,
5712    func: &'a Function,
5713    config: &FuzzConfig,
5714    contract_name: &str,
5715) -> Cow<'a, str> {
5716    let test_name = format!("{}-{:x}", func.name, func.selector());
5717    let overloaded = abi.functions.get(&func.name).is_some_and(|functions| functions.len() > 1);
5718    let contract = contract_short_name(contract_name);
5719    let has_qualified_artifact = config
5720        .failure_persist_dir
5721        .as_ref()
5722        .is_some_and(|dir| dir.join("failures").join(contract).join(&test_name).exists())
5723        || [&config.corpus.corpus_dir, &config.corpus.frontier_dir]
5724            .into_iter()
5725            .flatten()
5726            .any(|dir| dir.join(contract).join(&test_name).exists());
5727
5728    if overloaded || has_qualified_artifact {
5729        Cow::Owned(test_name)
5730    } else {
5731        Cow::Borrowed(&func.name)
5732    }
5733}
5734
5735/// Returns whether any canonical replay directory under `dir` holds a corpus entry.
5736fn corpus_has_entries(dir: &Path) -> bool {
5737    canonical_replay_dirs(dir).iter().any(|dir| read_corpus_dir(dir).next().is_some())
5738}
5739
5740/// Returns the legacy unqualified corpus when the qualified corpus has no entries.
5741fn legacy_fuzz_corpus_dir(
5742    root: Option<&Path>,
5743    contract_name: &str,
5744    func: &Function,
5745    test_name: &str,
5746) -> Option<PathBuf> {
5747    if test_name == func.name {
5748        return None;
5749    }
5750    let contract = root?.join(contract_short_name(contract_name));
5751    if corpus_has_entries(&contract.join(test_name)) {
5752        return None;
5753    }
5754    let legacy = contract.join(&func.name);
5755    corpus_has_entries(&legacy).then(|| canonicalized(legacy))
5756}
5757
5758/// Helper function to set test corpus dir and to compose persisted failure paths.
5759fn test_paths(
5760    corpus_config: &mut FuzzCorpusConfig,
5761    persist_dir: PathBuf,
5762    contract_name: &str,
5763    test_name: &str,
5764) -> (PathBuf, PathBuf) {
5765    let contract = contract_short_name(contract_name);
5766    // Update config with corpus dir for current test.
5767    corpus_config.with_test(contract, test_name);
5768
5769    let failures_dir = canonicalized(persist_dir.join("failures").join(contract));
5770    let failure_file = canonicalized(failures_dir.join(test_name));
5771    (failures_dir, failure_file)
5772}
5773
5774/// Returns the corpus directory of a shared contract campaign or an isolated campaign.
5775fn invariant_corpus_dir(
5776    root: &Path,
5777    contract_name: &str,
5778    isolated_campaign: Option<&str>,
5779) -> PathBuf {
5780    let dir = root.join(contract_short_name(contract_name));
5781    if let Some(name) = isolated_campaign { dir.join(name) } else { dir }
5782}
5783
5784/// Returns the collision-free directory for one stateful frontier campaign.
5785fn invariant_frontier_dir(
5786    root: &Path,
5787    contract_name: &str,
5788    isolated_campaign: Option<&str>,
5789    execution_profile: &str,
5790    execution_pass: &str,
5791) -> PathBuf {
5792    let contract = stable_hashed_component(contract_short_name(contract_name), contract_name);
5793    let campaign = if let Some(name) = isolated_campaign {
5794        PathBuf::from("isolated").join(sanitize_symbolic_artifact_component(name))
5795    } else {
5796        PathBuf::from("shared")
5797    };
5798    root.join("v2").join(contract).join(execution_profile).join(execution_pass).join(campaign)
5799}
5800
5801/// Sets the invariant corpus directory and returns the contract-level failure directory.
5802fn invariant_suite_paths(
5803    corpus_config: &mut FuzzCorpusConfig,
5804    persist_dir: PathBuf,
5805    contract_name: &str,
5806    isolated_campaign: Option<&str>,
5807    execution_profile: &str,
5808    execution_pass: &str,
5809) -> PathBuf {
5810    if let Some(root) = &corpus_config.corpus_dir {
5811        corpus_config.corpus_dir =
5812            Some(canonicalized(invariant_corpus_dir(root, contract_name, isolated_campaign)));
5813    }
5814    if let Some(root) = &corpus_config.frontier_dir {
5815        corpus_config.frontier_dir = Some(canonicalized(invariant_frontier_dir(
5816            root,
5817            contract_name,
5818            isolated_campaign,
5819            execution_profile,
5820            execution_pass,
5821        )));
5822    }
5823    canonicalized(persist_dir.join("failures").join(contract_short_name(contract_name)))
5824}
5825
5826/// Narrows a generated corpus root to the per-test directory when it exists.
5827fn narrow_generated_corpus_root(corpus_dir: PathBuf, target_dir: PathBuf) -> PathBuf {
5828    let target_is_dir =
5829        std::fs::symlink_metadata(&target_dir).is_ok_and(|metadata| metadata.file_type().is_dir());
5830    if target_is_dir { canonicalized(target_dir) } else { corpus_dir }
5831}
5832
5833fn sanitize_symbolic_artifact_component(value: &str) -> String {
5834    let sanitized = value
5835        .chars()
5836        .map(|ch| if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' { ch } else { '_' })
5837        .collect::<String>();
5838    if sanitized.is_empty() { "_".to_string() } else { sanitized }
5839}
5840
5841fn stable_hashed_component(label: &str, identity: &str) -> String {
5842    let hash = keccak256(identity.as_bytes());
5843    let hash = hex::encode(&hash[..16]);
5844    format!("{}-{hash}", sanitize_symbolic_artifact_component(label))
5845}
5846
5847fn symbolic_artifact_file_name(
5848    contract_id: &str,
5849    value: &str,
5850    kind: SymbolicCounterexampleArtifactKind,
5851) -> String {
5852    let identity = format!("{contract_id}\0{value}\0{kind:?}");
5853    format!("{}.json", stable_hashed_component(value, &identity))
5854}
5855
5856/// Persists an invariant failure, with any symbolic replay storage and confirmed failure site.
5857#[expect(clippy::too_many_arguments)]
5858fn record_invariant_failure(
5859    failure_file: &Path,
5860    call_sequence: &[BaseCounterExample],
5861    settings: &InvariantSettings,
5862    assertion_failure: bool,
5863    storage: &[SymbolicStorageAssignment],
5864    failure_site: Option<SymbolicInvariantFailureSite>,
5865    fingerprint_provenance: Option<PersistedFingerprintProvenance>,
5866    fork_block_number: Option<u64>,
5867) {
5868    if let Some(parent) = failure_file.parent()
5869        && let Err(err) = foundry_common::fs::create_dir_all(parent)
5870    {
5871        error!(%err, "Failed to create invariant failure file parent dir");
5872        return;
5873    }
5874
5875    if let Err(err) = foundry_common::fs::write_json_file(
5876        failure_file,
5877        &InvariantPersistedFailure {
5878            call_sequence: call_sequence.to_owned(),
5879            settings: settings.clone(),
5880            assertion_failure,
5881            storage: storage.to_vec(),
5882            failure_site,
5883            fingerprint_provenance,
5884            fork_block_number,
5885        },
5886    ) {
5887        error!(%err, "Failed to record call sequence");
5888    }
5889}
5890
5891/// Persists a handler-side assertion bug with symbolic replay storage.
5892#[expect(clippy::too_many_arguments)]
5893fn record_handler_failure(
5894    failure_dir: &Path,
5895    reverter: Address,
5896    selector: Selector,
5897    fingerprint: B256,
5898    call_sequence: &[BaseCounterExample],
5899    settings: &InvariantSettings,
5900    storage: &[SymbolicStorageAssignment],
5901    fingerprint_provenance: Option<PersistedFingerprintProvenance>,
5902    fork_block_number: Option<u64>,
5903) {
5904    let handlers_dir = failure_dir.join("handlers");
5905    let file = handler_failure_file(&handlers_dir, reverter, selector);
5906    record_invariant_failure(
5907        &file,
5908        call_sequence,
5909        settings,
5910        true,
5911        storage,
5912        Some(SymbolicInvariantFailureSite::SequenceCall {
5913            target: reverter,
5914            selector,
5915            fingerprint,
5916        }),
5917        fingerprint_provenance,
5918        fork_block_number,
5919    );
5920}
5921
5922fn handler_failure_file(handlers_dir: &Path, reverter: Address, selector: Selector) -> PathBuf {
5923    let mut buf = [0u8; 24];
5924    buf[..20].copy_from_slice(reverter.as_slice());
5925    buf[20..].copy_from_slice(selector.as_slice());
5926    handlers_dir.join(format!("{:x}.json", keccak256(buf)))
5927}
5928
5929fn invariant_handler_failure_name(
5930    identified_contracts: &ContractsByAddress,
5931    reverter: Address,
5932    selector: Selector,
5933) -> String {
5934    identified_contracts
5935        .get(&reverter)
5936        .and_then(|(contract_name, abi)| {
5937            abi.functions()
5938                .find(|f| f.selector() == selector)
5939                .map(|f| format!("{contract_name}::{}", f.name))
5940        })
5941        .unwrap_or_else(|| format!("{reverter}::{selector}"))
5942}
5943
5944fn should_symbolically_import_fuzz_corpus(config: &Config, func: &Function) -> bool {
5945    config.symbolic.use_fuzz_corpus && func.test_function_kind().is_fuzz_test()
5946}
5947
5948pub(crate) fn effective_test_function_kind(
5949    kind: TestFunctionKind,
5950    config: &Config,
5951    func: &Function,
5952) -> TestFunctionKind {
5953    if should_symbolically_import_fuzz_corpus(config, func) {
5954        TestFunctionKind::SymbolicTest
5955    } else {
5956        kind
5957    }
5958}
5959
5960fn symbolic_invariant_unsupported_domain_reason(
5961    invariant_config: &InvariantConfig,
5962    sender_filters: &SenderFilters,
5963    targets: &FuzzRunIdentifiedContracts,
5964    symbolic_targets: &[SymbolicInvariantTarget],
5965) -> Option<&'static str> {
5966    if sender_filters.targeted.is_empty() {
5967        return Some("symbolic invariant execution requires explicit target senders");
5968    }
5969    if invariant_config.has_delay() {
5970        return Some("symbolic invariant execution does not model warp/roll delays");
5971    }
5972    if invariant_config.call_override {
5973        return Some("symbolic invariant execution does not model call override targets");
5974    }
5975    if targets.is_updatable {
5976        return Some("symbolic invariant execution does not model dynamically updatable targets");
5977    }
5978    if invariant_config.corpus.payable_value_weight > 0
5979        && symbolic_targets
5980            .iter()
5981            .any(|target| target.function.state_mutability == StateMutability::Payable)
5982    {
5983        return Some("symbolic invariant execution does not model payable call values");
5984    }
5985    None
5986}
5987
5988/// Replays one corpus-minimization candidate and records its coverage observation.
5989fn replay_fuzz_minimize<FEN: FoundryEvmNetwork>(
5990    result: &mut TestResult,
5991    minimize: &FuzzMinimizeConfig,
5992    target: String,
5993    executor: &Executor<FEN>,
5994    corpus: &FuzzCorpusConfig,
5995    replay_target: ShowmapReplayTarget<'_>,
5996) {
5997    let Ok(mut evm_edge_indices_by_target) = minimize.evm_edge_indices.lock() else {
5998        result.single_fail(Some("minimize edge index lock poisoned".to_string()));
5999        return;
6000    };
6001    let evm_edge_indices = evm_edge_indices_by_target
6002        .entry(target.clone())
6003        .or_insert_with(|| Arc::new(Mutex::new(Default::default())))
6004        .clone();
6005    drop(evm_edge_indices_by_target);
6006    let Ok(mut evm_edge_indices) = evm_edge_indices.lock() else {
6007        result.single_fail(Some("minimize edge index lock poisoned".to_string()));
6008        return;
6009    };
6010    match replay_sequence_for_minimization(
6011        executor,
6012        MinimizationReplayInput {
6013            sequence: minimize.input.as_ref(),
6014            evm_edge_indices: &mut evm_edge_indices,
6015            corpus,
6016            stop_at_campaign_end: matches!(minimize.mode, FuzzMinimizeMode::Tmin),
6017        },
6018        replay_target,
6019    ) {
6020        Ok(observation) => {
6021            let replayed = observation.replayed;
6022            let skipped = observation.skipped + observation.unmatched;
6023            let Ok(mut observations) = minimize.observations.lock() else {
6024                result.single_fail(Some("minimize observations lock poisoned".to_string()));
6025                return;
6026            };
6027            observations.push(FuzzMinimizeObservation { target, observation });
6028            result.replay_result(replayed, 0, skipped, std::time::Duration::ZERO);
6029        }
6030        Err(e) => result.single_fail(Some(e.to_string())),
6031    }
6032}
6033
6034#[cfg(test)]
6035mod tests {
6036    use super::*;
6037    use foundry_common::EmptyTestFilter;
6038    use foundry_config::NatSpec;
6039
6040    const CONTRACT_NAME: &str = "src/Test.t.sol:InvariantTest";
6041
6042    fn stateful_frontier_record(
6043        id: u64,
6044        sequence_index: usize,
6045        call_index: usize,
6046        both_results_retained: bool,
6047    ) -> FuzzBranchFrontierRecord {
6048        FuzzBranchFrontierRecord {
6049            id,
6050            both_results_retained,
6051            call_index,
6052            sequence: Vec::new(),
6053            sequence_index: Some(sequence_index),
6054            site: FuzzBranchFrontierSite {
6055                address: Address::ZERO,
6056                pc: id as usize,
6057                opcode: opcode::EQ,
6058            },
6059            operands: FuzzBranchFrontierOperands { result: false },
6060        }
6061    }
6062
6063    #[test]
6064    fn symbolic_artifact_file_name_hashes_full_identity() {
6065        let single = symbolic_artifact_file_name(
6066            "src/A.t.sol:Contract",
6067            "test_collision()",
6068            SymbolicCounterexampleArtifactKind::SingleCall,
6069        );
6070        let same_file_component_different_contract = symbolic_artifact_file_name(
6071            "src/B.t.sol:Contract",
6072            "test_collision()",
6073            SymbolicCounterexampleArtifactKind::SingleCall,
6074        );
6075        let same_contract_different_kind = symbolic_artifact_file_name(
6076            "src/A.t.sol:Contract",
6077            "test_collision()",
6078            SymbolicCounterexampleArtifactKind::Sequence,
6079        );
6080
6081        assert_ne!(single, same_file_component_different_contract);
6082        assert_ne!(single, same_contract_different_kind);
6083
6084        let hash = single
6085            .strip_prefix("test_collision__-")
6086            .and_then(|value| value.strip_suffix(".json"))
6087            .expect("file name should include sanitized value prefix and json suffix");
6088        assert_eq!(hash.len(), 32);
6089    }
6090
6091    #[test]
6092    fn stateful_frontier_paths_include_artifact_pass_and_campaign() {
6093        let root = Path::new("/tmp/frontiers");
6094        let first =
6095            invariant_frontier_dir(root, "src/a/Same.t.sol:Same", None, "ethereum", "single");
6096        let other_artifact =
6097            invariant_frontier_dir(root, "src/b/Same.t.sol:Same", None, "ethereum", "single");
6098        let other_profile =
6099            invariant_frontier_dir(root, "src/a/Same.t.sol:Same", None, "tempo", "override");
6100        let default_pass =
6101            invariant_frontier_dir(root, "src/a/Same.t.sol:Same", None, "ethereum", "default");
6102        let override_pass =
6103            invariant_frontier_dir(root, "src/a/Same.t.sol:Same", None, "ethereum", "override");
6104        let isolated = invariant_frontier_dir(
6105            root,
6106            "src/a/Same.t.sol:Same",
6107            Some("invariant_one"),
6108            "ethereum",
6109            "single",
6110        );
6111
6112        assert_ne!(first, other_artifact);
6113        assert_ne!(first, other_profile);
6114        assert_ne!(default_pass, override_pass);
6115        assert_ne!(first, isolated);
6116        assert!(first.ends_with("ethereum/single/shared"));
6117        assert!(isolated.ends_with("ethereum/single/isolated/invariant_one"));
6118
6119        let mut corpus = FuzzCorpusConfig {
6120            corpus_dir: Some(PathBuf::from("/tmp/corpus")),
6121            frontier_dir: Some(root.to_path_buf()),
6122            ..Default::default()
6123        };
6124        let failures = invariant_suite_paths(
6125            &mut corpus,
6126            PathBuf::from("/tmp/persist"),
6127            "src/a/Same.t.sol:Same",
6128            Some("invariant_one"),
6129            "ethereum",
6130            "single",
6131        );
6132        assert_eq!(
6133            corpus.corpus_dir,
6134            Some(canonicalized(PathBuf::from("/tmp/corpus/Same/invariant_one")))
6135        );
6136        assert_eq!(corpus.frontier_dir, Some(canonicalized(isolated)));
6137        assert_eq!(failures, canonicalized(PathBuf::from("/tmp/persist/failures/Same")));
6138    }
6139
6140    #[test]
6141    fn symbolic_sequence_failure_identity_includes_failure_site() {
6142        let outcome = |site: CheckSequenceFailureSite| CheckSequenceOutcome {
6143            success: false,
6144            replayed_entirely: false,
6145            reason: Some("same reason".to_string()),
6146            calls_count: 1,
6147            reverts: 0,
6148            failure_site: Some(site),
6149            sequence_assertion_failure: true,
6150            sequence_reverter: None,
6151        };
6152        let site = |target: u8, fingerprint: u8| CheckSequenceFailureSite::SequenceCall {
6153            target: Address::with_last_byte(target),
6154            selector: Selector::from([0, 0, 0, 1]),
6155            fingerprint: B256::repeat_byte(fingerprint),
6156        };
6157        let expected = outcome(site(1, 1));
6158
6159        assert!(same_sequence_failure(&outcome(site(1, 1)), &expected));
6160        assert!(!same_sequence_failure(&outcome(site(2, 1)), &expected));
6161        assert!(!same_sequence_failure(&outcome(site(1, 2)), &expected));
6162    }
6163
6164    #[test]
6165    fn stateful_frontiers_sample_sequence_depth() {
6166        let frontiers =
6167            [(6, 7), (0, 1), (8, 9), (3, 4), (9, 9), (2, 3), (5, 6), (1, 2), (7, 8), (4, 5)]
6168                .into_iter()
6169                .map(|(id, call_index)| {
6170                    stateful_frontier_record(id, id as usize, call_index, false)
6171                })
6172                .collect();
6173
6174        let ids = select_stateful_frontiers(frontiers, 5, false)
6175            .into_iter()
6176            .map(|frontier| frontier.id)
6177            .collect::<Vec<_>>();
6178
6179        assert_eq!(ids, [1, 3, 5, 7, 9]);
6180    }
6181
6182    #[test]
6183    fn stateful_frontiers_reserve_deep_retained_context() {
6184        let frontiers = || {
6185            (0..12)
6186                .map(|id| stateful_frontier_record(id, id as usize, id as usize, id >= 10))
6187                .collect()
6188        };
6189
6190        let single_id = select_stateful_frontiers(frontiers(), 1, false)[0].id;
6191        assert_eq!(single_id, 5);
6192
6193        let ids = select_stateful_frontiers(frontiers(), 5, false)
6194            .into_iter()
6195            .map(|frontier| frontier.id)
6196            .collect::<Vec<_>>();
6197
6198        assert_eq!(ids, [1, 3, 5, 7, 11]);
6199    }
6200
6201    #[test]
6202    fn stateful_frontiers_prioritize_distinct_call_contexts() {
6203        let frontiers = [(0, 0, 0), (1, 0, 0), (2, 1, 1), (3, 1, 1), (4, 2, 2)]
6204            .into_iter()
6205            .map(|(id, sequence_index, call_index)| {
6206                stateful_frontier_record(id, sequence_index, call_index, false)
6207            })
6208            .collect();
6209
6210        let ids = select_stateful_frontiers(frontiers, 3, false)
6211            .into_iter()
6212            .map(|frontier| frontier.id)
6213            .collect::<Vec<_>>();
6214
6215        assert_eq!(ids, [1, 3, 4]);
6216    }
6217
6218    #[test]
6219    fn stateful_frontier_reservation_keeps_primary_contexts() {
6220        let frontiers = [
6221            (0, 0, 0, false),
6222            (1, 0, 0, false),
6223            (2, 1, 1, false),
6224            (3, 2, 2, false),
6225            (4, 3, 3, true),
6226        ]
6227        .into_iter()
6228        .map(|(id, sequence_index, call_index, both_results_retained)| {
6229            stateful_frontier_record(id, sequence_index, call_index, both_results_retained)
6230        })
6231        .collect();
6232
6233        let ids = select_stateful_frontiers(frontiers, 4, false)
6234            .into_iter()
6235            .map(|frontier| frontier.id)
6236            .collect::<Vec<_>>();
6237
6238        assert_eq!(ids, [1, 2, 3, 4]);
6239    }
6240
6241    #[test]
6242    fn stateful_frontier_reservation_keeps_fallback_contexts() {
6243        let frontiers =
6244            [(0, 0, 0, false), (1, 1, 1, true), (2, 1, 1, true), (3, 2, 2, true), (4, 3, 3, true)]
6245                .into_iter()
6246                .map(|(id, sequence_index, call_index, both_results_retained)| {
6247                    stateful_frontier_record(id, sequence_index, call_index, both_results_retained)
6248                })
6249                .collect();
6250
6251        let ids = select_stateful_frontiers(frontiers, 4, false)
6252            .into_iter()
6253            .map(|frontier| frontier.id)
6254            .collect::<Vec<_>>();
6255
6256        assert_eq!(ids, [0, 2, 3, 4]);
6257    }
6258
6259    #[test]
6260    fn stateful_frontiers_fill_from_retained_outcomes() {
6261        let frontiers =
6262            (0..5).map(|id| stateful_frontier_record(id, id as usize, id as usize, true)).collect();
6263
6264        let ids = select_stateful_frontiers(frontiers, 4, false)
6265            .into_iter()
6266            .map(|frontier| frontier.id)
6267            .collect::<Vec<_>>();
6268
6269        assert_eq!(ids, [0, 1, 3, 4]);
6270    }
6271
6272    #[test]
6273    fn stateful_frontier_replay_requires_opposite_result_at_same_site() {
6274        let address = Address::with_last_byte(1);
6275        let site = FuzzBranchFrontierSite { address, pc: 7, opcode: opcode::LT };
6276        let comparison = |address, pc, op1, op2| CmpOperands {
6277            address,
6278            pc,
6279            opcode: opcode::LT,
6280            op1: U256::from(op1),
6281            op2: U256::from(op2),
6282        };
6283
6284        assert!(frontier_comparison_flipped(site, true, &[comparison(address, 7, 2, 1)]));
6285        assert!(!frontier_comparison_flipped(site, true, &[comparison(address, 7, 1, 2)]));
6286        assert!(!frontier_comparison_flipped(
6287            site,
6288            true,
6289            &[comparison(Address::with_last_byte(2), 7, 2, 1)]
6290        ));
6291        assert!(!frontier_comparison_flipped(site, true, &[comparison(address, 8, 2, 1)]));
6292    }
6293
6294    fn count_anchors(abi: &JsonAbi, inline_config: &InlineConfig) -> usize {
6295        let config = Config::default();
6296        count_runnable_invariant_campaign_anchors(
6297            abi,
6298            &EmptyTestFilter::default(),
6299            InvariantCampaignScope {
6300                config: &config,
6301                inline_config,
6302                contract_name: CONTRACT_NAME,
6303                all_override_networks: &[],
6304                pass_network: None,
6305            },
6306        )
6307    }
6308
6309    #[test]
6310    fn runnable_campaign_anchor_count_merges_boolean_suite_and_counts_optimizations() {
6311        let abi = JsonAbi::parse([
6312            "function invariantOne() external",
6313            "function invariantTwo() external",
6314            "function invariantOptimizeA() external returns (int256)",
6315            "function invariantOptimizeB() external returns (int256)",
6316        ])
6317        .unwrap();
6318
6319        assert_eq!(count_anchors(&abi, &InlineConfig::new()), 3);
6320    }
6321
6322    #[test]
6323    fn runnable_campaign_anchor_count_splits_boolean_suite_when_configs_differ() {
6324        let abi = JsonAbi::parse([
6325            "function invariantOne() external",
6326            "function invariantTwo() external",
6327        ])
6328        .unwrap();
6329        let mut inline_config = InlineConfig::new();
6330        inline_config
6331            .insert(&NatSpec {
6332                contract: CONTRACT_NAME.to_string(),
6333                function: Some("invariantTwo".to_string()),
6334                line: "1:1".to_string(),
6335                docs: "forge-config: default.invariant.depth = 1".to_string(),
6336            })
6337            .unwrap();
6338
6339        assert_eq!(count_anchors(&abi, &inline_config), 2);
6340    }
6341
6342    #[test]
6343    fn selected_campaign_merges_without_changing_namespace() {
6344        let abi = JsonAbi::parse([
6345            "function invariantOne() external",
6346            "function invariantTwo() external",
6347            "function invariantThree() external",
6348        ])
6349        .unwrap();
6350        let functions = abi.functions().collect::<Vec<_>>();
6351        let selected = functions
6352            .iter()
6353            .copied()
6354            .filter(|func| func.name != "invariantThree")
6355            .collect::<Vec<_>>();
6356        let mut inline_config = InlineConfig::new();
6357        inline_config
6358            .insert(&NatSpec {
6359                contract: CONTRACT_NAME.to_string(),
6360                function: Some("invariantThree".to_string()),
6361                line: "1:1".to_string(),
6362                docs: "forge-config: default.invariant.fail-on-revert = true".to_string(),
6363            })
6364            .unwrap();
6365        let config = Config::default();
6366        let selection = select_invariant_campaigns(
6367            &functions,
6368            &selected,
6369            &config,
6370            &inline_config,
6371            CONTRACT_NAME,
6372        );
6373        assert_eq!(selection.anchor_count(), 1);
6374        assert!(selection.merge_boolean_suite);
6375        assert!(!selection.shared_boolean_namespace);
6376
6377        let uniform = select_invariant_campaigns(
6378            &functions,
6379            &selected,
6380            &config,
6381            &InlineConfig::new(),
6382            CONTRACT_NAME,
6383        );
6384        assert_eq!(uniform.anchor_count(), 1);
6385        assert!(uniform.merge_boolean_suite);
6386        assert!(uniform.shared_boolean_namespace);
6387    }
6388
6389    #[test]
6390    fn runnable_campaign_anchor_count_splits_boolean_suite_when_corpus_weight_provenance_differs() {
6391        let abi = JsonAbi::parse([
6392            "function invariantOne() external",
6393            "function invariantTwo() external",
6394        ])
6395        .unwrap();
6396        let mut inline_config = InlineConfig::new();
6397        inline_config
6398            .insert(&NatSpec {
6399                contract: CONTRACT_NAME.to_string(),
6400                function: Some("invariantTwo".to_string()),
6401                line: "1:1".to_string(),
6402                docs: "forge-config: default.invariant.corpus_random_sequence_weight = 10"
6403                    .to_string(),
6404            })
6405            .unwrap();
6406
6407        assert_eq!(count_anchors(&abi, &inline_config), 2);
6408    }
6409
6410    #[test]
6411    fn runnable_campaign_anchor_count_respects_network_pass() {
6412        let abi = JsonAbi::parse(["function invariantTempoOnly() external"]).unwrap();
6413        let mut inline_config = InlineConfig::new();
6414        inline_config
6415            .insert(&NatSpec {
6416                contract: CONTRACT_NAME.to_string(),
6417                function: Some("invariantTempoOnly".to_string()),
6418                line: "1:1".to_string(),
6419                docs: r#"forge-config: default.networks.network = "tempo""#.to_string(),
6420            })
6421            .unwrap();
6422        let config = Config::default();
6423        let override_networks = [NetworkVariant::Tempo];
6424
6425        let default_pass = count_runnable_invariant_campaign_anchors(
6426            &abi,
6427            &EmptyTestFilter::default(),
6428            InvariantCampaignScope {
6429                config: &config,
6430                inline_config: &inline_config,
6431                contract_name: CONTRACT_NAME,
6432                all_override_networks: &override_networks,
6433                pass_network: None,
6434            },
6435        );
6436        let tempo_pass = count_runnable_invariant_campaign_anchors(
6437            &abi,
6438            &EmptyTestFilter::default(),
6439            InvariantCampaignScope {
6440                config: &config,
6441                inline_config: &inline_config,
6442                contract_name: CONTRACT_NAME,
6443                all_override_networks: &override_networks,
6444                pass_network: Some(&NetworkVariant::Tempo),
6445            },
6446        );
6447
6448        assert_eq!(default_pass, 0);
6449        assert_eq!(tempo_pass, 1);
6450    }
6451}