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