Skip to main content

foundry_evm/executors/invariant/
mod.rs

1use crate::{
2    executors::{
3        DURATION_BETWEEN_METRICS_REPORT, EarlyExit, EvmError, Executor, FuzzTestTimer,
4        RawCallResult, corpus::WorkerCorpus,
5    },
6    inspectors::Fuzzer,
7};
8use alloy_primitives::{Address, Bytes, FixedBytes, I256, Selector, U256, map::AddressMap};
9use alloy_sol_types::{SolCall, sol};
10use eyre::{ContextCompat, Result, eyre};
11use foundry_common::{
12    TestFunctionExt,
13    contracts::{ContractsByAddress, ContractsByArtifact},
14    sh_eprintln, sh_println,
15};
16use foundry_config::InvariantConfig;
17use foundry_evm_core::{
18    FoundryBlock,
19    constants::{
20        CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS, MAGIC_ASSUME,
21    },
22    evm::FoundryEvmNetwork,
23    precompiles::PRECOMPILES,
24};
25use foundry_evm_fuzz::{
26    BasicTxDetails, FuzzCase, FuzzFixtures, FuzzedCases,
27    invariant::{
28        ArtifactFilters, FuzzRunIdentifiedContracts, InvariantContract, InvariantSettings,
29        RandomCallGenerator, SenderFilters, TargetedContract, TargetedContracts,
30    },
31    strategies::{EvmFuzzState, invariant_strat, override_call_strat},
32};
33use foundry_evm_traces::{CallTraceArena, SparsedTraceArena};
34use indicatif::ProgressBar;
35use parking_lot::RwLock;
36use proptest::{strategy::Strategy, test_runner::TestRunner};
37use result::{assert_after_invariant, assert_invariants, can_continue, did_fail_on_assert};
38use revm::{context::Block, state::Account};
39use serde::{Deserialize, Serialize};
40use serde_json::json;
41use std::{
42    collections::{HashMap as Map, HashSet, btree_map::Entry},
43    sync::Arc,
44    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
45};
46
47mod error;
48pub use error::{InvariantFailures, InvariantFuzzError};
49use foundry_evm_coverage::HitMaps;
50
51mod replay;
52pub use replay::{replay_error, replay_run};
53
54mod result;
55pub use result::InvariantFuzzTestResult;
56
57mod shrink;
58pub use shrink::{CheckSequenceOptions, check_sequence, check_sequence_value};
59
60sol! {
61    interface IInvariantTest {
62        #[derive(Default)]
63        struct FuzzSelector {
64            address addr;
65            bytes4[] selectors;
66        }
67
68        #[derive(Default)]
69        struct FuzzArtifactSelector {
70            string artifact;
71            bytes4[] selectors;
72        }
73
74        #[derive(Default)]
75        struct FuzzInterface {
76            address addr;
77            string[] artifacts;
78        }
79
80        function afterInvariant() external;
81
82        #[derive(Default)]
83        function excludeArtifacts() public view returns (string[] memory excludedArtifacts);
84
85        #[derive(Default)]
86        function excludeContracts() public view returns (address[] memory excludedContracts);
87
88        #[derive(Default)]
89        function excludeSelectors() public view returns (FuzzSelector[] memory excludedSelectors);
90
91        #[derive(Default)]
92        function excludeSenders() public view returns (address[] memory excludedSenders);
93
94        #[derive(Default)]
95        function targetArtifacts() public view returns (string[] memory targetedArtifacts);
96
97        #[derive(Default)]
98        function targetArtifactSelectors() public view returns (FuzzArtifactSelector[] memory targetedArtifactSelectors);
99
100        #[derive(Default)]
101        function targetContracts() public view returns (address[] memory targetedContracts);
102
103        #[derive(Default)]
104        function targetSelectors() public view returns (FuzzSelector[] memory targetedSelectors);
105
106        #[derive(Default)]
107        function targetSenders() public view returns (address[] memory targetedSenders);
108
109        #[derive(Default)]
110        function targetInterfaces() public view returns (FuzzInterface[] memory targetedInterfaces);
111    }
112}
113
114/// Contains invariant metrics for a single fuzzed selector.
115#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
116pub struct InvariantMetrics {
117    // Count of fuzzed selector calls.
118    pub calls: usize,
119    // Count of fuzzed selector reverts.
120    pub reverts: usize,
121    // Count of fuzzed selector discards (through assume cheatcodes).
122    pub discards: usize,
123}
124
125/// Campaign-level throughput metrics for invariant progress reporting.
126#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
127struct InvariantThroughputMetrics {
128    total_txs: u64,
129    total_gas: u64,
130}
131
132impl InvariantThroughputMetrics {
133    const fn record_call(&mut self, gas_used: u64) {
134        self.total_txs += 1;
135        self.total_gas += gas_used;
136    }
137
138    fn tx_per_sec(self, elapsed: Duration) -> f64 {
139        rate_per_sec(self.total_txs as f64, elapsed)
140    }
141
142    fn gas_per_sec(self, elapsed: Duration) -> f64 {
143        rate_per_sec(self.total_gas as f64, elapsed)
144    }
145}
146
147/// Converts a cumulative campaign total into an average per-second rate.
148///
149/// Returns `0.0` during the initial zero-elapsed startup window to avoid
150/// dividing by zero while progress reporting is warming up.
151fn rate_per_sec(total: f64, elapsed: Duration) -> f64 {
152    let elapsed_secs = elapsed.as_secs_f64();
153    if elapsed_secs > 0.0 { total / elapsed_secs } else { 0.0 }
154}
155
156/// Tracks invariant failure counts during a campaign.
157#[derive(Debug, Default)]
158struct InvariantFailureMetrics {
159    failures: u64,
160    unique_failures: HashSet<String>,
161}
162
163impl InvariantFailureMetrics {
164    /// Records a failure and emits a structured JSON `"failure"` event.
165    fn record_failure(&mut self, invariant_name: &str, target: &str, reason: &str) {
166        self.failures += 1;
167        self.unique_failures.insert(invariant_name.to_string());
168
169        let timestamp =
170            SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
171        let event = json!({
172            "timestamp": timestamp,
173            "event": "failure",
174            "invariant": invariant_name,
175            "target": target,
176            "reason": reason,
177        });
178        let _ = sh_eprintln!("{}", serde_json::to_string(&event).unwrap_or_default());
179    }
180}
181
182/// Builds the machine-readable invariant progress payload emitted during a
183/// campaign.
184///
185/// This keeps the existing corpus progress metrics together with cumulative and
186/// derived throughput fields so downstream benchmark tooling can consume a
187/// single JSON event shape.
188fn build_invariant_progress_json<M: Serialize>(
189    timestamp_secs: u64,
190    invariant_name: &str,
191    corpus_metrics: &M,
192    optimization_best: Option<I256>,
193    throughput: InvariantThroughputMetrics,
194    failure_metrics: &InvariantFailureMetrics,
195    elapsed: Duration,
196) -> serde_json::Value {
197    let mut metrics = serde_json::to_value(corpus_metrics).unwrap_or_default();
198    if let Some(obj) = metrics.as_object_mut() {
199        obj.insert("failures".to_string(), json!(failure_metrics.failures));
200        obj.insert("unique_failures".to_string(), json!(failure_metrics.unique_failures.len()));
201    }
202
203    let mut payload = json!({
204        "timestamp": timestamp_secs,
205        "event": "pulse",
206        "invariant": invariant_name,
207        "metrics": metrics,
208        "total_txs": throughput.total_txs,
209        "total_gas": throughput.total_gas,
210        "tx_per_sec": throughput.tx_per_sec(elapsed),
211        "gas_per_sec": throughput.gas_per_sec(elapsed),
212    });
213
214    if let Some(best) = optimization_best {
215        payload["optimization_best"] = json!(best.to_string());
216    }
217
218    payload
219}
220
221/// Contains data collected during invariant test runs.
222struct InvariantTestData<FEN: FoundryEvmNetwork> {
223    // Consumed gas and calldata of every successful fuzz call.
224    fuzz_cases: Vec<FuzzedCases>,
225    // Data related to reverts or failed assertions of the test.
226    failures: InvariantFailures,
227    // Calldata in the last invariant run.
228    last_run_inputs: Vec<BasicTxDetails>,
229    // Additional traces for gas report.
230    gas_report_traces: Vec<Vec<CallTraceArena>>,
231    // Last call results of the invariant test.
232    last_call_results: Option<RawCallResult<FEN>>,
233    // Line coverage information collected from all fuzzed calls.
234    line_coverage: Option<HitMaps>,
235    // Metrics for each fuzzed selector.
236    metrics: Map<String, InvariantMetrics>,
237
238    // Proptest runner to query for random values.
239    // The strategy only comes with the first `input`. We fill the rest of the `inputs`
240    // until the desired `depth` so we can use the evolving fuzz dictionary
241    // during the run.
242    branch_runner: TestRunner,
243
244    // Optimization mode state: tracks the best (maximum) value and the sequence that produced it.
245    // Only used when invariant function returns int256.
246    optimization_best_value: Option<I256>,
247    optimization_best_sequence: Vec<BasicTxDetails>,
248}
249
250/// Contains invariant test data.
251struct InvariantTest<FEN: FoundryEvmNetwork> {
252    // Fuzz state of invariant test.
253    fuzz_state: EvmFuzzState,
254    // Contracts fuzzed by the invariant test.
255    targeted_contracts: FuzzRunIdentifiedContracts,
256    // Data collected during invariant runs.
257    test_data: InvariantTestData<FEN>,
258}
259
260impl<FEN: FoundryEvmNetwork> InvariantTest<FEN> {
261    /// Instantiates an invariant test.
262    fn new(
263        fuzz_state: EvmFuzzState,
264        targeted_contracts: FuzzRunIdentifiedContracts,
265        failures: InvariantFailures,
266        last_call_results: Option<RawCallResult<FEN>>,
267        branch_runner: TestRunner,
268    ) -> Self {
269        let mut fuzz_cases = vec![];
270        if last_call_results.is_none() {
271            fuzz_cases.push(FuzzedCases::new(vec![]));
272        }
273        let test_data = InvariantTestData {
274            fuzz_cases,
275            failures,
276            last_run_inputs: vec![],
277            gas_report_traces: vec![],
278            last_call_results,
279            line_coverage: None,
280            metrics: Map::default(),
281            branch_runner,
282            optimization_best_value: None,
283            optimization_best_sequence: vec![],
284        };
285        Self { fuzz_state, targeted_contracts, test_data }
286    }
287
288    /// Returns number of invariant test reverts.
289    const fn reverts(&self) -> usize {
290        self.test_data.failures.reverts
291    }
292
293    /// Whether invariant test has errors or not.
294    const fn has_errors(&self) -> bool {
295        self.test_data.failures.error.is_some()
296    }
297
298    /// Set invariant test error.
299    fn set_error(&mut self, error: InvariantFuzzError) {
300        self.test_data.failures.error = Some(error);
301    }
302
303    /// Set last invariant test call results.
304    fn set_last_call_results(&mut self, call_result: Option<RawCallResult<FEN>>) {
305        self.test_data.last_call_results = call_result;
306    }
307
308    /// Set last invariant run call sequence.
309    fn set_last_run_inputs(&mut self, inputs: &Vec<BasicTxDetails>) {
310        self.test_data.last_run_inputs.clone_from(inputs);
311    }
312
313    /// Merge current collected line coverage with the new coverage from last fuzzed call.
314    fn merge_line_coverage(&mut self, new_coverage: Option<HitMaps>) {
315        HitMaps::merge_opt(&mut self.test_data.line_coverage, new_coverage);
316    }
317
318    /// Update metrics for a fuzzed selector, extracted from tx details.
319    /// Always increments number of calls; discarded runs (through assume cheatcodes) are tracked
320    /// separated from reverts.
321    fn record_metrics(&mut self, tx_details: &BasicTxDetails, reverted: bool, discarded: bool) {
322        if let Some(metric_key) =
323            self.targeted_contracts.targets.lock().fuzzed_metric_key(tx_details)
324        {
325            let test_metrics = &mut self.test_data.metrics;
326            let invariant_metrics = test_metrics.entry(metric_key).or_default();
327            invariant_metrics.calls += 1;
328            if discarded {
329                invariant_metrics.discards += 1;
330            } else if reverted {
331                invariant_metrics.reverts += 1;
332            }
333        }
334    }
335
336    /// End invariant test run by collecting results, cleaning collected artifacts and reverting
337    /// created fuzz state.
338    fn end_run(&mut self, run: InvariantTestRun<FEN>, gas_samples: usize) {
339        // We clear all the targeted contracts created during this run.
340        self.targeted_contracts.clear_created_contracts(run.created_contracts);
341
342        if self.test_data.gas_report_traces.len() < gas_samples {
343            self.test_data
344                .gas_report_traces
345                .push(run.run_traces.into_iter().map(|arena| arena.arena).collect());
346        }
347        self.test_data.fuzz_cases.push(FuzzedCases::new(run.fuzz_runs));
348
349        // Revert state to not persist values between runs.
350        self.fuzz_state.revert();
351    }
352
353    /// Updates the optimization state if the new value is better (higher) than the current best.
354    fn update_optimization_value(&mut self, value: I256, sequence: &[BasicTxDetails]) {
355        if self.test_data.optimization_best_value.is_none_or(|best| value > best) {
356            self.test_data.optimization_best_value = Some(value);
357            self.test_data.optimization_best_sequence = sequence.to_vec();
358        }
359    }
360}
361
362/// Contains data for an invariant test run.
363struct InvariantTestRun<FEN: FoundryEvmNetwork> {
364    // Invariant run call sequence.
365    inputs: Vec<BasicTxDetails>,
366    // Current invariant run executor.
367    executor: Executor<FEN>,
368    // Invariant run stat reports (eg. gas usage).
369    fuzz_runs: Vec<FuzzCase>,
370    // Contracts created during current invariant run.
371    created_contracts: Vec<Address>,
372    // Traces of each call of the invariant run call sequence.
373    run_traces: Vec<SparsedTraceArena>,
374    // Current depth of invariant run.
375    depth: u32,
376    // Current assume rejects of the invariant run.
377    rejects: u32,
378    // Whether new coverage was discovered during this run.
379    new_coverage: bool,
380    // For optimization mode: the best value found during this run (if any).
381    optimization_value: Option<I256>,
382    // For optimization mode: the length of the input prefix that produced the best value.
383    optimization_prefix_len: usize,
384}
385
386impl<FEN: FoundryEvmNetwork> InvariantTestRun<FEN> {
387    /// Instantiates an invariant test run.
388    fn new(first_input: BasicTxDetails, executor: Executor<FEN>, depth: usize) -> Self {
389        Self {
390            inputs: vec![first_input],
391            executor,
392            fuzz_runs: Vec::with_capacity(depth),
393            created_contracts: vec![],
394            run_traces: vec![],
395            depth: 0,
396            rejects: 0,
397            new_coverage: false,
398            optimization_value: None,
399            optimization_prefix_len: 0,
400        }
401    }
402}
403
404/// Wrapper around any [`Executor`] implementer which provides fuzzing support using [`proptest`].
405///
406/// After instantiation, calling `invariant_fuzz` will proceed to hammer the deployed smart
407/// contracts with inputs, until it finds a counterexample sequence. The provided [`TestRunner`]
408/// contains all the configuration which can be overridden via [environment
409/// variables](proptest::test_runner::Config)
410pub struct InvariantExecutor<'a, FEN: FoundryEvmNetwork> {
411    pub executor: Executor<FEN>,
412    /// Proptest runner.
413    runner: TestRunner,
414    /// The invariant configuration
415    config: InvariantConfig,
416    /// Contracts deployed with `setUp()`
417    setup_contracts: &'a ContractsByAddress,
418    /// Contracts that are part of the project but have not been deployed yet. We need the bytecode
419    /// to identify them from the stateset changes.
420    project_contracts: &'a ContractsByArtifact,
421    /// Filters contracts to be fuzzed through their artifact identifiers.
422    artifact_filters: ArtifactFilters,
423}
424
425impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
426    /// Instantiates a fuzzed executor EVM given a testrunner
427    pub fn new(
428        executor: Executor<FEN>,
429        runner: TestRunner,
430        config: InvariantConfig,
431        setup_contracts: &'a ContractsByAddress,
432        project_contracts: &'a ContractsByArtifact,
433    ) -> Self {
434        Self {
435            executor,
436            runner,
437            config,
438            setup_contracts,
439            project_contracts,
440            artifact_filters: ArtifactFilters::default(),
441        }
442    }
443
444    pub fn config(self) -> InvariantConfig {
445        self.config
446    }
447
448    /// Fuzzes any deployed contract and checks any broken invariant at `invariant_address`.
449    pub fn invariant_fuzz(
450        &mut self,
451        invariant_contract: InvariantContract<'_>,
452        fuzz_fixtures: &FuzzFixtures,
453        fuzz_state: EvmFuzzState,
454        progress: Option<&ProgressBar>,
455        early_exit: &EarlyExit,
456    ) -> Result<InvariantFuzzTestResult> {
457        // Throw an error to abort test run if the invariant function accepts input params
458        if !invariant_contract.invariant_function.inputs.is_empty() {
459            return Err(eyre!("Invariant test function should have no inputs"));
460        }
461
462        let (mut invariant_test, mut corpus_manager) =
463            self.prepare_test(&invariant_contract, fuzz_fixtures, fuzz_state)?;
464
465        // Start timer for this invariant test.
466        let mut runs = 0;
467        let timer = FuzzTestTimer::new(self.config.timeout);
468        let mut last_metrics_report = Instant::now();
469        let campaign_start = Instant::now();
470        let mut throughput = InvariantThroughputMetrics::default();
471        let mut failure_metrics = InvariantFailureMetrics::default();
472        let continue_campaign = |runs: u32| {
473            if early_exit.should_stop() {
474                return false;
475            }
476
477            if timer.is_enabled() { !timer.is_timed_out() } else { runs < self.config.runs }
478        };
479
480        // Invariant runs with edge coverage if corpus dir is set or showing edge coverage.
481        let edge_coverage_enabled = self.config.corpus.collect_edge_coverage();
482
483        'stop: while continue_campaign(runs) {
484            let initial_seq = corpus_manager.new_inputs(
485                &mut invariant_test.test_data.branch_runner,
486                &invariant_test.fuzz_state,
487                &invariant_test.targeted_contracts,
488            )?;
489
490            // Create current invariant run data.
491            let mut current_run = InvariantTestRun::new(
492                initial_seq[0].clone(),
493                // Before each run, we must reset the backend state.
494                self.executor.clone(),
495                self.config.depth as usize,
496            );
497
498            // We stop the run immediately if we have reverted, and `fail_on_revert` is set.
499            if self.config.fail_on_revert && invariant_test.reverts() > 0 {
500                return Err(eyre!("call reverted"));
501            }
502
503            while current_run.depth < self.config.depth {
504                // Check if the timeout has been reached.
505                if timer.is_timed_out() {
506                    // Since we never record a revert here the test is still considered
507                    // successful even though it timed out. We *want*
508                    // this behavior for now, so that's ok, but
509                    // future developers should be aware of this.
510                    break 'stop;
511                }
512
513                let tx = current_run
514                    .inputs
515                    .last()
516                    .ok_or_else(|| eyre!("no input generated to call fuzzed target."))?;
517
518                // Execute call from the randomly generated sequence without committing state.
519                // State is committed only if call is not a magic assume.
520                let mut call_result = execute_tx(&mut current_run.executor, tx)?;
521                let discarded = call_result.result.as_ref() == MAGIC_ASSUME;
522                if self.config.show_metrics {
523                    invariant_test.record_metrics(tx, call_result.reverted, discarded);
524                }
525
526                // Collect line coverage from last fuzzed call.
527                invariant_test.merge_line_coverage(call_result.line_coverage.clone());
528                // Collect edge coverage and set the flag in the current run.
529                if corpus_manager.merge_edge_coverage(&mut call_result) {
530                    current_run.new_coverage = true;
531                }
532
533                if discarded {
534                    current_run.inputs.pop();
535                    current_run.rejects += 1;
536                    if current_run.rejects > self.config.max_assume_rejects {
537                        invariant_test.set_error(InvariantFuzzError::MaxAssumeRejects(
538                            self.config.max_assume_rejects,
539                        ));
540                        break 'stop;
541                    }
542                } else {
543                    let assertion_failure =
544                        did_fail_on_assert(&call_result, &call_result.state_changeset);
545
546                    // Commit executed call result.
547                    current_run.executor.commit(&mut call_result);
548
549                    // Collect data for fuzzing from the state changeset.
550                    // This step updates the state dictionary and therefore invalidates the
551                    // ValueTree in use by the current run. This manifestsitself in proptest
552                    // observing a different input case than what it was called with, and creates
553                    // inconsistencies whenever proptest tries to use the input case after test
554                    // execution.
555                    // See <https://github.com/foundry-rs/foundry/issues/9764>.
556                    let mut state_changeset = std::mem::take(&mut call_result.state_changeset);
557                    if !call_result.reverted {
558                        collect_data(
559                            &invariant_test,
560                            &mut state_changeset,
561                            tx,
562                            &call_result,
563                            self.config.depth,
564                        );
565                    }
566
567                    // Collect created contracts and add to fuzz targets only if targeted contracts
568                    // are updatable.
569                    if let Err(error) =
570                        &invariant_test.targeted_contracts.collect_created_contracts(
571                            &state_changeset,
572                            self.project_contracts,
573                            self.setup_contracts,
574                            &self.artifact_filters,
575                            &mut current_run.created_contracts,
576                        )
577                    {
578                        warn!(target: "forge::test", "{error}");
579                    }
580                    current_run
581                        .fuzz_runs
582                        .push(FuzzCase { gas: call_result.gas_used, stipend: call_result.stipend });
583                    throughput.record_call(call_result.gas_used);
584
585                    // Determine if test can continue or should exit.
586                    // Check invariants based on check_interval to improve deep run performance.
587                    // - check_interval=0: only assert on the last call
588                    // - check_interval=1 (default): assert after every call
589                    // - check_interval=N: assert every N calls AND always on the last call
590                    let is_last_call = current_run.depth == self.config.depth - 1;
591                    // In optimization mode, always evaluate the invariant to track
592                    // the best value at every prefix — check_interval only gates
593                    // boolean invariant assertions.
594                    let is_optimization = invariant_contract.is_optimization();
595                    let should_check_invariant = is_optimization
596                        || if self.config.check_interval == 0 {
597                            is_last_call
598                        } else {
599                            self.config.check_interval == 1
600                                || (current_run.depth + 1)
601                                    .is_multiple_of(self.config.check_interval)
602                                || is_last_call
603                        };
604
605                    let result = if should_check_invariant {
606                        can_continue(
607                            &invariant_contract,
608                            &mut invariant_test,
609                            &mut current_run,
610                            &self.config,
611                            call_result,
612                            &state_changeset,
613                        )
614                        .map_err(|e| eyre!(e.to_string()))?
615                    } else {
616                        // Skip invariant check but still track reverts
617                        if call_result.reverted {
618                            invariant_test.test_data.failures.reverts += 1;
619                        }
620                        if assertion_failure || (call_result.reverted && self.config.fail_on_revert)
621                        {
622                            let case_data = error::FailedInvariantCaseData::new(
623                                &invariant_contract,
624                                &self.config,
625                                &invariant_test.targeted_contracts,
626                                &current_run.inputs,
627                                call_result,
628                                &[],
629                            )
630                            .with_assertion_failure(assertion_failure);
631                            invariant_test.test_data.failures.revert_reason =
632                                Some(case_data.revert_reason.clone());
633                            invariant_test.test_data.failures.error = Some(if assertion_failure {
634                                InvariantFuzzError::BrokenInvariant(case_data)
635                            } else {
636                                InvariantFuzzError::Revert(case_data)
637                            });
638                            result::RichInvariantResults::new(false, None)
639                        } else if call_result.reverted
640                            && !invariant_contract.is_optimization()
641                            && !self.config.has_delay()
642                        {
643                            // Delay-enabled campaigns keep reverted calls so shrinking can
644                            // preserve their warp/roll contribution when building the final
645                            // counterexample.
646                            current_run.inputs.pop();
647                            result::RichInvariantResults::new(true, None)
648                        } else {
649                            result::RichInvariantResults::new(true, None)
650                        }
651                    };
652
653                    if !result.can_continue || current_run.depth == self.config.depth - 1 {
654                        invariant_test.set_last_run_inputs(&current_run.inputs);
655                    }
656                    // If test cannot continue then stop current run and exit test suite.
657                    if !result.can_continue {
658                        let reason = invariant_test
659                            .test_data
660                            .failures
661                            .error
662                            .as_ref()
663                            .and_then(|e| e.revert_reason())
664                            .unwrap_or_default();
665                        failure_metrics.record_failure(
666                            &invariant_contract.invariant_function.name,
667                            invariant_contract.name,
668                            &reason,
669                        );
670                        break 'stop;
671                    }
672
673                    invariant_test.set_last_call_results(result.call_result);
674                    current_run.depth += 1;
675                }
676
677                current_run.inputs.push(corpus_manager.generate_next_input(
678                    &mut invariant_test.test_data.branch_runner,
679                    &initial_seq,
680                    discarded,
681                    current_run.depth as usize,
682                )?);
683            }
684
685            // Extend corpus with current run data.
686            // Materialize the optimization best prefix once at run end (avoids
687            // cloning inputs on every new in-run max).
688            let optimization = current_run.optimization_value.map(|v| {
689                let prefix = current_run.inputs[..current_run.optimization_prefix_len].to_vec();
690                (v, prefix)
691            });
692            corpus_manager.process_inputs(
693                &current_run.inputs,
694                current_run.new_coverage,
695                optimization,
696            );
697
698            // Call `afterInvariant` only if it is declared and test didn't fail already.
699            if invariant_contract.call_after_invariant && !invariant_test.has_errors() {
700                let success = assert_after_invariant(
701                    &invariant_contract,
702                    &mut invariant_test,
703                    &current_run,
704                    &self.config,
705                )
706                .map_err(|_| eyre!("Failed to call afterInvariant"))?;
707                if !success {
708                    let reason = invariant_test
709                        .test_data
710                        .failures
711                        .error
712                        .as_ref()
713                        .and_then(|e| e.revert_reason())
714                        .unwrap_or_default();
715                    failure_metrics.record_failure(
716                        &invariant_contract.invariant_function.name,
717                        invariant_contract.name,
718                        &reason,
719                    );
720                }
721            }
722
723            // End current invariant test run.
724            invariant_test.end_run(current_run, self.config.gas_report_samples as usize);
725            if let Some(progress) = progress {
726                // If running with progress then increment completed runs.
727                progress.inc(1);
728                // Display current best value and/or corpus metrics in progress bar.
729                let best = invariant_test.test_data.optimization_best_value;
730                if edge_coverage_enabled || best.is_some() {
731                    let mut msg = String::new();
732                    if let Some(best) = best {
733                        msg.push_str(&format!("best: {best}"));
734                    }
735                    if edge_coverage_enabled {
736                        if !msg.is_empty() {
737                            msg.push_str(", ");
738                        }
739                        msg.push_str(&format!("{}", &corpus_manager.metrics));
740                    }
741                    progress.set_message(msg);
742                }
743            } else if edge_coverage_enabled
744                && last_metrics_report.elapsed() > DURATION_BETWEEN_METRICS_REPORT
745            {
746                // Display corpus metrics inline as JSON.
747                let metrics = build_invariant_progress_json(
748                    SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
749                    &invariant_contract.invariant_function.name,
750                    &corpus_manager.metrics,
751                    invariant_test.test_data.optimization_best_value,
752                    throughput,
753                    &failure_metrics,
754                    campaign_start.elapsed(),
755                );
756                let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
757                last_metrics_report = Instant::now();
758            }
759
760            runs += 1;
761        }
762
763        trace!(?fuzz_fixtures);
764        invariant_test.fuzz_state.log_stats();
765
766        let result = invariant_test.test_data;
767        Ok(InvariantFuzzTestResult {
768            error: result.failures.error,
769            cases: result.fuzz_cases,
770            reverts: result.failures.reverts,
771            last_run_inputs: result.last_run_inputs,
772            gas_report_traces: result.gas_report_traces,
773            line_coverage: result.line_coverage,
774            metrics: result.metrics,
775            failed_corpus_replays: corpus_manager.failed_replays,
776            optimization_best_value: result.optimization_best_value,
777            optimization_best_sequence: result.optimization_best_sequence,
778        })
779    }
780
781    /// Prepares certain structures to execute the invariant tests:
782    /// * Invariant Fuzz Test.
783    /// * Invariant Corpus Manager.
784    fn prepare_test(
785        &mut self,
786        invariant_contract: &InvariantContract<'_>,
787        fuzz_fixtures: &FuzzFixtures,
788        fuzz_state: EvmFuzzState,
789    ) -> Result<(InvariantTest<FEN>, WorkerCorpus)> {
790        // Finds out the chosen deployed contracts and/or senders.
791        self.select_contract_artifacts(invariant_contract.address)?;
792        let (targeted_senders, targeted_contracts) =
793            self.select_contracts_and_senders(invariant_contract.address)?;
794
795        // Creates the invariant strategy.
796        let strategy = invariant_strat(
797            fuzz_state.clone(),
798            targeted_senders,
799            targeted_contracts.clone(),
800            self.config.clone(),
801            fuzz_fixtures.clone(),
802        )
803        .no_shrink();
804
805        // If any of the targeted contracts have the storage layout enabled then we can sample
806        // mapping values. To accomplish, we need to record the mapping storage slots and keys.
807        let fuzz_state =
808            if targeted_contracts.targets.lock().iter().any(|(_, t)| t.storage_layout.is_some()) {
809                fuzz_state.with_mapping_slots(AddressMap::default())
810            } else {
811                fuzz_state
812            };
813
814        // Set up fuzzer WITHOUT call_generator initially.
815        // We defer call_override until after the initial invariant check to avoid
816        // injecting random calls during setup which would break the invariant assertion.
817        self.executor.inspector_mut().set_fuzzer(Fuzzer {
818            call_generator: None,
819            fuzz_state: fuzz_state.clone(),
820            collect: true,
821        });
822
823        // Let's make sure the invariant is sound before actually starting the run:
824        // We'll assert the invariant in its initial state, and if it fails, we'll
825        // already know if we can early exit the invariant run.
826        // This does not count as a fuzz run. It will just register the revert.
827        let mut failures = InvariantFailures::new();
828        let last_call_results = assert_invariants(
829            invariant_contract,
830            &self.config,
831            &targeted_contracts,
832            &self.executor,
833            &[],
834            &mut failures,
835        )?;
836        if let Some(error) = failures.error {
837            return Err(eyre!(error.revert_reason().unwrap_or_default()));
838        }
839
840        // NOW enable call_override after the initial invariant check has passed.
841        // This allows `override_call_strat` to inject calls during actual fuzz runs
842        // for reentrancy vulnerability detection.
843        if self.config.call_override {
844            let target_contract_ref = Arc::new(RwLock::new(Address::ZERO));
845
846            // Collect handler addresses - these are the contracts we want to inject
847            // reentrancy into (simulating malicious receive() functions).
848            let handler_addresses: std::collections::HashSet<Address> =
849                targeted_contracts.targets.lock().keys().copied().collect();
850
851            let call_generator = RandomCallGenerator::new(
852                invariant_contract.address,
853                handler_addresses,
854                self.runner.clone(),
855                override_call_strat(
856                    fuzz_state.clone(),
857                    targeted_contracts.clone(),
858                    target_contract_ref.clone(),
859                    fuzz_fixtures.clone(),
860                ),
861                target_contract_ref,
862            );
863
864            if let Some(fuzzer) = self.executor.inspector_mut().fuzzer.as_mut() {
865                fuzzer.call_generator = Some(call_generator);
866            }
867        }
868
869        let worker = WorkerCorpus::new(
870            0,
871            self.config.corpus.clone(),
872            strategy.boxed(),
873            Some(&self.executor),
874            None,
875            Some(&targeted_contracts),
876        )?;
877
878        let mut invariant_test = InvariantTest::new(
879            fuzz_state,
880            targeted_contracts,
881            failures,
882            last_call_results,
883            self.runner.clone(),
884        );
885
886        // Seed invariant test with previously persisted optimization state,
887        // but only if the current invariant is in optimization mode.
888        if invariant_contract.is_optimization() {
889            let (opt_best_value, opt_best_sequence) = worker.optimization_initial_state();
890            invariant_test.test_data.optimization_best_value = opt_best_value;
891            invariant_test.test_data.optimization_best_sequence = opt_best_sequence;
892        }
893
894        Ok((invariant_test, worker))
895    }
896
897    /// Fills the `InvariantExecutor` with the artifact identifier filters (in `path:name` string
898    /// format). They will be used to filter contracts after the `setUp`, and more importantly,
899    /// during the runs.
900    ///
901    /// Also excludes any contract without any mutable functions.
902    ///
903    /// Priority:
904    ///
905    /// targetArtifactSelectors > excludeArtifacts > targetArtifacts
906    pub fn select_contract_artifacts(&mut self, invariant_address: Address) -> Result<()> {
907        let targeted_artifact_selectors = self
908            .executor
909            .call_sol_default(invariant_address, &IInvariantTest::targetArtifactSelectorsCall {});
910
911        // Insert them into the executor `targeted_abi`.
912        for IInvariantTest::FuzzArtifactSelector { artifact, selectors } in
913            targeted_artifact_selectors
914        {
915            let identifier = self.validate_selected_contract(artifact, &selectors)?;
916            self.artifact_filters.targeted.entry(identifier).or_default().extend(selectors);
917        }
918
919        let targeted_artifacts = self
920            .executor
921            .call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {});
922        let excluded_artifacts = self
923            .executor
924            .call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {});
925
926        // Insert `excludeArtifacts` into the executor `excluded_abi`.
927        for contract in excluded_artifacts {
928            let identifier = self.validate_selected_contract(contract, &[])?;
929
930            if !self.artifact_filters.excluded.contains(&identifier) {
931                self.artifact_filters.excluded.push(identifier);
932            }
933        }
934
935        // Exclude any artifact without mutable functions.
936        for (artifact, contract) in self.project_contracts.iter() {
937            if contract
938                .abi
939                .functions()
940                .filter(|func| {
941                    !matches!(
942                        func.state_mutability,
943                        alloy_json_abi::StateMutability::Pure
944                            | alloy_json_abi::StateMutability::View
945                    )
946                })
947                .count()
948                == 0
949                && !self.artifact_filters.excluded.contains(&artifact.identifier())
950            {
951                self.artifact_filters.excluded.push(artifact.identifier());
952            }
953        }
954
955        // Insert `targetArtifacts` into the executor `targeted_abi`, if they have not been seen
956        // before.
957        for contract in targeted_artifacts {
958            let identifier = self.validate_selected_contract(contract, &[])?;
959
960            if !self.artifact_filters.targeted.contains_key(&identifier)
961                && !self.artifact_filters.excluded.contains(&identifier)
962            {
963                self.artifact_filters.targeted.insert(identifier, vec![]);
964            }
965        }
966        Ok(())
967    }
968
969    /// Makes sure that the contract exists in the project. If so, it returns its artifact
970    /// identifier.
971    fn validate_selected_contract(
972        &mut self,
973        contract: String,
974        selectors: &[FixedBytes<4>],
975    ) -> Result<String> {
976        if let Some((artifact, contract_data)) =
977            self.project_contracts.find_by_name_or_identifier(&contract)?
978        {
979            // Check that the selectors really exist for this contract.
980            for selector in selectors {
981                contract_data
982                    .abi
983                    .functions()
984                    .find(|func| func.selector().as_slice() == selector.as_slice())
985                    .wrap_err(format!("{contract} does not have the selector {selector:?}"))?;
986            }
987
988            return Ok(artifact.identifier());
989        }
990        eyre::bail!(
991            "{contract} not found in the project. Allowed format: `contract_name` or `contract_path:contract_name`."
992        );
993    }
994
995    /// Selects senders and contracts based on the contract methods `targetSenders() -> address[]`,
996    /// `targetContracts() -> address[]` and `excludeContracts() -> address[]`.
997    pub fn select_contracts_and_senders(
998        &self,
999        to: Address,
1000    ) -> Result<(SenderFilters, FuzzRunIdentifiedContracts)> {
1001        let targeted_senders =
1002            self.executor.call_sol_default(to, &IInvariantTest::targetSendersCall {});
1003        let mut excluded_senders =
1004            self.executor.call_sol_default(to, &IInvariantTest::excludeSendersCall {});
1005        // Extend with default excluded addresses - https://github.com/foundry-rs/foundry/issues/4163
1006        excluded_senders.extend([
1007            CHEATCODE_ADDRESS,
1008            HARDHAT_CONSOLE_ADDRESS,
1009            DEFAULT_CREATE2_DEPLOYER,
1010        ]);
1011        // Extend with precompiles - https://github.com/foundry-rs/foundry/issues/4287
1012        excluded_senders.extend(PRECOMPILES);
1013        let sender_filters = SenderFilters::new(targeted_senders, excluded_senders);
1014
1015        let selected = self.executor.call_sol_default(to, &IInvariantTest::targetContractsCall {});
1016        let excluded = self.executor.call_sol_default(to, &IInvariantTest::excludeContractsCall {});
1017
1018        let contracts = self
1019            .setup_contracts
1020            .iter()
1021            .filter(|&(addr, (identifier, _))| {
1022                // Include to address if explicitly set as target.
1023                if *addr == to && selected.contains(&to) {
1024                    return true;
1025                }
1026
1027                *addr != to
1028                    && *addr != CHEATCODE_ADDRESS
1029                    && *addr != HARDHAT_CONSOLE_ADDRESS
1030                    && (selected.is_empty() || selected.contains(addr))
1031                    && (excluded.is_empty() || !excluded.contains(addr))
1032                    && self.artifact_filters.matches(identifier)
1033            })
1034            .map(|(addr, (identifier, abi))| {
1035                (
1036                    *addr,
1037                    TargetedContract::new(identifier.clone(), abi.clone())
1038                        .with_project_contracts(self.project_contracts),
1039                )
1040            })
1041            .collect();
1042        let mut contracts = TargetedContracts { inner: contracts };
1043
1044        self.target_interfaces(to, &mut contracts)?;
1045
1046        self.select_selectors(to, &mut contracts)?;
1047
1048        // There should be at least one contract identified as target for fuzz runs.
1049        if contracts.is_empty() {
1050            eyre::bail!("No contracts to fuzz.");
1051        }
1052
1053        Ok((sender_filters, FuzzRunIdentifiedContracts::new(contracts, selected.is_empty())))
1054    }
1055
1056    /// Extends the contracts and selectors to fuzz with the addresses and ABIs specified in
1057    /// `targetInterfaces() -> (address, string[])[]`. Enables targeting of addresses that are
1058    /// not deployed during `setUp` such as when fuzzing in a forked environment. Also enables
1059    /// targeting of delegate proxies and contracts deployed with `create` or `create2`.
1060    pub fn target_interfaces(
1061        &self,
1062        invariant_address: Address,
1063        targeted_contracts: &mut TargetedContracts,
1064    ) -> Result<()> {
1065        let interfaces = self
1066            .executor
1067            .call_sol_default(invariant_address, &IInvariantTest::targetInterfacesCall {});
1068
1069        // Since `targetInterfaces` returns a tuple array there is no guarantee
1070        // that the addresses are unique this map is used to merge functions of
1071        // the specified interfaces for the same address. For example:
1072        // `[(addr1, ["IERC20", "IOwnable"])]` and `[(addr1, ["IERC20"]), (addr1, ("IOwnable"))]`
1073        // should be equivalent.
1074        let mut combined = TargetedContracts::new();
1075
1076        // Loop through each address and its associated artifact identifiers.
1077        // We're borrowing here to avoid taking full ownership.
1078        for IInvariantTest::FuzzInterface { addr, artifacts } in &interfaces {
1079            // Identifiers are specified as an array, so we loop through them.
1080            for identifier in artifacts {
1081                // Try to find the contract by name or identifier in the project's contracts.
1082                if let Some((_, contract_data)) =
1083                    self.project_contracts.iter().find(|(artifact, _)| {
1084                        &artifact.name == identifier || &artifact.identifier() == identifier
1085                    })
1086                {
1087                    let abi = &contract_data.abi;
1088                    combined
1089                        // Check if there's an entry for the given key in the 'combined' map.
1090                        .entry(*addr)
1091                        // If the entry exists, extends its ABI with the function list.
1092                        .and_modify(|entry| {
1093                            // Extend the ABI's function list with the new functions.
1094                            entry.abi.functions.extend(abi.functions.clone());
1095                        })
1096                        // Otherwise insert it into the map.
1097                        .or_insert_with(|| {
1098                            let mut contract =
1099                                TargetedContract::new(identifier.clone(), abi.clone());
1100                            contract.storage_layout =
1101                                contract_data.storage_layout.as_ref().map(Arc::clone);
1102                            contract
1103                        });
1104                }
1105            }
1106        }
1107
1108        targeted_contracts.extend(combined.inner);
1109
1110        Ok(())
1111    }
1112
1113    /// Selects the functions to fuzz based on the contract method `targetSelectors()` and
1114    /// `targetArtifactSelectors()`.
1115    pub fn select_selectors(
1116        &self,
1117        address: Address,
1118        targeted_contracts: &mut TargetedContracts,
1119    ) -> Result<()> {
1120        for (address, (identifier, _)) in self.setup_contracts {
1121            if let Some(selectors) = self.artifact_filters.targeted.get(identifier) {
1122                self.add_address_with_functions(*address, selectors, false, targeted_contracts)?;
1123            }
1124        }
1125
1126        let mut target_test_selectors = vec![];
1127        let mut excluded_test_selectors = vec![];
1128
1129        // Collect contract functions marked as target for fuzzing campaign.
1130        let selectors =
1131            self.executor.call_sol_default(address, &IInvariantTest::targetSelectorsCall {});
1132        for IInvariantTest::FuzzSelector { addr, selectors } in selectors {
1133            if addr == address {
1134                target_test_selectors = selectors.clone();
1135            }
1136            self.add_address_with_functions(addr, &selectors, false, targeted_contracts)?;
1137        }
1138
1139        // Collect contract functions excluded from fuzzing campaign.
1140        let excluded_selectors =
1141            self.executor.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {});
1142        for IInvariantTest::FuzzSelector { addr, selectors } in excluded_selectors {
1143            if addr == address {
1144                // If fuzz selector address is the test contract, then record selectors to be
1145                // later excluded if needed.
1146                excluded_test_selectors = selectors.clone();
1147            }
1148            self.add_address_with_functions(addr, &selectors, true, targeted_contracts)?;
1149        }
1150
1151        if target_test_selectors.is_empty()
1152            && let Some(target) = targeted_contracts.get(&address)
1153        {
1154            // If test contract is marked as a target and no target selector explicitly set, then
1155            // include only state-changing functions that are not reserved and selectors that are
1156            // not explicitly excluded.
1157            let selectors: Vec<_> = target
1158                .abi
1159                .functions()
1160                .filter_map(|func| {
1161                    if matches!(
1162                        func.state_mutability,
1163                        alloy_json_abi::StateMutability::Pure
1164                            | alloy_json_abi::StateMutability::View
1165                    ) || func.is_reserved()
1166                        || excluded_test_selectors.contains(&func.selector())
1167                    {
1168                        None
1169                    } else {
1170                        Some(func.selector())
1171                    }
1172                })
1173                .collect();
1174            self.add_address_with_functions(address, &selectors, false, targeted_contracts)?;
1175        }
1176
1177        Ok(())
1178    }
1179
1180    /// Adds the address and fuzzed or excluded functions to `TargetedContracts`.
1181    fn add_address_with_functions(
1182        &self,
1183        address: Address,
1184        selectors: &[Selector],
1185        should_exclude: bool,
1186        targeted_contracts: &mut TargetedContracts,
1187    ) -> eyre::Result<()> {
1188        // Do not add address in target contracts if no function selected.
1189        if selectors.is_empty() {
1190            return Ok(());
1191        }
1192
1193        let contract = match targeted_contracts.entry(address) {
1194            Entry::Occupied(entry) => entry.into_mut(),
1195            Entry::Vacant(entry) => {
1196                let (identifier, abi) = self.setup_contracts.get(&address).ok_or_else(|| {
1197                    eyre::eyre!(
1198                        "[{}] address does not have an associated contract: {}",
1199                        if should_exclude { "excludeSelectors" } else { "targetSelectors" },
1200                        address
1201                    )
1202                })?;
1203                entry.insert(
1204                    TargetedContract::new(identifier.clone(), abi.clone())
1205                        .with_project_contracts(self.project_contracts),
1206                )
1207            }
1208        };
1209        contract.add_selectors(selectors.iter().copied(), should_exclude)?;
1210        Ok(())
1211    }
1212
1213    /// Computes the current invariant settings for the given invariant contract address.
1214    ///
1215    /// This extracts the target contracts, selectors, senders, and failure settings
1216    /// that are used to determine if a persisted counterexample is still valid.
1217    pub fn compute_settings(&mut self, invariant_address: Address) -> Result<InvariantSettings> {
1218        self.select_contract_artifacts(invariant_address)?;
1219        let (sender_filters, targeted_contracts) =
1220            self.select_contracts_and_senders(invariant_address)?;
1221        let targets = targeted_contracts.targets.lock();
1222        Ok(InvariantSettings::new(&targets, &sender_filters, self.config.fail_on_revert))
1223    }
1224}
1225
1226/// Collects data from call for fuzzing. However, it first verifies that the sender is not an EOA
1227/// before inserting it into the dictionary. Otherwise, we flood the dictionary with
1228/// randomly generated addresses.
1229fn collect_data<FEN: FoundryEvmNetwork>(
1230    invariant_test: &InvariantTest<FEN>,
1231    state_changeset: &mut AddressMap<Account>,
1232    tx: &BasicTxDetails,
1233    call_result: &RawCallResult<FEN>,
1234    run_depth: u32,
1235) {
1236    // Verify it has no code.
1237    let has_code = if let Some(Some(code)) =
1238        state_changeset.get(&tx.sender).map(|account| account.info.code.as_ref())
1239    {
1240        !code.is_empty()
1241    } else {
1242        false
1243    };
1244
1245    // We keep the nonce changes to apply later.
1246    let sender_changeset = if has_code { None } else { state_changeset.remove(&tx.sender) };
1247
1248    // Collect values from fuzzed call result and add them to fuzz dictionary.
1249    invariant_test.fuzz_state.collect_values_from_call(
1250        &invariant_test.targeted_contracts,
1251        tx,
1252        &call_result.result,
1253        &call_result.logs,
1254        &*state_changeset,
1255        run_depth,
1256    );
1257
1258    // Inject typed sancov trace-cmp operands into the fuzz dictionary.
1259    if let Some(cmp_values) = &call_result.sancov_cmp_values {
1260        invariant_test.fuzz_state.collect_typed_cmp_values(
1261            cmp_values.iter().map(|s| (s.width, alloy_primitives::B256::from(s.value))),
1262        );
1263    }
1264
1265    // Re-add changes
1266    if let Some(changed) = sender_changeset {
1267        state_changeset.insert(tx.sender, changed);
1268    }
1269}
1270
1271/// Calls the `afterInvariant()` function on a contract.
1272/// Returns call result and if call succeeded.
1273/// The state after the call is not persisted.
1274pub(crate) fn call_after_invariant_function<FEN: FoundryEvmNetwork>(
1275    executor: &Executor<FEN>,
1276    to: Address,
1277) -> Result<(RawCallResult<FEN>, bool), EvmError<FEN>> {
1278    let calldata = Bytes::from_static(&IInvariantTest::afterInvariantCall::SELECTOR);
1279    let mut call_result = executor.call_raw(CALLER, to, calldata, U256::ZERO)?;
1280    let success = executor.is_raw_call_mut_success(to, &mut call_result, false);
1281    Ok((call_result, success))
1282}
1283
1284/// Calls the invariant function and returns call result and if succeeded.
1285pub(crate) fn call_invariant_function<FEN: FoundryEvmNetwork>(
1286    executor: &Executor<FEN>,
1287    address: Address,
1288    calldata: Bytes,
1289) -> Result<(RawCallResult<FEN>, bool)> {
1290    let mut call_result = executor.call_raw(CALLER, address, calldata, U256::ZERO)?;
1291    let success = executor.is_raw_call_mut_success(address, &mut call_result, false);
1292    Ok((call_result, success))
1293}
1294
1295/// Executes a fuzz call and returns the result.
1296/// Applies any block timestamp (warp) and block number (roll) adjustments before the call.
1297pub(crate) fn execute_tx<FEN: FoundryEvmNetwork>(
1298    executor: &mut Executor<FEN>,
1299    tx: &BasicTxDetails,
1300) -> Result<RawCallResult<FEN>> {
1301    let warp = tx.warp.unwrap_or_default();
1302    let roll = tx.roll.unwrap_or_default();
1303
1304    if warp > 0 || roll > 0 {
1305        // Apply pre-call block adjustments to the executor's env.
1306        let ts = executor.evm_env().block_env.timestamp();
1307        let num = executor.evm_env().block_env.number();
1308        executor.evm_env_mut().block_env.set_timestamp(ts + warp);
1309        executor.evm_env_mut().block_env.set_number(num + roll);
1310
1311        // Also update the inspector's cheatcodes.block if set.
1312        // The inspector's block may override the env during interpreter initialization,
1313        // so we need to add our warp/roll on top of any existing cheatcode-set values.
1314        let block_env = executor.evm_env().block_env.clone();
1315        if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
1316            if let Some(block) = cheatcodes.block.as_mut() {
1317                let bts = block.timestamp();
1318                let bnum = block.number();
1319                block.set_timestamp(bts + warp);
1320                block.set_number(bnum + roll);
1321            } else {
1322                cheatcodes.block = Some(block_env);
1323            }
1324        }
1325    }
1326
1327    executor
1328        .call_raw(tx.sender, tx.call_details.target, tx.call_details.calldata.clone(), U256::ZERO)
1329        .map_err(|e| eyre!(format!("Could not make raw evm call: {e}")))
1330}
1331
1332#[cfg(test)]
1333mod tests {
1334    use super::*;
1335    use serde_json::json;
1336
1337    #[test]
1338    fn invariant_progress_json_includes_throughput_fields() {
1339        let mut throughput = InvariantThroughputMetrics::default();
1340        throughput.record_call(20);
1341        throughput.record_call(30);
1342
1343        let payload = build_invariant_progress_json(
1344            123,
1345            "invariant_balance",
1346            &json!({ "corpus_count": 7 }),
1347            Some(I256::try_from(42).unwrap()),
1348            throughput,
1349            &InvariantFailureMetrics::default(),
1350            Duration::from_secs(10),
1351        );
1352
1353        assert_eq!(payload["timestamp"], json!(123));
1354        assert_eq!(payload["invariant"], json!("invariant_balance"));
1355        assert_eq!(payload["metrics"]["corpus_count"], json!(7));
1356        assert_eq!(payload["total_txs"], json!(2));
1357        assert_eq!(payload["total_gas"], json!(50));
1358        assert!((payload["tx_per_sec"].as_f64().unwrap() - 0.2).abs() < 1e-12);
1359        assert!((payload["gas_per_sec"].as_f64().unwrap() - 5.0).abs() < 1e-12);
1360        assert_eq!(payload["optimization_best"], json!("42"));
1361    }
1362
1363    #[test]
1364    fn invariant_progress_json_zero_elapsed_reports_zero_rates() {
1365        let mut throughput = InvariantThroughputMetrics::default();
1366        throughput.record_call(21_000);
1367
1368        let payload = build_invariant_progress_json(
1369            456,
1370            "invariant_zero_elapsed",
1371            &json!({ "corpus_count": 1 }),
1372            None,
1373            throughput,
1374            &InvariantFailureMetrics::default(),
1375            Duration::ZERO,
1376        );
1377
1378        assert_eq!(payload["tx_per_sec"], json!(0.0));
1379        assert_eq!(payload["gas_per_sec"], json!(0.0));
1380        assert!(payload.get("optimization_best").is_none());
1381    }
1382
1383    #[test]
1384    fn invariant_progress_json_includes_failure_counts() {
1385        let mut failure_metrics = InvariantFailureMetrics::default();
1386        failure_metrics.record_failure("invariant_a", "TestContract", "revert");
1387        failure_metrics.record_failure("invariant_a", "TestContract", "revert");
1388        failure_metrics.record_failure("invariant_b", "TestContract", "assertion failed");
1389
1390        let payload = build_invariant_progress_json(
1391            789,
1392            "invariant_a",
1393            &json!({ "corpus_count": 5 }),
1394            None,
1395            InvariantThroughputMetrics::default(),
1396            &failure_metrics,
1397            Duration::from_secs(1),
1398        );
1399
1400        assert_eq!(payload["metrics"]["failures"], json!(3));
1401        assert_eq!(payload["metrics"]["unique_failures"], json!(2));
1402    }
1403
1404    #[test]
1405    fn failure_metrics_tracks_total_and_unique_failures() {
1406        let mut metrics = InvariantFailureMetrics::default();
1407        metrics.record_failure("invariant_a", "TestContract", "revert");
1408        metrics.record_failure("invariant_a", "TestContract", "revert");
1409        metrics.record_failure("invariant_b", "TestContract", "assertion failed");
1410
1411        assert_eq!(metrics.failures, 3);
1412        assert_eq!(metrics.unique_failures.len(), 2);
1413        assert!(metrics.unique_failures.contains("invariant_a"));
1414        assert!(metrics.unique_failures.contains("invariant_b"));
1415    }
1416
1417    #[test]
1418    fn failure_metrics_default_is_zero() {
1419        let metrics = InvariantFailureMetrics::default();
1420        assert_eq!(metrics.failures, 0);
1421        assert!(metrics.unique_failures.is_empty());
1422    }
1423}