Skip to main content

foundry_evm/executors/invariant/
mod.rs

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