Skip to main content

foundry_evm/executors/invariant/
mod.rs

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