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_edge_fingerprint, 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        keccak256(seed_data).into()
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 an invariant executor with the configured fuzz seed for deterministic worker
845    /// runner derivation.
846    pub fn new_with_fuzz_seed(
847        executor: Executor<FEN>,
848        runner: TestRunner,
849        fuzz_seed: Option<U256>,
850        config: InvariantConfig,
851        setup_contracts: &'a ContractsByAddress,
852        project_contracts: &'a ContractsByArtifact,
853        invariant_campaign_anchors: usize,
854    ) -> Self {
855        Self {
856            executor,
857            runner,
858            fuzz_seed,
859            config,
860            setup_contracts,
861            project_contracts,
862            artifact_filters: ArtifactFilters::default(),
863            invariant_campaign_anchors,
864        }
865    }
866
867    pub fn config(&self) -> InvariantConfig {
868        self.config.clone()
869    }
870
871    /// Retains corpus replay and result aggregation without starting fresh invariant runs.
872    pub const fn skip_fresh_runs(&mut self) {
873        self.config.runs = 0;
874        self.config.timeout = None;
875    }
876
877    /// Refs for tracking contracts deployed mid-sequence during corpus replay.
878    pub const fn dynamic_target_ctx(&self) -> DynamicTargetCtx<'_> {
879        DynamicTargetCtx {
880            project_contracts: self.project_contracts,
881            setup_contracts: self.setup_contracts,
882            artifact_filters: &self.artifact_filters,
883        }
884    }
885
886    /// Fuzzes any deployed contract and checks any broken invariant at `invariant_address`.
887    ///
888    /// `initial_handler_failures` pre-seeds the campaign's `broken_handlers` map with bugs
889    /// recovered from disk by the runner's persisted-failure replay step, so the live
890    /// progress bar and JSON pulse stream surface them from the first emission instead of
891    /// jumping at the final report.
892    pub fn invariant_fuzz(
893        &mut self,
894        invariant_contract: InvariantContract<'_>,
895        fuzz_fixtures: &FuzzFixtures,
896        fuzz_state: EvmFuzzState,
897        progress: Option<&ProgressBar>,
898        early_exit: &EarlyExit,
899        initial_handler_failures: std::collections::HashMap<
900            (Address, Selector),
901            InvariantFuzzError,
902        >,
903    ) -> Result<InvariantFuzzTestResult> {
904        let campaign_spec = InvariantCampaignSpec::new(self.config.runs);
905        let worker_plans = campaign_spec.worker_plans(invariant_worker_count_with_threads(
906            &self.config,
907            rayon::current_num_threads(),
908            self.invariant_campaign_anchors,
909        ))?;
910        let actual_worker_count = worker_plans.len();
911        let campaign_seed =
912            self.prepare_campaign_seed(&invariant_contract, initial_handler_failures)?;
913        let replay_targets = FuzzRunIdentifiedContracts::new(
914            campaign_seed.targeted_contracts.clone(),
915            campaign_seed.targets_are_updatable,
916        );
917        let mut corpus_replay_executor = self.executor.clone();
918        corpus_replay_executor.inspector_mut().collect_evm_cmp_log(
919            invariant_worker_collects_evm_cmp_log(&self.config, 0, actual_worker_count),
920        );
921        let dynamic = self.dynamic_target_ctx();
922        let mut corpus_seed = WorkerCorpusSeed::load_from_disk(
923            &self.config.corpus,
924            None,
925            Some(&corpus_replay_executor),
926            ReplayTarget {
927                stateless: None,
928                fuzzed_contracts: Some(&replay_targets),
929                dynamic: Some(&dynamic),
930                senders: Some(&campaign_seed.sender_filters),
931            },
932        )?;
933        if invariant_contract.is_optimization()
934            && let Some(best_value) = corpus_seed.optimization_best_value
935        {
936            let sequence = (0..corpus_seed.optimization_best_sequence.len()).collect::<Vec<_>>();
937            let replayed = check_sequence_value(
938                self.executor.clone(),
939                &corpus_seed.optimization_best_sequence,
940                &sequence,
941                invariant_contract.address,
942                invariant_contract.anchor_calldata(),
943            );
944            if !matches!(replayed, Ok(Some(value)) if value == best_value) {
945                // Keep the sequence in the mutation corpus, but do not report its stale value.
946                corpus_seed.optimization_best_value = None;
947                corpus_seed.optimization_best_sequence.clear();
948            }
949        }
950        let mut runner = self.runner.clone();
951        let config = self.config.clone();
952        let setup_contracts = self.setup_contracts;
953        let project_contracts = self.project_contracts;
954        let base_executor = self.executor.clone();
955        let focus_seed = invariant_focus_seed(&mut runner, self.fuzz_seed, actual_worker_count);
956        let campaign_state =
957            Arc::new(InvariantCampaignState::new(early_exit.clone(), self.config.timeout));
958
959        let frontier_test = self
960            .config
961            .corpus
962            .capture_branch_frontiers()
963            .then(|| invariant_contract.anchor().clone());
964        let mut worker_outputs = if actual_worker_count > 1 {
965            let worker_jobs = worker_plans
966                .into_iter()
967                .map(|worker_plan| {
968                    let worker_runner =
969                        invariant_worker_runner(&mut runner, worker_plan.worker_id, self.fuzz_seed);
970                    let gas_report_samples = gas_report_samples_for_worker(
971                        config.gas_report_samples,
972                        worker_plan.worker_id,
973                        actual_worker_count,
974                    );
975                    let collect_cmp_log = invariant_worker_collects_evm_cmp_log(
976                        &config,
977                        worker_plan.worker_id,
978                        actual_worker_count,
979                    );
980                    (worker_plan, worker_runner, gas_report_samples, collect_cmp_log)
981                })
982                .collect::<Vec<_>>();
983            worker_jobs
984                .into_par_iter()
985                .map(|(worker_plan, worker_runner, gas_report_samples, collect_cmp_log)| {
986                    let _guard =
987                        info_span!("invariant_worker", id = worker_plan.worker_id).entered();
988                    let timer = Instant::now();
989                    let (worker_campaign_seed, worker_corpus_seed) =
990                        campaign_seed_and_corpus_seed_for_worker(
991                            &campaign_seed,
992                            &corpus_seed,
993                            worker_plan,
994                            actual_worker_count,
995                            collect_cmp_log,
996                            focus_seed,
997                        );
998                    let output = Self::run_invariant_worker(
999                        base_executor.clone(),
1000                        worker_runner,
1001                        config.clone(),
1002                        setup_contracts,
1003                        project_contracts,
1004                        worker_plan,
1005                        invariant_contract.clone(),
1006                        fuzz_fixtures,
1007                        fuzz_state.fork(),
1008                        progress,
1009                        &campaign_state,
1010                        worker_campaign_seed,
1011                        worker_corpus_seed,
1012                        actual_worker_count,
1013                        gas_report_samples,
1014                    );
1015                    if output.is_err() {
1016                        campaign_state.request_terminal_stop();
1017                    }
1018                    debug!("finished in {:?}", timer.elapsed());
1019                    output
1020                })
1021                .collect::<Result<Vec<_>>>()?
1022        } else {
1023            let worker_plan = worker_plans[0];
1024            let runner =
1025                invariant_worker_runner(&mut runner, worker_plan.worker_id, self.fuzz_seed);
1026            let gas_report_samples = config.gas_report_samples as usize;
1027            let collect_cmp_log = invariant_worker_collects_evm_cmp_log(
1028                &config,
1029                worker_plan.worker_id,
1030                actual_worker_count,
1031            );
1032            let (worker_campaign_seed, worker_corpus_seed) =
1033                campaign_seed_and_corpus_seed_for_worker(
1034                    &campaign_seed,
1035                    &corpus_seed,
1036                    worker_plan,
1037                    actual_worker_count,
1038                    collect_cmp_log,
1039                    focus_seed,
1040                );
1041            vec![Self::run_invariant_worker(
1042                base_executor,
1043                runner,
1044                config,
1045                setup_contracts,
1046                project_contracts,
1047                worker_plan,
1048                invariant_contract,
1049                fuzz_fixtures,
1050                fuzz_state,
1051                progress,
1052                &campaign_state,
1053                worker_campaign_seed,
1054                worker_corpus_seed,
1055                actual_worker_count,
1056                gas_report_samples,
1057            )?]
1058        };
1059
1060        let frontier_limit = self.config.corpus.frontier_limit;
1061        if let (Some(frontier_dir), Some(frontier_test)) =
1062            (&self.config.corpus.frontier_dir, frontier_test.as_ref())
1063        {
1064            let frontiers = merge_frontiers(
1065                frontier_limit,
1066                worker_outputs.iter_mut().flat_map(|(_, frontiers)| frontiers.drain(..)),
1067            );
1068            if !frontiers.is_empty() {
1069                let artifact = StatefulFuzzBranchFrontierArtifact::new(
1070                    frontier_test,
1071                    frontier_limit,
1072                    frontiers,
1073                );
1074                if let Err(err) = write_frontier_artifact(frontier_dir, &artifact) {
1075                    warn!(%err, path = ?frontier_dir, "failed to write fuzz branch frontier artifact");
1076                }
1077            }
1078        }
1079
1080        let mut aggregator = InvariantCampaignAggregator::new(campaign_spec);
1081        for (worker_output, _) in worker_outputs {
1082            aggregator.push(worker_output);
1083        }
1084        let mut result = if campaign_state.is_timed_campaign() {
1085            aggregator.finish_partial()?
1086        } else {
1087            aggregator.finish_campaign()?
1088        };
1089        result.interrupted = campaign_state.interrupted(result.runs, self.config.runs);
1090        persist_campaign_optimization(
1091            &self.config.corpus,
1092            result.optimization_best_value,
1093            &result.optimization_best_sequence,
1094        );
1095        Ok(result)
1096    }
1097
1098    /// Runs one worker-local slice of an invariant campaign.
1099    #[allow(clippy::too_many_arguments)]
1100    fn run_invariant_worker(
1101        mut executor: Executor<FEN>,
1102        runner: TestRunner,
1103        config: InvariantConfig,
1104        setup_contracts: &'a ContractsByAddress,
1105        project_contracts: &'a ContractsByArtifact,
1106        plan: InvariantWorkerPlan,
1107        invariant_contract: InvariantContract<'_>,
1108        fuzz_fixtures: &FuzzFixtures,
1109        fuzz_state: EvmFuzzState,
1110        progress: Option<&ProgressBar>,
1111        campaign_state: &InvariantCampaignState,
1112        campaign_seed: InvariantCampaignSeed,
1113        corpus_seed: WorkerCorpusSeed,
1114        worker_count: usize,
1115        gas_report_samples: usize,
1116    ) -> Result<(InvariantWorkerOutput, Vec<FuzzBranchFrontier>)> {
1117        // Note: invariant function signatures (no inputs) are validated upstream in the
1118        // suite runner so parameterized `invariant_*` functions are rejected with a per-test
1119        // failure entry before any campaign runs.
1120        let config = invariant_worker_config(config, plan.worker_id, worker_count);
1121        let frontier_limit = if config.corpus.capture_branch_frontiers()
1122            && invariant_worker_collects_evm_cmp_log(&config, plan.worker_id, worker_count)
1123        {
1124            config.corpus.frontier_limit
1125        } else {
1126            0
1127        };
1128        let mut frontier_recorder = FuzzFrontierRecorder::new(frontier_limit);
1129        executor.inspector_mut().set_execution_cancellation(campaign_state.cancellation().clone());
1130
1131        let (mut invariant_test, mut corpus_manager) = Self::prepare_worker(
1132            &mut executor,
1133            plan,
1134            worker_count,
1135            &invariant_contract,
1136            fuzz_fixtures,
1137            fuzz_state,
1138            &runner,
1139            &config,
1140            &campaign_seed,
1141            corpus_seed,
1142        )?;
1143        let mut runs = 0;
1144        campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1145
1146        // Invariant runs with edge coverage if corpus dir is set or showing edge coverage.
1147        let edge_coverage_enabled = config.corpus.collect_edge_coverage();
1148
1149        'stop: while should_continue_invariant_worker(campaign_state, runs, plan) {
1150            // Per-run failure count snapshot used to gate `afterInvariant` below.
1151            let failures_before_run = invariant_test.test_data.failures.invariant_count();
1152            let failures_checkpoint = invariant_test.test_data.failures.clone();
1153            let failures_revision = failures_checkpoint.revision();
1154            let mut stop_after_run = false;
1155            let mut run_cancelled = false;
1156            let mut observed_call_entries = Vec::<(Vec<ObservedCall>, BasicTxDetails)>::new();
1157
1158            let call_campaign = FuzzCampaign::new(FuzzCampaignMode::Invariant {
1159                check_interval: config.check_interval,
1160                optimization: invariant_contract.is_optimization(),
1161            });
1162
1163            let sequence_plan =
1164                corpus_manager.new_sequence(&mut invariant_test.test_data.branch_runner)?;
1165            let initial_seq = sequence_plan.initial();
1166
1167            let run_depth =
1168                invariant_run_depth(&config, &mut invariant_test.test_data.branch_runner);
1169
1170            let mut corpus_run = Option::<RecordedCallSequence>::None;
1171            let mut frontier_run = (frontier_limit > 0).then(|| RecordedCallSequence {
1172                inputs: Vec::with_capacity(run_depth as usize),
1173                cmp_seq: Vec::with_capacity(run_depth as usize),
1174            });
1175
1176            // Create current invariant run data.
1177            let mut current_run = InvariantTestRun::new(
1178                initial_seq[0].clone(),
1179                // Before each run, we must reset the backend state.
1180                executor.clone(),
1181                run_depth as usize,
1182            );
1183
1184            // We stop the run immediately if we have reverted, and `fail_on_revert` is set.
1185            if config.fail_on_revert && invariant_test.reverts() > 0 {
1186                campaign_state.request_terminal_stop();
1187                return Err(eyre!("call reverted"));
1188            }
1189
1190            let mut call_cmp_values = Vec::new();
1191            let mut call_new_coverage = false;
1192            let mut coverage_prefix_len = 0;
1193            let mut assertion_failure = false;
1194            let mut pre_merge_edges_hash = None;
1195            let mut handler = None;
1196            let mut abort_campaign = false;
1197            let sequence_outcome = call_campaign.run_sequence(
1198                &mut current_run,
1199                run_depth,
1200                |run| {
1201                    let tx = run.inputs.last_mut().expect("campaign always has a current input");
1202                    (&mut run.executor, tx)
1203                },
1204                |run| run.depth,
1205                |_| campaign_state.should_stop(),
1206                |current_run, event| {
1207                    match event {
1208                        CampaignEvent::Feedback(call_result) => {
1209                            let current_tx = current_run.inputs.last().ok_or_else(|| {
1210                                eyre!("no input generated to call fuzzed target.")
1211                            })?;
1212                            let sel_bytes: [u8; 4] = current_tx
1213                                .call_details
1214                                .calldata
1215                                .get(..4)
1216                                .and_then(|selector| selector.try_into().ok())
1217                                .unwrap_or_default();
1218                            handler =
1219                                Some((current_tx.call_details.target, Selector::from(sel_bytes)));
1220                            if let Some(fuzzer) =
1221                                current_run.executor.inspector_mut().fuzzer.as_mut()
1222                            {
1223                                invariant_test.fuzz_state.collect_fuzzer_values(fuzzer);
1224                            }
1225                            call_cmp_values = call_result.evm_cmp_values.take().unwrap_or_default();
1226                            let discarded = call_result.result.as_ref() == MAGIC_ASSUME;
1227                            if config.show_metrics {
1228                                invariant_test.record_metrics(
1229                                    current_tx,
1230                                    call_result.reverted,
1231                                    discarded,
1232                                );
1233                            }
1234                            HitMaps::merge_opt(
1235                                &mut current_run.line_coverage,
1236                                call_result.line_coverage.take(),
1237                            );
1238                            assertion_failure = !discarded
1239                                && did_fail_on_assert(call_result, &call_result.state_changeset);
1240                            pre_merge_edges_hash = assertion_failure
1241                                .then(|| error::snapshot_edge_fingerprint(call_result))
1242                                .flatten();
1243                            call_new_coverage = corpus_manager.merge_edge_coverage(call_result);
1244                            if call_new_coverage {
1245                                current_run.new_coverage = true;
1246                            }
1247                            let observed_calls = std::mem::take(&mut call_result.observed_calls);
1248                            if call_new_coverage && !observed_calls.is_empty() {
1249                                observed_call_entries.push((observed_calls, current_tx.clone()));
1250                            }
1251                        }
1252                        CampaignEvent::Check { result, kind, should_check } => {
1253                            let mut result =
1254                                result.take().expect("campaign check result is available");
1255                            if kind == CampaignCallKind::AssumptionRejected {
1256                                current_run.inputs.pop();
1257                                current_run.rejects += 1;
1258                                if current_run.rejects > config.max_assume_rejects {
1259                                    invariant_test.set_error(
1260                                        invariant_contract.anchor(),
1261                                        InvariantFuzzError::MaxAssumeRejects(
1262                                            config.max_assume_rejects,
1263                                        ),
1264                                    );
1265                                    campaign_state.request_terminal_stop();
1266                                    abort_campaign = true;
1267                                    return Ok(CampaignControl::Stop);
1268                                }
1269                                return Ok(CampaignControl::Continue);
1270                            }
1271                            debug_assert_eq!(kind, CampaignCallKind::Accepted);
1272                            if let Some(frontier_run) = &mut frontier_run {
1273                                frontier_run.inputs.push(
1274                                    current_run
1275                                        .inputs
1276                                        .last()
1277                                        .expect("accepted call has a campaign input")
1278                                        .clone(),
1279                                );
1280                                frontier_run.cmp_seq.push(call_cmp_values.clone());
1281                            }
1282                            if config.corpus.is_coverage_guided() {
1283                                let preserve_revert =
1284                                    invariant_contract.is_optimization() || config.has_delay();
1285                                let retain_for_corpus =
1286                                    !result.reverted || preserve_revert || call_new_coverage;
1287                                if let Some(corpus_run) = &mut corpus_run {
1288                                    if retain_for_corpus {
1289                                        corpus_run.inputs.push(
1290                                            current_run
1291                                                .inputs
1292                                                .last()
1293                                                .expect("accepted call has a campaign input")
1294                                                .clone(),
1295                                        );
1296                                        corpus_run
1297                                            .cmp_seq
1298                                            .push(std::mem::take(&mut call_cmp_values));
1299                                    }
1300                                } else if result.reverted && !preserve_revert && call_new_coverage {
1301                                    let mut cmp_seq = std::mem::take(&mut current_run.cmp_seq);
1302                                    cmp_seq.push(std::mem::take(&mut call_cmp_values));
1303                                    corpus_run = Some(RecordedCallSequence {
1304                                        inputs: current_run.inputs.clone(),
1305                                        cmp_seq,
1306                                    });
1307                                } else if !result.reverted || preserve_revert {
1308                                    current_run.cmp_seq.push(std::mem::take(&mut call_cmp_values));
1309                                }
1310                                if call_new_coverage {
1311                                    coverage_prefix_len = corpus_run
1312                                        .as_ref()
1313                                        .map_or(current_run.inputs.len(), |history| {
1314                                            history.inputs.len()
1315                                        });
1316                                }
1317                            }
1318                            let (handler_target, handler_selector) =
1319                                handler.take().expect("feedback precedes campaign checks");
1320                            let mut state_changeset = std::mem::take(&mut result.state_changeset);
1321                            if !result.reverted {
1322                                let mapping_slots = current_run
1323                                    .executor
1324                                    .inspector()
1325                                    .fuzzer
1326                                    .as_ref()
1327                                    .and_then(|fuzzer| fuzzer.mapping_slots.as_ref());
1328                                collect_data(
1329                                    &invariant_test,
1330                                    &mut state_changeset,
1331                                    current_run.inputs.last().expect("checked above"),
1332                                    &result,
1333                                    run_depth,
1334                                    mapping_slots,
1335                                );
1336                            }
1337
1338                            let created_before = current_run.created_contracts.len();
1339                            if let Err(error) =
1340                                &invariant_test.targeted_contracts.collect_created_contracts(
1341                                    &state_changeset,
1342                                    project_contracts,
1343                                    setup_contracts,
1344                                    &campaign_seed.artifact_filters,
1345                                    &mut current_run.created_contracts,
1346                                )
1347                            {
1348                                warn!(target: "forge::test", "{error}");
1349                            }
1350                            invariant_test.invalidate_metric_key_cache(
1351                                &current_run.created_contracts[created_before..],
1352                            );
1353                            current_run
1354                                .fuzz_runs
1355                                .push(FuzzCase { gas: result.gas_used, stipend: result.stipend });
1356
1357                            let continues = if should_check {
1358                                let outcome = can_continue(
1359                                    &invariant_contract,
1360                                    &mut invariant_test,
1361                                    current_run,
1362                                    &config,
1363                                    result,
1364                                    &state_changeset,
1365                                    handler_target,
1366                                    handler_selector,
1367                                    assertion_failure,
1368                                    pre_merge_edges_hash,
1369                                )
1370                                .map_err(|error| eyre!(error.to_string()))?;
1371                                run_cancelled = outcome.cancelled;
1372                                outcome.continues
1373                            } else {
1374                                if result.reverted {
1375                                    invariant_test.test_data.failures.reverts += 1;
1376                                }
1377                                if assertion_failure {
1378                                    let call_reverted = result.reverted;
1379                                    error::record_handler_assertion_bug(
1380                                        &invariant_contract,
1381                                        &config,
1382                                        &invariant_test.targeted_contracts,
1383                                        &mut invariant_test.test_data.failures,
1384                                        &mut current_run.inputs,
1385                                        handler_target,
1386                                        handler_selector,
1387                                        pre_merge_edges_hash,
1388                                        result,
1389                                        call_reverted,
1390                                        invariant_contract.is_optimization(),
1391                                    );
1392                                    true
1393                                } else if result.reverted && config.fail_on_revert {
1394                                    let anchor = invariant_contract.anchor();
1395                                    let case_data = error::InvariantRunCtx {
1396                                        contract: &invariant_contract,
1397                                        config: &config,
1398                                        targeted_contracts: &invariant_test.targeted_contracts,
1399                                        calldata: &current_run.inputs,
1400                                    }
1401                                    .failed_case(anchor, config.fail_on_revert, false, result, &[]);
1402                                    invariant_test.test_data.failures.record_failure(
1403                                        anchor,
1404                                        InvariantFuzzError::Revert(case_data),
1405                                    );
1406                                    false
1407                                } else {
1408                                    if result.reverted
1409                                        && !invariant_contract.is_optimization()
1410                                        && !config.has_delay()
1411                                    {
1412                                        current_run.inputs.pop();
1413                                    }
1414                                    true
1415                                }
1416                            };
1417
1418                            if run_cancelled {
1419                                return Ok(CampaignControl::Stop);
1420                            }
1421                            if !continues || current_run.depth == run_depth - 1 {
1422                                current_run.save_last_run_inputs = true;
1423                            }
1424                            if !continues {
1425                                if invariant_contract.invariant_fns.len() == 1
1426                                    || config.fail_on_revert
1427                                {
1428                                    campaign_state.request_terminal_stop();
1429                                    stop_after_run = true;
1430                                }
1431                                return Ok(CampaignControl::Stop);
1432                            }
1433                        }
1434                        CampaignEvent::Advance => current_run.depth += 1,
1435                        CampaignEvent::Next { discarded, depth } => {
1436                            current_run.inputs.push(sequence_plan.next(
1437                                &mut invariant_test.test_data.branch_runner,
1438                                discarded,
1439                                depth as usize,
1440                            )?);
1441                        }
1442                        CampaignEvent::PostCheck => {
1443                            // Multi-predicate campaigns keep running after earlier failures, but
1444                            // the hook must still execute on subsequent clean runs.
1445                            if !abort_campaign
1446                                && !run_cancelled
1447                                && invariant_contract.call_after_invariant
1448                                && invariant_test.test_data.failures.invariant_count()
1449                                    == failures_before_run
1450                            {
1451                                let (broken, hook_cancelled) = assert_after_invariant(
1452                                    &invariant_contract,
1453                                    &mut invariant_test,
1454                                    current_run,
1455                                    &config,
1456                                )
1457                                .map_err(|_| eyre!("Failed to call afterInvariant"))?;
1458                                if hook_cancelled {
1459                                    run_cancelled = true;
1460                                } else if broken.is_some() {
1461                                    current_run.save_last_run_inputs = true;
1462                                }
1463                            }
1464                        }
1465                    }
1466                    Ok(CampaignControl::Continue)
1467                },
1468            )?;
1469            if sequence_outcome == CampaignSequenceOutcome::Cancelled {
1470                // A timed-out partial run remains successful, matching the previous worker
1471                // behavior, but it must not be persisted or counted.
1472                run_cancelled = true;
1473            }
1474            if abort_campaign {
1475                break 'stop;
1476            }
1477
1478            // The worker which requested a terminal stop for its own failure still owns a
1479            // complete failing run. All other campaign stops discard the partial run before any
1480            // corpus persistence or accounting.
1481            if campaign_state.should_stop() && !stop_after_run {
1482                run_cancelled = true;
1483            }
1484
1485            if run_cancelled {
1486                let completed_finding =
1487                    invariant_test.test_data.failures.revision() != failures_revision;
1488                if completed_finding {
1489                    record_new_invariant_failures(
1490                        campaign_state,
1491                        &invariant_contract,
1492                        &invariant_test.test_data.failures,
1493                    );
1494                    campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1495                } else {
1496                    invariant_test.test_data.failures.clone_from(&failures_checkpoint);
1497                }
1498                break 'stop;
1499            }
1500
1501            if invariant_test.test_data.failures.invariant_count() > failures_before_run {
1502                record_new_invariant_failures(
1503                    campaign_state,
1504                    &invariant_contract,
1505                    &invariant_test.test_data.failures,
1506                );
1507            }
1508            if invariant_test.test_data.failures.handler_count()
1509                > failures_checkpoint.handler_count()
1510            {
1511                campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1512            }
1513
1514            for (observed_calls, parent_tx) in observed_call_entries {
1515                corpus_manager.hoist_observed_calls(
1516                    &observed_calls,
1517                    &parent_tx,
1518                    &invariant_test.targeted_contracts,
1519                    &campaign_seed.sender_filters,
1520                    CorpusInsertionMode::Live,
1521                );
1522            }
1523
1524            // Extend corpus only after the run and its optional hook have completed.
1525            if let Some(frontier_run) = &frontier_run {
1526                frontier_recorder.capture_sequence(&frontier_run.inputs, &frontier_run.cmp_seq);
1527            }
1528            let optimization = current_run.optimization_value.map(|v| {
1529                let prefix = current_run.inputs[..current_run.optimization_prefix_len].to_vec();
1530                (v, prefix)
1531            });
1532            let mut corpus_inputs = corpus_run
1533                .as_ref()
1534                .map_or(current_run.inputs.as_slice(), |history| history.inputs.as_slice());
1535            let mut corpus_cmp_seq = corpus_run
1536                .as_ref()
1537                .map_or(current_run.cmp_seq.as_slice(), |history| history.cmp_seq.as_slice());
1538            if current_run.new_coverage && !invariant_contract.is_optimization() {
1539                corpus_inputs = &corpus_inputs[..coverage_prefix_len];
1540                corpus_cmp_seq = &corpus_cmp_seq[..coverage_prefix_len];
1541            }
1542            if worker_count > 1 {
1543                corpus_manager.process_inputs_for_campaign(
1544                    corpus_inputs,
1545                    corpus_cmp_seq,
1546                    current_run.new_coverage,
1547                    optimization,
1548                );
1549            } else {
1550                corpus_manager.process_inputs(
1551                    corpus_inputs,
1552                    corpus_cmp_seq,
1553                    current_run.new_coverage,
1554                    optimization,
1555                );
1556            }
1557
1558            // End current invariant test run.
1559            if current_run.save_last_run_inputs {
1560                invariant_test.set_last_run_inputs(&current_run.inputs);
1561            }
1562            if let Some(value) = current_run.optimization_value {
1563                invariant_test.update_optimization_value(
1564                    value,
1565                    &current_run.inputs[..current_run.optimization_prefix_len],
1566                );
1567            }
1568            invariant_test.merge_line_coverage(current_run.line_coverage.take());
1569            for fuzz_run in &current_run.fuzz_runs {
1570                campaign_state.record_call(fuzz_run.gas);
1571            }
1572            current_run.drop_corpus_payloads();
1573            invariant_test.end_run(current_run, gas_report_samples);
1574            runs += 1;
1575            let total_runs = campaign_state.increment_runs();
1576            debug_assert!(
1577                campaign_state.is_timed_campaign() || total_runs <= config.runs,
1578                "worker runs were not distributed correctly"
1579            );
1580            if let Some(progress) = progress {
1581                progress.inc(1);
1582                campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1583                // Display current best value, corpus metrics, and failure counts.
1584                let best = invariant_test.test_data.optimization_best_value;
1585                let failure_metrics = campaign_state.failure_metrics();
1586                let broken = failure_metrics.unique_failures.len();
1587                let handler_bugs = failure_metrics.broken_handlers;
1588                let total_invariants = invariant_contract.invariant_fns.len();
1589                if edge_coverage_enabled || best.is_some() || broken > 0 || handler_bugs > 0 {
1590                    let mut msg = String::new();
1591                    if let Some(best) = best {
1592                        msg.push_str(&format!("best: {best}"));
1593                    }
1594                    if edge_coverage_enabled {
1595                        if !msg.is_empty() {
1596                            msg.push_str(", ");
1597                        }
1598                        msg.push_str(&format!("{}", corpus_manager.metrics));
1599                        match corpus_manager.time_since_new_edge() {
1600                            Some(elapsed) => msg.push_str(&format!(
1601                                "\n        - time since new edge: {:.1}s",
1602                                elapsed.as_secs_f64()
1603                            )),
1604                            None => msg.push_str("\n        - time since new edge: never"),
1605                        }
1606                    }
1607                    if broken > 0 {
1608                        if !msg.is_empty() {
1609                            msg.push_str(", ");
1610                        }
1611                        msg.push_str(&format!("❌ {broken}/{total_invariants} broken"));
1612                    }
1613                    if handler_bugs > 0 {
1614                        if !msg.is_empty() {
1615                            msg.push_str(", ");
1616                        }
1617                        msg.push_str(&format!("⚠ {handler_bugs} handler bug(s)"));
1618                    }
1619                    let msg =
1620                        if worker_count > 1 { format!("[w{}] {msg}", plan.worker_id) } else { msg };
1621                    progress.set_message(msg);
1622                }
1623            } else if edge_coverage_enabled
1624                && campaign_state.should_emit_metrics_report(DURATION_BETWEEN_METRICS_REPORT)
1625            {
1626                campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1627                let failure_metrics = campaign_state.failure_metrics();
1628                let (total_txs, total_gas) = campaign_state.throughput_totals();
1629                let throughput = InvariantThroughputMetrics { total_txs, total_gas };
1630                // Display corpus metrics inline as JSON.
1631                let metrics = build_invariant_progress_json(
1632                    InvariantProgressContext {
1633                        timestamp_secs: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
1634                        contract_name: invariant_contract.name,
1635                        optimization_best: invariant_test.test_data.optimization_best_value,
1636                        throughput,
1637                        elapsed: campaign_state.elapsed(),
1638                        worker_id: plan.worker_id,
1639                        worker_count,
1640                        time_since_new_edge: corpus_manager.time_since_new_edge(),
1641                    },
1642                    &corpus_manager.metrics,
1643                    &failure_metrics,
1644                );
1645                let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
1646            }
1647
1648            if stop_after_run {
1649                break 'stop;
1650            }
1651        }
1652
1653        trace!(?fuzz_fixtures);
1654        invariant_test.fuzz_state.log_stats();
1655
1656        // Campaign-local terminal stops and deadlines must not suppress post-campaign shrinking.
1657        executor.inspector_mut().set_early_exit(campaign_state.early_exit().clone());
1658        Self::shrink_handler_failures(
1659            &config,
1660            &executor,
1661            &mut invariant_test.test_data,
1662            progress,
1663            campaign_state.early_exit(),
1664        );
1665
1666        // Move out the final test data and drop worker-local fuzz state before returning this
1667        // worker's aggregate output. Long invariant campaigns can leave large dictionaries and
1668        // target state behind; once shrinking is complete, only `test_data` is needed.
1669        let InvariantTest { fuzz_state: _, targeted_contracts: _, test_data: result } =
1670            invariant_test;
1671        let reverts = result.failures.reverts;
1672        let (errors, handler_errors) = result.failures.partition();
1673        let worker_result = InvariantFuzzTestResult::new(
1674            errors,
1675            handler_errors,
1676            result.runs,
1677            result.calls,
1678            reverts,
1679            result.last_run_inputs,
1680            result.gas_report_traces,
1681            result.line_coverage,
1682            result.metrics.into_iter().collect(),
1683            if plan.worker_id == 0 { corpus_manager.failed_replays } else { 0 },
1684            1,
1685            result.optimization_best_value,
1686            result.optimization_best_sequence,
1687        );
1688        drop(corpus_manager);
1689        let reported_plan = if campaign_state.is_timed_campaign() {
1690            InvariantWorkerPlan { runs, ..plan }
1691        } else {
1692            // Sharded campaigns must report the original assigned range. Early worker exit changes
1693            // the number of executed runs, but it must not shrink `plan.runs`: following workers'
1694            // `first_global_run` offsets were computed from the original partition.
1695            plan
1696        };
1697        Ok((
1698            InvariantWorkerOutput { plan: reported_plan, result: worker_result },
1699            frontier_recorder.into_frontiers(),
1700        ))
1701    }
1702
1703    fn shrink_handler_failures(
1704        config: &InvariantConfig,
1705        executor: &Executor<FEN>,
1706        result: &mut InvariantTestData,
1707        progress: Option<&ProgressBar>,
1708        early_exit: &EarlyExit,
1709    ) {
1710        let total = result.failures.handler_count();
1711        if total == 0 {
1712            return;
1713        }
1714
1715        for (idx, error) in result.failures.handler_failures_mut().enumerate() {
1716            if early_exit.should_stop() {
1717                break;
1718            }
1719            let Some(failure) = error.as_handler_assertion_mut() else {
1720                continue;
1721            };
1722            let shrink_progress = shrink::ShrinkProgress::new(
1723                config,
1724                progress,
1725                &format!("handler {:#x}::{}", failure.reverter, failure.selector),
1726                Some((idx + 1, total)),
1727                None,
1728                false,
1729            );
1730            match shrink::shrink_handler_sequence(
1731                config,
1732                &failure.call_sequence,
1733                failure.edge_fingerprint,
1734                executor,
1735                &shrink_progress,
1736                early_exit,
1737            ) {
1738                Ok(shrunk) if !shrunk.is_empty() => {
1739                    failure.call_sequence = shrunk;
1740                }
1741                Ok(_) => {}
1742                Err(e) => trace!(target: "forge::test", "handler shrink failed: {e}"),
1743            }
1744        }
1745    }
1746
1747    fn prepare_campaign_seed(
1748        &mut self,
1749        invariant_contract: &InvariantContract<'_>,
1750        initial_handler_failures: std::collections::HashMap<
1751            (Address, Selector),
1752            InvariantFuzzError,
1753        >,
1754    ) -> Result<InvariantCampaignSeed> {
1755        self.select_contract_artifacts(invariant_contract.address)?;
1756        let (sender_filters, targeted_contracts) =
1757            self.select_contracts_and_senders(invariant_contract.address)?;
1758        let targets_are_updatable = targeted_contracts.is_updatable;
1759        let targeted_contracts = targeted_contracts.targets().clone();
1760
1761        Ok(InvariantCampaignSeed {
1762            artifact_filters: self.artifact_filters.clone(),
1763            sender_filters,
1764            targeted_contracts,
1765            targets_are_updatable,
1766            initial_handler_failures,
1767        })
1768    }
1769
1770    /// Prepares worker-local structures to execute an invariant campaign slice.
1771    #[allow(clippy::too_many_arguments)]
1772    fn prepare_worker(
1773        executor: &mut Executor<FEN>,
1774        plan: InvariantWorkerPlan,
1775        worker_count: usize,
1776        invariant_contract: &InvariantContract<'_>,
1777        fuzz_fixtures: &FuzzFixtures,
1778        fuzz_state: EvmFuzzState,
1779        runner: &TestRunner,
1780        config: &InvariantConfig,
1781        campaign_seed: &InvariantCampaignSeed,
1782        corpus_seed: WorkerCorpusSeed,
1783    ) -> Result<(InvariantTest, WorkerCorpus)> {
1784        let fuzz_state = fuzz_state.into_invariant();
1785        let targeted_contracts = FuzzRunIdentifiedContracts::new(
1786            campaign_seed.targeted_contracts.clone(),
1787            campaign_seed.targets_are_updatable,
1788        );
1789        executor.inspector_mut().collect_evm_cmp_log(invariant_worker_collects_evm_cmp_log(
1790            config,
1791            plan.worker_id,
1792            worker_count,
1793        ));
1794
1795        // Creates the invariant strategy.
1796        let generator = TxGenerator::invariant(
1797            fuzz_state.clone(),
1798            campaign_seed.sender_filters.clone(),
1799            targeted_contracts.clone(),
1800            config.clone(),
1801            fuzz_fixtures.clone(),
1802        );
1803
1804        // If any of the targeted contracts have the storage layout enabled then we can sample
1805        // mapping values. To accomplish, we need to record the mapping storage slots and keys.
1806        let mapping_slots = targeted_contracts
1807            .targets()
1808            .iter()
1809            .any(|(_, t)| t.storage_layout.is_some())
1810            .then(AddressMap::default);
1811
1812        // Set up fuzzer WITHOUT call_generator initially.
1813        // We defer call_override until after the initial invariant check to avoid
1814        // injecting random calls during setup which would break the invariant assertion.
1815        let extra_cheatcode_addresses = executor.inspector().extra_cheatcode_addresses();
1816        executor.inspector_mut().set_fuzzer(
1817            Fuzzer::new(config.dictionary.max_fuzz_dictionary_values, mapping_slots)
1818                .with_extra_cheatcode_addresses(extra_cheatcode_addresses)
1819                .with_call_recording(config.corpus.is_coverage_guided()),
1820        );
1821
1822        // Let's make sure the invariant is sound before actually starting the run:
1823        // We'll assert the invariant in its initial state, and if it fails, we'll
1824        // already know if we can early exit the invariant run.
1825        // This does not count as a fuzz run. It will just register the revert.
1826        let mut failures = InvariantFailures::new();
1827        // Seed disk-recovered handler bugs so live counters reflect them from tick 0.
1828        for (&(addr, sel), err) in &campaign_seed.initial_handler_failures {
1829            failures.seed_handler_failure(addr, sel, err.clone());
1830        }
1831        invariant_preflight_check(
1832            invariant_contract,
1833            config,
1834            &targeted_contracts,
1835            executor,
1836            &[],
1837            &mut failures,
1838        )?;
1839        if let Some(fuzzer) = executor.inspector_mut().fuzzer.as_mut() {
1840            fuzz_state.collect_fuzzer_values(fuzzer);
1841            let _ = fuzzer.take_observed_calls();
1842        }
1843        let generator = foundry_evm_fuzz::sequence::SequenceGenerator::invariant_with_fixtures(
1844            generator,
1845            fuzz_state.clone(),
1846            fuzz_fixtures.clone(),
1847            targeted_contracts.clone(),
1848            &config.corpus,
1849        )?;
1850        let mut worker = WorkerCorpus::from_seed(
1851            plan.worker_id as usize,
1852            config.corpus.clone(),
1853            generator,
1854            corpus_seed,
1855        )?;
1856
1857        if let Err(err) = worker.seed_from_test_traces(
1858            invariant_contract,
1859            &targeted_contracts,
1860            &campaign_seed.sender_filters,
1861            executor,
1862        ) {
1863            debug!(target: "corpus", %err, "failed to seed corpus from test traces");
1864        }
1865
1866        // NOW enable call_override after the initial invariant check and corpus trace seeding have
1867        // passed. This allows `override_call_strat` to inject calls during actual fuzz runs for
1868        // reentrancy vulnerability detection.
1869        if config.call_override {
1870            let target_contract_ref = Arc::new(RwLock::new(Address::ZERO));
1871
1872            // Collect handler addresses - these are the contracts we want to inject
1873            // reentrancy into (simulating malicious receive() functions).
1874            let handler_addresses: AddressSet =
1875                targeted_contracts.targets().keys().copied().collect();
1876            let override_targets = targeted_contracts
1877                .targets()
1878                .iter()
1879                .filter_map(|(address, contract)| {
1880                    let functions = contract.abi_fuzzed_functions().cloned().collect::<Vec<_>>();
1881                    (!functions.is_empty()).then_some((*address, functions))
1882                })
1883                .collect::<Vec<_>>();
1884
1885            let call_generator = RandomCallGenerator::new(
1886                invariant_contract.address,
1887                handler_addresses,
1888                runner.clone(),
1889                override_call_strat(
1890                    fuzz_state.snapshot(),
1891                    override_targets,
1892                    target_contract_ref.clone(),
1893                    fuzz_fixtures.clone(),
1894                    config.dictionary.dictionary_weight,
1895                    config.corpus.payable_value_weight,
1896                ),
1897                target_contract_ref,
1898            );
1899
1900            if let Some(fuzzer) = executor.inspector_mut().fuzzer.as_mut() {
1901                fuzzer.call_generator = Some(call_generator);
1902            }
1903        }
1904
1905        let mut invariant_test =
1906            InvariantTest::new(fuzz_state, targeted_contracts, failures, runner.clone());
1907
1908        // Seed invariant test with previously persisted optimization state,
1909        // but only if the current invariant is in optimization mode. Persisted optimization state
1910        // is a master-worker artifact loaded with the initial corpus.
1911        if invariant_contract.is_optimization() {
1912            let (opt_best_value, opt_best_sequence) = worker.optimization_initial_state();
1913            if let Some(value) = opt_best_value {
1914                invariant_test.update_optimization_value(value, &opt_best_sequence);
1915            }
1916        }
1917
1918        Ok((invariant_test, worker))
1919    }
1920
1921    /// Fills the `InvariantExecutor` with the artifact identifier filters (in `path:name` string
1922    /// format). They will be used to filter contracts after the `setUp`, and more importantly,
1923    /// during the runs.
1924    ///
1925    /// Also excludes any contract without any mutable functions.
1926    ///
1927    /// Priority:
1928    ///
1929    /// targetArtifactSelectors > excludeArtifacts > targetArtifacts
1930    pub fn select_contract_artifacts(&mut self, invariant_address: Address) -> Result<()> {
1931        let targeted_artifact_selectors = self
1932            .executor
1933            .call_sol_default(invariant_address, &IInvariantTest::targetArtifactSelectorsCall {});
1934
1935        // Insert them into the executor `targeted_abi`.
1936        for IInvariantTest::FuzzArtifactSelector { artifact, selectors } in
1937            targeted_artifact_selectors
1938        {
1939            let identifier = self.validate_selected_contract(artifact, &selectors)?;
1940            self.artifact_filters.targeted.entry(identifier).or_default().extend(selectors);
1941        }
1942
1943        let targeted_artifacts = self
1944            .executor
1945            .call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {});
1946        let excluded_artifacts = self
1947            .executor
1948            .call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {});
1949
1950        // Insert `excludeArtifacts` into the executor `excluded_abi`.
1951        for contract in excluded_artifacts {
1952            let identifier = self.validate_selected_contract(contract, &[])?;
1953
1954            if !self.artifact_filters.excluded.contains(&identifier) {
1955                self.artifact_filters.excluded.push(identifier);
1956            }
1957        }
1958
1959        // Exclude any artifact without mutable functions.
1960        for (artifact, contract) in self.project_contracts.iter() {
1961            if contract
1962                .abi
1963                .functions()
1964                .filter(|func| {
1965                    !matches!(
1966                        func.state_mutability,
1967                        alloy_json_abi::StateMutability::Pure
1968                            | alloy_json_abi::StateMutability::View
1969                    )
1970                })
1971                .count()
1972                == 0
1973                && !self.artifact_filters.excluded.contains(&artifact.identifier())
1974            {
1975                self.artifact_filters.excluded.push(artifact.identifier());
1976            }
1977        }
1978
1979        // Insert `targetArtifacts` into the executor `targeted_abi`, if they have not been seen
1980        // before.
1981        for contract in targeted_artifacts {
1982            let identifier = self.validate_selected_contract(contract, &[])?;
1983
1984            if !self.artifact_filters.targeted.contains_key(&identifier)
1985                && !self.artifact_filters.excluded.contains(&identifier)
1986            {
1987                self.artifact_filters.targeted.insert(identifier, vec![]);
1988            }
1989        }
1990        Ok(())
1991    }
1992
1993    /// Makes sure that the contract exists in the project. If so, it returns its artifact
1994    /// identifier.
1995    fn validate_selected_contract(
1996        &mut self,
1997        contract: String,
1998        selectors: &[FixedBytes<4>],
1999    ) -> Result<String> {
2000        if let Some((artifact, contract_data)) =
2001            self.project_contracts.find_by_name_or_identifier(&contract)?
2002        {
2003            // Check that the selectors really exist for this contract.
2004            for selector in selectors {
2005                contract_data
2006                    .abi
2007                    .functions()
2008                    .find(|func| func.selector().as_slice() == selector.as_slice())
2009                    .wrap_err(format!("{contract} does not have the selector {selector:?}"))?;
2010            }
2011
2012            return Ok(artifact.identifier());
2013        }
2014        eyre::bail!(
2015            "{contract} not found in the project. Allowed format: `contract_name` or `contract_path:contract_name`."
2016        );
2017    }
2018
2019    /// Selects senders and contracts based on the contract methods `targetSenders() -> address[]`,
2020    /// `targetContracts() -> address[]` and `excludeContracts() -> address[]`.
2021    pub fn select_contracts_and_senders(
2022        &self,
2023        to: Address,
2024    ) -> Result<(SenderFilters, FuzzRunIdentifiedContracts)> {
2025        let targeted_senders =
2026            self.executor.call_sol_default(to, &IInvariantTest::targetSendersCall {});
2027        let mut excluded_senders =
2028            self.executor.call_sol_default(to, &IInvariantTest::excludeSendersCall {});
2029        // Extend with default excluded addresses - https://github.com/foundry-rs/foundry/issues/4163
2030        excluded_senders.extend([
2031            CHEATCODE_ADDRESS,
2032            HARDHAT_CONSOLE_ADDRESS,
2033            DEFAULT_CREATE2_DEPLOYER,
2034        ]);
2035        // Extend with precompiles - https://github.com/foundry-rs/foundry/issues/4287
2036        excluded_senders.extend(PRECOMPILES);
2037        let sender_filters = SenderFilters::new(targeted_senders, excluded_senders);
2038
2039        let selected = self.executor.call_sol_default(to, &IInvariantTest::targetContractsCall {});
2040        let excluded = self.executor.call_sol_default(to, &IInvariantTest::excludeContractsCall {});
2041
2042        let contracts = self
2043            .setup_contracts
2044            .iter()
2045            .filter(|&(addr, (identifier, _))| {
2046                // Include to address if explicitly set as target.
2047                if *addr == to && selected.contains(&to) {
2048                    return true;
2049                }
2050
2051                *addr != to
2052                    && *addr != CHEATCODE_ADDRESS
2053                    && *addr != HARDHAT_CONSOLE_ADDRESS
2054                    && (selected.is_empty() || selected.contains(addr))
2055                    && (excluded.is_empty() || !excluded.contains(addr))
2056                    && self.artifact_filters.matches(identifier)
2057            })
2058            .map(|(addr, (identifier, abi))| {
2059                (
2060                    *addr,
2061                    TargetedContract::new(identifier.clone(), abi.clone())
2062                        .with_project_contracts(self.project_contracts),
2063                )
2064            })
2065            .collect();
2066        let mut contracts = TargetedContracts { inner: contracts };
2067
2068        self.target_interfaces(to, &mut contracts)?;
2069
2070        self.select_selectors(to, &mut contracts)?;
2071        self.exclude_default_storage_hook_callbacks(&mut contracts)?;
2072
2073        // There should be at least one contract identified as target for fuzz runs.
2074        if contracts.is_empty() {
2075            eyre::bail!("No contracts to fuzz.");
2076        }
2077        if contracts.fuzzed_functions().next().is_none() {
2078            eyre::bail!("No functions to fuzz.");
2079        }
2080
2081        Ok((sender_filters, FuzzRunIdentifiedContracts::new(contracts, selected.is_empty())))
2082    }
2083
2084    /// Excludes registered storage-hook callbacks from implicit invariant targets.
2085    ///
2086    /// Explicit selector filters take precedence, so users can still target a callback
2087    /// intentionally.
2088    fn exclude_default_storage_hook_callbacks(
2089        &self,
2090        targeted_contracts: &mut TargetedContracts,
2091    ) -> Result<()> {
2092        let Some(cheatcodes) = self.executor.inspector().cheatcodes.as_deref() else {
2093            return Ok(());
2094        };
2095        let callbacks =
2096            cheatcodes
2097                .storage_load_hooks()
2098                .chain(cheatcodes.storage_store_hooks())
2099                .map(|(_, hook)| (hook.callback_target, Selector::from(hook.callback_selector)))
2100                .chain(cheatcodes.mapping_storage_store_hooks().map(|(_, _, hook)| {
2101                    (hook.callback_target, Selector::from(hook.callback_selector))
2102                }))
2103                .collect::<HashSet<_>>();
2104
2105        for (target, selector) in callbacks {
2106            let Some(contract) = targeted_contracts.get_mut(&target) else { continue };
2107            if !contract.targeted_functions.is_empty()
2108                || contract.function_by_selector(selector).is_none()
2109            {
2110                continue;
2111            }
2112            contract.add_selectors([selector], true)?;
2113        }
2114        Ok(())
2115    }
2116
2117    /// Extends the contracts and selectors to fuzz with the addresses and ABIs specified in
2118    /// `targetInterfaces() -> (address, string[])[]`. Enables targeting of addresses that are
2119    /// not deployed during `setUp` such as when fuzzing in a forked environment. Also enables
2120    /// targeting of delegate proxies and contracts deployed with `create` or `create2`.
2121    pub fn target_interfaces(
2122        &self,
2123        invariant_address: Address,
2124        targeted_contracts: &mut TargetedContracts,
2125    ) -> Result<()> {
2126        let interfaces = self
2127            .executor
2128            .call_sol_default(invariant_address, &IInvariantTest::targetInterfacesCall {});
2129
2130        // Since `targetInterfaces` returns a tuple array there is no guarantee
2131        // that the addresses are unique this map is used to merge functions of
2132        // the specified interfaces for the same address. For example:
2133        // `[(addr1, ["IERC20", "IOwnable"])]` and `[(addr1, ["IERC20"]), (addr1, ("IOwnable"))]`
2134        // should be equivalent.
2135        let mut combined = TargetedContracts::new();
2136
2137        // Loop through each address and its associated artifact identifiers.
2138        // We're borrowing here to avoid taking full ownership.
2139        for IInvariantTest::FuzzInterface { addr, artifacts } in &interfaces {
2140            // Identifiers are specified as an array, so we loop through them.
2141            for identifier in artifacts {
2142                // Try to find the contract by name or identifier in the project's contracts.
2143                if let Some((_, contract_data)) =
2144                    self.project_contracts.iter().find(|(artifact, _)| {
2145                        &artifact.name == identifier || &artifact.identifier() == identifier
2146                    })
2147                {
2148                    let abi = &contract_data.abi;
2149                    combined
2150                        // Check if there's an entry for the given key in the 'combined' map.
2151                        .entry(*addr)
2152                        // If the entry exists, extends its ABI with the function list.
2153                        .and_modify(|entry| {
2154                            // Extend the ABI's function list with the new functions.
2155                            entry.abi.functions.extend(abi.functions.clone());
2156                            entry.rebuild_function_lookups();
2157                        })
2158                        // Otherwise insert it into the map.
2159                        .or_insert_with(|| {
2160                            let mut contract =
2161                                TargetedContract::new(identifier.clone(), abi.clone());
2162                            contract.storage_layout =
2163                                contract_data.storage_layout.as_ref().map(Arc::clone);
2164                            contract
2165                        });
2166                }
2167            }
2168        }
2169
2170        targeted_contracts.extend(combined.inner);
2171
2172        Ok(())
2173    }
2174
2175    /// Selects the functions to fuzz based on the contract method `targetSelectors()` and
2176    /// `targetArtifactSelectors()`.
2177    pub fn select_selectors(
2178        &self,
2179        address: Address,
2180        targeted_contracts: &mut TargetedContracts,
2181    ) -> Result<()> {
2182        for (address, (identifier, _)) in self.setup_contracts {
2183            if let Some(selectors) = self.artifact_filters.targeted.get(identifier) {
2184                self.add_address_with_functions(*address, selectors, false, targeted_contracts)?;
2185            }
2186        }
2187
2188        let mut target_test_selectors = vec![];
2189        let mut excluded_test_selectors = vec![];
2190
2191        // Collect contract functions marked as target for fuzzing campaign.
2192        let selectors =
2193            self.executor.call_sol_default(address, &IInvariantTest::targetSelectorsCall {});
2194        for IInvariantTest::FuzzSelector { addr, selectors } in selectors {
2195            if addr == address {
2196                target_test_selectors = selectors.clone();
2197            }
2198            self.add_address_with_functions(addr, &selectors, false, targeted_contracts)?;
2199        }
2200
2201        // Collect contract functions excluded from fuzzing campaign.
2202        let excluded_selectors =
2203            self.executor.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {});
2204        for IInvariantTest::FuzzSelector { addr, selectors } in excluded_selectors {
2205            if addr == address {
2206                // If fuzz selector address is the test contract, then record selectors to be
2207                // later excluded if needed.
2208                excluded_test_selectors = selectors.clone();
2209            }
2210            self.add_address_with_functions(addr, &selectors, true, targeted_contracts)?;
2211        }
2212
2213        if target_test_selectors.is_empty()
2214            && let Some(target) = targeted_contracts.get(&address)
2215        {
2216            // If test contract is marked as a target and no target selector explicitly set, then
2217            // include only state-changing functions that are not reserved and selectors that are
2218            // not explicitly excluded.
2219            let selectors: Vec<_> = target
2220                .abi
2221                .functions()
2222                .filter_map(|func| {
2223                    if matches!(
2224                        func.state_mutability,
2225                        alloy_json_abi::StateMutability::Pure
2226                            | alloy_json_abi::StateMutability::View
2227                    ) || func.is_reserved()
2228                        || excluded_test_selectors.contains(&func.selector())
2229                    {
2230                        None
2231                    } else {
2232                        Some(func.selector())
2233                    }
2234                })
2235                .collect();
2236            self.add_address_with_functions(address, &selectors, false, targeted_contracts)?;
2237        }
2238
2239        Ok(())
2240    }
2241
2242    /// Adds the address and fuzzed or excluded functions to `TargetedContracts`.
2243    fn add_address_with_functions(
2244        &self,
2245        address: Address,
2246        selectors: &[Selector],
2247        should_exclude: bool,
2248        targeted_contracts: &mut TargetedContracts,
2249    ) -> eyre::Result<()> {
2250        // Do not add address in target contracts if no function selected.
2251        if selectors.is_empty() {
2252            return Ok(());
2253        }
2254
2255        let contract = match targeted_contracts.entry(address) {
2256            Entry::Occupied(entry) => entry.into_mut(),
2257            Entry::Vacant(entry) => {
2258                let (identifier, abi) = self.setup_contracts.get(&address).ok_or_else(|| {
2259                    eyre::eyre!(
2260                        "[{}] address does not have an associated contract: {}",
2261                        if should_exclude { "excludeSelectors" } else { "targetSelectors" },
2262                        address
2263                    )
2264                })?;
2265                entry.insert(
2266                    TargetedContract::new(identifier.clone(), abi.clone())
2267                        .with_project_contracts(self.project_contracts),
2268                )
2269            }
2270        };
2271        contract.add_selectors(selectors.iter().copied(), should_exclude)?;
2272        Ok(())
2273    }
2274}
2275
2276/// Collects data from call for fuzzing. However, it first verifies that the sender is not an EOA
2277/// before inserting it into the dictionary. Otherwise, we flood the dictionary with
2278/// randomly generated addresses.
2279fn collect_data<FEN: FoundryEvmNetwork>(
2280    invariant_test: &InvariantTest,
2281    state_changeset: &mut AddressMap<Account>,
2282    tx: &BasicTxDetails,
2283    call_result: &RawCallResult<FEN>,
2284    run_depth: u32,
2285    mapping_slots: Option<&AddressMap<foundry_common::mapping_slots::MappingSlots>>,
2286) {
2287    // We keep the nonce changes to apply later.
2288    let sender_changeset = match state_changeset.entry(tx.sender) {
2289        AddressMapEntry::Occupied(entry) => entry
2290            .get()
2291            .info
2292            .code
2293            .as_ref()
2294            .is_none_or(|code| code.is_empty())
2295            .then(|| entry.remove()),
2296        AddressMapEntry::Vacant(_) => None,
2297    };
2298
2299    // Collect values from fuzzed call result and add them to fuzz dictionary.
2300    invariant_test.fuzz_state.collect_values_from_call(
2301        &invariant_test.targeted_contracts,
2302        tx,
2303        &call_result.result,
2304        &call_result.logs,
2305        &*state_changeset,
2306        run_depth,
2307        mapping_slots,
2308    );
2309
2310    // Inject typed sancov trace-cmp operands into the fuzz dictionary.
2311    if let Some(cmp_values) = &call_result.sancov_cmp_values {
2312        invariant_test.fuzz_state.collect_typed_cmp_values(
2313            cmp_values.iter().map(|s| (s.width, alloy_primitives::B256::from(s.value))),
2314        );
2315    }
2316    // Re-add changes
2317    if let Some(changed) = sender_changeset {
2318        state_changeset.insert(tx.sender, changed);
2319    }
2320}
2321
2322/// Calls the `afterInvariant()` function on a contract.
2323/// Returns call result and if call succeeded.
2324/// The state after the call is not persisted.
2325///
2326/// Uses the handler-gate success check so a stale committed `GLOBAL_FAIL_SLOT` from a
2327/// previously-recorded handler bug doesn't false-positive this call (the slot is `1` from
2328/// the prior bug, but `afterInvariant` itself didn't write it in this changeset).
2329pub(crate) fn call_after_invariant_function<FEN: FoundryEvmNetwork>(
2330    executor: &Executor<FEN>,
2331    to: Address,
2332) -> Result<(RawCallResult<FEN>, bool), EvmError<FEN>> {
2333    let calldata = Bytes::from_static(&IInvariantTest::afterInvariantCall::SELECTOR);
2334    let mut call_result = executor.call_raw(CALLER, to, calldata, U256::ZERO)?;
2335    let success = executor.is_raw_call_mut_success_handler_gate(to, &mut call_result);
2336    Ok((call_result, success))
2337}
2338
2339/// Calls the invariant function and returns call result and if succeeded.
2340///
2341/// Uses the handler-gate success check (same rationale as `call_after_invariant_function`):
2342/// the predicate is broken iff this call's own changeset writes `GLOBAL_FAIL_SLOT` (via `t()` /
2343/// `vm.assert*`) or the call reverts; a stale committed slot from a prior handler bug must not
2344/// poison every later predicate evaluation in the run.
2345pub(crate) fn call_invariant_function<FEN: FoundryEvmNetwork>(
2346    executor: &Executor<FEN>,
2347    address: Address,
2348    calldata: Bytes,
2349) -> Result<(RawCallResult<FEN>, bool)> {
2350    let mut call_result = executor.call_raw(CALLER, address, calldata, U256::ZERO)?;
2351    let success = executor.is_raw_call_mut_success_handler_gate(address, &mut call_result);
2352    Ok((call_result, success))
2353}
2354
2355/// Executes an invariant replay fuzz call and returns the result.
2356///
2357/// This applies invariant replay semantics: warp/roll deltas are applied before the call and the
2358/// requested value is clamped to the sender balance. It is intended for invariant sequence replay,
2359/// shrinking, and artifact validation rather than as a general raw-call helper.
2360///
2361/// Applies any block timestamp (warp) and block number (roll) adjustments before the call.
2362pub fn execute_tx<FEN: FoundryEvmNetwork>(
2363    executor: &mut Executor<FEN>,
2364    tx: &BasicTxDetails,
2365) -> Result<RawCallResult<FEN>> {
2366    super::campaign::execute_invariant_tx(executor, &mut tx.clone())
2367}
2368
2369/// Executes an invariant replay call on a validation executor and registers created targets.
2370///
2371/// This uses live campaign acceptance and commit behavior while allowing callers to update
2372/// updatable target sets before validating later calls in the same artifact.
2373pub fn execute_tx_and_register_created<FEN: FoundryEvmNetwork>(
2374    executor: &mut Executor<FEN>,
2375    tx: &BasicTxDetails,
2376    targeted_contracts: &FuzzRunIdentifiedContracts,
2377    dynamic_target_ctx: &DynamicTargetCtx<'_>,
2378    created_contracts: &mut Vec<Address>,
2379) -> Result<()> {
2380    let (kind, call_result) = super::campaign::execute_invariant_replay_tx(executor, tx)?;
2381    if kind == CampaignCallKind::AssumptionRejected {
2382        return Err(eyre!("invariant replay prefix rejected by vm.assume"));
2383    }
2384    targeted_contracts.collect_created_contracts(
2385        &call_result.state_changeset,
2386        dynamic_target_ctx.project_contracts,
2387        dynamic_target_ctx.setup_contracts,
2388        dynamic_target_ctx.artifact_filters,
2389        created_contracts,
2390    )?;
2391    Ok(())
2392}
2393
2394#[cfg(test)]
2395mod tests {
2396
2397    use super::*;
2398    use crate::executors::ExecutorBuilder;
2399    use alloy_evm::EvmEnv;
2400    use foundry_cheatcodes::CheatsConfig;
2401    use foundry_config::FuzzDictionaryConfig;
2402    use foundry_evm_core::{backend::Backend, evm::EthEvmNetwork};
2403    use foundry_evm_fuzz::CallDetails;
2404    use proptest::{
2405        prelude::any,
2406        strategy::{Strategy, ValueTree},
2407        test_runner::Config,
2408    };
2409    use revm::{
2410        bytecode::Bytecode,
2411        context::{Block, TxEnv},
2412        database::InMemoryDB,
2413    };
2414    use serde_json::json;
2415    use std::{sync::mpsc, thread};
2416
2417    fn first_generated_u64(runner: &mut TestRunner) -> u64 {
2418        any::<u64>().new_tree(runner).unwrap().current()
2419    }
2420
2421    fn test_runner() -> TestRunner {
2422        TestRunner::new(Config { failure_persistence: None, ..Default::default() })
2423    }
2424
2425    fn seeded_test_runner(seed: U256) -> TestRunner {
2426        let config = Config { failure_persistence: None, ..Default::default() };
2427        let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
2428        TestRunner::new_with_rng(config, rng)
2429    }
2430
2431    #[test]
2432    fn assumption_rejection_restores_delayed_block_environment() {
2433        let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
2434        let mut executor = ExecutorBuilder::default()
2435            .inspectors(|stack| stack.cheatcodes(Arc::new(CheatsConfig::default())))
2436            .gas_limit(1 << 24)
2437            .build(EvmEnv::default(), TxEnv::default(), backend, Default::default());
2438        let target = Address::repeat_byte(0x11);
2439        let mut code = vec![0x6e]; // PUSH15.
2440        code.extend_from_slice(MAGIC_ASSUME);
2441        code.extend_from_slice(&[0x60, 0x00, 0x52, 0x60, 0x0f, 0x60, 0x11, 0xf3]);
2442        executor.set_code(target, Bytecode::new_raw(Bytes::from(code))).unwrap();
2443
2444        let initial_block = executor.evm_env().block_env.clone();
2445        let initial_cheatcode_block =
2446            executor.inspector().cheatcodes.as_ref().unwrap().block.clone();
2447        let expected_timestamp = initial_block.timestamp() + U256::from(10);
2448        let expected_number = initial_block.number() + U256::from(5);
2449        let tx = BasicTxDetails {
2450            warp: Some(U256::from(10)),
2451            roll: Some(U256::from(5)),
2452            sender: Address::ZERO,
2453            call_details: CallDetails { target, calldata: Bytes::new(), value: None },
2454        };
2455        let mut state = (executor, tx);
2456        let campaign = FuzzCampaign::new(FuzzCampaignMode::Invariant {
2457            check_interval: 1,
2458            optimization: false,
2459        });
2460
2461        let outcome = campaign
2462            .run_sequence(
2463                &mut state,
2464                1,
2465                |state| (&mut state.0, &mut state.1),
2466                |_| 0,
2467                |_| false,
2468                |state, event| {
2469                    match event {
2470                        CampaignEvent::Feedback(_) => {
2471                            let block = &state.0.evm_env().block_env;
2472                            assert_eq!(block.timestamp(), expected_timestamp);
2473                            assert_eq!(block.number(), expected_number);
2474                            let block = state
2475                                .0
2476                                .inspector()
2477                                .cheatcodes
2478                                .as_ref()
2479                                .unwrap()
2480                                .block
2481                                .as_ref()
2482                                .unwrap();
2483                            assert_eq!(block.timestamp(), expected_timestamp);
2484                            assert_eq!(block.number(), expected_number);
2485                        }
2486                        CampaignEvent::Check { kind, .. } => {
2487                            assert_eq!(kind, CampaignCallKind::AssumptionRejected);
2488                            return Ok(CampaignControl::Stop);
2489                        }
2490                        _ => {}
2491                    }
2492                    Ok(CampaignControl::Continue)
2493                },
2494            )
2495            .unwrap();
2496
2497        assert_eq!(outcome, CampaignSequenceOutcome::Stopped);
2498        assert_eq!(state.0.evm_env().block_env, initial_block);
2499        assert_eq!(state.0.inspector().cheatcodes.as_ref().unwrap().block, initial_cheatcode_block);
2500
2501        let (kind, _) =
2502            crate::executors::campaign::execute_invariant_replay_tx(&mut state.0, &state.1)
2503                .unwrap();
2504        assert_eq!(kind, CampaignCallKind::AssumptionRejected);
2505        assert_eq!(state.0.evm_env().block_env, initial_block);
2506        assert_eq!(state.0.inspector().cheatcodes.as_ref().unwrap().block, initial_cheatcode_block);
2507    }
2508
2509    #[test]
2510    fn invariant_worker_seed_preserves_master_seed_and_derives_workers() {
2511        let seed = U256::from(0x1234);
2512
2513        assert_eq!(invariant_worker_seed(seed, 0), seed);
2514        assert_ne!(invariant_worker_seed(seed, 1), seed);
2515        assert_ne!(invariant_worker_seed(seed, 1), invariant_worker_seed(seed, 2));
2516        assert_ne!(invariant_worker_seed(seed, 1), invariant_worker_seed(U256::from(0x5678), 1));
2517    }
2518
2519    #[test]
2520    fn invariant_worker_runner_preserves_seed_for_master_worker() {
2521        let seed = U256::from(0x1234);
2522        let mut seeded_runner = seeded_test_runner(seed);
2523        let mut parent = test_runner();
2524        let mut worker = invariant_worker_runner(&mut parent, 0, Some(seed));
2525
2526        assert_eq!(first_generated_u64(&mut worker), first_generated_u64(&mut seeded_runner));
2527    }
2528
2529    #[test]
2530    fn invariant_worker_runner_uses_seed_independent_of_parent_rng_state() {
2531        let seed = U256::from(0x1234);
2532        let mut parent = test_runner();
2533        let mut advanced_parent = test_runner();
2534        let _ = first_generated_u64(&mut advanced_parent);
2535
2536        let mut worker = invariant_worker_runner(&mut parent, 1, Some(seed));
2537        let mut worker_from_advanced_parent =
2538            invariant_worker_runner(&mut advanced_parent, 1, Some(seed));
2539
2540        assert_eq!(
2541            first_generated_u64(&mut worker),
2542            first_generated_u64(&mut worker_from_advanced_parent)
2543        );
2544    }
2545
2546    #[test]
2547    fn invariant_focus_seed_preserves_configured_seed() {
2548        let configured_seed = U256::from(0x1234);
2549        let mut parent = test_runner();
2550
2551        assert_eq!(
2552            invariant_focus_seed(&mut parent, Some(configured_seed), 2),
2553            Some(configured_seed)
2554        );
2555        assert_eq!(invariant_focus_seed(&mut parent, Some(configured_seed), 1), None);
2556    }
2557
2558    #[test]
2559    fn invariant_focus_seed_uses_parent_rng_when_unconfigured() {
2560        let mut parent = seeded_test_runner(U256::ONE);
2561        let mut matching_parent = seeded_test_runner(U256::ONE);
2562        let mut different_parent = seeded_test_runner(U256::from(2));
2563
2564        let focus_seed = invariant_focus_seed(&mut parent, None, 2).unwrap();
2565
2566        assert_eq!(focus_seed, invariant_focus_seed(&mut matching_parent, None, 2).unwrap());
2567        assert_ne!(focus_seed, invariant_focus_seed(&mut different_parent, None, 2).unwrap());
2568        assert_eq!(invariant_focus_seed(&mut parent, None, 1), None);
2569    }
2570
2571    #[test]
2572    fn invariant_progress_json_includes_throughput_fields() {
2573        let throughput = InvariantThroughputMetrics { total_txs: 2, total_gas: 50 };
2574
2575        let payload = build_invariant_progress_json(
2576            InvariantProgressContext {
2577                timestamp_secs: 123,
2578                contract_name: "InvariantContract",
2579                optimization_best: Some(I256::try_from(42).unwrap()),
2580                throughput,
2581                elapsed: Duration::from_secs(10),
2582                worker_id: 1,
2583                worker_count: 4,
2584                time_since_new_edge: Some(Duration::from_secs(3)),
2585            },
2586            &json!({ "corpus_count": 7 }),
2587            &InvariantFailureMetrics::default(),
2588        );
2589
2590        assert_eq!(payload["timestamp"], json!(123));
2591        assert_eq!(payload["contract"], json!("InvariantContract"));
2592        assert!(payload.get("invariant").is_none());
2593        assert_eq!(payload["metrics"]["corpus_count"], json!(7));
2594        assert_eq!(payload["metrics"]["broken_assertions"], json!(0));
2595        assert!(payload["metrics"].get("broken_handlers").is_none());
2596        assert_eq!(payload["total_txs"], json!(2));
2597        assert_eq!(payload["total_gas"], json!(50));
2598        assert_eq!(payload["tps"], json!(0.2));
2599        assert_eq!(payload["gps"], json!(5.0));
2600        assert!(payload.get("tx_per_sec").is_none());
2601        assert!(payload.get("gas_per_sec").is_none());
2602        assert_eq!(payload["worker"]["id"], json!(1));
2603        assert_eq!(payload["worker"]["count"], json!(4));
2604        assert_eq!(payload["optimization_best"], json!("42"));
2605    }
2606
2607    #[test]
2608    fn invariant_worker_count_keeps_short_campaigns_single_worker() {
2609        assert_eq!(
2610            max_invariant_workers_for_campaign(0, DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP),
2611            1
2612        );
2613        assert_eq!(
2614            max_invariant_workers_for_campaign(
2615                MIN_RUNS_PER_INVARIANT_WORKER - 1,
2616                DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2617            ),
2618            1
2619        );
2620        assert_eq!(
2621            max_invariant_workers_for_campaign(
2622                MIN_RUNS_PER_INVARIANT_WORKER,
2623                DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2624            ),
2625            1
2626        );
2627        assert_eq!(
2628            max_invariant_workers_for_campaign(
2629                MIN_RUNS_PER_INVARIANT_WORKER * 2,
2630                DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2631            ),
2632            2
2633        );
2634        assert_eq!(max_invariant_workers_for_campaign(256, 100_000), 5);
2635    }
2636
2637    #[test]
2638    fn invariant_run_depth_random_min_depth_zero_never_returns_zero() {
2639        let mut runner = test_runner();
2640        let config = InvariantConfig {
2641            depth: 8,
2642            min_depth: 0,
2643            depth_mode: InvariantDepthMode::Random,
2644            ..Default::default()
2645        };
2646
2647        for _ in 0..128 {
2648            assert!((1..=8).contains(&invariant_run_depth(&config, &mut runner)));
2649        }
2650    }
2651
2652    #[test]
2653    fn invariant_run_depth_fixed_zero_preserves_zero() {
2654        let mut runner = test_runner();
2655        let config = InvariantConfig {
2656            depth: 0,
2657            depth_mode: InvariantDepthMode::Fixed,
2658            ..Default::default()
2659        };
2660
2661        assert_eq!(invariant_run_depth(&config, &mut runner), 0);
2662    }
2663
2664    #[test]
2665    fn invariant_worker_config_keeps_single_worker_default() {
2666        let config = InvariantConfig::default();
2667
2668        assert_eq!(
2669            invariant_worker_config(config, 0, 1).corpus.corpus_random_sequence_weight,
2670            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2671        );
2672    }
2673
2674    #[test]
2675    fn invariant_worker_config_uses_one_exploratory_worker_with_default_config() {
2676        let config = InvariantConfig::default();
2677
2678        assert_eq!(
2679            invariant_worker_config(config.clone(), 0, 4).corpus.corpus_random_sequence_weight,
2680            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2681        );
2682        assert_eq!(
2683            invariant_worker_config(config.clone(), 1, 4).corpus.corpus_random_sequence_weight,
2684            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2685        );
2686        assert_eq!(
2687            invariant_worker_config(config.clone(), 2, 4).corpus.corpus_random_sequence_weight,
2688            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2689        );
2690        assert_eq!(
2691            invariant_worker_config(config, 3, 4).corpus.corpus_random_sequence_weight,
2692            FuzzCorpusConfig::ENSEMBLE_CORPUS_RANDOM_SEQUENCE_WEIGHT
2693        );
2694    }
2695
2696    #[test]
2697    fn invariant_worker_config_keeps_two_worker_campaign_on_default() {
2698        let config = InvariantConfig::default();
2699
2700        assert_eq!(
2701            invariant_worker_config(config.clone(), 0, 2).corpus.corpus_random_sequence_weight,
2702            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2703        );
2704        assert_eq!(
2705            invariant_worker_config(config, 1, 2).corpus.corpus_random_sequence_weight,
2706            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2707        );
2708    }
2709
2710    #[test]
2711    fn invariant_worker_config_uses_last_worker_for_three_worker_campaign() {
2712        let config = InvariantConfig::default();
2713
2714        assert_eq!(
2715            invariant_worker_config(config.clone(), 0, 3).corpus.corpus_random_sequence_weight,
2716            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2717        );
2718        assert_eq!(
2719            invariant_worker_config(config.clone(), 1, 3).corpus.corpus_random_sequence_weight,
2720            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2721        );
2722        assert_eq!(
2723            invariant_worker_config(config, 2, 3).corpus.corpus_random_sequence_weight,
2724            FuzzCorpusConfig::ENSEMBLE_CORPUS_RANDOM_SEQUENCE_WEIGHT
2725        );
2726    }
2727
2728    #[test]
2729    fn invariant_worker_config_preserves_explicit_corpus_random_sequence_weight() {
2730        let config = InvariantConfig {
2731            corpus: FuzzCorpusConfig {
2732                corpus_random_sequence_weight: 25,
2733                ..FuzzCorpusConfig::default()
2734            },
2735            corpus_random_sequence_weight_configured: true,
2736            ..InvariantConfig::default()
2737        };
2738
2739        assert_eq!(
2740            invariant_worker_config(config.clone(), 1, 4).corpus.corpus_random_sequence_weight,
2741            25
2742        );
2743        assert_eq!(invariant_worker_config(config, 0, 1).corpus.corpus_random_sequence_weight, 25);
2744
2745        let config = InvariantConfig {
2746            corpus_random_sequence_weight_configured: true,
2747            ..InvariantConfig::default()
2748        };
2749
2750        assert_eq!(
2751            invariant_worker_config(config, 1, 4).corpus.corpus_random_sequence_weight,
2752            FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2753        );
2754    }
2755
2756    #[test]
2757    fn invariant_worker_count_preserves_fixed_workers() {
2758        let mut config = InvariantConfig {
2759            runs: MIN_RUNS_PER_INVARIANT_WORKER * 4,
2760            workers: foundry_config::InvariantWorkers::Fixed(
2761                std::num::NonZeroUsize::new(4).unwrap(),
2762            ),
2763            ..Default::default()
2764        };
2765        assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2766
2767        config.corpus.show_edge_coverage = true;
2768        assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2769
2770        config.corpus.show_edge_coverage = false;
2771        config.corpus.corpus_dir = Some(std::path::PathBuf::from("corpus"));
2772        assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2773
2774        config.runs = MIN_RUNS_PER_INVARIANT_WORKER - 1;
2775        config.timeout = None;
2776        assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2777
2778        config.timeout = Some(1);
2779        assert_eq!(invariant_worker_count_with_threads(&config, 8, 4), 4);
2780    }
2781
2782    #[test]
2783    fn invariant_worker_count_does_not_cap_configured_workers_by_available_threads() {
2784        let config = InvariantConfig {
2785            runs: MIN_RUNS_PER_INVARIANT_WORKER * 8,
2786            workers: foundry_config::InvariantWorkers::Fixed(
2787                std::num::NonZeroUsize::new(8).unwrap(),
2788            ),
2789            ..Default::default()
2790        };
2791
2792        assert_eq!(invariant_worker_count_with_threads(&config, 4, 1), 8);
2793    }
2794
2795    #[test]
2796    fn invariant_worker_count_splits_available_threads_for_auto_workers() {
2797        let mut config = InvariantConfig {
2798            runs: MIN_RUNS_PER_INVARIANT_WORKER * 4,
2799            depth: DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP,
2800            workers: foundry_config::InvariantWorkers::Auto,
2801            ..Default::default()
2802        };
2803
2804        assert_eq!(invariant_worker_count_with_threads(&config, 4, 1), 4);
2805        assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2806        assert_eq!(invariant_worker_count_with_threads(&config, 8, 3), 2);
2807        assert_eq!(invariant_worker_count_with_threads(&config, 3, 8), 1);
2808        assert_eq!(invariant_worker_count_with_threads(&config, 0, 0), 1);
2809
2810        config.runs = MIN_RUNS_PER_INVARIANT_WORKER - 1;
2811        assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 1);
2812
2813        config.depth = 100_000;
2814        assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2815
2816        config.timeout = Some(1);
2817        assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2818    }
2819
2820    fn function(signature: &str) -> Function {
2821        Function::parse(signature).unwrap()
2822    }
2823
2824    fn targeted_contract(identifier: &str, functions: Vec<Function>) -> TargetedContract {
2825        let mut abi = alloy_json_abi::JsonAbi::new();
2826        for function in functions {
2827            abi.functions.entry(function.name.clone()).or_default().push(function);
2828        }
2829        TargetedContract::new(identifier.to_string(), abi)
2830    }
2831
2832    #[test]
2833    fn campaign_terminal_stop_interrupts_handler_without_accepting_run() {
2834        const GAS_LIMIT: u64 = 1 << 24;
2835        let invariant_address = Address::repeat_byte(0x11);
2836        let handler_address = Address::repeat_byte(0x22);
2837        let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
2838        let mut executor = ExecutorBuilder::default().gas_limit(GAS_LIMIT).build(
2839            EvmEnv::default(),
2840            TxEnv::default(),
2841            backend,
2842            Default::default(),
2843        );
2844        // Return ABI-encoded `true` for the invariant predicate.
2845        executor
2846            .set_code(
2847                invariant_address,
2848                Bytecode::new_raw(Bytes::from_static(&[
2849                    0x60, 0x01, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3,
2850                ])),
2851            )
2852            .unwrap();
2853        // JUMPDEST; PUSH1 0; JUMP loops until the campaign stop reaches the inspector.
2854        executor
2855            .set_code(
2856                handler_address,
2857                Bytecode::new_raw(Bytes::from_static(&[0x5b, 0x60, 0x00, 0x56])),
2858            )
2859            .unwrap();
2860
2861        let (handler_entered_tx, handler_entered_rx) = mpsc::channel();
2862        let (handler_release_tx, handler_release_rx) = mpsc::channel();
2863        executor.inspector_mut().set_early_exit_test_gate(
2864            handler_entered_tx,
2865            handler_release_rx,
2866            0,
2867        );
2868
2869        let invariant = function("invariant_ok() view returns (bool)");
2870        let mut invariant_abi = alloy_json_abi::JsonAbi::new();
2871        invariant_abi.functions.entry(invariant.name.clone()).or_default().push(invariant.clone());
2872        let invariant_contract = InvariantContract::new(
2873            invariant_address,
2874            "InvariantTest",
2875            vec![(&invariant, false)],
2876            0,
2877            false,
2878            &invariant_abi,
2879        );
2880
2881        let handler = function("loopForever()");
2882        let mut handler_contract = targeted_contract("Handler", vec![handler.clone()]);
2883        handler_contract.targeted_functions = vec![handler];
2884        let mut targeted_contracts = TargetedContracts::new();
2885        targeted_contracts.insert(handler_address, handler_contract);
2886        let campaign_seed = InvariantCampaignSeed {
2887            artifact_filters: ArtifactFilters::default(),
2888            sender_filters: SenderFilters::new(vec![CALLER], Vec::new()),
2889            targeted_contracts,
2890            targets_are_updatable: false,
2891            initial_handler_failures: Map::default(),
2892        };
2893
2894        let config =
2895            InvariantConfig { runs: 1, depth: 1, show_metrics: false, ..Default::default() };
2896        let campaign_state = InvariantCampaignState::new(EarlyExit::new(false), None);
2897        let fuzz_state =
2898            EvmFuzzState::new(&[], &InMemoryDB::default(), FuzzDictionaryConfig::default(), None);
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::repeat_byte(0x11);
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::repeat_byte(0x55);
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::repeat_byte(0x44);
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::repeat_byte(0x22);
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::repeat_byte(0x33);
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::repeat_byte(0x66);
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}