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