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