1use crate::{
2 executors::{
3 DURATION_BETWEEN_METRICS_REPORT, EarlyExit, EvmError, Executor, FuzzTestTimer,
4 RawCallResult, corpus::WorkerCorpus,
5 },
6 inspectors::Fuzzer,
7};
8use alloy_primitives::{Address, Bytes, FixedBytes, I256, Selector, U256, map::AddressMap};
9use alloy_sol_types::{SolCall, sol};
10use eyre::{ContextCompat, Result, eyre};
11use foundry_common::{
12 TestFunctionExt,
13 contracts::{ContractsByAddress, ContractsByArtifact},
14 sh_eprintln, sh_println,
15};
16use foundry_config::InvariantConfig;
17use foundry_evm_core::{
18 FoundryBlock,
19 constants::{
20 CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS, MAGIC_ASSUME,
21 },
22 evm::FoundryEvmNetwork,
23 precompiles::PRECOMPILES,
24};
25use foundry_evm_fuzz::{
26 BasicTxDetails, FuzzCase, FuzzFixtures, FuzzedCases,
27 invariant::{
28 ArtifactFilters, FuzzRunIdentifiedContracts, InvariantContract, InvariantSettings,
29 RandomCallGenerator, SenderFilters, TargetedContract, TargetedContracts,
30 },
31 strategies::{EvmFuzzState, invariant_strat, override_call_strat},
32};
33use foundry_evm_traces::{CallTraceArena, SparsedTraceArena};
34use indicatif::ProgressBar;
35use parking_lot::RwLock;
36use proptest::{strategy::Strategy, test_runner::TestRunner};
37use result::{assert_after_invariant, assert_invariants, can_continue, did_fail_on_assert};
38use revm::{context::Block, state::Account};
39use serde::{Deserialize, Serialize};
40use serde_json::json;
41use std::{
42 collections::{HashMap as Map, HashSet, btree_map::Entry},
43 sync::Arc,
44 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
45};
46
47mod error;
48pub use error::{InvariantFailures, InvariantFuzzError};
49use foundry_evm_coverage::HitMaps;
50
51mod replay;
52pub use replay::{replay_error, replay_run};
53
54mod result;
55pub use result::InvariantFuzzTestResult;
56
57mod shrink;
58pub use shrink::{CheckSequenceOptions, check_sequence, check_sequence_value};
59
60sol! {
61 interface IInvariantTest {
62 #[derive(Default)]
63 struct FuzzSelector {
64 address addr;
65 bytes4[] selectors;
66 }
67
68 #[derive(Default)]
69 struct FuzzArtifactSelector {
70 string artifact;
71 bytes4[] selectors;
72 }
73
74 #[derive(Default)]
75 struct FuzzInterface {
76 address addr;
77 string[] artifacts;
78 }
79
80 function afterInvariant() external;
81
82 #[derive(Default)]
83 function excludeArtifacts() public view returns (string[] memory excludedArtifacts);
84
85 #[derive(Default)]
86 function excludeContracts() public view returns (address[] memory excludedContracts);
87
88 #[derive(Default)]
89 function excludeSelectors() public view returns (FuzzSelector[] memory excludedSelectors);
90
91 #[derive(Default)]
92 function excludeSenders() public view returns (address[] memory excludedSenders);
93
94 #[derive(Default)]
95 function targetArtifacts() public view returns (string[] memory targetedArtifacts);
96
97 #[derive(Default)]
98 function targetArtifactSelectors() public view returns (FuzzArtifactSelector[] memory targetedArtifactSelectors);
99
100 #[derive(Default)]
101 function targetContracts() public view returns (address[] memory targetedContracts);
102
103 #[derive(Default)]
104 function targetSelectors() public view returns (FuzzSelector[] memory targetedSelectors);
105
106 #[derive(Default)]
107 function targetSenders() public view returns (address[] memory targetedSenders);
108
109 #[derive(Default)]
110 function targetInterfaces() public view returns (FuzzInterface[] memory targetedInterfaces);
111 }
112}
113
114#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
116pub struct InvariantMetrics {
117 pub calls: usize,
119 pub reverts: usize,
121 pub discards: usize,
123}
124
125#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
127struct InvariantThroughputMetrics {
128 total_txs: u64,
129 total_gas: u64,
130}
131
132impl InvariantThroughputMetrics {
133 const fn record_call(&mut self, gas_used: u64) {
134 self.total_txs += 1;
135 self.total_gas += gas_used;
136 }
137
138 fn tx_per_sec(self, elapsed: Duration) -> f64 {
139 rate_per_sec(self.total_txs as f64, elapsed)
140 }
141
142 fn gas_per_sec(self, elapsed: Duration) -> f64 {
143 rate_per_sec(self.total_gas as f64, elapsed)
144 }
145}
146
147fn rate_per_sec(total: f64, elapsed: Duration) -> f64 {
152 let elapsed_secs = elapsed.as_secs_f64();
153 if elapsed_secs > 0.0 { total / elapsed_secs } else { 0.0 }
154}
155
156#[derive(Debug, Default)]
158struct InvariantFailureMetrics {
159 failures: u64,
160 unique_failures: HashSet<String>,
161}
162
163impl InvariantFailureMetrics {
164 fn record_failure(&mut self, invariant_name: &str, target: &str, reason: &str) {
166 self.failures += 1;
167 self.unique_failures.insert(invariant_name.to_string());
168
169 let timestamp =
170 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
171 let event = json!({
172 "timestamp": timestamp,
173 "event": "failure",
174 "invariant": invariant_name,
175 "target": target,
176 "reason": reason,
177 });
178 let _ = sh_eprintln!("{}", serde_json::to_string(&event).unwrap_or_default());
179 }
180}
181
182fn build_invariant_progress_json<M: Serialize>(
189 timestamp_secs: u64,
190 invariant_name: &str,
191 corpus_metrics: &M,
192 optimization_best: Option<I256>,
193 throughput: InvariantThroughputMetrics,
194 failure_metrics: &InvariantFailureMetrics,
195 elapsed: Duration,
196) -> serde_json::Value {
197 let mut metrics = serde_json::to_value(corpus_metrics).unwrap_or_default();
198 if let Some(obj) = metrics.as_object_mut() {
199 obj.insert("failures".to_string(), json!(failure_metrics.failures));
200 obj.insert("unique_failures".to_string(), json!(failure_metrics.unique_failures.len()));
201 }
202
203 let mut payload = json!({
204 "timestamp": timestamp_secs,
205 "event": "pulse",
206 "invariant": invariant_name,
207 "metrics": metrics,
208 "total_txs": throughput.total_txs,
209 "total_gas": throughput.total_gas,
210 "tx_per_sec": throughput.tx_per_sec(elapsed),
211 "gas_per_sec": throughput.gas_per_sec(elapsed),
212 });
213
214 if let Some(best) = optimization_best {
215 payload["optimization_best"] = json!(best.to_string());
216 }
217
218 payload
219}
220
221struct InvariantTestData<FEN: FoundryEvmNetwork> {
223 fuzz_cases: Vec<FuzzedCases>,
225 failures: InvariantFailures,
227 last_run_inputs: Vec<BasicTxDetails>,
229 gas_report_traces: Vec<Vec<CallTraceArena>>,
231 last_call_results: Option<RawCallResult<FEN>>,
233 line_coverage: Option<HitMaps>,
235 metrics: Map<String, InvariantMetrics>,
237
238 branch_runner: TestRunner,
243
244 optimization_best_value: Option<I256>,
247 optimization_best_sequence: Vec<BasicTxDetails>,
248}
249
250struct InvariantTest<FEN: FoundryEvmNetwork> {
252 fuzz_state: EvmFuzzState,
254 targeted_contracts: FuzzRunIdentifiedContracts,
256 test_data: InvariantTestData<FEN>,
258}
259
260impl<FEN: FoundryEvmNetwork> InvariantTest<FEN> {
261 fn new(
263 fuzz_state: EvmFuzzState,
264 targeted_contracts: FuzzRunIdentifiedContracts,
265 failures: InvariantFailures,
266 last_call_results: Option<RawCallResult<FEN>>,
267 branch_runner: TestRunner,
268 ) -> Self {
269 let mut fuzz_cases = vec![];
270 if last_call_results.is_none() {
271 fuzz_cases.push(FuzzedCases::new(vec![]));
272 }
273 let test_data = InvariantTestData {
274 fuzz_cases,
275 failures,
276 last_run_inputs: vec![],
277 gas_report_traces: vec![],
278 last_call_results,
279 line_coverage: None,
280 metrics: Map::default(),
281 branch_runner,
282 optimization_best_value: None,
283 optimization_best_sequence: vec![],
284 };
285 Self { fuzz_state, targeted_contracts, test_data }
286 }
287
288 const fn reverts(&self) -> usize {
290 self.test_data.failures.reverts
291 }
292
293 const fn has_errors(&self) -> bool {
295 self.test_data.failures.error.is_some()
296 }
297
298 fn set_error(&mut self, error: InvariantFuzzError) {
300 self.test_data.failures.error = Some(error);
301 }
302
303 fn set_last_call_results(&mut self, call_result: Option<RawCallResult<FEN>>) {
305 self.test_data.last_call_results = call_result;
306 }
307
308 fn set_last_run_inputs(&mut self, inputs: &Vec<BasicTxDetails>) {
310 self.test_data.last_run_inputs.clone_from(inputs);
311 }
312
313 fn merge_line_coverage(&mut self, new_coverage: Option<HitMaps>) {
315 HitMaps::merge_opt(&mut self.test_data.line_coverage, new_coverage);
316 }
317
318 fn record_metrics(&mut self, tx_details: &BasicTxDetails, reverted: bool, discarded: bool) {
322 if let Some(metric_key) =
323 self.targeted_contracts.targets.lock().fuzzed_metric_key(tx_details)
324 {
325 let test_metrics = &mut self.test_data.metrics;
326 let invariant_metrics = test_metrics.entry(metric_key).or_default();
327 invariant_metrics.calls += 1;
328 if discarded {
329 invariant_metrics.discards += 1;
330 } else if reverted {
331 invariant_metrics.reverts += 1;
332 }
333 }
334 }
335
336 fn end_run(&mut self, run: InvariantTestRun<FEN>, gas_samples: usize) {
339 self.targeted_contracts.clear_created_contracts(run.created_contracts);
341
342 if self.test_data.gas_report_traces.len() < gas_samples {
343 self.test_data
344 .gas_report_traces
345 .push(run.run_traces.into_iter().map(|arena| arena.arena).collect());
346 }
347 self.test_data.fuzz_cases.push(FuzzedCases::new(run.fuzz_runs));
348
349 self.fuzz_state.revert();
351 }
352
353 fn update_optimization_value(&mut self, value: I256, sequence: &[BasicTxDetails]) {
355 if self.test_data.optimization_best_value.is_none_or(|best| value > best) {
356 self.test_data.optimization_best_value = Some(value);
357 self.test_data.optimization_best_sequence = sequence.to_vec();
358 }
359 }
360}
361
362struct InvariantTestRun<FEN: FoundryEvmNetwork> {
364 inputs: Vec<BasicTxDetails>,
366 executor: Executor<FEN>,
368 fuzz_runs: Vec<FuzzCase>,
370 created_contracts: Vec<Address>,
372 run_traces: Vec<SparsedTraceArena>,
374 depth: u32,
376 rejects: u32,
378 new_coverage: bool,
380 optimization_value: Option<I256>,
382 optimization_prefix_len: usize,
384}
385
386impl<FEN: FoundryEvmNetwork> InvariantTestRun<FEN> {
387 fn new(first_input: BasicTxDetails, executor: Executor<FEN>, depth: usize) -> Self {
389 Self {
390 inputs: vec![first_input],
391 executor,
392 fuzz_runs: Vec::with_capacity(depth),
393 created_contracts: vec![],
394 run_traces: vec![],
395 depth: 0,
396 rejects: 0,
397 new_coverage: false,
398 optimization_value: None,
399 optimization_prefix_len: 0,
400 }
401 }
402}
403
404pub struct InvariantExecutor<'a, FEN: FoundryEvmNetwork> {
411 pub executor: Executor<FEN>,
412 runner: TestRunner,
414 config: InvariantConfig,
416 setup_contracts: &'a ContractsByAddress,
418 project_contracts: &'a ContractsByArtifact,
421 artifact_filters: ArtifactFilters,
423}
424
425impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
426 pub fn new(
428 executor: Executor<FEN>,
429 runner: TestRunner,
430 config: InvariantConfig,
431 setup_contracts: &'a ContractsByAddress,
432 project_contracts: &'a ContractsByArtifact,
433 ) -> Self {
434 Self {
435 executor,
436 runner,
437 config,
438 setup_contracts,
439 project_contracts,
440 artifact_filters: ArtifactFilters::default(),
441 }
442 }
443
444 pub fn config(self) -> InvariantConfig {
445 self.config
446 }
447
448 pub fn invariant_fuzz(
450 &mut self,
451 invariant_contract: InvariantContract<'_>,
452 fuzz_fixtures: &FuzzFixtures,
453 fuzz_state: EvmFuzzState,
454 progress: Option<&ProgressBar>,
455 early_exit: &EarlyExit,
456 ) -> Result<InvariantFuzzTestResult> {
457 if !invariant_contract.invariant_function.inputs.is_empty() {
459 return Err(eyre!("Invariant test function should have no inputs"));
460 }
461
462 let (mut invariant_test, mut corpus_manager) =
463 self.prepare_test(&invariant_contract, fuzz_fixtures, fuzz_state)?;
464
465 let mut runs = 0;
467 let timer = FuzzTestTimer::new(self.config.timeout);
468 let mut last_metrics_report = Instant::now();
469 let campaign_start = Instant::now();
470 let mut throughput = InvariantThroughputMetrics::default();
471 let mut failure_metrics = InvariantFailureMetrics::default();
472 let continue_campaign = |runs: u32| {
473 if early_exit.should_stop() {
474 return false;
475 }
476
477 if timer.is_enabled() { !timer.is_timed_out() } else { runs < self.config.runs }
478 };
479
480 let edge_coverage_enabled = self.config.corpus.collect_edge_coverage();
482
483 'stop: while continue_campaign(runs) {
484 let initial_seq = corpus_manager.new_inputs(
485 &mut invariant_test.test_data.branch_runner,
486 &invariant_test.fuzz_state,
487 &invariant_test.targeted_contracts,
488 )?;
489
490 let mut current_run = InvariantTestRun::new(
492 initial_seq[0].clone(),
493 self.executor.clone(),
495 self.config.depth as usize,
496 );
497
498 if self.config.fail_on_revert && invariant_test.reverts() > 0 {
500 return Err(eyre!("call reverted"));
501 }
502
503 while current_run.depth < self.config.depth {
504 if timer.is_timed_out() {
506 break 'stop;
511 }
512
513 let tx = current_run
514 .inputs
515 .last()
516 .ok_or_else(|| eyre!("no input generated to call fuzzed target."))?;
517
518 let mut call_result = execute_tx(&mut current_run.executor, tx)?;
521 let discarded = call_result.result.as_ref() == MAGIC_ASSUME;
522 if self.config.show_metrics {
523 invariant_test.record_metrics(tx, call_result.reverted, discarded);
524 }
525
526 invariant_test.merge_line_coverage(call_result.line_coverage.clone());
528 if corpus_manager.merge_edge_coverage(&mut call_result) {
530 current_run.new_coverage = true;
531 }
532
533 if discarded {
534 current_run.inputs.pop();
535 current_run.rejects += 1;
536 if current_run.rejects > self.config.max_assume_rejects {
537 invariant_test.set_error(InvariantFuzzError::MaxAssumeRejects(
538 self.config.max_assume_rejects,
539 ));
540 break 'stop;
541 }
542 } else {
543 let assertion_failure =
544 did_fail_on_assert(&call_result, &call_result.state_changeset);
545
546 current_run.executor.commit(&mut call_result);
548
549 let mut state_changeset = std::mem::take(&mut call_result.state_changeset);
557 if !call_result.reverted {
558 collect_data(
559 &invariant_test,
560 &mut state_changeset,
561 tx,
562 &call_result,
563 self.config.depth,
564 );
565 }
566
567 if let Err(error) =
570 &invariant_test.targeted_contracts.collect_created_contracts(
571 &state_changeset,
572 self.project_contracts,
573 self.setup_contracts,
574 &self.artifact_filters,
575 &mut current_run.created_contracts,
576 )
577 {
578 warn!(target: "forge::test", "{error}");
579 }
580 current_run
581 .fuzz_runs
582 .push(FuzzCase { gas: call_result.gas_used, stipend: call_result.stipend });
583 throughput.record_call(call_result.gas_used);
584
585 let is_last_call = current_run.depth == self.config.depth - 1;
591 let is_optimization = invariant_contract.is_optimization();
595 let should_check_invariant = is_optimization
596 || if self.config.check_interval == 0 {
597 is_last_call
598 } else {
599 self.config.check_interval == 1
600 || (current_run.depth + 1)
601 .is_multiple_of(self.config.check_interval)
602 || is_last_call
603 };
604
605 let result = if should_check_invariant {
606 can_continue(
607 &invariant_contract,
608 &mut invariant_test,
609 &mut current_run,
610 &self.config,
611 call_result,
612 &state_changeset,
613 )
614 .map_err(|e| eyre!(e.to_string()))?
615 } else {
616 if call_result.reverted {
618 invariant_test.test_data.failures.reverts += 1;
619 }
620 if assertion_failure || (call_result.reverted && self.config.fail_on_revert)
621 {
622 let case_data = error::FailedInvariantCaseData::new(
623 &invariant_contract,
624 &self.config,
625 &invariant_test.targeted_contracts,
626 ¤t_run.inputs,
627 call_result,
628 &[],
629 )
630 .with_assertion_failure(assertion_failure);
631 invariant_test.test_data.failures.revert_reason =
632 Some(case_data.revert_reason.clone());
633 invariant_test.test_data.failures.error = Some(if assertion_failure {
634 InvariantFuzzError::BrokenInvariant(case_data)
635 } else {
636 InvariantFuzzError::Revert(case_data)
637 });
638 result::RichInvariantResults::new(false, None)
639 } else if call_result.reverted
640 && !invariant_contract.is_optimization()
641 && !self.config.has_delay()
642 {
643 current_run.inputs.pop();
647 result::RichInvariantResults::new(true, None)
648 } else {
649 result::RichInvariantResults::new(true, None)
650 }
651 };
652
653 if !result.can_continue || current_run.depth == self.config.depth - 1 {
654 invariant_test.set_last_run_inputs(¤t_run.inputs);
655 }
656 if !result.can_continue {
658 let reason = invariant_test
659 .test_data
660 .failures
661 .error
662 .as_ref()
663 .and_then(|e| e.revert_reason())
664 .unwrap_or_default();
665 failure_metrics.record_failure(
666 &invariant_contract.invariant_function.name,
667 invariant_contract.name,
668 &reason,
669 );
670 break 'stop;
671 }
672
673 invariant_test.set_last_call_results(result.call_result);
674 current_run.depth += 1;
675 }
676
677 current_run.inputs.push(corpus_manager.generate_next_input(
678 &mut invariant_test.test_data.branch_runner,
679 &initial_seq,
680 discarded,
681 current_run.depth as usize,
682 )?);
683 }
684
685 let optimization = current_run.optimization_value.map(|v| {
689 let prefix = current_run.inputs[..current_run.optimization_prefix_len].to_vec();
690 (v, prefix)
691 });
692 corpus_manager.process_inputs(
693 ¤t_run.inputs,
694 current_run.new_coverage,
695 optimization,
696 );
697
698 if invariant_contract.call_after_invariant && !invariant_test.has_errors() {
700 let success = assert_after_invariant(
701 &invariant_contract,
702 &mut invariant_test,
703 ¤t_run,
704 &self.config,
705 )
706 .map_err(|_| eyre!("Failed to call afterInvariant"))?;
707 if !success {
708 let reason = invariant_test
709 .test_data
710 .failures
711 .error
712 .as_ref()
713 .and_then(|e| e.revert_reason())
714 .unwrap_or_default();
715 failure_metrics.record_failure(
716 &invariant_contract.invariant_function.name,
717 invariant_contract.name,
718 &reason,
719 );
720 }
721 }
722
723 invariant_test.end_run(current_run, self.config.gas_report_samples as usize);
725 if let Some(progress) = progress {
726 progress.inc(1);
728 let best = invariant_test.test_data.optimization_best_value;
730 if edge_coverage_enabled || best.is_some() {
731 let mut msg = String::new();
732 if let Some(best) = best {
733 msg.push_str(&format!("best: {best}"));
734 }
735 if edge_coverage_enabled {
736 if !msg.is_empty() {
737 msg.push_str(", ");
738 }
739 msg.push_str(&format!("{}", &corpus_manager.metrics));
740 }
741 progress.set_message(msg);
742 }
743 } else if edge_coverage_enabled
744 && last_metrics_report.elapsed() > DURATION_BETWEEN_METRICS_REPORT
745 {
746 let metrics = build_invariant_progress_json(
748 SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
749 &invariant_contract.invariant_function.name,
750 &corpus_manager.metrics,
751 invariant_test.test_data.optimization_best_value,
752 throughput,
753 &failure_metrics,
754 campaign_start.elapsed(),
755 );
756 let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
757 last_metrics_report = Instant::now();
758 }
759
760 runs += 1;
761 }
762
763 trace!(?fuzz_fixtures);
764 invariant_test.fuzz_state.log_stats();
765
766 let result = invariant_test.test_data;
767 Ok(InvariantFuzzTestResult {
768 error: result.failures.error,
769 cases: result.fuzz_cases,
770 reverts: result.failures.reverts,
771 last_run_inputs: result.last_run_inputs,
772 gas_report_traces: result.gas_report_traces,
773 line_coverage: result.line_coverage,
774 metrics: result.metrics,
775 failed_corpus_replays: corpus_manager.failed_replays,
776 optimization_best_value: result.optimization_best_value,
777 optimization_best_sequence: result.optimization_best_sequence,
778 })
779 }
780
781 fn prepare_test(
785 &mut self,
786 invariant_contract: &InvariantContract<'_>,
787 fuzz_fixtures: &FuzzFixtures,
788 fuzz_state: EvmFuzzState,
789 ) -> Result<(InvariantTest<FEN>, WorkerCorpus)> {
790 self.select_contract_artifacts(invariant_contract.address)?;
792 let (targeted_senders, targeted_contracts) =
793 self.select_contracts_and_senders(invariant_contract.address)?;
794
795 let strategy = invariant_strat(
797 fuzz_state.clone(),
798 targeted_senders,
799 targeted_contracts.clone(),
800 self.config.clone(),
801 fuzz_fixtures.clone(),
802 )
803 .no_shrink();
804
805 let fuzz_state =
808 if targeted_contracts.targets.lock().iter().any(|(_, t)| t.storage_layout.is_some()) {
809 fuzz_state.with_mapping_slots(AddressMap::default())
810 } else {
811 fuzz_state
812 };
813
814 self.executor.inspector_mut().set_fuzzer(Fuzzer {
818 call_generator: None,
819 fuzz_state: fuzz_state.clone(),
820 collect: true,
821 });
822
823 let mut failures = InvariantFailures::new();
828 let last_call_results = assert_invariants(
829 invariant_contract,
830 &self.config,
831 &targeted_contracts,
832 &self.executor,
833 &[],
834 &mut failures,
835 )?;
836 if let Some(error) = failures.error {
837 return Err(eyre!(error.revert_reason().unwrap_or_default()));
838 }
839
840 if self.config.call_override {
844 let target_contract_ref = Arc::new(RwLock::new(Address::ZERO));
845
846 let handler_addresses: std::collections::HashSet<Address> =
849 targeted_contracts.targets.lock().keys().copied().collect();
850
851 let call_generator = RandomCallGenerator::new(
852 invariant_contract.address,
853 handler_addresses,
854 self.runner.clone(),
855 override_call_strat(
856 fuzz_state.clone(),
857 targeted_contracts.clone(),
858 target_contract_ref.clone(),
859 fuzz_fixtures.clone(),
860 ),
861 target_contract_ref,
862 );
863
864 if let Some(fuzzer) = self.executor.inspector_mut().fuzzer.as_mut() {
865 fuzzer.call_generator = Some(call_generator);
866 }
867 }
868
869 let worker = WorkerCorpus::new(
870 0,
871 self.config.corpus.clone(),
872 strategy.boxed(),
873 Some(&self.executor),
874 None,
875 Some(&targeted_contracts),
876 )?;
877
878 let mut invariant_test = InvariantTest::new(
879 fuzz_state,
880 targeted_contracts,
881 failures,
882 last_call_results,
883 self.runner.clone(),
884 );
885
886 if invariant_contract.is_optimization() {
889 let (opt_best_value, opt_best_sequence) = worker.optimization_initial_state();
890 invariant_test.test_data.optimization_best_value = opt_best_value;
891 invariant_test.test_data.optimization_best_sequence = opt_best_sequence;
892 }
893
894 Ok((invariant_test, worker))
895 }
896
897 pub fn select_contract_artifacts(&mut self, invariant_address: Address) -> Result<()> {
907 let targeted_artifact_selectors = self
908 .executor
909 .call_sol_default(invariant_address, &IInvariantTest::targetArtifactSelectorsCall {});
910
911 for IInvariantTest::FuzzArtifactSelector { artifact, selectors } in
913 targeted_artifact_selectors
914 {
915 let identifier = self.validate_selected_contract(artifact, &selectors)?;
916 self.artifact_filters.targeted.entry(identifier).or_default().extend(selectors);
917 }
918
919 let targeted_artifacts = self
920 .executor
921 .call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {});
922 let excluded_artifacts = self
923 .executor
924 .call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {});
925
926 for contract in excluded_artifacts {
928 let identifier = self.validate_selected_contract(contract, &[])?;
929
930 if !self.artifact_filters.excluded.contains(&identifier) {
931 self.artifact_filters.excluded.push(identifier);
932 }
933 }
934
935 for (artifact, contract) in self.project_contracts.iter() {
937 if contract
938 .abi
939 .functions()
940 .filter(|func| {
941 !matches!(
942 func.state_mutability,
943 alloy_json_abi::StateMutability::Pure
944 | alloy_json_abi::StateMutability::View
945 )
946 })
947 .count()
948 == 0
949 && !self.artifact_filters.excluded.contains(&artifact.identifier())
950 {
951 self.artifact_filters.excluded.push(artifact.identifier());
952 }
953 }
954
955 for contract in targeted_artifacts {
958 let identifier = self.validate_selected_contract(contract, &[])?;
959
960 if !self.artifact_filters.targeted.contains_key(&identifier)
961 && !self.artifact_filters.excluded.contains(&identifier)
962 {
963 self.artifact_filters.targeted.insert(identifier, vec![]);
964 }
965 }
966 Ok(())
967 }
968
969 fn validate_selected_contract(
972 &mut self,
973 contract: String,
974 selectors: &[FixedBytes<4>],
975 ) -> Result<String> {
976 if let Some((artifact, contract_data)) =
977 self.project_contracts.find_by_name_or_identifier(&contract)?
978 {
979 for selector in selectors {
981 contract_data
982 .abi
983 .functions()
984 .find(|func| func.selector().as_slice() == selector.as_slice())
985 .wrap_err(format!("{contract} does not have the selector {selector:?}"))?;
986 }
987
988 return Ok(artifact.identifier());
989 }
990 eyre::bail!(
991 "{contract} not found in the project. Allowed format: `contract_name` or `contract_path:contract_name`."
992 );
993 }
994
995 pub fn select_contracts_and_senders(
998 &self,
999 to: Address,
1000 ) -> Result<(SenderFilters, FuzzRunIdentifiedContracts)> {
1001 let targeted_senders =
1002 self.executor.call_sol_default(to, &IInvariantTest::targetSendersCall {});
1003 let mut excluded_senders =
1004 self.executor.call_sol_default(to, &IInvariantTest::excludeSendersCall {});
1005 excluded_senders.extend([
1007 CHEATCODE_ADDRESS,
1008 HARDHAT_CONSOLE_ADDRESS,
1009 DEFAULT_CREATE2_DEPLOYER,
1010 ]);
1011 excluded_senders.extend(PRECOMPILES);
1013 let sender_filters = SenderFilters::new(targeted_senders, excluded_senders);
1014
1015 let selected = self.executor.call_sol_default(to, &IInvariantTest::targetContractsCall {});
1016 let excluded = self.executor.call_sol_default(to, &IInvariantTest::excludeContractsCall {});
1017
1018 let contracts = self
1019 .setup_contracts
1020 .iter()
1021 .filter(|&(addr, (identifier, _))| {
1022 if *addr == to && selected.contains(&to) {
1024 return true;
1025 }
1026
1027 *addr != to
1028 && *addr != CHEATCODE_ADDRESS
1029 && *addr != HARDHAT_CONSOLE_ADDRESS
1030 && (selected.is_empty() || selected.contains(addr))
1031 && (excluded.is_empty() || !excluded.contains(addr))
1032 && self.artifact_filters.matches(identifier)
1033 })
1034 .map(|(addr, (identifier, abi))| {
1035 (
1036 *addr,
1037 TargetedContract::new(identifier.clone(), abi.clone())
1038 .with_project_contracts(self.project_contracts),
1039 )
1040 })
1041 .collect();
1042 let mut contracts = TargetedContracts { inner: contracts };
1043
1044 self.target_interfaces(to, &mut contracts)?;
1045
1046 self.select_selectors(to, &mut contracts)?;
1047
1048 if contracts.is_empty() {
1050 eyre::bail!("No contracts to fuzz.");
1051 }
1052
1053 Ok((sender_filters, FuzzRunIdentifiedContracts::new(contracts, selected.is_empty())))
1054 }
1055
1056 pub fn target_interfaces(
1061 &self,
1062 invariant_address: Address,
1063 targeted_contracts: &mut TargetedContracts,
1064 ) -> Result<()> {
1065 let interfaces = self
1066 .executor
1067 .call_sol_default(invariant_address, &IInvariantTest::targetInterfacesCall {});
1068
1069 let mut combined = TargetedContracts::new();
1075
1076 for IInvariantTest::FuzzInterface { addr, artifacts } in &interfaces {
1079 for identifier in artifacts {
1081 if let Some((_, contract_data)) =
1083 self.project_contracts.iter().find(|(artifact, _)| {
1084 &artifact.name == identifier || &artifact.identifier() == identifier
1085 })
1086 {
1087 let abi = &contract_data.abi;
1088 combined
1089 .entry(*addr)
1091 .and_modify(|entry| {
1093 entry.abi.functions.extend(abi.functions.clone());
1095 })
1096 .or_insert_with(|| {
1098 let mut contract =
1099 TargetedContract::new(identifier.clone(), abi.clone());
1100 contract.storage_layout =
1101 contract_data.storage_layout.as_ref().map(Arc::clone);
1102 contract
1103 });
1104 }
1105 }
1106 }
1107
1108 targeted_contracts.extend(combined.inner);
1109
1110 Ok(())
1111 }
1112
1113 pub fn select_selectors(
1116 &self,
1117 address: Address,
1118 targeted_contracts: &mut TargetedContracts,
1119 ) -> Result<()> {
1120 for (address, (identifier, _)) in self.setup_contracts {
1121 if let Some(selectors) = self.artifact_filters.targeted.get(identifier) {
1122 self.add_address_with_functions(*address, selectors, false, targeted_contracts)?;
1123 }
1124 }
1125
1126 let mut target_test_selectors = vec![];
1127 let mut excluded_test_selectors = vec![];
1128
1129 let selectors =
1131 self.executor.call_sol_default(address, &IInvariantTest::targetSelectorsCall {});
1132 for IInvariantTest::FuzzSelector { addr, selectors } in selectors {
1133 if addr == address {
1134 target_test_selectors = selectors.clone();
1135 }
1136 self.add_address_with_functions(addr, &selectors, false, targeted_contracts)?;
1137 }
1138
1139 let excluded_selectors =
1141 self.executor.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {});
1142 for IInvariantTest::FuzzSelector { addr, selectors } in excluded_selectors {
1143 if addr == address {
1144 excluded_test_selectors = selectors.clone();
1147 }
1148 self.add_address_with_functions(addr, &selectors, true, targeted_contracts)?;
1149 }
1150
1151 if target_test_selectors.is_empty()
1152 && let Some(target) = targeted_contracts.get(&address)
1153 {
1154 let selectors: Vec<_> = target
1158 .abi
1159 .functions()
1160 .filter_map(|func| {
1161 if matches!(
1162 func.state_mutability,
1163 alloy_json_abi::StateMutability::Pure
1164 | alloy_json_abi::StateMutability::View
1165 ) || func.is_reserved()
1166 || excluded_test_selectors.contains(&func.selector())
1167 {
1168 None
1169 } else {
1170 Some(func.selector())
1171 }
1172 })
1173 .collect();
1174 self.add_address_with_functions(address, &selectors, false, targeted_contracts)?;
1175 }
1176
1177 Ok(())
1178 }
1179
1180 fn add_address_with_functions(
1182 &self,
1183 address: Address,
1184 selectors: &[Selector],
1185 should_exclude: bool,
1186 targeted_contracts: &mut TargetedContracts,
1187 ) -> eyre::Result<()> {
1188 if selectors.is_empty() {
1190 return Ok(());
1191 }
1192
1193 let contract = match targeted_contracts.entry(address) {
1194 Entry::Occupied(entry) => entry.into_mut(),
1195 Entry::Vacant(entry) => {
1196 let (identifier, abi) = self.setup_contracts.get(&address).ok_or_else(|| {
1197 eyre::eyre!(
1198 "[{}] address does not have an associated contract: {}",
1199 if should_exclude { "excludeSelectors" } else { "targetSelectors" },
1200 address
1201 )
1202 })?;
1203 entry.insert(
1204 TargetedContract::new(identifier.clone(), abi.clone())
1205 .with_project_contracts(self.project_contracts),
1206 )
1207 }
1208 };
1209 contract.add_selectors(selectors.iter().copied(), should_exclude)?;
1210 Ok(())
1211 }
1212
1213 pub fn compute_settings(&mut self, invariant_address: Address) -> Result<InvariantSettings> {
1218 self.select_contract_artifacts(invariant_address)?;
1219 let (sender_filters, targeted_contracts) =
1220 self.select_contracts_and_senders(invariant_address)?;
1221 let targets = targeted_contracts.targets.lock();
1222 Ok(InvariantSettings::new(&targets, &sender_filters, self.config.fail_on_revert))
1223 }
1224}
1225
1226fn collect_data<FEN: FoundryEvmNetwork>(
1230 invariant_test: &InvariantTest<FEN>,
1231 state_changeset: &mut AddressMap<Account>,
1232 tx: &BasicTxDetails,
1233 call_result: &RawCallResult<FEN>,
1234 run_depth: u32,
1235) {
1236 let has_code = if let Some(Some(code)) =
1238 state_changeset.get(&tx.sender).map(|account| account.info.code.as_ref())
1239 {
1240 !code.is_empty()
1241 } else {
1242 false
1243 };
1244
1245 let sender_changeset = if has_code { None } else { state_changeset.remove(&tx.sender) };
1247
1248 invariant_test.fuzz_state.collect_values_from_call(
1250 &invariant_test.targeted_contracts,
1251 tx,
1252 &call_result.result,
1253 &call_result.logs,
1254 &*state_changeset,
1255 run_depth,
1256 );
1257
1258 if let Some(cmp_values) = &call_result.sancov_cmp_values {
1260 invariant_test.fuzz_state.collect_typed_cmp_values(
1261 cmp_values.iter().map(|s| (s.width, alloy_primitives::B256::from(s.value))),
1262 );
1263 }
1264
1265 if let Some(changed) = sender_changeset {
1267 state_changeset.insert(tx.sender, changed);
1268 }
1269}
1270
1271pub(crate) fn call_after_invariant_function<FEN: FoundryEvmNetwork>(
1275 executor: &Executor<FEN>,
1276 to: Address,
1277) -> Result<(RawCallResult<FEN>, bool), EvmError<FEN>> {
1278 let calldata = Bytes::from_static(&IInvariantTest::afterInvariantCall::SELECTOR);
1279 let mut call_result = executor.call_raw(CALLER, to, calldata, U256::ZERO)?;
1280 let success = executor.is_raw_call_mut_success(to, &mut call_result, false);
1281 Ok((call_result, success))
1282}
1283
1284pub(crate) fn call_invariant_function<FEN: FoundryEvmNetwork>(
1286 executor: &Executor<FEN>,
1287 address: Address,
1288 calldata: Bytes,
1289) -> Result<(RawCallResult<FEN>, bool)> {
1290 let mut call_result = executor.call_raw(CALLER, address, calldata, U256::ZERO)?;
1291 let success = executor.is_raw_call_mut_success(address, &mut call_result, false);
1292 Ok((call_result, success))
1293}
1294
1295pub(crate) fn execute_tx<FEN: FoundryEvmNetwork>(
1298 executor: &mut Executor<FEN>,
1299 tx: &BasicTxDetails,
1300) -> Result<RawCallResult<FEN>> {
1301 let warp = tx.warp.unwrap_or_default();
1302 let roll = tx.roll.unwrap_or_default();
1303
1304 if warp > 0 || roll > 0 {
1305 let ts = executor.evm_env().block_env.timestamp();
1307 let num = executor.evm_env().block_env.number();
1308 executor.evm_env_mut().block_env.set_timestamp(ts + warp);
1309 executor.evm_env_mut().block_env.set_number(num + roll);
1310
1311 let block_env = executor.evm_env().block_env.clone();
1315 if let Some(cheatcodes) = executor.inspector_mut().cheatcodes.as_mut() {
1316 if let Some(block) = cheatcodes.block.as_mut() {
1317 let bts = block.timestamp();
1318 let bnum = block.number();
1319 block.set_timestamp(bts + warp);
1320 block.set_number(bnum + roll);
1321 } else {
1322 cheatcodes.block = Some(block_env);
1323 }
1324 }
1325 }
1326
1327 executor
1328 .call_raw(tx.sender, tx.call_details.target, tx.call_details.calldata.clone(), U256::ZERO)
1329 .map_err(|e| eyre!(format!("Could not make raw evm call: {e}")))
1330}
1331
1332#[cfg(test)]
1333mod tests {
1334 use super::*;
1335 use serde_json::json;
1336
1337 #[test]
1338 fn invariant_progress_json_includes_throughput_fields() {
1339 let mut throughput = InvariantThroughputMetrics::default();
1340 throughput.record_call(20);
1341 throughput.record_call(30);
1342
1343 let payload = build_invariant_progress_json(
1344 123,
1345 "invariant_balance",
1346 &json!({ "corpus_count": 7 }),
1347 Some(I256::try_from(42).unwrap()),
1348 throughput,
1349 &InvariantFailureMetrics::default(),
1350 Duration::from_secs(10),
1351 );
1352
1353 assert_eq!(payload["timestamp"], json!(123));
1354 assert_eq!(payload["invariant"], json!("invariant_balance"));
1355 assert_eq!(payload["metrics"]["corpus_count"], json!(7));
1356 assert_eq!(payload["total_txs"], json!(2));
1357 assert_eq!(payload["total_gas"], json!(50));
1358 assert!((payload["tx_per_sec"].as_f64().unwrap() - 0.2).abs() < 1e-12);
1359 assert!((payload["gas_per_sec"].as_f64().unwrap() - 5.0).abs() < 1e-12);
1360 assert_eq!(payload["optimization_best"], json!("42"));
1361 }
1362
1363 #[test]
1364 fn invariant_progress_json_zero_elapsed_reports_zero_rates() {
1365 let mut throughput = InvariantThroughputMetrics::default();
1366 throughput.record_call(21_000);
1367
1368 let payload = build_invariant_progress_json(
1369 456,
1370 "invariant_zero_elapsed",
1371 &json!({ "corpus_count": 1 }),
1372 None,
1373 throughput,
1374 &InvariantFailureMetrics::default(),
1375 Duration::ZERO,
1376 );
1377
1378 assert_eq!(payload["tx_per_sec"], json!(0.0));
1379 assert_eq!(payload["gas_per_sec"], json!(0.0));
1380 assert!(payload.get("optimization_best").is_none());
1381 }
1382
1383 #[test]
1384 fn invariant_progress_json_includes_failure_counts() {
1385 let mut failure_metrics = InvariantFailureMetrics::default();
1386 failure_metrics.record_failure("invariant_a", "TestContract", "revert");
1387 failure_metrics.record_failure("invariant_a", "TestContract", "revert");
1388 failure_metrics.record_failure("invariant_b", "TestContract", "assertion failed");
1389
1390 let payload = build_invariant_progress_json(
1391 789,
1392 "invariant_a",
1393 &json!({ "corpus_count": 5 }),
1394 None,
1395 InvariantThroughputMetrics::default(),
1396 &failure_metrics,
1397 Duration::from_secs(1),
1398 );
1399
1400 assert_eq!(payload["metrics"]["failures"], json!(3));
1401 assert_eq!(payload["metrics"]["unique_failures"], json!(2));
1402 }
1403
1404 #[test]
1405 fn failure_metrics_tracks_total_and_unique_failures() {
1406 let mut metrics = InvariantFailureMetrics::default();
1407 metrics.record_failure("invariant_a", "TestContract", "revert");
1408 metrics.record_failure("invariant_a", "TestContract", "revert");
1409 metrics.record_failure("invariant_b", "TestContract", "assertion failed");
1410
1411 assert_eq!(metrics.failures, 3);
1412 assert_eq!(metrics.unique_failures.len(), 2);
1413 assert!(metrics.unique_failures.contains("invariant_a"));
1414 assert!(metrics.unique_failures.contains("invariant_b"));
1415 }
1416
1417 #[test]
1418 fn failure_metrics_default_is_zero() {
1419 let metrics = InvariantFailureMetrics::default();
1420 assert_eq!(metrics.failures, 0);
1421 assert!(metrics.unique_failures.is_empty());
1422 }
1423}