1use crate::{
2 executors::{
3 DURATION_BETWEEN_METRICS_REPORT, EarlyExit, EvmError, Executor, RawCallResult,
4 campaign::{
5 CampaignCallKind, CampaignControl, CampaignEvent, CampaignSequenceOutcome,
6 FuzzCampaign, FuzzCampaignMode,
7 },
8 corpus::{
9 CorpusInsertionMode, DynamicTargetCtx, ReplayTarget, WorkerCorpus, WorkerCorpusSeed,
10 persist_campaign_optimization,
11 },
12 fuzz::{
13 FuzzBranchFrontier, FuzzFrontierRecorder, StatefulFuzzBranchFrontierArtifact,
14 merge_frontiers, write_frontier_artifact,
15 },
16 worker_seed,
17 },
18 inspectors::Fuzzer,
19};
20use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
21use alloy_json_abi::{Function, StateMutability};
22use alloy_primitives::{
23 Address, Bytes, FixedBytes, I256, Selector, U256,
24 map::{AddressMap, AddressSet, HashMap, hash_map::Entry as AddressMapEntry},
25};
26use alloy_sol_types::{SolCall, sol};
27use campaign::{
28 InvariantCampaignAggregator, InvariantCampaignSpec, InvariantCampaignState,
29 InvariantWorkerOutput, InvariantWorkerPlan,
30};
31use eyre::{ContextCompat, Result, eyre};
32use foundry_common::{
33 TestFunctionExt,
34 contracts::{ContractsByAddress, ContractsByArtifact},
35 sh_eprintln, sh_println,
36};
37use foundry_config::{FuzzCorpusConfig, InvariantConfig, InvariantDepthMode, InvariantWorkers};
38use foundry_evm_core::{
39 constants::{
40 CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS, MAGIC_ASSUME,
41 },
42 evm::FoundryEvmNetwork,
43 precompiles::PRECOMPILES,
44};
45use foundry_evm_coverage::HitMaps;
46use foundry_evm_fuzz::{
47 BasicTxDetails, FuzzCase, FuzzFixtures, ObservedCall,
48 invariant::{
49 ArtifactFilters, FuzzRunIdentifiedContracts, InvariantContract, RandomCallGenerator,
50 SenderFilters, TargetedContract, TargetedContracts,
51 },
52 strategies::{EvmFuzzState, FuzzState, TxGenerator, override_call_strat},
53};
54use foundry_evm_traces::{CallTraceArena, SparsedTraceArena};
55use indicatif::ProgressBar;
56use parking_lot::RwLock;
57use proptest::{
58 prelude::Rng,
59 test_runner::{RngAlgorithm, TestRng, TestRunner},
60};
61use rayon::iter::{IntoParallelIterator, ParallelIterator};
62use result::{assert_after_invariant, can_continue, invariant_preflight_check};
63use revm::state::Account;
64use serde::{Deserialize, Serialize};
65use serde_json::{Value, json};
66use std::{
67 collections::{HashMap as Map, HashSet, btree_map::Entry},
68 sync::Arc,
69 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
70};
71
72mod error;
73pub(crate) use error::snapshot_edge_fingerprint;
74pub use error::{
75 FailureKey, HandlerAssertionFailure, InvariantFailures, InvariantFuzzError,
76 handler_edge_fingerprint, handler_site_already_minimal,
77};
78mod campaign;
79
80mod replay;
81pub use replay::{ReplayErrorResult, replay_error, replay_run};
82
83mod result;
84pub use result::{InvariantFuzzTestResult, did_fail_on_assert};
85
86mod shrink;
87pub use shrink::{
88 CheckSequenceFailureSite, CheckSequenceOptions, CheckSequenceOutcome, HandlerReplayOutcome,
89 SequenceShrink, ShrinkCandidateKeys, ShrinkRun, ShrinkRunStats, check_sequence,
90 check_sequence_value, replay_handler_failure_sequence, shrink_sequence_by_removing,
91};
92
93const MIN_RUNS_PER_INVARIANT_WORKER: u32 = 10_000;
98const DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP: u32 = 500;
100const MIN_ESTIMATED_CALLS_PER_INVARIANT_WORKER: u64 =
102 MIN_RUNS_PER_INVARIANT_WORKER as u64 * DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP as u64;
103const INVARIANT_FOCUS_WORKER_DIVISOR: usize = 8;
105
106sol! {
107 interface IInvariantTest {
108 #[derive(Default)]
109 struct FuzzSelector {
110 address addr;
111 bytes4[] selectors;
112 }
113
114 #[derive(Default)]
115 struct FuzzArtifactSelector {
116 string artifact;
117 bytes4[] selectors;
118 }
119
120 #[derive(Default)]
121 struct FuzzInterface {
122 address addr;
123 string[] artifacts;
124 }
125
126 function afterInvariant() external;
127
128 #[derive(Default)]
129 function excludeArtifacts() public view returns (string[] memory excludedArtifacts);
130
131 #[derive(Default)]
132 function excludeContracts() public view returns (address[] memory excludedContracts);
133
134 #[derive(Default)]
135 function excludeSelectors() public view returns (FuzzSelector[] memory excludedSelectors);
136
137 #[derive(Default)]
138 function excludeSenders() public view returns (address[] memory excludedSenders);
139
140 #[derive(Default)]
141 function targetArtifacts() public view returns (string[] memory targetedArtifacts);
142
143 #[derive(Default)]
144 function targetArtifactSelectors() public view returns (FuzzArtifactSelector[] memory targetedArtifactSelectors);
145
146 #[derive(Default)]
147 function targetContracts() public view returns (address[] memory targetedContracts);
148
149 #[derive(Default)]
150 function targetSelectors() public view returns (FuzzSelector[] memory targetedSelectors);
151
152 #[derive(Default)]
153 function targetSenders() public view returns (address[] memory targetedSenders);
154
155 #[derive(Default)]
156 function targetInterfaces() public view returns (FuzzInterface[] memory targetedInterfaces);
157 }
158}
159
160#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
162pub struct InvariantMetrics {
163 pub calls: usize,
165 pub reverts: usize,
167 pub discards: usize,
169}
170
171impl InvariantMetrics {
172 const fn record_call(&mut self, reverted: bool, discarded: bool) {
173 self.calls += 1;
174 if discarded {
175 self.discards += 1;
176 } else if reverted {
177 self.reverts += 1;
178 }
179 }
180}
181
182#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
184struct InvariantThroughputMetrics {
185 total_txs: u64,
186 total_gas: u64,
187}
188
189impl InvariantThroughputMetrics {
190 fn tps(self, elapsed: Duration) -> f64 {
191 round_rate_for_progress(rate_per_sec(self.total_txs as f64, elapsed))
192 }
193
194 fn gps(self, elapsed: Duration) -> f64 {
195 round_rate_for_progress(rate_per_sec(self.total_gas as f64, elapsed))
196 }
197}
198
199fn max_invariant_workers_for_campaign(runs: u32, depth: u32) -> usize {
200 let estimated_calls = u64::from(runs) * u64::from(depth.max(1));
201 usize::try_from((estimated_calls / MIN_ESTIMATED_CALLS_PER_INVARIANT_WORKER).max(1))
202 .unwrap_or(usize::MAX)
203}
204
205fn invariant_run_depth(config: &InvariantConfig, runner: &mut TestRunner) -> u32 {
206 match config.depth_mode {
207 InvariantDepthMode::Fixed => config.depth,
208 InvariantDepthMode::Random => {
209 let min_depth = config.min_depth.max(1);
210 if config.depth <= min_depth {
211 config.depth.max(1)
212 } else {
213 runner.rng().random_range(min_depth..=config.depth)
214 }
215 }
216 }
217}
218
219fn auto_invariant_worker_count(
220 available_threads: usize,
221 invariant_campaign_anchors: usize,
222) -> usize {
223 (available_threads.max(1) / invariant_campaign_anchors.max(1)).max(1)
224}
225
226fn invariant_worker_count_with_threads(
227 config: &InvariantConfig,
228 available_threads: usize,
229 invariant_campaign_anchors: usize,
230) -> usize {
231 match config.workers {
232 InvariantWorkers::Fixed(workers) => workers.get(),
233 InvariantWorkers::Auto => {
234 let requested =
235 auto_invariant_worker_count(available_threads, invariant_campaign_anchors);
236 if config.timeout.is_some() {
237 requested
238 } else {
239 requested.min(max_invariant_workers_for_campaign(config.runs, config.depth))
240 }
241 }
242 }
243}
244
245const fn invariant_worker_config(
246 mut config: InvariantConfig,
247 worker_id: u32,
248 worker_count: usize,
249) -> InvariantConfig {
250 let exploratory_worker_id = worker_count.saturating_sub(1) as u32;
255 if worker_count > 2
256 && worker_id == exploratory_worker_id
257 && !config.corpus_random_sequence_weight_configured
258 && config.corpus.corpus_random_sequence_weight
259 == FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
260 {
261 config.corpus.corpus_random_sequence_weight =
262 FuzzCorpusConfig::ENSEMBLE_CORPUS_RANDOM_SEQUENCE_WEIGHT;
263 }
264 config
265}
266
267fn gas_report_samples_for_worker(total_samples: u32, worker_id: u32, worker_count: usize) -> usize {
268 let total_samples = total_samples as usize;
269 let worker_count = worker_count.max(1);
270 total_samples / worker_count + usize::from((worker_id as usize) < total_samples % worker_count)
271}
272
273fn invariant_worker_collects_evm_cmp_log(
274 config: &InvariantConfig,
275 worker_id: u32,
276 worker_count: usize,
277) -> bool {
278 config.corpus.collect_evm_cmp_log() && (worker_count <= 1 || worker_id == 0)
279}
280
281fn invariant_focus_worker_count(worker_count: usize) -> usize {
282 if worker_count <= 1 {
283 0
284 } else {
285 let max_focus_workers = if worker_count > 2 { worker_count - 2 } else { worker_count - 1 };
286 (worker_count / INVARIANT_FOCUS_WORKER_DIVISOR).max(1).min(max_focus_workers)
287 }
288}
289
290fn invariant_focus_worker_index(worker_id: u32, worker_count: usize) -> Option<usize> {
291 let worker_id = worker_id as usize;
292 let focus_workers = invariant_focus_worker_count(worker_count);
293 if focus_workers == 0 || worker_id >= worker_count {
294 return None;
295 }
296
297 let focus_worker_end = if worker_count > 2 { worker_count - 1 } else { worker_count };
298 let first_focus_worker = focus_worker_end - focus_workers;
299 (worker_id >= first_focus_worker && worker_id < focus_worker_end)
300 .then(|| worker_id - first_focus_worker)
301}
302
303fn focused_campaign_seed_for_worker(
304 campaign_seed: &InvariantCampaignSeed,
305 plan: InvariantWorkerPlan,
306 worker_count: usize,
307 focus_seed: Option<U256>,
308) -> Option<InvariantCampaignSeed> {
309 let focus_index = invariant_focus_worker_index(plan.worker_id, worker_count)?;
310 let targeted_contracts =
311 focused_targeted_contracts(&campaign_seed.targeted_contracts, focus_index, focus_seed)?;
312
313 Some(InvariantCampaignSeed {
314 targeted_contracts,
315 targets_are_updatable: false,
316 ..campaign_seed.clone()
317 })
318}
319
320fn campaign_seed_and_corpus_seed_for_worker(
321 campaign_seed: &InvariantCampaignSeed,
322 corpus_seed: &WorkerCorpusSeed,
323 plan: InvariantWorkerPlan,
324 worker_count: usize,
325 include_cmp_seq: bool,
326 focus_seed: Option<U256>,
327) -> (InvariantCampaignSeed, WorkerCorpusSeed) {
328 let worker_corpus_seed =
329 corpus_seed.clone_for_worker(plan.worker_id as usize, worker_count, include_cmp_seq);
330 let Some(worker_campaign_seed) =
331 focused_campaign_seed_for_worker(campaign_seed, plan, worker_count, focus_seed)
332 else {
333 return (campaign_seed.clone(), worker_corpus_seed);
334 };
335
336 let mut worker_corpus_seed = worker_corpus_seed;
337 worker_corpus_seed.retain_replayable(&worker_campaign_seed.targeted_contracts);
338 (worker_campaign_seed, worker_corpus_seed)
339}
340
341fn focused_targeted_contracts(
342 targeted_contracts: &TargetedContracts,
343 focus_index: usize,
344 focus_seed: Option<U256>,
345) -> Option<TargetedContracts> {
346 let mut seen = HashSet::new();
347 let mut candidates = Vec::new();
348 for (address, contract) in targeted_contracts.iter() {
349 for function in contract.abi_fuzzed_functions() {
351 if seen.insert((*address, function.selector())) {
352 candidates.push((*address, function.clone()));
353 }
354 }
355 }
356 if candidates.len() <= 1 {
357 return None;
358 }
359
360 let seed_offset = focus_seed
361 .map(|seed| (seed % U256::from(candidates.len())).to::<usize>())
362 .unwrap_or_default();
363 let candidate_index = (focus_index % candidates.len() + seed_offset) % candidates.len();
364 let (address, function) = candidates[candidate_index].clone();
365 let mut contract = targeted_contracts.get(&address)?.clone();
366 contract.targeted_functions = vec![function];
367
368 let mut focused = TargetedContracts::new();
369 focused.insert(address, contract);
370 Some(focused)
371}
372
373fn should_continue_invariant_worker(
374 campaign_state: &InvariantCampaignState,
375 runs: u32,
376 plan: InvariantWorkerPlan,
377) -> bool {
378 if campaign_state.should_stop() {
379 return false;
380 }
381
382 campaign_state.is_timed_campaign() || runs < plan.runs
383}
384
385fn invariant_worker_runner(
386 runner: &mut TestRunner,
387 worker_id: u32,
388 seed: Option<U256>,
389) -> TestRunner {
390 if let Some(seed) = seed {
391 let worker_seed = worker_seed(seed, worker_id);
392 trace!(target: "forge::test", ?worker_seed, "deterministic seed for invariant worker {worker_id}");
393 let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &worker_seed.to_be_bytes::<32>());
394 TestRunner::new_with_rng(runner.config().clone(), rng)
395 } else if worker_id == 0 {
396 runner.clone()
397 } else {
398 TestRunner::new_with_rng(runner.config().clone(), runner.new_rng())
399 }
400}
401
402fn invariant_focus_seed(
403 runner: &mut TestRunner,
404 configured_seed: Option<U256>,
405 worker_count: usize,
406) -> Option<U256> {
407 if invariant_focus_worker_count(worker_count) == 0 {
408 return None;
409 }
410 configured_seed.or_else(|| {
411 let mut rng = runner.new_rng();
412 Some(U256::from_be_bytes(rng.random::<[u8; 32]>()))
413 })
414}
415
416fn rate_per_sec(total: f64, elapsed: Duration) -> f64 {
421 let elapsed_secs = elapsed.as_secs_f64();
422 if elapsed_secs > 0.0 { total / elapsed_secs } else { 0.0 }
423}
424
425fn round_rate_for_progress(rate: f64) -> f64 {
426 (rate * 100.0).round() / 100.0
427}
428
429#[derive(Clone, Debug, Default)]
431struct InvariantFailureMetrics {
432 failures: u64,
433 unique_failures: HashSet<String>,
434 broken_handlers: usize,
436}
437
438impl InvariantFailureMetrics {
439 fn record_failure(&mut self, invariant_name: &str, target: &str, reason: &str) {
441 self.failures += 1;
442 self.unique_failures.insert(invariant_name.to_string());
443
444 let timestamp =
445 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
446 let event = json!({
447 "timestamp": timestamp,
448 "event": "failure",
449 "invariant": invariant_name,
450 "target": target,
451 "reason": reason,
452 });
453 let _ = sh_eprintln!("{}", serde_json::to_string(&event).unwrap_or_default());
454 }
455
456 fn record_handler_failure(&mut self, target: Address, selector: Selector, reason: &str) {
458 self.broken_handlers += 1;
459
460 let timestamp =
461 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
462 let event = build_handler_failure_event(timestamp, target, selector, reason);
463 let _ = sh_eprintln!("{}", serde_json::to_string(&event).unwrap_or_default());
464 }
465}
466
467fn build_handler_failure_event(
468 timestamp_secs: u64,
469 target: Address,
470 selector: Selector,
471 reason: &str,
472) -> Value {
473 json!({
474 "timestamp": timestamp_secs,
475 "event": "failure",
476 "failure_type": "handler_assertion",
477 "target": target,
478 "selector": selector,
479 "reason": reason,
480 })
481}
482
483fn record_new_invariant_failures(
487 campaign_state: &InvariantCampaignState,
488 invariant_contract: &InvariantContract<'_>,
489 failures: &InvariantFailures,
490) {
491 for (f, _) in &invariant_contract.invariant_fns {
492 if let Some(failure) = failures.get_failure(f) {
493 let reason = failure.revert_reason().unwrap_or_default();
494 campaign_state.record_invariant_failure(&f.name, invariant_contract.name, &reason);
495 }
496 }
497}
498
499struct InvariantProgressContext<'a> {
500 timestamp_secs: u64,
501 contract_name: &'a str,
502 optimization_best: Option<I256>,
503 throughput: InvariantThroughputMetrics,
504 elapsed: Duration,
505 worker_id: u32,
506 worker_count: usize,
507 time_since_new_edge: Option<Duration>,
509}
510
511fn build_invariant_progress_json<M: Serialize>(
518 context: InvariantProgressContext<'_>,
519 corpus_metrics: &M,
520 failure_metrics: &InvariantFailureMetrics,
521) -> serde_json::Value {
522 let mut metrics = serde_json::to_value(corpus_metrics).unwrap_or_default();
523 if let Some(obj) = metrics.as_object_mut() {
524 obj.insert("broken_invariants".to_string(), json!(failure_metrics.unique_failures.len()));
525 obj.insert("broken_assertions".to_string(), json!(failure_metrics.broken_handlers));
526 }
527
528 let mut payload = json!({
529 "timestamp": context.timestamp_secs,
530 "event": "pulse",
531 "contract": context.contract_name,
532 "metrics": metrics,
533 "total_txs": context.throughput.total_txs,
534 "total_gas": context.throughput.total_gas,
535 "tps": context.throughput.tps(context.elapsed),
536 "gps": context.throughput.gps(context.elapsed),
537 "worker": {
538 "id": context.worker_id,
539 "count": context.worker_count,
540 "secs_since_new_edge": context
542 .time_since_new_edge
543 .map(|d| d.as_secs_f64()),
544 },
545 });
546
547 if let Some(best) = context.optimization_best {
548 payload["optimization_best"] = json!(best.to_string());
549 }
550
551 payload
552}
553
554struct InvariantTestData {
556 runs: usize,
558 calls: usize,
560 failures: InvariantFailures,
562 last_run_inputs: Vec<BasicTxDetails>,
564 gas_report_traces: Vec<Vec<CallTraceArena>>,
566 line_coverage: Option<HitMaps>,
568 metrics: HashMap<String, InvariantMetrics>,
570 metric_key_cache: HashMap<(Address, Selector), String>,
573
574 branch_runner: TestRunner,
579
580 optimization_best_value: Option<I256>,
583 optimization_best_sequence: Vec<BasicTxDetails>,
584}
585
586struct InvariantTest {
588 fuzz_state: FuzzState,
590 targeted_contracts: FuzzRunIdentifiedContracts,
592 test_data: InvariantTestData,
594}
595
596impl InvariantTest {
597 fn new(
599 fuzz_state: FuzzState,
600 targeted_contracts: FuzzRunIdentifiedContracts,
601 failures: InvariantFailures,
602 branch_runner: TestRunner,
603 ) -> Self {
604 let test_data = InvariantTestData {
605 runs: 0,
606 calls: 0,
607 failures,
608 last_run_inputs: vec![],
609 gas_report_traces: vec![],
610 line_coverage: None,
611 metrics: HashMap::default(),
612 metric_key_cache: HashMap::default(),
613 branch_runner,
614 optimization_best_value: None,
615 optimization_best_sequence: vec![],
616 };
617 Self { fuzz_state, targeted_contracts, test_data }
618 }
619
620 const fn reverts(&self) -> usize {
622 self.test_data.failures.reverts
623 }
624
625 fn set_error(&mut self, invariant: &Function, error: InvariantFuzzError) {
627 self.test_data.failures.record_failure(invariant, error);
628 }
629
630 fn set_last_run_inputs(&mut self, inputs: &Vec<BasicTxDetails>) {
632 self.test_data.last_run_inputs.clone_from(inputs);
633 }
634
635 fn merge_line_coverage(&mut self, new_coverage: Option<HitMaps>) {
637 HitMaps::merge_opt(&mut self.test_data.line_coverage, new_coverage);
638 }
639
640 fn record_metrics(&mut self, tx_details: &BasicTxDetails, reverted: bool, discarded: bool) {
644 let Some(selector) = tx_details
645 .call_details
646 .calldata
647 .get(..4)
648 .and_then(|selector| <[u8; 4]>::try_from(selector).ok())
649 .map(Selector::from)
650 else {
651 return;
652 };
653 let cache_key = (tx_details.call_details.target, selector);
654
655 if let Some(metric_key) = self.test_data.metric_key_cache.get(&cache_key) {
656 if let Some(invariant_metrics) = self.test_data.metrics.get_mut(metric_key) {
657 invariant_metrics.record_call(reverted, discarded);
658 } else {
659 self.test_data
660 .metrics
661 .entry(metric_key.to_owned())
662 .or_default()
663 .record_call(reverted, discarded);
664 }
665 return;
666 }
667
668 let Some(metric_key) = self
671 .targeted_contracts
672 .targets()
673 .fuzzed_metric_key_for_selector(tx_details.call_details.target, selector)
674 else {
675 return;
676 };
677 self.test_data.metric_key_cache.insert(cache_key, metric_key.clone());
678 self.test_data.metrics.entry(metric_key).or_default().record_call(reverted, discarded);
679 }
680
681 fn invalidate_metric_key_cache(&mut self, addresses: &[Address]) {
684 if addresses.is_empty() {
685 return;
686 }
687 self.test_data.metric_key_cache.retain(|(addr, _), _| !addresses.contains(addr));
688 }
689
690 fn end_run<FEN: FoundryEvmNetwork>(&mut self, run: InvariantTestRun<FEN>, gas_samples: usize) {
693 self.invalidate_metric_key_cache(&run.created_contracts);
696 self.targeted_contracts.clear_created_contracts(run.created_contracts);
697
698 if self.test_data.gas_report_traces.len() < gas_samples {
699 self.test_data
700 .gas_report_traces
701 .push(run.run_traces.into_iter().map(|arena| arena.arena).collect());
702 }
703 self.test_data.runs += 1;
704 self.test_data.calls += run.fuzz_runs.len();
705
706 self.fuzz_state.revert();
708 }
709
710 fn update_optimization_value(&mut self, value: I256, sequence: &[BasicTxDetails]) {
712 if self.test_data.optimization_best_value.is_none_or(|best| value > best) {
713 self.test_data.optimization_best_value = Some(value);
714 self.test_data.optimization_best_sequence = sequence.to_vec();
715 }
716 }
717}
718
719struct InvariantTestRun<FEN: FoundryEvmNetwork> {
721 inputs: Vec<BasicTxDetails>,
723 cmp_seq: Vec<Vec<crate::inspectors::CmpOperands>>,
725 executor: Executor<FEN>,
727 fuzz_runs: Vec<FuzzCase>,
729 created_contracts: Vec<Address>,
731 run_traces: Vec<SparsedTraceArena>,
733 depth: u32,
735 rejects: u32,
737 new_coverage: bool,
739 line_coverage: Option<HitMaps>,
741 save_last_run_inputs: bool,
743 optimization_value: Option<I256>,
745 optimization_prefix_len: usize,
747}
748
749struct RecordedCallSequence {
751 inputs: Vec<BasicTxDetails>,
752 cmp_seq: Vec<Vec<crate::inspectors::CmpOperands>>,
753}
754
755#[derive(Clone)]
757struct InvariantCampaignSeed {
758 artifact_filters: ArtifactFilters,
759 sender_filters: SenderFilters,
760 targeted_contracts: TargetedContracts,
761 targets_are_updatable: bool,
762 initial_handler_failures: Map<(Address, Selector), InvariantFuzzError>,
763}
764
765impl<FEN: FoundryEvmNetwork> InvariantTestRun<FEN> {
766 fn new(first_input: BasicTxDetails, executor: Executor<FEN>, depth: usize) -> Self {
768 let mut inputs = Vec::with_capacity(depth.saturating_add(1));
769 inputs.push(first_input);
770 Self {
771 inputs,
772 cmp_seq: Vec::with_capacity(depth),
773 executor,
774 fuzz_runs: Vec::with_capacity(depth),
775 created_contracts: vec![],
776 run_traces: vec![],
777 depth: 0,
778 rejects: 0,
779 new_coverage: false,
780 line_coverage: None,
781 save_last_run_inputs: false,
782 optimization_value: None,
783 optimization_prefix_len: 0,
784 }
785 }
786
787 fn drop_corpus_payloads(&mut self) {
794 self.inputs.clear();
795 self.inputs.shrink_to_fit();
796 self.cmp_seq.clear();
797 self.cmp_seq.shrink_to_fit();
798 }
799}
800
801pub struct InvariantExecutor<'a, FEN: FoundryEvmNetwork> {
808 pub executor: Executor<FEN>,
809 runner: TestRunner,
811 fuzz_seed: Option<U256>,
813 config: InvariantConfig,
815 setup_contracts: &'a ContractsByAddress,
817 project_contracts: &'a ContractsByArtifact,
820 artifact_filters: ArtifactFilters,
822 invariant_campaign_anchors: usize,
824}
825
826impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
827 pub fn new_with_fuzz_seed(
830 executor: Executor<FEN>,
831 runner: TestRunner,
832 fuzz_seed: Option<U256>,
833 config: InvariantConfig,
834 setup_contracts: &'a ContractsByAddress,
835 project_contracts: &'a ContractsByArtifact,
836 invariant_campaign_anchors: usize,
837 ) -> Self {
838 Self {
839 executor,
840 runner,
841 fuzz_seed,
842 config,
843 setup_contracts,
844 project_contracts,
845 artifact_filters: ArtifactFilters::default(),
846 invariant_campaign_anchors,
847 }
848 }
849
850 pub fn config(&self) -> InvariantConfig {
851 self.config.clone()
852 }
853
854 pub const fn skip_fresh_runs(&mut self) {
856 self.config.runs = 0;
857 self.config.timeout = None;
858 }
859
860 pub const fn dynamic_target_ctx(&self) -> DynamicTargetCtx<'_> {
862 DynamicTargetCtx {
863 project_contracts: self.project_contracts,
864 setup_contracts: self.setup_contracts,
865 artifact_filters: &self.artifact_filters,
866 }
867 }
868
869 pub fn invariant_fuzz(
876 &mut self,
877 invariant_contract: InvariantContract<'_>,
878 fuzz_fixtures: &FuzzFixtures,
879 fuzz_state: EvmFuzzState,
880 progress: Option<&ProgressBar>,
881 early_exit: &EarlyExit,
882 initial_handler_failures: std::collections::HashMap<
883 (Address, Selector),
884 InvariantFuzzError,
885 >,
886 ) -> Result<InvariantFuzzTestResult> {
887 let campaign_spec = InvariantCampaignSpec::new(self.config.runs);
888 let worker_plans = campaign_spec.worker_plans(invariant_worker_count_with_threads(
889 &self.config,
890 rayon::current_num_threads(),
891 self.invariant_campaign_anchors,
892 ))?;
893 let actual_worker_count = worker_plans.len();
894 let campaign_seed =
895 self.prepare_campaign_seed(&invariant_contract, initial_handler_failures)?;
896 let replay_targets = FuzzRunIdentifiedContracts::new(
897 campaign_seed.targeted_contracts.clone(),
898 campaign_seed.targets_are_updatable,
899 );
900 let mut corpus_replay_executor = self.executor.clone();
901 corpus_replay_executor.inspector_mut().collect_evm_cmp_log(
902 invariant_worker_collects_evm_cmp_log(&self.config, 0, actual_worker_count),
903 );
904 let dynamic = self.dynamic_target_ctx();
905 let mut corpus_seed = WorkerCorpusSeed::load_from_disk(
906 &self.config.corpus,
907 None,
908 Some(&corpus_replay_executor),
909 ReplayTarget {
910 stateless: None,
911 fuzzed_contracts: Some(&replay_targets),
912 dynamic: Some(&dynamic),
913 senders: Some(&campaign_seed.sender_filters),
914 },
915 )?;
916 if invariant_contract.is_optimization()
917 && let Some(best_value) = corpus_seed.optimization_best_value
918 {
919 let sequence = (0..corpus_seed.optimization_best_sequence.len()).collect::<Vec<_>>();
920 let replayed = check_sequence_value(
921 self.executor.clone(),
922 &corpus_seed.optimization_best_sequence,
923 &sequence,
924 invariant_contract.address,
925 invariant_contract.anchor_calldata(),
926 self.config.fail_on_revert,
927 &campaign_seed.targeted_contracts.keys().copied().collect::<Vec<_>>(),
928 );
929 if !matches!(replayed, Ok(Some(value)) if value == best_value) {
930 corpus_seed.optimization_best_value = None;
932 corpus_seed.optimization_best_sequence.clear();
933 }
934 }
935 let mut runner = self.runner.clone();
936 let config = self.config.clone();
937 let setup_contracts = self.setup_contracts;
938 let project_contracts = self.project_contracts;
939 let base_executor = self.executor.clone();
940 let focus_seed = invariant_focus_seed(&mut runner, self.fuzz_seed, actual_worker_count);
941 let campaign_state =
942 Arc::new(InvariantCampaignState::new(early_exit.clone(), self.config.timeout));
943
944 let frontier_test = self
945 .config
946 .corpus
947 .capture_branch_frontiers()
948 .then(|| invariant_contract.anchor().clone());
949 let mut worker_outputs = if actual_worker_count > 1 {
950 let worker_jobs = worker_plans
951 .into_iter()
952 .map(|worker_plan| {
953 let worker_runner =
954 invariant_worker_runner(&mut runner, worker_plan.worker_id, self.fuzz_seed);
955 let gas_report_samples = gas_report_samples_for_worker(
956 config.gas_report_samples,
957 worker_plan.worker_id,
958 actual_worker_count,
959 );
960 let collect_cmp_log = invariant_worker_collects_evm_cmp_log(
961 &config,
962 worker_plan.worker_id,
963 actual_worker_count,
964 );
965 (worker_plan, worker_runner, gas_report_samples, collect_cmp_log)
966 })
967 .collect::<Vec<_>>();
968 worker_jobs
969 .into_par_iter()
970 .map(|(worker_plan, worker_runner, gas_report_samples, collect_cmp_log)| {
971 let _guard =
972 info_span!("invariant_worker", id = worker_plan.worker_id).entered();
973 let timer = Instant::now();
974 let (worker_campaign_seed, worker_corpus_seed) =
975 campaign_seed_and_corpus_seed_for_worker(
976 &campaign_seed,
977 &corpus_seed,
978 worker_plan,
979 actual_worker_count,
980 collect_cmp_log,
981 focus_seed,
982 );
983 let output = Self::run_invariant_worker(
984 base_executor.clone(),
985 worker_runner,
986 config.clone(),
987 setup_contracts,
988 project_contracts,
989 worker_plan,
990 invariant_contract.clone(),
991 fuzz_fixtures,
992 fuzz_state.fork(),
993 progress,
994 &campaign_state,
995 worker_campaign_seed,
996 worker_corpus_seed,
997 actual_worker_count,
998 gas_report_samples,
999 );
1000 if output.is_err() {
1001 campaign_state.request_terminal_stop();
1002 }
1003 debug!("finished in {:?}", timer.elapsed());
1004 output
1005 })
1006 .collect::<Result<Vec<_>>>()?
1007 } else {
1008 let worker_plan = worker_plans[0];
1009 let runner =
1010 invariant_worker_runner(&mut runner, worker_plan.worker_id, self.fuzz_seed);
1011 let gas_report_samples = config.gas_report_samples as usize;
1012 let collect_cmp_log = invariant_worker_collects_evm_cmp_log(
1013 &config,
1014 worker_plan.worker_id,
1015 actual_worker_count,
1016 );
1017 let (worker_campaign_seed, worker_corpus_seed) =
1018 campaign_seed_and_corpus_seed_for_worker(
1019 &campaign_seed,
1020 &corpus_seed,
1021 worker_plan,
1022 actual_worker_count,
1023 collect_cmp_log,
1024 focus_seed,
1025 );
1026 vec![Self::run_invariant_worker(
1027 base_executor,
1028 runner,
1029 config,
1030 setup_contracts,
1031 project_contracts,
1032 worker_plan,
1033 invariant_contract,
1034 fuzz_fixtures,
1035 fuzz_state,
1036 progress,
1037 &campaign_state,
1038 worker_campaign_seed,
1039 worker_corpus_seed,
1040 actual_worker_count,
1041 gas_report_samples,
1042 )?]
1043 };
1044
1045 let frontier_limit = self.config.corpus.frontier_limit;
1046 if let (Some(frontier_dir), Some(frontier_test)) =
1047 (&self.config.corpus.frontier_dir, frontier_test.as_ref())
1048 {
1049 let frontiers = merge_frontiers(
1050 frontier_limit,
1051 worker_outputs.iter_mut().flat_map(|(_, frontiers)| frontiers.drain(..)),
1052 );
1053 if !frontiers.is_empty() {
1054 let artifact = StatefulFuzzBranchFrontierArtifact::new(
1055 frontier_test,
1056 frontier_limit,
1057 frontiers,
1058 );
1059 if let Err(err) = write_frontier_artifact(frontier_dir, &artifact) {
1060 warn!(%err, path = ?frontier_dir, "failed to write fuzz branch frontier artifact");
1061 }
1062 }
1063 }
1064
1065 let mut aggregator = InvariantCampaignAggregator::new(campaign_spec);
1066 for (worker_output, _) in worker_outputs {
1067 aggregator.push(worker_output);
1068 }
1069 let mut result = if campaign_state.is_timed_campaign() {
1070 aggregator.finish_partial()?
1071 } else {
1072 aggregator.finish_campaign()?
1073 };
1074 result.interrupted = campaign_state.interrupted(result.runs, self.config.runs);
1075 persist_campaign_optimization(
1076 &self.config.corpus,
1077 result.optimization_best_value,
1078 &result.optimization_best_sequence,
1079 );
1080 Ok(result)
1081 }
1082
1083 #[allow(clippy::too_many_arguments)]
1085 fn run_invariant_worker(
1086 mut executor: Executor<FEN>,
1087 runner: TestRunner,
1088 config: InvariantConfig,
1089 setup_contracts: &'a ContractsByAddress,
1090 project_contracts: &'a ContractsByArtifact,
1091 plan: InvariantWorkerPlan,
1092 invariant_contract: InvariantContract<'_>,
1093 fuzz_fixtures: &FuzzFixtures,
1094 fuzz_state: EvmFuzzState,
1095 progress: Option<&ProgressBar>,
1096 campaign_state: &InvariantCampaignState,
1097 campaign_seed: InvariantCampaignSeed,
1098 corpus_seed: WorkerCorpusSeed,
1099 worker_count: usize,
1100 gas_report_samples: usize,
1101 ) -> Result<(InvariantWorkerOutput, Vec<FuzzBranchFrontier>)> {
1102 let config = invariant_worker_config(config, plan.worker_id, worker_count);
1106 let frontier_limit = if config.corpus.capture_branch_frontiers()
1107 && invariant_worker_collects_evm_cmp_log(&config, plan.worker_id, worker_count)
1108 {
1109 config.corpus.frontier_limit
1110 } else {
1111 0
1112 };
1113 let mut frontier_recorder = FuzzFrontierRecorder::new(frontier_limit);
1114 executor.inspector_mut().set_execution_cancellation(campaign_state.cancellation().clone());
1115
1116 let (mut invariant_test, mut corpus_manager) = Self::prepare_worker(
1117 &mut executor,
1118 plan,
1119 worker_count,
1120 &invariant_contract,
1121 fuzz_fixtures,
1122 fuzz_state,
1123 &runner,
1124 &config,
1125 &campaign_seed,
1126 corpus_seed,
1127 )?;
1128 let mut runs = 0;
1129 let mut view_cursor = 0;
1131 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1132 record_new_invariant_failures(
1135 campaign_state,
1136 &invariant_contract,
1137 &invariant_test.test_data.failures,
1138 );
1139
1140 let edge_coverage_enabled = config.corpus.collect_edge_coverage();
1142
1143 'stop: while should_continue_invariant_worker(campaign_state, runs, plan) {
1144 let failures_before_run = invariant_test.test_data.failures.invariant_count();
1146 let failures_checkpoint = invariant_test.test_data.failures.clone();
1147 let failures_revision = failures_checkpoint.revision();
1148 let mut stop_after_run = false;
1149 let mut run_cancelled = false;
1150 let mut observed_call_entries = Vec::<(Vec<ObservedCall>, BasicTxDetails)>::new();
1151
1152 let call_campaign = FuzzCampaign::new(FuzzCampaignMode::Invariant {
1153 check_interval: config.check_interval,
1154 optimization: invariant_contract.is_optimization(),
1155 });
1156
1157 let sequence_plan =
1158 corpus_manager.new_sequence(&mut invariant_test.test_data.branch_runner)?;
1159 let initial_seq = sequence_plan.initial();
1160
1161 let run_depth =
1162 invariant_run_depth(&config, &mut invariant_test.test_data.branch_runner);
1163
1164 let mut corpus_run = Option::<RecordedCallSequence>::None;
1165 let mut frontier_run = (frontier_limit > 0).then(|| RecordedCallSequence {
1166 inputs: Vec::with_capacity(run_depth as usize),
1167 cmp_seq: Vec::with_capacity(run_depth as usize),
1168 });
1169
1170 let mut current_run = InvariantTestRun::new(
1172 initial_seq[0].clone(),
1173 executor.clone(),
1175 run_depth as usize,
1176 );
1177
1178 if config.fail_on_revert && invariant_test.reverts() > 0 {
1180 campaign_state.request_terminal_stop();
1181 return Err(eyre!("call reverted"));
1182 }
1183
1184 let mut call_cmp_values = Vec::new();
1185 let mut call_new_coverage = false;
1186 let mut coverage_prefix_len = 0;
1187 let mut assertion_failure = false;
1188 let mut pre_merge_edges_hash = None;
1189 let mut handler = None;
1190 let mut abort_campaign = false;
1191 let sequence_outcome = call_campaign.run_sequence(
1192 &mut current_run,
1193 run_depth,
1194 |run| {
1195 let tx = run.inputs.last_mut().expect("campaign always has a current input");
1196 (&mut run.executor, tx)
1197 },
1198 |run| run.depth,
1199 |_| campaign_state.should_stop(),
1200 |current_run, event| {
1201 match event {
1202 CampaignEvent::Feedback(call_result) => {
1203 let current_tx = current_run.inputs.last().ok_or_else(|| {
1204 eyre!("no input generated to call fuzzed target.")
1205 })?;
1206 let sel_bytes: [u8; 4] = current_tx
1207 .call_details
1208 .calldata
1209 .get(..4)
1210 .and_then(|selector| selector.try_into().ok())
1211 .unwrap_or_default();
1212 handler =
1213 Some((current_tx.call_details.target, Selector::from(sel_bytes)));
1214 if let Some(fuzzer) =
1215 current_run.executor.inspector_mut().fuzzer.as_mut()
1216 {
1217 invariant_test.fuzz_state.collect_fuzzer_values(fuzzer);
1218 }
1219 call_cmp_values = call_result.evm_cmp_values.take().unwrap_or_default();
1220 let discarded = call_result.result.as_ref() == MAGIC_ASSUME;
1221 if config.show_metrics {
1222 invariant_test.record_metrics(
1223 current_tx,
1224 call_result.reverted,
1225 discarded,
1226 );
1227 }
1228 HitMaps::merge_opt(
1229 &mut current_run.line_coverage,
1230 call_result.line_coverage.take(),
1231 );
1232 assertion_failure = !discarded
1233 && did_fail_on_assert(call_result, &call_result.state_changeset);
1234 pre_merge_edges_hash = assertion_failure
1235 .then(|| error::snapshot_edge_fingerprint(call_result))
1236 .flatten();
1237 call_new_coverage = corpus_manager.merge_edge_coverage(call_result);
1238 if call_new_coverage {
1239 current_run.new_coverage = true;
1240 }
1241 let observed_calls = std::mem::take(&mut call_result.observed_calls);
1242 if call_new_coverage && !observed_calls.is_empty() {
1243 observed_call_entries.push((observed_calls, current_tx.clone()));
1244 }
1245 }
1246 CampaignEvent::Check { result, kind, should_check } => {
1247 let mut result =
1248 result.take().expect("campaign check result is available");
1249 if kind == CampaignCallKind::AssumptionRejected {
1250 current_run.inputs.pop();
1251 current_run.rejects += 1;
1252 if current_run.rejects > config.max_assume_rejects {
1253 invariant_test.set_error(
1254 invariant_contract.anchor(),
1255 InvariantFuzzError::MaxAssumeRejects(
1256 config.max_assume_rejects,
1257 ),
1258 );
1259 campaign_state.request_terminal_stop();
1260 abort_campaign = true;
1261 return Ok(CampaignControl::Stop);
1262 }
1263 return Ok(CampaignControl::Continue);
1264 }
1265 debug_assert_eq!(kind, CampaignCallKind::Accepted);
1266 if let Some(frontier_run) = &mut frontier_run {
1267 frontier_run.inputs.push(
1268 current_run
1269 .inputs
1270 .last()
1271 .expect("accepted call has a campaign input")
1272 .clone(),
1273 );
1274 frontier_run.cmp_seq.push(call_cmp_values.clone());
1275 }
1276 if config.corpus.is_coverage_guided() {
1277 let preserve_revert =
1278 invariant_contract.is_optimization() || config.has_delay();
1279 let retain_for_corpus =
1280 !result.reverted || preserve_revert || call_new_coverage;
1281 if let Some(corpus_run) = &mut corpus_run {
1282 if retain_for_corpus {
1283 corpus_run.inputs.push(
1284 current_run
1285 .inputs
1286 .last()
1287 .expect("accepted call has a campaign input")
1288 .clone(),
1289 );
1290 corpus_run
1291 .cmp_seq
1292 .push(std::mem::take(&mut call_cmp_values));
1293 }
1294 } else if result.reverted && !preserve_revert && call_new_coverage {
1295 let mut cmp_seq = std::mem::take(&mut current_run.cmp_seq);
1296 cmp_seq.push(std::mem::take(&mut call_cmp_values));
1297 corpus_run = Some(RecordedCallSequence {
1298 inputs: current_run.inputs.clone(),
1299 cmp_seq,
1300 });
1301 } else if !result.reverted || preserve_revert {
1302 current_run.cmp_seq.push(std::mem::take(&mut call_cmp_values));
1303 }
1304 if call_new_coverage {
1305 coverage_prefix_len = corpus_run
1306 .as_ref()
1307 .map_or(current_run.inputs.len(), |history| {
1308 history.inputs.len()
1309 });
1310 }
1311 }
1312 let (handler_target, handler_selector) =
1313 handler.take().expect("feedback precedes campaign checks");
1314 let mut state_changeset = std::mem::take(&mut result.state_changeset);
1315 if !result.reverted {
1316 let mapping_slots = current_run
1317 .executor
1318 .inspector()
1319 .fuzzer
1320 .as_ref()
1321 .and_then(|fuzzer| fuzzer.mapping_slots.as_ref());
1322 collect_data(
1323 &invariant_test,
1324 &mut state_changeset,
1325 current_run.inputs.last().expect("checked above"),
1326 &result,
1327 run_depth,
1328 mapping_slots,
1329 );
1330 if config.dictionary.include_view_values {
1331 collect_view_values(
1332 &invariant_test,
1333 ¤t_run.executor,
1334 &state_changeset,
1335 current_run.inputs.last().expect("checked above"),
1336 run_depth,
1337 &mut view_cursor,
1338 );
1339 }
1340 }
1341
1342 let created_before = current_run.created_contracts.len();
1343 if let Err(error) =
1344 &invariant_test.targeted_contracts.collect_created_contracts(
1345 &state_changeset,
1346 project_contracts,
1347 setup_contracts,
1348 &campaign_seed.artifact_filters,
1349 &mut current_run.created_contracts,
1350 )
1351 {
1352 warn!(target: "forge::test", "{error}");
1353 }
1354 invariant_test.invalidate_metric_key_cache(
1355 ¤t_run.created_contracts[created_before..],
1356 );
1357 current_run
1358 .fuzz_runs
1359 .push(FuzzCase { gas: result.gas_used, stipend: result.stipend });
1360
1361 let continues = if should_check {
1362 let outcome = can_continue(
1363 &invariant_contract,
1364 &mut invariant_test,
1365 current_run,
1366 &config,
1367 result,
1368 &state_changeset,
1369 handler_target,
1370 handler_selector,
1371 assertion_failure,
1372 pre_merge_edges_hash,
1373 )
1374 .map_err(|error| eyre!(error.to_string()))?;
1375 run_cancelled = outcome.cancelled;
1376 outcome.continues
1377 } else {
1378 if result.reverted {
1379 invariant_test.test_data.failures.reverts += 1;
1380 }
1381 if assertion_failure {
1382 let call_reverted = result.reverted;
1383 error::record_handler_assertion_bug(
1384 &invariant_contract,
1385 &config,
1386 &invariant_test.targeted_contracts,
1387 &mut invariant_test.test_data.failures,
1388 &mut current_run.inputs,
1389 handler_target,
1390 handler_selector,
1391 pre_merge_edges_hash,
1392 result,
1393 call_reverted,
1394 invariant_contract.is_optimization(),
1395 );
1396 true
1397 } else if result.reverted && config.fail_on_revert {
1398 let anchor = invariant_contract.anchor();
1399 let case_data = error::InvariantRunCtx {
1400 contract: &invariant_contract,
1401 config: &config,
1402 targeted_contracts: &invariant_test.targeted_contracts,
1403 calldata: ¤t_run.inputs,
1404 }
1405 .failed_case(anchor, config.fail_on_revert, false, result, &[]);
1406 invariant_test.test_data.failures.record_failure(
1407 anchor,
1408 InvariantFuzzError::Revert(case_data),
1409 );
1410 false
1411 } else {
1412 if result.reverted
1413 && !invariant_contract.is_optimization()
1414 && !config.has_delay()
1415 {
1416 current_run.inputs.pop();
1417 }
1418 true
1419 }
1420 };
1421
1422 if run_cancelled {
1423 return Ok(CampaignControl::Stop);
1424 }
1425 if !continues || current_run.depth == run_depth - 1 {
1426 current_run.save_last_run_inputs = true;
1427 }
1428 if !continues {
1429 if invariant_contract.invariant_fns.len() == 1
1430 || config.fail_on_revert
1431 {
1432 campaign_state.request_terminal_stop();
1433 stop_after_run = true;
1434 }
1435 return Ok(CampaignControl::Stop);
1436 }
1437 }
1438 CampaignEvent::Advance => current_run.depth += 1,
1439 CampaignEvent::Next { discarded, depth } => {
1440 current_run.inputs.push(sequence_plan.next(
1441 &mut invariant_test.test_data.branch_runner,
1442 discarded,
1443 depth as usize,
1444 )?);
1445 }
1446 CampaignEvent::PostCheck => {
1447 if !abort_campaign
1450 && !run_cancelled
1451 && invariant_contract.call_after_invariant
1452 && invariant_test.test_data.failures.invariant_count()
1453 == failures_before_run
1454 {
1455 let (broken, hook_cancelled) = assert_after_invariant(
1456 &invariant_contract,
1457 &mut invariant_test,
1458 current_run,
1459 &config,
1460 )
1461 .map_err(|_| eyre!("Failed to call afterInvariant"))?;
1462 if hook_cancelled {
1463 run_cancelled = true;
1464 } else if broken.is_some() {
1465 current_run.save_last_run_inputs = true;
1466 }
1467 }
1468 }
1469 }
1470 Ok(CampaignControl::Continue)
1471 },
1472 )?;
1473 if sequence_outcome == CampaignSequenceOutcome::Cancelled {
1474 run_cancelled = true;
1477 }
1478 if abort_campaign {
1479 break 'stop;
1480 }
1481
1482 if campaign_state.should_stop() && !stop_after_run {
1486 run_cancelled = true;
1487 }
1488
1489 if run_cancelled {
1490 let completed_finding =
1491 invariant_test.test_data.failures.revision() != failures_revision;
1492 if completed_finding {
1493 record_new_invariant_failures(
1494 campaign_state,
1495 &invariant_contract,
1496 &invariant_test.test_data.failures,
1497 );
1498 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1499 } else {
1500 invariant_test.test_data.failures.clone_from(&failures_checkpoint);
1501 }
1502 break 'stop;
1503 }
1504
1505 if invariant_test.test_data.failures.invariant_count() > failures_before_run {
1506 record_new_invariant_failures(
1507 campaign_state,
1508 &invariant_contract,
1509 &invariant_test.test_data.failures,
1510 );
1511 }
1512 if invariant_test.test_data.failures.handler_count()
1513 > failures_checkpoint.handler_count()
1514 {
1515 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1516 }
1517
1518 for (observed_calls, parent_tx) in observed_call_entries {
1519 corpus_manager.hoist_observed_calls(
1520 &observed_calls,
1521 &parent_tx,
1522 &invariant_test.targeted_contracts,
1523 &campaign_seed.sender_filters,
1524 CorpusInsertionMode::Live,
1525 );
1526 }
1527
1528 if let Some(frontier_run) = &frontier_run {
1530 frontier_recorder.capture_sequence(&frontier_run.inputs, &frontier_run.cmp_seq);
1531 }
1532 let optimization = current_run.optimization_value.map(|v| {
1533 let prefix = current_run.inputs[..current_run.optimization_prefix_len].to_vec();
1534 (v, prefix)
1535 });
1536 let mut corpus_inputs = corpus_run
1537 .as_ref()
1538 .map_or(current_run.inputs.as_slice(), |history| history.inputs.as_slice());
1539 let mut corpus_cmp_seq = corpus_run
1540 .as_ref()
1541 .map_or(current_run.cmp_seq.as_slice(), |history| history.cmp_seq.as_slice());
1542 if current_run.new_coverage && !invariant_contract.is_optimization() {
1543 corpus_inputs = &corpus_inputs[..coverage_prefix_len];
1544 corpus_cmp_seq = &corpus_cmp_seq[..coverage_prefix_len];
1545 }
1546 if worker_count > 1 {
1547 corpus_manager.process_inputs_for_campaign(
1548 corpus_inputs,
1549 corpus_cmp_seq,
1550 current_run.new_coverage,
1551 optimization,
1552 );
1553 } else {
1554 corpus_manager.process_inputs(
1555 corpus_inputs,
1556 corpus_cmp_seq,
1557 current_run.new_coverage,
1558 optimization,
1559 );
1560 }
1561
1562 if current_run.save_last_run_inputs {
1564 invariant_test.set_last_run_inputs(¤t_run.inputs);
1565 }
1566 if let Some(value) = current_run.optimization_value {
1567 invariant_test.update_optimization_value(
1568 value,
1569 ¤t_run.inputs[..current_run.optimization_prefix_len],
1570 );
1571 }
1572 invariant_test.merge_line_coverage(current_run.line_coverage.take());
1573 for fuzz_run in ¤t_run.fuzz_runs {
1574 campaign_state.record_call(fuzz_run.gas);
1575 }
1576 current_run.drop_corpus_payloads();
1577 invariant_test.end_run(current_run, gas_report_samples);
1578 runs += 1;
1579 let total_runs = campaign_state.increment_runs();
1580 debug_assert!(
1581 campaign_state.is_timed_campaign() || total_runs <= config.runs,
1582 "worker runs were not distributed correctly"
1583 );
1584 if let Some(progress) = progress {
1585 progress.inc(1);
1586 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1587 let best = invariant_test.test_data.optimization_best_value;
1589 let failure_metrics = campaign_state.failure_metrics();
1590 let broken = failure_metrics.unique_failures.len();
1591 let handler_bugs = failure_metrics.broken_handlers;
1592 let total_invariants = invariant_contract.invariant_fns.len();
1593 if edge_coverage_enabled || best.is_some() || broken > 0 || handler_bugs > 0 {
1594 let mut msg = String::new();
1595 if let Some(best) = best {
1596 msg.push_str(&format!("best: {best}"));
1597 }
1598 if edge_coverage_enabled {
1599 if !msg.is_empty() {
1600 msg.push_str(", ");
1601 }
1602 msg.push_str(&format!("{}", corpus_manager.metrics));
1603 match corpus_manager.time_since_new_edge() {
1604 Some(elapsed) => msg.push_str(&format!(
1605 "\n - time since new edge: {:.1}s",
1606 elapsed.as_secs_f64()
1607 )),
1608 None => msg.push_str("\n - time since new edge: never"),
1609 }
1610 }
1611 if broken > 0 {
1612 if !msg.is_empty() {
1613 msg.push_str(", ");
1614 }
1615 msg.push_str(&format!("❌ {broken}/{total_invariants} broken"));
1616 }
1617 if handler_bugs > 0 {
1618 if !msg.is_empty() {
1619 msg.push_str(", ");
1620 }
1621 msg.push_str(&format!("⚠ {handler_bugs} handler bug(s)"));
1622 }
1623 let msg =
1624 if worker_count > 1 { format!("[w{}] {msg}", plan.worker_id) } else { msg };
1625 progress.set_message(msg);
1626 }
1627 } else if edge_coverage_enabled
1628 && campaign_state.should_emit_metrics_report(DURATION_BETWEEN_METRICS_REPORT)
1629 {
1630 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1631 let failure_metrics = campaign_state.failure_metrics();
1632 let (total_txs, total_gas) = campaign_state.throughput_totals();
1633 let throughput = InvariantThroughputMetrics { total_txs, total_gas };
1634 let metrics = build_invariant_progress_json(
1636 InvariantProgressContext {
1637 timestamp_secs: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
1638 contract_name: invariant_contract.name,
1639 optimization_best: invariant_test.test_data.optimization_best_value,
1640 throughput,
1641 elapsed: campaign_state.elapsed(),
1642 worker_id: plan.worker_id,
1643 worker_count,
1644 time_since_new_edge: corpus_manager.time_since_new_edge(),
1645 },
1646 &corpus_manager.metrics,
1647 &failure_metrics,
1648 );
1649 let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
1650 }
1651
1652 if stop_after_run {
1653 break 'stop;
1654 }
1655 }
1656
1657 trace!(?fuzz_fixtures);
1658 invariant_test.fuzz_state.log_stats();
1659
1660 executor.inspector_mut().set_early_exit(campaign_state.early_exit().clone());
1662 Self::shrink_handler_failures(
1663 &config,
1664 &executor,
1665 &mut invariant_test.test_data,
1666 progress,
1667 campaign_state.early_exit(),
1668 );
1669
1670 let InvariantTest { fuzz_state: _, targeted_contracts: _, test_data: result } =
1674 invariant_test;
1675 let reverts = result.failures.reverts;
1676 let (errors, handler_errors) = result.failures.partition();
1677 let worker_result = InvariantFuzzTestResult::new(
1678 errors,
1679 handler_errors,
1680 result.runs,
1681 result.calls,
1682 reverts,
1683 result.last_run_inputs,
1684 result.gas_report_traces,
1685 result.line_coverage,
1686 result.metrics.into_iter().collect(),
1687 if plan.worker_id == 0 { corpus_manager.failed_replays } else { 0 },
1688 1,
1689 result.optimization_best_value,
1690 result.optimization_best_sequence,
1691 );
1692 drop(corpus_manager);
1693 let reported_plan = if campaign_state.is_timed_campaign() {
1694 InvariantWorkerPlan { runs, ..plan }
1695 } else {
1696 plan
1700 };
1701 Ok((
1702 InvariantWorkerOutput { plan: reported_plan, result: worker_result },
1703 frontier_recorder.into_frontiers(),
1704 ))
1705 }
1706
1707 fn shrink_handler_failures(
1708 config: &InvariantConfig,
1709 executor: &Executor<FEN>,
1710 result: &mut InvariantTestData,
1711 progress: Option<&ProgressBar>,
1712 early_exit: &EarlyExit,
1713 ) {
1714 let total = result.failures.handler_count();
1715 if total == 0 {
1716 return;
1717 }
1718
1719 for (idx, error) in result.failures.handler_failures_mut().enumerate() {
1720 if early_exit.should_stop() {
1721 break;
1722 }
1723 let Some(failure) = error.as_handler_assertion_mut() else {
1724 continue;
1725 };
1726 let shrink_progress = shrink::ShrinkProgress::new(
1727 config,
1728 progress,
1729 &format!("handler {:#x}::{}", failure.reverter, failure.selector),
1730 Some((idx + 1, total)),
1731 None,
1732 false,
1733 );
1734 match shrink::shrink_handler_sequence(
1735 config,
1736 &failure.call_sequence,
1737 failure.edge_fingerprint,
1738 executor,
1739 &shrink_progress,
1740 early_exit,
1741 ) {
1742 Ok(shrunk) if !shrunk.is_empty() => {
1743 failure.call_sequence = shrunk;
1744 }
1745 Ok(_) => {}
1746 Err(e) => trace!(target: "forge::test", "handler shrink failed: {e}"),
1747 }
1748 }
1749 }
1750
1751 fn prepare_campaign_seed(
1752 &mut self,
1753 invariant_contract: &InvariantContract<'_>,
1754 initial_handler_failures: std::collections::HashMap<
1755 (Address, Selector),
1756 InvariantFuzzError,
1757 >,
1758 ) -> Result<InvariantCampaignSeed> {
1759 self.select_contract_artifacts(invariant_contract.address)?;
1760 let (sender_filters, targeted_contracts) =
1761 self.select_contracts_and_senders(invariant_contract.address)?;
1762 let targets_are_updatable = targeted_contracts.is_updatable;
1763 let targeted_contracts = targeted_contracts.targets().clone();
1764
1765 Ok(InvariantCampaignSeed {
1766 artifact_filters: self.artifact_filters.clone(),
1767 sender_filters,
1768 targeted_contracts,
1769 targets_are_updatable,
1770 initial_handler_failures,
1771 })
1772 }
1773
1774 #[allow(clippy::too_many_arguments)]
1776 fn prepare_worker(
1777 executor: &mut Executor<FEN>,
1778 plan: InvariantWorkerPlan,
1779 worker_count: usize,
1780 invariant_contract: &InvariantContract<'_>,
1781 fuzz_fixtures: &FuzzFixtures,
1782 fuzz_state: EvmFuzzState,
1783 runner: &TestRunner,
1784 config: &InvariantConfig,
1785 campaign_seed: &InvariantCampaignSeed,
1786 corpus_seed: WorkerCorpusSeed,
1787 ) -> Result<(InvariantTest, WorkerCorpus)> {
1788 let fuzz_state = fuzz_state.into_invariant();
1789 let targeted_contracts = FuzzRunIdentifiedContracts::new(
1790 campaign_seed.targeted_contracts.clone(),
1791 campaign_seed.targets_are_updatable,
1792 );
1793 executor.inspector_mut().collect_evm_cmp_log(invariant_worker_collects_evm_cmp_log(
1794 config,
1795 plan.worker_id,
1796 worker_count,
1797 ));
1798
1799 let generator = TxGenerator::invariant(
1801 fuzz_state.clone(),
1802 campaign_seed.sender_filters.clone(),
1803 targeted_contracts.clone(),
1804 config.clone(),
1805 fuzz_fixtures.clone(),
1806 );
1807
1808 let mapping_slots = targeted_contracts
1811 .targets()
1812 .iter()
1813 .any(|(_, t)| t.storage_layout.is_some())
1814 .then(AddressMap::default);
1815
1816 let extra_cheatcode_addresses = executor.inspector().extra_cheatcode_addresses();
1820 executor.inspector_mut().set_fuzzer(
1821 Fuzzer::new(config.dictionary.max_fuzz_dictionary_values, mapping_slots)
1822 .with_extra_cheatcode_addresses(extra_cheatcode_addresses)
1823 .with_call_recording(config.corpus.is_coverage_guided()),
1824 );
1825
1826 let mut failures = InvariantFailures::new();
1831 for (&(addr, sel), err) in &campaign_seed.initial_handler_failures {
1833 failures.seed_handler_failure(addr, sel, err.clone());
1834 }
1835 invariant_preflight_check(
1836 invariant_contract,
1837 config,
1838 &targeted_contracts,
1839 executor,
1840 &[],
1841 &mut failures,
1842 )?;
1843 if let Some(fuzzer) = executor.inspector_mut().fuzzer.as_mut() {
1844 fuzz_state.collect_fuzzer_values(fuzzer);
1845 let _ = fuzzer.take_observed_calls();
1846 }
1847 let generator = foundry_evm_fuzz::sequence::SequenceGenerator::invariant_with_fixtures(
1848 generator,
1849 fuzz_state.clone(),
1850 fuzz_fixtures.clone(),
1851 targeted_contracts.clone(),
1852 &config.corpus,
1853 )?;
1854 let mut worker = WorkerCorpus::from_seed(
1855 plan.worker_id as usize,
1856 config.corpus.clone(),
1857 generator,
1858 corpus_seed,
1859 )?;
1860
1861 if let Err(err) = worker.seed_from_test_traces(
1862 invariant_contract,
1863 &targeted_contracts,
1864 &campaign_seed.sender_filters,
1865 executor,
1866 ) {
1867 debug!(target: "corpus", %err, "failed to seed corpus from test traces");
1868 }
1869
1870 if config.call_override {
1874 let target_contract_ref = Arc::new(RwLock::new(Address::ZERO));
1875
1876 let handler_addresses: AddressSet =
1879 targeted_contracts.targets().keys().copied().collect();
1880 let override_targets = targeted_contracts
1881 .targets()
1882 .iter()
1883 .filter_map(|(address, contract)| {
1884 let functions = contract.abi_fuzzed_functions().cloned().collect::<Vec<_>>();
1885 (!functions.is_empty()).then_some((*address, functions))
1886 })
1887 .collect::<Vec<_>>();
1888
1889 let call_generator = RandomCallGenerator::new(
1890 invariant_contract.address,
1891 handler_addresses,
1892 runner.clone(),
1893 override_call_strat(
1894 fuzz_state.snapshot(),
1895 override_targets,
1896 target_contract_ref.clone(),
1897 fuzz_fixtures.clone(),
1898 config.dictionary.dictionary_weight,
1899 config.corpus.payable_value_weight,
1900 ),
1901 target_contract_ref,
1902 );
1903
1904 if let Some(fuzzer) = executor.inspector_mut().fuzzer.as_mut() {
1905 fuzzer.call_generator = Some(call_generator);
1906 }
1907 }
1908
1909 let mut invariant_test =
1910 InvariantTest::new(fuzz_state, targeted_contracts, failures, runner.clone());
1911
1912 if invariant_contract.is_optimization() {
1916 let (opt_best_value, opt_best_sequence) = worker.optimization_initial_state();
1917 if let Some(value) = opt_best_value {
1918 invariant_test.update_optimization_value(value, &opt_best_sequence);
1919 }
1920 }
1921
1922 Ok((invariant_test, worker))
1923 }
1924
1925 pub fn select_contract_artifacts(&mut self, invariant_address: Address) -> Result<()> {
1935 let targeted_artifact_selectors = self
1936 .executor
1937 .call_sol_default(invariant_address, &IInvariantTest::targetArtifactSelectorsCall {});
1938
1939 for IInvariantTest::FuzzArtifactSelector { artifact, selectors } in
1941 targeted_artifact_selectors
1942 {
1943 let identifier = self.validate_selected_contract(artifact, &selectors)?;
1944 self.artifact_filters.targeted.entry(identifier).or_default().extend(selectors);
1945 }
1946
1947 let targeted_artifacts = self
1948 .executor
1949 .call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {});
1950 let excluded_artifacts = self
1951 .executor
1952 .call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {});
1953
1954 for contract in excluded_artifacts {
1956 let identifier = self.validate_selected_contract(contract, &[])?;
1957
1958 if !self.artifact_filters.excluded.contains(&identifier) {
1959 self.artifact_filters.excluded.push(identifier);
1960 }
1961 }
1962
1963 for (artifact, contract) in self.project_contracts.iter() {
1965 if contract
1966 .abi
1967 .functions()
1968 .filter(|func| {
1969 !matches!(
1970 func.state_mutability,
1971 alloy_json_abi::StateMutability::Pure
1972 | alloy_json_abi::StateMutability::View
1973 )
1974 })
1975 .count()
1976 == 0
1977 && !self.artifact_filters.excluded.contains(&artifact.identifier())
1978 {
1979 self.artifact_filters.excluded.push(artifact.identifier());
1980 }
1981 }
1982
1983 for contract in targeted_artifacts {
1986 let identifier = self.validate_selected_contract(contract, &[])?;
1987
1988 if !self.artifact_filters.targeted.contains_key(&identifier)
1989 && !self.artifact_filters.excluded.contains(&identifier)
1990 {
1991 self.artifact_filters.targeted.insert(identifier, vec![]);
1992 }
1993 }
1994 Ok(())
1995 }
1996
1997 fn validate_selected_contract(
2000 &mut self,
2001 contract: String,
2002 selectors: &[FixedBytes<4>],
2003 ) -> Result<String> {
2004 if let Some((artifact, contract_data)) =
2005 self.project_contracts.find_by_name_or_identifier(&contract)?
2006 {
2007 for selector in selectors {
2009 contract_data
2010 .abi
2011 .functions()
2012 .find(|func| func.selector().as_slice() == selector.as_slice())
2013 .wrap_err(format!("{contract} does not have the selector {selector:?}"))?;
2014 }
2015
2016 return Ok(artifact.identifier());
2017 }
2018 eyre::bail!(
2019 "{contract} not found in the project. Allowed format: `contract_name` or `contract_path:contract_name`."
2020 );
2021 }
2022
2023 pub fn select_contracts_and_senders(
2026 &self,
2027 to: Address,
2028 ) -> Result<(SenderFilters, FuzzRunIdentifiedContracts)> {
2029 let targeted_senders =
2030 self.executor.call_sol_default(to, &IInvariantTest::targetSendersCall {});
2031 let mut excluded_senders =
2032 self.executor.call_sol_default(to, &IInvariantTest::excludeSendersCall {});
2033 excluded_senders.extend([
2035 CHEATCODE_ADDRESS,
2036 HARDHAT_CONSOLE_ADDRESS,
2037 DEFAULT_CREATE2_DEPLOYER,
2038 ]);
2039 excluded_senders.extend(PRECOMPILES);
2041 let sender_filters = SenderFilters::new(targeted_senders, excluded_senders);
2042
2043 let selected = self.executor.call_sol_default(to, &IInvariantTest::targetContractsCall {});
2044 let excluded = self.executor.call_sol_default(to, &IInvariantTest::excludeContractsCall {});
2045
2046 let contracts = self
2047 .setup_contracts
2048 .iter()
2049 .filter(|&(addr, (identifier, _))| {
2050 if *addr == to && selected.contains(&to) {
2052 return true;
2053 }
2054
2055 *addr != to
2056 && *addr != CHEATCODE_ADDRESS
2057 && *addr != HARDHAT_CONSOLE_ADDRESS
2058 && (selected.is_empty() || selected.contains(addr))
2059 && (excluded.is_empty() || !excluded.contains(addr))
2060 && self.artifact_filters.matches(identifier)
2061 })
2062 .map(|(addr, (identifier, abi))| {
2063 (
2064 *addr,
2065 TargetedContract::new(identifier.clone(), abi.clone())
2066 .with_project_contracts(self.project_contracts),
2067 )
2068 })
2069 .collect();
2070 let mut contracts = TargetedContracts { inner: contracts };
2071
2072 self.target_interfaces(to, &mut contracts)?;
2073
2074 self.select_selectors(to, &mut contracts)?;
2075 self.exclude_default_storage_hook_callbacks(&mut contracts)?;
2076
2077 if contracts.is_empty() {
2079 eyre::bail!("No contracts to fuzz.");
2080 }
2081 if contracts.fuzzed_functions().next().is_none() {
2082 eyre::bail!("No functions to fuzz.");
2083 }
2084
2085 Ok((sender_filters, FuzzRunIdentifiedContracts::new(contracts, selected.is_empty())))
2086 }
2087
2088 fn exclude_default_storage_hook_callbacks(
2093 &self,
2094 targeted_contracts: &mut TargetedContracts,
2095 ) -> Result<()> {
2096 let Some(cheatcodes) = self.executor.inspector().cheatcodes.as_deref() else {
2097 return Ok(());
2098 };
2099 let callbacks =
2100 cheatcodes
2101 .storage_load_hooks()
2102 .chain(cheatcodes.storage_store_hooks())
2103 .map(|(_, hook)| (hook.callback_target, Selector::from(hook.callback_selector)))
2104 .chain(cheatcodes.mapping_storage_store_hooks().map(|(_, _, hook)| {
2105 (hook.callback_target, Selector::from(hook.callback_selector))
2106 }))
2107 .collect::<HashSet<_>>();
2108
2109 for (target, selector) in callbacks {
2110 let Some(contract) = targeted_contracts.get_mut(&target) else { continue };
2111 if !contract.targeted_functions.is_empty()
2112 || contract.function_by_selector(selector).is_none()
2113 {
2114 continue;
2115 }
2116 contract.add_selectors([selector], true)?;
2117 }
2118 Ok(())
2119 }
2120
2121 pub fn target_interfaces(
2126 &self,
2127 invariant_address: Address,
2128 targeted_contracts: &mut TargetedContracts,
2129 ) -> Result<()> {
2130 let interfaces = self
2131 .executor
2132 .call_sol_default(invariant_address, &IInvariantTest::targetInterfacesCall {});
2133
2134 let mut combined = TargetedContracts::new();
2140
2141 for IInvariantTest::FuzzInterface { addr, artifacts } in &interfaces {
2144 for identifier in artifacts {
2146 if let Some((_, contract_data)) =
2148 self.project_contracts.iter().find(|(artifact, _)| {
2149 &artifact.name == identifier || &artifact.identifier() == identifier
2150 })
2151 {
2152 let abi = &contract_data.abi;
2153 combined
2154 .entry(*addr)
2156 .and_modify(|entry| {
2158 entry.abi.functions.extend(abi.functions.clone());
2160 entry.rebuild_function_lookups();
2161 })
2162 .or_insert_with(|| {
2164 let mut contract =
2165 TargetedContract::new(identifier.clone(), abi.clone());
2166 contract.storage_layout =
2167 contract_data.storage_layout.as_ref().map(Arc::clone);
2168 contract
2169 });
2170 }
2171 }
2172 }
2173
2174 targeted_contracts.extend(combined.inner);
2175
2176 Ok(())
2177 }
2178
2179 pub fn select_selectors(
2182 &self,
2183 address: Address,
2184 targeted_contracts: &mut TargetedContracts,
2185 ) -> Result<()> {
2186 for (address, (identifier, _)) in self.setup_contracts {
2187 if let Some(selectors) = self.artifact_filters.targeted.get(identifier) {
2188 self.add_address_with_functions(*address, selectors, false, targeted_contracts)?;
2189 }
2190 }
2191
2192 let mut target_test_selectors = vec![];
2193 let mut excluded_test_selectors = vec![];
2194
2195 let selectors =
2197 self.executor.call_sol_default(address, &IInvariantTest::targetSelectorsCall {});
2198 for IInvariantTest::FuzzSelector { addr, selectors } in selectors {
2199 if addr == address {
2200 target_test_selectors = selectors.clone();
2201 }
2202 self.add_address_with_functions(addr, &selectors, false, targeted_contracts)?;
2203 }
2204
2205 let excluded_selectors =
2207 self.executor.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {});
2208 for IInvariantTest::FuzzSelector { addr, selectors } in excluded_selectors {
2209 if addr == address {
2210 excluded_test_selectors = selectors.clone();
2213 }
2214 self.add_address_with_functions(addr, &selectors, true, targeted_contracts)?;
2215 }
2216
2217 if target_test_selectors.is_empty()
2218 && let Some(target) = targeted_contracts.get(&address)
2219 {
2220 let selectors: Vec<_> = target
2224 .abi
2225 .functions()
2226 .filter_map(|func| {
2227 if matches!(
2228 func.state_mutability,
2229 alloy_json_abi::StateMutability::Pure
2230 | alloy_json_abi::StateMutability::View
2231 ) || func.is_reserved()
2232 || excluded_test_selectors.contains(&func.selector())
2233 {
2234 None
2235 } else {
2236 Some(func.selector())
2237 }
2238 })
2239 .collect();
2240 self.add_address_with_functions(address, &selectors, false, targeted_contracts)?;
2241 }
2242
2243 Ok(())
2244 }
2245
2246 fn add_address_with_functions(
2248 &self,
2249 address: Address,
2250 selectors: &[Selector],
2251 should_exclude: bool,
2252 targeted_contracts: &mut TargetedContracts,
2253 ) -> eyre::Result<()> {
2254 if selectors.is_empty() {
2256 return Ok(());
2257 }
2258
2259 let contract = match targeted_contracts.entry(address) {
2260 Entry::Occupied(entry) => entry.into_mut(),
2261 Entry::Vacant(entry) => {
2262 let (identifier, abi) = self.setup_contracts.get(&address).ok_or_else(|| {
2263 eyre::eyre!(
2264 "[{}] address does not have an associated contract: {}",
2265 if should_exclude { "excludeSelectors" } else { "targetSelectors" },
2266 address
2267 )
2268 })?;
2269 entry.insert(
2270 TargetedContract::new(identifier.clone(), abi.clone())
2271 .with_project_contracts(self.project_contracts),
2272 )
2273 }
2274 };
2275 contract.add_selectors(selectors.iter().copied(), should_exclude)?;
2276 Ok(())
2277 }
2278}
2279
2280fn collect_data<FEN: FoundryEvmNetwork>(
2284 invariant_test: &InvariantTest,
2285 state_changeset: &mut AddressMap<Account>,
2286 tx: &BasicTxDetails,
2287 call_result: &RawCallResult<FEN>,
2288 run_depth: u32,
2289 mapping_slots: Option<&AddressMap<foundry_common::mapping_slots::MappingSlots>>,
2290) {
2291 let sender_changeset = match state_changeset.entry(tx.sender) {
2293 AddressMapEntry::Occupied(entry) => entry
2294 .get()
2295 .info
2296 .code
2297 .as_ref()
2298 .is_none_or(|code| code.is_empty())
2299 .then(|| entry.remove()),
2300 AddressMapEntry::Vacant(_) => None,
2301 };
2302
2303 invariant_test.fuzz_state.collect_values_from_call(
2305 &invariant_test.targeted_contracts,
2306 tx,
2307 &call_result.result,
2308 &call_result.logs,
2309 &*state_changeset,
2310 run_depth,
2311 mapping_slots,
2312 );
2313
2314 if let Some(cmp_values) = &call_result.sancov_cmp_values {
2316 invariant_test.fuzz_state.collect_typed_cmp_values(
2317 cmp_values.iter().map(|s| (s.width, alloy_primitives::B256::from(s.value))),
2318 );
2319 }
2320 if let Some(changed) = sender_changeset {
2322 state_changeset.insert(tx.sender, changed);
2323 }
2324}
2325
2326const MAX_VIEW_CALLS_PER_TX: usize = 32;
2328
2329fn collect_view_values<FEN: FoundryEvmNetwork>(
2340 invariant_test: &InvariantTest,
2341 executor: &Executor<FEN>,
2342 state_changeset: &AddressMap<Account>,
2343 tx: &BasicTxDetails,
2344 run_depth: u32,
2345 view_cursor: &mut usize,
2346) {
2347 let calls = {
2348 let targets = invariant_test.targeted_contracts.targets();
2349 let mut candidates = targets
2350 .iter()
2351 .filter(|(address, _)| state_changeset.contains_key(*address))
2352 .flat_map(|(address, contract)| {
2353 contract
2354 .abi
2355 .functions()
2356 .filter(|function| is_harvestable_view(function))
2357 .map(move |function| (*address, function))
2358 })
2359 .collect::<Vec<_>>();
2360 if candidates.len() > MAX_VIEW_CALLS_PER_TX {
2361 let start = *view_cursor % candidates.len();
2362 candidates.rotate_left(start);
2363 candidates.truncate(MAX_VIEW_CALLS_PER_TX);
2364 *view_cursor = start + MAX_VIEW_CALLS_PER_TX;
2365 }
2366 candidates
2367 .into_iter()
2368 .map(|(address, function)| (address, function.clone()))
2369 .collect::<Vec<_>>()
2370 };
2371
2372 for (address, function) in calls {
2373 let args = vec![DynSolValue::Address(tx.sender); function.inputs.len()];
2374 let Ok(calldata) = function.abi_encode_input(&args) else { continue };
2375 let Ok(call_result) = executor.call_raw(tx.sender, address, calldata.into(), U256::ZERO)
2376 else {
2377 continue;
2378 };
2379 if !call_result.reverted {
2380 invariant_test.fuzz_state.collect_values_from_view(
2381 &function,
2382 &call_result.result,
2383 run_depth,
2384 );
2385 }
2386 }
2387}
2388
2389fn is_harvestable_view(function: &Function) -> bool {
2391 matches!(function.state_mutability, StateMutability::View | StateMutability::Pure)
2392 && !function.outputs.is_empty()
2393 && function.inputs.iter().all(|input| input.ty == "address")
2394 && !function.name.starts_with("invariant")
2395 && !function.name.starts_with("target")
2396 && !function.name.starts_with("exclude")
2397}
2398
2399pub(crate) fn call_after_invariant_function<FEN: FoundryEvmNetwork>(
2407 executor: &Executor<FEN>,
2408 to: Address,
2409) -> Result<(RawCallResult<FEN>, bool), EvmError<FEN>> {
2410 let calldata = Bytes::from_static(&IInvariantTest::afterInvariantCall::SELECTOR);
2411 let mut call_result = executor.call_raw(CALLER, to, calldata, U256::ZERO)?;
2412 let success = executor.is_raw_call_mut_success_handler_gate(to, &mut call_result);
2413 Ok((call_result, success))
2414}
2415
2416pub(crate) fn call_invariant_function<FEN: FoundryEvmNetwork>(
2423 executor: &Executor<FEN>,
2424 address: Address,
2425 calldata: Bytes,
2426) -> Result<(RawCallResult<FEN>, bool)> {
2427 let mut call_result = executor.call_raw(CALLER, address, calldata, U256::ZERO)?;
2428 let success = executor.is_raw_call_mut_success_handler_gate(address, &mut call_result);
2429 Ok((call_result, success))
2430}
2431
2432pub fn execute_tx<FEN: FoundryEvmNetwork>(
2440 executor: &mut Executor<FEN>,
2441 tx: &BasicTxDetails,
2442) -> Result<RawCallResult<FEN>> {
2443 super::campaign::execute_invariant_tx(executor, &mut tx.clone())
2444}
2445
2446pub fn execute_tx_and_register_created<FEN: FoundryEvmNetwork>(
2451 executor: &mut Executor<FEN>,
2452 tx: &BasicTxDetails,
2453 targeted_contracts: &FuzzRunIdentifiedContracts,
2454 dynamic_target_ctx: &DynamicTargetCtx<'_>,
2455 created_contracts: &mut Vec<Address>,
2456) -> Result<()> {
2457 let (kind, call_result) = super::campaign::execute_invariant_replay_tx(executor, tx)?;
2458 if kind == CampaignCallKind::AssumptionRejected {
2459 return Err(eyre!("invariant replay prefix rejected by vm.assume"));
2460 }
2461 targeted_contracts.collect_created_contracts(
2462 &call_result.state_changeset,
2463 dynamic_target_ctx.project_contracts,
2464 dynamic_target_ctx.setup_contracts,
2465 dynamic_target_ctx.artifact_filters,
2466 created_contracts,
2467 )?;
2468 Ok(())
2469}
2470
2471#[cfg(test)]
2472mod tests {
2473
2474 use super::*;
2475 use crate::executors::ExecutorBuilder;
2476 use alloy_evm::EvmEnv;
2477 use foundry_cheatcodes::CheatsConfig;
2478 use foundry_config::FuzzDictionaryConfig;
2479 use foundry_evm_core::{backend::Backend, evm::EthEvmNetwork};
2480 use foundry_evm_fuzz::CallDetails;
2481 use proptest::{
2482 prelude::any,
2483 strategy::{Strategy, ValueTree},
2484 test_runner::Config,
2485 };
2486 use revm::{
2487 bytecode::Bytecode,
2488 context::{Block, TxEnv},
2489 database::InMemoryDB,
2490 };
2491 use serde_json::json;
2492 use std::{sync::mpsc, thread};
2493
2494 fn first_generated_u64(runner: &mut TestRunner) -> u64 {
2495 any::<u64>().new_tree(runner).unwrap().current()
2496 }
2497
2498 fn test_runner() -> TestRunner {
2499 TestRunner::new(Config { failure_persistence: None, ..Default::default() })
2500 }
2501
2502 fn seeded_test_runner(seed: U256) -> TestRunner {
2503 let config = Config { failure_persistence: None, ..Default::default() };
2504 let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
2505 TestRunner::new_with_rng(config, rng)
2506 }
2507
2508 #[test]
2509 fn assumption_rejection_restores_delayed_block_environment() {
2510 let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
2511 let mut executor = ExecutorBuilder::default()
2512 .inspectors(|stack| stack.cheatcodes(Arc::new(CheatsConfig::default())))
2513 .gas_limit(1 << 24)
2514 .build(EvmEnv::default(), TxEnv::default(), backend, Default::default());
2515 let target = Address::repeat_byte(0x11);
2516 let mut code = vec![0x6e]; code.extend_from_slice(MAGIC_ASSUME);
2518 code.extend_from_slice(&[0x60, 0x00, 0x52, 0x60, 0x0f, 0x60, 0x11, 0xf3]);
2519 executor.set_code(target, Bytecode::new_raw(Bytes::from(code))).unwrap();
2520
2521 let initial_block = executor.evm_env().block_env.clone();
2522 let initial_cheatcode_block =
2523 executor.inspector().cheatcodes.as_ref().unwrap().block.clone();
2524 let expected_timestamp = initial_block.timestamp() + U256::from(10);
2525 let expected_number = initial_block.number() + U256::from(5);
2526 let tx = BasicTxDetails {
2527 warp: Some(U256::from(10)),
2528 roll: Some(U256::from(5)),
2529 sender: Address::ZERO,
2530 call_details: CallDetails { target, calldata: Bytes::new(), value: None },
2531 };
2532 let mut state = (executor, tx);
2533 let campaign = FuzzCampaign::new(FuzzCampaignMode::Invariant {
2534 check_interval: 1,
2535 optimization: false,
2536 });
2537
2538 let outcome = campaign
2539 .run_sequence(
2540 &mut state,
2541 1,
2542 |state| (&mut state.0, &mut state.1),
2543 |_| 0,
2544 |_| false,
2545 |state, event| {
2546 match event {
2547 CampaignEvent::Feedback(_) => {
2548 let block = &state.0.evm_env().block_env;
2549 assert_eq!(block.timestamp(), expected_timestamp);
2550 assert_eq!(block.number(), expected_number);
2551 let block = state
2552 .0
2553 .inspector()
2554 .cheatcodes
2555 .as_ref()
2556 .unwrap()
2557 .block
2558 .as_ref()
2559 .unwrap();
2560 assert_eq!(block.timestamp(), expected_timestamp);
2561 assert_eq!(block.number(), expected_number);
2562 }
2563 CampaignEvent::Check { kind, .. } => {
2564 assert_eq!(kind, CampaignCallKind::AssumptionRejected);
2565 return Ok(CampaignControl::Stop);
2566 }
2567 _ => {}
2568 }
2569 Ok(CampaignControl::Continue)
2570 },
2571 )
2572 .unwrap();
2573
2574 assert_eq!(outcome, CampaignSequenceOutcome::Stopped);
2575 assert_eq!(state.0.evm_env().block_env, initial_block);
2576 assert_eq!(state.0.inspector().cheatcodes.as_ref().unwrap().block, initial_cheatcode_block);
2577
2578 let (kind, _) =
2579 crate::executors::campaign::execute_invariant_replay_tx(&mut state.0, &state.1)
2580 .unwrap();
2581 assert_eq!(kind, CampaignCallKind::AssumptionRejected);
2582 assert_eq!(state.0.evm_env().block_env, initial_block);
2583 assert_eq!(state.0.inspector().cheatcodes.as_ref().unwrap().block, initial_cheatcode_block);
2584 }
2585
2586 #[test]
2587 fn invariant_worker_runner_preserves_seed_for_master_worker() {
2588 let seed = U256::from(0x1234);
2589 let mut seeded_runner = seeded_test_runner(seed);
2590 let mut parent = test_runner();
2591 let mut worker = invariant_worker_runner(&mut parent, 0, Some(seed));
2592
2593 assert_eq!(first_generated_u64(&mut worker), first_generated_u64(&mut seeded_runner));
2594 }
2595
2596 #[test]
2597 fn invariant_worker_runner_uses_seed_independent_of_parent_rng_state() {
2598 let seed = U256::from(0x1234);
2599 let mut parent = test_runner();
2600 let mut advanced_parent = test_runner();
2601 let _ = first_generated_u64(&mut advanced_parent);
2602
2603 let mut worker = invariant_worker_runner(&mut parent, 1, Some(seed));
2604 let mut worker_from_advanced_parent =
2605 invariant_worker_runner(&mut advanced_parent, 1, Some(seed));
2606
2607 assert_eq!(
2608 first_generated_u64(&mut worker),
2609 first_generated_u64(&mut worker_from_advanced_parent)
2610 );
2611 }
2612
2613 #[test]
2614 fn invariant_focus_seed_preserves_configured_seed() {
2615 let configured_seed = U256::from(0x1234);
2616 let mut parent = test_runner();
2617
2618 assert_eq!(
2619 invariant_focus_seed(&mut parent, Some(configured_seed), 2),
2620 Some(configured_seed)
2621 );
2622 assert_eq!(invariant_focus_seed(&mut parent, Some(configured_seed), 1), None);
2623 }
2624
2625 #[test]
2626 fn invariant_focus_seed_uses_parent_rng_when_unconfigured() {
2627 let mut parent = seeded_test_runner(U256::ONE);
2628 let mut matching_parent = seeded_test_runner(U256::ONE);
2629 let mut different_parent = seeded_test_runner(U256::from(2));
2630
2631 let focus_seed = invariant_focus_seed(&mut parent, None, 2).unwrap();
2632
2633 assert_eq!(focus_seed, invariant_focus_seed(&mut matching_parent, None, 2).unwrap());
2634 assert_ne!(focus_seed, invariant_focus_seed(&mut different_parent, None, 2).unwrap());
2635 assert_eq!(invariant_focus_seed(&mut parent, None, 1), None);
2636 }
2637
2638 #[test]
2639 fn invariant_progress_json_includes_throughput_fields() {
2640 let throughput = InvariantThroughputMetrics { total_txs: 2, total_gas: 50 };
2641
2642 let payload = build_invariant_progress_json(
2643 InvariantProgressContext {
2644 timestamp_secs: 123,
2645 contract_name: "InvariantContract",
2646 optimization_best: Some(I256::try_from(42).unwrap()),
2647 throughput,
2648 elapsed: Duration::from_secs(10),
2649 worker_id: 1,
2650 worker_count: 4,
2651 time_since_new_edge: Some(Duration::from_secs(3)),
2652 },
2653 &json!({ "corpus_count": 7 }),
2654 &InvariantFailureMetrics::default(),
2655 );
2656
2657 assert_eq!(payload["timestamp"], json!(123));
2658 assert_eq!(payload["contract"], json!("InvariantContract"));
2659 assert!(payload.get("invariant").is_none());
2660 assert_eq!(payload["metrics"]["corpus_count"], json!(7));
2661 assert_eq!(payload["metrics"]["broken_assertions"], json!(0));
2662 assert!(payload["metrics"].get("broken_handlers").is_none());
2663 assert_eq!(payload["total_txs"], json!(2));
2664 assert_eq!(payload["total_gas"], json!(50));
2665 assert_eq!(payload["tps"], json!(0.2));
2666 assert_eq!(payload["gps"], json!(5.0));
2667 assert!(payload.get("tx_per_sec").is_none());
2668 assert!(payload.get("gas_per_sec").is_none());
2669 assert_eq!(payload["worker"]["id"], json!(1));
2670 assert_eq!(payload["worker"]["count"], json!(4));
2671 assert_eq!(payload["optimization_best"], json!("42"));
2672 }
2673
2674 #[test]
2675 fn invariant_worker_count_keeps_short_campaigns_single_worker() {
2676 assert_eq!(
2677 max_invariant_workers_for_campaign(0, DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP),
2678 1
2679 );
2680 assert_eq!(
2681 max_invariant_workers_for_campaign(
2682 MIN_RUNS_PER_INVARIANT_WORKER - 1,
2683 DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2684 ),
2685 1
2686 );
2687 assert_eq!(
2688 max_invariant_workers_for_campaign(
2689 MIN_RUNS_PER_INVARIANT_WORKER,
2690 DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2691 ),
2692 1
2693 );
2694 assert_eq!(
2695 max_invariant_workers_for_campaign(
2696 MIN_RUNS_PER_INVARIANT_WORKER * 2,
2697 DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2698 ),
2699 2
2700 );
2701 assert_eq!(max_invariant_workers_for_campaign(256, 100_000), 5);
2702 }
2703
2704 #[test]
2705 fn invariant_run_depth_random_min_depth_zero_never_returns_zero() {
2706 let mut runner = test_runner();
2707 let config = InvariantConfig {
2708 depth: 8,
2709 min_depth: 0,
2710 depth_mode: InvariantDepthMode::Random,
2711 ..Default::default()
2712 };
2713
2714 for _ in 0..128 {
2715 assert!((1..=8).contains(&invariant_run_depth(&config, &mut runner)));
2716 }
2717 }
2718
2719 #[test]
2720 fn invariant_run_depth_fixed_zero_preserves_zero() {
2721 let mut runner = test_runner();
2722 let config = InvariantConfig {
2723 depth: 0,
2724 depth_mode: InvariantDepthMode::Fixed,
2725 ..Default::default()
2726 };
2727
2728 assert_eq!(invariant_run_depth(&config, &mut runner), 0);
2729 }
2730
2731 #[test]
2732 fn invariant_worker_config_keeps_single_worker_default() {
2733 let config = InvariantConfig::default();
2734
2735 assert_eq!(
2736 invariant_worker_config(config, 0, 1).corpus.corpus_random_sequence_weight,
2737 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2738 );
2739 }
2740
2741 #[test]
2742 fn invariant_worker_config_uses_one_exploratory_worker_with_default_config() {
2743 let config = InvariantConfig::default();
2744
2745 assert_eq!(
2746 invariant_worker_config(config.clone(), 0, 4).corpus.corpus_random_sequence_weight,
2747 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2748 );
2749 assert_eq!(
2750 invariant_worker_config(config.clone(), 1, 4).corpus.corpus_random_sequence_weight,
2751 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2752 );
2753 assert_eq!(
2754 invariant_worker_config(config.clone(), 2, 4).corpus.corpus_random_sequence_weight,
2755 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2756 );
2757 assert_eq!(
2758 invariant_worker_config(config, 3, 4).corpus.corpus_random_sequence_weight,
2759 FuzzCorpusConfig::ENSEMBLE_CORPUS_RANDOM_SEQUENCE_WEIGHT
2760 );
2761 }
2762
2763 #[test]
2764 fn invariant_worker_config_keeps_two_worker_campaign_on_default() {
2765 let config = InvariantConfig::default();
2766
2767 assert_eq!(
2768 invariant_worker_config(config.clone(), 0, 2).corpus.corpus_random_sequence_weight,
2769 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2770 );
2771 assert_eq!(
2772 invariant_worker_config(config, 1, 2).corpus.corpus_random_sequence_weight,
2773 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2774 );
2775 }
2776
2777 #[test]
2778 fn invariant_worker_config_uses_last_worker_for_three_worker_campaign() {
2779 let config = InvariantConfig::default();
2780
2781 assert_eq!(
2782 invariant_worker_config(config.clone(), 0, 3).corpus.corpus_random_sequence_weight,
2783 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2784 );
2785 assert_eq!(
2786 invariant_worker_config(config.clone(), 1, 3).corpus.corpus_random_sequence_weight,
2787 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2788 );
2789 assert_eq!(
2790 invariant_worker_config(config, 2, 3).corpus.corpus_random_sequence_weight,
2791 FuzzCorpusConfig::ENSEMBLE_CORPUS_RANDOM_SEQUENCE_WEIGHT
2792 );
2793 }
2794
2795 #[test]
2796 fn invariant_worker_config_preserves_explicit_corpus_random_sequence_weight() {
2797 let config = InvariantConfig {
2798 corpus: FuzzCorpusConfig {
2799 corpus_random_sequence_weight: 25,
2800 ..FuzzCorpusConfig::default()
2801 },
2802 corpus_random_sequence_weight_configured: true,
2803 ..InvariantConfig::default()
2804 };
2805
2806 assert_eq!(
2807 invariant_worker_config(config.clone(), 1, 4).corpus.corpus_random_sequence_weight,
2808 25
2809 );
2810 assert_eq!(invariant_worker_config(config, 0, 1).corpus.corpus_random_sequence_weight, 25);
2811
2812 let config = InvariantConfig {
2813 corpus_random_sequence_weight_configured: true,
2814 ..InvariantConfig::default()
2815 };
2816
2817 assert_eq!(
2818 invariant_worker_config(config, 1, 4).corpus.corpus_random_sequence_weight,
2819 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2820 );
2821 }
2822
2823 #[test]
2824 fn invariant_worker_count_preserves_fixed_workers() {
2825 let mut config = InvariantConfig {
2826 runs: MIN_RUNS_PER_INVARIANT_WORKER * 4,
2827 workers: foundry_config::InvariantWorkers::Fixed(
2828 std::num::NonZeroUsize::new(4).unwrap(),
2829 ),
2830 ..Default::default()
2831 };
2832 assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2833
2834 config.corpus.show_edge_coverage = true;
2835 assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2836
2837 config.corpus.show_edge_coverage = false;
2838 config.corpus.corpus_dir = Some(std::path::PathBuf::from("corpus"));
2839 assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2840
2841 config.runs = MIN_RUNS_PER_INVARIANT_WORKER - 1;
2842 config.timeout = None;
2843 assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2844
2845 config.timeout = Some(1);
2846 assert_eq!(invariant_worker_count_with_threads(&config, 8, 4), 4);
2847 }
2848
2849 #[test]
2850 fn invariant_worker_count_does_not_cap_configured_workers_by_available_threads() {
2851 let config = InvariantConfig {
2852 runs: MIN_RUNS_PER_INVARIANT_WORKER * 8,
2853 workers: foundry_config::InvariantWorkers::Fixed(
2854 std::num::NonZeroUsize::new(8).unwrap(),
2855 ),
2856 ..Default::default()
2857 };
2858
2859 assert_eq!(invariant_worker_count_with_threads(&config, 4, 1), 8);
2860 }
2861
2862 #[test]
2863 fn invariant_worker_count_splits_available_threads_for_auto_workers() {
2864 let mut config = InvariantConfig {
2865 runs: MIN_RUNS_PER_INVARIANT_WORKER * 4,
2866 depth: DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP,
2867 workers: foundry_config::InvariantWorkers::Auto,
2868 ..Default::default()
2869 };
2870
2871 assert_eq!(invariant_worker_count_with_threads(&config, 4, 1), 4);
2872 assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2873 assert_eq!(invariant_worker_count_with_threads(&config, 8, 3), 2);
2874 assert_eq!(invariant_worker_count_with_threads(&config, 3, 8), 1);
2875 assert_eq!(invariant_worker_count_with_threads(&config, 0, 0), 1);
2876
2877 config.runs = MIN_RUNS_PER_INVARIANT_WORKER - 1;
2878 assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 1);
2879
2880 config.depth = 100_000;
2881 assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2882
2883 config.timeout = Some(1);
2884 assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2885 }
2886
2887 fn function(signature: &str) -> Function {
2888 Function::parse(signature).unwrap()
2889 }
2890
2891 fn targeted_contract(identifier: &str, functions: Vec<Function>) -> TargetedContract {
2892 let mut abi = alloy_json_abi::JsonAbi::new();
2893 for function in functions {
2894 abi.functions.entry(function.name.clone()).or_default().push(function);
2895 }
2896 TargetedContract::new(identifier.to_string(), abi)
2897 }
2898
2899 #[test]
2900 fn campaign_terminal_stop_interrupts_handler_without_accepting_run() {
2901 const GAS_LIMIT: u64 = 1 << 24;
2902 let invariant_address = Address::repeat_byte(0x11);
2903 let handler_address = Address::repeat_byte(0x22);
2904 let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
2905 let mut executor = ExecutorBuilder::default().gas_limit(GAS_LIMIT).build(
2906 EvmEnv::default(),
2907 TxEnv::default(),
2908 backend,
2909 Default::default(),
2910 );
2911 executor
2913 .set_code(
2914 invariant_address,
2915 Bytecode::new_raw(Bytes::from_static(&[
2916 0x60, 0x01, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3,
2917 ])),
2918 )
2919 .unwrap();
2920 executor
2922 .set_code(
2923 handler_address,
2924 Bytecode::new_raw(Bytes::from_static(&[0x5b, 0x60, 0x00, 0x56])),
2925 )
2926 .unwrap();
2927
2928 let (handler_entered_tx, handler_entered_rx) = mpsc::channel();
2929 let (handler_release_tx, handler_release_rx) = mpsc::channel();
2930 executor.inspector_mut().set_early_exit_test_gate(
2931 handler_entered_tx,
2932 handler_release_rx,
2933 0,
2934 );
2935
2936 let invariant = function("invariant_ok() view returns (bool)");
2937 let mut invariant_abi = alloy_json_abi::JsonAbi::new();
2938 invariant_abi.functions.entry(invariant.name.clone()).or_default().push(invariant.clone());
2939 let invariant_contract = InvariantContract::new(
2940 invariant_address,
2941 "InvariantTest",
2942 vec![(&invariant, false)],
2943 0,
2944 false,
2945 &invariant_abi,
2946 );
2947
2948 let handler = function("loopForever()");
2949 let mut handler_contract = targeted_contract("Handler", vec![handler.clone()]);
2950 handler_contract.targeted_functions = vec![handler];
2951 let mut targeted_contracts = TargetedContracts::new();
2952 targeted_contracts.insert(handler_address, handler_contract);
2953 let campaign_seed = InvariantCampaignSeed {
2954 artifact_filters: ArtifactFilters::default(),
2955 sender_filters: SenderFilters::new(vec![CALLER], Vec::new()),
2956 targeted_contracts,
2957 targets_are_updatable: false,
2958 initial_handler_failures: Map::default(),
2959 };
2960
2961 let config =
2962 InvariantConfig { runs: 1, depth: 1, show_metrics: false, ..Default::default() };
2963 let campaign_state = InvariantCampaignState::new(EarlyExit::new(false), None);
2964 let fuzz_state =
2965 EvmFuzzState::new(&[], &InMemoryDB::default(), FuzzDictionaryConfig::default(), None);
2966 let setup_contracts = ContractsByAddress::default();
2967 let project_contracts = ContractsByArtifact::default();
2968 let (result_tx, result_rx) = mpsc::channel();
2969
2970 let (handler_entered, result) = thread::scope(|scope| {
2971 let handle = scope.spawn(|| {
2972 let result = InvariantExecutor::<EthEvmNetwork>::run_invariant_worker(
2973 executor,
2974 test_runner(),
2975 config,
2976 &setup_contracts,
2977 &project_contracts,
2978 InvariantWorkerPlan { worker_id: 0, first_global_run: 0, runs: 1 },
2979 invariant_contract,
2980 &FuzzFixtures::default(),
2981 fuzz_state,
2982 None,
2983 &campaign_state,
2984 campaign_seed,
2985 WorkerCorpusSeed::default(),
2986 1,
2987 1,
2988 );
2989 let _ = result_tx.send(result);
2990 });
2991
2992 let handler_entered = handler_entered_rx.recv_timeout(Duration::from_secs(1)).is_ok();
2993 campaign_state.request_terminal_stop();
2994 let _ = handler_release_tx.send(());
2995
2996 let result = result_rx.recv_timeout(Duration::from_secs(1));
2997 handle.join().unwrap();
2998 (handler_entered, result)
2999 });
3000 assert!(handler_entered, "invariant handler did not begin EVM execution");
3001 let (output, _) = result.expect("invariant campaign did not observe early exit").unwrap();
3002 assert_eq!(output.result.runs, 0);
3003 assert_eq!(output.result.calls, 0);
3004 assert_eq!(campaign_state.total_runs(), 0);
3005 assert_eq!(campaign_state.throughput_totals(), (0, 0));
3006 assert!(output.result.errors.is_empty());
3007 assert!(output.result.handler_errors.is_empty());
3008 assert!(output.result.last_run_inputs.is_empty());
3009 assert!(output.result.line_coverage.is_none());
3010 assert!(output.result.metrics.is_empty());
3011 assert!(output.result.optimization_best_value.is_none());
3012 }
3013
3014 #[test]
3015 fn invariant_focus_workers_stay_before_exploratory_tail_worker() {
3016 assert_eq!(invariant_focus_worker_count(1), 0);
3017 assert_eq!(invariant_focus_worker_count(2), 1);
3018 assert_eq!(invariant_focus_worker_count(4), 1);
3019 assert_eq!(invariant_focus_worker_count(8), 1);
3020 assert_eq!(invariant_focus_worker_count(16), 2);
3021
3022 assert_eq!(invariant_focus_worker_index(0, 1), None);
3023 assert_eq!(invariant_focus_worker_index(1, 2), Some(0));
3024 assert_eq!(invariant_focus_worker_index(0, 4), None);
3025 assert_eq!(invariant_focus_worker_index(2, 4), Some(0));
3026 assert_eq!(invariant_focus_worker_index(3, 4), None);
3027 assert_eq!(invariant_focus_worker_index(13, 16), Some(0));
3028 assert_eq!(invariant_focus_worker_index(14, 16), Some(1));
3029 assert_eq!(invariant_focus_worker_index(15, 16), None);
3030 assert_eq!(invariant_focus_worker_index(16, 16), None);
3031 }
3032
3033 #[test]
3034 fn invariant_focus_narrows_to_one_effective_selector() {
3035 let target = Address::repeat_byte(0x11);
3036 let first = function("first(uint256)");
3037 let second = function("second(uint256)");
3038 let second_selector = second.selector();
3039 let mut contract = targeted_contract("Target", vec![first.clone(), second.clone()]);
3040 contract.targeted_functions = vec![first, second];
3041
3042 let mut targets = TargetedContracts::new();
3043 targets.insert(target, contract);
3044
3045 let focused = focused_targeted_contracts(&targets, 1, None).unwrap();
3046 let focused_functions = focused[&target].abi_fuzzed_functions().collect::<Vec<_>>();
3047
3048 assert_eq!(focused.len(), 1);
3049 assert_eq!(focused_functions.len(), 1);
3050 assert_eq!(focused_functions[0].selector(), second_selector);
3051 }
3052
3053 #[test]
3054 fn invariant_focus_seed_rotates_effective_selector() {
3055 let target = Address::repeat_byte(0x55);
3056 let first = function("first(uint256)");
3057 let second = function("second(uint256)");
3058 let third = function("third(uint256)");
3059 let third_selector = third.selector();
3060 let mut contract =
3061 targeted_contract("Target", vec![first.clone(), second.clone(), third.clone()]);
3062 contract.targeted_functions = vec![first, second, third];
3063
3064 let mut targets = TargetedContracts::new();
3065 targets.insert(target, contract);
3066
3067 let focused = focused_targeted_contracts(&targets, 0, Some(U256::from(2))).unwrap();
3068 let focused_functions = focused[&target].abi_fuzzed_functions().collect::<Vec<_>>();
3069
3070 assert_eq!(focused_functions.len(), 1);
3071 assert_eq!(focused_functions[0].selector(), third_selector);
3072 }
3073
3074 #[test]
3075 fn invariant_focus_freezes_dynamic_target_updates() {
3076 let target = Address::repeat_byte(0x44);
3077 let first = function("first(uint256)");
3078 let second = function("second(uint256)");
3079 let mut contract = targeted_contract("Target", vec![first.clone(), second.clone()]);
3080 contract.targeted_functions = vec![first, second];
3081
3082 let mut targeted_contracts = TargetedContracts::new();
3083 targeted_contracts.insert(target, contract);
3084 let campaign_seed = InvariantCampaignSeed {
3085 artifact_filters: ArtifactFilters::default(),
3086 sender_filters: SenderFilters::default(),
3087 targeted_contracts,
3088 targets_are_updatable: true,
3089 initial_handler_failures: Map::default(),
3090 };
3091
3092 let normal_worker = campaign_seed_and_corpus_seed_for_worker(
3093 &campaign_seed,
3094 &WorkerCorpusSeed::default(),
3095 InvariantWorkerPlan { worker_id: 0, first_global_run: 0, runs: 1 },
3096 2,
3097 false,
3098 None,
3099 )
3100 .0;
3101 let focus_worker = campaign_seed_and_corpus_seed_for_worker(
3102 &campaign_seed,
3103 &WorkerCorpusSeed::default(),
3104 InvariantWorkerPlan { worker_id: 1, first_global_run: 1, runs: 1 },
3105 2,
3106 false,
3107 None,
3108 )
3109 .0;
3110
3111 assert!(normal_worker.targets_are_updatable);
3112 assert!(!focus_worker.targets_are_updatable);
3113 }
3114
3115 #[test]
3116 fn invariant_focus_does_not_widen_target_selectors() {
3117 let target = Address::repeat_byte(0x22);
3118 let allowed = function("allowed(uint256)");
3119 let hidden = function("hidden(uint256)");
3120 let mut contract = targeted_contract("Target", vec![allowed.clone(), hidden]);
3121 contract.targeted_functions = vec![allowed];
3122
3123 let mut targets = TargetedContracts::new();
3124 targets.insert(target, contract);
3125
3126 assert!(focused_targeted_contracts(&targets, 0, None).is_none());
3127 }
3128
3129 #[test]
3130 fn invariant_focus_skips_excluded_selectors() {
3131 let target = Address::repeat_byte(0x33);
3132 let first = function("aaa(uint256)");
3133 let second = function("bbb(uint256)");
3134 let excluded = function("ccc(uint256)");
3135 let excluded_selector = excluded.selector();
3136 let mut contract = targeted_contract("Target", vec![first, second, excluded.clone()]);
3137 contract.excluded_functions = vec![excluded];
3138
3139 let mut targets = TargetedContracts::new();
3140 targets.insert(target, contract);
3141
3142 let focused = focused_targeted_contracts(&targets, 3, None).unwrap();
3143 let focused_functions = focused[&target].abi_fuzzed_functions().collect::<Vec<_>>();
3144
3145 assert_eq!(focused_functions.len(), 1);
3146 assert_ne!(focused_functions[0].selector(), excluded_selector);
3147 }
3148
3149 #[test]
3150 fn invariant_focus_skips_excluded_targeted_selectors() {
3151 let target = Address::repeat_byte(0x66);
3152 let first = function("aaa(uint256)");
3153 let second = function("bbb(uint256)");
3154 let excluded = function("ccc(uint256)");
3155 let excluded_selector = excluded.selector();
3156 let mut contract =
3157 targeted_contract("Target", vec![first.clone(), second.clone(), excluded.clone()]);
3158 contract.targeted_functions = vec![first, second, excluded.clone()];
3159 contract.excluded_functions = vec![excluded];
3160
3161 let mut targets = TargetedContracts::new();
3162 targets.insert(target, contract);
3163
3164 let focused = focused_targeted_contracts(&targets, 2, None).unwrap();
3165 let focused_functions = focused[&target].abi_fuzzed_functions().collect::<Vec<_>>();
3166
3167 assert_eq!(focused_functions.len(), 1);
3168 assert_ne!(focused_functions[0].selector(), excluded_selector);
3169 }
3170
3171 #[test]
3172 fn invariant_worker_cmp_log_selection_uses_one_worker_per_campaign() {
3173 use foundry_config::FuzzCorpusMutationWeights;
3174
3175 let mut config = InvariantConfig::default();
3176 assert!(!invariant_worker_collects_evm_cmp_log(&config, 0, 1));
3177
3178 config.corpus.corpus_dir = Some("corpus".into());
3179 assert!(invariant_worker_collects_evm_cmp_log(&config, 0, 1));
3180 assert!(invariant_worker_collects_evm_cmp_log(&config, 0, 4));
3181 assert!(!invariant_worker_collects_evm_cmp_log(&config, 1, 4));
3182 assert!(!invariant_worker_collects_evm_cmp_log(&config, 3, 4));
3183
3184 config.corpus.mutation_weights = FuzzCorpusMutationWeights {
3185 mutation_weight_splice: 1,
3186 mutation_weight_repeat: 1,
3187 mutation_weight_interleave: 1,
3188 mutation_weight_prefix: 1,
3189 mutation_weight_suffix: 1,
3190 mutation_weight_abi: 1,
3191 mutation_weight_cmp: 0,
3192 };
3193 assert!(!invariant_worker_collects_evm_cmp_log(&config, 0, 1));
3194
3195 config.corpus.mutation_weights = FuzzCorpusMutationWeights {
3197 mutation_weight_splice: 0,
3198 mutation_weight_repeat: 0,
3199 mutation_weight_interleave: 0,
3200 mutation_weight_prefix: 0,
3201 mutation_weight_suffix: 0,
3202 mutation_weight_abi: 0,
3203 mutation_weight_cmp: 0,
3204 };
3205 assert!(invariant_worker_collects_evm_cmp_log(&config, 0, 1));
3206
3207 config.corpus.sancov_edges = true;
3208 assert!(!invariant_worker_collects_evm_cmp_log(&config, 0, 1));
3209 assert!(!invariant_worker_collects_evm_cmp_log(&config, 0, 4));
3210 }
3211
3212 #[test]
3213 fn timed_invariant_workers_are_not_bounded_by_assigned_runs() {
3214 let plan = InvariantWorkerPlan { worker_id: 0, first_global_run: 0, runs: 1 };
3215
3216 let untimed = InvariantCampaignState::new(EarlyExit::new(false), None);
3217 assert!(should_continue_invariant_worker(&untimed, 0, plan));
3218 assert!(!should_continue_invariant_worker(&untimed, 1, plan));
3219
3220 let timed = InvariantCampaignState::new(EarlyExit::new(false), Some(60));
3221 assert!(should_continue_invariant_worker(&timed, 0, plan));
3222 assert!(should_continue_invariant_worker(&timed, 1, plan));
3223 assert!(should_continue_invariant_worker(&timed, 10_000, plan));
3224 }
3225
3226 #[test]
3227 fn gas_report_samples_are_split_across_workers() {
3228 assert_eq!(gas_report_samples_for_worker(0, 0, 4), 0);
3229 assert_eq!(gas_report_samples_for_worker(8, 0, 4), 2);
3230 assert_eq!(gas_report_samples_for_worker(8, 3, 4), 2);
3231 assert_eq!(gas_report_samples_for_worker(10, 0, 4), 3);
3232 assert_eq!(gas_report_samples_for_worker(10, 1, 4), 3);
3233 assert_eq!(gas_report_samples_for_worker(10, 2, 4), 2);
3234 assert_eq!(gas_report_samples_for_worker(10, 3, 4), 2);
3235 assert_eq!(gas_report_samples_for_worker(3, 3, 4), 0);
3236 }
3237
3238 #[test]
3239 fn invariant_progress_json_zero_elapsed_reports_zero_rates() {
3240 let throughput = InvariantThroughputMetrics { total_txs: 1, total_gas: 21_000 };
3241
3242 let payload = build_invariant_progress_json(
3243 InvariantProgressContext {
3244 timestamp_secs: 456,
3245 contract_name: "invariant_zero_elapsed",
3246 optimization_best: None,
3247 throughput,
3248 elapsed: Duration::ZERO,
3249 worker_id: 0,
3250 worker_count: 1,
3251 time_since_new_edge: None,
3252 },
3253 &json!({ "corpus_count": 1 }),
3254 &InvariantFailureMetrics::default(),
3255 );
3256
3257 assert_eq!(payload["tps"], json!(0.0));
3258 assert_eq!(payload["gps"], json!(0.0));
3259 assert!(payload.get("optimization_best").is_none());
3260 assert_eq!(payload["worker"]["secs_since_new_edge"], json!(null));
3262 }
3263
3264 #[test]
3265 fn invariant_progress_json_reports_secs_since_new_edge() {
3266 let payload = build_invariant_progress_json(
3267 InvariantProgressContext {
3268 timestamp_secs: 1,
3269 contract_name: "TestContract",
3270 optimization_best: None,
3271 throughput: InvariantThroughputMetrics::default(),
3272 elapsed: Duration::from_secs(1),
3273 worker_id: 2,
3274 worker_count: 4,
3275 time_since_new_edge: Some(Duration::from_millis(1500)),
3276 },
3277 &json!({ "corpus_count": 1 }),
3278 &InvariantFailureMetrics::default(),
3279 );
3280
3281 assert_eq!(payload["worker"]["id"], json!(2));
3282 assert_eq!(payload["worker"]["secs_since_new_edge"], json!(1.5));
3283 }
3284
3285 #[test]
3286 fn invariant_progress_json_rounds_fractional_rates() {
3287 let payload = build_invariant_progress_json(
3288 InvariantProgressContext {
3289 timestamp_secs: 456,
3290 contract_name: "TestContract",
3291 optimization_best: None,
3292 throughput: InvariantThroughputMetrics { total_txs: 1, total_gas: 1 },
3293 elapsed: Duration::from_secs(3),
3294 worker_id: 0,
3295 worker_count: 1,
3296 time_since_new_edge: None,
3297 },
3298 &json!({ "corpus_count": 1 }),
3299 &InvariantFailureMetrics::default(),
3300 );
3301
3302 assert_eq!(payload["tps"], json!(0.33));
3303 assert_eq!(payload["gps"], json!(0.33));
3304 }
3305
3306 #[test]
3307 fn invariant_progress_json_includes_broken_counts() {
3308 let mut failure_metrics = InvariantFailureMetrics::default();
3309 failure_metrics.record_failure("invariant_a", "TestContract", "revert");
3310 failure_metrics.record_failure("invariant_a", "TestContract", "revert");
3311 failure_metrics.record_failure("invariant_b", "TestContract", "assertion failed");
3312 failure_metrics.broken_handlers = 7;
3313
3314 let payload = build_invariant_progress_json(
3315 InvariantProgressContext {
3316 timestamp_secs: 789,
3317 contract_name: "TestContract",
3318 optimization_best: None,
3319 throughput: InvariantThroughputMetrics::default(),
3320 elapsed: Duration::from_secs(1),
3321 worker_id: 0,
3322 worker_count: 1,
3323 time_since_new_edge: None,
3324 },
3325 &json!({ "corpus_count": 5 }),
3326 &failure_metrics,
3327 );
3328
3329 assert!(payload["metrics"].get("failures").is_none());
3330 assert!(payload["metrics"].get("unique_failures").is_none());
3331 assert_eq!(payload["metrics"]["broken_invariants"], json!(2));
3332 assert_eq!(payload["metrics"]["broken_assertions"], json!(7));
3333 assert!(payload["metrics"].get("broken_handlers").is_none());
3334 }
3335
3336 #[test]
3337 fn handler_assertion_failure_event_includes_site_and_reason() {
3338 let target = Address::repeat_byte(0x11);
3339 let selector = Selector::from([0xde, 0xad, 0xbe, 0xef]);
3340
3341 assert_eq!(
3342 build_handler_failure_event(123, target, selector, "assertion failed"),
3343 json!({
3344 "timestamp": 123,
3345 "event": "failure",
3346 "failure_type": "handler_assertion",
3347 "target": target,
3348 "selector": "0xdeadbeef",
3349 "reason": "assertion failed",
3350 })
3351 );
3352 }
3353
3354 #[test]
3355 fn failure_metrics_tracks_total_and_unique_failures() {
3356 let mut metrics = InvariantFailureMetrics::default();
3357 metrics.record_failure("invariant_a", "TestContract", "revert");
3358 metrics.record_failure("invariant_a", "TestContract", "revert");
3359 metrics.record_failure("invariant_b", "TestContract", "assertion failed");
3360
3361 assert_eq!(metrics.failures, 3);
3362 assert_eq!(metrics.unique_failures.len(), 2);
3363 assert!(metrics.unique_failures.contains("invariant_a"));
3364 assert!(metrics.unique_failures.contains("invariant_b"));
3365 }
3366
3367 #[test]
3368 fn failure_metrics_default_is_zero() {
3369 let metrics = InvariantFailureMetrics::default();
3370 assert_eq!(metrics.failures, 0);
3371 assert!(metrics.unique_failures.is_empty());
3372 assert_eq!(metrics.broken_handlers, 0);
3373 }
3374}