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