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