Skip to main content

foundry_evm/executors/invariant/
mod.rs

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