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