Skip to main content

foundry_evm/executors/invariant/
mod.rs

1use crate::{
2    executors::{
3        DURATION_BETWEEN_METRICS_REPORT, EarlyExit, EvmError, Executor, RawCallResult,
4        campaign::{
5            CampaignCallKind, CampaignControl, CampaignEvent, CampaignSequenceOutcome,
6            FuzzCampaign, FuzzCampaignMode,
7        },
8        corpus::{
9            CorpusInsertionMode, DynamicTargetCtx, ReplayTarget, WorkerCorpus, WorkerCorpusSeed,
10            persist_campaign_optimization,
11        },
12        fuzz::{
13            FuzzBranchFrontier, FuzzFrontierRecorder, StatefulFuzzBranchFrontierArtifact,
14            merge_frontiers, write_frontier_artifact,
15        },
16    },
17    inspectors::Fuzzer,
18};
19use alloy_json_abi::Function;
20use alloy_primitives::{
21    Address, Bytes, FixedBytes, I256, Selector, U256, keccak256,
22    map::{AddressMap, AddressSet, HashMap, hash_map::Entry as AddressMapEntry},
23};
24use alloy_sol_types::{SolCall, sol};
25use campaign::{
26    InvariantCampaignAggregator, InvariantCampaignSpec, InvariantCampaignState,
27    InvariantWorkerOutput, InvariantWorkerPlan,
28};
29use eyre::{ContextCompat, Result, eyre};
30use foundry_common::{
31    TestFunctionExt,
32    contracts::{ContractsByAddress, ContractsByArtifact},
33    sh_eprintln, sh_println,
34};
35use foundry_config::{FuzzCorpusConfig, InvariantConfig, InvariantDepthMode, InvariantWorkers};
36use foundry_evm_core::{
37    constants::{
38        CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS, MAGIC_ASSUME,
39    },
40    evm::FoundryEvmNetwork,
41    precompiles::PRECOMPILES,
42};
43use foundry_evm_coverage::HitMaps;
44use foundry_evm_fuzz::{
45    BasicTxDetails, FuzzCase, FuzzFixtures, ObservedCall,
46    invariant::{
47        ArtifactFilters, FuzzRunIdentifiedContracts, InvariantContract, RandomCallGenerator,
48        SenderFilters, TargetedContract, TargetedContracts,
49    },
50    strategies::{EvmFuzzState, FuzzState, TxGenerator, override_call_strat},
51};
52use foundry_evm_traces::{CallTraceArena, SparsedTraceArena};
53use indicatif::ProgressBar;
54use parking_lot::RwLock;
55use proptest::{
56    prelude::Rng,
57    test_runner::{RngAlgorithm, TestRng, TestRunner},
58};
59use rayon::iter::{IntoParallelIterator, ParallelIterator};
60use result::{assert_after_invariant, can_continue, invariant_preflight_check};
61use revm::state::Account;
62use serde::{Deserialize, Serialize};
63use serde_json::{Value, json};
64use std::{
65    collections::{HashMap as Map, HashSet, btree_map::Entry},
66    sync::Arc,
67    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
68};
69
70mod error;
71pub(crate) use error::snapshot_edge_fingerprint;
72pub use error::{
73    FailureKey, HandlerAssertionFailure, InvariantFailures, InvariantFuzzError,
74    handler_site_already_minimal,
75};
76mod campaign;
77
78mod replay;
79pub use replay::{ReplayErrorResult, replay_error, replay_run};
80
81mod result;
82pub use result::{InvariantFuzzTestResult, did_fail_on_assert};
83
84mod shrink;
85pub use shrink::{
86    CheckSequenceFailureSite, CheckSequenceOptions, CheckSequenceOutcome, HandlerReplayOutcome,
87    SequenceShrink, ShrinkCandidateKeys, ShrinkRun, ShrinkRunStats, check_sequence,
88    check_sequence_value, replay_handler_failure_sequence, shrink_sequence_by_removing,
89};
90
91/// Minimum number of logical runs assigned to each auto invariant worker at the default invariant
92/// depth.
93///
94/// Keeps short campaigns single-threaded and avoids producing many small rayon jobs.
95const MIN_RUNS_PER_INVARIANT_WORKER: u32 = 10_000;
96/// Baseline depth used to preserve the previous default-depth worker heuristic.
97const DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP: u32 = 500;
98/// Minimum estimated handler calls assigned to each auto invariant worker.
99const MIN_ESTIMATED_CALLS_PER_INVARIANT_WORKER: u64 =
100    MIN_RUNS_PER_INVARIANT_WORKER as u64 * DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP as u64;
101/// Share of parallel workers reserved for selector focus mode.
102const INVARIANT_FOCUS_WORKER_DIVISOR: usize = 8;
103
104sol! {
105    interface IInvariantTest {
106        #[derive(Default)]
107        struct FuzzSelector {
108            address addr;
109            bytes4[] selectors;
110        }
111
112        #[derive(Default)]
113        struct FuzzArtifactSelector {
114            string artifact;
115            bytes4[] selectors;
116        }
117
118        #[derive(Default)]
119        struct FuzzInterface {
120            address addr;
121            string[] artifacts;
122        }
123
124        function afterInvariant() external;
125
126        #[derive(Default)]
127        function excludeArtifacts() public view returns (string[] memory excludedArtifacts);
128
129        #[derive(Default)]
130        function excludeContracts() public view returns (address[] memory excludedContracts);
131
132        #[derive(Default)]
133        function excludeSelectors() public view returns (FuzzSelector[] memory excludedSelectors);
134
135        #[derive(Default)]
136        function excludeSenders() public view returns (address[] memory excludedSenders);
137
138        #[derive(Default)]
139        function targetArtifacts() public view returns (string[] memory targetedArtifacts);
140
141        #[derive(Default)]
142        function targetArtifactSelectors() public view returns (FuzzArtifactSelector[] memory targetedArtifactSelectors);
143
144        #[derive(Default)]
145        function targetContracts() public view returns (address[] memory targetedContracts);
146
147        #[derive(Default)]
148        function targetSelectors() public view returns (FuzzSelector[] memory targetedSelectors);
149
150        #[derive(Default)]
151        function targetSenders() public view returns (address[] memory targetedSenders);
152
153        #[derive(Default)]
154        function targetInterfaces() public view returns (FuzzInterface[] memory targetedInterfaces);
155    }
156}
157
158/// Contains invariant metrics for a single fuzzed selector.
159#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
160pub struct InvariantMetrics {
161    // Count of fuzzed selector calls.
162    pub calls: usize,
163    // Count of fuzzed selector reverts.
164    pub reverts: usize,
165    // Count of fuzzed selector discards (through assume cheatcodes).
166    pub discards: usize,
167}
168
169impl InvariantMetrics {
170    const fn record_call(&mut self, reverted: bool, discarded: bool) {
171        self.calls += 1;
172        if discarded {
173            self.discards += 1;
174        } else if reverted {
175            self.reverts += 1;
176        }
177    }
178}
179
180/// Campaign-level throughput metrics for invariant progress reporting.
181#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
182struct InvariantThroughputMetrics {
183    total_txs: u64,
184    total_gas: u64,
185}
186
187impl InvariantThroughputMetrics {
188    fn tps(self, elapsed: Duration) -> f64 {
189        round_rate_for_progress(rate_per_sec(self.total_txs as f64, elapsed))
190    }
191
192    fn gps(self, elapsed: Duration) -> f64 {
193        round_rate_for_progress(rate_per_sec(self.total_gas as f64, elapsed))
194    }
195}
196
197fn max_invariant_workers_for_campaign(runs: u32, depth: u32) -> usize {
198    let estimated_calls = u64::from(runs) * u64::from(depth.max(1));
199    usize::try_from((estimated_calls / MIN_ESTIMATED_CALLS_PER_INVARIANT_WORKER).max(1))
200        .unwrap_or(usize::MAX)
201}
202
203fn invariant_run_depth(config: &InvariantConfig, runner: &mut TestRunner) -> u32 {
204    match config.depth_mode {
205        InvariantDepthMode::Fixed => config.depth,
206        InvariantDepthMode::Random => {
207            let min_depth = config.min_depth.max(1);
208            if config.depth <= min_depth {
209                config.depth.max(1)
210            } else {
211                runner.rng().random_range(min_depth..=config.depth)
212            }
213        }
214    }
215}
216
217fn auto_invariant_worker_count(
218    available_threads: usize,
219    invariant_campaign_anchors: usize,
220) -> usize {
221    (available_threads.max(1) / invariant_campaign_anchors.max(1)).max(1)
222}
223
224fn invariant_worker_count_with_threads(
225    config: &InvariantConfig,
226    available_threads: usize,
227    invariant_campaign_anchors: usize,
228) -> usize {
229    match config.workers {
230        InvariantWorkers::Fixed(workers) => workers.get(),
231        InvariantWorkers::Auto => {
232            let requested =
233                auto_invariant_worker_count(available_threads, invariant_campaign_anchors);
234            if config.timeout.is_some() {
235                requested
236            } else {
237                requested.min(max_invariant_workers_for_campaign(config.runs, config.depth))
238            }
239        }
240    }
241}
242
243const fn invariant_worker_config(
244    mut config: InvariantConfig,
245    worker_id: u32,
246    worker_count: usize,
247) -> InvariantConfig {
248    // Keep one- and two-worker campaigns on the stable default, but let one extra worker explore
249    // the broader fresh-input setting once the user has enough workers to keep the exploratory
250    // share below half of the campaign. The last worker does not receive remainder runs from
251    // campaign sharding, so this keeps exposure bounded to one worker shard.
252    let exploratory_worker_id = worker_count.saturating_sub(1) as u32;
253    if worker_count > 2
254        && worker_id == exploratory_worker_id
255        && !config.corpus_random_sequence_weight_configured
256        && config.corpus.corpus_random_sequence_weight
257            == FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
258    {
259        config.corpus.corpus_random_sequence_weight =
260            FuzzCorpusConfig::ENSEMBLE_CORPUS_RANDOM_SEQUENCE_WEIGHT;
261    }
262    config
263}
264
265fn gas_report_samples_for_worker(total_samples: u32, worker_id: u32, worker_count: usize) -> usize {
266    let total_samples = total_samples as usize;
267    let worker_count = worker_count.max(1);
268    total_samples / worker_count + usize::from((worker_id as usize) < total_samples % worker_count)
269}
270
271fn invariant_worker_collects_evm_cmp_log(
272    config: &InvariantConfig,
273    worker_id: u32,
274    worker_count: usize,
275) -> bool {
276    config.corpus.collect_evm_cmp_log() && (worker_count <= 1 || worker_id == 0)
277}
278
279fn invariant_focus_worker_count(worker_count: usize) -> usize {
280    if worker_count <= 1 {
281        0
282    } else {
283        let max_focus_workers = if worker_count > 2 { worker_count - 2 } else { worker_count - 1 };
284        (worker_count / INVARIANT_FOCUS_WORKER_DIVISOR).max(1).min(max_focus_workers)
285    }
286}
287
288fn invariant_focus_worker_index(worker_id: u32, worker_count: usize) -> Option<usize> {
289    let worker_id = worker_id as usize;
290    let focus_workers = invariant_focus_worker_count(worker_count);
291    if focus_workers == 0 || worker_id >= worker_count {
292        return None;
293    }
294
295    let focus_worker_end = if worker_count > 2 { worker_count - 1 } else { worker_count };
296    let first_focus_worker = focus_worker_end - focus_workers;
297    (worker_id >= first_focus_worker && worker_id < focus_worker_end)
298        .then(|| worker_id - first_focus_worker)
299}
300
301#[cfg(test)]
302fn campaign_seed_for_worker(
303    campaign_seed: &InvariantCampaignSeed,
304    plan: InvariantWorkerPlan,
305    worker_count: usize,
306) -> InvariantCampaignSeed {
307    focused_campaign_seed_for_worker(campaign_seed, plan, worker_count, None)
308        .unwrap_or_else(|| campaign_seed.clone())
309}
310
311fn focused_campaign_seed_for_worker(
312    campaign_seed: &InvariantCampaignSeed,
313    plan: InvariantWorkerPlan,
314    worker_count: usize,
315    focus_seed: Option<U256>,
316) -> Option<InvariantCampaignSeed> {
317    let focus_index = invariant_focus_worker_index(plan.worker_id, worker_count)?;
318    let targeted_contracts =
319        focused_targeted_contracts(&campaign_seed.targeted_contracts, focus_index, focus_seed)?;
320
321    Some(InvariantCampaignSeed {
322        targeted_contracts,
323        targets_are_updatable: false,
324        ..campaign_seed.clone()
325    })
326}
327
328fn campaign_seed_and_corpus_seed_for_worker(
329    campaign_seed: &InvariantCampaignSeed,
330    corpus_seed: &WorkerCorpusSeed,
331    plan: InvariantWorkerPlan,
332    worker_count: usize,
333    include_cmp_seq: bool,
334    focus_seed: Option<U256>,
335) -> (InvariantCampaignSeed, WorkerCorpusSeed) {
336    let worker_corpus_seed =
337        corpus_seed.clone_for_worker(plan.worker_id as usize, worker_count, include_cmp_seq);
338    let Some(worker_campaign_seed) =
339        focused_campaign_seed_for_worker(campaign_seed, plan, worker_count, focus_seed)
340    else {
341        return (campaign_seed.clone(), worker_corpus_seed);
342    };
343
344    let mut worker_corpus_seed = worker_corpus_seed;
345    worker_corpus_seed.retain_replayable(&worker_campaign_seed.targeted_contracts);
346    (worker_campaign_seed, worker_corpus_seed)
347}
348
349fn focused_targeted_contracts(
350    targeted_contracts: &TargetedContracts,
351    focus_index: usize,
352    focus_seed: Option<U256>,
353) -> Option<TargetedContracts> {
354    let mut seen = HashSet::new();
355    let mut candidates = Vec::new();
356    for (address, contract) in targeted_contracts.iter() {
357        // Build from the effective selector set so user target/exclude config stays authoritative.
358        for function in contract.abi_fuzzed_functions() {
359            if seen.insert((*address, function.selector())) {
360                candidates.push((*address, function.clone()));
361            }
362        }
363    }
364    if candidates.len() <= 1 {
365        return None;
366    }
367
368    let seed_offset = focus_seed
369        .map(|seed| (seed % U256::from(candidates.len())).to::<usize>())
370        .unwrap_or_default();
371    let candidate_index = (focus_index % candidates.len() + seed_offset) % candidates.len();
372    let (address, function) = candidates[candidate_index].clone();
373    let mut contract = targeted_contracts.get(&address)?.clone();
374    contract.targeted_functions = vec![function];
375
376    let mut focused = TargetedContracts::new();
377    focused.insert(address, contract);
378    Some(focused)
379}
380
381fn invariant_worker_seed(seed: U256, worker_id: u32) -> U256 {
382    if worker_id == 0 {
383        seed
384    } else {
385        let seed_data = [&seed.to_be_bytes::<32>()[..], &worker_id.to_be_bytes()[..]].concat();
386        U256::from_be_bytes(keccak256(seed_data).0)
387    }
388}
389
390fn should_continue_invariant_worker(
391    campaign_state: &InvariantCampaignState,
392    runs: u32,
393    plan: InvariantWorkerPlan,
394) -> bool {
395    if campaign_state.should_stop() {
396        return false;
397    }
398
399    campaign_state.is_timed_campaign() || runs < plan.runs
400}
401
402fn invariant_worker_runner(
403    runner: &mut TestRunner,
404    worker_id: u32,
405    seed: Option<U256>,
406) -> TestRunner {
407    if let Some(seed) = seed {
408        let worker_seed = invariant_worker_seed(seed, worker_id);
409        trace!(target: "forge::test", ?worker_seed, "deterministic seed for invariant worker {worker_id}");
410        let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &worker_seed.to_be_bytes::<32>());
411        TestRunner::new_with_rng(runner.config().clone(), rng)
412    } else if worker_id == 0 {
413        runner.clone()
414    } else {
415        TestRunner::new_with_rng(runner.config().clone(), runner.new_rng())
416    }
417}
418
419fn invariant_focus_seed(
420    runner: &mut TestRunner,
421    configured_seed: Option<U256>,
422    worker_count: usize,
423) -> Option<U256> {
424    if invariant_focus_worker_count(worker_count) == 0 {
425        return None;
426    }
427    configured_seed.or_else(|| {
428        let mut rng = runner.new_rng();
429        Some(U256::from_be_bytes(rng.random::<[u8; 32]>()))
430    })
431}
432
433/// Converts a cumulative campaign total into an average per-second rate.
434///
435/// Returns `0.0` during the initial zero-elapsed startup window to avoid
436/// dividing by zero while progress reporting is warming up.
437fn rate_per_sec(total: f64, elapsed: Duration) -> f64 {
438    let elapsed_secs = elapsed.as_secs_f64();
439    if elapsed_secs > 0.0 { total / elapsed_secs } else { 0.0 }
440}
441
442fn round_rate_for_progress(rate: f64) -> f64 {
443    (rate * 100.0).round() / 100.0
444}
445
446/// Tracks invariant failure counts during a campaign.
447#[derive(Clone, Debug, Default)]
448struct InvariantFailureMetrics {
449    failures: u64,
450    unique_failures: HashSet<String>,
451    /// Unique handler-side assertion bugs found so far.
452    broken_handlers: usize,
453}
454
455impl InvariantFailureMetrics {
456    /// Records a failure and emits a structured JSON `"failure"` event.
457    fn record_failure(&mut self, invariant_name: &str, target: &str, reason: &str) {
458        self.failures += 1;
459        self.unique_failures.insert(invariant_name.to_string());
460
461        let timestamp =
462            SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
463        let event = json!({
464            "timestamp": timestamp,
465            "event": "failure",
466            "invariant": invariant_name,
467            "target": target,
468            "reason": reason,
469        });
470        let _ = sh_eprintln!("{}", serde_json::to_string(&event).unwrap_or_default());
471    }
472
473    /// Records a handler assertion and emits a structured JSON `"failure"` event.
474    fn record_handler_failure(&mut self, target: Address, selector: Selector, reason: &str) {
475        self.broken_handlers += 1;
476
477        let timestamp =
478            SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
479        let event = build_handler_failure_event(timestamp, target, selector, reason);
480        let _ = sh_eprintln!("{}", serde_json::to_string(&event).unwrap_or_default());
481    }
482}
483
484fn build_handler_failure_event(
485    timestamp_secs: u64,
486    target: Address,
487    selector: Selector,
488    reason: &str,
489) -> Value {
490    json!({
491        "timestamp": timestamp_secs,
492        "event": "failure",
493        "failure_type": "handler_assertion",
494        "target": target,
495        "selector": selector,
496        "reason": reason,
497    })
498}
499
500/// Bridges newly-recorded invariant breaks from `failures.errors` into the pulse
501/// `failure_metrics` so the live progress stream reflects breaks as they happen.
502/// Iterates in declaration order so the emitted "failure" events are deterministic.
503fn record_new_invariant_failures(
504    campaign_state: &InvariantCampaignState,
505    invariant_contract: &InvariantContract<'_>,
506    failures: &InvariantFailures,
507) {
508    for (f, _) in &invariant_contract.invariant_fns {
509        if let Some(failure) = failures.get_failure(f) {
510            let reason = failure.revert_reason().unwrap_or_default();
511            campaign_state.record_invariant_failure(&f.name, invariant_contract.name, &reason);
512        }
513    }
514}
515
516struct InvariantProgressContext<'a> {
517    timestamp_secs: u64,
518    contract_name: &'a str,
519    optimization_best: Option<I256>,
520    throughput: InvariantThroughputMetrics,
521    elapsed: Duration,
522    worker_id: u32,
523    worker_count: usize,
524    /// Time since this worker last saw a new edge, or `None` if it has not seen one yet.
525    time_since_new_edge: Option<Duration>,
526}
527
528/// Builds the machine-readable invariant progress payload emitted during a
529/// campaign.
530///
531/// This keeps the existing corpus progress metrics together with cumulative and
532/// derived throughput fields so downstream benchmark tooling can consume a
533/// single JSON event shape.
534fn build_invariant_progress_json<M: Serialize>(
535    context: InvariantProgressContext<'_>,
536    corpus_metrics: &M,
537    failure_metrics: &InvariantFailureMetrics,
538) -> serde_json::Value {
539    let mut metrics = serde_json::to_value(corpus_metrics).unwrap_or_default();
540    if let Some(obj) = metrics.as_object_mut() {
541        obj.insert("broken_invariants".to_string(), json!(failure_metrics.unique_failures.len()));
542        obj.insert("broken_assertions".to_string(), json!(failure_metrics.broken_handlers));
543    }
544
545    let mut payload = json!({
546        "timestamp": context.timestamp_secs,
547        "event": "pulse",
548        "contract": context.contract_name,
549        "metrics": metrics,
550        "total_txs": context.throughput.total_txs,
551        "total_gas": context.throughput.total_gas,
552        "tps": context.throughput.tps(context.elapsed),
553        "gps": context.throughput.gps(context.elapsed),
554        "worker": {
555            "id": context.worker_id,
556            "count": context.worker_count,
557            // `null` until this worker sees its first edge.
558            "secs_since_new_edge": context
559                .time_since_new_edge
560                .map(|d| d.as_secs_f64()),
561        },
562    });
563
564    if let Some(best) = context.optimization_best {
565        payload["optimization_best"] = json!(best.to_string());
566    }
567
568    payload
569}
570
571/// Contains data collected during invariant test runs.
572struct InvariantTestData {
573    // Number of completed invariant runs.
574    runs: usize,
575    // Number of completed fuzzed calls across all invariant runs.
576    calls: usize,
577    // Data related to reverts or failed assertions of the test.
578    failures: InvariantFailures,
579    // Calldata in the last invariant run.
580    last_run_inputs: Vec<BasicTxDetails>,
581    // Additional traces for gas report.
582    gas_report_traces: Vec<Vec<CallTraceArena>>,
583    // Line coverage information collected from all fuzzed calls.
584    line_coverage: Option<HitMaps>,
585    // Metrics for each fuzzed selector.
586    metrics: HashMap<String, InvariantMetrics>,
587    // Cache from fuzzed (target, selector) to its metric key. Only resolved keys are cached and
588    // they are invalidated when targets change (see `invalidate_metric_key_cache`).
589    metric_key_cache: HashMap<(Address, Selector), String>,
590
591    // Proptest runner to query for random values.
592    // The strategy only comes with the first `input`. We fill the rest of the `inputs`
593    // until the desired `depth` so we can use the evolving fuzz dictionary
594    // during the run.
595    branch_runner: TestRunner,
596
597    // Optimization mode state: tracks the best (maximum) value and the sequence that produced it.
598    // Only used when invariant function returns int256.
599    optimization_best_value: Option<I256>,
600    optimization_best_sequence: Vec<BasicTxDetails>,
601}
602
603/// Contains invariant test data.
604struct InvariantTest {
605    // Fuzz state of invariant test.
606    fuzz_state: FuzzState,
607    // Contracts fuzzed by the invariant test.
608    targeted_contracts: FuzzRunIdentifiedContracts,
609    // Data collected during invariant runs.
610    test_data: InvariantTestData,
611}
612
613impl InvariantTest {
614    /// Instantiates an invariant test.
615    fn new(
616        fuzz_state: FuzzState,
617        targeted_contracts: FuzzRunIdentifiedContracts,
618        failures: InvariantFailures,
619        branch_runner: TestRunner,
620    ) -> Self {
621        let test_data = InvariantTestData {
622            runs: 0,
623            calls: 0,
624            failures,
625            last_run_inputs: vec![],
626            gas_report_traces: vec![],
627            line_coverage: None,
628            metrics: HashMap::default(),
629            metric_key_cache: HashMap::default(),
630            branch_runner,
631            optimization_best_value: None,
632            optimization_best_sequence: vec![],
633        };
634        Self { fuzz_state, targeted_contracts, test_data }
635    }
636
637    /// Returns number of invariant test reverts.
638    const fn reverts(&self) -> usize {
639        self.test_data.failures.reverts
640    }
641
642    /// Set invariant test error.
643    fn set_error(&mut self, invariant: &Function, error: InvariantFuzzError) {
644        self.test_data.failures.record_failure(invariant, error);
645    }
646
647    /// Set last invariant run call sequence.
648    fn set_last_run_inputs(&mut self, inputs: &Vec<BasicTxDetails>) {
649        self.test_data.last_run_inputs.clone_from(inputs);
650    }
651
652    /// Merge current collected line coverage with the new coverage from last fuzzed call.
653    fn merge_line_coverage(&mut self, new_coverage: Option<HitMaps>) {
654        HitMaps::merge_opt(&mut self.test_data.line_coverage, new_coverage);
655    }
656
657    /// Update metrics for a fuzzed selector, extracted from tx details.
658    /// Always increments number of calls; discarded runs (through assume cheatcodes) are tracked
659    /// separated from reverts.
660    fn record_metrics(&mut self, tx_details: &BasicTxDetails, reverted: bool, discarded: bool) {
661        let Some(selector) = tx_details
662            .call_details
663            .calldata
664            .get(..4)
665            .and_then(|selector| <[u8; 4]>::try_from(selector).ok())
666            .map(Selector::from)
667        else {
668            return;
669        };
670        let cache_key = (tx_details.call_details.target, selector);
671
672        if let Some(metric_key) = self.test_data.metric_key_cache.get(&cache_key) {
673            if let Some(invariant_metrics) = self.test_data.metrics.get_mut(metric_key) {
674                invariant_metrics.record_call(reverted, discarded);
675            } else {
676                self.test_data
677                    .metrics
678                    .entry(metric_key.to_owned())
679                    .or_default()
680                    .record_call(reverted, discarded);
681            }
682            return;
683        }
684
685        // Not cached: resolve from the current target set. Unresolved keys aren't cached so a tx
686        // whose target isn't known yet is re-resolved once that target is added.
687        let Some(metric_key) = self
688            .targeted_contracts
689            .targets()
690            .fuzzed_metric_key_for_selector(tx_details.call_details.target, selector)
691        else {
692            return;
693        };
694        self.test_data.metric_key_cache.insert(cache_key, metric_key.clone());
695        self.test_data.metrics.entry(metric_key).or_default().record_call(reverted, discarded);
696    }
697
698    /// Drops cached metric keys for the given addresses, keeping the cache coherent when targets
699    /// are added or removed (an address can be reused for a different artifact across runs).
700    fn invalidate_metric_key_cache(&mut self, addresses: &[Address]) {
701        if addresses.is_empty() {
702            return;
703        }
704        self.test_data.metric_key_cache.retain(|(addr, _), _| !addresses.contains(addr));
705    }
706
707    /// End invariant test run by collecting results, cleaning collected artifacts and reverting
708    /// created fuzz state.
709    fn end_run<FEN: FoundryEvmNetwork>(&mut self, run: InvariantTestRun<FEN>, gas_samples: usize) {
710        // Clear contracts created during this run, dropping their cached metric keys so a reused
711        // address can't resolve to a stale contract in a later run.
712        self.invalidate_metric_key_cache(&run.created_contracts);
713        self.targeted_contracts.clear_created_contracts(run.created_contracts);
714
715        if self.test_data.gas_report_traces.len() < gas_samples {
716            self.test_data
717                .gas_report_traces
718                .push(run.run_traces.into_iter().map(|arena| arena.arena).collect());
719        }
720        self.test_data.runs += 1;
721        self.test_data.calls += run.fuzz_runs.len();
722
723        // Revert state to not persist values between runs.
724        self.fuzz_state.revert();
725    }
726
727    /// Updates the optimization state if the new value is better (higher) than the current best.
728    fn update_optimization_value(&mut self, value: I256, sequence: &[BasicTxDetails]) {
729        if self.test_data.optimization_best_value.is_none_or(|best| value > best) {
730            self.test_data.optimization_best_value = Some(value);
731            self.test_data.optimization_best_sequence = sequence.to_vec();
732        }
733    }
734}
735
736/// Contains data for an invariant test run.
737struct InvariantTestRun<FEN: FoundryEvmNetwork> {
738    // Invariant run call sequence.
739    inputs: Vec<BasicTxDetails>,
740    // Per-call EVM comparison operands (parallel to `inputs`), captured for I2S corpus mutation.
741    cmp_seq: Vec<Vec<crate::inspectors::CmpOperands>>,
742    // Current invariant run executor.
743    executor: Executor<FEN>,
744    // Invariant run stat reports (eg. gas usage).
745    fuzz_runs: Vec<FuzzCase>,
746    // Contracts created during current invariant run.
747    created_contracts: Vec<Address>,
748    // Traces of each call of the invariant run call sequence.
749    run_traces: Vec<SparsedTraceArena>,
750    // Current depth of invariant run.
751    depth: u32,
752    // Current assume rejects of the invariant run.
753    rejects: u32,
754    // Whether new coverage was discovered during this run.
755    new_coverage: bool,
756    // Line coverage staged until the run is accepted.
757    line_coverage: Option<HitMaps>,
758    // Whether this run's inputs should become the reported last run.
759    save_last_run_inputs: bool,
760    // For optimization mode: the best value found during this run (if any).
761    optimization_value: Option<I256>,
762    // For optimization mode: the length of the input prefix that produced the best value.
763    optimization_prefix_len: usize,
764}
765
766/// Recorded call sequence used by corpus feedback and frontier recording.
767struct RecordedCallSequence {
768    inputs: Vec<BasicTxDetails>,
769    cmp_seq: Vec<Vec<crate::inspectors::CmpOperands>>,
770}
771
772/// Immutable state selected once for a logical invariant campaign and cloned into each worker.
773#[derive(Clone)]
774struct InvariantCampaignSeed {
775    artifact_filters: ArtifactFilters,
776    sender_filters: SenderFilters,
777    targeted_contracts: TargetedContracts,
778    targets_are_updatable: bool,
779    initial_handler_failures: Map<(Address, Selector), InvariantFuzzError>,
780}
781
782impl<FEN: FoundryEvmNetwork> InvariantTestRun<FEN> {
783    /// Instantiates an invariant test run.
784    fn new(first_input: BasicTxDetails, executor: Executor<FEN>, depth: usize) -> Self {
785        let mut inputs = Vec::with_capacity(depth.saturating_add(1));
786        inputs.push(first_input);
787        Self {
788            inputs,
789            cmp_seq: Vec::with_capacity(depth),
790            executor,
791            fuzz_runs: Vec::with_capacity(depth),
792            created_contracts: vec![],
793            run_traces: vec![],
794            depth: 0,
795            rejects: 0,
796            new_coverage: false,
797            line_coverage: None,
798            save_last_run_inputs: false,
799            optimization_value: None,
800            optimization_prefix_len: 0,
801        }
802    }
803
804    /// Releases per-run corpus payloads once the worker corpus manager has consumed them.
805    ///
806    /// Successful runs only need `fuzz_runs`, traces, and created-contract bookkeeping for final
807    /// reporting. Counterexample inputs are copied into `InvariantTestData::last_run_inputs`
808    /// before this point, so retaining the full per-run input/cmp buffers until `end_run` only
809    /// extends peak memory in long invariant campaigns.
810    fn drop_corpus_payloads(&mut self) {
811        self.inputs.clear();
812        self.inputs.shrink_to_fit();
813        self.cmp_seq.clear();
814        self.cmp_seq.shrink_to_fit();
815    }
816}
817
818/// Wrapper around any [`Executor`] implementer which provides fuzzing support using [`proptest`].
819///
820/// After instantiation, calling `invariant_fuzz` will proceed to hammer the deployed smart
821/// contracts with inputs, until it finds a counterexample sequence. The provided [`TestRunner`]
822/// contains all the configuration which can be overridden via [environment
823/// variables](proptest::test_runner::Config)
824pub struct InvariantExecutor<'a, FEN: FoundryEvmNetwork> {
825    pub executor: Executor<FEN>,
826    /// Proptest runner.
827    runner: TestRunner,
828    /// Configured fuzz seed used to derive deterministic invariant worker runners.
829    fuzz_seed: Option<U256>,
830    /// The invariant configuration
831    config: InvariantConfig,
832    /// Contracts deployed with `setUp()`
833    setup_contracts: &'a ContractsByAddress,
834    /// Contracts that are part of the project but have not been deployed yet. We need the bytecode
835    /// to identify them from the stateset changes.
836    project_contracts: &'a ContractsByArtifact,
837    /// Filters contracts to be fuzzed through their artifact identifiers.
838    artifact_filters: ArtifactFilters,
839    /// Number of matching invariant campaign anchors in the current test pass.
840    invariant_campaign_anchors: usize,
841}
842
843impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
844    /// Instantiates a fuzzed executor EVM given a testrunner
845    pub fn new(
846        executor: Executor<FEN>,
847        runner: TestRunner,
848        config: InvariantConfig,
849        setup_contracts: &'a ContractsByAddress,
850        project_contracts: &'a ContractsByArtifact,
851    ) -> Self {
852        Self::new_with_fuzz_seed(
853            executor,
854            runner,
855            None,
856            config,
857            setup_contracts,
858            project_contracts,
859            1,
860        )
861    }
862
863    /// Instantiates an invariant executor with the configured fuzz seed for deterministic worker
864    /// runner derivation.
865    pub fn new_with_fuzz_seed(
866        executor: Executor<FEN>,
867        runner: TestRunner,
868        fuzz_seed: Option<U256>,
869        config: InvariantConfig,
870        setup_contracts: &'a ContractsByAddress,
871        project_contracts: &'a ContractsByArtifact,
872        invariant_campaign_anchors: usize,
873    ) -> Self {
874        Self {
875            executor,
876            runner,
877            fuzz_seed,
878            config,
879            setup_contracts,
880            project_contracts,
881            artifact_filters: ArtifactFilters::default(),
882            invariant_campaign_anchors,
883        }
884    }
885
886    pub fn config(&self) -> InvariantConfig {
887        self.config.clone()
888    }
889
890    /// Refs for tracking contracts deployed mid-sequence during corpus replay.
891    pub const fn dynamic_target_ctx(&self) -> DynamicTargetCtx<'_> {
892        DynamicTargetCtx {
893            project_contracts: self.project_contracts,
894            setup_contracts: self.setup_contracts,
895            artifact_filters: &self.artifact_filters,
896        }
897    }
898
899    /// Fuzzes any deployed contract and checks any broken invariant at `invariant_address`.
900    ///
901    /// `initial_handler_failures` pre-seeds the campaign's `broken_handlers` map with bugs
902    /// recovered from disk by the runner's persisted-failure replay step, so the live
903    /// progress bar and JSON pulse stream surface them from the first emission instead of
904    /// jumping at the final report.
905    pub fn invariant_fuzz(
906        &mut self,
907        invariant_contract: InvariantContract<'_>,
908        fuzz_fixtures: &FuzzFixtures,
909        fuzz_state: EvmFuzzState,
910        progress: Option<&ProgressBar>,
911        early_exit: &EarlyExit,
912        initial_handler_failures: std::collections::HashMap<
913            (Address, Selector),
914            InvariantFuzzError,
915        >,
916    ) -> Result<InvariantFuzzTestResult> {
917        let campaign_spec = InvariantCampaignSpec::new(self.config.runs);
918        let worker_plans = campaign_spec.worker_plans(invariant_worker_count_with_threads(
919            &self.config,
920            rayon::current_num_threads(),
921            self.invariant_campaign_anchors,
922        ))?;
923        let actual_worker_count = worker_plans.len();
924        let campaign_seed =
925            self.prepare_campaign_seed(&invariant_contract, initial_handler_failures)?;
926        let replay_targets = FuzzRunIdentifiedContracts::new(
927            campaign_seed.targeted_contracts.clone(),
928            campaign_seed.targets_are_updatable,
929        );
930        let mut corpus_replay_executor = self.executor.clone();
931        corpus_replay_executor.inspector_mut().collect_evm_cmp_log(
932            invariant_worker_collects_evm_cmp_log(&self.config, 0, actual_worker_count),
933        );
934        let dynamic = self.dynamic_target_ctx();
935        let corpus_seed = WorkerCorpusSeed::load_from_disk(
936            &self.config.corpus,
937            None,
938            Some(&corpus_replay_executor),
939            ReplayTarget {
940                stateless: None,
941                fuzzed_contracts: Some(&replay_targets),
942                dynamic: Some(&dynamic),
943            },
944        )?;
945        let mut runner = self.runner.clone();
946        let config = self.config.clone();
947        let setup_contracts = self.setup_contracts;
948        let project_contracts = self.project_contracts;
949        let base_executor = self.executor.clone();
950        let focus_seed = invariant_focus_seed(&mut runner, self.fuzz_seed, actual_worker_count);
951        let campaign_state =
952            Arc::new(InvariantCampaignState::new(early_exit.clone(), self.config.timeout));
953
954        let frontier_test = self
955            .config
956            .corpus
957            .capture_branch_frontiers()
958            .then(|| invariant_contract.anchor().clone());
959        let mut worker_outputs = if actual_worker_count > 1 {
960            let worker_jobs = worker_plans
961                .into_iter()
962                .map(|worker_plan| {
963                    let worker_runner =
964                        invariant_worker_runner(&mut runner, worker_plan.worker_id, self.fuzz_seed);
965                    let gas_report_samples = gas_report_samples_for_worker(
966                        config.gas_report_samples,
967                        worker_plan.worker_id,
968                        actual_worker_count,
969                    );
970                    let collect_cmp_log = invariant_worker_collects_evm_cmp_log(
971                        &config,
972                        worker_plan.worker_id,
973                        actual_worker_count,
974                    );
975                    (worker_plan, worker_runner, gas_report_samples, collect_cmp_log)
976                })
977                .collect::<Vec<_>>();
978            worker_jobs
979                .into_par_iter()
980                .map(|(worker_plan, worker_runner, gas_report_samples, collect_cmp_log)| {
981                    let _guard =
982                        info_span!("invariant_worker", id = worker_plan.worker_id).entered();
983                    let timer = Instant::now();
984                    let (worker_campaign_seed, worker_corpus_seed) =
985                        campaign_seed_and_corpus_seed_for_worker(
986                            &campaign_seed,
987                            &corpus_seed,
988                            worker_plan,
989                            actual_worker_count,
990                            collect_cmp_log,
991                            focus_seed,
992                        );
993                    let output = Self::run_invariant_worker(
994                        base_executor.clone(),
995                        worker_runner,
996                        config.clone(),
997                        setup_contracts,
998                        project_contracts,
999                        worker_plan,
1000                        invariant_contract.clone(),
1001                        fuzz_fixtures,
1002                        fuzz_state.fork(),
1003                        progress,
1004                        &campaign_state,
1005                        worker_campaign_seed,
1006                        worker_corpus_seed,
1007                        actual_worker_count,
1008                        gas_report_samples,
1009                    );
1010                    if output.is_err() {
1011                        campaign_state.request_terminal_stop();
1012                    }
1013                    debug!("finished in {:?}", timer.elapsed());
1014                    output
1015                })
1016                .collect::<Result<Vec<_>>>()?
1017        } else {
1018            let worker_plan = worker_plans[0];
1019            let runner =
1020                invariant_worker_runner(&mut runner, worker_plan.worker_id, self.fuzz_seed);
1021            let gas_report_samples = config.gas_report_samples as usize;
1022            let collect_cmp_log = invariant_worker_collects_evm_cmp_log(
1023                &config,
1024                worker_plan.worker_id,
1025                actual_worker_count,
1026            );
1027            let (worker_campaign_seed, worker_corpus_seed) =
1028                campaign_seed_and_corpus_seed_for_worker(
1029                    &campaign_seed,
1030                    &corpus_seed,
1031                    worker_plan,
1032                    actual_worker_count,
1033                    collect_cmp_log,
1034                    focus_seed,
1035                );
1036            vec![Self::run_invariant_worker(
1037                base_executor,
1038                runner,
1039                config,
1040                setup_contracts,
1041                project_contracts,
1042                worker_plan,
1043                invariant_contract,
1044                fuzz_fixtures,
1045                fuzz_state,
1046                progress,
1047                &campaign_state,
1048                worker_campaign_seed,
1049                worker_corpus_seed,
1050                actual_worker_count,
1051                gas_report_samples,
1052            )?]
1053        };
1054
1055        let frontier_limit = self.config.corpus.frontier_limit;
1056        if let (Some(frontier_dir), Some(frontier_test)) =
1057            (&self.config.corpus.frontier_dir, frontier_test.as_ref())
1058        {
1059            let frontiers = merge_frontiers(
1060                frontier_limit,
1061                worker_outputs.iter_mut().flat_map(|(_, frontiers)| frontiers.drain(..)),
1062            );
1063            if !frontiers.is_empty() {
1064                let artifact = StatefulFuzzBranchFrontierArtifact::new(
1065                    frontier_test,
1066                    frontier_limit,
1067                    frontiers,
1068                );
1069                if let Err(err) = write_frontier_artifact(frontier_dir, &artifact) {
1070                    warn!(%err, path = ?frontier_dir, "failed to write fuzz branch frontier artifact");
1071                }
1072            }
1073        }
1074
1075        let mut aggregator = InvariantCampaignAggregator::new(campaign_spec);
1076        for (worker_output, _) in worker_outputs {
1077            aggregator.push(worker_output);
1078        }
1079        let result = if campaign_state.is_timed_campaign() {
1080            aggregator.finish_partial()?
1081        } else {
1082            aggregator.finish_campaign()?
1083        };
1084        persist_campaign_optimization(
1085            &self.config.corpus,
1086            result.optimization_best_value,
1087            &result.optimization_best_sequence,
1088        );
1089        Ok(result)
1090    }
1091
1092    /// Runs one worker-local slice of an invariant campaign.
1093    #[allow(clippy::too_many_arguments)]
1094    fn run_invariant_worker(
1095        mut executor: Executor<FEN>,
1096        runner: TestRunner,
1097        config: InvariantConfig,
1098        setup_contracts: &'a ContractsByAddress,
1099        project_contracts: &'a ContractsByArtifact,
1100        plan: InvariantWorkerPlan,
1101        invariant_contract: InvariantContract<'_>,
1102        fuzz_fixtures: &FuzzFixtures,
1103        fuzz_state: EvmFuzzState,
1104        progress: Option<&ProgressBar>,
1105        campaign_state: &InvariantCampaignState,
1106        campaign_seed: InvariantCampaignSeed,
1107        corpus_seed: WorkerCorpusSeed,
1108        worker_count: usize,
1109        gas_report_samples: usize,
1110    ) -> Result<(InvariantWorkerOutput, Vec<FuzzBranchFrontier>)> {
1111        // Note: invariant function signatures (no inputs) are validated upstream in the
1112        // suite runner so parameterized `invariant_*` functions are rejected with a per-test
1113        // failure entry before any campaign runs.
1114        let config = invariant_worker_config(config, plan.worker_id, worker_count);
1115        let frontier_limit = if config.corpus.capture_branch_frontiers()
1116            && invariant_worker_collects_evm_cmp_log(&config, plan.worker_id, worker_count)
1117        {
1118            config.corpus.frontier_limit
1119        } else {
1120            0
1121        };
1122        let mut frontier_recorder = FuzzFrontierRecorder::new(frontier_limit);
1123        executor.inspector_mut().set_execution_cancellation(campaign_state.cancellation().clone());
1124
1125        let (mut invariant_test, mut corpus_manager) = Self::prepare_worker(
1126            &mut executor,
1127            plan,
1128            worker_count,
1129            &invariant_contract,
1130            fuzz_fixtures,
1131            fuzz_state,
1132            &runner,
1133            &config,
1134            &campaign_seed,
1135            corpus_seed,
1136        )?;
1137        let mut runs = 0;
1138        campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1139
1140        // Invariant runs with edge coverage if corpus dir is set or showing edge coverage.
1141        let edge_coverage_enabled = config.corpus.collect_edge_coverage();
1142
1143        'stop: while should_continue_invariant_worker(campaign_state, runs, plan) {
1144            // Per-run failure count snapshot used to gate `afterInvariant` below.
1145            let failures_before_run = invariant_test.test_data.failures.invariant_count();
1146            let failures_checkpoint = invariant_test.test_data.failures.clone();
1147            let failures_revision = failures_checkpoint.revision();
1148            let mut stop_after_run = false;
1149            let mut run_cancelled = false;
1150            let mut observed_call_entries = Vec::<(Vec<ObservedCall>, BasicTxDetails)>::new();
1151
1152            let call_campaign = FuzzCampaign::new(FuzzCampaignMode::Invariant {
1153                check_interval: config.check_interval,
1154                optimization: invariant_contract.is_optimization(),
1155            });
1156
1157            let sequence_plan =
1158                corpus_manager.new_sequence(&mut invariant_test.test_data.branch_runner)?;
1159            let initial_seq = sequence_plan.initial();
1160
1161            let run_depth =
1162                invariant_run_depth(&config, &mut invariant_test.test_data.branch_runner);
1163
1164            let mut corpus_run = Option::<RecordedCallSequence>::None;
1165            let mut frontier_run = (frontier_limit > 0).then(|| RecordedCallSequence {
1166                inputs: Vec::with_capacity(run_depth as usize),
1167                cmp_seq: Vec::with_capacity(run_depth as usize),
1168            });
1169
1170            // Create current invariant run data.
1171            let mut current_run = InvariantTestRun::new(
1172                initial_seq[0].clone(),
1173                // Before each run, we must reset the backend state.
1174                executor.clone(),
1175                run_depth as usize,
1176            );
1177
1178            // We stop the run immediately if we have reverted, and `fail_on_revert` is set.
1179            if config.fail_on_revert && invariant_test.reverts() > 0 {
1180                campaign_state.request_terminal_stop();
1181                return Err(eyre!("call reverted"));
1182            }
1183
1184            let mut call_cmp_values = Vec::new();
1185            let mut call_new_coverage = false;
1186            let mut coverage_prefix_len = 0;
1187            let mut assertion_failure = false;
1188            let mut pre_merge_edges_hash = None;
1189            let mut handler = None;
1190            let mut abort_campaign = false;
1191            let sequence_outcome = call_campaign.run_sequence(
1192                &mut current_run,
1193                run_depth,
1194                |run| {
1195                    let tx = run.inputs.last_mut().expect("campaign always has a current input");
1196                    (&mut run.executor, tx)
1197                },
1198                |run| run.depth,
1199                |_| campaign_state.should_stop(),
1200                |current_run, event| {
1201                    match event {
1202                        CampaignEvent::Feedback(call_result) => {
1203                            let current_tx = current_run.inputs.last().ok_or_else(|| {
1204                                eyre!("no input generated to call fuzzed target.")
1205                            })?;
1206                            let sel_bytes: [u8; 4] = current_tx
1207                                .call_details
1208                                .calldata
1209                                .get(..4)
1210                                .and_then(|selector| selector.try_into().ok())
1211                                .unwrap_or_default();
1212                            handler =
1213                                Some((current_tx.call_details.target, Selector::from(sel_bytes)));
1214                            if let Some(fuzzer) =
1215                                current_run.executor.inspector_mut().fuzzer.as_mut()
1216                            {
1217                                invariant_test.fuzz_state.collect_fuzzer_values(fuzzer);
1218                            }
1219                            call_cmp_values = call_result.evm_cmp_values.take().unwrap_or_default();
1220                            let discarded = call_result.result.as_ref() == MAGIC_ASSUME;
1221                            if config.show_metrics {
1222                                invariant_test.record_metrics(
1223                                    current_tx,
1224                                    call_result.reverted,
1225                                    discarded,
1226                                );
1227                            }
1228                            HitMaps::merge_opt(
1229                                &mut current_run.line_coverage,
1230                                call_result.line_coverage.take(),
1231                            );
1232                            assertion_failure = !discarded
1233                                && did_fail_on_assert(call_result, &call_result.state_changeset);
1234                            pre_merge_edges_hash = assertion_failure
1235                                .then(|| error::snapshot_edge_fingerprint(call_result))
1236                                .flatten();
1237                            call_new_coverage = corpus_manager.merge_edge_coverage(call_result);
1238                            if call_new_coverage {
1239                                current_run.new_coverage = true;
1240                            }
1241                            let observed_calls = std::mem::take(&mut call_result.observed_calls);
1242                            if call_new_coverage && !observed_calls.is_empty() {
1243                                observed_call_entries.push((observed_calls, current_tx.clone()));
1244                            }
1245                        }
1246                        CampaignEvent::Check { result, kind, should_check } => {
1247                            let mut result =
1248                                result.take().expect("campaign check result is available");
1249                            if kind == CampaignCallKind::AssumptionRejected {
1250                                current_run.inputs.pop();
1251                                current_run.rejects += 1;
1252                                if current_run.rejects > config.max_assume_rejects {
1253                                    invariant_test.set_error(
1254                                        invariant_contract.anchor(),
1255                                        InvariantFuzzError::MaxAssumeRejects(
1256                                            config.max_assume_rejects,
1257                                        ),
1258                                    );
1259                                    campaign_state.request_terminal_stop();
1260                                    abort_campaign = true;
1261                                    return Ok(CampaignControl::Stop);
1262                                }
1263                                return Ok(CampaignControl::Continue);
1264                            }
1265                            debug_assert_eq!(kind, CampaignCallKind::Accepted);
1266                            if let Some(frontier_run) = &mut frontier_run {
1267                                frontier_run.inputs.push(
1268                                    current_run
1269                                        .inputs
1270                                        .last()
1271                                        .expect("accepted call has a campaign input")
1272                                        .clone(),
1273                                );
1274                                frontier_run.cmp_seq.push(call_cmp_values.clone());
1275                            }
1276                            if config.corpus.is_coverage_guided() {
1277                                let preserve_revert =
1278                                    invariant_contract.is_optimization() || config.has_delay();
1279                                let retain_for_corpus =
1280                                    !result.reverted || preserve_revert || call_new_coverage;
1281                                if let Some(corpus_run) = &mut corpus_run {
1282                                    if retain_for_corpus {
1283                                        corpus_run.inputs.push(
1284                                            current_run
1285                                                .inputs
1286                                                .last()
1287                                                .expect("accepted call has a campaign input")
1288                                                .clone(),
1289                                        );
1290                                        corpus_run
1291                                            .cmp_seq
1292                                            .push(std::mem::take(&mut call_cmp_values));
1293                                    }
1294                                } else if result.reverted && !preserve_revert && call_new_coverage {
1295                                    let mut cmp_seq = std::mem::take(&mut current_run.cmp_seq);
1296                                    cmp_seq.push(std::mem::take(&mut call_cmp_values));
1297                                    corpus_run = Some(RecordedCallSequence {
1298                                        inputs: current_run.inputs.clone(),
1299                                        cmp_seq,
1300                                    });
1301                                } else if !result.reverted || preserve_revert {
1302                                    current_run.cmp_seq.push(std::mem::take(&mut call_cmp_values));
1303                                }
1304                                if call_new_coverage {
1305                                    coverage_prefix_len = corpus_run
1306                                        .as_ref()
1307                                        .map_or(current_run.inputs.len(), |history| {
1308                                            history.inputs.len()
1309                                        });
1310                                }
1311                            }
1312                            let (handler_target, handler_selector) =
1313                                handler.take().expect("feedback precedes campaign checks");
1314                            let mut state_changeset = std::mem::take(&mut result.state_changeset);
1315                            if !result.reverted {
1316                                let mapping_slots = current_run
1317                                    .executor
1318                                    .inspector()
1319                                    .fuzzer
1320                                    .as_ref()
1321                                    .and_then(|fuzzer| fuzzer.mapping_slots.as_ref());
1322                                collect_data(
1323                                    &invariant_test,
1324                                    &mut state_changeset,
1325                                    current_run.inputs.last().expect("checked above"),
1326                                    &result,
1327                                    run_depth,
1328                                    mapping_slots,
1329                                );
1330                            }
1331
1332                            let created_before = current_run.created_contracts.len();
1333                            if let Err(error) =
1334                                &invariant_test.targeted_contracts.collect_created_contracts(
1335                                    &state_changeset,
1336                                    project_contracts,
1337                                    setup_contracts,
1338                                    &campaign_seed.artifact_filters,
1339                                    &mut current_run.created_contracts,
1340                                )
1341                            {
1342                                warn!(target: "forge::test", "{error}");
1343                            }
1344                            invariant_test.invalidate_metric_key_cache(
1345                                &current_run.created_contracts[created_before..],
1346                            );
1347                            current_run
1348                                .fuzz_runs
1349                                .push(FuzzCase { gas: result.gas_used, stipend: result.stipend });
1350
1351                            let continues = if should_check {
1352                                let outcome = can_continue(
1353                                    &invariant_contract,
1354                                    &mut invariant_test,
1355                                    current_run,
1356                                    &config,
1357                                    result,
1358                                    &state_changeset,
1359                                    handler_target,
1360                                    handler_selector,
1361                                    assertion_failure,
1362                                    pre_merge_edges_hash,
1363                                )
1364                                .map_err(|error| eyre!(error.to_string()))?;
1365                                run_cancelled = outcome.cancelled;
1366                                outcome.continues
1367                            } else {
1368                                if result.reverted {
1369                                    invariant_test.test_data.failures.reverts += 1;
1370                                }
1371                                if assertion_failure {
1372                                    let call_reverted = result.reverted;
1373                                    error::record_handler_assertion_bug(
1374                                        &invariant_contract,
1375                                        &config,
1376                                        &invariant_test.targeted_contracts,
1377                                        &mut invariant_test.test_data.failures,
1378                                        &mut current_run.inputs,
1379                                        handler_target,
1380                                        handler_selector,
1381                                        pre_merge_edges_hash,
1382                                        result,
1383                                        call_reverted,
1384                                        invariant_contract.is_optimization(),
1385                                    );
1386                                    true
1387                                } else if result.reverted && config.fail_on_revert {
1388                                    let anchor = invariant_contract.anchor();
1389                                    let case_data = error::InvariantRunCtx {
1390                                        contract: &invariant_contract,
1391                                        config: &config,
1392                                        targeted_contracts: &invariant_test.targeted_contracts,
1393                                        calldata: &current_run.inputs,
1394                                    }
1395                                    .failed_case(anchor, config.fail_on_revert, false, result, &[]);
1396                                    invariant_test.test_data.failures.record_failure(
1397                                        anchor,
1398                                        InvariantFuzzError::Revert(case_data),
1399                                    );
1400                                    false
1401                                } else {
1402                                    if result.reverted
1403                                        && !invariant_contract.is_optimization()
1404                                        && !config.has_delay()
1405                                    {
1406                                        current_run.inputs.pop();
1407                                    }
1408                                    true
1409                                }
1410                            };
1411
1412                            if run_cancelled {
1413                                return Ok(CampaignControl::Stop);
1414                            }
1415                            if !continues || current_run.depth == run_depth - 1 {
1416                                current_run.save_last_run_inputs = true;
1417                            }
1418                            if !continues {
1419                                if invariant_contract.invariant_fns.len() == 1
1420                                    || config.fail_on_revert
1421                                {
1422                                    campaign_state.request_terminal_stop();
1423                                    stop_after_run = true;
1424                                }
1425                                return Ok(CampaignControl::Stop);
1426                            }
1427                        }
1428                        CampaignEvent::Advance => current_run.depth += 1,
1429                        CampaignEvent::Next { discarded, depth } => {
1430                            current_run.inputs.push(sequence_plan.next(
1431                                &mut invariant_test.test_data.branch_runner,
1432                                discarded,
1433                                depth as usize,
1434                            )?);
1435                        }
1436                        CampaignEvent::PostCheck => {
1437                            // Multi-predicate campaigns keep running after earlier failures, but
1438                            // the hook must still execute on subsequent clean runs.
1439                            if !abort_campaign
1440                                && !run_cancelled
1441                                && invariant_contract.call_after_invariant
1442                                && invariant_test.test_data.failures.invariant_count()
1443                                    == failures_before_run
1444                            {
1445                                let (broken, hook_cancelled) = assert_after_invariant(
1446                                    &invariant_contract,
1447                                    &mut invariant_test,
1448                                    current_run,
1449                                    &config,
1450                                )
1451                                .map_err(|_| eyre!("Failed to call afterInvariant"))?;
1452                                if hook_cancelled {
1453                                    run_cancelled = true;
1454                                } else if broken.is_some() {
1455                                    current_run.save_last_run_inputs = true;
1456                                }
1457                            }
1458                        }
1459                    }
1460                    Ok(CampaignControl::Continue)
1461                },
1462            )?;
1463            if sequence_outcome == CampaignSequenceOutcome::Cancelled {
1464                // A timed-out partial run remains successful, matching the previous worker
1465                // behavior, but it must not be persisted or counted.
1466                run_cancelled = true;
1467            }
1468            if abort_campaign {
1469                break 'stop;
1470            }
1471
1472            // The worker which requested a terminal stop for its own failure still owns a
1473            // complete failing run. All other campaign stops discard the partial run before any
1474            // corpus persistence or accounting.
1475            if campaign_state.should_stop() && !stop_after_run {
1476                run_cancelled = true;
1477            }
1478
1479            if run_cancelled {
1480                let completed_finding =
1481                    invariant_test.test_data.failures.revision() != failures_revision;
1482                if completed_finding {
1483                    record_new_invariant_failures(
1484                        campaign_state,
1485                        &invariant_contract,
1486                        &invariant_test.test_data.failures,
1487                    );
1488                    campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1489                } else {
1490                    invariant_test.test_data.failures.clone_from(&failures_checkpoint);
1491                }
1492                break 'stop;
1493            }
1494
1495            if invariant_test.test_data.failures.invariant_count() > failures_before_run {
1496                record_new_invariant_failures(
1497                    campaign_state,
1498                    &invariant_contract,
1499                    &invariant_test.test_data.failures,
1500                );
1501            }
1502            if invariant_test.test_data.failures.handler_count()
1503                > failures_checkpoint.handler_count()
1504            {
1505                campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1506            }
1507
1508            for (observed_calls, parent_tx) in observed_call_entries {
1509                corpus_manager.hoist_observed_calls(
1510                    &observed_calls,
1511                    &parent_tx,
1512                    &invariant_test.targeted_contracts,
1513                    CorpusInsertionMode::Live,
1514                );
1515            }
1516
1517            // Extend corpus only after the run and its optional hook have completed.
1518            if let Some(frontier_run) = &frontier_run {
1519                frontier_recorder.capture_sequence(&frontier_run.inputs, &frontier_run.cmp_seq);
1520            }
1521            let optimization = current_run.optimization_value.map(|v| {
1522                let prefix = current_run.inputs[..current_run.optimization_prefix_len].to_vec();
1523                (v, prefix)
1524            });
1525            let mut corpus_inputs = corpus_run
1526                .as_ref()
1527                .map_or(current_run.inputs.as_slice(), |history| history.inputs.as_slice());
1528            let mut corpus_cmp_seq = corpus_run
1529                .as_ref()
1530                .map_or(current_run.cmp_seq.as_slice(), |history| history.cmp_seq.as_slice());
1531            if current_run.new_coverage && !invariant_contract.is_optimization() {
1532                corpus_inputs = &corpus_inputs[..coverage_prefix_len];
1533                corpus_cmp_seq = &corpus_cmp_seq[..coverage_prefix_len];
1534            }
1535            if worker_count > 1 {
1536                corpus_manager.process_inputs_for_campaign(
1537                    corpus_inputs,
1538                    corpus_cmp_seq,
1539                    current_run.new_coverage,
1540                    optimization,
1541                );
1542            } else {
1543                corpus_manager.process_inputs(
1544                    corpus_inputs,
1545                    corpus_cmp_seq,
1546                    current_run.new_coverage,
1547                    optimization,
1548                );
1549            }
1550
1551            // End current invariant test run.
1552            if current_run.save_last_run_inputs {
1553                invariant_test.set_last_run_inputs(&current_run.inputs);
1554            }
1555            if let Some(value) = current_run.optimization_value {
1556                invariant_test.update_optimization_value(
1557                    value,
1558                    &current_run.inputs[..current_run.optimization_prefix_len],
1559                );
1560            }
1561            invariant_test.merge_line_coverage(current_run.line_coverage.take());
1562            for fuzz_run in &current_run.fuzz_runs {
1563                campaign_state.record_call(fuzz_run.gas);
1564            }
1565            current_run.drop_corpus_payloads();
1566            invariant_test.end_run(current_run, gas_report_samples);
1567            runs += 1;
1568            let total_runs = campaign_state.increment_runs();
1569            debug_assert!(
1570                campaign_state.is_timed_campaign() || total_runs <= config.runs,
1571                "worker runs were not distributed correctly"
1572            );
1573            if let Some(progress) = progress {
1574                progress.inc(1);
1575                campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1576                // Display current best value, corpus metrics, and failure counts.
1577                let best = invariant_test.test_data.optimization_best_value;
1578                let failure_metrics = campaign_state.failure_metrics();
1579                let broken = failure_metrics.unique_failures.len();
1580                let handler_bugs = failure_metrics.broken_handlers;
1581                let total_invariants = invariant_contract.invariant_fns.len();
1582                if edge_coverage_enabled || best.is_some() || broken > 0 || handler_bugs > 0 {
1583                    let mut msg = String::new();
1584                    if let Some(best) = best {
1585                        msg.push_str(&format!("best: {best}"));
1586                    }
1587                    if edge_coverage_enabled {
1588                        if !msg.is_empty() {
1589                            msg.push_str(", ");
1590                        }
1591                        msg.push_str(&format!("{}", corpus_manager.metrics));
1592                        match corpus_manager.time_since_new_edge() {
1593                            Some(elapsed) => msg.push_str(&format!(
1594                                "\n        - time since new edge: {:.1}s",
1595                                elapsed.as_secs_f64()
1596                            )),
1597                            None => msg.push_str("\n        - time since new edge: never"),
1598                        }
1599                    }
1600                    if broken > 0 {
1601                        if !msg.is_empty() {
1602                            msg.push_str(", ");
1603                        }
1604                        msg.push_str(&format!("❌ {broken}/{total_invariants} broken"));
1605                    }
1606                    if handler_bugs > 0 {
1607                        if !msg.is_empty() {
1608                            msg.push_str(", ");
1609                        }
1610                        msg.push_str(&format!("⚠ {handler_bugs} handler bug(s)"));
1611                    }
1612                    let msg =
1613                        if worker_count > 1 { format!("[w{}] {msg}", plan.worker_id) } else { msg };
1614                    progress.set_message(msg);
1615                }
1616            } else if edge_coverage_enabled
1617                && campaign_state.should_emit_metrics_report(DURATION_BETWEEN_METRICS_REPORT)
1618            {
1619                campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1620                let failure_metrics = campaign_state.failure_metrics();
1621                let (total_txs, total_gas) = campaign_state.throughput_totals();
1622                let throughput = InvariantThroughputMetrics { total_txs, total_gas };
1623                // Display corpus metrics inline as JSON.
1624                let metrics = build_invariant_progress_json(
1625                    InvariantProgressContext {
1626                        timestamp_secs: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
1627                        contract_name: invariant_contract.name,
1628                        optimization_best: invariant_test.test_data.optimization_best_value,
1629                        throughput,
1630                        elapsed: campaign_state.elapsed(),
1631                        worker_id: plan.worker_id,
1632                        worker_count,
1633                        time_since_new_edge: corpus_manager.time_since_new_edge(),
1634                    },
1635                    &corpus_manager.metrics,
1636                    &failure_metrics,
1637                );
1638                let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
1639            }
1640
1641            if stop_after_run {
1642                break 'stop;
1643            }
1644        }
1645
1646        trace!(?fuzz_fixtures);
1647        invariant_test.fuzz_state.log_stats();
1648
1649        // Campaign-local terminal stops and deadlines must not suppress post-campaign shrinking.
1650        executor.inspector_mut().set_early_exit(campaign_state.early_exit().clone());
1651        Self::shrink_handler_failures(
1652            &config,
1653            &executor,
1654            &mut invariant_test.test_data,
1655            progress,
1656            campaign_state.early_exit(),
1657        );
1658
1659        // Move out the final test data and drop worker-local fuzz state before returning this
1660        // worker's aggregate output. Long invariant campaigns can leave large dictionaries and
1661        // target state behind; once shrinking is complete, only `test_data` is needed.
1662        let InvariantTest { fuzz_state: _, targeted_contracts: _, test_data: result } =
1663            invariant_test;
1664        let reverts = result.failures.reverts;
1665        let (errors, handler_errors) = result.failures.partition();
1666        let worker_result = InvariantFuzzTestResult::new(
1667            errors,
1668            handler_errors,
1669            result.runs,
1670            result.calls,
1671            reverts,
1672            result.last_run_inputs,
1673            result.gas_report_traces,
1674            result.line_coverage,
1675            result.metrics.into_iter().collect(),
1676            if plan.worker_id == 0 { corpus_manager.failed_replays } else { 0 },
1677            1,
1678            result.optimization_best_value,
1679            result.optimization_best_sequence,
1680        );
1681        drop(corpus_manager);
1682        let reported_plan = if campaign_state.is_timed_campaign() {
1683            InvariantWorkerPlan { runs, ..plan }
1684        } else {
1685            // Sharded campaigns must report the original assigned range. Early worker exit changes
1686            // the number of executed runs, but it must not shrink `plan.runs`: following workers'
1687            // `first_global_run` offsets were computed from the original partition.
1688            plan
1689        };
1690        Ok((
1691            InvariantWorkerOutput { plan: reported_plan, result: worker_result },
1692            frontier_recorder.into_frontiers(),
1693        ))
1694    }
1695
1696    fn shrink_handler_failures(
1697        config: &InvariantConfig,
1698        executor: &Executor<FEN>,
1699        result: &mut InvariantTestData,
1700        progress: Option<&ProgressBar>,
1701        early_exit: &EarlyExit,
1702    ) {
1703        let total = result.failures.handler_count();
1704        if total == 0 {
1705            return;
1706        }
1707
1708        for (idx, error) in result.failures.handler_failures_mut().enumerate() {
1709            if early_exit.should_stop() {
1710                break;
1711            }
1712            let Some(failure) = error.as_handler_assertion_mut() else {
1713                continue;
1714            };
1715            let shrink_progress = shrink::ShrinkProgress::new(
1716                config,
1717                progress,
1718                &format!("handler {:#x}::{}", failure.reverter, failure.selector),
1719                Some((idx + 1, total)),
1720                None,
1721                false,
1722            );
1723            match shrink::shrink_handler_sequence(
1724                config,
1725                &failure.call_sequence,
1726                failure.edge_fingerprint,
1727                executor,
1728                &shrink_progress,
1729                early_exit,
1730            ) {
1731                Ok(shrunk) if !shrunk.is_empty() => {
1732                    failure.call_sequence = shrunk;
1733                }
1734                Ok(_) => {}
1735                Err(e) => trace!(target: "forge::test", "handler shrink failed: {e}"),
1736            }
1737        }
1738    }
1739
1740    fn prepare_campaign_seed(
1741        &mut self,
1742        invariant_contract: &InvariantContract<'_>,
1743        initial_handler_failures: std::collections::HashMap<
1744            (Address, Selector),
1745            InvariantFuzzError,
1746        >,
1747    ) -> Result<InvariantCampaignSeed> {
1748        self.select_contract_artifacts(invariant_contract.address)?;
1749        let (sender_filters, targeted_contracts) =
1750            self.select_contracts_and_senders(invariant_contract.address)?;
1751        let targets_are_updatable = targeted_contracts.is_updatable;
1752        let targeted_contracts = targeted_contracts.targets().clone();
1753
1754        Ok(InvariantCampaignSeed {
1755            artifact_filters: self.artifact_filters.clone(),
1756            sender_filters,
1757            targeted_contracts,
1758            targets_are_updatable,
1759            initial_handler_failures,
1760        })
1761    }
1762
1763    /// Prepares worker-local structures to execute an invariant campaign slice.
1764    #[allow(clippy::too_many_arguments)]
1765    fn prepare_worker(
1766        executor: &mut Executor<FEN>,
1767        plan: InvariantWorkerPlan,
1768        worker_count: usize,
1769        invariant_contract: &InvariantContract<'_>,
1770        fuzz_fixtures: &FuzzFixtures,
1771        fuzz_state: EvmFuzzState,
1772        runner: &TestRunner,
1773        config: &InvariantConfig,
1774        campaign_seed: &InvariantCampaignSeed,
1775        corpus_seed: WorkerCorpusSeed,
1776    ) -> Result<(InvariantTest, WorkerCorpus)> {
1777        let fuzz_state = fuzz_state.into_invariant();
1778        let targeted_contracts = FuzzRunIdentifiedContracts::new(
1779            campaign_seed.targeted_contracts.clone(),
1780            campaign_seed.targets_are_updatable,
1781        );
1782        executor.inspector_mut().collect_evm_cmp_log(invariant_worker_collects_evm_cmp_log(
1783            config,
1784            plan.worker_id,
1785            worker_count,
1786        ));
1787
1788        // Creates the invariant strategy.
1789        let generator = TxGenerator::invariant(
1790            fuzz_state.clone(),
1791            campaign_seed.sender_filters.clone(),
1792            targeted_contracts.clone(),
1793            config.clone(),
1794            fuzz_fixtures.clone(),
1795        );
1796
1797        // If any of the targeted contracts have the storage layout enabled then we can sample
1798        // mapping values. To accomplish, we need to record the mapping storage slots and keys.
1799        let mapping_slots = targeted_contracts
1800            .targets()
1801            .iter()
1802            .any(|(_, t)| t.storage_layout.is_some())
1803            .then(AddressMap::default);
1804
1805        // Set up fuzzer WITHOUT call_generator initially.
1806        // We defer call_override until after the initial invariant check to avoid
1807        // injecting random calls during setup which would break the invariant assertion.
1808        let extra_cheatcode_addresses = executor.inspector().extra_cheatcode_addresses();
1809        executor.inspector_mut().set_fuzzer(
1810            Fuzzer::new(config.dictionary.max_fuzz_dictionary_values, mapping_slots)
1811                .with_extra_cheatcode_addresses(extra_cheatcode_addresses)
1812                .with_call_recording(config.corpus.is_coverage_guided()),
1813        );
1814
1815        // Let's make sure the invariant is sound before actually starting the run:
1816        // We'll assert the invariant in its initial state, and if it fails, we'll
1817        // already know if we can early exit the invariant run.
1818        // This does not count as a fuzz run. It will just register the revert.
1819        let mut failures = InvariantFailures::new();
1820        // Seed disk-recovered handler bugs so live counters reflect them from tick 0.
1821        for (&(addr, sel), err) in &campaign_seed.initial_handler_failures {
1822            failures.seed_handler_failure(addr, sel, err.clone());
1823        }
1824        invariant_preflight_check(
1825            invariant_contract,
1826            config,
1827            &targeted_contracts,
1828            executor,
1829            &[],
1830            &mut failures,
1831        )?;
1832        if let Some(fuzzer) = executor.inspector_mut().fuzzer.as_mut() {
1833            fuzz_state.collect_fuzzer_values(fuzzer);
1834            let _ = fuzzer.take_observed_calls();
1835        }
1836        let generator = foundry_evm_fuzz::sequence::SequenceGenerator::invariant_with_fixtures(
1837            generator,
1838            fuzz_state.clone(),
1839            fuzz_fixtures.clone(),
1840            targeted_contracts.clone(),
1841            &config.corpus,
1842        )?;
1843        let mut worker = WorkerCorpus::from_seed(
1844            plan.worker_id as usize,
1845            config.corpus.clone(),
1846            generator,
1847            corpus_seed,
1848        )?;
1849
1850        if let Err(err) =
1851            worker.seed_from_test_traces(invariant_contract, &targeted_contracts, executor)
1852        {
1853            debug!(target: "corpus", %err, "failed to seed corpus from test traces");
1854        }
1855
1856        // NOW enable call_override after the initial invariant check and corpus trace seeding have
1857        // passed. This allows `override_call_strat` to inject calls during actual fuzz runs for
1858        // reentrancy vulnerability detection.
1859        if config.call_override {
1860            let target_contract_ref = Arc::new(RwLock::new(Address::ZERO));
1861
1862            // Collect handler addresses - these are the contracts we want to inject
1863            // reentrancy into (simulating malicious receive() functions).
1864            let handler_addresses: AddressSet =
1865                targeted_contracts.targets().keys().copied().collect();
1866            let override_targets = targeted_contracts
1867                .targets()
1868                .iter()
1869                .filter_map(|(address, contract)| {
1870                    let functions = contract.abi_fuzzed_functions().cloned().collect::<Vec<_>>();
1871                    (!functions.is_empty()).then_some((*address, functions))
1872                })
1873                .collect::<Vec<_>>();
1874
1875            let call_generator = RandomCallGenerator::new(
1876                invariant_contract.address,
1877                handler_addresses,
1878                runner.clone(),
1879                override_call_strat(
1880                    fuzz_state.snapshot(),
1881                    override_targets,
1882                    target_contract_ref.clone(),
1883                    fuzz_fixtures.clone(),
1884                    config.dictionary.dictionary_weight,
1885                    config.corpus.payable_value_weight,
1886                ),
1887                target_contract_ref,
1888            );
1889
1890            if let Some(fuzzer) = executor.inspector_mut().fuzzer.as_mut() {
1891                fuzzer.call_generator = Some(call_generator);
1892            }
1893        }
1894
1895        let mut invariant_test =
1896            InvariantTest::new(fuzz_state, targeted_contracts, failures, runner.clone());
1897
1898        // Seed invariant test with previously persisted optimization state,
1899        // but only if the current invariant is in optimization mode. Persisted optimization state
1900        // is a master-worker artifact loaded with the initial corpus.
1901        if invariant_contract.is_optimization() {
1902            let (opt_best_value, opt_best_sequence) = worker.optimization_initial_state();
1903            if let Some(value) = opt_best_value {
1904                invariant_test.update_optimization_value(value, &opt_best_sequence);
1905            }
1906        }
1907
1908        Ok((invariant_test, worker))
1909    }
1910
1911    /// Fills the `InvariantExecutor` with the artifact identifier filters (in `path:name` string
1912    /// format). They will be used to filter contracts after the `setUp`, and more importantly,
1913    /// during the runs.
1914    ///
1915    /// Also excludes any contract without any mutable functions.
1916    ///
1917    /// Priority:
1918    ///
1919    /// targetArtifactSelectors > excludeArtifacts > targetArtifacts
1920    pub fn select_contract_artifacts(&mut self, invariant_address: Address) -> Result<()> {
1921        let targeted_artifact_selectors = self
1922            .executor
1923            .call_sol_default(invariant_address, &IInvariantTest::targetArtifactSelectorsCall {});
1924
1925        // Insert them into the executor `targeted_abi`.
1926        for IInvariantTest::FuzzArtifactSelector { artifact, selectors } in
1927            targeted_artifact_selectors
1928        {
1929            let identifier = self.validate_selected_contract(artifact, &selectors)?;
1930            self.artifact_filters.targeted.entry(identifier).or_default().extend(selectors);
1931        }
1932
1933        let targeted_artifacts = self
1934            .executor
1935            .call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {});
1936        let excluded_artifacts = self
1937            .executor
1938            .call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {});
1939
1940        // Insert `excludeArtifacts` into the executor `excluded_abi`.
1941        for contract in excluded_artifacts {
1942            let identifier = self.validate_selected_contract(contract, &[])?;
1943
1944            if !self.artifact_filters.excluded.contains(&identifier) {
1945                self.artifact_filters.excluded.push(identifier);
1946            }
1947        }
1948
1949        // Exclude any artifact without mutable functions.
1950        for (artifact, contract) in self.project_contracts.iter() {
1951            if contract
1952                .abi
1953                .functions()
1954                .filter(|func| {
1955                    !matches!(
1956                        func.state_mutability,
1957                        alloy_json_abi::StateMutability::Pure
1958                            | alloy_json_abi::StateMutability::View
1959                    )
1960                })
1961                .count()
1962                == 0
1963                && !self.artifact_filters.excluded.contains(&artifact.identifier())
1964            {
1965                self.artifact_filters.excluded.push(artifact.identifier());
1966            }
1967        }
1968
1969        // Insert `targetArtifacts` into the executor `targeted_abi`, if they have not been seen
1970        // before.
1971        for contract in targeted_artifacts {
1972            let identifier = self.validate_selected_contract(contract, &[])?;
1973
1974            if !self.artifact_filters.targeted.contains_key(&identifier)
1975                && !self.artifact_filters.excluded.contains(&identifier)
1976            {
1977                self.artifact_filters.targeted.insert(identifier, vec![]);
1978            }
1979        }
1980        Ok(())
1981    }
1982
1983    /// Makes sure that the contract exists in the project. If so, it returns its artifact
1984    /// identifier.
1985    fn validate_selected_contract(
1986        &mut self,
1987        contract: String,
1988        selectors: &[FixedBytes<4>],
1989    ) -> Result<String> {
1990        if let Some((artifact, contract_data)) =
1991            self.project_contracts.find_by_name_or_identifier(&contract)?
1992        {
1993            // Check that the selectors really exist for this contract.
1994            for selector in selectors {
1995                contract_data
1996                    .abi
1997                    .functions()
1998                    .find(|func| func.selector().as_slice() == selector.as_slice())
1999                    .wrap_err(format!("{contract} does not have the selector {selector:?}"))?;
2000            }
2001
2002            return Ok(artifact.identifier());
2003        }
2004        eyre::bail!(
2005            "{contract} not found in the project. Allowed format: `contract_name` or `contract_path:contract_name`."
2006        );
2007    }
2008
2009    /// Selects senders and contracts based on the contract methods `targetSenders() -> address[]`,
2010    /// `targetContracts() -> address[]` and `excludeContracts() -> address[]`.
2011    pub fn select_contracts_and_senders(
2012        &self,
2013        to: Address,
2014    ) -> Result<(SenderFilters, FuzzRunIdentifiedContracts)> {
2015        let targeted_senders =
2016            self.executor.call_sol_default(to, &IInvariantTest::targetSendersCall {});
2017        let mut excluded_senders =
2018            self.executor.call_sol_default(to, &IInvariantTest::excludeSendersCall {});
2019        // Extend with default excluded addresses - https://github.com/foundry-rs/foundry/issues/4163
2020        excluded_senders.extend([
2021            CHEATCODE_ADDRESS,
2022            HARDHAT_CONSOLE_ADDRESS,
2023            DEFAULT_CREATE2_DEPLOYER,
2024        ]);
2025        // Extend with precompiles - https://github.com/foundry-rs/foundry/issues/4287
2026        excluded_senders.extend(PRECOMPILES);
2027        let sender_filters = SenderFilters::new(targeted_senders, excluded_senders);
2028
2029        let selected = self.executor.call_sol_default(to, &IInvariantTest::targetContractsCall {});
2030        let excluded = self.executor.call_sol_default(to, &IInvariantTest::excludeContractsCall {});
2031
2032        let contracts = self
2033            .setup_contracts
2034            .iter()
2035            .filter(|&(addr, (identifier, _))| {
2036                // Include to address if explicitly set as target.
2037                if *addr == to && selected.contains(&to) {
2038                    return true;
2039                }
2040
2041                *addr != to
2042                    && *addr != CHEATCODE_ADDRESS
2043                    && *addr != HARDHAT_CONSOLE_ADDRESS
2044                    && (selected.is_empty() || selected.contains(addr))
2045                    && (excluded.is_empty() || !excluded.contains(addr))
2046                    && self.artifact_filters.matches(identifier)
2047            })
2048            .map(|(addr, (identifier, abi))| {
2049                (
2050                    *addr,
2051                    TargetedContract::new(identifier.clone(), abi.clone())
2052                        .with_project_contracts(self.project_contracts),
2053                )
2054            })
2055            .collect();
2056        let mut contracts = TargetedContracts { inner: contracts };
2057
2058        self.target_interfaces(to, &mut contracts)?;
2059
2060        self.select_selectors(to, &mut contracts)?;
2061        self.exclude_default_storage_hook_callbacks(&mut contracts)?;
2062
2063        // There should be at least one contract identified as target for fuzz runs.
2064        if contracts.is_empty() {
2065            eyre::bail!("No contracts to fuzz.");
2066        }
2067        if contracts.fuzzed_functions().next().is_none() {
2068            eyre::bail!("No functions to fuzz.");
2069        }
2070
2071        Ok((sender_filters, FuzzRunIdentifiedContracts::new(contracts, selected.is_empty())))
2072    }
2073
2074    /// Excludes registered storage-hook callbacks from implicit invariant targets.
2075    ///
2076    /// Explicit selector filters take precedence, so users can still target a callback
2077    /// intentionally.
2078    fn exclude_default_storage_hook_callbacks(
2079        &self,
2080        targeted_contracts: &mut TargetedContracts,
2081    ) -> Result<()> {
2082        let Some(cheatcodes) = self.executor.inspector().cheatcodes.as_deref() else {
2083            return Ok(());
2084        };
2085        let callbacks =
2086            cheatcodes
2087                .storage_load_hooks()
2088                .chain(cheatcodes.storage_store_hooks())
2089                .map(|(_, hook)| (hook.callback_target, Selector::from(hook.callback_selector)))
2090                .chain(cheatcodes.mapping_storage_store_hooks().map(|(_, _, hook)| {
2091                    (hook.callback_target, Selector::from(hook.callback_selector))
2092                }))
2093                .collect::<HashSet<_>>();
2094
2095        for (target, selector) in callbacks {
2096            let Some(contract) = targeted_contracts.get_mut(&target) else { continue };
2097            if !contract.targeted_functions.is_empty()
2098                || contract.function_by_selector(selector).is_none()
2099            {
2100                continue;
2101            }
2102            contract.add_selectors([selector], true)?;
2103        }
2104        Ok(())
2105    }
2106
2107    /// Extends the contracts and selectors to fuzz with the addresses and ABIs specified in
2108    /// `targetInterfaces() -> (address, string[])[]`. Enables targeting of addresses that are
2109    /// not deployed during `setUp` such as when fuzzing in a forked environment. Also enables
2110    /// targeting of delegate proxies and contracts deployed with `create` or `create2`.
2111    pub fn target_interfaces(
2112        &self,
2113        invariant_address: Address,
2114        targeted_contracts: &mut TargetedContracts,
2115    ) -> Result<()> {
2116        let interfaces = self
2117            .executor
2118            .call_sol_default(invariant_address, &IInvariantTest::targetInterfacesCall {});
2119
2120        // Since `targetInterfaces` returns a tuple array there is no guarantee
2121        // that the addresses are unique this map is used to merge functions of
2122        // the specified interfaces for the same address. For example:
2123        // `[(addr1, ["IERC20", "IOwnable"])]` and `[(addr1, ["IERC20"]), (addr1, ("IOwnable"))]`
2124        // should be equivalent.
2125        let mut combined = TargetedContracts::new();
2126
2127        // Loop through each address and its associated artifact identifiers.
2128        // We're borrowing here to avoid taking full ownership.
2129        for IInvariantTest::FuzzInterface { addr, artifacts } in &interfaces {
2130            // Identifiers are specified as an array, so we loop through them.
2131            for identifier in artifacts {
2132                // Try to find the contract by name or identifier in the project's contracts.
2133                if let Some((_, contract_data)) =
2134                    self.project_contracts.iter().find(|(artifact, _)| {
2135                        &artifact.name == identifier || &artifact.identifier() == identifier
2136                    })
2137                {
2138                    let abi = &contract_data.abi;
2139                    combined
2140                        // Check if there's an entry for the given key in the 'combined' map.
2141                        .entry(*addr)
2142                        // If the entry exists, extends its ABI with the function list.
2143                        .and_modify(|entry| {
2144                            // Extend the ABI's function list with the new functions.
2145                            entry.abi.functions.extend(abi.functions.clone());
2146                            entry.rebuild_function_lookups();
2147                        })
2148                        // Otherwise insert it into the map.
2149                        .or_insert_with(|| {
2150                            let mut contract =
2151                                TargetedContract::new(identifier.clone(), abi.clone());
2152                            contract.storage_layout =
2153                                contract_data.storage_layout.as_ref().map(Arc::clone);
2154                            contract
2155                        });
2156                }
2157            }
2158        }
2159
2160        targeted_contracts.extend(combined.inner);
2161
2162        Ok(())
2163    }
2164
2165    /// Selects the functions to fuzz based on the contract method `targetSelectors()` and
2166    /// `targetArtifactSelectors()`.
2167    pub fn select_selectors(
2168        &self,
2169        address: Address,
2170        targeted_contracts: &mut TargetedContracts,
2171    ) -> Result<()> {
2172        for (address, (identifier, _)) in self.setup_contracts {
2173            if let Some(selectors) = self.artifact_filters.targeted.get(identifier) {
2174                self.add_address_with_functions(*address, selectors, false, targeted_contracts)?;
2175            }
2176        }
2177
2178        let mut target_test_selectors = vec![];
2179        let mut excluded_test_selectors = vec![];
2180
2181        // Collect contract functions marked as target for fuzzing campaign.
2182        let selectors =
2183            self.executor.call_sol_default(address, &IInvariantTest::targetSelectorsCall {});
2184        for IInvariantTest::FuzzSelector { addr, selectors } in selectors {
2185            if addr == address {
2186                target_test_selectors = selectors.clone();
2187            }
2188            self.add_address_with_functions(addr, &selectors, false, targeted_contracts)?;
2189        }
2190
2191        // Collect contract functions excluded from fuzzing campaign.
2192        let excluded_selectors =
2193            self.executor.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {});
2194        for IInvariantTest::FuzzSelector { addr, selectors } in excluded_selectors {
2195            if addr == address {
2196                // If fuzz selector address is the test contract, then record selectors to be
2197                // later excluded if needed.
2198                excluded_test_selectors = selectors.clone();
2199            }
2200            self.add_address_with_functions(addr, &selectors, true, targeted_contracts)?;
2201        }
2202
2203        if target_test_selectors.is_empty()
2204            && let Some(target) = targeted_contracts.get(&address)
2205        {
2206            // If test contract is marked as a target and no target selector explicitly set, then
2207            // include only state-changing functions that are not reserved and selectors that are
2208            // not explicitly excluded.
2209            let selectors: Vec<_> = target
2210                .abi
2211                .functions()
2212                .filter_map(|func| {
2213                    if matches!(
2214                        func.state_mutability,
2215                        alloy_json_abi::StateMutability::Pure
2216                            | alloy_json_abi::StateMutability::View
2217                    ) || func.is_reserved()
2218                        || excluded_test_selectors.contains(&func.selector())
2219                    {
2220                        None
2221                    } else {
2222                        Some(func.selector())
2223                    }
2224                })
2225                .collect();
2226            self.add_address_with_functions(address, &selectors, false, targeted_contracts)?;
2227        }
2228
2229        Ok(())
2230    }
2231
2232    /// Adds the address and fuzzed or excluded functions to `TargetedContracts`.
2233    fn add_address_with_functions(
2234        &self,
2235        address: Address,
2236        selectors: &[Selector],
2237        should_exclude: bool,
2238        targeted_contracts: &mut TargetedContracts,
2239    ) -> eyre::Result<()> {
2240        // Do not add address in target contracts if no function selected.
2241        if selectors.is_empty() {
2242            return Ok(());
2243        }
2244
2245        let contract = match targeted_contracts.entry(address) {
2246            Entry::Occupied(entry) => entry.into_mut(),
2247            Entry::Vacant(entry) => {
2248                let (identifier, abi) = self.setup_contracts.get(&address).ok_or_else(|| {
2249                    eyre::eyre!(
2250                        "[{}] address does not have an associated contract: {}",
2251                        if should_exclude { "excludeSelectors" } else { "targetSelectors" },
2252                        address
2253                    )
2254                })?;
2255                entry.insert(
2256                    TargetedContract::new(identifier.clone(), abi.clone())
2257                        .with_project_contracts(self.project_contracts),
2258                )
2259            }
2260        };
2261        contract.add_selectors(selectors.iter().copied(), should_exclude)?;
2262        Ok(())
2263    }
2264}
2265
2266/// Collects data from call for fuzzing. However, it first verifies that the sender is not an EOA
2267/// before inserting it into the dictionary. Otherwise, we flood the dictionary with
2268/// randomly generated addresses.
2269fn collect_data<FEN: FoundryEvmNetwork>(
2270    invariant_test: &InvariantTest,
2271    state_changeset: &mut AddressMap<Account>,
2272    tx: &BasicTxDetails,
2273    call_result: &RawCallResult<FEN>,
2274    run_depth: u32,
2275    mapping_slots: Option<&AddressMap<foundry_common::mapping_slots::MappingSlots>>,
2276) {
2277    // We keep the nonce changes to apply later.
2278    let sender_changeset = match state_changeset.entry(tx.sender) {
2279        AddressMapEntry::Occupied(entry) => entry
2280            .get()
2281            .info
2282            .code
2283            .as_ref()
2284            .is_none_or(|code| code.is_empty())
2285            .then(|| entry.remove()),
2286        AddressMapEntry::Vacant(_) => None,
2287    };
2288
2289    // Collect values from fuzzed call result and add them to fuzz dictionary.
2290    invariant_test.fuzz_state.collect_values_from_call(
2291        &invariant_test.targeted_contracts,
2292        tx,
2293        &call_result.result,
2294        &call_result.logs,
2295        &*state_changeset,
2296        run_depth,
2297        mapping_slots,
2298    );
2299
2300    // Inject typed sancov trace-cmp operands into the fuzz dictionary.
2301    if let Some(cmp_values) = &call_result.sancov_cmp_values {
2302        invariant_test.fuzz_state.collect_typed_cmp_values(
2303            cmp_values.iter().map(|s| (s.width, alloy_primitives::B256::from(s.value))),
2304        );
2305    }
2306    // Re-add changes
2307    if let Some(changed) = sender_changeset {
2308        state_changeset.insert(tx.sender, changed);
2309    }
2310}
2311
2312/// Calls the `afterInvariant()` function on a contract.
2313/// Returns call result and if call succeeded.
2314/// The state after the call is not persisted.
2315///
2316/// Uses the handler-gate success check so a stale committed `GLOBAL_FAIL_SLOT` from a
2317/// previously-recorded handler bug doesn't false-positive this call (the slot is `1` from
2318/// the prior bug, but `afterInvariant` itself didn't write it in this changeset).
2319pub(crate) fn call_after_invariant_function<FEN: FoundryEvmNetwork>(
2320    executor: &Executor<FEN>,
2321    to: Address,
2322) -> Result<(RawCallResult<FEN>, bool), EvmError<FEN>> {
2323    let calldata = Bytes::from_static(&IInvariantTest::afterInvariantCall::SELECTOR);
2324    let mut call_result = executor.call_raw(CALLER, to, calldata, U256::ZERO)?;
2325    let success = executor.is_raw_call_mut_success_handler_gate(to, &mut call_result);
2326    Ok((call_result, success))
2327}
2328
2329/// Calls the invariant function and returns call result and if succeeded.
2330///
2331/// Uses the handler-gate success check (same rationale as `call_after_invariant_function`):
2332/// the predicate is broken iff this call's own changeset writes `GLOBAL_FAIL_SLOT` (via `t()` /
2333/// `vm.assert*`) or the call reverts; a stale committed slot from a prior handler bug must not
2334/// poison every later predicate evaluation in the run.
2335pub(crate) fn call_invariant_function<FEN: FoundryEvmNetwork>(
2336    executor: &Executor<FEN>,
2337    address: Address,
2338    calldata: Bytes,
2339) -> Result<(RawCallResult<FEN>, bool)> {
2340    let mut call_result = executor.call_raw(CALLER, address, calldata, U256::ZERO)?;
2341    let success = executor.is_raw_call_mut_success_handler_gate(address, &mut call_result);
2342    Ok((call_result, success))
2343}
2344
2345/// Executes an invariant replay fuzz call and returns the result.
2346///
2347/// This applies invariant replay semantics: warp/roll deltas are applied before the call and the
2348/// requested value is clamped to the sender balance. It is intended for invariant sequence replay,
2349/// shrinking, and artifact validation rather than as a general raw-call helper.
2350///
2351/// Applies any block timestamp (warp) and block number (roll) adjustments before the call.
2352pub fn execute_tx<FEN: FoundryEvmNetwork>(
2353    executor: &mut Executor<FEN>,
2354    tx: &BasicTxDetails,
2355) -> Result<RawCallResult<FEN>> {
2356    super::campaign::execute_invariant_tx(executor, &mut tx.clone())
2357}
2358
2359/// Executes an invariant replay call on a validation executor and registers created targets.
2360///
2361/// This uses live campaign acceptance and commit behavior while allowing callers to update
2362/// updatable target sets before validating later calls in the same artifact.
2363pub fn execute_tx_and_register_created<FEN: FoundryEvmNetwork>(
2364    executor: &mut Executor<FEN>,
2365    tx: &BasicTxDetails,
2366    targeted_contracts: &FuzzRunIdentifiedContracts,
2367    dynamic_target_ctx: &DynamicTargetCtx<'_>,
2368    created_contracts: &mut Vec<Address>,
2369) -> Result<()> {
2370    let (kind, call_result) = super::campaign::execute_invariant_replay_tx(executor, tx)?;
2371    if kind == CampaignCallKind::AssumptionRejected {
2372        return Err(eyre!("invariant replay prefix rejected by vm.assume"));
2373    }
2374    targeted_contracts.collect_created_contracts(
2375        &call_result.state_changeset,
2376        dynamic_target_ctx.project_contracts,
2377        dynamic_target_ctx.setup_contracts,
2378        dynamic_target_ctx.artifact_filters,
2379        created_contracts,
2380    )?;
2381    Ok(())
2382}
2383
2384#[cfg(test)]
2385mod tests {
2386    use super::*;
2387    use crate::executors::ExecutorBuilder;
2388    use foundry_cheatcodes::CheatsConfig;
2389    use foundry_config::FuzzDictionaryConfig;
2390    use foundry_evm_core::{
2391        backend::Backend,
2392        evm::{EthEvmNetwork, EvmEnvFor, TxEnvFor},
2393    };
2394    use foundry_evm_fuzz::CallDetails;
2395    use proptest::{
2396        prelude::any,
2397        strategy::{Strategy, ValueTree},
2398        test_runner::Config,
2399    };
2400    use revm::{
2401        bytecode::Bytecode,
2402        context::Block,
2403        database::{CacheDB, EmptyDB},
2404    };
2405    use serde_json::json;
2406    use std::{sync::mpsc, thread};
2407
2408    fn first_generated_u64(runner: &mut TestRunner) -> u64 {
2409        any::<u64>().new_tree(runner).unwrap().current()
2410    }
2411
2412    fn test_runner() -> TestRunner {
2413        TestRunner::new(Config { failure_persistence: None, ..Default::default() })
2414    }
2415
2416    fn seeded_test_runner(seed: U256) -> TestRunner {
2417        let config = Config { failure_persistence: None, ..Default::default() };
2418        let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
2419        TestRunner::new_with_rng(config, rng)
2420    }
2421
2422    #[test]
2423    fn assumption_rejection_restores_delayed_block_environment() {
2424        let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
2425        let mut executor = ExecutorBuilder::default()
2426            .inspectors(|stack| stack.cheatcodes(Arc::new(CheatsConfig::default())))
2427            .gas_limit(1 << 24)
2428            .build(
2429                EvmEnvFor::<EthEvmNetwork>::default(),
2430                TxEnvFor::<EthEvmNetwork>::default(),
2431                backend,
2432                Default::default(),
2433            );
2434        let target = Address::repeat_byte(0x11);
2435        let mut code = vec![0x6e]; // PUSH15.
2436        code.extend_from_slice(MAGIC_ASSUME);
2437        code.extend_from_slice(&[0x60, 0x00, 0x52, 0x60, 0x0f, 0x60, 0x11, 0xf3]);
2438        executor.set_code(target, Bytecode::new_raw(Bytes::from(code))).unwrap();
2439
2440        let initial_block = executor.evm_env().block_env.clone();
2441        let initial_cheatcode_block =
2442            executor.inspector().cheatcodes.as_ref().unwrap().block.clone();
2443        let expected_timestamp = initial_block.timestamp() + U256::from(10);
2444        let expected_number = initial_block.number() + U256::from(5);
2445        let tx = BasicTxDetails {
2446            warp: Some(U256::from(10)),
2447            roll: Some(U256::from(5)),
2448            sender: Address::ZERO,
2449            call_details: CallDetails { target, calldata: Bytes::new(), value: None },
2450        };
2451        let mut state = (executor, tx);
2452        let campaign = FuzzCampaign::new(FuzzCampaignMode::Invariant {
2453            check_interval: 1,
2454            optimization: false,
2455        });
2456
2457        let outcome = campaign
2458            .run_sequence(
2459                &mut state,
2460                1,
2461                |state| (&mut state.0, &mut state.1),
2462                |_| 0,
2463                |_| false,
2464                |state, event| {
2465                    match event {
2466                        CampaignEvent::Feedback(_) => {
2467                            let block = &state.0.evm_env().block_env;
2468                            assert_eq!(block.timestamp(), expected_timestamp);
2469                            assert_eq!(block.number(), expected_number);
2470                            let block = state
2471                                .0
2472                                .inspector()
2473                                .cheatcodes
2474                                .as_ref()
2475                                .unwrap()
2476                                .block
2477                                .as_ref()
2478                                .unwrap();
2479                            assert_eq!(block.timestamp(), expected_timestamp);
2480                            assert_eq!(block.number(), expected_number);
2481                        }
2482                        CampaignEvent::Check { kind, .. } => {
2483                            assert_eq!(kind, CampaignCallKind::AssumptionRejected);
2484                            return Ok(CampaignControl::Stop);
2485                        }
2486                        _ => {}
2487                    }
2488                    Ok(CampaignControl::Continue)
2489                },
2490            )
2491            .unwrap();
2492
2493        assert_eq!(outcome, CampaignSequenceOutcome::Stopped);
2494        assert_eq!(state.0.evm_env().block_env, initial_block);
2495        assert_eq!(state.0.inspector().cheatcodes.as_ref().unwrap().block, initial_cheatcode_block);
2496
2497        let (kind, _) =
2498            crate::executors::campaign::execute_invariant_replay_tx(&mut state.0, &state.1)
2499                .unwrap();
2500        assert_eq!(kind, CampaignCallKind::AssumptionRejected);
2501        assert_eq!(state.0.evm_env().block_env, initial_block);
2502        assert_eq!(state.0.inspector().cheatcodes.as_ref().unwrap().block, initial_cheatcode_block);
2503    }
2504
2505    #[test]
2506    fn invariant_worker_seed_preserves_master_seed_and_derives_workers() {
2507        let seed = U256::from(0x1234);
2508
2509        assert_eq!(invariant_worker_seed(seed, 0), seed);
2510        assert_ne!(invariant_worker_seed(seed, 1), seed);
2511        assert_ne!(invariant_worker_seed(seed, 1), invariant_worker_seed(seed, 2));
2512        assert_ne!(invariant_worker_seed(seed, 1), invariant_worker_seed(U256::from(0x5678), 1));
2513    }
2514
2515    #[test]
2516    fn invariant_worker_runner_preserves_seed_for_master_worker() {
2517        let seed = U256::from(0x1234);
2518        let mut seeded_runner = seeded_test_runner(seed);
2519        let mut parent = test_runner();
2520        let mut worker = invariant_worker_runner(&mut parent, 0, Some(seed));
2521
2522        assert_eq!(first_generated_u64(&mut worker), first_generated_u64(&mut seeded_runner));
2523    }
2524
2525    #[test]
2526    fn invariant_worker_runner_uses_seed_independent_of_parent_rng_state() {
2527        let seed = U256::from(0x1234);
2528        let mut parent = test_runner();
2529        let mut advanced_parent = test_runner();
2530        let _ = first_generated_u64(&mut advanced_parent);
2531
2532        let mut worker = invariant_worker_runner(&mut parent, 1, Some(seed));
2533        let mut worker_from_advanced_parent =
2534            invariant_worker_runner(&mut advanced_parent, 1, Some(seed));
2535
2536        assert_eq!(
2537            first_generated_u64(&mut worker),
2538            first_generated_u64(&mut worker_from_advanced_parent)
2539        );
2540    }
2541
2542    #[test]
2543    fn invariant_focus_seed_preserves_configured_seed() {
2544        let configured_seed = U256::from(0x1234);
2545        let mut parent = test_runner();
2546
2547        assert_eq!(
2548            invariant_focus_seed(&mut parent, Some(configured_seed), 2),
2549            Some(configured_seed)
2550        );
2551        assert_eq!(invariant_focus_seed(&mut parent, Some(configured_seed), 1), None);
2552    }
2553
2554    #[test]
2555    fn invariant_focus_seed_uses_parent_rng_when_unconfigured() {
2556        let mut parent = seeded_test_runner(U256::from(1));
2557        let mut matching_parent = seeded_test_runner(U256::from(1));
2558        let mut different_parent = seeded_test_runner(U256::from(2));
2559
2560        let focus_seed = invariant_focus_seed(&mut parent, None, 2).unwrap();
2561
2562        assert_eq!(focus_seed, invariant_focus_seed(&mut matching_parent, None, 2).unwrap());
2563        assert_ne!(focus_seed, invariant_focus_seed(&mut different_parent, None, 2).unwrap());
2564        assert_eq!(invariant_focus_seed(&mut parent, None, 1), None);
2565    }
2566
2567    #[test]
2568    fn invariant_progress_json_includes_throughput_fields() {
2569        let throughput = InvariantThroughputMetrics { total_txs: 2, total_gas: 50 };
2570
2571        let payload = build_invariant_progress_json(
2572            InvariantProgressContext {
2573                timestamp_secs: 123,
2574                contract_name: "InvariantContract",
2575                optimization_best: Some(I256::try_from(42).unwrap()),
2576                throughput,
2577                elapsed: Duration::from_secs(10),
2578                worker_id: 1,
2579                worker_count: 4,
2580                time_since_new_edge: Some(Duration::from_secs(3)),
2581            },
2582            &json!({ "corpus_count": 7 }),
2583            &InvariantFailureMetrics::default(),
2584        );
2585
2586        assert_eq!(payload["timestamp"], json!(123));
2587        assert_eq!(payload["contract"], json!("InvariantContract"));
2588        assert!(payload.get("invariant").is_none());
2589        assert_eq!(payload["metrics"]["corpus_count"], json!(7));
2590        assert_eq!(payload["metrics"]["broken_assertions"], json!(0));
2591        assert!(payload["metrics"].get("broken_handlers").is_none());
2592        assert_eq!(payload["total_txs"], json!(2));
2593        assert_eq!(payload["total_gas"], json!(50));
2594        assert_eq!(payload["tps"], json!(0.2));
2595        assert_eq!(payload["gps"], json!(5.0));
2596        assert!(payload.get("tx_per_sec").is_none());
2597        assert!(payload.get("gas_per_sec").is_none());
2598        assert_eq!(payload["worker"]["id"], json!(1));
2599        assert_eq!(payload["worker"]["count"], json!(4));
2600        assert_eq!(payload["optimization_best"], json!("42"));
2601    }
2602
2603    #[test]
2604    fn invariant_worker_count_keeps_short_campaigns_single_worker() {
2605        assert_eq!(
2606            max_invariant_workers_for_campaign(0, DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP),
2607            1
2608        );
2609        assert_eq!(
2610            max_invariant_workers_for_campaign(
2611                MIN_RUNS_PER_INVARIANT_WORKER - 1,
2612                DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2613            ),
2614            1
2615        );
2616        assert_eq!(
2617            max_invariant_workers_for_campaign(
2618                MIN_RUNS_PER_INVARIANT_WORKER,
2619                DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2620            ),
2621            1
2622        );
2623        assert_eq!(
2624            max_invariant_workers_for_campaign(
2625                MIN_RUNS_PER_INVARIANT_WORKER * 2,
2626                DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2627            ),
2628            2
2629        );
2630        assert_eq!(max_invariant_workers_for_campaign(256, 100_000), 5);
2631    }
2632
2633    #[test]
2634    fn invariant_run_depth_random_min_depth_zero_never_returns_zero() {
2635        let mut runner = test_runner();
2636        let config = InvariantConfig {
2637            depth: 8,
2638            min_depth: 0,
2639            depth_mode: InvariantDepthMode::Random,
2640            ..Default::default()
2641        };
2642
2643        for _ in 0..128 {
2644            assert!((1..=8).contains(&invariant_run_depth(&config, &mut runner)));
2645        }
2646    }
2647
2648    #[test]
2649    fn invariant_run_depth_fixed_zero_preserves_zero() {
2650        let mut runner = test_runner();
2651        let config = InvariantConfig {
2652            depth: 0,
2653            depth_mode: InvariantDepthMode::Fixed,
2654            ..Default::default()
2655        };
2656
2657        assert_eq!(invariant_run_depth(&config, &mut runner), 0);
2658    }
2659
2660    #[test]
2661    fn invariant_worker_config_keeps_single_worker_default() {
2662        let config = InvariantConfig::default();
2663
2664        assert_eq!(
2665            invariant_worker_config(config, 0, 1).corpus.corpus_random_sequence_weight,
2666            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2667        );
2668    }
2669
2670    #[test]
2671    fn invariant_worker_config_uses_one_exploratory_worker_with_default_config() {
2672        let config = InvariantConfig::default();
2673
2674        assert_eq!(
2675            invariant_worker_config(config.clone(), 0, 4).corpus.corpus_random_sequence_weight,
2676            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2677        );
2678        assert_eq!(
2679            invariant_worker_config(config.clone(), 1, 4).corpus.corpus_random_sequence_weight,
2680            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2681        );
2682        assert_eq!(
2683            invariant_worker_config(config.clone(), 2, 4).corpus.corpus_random_sequence_weight,
2684            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2685        );
2686        assert_eq!(
2687            invariant_worker_config(config, 3, 4).corpus.corpus_random_sequence_weight,
2688            FuzzCorpusConfig::ENSEMBLE_CORPUS_RANDOM_SEQUENCE_WEIGHT
2689        );
2690    }
2691
2692    #[test]
2693    fn invariant_worker_config_keeps_two_worker_campaign_on_default() {
2694        let config = InvariantConfig::default();
2695
2696        assert_eq!(
2697            invariant_worker_config(config.clone(), 0, 2).corpus.corpus_random_sequence_weight,
2698            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2699        );
2700        assert_eq!(
2701            invariant_worker_config(config, 1, 2).corpus.corpus_random_sequence_weight,
2702            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2703        );
2704    }
2705
2706    #[test]
2707    fn invariant_worker_config_uses_last_worker_for_three_worker_campaign() {
2708        let config = InvariantConfig::default();
2709
2710        assert_eq!(
2711            invariant_worker_config(config.clone(), 0, 3).corpus.corpus_random_sequence_weight,
2712            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2713        );
2714        assert_eq!(
2715            invariant_worker_config(config.clone(), 1, 3).corpus.corpus_random_sequence_weight,
2716            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2717        );
2718        assert_eq!(
2719            invariant_worker_config(config, 2, 3).corpus.corpus_random_sequence_weight,
2720            FuzzCorpusConfig::ENSEMBLE_CORPUS_RANDOM_SEQUENCE_WEIGHT
2721        );
2722    }
2723
2724    #[test]
2725    fn invariant_worker_config_preserves_explicit_corpus_random_sequence_weight() {
2726        let config = InvariantConfig {
2727            corpus: FuzzCorpusConfig {
2728                corpus_random_sequence_weight: 25,
2729                ..FuzzCorpusConfig::default()
2730            },
2731            corpus_random_sequence_weight_configured: true,
2732            ..InvariantConfig::default()
2733        };
2734
2735        assert_eq!(
2736            invariant_worker_config(config.clone(), 1, 4).corpus.corpus_random_sequence_weight,
2737            25
2738        );
2739        assert_eq!(invariant_worker_config(config, 0, 1).corpus.corpus_random_sequence_weight, 25);
2740
2741        let config = InvariantConfig {
2742            corpus_random_sequence_weight_configured: true,
2743            ..InvariantConfig::default()
2744        };
2745
2746        assert_eq!(
2747            invariant_worker_config(config, 1, 4).corpus.corpus_random_sequence_weight,
2748            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2749        );
2750    }
2751
2752    #[test]
2753    fn invariant_worker_count_preserves_fixed_workers() {
2754        let mut config = InvariantConfig {
2755            runs: MIN_RUNS_PER_INVARIANT_WORKER * 4,
2756            workers: foundry_config::InvariantWorkers::Fixed(
2757                std::num::NonZeroUsize::new(4).unwrap(),
2758            ),
2759            ..Default::default()
2760        };
2761        assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2762
2763        config.corpus.show_edge_coverage = true;
2764        assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2765
2766        config.corpus.show_edge_coverage = false;
2767        config.corpus.corpus_dir = Some(std::path::PathBuf::from("corpus"));
2768        assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2769
2770        config.runs = MIN_RUNS_PER_INVARIANT_WORKER - 1;
2771        config.timeout = None;
2772        assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2773
2774        config.timeout = Some(1);
2775        assert_eq!(invariant_worker_count_with_threads(&config, 8, 4), 4);
2776    }
2777
2778    #[test]
2779    fn invariant_worker_count_does_not_cap_configured_workers_by_available_threads() {
2780        let config = InvariantConfig {
2781            runs: MIN_RUNS_PER_INVARIANT_WORKER * 8,
2782            workers: foundry_config::InvariantWorkers::Fixed(
2783                std::num::NonZeroUsize::new(8).unwrap(),
2784            ),
2785            ..Default::default()
2786        };
2787
2788        assert_eq!(invariant_worker_count_with_threads(&config, 4, 1), 8);
2789    }
2790
2791    #[test]
2792    fn invariant_worker_count_splits_available_threads_for_auto_workers() {
2793        let mut config = InvariantConfig {
2794            runs: MIN_RUNS_PER_INVARIANT_WORKER * 4,
2795            depth: DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP,
2796            workers: foundry_config::InvariantWorkers::Auto,
2797            ..Default::default()
2798        };
2799
2800        assert_eq!(invariant_worker_count_with_threads(&config, 4, 1), 4);
2801        assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2802        assert_eq!(invariant_worker_count_with_threads(&config, 8, 3), 2);
2803        assert_eq!(invariant_worker_count_with_threads(&config, 3, 8), 1);
2804        assert_eq!(invariant_worker_count_with_threads(&config, 0, 0), 1);
2805
2806        config.runs = MIN_RUNS_PER_INVARIANT_WORKER - 1;
2807        assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 1);
2808
2809        config.depth = 100_000;
2810        assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2811
2812        config.timeout = Some(1);
2813        assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2814    }
2815
2816    fn function(signature: &str) -> Function {
2817        Function::parse(signature).unwrap()
2818    }
2819
2820    fn targeted_contract(identifier: &str, functions: Vec<Function>) -> TargetedContract {
2821        let mut abi = alloy_json_abi::JsonAbi::new();
2822        for function in functions {
2823            abi.functions.entry(function.name.clone()).or_default().push(function);
2824        }
2825        TargetedContract::new(identifier.to_string(), abi)
2826    }
2827
2828    #[test]
2829    fn campaign_terminal_stop_interrupts_handler_without_accepting_run() {
2830        const GAS_LIMIT: u64 = 1 << 24;
2831        let invariant_address = Address::repeat_byte(0x11);
2832        let handler_address = Address::repeat_byte(0x22);
2833        let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
2834        let mut executor = ExecutorBuilder::default().gas_limit(GAS_LIMIT).build(
2835            EvmEnvFor::<EthEvmNetwork>::default(),
2836            TxEnvFor::<EthEvmNetwork>::default(),
2837            backend,
2838            Default::default(),
2839        );
2840        // Return ABI-encoded `true` for the invariant predicate.
2841        executor
2842            .set_code(
2843                invariant_address,
2844                Bytecode::new_raw(Bytes::from_static(&[
2845                    0x60, 0x01, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3,
2846                ])),
2847            )
2848            .unwrap();
2849        // JUMPDEST; PUSH1 0; JUMP loops until the campaign stop reaches the inspector.
2850        executor
2851            .set_code(
2852                handler_address,
2853                Bytecode::new_raw(Bytes::from_static(&[0x5b, 0x60, 0x00, 0x56])),
2854            )
2855            .unwrap();
2856
2857        let (handler_entered_tx, handler_entered_rx) = mpsc::channel();
2858        let (handler_release_tx, handler_release_rx) = mpsc::channel();
2859        executor.inspector_mut().set_early_exit_test_gate(
2860            handler_entered_tx,
2861            handler_release_rx,
2862            0,
2863        );
2864
2865        let invariant = function("invariant_ok() view returns (bool)");
2866        let mut invariant_abi = alloy_json_abi::JsonAbi::new();
2867        invariant_abi.functions.entry(invariant.name.clone()).or_default().push(invariant.clone());
2868        let invariant_contract = InvariantContract::new(
2869            invariant_address,
2870            "InvariantTest",
2871            vec![(&invariant, false)],
2872            0,
2873            false,
2874            &invariant_abi,
2875        );
2876
2877        let handler = function("loopForever()");
2878        let mut handler_contract = targeted_contract("Handler", vec![handler.clone()]);
2879        handler_contract.targeted_functions = vec![handler];
2880        let mut targeted_contracts = TargetedContracts::new();
2881        targeted_contracts.insert(handler_address, handler_contract);
2882        let campaign_seed = InvariantCampaignSeed {
2883            artifact_filters: ArtifactFilters::default(),
2884            sender_filters: SenderFilters::new(vec![CALLER], Vec::new()),
2885            targeted_contracts,
2886            targets_are_updatable: false,
2887            initial_handler_failures: Map::default(),
2888        };
2889
2890        let config =
2891            InvariantConfig { runs: 1, depth: 1, show_metrics: false, ..Default::default() };
2892        let campaign_state = InvariantCampaignState::new(EarlyExit::new(false), None);
2893        let fuzz_state = EvmFuzzState::new(
2894            &[],
2895            &CacheDB::<EmptyDB>::default(),
2896            FuzzDictionaryConfig::default(),
2897            None,
2898        );
2899        let setup_contracts = ContractsByAddress::default();
2900        let project_contracts = ContractsByArtifact::default();
2901        let (result_tx, result_rx) = mpsc::channel();
2902
2903        let (handler_entered, result) = thread::scope(|scope| {
2904            let handle = scope.spawn(|| {
2905                let result = InvariantExecutor::<EthEvmNetwork>::run_invariant_worker(
2906                    executor,
2907                    test_runner(),
2908                    config,
2909                    &setup_contracts,
2910                    &project_contracts,
2911                    InvariantWorkerPlan { worker_id: 0, first_global_run: 0, runs: 1 },
2912                    invariant_contract,
2913                    &FuzzFixtures::default(),
2914                    fuzz_state,
2915                    None,
2916                    &campaign_state,
2917                    campaign_seed,
2918                    WorkerCorpusSeed::default(),
2919                    1,
2920                    1,
2921                );
2922                let _ = result_tx.send(result);
2923            });
2924
2925            let handler_entered = handler_entered_rx.recv_timeout(Duration::from_secs(1)).is_ok();
2926            campaign_state.request_terminal_stop();
2927            let _ = handler_release_tx.send(());
2928
2929            let result = result_rx.recv_timeout(Duration::from_secs(1));
2930            handle.join().unwrap();
2931            (handler_entered, result)
2932        });
2933        assert!(handler_entered, "invariant handler did not begin EVM execution");
2934        let (output, _) = result.expect("invariant campaign did not observe early exit").unwrap();
2935        assert_eq!(output.result.runs, 0);
2936        assert_eq!(output.result.calls, 0);
2937        assert_eq!(campaign_state.total_runs(), 0);
2938        assert_eq!(campaign_state.throughput_totals(), (0, 0));
2939        assert!(output.result.errors.is_empty());
2940        assert!(output.result.handler_errors.is_empty());
2941        assert!(output.result.last_run_inputs.is_empty());
2942        assert!(output.result.line_coverage.is_none());
2943        assert!(output.result.metrics.is_empty());
2944        assert!(output.result.optimization_best_value.is_none());
2945    }
2946
2947    #[test]
2948    fn invariant_focus_workers_stay_before_exploratory_tail_worker() {
2949        assert_eq!(invariant_focus_worker_count(1), 0);
2950        assert_eq!(invariant_focus_worker_count(2), 1);
2951        assert_eq!(invariant_focus_worker_count(4), 1);
2952        assert_eq!(invariant_focus_worker_count(8), 1);
2953        assert_eq!(invariant_focus_worker_count(16), 2);
2954
2955        assert_eq!(invariant_focus_worker_index(0, 1), None);
2956        assert_eq!(invariant_focus_worker_index(1, 2), Some(0));
2957        assert_eq!(invariant_focus_worker_index(0, 4), None);
2958        assert_eq!(invariant_focus_worker_index(2, 4), Some(0));
2959        assert_eq!(invariant_focus_worker_index(3, 4), None);
2960        assert_eq!(invariant_focus_worker_index(13, 16), Some(0));
2961        assert_eq!(invariant_focus_worker_index(14, 16), Some(1));
2962        assert_eq!(invariant_focus_worker_index(15, 16), None);
2963        assert_eq!(invariant_focus_worker_index(16, 16), None);
2964    }
2965
2966    #[test]
2967    fn invariant_focus_narrows_to_one_effective_selector() {
2968        let target = Address::from([0x11; 20]);
2969        let first = function("first(uint256)");
2970        let second = function("second(uint256)");
2971        let second_selector = second.selector();
2972        let mut contract = targeted_contract("Target", vec![first.clone(), second.clone()]);
2973        contract.targeted_functions = vec![first, second];
2974
2975        let mut targets = TargetedContracts::new();
2976        targets.insert(target, contract);
2977
2978        let focused = focused_targeted_contracts(&targets, 1, None).unwrap();
2979        let focused_functions = focused[&target].abi_fuzzed_functions().collect::<Vec<_>>();
2980
2981        assert_eq!(focused.len(), 1);
2982        assert_eq!(focused_functions.len(), 1);
2983        assert_eq!(focused_functions[0].selector(), second_selector);
2984    }
2985
2986    #[test]
2987    fn invariant_focus_seed_rotates_effective_selector() {
2988        let target = Address::from([0x55; 20]);
2989        let first = function("first(uint256)");
2990        let second = function("second(uint256)");
2991        let third = function("third(uint256)");
2992        let third_selector = third.selector();
2993        let mut contract =
2994            targeted_contract("Target", vec![first.clone(), second.clone(), third.clone()]);
2995        contract.targeted_functions = vec![first, second, third];
2996
2997        let mut targets = TargetedContracts::new();
2998        targets.insert(target, contract);
2999
3000        let focused = focused_targeted_contracts(&targets, 0, Some(U256::from(2))).unwrap();
3001        let focused_functions = focused[&target].abi_fuzzed_functions().collect::<Vec<_>>();
3002
3003        assert_eq!(focused_functions.len(), 1);
3004        assert_eq!(focused_functions[0].selector(), third_selector);
3005    }
3006
3007    #[test]
3008    fn invariant_focus_freezes_dynamic_target_updates() {
3009        let target = Address::from([0x44; 20]);
3010        let first = function("first(uint256)");
3011        let second = function("second(uint256)");
3012        let mut contract = targeted_contract("Target", vec![first.clone(), second.clone()]);
3013        contract.targeted_functions = vec![first, second];
3014
3015        let mut targeted_contracts = TargetedContracts::new();
3016        targeted_contracts.insert(target, contract);
3017        let campaign_seed = InvariantCampaignSeed {
3018            artifact_filters: ArtifactFilters::default(),
3019            sender_filters: SenderFilters::default(),
3020            targeted_contracts,
3021            targets_are_updatable: true,
3022            initial_handler_failures: Map::default(),
3023        };
3024
3025        let normal_worker = campaign_seed_for_worker(
3026            &campaign_seed,
3027            InvariantWorkerPlan { worker_id: 0, first_global_run: 0, runs: 1 },
3028            2,
3029        );
3030        let focus_worker = campaign_seed_for_worker(
3031            &campaign_seed,
3032            InvariantWorkerPlan { worker_id: 1, first_global_run: 1, runs: 1 },
3033            2,
3034        );
3035
3036        assert!(normal_worker.targets_are_updatable);
3037        assert!(!focus_worker.targets_are_updatable);
3038    }
3039
3040    #[test]
3041    fn invariant_focus_does_not_widen_target_selectors() {
3042        let target = Address::from([0x22; 20]);
3043        let allowed = function("allowed(uint256)");
3044        let hidden = function("hidden(uint256)");
3045        let mut contract = targeted_contract("Target", vec![allowed.clone(), hidden]);
3046        contract.targeted_functions = vec![allowed];
3047
3048        let mut targets = TargetedContracts::new();
3049        targets.insert(target, contract);
3050
3051        assert!(focused_targeted_contracts(&targets, 0, None).is_none());
3052    }
3053
3054    #[test]
3055    fn invariant_focus_skips_excluded_selectors() {
3056        let target = Address::from([0x33; 20]);
3057        let first = function("aaa(uint256)");
3058        let second = function("bbb(uint256)");
3059        let excluded = function("ccc(uint256)");
3060        let excluded_selector = excluded.selector();
3061        let mut contract = targeted_contract("Target", vec![first, second, excluded.clone()]);
3062        contract.excluded_functions = vec![excluded];
3063
3064        let mut targets = TargetedContracts::new();
3065        targets.insert(target, contract);
3066
3067        let focused = focused_targeted_contracts(&targets, 3, None).unwrap();
3068        let focused_functions = focused[&target].abi_fuzzed_functions().collect::<Vec<_>>();
3069
3070        assert_eq!(focused_functions.len(), 1);
3071        assert_ne!(focused_functions[0].selector(), excluded_selector);
3072    }
3073
3074    #[test]
3075    fn invariant_focus_skips_excluded_targeted_selectors() {
3076        let target = Address::from([0x66; 20]);
3077        let first = function("aaa(uint256)");
3078        let second = function("bbb(uint256)");
3079        let excluded = function("ccc(uint256)");
3080        let excluded_selector = excluded.selector();
3081        let mut contract =
3082            targeted_contract("Target", vec![first.clone(), second.clone(), excluded.clone()]);
3083        contract.targeted_functions = vec![first, second, excluded.clone()];
3084        contract.excluded_functions = vec![excluded];
3085
3086        let mut targets = TargetedContracts::new();
3087        targets.insert(target, contract);
3088
3089        let focused = focused_targeted_contracts(&targets, 2, None).unwrap();
3090        let focused_functions = focused[&target].abi_fuzzed_functions().collect::<Vec<_>>();
3091
3092        assert_eq!(focused_functions.len(), 1);
3093        assert_ne!(focused_functions[0].selector(), excluded_selector);
3094    }
3095
3096    #[test]
3097    fn invariant_worker_cmp_log_selection_uses_one_worker_per_campaign() {
3098        use foundry_config::FuzzCorpusMutationWeights;
3099
3100        let mut config = InvariantConfig::default();
3101        assert!(!invariant_worker_collects_evm_cmp_log(&config, 0, 1));
3102
3103        config.corpus.corpus_dir = Some("corpus".into());
3104        assert!(invariant_worker_collects_evm_cmp_log(&config, 0, 1));
3105        assert!(invariant_worker_collects_evm_cmp_log(&config, 0, 4));
3106        assert!(!invariant_worker_collects_evm_cmp_log(&config, 1, 4));
3107        assert!(!invariant_worker_collects_evm_cmp_log(&config, 3, 4));
3108
3109        config.corpus.mutation_weights = FuzzCorpusMutationWeights {
3110            mutation_weight_splice: 1,
3111            mutation_weight_repeat: 1,
3112            mutation_weight_interleave: 1,
3113            mutation_weight_prefix: 1,
3114            mutation_weight_suffix: 1,
3115            mutation_weight_abi: 1,
3116            mutation_weight_cmp: 0,
3117        };
3118        assert!(!invariant_worker_collects_evm_cmp_log(&config, 0, 1));
3119
3120        // All-zero configured weights resolve to the default mutation distribution.
3121        config.corpus.mutation_weights = FuzzCorpusMutationWeights {
3122            mutation_weight_splice: 0,
3123            mutation_weight_repeat: 0,
3124            mutation_weight_interleave: 0,
3125            mutation_weight_prefix: 0,
3126            mutation_weight_suffix: 0,
3127            mutation_weight_abi: 0,
3128            mutation_weight_cmp: 0,
3129        };
3130        assert!(invariant_worker_collects_evm_cmp_log(&config, 0, 1));
3131
3132        config.corpus.sancov_edges = true;
3133        assert!(!invariant_worker_collects_evm_cmp_log(&config, 0, 1));
3134        assert!(!invariant_worker_collects_evm_cmp_log(&config, 0, 4));
3135    }
3136
3137    #[test]
3138    fn timed_invariant_workers_are_not_bounded_by_assigned_runs() {
3139        let plan = InvariantWorkerPlan { worker_id: 0, first_global_run: 0, runs: 1 };
3140
3141        let untimed = InvariantCampaignState::new(EarlyExit::new(false), None);
3142        assert!(should_continue_invariant_worker(&untimed, 0, plan));
3143        assert!(!should_continue_invariant_worker(&untimed, 1, plan));
3144
3145        let timed = InvariantCampaignState::new(EarlyExit::new(false), Some(60));
3146        assert!(should_continue_invariant_worker(&timed, 0, plan));
3147        assert!(should_continue_invariant_worker(&timed, 1, plan));
3148        assert!(should_continue_invariant_worker(&timed, 10_000, plan));
3149    }
3150
3151    #[test]
3152    fn gas_report_samples_are_split_across_workers() {
3153        assert_eq!(gas_report_samples_for_worker(0, 0, 4), 0);
3154        assert_eq!(gas_report_samples_for_worker(8, 0, 4), 2);
3155        assert_eq!(gas_report_samples_for_worker(8, 3, 4), 2);
3156        assert_eq!(gas_report_samples_for_worker(10, 0, 4), 3);
3157        assert_eq!(gas_report_samples_for_worker(10, 1, 4), 3);
3158        assert_eq!(gas_report_samples_for_worker(10, 2, 4), 2);
3159        assert_eq!(gas_report_samples_for_worker(10, 3, 4), 2);
3160        assert_eq!(gas_report_samples_for_worker(3, 3, 4), 0);
3161    }
3162
3163    #[test]
3164    fn invariant_progress_json_zero_elapsed_reports_zero_rates() {
3165        let throughput = InvariantThroughputMetrics { total_txs: 1, total_gas: 21_000 };
3166
3167        let payload = build_invariant_progress_json(
3168            InvariantProgressContext {
3169                timestamp_secs: 456,
3170                contract_name: "invariant_zero_elapsed",
3171                optimization_best: None,
3172                throughput,
3173                elapsed: Duration::ZERO,
3174                worker_id: 0,
3175                worker_count: 1,
3176                time_since_new_edge: None,
3177            },
3178            &json!({ "corpus_count": 1 }),
3179            &InvariantFailureMetrics::default(),
3180        );
3181
3182        assert_eq!(payload["tps"], json!(0.0));
3183        assert_eq!(payload["gps"], json!(0.0));
3184        assert!(payload.get("optimization_best").is_none());
3185        // No edge seen yet -> `null`.
3186        assert_eq!(payload["worker"]["secs_since_new_edge"], json!(null));
3187    }
3188
3189    #[test]
3190    fn invariant_progress_json_reports_secs_since_new_edge() {
3191        let payload = build_invariant_progress_json(
3192            InvariantProgressContext {
3193                timestamp_secs: 1,
3194                contract_name: "TestContract",
3195                optimization_best: None,
3196                throughput: InvariantThroughputMetrics::default(),
3197                elapsed: Duration::from_secs(1),
3198                worker_id: 2,
3199                worker_count: 4,
3200                time_since_new_edge: Some(Duration::from_millis(1500)),
3201            },
3202            &json!({ "corpus_count": 1 }),
3203            &InvariantFailureMetrics::default(),
3204        );
3205
3206        assert_eq!(payload["worker"]["id"], json!(2));
3207        assert_eq!(payload["worker"]["secs_since_new_edge"], json!(1.5));
3208    }
3209
3210    #[test]
3211    fn invariant_progress_json_rounds_fractional_rates() {
3212        let payload = build_invariant_progress_json(
3213            InvariantProgressContext {
3214                timestamp_secs: 456,
3215                contract_name: "TestContract",
3216                optimization_best: None,
3217                throughput: InvariantThroughputMetrics { total_txs: 1, total_gas: 1 },
3218                elapsed: Duration::from_secs(3),
3219                worker_id: 0,
3220                worker_count: 1,
3221                time_since_new_edge: None,
3222            },
3223            &json!({ "corpus_count": 1 }),
3224            &InvariantFailureMetrics::default(),
3225        );
3226
3227        assert_eq!(payload["tps"], json!(0.33));
3228        assert_eq!(payload["gps"], json!(0.33));
3229    }
3230
3231    #[test]
3232    fn invariant_progress_json_includes_broken_counts() {
3233        let mut failure_metrics = InvariantFailureMetrics::default();
3234        failure_metrics.record_failure("invariant_a", "TestContract", "revert");
3235        failure_metrics.record_failure("invariant_a", "TestContract", "revert");
3236        failure_metrics.record_failure("invariant_b", "TestContract", "assertion failed");
3237        failure_metrics.broken_handlers = 7;
3238
3239        let payload = build_invariant_progress_json(
3240            InvariantProgressContext {
3241                timestamp_secs: 789,
3242                contract_name: "TestContract",
3243                optimization_best: None,
3244                throughput: InvariantThroughputMetrics::default(),
3245                elapsed: Duration::from_secs(1),
3246                worker_id: 0,
3247                worker_count: 1,
3248                time_since_new_edge: None,
3249            },
3250            &json!({ "corpus_count": 5 }),
3251            &failure_metrics,
3252        );
3253
3254        assert!(payload["metrics"].get("failures").is_none());
3255        assert!(payload["metrics"].get("unique_failures").is_none());
3256        assert_eq!(payload["metrics"]["broken_invariants"], json!(2));
3257        assert_eq!(payload["metrics"]["broken_assertions"], json!(7));
3258        assert!(payload["metrics"].get("broken_handlers").is_none());
3259    }
3260
3261    #[test]
3262    fn handler_assertion_failure_event_includes_site_and_reason() {
3263        let target = Address::repeat_byte(0x11);
3264        let selector = Selector::from([0xde, 0xad, 0xbe, 0xef]);
3265
3266        assert_eq!(
3267            build_handler_failure_event(123, target, selector, "assertion failed"),
3268            json!({
3269                "timestamp": 123,
3270                "event": "failure",
3271                "failure_type": "handler_assertion",
3272                "target": target,
3273                "selector": "0xdeadbeef",
3274                "reason": "assertion failed",
3275            })
3276        );
3277    }
3278
3279    #[test]
3280    fn failure_metrics_tracks_total_and_unique_failures() {
3281        let mut metrics = InvariantFailureMetrics::default();
3282        metrics.record_failure("invariant_a", "TestContract", "revert");
3283        metrics.record_failure("invariant_a", "TestContract", "revert");
3284        metrics.record_failure("invariant_b", "TestContract", "assertion failed");
3285
3286        assert_eq!(metrics.failures, 3);
3287        assert_eq!(metrics.unique_failures.len(), 2);
3288        assert!(metrics.unique_failures.contains("invariant_a"));
3289        assert!(metrics.unique_failures.contains("invariant_b"));
3290    }
3291
3292    #[test]
3293    fn failure_metrics_default_is_zero() {
3294        let metrics = InvariantFailureMetrics::default();
3295        assert_eq!(metrics.failures, 0);
3296        assert!(metrics.unique_failures.is_empty());
3297        assert_eq!(metrics.broken_handlers, 0);
3298    }
3299}