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