Skip to main content

foundry_evm/executors/invariant/
mod.rs

1use crate::{
2    executors::{
3        DURATION_BETWEEN_METRICS_REPORT, EarlyExit, EvmError, Executor, RawCallResult,
4        corpus::{DynamicTargetCtx, ReplayTarget, WorkerCorpus, WorkerCorpusSeed},
5    },
6    inspectors::Fuzzer,
7};
8use alloy_json_abi::Function;
9use alloy_primitives::{
10    Address, Bytes, FixedBytes, I256, Selector, U256, keccak256, map::AddressMap,
11};
12use alloy_sol_types::{SolCall, sol};
13use eyre::{ContextCompat, Result, eyre};
14use foundry_common::{
15    TestFunctionExt,
16    contracts::{ContractsByAddress, ContractsByArtifact},
17    sh_eprintln, sh_println,
18};
19use foundry_config::{InvariantConfig, InvariantWorkers};
20use foundry_evm_core::{
21    FoundryBlock,
22    constants::{
23        CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS, MAGIC_ASSUME,
24    },
25    evm::FoundryEvmNetwork,
26    precompiles::PRECOMPILES,
27};
28use foundry_evm_fuzz::{
29    BasicTxDetails, FuzzCase, FuzzFixtures,
30    invariant::{
31        ArtifactFilters, FuzzRunIdentifiedContracts, InvariantContract, InvariantSettings,
32        RandomCallGenerator, SenderFilters, TargetedContract, TargetedContracts,
33    },
34    strategies::{EvmFuzzState, InvariantFuzzState, invariant_strat, override_call_strat},
35};
36use foundry_evm_traces::{CallTraceArena, SparsedTraceArena};
37use indicatif::ProgressBar;
38use parking_lot::RwLock;
39use proptest::{
40    strategy::Strategy,
41    test_runner::{RngAlgorithm, TestRng, TestRunner},
42};
43use rayon::iter::{IntoParallelIterator, ParallelIterator};
44use result::{assert_after_invariant, can_continue, did_fail_on_assert, invariant_preflight_check};
45use revm::{context::Block, state::Account};
46use serde::{Deserialize, Serialize};
47use serde_json::json;
48use std::{
49    collections::{HashMap as Map, HashSet, btree_map::Entry},
50    sync::Arc,
51    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
52};
53
54mod error;
55pub use error::{
56    FailureKey, HandlerAssertionFailure, InvariantFailures, InvariantFuzzError,
57    handler_site_already_minimal,
58};
59use foundry_evm_coverage::HitMaps;
60
61mod campaign;
62use campaign::{
63    InvariantCampaignAggregator, InvariantCampaignSpec, InvariantCampaignState,
64    InvariantWorkerOutput, InvariantWorkerPlan,
65};
66
67mod replay;
68pub use replay::{replay_error, replay_run};
69
70mod result;
71pub use result::InvariantFuzzTestResult;
72
73mod shrink;
74pub use shrink::{
75    CheckSequenceOptions, HandlerReplayOutcome, check_sequence, check_sequence_value,
76    replay_handler_failure_sequence,
77};
78
79/// Minimum number of logical runs assigned to each auto invariant worker at the default invariant
80/// depth.
81///
82/// Keeps short campaigns single-threaded and avoids producing many small rayon jobs.
83const MIN_RUNS_PER_INVARIANT_WORKER: u32 = 10_000;
84/// Baseline depth used to preserve the previous default-depth worker heuristic.
85const DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP: u32 = 500;
86/// Minimum estimated handler calls assigned to each auto invariant worker.
87const MIN_ESTIMATED_CALLS_PER_INVARIANT_WORKER: u64 =
88    MIN_RUNS_PER_INVARIANT_WORKER as u64 * DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP as u64;
89
90sol! {
91    interface IInvariantTest {
92        #[derive(Default)]
93        struct FuzzSelector {
94            address addr;
95            bytes4[] selectors;
96        }
97
98        #[derive(Default)]
99        struct FuzzArtifactSelector {
100            string artifact;
101            bytes4[] selectors;
102        }
103
104        #[derive(Default)]
105        struct FuzzInterface {
106            address addr;
107            string[] artifacts;
108        }
109
110        function afterInvariant() external;
111
112        #[derive(Default)]
113        function excludeArtifacts() public view returns (string[] memory excludedArtifacts);
114
115        #[derive(Default)]
116        function excludeContracts() public view returns (address[] memory excludedContracts);
117
118        #[derive(Default)]
119        function excludeSelectors() public view returns (FuzzSelector[] memory excludedSelectors);
120
121        #[derive(Default)]
122        function excludeSenders() public view returns (address[] memory excludedSenders);
123
124        #[derive(Default)]
125        function targetArtifacts() public view returns (string[] memory targetedArtifacts);
126
127        #[derive(Default)]
128        function targetArtifactSelectors() public view returns (FuzzArtifactSelector[] memory targetedArtifactSelectors);
129
130        #[derive(Default)]
131        function targetContracts() public view returns (address[] memory targetedContracts);
132
133        #[derive(Default)]
134        function targetSelectors() public view returns (FuzzSelector[] memory targetedSelectors);
135
136        #[derive(Default)]
137        function targetSenders() public view returns (address[] memory targetedSenders);
138
139        #[derive(Default)]
140        function targetInterfaces() public view returns (FuzzInterface[] memory targetedInterfaces);
141    }
142}
143
144/// Contains invariant metrics for a single fuzzed selector.
145#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
146pub struct InvariantMetrics {
147    // Count of fuzzed selector calls.
148    pub calls: usize,
149    // Count of fuzzed selector reverts.
150    pub reverts: usize,
151    // Count of fuzzed selector discards (through assume cheatcodes).
152    pub discards: usize,
153}
154
155/// Campaign-level throughput metrics for invariant progress reporting.
156#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
157struct InvariantThroughputMetrics {
158    total_txs: u64,
159    total_gas: u64,
160}
161
162impl InvariantThroughputMetrics {
163    fn tx_per_sec(self, elapsed: Duration) -> f64 {
164        rate_per_sec(self.total_txs as f64, elapsed)
165    }
166
167    fn gas_per_sec(self, elapsed: Duration) -> f64 {
168        rate_per_sec(self.total_gas as f64, elapsed)
169    }
170}
171
172fn max_invariant_workers_for_campaign(runs: u32, depth: u32) -> usize {
173    let estimated_calls = u64::from(runs) * u64::from(depth.max(1));
174    usize::try_from((estimated_calls / MIN_ESTIMATED_CALLS_PER_INVARIANT_WORKER).max(1))
175        .unwrap_or(usize::MAX)
176}
177
178fn auto_invariant_worker_count(
179    available_threads: usize,
180    invariant_campaign_anchors: usize,
181) -> usize {
182    (available_threads.max(1) / invariant_campaign_anchors.max(1)).max(1)
183}
184
185fn invariant_worker_count_with_threads(
186    config: &InvariantConfig,
187    available_threads: usize,
188    invariant_campaign_anchors: usize,
189) -> usize {
190    match config.workers {
191        InvariantWorkers::Fixed(workers) => workers.get(),
192        InvariantWorkers::Auto => {
193            let requested =
194                auto_invariant_worker_count(available_threads, invariant_campaign_anchors);
195            if config.timeout.is_some() {
196                requested
197            } else {
198                requested.min(max_invariant_workers_for_campaign(config.runs, config.depth))
199            }
200        }
201    }
202}
203
204fn gas_report_samples_for_worker(total_samples: u32, worker_id: u32, worker_count: usize) -> usize {
205    let total_samples = total_samples as usize;
206    let worker_count = worker_count.max(1);
207    total_samples / worker_count + usize::from((worker_id as usize) < total_samples % worker_count)
208}
209
210fn invariant_worker_seed(seed: U256, worker_id: u32) -> U256 {
211    if worker_id == 0 {
212        seed
213    } else {
214        let seed_data = [&seed.to_be_bytes::<32>()[..], &worker_id.to_be_bytes()[..]].concat();
215        U256::from_be_bytes(keccak256(seed_data).0)
216    }
217}
218
219fn should_continue_invariant_worker(
220    campaign_state: &InvariantCampaignState,
221    runs: u32,
222    plan: InvariantWorkerPlan,
223) -> bool {
224    if campaign_state.should_stop() {
225        return false;
226    }
227
228    campaign_state.is_timed_campaign() || runs < plan.runs
229}
230
231fn invariant_worker_runner(
232    runner: &mut TestRunner,
233    worker_id: u32,
234    seed: Option<U256>,
235) -> TestRunner {
236    if let Some(seed) = seed {
237        let worker_seed = invariant_worker_seed(seed, worker_id);
238        trace!(target: "forge::test", ?worker_seed, "deterministic seed for invariant worker {worker_id}");
239        let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &worker_seed.to_be_bytes::<32>());
240        TestRunner::new_with_rng(runner.config().clone(), rng)
241    } else if worker_id == 0 {
242        runner.clone()
243    } else {
244        TestRunner::new_with_rng(runner.config().clone(), runner.new_rng())
245    }
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249enum InvariantCorpusPersistence {
250    /// Preserve the legacy single-worker behavior: each interesting input is written immediately.
251    Live,
252    /// Parallel workers return interesting inputs to the campaign coordinator for merged writes.
253    Deferred,
254}
255
256impl InvariantCorpusPersistence {
257    const fn is_deferred(self) -> bool {
258        matches!(self, Self::Deferred)
259    }
260}
261
262/// Converts a cumulative campaign total into an average per-second rate.
263///
264/// Returns `0.0` during the initial zero-elapsed startup window to avoid
265/// dividing by zero while progress reporting is warming up.
266fn rate_per_sec(total: f64, elapsed: Duration) -> f64 {
267    let elapsed_secs = elapsed.as_secs_f64();
268    if elapsed_secs > 0.0 { total / elapsed_secs } else { 0.0 }
269}
270
271/// Tracks invariant failure counts during a campaign.
272#[derive(Clone, Debug, Default)]
273struct InvariantFailureMetrics {
274    failures: u64,
275    unique_failures: HashSet<String>,
276    /// Unique handler-side assertion bugs found so far.
277    broken_handlers: usize,
278}
279
280impl InvariantFailureMetrics {
281    /// Records a failure and emits a structured JSON `"failure"` event.
282    fn record_failure(&mut self, invariant_name: &str, target: &str, reason: &str) {
283        self.failures += 1;
284        self.unique_failures.insert(invariant_name.to_string());
285
286        let timestamp =
287            SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
288        let event = json!({
289            "timestamp": timestamp,
290            "event": "failure",
291            "invariant": invariant_name,
292            "target": target,
293            "reason": reason,
294        });
295        let _ = sh_eprintln!("{}", serde_json::to_string(&event).unwrap_or_default());
296    }
297}
298
299/// Bridges newly-recorded invariant breaks from `failures.errors` into the pulse
300/// `failure_metrics` so the live progress stream reflects breaks as they happen.
301/// Iterates in declaration order so the emitted "failure" events are deterministic.
302fn record_new_invariant_failures(
303    campaign_state: &InvariantCampaignState,
304    invariant_contract: &InvariantContract<'_>,
305    failures: &InvariantFailures,
306) {
307    for (f, _) in &invariant_contract.invariant_fns {
308        if let Some(failure) = failures.get_failure(f) {
309            let reason = failure.revert_reason().unwrap_or_default();
310            campaign_state.record_invariant_failure(&f.name, invariant_contract.name, &reason);
311        }
312    }
313}
314
315/// Builds the machine-readable invariant progress payload emitted during a
316/// campaign.
317///
318/// This keeps the existing corpus progress metrics together with cumulative and
319/// derived throughput fields so downstream benchmark tooling can consume a
320/// single JSON event shape.
321fn build_invariant_progress_json<M: Serialize>(
322    timestamp_secs: u64,
323    invariant_name: &str,
324    corpus_metrics: &M,
325    optimization_best: Option<I256>,
326    throughput: InvariantThroughputMetrics,
327    failure_metrics: &InvariantFailureMetrics,
328    elapsed: Duration,
329) -> serde_json::Value {
330    let mut metrics = serde_json::to_value(corpus_metrics).unwrap_or_default();
331    if let Some(obj) = metrics.as_object_mut() {
332        obj.insert("failures".to_string(), json!(failure_metrics.failures));
333        obj.insert("unique_failures".to_string(), json!(failure_metrics.unique_failures.len()));
334        // Surface unique handler-side assertion bugs in live progress, separate from
335        // invariant predicate violations counted by `failures`.
336        obj.insert("broken_handlers".to_string(), json!(failure_metrics.broken_handlers));
337    }
338
339    let mut payload = json!({
340        "timestamp": timestamp_secs,
341        "event": "pulse",
342        "invariant": invariant_name,
343        "metrics": metrics,
344        "total_txs": throughput.total_txs,
345        "total_gas": throughput.total_gas,
346        "tx_per_sec": throughput.tx_per_sec(elapsed),
347        "gas_per_sec": throughput.gas_per_sec(elapsed),
348    });
349
350    if let Some(best) = optimization_best {
351        payload["optimization_best"] = json!(best.to_string());
352    }
353
354    payload
355}
356
357/// Contains data collected during invariant test runs.
358struct InvariantTestData {
359    // Number of completed invariant runs.
360    runs: usize,
361    // Number of completed fuzzed calls across all invariant runs.
362    calls: usize,
363    // Data related to reverts or failed assertions of the test.
364    failures: InvariantFailures,
365    // Calldata in the last invariant run.
366    last_run_inputs: Vec<BasicTxDetails>,
367    // Additional traces for gas report.
368    gas_report_traces: Vec<Vec<CallTraceArena>>,
369    // Line coverage information collected from all fuzzed calls.
370    line_coverage: Option<HitMaps>,
371    // Metrics for each fuzzed selector.
372    metrics: Map<String, InvariantMetrics>,
373
374    // Proptest runner to query for random values.
375    // The strategy only comes with the first `input`. We fill the rest of the `inputs`
376    // until the desired `depth` so we can use the evolving fuzz dictionary
377    // during the run.
378    branch_runner: TestRunner,
379
380    // Optimization mode state: tracks the best (maximum) value and the sequence that produced it.
381    // Only used when invariant function returns int256.
382    optimization_best_value: Option<I256>,
383    optimization_best_sequence: Vec<BasicTxDetails>,
384}
385
386/// Contains invariant test data.
387struct InvariantTest {
388    // Fuzz state of invariant test.
389    fuzz_state: InvariantFuzzState,
390    // Contracts fuzzed by the invariant test.
391    targeted_contracts: FuzzRunIdentifiedContracts,
392    // Data collected during invariant runs.
393    test_data: InvariantTestData,
394}
395
396impl InvariantTest {
397    /// Instantiates an invariant test.
398    fn new(
399        fuzz_state: InvariantFuzzState,
400        targeted_contracts: FuzzRunIdentifiedContracts,
401        failures: InvariantFailures,
402        branch_runner: TestRunner,
403    ) -> Self {
404        let test_data = InvariantTestData {
405            runs: 0,
406            calls: 0,
407            failures,
408            last_run_inputs: vec![],
409            gas_report_traces: vec![],
410            line_coverage: None,
411            metrics: Map::default(),
412            branch_runner,
413            optimization_best_value: None,
414            optimization_best_sequence: vec![],
415        };
416        Self { fuzz_state, targeted_contracts, test_data }
417    }
418
419    /// Returns number of invariant test reverts.
420    const fn reverts(&self) -> usize {
421        self.test_data.failures.reverts
422    }
423
424    /// Set invariant test error.
425    fn set_error(&mut self, invariant: &Function, error: InvariantFuzzError) {
426        self.test_data.failures.record_failure(invariant, error);
427    }
428
429    /// Set last invariant run call sequence.
430    fn set_last_run_inputs(&mut self, inputs: &Vec<BasicTxDetails>) {
431        self.test_data.last_run_inputs.clone_from(inputs);
432    }
433
434    /// Merge current collected line coverage with the new coverage from last fuzzed call.
435    fn merge_line_coverage(&mut self, new_coverage: Option<HitMaps>) {
436        HitMaps::merge_opt(&mut self.test_data.line_coverage, new_coverage);
437    }
438
439    /// Update metrics for a fuzzed selector, extracted from tx details.
440    /// Always increments number of calls; discarded runs (through assume cheatcodes) are tracked
441    /// separated from reverts.
442    fn record_metrics(&mut self, tx_details: &BasicTxDetails, reverted: bool, discarded: bool) {
443        if let Some(metric_key) = self.targeted_contracts.targets().fuzzed_metric_key(tx_details) {
444            let test_metrics = &mut self.test_data.metrics;
445            let invariant_metrics = test_metrics.entry(metric_key).or_default();
446            invariant_metrics.calls += 1;
447            if discarded {
448                invariant_metrics.discards += 1;
449            } else if reverted {
450                invariant_metrics.reverts += 1;
451            }
452        }
453    }
454
455    /// End invariant test run by collecting results, cleaning collected artifacts and reverting
456    /// created fuzz state.
457    fn end_run<FEN: FoundryEvmNetwork>(&mut self, run: InvariantTestRun<FEN>, gas_samples: usize) {
458        // We clear all the targeted contracts created during this run.
459        self.targeted_contracts.clear_created_contracts(run.created_contracts);
460
461        if self.test_data.gas_report_traces.len() < gas_samples {
462            self.test_data
463                .gas_report_traces
464                .push(run.run_traces.into_iter().map(|arena| arena.arena).collect());
465        }
466        self.test_data.runs += 1;
467        self.test_data.calls += run.fuzz_runs.len();
468
469        // Revert state to not persist values between runs.
470        self.fuzz_state.revert();
471    }
472
473    /// Updates the optimization state if the new value is better (higher) than the current best.
474    fn update_optimization_value(&mut self, value: I256, sequence: &[BasicTxDetails]) {
475        if self.test_data.optimization_best_value.is_none_or(|best| value > best) {
476            self.test_data.optimization_best_value = Some(value);
477            self.test_data.optimization_best_sequence = sequence.to_vec();
478        }
479    }
480}
481
482/// Contains data for an invariant test run.
483struct InvariantTestRun<FEN: FoundryEvmNetwork> {
484    // Invariant run call sequence.
485    inputs: Vec<BasicTxDetails>,
486    // Per-call EVM comparison operands (parallel to `inputs`), captured for I2S corpus mutation.
487    cmp_seq: Vec<Vec<crate::inspectors::CmpOperands>>,
488    // Current invariant run executor.
489    executor: Executor<FEN>,
490    // Invariant run stat reports (eg. gas usage).
491    fuzz_runs: Vec<FuzzCase>,
492    // Contracts created during current invariant run.
493    created_contracts: Vec<Address>,
494    // Traces of each call of the invariant run call sequence.
495    run_traces: Vec<SparsedTraceArena>,
496    // Current depth of invariant run.
497    depth: u32,
498    // Current assume rejects of the invariant run.
499    rejects: u32,
500    // Whether new coverage was discovered during this run.
501    new_coverage: bool,
502    // For optimization mode: the best value found during this run (if any).
503    optimization_value: Option<I256>,
504    // For optimization mode: the length of the input prefix that produced the best value.
505    optimization_prefix_len: usize,
506}
507
508/// Immutable state selected once for a logical invariant campaign and cloned into each worker.
509#[derive(Clone)]
510struct InvariantCampaignSeed {
511    artifact_filters: ArtifactFilters,
512    sender_filters: SenderFilters,
513    targeted_contracts: TargetedContracts,
514    targets_are_updatable: bool,
515    initial_handler_failures: Map<(Address, Selector), InvariantFuzzError>,
516}
517
518impl<FEN: FoundryEvmNetwork> InvariantTestRun<FEN> {
519    /// Instantiates an invariant test run.
520    fn new(first_input: BasicTxDetails, executor: Executor<FEN>, depth: usize) -> Self {
521        Self {
522            inputs: vec![first_input],
523            cmp_seq: Vec::with_capacity(depth),
524            executor,
525            fuzz_runs: Vec::with_capacity(depth),
526            created_contracts: vec![],
527            run_traces: vec![],
528            depth: 0,
529            rejects: 0,
530            new_coverage: false,
531            optimization_value: None,
532            optimization_prefix_len: 0,
533        }
534    }
535
536    /// Releases per-run corpus payloads once the worker corpus manager has consumed them.
537    ///
538    /// Successful runs only need `fuzz_runs`, traces, and created-contract bookkeeping for final
539    /// reporting. Counterexample inputs are copied into `InvariantTestData::last_run_inputs`
540    /// before this point, so retaining the full per-run input/cmp buffers until `end_run` only
541    /// extends peak memory in long invariant campaigns.
542    fn drop_corpus_payloads(&mut self) {
543        self.inputs.clear();
544        self.inputs.shrink_to_fit();
545        self.cmp_seq.clear();
546        self.cmp_seq.shrink_to_fit();
547    }
548}
549
550/// Wrapper around any [`Executor`] implementer which provides fuzzing support using [`proptest`].
551///
552/// After instantiation, calling `invariant_fuzz` will proceed to hammer the deployed smart
553/// contracts with inputs, until it finds a counterexample sequence. The provided [`TestRunner`]
554/// contains all the configuration which can be overridden via [environment
555/// variables](proptest::test_runner::Config)
556pub struct InvariantExecutor<'a, FEN: FoundryEvmNetwork> {
557    pub executor: Executor<FEN>,
558    /// Proptest runner.
559    runner: TestRunner,
560    /// Configured fuzz seed used to derive deterministic invariant worker runners.
561    fuzz_seed: Option<U256>,
562    /// The invariant configuration
563    config: InvariantConfig,
564    /// Contracts deployed with `setUp()`
565    setup_contracts: &'a ContractsByAddress,
566    /// Contracts that are part of the project but have not been deployed yet. We need the bytecode
567    /// to identify them from the stateset changes.
568    project_contracts: &'a ContractsByArtifact,
569    /// Filters contracts to be fuzzed through their artifact identifiers.
570    artifact_filters: ArtifactFilters,
571    /// Number of matching invariant campaign anchors in the current test pass.
572    invariant_campaign_anchors: usize,
573}
574
575impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
576    /// Instantiates a fuzzed executor EVM given a testrunner
577    pub fn new(
578        executor: Executor<FEN>,
579        runner: TestRunner,
580        config: InvariantConfig,
581        setup_contracts: &'a ContractsByAddress,
582        project_contracts: &'a ContractsByArtifact,
583    ) -> Self {
584        Self::new_with_fuzz_seed(
585            executor,
586            runner,
587            None,
588            config,
589            setup_contracts,
590            project_contracts,
591            1,
592        )
593    }
594
595    /// Instantiates an invariant executor with the configured fuzz seed for deterministic worker
596    /// runner derivation.
597    pub fn new_with_fuzz_seed(
598        executor: Executor<FEN>,
599        runner: TestRunner,
600        fuzz_seed: Option<U256>,
601        config: InvariantConfig,
602        setup_contracts: &'a ContractsByAddress,
603        project_contracts: &'a ContractsByArtifact,
604        invariant_campaign_anchors: usize,
605    ) -> Self {
606        Self {
607            executor,
608            runner,
609            fuzz_seed,
610            config,
611            setup_contracts,
612            project_contracts,
613            artifact_filters: ArtifactFilters::default(),
614            invariant_campaign_anchors,
615        }
616    }
617
618    pub fn config(&self) -> InvariantConfig {
619        self.config.clone()
620    }
621
622    /// Refs for tracking contracts deployed mid-sequence during corpus replay.
623    pub const fn dynamic_target_ctx(&self) -> DynamicTargetCtx<'_> {
624        DynamicTargetCtx {
625            project_contracts: self.project_contracts,
626            setup_contracts: self.setup_contracts,
627            artifact_filters: &self.artifact_filters,
628        }
629    }
630
631    /// Fuzzes any deployed contract and checks any broken invariant at `invariant_address`.
632    ///
633    /// `initial_handler_failures` pre-seeds the campaign's `broken_handlers` map with bugs
634    /// recovered from disk by the runner's persisted-failure replay step, so the live
635    /// progress bar and JSON pulse stream surface them from the first emission instead of
636    /// jumping at the final report.
637    pub fn invariant_fuzz(
638        &mut self,
639        invariant_contract: InvariantContract<'_>,
640        fuzz_fixtures: &FuzzFixtures,
641        fuzz_state: EvmFuzzState,
642        progress: Option<&ProgressBar>,
643        early_exit: &EarlyExit,
644        initial_handler_failures: std::collections::HashMap<
645            (Address, Selector),
646            InvariantFuzzError,
647        >,
648    ) -> Result<InvariantFuzzTestResult> {
649        let campaign_spec = InvariantCampaignSpec::new(self.config.runs);
650        let worker_plans = campaign_spec.worker_plans(invariant_worker_count_with_threads(
651            &self.config,
652            rayon::current_num_threads(),
653            self.invariant_campaign_anchors,
654        ))?;
655        let actual_worker_count = worker_plans.len();
656        let campaign_seed =
657            self.prepare_campaign_seed(&invariant_contract, initial_handler_failures)?;
658        let replay_targets = FuzzRunIdentifiedContracts::new(
659            campaign_seed.targeted_contracts.clone(),
660            campaign_seed.targets_are_updatable,
661        );
662        let corpus_seed = WorkerCorpusSeed::load_from_disk(
663            &self.config.corpus,
664            Some(&self.executor),
665            None,
666            Some(&replay_targets),
667            Some(self.dynamic_target_ctx()),
668        )?;
669        let corpus_persistence = if actual_worker_count > 1 {
670            InvariantCorpusPersistence::Deferred
671        } else {
672            InvariantCorpusPersistence::Live
673        };
674        let mut runner = self.runner.clone();
675        let config = self.config.clone();
676        let setup_contracts = self.setup_contracts;
677        let project_contracts = self.project_contracts;
678        let base_executor = self.executor.clone();
679        let campaign_state =
680            Arc::new(InvariantCampaignState::new(early_exit.clone(), self.config.timeout));
681
682        let worker_outputs = if corpus_persistence.is_deferred() {
683            let worker_jobs = worker_plans
684                .into_iter()
685                .map(|worker_plan| {
686                    let worker_runner =
687                        invariant_worker_runner(&mut runner, worker_plan.worker_id, self.fuzz_seed);
688                    let gas_report_samples = gas_report_samples_for_worker(
689                        config.gas_report_samples,
690                        worker_plan.worker_id,
691                        actual_worker_count,
692                    );
693                    (worker_plan, worker_runner, gas_report_samples)
694                })
695                .collect::<Vec<_>>();
696            worker_jobs
697                .into_par_iter()
698                .map(|(worker_plan, worker_runner, gas_report_samples)| {
699                    let _guard =
700                        info_span!("invariant_worker", id = worker_plan.worker_id).entered();
701                    let timer = Instant::now();
702                    let output = Self::run_invariant_worker(
703                        base_executor.clone(),
704                        worker_runner,
705                        config.clone(),
706                        setup_contracts,
707                        project_contracts,
708                        worker_plan,
709                        invariant_contract.clone(),
710                        fuzz_fixtures,
711                        fuzz_state.fork(),
712                        progress,
713                        &campaign_state,
714                        campaign_seed.clone(),
715                        corpus_seed
716                            .clone_for_worker(worker_plan.worker_id as usize, actual_worker_count),
717                        corpus_persistence,
718                        gas_report_samples,
719                    );
720                    debug!("finished in {:?}", timer.elapsed());
721                    output
722                })
723                .collect::<Result<Vec<_>>>()?
724        } else {
725            let worker_plan = worker_plans[0];
726            let runner =
727                invariant_worker_runner(&mut runner, worker_plan.worker_id, self.fuzz_seed);
728            let gas_report_samples = config.gas_report_samples as usize;
729            vec![Self::run_invariant_worker(
730                base_executor,
731                runner,
732                config,
733                setup_contracts,
734                project_contracts,
735                worker_plan,
736                invariant_contract,
737                fuzz_fixtures,
738                fuzz_state,
739                progress,
740                &campaign_state,
741                campaign_seed,
742                corpus_seed.clone(),
743                corpus_persistence,
744                gas_report_samples,
745            )?]
746        };
747
748        let mut aggregator = InvariantCampaignAggregator::new(campaign_spec);
749        for worker_output in worker_outputs {
750            aggregator.push(worker_output);
751        }
752        let (result, corpus_entries) = if campaign_state.is_timed_campaign() {
753            aggregator.finish_partial_with_corpus_entries()?
754        } else {
755            aggregator.finish_with_corpus_entries()?
756        };
757        if corpus_persistence.is_deferred() {
758            let dynamic_target_ctx = self.dynamic_target_ctx();
759            corpus_seed.persist_filtered_campaign_outputs(
760                &self.config.corpus,
761                corpus_entries,
762                &self.executor,
763                ReplayTarget {
764                    fuzzed_function: None,
765                    fuzzed_contracts: Some(&replay_targets),
766                    dynamic: Some(&dynamic_target_ctx),
767                },
768                result
769                    .optimization_best_value
770                    .map(|value| (value, result.optimization_best_sequence.as_slice())),
771            )?;
772        }
773        Ok(result)
774    }
775
776    /// Runs one worker-local slice of an invariant campaign.
777    #[allow(clippy::too_many_arguments)]
778    fn run_invariant_worker(
779        mut executor: Executor<FEN>,
780        runner: TestRunner,
781        config: InvariantConfig,
782        setup_contracts: &'a ContractsByAddress,
783        project_contracts: &'a ContractsByArtifact,
784        plan: InvariantWorkerPlan,
785        invariant_contract: InvariantContract<'_>,
786        fuzz_fixtures: &FuzzFixtures,
787        fuzz_state: EvmFuzzState,
788        progress: Option<&ProgressBar>,
789        campaign_state: &InvariantCampaignState,
790        campaign_seed: InvariantCampaignSeed,
791        corpus_seed: WorkerCorpusSeed,
792        corpus_persistence: InvariantCorpusPersistence,
793        gas_report_samples: usize,
794    ) -> Result<InvariantWorkerOutput> {
795        // Note: invariant function signatures (no inputs) are validated upstream in the
796        // suite runner so parameterized `invariant_*` functions are rejected with a per-test
797        // failure entry before any campaign runs.
798
799        let (mut invariant_test, mut corpus_manager) = Self::prepare_worker(
800            &mut executor,
801            plan,
802            &invariant_contract,
803            fuzz_fixtures,
804            fuzz_state,
805            &runner,
806            &config,
807            &campaign_seed,
808            corpus_seed,
809        )?;
810        let mut corpus_entries = Vec::new();
811
812        let mut runs = 0;
813        campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
814
815        // Invariant runs with edge coverage if corpus dir is set or showing edge coverage.
816        let edge_coverage_enabled = config.corpus.collect_edge_coverage();
817
818        'stop: while should_continue_invariant_worker(campaign_state, runs, plan) {
819            // Per-run failure count snapshot used to gate `afterInvariant` below.
820            let failures_before_run = invariant_test.test_data.failures.invariant_count();
821            let mut stop_after_run = false;
822
823            let initial_seq = corpus_manager.new_inputs(
824                &mut invariant_test.test_data.branch_runner,
825                &invariant_test.fuzz_state,
826                &invariant_test.targeted_contracts,
827            )?;
828
829            // Create current invariant run data.
830            let mut current_run = InvariantTestRun::new(
831                initial_seq[0].clone(),
832                // Before each run, we must reset the backend state.
833                executor.clone(),
834                config.depth as usize,
835            );
836
837            // We stop the run immediately if we have reverted, and `fail_on_revert` is set.
838            if config.fail_on_revert && invariant_test.reverts() > 0 {
839                campaign_state.request_terminal_stop();
840                return Err(eyre!("call reverted"));
841            }
842
843            while current_run.depth < config.depth {
844                // Check if the timeout has been reached.
845                if campaign_state.should_stop() {
846                    // Since we never record a revert here the test is still considered
847                    // successful even though it timed out. We *want*
848                    // this behavior for now, so that's ok, but
849                    // future developers should be aware of this.
850                    break 'stop;
851                }
852
853                // Snapshot `(target, selector)` so `can_continue` can borrow `&mut current_run`
854                // later without cloning the full `BasicTxDetails`.
855                let (handler_target, handler_selector) = {
856                    let last = current_run
857                        .inputs
858                        .last()
859                        .ok_or_else(|| eyre!("no input generated to call fuzzed target."))?;
860                    let sel_bytes: [u8; 4] = last
861                        .call_details
862                        .calldata
863                        .get(..4)
864                        .and_then(|s| s.try_into().ok())
865                        .unwrap_or_default();
866                    (last.call_details.target, Selector::from(sel_bytes))
867                };
868
869                // Execute call from the randomly generated sequence without committing state.
870                // State is committed only if call is not a magic assume.
871                let mut call_result = execute_tx(
872                    &mut current_run.executor,
873                    current_run.inputs.last().expect("checked above"),
874                )?;
875                if let Some(fuzzer) = current_run.executor.inspector_mut().fuzzer.as_mut() {
876                    invariant_test.fuzz_state.collect_values(fuzzer.drain_collected_values());
877                }
878                // Capture per-call EVM cmp operands for I2S corpus mutation. Kept parallel
879                // to `current_run.inputs`; populated unconditionally so dropped calls (magic
880                // assumes / pops below) get zero-length entries that the corpus side filters out.
881                let call_cmp_values = call_result.evm_cmp_values.take().unwrap_or_default();
882                let discarded = call_result.result.as_ref() == MAGIC_ASSUME;
883                if config.show_metrics {
884                    invariant_test.record_metrics(
885                        current_run.inputs.last().expect("checked above"),
886                        call_result.reverted,
887                        discarded,
888                    );
889                }
890
891                // Collect line coverage from last fuzzed call.
892                invariant_test.merge_line_coverage(call_result.line_coverage.clone());
893                // Snapshot the edge fingerprint before `merge_edge_coverage` zeroes the
894                // buffer. Gate on `assertion_failure` to skip keccak on plain reverts.
895                let assertion_failure =
896                    !discarded && did_fail_on_assert(&call_result, &call_result.state_changeset);
897                let pre_merge_edges_hash = if assertion_failure {
898                    error::snapshot_edge_fingerprint(&call_result)
899                } else {
900                    None
901                };
902                // Collect edge coverage and set the flag in the current run.
903                if corpus_manager.merge_edge_coverage(&mut call_result) {
904                    current_run.new_coverage = true;
905                }
906
907                if discarded {
908                    current_run.inputs.pop();
909                    current_run.rejects += 1;
910                    if current_run.rejects > config.max_assume_rejects {
911                        invariant_test.set_error(
912                            invariant_contract.anchor(),
913                            InvariantFuzzError::MaxAssumeRejects(config.max_assume_rejects),
914                        );
915                        campaign_state.request_terminal_stop();
916                        break 'stop;
917                    }
918                } else {
919                    // Commit executed call result.
920                    current_run.executor.commit(&mut call_result);
921
922                    // Collect data for fuzzing from the state changeset.
923                    // This step updates the state dictionary and therefore invalidates the
924                    // ValueTree in use by the current run. This manifestsitself in proptest
925                    // observing a different input case than what it was called with, and creates
926                    // inconsistencies whenever proptest tries to use the input case after test
927                    // execution.
928                    // See <https://github.com/foundry-rs/foundry/issues/9764>.
929                    let mut state_changeset = std::mem::take(&mut call_result.state_changeset);
930                    if !call_result.reverted {
931                        let mapping_slots = current_run
932                            .executor
933                            .inspector()
934                            .fuzzer
935                            .as_ref()
936                            .and_then(|fuzzer| fuzzer.mapping_slots.as_ref());
937                        collect_data(
938                            &invariant_test,
939                            &mut state_changeset,
940                            current_run.inputs.last().expect("checked above"),
941                            &call_result,
942                            config.depth,
943                            mapping_slots,
944                        );
945                    }
946
947                    // Collect created contracts and add to fuzz targets only if targeted contracts
948                    // are updatable.
949                    if let Err(error) =
950                        &invariant_test.targeted_contracts.collect_created_contracts(
951                            &state_changeset,
952                            project_contracts,
953                            setup_contracts,
954                            &campaign_seed.artifact_filters,
955                            &mut current_run.created_contracts,
956                        )
957                    {
958                        warn!(target: "forge::test", "{error}");
959                    }
960                    current_run
961                        .fuzz_runs
962                        .push(FuzzCase { gas: call_result.gas_used, stipend: call_result.stipend });
963                    campaign_state.record_call(call_result.gas_used);
964
965                    // Determine if test can continue or should exit.
966                    // Check invariants based on check_interval to improve deep run performance.
967                    // - check_interval=0: only assert on the last call
968                    // - check_interval=1 (default): assert after every call
969                    // - check_interval=N: assert every N calls AND always on the last call
970                    let is_last_call = current_run.depth == config.depth - 1;
971                    // In optimization mode, always evaluate the invariant to track
972                    // the best value at every prefix — check_interval only gates
973                    // boolean invariant assertions.
974                    let is_optimization = invariant_contract.is_optimization();
975                    let should_check_invariant = is_optimization
976                        || if config.check_interval == 0 {
977                            is_last_call
978                        } else {
979                            config.check_interval == 1
980                                || (current_run.depth + 1).is_multiple_of(config.check_interval)
981                                || is_last_call
982                        };
983
984                    let errors_before_check = invariant_test.test_data.failures.invariant_count();
985                    let (continues, broken) = if should_check_invariant {
986                        let outcome = can_continue(
987                            &invariant_contract,
988                            &mut invariant_test,
989                            &mut current_run,
990                            &config,
991                            call_result,
992                            &state_changeset,
993                            handler_target,
994                            handler_selector,
995                            pre_merge_edges_hash,
996                        )
997                        .map_err(|e| eyre!(e.to_string()))?;
998                        (outcome.continues, outcome.broken)
999                    } else {
1000                        // Skip invariant check but still track reverts
1001                        if call_result.reverted {
1002                            invariant_test.test_data.failures.reverts += 1;
1003                        }
1004                        if assertion_failure {
1005                            // Handler-side assertion: deduped by `(reverter, selector)` site;
1006                            // campaign keeps running to surface more bugs.
1007                            let call_reverted = call_result.reverted;
1008                            error::record_handler_assertion_bug(
1009                                &invariant_contract,
1010                                &config,
1011                                &invariant_test.targeted_contracts,
1012                                &mut invariant_test.test_data.failures,
1013                                &mut current_run.inputs,
1014                                handler_target,
1015                                handler_selector,
1016                                pre_merge_edges_hash,
1017                                call_result,
1018                                call_reverted,
1019                                invariant_contract.is_optimization(),
1020                            );
1021                            (true, None)
1022                        } else if call_result.reverted && config.fail_on_revert {
1023                            // Plain revert under fail_on_revert: attribute to the anchor.
1024                            let anchor = invariant_contract.anchor();
1025                            let case_data = error::InvariantRunCtx {
1026                                contract: &invariant_contract,
1027                                config: &config,
1028                                targeted_contracts: &invariant_test.targeted_contracts,
1029                                calldata: &current_run.inputs,
1030                            }
1031                            .failed_case(
1032                                anchor,
1033                                config.fail_on_revert,
1034                                false,
1035                                call_result,
1036                                &[],
1037                            );
1038                            invariant_test
1039                                .test_data
1040                                .failures
1041                                .record_failure(anchor, InvariantFuzzError::Revert(case_data));
1042                            (false, Some(anchor))
1043                        } else if call_result.reverted
1044                            && !invariant_contract.is_optimization()
1045                            && !config.has_delay()
1046                        {
1047                            // Delay campaigns keep reverted calls so warp/roll survives shrinking.
1048                            current_run.inputs.pop();
1049                            (true, None)
1050                        } else {
1051                            (true, None)
1052                        }
1053                    };
1054
1055                    // Keep `cmp_seq` parallel to `inputs`: only push when the input survived the
1056                    // pop branch above.
1057                    if current_run.cmp_seq.len() < current_run.inputs.len() {
1058                        current_run.cmp_seq.push(call_cmp_values);
1059                    }
1060
1061                    if !continues || current_run.depth == config.depth - 1 {
1062                        invariant_test.set_last_run_inputs(&current_run.inputs);
1063                    }
1064                    // Bridge newly-recorded predicate breaks into `failure_metrics` even when
1065                    // `continues == true` in multi-predicate campaigns.
1066                    if invariant_test.test_data.failures.invariant_count() > errors_before_check
1067                        || broken.is_some()
1068                    {
1069                        record_new_invariant_failures(
1070                            campaign_state,
1071                            &invariant_contract,
1072                            &invariant_test.test_data.failures,
1073                        );
1074                    }
1075                    if !continues {
1076                        if invariant_contract.invariant_fns.len() > 1 && !config.fail_on_revert {
1077                            break;
1078                        }
1079                        campaign_state.request_terminal_stop();
1080                        stop_after_run = true;
1081                        break;
1082                    }
1083                    current_run.depth += 1;
1084                }
1085
1086                current_run.inputs.push(corpus_manager.generate_next_input(
1087                    &mut invariant_test.test_data.branch_runner,
1088                    &initial_seq,
1089                    discarded,
1090                    current_run.depth as usize,
1091                )?);
1092            }
1093
1094            // Extend corpus with current run data.
1095            // Materialize the optimization best prefix once at run end (avoids
1096            // cloning inputs on every new in-run max).
1097            let optimization = current_run.optimization_value.map(|v| {
1098                let prefix = current_run.inputs[..current_run.optimization_prefix_len].to_vec();
1099                (v, prefix)
1100            });
1101            if corpus_persistence.is_deferred() {
1102                if let Some(input) = corpus_manager.process_inputs_for_campaign(
1103                    &current_run.inputs,
1104                    &current_run.cmp_seq,
1105                    current_run.new_coverage,
1106                    optimization,
1107                ) {
1108                    corpus_entries.push(input);
1109                }
1110            } else {
1111                corpus_manager.process_inputs(
1112                    &current_run.inputs,
1113                    &current_run.cmp_seq,
1114                    current_run.new_coverage,
1115                    optimization,
1116                );
1117            }
1118
1119            // Call `afterInvariant` only if declared and the current run produced no new
1120            // failure. Multi-predicate campaigns keep running after earlier failures, but the
1121            // hook must still execute on subsequent runs.
1122            if invariant_contract.call_after_invariant
1123                && invariant_test.test_data.failures.invariant_count() == failures_before_run
1124            {
1125                let broken = assert_after_invariant(
1126                    &invariant_contract,
1127                    &mut invariant_test,
1128                    &current_run,
1129                    &config,
1130                )
1131                .map_err(|_| eyre!("Failed to call afterInvariant"))?;
1132                if broken.is_some() {
1133                    // Bridge breaks into pulse metrics, mirroring the in-run path above.
1134                    record_new_invariant_failures(
1135                        campaign_state,
1136                        &invariant_contract,
1137                        &invariant_test.test_data.failures,
1138                    );
1139                }
1140            }
1141
1142            // End current invariant test run.
1143            current_run.drop_corpus_payloads();
1144            invariant_test.end_run(current_run, gas_report_samples);
1145            runs += 1;
1146            let total_runs = campaign_state.increment_runs();
1147            debug_assert!(
1148                campaign_state.is_timed_campaign() || total_runs <= config.runs,
1149                "worker runs were not distributed correctly"
1150            );
1151            if let Some(progress) = progress {
1152                progress.inc(1);
1153                campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1154                // Display current best value, corpus metrics, and failure counts.
1155                let best = invariant_test.test_data.optimization_best_value;
1156                let failure_metrics = campaign_state.failure_metrics();
1157                let broken = failure_metrics.unique_failures.len();
1158                let handler_bugs = failure_metrics.broken_handlers;
1159                let total_invariants = invariant_contract.invariant_fns.len();
1160                if edge_coverage_enabled || best.is_some() || broken > 0 || handler_bugs > 0 {
1161                    let mut msg = String::new();
1162                    if let Some(best) = best {
1163                        msg.push_str(&format!("best: {best}"));
1164                    }
1165                    if edge_coverage_enabled {
1166                        if !msg.is_empty() {
1167                            msg.push_str(", ");
1168                        }
1169                        msg.push_str(&format!("{}", corpus_manager.metrics));
1170                    }
1171                    if broken > 0 {
1172                        if !msg.is_empty() {
1173                            msg.push_str(", ");
1174                        }
1175                        msg.push_str(&format!("❌ {broken}/{total_invariants} broken"));
1176                    }
1177                    if handler_bugs > 0 {
1178                        if !msg.is_empty() {
1179                            msg.push_str(", ");
1180                        }
1181                        msg.push_str(&format!("⚠ {handler_bugs} handler bug(s)"));
1182                    }
1183                    let msg = if corpus_persistence.is_deferred() {
1184                        format!("[w{}] {msg}", plan.worker_id)
1185                    } else {
1186                        msg
1187                    };
1188                    progress.set_message(msg);
1189                }
1190            } else if edge_coverage_enabled
1191                && campaign_state.should_emit_metrics_report(DURATION_BETWEEN_METRICS_REPORT)
1192            {
1193                campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1194                let failure_metrics = campaign_state.failure_metrics();
1195                let (total_txs, total_gas) = campaign_state.throughput_totals();
1196                let throughput = InvariantThroughputMetrics { total_txs, total_gas };
1197                // Display corpus metrics inline as JSON.
1198                let metrics = build_invariant_progress_json(
1199                    SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
1200                    &invariant_contract.anchor().name,
1201                    &corpus_manager.metrics,
1202                    invariant_test.test_data.optimization_best_value,
1203                    throughput,
1204                    &failure_metrics,
1205                    campaign_state.elapsed(),
1206                );
1207                let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
1208            }
1209
1210            if stop_after_run {
1211                break 'stop;
1212            }
1213        }
1214
1215        trace!(?fuzz_fixtures);
1216        invariant_test.fuzz_state.log_stats();
1217
1218        Self::shrink_handler_failures(
1219            &config,
1220            &executor,
1221            &mut invariant_test.test_data,
1222            progress,
1223            campaign_state.early_exit(),
1224        );
1225
1226        // Move out the final test data and drop worker-local fuzz state before returning this
1227        // worker's aggregate output. Long invariant campaigns can leave large dictionaries and
1228        // target state behind; once shrinking is complete, only `test_data` is needed.
1229        let InvariantTest { fuzz_state: _, targeted_contracts: _, test_data: result } =
1230            invariant_test;
1231        let reverts = result.failures.reverts;
1232        let (errors, handler_errors) = result.failures.partition();
1233        let worker_result = InvariantFuzzTestResult::new(
1234            errors,
1235            handler_errors,
1236            result.runs,
1237            result.calls,
1238            reverts,
1239            result.last_run_inputs,
1240            result.gas_report_traces,
1241            result.line_coverage,
1242            result.metrics,
1243            if plan.worker_id == 0 { corpus_manager.failed_replays } else { 0 },
1244            1,
1245            result.optimization_best_value,
1246            result.optimization_best_sequence,
1247        );
1248        drop(corpus_manager);
1249        let reported_plan = if campaign_state.is_timed_campaign() {
1250            InvariantWorkerPlan { runs, ..plan }
1251        } else {
1252            // Sharded campaigns must report the original assigned range. Early worker exit changes
1253            // the number of executed runs, but it must not shrink `plan.runs`: following workers'
1254            // `first_global_run` offsets were computed from the original partition.
1255            plan
1256        };
1257        Ok(InvariantWorkerOutput { plan: reported_plan, result: worker_result, corpus_entries })
1258    }
1259
1260    fn shrink_handler_failures(
1261        config: &InvariantConfig,
1262        executor: &Executor<FEN>,
1263        result: &mut InvariantTestData,
1264        progress: Option<&ProgressBar>,
1265        early_exit: &EarlyExit,
1266    ) {
1267        let total = result.failures.handler_count();
1268        if total == 0 {
1269            return;
1270        }
1271
1272        for (idx, error) in result.failures.handler_failures_mut().enumerate() {
1273            if early_exit.should_stop() {
1274                break;
1275            }
1276            let Some(failure) = error.as_handler_assertion_mut() else {
1277                continue;
1278            };
1279            shrink::reset_shrink_progress(
1280                config,
1281                progress,
1282                &format!("handler {:#x}::{}", failure.reverter, failure.selector),
1283                Some((idx + 1, total)),
1284            );
1285            match shrink::shrink_handler_sequence(
1286                config,
1287                &failure.call_sequence,
1288                failure.edge_fingerprint,
1289                executor,
1290                progress,
1291                early_exit,
1292            ) {
1293                Ok(shrunk) if !shrunk.is_empty() => {
1294                    failure.call_sequence = shrunk;
1295                }
1296                Ok(_) => {}
1297                Err(e) => trace!(target: "forge::test", "handler shrink failed: {e}"),
1298            }
1299        }
1300    }
1301
1302    fn prepare_campaign_seed(
1303        &mut self,
1304        invariant_contract: &InvariantContract<'_>,
1305        initial_handler_failures: std::collections::HashMap<
1306            (Address, Selector),
1307            InvariantFuzzError,
1308        >,
1309    ) -> Result<InvariantCampaignSeed> {
1310        self.select_contract_artifacts(invariant_contract.address)?;
1311        let (sender_filters, targeted_contracts) =
1312            self.select_contracts_and_senders(invariant_contract.address)?;
1313        let targets_are_updatable = targeted_contracts.is_updatable;
1314        let targeted_contracts = targeted_contracts.targets().clone();
1315
1316        Ok(InvariantCampaignSeed {
1317            artifact_filters: self.artifact_filters.clone(),
1318            sender_filters,
1319            targeted_contracts,
1320            targets_are_updatable,
1321            initial_handler_failures,
1322        })
1323    }
1324
1325    /// Prepares worker-local structures to execute an invariant campaign slice.
1326    #[allow(clippy::too_many_arguments)]
1327    fn prepare_worker(
1328        executor: &mut Executor<FEN>,
1329        plan: InvariantWorkerPlan,
1330        invariant_contract: &InvariantContract<'_>,
1331        fuzz_fixtures: &FuzzFixtures,
1332        fuzz_state: EvmFuzzState,
1333        runner: &TestRunner,
1334        config: &InvariantConfig,
1335        campaign_seed: &InvariantCampaignSeed,
1336        corpus_seed: WorkerCorpusSeed,
1337    ) -> Result<(InvariantTest, WorkerCorpus)> {
1338        let fuzz_state = fuzz_state.into_invariant();
1339        let targeted_contracts = FuzzRunIdentifiedContracts::new(
1340            campaign_seed.targeted_contracts.clone(),
1341            campaign_seed.targets_are_updatable,
1342        );
1343
1344        // Creates the invariant strategy.
1345        let strategy = invariant_strat(
1346            fuzz_state.clone(),
1347            campaign_seed.sender_filters.clone(),
1348            targeted_contracts.clone(),
1349            config.clone(),
1350            fuzz_fixtures.clone(),
1351        )
1352        .no_shrink();
1353
1354        // If any of the targeted contracts have the storage layout enabled then we can sample
1355        // mapping values. To accomplish, we need to record the mapping storage slots and keys.
1356        let mapping_slots = targeted_contracts
1357            .targets()
1358            .iter()
1359            .any(|(_, t)| t.storage_layout.is_some())
1360            .then(AddressMap::default);
1361
1362        // Set up fuzzer WITHOUT call_generator initially.
1363        // We defer call_override until after the initial invariant check to avoid
1364        // injecting random calls during setup which would break the invariant assertion.
1365        executor.inspector_mut().set_fuzzer(Fuzzer {
1366            call_generator: None,
1367            collected_values: Vec::new(),
1368            max_collected_values: config.dictionary.max_fuzz_dictionary_values,
1369            mapping_slots,
1370            collect: true,
1371        });
1372
1373        // Let's make sure the invariant is sound before actually starting the run:
1374        // We'll assert the invariant in its initial state, and if it fails, we'll
1375        // already know if we can early exit the invariant run.
1376        // This does not count as a fuzz run. It will just register the revert.
1377        let mut failures = InvariantFailures::new();
1378        // Seed disk-recovered handler bugs so live counters reflect them from tick 0.
1379        for (&(addr, sel), err) in &campaign_seed.initial_handler_failures {
1380            failures.seed_handler_failure(addr, sel, err.clone());
1381        }
1382        invariant_preflight_check(
1383            invariant_contract,
1384            config,
1385            &targeted_contracts,
1386            executor,
1387            &[],
1388            &mut failures,
1389        )?;
1390        if let Some(fuzzer) = executor.inspector_mut().fuzzer.as_mut() {
1391            fuzz_state.collect_values(fuzzer.drain_collected_values());
1392        }
1393        // NOW enable call_override after the initial invariant check has passed.
1394        // This allows `override_call_strat` to inject calls during actual fuzz runs
1395        // for reentrancy vulnerability detection.
1396        if config.call_override {
1397            let target_contract_ref = Arc::new(RwLock::new(Address::ZERO));
1398
1399            // Collect handler addresses - these are the contracts we want to inject
1400            // reentrancy into (simulating malicious receive() functions).
1401            let handler_addresses: std::collections::HashSet<Address> =
1402                targeted_contracts.targets().keys().copied().collect();
1403            let override_targets = targeted_contracts
1404                .targets()
1405                .iter()
1406                .filter_map(|(address, contract)| {
1407                    let functions = contract.abi_fuzzed_functions().cloned().collect::<Vec<_>>();
1408                    (!functions.is_empty()).then_some((*address, functions))
1409                })
1410                .collect::<Vec<_>>();
1411
1412            let call_generator = RandomCallGenerator::new(
1413                invariant_contract.address,
1414                handler_addresses,
1415                runner.clone(),
1416                override_call_strat(
1417                    fuzz_state.snapshot(),
1418                    override_targets,
1419                    target_contract_ref.clone(),
1420                    fuzz_fixtures.clone(),
1421                ),
1422                target_contract_ref,
1423            );
1424
1425            if let Some(fuzzer) = executor.inspector_mut().fuzzer.as_mut() {
1426                fuzzer.call_generator = Some(call_generator);
1427            }
1428        }
1429
1430        let worker = WorkerCorpus::from_seed(
1431            plan.worker_id as usize,
1432            config.corpus.clone(),
1433            strategy.boxed(),
1434            corpus_seed,
1435        );
1436
1437        let mut invariant_test =
1438            InvariantTest::new(fuzz_state, targeted_contracts, failures, runner.clone());
1439
1440        // Seed invariant test with previously persisted optimization state,
1441        // but only if the current invariant is in optimization mode. Persisted optimization state
1442        // is a master-worker artifact loaded with the initial corpus.
1443        if invariant_contract.is_optimization() {
1444            let (opt_best_value, opt_best_sequence) = worker.optimization_initial_state();
1445            if let Some(value) = opt_best_value {
1446                invariant_test.update_optimization_value(value, &opt_best_sequence);
1447            }
1448        }
1449
1450        Ok((invariant_test, worker))
1451    }
1452
1453    /// Fills the `InvariantExecutor` with the artifact identifier filters (in `path:name` string
1454    /// format). They will be used to filter contracts after the `setUp`, and more importantly,
1455    /// during the runs.
1456    ///
1457    /// Also excludes any contract without any mutable functions.
1458    ///
1459    /// Priority:
1460    ///
1461    /// targetArtifactSelectors > excludeArtifacts > targetArtifacts
1462    pub fn select_contract_artifacts(&mut self, invariant_address: Address) -> Result<()> {
1463        let targeted_artifact_selectors = self
1464            .executor
1465            .call_sol_default(invariant_address, &IInvariantTest::targetArtifactSelectorsCall {});
1466
1467        // Insert them into the executor `targeted_abi`.
1468        for IInvariantTest::FuzzArtifactSelector { artifact, selectors } in
1469            targeted_artifact_selectors
1470        {
1471            let identifier = self.validate_selected_contract(artifact, &selectors)?;
1472            self.artifact_filters.targeted.entry(identifier).or_default().extend(selectors);
1473        }
1474
1475        let targeted_artifacts = self
1476            .executor
1477            .call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {});
1478        let excluded_artifacts = self
1479            .executor
1480            .call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {});
1481
1482        // Insert `excludeArtifacts` into the executor `excluded_abi`.
1483        for contract in excluded_artifacts {
1484            let identifier = self.validate_selected_contract(contract, &[])?;
1485
1486            if !self.artifact_filters.excluded.contains(&identifier) {
1487                self.artifact_filters.excluded.push(identifier);
1488            }
1489        }
1490
1491        // Exclude any artifact without mutable functions.
1492        for (artifact, contract) in self.project_contracts.iter() {
1493            if contract
1494                .abi
1495                .functions()
1496                .filter(|func| {
1497                    !matches!(
1498                        func.state_mutability,
1499                        alloy_json_abi::StateMutability::Pure
1500                            | alloy_json_abi::StateMutability::View
1501                    )
1502                })
1503                .count()
1504                == 0
1505                && !self.artifact_filters.excluded.contains(&artifact.identifier())
1506            {
1507                self.artifact_filters.excluded.push(artifact.identifier());
1508            }
1509        }
1510
1511        // Insert `targetArtifacts` into the executor `targeted_abi`, if they have not been seen
1512        // before.
1513        for contract in targeted_artifacts {
1514            let identifier = self.validate_selected_contract(contract, &[])?;
1515
1516            if !self.artifact_filters.targeted.contains_key(&identifier)
1517                && !self.artifact_filters.excluded.contains(&identifier)
1518            {
1519                self.artifact_filters.targeted.insert(identifier, vec![]);
1520            }
1521        }
1522        Ok(())
1523    }
1524
1525    /// Makes sure that the contract exists in the project. If so, it returns its artifact
1526    /// identifier.
1527    fn validate_selected_contract(
1528        &mut self,
1529        contract: String,
1530        selectors: &[FixedBytes<4>],
1531    ) -> Result<String> {
1532        if let Some((artifact, contract_data)) =
1533            self.project_contracts.find_by_name_or_identifier(&contract)?
1534        {
1535            // Check that the selectors really exist for this contract.
1536            for selector in selectors {
1537                contract_data
1538                    .abi
1539                    .functions()
1540                    .find(|func| func.selector().as_slice() == selector.as_slice())
1541                    .wrap_err(format!("{contract} does not have the selector {selector:?}"))?;
1542            }
1543
1544            return Ok(artifact.identifier());
1545        }
1546        eyre::bail!(
1547            "{contract} not found in the project. Allowed format: `contract_name` or `contract_path:contract_name`."
1548        );
1549    }
1550
1551    /// Selects senders and contracts based on the contract methods `targetSenders() -> address[]`,
1552    /// `targetContracts() -> address[]` and `excludeContracts() -> address[]`.
1553    pub fn select_contracts_and_senders(
1554        &self,
1555        to: Address,
1556    ) -> Result<(SenderFilters, FuzzRunIdentifiedContracts)> {
1557        let targeted_senders =
1558            self.executor.call_sol_default(to, &IInvariantTest::targetSendersCall {});
1559        let mut excluded_senders =
1560            self.executor.call_sol_default(to, &IInvariantTest::excludeSendersCall {});
1561        // Extend with default excluded addresses - https://github.com/foundry-rs/foundry/issues/4163
1562        excluded_senders.extend([
1563            CHEATCODE_ADDRESS,
1564            HARDHAT_CONSOLE_ADDRESS,
1565            DEFAULT_CREATE2_DEPLOYER,
1566        ]);
1567        // Extend with precompiles - https://github.com/foundry-rs/foundry/issues/4287
1568        excluded_senders.extend(PRECOMPILES);
1569        let sender_filters = SenderFilters::new(targeted_senders, excluded_senders);
1570
1571        let selected = self.executor.call_sol_default(to, &IInvariantTest::targetContractsCall {});
1572        let excluded = self.executor.call_sol_default(to, &IInvariantTest::excludeContractsCall {});
1573
1574        let contracts = self
1575            .setup_contracts
1576            .iter()
1577            .filter(|&(addr, (identifier, _))| {
1578                // Include to address if explicitly set as target.
1579                if *addr == to && selected.contains(&to) {
1580                    return true;
1581                }
1582
1583                *addr != to
1584                    && *addr != CHEATCODE_ADDRESS
1585                    && *addr != HARDHAT_CONSOLE_ADDRESS
1586                    && (selected.is_empty() || selected.contains(addr))
1587                    && (excluded.is_empty() || !excluded.contains(addr))
1588                    && self.artifact_filters.matches(identifier)
1589            })
1590            .map(|(addr, (identifier, abi))| {
1591                (
1592                    *addr,
1593                    TargetedContract::new(identifier.clone(), abi.clone())
1594                        .with_project_contracts(self.project_contracts),
1595                )
1596            })
1597            .collect();
1598        let mut contracts = TargetedContracts { inner: contracts };
1599
1600        self.target_interfaces(to, &mut contracts)?;
1601
1602        self.select_selectors(to, &mut contracts)?;
1603
1604        // There should be at least one contract identified as target for fuzz runs.
1605        if contracts.is_empty() {
1606            eyre::bail!("No contracts to fuzz.");
1607        }
1608
1609        Ok((sender_filters, FuzzRunIdentifiedContracts::new(contracts, selected.is_empty())))
1610    }
1611
1612    /// Extends the contracts and selectors to fuzz with the addresses and ABIs specified in
1613    /// `targetInterfaces() -> (address, string[])[]`. Enables targeting of addresses that are
1614    /// not deployed during `setUp` such as when fuzzing in a forked environment. Also enables
1615    /// targeting of delegate proxies and contracts deployed with `create` or `create2`.
1616    pub fn target_interfaces(
1617        &self,
1618        invariant_address: Address,
1619        targeted_contracts: &mut TargetedContracts,
1620    ) -> Result<()> {
1621        let interfaces = self
1622            .executor
1623            .call_sol_default(invariant_address, &IInvariantTest::targetInterfacesCall {});
1624
1625        // Since `targetInterfaces` returns a tuple array there is no guarantee
1626        // that the addresses are unique this map is used to merge functions of
1627        // the specified interfaces for the same address. For example:
1628        // `[(addr1, ["IERC20", "IOwnable"])]` and `[(addr1, ["IERC20"]), (addr1, ("IOwnable"))]`
1629        // should be equivalent.
1630        let mut combined = TargetedContracts::new();
1631
1632        // Loop through each address and its associated artifact identifiers.
1633        // We're borrowing here to avoid taking full ownership.
1634        for IInvariantTest::FuzzInterface { addr, artifacts } in &interfaces {
1635            // Identifiers are specified as an array, so we loop through them.
1636            for identifier in artifacts {
1637                // Try to find the contract by name or identifier in the project's contracts.
1638                if let Some((_, contract_data)) =
1639                    self.project_contracts.iter().find(|(artifact, _)| {
1640                        &artifact.name == identifier || &artifact.identifier() == identifier
1641                    })
1642                {
1643                    let abi = &contract_data.abi;
1644                    combined
1645                        // Check if there's an entry for the given key in the 'combined' map.
1646                        .entry(*addr)
1647                        // If the entry exists, extends its ABI with the function list.
1648                        .and_modify(|entry| {
1649                            // Extend the ABI's function list with the new functions.
1650                            entry.abi.functions.extend(abi.functions.clone());
1651                        })
1652                        // Otherwise insert it into the map.
1653                        .or_insert_with(|| {
1654                            let mut contract =
1655                                TargetedContract::new(identifier.clone(), abi.clone());
1656                            contract.storage_layout =
1657                                contract_data.storage_layout.as_ref().map(Arc::clone);
1658                            contract
1659                        });
1660                }
1661            }
1662        }
1663
1664        targeted_contracts.extend(combined.inner);
1665
1666        Ok(())
1667    }
1668
1669    /// Selects the functions to fuzz based on the contract method `targetSelectors()` and
1670    /// `targetArtifactSelectors()`.
1671    pub fn select_selectors(
1672        &self,
1673        address: Address,
1674        targeted_contracts: &mut TargetedContracts,
1675    ) -> Result<()> {
1676        for (address, (identifier, _)) in self.setup_contracts {
1677            if let Some(selectors) = self.artifact_filters.targeted.get(identifier) {
1678                self.add_address_with_functions(*address, selectors, false, targeted_contracts)?;
1679            }
1680        }
1681
1682        let mut target_test_selectors = vec![];
1683        let mut excluded_test_selectors = vec![];
1684
1685        // Collect contract functions marked as target for fuzzing campaign.
1686        let selectors =
1687            self.executor.call_sol_default(address, &IInvariantTest::targetSelectorsCall {});
1688        for IInvariantTest::FuzzSelector { addr, selectors } in selectors {
1689            if addr == address {
1690                target_test_selectors = selectors.clone();
1691            }
1692            self.add_address_with_functions(addr, &selectors, false, targeted_contracts)?;
1693        }
1694
1695        // Collect contract functions excluded from fuzzing campaign.
1696        let excluded_selectors =
1697            self.executor.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {});
1698        for IInvariantTest::FuzzSelector { addr, selectors } in excluded_selectors {
1699            if addr == address {
1700                // If fuzz selector address is the test contract, then record selectors to be
1701                // later excluded if needed.
1702                excluded_test_selectors = selectors.clone();
1703            }
1704            self.add_address_with_functions(addr, &selectors, true, targeted_contracts)?;
1705        }
1706
1707        if target_test_selectors.is_empty()
1708            && let Some(target) = targeted_contracts.get(&address)
1709        {
1710            // If test contract is marked as a target and no target selector explicitly set, then
1711            // include only state-changing functions that are not reserved and selectors that are
1712            // not explicitly excluded.
1713            let selectors: Vec<_> = target
1714                .abi
1715                .functions()
1716                .filter_map(|func| {
1717                    if matches!(
1718                        func.state_mutability,
1719                        alloy_json_abi::StateMutability::Pure
1720                            | alloy_json_abi::StateMutability::View
1721                    ) || func.is_reserved()
1722                        || excluded_test_selectors.contains(&func.selector())
1723                    {
1724                        None
1725                    } else {
1726                        Some(func.selector())
1727                    }
1728                })
1729                .collect();
1730            self.add_address_with_functions(address, &selectors, false, targeted_contracts)?;
1731        }
1732
1733        Ok(())
1734    }
1735
1736    /// Adds the address and fuzzed or excluded functions to `TargetedContracts`.
1737    fn add_address_with_functions(
1738        &self,
1739        address: Address,
1740        selectors: &[Selector],
1741        should_exclude: bool,
1742        targeted_contracts: &mut TargetedContracts,
1743    ) -> eyre::Result<()> {
1744        // Do not add address in target contracts if no function selected.
1745        if selectors.is_empty() {
1746            return Ok(());
1747        }
1748
1749        let contract = match targeted_contracts.entry(address) {
1750            Entry::Occupied(entry) => entry.into_mut(),
1751            Entry::Vacant(entry) => {
1752                let (identifier, abi) = self.setup_contracts.get(&address).ok_or_else(|| {
1753                    eyre::eyre!(
1754                        "[{}] address does not have an associated contract: {}",
1755                        if should_exclude { "excludeSelectors" } else { "targetSelectors" },
1756                        address
1757                    )
1758                })?;
1759                entry.insert(
1760                    TargetedContract::new(identifier.clone(), abi.clone())
1761                        .with_project_contracts(self.project_contracts),
1762                )
1763            }
1764        };
1765        contract.add_selectors(selectors.iter().copied(), should_exclude)?;
1766        Ok(())
1767    }
1768
1769    /// Computes the current invariant settings for the given invariant contract address.
1770    ///
1771    /// This extracts the target contracts, selectors, senders, and failure settings
1772    /// that are used to determine if a persisted counterexample is still valid.
1773    pub fn compute_settings(&mut self, invariant_address: Address) -> Result<InvariantSettings> {
1774        self.select_contract_artifacts(invariant_address)?;
1775        let (sender_filters, targeted_contracts) =
1776            self.select_contracts_and_senders(invariant_address)?;
1777        let targets = targeted_contracts.targets();
1778        Ok(InvariantSettings::new(&targets, &sender_filters, self.config.fail_on_revert))
1779    }
1780}
1781
1782/// Collects data from call for fuzzing. However, it first verifies that the sender is not an EOA
1783/// before inserting it into the dictionary. Otherwise, we flood the dictionary with
1784/// randomly generated addresses.
1785fn collect_data<FEN: FoundryEvmNetwork>(
1786    invariant_test: &InvariantTest,
1787    state_changeset: &mut AddressMap<Account>,
1788    tx: &BasicTxDetails,
1789    call_result: &RawCallResult<FEN>,
1790    run_depth: u32,
1791    mapping_slots: Option<&AddressMap<foundry_common::mapping_slots::MappingSlots>>,
1792) {
1793    // Verify it has no code.
1794    let has_code = if let Some(Some(code)) =
1795        state_changeset.get(&tx.sender).map(|account| account.info.code.as_ref())
1796    {
1797        !code.is_empty()
1798    } else {
1799        false
1800    };
1801
1802    // We keep the nonce changes to apply later.
1803    let sender_changeset = if has_code { None } else { state_changeset.remove(&tx.sender) };
1804
1805    // Collect values from fuzzed call result and add them to fuzz dictionary.
1806    invariant_test.fuzz_state.collect_values_from_call(
1807        &invariant_test.targeted_contracts,
1808        tx,
1809        &call_result.result,
1810        &call_result.logs,
1811        &*state_changeset,
1812        run_depth,
1813        mapping_slots,
1814    );
1815
1816    // Inject typed sancov trace-cmp operands into the fuzz dictionary.
1817    if let Some(cmp_values) = &call_result.sancov_cmp_values {
1818        invariant_test.fuzz_state.collect_typed_cmp_values(
1819            cmp_values.iter().map(|s| (s.width, alloy_primitives::B256::from(s.value))),
1820        );
1821    }
1822    // Re-add changes
1823    if let Some(changed) = sender_changeset {
1824        state_changeset.insert(tx.sender, changed);
1825    }
1826}
1827
1828/// Calls the `afterInvariant()` function on a contract.
1829/// Returns call result and if call succeeded.
1830/// The state after the call is not persisted.
1831///
1832/// Uses the handler-gate success check so a stale committed `GLOBAL_FAIL_SLOT` from a
1833/// previously-recorded handler bug doesn't false-positive this call (the slot is `1` from
1834/// the prior bug, but `afterInvariant` itself didn't write it in this changeset).
1835pub(crate) fn call_after_invariant_function<FEN: FoundryEvmNetwork>(
1836    executor: &Executor<FEN>,
1837    to: Address,
1838) -> Result<(RawCallResult<FEN>, bool), EvmError<FEN>> {
1839    let calldata = Bytes::from_static(&IInvariantTest::afterInvariantCall::SELECTOR);
1840    let mut call_result = executor.call_raw(CALLER, to, calldata, U256::ZERO)?;
1841    let success = executor.is_raw_call_mut_success_handler_gate(to, &mut call_result);
1842    Ok((call_result, success))
1843}
1844
1845/// Calls the invariant function and returns call result and if succeeded.
1846///
1847/// Uses the handler-gate success check (same rationale as `call_after_invariant_function`):
1848/// the predicate is broken iff this call's own changeset writes `GLOBAL_FAIL_SLOT` (via `t()` /
1849/// `vm.assert*`) or the call reverts; a stale committed slot from a prior handler bug must not
1850/// poison every later predicate evaluation in the run.
1851pub(crate) fn call_invariant_function<FEN: FoundryEvmNetwork>(
1852    executor: &Executor<FEN>,
1853    address: Address,
1854    calldata: Bytes,
1855) -> Result<(RawCallResult<FEN>, bool)> {
1856    let mut call_result = executor.call_raw(CALLER, address, calldata, U256::ZERO)?;
1857    let success = executor.is_raw_call_mut_success_handler_gate(address, &mut call_result);
1858    Ok((call_result, success))
1859}
1860
1861/// Executes a fuzz call and returns the result.
1862/// Applies any block timestamp (warp) and block number (roll) adjustments before the call.
1863pub(crate) fn execute_tx<FEN: FoundryEvmNetwork>(
1864    executor: &mut Executor<FEN>,
1865    tx: &BasicTxDetails,
1866) -> Result<RawCallResult<FEN>> {
1867    let warp = tx.warp.unwrap_or_default();
1868    let roll = tx.roll.unwrap_or_default();
1869
1870    if warp > 0 || roll > 0 {
1871        // Apply pre-call block adjustments to the executor's env.
1872        let ts = executor.evm_env().block_env.timestamp();
1873        let num = executor.evm_env().block_env.number();
1874        executor.evm_env_mut().block_env.set_timestamp(ts + warp);
1875        executor.evm_env_mut().block_env.set_number(num + roll);
1876
1877        // Also update the inspector's cheatcodes.block if set.
1878        // The inspector's block may override the env during interpreter initialization,
1879        // so we need to add our warp/roll on top of any existing cheatcode-set values.
1880        let block_env = executor.evm_env().block_env.clone();
1881        if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
1882            if let Some(block) = cheatcodes.block.as_mut() {
1883                let bts = block.timestamp();
1884                let bnum = block.number();
1885                block.set_timestamp(bts + warp);
1886                block.set_number(bnum + roll);
1887            } else {
1888                cheatcodes.block = Some(block_env);
1889            }
1890        }
1891    }
1892
1893    // Bound requested value by sender's available balance so payable paths still get
1894    // exercised when the requested value exceeds balance, instead of collapsing to zero.
1895    let requested_value = tx.call_details.value.unwrap_or(U256::ZERO);
1896    let sender_balance = executor.get_balance(tx.sender)?;
1897    let value = requested_value.min(sender_balance);
1898    executor
1899        .call_raw(tx.sender, tx.call_details.target, tx.call_details.calldata.clone(), value)
1900        .map_err(|e| eyre!(format!("Could not make raw evm call: {e}")))
1901}
1902
1903#[cfg(test)]
1904mod tests {
1905    use super::*;
1906    use proptest::{prelude::any, strategy::ValueTree, test_runner::Config};
1907    use serde_json::json;
1908
1909    fn first_generated_u64(runner: &mut TestRunner) -> u64 {
1910        any::<u64>().new_tree(runner).unwrap().current()
1911    }
1912
1913    fn test_runner() -> TestRunner {
1914        TestRunner::new(Config { failure_persistence: None, ..Default::default() })
1915    }
1916
1917    fn seeded_test_runner(seed: U256) -> TestRunner {
1918        let config = Config { failure_persistence: None, ..Default::default() };
1919        let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
1920        TestRunner::new_with_rng(config, rng)
1921    }
1922
1923    #[test]
1924    fn invariant_worker_seed_preserves_master_seed_and_derives_workers() {
1925        let seed = U256::from(0x1234);
1926
1927        assert_eq!(invariant_worker_seed(seed, 0), seed);
1928        assert_ne!(invariant_worker_seed(seed, 1), seed);
1929        assert_ne!(invariant_worker_seed(seed, 1), invariant_worker_seed(seed, 2));
1930        assert_ne!(invariant_worker_seed(seed, 1), invariant_worker_seed(U256::from(0x5678), 1));
1931    }
1932
1933    #[test]
1934    fn invariant_worker_runner_preserves_seed_for_master_worker() {
1935        let seed = U256::from(0x1234);
1936        let mut seeded_runner = seeded_test_runner(seed);
1937        let mut parent = test_runner();
1938        let mut worker = invariant_worker_runner(&mut parent, 0, Some(seed));
1939
1940        assert_eq!(first_generated_u64(&mut worker), first_generated_u64(&mut seeded_runner));
1941    }
1942
1943    #[test]
1944    fn invariant_worker_runner_uses_seed_independent_of_parent_rng_state() {
1945        let seed = U256::from(0x1234);
1946        let mut parent = test_runner();
1947        let mut advanced_parent = test_runner();
1948        let _ = first_generated_u64(&mut advanced_parent);
1949
1950        let mut worker = invariant_worker_runner(&mut parent, 1, Some(seed));
1951        let mut worker_from_advanced_parent =
1952            invariant_worker_runner(&mut advanced_parent, 1, Some(seed));
1953
1954        assert_eq!(
1955            first_generated_u64(&mut worker),
1956            first_generated_u64(&mut worker_from_advanced_parent)
1957        );
1958    }
1959
1960    #[test]
1961    fn invariant_progress_json_includes_throughput_fields() {
1962        let throughput = InvariantThroughputMetrics { total_txs: 2, total_gas: 50 };
1963
1964        let payload = build_invariant_progress_json(
1965            123,
1966            "invariant_balance",
1967            &json!({ "corpus_count": 7 }),
1968            Some(I256::try_from(42).unwrap()),
1969            throughput,
1970            &InvariantFailureMetrics::default(),
1971            Duration::from_secs(10),
1972        );
1973
1974        assert_eq!(payload["timestamp"], json!(123));
1975        assert_eq!(payload["invariant"], json!("invariant_balance"));
1976        assert_eq!(payload["metrics"]["corpus_count"], json!(7));
1977        assert_eq!(payload["metrics"]["broken_handlers"], json!(0));
1978        assert_eq!(payload["total_txs"], json!(2));
1979        assert_eq!(payload["total_gas"], json!(50));
1980        assert!((payload["tx_per_sec"].as_f64().unwrap() - 0.2).abs() < 1e-12);
1981        assert!((payload["gas_per_sec"].as_f64().unwrap() - 5.0).abs() < 1e-12);
1982        assert_eq!(payload["optimization_best"], json!("42"));
1983    }
1984
1985    #[test]
1986    fn invariant_worker_count_keeps_short_campaigns_single_worker() {
1987        assert_eq!(
1988            max_invariant_workers_for_campaign(0, DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP),
1989            1
1990        );
1991        assert_eq!(
1992            max_invariant_workers_for_campaign(
1993                MIN_RUNS_PER_INVARIANT_WORKER - 1,
1994                DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
1995            ),
1996            1
1997        );
1998        assert_eq!(
1999            max_invariant_workers_for_campaign(
2000                MIN_RUNS_PER_INVARIANT_WORKER,
2001                DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2002            ),
2003            1
2004        );
2005        assert_eq!(
2006            max_invariant_workers_for_campaign(
2007                MIN_RUNS_PER_INVARIANT_WORKER * 2,
2008                DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2009            ),
2010            2
2011        );
2012        assert_eq!(max_invariant_workers_for_campaign(256, 100_000), 5);
2013    }
2014
2015    #[test]
2016    fn invariant_worker_count_preserves_fixed_workers() {
2017        let mut config = InvariantConfig {
2018            runs: MIN_RUNS_PER_INVARIANT_WORKER * 4,
2019            workers: foundry_config::InvariantWorkers::Fixed(
2020                std::num::NonZeroUsize::new(4).unwrap(),
2021            ),
2022            ..Default::default()
2023        };
2024        assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2025
2026        config.corpus.show_edge_coverage = true;
2027        assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2028
2029        config.corpus.show_edge_coverage = false;
2030        config.corpus.corpus_dir = Some(std::path::PathBuf::from("corpus"));
2031        assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2032
2033        config.runs = MIN_RUNS_PER_INVARIANT_WORKER - 1;
2034        config.timeout = None;
2035        assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2036
2037        config.timeout = Some(1);
2038        assert_eq!(invariant_worker_count_with_threads(&config, 8, 4), 4);
2039    }
2040
2041    #[test]
2042    fn invariant_worker_count_does_not_cap_configured_workers_by_available_threads() {
2043        let config = InvariantConfig {
2044            runs: MIN_RUNS_PER_INVARIANT_WORKER * 8,
2045            workers: foundry_config::InvariantWorkers::Fixed(
2046                std::num::NonZeroUsize::new(8).unwrap(),
2047            ),
2048            ..Default::default()
2049        };
2050
2051        assert_eq!(invariant_worker_count_with_threads(&config, 4, 1), 8);
2052    }
2053
2054    #[test]
2055    fn invariant_worker_count_splits_available_threads_for_auto_workers() {
2056        let mut config = InvariantConfig {
2057            runs: MIN_RUNS_PER_INVARIANT_WORKER * 4,
2058            depth: DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP,
2059            workers: foundry_config::InvariantWorkers::Auto,
2060            ..Default::default()
2061        };
2062
2063        assert_eq!(invariant_worker_count_with_threads(&config, 4, 1), 4);
2064        assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2065        assert_eq!(invariant_worker_count_with_threads(&config, 8, 3), 2);
2066        assert_eq!(invariant_worker_count_with_threads(&config, 3, 8), 1);
2067        assert_eq!(invariant_worker_count_with_threads(&config, 0, 0), 1);
2068
2069        config.runs = MIN_RUNS_PER_INVARIANT_WORKER - 1;
2070        assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 1);
2071
2072        config.depth = 100_000;
2073        assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2074
2075        config.timeout = Some(1);
2076        assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2077    }
2078
2079    #[test]
2080    fn timed_invariant_workers_are_not_bounded_by_assigned_runs() {
2081        let plan = InvariantWorkerPlan { worker_id: 0, first_global_run: 0, runs: 1 };
2082
2083        let untimed = InvariantCampaignState::new(EarlyExit::new(false), None);
2084        assert!(should_continue_invariant_worker(&untimed, 0, plan));
2085        assert!(!should_continue_invariant_worker(&untimed, 1, plan));
2086
2087        let timed = InvariantCampaignState::new(EarlyExit::new(false), Some(60));
2088        assert!(should_continue_invariant_worker(&timed, 0, plan));
2089        assert!(should_continue_invariant_worker(&timed, 1, plan));
2090        assert!(should_continue_invariant_worker(&timed, 10_000, plan));
2091    }
2092
2093    #[test]
2094    fn gas_report_samples_are_split_across_workers() {
2095        assert_eq!(gas_report_samples_for_worker(0, 0, 4), 0);
2096        assert_eq!(gas_report_samples_for_worker(8, 0, 4), 2);
2097        assert_eq!(gas_report_samples_for_worker(8, 3, 4), 2);
2098        assert_eq!(gas_report_samples_for_worker(10, 0, 4), 3);
2099        assert_eq!(gas_report_samples_for_worker(10, 1, 4), 3);
2100        assert_eq!(gas_report_samples_for_worker(10, 2, 4), 2);
2101        assert_eq!(gas_report_samples_for_worker(10, 3, 4), 2);
2102        assert_eq!(gas_report_samples_for_worker(3, 3, 4), 0);
2103    }
2104
2105    #[test]
2106    fn invariant_progress_json_zero_elapsed_reports_zero_rates() {
2107        let throughput = InvariantThroughputMetrics { total_txs: 1, total_gas: 21_000 };
2108
2109        let payload = build_invariant_progress_json(
2110            456,
2111            "invariant_zero_elapsed",
2112            &json!({ "corpus_count": 1 }),
2113            None,
2114            throughput,
2115            &InvariantFailureMetrics::default(),
2116            Duration::ZERO,
2117        );
2118
2119        assert_eq!(payload["tx_per_sec"], json!(0.0));
2120        assert_eq!(payload["gas_per_sec"], json!(0.0));
2121        assert!(payload.get("optimization_best").is_none());
2122    }
2123
2124    #[test]
2125    fn invariant_progress_json_includes_failure_counts() {
2126        let mut failure_metrics = InvariantFailureMetrics::default();
2127        failure_metrics.record_failure("invariant_a", "TestContract", "revert");
2128        failure_metrics.record_failure("invariant_a", "TestContract", "revert");
2129        failure_metrics.record_failure("invariant_b", "TestContract", "assertion failed");
2130        failure_metrics.broken_handlers = 7;
2131
2132        let payload = build_invariant_progress_json(
2133            789,
2134            "invariant_a",
2135            &json!({ "corpus_count": 5 }),
2136            None,
2137            InvariantThroughputMetrics::default(),
2138            &failure_metrics,
2139            Duration::from_secs(1),
2140        );
2141
2142        assert_eq!(payload["metrics"]["failures"], json!(3));
2143        assert_eq!(payload["metrics"]["unique_failures"], json!(2));
2144        assert_eq!(payload["metrics"]["broken_handlers"], json!(7));
2145    }
2146
2147    #[test]
2148    fn failure_metrics_tracks_total_and_unique_failures() {
2149        let mut metrics = InvariantFailureMetrics::default();
2150        metrics.record_failure("invariant_a", "TestContract", "revert");
2151        metrics.record_failure("invariant_a", "TestContract", "revert");
2152        metrics.record_failure("invariant_b", "TestContract", "assertion failed");
2153
2154        assert_eq!(metrics.failures, 3);
2155        assert_eq!(metrics.unique_failures.len(), 2);
2156        assert!(metrics.unique_failures.contains("invariant_a"));
2157        assert!(metrics.unique_failures.contains("invariant_b"));
2158    }
2159
2160    #[test]
2161    fn failure_metrics_default_is_zero() {
2162        let metrics = InvariantFailureMetrics::default();
2163        assert_eq!(metrics.failures, 0);
2164        assert!(metrics.unique_failures.is_empty());
2165        assert_eq!(metrics.broken_handlers, 0);
2166    }
2167}