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