1use crate::{
2 executors::{
3 DURATION_BETWEEN_METRICS_REPORT, EarlyExit, EvmError, Executor, RawCallResult,
4 corpus::{DynamicTargetCtx, ReplayTarget, WorkerCorpus, WorkerCorpusSeed},
5 },
6 inspectors::Fuzzer,
7};
8use alloy_json_abi::Function;
9use alloy_primitives::{
10 Address, Bytes, FixedBytes, I256, Selector, U256, keccak256, map::AddressMap,
11};
12use alloy_sol_types::{SolCall, sol};
13use eyre::{ContextCompat, Result, eyre};
14use foundry_common::{
15 TestFunctionExt,
16 contracts::{ContractsByAddress, ContractsByArtifact},
17 sh_eprintln, sh_println,
18};
19use foundry_config::{InvariantConfig, InvariantWorkers};
20use foundry_evm_core::{
21 FoundryBlock,
22 constants::{
23 CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS, MAGIC_ASSUME,
24 },
25 evm::FoundryEvmNetwork,
26 precompiles::PRECOMPILES,
27};
28use foundry_evm_fuzz::{
29 BasicTxDetails, FuzzCase, FuzzFixtures,
30 invariant::{
31 ArtifactFilters, FuzzRunIdentifiedContracts, InvariantContract, InvariantSettings,
32 RandomCallGenerator, SenderFilters, TargetedContract, TargetedContracts,
33 },
34 strategies::{EvmFuzzState, InvariantFuzzState, invariant_strat, override_call_strat},
35};
36use foundry_evm_traces::{CallTraceArena, SparsedTraceArena};
37use indicatif::ProgressBar;
38use parking_lot::RwLock;
39use proptest::{
40 strategy::Strategy,
41 test_runner::{RngAlgorithm, TestRng, TestRunner},
42};
43use rayon::iter::{IntoParallelIterator, ParallelIterator};
44use result::{assert_after_invariant, can_continue, did_fail_on_assert, invariant_preflight_check};
45use revm::{context::Block, state::Account};
46use serde::{Deserialize, Serialize};
47use serde_json::json;
48use std::{
49 collections::{HashMap as Map, HashSet, btree_map::Entry},
50 sync::Arc,
51 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
52};
53
54mod error;
55pub use error::{
56 FailureKey, HandlerAssertionFailure, InvariantFailures, InvariantFuzzError,
57 handler_site_already_minimal,
58};
59use foundry_evm_coverage::HitMaps;
60
61mod campaign;
62use campaign::{
63 InvariantCampaignAggregator, InvariantCampaignSpec, InvariantCampaignState,
64 InvariantWorkerOutput, InvariantWorkerPlan,
65};
66
67mod replay;
68pub use replay::{replay_error, replay_run};
69
70mod result;
71pub use result::InvariantFuzzTestResult;
72
73mod shrink;
74pub use shrink::{
75 CheckSequenceOptions, HandlerReplayOutcome, check_sequence, check_sequence_value,
76 replay_handler_failure_sequence,
77};
78
79const MIN_RUNS_PER_INVARIANT_WORKER: u32 = 10_000;
84const DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP: u32 = 500;
86const MIN_ESTIMATED_CALLS_PER_INVARIANT_WORKER: u64 =
88 MIN_RUNS_PER_INVARIANT_WORKER as u64 * DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP as u64;
89
90sol! {
91 interface IInvariantTest {
92 #[derive(Default)]
93 struct FuzzSelector {
94 address addr;
95 bytes4[] selectors;
96 }
97
98 #[derive(Default)]
99 struct FuzzArtifactSelector {
100 string artifact;
101 bytes4[] selectors;
102 }
103
104 #[derive(Default)]
105 struct FuzzInterface {
106 address addr;
107 string[] artifacts;
108 }
109
110 function afterInvariant() external;
111
112 #[derive(Default)]
113 function excludeArtifacts() public view returns (string[] memory excludedArtifacts);
114
115 #[derive(Default)]
116 function excludeContracts() public view returns (address[] memory excludedContracts);
117
118 #[derive(Default)]
119 function excludeSelectors() public view returns (FuzzSelector[] memory excludedSelectors);
120
121 #[derive(Default)]
122 function excludeSenders() public view returns (address[] memory excludedSenders);
123
124 #[derive(Default)]
125 function targetArtifacts() public view returns (string[] memory targetedArtifacts);
126
127 #[derive(Default)]
128 function targetArtifactSelectors() public view returns (FuzzArtifactSelector[] memory targetedArtifactSelectors);
129
130 #[derive(Default)]
131 function targetContracts() public view returns (address[] memory targetedContracts);
132
133 #[derive(Default)]
134 function targetSelectors() public view returns (FuzzSelector[] memory targetedSelectors);
135
136 #[derive(Default)]
137 function targetSenders() public view returns (address[] memory targetedSenders);
138
139 #[derive(Default)]
140 function targetInterfaces() public view returns (FuzzInterface[] memory targetedInterfaces);
141 }
142}
143
144#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
146pub struct InvariantMetrics {
147 pub calls: usize,
149 pub reverts: usize,
151 pub discards: usize,
153}
154
155#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
157struct InvariantThroughputMetrics {
158 total_txs: u64,
159 total_gas: u64,
160}
161
162impl InvariantThroughputMetrics {
163 fn tx_per_sec(self, elapsed: Duration) -> f64 {
164 rate_per_sec(self.total_txs as f64, elapsed)
165 }
166
167 fn gas_per_sec(self, elapsed: Duration) -> f64 {
168 rate_per_sec(self.total_gas as f64, elapsed)
169 }
170}
171
172fn max_invariant_workers_for_campaign(runs: u32, depth: u32) -> usize {
173 let estimated_calls = u64::from(runs) * u64::from(depth.max(1));
174 usize::try_from((estimated_calls / MIN_ESTIMATED_CALLS_PER_INVARIANT_WORKER).max(1))
175 .unwrap_or(usize::MAX)
176}
177
178fn auto_invariant_worker_count(
179 available_threads: usize,
180 invariant_campaign_anchors: usize,
181) -> usize {
182 (available_threads.max(1) / invariant_campaign_anchors.max(1)).max(1)
183}
184
185fn invariant_worker_count_with_threads(
186 config: &InvariantConfig,
187 available_threads: usize,
188 invariant_campaign_anchors: usize,
189) -> usize {
190 match config.workers {
191 InvariantWorkers::Fixed(workers) => workers.get(),
192 InvariantWorkers::Auto => {
193 let requested =
194 auto_invariant_worker_count(available_threads, invariant_campaign_anchors);
195 if config.timeout.is_some() {
196 requested
197 } else {
198 requested.min(max_invariant_workers_for_campaign(config.runs, config.depth))
199 }
200 }
201 }
202}
203
204fn gas_report_samples_for_worker(total_samples: u32, worker_id: u32, worker_count: usize) -> usize {
205 let total_samples = total_samples as usize;
206 let worker_count = worker_count.max(1);
207 total_samples / worker_count + usize::from((worker_id as usize) < total_samples % worker_count)
208}
209
210fn invariant_worker_seed(seed: U256, worker_id: u32) -> U256 {
211 if worker_id == 0 {
212 seed
213 } else {
214 let seed_data = [&seed.to_be_bytes::<32>()[..], &worker_id.to_be_bytes()[..]].concat();
215 U256::from_be_bytes(keccak256(seed_data).0)
216 }
217}
218
219fn should_continue_invariant_worker(
220 campaign_state: &InvariantCampaignState,
221 runs: u32,
222 plan: InvariantWorkerPlan,
223) -> bool {
224 if campaign_state.should_stop() {
225 return false;
226 }
227
228 campaign_state.is_timed_campaign() || runs < plan.runs
229}
230
231fn invariant_worker_runner(
232 runner: &mut TestRunner,
233 worker_id: u32,
234 seed: Option<U256>,
235) -> TestRunner {
236 if let Some(seed) = seed {
237 let worker_seed = invariant_worker_seed(seed, worker_id);
238 trace!(target: "forge::test", ?worker_seed, "deterministic seed for invariant worker {worker_id}");
239 let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &worker_seed.to_be_bytes::<32>());
240 TestRunner::new_with_rng(runner.config().clone(), rng)
241 } else if worker_id == 0 {
242 runner.clone()
243 } else {
244 TestRunner::new_with_rng(runner.config().clone(), runner.new_rng())
245 }
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249enum InvariantCorpusPersistence {
250 Live,
252 Deferred,
254}
255
256impl InvariantCorpusPersistence {
257 const fn is_deferred(self) -> bool {
258 matches!(self, Self::Deferred)
259 }
260}
261
262fn rate_per_sec(total: f64, elapsed: Duration) -> f64 {
267 let elapsed_secs = elapsed.as_secs_f64();
268 if elapsed_secs > 0.0 { total / elapsed_secs } else { 0.0 }
269}
270
271#[derive(Clone, Debug, Default)]
273struct InvariantFailureMetrics {
274 failures: u64,
275 unique_failures: HashSet<String>,
276 broken_handlers: usize,
278}
279
280impl InvariantFailureMetrics {
281 fn record_failure(&mut self, invariant_name: &str, target: &str, reason: &str) {
283 self.failures += 1;
284 self.unique_failures.insert(invariant_name.to_string());
285
286 let timestamp =
287 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
288 let event = json!({
289 "timestamp": timestamp,
290 "event": "failure",
291 "invariant": invariant_name,
292 "target": target,
293 "reason": reason,
294 });
295 let _ = sh_eprintln!("{}", serde_json::to_string(&event).unwrap_or_default());
296 }
297}
298
299fn record_new_invariant_failures(
303 campaign_state: &InvariantCampaignState,
304 invariant_contract: &InvariantContract<'_>,
305 failures: &InvariantFailures,
306) {
307 for (f, _) in &invariant_contract.invariant_fns {
308 if let Some(failure) = failures.get_failure(f) {
309 let reason = failure.revert_reason().unwrap_or_default();
310 campaign_state.record_invariant_failure(&f.name, invariant_contract.name, &reason);
311 }
312 }
313}
314
315fn build_invariant_progress_json<M: Serialize>(
322 timestamp_secs: u64,
323 invariant_name: &str,
324 corpus_metrics: &M,
325 optimization_best: Option<I256>,
326 throughput: InvariantThroughputMetrics,
327 failure_metrics: &InvariantFailureMetrics,
328 elapsed: Duration,
329) -> serde_json::Value {
330 let mut metrics = serde_json::to_value(corpus_metrics).unwrap_or_default();
331 if let Some(obj) = metrics.as_object_mut() {
332 obj.insert("failures".to_string(), json!(failure_metrics.failures));
333 obj.insert("unique_failures".to_string(), json!(failure_metrics.unique_failures.len()));
334 obj.insert("broken_handlers".to_string(), json!(failure_metrics.broken_handlers));
337 }
338
339 let mut payload = json!({
340 "timestamp": timestamp_secs,
341 "event": "pulse",
342 "invariant": invariant_name,
343 "metrics": metrics,
344 "total_txs": throughput.total_txs,
345 "total_gas": throughput.total_gas,
346 "tx_per_sec": throughput.tx_per_sec(elapsed),
347 "gas_per_sec": throughput.gas_per_sec(elapsed),
348 });
349
350 if let Some(best) = optimization_best {
351 payload["optimization_best"] = json!(best.to_string());
352 }
353
354 payload
355}
356
357struct InvariantTestData {
359 runs: usize,
361 calls: usize,
363 failures: InvariantFailures,
365 last_run_inputs: Vec<BasicTxDetails>,
367 gas_report_traces: Vec<Vec<CallTraceArena>>,
369 line_coverage: Option<HitMaps>,
371 metrics: Map<String, InvariantMetrics>,
373
374 branch_runner: TestRunner,
379
380 optimization_best_value: Option<I256>,
383 optimization_best_sequence: Vec<BasicTxDetails>,
384}
385
386struct InvariantTest {
388 fuzz_state: InvariantFuzzState,
390 targeted_contracts: FuzzRunIdentifiedContracts,
392 test_data: InvariantTestData,
394}
395
396impl InvariantTest {
397 fn new(
399 fuzz_state: InvariantFuzzState,
400 targeted_contracts: FuzzRunIdentifiedContracts,
401 failures: InvariantFailures,
402 branch_runner: TestRunner,
403 ) -> Self {
404 let test_data = InvariantTestData {
405 runs: 0,
406 calls: 0,
407 failures,
408 last_run_inputs: vec![],
409 gas_report_traces: vec![],
410 line_coverage: None,
411 metrics: Map::default(),
412 branch_runner,
413 optimization_best_value: None,
414 optimization_best_sequence: vec![],
415 };
416 Self { fuzz_state, targeted_contracts, test_data }
417 }
418
419 const fn reverts(&self) -> usize {
421 self.test_data.failures.reverts
422 }
423
424 fn set_error(&mut self, invariant: &Function, error: InvariantFuzzError) {
426 self.test_data.failures.record_failure(invariant, error);
427 }
428
429 fn set_last_run_inputs(&mut self, inputs: &Vec<BasicTxDetails>) {
431 self.test_data.last_run_inputs.clone_from(inputs);
432 }
433
434 fn merge_line_coverage(&mut self, new_coverage: Option<HitMaps>) {
436 HitMaps::merge_opt(&mut self.test_data.line_coverage, new_coverage);
437 }
438
439 fn record_metrics(&mut self, tx_details: &BasicTxDetails, reverted: bool, discarded: bool) {
443 if let Some(metric_key) = self.targeted_contracts.targets().fuzzed_metric_key(tx_details) {
444 let test_metrics = &mut self.test_data.metrics;
445 let invariant_metrics = test_metrics.entry(metric_key).or_default();
446 invariant_metrics.calls += 1;
447 if discarded {
448 invariant_metrics.discards += 1;
449 } else if reverted {
450 invariant_metrics.reverts += 1;
451 }
452 }
453 }
454
455 fn end_run<FEN: FoundryEvmNetwork>(&mut self, run: InvariantTestRun<FEN>, gas_samples: usize) {
458 self.targeted_contracts.clear_created_contracts(run.created_contracts);
460
461 if self.test_data.gas_report_traces.len() < gas_samples {
462 self.test_data
463 .gas_report_traces
464 .push(run.run_traces.into_iter().map(|arena| arena.arena).collect());
465 }
466 self.test_data.runs += 1;
467 self.test_data.calls += run.fuzz_runs.len();
468
469 self.fuzz_state.revert();
471 }
472
473 fn update_optimization_value(&mut self, value: I256, sequence: &[BasicTxDetails]) {
475 if self.test_data.optimization_best_value.is_none_or(|best| value > best) {
476 self.test_data.optimization_best_value = Some(value);
477 self.test_data.optimization_best_sequence = sequence.to_vec();
478 }
479 }
480}
481
482struct InvariantTestRun<FEN: FoundryEvmNetwork> {
484 inputs: Vec<BasicTxDetails>,
486 cmp_seq: Vec<Vec<crate::inspectors::CmpOperands>>,
488 executor: Executor<FEN>,
490 fuzz_runs: Vec<FuzzCase>,
492 created_contracts: Vec<Address>,
494 run_traces: Vec<SparsedTraceArena>,
496 depth: u32,
498 rejects: u32,
500 new_coverage: bool,
502 optimization_value: Option<I256>,
504 optimization_prefix_len: usize,
506}
507
508#[derive(Clone)]
510struct InvariantCampaignSeed {
511 artifact_filters: ArtifactFilters,
512 sender_filters: SenderFilters,
513 targeted_contracts: TargetedContracts,
514 targets_are_updatable: bool,
515 initial_handler_failures: Map<(Address, Selector), InvariantFuzzError>,
516}
517
518impl<FEN: FoundryEvmNetwork> InvariantTestRun<FEN> {
519 fn new(first_input: BasicTxDetails, executor: Executor<FEN>, depth: usize) -> Self {
521 Self {
522 inputs: vec![first_input],
523 cmp_seq: Vec::with_capacity(depth),
524 executor,
525 fuzz_runs: Vec::with_capacity(depth),
526 created_contracts: vec![],
527 run_traces: vec![],
528 depth: 0,
529 rejects: 0,
530 new_coverage: false,
531 optimization_value: None,
532 optimization_prefix_len: 0,
533 }
534 }
535
536 fn drop_corpus_payloads(&mut self) {
543 self.inputs.clear();
544 self.inputs.shrink_to_fit();
545 self.cmp_seq.clear();
546 self.cmp_seq.shrink_to_fit();
547 }
548}
549
550pub struct InvariantExecutor<'a, FEN: FoundryEvmNetwork> {
557 pub executor: Executor<FEN>,
558 runner: TestRunner,
560 fuzz_seed: Option<U256>,
562 config: InvariantConfig,
564 setup_contracts: &'a ContractsByAddress,
566 project_contracts: &'a ContractsByArtifact,
569 artifact_filters: ArtifactFilters,
571 invariant_campaign_anchors: usize,
573}
574
575impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
576 pub fn new(
578 executor: Executor<FEN>,
579 runner: TestRunner,
580 config: InvariantConfig,
581 setup_contracts: &'a ContractsByAddress,
582 project_contracts: &'a ContractsByArtifact,
583 ) -> Self {
584 Self::new_with_fuzz_seed(
585 executor,
586 runner,
587 None,
588 config,
589 setup_contracts,
590 project_contracts,
591 1,
592 )
593 }
594
595 pub fn new_with_fuzz_seed(
598 executor: Executor<FEN>,
599 runner: TestRunner,
600 fuzz_seed: Option<U256>,
601 config: InvariantConfig,
602 setup_contracts: &'a ContractsByAddress,
603 project_contracts: &'a ContractsByArtifact,
604 invariant_campaign_anchors: usize,
605 ) -> Self {
606 Self {
607 executor,
608 runner,
609 fuzz_seed,
610 config,
611 setup_contracts,
612 project_contracts,
613 artifact_filters: ArtifactFilters::default(),
614 invariant_campaign_anchors,
615 }
616 }
617
618 pub fn config(&self) -> InvariantConfig {
619 self.config.clone()
620 }
621
622 pub const fn dynamic_target_ctx(&self) -> DynamicTargetCtx<'_> {
624 DynamicTargetCtx {
625 project_contracts: self.project_contracts,
626 setup_contracts: self.setup_contracts,
627 artifact_filters: &self.artifact_filters,
628 }
629 }
630
631 pub fn invariant_fuzz(
638 &mut self,
639 invariant_contract: InvariantContract<'_>,
640 fuzz_fixtures: &FuzzFixtures,
641 fuzz_state: EvmFuzzState,
642 progress: Option<&ProgressBar>,
643 early_exit: &EarlyExit,
644 initial_handler_failures: std::collections::HashMap<
645 (Address, Selector),
646 InvariantFuzzError,
647 >,
648 ) -> Result<InvariantFuzzTestResult> {
649 let campaign_spec = InvariantCampaignSpec::new(self.config.runs);
650 let worker_plans = campaign_spec.worker_plans(invariant_worker_count_with_threads(
651 &self.config,
652 rayon::current_num_threads(),
653 self.invariant_campaign_anchors,
654 ))?;
655 let actual_worker_count = worker_plans.len();
656 let campaign_seed =
657 self.prepare_campaign_seed(&invariant_contract, initial_handler_failures)?;
658 let replay_targets = FuzzRunIdentifiedContracts::new(
659 campaign_seed.targeted_contracts.clone(),
660 campaign_seed.targets_are_updatable,
661 );
662 let corpus_seed = WorkerCorpusSeed::load_from_disk(
663 &self.config.corpus,
664 Some(&self.executor),
665 None,
666 Some(&replay_targets),
667 Some(self.dynamic_target_ctx()),
668 )?;
669 let corpus_persistence = if actual_worker_count > 1 {
670 InvariantCorpusPersistence::Deferred
671 } else {
672 InvariantCorpusPersistence::Live
673 };
674 let mut runner = self.runner.clone();
675 let config = self.config.clone();
676 let setup_contracts = self.setup_contracts;
677 let project_contracts = self.project_contracts;
678 let base_executor = self.executor.clone();
679 let campaign_state =
680 Arc::new(InvariantCampaignState::new(early_exit.clone(), self.config.timeout));
681
682 let worker_outputs = if corpus_persistence.is_deferred() {
683 let worker_jobs = worker_plans
684 .into_iter()
685 .map(|worker_plan| {
686 let worker_runner =
687 invariant_worker_runner(&mut runner, worker_plan.worker_id, self.fuzz_seed);
688 let gas_report_samples = gas_report_samples_for_worker(
689 config.gas_report_samples,
690 worker_plan.worker_id,
691 actual_worker_count,
692 );
693 (worker_plan, worker_runner, gas_report_samples)
694 })
695 .collect::<Vec<_>>();
696 worker_jobs
697 .into_par_iter()
698 .map(|(worker_plan, worker_runner, gas_report_samples)| {
699 let _guard =
700 info_span!("invariant_worker", id = worker_plan.worker_id).entered();
701 let timer = Instant::now();
702 let output = Self::run_invariant_worker(
703 base_executor.clone(),
704 worker_runner,
705 config.clone(),
706 setup_contracts,
707 project_contracts,
708 worker_plan,
709 invariant_contract.clone(),
710 fuzz_fixtures,
711 fuzz_state.fork(),
712 progress,
713 &campaign_state,
714 campaign_seed.clone(),
715 corpus_seed
716 .clone_for_worker(worker_plan.worker_id as usize, actual_worker_count),
717 corpus_persistence,
718 gas_report_samples,
719 );
720 debug!("finished in {:?}", timer.elapsed());
721 output
722 })
723 .collect::<Result<Vec<_>>>()?
724 } else {
725 let worker_plan = worker_plans[0];
726 let runner =
727 invariant_worker_runner(&mut runner, worker_plan.worker_id, self.fuzz_seed);
728 let gas_report_samples = config.gas_report_samples as usize;
729 vec![Self::run_invariant_worker(
730 base_executor,
731 runner,
732 config,
733 setup_contracts,
734 project_contracts,
735 worker_plan,
736 invariant_contract,
737 fuzz_fixtures,
738 fuzz_state,
739 progress,
740 &campaign_state,
741 campaign_seed,
742 corpus_seed.clone(),
743 corpus_persistence,
744 gas_report_samples,
745 )?]
746 };
747
748 let mut aggregator = InvariantCampaignAggregator::new(campaign_spec);
749 for worker_output in worker_outputs {
750 aggregator.push(worker_output);
751 }
752 let (result, corpus_entries) = if campaign_state.is_timed_campaign() {
753 aggregator.finish_partial_with_corpus_entries()?
754 } else {
755 aggregator.finish_with_corpus_entries()?
756 };
757 if corpus_persistence.is_deferred() {
758 let dynamic_target_ctx = self.dynamic_target_ctx();
759 corpus_seed.persist_filtered_campaign_outputs(
760 &self.config.corpus,
761 corpus_entries,
762 &self.executor,
763 ReplayTarget {
764 fuzzed_function: None,
765 fuzzed_contracts: Some(&replay_targets),
766 dynamic: Some(&dynamic_target_ctx),
767 },
768 result
769 .optimization_best_value
770 .map(|value| (value, result.optimization_best_sequence.as_slice())),
771 )?;
772 }
773 Ok(result)
774 }
775
776 #[allow(clippy::too_many_arguments)]
778 fn run_invariant_worker(
779 mut executor: Executor<FEN>,
780 runner: TestRunner,
781 config: InvariantConfig,
782 setup_contracts: &'a ContractsByAddress,
783 project_contracts: &'a ContractsByArtifact,
784 plan: InvariantWorkerPlan,
785 invariant_contract: InvariantContract<'_>,
786 fuzz_fixtures: &FuzzFixtures,
787 fuzz_state: EvmFuzzState,
788 progress: Option<&ProgressBar>,
789 campaign_state: &InvariantCampaignState,
790 campaign_seed: InvariantCampaignSeed,
791 corpus_seed: WorkerCorpusSeed,
792 corpus_persistence: InvariantCorpusPersistence,
793 gas_report_samples: usize,
794 ) -> Result<InvariantWorkerOutput> {
795 let (mut invariant_test, mut corpus_manager) = Self::prepare_worker(
800 &mut executor,
801 plan,
802 &invariant_contract,
803 fuzz_fixtures,
804 fuzz_state,
805 &runner,
806 &config,
807 &campaign_seed,
808 corpus_seed,
809 )?;
810 let mut corpus_entries = Vec::new();
811
812 let mut runs = 0;
813 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
814
815 let edge_coverage_enabled = config.corpus.collect_edge_coverage();
817
818 'stop: while should_continue_invariant_worker(campaign_state, runs, plan) {
819 let failures_before_run = invariant_test.test_data.failures.invariant_count();
821 let mut stop_after_run = false;
822
823 let initial_seq = corpus_manager.new_inputs(
824 &mut invariant_test.test_data.branch_runner,
825 &invariant_test.fuzz_state,
826 &invariant_test.targeted_contracts,
827 )?;
828
829 let mut current_run = InvariantTestRun::new(
831 initial_seq[0].clone(),
832 executor.clone(),
834 config.depth as usize,
835 );
836
837 if config.fail_on_revert && invariant_test.reverts() > 0 {
839 campaign_state.request_terminal_stop();
840 return Err(eyre!("call reverted"));
841 }
842
843 while current_run.depth < config.depth {
844 if campaign_state.should_stop() {
846 break 'stop;
851 }
852
853 let (handler_target, handler_selector) = {
856 let last = current_run
857 .inputs
858 .last()
859 .ok_or_else(|| eyre!("no input generated to call fuzzed target."))?;
860 let sel_bytes: [u8; 4] = last
861 .call_details
862 .calldata
863 .get(..4)
864 .and_then(|s| s.try_into().ok())
865 .unwrap_or_default();
866 (last.call_details.target, Selector::from(sel_bytes))
867 };
868
869 let mut call_result = execute_tx(
872 &mut current_run.executor,
873 current_run.inputs.last().expect("checked above"),
874 )?;
875 if let Some(fuzzer) = current_run.executor.inspector_mut().fuzzer.as_mut() {
876 invariant_test.fuzz_state.collect_values(fuzzer.drain_collected_values());
877 }
878 let call_cmp_values = call_result.evm_cmp_values.take().unwrap_or_default();
882 let discarded = call_result.result.as_ref() == MAGIC_ASSUME;
883 if config.show_metrics {
884 invariant_test.record_metrics(
885 current_run.inputs.last().expect("checked above"),
886 call_result.reverted,
887 discarded,
888 );
889 }
890
891 invariant_test.merge_line_coverage(call_result.line_coverage.clone());
893 let assertion_failure =
896 !discarded && did_fail_on_assert(&call_result, &call_result.state_changeset);
897 let pre_merge_edges_hash = if assertion_failure {
898 error::snapshot_edge_fingerprint(&call_result)
899 } else {
900 None
901 };
902 if corpus_manager.merge_edge_coverage(&mut call_result) {
904 current_run.new_coverage = true;
905 }
906
907 if discarded {
908 current_run.inputs.pop();
909 current_run.rejects += 1;
910 if current_run.rejects > config.max_assume_rejects {
911 invariant_test.set_error(
912 invariant_contract.anchor(),
913 InvariantFuzzError::MaxAssumeRejects(config.max_assume_rejects),
914 );
915 campaign_state.request_terminal_stop();
916 break 'stop;
917 }
918 } else {
919 current_run.executor.commit(&mut call_result);
921
922 let mut state_changeset = std::mem::take(&mut call_result.state_changeset);
930 if !call_result.reverted {
931 let mapping_slots = current_run
932 .executor
933 .inspector()
934 .fuzzer
935 .as_ref()
936 .and_then(|fuzzer| fuzzer.mapping_slots.as_ref());
937 collect_data(
938 &invariant_test,
939 &mut state_changeset,
940 current_run.inputs.last().expect("checked above"),
941 &call_result,
942 config.depth,
943 mapping_slots,
944 );
945 }
946
947 if let Err(error) =
950 &invariant_test.targeted_contracts.collect_created_contracts(
951 &state_changeset,
952 project_contracts,
953 setup_contracts,
954 &campaign_seed.artifact_filters,
955 &mut current_run.created_contracts,
956 )
957 {
958 warn!(target: "forge::test", "{error}");
959 }
960 current_run
961 .fuzz_runs
962 .push(FuzzCase { gas: call_result.gas_used, stipend: call_result.stipend });
963 campaign_state.record_call(call_result.gas_used);
964
965 let is_last_call = current_run.depth == config.depth - 1;
971 let is_optimization = invariant_contract.is_optimization();
975 let should_check_invariant = is_optimization
976 || if config.check_interval == 0 {
977 is_last_call
978 } else {
979 config.check_interval == 1
980 || (current_run.depth + 1).is_multiple_of(config.check_interval)
981 || is_last_call
982 };
983
984 let errors_before_check = invariant_test.test_data.failures.invariant_count();
985 let (continues, broken) = if should_check_invariant {
986 let outcome = can_continue(
987 &invariant_contract,
988 &mut invariant_test,
989 &mut current_run,
990 &config,
991 call_result,
992 &state_changeset,
993 handler_target,
994 handler_selector,
995 pre_merge_edges_hash,
996 )
997 .map_err(|e| eyre!(e.to_string()))?;
998 (outcome.continues, outcome.broken)
999 } else {
1000 if call_result.reverted {
1002 invariant_test.test_data.failures.reverts += 1;
1003 }
1004 if assertion_failure {
1005 let call_reverted = call_result.reverted;
1008 error::record_handler_assertion_bug(
1009 &invariant_contract,
1010 &config,
1011 &invariant_test.targeted_contracts,
1012 &mut invariant_test.test_data.failures,
1013 &mut current_run.inputs,
1014 handler_target,
1015 handler_selector,
1016 pre_merge_edges_hash,
1017 call_result,
1018 call_reverted,
1019 invariant_contract.is_optimization(),
1020 );
1021 (true, None)
1022 } else if call_result.reverted && config.fail_on_revert {
1023 let anchor = invariant_contract.anchor();
1025 let case_data = error::InvariantRunCtx {
1026 contract: &invariant_contract,
1027 config: &config,
1028 targeted_contracts: &invariant_test.targeted_contracts,
1029 calldata: ¤t_run.inputs,
1030 }
1031 .failed_case(
1032 anchor,
1033 config.fail_on_revert,
1034 false,
1035 call_result,
1036 &[],
1037 );
1038 invariant_test
1039 .test_data
1040 .failures
1041 .record_failure(anchor, InvariantFuzzError::Revert(case_data));
1042 (false, Some(anchor))
1043 } else if call_result.reverted
1044 && !invariant_contract.is_optimization()
1045 && !config.has_delay()
1046 {
1047 current_run.inputs.pop();
1049 (true, None)
1050 } else {
1051 (true, None)
1052 }
1053 };
1054
1055 if current_run.cmp_seq.len() < current_run.inputs.len() {
1058 current_run.cmp_seq.push(call_cmp_values);
1059 }
1060
1061 if !continues || current_run.depth == config.depth - 1 {
1062 invariant_test.set_last_run_inputs(¤t_run.inputs);
1063 }
1064 if invariant_test.test_data.failures.invariant_count() > errors_before_check
1067 || broken.is_some()
1068 {
1069 record_new_invariant_failures(
1070 campaign_state,
1071 &invariant_contract,
1072 &invariant_test.test_data.failures,
1073 );
1074 }
1075 if !continues {
1076 if invariant_contract.invariant_fns.len() > 1 && !config.fail_on_revert {
1077 break;
1078 }
1079 campaign_state.request_terminal_stop();
1080 stop_after_run = true;
1081 break;
1082 }
1083 current_run.depth += 1;
1084 }
1085
1086 current_run.inputs.push(corpus_manager.generate_next_input(
1087 &mut invariant_test.test_data.branch_runner,
1088 &initial_seq,
1089 discarded,
1090 current_run.depth as usize,
1091 )?);
1092 }
1093
1094 let optimization = current_run.optimization_value.map(|v| {
1098 let prefix = current_run.inputs[..current_run.optimization_prefix_len].to_vec();
1099 (v, prefix)
1100 });
1101 if corpus_persistence.is_deferred() {
1102 if let Some(input) = corpus_manager.process_inputs_for_campaign(
1103 ¤t_run.inputs,
1104 ¤t_run.cmp_seq,
1105 current_run.new_coverage,
1106 optimization,
1107 ) {
1108 corpus_entries.push(input);
1109 }
1110 } else {
1111 corpus_manager.process_inputs(
1112 ¤t_run.inputs,
1113 ¤t_run.cmp_seq,
1114 current_run.new_coverage,
1115 optimization,
1116 );
1117 }
1118
1119 if invariant_contract.call_after_invariant
1123 && invariant_test.test_data.failures.invariant_count() == failures_before_run
1124 {
1125 let broken = assert_after_invariant(
1126 &invariant_contract,
1127 &mut invariant_test,
1128 ¤t_run,
1129 &config,
1130 )
1131 .map_err(|_| eyre!("Failed to call afterInvariant"))?;
1132 if broken.is_some() {
1133 record_new_invariant_failures(
1135 campaign_state,
1136 &invariant_contract,
1137 &invariant_test.test_data.failures,
1138 );
1139 }
1140 }
1141
1142 current_run.drop_corpus_payloads();
1144 invariant_test.end_run(current_run, gas_report_samples);
1145 runs += 1;
1146 let total_runs = campaign_state.increment_runs();
1147 debug_assert!(
1148 campaign_state.is_timed_campaign() || total_runs <= config.runs,
1149 "worker runs were not distributed correctly"
1150 );
1151 if let Some(progress) = progress {
1152 progress.inc(1);
1153 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1154 let best = invariant_test.test_data.optimization_best_value;
1156 let failure_metrics = campaign_state.failure_metrics();
1157 let broken = failure_metrics.unique_failures.len();
1158 let handler_bugs = failure_metrics.broken_handlers;
1159 let total_invariants = invariant_contract.invariant_fns.len();
1160 if edge_coverage_enabled || best.is_some() || broken > 0 || handler_bugs > 0 {
1161 let mut msg = String::new();
1162 if let Some(best) = best {
1163 msg.push_str(&format!("best: {best}"));
1164 }
1165 if edge_coverage_enabled {
1166 if !msg.is_empty() {
1167 msg.push_str(", ");
1168 }
1169 msg.push_str(&format!("{}", corpus_manager.metrics));
1170 }
1171 if broken > 0 {
1172 if !msg.is_empty() {
1173 msg.push_str(", ");
1174 }
1175 msg.push_str(&format!("❌ {broken}/{total_invariants} broken"));
1176 }
1177 if handler_bugs > 0 {
1178 if !msg.is_empty() {
1179 msg.push_str(", ");
1180 }
1181 msg.push_str(&format!("⚠ {handler_bugs} handler bug(s)"));
1182 }
1183 let msg = if corpus_persistence.is_deferred() {
1184 format!("[w{}] {msg}", plan.worker_id)
1185 } else {
1186 msg
1187 };
1188 progress.set_message(msg);
1189 }
1190 } else if edge_coverage_enabled
1191 && campaign_state.should_emit_metrics_report(DURATION_BETWEEN_METRICS_REPORT)
1192 {
1193 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1194 let failure_metrics = campaign_state.failure_metrics();
1195 let (total_txs, total_gas) = campaign_state.throughput_totals();
1196 let throughput = InvariantThroughputMetrics { total_txs, total_gas };
1197 let metrics = build_invariant_progress_json(
1199 SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
1200 &invariant_contract.anchor().name,
1201 &corpus_manager.metrics,
1202 invariant_test.test_data.optimization_best_value,
1203 throughput,
1204 &failure_metrics,
1205 campaign_state.elapsed(),
1206 );
1207 let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
1208 }
1209
1210 if stop_after_run {
1211 break 'stop;
1212 }
1213 }
1214
1215 trace!(?fuzz_fixtures);
1216 invariant_test.fuzz_state.log_stats();
1217
1218 Self::shrink_handler_failures(
1219 &config,
1220 &executor,
1221 &mut invariant_test.test_data,
1222 progress,
1223 campaign_state.early_exit(),
1224 );
1225
1226 let InvariantTest { fuzz_state: _, targeted_contracts: _, test_data: result } =
1230 invariant_test;
1231 let reverts = result.failures.reverts;
1232 let (errors, handler_errors) = result.failures.partition();
1233 let worker_result = InvariantFuzzTestResult::new(
1234 errors,
1235 handler_errors,
1236 result.runs,
1237 result.calls,
1238 reverts,
1239 result.last_run_inputs,
1240 result.gas_report_traces,
1241 result.line_coverage,
1242 result.metrics,
1243 if plan.worker_id == 0 { corpus_manager.failed_replays } else { 0 },
1244 1,
1245 result.optimization_best_value,
1246 result.optimization_best_sequence,
1247 );
1248 drop(corpus_manager);
1249 let reported_plan = if campaign_state.is_timed_campaign() {
1250 InvariantWorkerPlan { runs, ..plan }
1251 } else {
1252 plan
1256 };
1257 Ok(InvariantWorkerOutput { plan: reported_plan, result: worker_result, corpus_entries })
1258 }
1259
1260 fn shrink_handler_failures(
1261 config: &InvariantConfig,
1262 executor: &Executor<FEN>,
1263 result: &mut InvariantTestData,
1264 progress: Option<&ProgressBar>,
1265 early_exit: &EarlyExit,
1266 ) {
1267 let total = result.failures.handler_count();
1268 if total == 0 {
1269 return;
1270 }
1271
1272 for (idx, error) in result.failures.handler_failures_mut().enumerate() {
1273 if early_exit.should_stop() {
1274 break;
1275 }
1276 let Some(failure) = error.as_handler_assertion_mut() else {
1277 continue;
1278 };
1279 shrink::reset_shrink_progress(
1280 config,
1281 progress,
1282 &format!("handler {:#x}::{}", failure.reverter, failure.selector),
1283 Some((idx + 1, total)),
1284 );
1285 match shrink::shrink_handler_sequence(
1286 config,
1287 &failure.call_sequence,
1288 failure.edge_fingerprint,
1289 executor,
1290 progress,
1291 early_exit,
1292 ) {
1293 Ok(shrunk) if !shrunk.is_empty() => {
1294 failure.call_sequence = shrunk;
1295 }
1296 Ok(_) => {}
1297 Err(e) => trace!(target: "forge::test", "handler shrink failed: {e}"),
1298 }
1299 }
1300 }
1301
1302 fn prepare_campaign_seed(
1303 &mut self,
1304 invariant_contract: &InvariantContract<'_>,
1305 initial_handler_failures: std::collections::HashMap<
1306 (Address, Selector),
1307 InvariantFuzzError,
1308 >,
1309 ) -> Result<InvariantCampaignSeed> {
1310 self.select_contract_artifacts(invariant_contract.address)?;
1311 let (sender_filters, targeted_contracts) =
1312 self.select_contracts_and_senders(invariant_contract.address)?;
1313 let targets_are_updatable = targeted_contracts.is_updatable;
1314 let targeted_contracts = targeted_contracts.targets().clone();
1315
1316 Ok(InvariantCampaignSeed {
1317 artifact_filters: self.artifact_filters.clone(),
1318 sender_filters,
1319 targeted_contracts,
1320 targets_are_updatable,
1321 initial_handler_failures,
1322 })
1323 }
1324
1325 #[allow(clippy::too_many_arguments)]
1327 fn prepare_worker(
1328 executor: &mut Executor<FEN>,
1329 plan: InvariantWorkerPlan,
1330 invariant_contract: &InvariantContract<'_>,
1331 fuzz_fixtures: &FuzzFixtures,
1332 fuzz_state: EvmFuzzState,
1333 runner: &TestRunner,
1334 config: &InvariantConfig,
1335 campaign_seed: &InvariantCampaignSeed,
1336 corpus_seed: WorkerCorpusSeed,
1337 ) -> Result<(InvariantTest, WorkerCorpus)> {
1338 let fuzz_state = fuzz_state.into_invariant();
1339 let targeted_contracts = FuzzRunIdentifiedContracts::new(
1340 campaign_seed.targeted_contracts.clone(),
1341 campaign_seed.targets_are_updatable,
1342 );
1343
1344 let strategy = invariant_strat(
1346 fuzz_state.clone(),
1347 campaign_seed.sender_filters.clone(),
1348 targeted_contracts.clone(),
1349 config.clone(),
1350 fuzz_fixtures.clone(),
1351 )
1352 .no_shrink();
1353
1354 let mapping_slots = targeted_contracts
1357 .targets()
1358 .iter()
1359 .any(|(_, t)| t.storage_layout.is_some())
1360 .then(AddressMap::default);
1361
1362 executor.inspector_mut().set_fuzzer(Fuzzer {
1366 call_generator: None,
1367 collected_values: Vec::new(),
1368 max_collected_values: config.dictionary.max_fuzz_dictionary_values,
1369 mapping_slots,
1370 collect: true,
1371 });
1372
1373 let mut failures = InvariantFailures::new();
1378 for (&(addr, sel), err) in &campaign_seed.initial_handler_failures {
1380 failures.seed_handler_failure(addr, sel, err.clone());
1381 }
1382 invariant_preflight_check(
1383 invariant_contract,
1384 config,
1385 &targeted_contracts,
1386 executor,
1387 &[],
1388 &mut failures,
1389 )?;
1390 if let Some(fuzzer) = executor.inspector_mut().fuzzer.as_mut() {
1391 fuzz_state.collect_values(fuzzer.drain_collected_values());
1392 }
1393 if config.call_override {
1397 let target_contract_ref = Arc::new(RwLock::new(Address::ZERO));
1398
1399 let handler_addresses: std::collections::HashSet<Address> =
1402 targeted_contracts.targets().keys().copied().collect();
1403 let override_targets = targeted_contracts
1404 .targets()
1405 .iter()
1406 .filter_map(|(address, contract)| {
1407 let functions = contract.abi_fuzzed_functions().cloned().collect::<Vec<_>>();
1408 (!functions.is_empty()).then_some((*address, functions))
1409 })
1410 .collect::<Vec<_>>();
1411
1412 let call_generator = RandomCallGenerator::new(
1413 invariant_contract.address,
1414 handler_addresses,
1415 runner.clone(),
1416 override_call_strat(
1417 fuzz_state.snapshot(),
1418 override_targets,
1419 target_contract_ref.clone(),
1420 fuzz_fixtures.clone(),
1421 ),
1422 target_contract_ref,
1423 );
1424
1425 if let Some(fuzzer) = executor.inspector_mut().fuzzer.as_mut() {
1426 fuzzer.call_generator = Some(call_generator);
1427 }
1428 }
1429
1430 let worker = WorkerCorpus::from_seed(
1431 plan.worker_id as usize,
1432 config.corpus.clone(),
1433 strategy.boxed(),
1434 corpus_seed,
1435 );
1436
1437 let mut invariant_test =
1438 InvariantTest::new(fuzz_state, targeted_contracts, failures, runner.clone());
1439
1440 if invariant_contract.is_optimization() {
1444 let (opt_best_value, opt_best_sequence) = worker.optimization_initial_state();
1445 if let Some(value) = opt_best_value {
1446 invariant_test.update_optimization_value(value, &opt_best_sequence);
1447 }
1448 }
1449
1450 Ok((invariant_test, worker))
1451 }
1452
1453 pub fn select_contract_artifacts(&mut self, invariant_address: Address) -> Result<()> {
1463 let targeted_artifact_selectors = self
1464 .executor
1465 .call_sol_default(invariant_address, &IInvariantTest::targetArtifactSelectorsCall {});
1466
1467 for IInvariantTest::FuzzArtifactSelector { artifact, selectors } in
1469 targeted_artifact_selectors
1470 {
1471 let identifier = self.validate_selected_contract(artifact, &selectors)?;
1472 self.artifact_filters.targeted.entry(identifier).or_default().extend(selectors);
1473 }
1474
1475 let targeted_artifacts = self
1476 .executor
1477 .call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {});
1478 let excluded_artifacts = self
1479 .executor
1480 .call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {});
1481
1482 for contract in excluded_artifacts {
1484 let identifier = self.validate_selected_contract(contract, &[])?;
1485
1486 if !self.artifact_filters.excluded.contains(&identifier) {
1487 self.artifact_filters.excluded.push(identifier);
1488 }
1489 }
1490
1491 for (artifact, contract) in self.project_contracts.iter() {
1493 if contract
1494 .abi
1495 .functions()
1496 .filter(|func| {
1497 !matches!(
1498 func.state_mutability,
1499 alloy_json_abi::StateMutability::Pure
1500 | alloy_json_abi::StateMutability::View
1501 )
1502 })
1503 .count()
1504 == 0
1505 && !self.artifact_filters.excluded.contains(&artifact.identifier())
1506 {
1507 self.artifact_filters.excluded.push(artifact.identifier());
1508 }
1509 }
1510
1511 for contract in targeted_artifacts {
1514 let identifier = self.validate_selected_contract(contract, &[])?;
1515
1516 if !self.artifact_filters.targeted.contains_key(&identifier)
1517 && !self.artifact_filters.excluded.contains(&identifier)
1518 {
1519 self.artifact_filters.targeted.insert(identifier, vec![]);
1520 }
1521 }
1522 Ok(())
1523 }
1524
1525 fn validate_selected_contract(
1528 &mut self,
1529 contract: String,
1530 selectors: &[FixedBytes<4>],
1531 ) -> Result<String> {
1532 if let Some((artifact, contract_data)) =
1533 self.project_contracts.find_by_name_or_identifier(&contract)?
1534 {
1535 for selector in selectors {
1537 contract_data
1538 .abi
1539 .functions()
1540 .find(|func| func.selector().as_slice() == selector.as_slice())
1541 .wrap_err(format!("{contract} does not have the selector {selector:?}"))?;
1542 }
1543
1544 return Ok(artifact.identifier());
1545 }
1546 eyre::bail!(
1547 "{contract} not found in the project. Allowed format: `contract_name` or `contract_path:contract_name`."
1548 );
1549 }
1550
1551 pub fn select_contracts_and_senders(
1554 &self,
1555 to: Address,
1556 ) -> Result<(SenderFilters, FuzzRunIdentifiedContracts)> {
1557 let targeted_senders =
1558 self.executor.call_sol_default(to, &IInvariantTest::targetSendersCall {});
1559 let mut excluded_senders =
1560 self.executor.call_sol_default(to, &IInvariantTest::excludeSendersCall {});
1561 excluded_senders.extend([
1563 CHEATCODE_ADDRESS,
1564 HARDHAT_CONSOLE_ADDRESS,
1565 DEFAULT_CREATE2_DEPLOYER,
1566 ]);
1567 excluded_senders.extend(PRECOMPILES);
1569 let sender_filters = SenderFilters::new(targeted_senders, excluded_senders);
1570
1571 let selected = self.executor.call_sol_default(to, &IInvariantTest::targetContractsCall {});
1572 let excluded = self.executor.call_sol_default(to, &IInvariantTest::excludeContractsCall {});
1573
1574 let contracts = self
1575 .setup_contracts
1576 .iter()
1577 .filter(|&(addr, (identifier, _))| {
1578 if *addr == to && selected.contains(&to) {
1580 return true;
1581 }
1582
1583 *addr != to
1584 && *addr != CHEATCODE_ADDRESS
1585 && *addr != HARDHAT_CONSOLE_ADDRESS
1586 && (selected.is_empty() || selected.contains(addr))
1587 && (excluded.is_empty() || !excluded.contains(addr))
1588 && self.artifact_filters.matches(identifier)
1589 })
1590 .map(|(addr, (identifier, abi))| {
1591 (
1592 *addr,
1593 TargetedContract::new(identifier.clone(), abi.clone())
1594 .with_project_contracts(self.project_contracts),
1595 )
1596 })
1597 .collect();
1598 let mut contracts = TargetedContracts { inner: contracts };
1599
1600 self.target_interfaces(to, &mut contracts)?;
1601
1602 self.select_selectors(to, &mut contracts)?;
1603
1604 if contracts.is_empty() {
1606 eyre::bail!("No contracts to fuzz.");
1607 }
1608
1609 Ok((sender_filters, FuzzRunIdentifiedContracts::new(contracts, selected.is_empty())))
1610 }
1611
1612 pub fn target_interfaces(
1617 &self,
1618 invariant_address: Address,
1619 targeted_contracts: &mut TargetedContracts,
1620 ) -> Result<()> {
1621 let interfaces = self
1622 .executor
1623 .call_sol_default(invariant_address, &IInvariantTest::targetInterfacesCall {});
1624
1625 let mut combined = TargetedContracts::new();
1631
1632 for IInvariantTest::FuzzInterface { addr, artifacts } in &interfaces {
1635 for identifier in artifacts {
1637 if let Some((_, contract_data)) =
1639 self.project_contracts.iter().find(|(artifact, _)| {
1640 &artifact.name == identifier || &artifact.identifier() == identifier
1641 })
1642 {
1643 let abi = &contract_data.abi;
1644 combined
1645 .entry(*addr)
1647 .and_modify(|entry| {
1649 entry.abi.functions.extend(abi.functions.clone());
1651 })
1652 .or_insert_with(|| {
1654 let mut contract =
1655 TargetedContract::new(identifier.clone(), abi.clone());
1656 contract.storage_layout =
1657 contract_data.storage_layout.as_ref().map(Arc::clone);
1658 contract
1659 });
1660 }
1661 }
1662 }
1663
1664 targeted_contracts.extend(combined.inner);
1665
1666 Ok(())
1667 }
1668
1669 pub fn select_selectors(
1672 &self,
1673 address: Address,
1674 targeted_contracts: &mut TargetedContracts,
1675 ) -> Result<()> {
1676 for (address, (identifier, _)) in self.setup_contracts {
1677 if let Some(selectors) = self.artifact_filters.targeted.get(identifier) {
1678 self.add_address_with_functions(*address, selectors, false, targeted_contracts)?;
1679 }
1680 }
1681
1682 let mut target_test_selectors = vec![];
1683 let mut excluded_test_selectors = vec![];
1684
1685 let selectors =
1687 self.executor.call_sol_default(address, &IInvariantTest::targetSelectorsCall {});
1688 for IInvariantTest::FuzzSelector { addr, selectors } in selectors {
1689 if addr == address {
1690 target_test_selectors = selectors.clone();
1691 }
1692 self.add_address_with_functions(addr, &selectors, false, targeted_contracts)?;
1693 }
1694
1695 let excluded_selectors =
1697 self.executor.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {});
1698 for IInvariantTest::FuzzSelector { addr, selectors } in excluded_selectors {
1699 if addr == address {
1700 excluded_test_selectors = selectors.clone();
1703 }
1704 self.add_address_with_functions(addr, &selectors, true, targeted_contracts)?;
1705 }
1706
1707 if target_test_selectors.is_empty()
1708 && let Some(target) = targeted_contracts.get(&address)
1709 {
1710 let selectors: Vec<_> = target
1714 .abi
1715 .functions()
1716 .filter_map(|func| {
1717 if matches!(
1718 func.state_mutability,
1719 alloy_json_abi::StateMutability::Pure
1720 | alloy_json_abi::StateMutability::View
1721 ) || func.is_reserved()
1722 || excluded_test_selectors.contains(&func.selector())
1723 {
1724 None
1725 } else {
1726 Some(func.selector())
1727 }
1728 })
1729 .collect();
1730 self.add_address_with_functions(address, &selectors, false, targeted_contracts)?;
1731 }
1732
1733 Ok(())
1734 }
1735
1736 fn add_address_with_functions(
1738 &self,
1739 address: Address,
1740 selectors: &[Selector],
1741 should_exclude: bool,
1742 targeted_contracts: &mut TargetedContracts,
1743 ) -> eyre::Result<()> {
1744 if selectors.is_empty() {
1746 return Ok(());
1747 }
1748
1749 let contract = match targeted_contracts.entry(address) {
1750 Entry::Occupied(entry) => entry.into_mut(),
1751 Entry::Vacant(entry) => {
1752 let (identifier, abi) = self.setup_contracts.get(&address).ok_or_else(|| {
1753 eyre::eyre!(
1754 "[{}] address does not have an associated contract: {}",
1755 if should_exclude { "excludeSelectors" } else { "targetSelectors" },
1756 address
1757 )
1758 })?;
1759 entry.insert(
1760 TargetedContract::new(identifier.clone(), abi.clone())
1761 .with_project_contracts(self.project_contracts),
1762 )
1763 }
1764 };
1765 contract.add_selectors(selectors.iter().copied(), should_exclude)?;
1766 Ok(())
1767 }
1768
1769 pub fn compute_settings(&mut self, invariant_address: Address) -> Result<InvariantSettings> {
1774 self.select_contract_artifacts(invariant_address)?;
1775 let (sender_filters, targeted_contracts) =
1776 self.select_contracts_and_senders(invariant_address)?;
1777 let targets = targeted_contracts.targets();
1778 Ok(InvariantSettings::new(&targets, &sender_filters, self.config.fail_on_revert))
1779 }
1780}
1781
1782fn collect_data<FEN: FoundryEvmNetwork>(
1786 invariant_test: &InvariantTest,
1787 state_changeset: &mut AddressMap<Account>,
1788 tx: &BasicTxDetails,
1789 call_result: &RawCallResult<FEN>,
1790 run_depth: u32,
1791 mapping_slots: Option<&AddressMap<foundry_common::mapping_slots::MappingSlots>>,
1792) {
1793 let has_code = if let Some(Some(code)) =
1795 state_changeset.get(&tx.sender).map(|account| account.info.code.as_ref())
1796 {
1797 !code.is_empty()
1798 } else {
1799 false
1800 };
1801
1802 let sender_changeset = if has_code { None } else { state_changeset.remove(&tx.sender) };
1804
1805 invariant_test.fuzz_state.collect_values_from_call(
1807 &invariant_test.targeted_contracts,
1808 tx,
1809 &call_result.result,
1810 &call_result.logs,
1811 &*state_changeset,
1812 run_depth,
1813 mapping_slots,
1814 );
1815
1816 if let Some(cmp_values) = &call_result.sancov_cmp_values {
1818 invariant_test.fuzz_state.collect_typed_cmp_values(
1819 cmp_values.iter().map(|s| (s.width, alloy_primitives::B256::from(s.value))),
1820 );
1821 }
1822 if let Some(changed) = sender_changeset {
1824 state_changeset.insert(tx.sender, changed);
1825 }
1826}
1827
1828pub(crate) fn call_after_invariant_function<FEN: FoundryEvmNetwork>(
1836 executor: &Executor<FEN>,
1837 to: Address,
1838) -> Result<(RawCallResult<FEN>, bool), EvmError<FEN>> {
1839 let calldata = Bytes::from_static(&IInvariantTest::afterInvariantCall::SELECTOR);
1840 let mut call_result = executor.call_raw(CALLER, to, calldata, U256::ZERO)?;
1841 let success = executor.is_raw_call_mut_success_handler_gate(to, &mut call_result);
1842 Ok((call_result, success))
1843}
1844
1845pub(crate) fn call_invariant_function<FEN: FoundryEvmNetwork>(
1852 executor: &Executor<FEN>,
1853 address: Address,
1854 calldata: Bytes,
1855) -> Result<(RawCallResult<FEN>, bool)> {
1856 let mut call_result = executor.call_raw(CALLER, address, calldata, U256::ZERO)?;
1857 let success = executor.is_raw_call_mut_success_handler_gate(address, &mut call_result);
1858 Ok((call_result, success))
1859}
1860
1861pub(crate) fn execute_tx<FEN: FoundryEvmNetwork>(
1864 executor: &mut Executor<FEN>,
1865 tx: &BasicTxDetails,
1866) -> Result<RawCallResult<FEN>> {
1867 let warp = tx.warp.unwrap_or_default();
1868 let roll = tx.roll.unwrap_or_default();
1869
1870 if warp > 0 || roll > 0 {
1871 let ts = executor.evm_env().block_env.timestamp();
1873 let num = executor.evm_env().block_env.number();
1874 executor.evm_env_mut().block_env.set_timestamp(ts + warp);
1875 executor.evm_env_mut().block_env.set_number(num + roll);
1876
1877 let block_env = executor.evm_env().block_env.clone();
1881 if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
1882 if let Some(block) = cheatcodes.block.as_mut() {
1883 let bts = block.timestamp();
1884 let bnum = block.number();
1885 block.set_timestamp(bts + warp);
1886 block.set_number(bnum + roll);
1887 } else {
1888 cheatcodes.block = Some(block_env);
1889 }
1890 }
1891 }
1892
1893 let requested_value = tx.call_details.value.unwrap_or(U256::ZERO);
1896 let sender_balance = executor.get_balance(tx.sender)?;
1897 let value = requested_value.min(sender_balance);
1898 executor
1899 .call_raw(tx.sender, tx.call_details.target, tx.call_details.calldata.clone(), value)
1900 .map_err(|e| eyre!(format!("Could not make raw evm call: {e}")))
1901}
1902
1903#[cfg(test)]
1904mod tests {
1905 use super::*;
1906 use proptest::{prelude::any, strategy::ValueTree, test_runner::Config};
1907 use serde_json::json;
1908
1909 fn first_generated_u64(runner: &mut TestRunner) -> u64 {
1910 any::<u64>().new_tree(runner).unwrap().current()
1911 }
1912
1913 fn test_runner() -> TestRunner {
1914 TestRunner::new(Config { failure_persistence: None, ..Default::default() })
1915 }
1916
1917 fn seeded_test_runner(seed: U256) -> TestRunner {
1918 let config = Config { failure_persistence: None, ..Default::default() };
1919 let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
1920 TestRunner::new_with_rng(config, rng)
1921 }
1922
1923 #[test]
1924 fn invariant_worker_seed_preserves_master_seed_and_derives_workers() {
1925 let seed = U256::from(0x1234);
1926
1927 assert_eq!(invariant_worker_seed(seed, 0), seed);
1928 assert_ne!(invariant_worker_seed(seed, 1), seed);
1929 assert_ne!(invariant_worker_seed(seed, 1), invariant_worker_seed(seed, 2));
1930 assert_ne!(invariant_worker_seed(seed, 1), invariant_worker_seed(U256::from(0x5678), 1));
1931 }
1932
1933 #[test]
1934 fn invariant_worker_runner_preserves_seed_for_master_worker() {
1935 let seed = U256::from(0x1234);
1936 let mut seeded_runner = seeded_test_runner(seed);
1937 let mut parent = test_runner();
1938 let mut worker = invariant_worker_runner(&mut parent, 0, Some(seed));
1939
1940 assert_eq!(first_generated_u64(&mut worker), first_generated_u64(&mut seeded_runner));
1941 }
1942
1943 #[test]
1944 fn invariant_worker_runner_uses_seed_independent_of_parent_rng_state() {
1945 let seed = U256::from(0x1234);
1946 let mut parent = test_runner();
1947 let mut advanced_parent = test_runner();
1948 let _ = first_generated_u64(&mut advanced_parent);
1949
1950 let mut worker = invariant_worker_runner(&mut parent, 1, Some(seed));
1951 let mut worker_from_advanced_parent =
1952 invariant_worker_runner(&mut advanced_parent, 1, Some(seed));
1953
1954 assert_eq!(
1955 first_generated_u64(&mut worker),
1956 first_generated_u64(&mut worker_from_advanced_parent)
1957 );
1958 }
1959
1960 #[test]
1961 fn invariant_progress_json_includes_throughput_fields() {
1962 let throughput = InvariantThroughputMetrics { total_txs: 2, total_gas: 50 };
1963
1964 let payload = build_invariant_progress_json(
1965 123,
1966 "invariant_balance",
1967 &json!({ "corpus_count": 7 }),
1968 Some(I256::try_from(42).unwrap()),
1969 throughput,
1970 &InvariantFailureMetrics::default(),
1971 Duration::from_secs(10),
1972 );
1973
1974 assert_eq!(payload["timestamp"], json!(123));
1975 assert_eq!(payload["invariant"], json!("invariant_balance"));
1976 assert_eq!(payload["metrics"]["corpus_count"], json!(7));
1977 assert_eq!(payload["metrics"]["broken_handlers"], json!(0));
1978 assert_eq!(payload["total_txs"], json!(2));
1979 assert_eq!(payload["total_gas"], json!(50));
1980 assert!((payload["tx_per_sec"].as_f64().unwrap() - 0.2).abs() < 1e-12);
1981 assert!((payload["gas_per_sec"].as_f64().unwrap() - 5.0).abs() < 1e-12);
1982 assert_eq!(payload["optimization_best"], json!("42"));
1983 }
1984
1985 #[test]
1986 fn invariant_worker_count_keeps_short_campaigns_single_worker() {
1987 assert_eq!(
1988 max_invariant_workers_for_campaign(0, DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP),
1989 1
1990 );
1991 assert_eq!(
1992 max_invariant_workers_for_campaign(
1993 MIN_RUNS_PER_INVARIANT_WORKER - 1,
1994 DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
1995 ),
1996 1
1997 );
1998 assert_eq!(
1999 max_invariant_workers_for_campaign(
2000 MIN_RUNS_PER_INVARIANT_WORKER,
2001 DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2002 ),
2003 1
2004 );
2005 assert_eq!(
2006 max_invariant_workers_for_campaign(
2007 MIN_RUNS_PER_INVARIANT_WORKER * 2,
2008 DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2009 ),
2010 2
2011 );
2012 assert_eq!(max_invariant_workers_for_campaign(256, 100_000), 5);
2013 }
2014
2015 #[test]
2016 fn invariant_worker_count_preserves_fixed_workers() {
2017 let mut config = InvariantConfig {
2018 runs: MIN_RUNS_PER_INVARIANT_WORKER * 4,
2019 workers: foundry_config::InvariantWorkers::Fixed(
2020 std::num::NonZeroUsize::new(4).unwrap(),
2021 ),
2022 ..Default::default()
2023 };
2024 assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2025
2026 config.corpus.show_edge_coverage = true;
2027 assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2028
2029 config.corpus.show_edge_coverage = false;
2030 config.corpus.corpus_dir = Some(std::path::PathBuf::from("corpus"));
2031 assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2032
2033 config.runs = MIN_RUNS_PER_INVARIANT_WORKER - 1;
2034 config.timeout = None;
2035 assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2036
2037 config.timeout = Some(1);
2038 assert_eq!(invariant_worker_count_with_threads(&config, 8, 4), 4);
2039 }
2040
2041 #[test]
2042 fn invariant_worker_count_does_not_cap_configured_workers_by_available_threads() {
2043 let config = InvariantConfig {
2044 runs: MIN_RUNS_PER_INVARIANT_WORKER * 8,
2045 workers: foundry_config::InvariantWorkers::Fixed(
2046 std::num::NonZeroUsize::new(8).unwrap(),
2047 ),
2048 ..Default::default()
2049 };
2050
2051 assert_eq!(invariant_worker_count_with_threads(&config, 4, 1), 8);
2052 }
2053
2054 #[test]
2055 fn invariant_worker_count_splits_available_threads_for_auto_workers() {
2056 let mut config = InvariantConfig {
2057 runs: MIN_RUNS_PER_INVARIANT_WORKER * 4,
2058 depth: DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP,
2059 workers: foundry_config::InvariantWorkers::Auto,
2060 ..Default::default()
2061 };
2062
2063 assert_eq!(invariant_worker_count_with_threads(&config, 4, 1), 4);
2064 assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2065 assert_eq!(invariant_worker_count_with_threads(&config, 8, 3), 2);
2066 assert_eq!(invariant_worker_count_with_threads(&config, 3, 8), 1);
2067 assert_eq!(invariant_worker_count_with_threads(&config, 0, 0), 1);
2068
2069 config.runs = MIN_RUNS_PER_INVARIANT_WORKER - 1;
2070 assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 1);
2071
2072 config.depth = 100_000;
2073 assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2074
2075 config.timeout = Some(1);
2076 assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2077 }
2078
2079 #[test]
2080 fn timed_invariant_workers_are_not_bounded_by_assigned_runs() {
2081 let plan = InvariantWorkerPlan { worker_id: 0, first_global_run: 0, runs: 1 };
2082
2083 let untimed = InvariantCampaignState::new(EarlyExit::new(false), None);
2084 assert!(should_continue_invariant_worker(&untimed, 0, plan));
2085 assert!(!should_continue_invariant_worker(&untimed, 1, plan));
2086
2087 let timed = InvariantCampaignState::new(EarlyExit::new(false), Some(60));
2088 assert!(should_continue_invariant_worker(&timed, 0, plan));
2089 assert!(should_continue_invariant_worker(&timed, 1, plan));
2090 assert!(should_continue_invariant_worker(&timed, 10_000, plan));
2091 }
2092
2093 #[test]
2094 fn gas_report_samples_are_split_across_workers() {
2095 assert_eq!(gas_report_samples_for_worker(0, 0, 4), 0);
2096 assert_eq!(gas_report_samples_for_worker(8, 0, 4), 2);
2097 assert_eq!(gas_report_samples_for_worker(8, 3, 4), 2);
2098 assert_eq!(gas_report_samples_for_worker(10, 0, 4), 3);
2099 assert_eq!(gas_report_samples_for_worker(10, 1, 4), 3);
2100 assert_eq!(gas_report_samples_for_worker(10, 2, 4), 2);
2101 assert_eq!(gas_report_samples_for_worker(10, 3, 4), 2);
2102 assert_eq!(gas_report_samples_for_worker(3, 3, 4), 0);
2103 }
2104
2105 #[test]
2106 fn invariant_progress_json_zero_elapsed_reports_zero_rates() {
2107 let throughput = InvariantThroughputMetrics { total_txs: 1, total_gas: 21_000 };
2108
2109 let payload = build_invariant_progress_json(
2110 456,
2111 "invariant_zero_elapsed",
2112 &json!({ "corpus_count": 1 }),
2113 None,
2114 throughput,
2115 &InvariantFailureMetrics::default(),
2116 Duration::ZERO,
2117 );
2118
2119 assert_eq!(payload["tx_per_sec"], json!(0.0));
2120 assert_eq!(payload["gas_per_sec"], json!(0.0));
2121 assert!(payload.get("optimization_best").is_none());
2122 }
2123
2124 #[test]
2125 fn invariant_progress_json_includes_failure_counts() {
2126 let mut failure_metrics = InvariantFailureMetrics::default();
2127 failure_metrics.record_failure("invariant_a", "TestContract", "revert");
2128 failure_metrics.record_failure("invariant_a", "TestContract", "revert");
2129 failure_metrics.record_failure("invariant_b", "TestContract", "assertion failed");
2130 failure_metrics.broken_handlers = 7;
2131
2132 let payload = build_invariant_progress_json(
2133 789,
2134 "invariant_a",
2135 &json!({ "corpus_count": 5 }),
2136 None,
2137 InvariantThroughputMetrics::default(),
2138 &failure_metrics,
2139 Duration::from_secs(1),
2140 );
2141
2142 assert_eq!(payload["metrics"]["failures"], json!(3));
2143 assert_eq!(payload["metrics"]["unique_failures"], json!(2));
2144 assert_eq!(payload["metrics"]["broken_handlers"], json!(7));
2145 }
2146
2147 #[test]
2148 fn failure_metrics_tracks_total_and_unique_failures() {
2149 let mut metrics = InvariantFailureMetrics::default();
2150 metrics.record_failure("invariant_a", "TestContract", "revert");
2151 metrics.record_failure("invariant_a", "TestContract", "revert");
2152 metrics.record_failure("invariant_b", "TestContract", "assertion failed");
2153
2154 assert_eq!(metrics.failures, 3);
2155 assert_eq!(metrics.unique_failures.len(), 2);
2156 assert!(metrics.unique_failures.contains("invariant_a"));
2157 assert!(metrics.unique_failures.contains("invariant_b"));
2158 }
2159
2160 #[test]
2161 fn failure_metrics_default_is_zero() {
2162 let metrics = InvariantFailureMetrics::default();
2163 assert_eq!(metrics.failures, 0);
2164 assert!(metrics.unique_failures.is_empty());
2165 assert_eq!(metrics.broken_handlers, 0);
2166 }
2167}