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_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};
38use revm::{context::Block, state::Account};
39use serde::{Deserialize, Serialize};
40use serde_json::json;
41use std::{
42    collections::{HashMap as Map, btree_map::Entry},
43    sync::Arc,
44    time::{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/// Contains data collected during invariant test runs.
126struct InvariantTestData<FEN: FoundryEvmNetwork> {
127    // Consumed gas and calldata of every successful fuzz call.
128    fuzz_cases: Vec<FuzzedCases>,
129    // Data related to reverts or failed assertions of the test.
130    failures: InvariantFailures,
131    // Calldata in the last invariant run.
132    last_run_inputs: Vec<BasicTxDetails>,
133    // Additional traces for gas report.
134    gas_report_traces: Vec<Vec<CallTraceArena>>,
135    // Last call results of the invariant test.
136    last_call_results: Option<RawCallResult<FEN>>,
137    // Line coverage information collected from all fuzzed calls.
138    line_coverage: Option<HitMaps>,
139    // Metrics for each fuzzed selector.
140    metrics: Map<String, InvariantMetrics>,
141
142    // Proptest runner to query for random values.
143    // The strategy only comes with the first `input`. We fill the rest of the `inputs`
144    // until the desired `depth` so we can use the evolving fuzz dictionary
145    // during the run.
146    branch_runner: TestRunner,
147
148    // Optimization mode state: tracks the best (maximum) value and the sequence that produced it.
149    // Only used when invariant function returns int256.
150    optimization_best_value: Option<I256>,
151    optimization_best_sequence: Vec<BasicTxDetails>,
152}
153
154/// Contains invariant test data.
155struct InvariantTest<FEN: FoundryEvmNetwork> {
156    // Fuzz state of invariant test.
157    fuzz_state: EvmFuzzState,
158    // Contracts fuzzed by the invariant test.
159    targeted_contracts: FuzzRunIdentifiedContracts,
160    // Data collected during invariant runs.
161    test_data: InvariantTestData<FEN>,
162}
163
164impl<FEN: FoundryEvmNetwork> InvariantTest<FEN> {
165    /// Instantiates an invariant test.
166    fn new(
167        fuzz_state: EvmFuzzState,
168        targeted_contracts: FuzzRunIdentifiedContracts,
169        failures: InvariantFailures,
170        last_call_results: Option<RawCallResult<FEN>>,
171        branch_runner: TestRunner,
172    ) -> Self {
173        let mut fuzz_cases = vec![];
174        if last_call_results.is_none() {
175            fuzz_cases.push(FuzzedCases::new(vec![]));
176        }
177        let test_data = InvariantTestData {
178            fuzz_cases,
179            failures,
180            last_run_inputs: vec![],
181            gas_report_traces: vec![],
182            last_call_results,
183            line_coverage: None,
184            metrics: Map::default(),
185            branch_runner,
186            optimization_best_value: None,
187            optimization_best_sequence: vec![],
188        };
189        Self { fuzz_state, targeted_contracts, test_data }
190    }
191
192    /// Returns number of invariant test reverts.
193    fn reverts(&self) -> usize {
194        self.test_data.failures.reverts
195    }
196
197    /// Whether invariant test has errors or not.
198    fn has_errors(&self) -> bool {
199        self.test_data.failures.error.is_some()
200    }
201
202    /// Set invariant test error.
203    fn set_error(&mut self, error: InvariantFuzzError) {
204        self.test_data.failures.error = Some(error);
205    }
206
207    /// Set last invariant test call results.
208    fn set_last_call_results(&mut self, call_result: Option<RawCallResult<FEN>>) {
209        self.test_data.last_call_results = call_result;
210    }
211
212    /// Set last invariant run call sequence.
213    fn set_last_run_inputs(&mut self, inputs: &Vec<BasicTxDetails>) {
214        self.test_data.last_run_inputs.clone_from(inputs);
215    }
216
217    /// Merge current collected line coverage with the new coverage from last fuzzed call.
218    fn merge_line_coverage(&mut self, new_coverage: Option<HitMaps>) {
219        HitMaps::merge_opt(&mut self.test_data.line_coverage, new_coverage);
220    }
221
222    /// Update metrics for a fuzzed selector, extracted from tx details.
223    /// Always increments number of calls; discarded runs (through assume cheatcodes) are tracked
224    /// separated from reverts.
225    fn record_metrics(&mut self, tx_details: &BasicTxDetails, reverted: bool, discarded: bool) {
226        if let Some(metric_key) =
227            self.targeted_contracts.targets.lock().fuzzed_metric_key(tx_details)
228        {
229            let test_metrics = &mut self.test_data.metrics;
230            let invariant_metrics = test_metrics.entry(metric_key).or_default();
231            invariant_metrics.calls += 1;
232            if discarded {
233                invariant_metrics.discards += 1;
234            } else if reverted {
235                invariant_metrics.reverts += 1;
236            }
237        }
238    }
239
240    /// End invariant test run by collecting results, cleaning collected artifacts and reverting
241    /// created fuzz state.
242    fn end_run(&mut self, run: InvariantTestRun<FEN>, gas_samples: usize) {
243        // We clear all the targeted contracts created during this run.
244        self.targeted_contracts.clear_created_contracts(run.created_contracts);
245
246        if self.test_data.gas_report_traces.len() < gas_samples {
247            self.test_data
248                .gas_report_traces
249                .push(run.run_traces.into_iter().map(|arena| arena.arena).collect());
250        }
251        self.test_data.fuzz_cases.push(FuzzedCases::new(run.fuzz_runs));
252
253        // Revert state to not persist values between runs.
254        self.fuzz_state.revert();
255    }
256
257    /// Updates the optimization state if the new value is better (higher) than the current best.
258    fn update_optimization_value(&mut self, value: I256, sequence: &[BasicTxDetails]) {
259        if self.test_data.optimization_best_value.is_none_or(|best| value > best) {
260            self.test_data.optimization_best_value = Some(value);
261            self.test_data.optimization_best_sequence = sequence.to_vec();
262        }
263    }
264}
265
266/// Contains data for an invariant test run.
267struct InvariantTestRun<FEN: FoundryEvmNetwork> {
268    // Invariant run call sequence.
269    inputs: Vec<BasicTxDetails>,
270    // Current invariant run executor.
271    executor: Executor<FEN>,
272    // Invariant run stat reports (eg. gas usage).
273    fuzz_runs: Vec<FuzzCase>,
274    // Contracts created during current invariant run.
275    created_contracts: Vec<Address>,
276    // Traces of each call of the invariant run call sequence.
277    run_traces: Vec<SparsedTraceArena>,
278    // Current depth of invariant run.
279    depth: u32,
280    // Current assume rejects of the invariant run.
281    rejects: u32,
282    // Whether new coverage was discovered during this run.
283    new_coverage: bool,
284    // For optimization mode: the best value found during this run (if any).
285    optimization_value: Option<I256>,
286    // For optimization mode: the length of the input prefix that produced the best value.
287    optimization_prefix_len: usize,
288}
289
290impl<FEN: FoundryEvmNetwork> InvariantTestRun<FEN> {
291    /// Instantiates an invariant test run.
292    fn new(first_input: BasicTxDetails, executor: Executor<FEN>, depth: usize) -> Self {
293        Self {
294            inputs: vec![first_input],
295            executor,
296            fuzz_runs: Vec::with_capacity(depth),
297            created_contracts: vec![],
298            run_traces: vec![],
299            depth: 0,
300            rejects: 0,
301            new_coverage: false,
302            optimization_value: None,
303            optimization_prefix_len: 0,
304        }
305    }
306}
307
308/// Wrapper around any [`Executor`] implementer which provides fuzzing support using [`proptest`].
309///
310/// After instantiation, calling `invariant_fuzz` will proceed to hammer the deployed smart
311/// contracts with inputs, until it finds a counterexample sequence. The provided [`TestRunner`]
312/// contains all the configuration which can be overridden via [environment
313/// variables](proptest::test_runner::Config)
314pub struct InvariantExecutor<'a, FEN: FoundryEvmNetwork> {
315    pub executor: Executor<FEN>,
316    /// Proptest runner.
317    runner: TestRunner,
318    /// The invariant configuration
319    config: InvariantConfig,
320    /// Contracts deployed with `setUp()`
321    setup_contracts: &'a ContractsByAddress,
322    /// Contracts that are part of the project but have not been deployed yet. We need the bytecode
323    /// to identify them from the stateset changes.
324    project_contracts: &'a ContractsByArtifact,
325    /// Filters contracts to be fuzzed through their artifact identifiers.
326    artifact_filters: ArtifactFilters,
327}
328
329impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
330    /// Instantiates a fuzzed executor EVM given a testrunner
331    pub fn new(
332        executor: Executor<FEN>,
333        runner: TestRunner,
334        config: InvariantConfig,
335        setup_contracts: &'a ContractsByAddress,
336        project_contracts: &'a ContractsByArtifact,
337    ) -> Self {
338        Self {
339            executor,
340            runner,
341            config,
342            setup_contracts,
343            project_contracts,
344            artifact_filters: ArtifactFilters::default(),
345        }
346    }
347
348    pub fn config(self) -> InvariantConfig {
349        self.config
350    }
351
352    /// Fuzzes any deployed contract and checks any broken invariant at `invariant_address`.
353    pub fn invariant_fuzz(
354        &mut self,
355        invariant_contract: InvariantContract<'_>,
356        fuzz_fixtures: &FuzzFixtures,
357        fuzz_state: EvmFuzzState,
358        progress: Option<&ProgressBar>,
359        early_exit: &EarlyExit,
360    ) -> Result<InvariantFuzzTestResult> {
361        // Throw an error to abort test run if the invariant function accepts input params
362        if !invariant_contract.invariant_function.inputs.is_empty() {
363            return Err(eyre!("Invariant test function should have no inputs"));
364        }
365
366        let (mut invariant_test, mut corpus_manager) =
367            self.prepare_test(&invariant_contract, fuzz_fixtures, fuzz_state)?;
368
369        // Start timer for this invariant test.
370        let mut runs = 0;
371        let timer = FuzzTestTimer::new(self.config.timeout);
372        let mut last_metrics_report = Instant::now();
373        let continue_campaign = |runs: u32| {
374            if early_exit.should_stop() {
375                return false;
376            }
377
378            if timer.is_enabled() { !timer.is_timed_out() } else { runs < self.config.runs }
379        };
380
381        // Invariant runs with edge coverage if corpus dir is set or showing edge coverage.
382        let edge_coverage_enabled = self.config.corpus.collect_edge_coverage();
383
384        'stop: while continue_campaign(runs) {
385            let initial_seq = corpus_manager.new_inputs(
386                &mut invariant_test.test_data.branch_runner,
387                &invariant_test.fuzz_state,
388                &invariant_test.targeted_contracts,
389            )?;
390
391            // Create current invariant run data.
392            let mut current_run = InvariantTestRun::new(
393                initial_seq[0].clone(),
394                // Before each run, we must reset the backend state.
395                self.executor.clone(),
396                self.config.depth as usize,
397            );
398
399            // We stop the run immediately if we have reverted, and `fail_on_revert` is set.
400            if self.config.fail_on_revert && invariant_test.reverts() > 0 {
401                return Err(eyre!("call reverted"));
402            }
403
404            while current_run.depth < self.config.depth {
405                // Check if the timeout has been reached.
406                if timer.is_timed_out() {
407                    // Since we never record a revert here the test is still considered
408                    // successful even though it timed out. We *want*
409                    // this behavior for now, so that's ok, but
410                    // future developers should be aware of this.
411                    break 'stop;
412                }
413
414                let tx = current_run
415                    .inputs
416                    .last()
417                    .ok_or_else(|| eyre!("no input generated to call fuzzed target."))?;
418
419                // Execute call from the randomly generated sequence without committing state.
420                // State is committed only if call is not a magic assume.
421                let mut call_result = execute_tx(&mut current_run.executor, tx)?;
422                let discarded = call_result.result.as_ref() == MAGIC_ASSUME;
423                if self.config.show_metrics {
424                    invariant_test.record_metrics(tx, call_result.reverted, discarded);
425                }
426
427                // Collect line coverage from last fuzzed call.
428                invariant_test.merge_line_coverage(call_result.line_coverage.clone());
429                // Collect edge coverage and set the flag in the current run.
430                if corpus_manager.merge_edge_coverage(&mut call_result) {
431                    current_run.new_coverage = true;
432                }
433
434                if discarded {
435                    current_run.inputs.pop();
436                    current_run.rejects += 1;
437                    if current_run.rejects > self.config.max_assume_rejects {
438                        invariant_test.set_error(InvariantFuzzError::MaxAssumeRejects(
439                            self.config.max_assume_rejects,
440                        ));
441                        break 'stop;
442                    }
443                } else {
444                    // Commit executed call result.
445                    current_run.executor.commit(&mut call_result);
446
447                    // Collect data for fuzzing from the state changeset.
448                    // This step updates the state dictionary and therefore invalidates the
449                    // ValueTree in use by the current run. This manifestsitself in proptest
450                    // observing a different input case than what it was called with, and creates
451                    // inconsistencies whenever proptest tries to use the input case after test
452                    // execution.
453                    // See <https://github.com/foundry-rs/foundry/issues/9764>.
454                    let mut state_changeset = std::mem::take(&mut call_result.state_changeset);
455                    if !call_result.reverted {
456                        collect_data(
457                            &invariant_test,
458                            &mut state_changeset,
459                            tx,
460                            &call_result,
461                            self.config.depth,
462                        );
463                    }
464
465                    // Collect created contracts and add to fuzz targets only if targeted contracts
466                    // are updatable.
467                    if let Err(error) =
468                        &invariant_test.targeted_contracts.collect_created_contracts(
469                            &state_changeset,
470                            self.project_contracts,
471                            self.setup_contracts,
472                            &self.artifact_filters,
473                            &mut current_run.created_contracts,
474                        )
475                    {
476                        warn!(target: "forge::test", "{error}");
477                    }
478                    current_run
479                        .fuzz_runs
480                        .push(FuzzCase { gas: call_result.gas_used, stipend: call_result.stipend });
481
482                    // Determine if test can continue or should exit.
483                    // Check invariants based on check_interval to improve deep run performance.
484                    // - check_interval=0: only assert on the last call
485                    // - check_interval=1 (default): assert after every call
486                    // - check_interval=N: assert every N calls AND always on the last call
487                    let is_last_call = current_run.depth == self.config.depth - 1;
488                    // In optimization mode, always evaluate the invariant to track
489                    // the best value at every prefix — check_interval only gates
490                    // boolean invariant assertions.
491                    let is_optimization = invariant_contract.is_optimization();
492                    let should_check_invariant = is_optimization
493                        || if self.config.check_interval == 0 {
494                            is_last_call
495                        } else {
496                            self.config.check_interval == 1
497                                || (current_run.depth + 1)
498                                    .is_multiple_of(self.config.check_interval)
499                                || is_last_call
500                        };
501
502                    let result = if should_check_invariant {
503                        can_continue(
504                            &invariant_contract,
505                            &mut invariant_test,
506                            &mut current_run,
507                            &self.config,
508                            call_result,
509                            &state_changeset,
510                        )
511                        .map_err(|e| eyre!(e.to_string()))?
512                    } else {
513                        // Skip invariant check but still track reverts
514                        if call_result.reverted {
515                            invariant_test.test_data.failures.reverts += 1;
516                            if self.config.fail_on_revert {
517                                let case_data = error::FailedInvariantCaseData::new(
518                                    &invariant_contract,
519                                    &self.config,
520                                    &invariant_test.targeted_contracts,
521                                    &current_run.inputs,
522                                    call_result,
523                                    &[],
524                                );
525                                invariant_test.test_data.failures.revert_reason =
526                                    Some(case_data.revert_reason.clone());
527                                invariant_test.test_data.failures.error =
528                                    Some(InvariantFuzzError::Revert(case_data));
529                                result::RichInvariantResults::new(false, None)
530                            } else if !invariant_contract.is_optimization()
531                                && !self.config.has_delay()
532                            {
533                                // Delay-enabled campaigns keep reverted calls so shrinking can
534                                // preserve their warp/roll contribution when building the final
535                                // counterexample.
536                                current_run.inputs.pop();
537                                result::RichInvariantResults::new(true, None)
538                            } else {
539                                result::RichInvariantResults::new(true, None)
540                            }
541                        } else {
542                            result::RichInvariantResults::new(true, None)
543                        }
544                    };
545
546                    if !result.can_continue || current_run.depth == self.config.depth - 1 {
547                        invariant_test.set_last_run_inputs(&current_run.inputs);
548                    }
549                    // If test cannot continue then stop current run and exit test suite.
550                    if !result.can_continue {
551                        break 'stop;
552                    }
553
554                    invariant_test.set_last_call_results(result.call_result);
555                    current_run.depth += 1;
556                }
557
558                current_run.inputs.push(corpus_manager.generate_next_input(
559                    &mut invariant_test.test_data.branch_runner,
560                    &initial_seq,
561                    discarded,
562                    current_run.depth as usize,
563                )?);
564            }
565
566            // Extend corpus with current run data.
567            // Materialize the optimization best prefix once at run end (avoids
568            // cloning inputs on every new in-run max).
569            let optimization = current_run.optimization_value.map(|v| {
570                let prefix = current_run.inputs[..current_run.optimization_prefix_len].to_vec();
571                (v, prefix)
572            });
573            corpus_manager.process_inputs(
574                &current_run.inputs,
575                current_run.new_coverage,
576                optimization,
577            );
578
579            // Call `afterInvariant` only if it is declared and test didn't fail already.
580            if invariant_contract.call_after_invariant && !invariant_test.has_errors() {
581                assert_after_invariant(
582                    &invariant_contract,
583                    &mut invariant_test,
584                    &current_run,
585                    &self.config,
586                )
587                .map_err(|_| eyre!("Failed to call afterInvariant"))?;
588            }
589
590            // End current invariant test run.
591            invariant_test.end_run(current_run, self.config.gas_report_samples as usize);
592            if let Some(progress) = progress {
593                // If running with progress then increment completed runs.
594                progress.inc(1);
595                // Display current best value and/or corpus metrics in progress bar.
596                let best = invariant_test.test_data.optimization_best_value;
597                if edge_coverage_enabled || best.is_some() {
598                    let mut msg = String::new();
599                    if let Some(best) = best {
600                        msg.push_str(&format!("best: {best}"));
601                    }
602                    if edge_coverage_enabled {
603                        if !msg.is_empty() {
604                            msg.push_str(", ");
605                        }
606                        msg.push_str(&format!("{}", &corpus_manager.metrics));
607                    }
608                    progress.set_message(msg);
609                }
610            } else if edge_coverage_enabled
611                && last_metrics_report.elapsed() > DURATION_BETWEEN_METRICS_REPORT
612            {
613                // Display corpus metrics inline as JSON.
614                let mut metrics = json!({
615                    "timestamp": SystemTime::now()
616                        .duration_since(UNIX_EPOCH)?
617                        .as_secs(),
618                    "invariant": invariant_contract.invariant_function.name,
619                    "metrics": &corpus_manager.metrics,
620                });
621                if let Some(best) = invariant_test.test_data.optimization_best_value {
622                    metrics["optimization_best"] = json!(best.to_string());
623                }
624                let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
625                last_metrics_report = Instant::now();
626            }
627
628            runs += 1;
629        }
630
631        trace!(?fuzz_fixtures);
632        invariant_test.fuzz_state.log_stats();
633
634        let result = invariant_test.test_data;
635        Ok(InvariantFuzzTestResult {
636            error: result.failures.error,
637            cases: result.fuzz_cases,
638            reverts: result.failures.reverts,
639            last_run_inputs: result.last_run_inputs,
640            gas_report_traces: result.gas_report_traces,
641            line_coverage: result.line_coverage,
642            metrics: result.metrics,
643            failed_corpus_replays: corpus_manager.failed_replays,
644            optimization_best_value: result.optimization_best_value,
645            optimization_best_sequence: result.optimization_best_sequence,
646        })
647    }
648
649    /// Prepares certain structures to execute the invariant tests:
650    /// * Invariant Fuzz Test.
651    /// * Invariant Corpus Manager.
652    fn prepare_test(
653        &mut self,
654        invariant_contract: &InvariantContract<'_>,
655        fuzz_fixtures: &FuzzFixtures,
656        fuzz_state: EvmFuzzState,
657    ) -> Result<(InvariantTest<FEN>, WorkerCorpus)> {
658        // Finds out the chosen deployed contracts and/or senders.
659        self.select_contract_artifacts(invariant_contract.address)?;
660        let (targeted_senders, targeted_contracts) =
661            self.select_contracts_and_senders(invariant_contract.address)?;
662
663        // Creates the invariant strategy.
664        let strategy = invariant_strat(
665            fuzz_state.clone(),
666            targeted_senders,
667            targeted_contracts.clone(),
668            self.config.clone(),
669            fuzz_fixtures.clone(),
670        )
671        .no_shrink();
672
673        // If any of the targeted contracts have the storage layout enabled then we can sample
674        // mapping values. To accomplish, we need to record the mapping storage slots and keys.
675        let fuzz_state =
676            if targeted_contracts.targets.lock().iter().any(|(_, t)| t.storage_layout.is_some()) {
677                fuzz_state.with_mapping_slots(AddressMap::default())
678            } else {
679                fuzz_state
680            };
681
682        // Set up fuzzer WITHOUT call_generator initially.
683        // We defer call_override until after the initial invariant check to avoid
684        // injecting random calls during setup which would break the invariant assertion.
685        self.executor.inspector_mut().set_fuzzer(Fuzzer {
686            call_generator: None,
687            fuzz_state: fuzz_state.clone(),
688            collect: true,
689        });
690
691        // Let's make sure the invariant is sound before actually starting the run:
692        // We'll assert the invariant in its initial state, and if it fails, we'll
693        // already know if we can early exit the invariant run.
694        // This does not count as a fuzz run. It will just register the revert.
695        let mut failures = InvariantFailures::new();
696        let last_call_results = assert_invariants(
697            invariant_contract,
698            &self.config,
699            &targeted_contracts,
700            &self.executor,
701            &[],
702            &mut failures,
703        )?;
704        if let Some(error) = failures.error {
705            return Err(eyre!(error.revert_reason().unwrap_or_default()));
706        }
707
708        // NOW enable call_override after the initial invariant check has passed.
709        // This allows `override_call_strat` to inject calls during actual fuzz runs
710        // for reentrancy vulnerability detection.
711        if self.config.call_override {
712            let target_contract_ref = Arc::new(RwLock::new(Address::ZERO));
713
714            // Collect handler addresses - these are the contracts we want to inject
715            // reentrancy into (simulating malicious receive() functions).
716            let handler_addresses: std::collections::HashSet<Address> =
717                targeted_contracts.targets.lock().keys().copied().collect();
718
719            let call_generator = RandomCallGenerator::new(
720                invariant_contract.address,
721                handler_addresses,
722                self.runner.clone(),
723                override_call_strat(
724                    fuzz_state.clone(),
725                    targeted_contracts.clone(),
726                    target_contract_ref.clone(),
727                    fuzz_fixtures.clone(),
728                ),
729                target_contract_ref,
730            );
731
732            if let Some(fuzzer) = self.executor.inspector_mut().fuzzer.as_mut() {
733                fuzzer.call_generator = Some(call_generator);
734            }
735        }
736
737        let worker = WorkerCorpus::new(
738            0,
739            self.config.corpus.clone(),
740            strategy.boxed(),
741            Some(&self.executor),
742            None,
743            Some(&targeted_contracts),
744        )?;
745
746        let mut invariant_test = InvariantTest::new(
747            fuzz_state,
748            targeted_contracts,
749            failures,
750            last_call_results,
751            self.runner.clone(),
752        );
753
754        // Seed invariant test with previously persisted optimization state,
755        // but only if the current invariant is in optimization mode.
756        if invariant_contract.is_optimization() {
757            let (opt_best_value, opt_best_sequence) = worker.optimization_initial_state();
758            invariant_test.test_data.optimization_best_value = opt_best_value;
759            invariant_test.test_data.optimization_best_sequence = opt_best_sequence;
760        }
761
762        Ok((invariant_test, worker))
763    }
764
765    /// Fills the `InvariantExecutor` with the artifact identifier filters (in `path:name` string
766    /// format). They will be used to filter contracts after the `setUp`, and more importantly,
767    /// during the runs.
768    ///
769    /// Also excludes any contract without any mutable functions.
770    ///
771    /// Priority:
772    ///
773    /// targetArtifactSelectors > excludeArtifacts > targetArtifacts
774    pub fn select_contract_artifacts(&mut self, invariant_address: Address) -> Result<()> {
775        let targeted_artifact_selectors = self
776            .executor
777            .call_sol_default(invariant_address, &IInvariantTest::targetArtifactSelectorsCall {});
778
779        // Insert them into the executor `targeted_abi`.
780        for IInvariantTest::FuzzArtifactSelector { artifact, selectors } in
781            targeted_artifact_selectors
782        {
783            let identifier = self.validate_selected_contract(artifact, &selectors)?;
784            self.artifact_filters.targeted.entry(identifier).or_default().extend(selectors);
785        }
786
787        let targeted_artifacts = self
788            .executor
789            .call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {});
790        let excluded_artifacts = self
791            .executor
792            .call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {});
793
794        // Insert `excludeArtifacts` into the executor `excluded_abi`.
795        for contract in excluded_artifacts {
796            let identifier = self.validate_selected_contract(contract, &[])?;
797
798            if !self.artifact_filters.excluded.contains(&identifier) {
799                self.artifact_filters.excluded.push(identifier);
800            }
801        }
802
803        // Exclude any artifact without mutable functions.
804        for (artifact, contract) in self.project_contracts.iter() {
805            if contract
806                .abi
807                .functions()
808                .filter(|func| {
809                    !matches!(
810                        func.state_mutability,
811                        alloy_json_abi::StateMutability::Pure
812                            | alloy_json_abi::StateMutability::View
813                    )
814                })
815                .count()
816                == 0
817                && !self.artifact_filters.excluded.contains(&artifact.identifier())
818            {
819                self.artifact_filters.excluded.push(artifact.identifier());
820            }
821        }
822
823        // Insert `targetArtifacts` into the executor `targeted_abi`, if they have not been seen
824        // before.
825        for contract in targeted_artifacts {
826            let identifier = self.validate_selected_contract(contract, &[])?;
827
828            if !self.artifact_filters.targeted.contains_key(&identifier)
829                && !self.artifact_filters.excluded.contains(&identifier)
830            {
831                self.artifact_filters.targeted.insert(identifier, vec![]);
832            }
833        }
834        Ok(())
835    }
836
837    /// Makes sure that the contract exists in the project. If so, it returns its artifact
838    /// identifier.
839    fn validate_selected_contract(
840        &mut self,
841        contract: String,
842        selectors: &[FixedBytes<4>],
843    ) -> Result<String> {
844        if let Some((artifact, contract_data)) =
845            self.project_contracts.find_by_name_or_identifier(&contract)?
846        {
847            // Check that the selectors really exist for this contract.
848            for selector in selectors {
849                contract_data
850                    .abi
851                    .functions()
852                    .find(|func| func.selector().as_slice() == selector.as_slice())
853                    .wrap_err(format!("{contract} does not have the selector {selector:?}"))?;
854            }
855
856            return Ok(artifact.identifier());
857        }
858        eyre::bail!(
859            "{contract} not found in the project. Allowed format: `contract_name` or `contract_path:contract_name`."
860        );
861    }
862
863    /// Selects senders and contracts based on the contract methods `targetSenders() -> address[]`,
864    /// `targetContracts() -> address[]` and `excludeContracts() -> address[]`.
865    pub fn select_contracts_and_senders(
866        &self,
867        to: Address,
868    ) -> Result<(SenderFilters, FuzzRunIdentifiedContracts)> {
869        let targeted_senders =
870            self.executor.call_sol_default(to, &IInvariantTest::targetSendersCall {});
871        let mut excluded_senders =
872            self.executor.call_sol_default(to, &IInvariantTest::excludeSendersCall {});
873        // Extend with default excluded addresses - https://github.com/foundry-rs/foundry/issues/4163
874        excluded_senders.extend([
875            CHEATCODE_ADDRESS,
876            HARDHAT_CONSOLE_ADDRESS,
877            DEFAULT_CREATE2_DEPLOYER,
878        ]);
879        // Extend with precompiles - https://github.com/foundry-rs/foundry/issues/4287
880        excluded_senders.extend(PRECOMPILES);
881        let sender_filters = SenderFilters::new(targeted_senders, excluded_senders);
882
883        let selected = self.executor.call_sol_default(to, &IInvariantTest::targetContractsCall {});
884        let excluded = self.executor.call_sol_default(to, &IInvariantTest::excludeContractsCall {});
885
886        let contracts = self
887            .setup_contracts
888            .iter()
889            .filter(|&(addr, (identifier, _))| {
890                // Include to address if explicitly set as target.
891                if *addr == to && selected.contains(&to) {
892                    return true;
893                }
894
895                *addr != to
896                    && *addr != CHEATCODE_ADDRESS
897                    && *addr != HARDHAT_CONSOLE_ADDRESS
898                    && (selected.is_empty() || selected.contains(addr))
899                    && (excluded.is_empty() || !excluded.contains(addr))
900                    && self.artifact_filters.matches(identifier)
901            })
902            .map(|(addr, (identifier, abi))| {
903                (
904                    *addr,
905                    TargetedContract::new(identifier.clone(), abi.clone())
906                        .with_project_contracts(self.project_contracts),
907                )
908            })
909            .collect();
910        let mut contracts = TargetedContracts { inner: contracts };
911
912        self.target_interfaces(to, &mut contracts)?;
913
914        self.select_selectors(to, &mut contracts)?;
915
916        // There should be at least one contract identified as target for fuzz runs.
917        if contracts.is_empty() {
918            eyre::bail!("No contracts to fuzz.");
919        }
920
921        Ok((sender_filters, FuzzRunIdentifiedContracts::new(contracts, selected.is_empty())))
922    }
923
924    /// Extends the contracts and selectors to fuzz with the addresses and ABIs specified in
925    /// `targetInterfaces() -> (address, string[])[]`. Enables targeting of addresses that are
926    /// not deployed during `setUp` such as when fuzzing in a forked environment. Also enables
927    /// targeting of delegate proxies and contracts deployed with `create` or `create2`.
928    pub fn target_interfaces(
929        &self,
930        invariant_address: Address,
931        targeted_contracts: &mut TargetedContracts,
932    ) -> Result<()> {
933        let interfaces = self
934            .executor
935            .call_sol_default(invariant_address, &IInvariantTest::targetInterfacesCall {});
936
937        // Since `targetInterfaces` returns a tuple array there is no guarantee
938        // that the addresses are unique this map is used to merge functions of
939        // the specified interfaces for the same address. For example:
940        // `[(addr1, ["IERC20", "IOwnable"])]` and `[(addr1, ["IERC20"]), (addr1, ("IOwnable"))]`
941        // should be equivalent.
942        let mut combined = TargetedContracts::new();
943
944        // Loop through each address and its associated artifact identifiers.
945        // We're borrowing here to avoid taking full ownership.
946        for IInvariantTest::FuzzInterface { addr, artifacts } in &interfaces {
947            // Identifiers are specified as an array, so we loop through them.
948            for identifier in artifacts {
949                // Try to find the contract by name or identifier in the project's contracts.
950                if let Some((_, contract_data)) =
951                    self.project_contracts.iter().find(|(artifact, _)| {
952                        &artifact.name == identifier || &artifact.identifier() == identifier
953                    })
954                {
955                    let abi = &contract_data.abi;
956                    combined
957                        // Check if there's an entry for the given key in the 'combined' map.
958                        .entry(*addr)
959                        // If the entry exists, extends its ABI with the function list.
960                        .and_modify(|entry| {
961                            // Extend the ABI's function list with the new functions.
962                            entry.abi.functions.extend(abi.functions.clone());
963                        })
964                        // Otherwise insert it into the map.
965                        .or_insert_with(|| {
966                            let mut contract =
967                                TargetedContract::new(identifier.clone(), abi.clone());
968                            contract.storage_layout =
969                                contract_data.storage_layout.as_ref().map(Arc::clone);
970                            contract
971                        });
972                }
973            }
974        }
975
976        targeted_contracts.extend(combined.inner);
977
978        Ok(())
979    }
980
981    /// Selects the functions to fuzz based on the contract method `targetSelectors()` and
982    /// `targetArtifactSelectors()`.
983    pub fn select_selectors(
984        &self,
985        address: Address,
986        targeted_contracts: &mut TargetedContracts,
987    ) -> Result<()> {
988        for (address, (identifier, _)) in self.setup_contracts {
989            if let Some(selectors) = self.artifact_filters.targeted.get(identifier) {
990                self.add_address_with_functions(*address, selectors, false, targeted_contracts)?;
991            }
992        }
993
994        let mut target_test_selectors = vec![];
995        let mut excluded_test_selectors = vec![];
996
997        // Collect contract functions marked as target for fuzzing campaign.
998        let selectors =
999            self.executor.call_sol_default(address, &IInvariantTest::targetSelectorsCall {});
1000        for IInvariantTest::FuzzSelector { addr, selectors } in selectors {
1001            if addr == address {
1002                target_test_selectors = selectors.clone();
1003            }
1004            self.add_address_with_functions(addr, &selectors, false, targeted_contracts)?;
1005        }
1006
1007        // Collect contract functions excluded from fuzzing campaign.
1008        let excluded_selectors =
1009            self.executor.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {});
1010        for IInvariantTest::FuzzSelector { addr, selectors } in excluded_selectors {
1011            if addr == address {
1012                // If fuzz selector address is the test contract, then record selectors to be
1013                // later excluded if needed.
1014                excluded_test_selectors = selectors.clone();
1015            }
1016            self.add_address_with_functions(addr, &selectors, true, targeted_contracts)?;
1017        }
1018
1019        if target_test_selectors.is_empty()
1020            && let Some(target) = targeted_contracts.get(&address)
1021        {
1022            // If test contract is marked as a target and no target selector explicitly set, then
1023            // include only state-changing functions that are not reserved and selectors that are
1024            // not explicitly excluded.
1025            let selectors: Vec<_> = target
1026                .abi
1027                .functions()
1028                .filter_map(|func| {
1029                    if matches!(
1030                        func.state_mutability,
1031                        alloy_json_abi::StateMutability::Pure
1032                            | alloy_json_abi::StateMutability::View
1033                    ) || func.is_reserved()
1034                        || excluded_test_selectors.contains(&func.selector())
1035                    {
1036                        None
1037                    } else {
1038                        Some(func.selector())
1039                    }
1040                })
1041                .collect();
1042            self.add_address_with_functions(address, &selectors, false, targeted_contracts)?;
1043        }
1044
1045        Ok(())
1046    }
1047
1048    /// Adds the address and fuzzed or excluded functions to `TargetedContracts`.
1049    fn add_address_with_functions(
1050        &self,
1051        address: Address,
1052        selectors: &[Selector],
1053        should_exclude: bool,
1054        targeted_contracts: &mut TargetedContracts,
1055    ) -> eyre::Result<()> {
1056        // Do not add address in target contracts if no function selected.
1057        if selectors.is_empty() {
1058            return Ok(());
1059        }
1060
1061        let contract = match targeted_contracts.entry(address) {
1062            Entry::Occupied(entry) => entry.into_mut(),
1063            Entry::Vacant(entry) => {
1064                let (identifier, abi) = self.setup_contracts.get(&address).ok_or_else(|| {
1065                    eyre::eyre!(
1066                        "[{}] address does not have an associated contract: {}",
1067                        if should_exclude { "excludeSelectors" } else { "targetSelectors" },
1068                        address
1069                    )
1070                })?;
1071                entry.insert(
1072                    TargetedContract::new(identifier.clone(), abi.clone())
1073                        .with_project_contracts(self.project_contracts),
1074                )
1075            }
1076        };
1077        contract.add_selectors(selectors.iter().copied(), should_exclude)?;
1078        Ok(())
1079    }
1080
1081    /// Computes the current invariant settings for the given invariant contract address.
1082    ///
1083    /// This extracts the target contracts, selectors, senders, and fail_on_revert setting
1084    /// that are used to determine if a persisted counterexample is still valid.
1085    pub fn compute_settings(&mut self, invariant_address: Address) -> Result<InvariantSettings> {
1086        self.select_contract_artifacts(invariant_address)?;
1087        let (sender_filters, targeted_contracts) =
1088            self.select_contracts_and_senders(invariant_address)?;
1089        let targets = targeted_contracts.targets.lock();
1090        Ok(InvariantSettings::new(&targets, &sender_filters, self.config.fail_on_revert))
1091    }
1092}
1093
1094/// Collects data from call for fuzzing. However, it first verifies that the sender is not an EOA
1095/// before inserting it into the dictionary. Otherwise, we flood the dictionary with
1096/// randomly generated addresses.
1097fn collect_data<FEN: FoundryEvmNetwork>(
1098    invariant_test: &InvariantTest<FEN>,
1099    state_changeset: &mut AddressMap<Account>,
1100    tx: &BasicTxDetails,
1101    call_result: &RawCallResult<FEN>,
1102    run_depth: u32,
1103) {
1104    // Verify it has no code.
1105    let mut has_code = false;
1106    if let Some(Some(code)) =
1107        state_changeset.get(&tx.sender).map(|account| account.info.code.as_ref())
1108    {
1109        has_code = !code.is_empty();
1110    }
1111
1112    // We keep the nonce changes to apply later.
1113    let mut sender_changeset = None;
1114    if !has_code {
1115        sender_changeset = state_changeset.remove(&tx.sender);
1116    }
1117
1118    // Collect values from fuzzed call result and add them to fuzz dictionary.
1119    invariant_test.fuzz_state.collect_values_from_call(
1120        &invariant_test.targeted_contracts,
1121        tx,
1122        &call_result.result,
1123        &call_result.logs,
1124        &*state_changeset,
1125        run_depth,
1126    );
1127
1128    // Re-add changes
1129    if let Some(changed) = sender_changeset {
1130        state_changeset.insert(tx.sender, changed);
1131    }
1132}
1133
1134/// Calls the `afterInvariant()` function on a contract.
1135/// Returns call result and if call succeeded.
1136/// The state after the call is not persisted.
1137pub(crate) fn call_after_invariant_function<FEN: FoundryEvmNetwork>(
1138    executor: &Executor<FEN>,
1139    to: Address,
1140) -> Result<(RawCallResult<FEN>, bool), EvmError<FEN>> {
1141    let calldata = Bytes::from_static(&IInvariantTest::afterInvariantCall::SELECTOR);
1142    let mut call_result = executor.call_raw(CALLER, to, calldata, U256::ZERO)?;
1143    let success = executor.is_raw_call_mut_success(to, &mut call_result, false);
1144    Ok((call_result, success))
1145}
1146
1147/// Calls the invariant function and returns call result and if succeeded.
1148pub(crate) fn call_invariant_function<FEN: FoundryEvmNetwork>(
1149    executor: &Executor<FEN>,
1150    address: Address,
1151    calldata: Bytes,
1152) -> Result<(RawCallResult<FEN>, bool)> {
1153    let mut call_result = executor.call_raw(CALLER, address, calldata, U256::ZERO)?;
1154    let success = executor.is_raw_call_mut_success(address, &mut call_result, false);
1155    Ok((call_result, success))
1156}
1157
1158/// Executes a fuzz call and returns the result.
1159/// Applies any block timestamp (warp) and block number (roll) adjustments before the call.
1160pub(crate) fn execute_tx<FEN: FoundryEvmNetwork>(
1161    executor: &mut Executor<FEN>,
1162    tx: &BasicTxDetails,
1163) -> Result<RawCallResult<FEN>> {
1164    let warp = tx.warp.unwrap_or_default();
1165    let roll = tx.roll.unwrap_or_default();
1166
1167    if warp > 0 || roll > 0 {
1168        // Apply pre-call block adjustments to the executor's env.
1169        let ts = executor.evm_env().block_env.timestamp();
1170        let num = executor.evm_env().block_env.number();
1171        executor.evm_env_mut().block_env.set_timestamp(ts + warp);
1172        executor.evm_env_mut().block_env.set_number(num + roll);
1173
1174        // Also update the inspector's cheatcodes.block if set.
1175        // The inspector's block may override the env during interpreter initialization,
1176        // so we need to add our warp/roll on top of any existing cheatcode-set values.
1177        let block_env = executor.evm_env().block_env.clone();
1178        if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
1179            if let Some(block) = cheatcodes.block.as_mut() {
1180                let bts = block.timestamp();
1181                let bnum = block.number();
1182                block.set_timestamp(bts + warp);
1183                block.set_number(bnum + roll);
1184            } else {
1185                cheatcodes.block = Some(block_env);
1186            }
1187        }
1188    }
1189
1190    executor
1191        .call_raw(tx.sender, tx.call_details.target, tx.call_details.calldata.clone(), U256::ZERO)
1192        .map_err(|e| eyre!(format!("Could not make raw evm call: {e}")))
1193}