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_edge_fingerprint, 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 keccak256(seed_data).into()
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_with_fuzz_seed(
847 executor: Executor<FEN>,
848 runner: TestRunner,
849 fuzz_seed: Option<U256>,
850 config: InvariantConfig,
851 setup_contracts: &'a ContractsByAddress,
852 project_contracts: &'a ContractsByArtifact,
853 invariant_campaign_anchors: usize,
854 ) -> Self {
855 Self {
856 executor,
857 runner,
858 fuzz_seed,
859 config,
860 setup_contracts,
861 project_contracts,
862 artifact_filters: ArtifactFilters::default(),
863 invariant_campaign_anchors,
864 }
865 }
866
867 pub fn config(&self) -> InvariantConfig {
868 self.config.clone()
869 }
870
871 pub const fn skip_fresh_runs(&mut self) {
873 self.config.runs = 0;
874 self.config.timeout = None;
875 }
876
877 pub const fn dynamic_target_ctx(&self) -> DynamicTargetCtx<'_> {
879 DynamicTargetCtx {
880 project_contracts: self.project_contracts,
881 setup_contracts: self.setup_contracts,
882 artifact_filters: &self.artifact_filters,
883 }
884 }
885
886 pub fn invariant_fuzz(
893 &mut self,
894 invariant_contract: InvariantContract<'_>,
895 fuzz_fixtures: &FuzzFixtures,
896 fuzz_state: EvmFuzzState,
897 progress: Option<&ProgressBar>,
898 early_exit: &EarlyExit,
899 initial_handler_failures: std::collections::HashMap<
900 (Address, Selector),
901 InvariantFuzzError,
902 >,
903 ) -> Result<InvariantFuzzTestResult> {
904 let campaign_spec = InvariantCampaignSpec::new(self.config.runs);
905 let worker_plans = campaign_spec.worker_plans(invariant_worker_count_with_threads(
906 &self.config,
907 rayon::current_num_threads(),
908 self.invariant_campaign_anchors,
909 ))?;
910 let actual_worker_count = worker_plans.len();
911 let campaign_seed =
912 self.prepare_campaign_seed(&invariant_contract, initial_handler_failures)?;
913 let replay_targets = FuzzRunIdentifiedContracts::new(
914 campaign_seed.targeted_contracts.clone(),
915 campaign_seed.targets_are_updatable,
916 );
917 let mut corpus_replay_executor = self.executor.clone();
918 corpus_replay_executor.inspector_mut().collect_evm_cmp_log(
919 invariant_worker_collects_evm_cmp_log(&self.config, 0, actual_worker_count),
920 );
921 let dynamic = self.dynamic_target_ctx();
922 let mut corpus_seed = WorkerCorpusSeed::load_from_disk(
923 &self.config.corpus,
924 None,
925 Some(&corpus_replay_executor),
926 ReplayTarget {
927 stateless: None,
928 fuzzed_contracts: Some(&replay_targets),
929 dynamic: Some(&dynamic),
930 senders: Some(&campaign_seed.sender_filters),
931 },
932 )?;
933 if invariant_contract.is_optimization()
934 && let Some(best_value) = corpus_seed.optimization_best_value
935 {
936 let sequence = (0..corpus_seed.optimization_best_sequence.len()).collect::<Vec<_>>();
937 let replayed = check_sequence_value(
938 self.executor.clone(),
939 &corpus_seed.optimization_best_sequence,
940 &sequence,
941 invariant_contract.address,
942 invariant_contract.anchor_calldata(),
943 );
944 if !matches!(replayed, Ok(Some(value)) if value == best_value) {
945 corpus_seed.optimization_best_value = None;
947 corpus_seed.optimization_best_sequence.clear();
948 }
949 }
950 let mut runner = self.runner.clone();
951 let config = self.config.clone();
952 let setup_contracts = self.setup_contracts;
953 let project_contracts = self.project_contracts;
954 let base_executor = self.executor.clone();
955 let focus_seed = invariant_focus_seed(&mut runner, self.fuzz_seed, actual_worker_count);
956 let campaign_state =
957 Arc::new(InvariantCampaignState::new(early_exit.clone(), self.config.timeout));
958
959 let frontier_test = self
960 .config
961 .corpus
962 .capture_branch_frontiers()
963 .then(|| invariant_contract.anchor().clone());
964 let mut worker_outputs = if actual_worker_count > 1 {
965 let worker_jobs = worker_plans
966 .into_iter()
967 .map(|worker_plan| {
968 let worker_runner =
969 invariant_worker_runner(&mut runner, worker_plan.worker_id, self.fuzz_seed);
970 let gas_report_samples = gas_report_samples_for_worker(
971 config.gas_report_samples,
972 worker_plan.worker_id,
973 actual_worker_count,
974 );
975 let collect_cmp_log = invariant_worker_collects_evm_cmp_log(
976 &config,
977 worker_plan.worker_id,
978 actual_worker_count,
979 );
980 (worker_plan, worker_runner, gas_report_samples, collect_cmp_log)
981 })
982 .collect::<Vec<_>>();
983 worker_jobs
984 .into_par_iter()
985 .map(|(worker_plan, worker_runner, gas_report_samples, collect_cmp_log)| {
986 let _guard =
987 info_span!("invariant_worker", id = worker_plan.worker_id).entered();
988 let timer = Instant::now();
989 let (worker_campaign_seed, worker_corpus_seed) =
990 campaign_seed_and_corpus_seed_for_worker(
991 &campaign_seed,
992 &corpus_seed,
993 worker_plan,
994 actual_worker_count,
995 collect_cmp_log,
996 focus_seed,
997 );
998 let output = Self::run_invariant_worker(
999 base_executor.clone(),
1000 worker_runner,
1001 config.clone(),
1002 setup_contracts,
1003 project_contracts,
1004 worker_plan,
1005 invariant_contract.clone(),
1006 fuzz_fixtures,
1007 fuzz_state.fork(),
1008 progress,
1009 &campaign_state,
1010 worker_campaign_seed,
1011 worker_corpus_seed,
1012 actual_worker_count,
1013 gas_report_samples,
1014 );
1015 if output.is_err() {
1016 campaign_state.request_terminal_stop();
1017 }
1018 debug!("finished in {:?}", timer.elapsed());
1019 output
1020 })
1021 .collect::<Result<Vec<_>>>()?
1022 } else {
1023 let worker_plan = worker_plans[0];
1024 let runner =
1025 invariant_worker_runner(&mut runner, worker_plan.worker_id, self.fuzz_seed);
1026 let gas_report_samples = config.gas_report_samples as usize;
1027 let collect_cmp_log = invariant_worker_collects_evm_cmp_log(
1028 &config,
1029 worker_plan.worker_id,
1030 actual_worker_count,
1031 );
1032 let (worker_campaign_seed, worker_corpus_seed) =
1033 campaign_seed_and_corpus_seed_for_worker(
1034 &campaign_seed,
1035 &corpus_seed,
1036 worker_plan,
1037 actual_worker_count,
1038 collect_cmp_log,
1039 focus_seed,
1040 );
1041 vec![Self::run_invariant_worker(
1042 base_executor,
1043 runner,
1044 config,
1045 setup_contracts,
1046 project_contracts,
1047 worker_plan,
1048 invariant_contract,
1049 fuzz_fixtures,
1050 fuzz_state,
1051 progress,
1052 &campaign_state,
1053 worker_campaign_seed,
1054 worker_corpus_seed,
1055 actual_worker_count,
1056 gas_report_samples,
1057 )?]
1058 };
1059
1060 let frontier_limit = self.config.corpus.frontier_limit;
1061 if let (Some(frontier_dir), Some(frontier_test)) =
1062 (&self.config.corpus.frontier_dir, frontier_test.as_ref())
1063 {
1064 let frontiers = merge_frontiers(
1065 frontier_limit,
1066 worker_outputs.iter_mut().flat_map(|(_, frontiers)| frontiers.drain(..)),
1067 );
1068 if !frontiers.is_empty() {
1069 let artifact = StatefulFuzzBranchFrontierArtifact::new(
1070 frontier_test,
1071 frontier_limit,
1072 frontiers,
1073 );
1074 if let Err(err) = write_frontier_artifact(frontier_dir, &artifact) {
1075 warn!(%err, path = ?frontier_dir, "failed to write fuzz branch frontier artifact");
1076 }
1077 }
1078 }
1079
1080 let mut aggregator = InvariantCampaignAggregator::new(campaign_spec);
1081 for (worker_output, _) in worker_outputs {
1082 aggregator.push(worker_output);
1083 }
1084 let mut result = if campaign_state.is_timed_campaign() {
1085 aggregator.finish_partial()?
1086 } else {
1087 aggregator.finish_campaign()?
1088 };
1089 result.interrupted = campaign_state.interrupted(result.runs, self.config.runs);
1090 persist_campaign_optimization(
1091 &self.config.corpus,
1092 result.optimization_best_value,
1093 &result.optimization_best_sequence,
1094 );
1095 Ok(result)
1096 }
1097
1098 #[allow(clippy::too_many_arguments)]
1100 fn run_invariant_worker(
1101 mut executor: Executor<FEN>,
1102 runner: TestRunner,
1103 config: InvariantConfig,
1104 setup_contracts: &'a ContractsByAddress,
1105 project_contracts: &'a ContractsByArtifact,
1106 plan: InvariantWorkerPlan,
1107 invariant_contract: InvariantContract<'_>,
1108 fuzz_fixtures: &FuzzFixtures,
1109 fuzz_state: EvmFuzzState,
1110 progress: Option<&ProgressBar>,
1111 campaign_state: &InvariantCampaignState,
1112 campaign_seed: InvariantCampaignSeed,
1113 corpus_seed: WorkerCorpusSeed,
1114 worker_count: usize,
1115 gas_report_samples: usize,
1116 ) -> Result<(InvariantWorkerOutput, Vec<FuzzBranchFrontier>)> {
1117 let config = invariant_worker_config(config, plan.worker_id, worker_count);
1121 let frontier_limit = if config.corpus.capture_branch_frontiers()
1122 && invariant_worker_collects_evm_cmp_log(&config, plan.worker_id, worker_count)
1123 {
1124 config.corpus.frontier_limit
1125 } else {
1126 0
1127 };
1128 let mut frontier_recorder = FuzzFrontierRecorder::new(frontier_limit);
1129 executor.inspector_mut().set_execution_cancellation(campaign_state.cancellation().clone());
1130
1131 let (mut invariant_test, mut corpus_manager) = Self::prepare_worker(
1132 &mut executor,
1133 plan,
1134 worker_count,
1135 &invariant_contract,
1136 fuzz_fixtures,
1137 fuzz_state,
1138 &runner,
1139 &config,
1140 &campaign_seed,
1141 corpus_seed,
1142 )?;
1143 let mut runs = 0;
1144 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1145
1146 let edge_coverage_enabled = config.corpus.collect_edge_coverage();
1148
1149 'stop: while should_continue_invariant_worker(campaign_state, runs, plan) {
1150 let failures_before_run = invariant_test.test_data.failures.invariant_count();
1152 let failures_checkpoint = invariant_test.test_data.failures.clone();
1153 let failures_revision = failures_checkpoint.revision();
1154 let mut stop_after_run = false;
1155 let mut run_cancelled = false;
1156 let mut observed_call_entries = Vec::<(Vec<ObservedCall>, BasicTxDetails)>::new();
1157
1158 let call_campaign = FuzzCampaign::new(FuzzCampaignMode::Invariant {
1159 check_interval: config.check_interval,
1160 optimization: invariant_contract.is_optimization(),
1161 });
1162
1163 let sequence_plan =
1164 corpus_manager.new_sequence(&mut invariant_test.test_data.branch_runner)?;
1165 let initial_seq = sequence_plan.initial();
1166
1167 let run_depth =
1168 invariant_run_depth(&config, &mut invariant_test.test_data.branch_runner);
1169
1170 let mut corpus_run = Option::<RecordedCallSequence>::None;
1171 let mut frontier_run = (frontier_limit > 0).then(|| RecordedCallSequence {
1172 inputs: Vec::with_capacity(run_depth as usize),
1173 cmp_seq: Vec::with_capacity(run_depth as usize),
1174 });
1175
1176 let mut current_run = InvariantTestRun::new(
1178 initial_seq[0].clone(),
1179 executor.clone(),
1181 run_depth as usize,
1182 );
1183
1184 if config.fail_on_revert && invariant_test.reverts() > 0 {
1186 campaign_state.request_terminal_stop();
1187 return Err(eyre!("call reverted"));
1188 }
1189
1190 let mut call_cmp_values = Vec::new();
1191 let mut call_new_coverage = false;
1192 let mut coverage_prefix_len = 0;
1193 let mut assertion_failure = false;
1194 let mut pre_merge_edges_hash = None;
1195 let mut handler = None;
1196 let mut abort_campaign = false;
1197 let sequence_outcome = call_campaign.run_sequence(
1198 &mut current_run,
1199 run_depth,
1200 |run| {
1201 let tx = run.inputs.last_mut().expect("campaign always has a current input");
1202 (&mut run.executor, tx)
1203 },
1204 |run| run.depth,
1205 |_| campaign_state.should_stop(),
1206 |current_run, event| {
1207 match event {
1208 CampaignEvent::Feedback(call_result) => {
1209 let current_tx = current_run.inputs.last().ok_or_else(|| {
1210 eyre!("no input generated to call fuzzed target.")
1211 })?;
1212 let sel_bytes: [u8; 4] = current_tx
1213 .call_details
1214 .calldata
1215 .get(..4)
1216 .and_then(|selector| selector.try_into().ok())
1217 .unwrap_or_default();
1218 handler =
1219 Some((current_tx.call_details.target, Selector::from(sel_bytes)));
1220 if let Some(fuzzer) =
1221 current_run.executor.inspector_mut().fuzzer.as_mut()
1222 {
1223 invariant_test.fuzz_state.collect_fuzzer_values(fuzzer);
1224 }
1225 call_cmp_values = call_result.evm_cmp_values.take().unwrap_or_default();
1226 let discarded = call_result.result.as_ref() == MAGIC_ASSUME;
1227 if config.show_metrics {
1228 invariant_test.record_metrics(
1229 current_tx,
1230 call_result.reverted,
1231 discarded,
1232 );
1233 }
1234 HitMaps::merge_opt(
1235 &mut current_run.line_coverage,
1236 call_result.line_coverage.take(),
1237 );
1238 assertion_failure = !discarded
1239 && did_fail_on_assert(call_result, &call_result.state_changeset);
1240 pre_merge_edges_hash = assertion_failure
1241 .then(|| error::snapshot_edge_fingerprint(call_result))
1242 .flatten();
1243 call_new_coverage = corpus_manager.merge_edge_coverage(call_result);
1244 if call_new_coverage {
1245 current_run.new_coverage = true;
1246 }
1247 let observed_calls = std::mem::take(&mut call_result.observed_calls);
1248 if call_new_coverage && !observed_calls.is_empty() {
1249 observed_call_entries.push((observed_calls, current_tx.clone()));
1250 }
1251 }
1252 CampaignEvent::Check { result, kind, should_check } => {
1253 let mut result =
1254 result.take().expect("campaign check result is available");
1255 if kind == CampaignCallKind::AssumptionRejected {
1256 current_run.inputs.pop();
1257 current_run.rejects += 1;
1258 if current_run.rejects > config.max_assume_rejects {
1259 invariant_test.set_error(
1260 invariant_contract.anchor(),
1261 InvariantFuzzError::MaxAssumeRejects(
1262 config.max_assume_rejects,
1263 ),
1264 );
1265 campaign_state.request_terminal_stop();
1266 abort_campaign = true;
1267 return Ok(CampaignControl::Stop);
1268 }
1269 return Ok(CampaignControl::Continue);
1270 }
1271 debug_assert_eq!(kind, CampaignCallKind::Accepted);
1272 if let Some(frontier_run) = &mut frontier_run {
1273 frontier_run.inputs.push(
1274 current_run
1275 .inputs
1276 .last()
1277 .expect("accepted call has a campaign input")
1278 .clone(),
1279 );
1280 frontier_run.cmp_seq.push(call_cmp_values.clone());
1281 }
1282 if config.corpus.is_coverage_guided() {
1283 let preserve_revert =
1284 invariant_contract.is_optimization() || config.has_delay();
1285 let retain_for_corpus =
1286 !result.reverted || preserve_revert || call_new_coverage;
1287 if let Some(corpus_run) = &mut corpus_run {
1288 if retain_for_corpus {
1289 corpus_run.inputs.push(
1290 current_run
1291 .inputs
1292 .last()
1293 .expect("accepted call has a campaign input")
1294 .clone(),
1295 );
1296 corpus_run
1297 .cmp_seq
1298 .push(std::mem::take(&mut call_cmp_values));
1299 }
1300 } else if result.reverted && !preserve_revert && call_new_coverage {
1301 let mut cmp_seq = std::mem::take(&mut current_run.cmp_seq);
1302 cmp_seq.push(std::mem::take(&mut call_cmp_values));
1303 corpus_run = Some(RecordedCallSequence {
1304 inputs: current_run.inputs.clone(),
1305 cmp_seq,
1306 });
1307 } else if !result.reverted || preserve_revert {
1308 current_run.cmp_seq.push(std::mem::take(&mut call_cmp_values));
1309 }
1310 if call_new_coverage {
1311 coverage_prefix_len = corpus_run
1312 .as_ref()
1313 .map_or(current_run.inputs.len(), |history| {
1314 history.inputs.len()
1315 });
1316 }
1317 }
1318 let (handler_target, handler_selector) =
1319 handler.take().expect("feedback precedes campaign checks");
1320 let mut state_changeset = std::mem::take(&mut result.state_changeset);
1321 if !result.reverted {
1322 let mapping_slots = current_run
1323 .executor
1324 .inspector()
1325 .fuzzer
1326 .as_ref()
1327 .and_then(|fuzzer| fuzzer.mapping_slots.as_ref());
1328 collect_data(
1329 &invariant_test,
1330 &mut state_changeset,
1331 current_run.inputs.last().expect("checked above"),
1332 &result,
1333 run_depth,
1334 mapping_slots,
1335 );
1336 }
1337
1338 let created_before = current_run.created_contracts.len();
1339 if let Err(error) =
1340 &invariant_test.targeted_contracts.collect_created_contracts(
1341 &state_changeset,
1342 project_contracts,
1343 setup_contracts,
1344 &campaign_seed.artifact_filters,
1345 &mut current_run.created_contracts,
1346 )
1347 {
1348 warn!(target: "forge::test", "{error}");
1349 }
1350 invariant_test.invalidate_metric_key_cache(
1351 ¤t_run.created_contracts[created_before..],
1352 );
1353 current_run
1354 .fuzz_runs
1355 .push(FuzzCase { gas: result.gas_used, stipend: result.stipend });
1356
1357 let continues = if should_check {
1358 let outcome = can_continue(
1359 &invariant_contract,
1360 &mut invariant_test,
1361 current_run,
1362 &config,
1363 result,
1364 &state_changeset,
1365 handler_target,
1366 handler_selector,
1367 assertion_failure,
1368 pre_merge_edges_hash,
1369 )
1370 .map_err(|error| eyre!(error.to_string()))?;
1371 run_cancelled = outcome.cancelled;
1372 outcome.continues
1373 } else {
1374 if result.reverted {
1375 invariant_test.test_data.failures.reverts += 1;
1376 }
1377 if assertion_failure {
1378 let call_reverted = result.reverted;
1379 error::record_handler_assertion_bug(
1380 &invariant_contract,
1381 &config,
1382 &invariant_test.targeted_contracts,
1383 &mut invariant_test.test_data.failures,
1384 &mut current_run.inputs,
1385 handler_target,
1386 handler_selector,
1387 pre_merge_edges_hash,
1388 result,
1389 call_reverted,
1390 invariant_contract.is_optimization(),
1391 );
1392 true
1393 } else if result.reverted && config.fail_on_revert {
1394 let anchor = invariant_contract.anchor();
1395 let case_data = error::InvariantRunCtx {
1396 contract: &invariant_contract,
1397 config: &config,
1398 targeted_contracts: &invariant_test.targeted_contracts,
1399 calldata: ¤t_run.inputs,
1400 }
1401 .failed_case(anchor, config.fail_on_revert, false, result, &[]);
1402 invariant_test.test_data.failures.record_failure(
1403 anchor,
1404 InvariantFuzzError::Revert(case_data),
1405 );
1406 false
1407 } else {
1408 if result.reverted
1409 && !invariant_contract.is_optimization()
1410 && !config.has_delay()
1411 {
1412 current_run.inputs.pop();
1413 }
1414 true
1415 }
1416 };
1417
1418 if run_cancelled {
1419 return Ok(CampaignControl::Stop);
1420 }
1421 if !continues || current_run.depth == run_depth - 1 {
1422 current_run.save_last_run_inputs = true;
1423 }
1424 if !continues {
1425 if invariant_contract.invariant_fns.len() == 1
1426 || config.fail_on_revert
1427 {
1428 campaign_state.request_terminal_stop();
1429 stop_after_run = true;
1430 }
1431 return Ok(CampaignControl::Stop);
1432 }
1433 }
1434 CampaignEvent::Advance => current_run.depth += 1,
1435 CampaignEvent::Next { discarded, depth } => {
1436 current_run.inputs.push(sequence_plan.next(
1437 &mut invariant_test.test_data.branch_runner,
1438 discarded,
1439 depth as usize,
1440 )?);
1441 }
1442 CampaignEvent::PostCheck => {
1443 if !abort_campaign
1446 && !run_cancelled
1447 && invariant_contract.call_after_invariant
1448 && invariant_test.test_data.failures.invariant_count()
1449 == failures_before_run
1450 {
1451 let (broken, hook_cancelled) = assert_after_invariant(
1452 &invariant_contract,
1453 &mut invariant_test,
1454 current_run,
1455 &config,
1456 )
1457 .map_err(|_| eyre!("Failed to call afterInvariant"))?;
1458 if hook_cancelled {
1459 run_cancelled = true;
1460 } else if broken.is_some() {
1461 current_run.save_last_run_inputs = true;
1462 }
1463 }
1464 }
1465 }
1466 Ok(CampaignControl::Continue)
1467 },
1468 )?;
1469 if sequence_outcome == CampaignSequenceOutcome::Cancelled {
1470 run_cancelled = true;
1473 }
1474 if abort_campaign {
1475 break 'stop;
1476 }
1477
1478 if campaign_state.should_stop() && !stop_after_run {
1482 run_cancelled = true;
1483 }
1484
1485 if run_cancelled {
1486 let completed_finding =
1487 invariant_test.test_data.failures.revision() != failures_revision;
1488 if completed_finding {
1489 record_new_invariant_failures(
1490 campaign_state,
1491 &invariant_contract,
1492 &invariant_test.test_data.failures,
1493 );
1494 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1495 } else {
1496 invariant_test.test_data.failures.clone_from(&failures_checkpoint);
1497 }
1498 break 'stop;
1499 }
1500
1501 if invariant_test.test_data.failures.invariant_count() > failures_before_run {
1502 record_new_invariant_failures(
1503 campaign_state,
1504 &invariant_contract,
1505 &invariant_test.test_data.failures,
1506 );
1507 }
1508 if invariant_test.test_data.failures.handler_count()
1509 > failures_checkpoint.handler_count()
1510 {
1511 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1512 }
1513
1514 for (observed_calls, parent_tx) in observed_call_entries {
1515 corpus_manager.hoist_observed_calls(
1516 &observed_calls,
1517 &parent_tx,
1518 &invariant_test.targeted_contracts,
1519 &campaign_seed.sender_filters,
1520 CorpusInsertionMode::Live,
1521 );
1522 }
1523
1524 if let Some(frontier_run) = &frontier_run {
1526 frontier_recorder.capture_sequence(&frontier_run.inputs, &frontier_run.cmp_seq);
1527 }
1528 let optimization = current_run.optimization_value.map(|v| {
1529 let prefix = current_run.inputs[..current_run.optimization_prefix_len].to_vec();
1530 (v, prefix)
1531 });
1532 let mut corpus_inputs = corpus_run
1533 .as_ref()
1534 .map_or(current_run.inputs.as_slice(), |history| history.inputs.as_slice());
1535 let mut corpus_cmp_seq = corpus_run
1536 .as_ref()
1537 .map_or(current_run.cmp_seq.as_slice(), |history| history.cmp_seq.as_slice());
1538 if current_run.new_coverage && !invariant_contract.is_optimization() {
1539 corpus_inputs = &corpus_inputs[..coverage_prefix_len];
1540 corpus_cmp_seq = &corpus_cmp_seq[..coverage_prefix_len];
1541 }
1542 if worker_count > 1 {
1543 corpus_manager.process_inputs_for_campaign(
1544 corpus_inputs,
1545 corpus_cmp_seq,
1546 current_run.new_coverage,
1547 optimization,
1548 );
1549 } else {
1550 corpus_manager.process_inputs(
1551 corpus_inputs,
1552 corpus_cmp_seq,
1553 current_run.new_coverage,
1554 optimization,
1555 );
1556 }
1557
1558 if current_run.save_last_run_inputs {
1560 invariant_test.set_last_run_inputs(¤t_run.inputs);
1561 }
1562 if let Some(value) = current_run.optimization_value {
1563 invariant_test.update_optimization_value(
1564 value,
1565 ¤t_run.inputs[..current_run.optimization_prefix_len],
1566 );
1567 }
1568 invariant_test.merge_line_coverage(current_run.line_coverage.take());
1569 for fuzz_run in ¤t_run.fuzz_runs {
1570 campaign_state.record_call(fuzz_run.gas);
1571 }
1572 current_run.drop_corpus_payloads();
1573 invariant_test.end_run(current_run, gas_report_samples);
1574 runs += 1;
1575 let total_runs = campaign_state.increment_runs();
1576 debug_assert!(
1577 campaign_state.is_timed_campaign() || total_runs <= config.runs,
1578 "worker runs were not distributed correctly"
1579 );
1580 if let Some(progress) = progress {
1581 progress.inc(1);
1582 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1583 let best = invariant_test.test_data.optimization_best_value;
1585 let failure_metrics = campaign_state.failure_metrics();
1586 let broken = failure_metrics.unique_failures.len();
1587 let handler_bugs = failure_metrics.broken_handlers;
1588 let total_invariants = invariant_contract.invariant_fns.len();
1589 if edge_coverage_enabled || best.is_some() || broken > 0 || handler_bugs > 0 {
1590 let mut msg = String::new();
1591 if let Some(best) = best {
1592 msg.push_str(&format!("best: {best}"));
1593 }
1594 if edge_coverage_enabled {
1595 if !msg.is_empty() {
1596 msg.push_str(", ");
1597 }
1598 msg.push_str(&format!("{}", corpus_manager.metrics));
1599 match corpus_manager.time_since_new_edge() {
1600 Some(elapsed) => msg.push_str(&format!(
1601 "\n - time since new edge: {:.1}s",
1602 elapsed.as_secs_f64()
1603 )),
1604 None => msg.push_str("\n - time since new edge: never"),
1605 }
1606 }
1607 if broken > 0 {
1608 if !msg.is_empty() {
1609 msg.push_str(", ");
1610 }
1611 msg.push_str(&format!("❌ {broken}/{total_invariants} broken"));
1612 }
1613 if handler_bugs > 0 {
1614 if !msg.is_empty() {
1615 msg.push_str(", ");
1616 }
1617 msg.push_str(&format!("⚠ {handler_bugs} handler bug(s)"));
1618 }
1619 let msg =
1620 if worker_count > 1 { format!("[w{}] {msg}", plan.worker_id) } else { msg };
1621 progress.set_message(msg);
1622 }
1623 } else if edge_coverage_enabled
1624 && campaign_state.should_emit_metrics_report(DURATION_BETWEEN_METRICS_REPORT)
1625 {
1626 campaign_state.sync_handler_failures(&invariant_test.test_data.failures);
1627 let failure_metrics = campaign_state.failure_metrics();
1628 let (total_txs, total_gas) = campaign_state.throughput_totals();
1629 let throughput = InvariantThroughputMetrics { total_txs, total_gas };
1630 let metrics = build_invariant_progress_json(
1632 InvariantProgressContext {
1633 timestamp_secs: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
1634 contract_name: invariant_contract.name,
1635 optimization_best: invariant_test.test_data.optimization_best_value,
1636 throughput,
1637 elapsed: campaign_state.elapsed(),
1638 worker_id: plan.worker_id,
1639 worker_count,
1640 time_since_new_edge: corpus_manager.time_since_new_edge(),
1641 },
1642 &corpus_manager.metrics,
1643 &failure_metrics,
1644 );
1645 let _ = sh_println!("{}", serde_json::to_string(&metrics)?);
1646 }
1647
1648 if stop_after_run {
1649 break 'stop;
1650 }
1651 }
1652
1653 trace!(?fuzz_fixtures);
1654 invariant_test.fuzz_state.log_stats();
1655
1656 executor.inspector_mut().set_early_exit(campaign_state.early_exit().clone());
1658 Self::shrink_handler_failures(
1659 &config,
1660 &executor,
1661 &mut invariant_test.test_data,
1662 progress,
1663 campaign_state.early_exit(),
1664 );
1665
1666 let InvariantTest { fuzz_state: _, targeted_contracts: _, test_data: result } =
1670 invariant_test;
1671 let reverts = result.failures.reverts;
1672 let (errors, handler_errors) = result.failures.partition();
1673 let worker_result = InvariantFuzzTestResult::new(
1674 errors,
1675 handler_errors,
1676 result.runs,
1677 result.calls,
1678 reverts,
1679 result.last_run_inputs,
1680 result.gas_report_traces,
1681 result.line_coverage,
1682 result.metrics.into_iter().collect(),
1683 if plan.worker_id == 0 { corpus_manager.failed_replays } else { 0 },
1684 1,
1685 result.optimization_best_value,
1686 result.optimization_best_sequence,
1687 );
1688 drop(corpus_manager);
1689 let reported_plan = if campaign_state.is_timed_campaign() {
1690 InvariantWorkerPlan { runs, ..plan }
1691 } else {
1692 plan
1696 };
1697 Ok((
1698 InvariantWorkerOutput { plan: reported_plan, result: worker_result },
1699 frontier_recorder.into_frontiers(),
1700 ))
1701 }
1702
1703 fn shrink_handler_failures(
1704 config: &InvariantConfig,
1705 executor: &Executor<FEN>,
1706 result: &mut InvariantTestData,
1707 progress: Option<&ProgressBar>,
1708 early_exit: &EarlyExit,
1709 ) {
1710 let total = result.failures.handler_count();
1711 if total == 0 {
1712 return;
1713 }
1714
1715 for (idx, error) in result.failures.handler_failures_mut().enumerate() {
1716 if early_exit.should_stop() {
1717 break;
1718 }
1719 let Some(failure) = error.as_handler_assertion_mut() else {
1720 continue;
1721 };
1722 let shrink_progress = shrink::ShrinkProgress::new(
1723 config,
1724 progress,
1725 &format!("handler {:#x}::{}", failure.reverter, failure.selector),
1726 Some((idx + 1, total)),
1727 None,
1728 false,
1729 );
1730 match shrink::shrink_handler_sequence(
1731 config,
1732 &failure.call_sequence,
1733 failure.edge_fingerprint,
1734 executor,
1735 &shrink_progress,
1736 early_exit,
1737 ) {
1738 Ok(shrunk) if !shrunk.is_empty() => {
1739 failure.call_sequence = shrunk;
1740 }
1741 Ok(_) => {}
1742 Err(e) => trace!(target: "forge::test", "handler shrink failed: {e}"),
1743 }
1744 }
1745 }
1746
1747 fn prepare_campaign_seed(
1748 &mut self,
1749 invariant_contract: &InvariantContract<'_>,
1750 initial_handler_failures: std::collections::HashMap<
1751 (Address, Selector),
1752 InvariantFuzzError,
1753 >,
1754 ) -> Result<InvariantCampaignSeed> {
1755 self.select_contract_artifacts(invariant_contract.address)?;
1756 let (sender_filters, targeted_contracts) =
1757 self.select_contracts_and_senders(invariant_contract.address)?;
1758 let targets_are_updatable = targeted_contracts.is_updatable;
1759 let targeted_contracts = targeted_contracts.targets().clone();
1760
1761 Ok(InvariantCampaignSeed {
1762 artifact_filters: self.artifact_filters.clone(),
1763 sender_filters,
1764 targeted_contracts,
1765 targets_are_updatable,
1766 initial_handler_failures,
1767 })
1768 }
1769
1770 #[allow(clippy::too_many_arguments)]
1772 fn prepare_worker(
1773 executor: &mut Executor<FEN>,
1774 plan: InvariantWorkerPlan,
1775 worker_count: usize,
1776 invariant_contract: &InvariantContract<'_>,
1777 fuzz_fixtures: &FuzzFixtures,
1778 fuzz_state: EvmFuzzState,
1779 runner: &TestRunner,
1780 config: &InvariantConfig,
1781 campaign_seed: &InvariantCampaignSeed,
1782 corpus_seed: WorkerCorpusSeed,
1783 ) -> Result<(InvariantTest, WorkerCorpus)> {
1784 let fuzz_state = fuzz_state.into_invariant();
1785 let targeted_contracts = FuzzRunIdentifiedContracts::new(
1786 campaign_seed.targeted_contracts.clone(),
1787 campaign_seed.targets_are_updatable,
1788 );
1789 executor.inspector_mut().collect_evm_cmp_log(invariant_worker_collects_evm_cmp_log(
1790 config,
1791 plan.worker_id,
1792 worker_count,
1793 ));
1794
1795 let generator = TxGenerator::invariant(
1797 fuzz_state.clone(),
1798 campaign_seed.sender_filters.clone(),
1799 targeted_contracts.clone(),
1800 config.clone(),
1801 fuzz_fixtures.clone(),
1802 );
1803
1804 let mapping_slots = targeted_contracts
1807 .targets()
1808 .iter()
1809 .any(|(_, t)| t.storage_layout.is_some())
1810 .then(AddressMap::default);
1811
1812 let extra_cheatcode_addresses = executor.inspector().extra_cheatcode_addresses();
1816 executor.inspector_mut().set_fuzzer(
1817 Fuzzer::new(config.dictionary.max_fuzz_dictionary_values, mapping_slots)
1818 .with_extra_cheatcode_addresses(extra_cheatcode_addresses)
1819 .with_call_recording(config.corpus.is_coverage_guided()),
1820 );
1821
1822 let mut failures = InvariantFailures::new();
1827 for (&(addr, sel), err) in &campaign_seed.initial_handler_failures {
1829 failures.seed_handler_failure(addr, sel, err.clone());
1830 }
1831 invariant_preflight_check(
1832 invariant_contract,
1833 config,
1834 &targeted_contracts,
1835 executor,
1836 &[],
1837 &mut failures,
1838 )?;
1839 if let Some(fuzzer) = executor.inspector_mut().fuzzer.as_mut() {
1840 fuzz_state.collect_fuzzer_values(fuzzer);
1841 let _ = fuzzer.take_observed_calls();
1842 }
1843 let generator = foundry_evm_fuzz::sequence::SequenceGenerator::invariant_with_fixtures(
1844 generator,
1845 fuzz_state.clone(),
1846 fuzz_fixtures.clone(),
1847 targeted_contracts.clone(),
1848 &config.corpus,
1849 )?;
1850 let mut worker = WorkerCorpus::from_seed(
1851 plan.worker_id as usize,
1852 config.corpus.clone(),
1853 generator,
1854 corpus_seed,
1855 )?;
1856
1857 if let Err(err) = worker.seed_from_test_traces(
1858 invariant_contract,
1859 &targeted_contracts,
1860 &campaign_seed.sender_filters,
1861 executor,
1862 ) {
1863 debug!(target: "corpus", %err, "failed to seed corpus from test traces");
1864 }
1865
1866 if config.call_override {
1870 let target_contract_ref = Arc::new(RwLock::new(Address::ZERO));
1871
1872 let handler_addresses: AddressSet =
1875 targeted_contracts.targets().keys().copied().collect();
1876 let override_targets = targeted_contracts
1877 .targets()
1878 .iter()
1879 .filter_map(|(address, contract)| {
1880 let functions = contract.abi_fuzzed_functions().cloned().collect::<Vec<_>>();
1881 (!functions.is_empty()).then_some((*address, functions))
1882 })
1883 .collect::<Vec<_>>();
1884
1885 let call_generator = RandomCallGenerator::new(
1886 invariant_contract.address,
1887 handler_addresses,
1888 runner.clone(),
1889 override_call_strat(
1890 fuzz_state.snapshot(),
1891 override_targets,
1892 target_contract_ref.clone(),
1893 fuzz_fixtures.clone(),
1894 config.dictionary.dictionary_weight,
1895 config.corpus.payable_value_weight,
1896 ),
1897 target_contract_ref,
1898 );
1899
1900 if let Some(fuzzer) = executor.inspector_mut().fuzzer.as_mut() {
1901 fuzzer.call_generator = Some(call_generator);
1902 }
1903 }
1904
1905 let mut invariant_test =
1906 InvariantTest::new(fuzz_state, targeted_contracts, failures, runner.clone());
1907
1908 if invariant_contract.is_optimization() {
1912 let (opt_best_value, opt_best_sequence) = worker.optimization_initial_state();
1913 if let Some(value) = opt_best_value {
1914 invariant_test.update_optimization_value(value, &opt_best_sequence);
1915 }
1916 }
1917
1918 Ok((invariant_test, worker))
1919 }
1920
1921 pub fn select_contract_artifacts(&mut self, invariant_address: Address) -> Result<()> {
1931 let targeted_artifact_selectors = self
1932 .executor
1933 .call_sol_default(invariant_address, &IInvariantTest::targetArtifactSelectorsCall {});
1934
1935 for IInvariantTest::FuzzArtifactSelector { artifact, selectors } in
1937 targeted_artifact_selectors
1938 {
1939 let identifier = self.validate_selected_contract(artifact, &selectors)?;
1940 self.artifact_filters.targeted.entry(identifier).or_default().extend(selectors);
1941 }
1942
1943 let targeted_artifacts = self
1944 .executor
1945 .call_sol_default(invariant_address, &IInvariantTest::targetArtifactsCall {});
1946 let excluded_artifacts = self
1947 .executor
1948 .call_sol_default(invariant_address, &IInvariantTest::excludeArtifactsCall {});
1949
1950 for contract in excluded_artifacts {
1952 let identifier = self.validate_selected_contract(contract, &[])?;
1953
1954 if !self.artifact_filters.excluded.contains(&identifier) {
1955 self.artifact_filters.excluded.push(identifier);
1956 }
1957 }
1958
1959 for (artifact, contract) in self.project_contracts.iter() {
1961 if contract
1962 .abi
1963 .functions()
1964 .filter(|func| {
1965 !matches!(
1966 func.state_mutability,
1967 alloy_json_abi::StateMutability::Pure
1968 | alloy_json_abi::StateMutability::View
1969 )
1970 })
1971 .count()
1972 == 0
1973 && !self.artifact_filters.excluded.contains(&artifact.identifier())
1974 {
1975 self.artifact_filters.excluded.push(artifact.identifier());
1976 }
1977 }
1978
1979 for contract in targeted_artifacts {
1982 let identifier = self.validate_selected_contract(contract, &[])?;
1983
1984 if !self.artifact_filters.targeted.contains_key(&identifier)
1985 && !self.artifact_filters.excluded.contains(&identifier)
1986 {
1987 self.artifact_filters.targeted.insert(identifier, vec![]);
1988 }
1989 }
1990 Ok(())
1991 }
1992
1993 fn validate_selected_contract(
1996 &mut self,
1997 contract: String,
1998 selectors: &[FixedBytes<4>],
1999 ) -> Result<String> {
2000 if let Some((artifact, contract_data)) =
2001 self.project_contracts.find_by_name_or_identifier(&contract)?
2002 {
2003 for selector in selectors {
2005 contract_data
2006 .abi
2007 .functions()
2008 .find(|func| func.selector().as_slice() == selector.as_slice())
2009 .wrap_err(format!("{contract} does not have the selector {selector:?}"))?;
2010 }
2011
2012 return Ok(artifact.identifier());
2013 }
2014 eyre::bail!(
2015 "{contract} not found in the project. Allowed format: `contract_name` or `contract_path:contract_name`."
2016 );
2017 }
2018
2019 pub fn select_contracts_and_senders(
2022 &self,
2023 to: Address,
2024 ) -> Result<(SenderFilters, FuzzRunIdentifiedContracts)> {
2025 let targeted_senders =
2026 self.executor.call_sol_default(to, &IInvariantTest::targetSendersCall {});
2027 let mut excluded_senders =
2028 self.executor.call_sol_default(to, &IInvariantTest::excludeSendersCall {});
2029 excluded_senders.extend([
2031 CHEATCODE_ADDRESS,
2032 HARDHAT_CONSOLE_ADDRESS,
2033 DEFAULT_CREATE2_DEPLOYER,
2034 ]);
2035 excluded_senders.extend(PRECOMPILES);
2037 let sender_filters = SenderFilters::new(targeted_senders, excluded_senders);
2038
2039 let selected = self.executor.call_sol_default(to, &IInvariantTest::targetContractsCall {});
2040 let excluded = self.executor.call_sol_default(to, &IInvariantTest::excludeContractsCall {});
2041
2042 let contracts = self
2043 .setup_contracts
2044 .iter()
2045 .filter(|&(addr, (identifier, _))| {
2046 if *addr == to && selected.contains(&to) {
2048 return true;
2049 }
2050
2051 *addr != to
2052 && *addr != CHEATCODE_ADDRESS
2053 && *addr != HARDHAT_CONSOLE_ADDRESS
2054 && (selected.is_empty() || selected.contains(addr))
2055 && (excluded.is_empty() || !excluded.contains(addr))
2056 && self.artifact_filters.matches(identifier)
2057 })
2058 .map(|(addr, (identifier, abi))| {
2059 (
2060 *addr,
2061 TargetedContract::new(identifier.clone(), abi.clone())
2062 .with_project_contracts(self.project_contracts),
2063 )
2064 })
2065 .collect();
2066 let mut contracts = TargetedContracts { inner: contracts };
2067
2068 self.target_interfaces(to, &mut contracts)?;
2069
2070 self.select_selectors(to, &mut contracts)?;
2071 self.exclude_default_storage_hook_callbacks(&mut contracts)?;
2072
2073 if contracts.is_empty() {
2075 eyre::bail!("No contracts to fuzz.");
2076 }
2077 if contracts.fuzzed_functions().next().is_none() {
2078 eyre::bail!("No functions to fuzz.");
2079 }
2080
2081 Ok((sender_filters, FuzzRunIdentifiedContracts::new(contracts, selected.is_empty())))
2082 }
2083
2084 fn exclude_default_storage_hook_callbacks(
2089 &self,
2090 targeted_contracts: &mut TargetedContracts,
2091 ) -> Result<()> {
2092 let Some(cheatcodes) = self.executor.inspector().cheatcodes.as_deref() else {
2093 return Ok(());
2094 };
2095 let callbacks =
2096 cheatcodes
2097 .storage_load_hooks()
2098 .chain(cheatcodes.storage_store_hooks())
2099 .map(|(_, hook)| (hook.callback_target, Selector::from(hook.callback_selector)))
2100 .chain(cheatcodes.mapping_storage_store_hooks().map(|(_, _, hook)| {
2101 (hook.callback_target, Selector::from(hook.callback_selector))
2102 }))
2103 .collect::<HashSet<_>>();
2104
2105 for (target, selector) in callbacks {
2106 let Some(contract) = targeted_contracts.get_mut(&target) else { continue };
2107 if !contract.targeted_functions.is_empty()
2108 || contract.function_by_selector(selector).is_none()
2109 {
2110 continue;
2111 }
2112 contract.add_selectors([selector], true)?;
2113 }
2114 Ok(())
2115 }
2116
2117 pub fn target_interfaces(
2122 &self,
2123 invariant_address: Address,
2124 targeted_contracts: &mut TargetedContracts,
2125 ) -> Result<()> {
2126 let interfaces = self
2127 .executor
2128 .call_sol_default(invariant_address, &IInvariantTest::targetInterfacesCall {});
2129
2130 let mut combined = TargetedContracts::new();
2136
2137 for IInvariantTest::FuzzInterface { addr, artifacts } in &interfaces {
2140 for identifier in artifacts {
2142 if let Some((_, contract_data)) =
2144 self.project_contracts.iter().find(|(artifact, _)| {
2145 &artifact.name == identifier || &artifact.identifier() == identifier
2146 })
2147 {
2148 let abi = &contract_data.abi;
2149 combined
2150 .entry(*addr)
2152 .and_modify(|entry| {
2154 entry.abi.functions.extend(abi.functions.clone());
2156 entry.rebuild_function_lookups();
2157 })
2158 .or_insert_with(|| {
2160 let mut contract =
2161 TargetedContract::new(identifier.clone(), abi.clone());
2162 contract.storage_layout =
2163 contract_data.storage_layout.as_ref().map(Arc::clone);
2164 contract
2165 });
2166 }
2167 }
2168 }
2169
2170 targeted_contracts.extend(combined.inner);
2171
2172 Ok(())
2173 }
2174
2175 pub fn select_selectors(
2178 &self,
2179 address: Address,
2180 targeted_contracts: &mut TargetedContracts,
2181 ) -> Result<()> {
2182 for (address, (identifier, _)) in self.setup_contracts {
2183 if let Some(selectors) = self.artifact_filters.targeted.get(identifier) {
2184 self.add_address_with_functions(*address, selectors, false, targeted_contracts)?;
2185 }
2186 }
2187
2188 let mut target_test_selectors = vec![];
2189 let mut excluded_test_selectors = vec![];
2190
2191 let selectors =
2193 self.executor.call_sol_default(address, &IInvariantTest::targetSelectorsCall {});
2194 for IInvariantTest::FuzzSelector { addr, selectors } in selectors {
2195 if addr == address {
2196 target_test_selectors = selectors.clone();
2197 }
2198 self.add_address_with_functions(addr, &selectors, false, targeted_contracts)?;
2199 }
2200
2201 let excluded_selectors =
2203 self.executor.call_sol_default(address, &IInvariantTest::excludeSelectorsCall {});
2204 for IInvariantTest::FuzzSelector { addr, selectors } in excluded_selectors {
2205 if addr == address {
2206 excluded_test_selectors = selectors.clone();
2209 }
2210 self.add_address_with_functions(addr, &selectors, true, targeted_contracts)?;
2211 }
2212
2213 if target_test_selectors.is_empty()
2214 && let Some(target) = targeted_contracts.get(&address)
2215 {
2216 let selectors: Vec<_> = target
2220 .abi
2221 .functions()
2222 .filter_map(|func| {
2223 if matches!(
2224 func.state_mutability,
2225 alloy_json_abi::StateMutability::Pure
2226 | alloy_json_abi::StateMutability::View
2227 ) || func.is_reserved()
2228 || excluded_test_selectors.contains(&func.selector())
2229 {
2230 None
2231 } else {
2232 Some(func.selector())
2233 }
2234 })
2235 .collect();
2236 self.add_address_with_functions(address, &selectors, false, targeted_contracts)?;
2237 }
2238
2239 Ok(())
2240 }
2241
2242 fn add_address_with_functions(
2244 &self,
2245 address: Address,
2246 selectors: &[Selector],
2247 should_exclude: bool,
2248 targeted_contracts: &mut TargetedContracts,
2249 ) -> eyre::Result<()> {
2250 if selectors.is_empty() {
2252 return Ok(());
2253 }
2254
2255 let contract = match targeted_contracts.entry(address) {
2256 Entry::Occupied(entry) => entry.into_mut(),
2257 Entry::Vacant(entry) => {
2258 let (identifier, abi) = self.setup_contracts.get(&address).ok_or_else(|| {
2259 eyre::eyre!(
2260 "[{}] address does not have an associated contract: {}",
2261 if should_exclude { "excludeSelectors" } else { "targetSelectors" },
2262 address
2263 )
2264 })?;
2265 entry.insert(
2266 TargetedContract::new(identifier.clone(), abi.clone())
2267 .with_project_contracts(self.project_contracts),
2268 )
2269 }
2270 };
2271 contract.add_selectors(selectors.iter().copied(), should_exclude)?;
2272 Ok(())
2273 }
2274}
2275
2276fn collect_data<FEN: FoundryEvmNetwork>(
2280 invariant_test: &InvariantTest,
2281 state_changeset: &mut AddressMap<Account>,
2282 tx: &BasicTxDetails,
2283 call_result: &RawCallResult<FEN>,
2284 run_depth: u32,
2285 mapping_slots: Option<&AddressMap<foundry_common::mapping_slots::MappingSlots>>,
2286) {
2287 let sender_changeset = match state_changeset.entry(tx.sender) {
2289 AddressMapEntry::Occupied(entry) => entry
2290 .get()
2291 .info
2292 .code
2293 .as_ref()
2294 .is_none_or(|code| code.is_empty())
2295 .then(|| entry.remove()),
2296 AddressMapEntry::Vacant(_) => None,
2297 };
2298
2299 invariant_test.fuzz_state.collect_values_from_call(
2301 &invariant_test.targeted_contracts,
2302 tx,
2303 &call_result.result,
2304 &call_result.logs,
2305 &*state_changeset,
2306 run_depth,
2307 mapping_slots,
2308 );
2309
2310 if let Some(cmp_values) = &call_result.sancov_cmp_values {
2312 invariant_test.fuzz_state.collect_typed_cmp_values(
2313 cmp_values.iter().map(|s| (s.width, alloy_primitives::B256::from(s.value))),
2314 );
2315 }
2316 if let Some(changed) = sender_changeset {
2318 state_changeset.insert(tx.sender, changed);
2319 }
2320}
2321
2322pub(crate) fn call_after_invariant_function<FEN: FoundryEvmNetwork>(
2330 executor: &Executor<FEN>,
2331 to: Address,
2332) -> Result<(RawCallResult<FEN>, bool), EvmError<FEN>> {
2333 let calldata = Bytes::from_static(&IInvariantTest::afterInvariantCall::SELECTOR);
2334 let mut call_result = executor.call_raw(CALLER, to, calldata, U256::ZERO)?;
2335 let success = executor.is_raw_call_mut_success_handler_gate(to, &mut call_result);
2336 Ok((call_result, success))
2337}
2338
2339pub(crate) fn call_invariant_function<FEN: FoundryEvmNetwork>(
2346 executor: &Executor<FEN>,
2347 address: Address,
2348 calldata: Bytes,
2349) -> Result<(RawCallResult<FEN>, bool)> {
2350 let mut call_result = executor.call_raw(CALLER, address, calldata, U256::ZERO)?;
2351 let success = executor.is_raw_call_mut_success_handler_gate(address, &mut call_result);
2352 Ok((call_result, success))
2353}
2354
2355pub fn execute_tx<FEN: FoundryEvmNetwork>(
2363 executor: &mut Executor<FEN>,
2364 tx: &BasicTxDetails,
2365) -> Result<RawCallResult<FEN>> {
2366 super::campaign::execute_invariant_tx(executor, &mut tx.clone())
2367}
2368
2369pub fn execute_tx_and_register_created<FEN: FoundryEvmNetwork>(
2374 executor: &mut Executor<FEN>,
2375 tx: &BasicTxDetails,
2376 targeted_contracts: &FuzzRunIdentifiedContracts,
2377 dynamic_target_ctx: &DynamicTargetCtx<'_>,
2378 created_contracts: &mut Vec<Address>,
2379) -> Result<()> {
2380 let (kind, call_result) = super::campaign::execute_invariant_replay_tx(executor, tx)?;
2381 if kind == CampaignCallKind::AssumptionRejected {
2382 return Err(eyre!("invariant replay prefix rejected by vm.assume"));
2383 }
2384 targeted_contracts.collect_created_contracts(
2385 &call_result.state_changeset,
2386 dynamic_target_ctx.project_contracts,
2387 dynamic_target_ctx.setup_contracts,
2388 dynamic_target_ctx.artifact_filters,
2389 created_contracts,
2390 )?;
2391 Ok(())
2392}
2393
2394#[cfg(test)]
2395mod tests {
2396
2397 use super::*;
2398 use crate::executors::ExecutorBuilder;
2399 use alloy_evm::EvmEnv;
2400 use foundry_cheatcodes::CheatsConfig;
2401 use foundry_config::FuzzDictionaryConfig;
2402 use foundry_evm_core::{backend::Backend, evm::EthEvmNetwork};
2403 use foundry_evm_fuzz::CallDetails;
2404 use proptest::{
2405 prelude::any,
2406 strategy::{Strategy, ValueTree},
2407 test_runner::Config,
2408 };
2409 use revm::{
2410 bytecode::Bytecode,
2411 context::{Block, TxEnv},
2412 database::InMemoryDB,
2413 };
2414 use serde_json::json;
2415 use std::{sync::mpsc, thread};
2416
2417 fn first_generated_u64(runner: &mut TestRunner) -> u64 {
2418 any::<u64>().new_tree(runner).unwrap().current()
2419 }
2420
2421 fn test_runner() -> TestRunner {
2422 TestRunner::new(Config { failure_persistence: None, ..Default::default() })
2423 }
2424
2425 fn seeded_test_runner(seed: U256) -> TestRunner {
2426 let config = Config { failure_persistence: None, ..Default::default() };
2427 let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
2428 TestRunner::new_with_rng(config, rng)
2429 }
2430
2431 #[test]
2432 fn assumption_rejection_restores_delayed_block_environment() {
2433 let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
2434 let mut executor = ExecutorBuilder::default()
2435 .inspectors(|stack| stack.cheatcodes(Arc::new(CheatsConfig::default())))
2436 .gas_limit(1 << 24)
2437 .build(EvmEnv::default(), TxEnv::default(), backend, Default::default());
2438 let target = Address::repeat_byte(0x11);
2439 let mut code = vec![0x6e]; code.extend_from_slice(MAGIC_ASSUME);
2441 code.extend_from_slice(&[0x60, 0x00, 0x52, 0x60, 0x0f, 0x60, 0x11, 0xf3]);
2442 executor.set_code(target, Bytecode::new_raw(Bytes::from(code))).unwrap();
2443
2444 let initial_block = executor.evm_env().block_env.clone();
2445 let initial_cheatcode_block =
2446 executor.inspector().cheatcodes.as_ref().unwrap().block.clone();
2447 let expected_timestamp = initial_block.timestamp() + U256::from(10);
2448 let expected_number = initial_block.number() + U256::from(5);
2449 let tx = BasicTxDetails {
2450 warp: Some(U256::from(10)),
2451 roll: Some(U256::from(5)),
2452 sender: Address::ZERO,
2453 call_details: CallDetails { target, calldata: Bytes::new(), value: None },
2454 };
2455 let mut state = (executor, tx);
2456 let campaign = FuzzCampaign::new(FuzzCampaignMode::Invariant {
2457 check_interval: 1,
2458 optimization: false,
2459 });
2460
2461 let outcome = campaign
2462 .run_sequence(
2463 &mut state,
2464 1,
2465 |state| (&mut state.0, &mut state.1),
2466 |_| 0,
2467 |_| false,
2468 |state, event| {
2469 match event {
2470 CampaignEvent::Feedback(_) => {
2471 let block = &state.0.evm_env().block_env;
2472 assert_eq!(block.timestamp(), expected_timestamp);
2473 assert_eq!(block.number(), expected_number);
2474 let block = state
2475 .0
2476 .inspector()
2477 .cheatcodes
2478 .as_ref()
2479 .unwrap()
2480 .block
2481 .as_ref()
2482 .unwrap();
2483 assert_eq!(block.timestamp(), expected_timestamp);
2484 assert_eq!(block.number(), expected_number);
2485 }
2486 CampaignEvent::Check { kind, .. } => {
2487 assert_eq!(kind, CampaignCallKind::AssumptionRejected);
2488 return Ok(CampaignControl::Stop);
2489 }
2490 _ => {}
2491 }
2492 Ok(CampaignControl::Continue)
2493 },
2494 )
2495 .unwrap();
2496
2497 assert_eq!(outcome, CampaignSequenceOutcome::Stopped);
2498 assert_eq!(state.0.evm_env().block_env, initial_block);
2499 assert_eq!(state.0.inspector().cheatcodes.as_ref().unwrap().block, initial_cheatcode_block);
2500
2501 let (kind, _) =
2502 crate::executors::campaign::execute_invariant_replay_tx(&mut state.0, &state.1)
2503 .unwrap();
2504 assert_eq!(kind, CampaignCallKind::AssumptionRejected);
2505 assert_eq!(state.0.evm_env().block_env, initial_block);
2506 assert_eq!(state.0.inspector().cheatcodes.as_ref().unwrap().block, initial_cheatcode_block);
2507 }
2508
2509 #[test]
2510 fn invariant_worker_seed_preserves_master_seed_and_derives_workers() {
2511 let seed = U256::from(0x1234);
2512
2513 assert_eq!(invariant_worker_seed(seed, 0), seed);
2514 assert_ne!(invariant_worker_seed(seed, 1), seed);
2515 assert_ne!(invariant_worker_seed(seed, 1), invariant_worker_seed(seed, 2));
2516 assert_ne!(invariant_worker_seed(seed, 1), invariant_worker_seed(U256::from(0x5678), 1));
2517 }
2518
2519 #[test]
2520 fn invariant_worker_runner_preserves_seed_for_master_worker() {
2521 let seed = U256::from(0x1234);
2522 let mut seeded_runner = seeded_test_runner(seed);
2523 let mut parent = test_runner();
2524 let mut worker = invariant_worker_runner(&mut parent, 0, Some(seed));
2525
2526 assert_eq!(first_generated_u64(&mut worker), first_generated_u64(&mut seeded_runner));
2527 }
2528
2529 #[test]
2530 fn invariant_worker_runner_uses_seed_independent_of_parent_rng_state() {
2531 let seed = U256::from(0x1234);
2532 let mut parent = test_runner();
2533 let mut advanced_parent = test_runner();
2534 let _ = first_generated_u64(&mut advanced_parent);
2535
2536 let mut worker = invariant_worker_runner(&mut parent, 1, Some(seed));
2537 let mut worker_from_advanced_parent =
2538 invariant_worker_runner(&mut advanced_parent, 1, Some(seed));
2539
2540 assert_eq!(
2541 first_generated_u64(&mut worker),
2542 first_generated_u64(&mut worker_from_advanced_parent)
2543 );
2544 }
2545
2546 #[test]
2547 fn invariant_focus_seed_preserves_configured_seed() {
2548 let configured_seed = U256::from(0x1234);
2549 let mut parent = test_runner();
2550
2551 assert_eq!(
2552 invariant_focus_seed(&mut parent, Some(configured_seed), 2),
2553 Some(configured_seed)
2554 );
2555 assert_eq!(invariant_focus_seed(&mut parent, Some(configured_seed), 1), None);
2556 }
2557
2558 #[test]
2559 fn invariant_focus_seed_uses_parent_rng_when_unconfigured() {
2560 let mut parent = seeded_test_runner(U256::ONE);
2561 let mut matching_parent = seeded_test_runner(U256::ONE);
2562 let mut different_parent = seeded_test_runner(U256::from(2));
2563
2564 let focus_seed = invariant_focus_seed(&mut parent, None, 2).unwrap();
2565
2566 assert_eq!(focus_seed, invariant_focus_seed(&mut matching_parent, None, 2).unwrap());
2567 assert_ne!(focus_seed, invariant_focus_seed(&mut different_parent, None, 2).unwrap());
2568 assert_eq!(invariant_focus_seed(&mut parent, None, 1), None);
2569 }
2570
2571 #[test]
2572 fn invariant_progress_json_includes_throughput_fields() {
2573 let throughput = InvariantThroughputMetrics { total_txs: 2, total_gas: 50 };
2574
2575 let payload = build_invariant_progress_json(
2576 InvariantProgressContext {
2577 timestamp_secs: 123,
2578 contract_name: "InvariantContract",
2579 optimization_best: Some(I256::try_from(42).unwrap()),
2580 throughput,
2581 elapsed: Duration::from_secs(10),
2582 worker_id: 1,
2583 worker_count: 4,
2584 time_since_new_edge: Some(Duration::from_secs(3)),
2585 },
2586 &json!({ "corpus_count": 7 }),
2587 &InvariantFailureMetrics::default(),
2588 );
2589
2590 assert_eq!(payload["timestamp"], json!(123));
2591 assert_eq!(payload["contract"], json!("InvariantContract"));
2592 assert!(payload.get("invariant").is_none());
2593 assert_eq!(payload["metrics"]["corpus_count"], json!(7));
2594 assert_eq!(payload["metrics"]["broken_assertions"], json!(0));
2595 assert!(payload["metrics"].get("broken_handlers").is_none());
2596 assert_eq!(payload["total_txs"], json!(2));
2597 assert_eq!(payload["total_gas"], json!(50));
2598 assert_eq!(payload["tps"], json!(0.2));
2599 assert_eq!(payload["gps"], json!(5.0));
2600 assert!(payload.get("tx_per_sec").is_none());
2601 assert!(payload.get("gas_per_sec").is_none());
2602 assert_eq!(payload["worker"]["id"], json!(1));
2603 assert_eq!(payload["worker"]["count"], json!(4));
2604 assert_eq!(payload["optimization_best"], json!("42"));
2605 }
2606
2607 #[test]
2608 fn invariant_worker_count_keeps_short_campaigns_single_worker() {
2609 assert_eq!(
2610 max_invariant_workers_for_campaign(0, DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP),
2611 1
2612 );
2613 assert_eq!(
2614 max_invariant_workers_for_campaign(
2615 MIN_RUNS_PER_INVARIANT_WORKER - 1,
2616 DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2617 ),
2618 1
2619 );
2620 assert_eq!(
2621 max_invariant_workers_for_campaign(
2622 MIN_RUNS_PER_INVARIANT_WORKER,
2623 DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2624 ),
2625 1
2626 );
2627 assert_eq!(
2628 max_invariant_workers_for_campaign(
2629 MIN_RUNS_PER_INVARIANT_WORKER * 2,
2630 DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP
2631 ),
2632 2
2633 );
2634 assert_eq!(max_invariant_workers_for_campaign(256, 100_000), 5);
2635 }
2636
2637 #[test]
2638 fn invariant_run_depth_random_min_depth_zero_never_returns_zero() {
2639 let mut runner = test_runner();
2640 let config = InvariantConfig {
2641 depth: 8,
2642 min_depth: 0,
2643 depth_mode: InvariantDepthMode::Random,
2644 ..Default::default()
2645 };
2646
2647 for _ in 0..128 {
2648 assert!((1..=8).contains(&invariant_run_depth(&config, &mut runner)));
2649 }
2650 }
2651
2652 #[test]
2653 fn invariant_run_depth_fixed_zero_preserves_zero() {
2654 let mut runner = test_runner();
2655 let config = InvariantConfig {
2656 depth: 0,
2657 depth_mode: InvariantDepthMode::Fixed,
2658 ..Default::default()
2659 };
2660
2661 assert_eq!(invariant_run_depth(&config, &mut runner), 0);
2662 }
2663
2664 #[test]
2665 fn invariant_worker_config_keeps_single_worker_default() {
2666 let config = InvariantConfig::default();
2667
2668 assert_eq!(
2669 invariant_worker_config(config, 0, 1).corpus.corpus_random_sequence_weight,
2670 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2671 );
2672 }
2673
2674 #[test]
2675 fn invariant_worker_config_uses_one_exploratory_worker_with_default_config() {
2676 let config = InvariantConfig::default();
2677
2678 assert_eq!(
2679 invariant_worker_config(config.clone(), 0, 4).corpus.corpus_random_sequence_weight,
2680 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2681 );
2682 assert_eq!(
2683 invariant_worker_config(config.clone(), 1, 4).corpus.corpus_random_sequence_weight,
2684 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2685 );
2686 assert_eq!(
2687 invariant_worker_config(config.clone(), 2, 4).corpus.corpus_random_sequence_weight,
2688 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2689 );
2690 assert_eq!(
2691 invariant_worker_config(config, 3, 4).corpus.corpus_random_sequence_weight,
2692 FuzzCorpusConfig::ENSEMBLE_CORPUS_RANDOM_SEQUENCE_WEIGHT
2693 );
2694 }
2695
2696 #[test]
2697 fn invariant_worker_config_keeps_two_worker_campaign_on_default() {
2698 let config = InvariantConfig::default();
2699
2700 assert_eq!(
2701 invariant_worker_config(config.clone(), 0, 2).corpus.corpus_random_sequence_weight,
2702 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2703 );
2704 assert_eq!(
2705 invariant_worker_config(config, 1, 2).corpus.corpus_random_sequence_weight,
2706 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2707 );
2708 }
2709
2710 #[test]
2711 fn invariant_worker_config_uses_last_worker_for_three_worker_campaign() {
2712 let config = InvariantConfig::default();
2713
2714 assert_eq!(
2715 invariant_worker_config(config.clone(), 0, 3).corpus.corpus_random_sequence_weight,
2716 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2717 );
2718 assert_eq!(
2719 invariant_worker_config(config.clone(), 1, 3).corpus.corpus_random_sequence_weight,
2720 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2721 );
2722 assert_eq!(
2723 invariant_worker_config(config, 2, 3).corpus.corpus_random_sequence_weight,
2724 FuzzCorpusConfig::ENSEMBLE_CORPUS_RANDOM_SEQUENCE_WEIGHT
2725 );
2726 }
2727
2728 #[test]
2729 fn invariant_worker_config_preserves_explicit_corpus_random_sequence_weight() {
2730 let config = InvariantConfig {
2731 corpus: FuzzCorpusConfig {
2732 corpus_random_sequence_weight: 25,
2733 ..FuzzCorpusConfig::default()
2734 },
2735 corpus_random_sequence_weight_configured: true,
2736 ..InvariantConfig::default()
2737 };
2738
2739 assert_eq!(
2740 invariant_worker_config(config.clone(), 1, 4).corpus.corpus_random_sequence_weight,
2741 25
2742 );
2743 assert_eq!(invariant_worker_config(config, 0, 1).corpus.corpus_random_sequence_weight, 25);
2744
2745 let config = InvariantConfig {
2746 corpus_random_sequence_weight_configured: true,
2747 ..InvariantConfig::default()
2748 };
2749
2750 assert_eq!(
2751 invariant_worker_config(config, 1, 4).corpus.corpus_random_sequence_weight,
2752 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
2753 );
2754 }
2755
2756 #[test]
2757 fn invariant_worker_count_preserves_fixed_workers() {
2758 let mut config = InvariantConfig {
2759 runs: MIN_RUNS_PER_INVARIANT_WORKER * 4,
2760 workers: foundry_config::InvariantWorkers::Fixed(
2761 std::num::NonZeroUsize::new(4).unwrap(),
2762 ),
2763 ..Default::default()
2764 };
2765 assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2766
2767 config.corpus.show_edge_coverage = true;
2768 assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2769
2770 config.corpus.show_edge_coverage = false;
2771 config.corpus.corpus_dir = Some(std::path::PathBuf::from("corpus"));
2772 assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2773
2774 config.runs = MIN_RUNS_PER_INVARIANT_WORKER - 1;
2775 config.timeout = None;
2776 assert_eq!(invariant_worker_count_with_threads(&config, 8, 1), 4);
2777
2778 config.timeout = Some(1);
2779 assert_eq!(invariant_worker_count_with_threads(&config, 8, 4), 4);
2780 }
2781
2782 #[test]
2783 fn invariant_worker_count_does_not_cap_configured_workers_by_available_threads() {
2784 let config = InvariantConfig {
2785 runs: MIN_RUNS_PER_INVARIANT_WORKER * 8,
2786 workers: foundry_config::InvariantWorkers::Fixed(
2787 std::num::NonZeroUsize::new(8).unwrap(),
2788 ),
2789 ..Default::default()
2790 };
2791
2792 assert_eq!(invariant_worker_count_with_threads(&config, 4, 1), 8);
2793 }
2794
2795 #[test]
2796 fn invariant_worker_count_splits_available_threads_for_auto_workers() {
2797 let mut config = InvariantConfig {
2798 runs: MIN_RUNS_PER_INVARIANT_WORKER * 4,
2799 depth: DEFAULT_DEPTH_FOR_INVARIANT_WORKER_CAP,
2800 workers: foundry_config::InvariantWorkers::Auto,
2801 ..Default::default()
2802 };
2803
2804 assert_eq!(invariant_worker_count_with_threads(&config, 4, 1), 4);
2805 assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2806 assert_eq!(invariant_worker_count_with_threads(&config, 8, 3), 2);
2807 assert_eq!(invariant_worker_count_with_threads(&config, 3, 8), 1);
2808 assert_eq!(invariant_worker_count_with_threads(&config, 0, 0), 1);
2809
2810 config.runs = MIN_RUNS_PER_INVARIANT_WORKER - 1;
2811 assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 1);
2812
2813 config.depth = 100_000;
2814 assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2815
2816 config.timeout = Some(1);
2817 assert_eq!(invariant_worker_count_with_threads(&config, 8, 2), 4);
2818 }
2819
2820 fn function(signature: &str) -> Function {
2821 Function::parse(signature).unwrap()
2822 }
2823
2824 fn targeted_contract(identifier: &str, functions: Vec<Function>) -> TargetedContract {
2825 let mut abi = alloy_json_abi::JsonAbi::new();
2826 for function in functions {
2827 abi.functions.entry(function.name.clone()).or_default().push(function);
2828 }
2829 TargetedContract::new(identifier.to_string(), abi)
2830 }
2831
2832 #[test]
2833 fn campaign_terminal_stop_interrupts_handler_without_accepting_run() {
2834 const GAS_LIMIT: u64 = 1 << 24;
2835 let invariant_address = Address::repeat_byte(0x11);
2836 let handler_address = Address::repeat_byte(0x22);
2837 let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
2838 let mut executor = ExecutorBuilder::default().gas_limit(GAS_LIMIT).build(
2839 EvmEnv::default(),
2840 TxEnv::default(),
2841 backend,
2842 Default::default(),
2843 );
2844 executor
2846 .set_code(
2847 invariant_address,
2848 Bytecode::new_raw(Bytes::from_static(&[
2849 0x60, 0x01, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3,
2850 ])),
2851 )
2852 .unwrap();
2853 executor
2855 .set_code(
2856 handler_address,
2857 Bytecode::new_raw(Bytes::from_static(&[0x5b, 0x60, 0x00, 0x56])),
2858 )
2859 .unwrap();
2860
2861 let (handler_entered_tx, handler_entered_rx) = mpsc::channel();
2862 let (handler_release_tx, handler_release_rx) = mpsc::channel();
2863 executor.inspector_mut().set_early_exit_test_gate(
2864 handler_entered_tx,
2865 handler_release_rx,
2866 0,
2867 );
2868
2869 let invariant = function("invariant_ok() view returns (bool)");
2870 let mut invariant_abi = alloy_json_abi::JsonAbi::new();
2871 invariant_abi.functions.entry(invariant.name.clone()).or_default().push(invariant.clone());
2872 let invariant_contract = InvariantContract::new(
2873 invariant_address,
2874 "InvariantTest",
2875 vec![(&invariant, false)],
2876 0,
2877 false,
2878 &invariant_abi,
2879 );
2880
2881 let handler = function("loopForever()");
2882 let mut handler_contract = targeted_contract("Handler", vec![handler.clone()]);
2883 handler_contract.targeted_functions = vec![handler];
2884 let mut targeted_contracts = TargetedContracts::new();
2885 targeted_contracts.insert(handler_address, handler_contract);
2886 let campaign_seed = InvariantCampaignSeed {
2887 artifact_filters: ArtifactFilters::default(),
2888 sender_filters: SenderFilters::new(vec![CALLER], Vec::new()),
2889 targeted_contracts,
2890 targets_are_updatable: false,
2891 initial_handler_failures: Map::default(),
2892 };
2893
2894 let config =
2895 InvariantConfig { runs: 1, depth: 1, show_metrics: false, ..Default::default() };
2896 let campaign_state = InvariantCampaignState::new(EarlyExit::new(false), None);
2897 let fuzz_state =
2898 EvmFuzzState::new(&[], &InMemoryDB::default(), FuzzDictionaryConfig::default(), None);
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::repeat_byte(0x11);
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::repeat_byte(0x55);
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::repeat_byte(0x44);
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::repeat_byte(0x22);
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::repeat_byte(0x33);
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::repeat_byte(0x66);
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}