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