1use crate::{
4 MultiContractRunner, TestFilter,
5 coverage::HitMaps,
6 fuzz::{BaseCounterExample, FuzzTestResult},
7 multi_runner::{
8 FuzzMinimizeConfig, FuzzMinimizeMode, FuzzMinimizeObservation, LibraryDeployment,
9 TestContract, TestFunctionMatcher, TestRunnerConfig,
10 is_generated_symbolic_regression_contract,
11 },
12 progress::TestsProgress,
13 result::{
14 InvariantFailure, InvariantOutcome, InvariantPredicateResult, SuiteResult,
15 SymbolicArtifactRef, SymbolicCallTrace, SymbolicCorpusSeedMetadata, SymbolicCorpusSeedRef,
16 SymbolicCounterexample, SymbolicCounterexampleArtifact, SymbolicCounterexampleArtifactKind,
17 SymbolicCounterexampleCall, SymbolicCounterexampleMinimization,
18 SymbolicCounterexampleReplaySemantics, SymbolicCounterexampleTestIdentity,
19 SymbolicInvariantArtifactFailure, SymbolicInvariantFailureSite, SymbolicReplayMetadata,
20 SymbolicReplayStatus, SymbolicResult, TestKind, TestResult, TestSetup, TestStatus,
21 invariant_campaign_display_name, invariant_kind,
22 },
23 symbolic_minimizer::{
24 MinimizedSequence, minimize_sequence_counterexample, minimize_single_call_counterexample,
25 },
26};
27use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
28use alloy_json_abi::{Function, JsonAbi, StateMutability};
29use alloy_primitives::{
30 Address, B256, Bytes, I256, Selector, U256, address, hex, keccak256,
31 map::{Entry, HashMap, HashSet},
32};
33use eyre::Result;
34use foundry_common::{
35 LIBRARY_DEPLOYER, TestFunctionExt, TestFunctionKind, contracts::ContractsByAddress,
36};
37use foundry_compilers::utils::canonicalized;
38use foundry_config::{
39 Config, FuzzConfig, FuzzCorpusConfig, FuzzDictionaryConfig, InlineConfig, InvariantConfig,
40 SymbolicConfig,
41};
42use foundry_evm::{
43 constants::{CALLER, MAGIC_ASSUME},
44 core::{backend::DatabaseExt, evm::FoundryEvmNetwork},
45 decode::{RevertDecoder, SkipReason},
46 executors::{
47 CallResult, DynamicTargetCtx, EvmError, Executor, ITest, InvariantReplayOptions,
48 MinimizationReplayInput, RawCallResult, ShowmapOpts, ShowmapReplayTarget,
49 StatelessReplayTarget, canonical_replay_dirs,
50 fuzz::FuzzedExecutor,
51 invariant::{
52 CheckSequenceFailureSite, CheckSequenceOptions, CheckSequenceOutcome,
53 HandlerAssertionFailure, InvariantExecutor, InvariantFuzzError, ReplayErrorResult,
54 check_sequence, did_fail_on_assert, execute_tx, execute_tx_and_register_created,
55 replay_error, replay_handler_failure_sequence, replay_run,
56 },
57 persist_corpus_seed, read_corpus_dir, replay_corpus_to_showmap,
58 replay_sequence_for_minimization, should_ignore_revert,
59 },
60 fuzz::{
61 BasicTxDetails, CallDetails, CounterExample, FuzzFixtures, fixture_name,
62 invariant::{
63 FuzzRunIdentifiedContracts, InvariantContract, InvariantSettings, SenderFilters,
64 is_optimization_invariant,
65 },
66 strategies::EvmFuzzState,
67 },
68 inspectors::{CmpOperands, cheatcodes::Vm::AccountAccess},
69 revm::{bytecode::opcode, primitives::hardfork::SpecId},
70 traces::{TraceKind, TraceRequirements, load_contracts},
71};
72use foundry_evm_networks::NetworkVariant;
73use foundry_evm_symbolic::{
74 SymbolicBranchTarget, SymbolicConcreteInput, SymbolicExecutor, SymbolicInvariantCandidateInput,
75 SymbolicInvariantCounterexampleKind, SymbolicInvariantRunInput, SymbolicInvariantRunResult,
76 SymbolicInvariantStep, SymbolicInvariantTarget, SymbolicRunInput, SymbolicRunResult,
77 SymbolicStats, SymbolicStopReason, SymbolicStorageAssignment,
78};
79use itertools::Itertools;
80use proptest::test_runner::{RngAlgorithm, TestError, TestRng, TestRunner};
81use rayon::prelude::*;
82use serde::{Deserialize, Serialize};
83use std::{
84 borrow::Cow,
85 cmp::min,
86 collections::BTreeMap,
87 ops::Deref,
88 path::{Path, PathBuf},
89 sync::{Arc, Mutex},
90 time::Instant,
91};
92use tokio::signal;
93use tracing::Span;
94
95const FUZZ_BRANCH_FRONTIER_SCHEMA: &str = "foundry:fuzz.branch-frontiers@v1";
96const STATEFUL_FUZZ_BRANCH_FRONTIER_SCHEMA: &str = "foundry:fuzz.branch-frontiers@v2";
97const FUZZ_BRANCH_FRONTIER_FILE: &str = "branch-frontiers.json";
98
99#[derive(Deserialize)]
100struct FuzzBranchFrontierArtifact {
101 schema: String,
102 version: u32,
103 test: String,
104 #[serde(default)]
105 sequences: Vec<Vec<BasicTxDetails>>,
106 frontiers: Vec<FuzzBranchFrontierRecord>,
107}
108
109#[derive(Deserialize)]
110struct FuzzBranchFrontierRecord {
111 id: u64,
112 #[serde(skip)]
113 both_results_retained: bool,
114 call_index: usize,
115 #[serde(default)]
116 sequence: Vec<BasicTxDetails>,
117 sequence_index: Option<usize>,
118 site: FuzzBranchFrontierSite,
119 operands: FuzzBranchFrontierOperands,
120}
121
122#[derive(Clone, Copy, Deserialize)]
123struct FuzzBranchFrontierSite {
124 address: Address,
125 pc: usize,
126 opcode: u8,
127}
128
129#[derive(Deserialize)]
130struct FuzzBranchFrontierOperands {
131 result: bool,
132}
133
134fn select_stateful_frontiers(
135 frontiers: Vec<FuzzBranchFrontierRecord>,
136 limit: usize,
137 explicit_selection: bool,
138) -> Vec<FuzzBranchFrontierRecord> {
139 if limit == 0 {
140 return Vec::new();
141 }
142
143 if explicit_selection {
144 return sample_stateful_frontiers(frontiers, limit);
145 }
146
147 let mut candidates = Vec::with_capacity(frontiers.len());
148 let mut lower_priority = Vec::new();
149 for frontier in frontiers {
150 if frontier.both_results_retained {
151 lower_priority.push(frontier);
152 } else {
153 candidates.push(frontier);
154 }
155 }
156
157 let deep_context_index = if !candidates.is_empty() && limit > 1 {
158 lower_priority
159 .iter()
160 .enumerate()
161 .max_by_key(|(_, frontier)| (frontier.call_index, frontier.id))
162 .map(|(index, _)| index)
163 } else {
164 None
165 };
166 let deep_context = deep_context_index.map(|index| lower_priority.swap_remove(index));
167 let mut selected = select_context_diverse_frontiers(candidates, limit);
168 selected.extend(select_context_diverse_frontiers(
169 lower_priority,
170 limit.saturating_sub(selected.len()),
171 ));
172 if let Some(deep_context) = deep_context {
173 if selected.len() == limit {
174 let mut context_counts = HashMap::<(Option<usize>, usize), usize>::default();
175 for frontier in &selected {
176 *context_counts
177 .entry((frontier.sequence_index, frontier.call_index))
178 .or_default() += 1;
179 }
180 let repeated_context = selected
181 .iter()
182 .enumerate()
183 .filter(|(_, frontier)| {
184 context_counts
185 .get(&(frontier.sequence_index, frontier.call_index))
186 .is_some_and(|count| *count > 1)
187 })
188 .min_by_key(|(_, frontier)| frontier.id)
189 .map(|(index, _)| index);
190 if let Some(index) = repeated_context {
191 selected.remove(index);
192 } else {
193 selected.pop();
194 }
195 }
196 selected.push(deep_context);
197 }
198 selected
199}
200
201fn select_context_diverse_frontiers(
202 mut frontiers: Vec<FuzzBranchFrontierRecord>,
203 limit: usize,
204) -> Vec<FuzzBranchFrontierRecord> {
205 if limit == 0 {
206 return Vec::new();
207 }
208 if frontiers.len() <= limit {
209 return frontiers;
210 }
211
212 frontiers.sort_unstable_by_key(|frontier| {
213 (frontier.sequence_index, frontier.call_index, frontier.id)
214 });
215 let mut representatives = Vec::<FuzzBranchFrontierRecord>::new();
216 let mut remaining = Vec::new();
217 for frontier in frontiers {
218 let context = (frontier.sequence_index, frontier.call_index);
219 if let Some(previous) = representatives.last_mut()
220 && (previous.sequence_index, previous.call_index) == context
221 {
222 remaining.push(std::mem::replace(previous, frontier));
223 } else {
224 representatives.push(frontier);
225 }
226 }
227
228 let mut selected = sample_stateful_frontiers(representatives, limit);
229 selected.extend(sample_stateful_frontiers(remaining, limit - selected.len()));
230 selected
231}
232
233fn sample_stateful_frontiers(
234 mut frontiers: Vec<FuzzBranchFrontierRecord>,
235 limit: usize,
236) -> Vec<FuzzBranchFrontierRecord> {
237 if limit == 0 {
238 return Vec::new();
239 }
240 frontiers.sort_unstable_by_key(|frontier| (frontier.call_index, frontier.id));
241 if frontiers.len() <= limit {
242 return frontiers;
243 }
244
245 let total = frontiers.len();
246 let denominator = 2 * limit as u128;
247 let mut indexes =
248 (0..limit).map(|index| (((2 * index + 1) as u128 * total as u128) / denominator) as usize);
249 let mut next = indexes.next();
250 frontiers
251 .into_iter()
252 .enumerate()
253 .filter_map(|(index, frontier)| {
254 (Some(index) == next).then(|| {
255 next = indexes.next();
256 frontier
257 })
258 })
259 .collect()
260}
261
262fn comparison_result(opcode: u8, lhs: U256, rhs: U256) -> Option<bool> {
263 match opcode {
264 opcode::EQ => Some(lhs == rhs),
265 opcode::LT => Some(lhs < rhs),
266 opcode::GT => Some(lhs > rhs),
267 opcode::SLT => Some(I256::from_raw(lhs) < I256::from_raw(rhs)),
268 opcode::SGT => Some(I256::from_raw(lhs) > I256::from_raw(rhs)),
269 opcode::ISZERO => Some(lhs.is_zero()),
270 _ => None,
271 }
272}
273
274fn frontier_comparison_flipped(
275 site: FuzzBranchFrontierSite,
276 observed_result: bool,
277 comparisons: &[CmpOperands],
278) -> bool {
279 comparisons.iter().any(|comparison| {
280 comparison.address == site.address
281 && comparison.pc == site.pc
282 && comparison.opcode == site.opcode
283 && comparison_result(comparison.opcode, comparison.op1, comparison.op2)
284 == Some(!observed_result)
285 })
286}
287
288pub(crate) struct InvariantCampaignScope<'a> {
289 pub config: &'a Config,
290 pub inline_config: &'a InlineConfig,
291 pub contract_name: &'a str,
292 pub all_override_networks: &'a [NetworkVariant],
293 pub pass_network: Option<&'a NetworkVariant>,
294}
295
296struct InvariantCampaignSelection<'a> {
297 matched_boolean_invariant_fns: Vec<&'a Function>,
298 merge_boolean_suite: bool,
299 shared_boolean_namespace: bool,
300 boolean_suite_anchor: Option<&'a Function>,
301 optimization_anchors: usize,
302}
303
304impl InvariantCampaignSelection<'_> {
305 const fn anchor_count(&self) -> usize {
306 self.optimization_anchors
307 + if self.matched_boolean_invariant_fns.is_empty() {
308 0
309 } else if self.merge_boolean_suite {
310 1
311 } else {
312 self.matched_boolean_invariant_fns.len()
313 }
314 }
315}
316
317pub(crate) fn count_runnable_invariant_campaign_anchors(
318 abi: &JsonAbi,
319 filter: &dyn TestFilter,
320 scope: InvariantCampaignScope<'_>,
321) -> usize {
322 let invariant_fns = abi.functions().filter(|func| func.is_invariant_test()).collect::<Vec<_>>();
323 if invariant_fns.iter().any(|func| !func.inputs.is_empty()) {
324 return 0;
325 }
326
327 let functions = abi
328 .functions()
329 .filter(|func| filter.matches_test_function(func))
330 .filter(|func| {
331 function_matches_network_pass(
332 scope.all_override_networks,
333 scope.pass_network,
334 scope.inline_config.network_for(
335 &scope.config.profile,
336 scope.contract_name,
337 &func.name,
338 ),
339 )
340 })
341 .collect::<Vec<_>>();
342
343 select_invariant_campaigns(
344 &invariant_fns,
345 &functions,
346 scope.config,
347 scope.inline_config,
348 scope.contract_name,
349 )
350 .anchor_count()
351}
352
353pub(crate) fn function_matches_network_pass(
354 all_override_networks: &[NetworkVariant],
355 pass_network: Option<&NetworkVariant>,
356 func_network: Option<NetworkVariant>,
357) -> bool {
358 if all_override_networks.is_empty() {
359 return true;
360 }
361 match pass_network {
362 None => func_network.is_none_or(|network| !all_override_networks.contains(&network)),
363 Some(target) => func_network.as_ref() == Some(target),
364 }
365}
366
367pub(crate) fn inline_config_for(
368 config: &Config,
369 inline_config: &InlineConfig,
370 contract_name: &str,
371 func: Option<&Function>,
372) -> Result<Config> {
373 let function = func.map(|f| f.name.as_str()).unwrap_or("");
374 Ok(config.merge_inline_provider(inline_config.provide(contract_name, function))?)
375}
376
377fn invariant_suite_configs_match(
378 config: &Config,
379 inline_config: &InlineConfig,
380 contract_name: &str,
381 funcs: &[&Function],
382) -> bool {
383 let Some((anchor, rest)) = funcs.split_first() else {
384 return true;
385 };
386 let anchor_config = match inline_config_for(config, inline_config, contract_name, Some(anchor))
387 {
388 Ok(config) => config.invariant,
389 Err(_) => return false,
390 };
391 rest.iter().all(|func| {
392 inline_config_for(config, inline_config, contract_name, Some(func))
393 .map(|config| config.invariant == anchor_config)
394 .unwrap_or(false)
395 })
396}
397
398fn select_invariant_campaigns<'a>(
399 invariant_fns: &[&'a Function],
400 functions: &[&'a Function],
401 config: &Config,
402 inline_config: &InlineConfig,
403 contract_name: &str,
404) -> InvariantCampaignSelection<'a> {
405 let boolean_invariant_fns = invariant_fns
406 .iter()
407 .copied()
408 .filter(|func| !is_optimization_invariant(func))
409 .collect::<Vec<_>>();
410 let matched_boolean_invariant_fns = functions
411 .iter()
412 .copied()
413 .filter(|func| func.is_invariant_test() && !is_optimization_invariant(func))
414 .collect::<Vec<_>>();
415 let optimization_anchors = functions
416 .iter()
417 .filter(|func| func.is_invariant_test() && is_optimization_invariant(func))
418 .count();
419
420 let canonical_boolean_anchor = boolean_invariant_fns.first().copied();
424 let merge_boolean_suite = !matched_boolean_invariant_fns.is_empty()
425 && invariant_suite_configs_match(
426 config,
427 inline_config,
428 contract_name,
429 &matched_boolean_invariant_fns,
430 );
431 let shared_boolean_namespace = merge_boolean_suite
432 && invariant_suite_configs_match(
433 config,
434 inline_config,
435 contract_name,
436 &boolean_invariant_fns,
437 );
438 let boolean_suite_anchor = merge_boolean_suite
439 .then(|| {
440 canonical_boolean_anchor
441 .filter(|anchor| matched_boolean_invariant_fns.contains(anchor))
442 .or_else(|| matched_boolean_invariant_fns.first().copied())
443 })
444 .flatten();
445
446 InvariantCampaignSelection {
447 matched_boolean_invariant_fns,
448 merge_boolean_suite,
449 shared_boolean_namespace,
450 boolean_suite_anchor,
451 optimization_anchors,
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use foundry_common::EmptyTestFilter;
459 use foundry_config::NatSpec;
460
461 const CONTRACT_NAME: &str = "src/Test.t.sol:InvariantTest";
462
463 fn stateful_frontier_record(
464 id: u64,
465 sequence_index: usize,
466 call_index: usize,
467 both_results_retained: bool,
468 ) -> FuzzBranchFrontierRecord {
469 FuzzBranchFrontierRecord {
470 id,
471 both_results_retained,
472 call_index,
473 sequence: Vec::new(),
474 sequence_index: Some(sequence_index),
475 site: FuzzBranchFrontierSite {
476 address: Address::ZERO,
477 pc: id as usize,
478 opcode: opcode::EQ,
479 },
480 operands: FuzzBranchFrontierOperands { result: false },
481 }
482 }
483
484 #[test]
485 fn symbolic_artifact_file_name_hashes_full_identity() {
486 let single = symbolic_artifact_file_name(
487 "src/A.t.sol:Contract",
488 "test_collision()",
489 SymbolicCounterexampleArtifactKind::SingleCall,
490 );
491 let same_file_component_different_contract = symbolic_artifact_file_name(
492 "src/B.t.sol:Contract",
493 "test_collision()",
494 SymbolicCounterexampleArtifactKind::SingleCall,
495 );
496 let same_contract_different_kind = symbolic_artifact_file_name(
497 "src/A.t.sol:Contract",
498 "test_collision()",
499 SymbolicCounterexampleArtifactKind::Sequence,
500 );
501
502 assert_ne!(single, same_file_component_different_contract);
503 assert_ne!(single, same_contract_different_kind);
504
505 let hash = single
506 .strip_prefix("test_collision__-")
507 .and_then(|value| value.strip_suffix(".json"))
508 .expect("file name should include sanitized value prefix and json suffix");
509 assert_eq!(hash.len(), 32);
510 }
511
512 #[test]
513 fn stateful_frontier_paths_include_artifact_pass_and_campaign() {
514 let root = Path::new("/tmp/frontiers");
515 let first =
516 invariant_frontier_dir(root, "src/a/Same.t.sol:Same", None, "ethereum", "single");
517 let other_artifact =
518 invariant_frontier_dir(root, "src/b/Same.t.sol:Same", None, "ethereum", "single");
519 let other_profile =
520 invariant_frontier_dir(root, "src/a/Same.t.sol:Same", None, "tempo", "override");
521 let default_pass =
522 invariant_frontier_dir(root, "src/a/Same.t.sol:Same", None, "ethereum", "default");
523 let override_pass =
524 invariant_frontier_dir(root, "src/a/Same.t.sol:Same", None, "ethereum", "override");
525 let isolated = invariant_frontier_dir(
526 root,
527 "src/a/Same.t.sol:Same",
528 Some("invariant_one"),
529 "ethereum",
530 "single",
531 );
532
533 assert_ne!(first, other_artifact);
534 assert_ne!(first, other_profile);
535 assert_ne!(default_pass, override_pass);
536 assert_ne!(first, isolated);
537 assert!(first.ends_with("ethereum/single/shared"));
538 assert!(isolated.ends_with("ethereum/single/isolated/invariant_one"));
539
540 let mut corpus = FuzzCorpusConfig {
541 corpus_dir: Some(PathBuf::from("/tmp/corpus")),
542 frontier_dir: Some(root.to_path_buf()),
543 ..Default::default()
544 };
545 let failures = invariant_suite_paths(
546 &mut corpus,
547 PathBuf::from("/tmp/persist"),
548 "src/a/Same.t.sol:Same",
549 Some("invariant_one"),
550 "ethereum",
551 "single",
552 );
553 assert_eq!(
554 corpus.corpus_dir,
555 Some(canonicalized(PathBuf::from("/tmp/corpus/Same/invariant_one")))
556 );
557 assert_eq!(corpus.frontier_dir, Some(canonicalized(isolated)));
558 assert_eq!(failures, canonicalized(PathBuf::from("/tmp/persist/failures/Same")));
559 }
560
561 #[test]
562 fn symbolic_sequence_failure_identity_includes_failure_site() {
563 let outcome = |site: CheckSequenceFailureSite| CheckSequenceOutcome {
564 success: false,
565 replayed_entirely: false,
566 reason: Some("same reason".to_string()),
567 calls_count: 1,
568 reverts: 0,
569 failure_site: Some(site),
570 sequence_assertion_failure: true,
571 };
572 let site = |target: u8, fingerprint: u8| CheckSequenceFailureSite::SequenceCall {
573 target: Address::with_last_byte(target),
574 selector: Selector::from([0, 0, 0, 1]),
575 fingerprint: B256::from([fingerprint; 32]),
576 };
577 let expected = outcome(site(1, 1));
578
579 assert!(same_sequence_failure(&outcome(site(1, 1)), &expected));
580 assert!(!same_sequence_failure(&outcome(site(2, 1)), &expected));
581 assert!(!same_sequence_failure(&outcome(site(1, 2)), &expected));
582 }
583
584 #[test]
585 fn stateful_frontiers_sample_sequence_depth() {
586 let frontiers =
587 [(6, 7), (0, 1), (8, 9), (3, 4), (9, 9), (2, 3), (5, 6), (1, 2), (7, 8), (4, 5)]
588 .into_iter()
589 .map(|(id, call_index)| {
590 stateful_frontier_record(id, id as usize, call_index, false)
591 })
592 .collect();
593
594 let ids = select_stateful_frontiers(frontiers, 5, false)
595 .into_iter()
596 .map(|frontier| frontier.id)
597 .collect::<Vec<_>>();
598
599 assert_eq!(ids, [1, 3, 5, 7, 9]);
600 }
601
602 #[test]
603 fn stateful_frontiers_reserve_deep_retained_context() {
604 let frontiers = || {
605 (0..12)
606 .map(|id| stateful_frontier_record(id, id as usize, id as usize, id >= 10))
607 .collect()
608 };
609
610 let single_id = select_stateful_frontiers(frontiers(), 1, false)[0].id;
611 assert_eq!(single_id, 5);
612
613 let ids = select_stateful_frontiers(frontiers(), 5, false)
614 .into_iter()
615 .map(|frontier| frontier.id)
616 .collect::<Vec<_>>();
617
618 assert_eq!(ids, [1, 3, 5, 7, 11]);
619 }
620
621 #[test]
622 fn stateful_frontiers_prioritize_distinct_call_contexts() {
623 let frontiers = [(0, 0, 0), (1, 0, 0), (2, 1, 1), (3, 1, 1), (4, 2, 2)]
624 .into_iter()
625 .map(|(id, sequence_index, call_index)| {
626 stateful_frontier_record(id, sequence_index, call_index, false)
627 })
628 .collect();
629
630 let ids = select_stateful_frontiers(frontiers, 3, false)
631 .into_iter()
632 .map(|frontier| frontier.id)
633 .collect::<Vec<_>>();
634
635 assert_eq!(ids, [1, 3, 4]);
636 }
637
638 #[test]
639 fn stateful_frontier_reservation_keeps_primary_contexts() {
640 let frontiers = [
641 (0, 0, 0, false),
642 (1, 0, 0, false),
643 (2, 1, 1, false),
644 (3, 2, 2, false),
645 (4, 3, 3, true),
646 ]
647 .into_iter()
648 .map(|(id, sequence_index, call_index, both_results_retained)| {
649 stateful_frontier_record(id, sequence_index, call_index, both_results_retained)
650 })
651 .collect();
652
653 let ids = select_stateful_frontiers(frontiers, 4, false)
654 .into_iter()
655 .map(|frontier| frontier.id)
656 .collect::<Vec<_>>();
657
658 assert_eq!(ids, [1, 2, 3, 4]);
659 }
660
661 #[test]
662 fn stateful_frontier_reservation_keeps_fallback_contexts() {
663 let frontiers =
664 [(0, 0, 0, false), (1, 1, 1, true), (2, 1, 1, true), (3, 2, 2, true), (4, 3, 3, true)]
665 .into_iter()
666 .map(|(id, sequence_index, call_index, both_results_retained)| {
667 stateful_frontier_record(id, sequence_index, call_index, both_results_retained)
668 })
669 .collect();
670
671 let ids = select_stateful_frontiers(frontiers, 4, false)
672 .into_iter()
673 .map(|frontier| frontier.id)
674 .collect::<Vec<_>>();
675
676 assert_eq!(ids, [0, 2, 3, 4]);
677 }
678
679 #[test]
680 fn stateful_frontiers_fill_from_retained_outcomes() {
681 let frontiers =
682 (0..5).map(|id| stateful_frontier_record(id, id as usize, id as usize, true)).collect();
683
684 let ids = select_stateful_frontiers(frontiers, 4, false)
685 .into_iter()
686 .map(|frontier| frontier.id)
687 .collect::<Vec<_>>();
688
689 assert_eq!(ids, [0, 1, 3, 4]);
690 }
691
692 #[test]
693 fn stateful_frontier_replay_requires_opposite_result_at_same_site() {
694 let address = Address::with_last_byte(1);
695 let site = FuzzBranchFrontierSite { address, pc: 7, opcode: opcode::LT };
696 let comparison = |address, pc, op1, op2| CmpOperands {
697 address,
698 pc,
699 opcode: opcode::LT,
700 op1: U256::from(op1),
701 op2: U256::from(op2),
702 };
703
704 assert!(frontier_comparison_flipped(site, true, &[comparison(address, 7, 2, 1)]));
705 assert!(!frontier_comparison_flipped(site, true, &[comparison(address, 7, 1, 2)]));
706 assert!(!frontier_comparison_flipped(
707 site,
708 true,
709 &[comparison(Address::with_last_byte(2), 7, 2, 1)]
710 ));
711 assert!(!frontier_comparison_flipped(site, true, &[comparison(address, 8, 2, 1)]));
712 }
713
714 fn count_anchors(abi: &JsonAbi, inline_config: &InlineConfig) -> usize {
715 let config = Config::default();
716 count_runnable_invariant_campaign_anchors(
717 abi,
718 &EmptyTestFilter::default(),
719 InvariantCampaignScope {
720 config: &config,
721 inline_config,
722 contract_name: CONTRACT_NAME,
723 all_override_networks: &[],
724 pass_network: None,
725 },
726 )
727 }
728
729 #[test]
730 fn runnable_campaign_anchor_count_merges_boolean_suite_and_counts_optimizations() {
731 let abi = JsonAbi::parse([
732 "function invariantOne() external",
733 "function invariantTwo() external",
734 "function invariantOptimizeA() external returns (int256)",
735 "function invariantOptimizeB() external returns (int256)",
736 ])
737 .unwrap();
738
739 assert_eq!(count_anchors(&abi, &InlineConfig::new()), 3);
740 }
741
742 #[test]
743 fn runnable_campaign_anchor_count_splits_boolean_suite_when_configs_differ() {
744 let abi = JsonAbi::parse([
745 "function invariantOne() external",
746 "function invariantTwo() external",
747 ])
748 .unwrap();
749 let mut inline_config = InlineConfig::new();
750 inline_config
751 .insert(&NatSpec {
752 contract: CONTRACT_NAME.to_string(),
753 function: Some("invariantTwo".to_string()),
754 line: "1:1".to_string(),
755 docs: "forge-config: default.invariant.depth = 1".to_string(),
756 })
757 .unwrap();
758
759 assert_eq!(count_anchors(&abi, &inline_config), 2);
760 }
761
762 #[test]
763 fn selected_campaign_merges_without_changing_namespace() {
764 let abi = JsonAbi::parse([
765 "function invariantOne() external",
766 "function invariantTwo() external",
767 "function invariantThree() external",
768 ])
769 .unwrap();
770 let functions = abi.functions().collect::<Vec<_>>();
771 let selected = functions
772 .iter()
773 .copied()
774 .filter(|func| func.name != "invariantThree")
775 .collect::<Vec<_>>();
776 let mut inline_config = InlineConfig::new();
777 inline_config
778 .insert(&NatSpec {
779 contract: CONTRACT_NAME.to_string(),
780 function: Some("invariantThree".to_string()),
781 line: "1:1".to_string(),
782 docs: "forge-config: default.invariant.fail-on-revert = true".to_string(),
783 })
784 .unwrap();
785 let config = Config::default();
786 let selection = select_invariant_campaigns(
787 &functions,
788 &selected,
789 &config,
790 &inline_config,
791 CONTRACT_NAME,
792 );
793 assert_eq!(selection.anchor_count(), 1);
794 assert!(selection.merge_boolean_suite);
795 assert!(!selection.shared_boolean_namespace);
796
797 let uniform = select_invariant_campaigns(
798 &functions,
799 &selected,
800 &config,
801 &InlineConfig::new(),
802 CONTRACT_NAME,
803 );
804 assert_eq!(uniform.anchor_count(), 1);
805 assert!(uniform.merge_boolean_suite);
806 assert!(uniform.shared_boolean_namespace);
807 }
808
809 #[test]
810 fn runnable_campaign_anchor_count_splits_boolean_suite_when_corpus_weight_provenance_differs() {
811 let abi = JsonAbi::parse([
812 "function invariantOne() external",
813 "function invariantTwo() external",
814 ])
815 .unwrap();
816 let mut inline_config = InlineConfig::new();
817 inline_config
818 .insert(&NatSpec {
819 contract: CONTRACT_NAME.to_string(),
820 function: Some("invariantTwo".to_string()),
821 line: "1:1".to_string(),
822 docs: "forge-config: default.invariant.corpus_random_sequence_weight = 10"
823 .to_string(),
824 })
825 .unwrap();
826
827 assert_eq!(count_anchors(&abi, &inline_config), 2);
828 }
829
830 #[test]
831 fn runnable_campaign_anchor_count_respects_network_pass() {
832 let abi = JsonAbi::parse(["function invariantTempoOnly() external"]).unwrap();
833 let mut inline_config = InlineConfig::new();
834 inline_config
835 .insert(&NatSpec {
836 contract: CONTRACT_NAME.to_string(),
837 function: Some("invariantTempoOnly".to_string()),
838 line: "1:1".to_string(),
839 docs: r#"forge-config: default.networks.network = "tempo""#.to_string(),
840 })
841 .unwrap();
842 let config = Config::default();
843 let override_networks = [NetworkVariant::Tempo];
844
845 let default_pass = count_runnable_invariant_campaign_anchors(
846 &abi,
847 &EmptyTestFilter::default(),
848 InvariantCampaignScope {
849 config: &config,
850 inline_config: &inline_config,
851 contract_name: CONTRACT_NAME,
852 all_override_networks: &override_networks,
853 pass_network: None,
854 },
855 );
856 let tempo_pass = count_runnable_invariant_campaign_anchors(
857 &abi,
858 &EmptyTestFilter::default(),
859 InvariantCampaignScope {
860 config: &config,
861 inline_config: &inline_config,
862 contract_name: CONTRACT_NAME,
863 all_override_networks: &override_networks,
864 pass_network: Some(&NetworkVariant::Tempo),
865 },
866 );
867
868 assert_eq!(default_pass, 0);
869 assert_eq!(tempo_pass, 1);
870 }
871}
872
873pub struct ContractRunner<'a, FEN: FoundryEvmNetwork> {
875 name: &'a str,
877 contract: &'a TestContract,
879 executor: Executor<FEN>,
881 progress: Option<&'a TestsProgress>,
883 tokio_handle: tokio::runtime::Handle,
885 span: tracing::Span,
887 tcfg: Cow<'a, TestRunnerConfig<FEN>>,
889 mcr: &'a MultiContractRunner<FEN>,
891 num_invariant_campaign_anchors: usize,
893}
894
895pub(crate) struct ContractRunnerContext<'a> {
896 pub(crate) progress: Option<&'a TestsProgress>,
897 pub(crate) tokio_handle: tokio::runtime::Handle,
898 pub(crate) num_invariant_campaign_anchors: usize,
899}
900
901impl<'a, FEN: FoundryEvmNetwork> Deref for ContractRunner<'a, FEN> {
902 type Target = Cow<'a, TestRunnerConfig<FEN>>;
903
904 #[inline(always)]
905 fn deref(&self) -> &Self::Target {
906 &self.tcfg
907 }
908}
909
910impl<'a, FEN: FoundryEvmNetwork> ContractRunner<'a, FEN> {
911 pub(crate) fn new(
912 name: &'a str,
913 contract: &'a TestContract,
914 executor: Executor<FEN>,
915 span: Span,
916 mcr: &'a MultiContractRunner<FEN>,
917 context: ContractRunnerContext<'a>,
918 ) -> Self {
919 Self {
920 name,
921 contract,
922 executor,
923 progress: context.progress,
924 tokio_handle: context.tokio_handle,
925 span,
926 tcfg: Cow::Borrowed(&mcr.tcfg),
927 mcr,
928 num_invariant_campaign_anchors: context.num_invariant_campaign_anchors,
929 }
930 }
931
932 fn function_matches_network_pass(&self, func: &Function) -> bool {
939 function_matches_network_pass(
940 &self.mcr.tcfg.multi_network.all_override_networks,
941 self.mcr.tcfg.multi_network.pass_network.as_ref(),
942 self.mcr.inline_config.network_for(&self.tcfg.config.profile, self.name, &func.name),
943 )
944 }
945
946 pub fn setup(&mut self, call_setup: bool) -> TestSetup {
949 self._setup(call_setup).unwrap_or_else(|err| {
950 if err.to_string().contains("skipped") {
951 TestSetup::skipped(err.to_string())
952 } else {
953 TestSetup::failed(err.to_string())
954 }
955 })
956 }
957
958 fn _setup(&mut self, call_setup: bool) -> Result<TestSetup> {
959 trace!(call_setup, "setting up");
960
961 self.apply_contract_inline_config()?;
962
963 self.executor.set_balance(self.sender, U256::MAX)?;
965 self.executor.set_balance(CALLER, U256::MAX)?;
966
967 self.executor.set_nonce(self.sender, 1)?;
969
970 self.executor.set_balance(LIBRARY_DEPLOYER, U256::MAX)?;
972
973 let rd = &self.mcr.revert_decoder;
974 let mut result = TestSetup::default();
975 let mut pending_account_diffs = Vec::new();
976 match self.mcr.library_deployment {
977 LibraryDeployment::Nonce => {
978 for (nonce, code) in self.mcr.libs_to_deploy.iter().enumerate() {
979 let expected_address = LIBRARY_DEPLOYER.create(nonce as u64);
981 let (deploy_result, recorded_account_diffs) =
982 self.deploy_library(expected_address, |executor| {
983 executor.deploy(LIBRARY_DEPLOYER, code.clone(), U256::ZERO, Some(rd))
984 });
985
986 if let Ok(deployed) = &deploy_result {
987 result.deployed_libs.push(deployed.address);
988 if self.contract.library_addresses.contains(&deployed.address) {
989 pending_account_diffs.extend(recorded_account_diffs);
990 }
991 }
992
993 let (raw, reason) =
994 RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
995 result.extend(raw, TraceKind::Deployment);
996 if reason.is_some() {
997 debug!(?reason, "deployment of library failed");
998 result.reason = reason;
999 return Ok(result);
1000 }
1001 }
1002 }
1003 LibraryDeployment::Create2 { deployer, salt } => {
1004 if deployer == foundry_evm::constants::DEFAULT_CREATE2_DEPLOYER
1008 && !self.evm_opts.networks.is_tempo()
1009 {
1010 self.executor.deploy_create2_deployer()?;
1011 }
1012 for code in &self.mcr.libs_to_deploy {
1013 let address = deployer.create2_from_code(salt, code);
1014 if self.executor.is_empty_code(address)? {
1015 let calldata = [salt.as_slice(), code.as_ref()].concat().into();
1016 let (raw, recorded_account_diffs) =
1017 self.deploy_library(address, |executor| {
1018 executor.transact_raw(
1019 LIBRARY_DEPLOYER,
1020 deployer,
1021 calldata,
1022 U256::ZERO,
1023 )
1024 });
1025 let raw = raw?;
1026 let (raw, reason) = if raw.reverted {
1027 RawCallResult::from_evm_result(Err(raw.into_evm_error(Some(rd))))?
1028 } else {
1029 (raw, None)
1030 };
1031 result.extend(raw, TraceKind::Deployment);
1032 if reason.is_some() {
1033 debug!(?reason, "CREATE2 deployment of library failed");
1034 result.reason = reason;
1035 return Ok(result);
1036 }
1037 if self.executor.is_empty_code(address)? {
1038 result.reason = Some(format!(
1039 "CREATE2 library deployment succeeded but no code was found at {address}"
1040 ));
1041 return Ok(result);
1042 }
1043 pending_account_diffs.extend(recorded_account_diffs);
1044 }
1045 self.executor.backend_mut().add_persistent_account(address);
1046 result.deployed_libs.push(address);
1047 }
1048
1049 if let Some(cheats) = self.executor.inspector_mut().cheatcodes.as_mut() {
1052 cheats.gas_metering.last_call_gas = None;
1053 cheats.gas_metering.last_frame_gas = None;
1054 }
1055 }
1056 }
1057 if !pending_account_diffs.is_empty()
1058 && let Some(cheats) = self.executor.inspector_mut().cheatcodes.as_deref_mut()
1059 {
1060 cheats.set_pending_account_diffs(pending_account_diffs);
1061 }
1062
1063 for &address in &self.mcr.library_addresses {
1065 if !self.executor.is_empty_code(address)? {
1066 result.deployed_libs.push(address);
1067 }
1068 }
1069 result.deployed_libs.sort_unstable();
1070 result.deployed_libs.dedup();
1071
1072 let address = self.sender.create(self.executor.get_nonce(self.sender)?);
1073 result.address = address;
1074
1075 self.executor.set_balance(address, self.initial_balance())?;
1078
1079 let deploy_result =
1081 self.executor.deploy(self.sender, self.contract.bytecode.clone(), U256::ZERO, Some(rd));
1082
1083 result.deployment_failure = deploy_result.is_err();
1084
1085 if let Ok(dr) = &deploy_result {
1086 debug_assert_eq!(dr.address, address);
1087 }
1088 let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
1089 result.extend(raw, TraceKind::Deployment);
1090 if reason.is_some() {
1091 debug!(?reason, "deployment of test contract failed");
1092 result.reason = reason;
1093 return Ok(result);
1094 }
1095
1096 self.executor.set_balance(self.sender, self.initial_balance())?;
1098 self.executor.set_balance(CALLER, self.initial_balance())?;
1099 self.executor.set_balance(LIBRARY_DEPLOYER, self.initial_balance())?;
1100
1101 if matches!(self.mcr.library_deployment, LibraryDeployment::Nonce)
1102 && !self.evm_opts.networks.is_tempo()
1103 {
1104 self.executor.deploy_create2_deployer()?;
1105 }
1106
1107 if call_setup {
1109 trace!("calling setUp");
1110 let res = self.executor.setup(None, address, Some(rd));
1111 let (raw, reason) = RawCallResult::from_evm_result(res)?;
1112 result.extend(raw, TraceKind::Setup);
1113 result.reason = reason;
1114 }
1115
1116 Ok(result)
1117 }
1118
1119 fn initial_balance(&self) -> U256 {
1120 self.evm_opts.initial_balance
1121 }
1122
1123 fn deploy_library<T>(
1126 &mut self,
1127 address: Address,
1128 deploy: impl FnOnce(&mut Executor<FEN>) -> T,
1129 ) -> (T, Vec<AccountAccess>) {
1130 let recording = self.contract.library_addresses.contains(&address)
1131 && self
1132 .executor
1133 .inspector_mut()
1134 .cheatcodes
1135 .as_deref_mut()
1136 .is_some_and(|cheats| cheats.start_internal_state_diff_recording());
1137 let result = deploy(&mut self.executor);
1138 let diffs = if recording {
1139 self.executor
1140 .inspector_mut()
1141 .cheatcodes
1142 .as_deref_mut()
1143 .map(|cheats| cheats.stop_internal_state_diff_recording())
1144 .unwrap_or_default()
1145 } else {
1146 Vec::new()
1147 };
1148 (result, diffs)
1149 }
1150
1151 fn apply_contract_inline_config(&mut self) -> Result<()> {
1153 if self.inline_config.contains_contract(self.name) {
1154 let new_config = Arc::new(self.inline_config(None)?);
1155 self.tcfg.to_mut().reconfigure_with(new_config);
1156 let prev_tracer = self.executor.inspector_mut().tracer.take();
1157 self.tcfg.configure_executor(&mut self.executor);
1158 self.executor.inspector_mut().tracer = prev_tracer;
1160 }
1161 Ok(())
1162 }
1163
1164 fn inline_config(&self, func: Option<&Function>) -> Result<Config> {
1166 let mut config = inline_config_for(&self.config, &self.mcr.inline_config, self.name, func)?;
1167 config.networks = config.networks.with_execution_profile(self.tcfg.evm_opts.networks);
1168 Ok(config)
1169 }
1170
1171 fn fuzz_fixtures(&mut self, address: Address) -> FuzzFixtures {
1187 let mut fixtures = HashMap::default();
1188 let fixture_functions = self.contract.abi.functions().filter(|func| func.is_fixture());
1189 for func in fixture_functions {
1190 if func.inputs.is_empty() {
1191 if let Ok(CallResult { raw: _, decoded_result }) =
1193 self.executor.call(CALLER, address, func, &[], U256::ZERO, None)
1194 {
1195 fixtures.insert(fixture_name(func.name.clone()), decoded_result);
1196 }
1197 } else {
1198 let mut vals = Vec::new();
1201 let mut index = 0;
1202 loop {
1203 if let Ok(CallResult { raw: _, decoded_result }) = self.executor.call(
1204 CALLER,
1205 address,
1206 func,
1207 &[DynSolValue::Uint(U256::from(index), 256)],
1208 U256::ZERO,
1209 None,
1210 ) {
1211 vals.push(decoded_result);
1212 } else {
1213 break;
1216 }
1217 index += 1;
1218 }
1219 fixtures.insert(fixture_name(func.name.clone()), DynSolValue::Array(vals));
1220 };
1221 }
1222 FuzzFixtures::new(fixtures).with_enum_bounds(self.mcr.enum_bounds.clone())
1223 }
1224
1225 fn test_matcher(&self) -> TestFunctionMatcher<'_> {
1227 TestFunctionMatcher::new(
1228 &self.config,
1229 &self.mcr.inline_config,
1230 self.mcr.tcfg.symbolic_artifact_replay.as_ref(),
1231 )
1232 }
1233
1234 fn matching_test_functions(
1236 &self,
1237 filter: &dyn TestFilter,
1238 test_matcher: &TestFunctionMatcher<'_>,
1239 ) -> Vec<&'a Function> {
1240 test_matcher
1241 .test_functions(self.name.to_string(), &self.contract.abi, |contract_id, func, kind| {
1242 filter.matches_test_function_kind_in_contract(contract_id, func, kind)
1243 && self.function_matches_network_pass(func)
1244 })
1245 .collect()
1246 }
1247
1248 pub fn run_tests(mut self, filter: &dyn TestFilter) -> SuiteResult {
1250 let start = Instant::now();
1251 let mut warnings = Vec::new();
1252 let generated_symbolic_regression =
1253 is_generated_symbolic_regression_contract(&self.contract.abi);
1254 let test_matcher = self.test_matcher();
1257 if self.mcr.tcfg.fuzz_only
1260 && !self.matching_test_functions(filter, &test_matcher).into_iter().any(|func| {
1261 matches!(
1262 test_matcher.test_function_kind(self.name, func, generated_symbolic_regression),
1263 TestFunctionKind::FuzzTest { .. } | TestFunctionKind::InvariantTest
1264 )
1265 })
1266 {
1267 return SuiteResult::new(start.elapsed(), BTreeMap::new(), warnings);
1268 }
1269
1270 let setup_fns: Vec<_> =
1272 self.contract.abi.functions().filter(|func| func.name.is_setup()).collect();
1273 let call_setup = setup_fns.len() == 1 && setup_fns[0].name == "setUp";
1274 for &setup_fn in &setup_fns {
1276 if setup_fn.name != "setUp" {
1277 warnings.push(format!(
1278 "Found invalid setup function \"{}\" did you mean \"setUp()\"?",
1279 setup_fn.signature()
1280 ));
1281 }
1282 }
1283
1284 if setup_fns.len() > 1 {
1286 return self.failed_suite(
1287 start,
1288 warnings,
1289 [("setUp()".to_string(), TestResult::fail("multiple setUp functions".to_string()))],
1290 );
1291 }
1292
1293 let after_invariant_fns: Vec<_> =
1295 self.contract.abi.functions().filter(|func| func.name.is_after_invariant()).collect();
1296 if after_invariant_fns.len() > 1 {
1297 return self.failed_suite(
1298 start,
1299 warnings,
1300 [(
1301 "afterInvariant()".to_string(),
1302 TestResult::fail("multiple afterInvariant functions".to_string()),
1303 )],
1304 );
1305 }
1306 let call_after_invariant = after_invariant_fns.first().is_some_and(|after_invariant_fn| {
1307 let match_sig = after_invariant_fn.name == "afterInvariant";
1308 if !match_sig {
1309 warnings.push(format!(
1310 "Found invalid afterInvariant function \"{}\" did you mean \"afterInvariant()\"?",
1311 after_invariant_fn.signature()
1312 ));
1313 }
1314 match_sig
1315 });
1316
1317 let invariant_fns = self
1318 .contract
1319 .abi
1320 .functions()
1321 .filter(|func| {
1322 test_matcher
1323 .test_function_kind(self.name, func, generated_symbolic_regression)
1324 .is_invariant_test()
1325 })
1326 .collect::<Vec<_>>();
1327
1328 let invalid_invariants = invariant_fns
1333 .iter()
1334 .filter(|f| !f.inputs.is_empty())
1335 .map(|f| {
1336 let signature = f.signature();
1337 let reason = format!("invariant `{signature}` must take no parameters");
1338 (signature, TestResult::fail(reason))
1339 })
1340 .collect::<Vec<_>>();
1341 if !invalid_invariants.is_empty() {
1342 return self.failed_suite(start, warnings, invalid_invariants);
1343 }
1344
1345 for invariant in &invariant_fns {
1346 if invariant.outputs.len() == 1 && invariant.outputs[0].ty == "bool" {
1347 warnings.push(format!(
1348 "Invariant function `{}` returns `bool`, but its return value is ignored; use assertions or revert to indicate failure.",
1349 invariant.signature()
1350 ));
1351 }
1352 }
1353
1354 let has_invariants = !invariant_fns.is_empty();
1359
1360 let should_override_setup_tracing =
1361 !self.tcfg.debug && (self.executor.inspector().tracer.is_some() || has_invariants);
1362
1363 let prev_tracer = should_override_setup_tracing.then(|| {
1364 let prev_tracer = self.executor.inspector_mut().tracer.take();
1365 self.executor.set_trace_requirements(TraceRequirements::none().with_calls(true));
1366 prev_tracer
1367 });
1368
1369 let setup_time = Instant::now();
1370 let mut setup = self.setup(call_setup);
1371 debug!("finished setting up in {:?}", setup_time.elapsed());
1372
1373 if let Some(prev_tracer) = prev_tracer {
1374 self.executor.inspector_mut().tracer = prev_tracer;
1375 }
1376
1377 if setup.reason.is_some() {
1378 let name = if setup.deployment_failure { "constructor()" } else { "setUp()" };
1380 return self.failed_suite(
1381 start,
1382 warnings,
1383 [(name.to_string(), TestResult::setup_result(setup))],
1384 );
1385 }
1386
1387 let find_timer = Instant::now();
1390 let functions = self.matching_test_functions(filter, &self.test_matcher());
1391 debug!(
1392 "Found {} test functions out of {} in {:?}",
1393 functions.len(),
1394 self.contract.abi.functions().count(),
1395 find_timer.elapsed(),
1396 );
1397
1398 let identified_contracts = has_invariants.then(|| {
1399 load_contracts(setup.traces.iter().map(|(_, t)| &t.arena), &self.mcr.known_contracts)
1400 });
1401
1402 if let Some(replay) = &self.mcr.tcfg.symbolic_artifact_replay {
1403 let artifact = &replay.artifact;
1404 let target = &artifact.test;
1405 let replay_functions =
1406 functions.iter().filter(|func| func.signature() == target.test).collect::<Vec<_>>();
1407 let func = match replay_functions[..] {
1408 [] if !self.mcr.tcfg.multi_network.all_override_networks.is_empty() => {
1409 return SuiteResult::new(start.elapsed(), BTreeMap::new(), warnings);
1410 }
1411 [] => {
1412 let reason = format!(
1413 "symbolic artifact target `{}` was not found in `{}`",
1414 target.test, target.contract
1415 );
1416 let results = [(target.test.clone(), TestResult::fail(reason))];
1417 return SuiteResult::new(start.elapsed(), results.into(), warnings);
1418 }
1419 [func] => *func,
1420 _ => {
1421 let reason = format!(
1422 "symbolic artifact target `{}` matched {} functions in `{}`",
1423 target.test,
1424 replay_functions.len(),
1425 target.contract
1426 );
1427 let results = [(target.test.clone(), TestResult::fail(reason))];
1428 return SuiteResult::new(start.elapsed(), results.into(), warnings);
1429 }
1430 };
1431
1432 let is_sequence = artifact.kind == SymbolicCounterexampleArtifactKind::Sequence;
1433 let kind = if is_sequence {
1434 func.test_function_kind()
1435 } else {
1436 TestFunctionKind::SymbolicTest
1437 };
1438 let test_start = Instant::now();
1439 let mut res = if is_sequence && !kind.is_invariant_test() {
1440 TestResult::fail(format!(
1441 "sequence symbolic artifact must target an invariant test, but matched {} function `{}`",
1442 kind.name(),
1443 func.signature(),
1444 ))
1445 } else {
1446 let invariants = if is_sequence { std::slice::from_ref(&func) } else { &[][..] };
1447 FunctionRunner::new(&self, &setup).run_symbolic_artifact_replay(
1448 func,
1449 invariants,
1450 call_after_invariant,
1451 )
1452 };
1453 res.duration = test_start.elapsed();
1454 debug!(%kind, path = %replay.path.display(), "replayed symbolic artifact");
1455 return SuiteResult::new(start.elapsed(), [(func.signature(), res)].into(), warnings);
1456 }
1457
1458 let test_fail_results = functions
1459 .iter()
1460 .filter(|func| func.test_function_kind().is_any_test_fail())
1461 .map(|func| {
1462 let reason = "`testFail*` has been removed. Consider changing to test_Revert[If|When]_Condition and expecting a revert";
1463 (func.signature(), TestResult::fail(reason.to_string()))
1464 })
1465 .collect::<Vec<_>>();
1466 if !test_fail_results.is_empty() {
1467 return self.failed_suite(start, warnings, test_fail_results);
1468 }
1469
1470 if functions.iter().any(|func| {
1471 matches!(
1472 func.test_function_kind(),
1473 TestFunctionKind::FuzzTest { .. }
1474 | TestFunctionKind::TableTest
1475 | TestFunctionKind::InvariantTest
1476 )
1477 }) {
1478 setup.fuzz_fixtures = self.fuzz_fixtures(setup.address);
1479 }
1480
1481 let early_exit = &self.tcfg.early_exit;
1482 let test_matcher = self.test_matcher();
1483 if self.progress.is_some() {
1484 let interrupt = early_exit.clone();
1485 self.tokio_handle.spawn(async move {
1486 signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
1487 interrupt.record_ctrl_c();
1488 });
1489 }
1490
1491 let InvariantCampaignSelection {
1492 matched_boolean_invariant_fns,
1493 merge_boolean_suite: merge_invariant_suite,
1494 shared_boolean_namespace,
1495 boolean_suite_anchor: invariant_suite_anchor,
1496 optimization_anchors: _,
1497 } = select_invariant_campaigns(
1498 &invariant_fns,
1499 &functions,
1500 &self.config,
1501 &self.mcr.inline_config,
1502 self.name,
1503 );
1504
1505 let test_results = functions
1506 .par_iter()
1507 .filter_map(|&func| {
1508 if early_exit.should_stop() {
1510 return None;
1511 }
1512 let invariants: &[&Function] = if func.is_invariant_test() {
1515 if is_optimization_invariant(func) {
1516 std::slice::from_ref(&func)
1517 } else if merge_invariant_suite {
1518 if invariant_suite_anchor != Some(func) {
1520 return None;
1521 }
1522 matched_boolean_invariant_fns.as_slice()
1523 } else {
1524 std::slice::from_ref(&func)
1525 }
1526 } else {
1527 invariant_fns.as_slice()
1528 };
1529
1530 if func.is_invariant_test() && invariants.is_empty() {
1532 return None;
1533 }
1534
1535 let start = Instant::now();
1536
1537 let _guard = self.tokio_handle.enter();
1538
1539 let _guard;
1540 let current_span = tracing::Span::current();
1541 if current_span.is_none() || current_span.id() != self.span.id() {
1542 _guard = self.span.enter();
1543 }
1544
1545 let sig = func.signature();
1546 let kind =
1547 test_matcher.test_function_kind(self.name, func, generated_symbolic_regression);
1548
1549 let _guard = debug_span!(
1550 "test",
1551 %kind,
1552 name = %if enabled!(tracing::Level::TRACE) { &sig } else { &func.name },
1553 )
1554 .entered();
1555
1556 let mut res = FunctionRunner::new(&self, &setup).run(
1557 func,
1558 invariants,
1559 shared_boolean_namespace,
1560 kind,
1561 call_after_invariant,
1562 identified_contracts.as_ref(),
1563 );
1564 res.duration = start.elapsed();
1565
1566 if res.status.is_failure() {
1568 early_exit.record_failure();
1569 }
1570
1571 Some((sig, res))
1572 })
1573 .collect::<BTreeMap<_, _>>();
1574
1575 SuiteResult::new(start.elapsed(), test_results, warnings)
1576 }
1577
1578 fn failed_suite(
1582 &self,
1583 start: Instant,
1584 warnings: Vec<String>,
1585 results: impl IntoIterator<Item = (String, TestResult)>,
1586 ) -> SuiteResult {
1587 self.tcfg.early_exit.record_failure();
1588 SuiteResult::new(start.elapsed(), results.into_iter().collect(), warnings)
1589 }
1590}
1591
1592struct FunctionRunner<'a, FEN: FoundryEvmNetwork> {
1594 tcfg: Cow<'a, TestRunnerConfig<FEN>>,
1596 executor: Cow<'a, Executor<FEN>>,
1598 cr: &'a ContractRunner<'a, FEN>,
1600 address: Address,
1602 setup: &'a TestSetup,
1604 result: TestResult,
1606}
1607
1608struct ReplayedInvariantSequence {
1610 call_sequence: Vec<BaseCounterExample>,
1611 artifact: Option<SymbolicArtifactRef>,
1612 minimization: Option<SymbolicCounterexampleMinimization>,
1613 fork_block_number: Option<u64>,
1614}
1615
1616#[derive(Clone, Copy)]
1618struct SequenceReplay<'a> {
1619 invariant_config: &'a InvariantConfig,
1620 invariant_contract: &'a InvariantContract<'a>,
1621 target_invariant: &'a Function,
1622 assertion_failure: bool,
1623 storage: &'a [SymbolicStorageAssignment],
1624}
1625
1626struct SequenceArtifactSpec<'a> {
1628 file_name: &'a str,
1629 fail_on_revert: bool,
1630 failure: Option<SymbolicInvariantArtifactFailure>,
1631}
1632
1633fn same_sequence_failure(actual: &CheckSequenceOutcome, expected: &CheckSequenceOutcome) -> bool {
1635 actual.replayed_entirely == expected.replayed_entirely
1636 && actual.reason == expected.reason
1637 && actual.failure_site == expected.failure_site
1638 && actual.sequence_assertion_failure == expected.sequence_assertion_failure
1639}
1640
1641impl<'a, FEN: FoundryEvmNetwork> Deref for FunctionRunner<'a, FEN> {
1642 type Target = Cow<'a, TestRunnerConfig<FEN>>;
1643
1644 #[inline(always)]
1645 fn deref(&self) -> &Self::Target {
1646 &self.tcfg
1647 }
1648}
1649
1650impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> {
1651 fn new(cr: &'a ContractRunner<'a, FEN>, setup: &'a TestSetup) -> Self {
1652 Self {
1653 tcfg: Cow::Borrowed(cr.tcfg.as_ref()),
1654 executor: Cow::Borrowed(&cr.executor),
1655 cr,
1656 address: setup.address,
1657 setup,
1658 result: TestResult::new(setup),
1659 }
1660 }
1661
1662 const fn revert_decoder(&self) -> &'a RevertDecoder {
1663 &self.cr.mcr.revert_decoder
1664 }
1665
1666 fn fuzz_progress(
1668 &self,
1669 test_name: &str,
1670 timeout: Option<u32>,
1671 runs: u32,
1672 ) -> Option<indicatif::ProgressBar> {
1673 self.cr.progress?.inner.lock().start_fuzz_progress(self.cr.name, test_name, timeout, runs)
1674 }
1675
1676 fn fuzz_minimize_target_id(&self, test_name: &str) -> String {
1677 let network = self
1678 .cr
1679 .mcr
1680 .tcfg
1681 .multi_network
1682 .pass_network
1683 .as_ref()
1684 .map(|network| format!("{network:?}"))
1685 .unwrap_or_else(|| "default".to_string());
1686 format!("{network}:{}::{test_name}", self.cr.name)
1687 }
1688
1689 fn symbolic_artifact(
1691 &self,
1692 test_name: &str,
1693 kind: SymbolicCounterexampleArtifactKind,
1694 symbolic: &SymbolicResult,
1695 fail_on_revert: bool,
1696 calls: Vec<SymbolicCounterexampleCall>,
1697 ) -> SymbolicCounterexampleArtifact {
1698 SymbolicCounterexampleArtifact::new(
1699 kind,
1700 SymbolicCounterexampleTestIdentity {
1701 contract: self.cr.name.to_string(),
1702 test: test_name.to_string(),
1703 },
1704 symbolic,
1705 SymbolicCounterexampleReplaySemantics { fail_on_revert },
1706 calls,
1707 )
1708 }
1709
1710 fn write_symbolic_artifact(
1713 &self,
1714 file_name: &str,
1715 artifact: &SymbolicCounterexampleArtifact,
1716 ) -> Option<SymbolicArtifactRef> {
1717 let dir = self
1718 .config
1719 .cache_path
1720 .join("symbolic")
1721 .join(sanitize_symbolic_artifact_component(self.cr.name));
1722 let path = dir.join(symbolic_artifact_file_name(self.cr.name, file_name, artifact.kind));
1723 if let Err(err) = foundry_common::fs::create_dir_all(&dir) {
1724 tracing::error!(%err, path = %dir.display(), "Failed to create symbolic artifact dir");
1725 return None;
1726 }
1727 if let Err(err) = foundry_common::fs::write_json_file(&path, artifact) {
1728 tracing::error!(%err, path = %path.display(), "Failed to write symbolic artifact");
1729 return None;
1730 }
1731 Some(SymbolicArtifactRef::new(path))
1732 }
1733
1734 fn persist_sequence_artifact(
1736 &self,
1737 test_name: &str,
1738 file_name: &str,
1739 calls: Vec<SymbolicCounterexampleCall>,
1740 fail_on_revert: bool,
1741 storage: &[SymbolicStorageAssignment],
1742 failure: Option<SymbolicInvariantArtifactFailure>,
1743 ) -> Option<SymbolicArtifactRef> {
1744 if calls.is_empty() || !self.config.symbolic.enabled {
1745 return None;
1746 }
1747 let symbolic = SymbolicResult::incomplete(
1748 &self.config.symbolic,
1749 SymbolicStopReason::Error,
1750 "concrete replay confirmed stateful counterexample",
1751 SymbolicStats::default(),
1752 SymbolicReplayMetadata::confirmed(),
1753 SymbolicCallTrace::none(),
1754 None,
1755 );
1756 let mut artifact = self.symbolic_artifact(
1757 test_name,
1758 SymbolicCounterexampleArtifactKind::Sequence,
1759 &symbolic,
1760 fail_on_revert,
1761 calls,
1762 );
1763 if !storage.is_empty() {
1764 artifact = artifact.with_storage(storage.to_vec());
1765 }
1766 if let Some(failure) = failure {
1767 artifact = artifact.with_invariant_failure(failure);
1768 }
1769 self.write_symbolic_artifact(file_name, &artifact)
1770 }
1771
1772 fn sequence_calls(
1774 &self,
1775 call_sequence: &[BaseCounterExample],
1776 ) -> Vec<SymbolicCounterexampleCall> {
1777 call_sequence
1778 .iter()
1779 .map(|counterexample| {
1780 SymbolicCounterexampleCall::from_base_counterexample(
1781 counterexample,
1782 CALLER,
1783 self.address,
1784 )
1785 })
1786 .collect()
1787 }
1788
1789 fn replay_confirmed_symbolic_single_call(
1791 &self,
1792 call: &SymbolicCounterexampleCall,
1793 expected_reason: Option<&str>,
1794 ) -> Result<(RawCallResult<FEN>, Option<String>), String> {
1795 let Some(expected_reason) = expected_reason else {
1796 return Err("candidate replay has no stable failure reason to compare".to_string());
1797 };
1798
1799 let mut executor = self.clone_executor();
1800 let raw = execute_tx(&mut executor, &call.to_basic_tx_details())
1801 .map_err(|err| err.to_string())?;
1802 if executor.is_raw_call_success(
1803 self.address,
1804 Cow::Borrowed(&raw.state_changeset),
1805 &raw,
1806 false,
1807 ) {
1808 return Err("candidate replay succeeded".to_string());
1809 }
1810 if let Some(reason) = raw.skip_reason() {
1811 return Err(format!("vm.skip during concrete replay: {reason}"));
1812 }
1813
1814 let reason = (raw.reverted || raw.exit_reason.is_some_and(|reason| !reason.is_ok()))
1815 .then(|| self.revert_decoder().decode(&raw.result, raw.exit_reason));
1816 if reason.as_deref() != Some(expected_reason) {
1817 return Err(format!(
1818 "candidate replay failed with different reason: expected `{expected_reason}`, got `{}`",
1819 reason.as_deref().unwrap_or("")
1820 ));
1821 }
1822 Ok((raw, reason))
1823 }
1824
1825 #[expect(clippy::too_many_arguments)]
1828 fn replay_invariant_error_sequence(
1829 &mut self,
1830 replay: SequenceReplay<'_>,
1831 original_calls: &[BasicTxDetails],
1832 inner_sequence: Option<Vec<Option<BasicTxDetails>>>,
1833 identified_contracts: &ContractsByAddress,
1834 current_settings: &InvariantSettings,
1835 artifact: SequenceArtifactSpec<'_>,
1836 progress: Option<&indicatif::ProgressBar>,
1837 position: Option<(usize, usize)>,
1838 ) -> Result<ReplayedInvariantSequence> {
1839 let minimization = self.minimize_symbolic_invariant_sequence(
1840 replay,
1841 original_calls,
1842 identified_contracts,
1843 current_settings,
1844 );
1845
1846 let mut replay_config = replay.invariant_config.clone();
1847 let minimized_txes;
1848 let replay_calls = if let Some(minimization) = &minimization {
1849 minimized_txes = minimization
1850 .minimized_calls
1851 .iter()
1852 .map(SymbolicCounterexampleCall::to_basic_tx_details)
1853 .collect::<Vec<_>>();
1854 replay_config.shrink_run_limit = 0;
1855 minimized_txes.as_slice()
1856 } else {
1857 original_calls
1858 };
1859
1860 let ReplayErrorResult { counterexample_sequence: call_sequence, fork_block_number, .. } =
1861 self.replay_error(
1862 replay_config,
1863 self.clone_executor_with_symbolic_storage(replay.storage)?,
1864 replay_calls,
1865 inner_sequence,
1866 replay.assertion_failure,
1867 None,
1868 replay.invariant_contract,
1869 replay.target_invariant,
1870 identified_contracts,
1871 progress,
1872 position,
1873 )?;
1874
1875 let test_name = replay.target_invariant.signature();
1876 let calls = self.sequence_calls(&call_sequence);
1877 let (artifact_ref, minimization) = match minimization {
1878 None => (
1879 self.persist_sequence_artifact(
1880 &test_name,
1881 artifact.file_name,
1882 calls,
1883 artifact.fail_on_revert,
1884 replay.storage,
1885 artifact.failure,
1886 ),
1887 None,
1888 ),
1889 Some(minimization) => {
1890 let original = self.persist_sequence_artifact(
1891 &test_name,
1892 &format!("original__{}", artifact.file_name),
1893 minimization.original_calls.clone(),
1894 artifact.fail_on_revert,
1895 replay.storage,
1896 artifact.failure.clone(),
1897 );
1898 let minimized = self.persist_sequence_artifact(
1899 &test_name,
1900 artifact.file_name,
1901 calls,
1902 artifact.fail_on_revert,
1903 replay.storage,
1904 artifact.failure,
1905 );
1906 let primary = if call_sequence.is_empty() { &original } else { &minimized };
1909 let primary = primary.clone();
1910 let metadata = original.zip(minimized).map(|(original, minimized)| {
1911 SymbolicCounterexampleMinimization::new(
1912 original,
1913 minimized,
1914 minimization.attempts,
1915 minimization.accepted,
1916 minimization.original_calldata_bytes(),
1917 minimization.minimized_calldata_bytes(),
1918 )
1919 .with_sequence_lengths(
1920 minimization.original_calls.len(),
1921 minimization.minimized_calls.len(),
1922 )
1923 });
1924 (primary, metadata)
1925 }
1926 };
1927
1928 Ok(ReplayedInvariantSequence {
1929 call_sequence,
1930 artifact: artifact_ref,
1931 minimization,
1932 fork_block_number,
1933 })
1934 }
1935
1936 #[expect(clippy::too_many_arguments)]
1940 fn replay_error(
1941 &mut self,
1942 config: InvariantConfig,
1943 executor: Executor<FEN>,
1944 calls: &[BasicTxDetails],
1945 inner_sequence: Option<Vec<Option<BasicTxDetails>>>,
1946 expect_assertion_failure: bool,
1947 target_value: Option<I256>,
1948 invariant_contract: &InvariantContract<'_>,
1949 target_invariant: &Function,
1950 identified_contracts: &ContractsByAddress,
1951 progress: Option<&indicatif::ProgressBar>,
1952 position: Option<(usize, usize)>,
1953 ) -> Result<ReplayErrorResult> {
1954 replay_error(
1955 config,
1956 executor,
1957 calls,
1958 inner_sequence,
1959 expect_assertion_failure,
1960 target_value.is_none().then(|| self.revert_decoder()),
1961 target_value,
1962 invariant_contract,
1963 target_invariant,
1964 &self.cr.mcr.known_contracts,
1965 identified_contracts.clone(),
1966 &mut self.result.logs,
1967 &mut self.result.traces,
1968 &mut self.result.debug_bytecodes,
1969 &mut self.result.line_coverage,
1970 &mut self.result.deprecated_cheatcodes,
1971 progress,
1972 &self.tcfg.early_exit,
1973 position,
1974 )
1975 }
1976
1977 fn minimize_symbolic_invariant_sequence(
1978 &self,
1979 replay: SequenceReplay<'_>,
1980 calls: &[BasicTxDetails],
1981 identified_contracts: &ContractsByAddress,
1982 current_settings: &InvariantSettings,
1983 ) -> Option<MinimizedSequence> {
1984 if !self.config.symbolic.enabled || calls.is_empty() {
1985 return None;
1986 }
1987
1988 let original_calls = self.sequence_calls(&base_counterexamples(
1989 calls,
1990 identified_contracts,
1991 replay.invariant_config.show_solidity,
1992 ));
1993 let expected = self.symbolic_sequence_failure(replay, &original_calls)?;
1994 let preserves = |candidate: &[SymbolicCounterexampleCall]| {
1995 self.symbolic_sequence_failure(replay, candidate)
1996 .is_some_and(|actual| same_sequence_failure(&actual, &expected))
1997 };
1998
1999 let minimization = minimize_sequence_counterexample(
2000 &original_calls,
2001 &self.symbolic_sequence_sender_candidates(current_settings),
2002 replay.invariant_config.shrink_run_limit as usize,
2003 preserves,
2004 )?;
2005 preserves(&minimization.minimized_calls).then_some(minimization)
2006 }
2007
2008 fn symbolic_sequence_sender_candidates(
2009 &self,
2010 current_settings: &InvariantSettings,
2011 ) -> Vec<Address> {
2012 let mut candidates = if current_settings.target_senders.is_empty() {
2013 vec![self.sender, CALLER, address!("0x0000000000000000000000000000000000000100")]
2014 } else {
2015 current_settings.target_senders.clone()
2016 };
2017
2018 candidates.retain(|sender| {
2019 !current_settings.excluded_senders.contains(sender)
2020 && (current_settings.target_senders.is_empty()
2021 || current_settings.target_senders.contains(sender))
2022 });
2023 candidates.sort_unstable();
2024 candidates.dedup();
2025 candidates
2026 }
2027
2028 fn symbolic_sequence_failure(
2030 &self,
2031 replay: SequenceReplay<'_>,
2032 calls: &[SymbolicCounterexampleCall],
2033 ) -> Option<CheckSequenceOutcome> {
2034 let txes =
2035 calls.iter().map(SymbolicCounterexampleCall::to_basic_tx_details).collect::<Vec<_>>();
2036 let sequence = (0..txes.len()).collect::<Vec<_>>();
2037 let outcome = check_sequence(
2038 self.clone_executor_with_symbolic_storage(replay.storage).ok()?,
2039 &txes,
2040 &sequence,
2041 replay.invariant_contract.address,
2042 replay.target_invariant.selector().to_vec().into(),
2043 CheckSequenceOptions {
2044 accumulate_warp_roll: false,
2045 fail_on_revert: replay.invariant_config.fail_on_revert,
2046 expect_assertion_failure: replay.assertion_failure,
2047 call_after_invariant: replay.invariant_contract.call_after_invariant,
2048 rd: Some(self.revert_decoder()),
2049 },
2050 )
2051 .ok()?;
2052 (!outcome.success).then_some(outcome)
2053 }
2054
2055 fn replay_persisted_call_sequence(
2058 &self,
2059 invariant_contract: &InvariantContract<'_>,
2060 call_sequence: &mut [BaseCounterExample],
2061 expect_assertion_failure: bool,
2062 storage: &[SymbolicStorageAssignment],
2063 ) -> Result<(Vec<BasicTxDetails>, CheckSequenceOutcome)> {
2064 let config = &self.config.invariant;
2065 let txes = base_counterexamples_to_txes(call_sequence, config.show_solidity);
2066 let sequence = (0..min(txes.len(), config.depth as usize)).collect::<Vec<_>>();
2067 let outcome = check_sequence(
2068 self.clone_executor_with_symbolic_storage(storage)?,
2069 &txes,
2070 &sequence,
2071 invariant_contract.address,
2072 invariant_contract.anchor().selector().to_vec().into(),
2073 CheckSequenceOptions {
2074 accumulate_warp_roll: config.has_delay(),
2075 fail_on_revert: config.fail_on_revert,
2076 expect_assertion_failure,
2077 call_after_invariant: invariant_contract.call_after_invariant,
2078 rd: Some(self.revert_decoder()),
2079 },
2080 )?;
2081 Ok((txes, outcome))
2082 }
2083
2084 fn replay_persisted_handler_failures(
2088 &self,
2089 handlers_dir: &Path,
2090 current_settings: &InvariantSettings,
2091 ) -> (HandlerFailureMap, SymbolicHandlerStorageMap) {
2092 let mut replayed = HandlerFailureMap::new();
2093 let mut replayed_storage = SymbolicHandlerStorageMap::default();
2094 let entries = match std::fs::read_dir(handlers_dir) {
2095 Ok(entries) => entries,
2096 Err(err) => {
2097 if err.kind() != std::io::ErrorKind::NotFound {
2098 error!(%err, "Failed to read handler failure dir");
2099 }
2100 return (replayed, replayed_storage);
2101 }
2102 };
2103 let config = &self.config.invariant;
2104 for entry in entries.flatten() {
2105 let path = entry.path();
2106 if path.extension().and_then(|s| s.to_str()) != Some("json") {
2107 continue;
2108 }
2109 let Some(InvariantPersistedFailure {
2110 mut call_sequence, storage, failure_site, ..
2111 }) = persisted_call_sequence(&path, current_settings)
2112 else {
2113 continue;
2114 };
2115 if call_sequence.is_empty() {
2116 let _ = std::fs::remove_file(&path);
2117 continue;
2118 }
2119 let txes = base_counterexamples_to_txes(&mut call_sequence, config.show_solidity);
2120 let sequence = (0..min(txes.len(), config.depth as usize)).collect::<Vec<_>>();
2121 let replay_executor = match self.clone_executor_with_symbolic_storage(&storage) {
2122 Ok(executor) => executor,
2123 Err(err) => {
2124 error!(%err, "Failed to apply symbolic storage for handler-side assertion replay");
2125 continue;
2126 }
2127 };
2128 match replay_handler_failure_sequence(
2129 replay_executor,
2130 &txes,
2131 &sequence,
2132 config.has_delay(),
2133 Some(self.revert_decoder()),
2134 ) {
2135 Ok(outcome) if outcome.anchor_asserted => {
2136 let _ = sh_warn!(
2137 "Replayed handler-side assertion bug from {path:?}. \nRun `forge clean` or remove file to ignore."
2138 );
2139 let actual_site = SymbolicInvariantFailureSite::SequenceCall {
2140 target: outcome.reverter,
2141 selector: outcome.selector,
2142 fingerprint: outcome.anchor_fingerprint,
2143 };
2144 if failure_site.is_some_and(|expected| expected != actual_site) {
2145 let _ = std::fs::remove_file(&path);
2146 continue;
2147 }
2148 let failure = HandlerAssertionFailure::from_replayed_sequence(
2149 txes,
2150 outcome.reverter,
2151 outcome.selector,
2152 outcome.anchor_fingerprint,
2153 outcome.revert_reason.unwrap_or_default(),
2154 );
2155 let site = (failure.reverter, failure.selector);
2156 let already_shorter = replayed
2158 .get(&site)
2159 .and_then(InvariantFuzzError::as_handler_assertion)
2160 .is_some_and(|existing| {
2161 existing.call_sequence.len() <= failure.call_sequence.len()
2162 });
2163 if !already_shorter {
2164 replayed_storage.insert(
2165 (failure.reverter, failure.selector, failure.edge_fingerprint),
2166 SymbolicHandlerReplayStorage {
2167 call_sequence: failure.call_sequence.clone(),
2168 assignments: storage,
2169 },
2170 );
2171 replayed.insert(site, InvariantFuzzError::HandlerAssertion(failure));
2172 }
2173 }
2174 Ok(_) => {
2176 let _ = std::fs::remove_file(&path);
2177 }
2178 Err(err) => {
2179 error!(%err, "Failed to replay handler-side assertion bug");
2180 }
2181 }
2182 }
2183 (replayed, replayed_storage)
2184 }
2185
2186 fn apply_function_inline_config(&mut self, func: &Function) -> Result<()> {
2188 if self.inline_config.contains_function(self.cr.name, &func.name) {
2189 let new_config = Arc::new(self.cr.inline_config(Some(func))?);
2190 self.tcfg.to_mut().reconfigure_with(new_config);
2191 self.tcfg.configure_executor(self.executor.to_mut());
2192 }
2193 Ok(())
2194 }
2195
2196 fn run(
2197 mut self,
2198 func: &Function,
2199 invariants: &[&Function],
2200 shared_invariant_namespace: bool,
2201 kind: TestFunctionKind,
2202 call_after_invariant: bool,
2203 identified_contracts: Option<&ContractsByAddress>,
2204 ) -> TestResult {
2205 if let Err(e) = self.apply_function_inline_config(func) {
2206 self.result.single_fail(Some(e.to_string()));
2207 return self.result;
2208 }
2209 let kind = effective_test_function_kind(kind, &self.config, func);
2210
2211 if (self.cr.mcr.tcfg.showmap.is_some() || self.cr.mcr.tcfg.fuzz_only)
2213 && matches!(
2214 kind,
2215 TestFunctionKind::UnitTest { .. }
2216 | TestFunctionKind::TableTest
2217 | TestFunctionKind::SymbolicTest
2218 )
2219 {
2220 if let Some(showmap) = self.cr.mcr.tcfg.showmap.as_ref() {
2221 let mode = if showmap.emit_files { "showmap" } else { "replay" };
2222 self.result.replay_skip(format!("not runnable in {mode} mode"));
2223 } else if self.cr.mcr.tcfg.fuzz_failure_replay {
2224 self.result
2225 .single_skip(SkipReason(Some("not runnable in replay mode".to_string())));
2226 } else {
2227 self.result.single_skip(SkipReason(Some("not runnable in fuzz mode".to_string())));
2228 }
2229 return self.result;
2230 }
2231
2232 match kind {
2233 TestFunctionKind::UnitTest { .. } => self.run_unit_test(func),
2234 TestFunctionKind::FuzzTest { .. } => self.run_fuzz_test(func),
2235 TestFunctionKind::TableTest => self.run_table_test(func),
2236 TestFunctionKind::SymbolicTest => self.run_symbolic_test(func),
2237 TestFunctionKind::InvariantTest => {
2238 let fail_on_revert_for = |f: &Function| {
2239 if self.inline_config.contains_function(self.cr.name, &f.name)
2240 && let Ok(config) = self.cr.inline_config(Some(f))
2241 {
2242 return config.invariant.fail_on_revert;
2243 }
2244 self.config.invariant.fail_on_revert
2245 };
2246 let invariant_fns: Vec<_> =
2247 invariants.iter().copied().map(|f| (f, fail_on_revert_for(f))).collect();
2248 self.run_invariant_test(
2249 func,
2250 invariant_fns,
2251 shared_invariant_namespace,
2252 call_after_invariant,
2253 identified_contracts.unwrap(),
2254 )
2255 }
2256 _ => unreachable!(),
2257 }
2258 }
2259
2260 fn run_unit_test(mut self, func: &Function) -> TestResult {
2269 if self.prepare_test(func).is_err() {
2271 return self.result;
2272 }
2273
2274 let Ok((mut raw_call_result, reason)) = self.call_test(func, &[]) else {
2276 return self.result;
2277 };
2278 let success =
2279 self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, false);
2280 self.result.single_result(success, reason, raw_call_result);
2281 self.result
2282 }
2283
2284 fn call_test(
2287 &mut self,
2288 func: &Function,
2289 args: &[DynSolValue],
2290 ) -> Result<(RawCallResult<FEN>, Option<String>), ()> {
2291 match self.executor.call(
2292 self.sender,
2293 self.address,
2294 func,
2295 args,
2296 U256::ZERO,
2297 Some(self.revert_decoder()),
2298 ) {
2299 Ok(res) => Ok((res.raw, None)),
2300 Err(EvmError::Execution(err)) => Ok((err.raw, Some(err.reason))),
2301 Err(EvmError::Skip(reason)) => {
2302 self.result.single_skip(reason);
2303 Err(())
2304 }
2305 Err(err) => {
2306 self.result.single_fail(Some(err.to_string()));
2307 Err(())
2308 }
2309 }
2310 }
2311
2312 fn symbolic_run_input<'f>(
2314 &'f self,
2315 func: &'f Function,
2316 sender: Address,
2317 collect_success_input: bool,
2318 corpus_seeds: Vec<SymbolicConcreteInput>,
2319 branch_target: Option<SymbolicBranchTarget>,
2320 ) -> SymbolicRunInput<'f, FEN> {
2321 SymbolicRunInput {
2322 executor: self.executor.as_ref(),
2323 target: self.address,
2324 sender,
2325 function: func,
2326 value: U256::ZERO,
2327 ffi_enabled: self.config.ffi,
2328 collect_success_input,
2329 corpus_seeds,
2330 branch_target,
2331 }
2332 }
2333
2334 fn fuzz_test_paths<'f>(
2337 &self,
2338 func: &'f Function,
2339 fuzz_config: &mut FuzzConfig,
2340 ) -> (Cow<'f, str>, Option<PathBuf>, (PathBuf, PathBuf)) {
2341 let test_name = fuzz_test_path_name(&self.cr.contract.abi, func, fuzz_config, self.cr.name);
2342 let legacy_corpus_dir = legacy_fuzz_corpus_dir(
2343 fuzz_config.corpus.corpus_dir.as_deref(),
2344 self.cr.name,
2345 func,
2346 &test_name,
2347 );
2348 let failure_paths = test_paths(
2349 &mut fuzz_config.corpus,
2350 fuzz_config.failure_persist_dir.clone().unwrap(),
2351 self.cr.name,
2352 &test_name,
2353 );
2354 (test_name, legacy_corpus_dir, failure_paths)
2355 }
2356
2357 fn import_symbolic_fuzz_corpus(
2359 &self,
2360 func: &Function,
2361 ) -> (Vec<SymbolicConcreteInput>, Option<SymbolicCorpusSeedMetadata>) {
2362 let mut inputs = Vec::new();
2363 if !should_symbolically_import_fuzz_corpus(&self.config, func) {
2364 return (inputs, None);
2365 }
2366
2367 let mut fuzz_config = self.config.fuzz.clone();
2368 let (_, legacy_corpus_dir, _) = self.fuzz_test_paths(func, &mut fuzz_config);
2369 let corpus_dir = legacy_corpus_dir.or(fuzz_config.corpus.corpus_dir);
2370 let limit = self.config.symbolic.corpus_seed_limit;
2371 let mut metadata = SymbolicCorpusSeedMetadata {
2372 corpus_dir: corpus_dir.clone(),
2373 limit,
2374 loaded: 0,
2375 skipped: 0,
2376 used: Vec::new(),
2377 };
2378 let Some(corpus_dir) = corpus_dir else {
2379 let _ = sh_warn!(
2380 "`--symbolic-use-fuzz-corpus` requires `--fuzz-corpus-dir` or `fuzz.corpus_dir`; \
2381 running without imported corpus seeds"
2382 );
2383 return (inputs, Some(metadata));
2384 };
2385 if limit == 0 {
2386 return (inputs, Some(metadata));
2387 }
2388
2389 'dirs: for replay_dir in canonical_replay_dirs(&corpus_dir) {
2390 let mut entries = read_corpus_dir(&replay_dir).collect::<Vec<_>>();
2391 entries.sort_by(|left, right| left.path.cmp(&right.path));
2392 for entry in entries {
2393 if inputs.len() >= limit {
2394 break 'dirs;
2395 }
2396 metadata.loaded += 1;
2397 let input = match entry.read_tx_seq() {
2398 Ok(tx_seq) => self.symbolic_corpus_seed_input(func, &tx_seq),
2399 Err(err) => {
2400 debug!(%err, path = %entry.path.display(), "failed to read symbolic corpus seed");
2401 None
2402 }
2403 };
2404 let Some(input) = input else {
2405 metadata.skipped += 1;
2406 continue;
2407 };
2408 metadata.used.push(SymbolicCorpusSeedRef {
2409 path: entry.path,
2410 calldata: input.calldata.clone(),
2411 });
2412 inputs.push(input);
2413 }
2414 }
2415
2416 debug!(
2417 test = %func.signature(),
2418 corpus_dir = %corpus_dir.display(),
2419 loaded = metadata.loaded,
2420 skipped = metadata.skipped,
2421 imported = inputs.len(),
2422 "imported symbolic fuzz corpus seeds"
2423 );
2424 (inputs, Some(metadata))
2425 }
2426
2427 fn import_symbolic_fuzz_frontiers(
2429 &self,
2430 func: &Function,
2431 fuzz_config: &FuzzConfig,
2432 ) -> Vec<(u64, Address, SymbolicBranchTarget, SymbolicConcreteInput)> {
2433 let limit = self.config.symbolic.frontier_limit;
2434 if limit == 0 {
2435 return Vec::new();
2436 }
2437
2438 let Some(frontier_dir) = fuzz_config.corpus.frontier_dir.as_ref() else {
2439 let _ = sh_warn!(
2440 "`--symbolic-use-fuzz-frontiers` requires `--fuzz-frontier-dir` or \
2441 `fuzz.frontier_dir`; running without targeted frontier seeds"
2442 );
2443 return Vec::new();
2444 };
2445
2446 let frontier_path = frontier_dir.join(FUZZ_BRANCH_FRONTIER_FILE);
2447 let artifact = match foundry_common::fs::read_json_file::<FuzzBranchFrontierArtifact>(
2448 &frontier_path,
2449 ) {
2450 Ok(artifact) => artifact,
2451 Err(err) => {
2452 debug!(
2453 %err,
2454 path = %frontier_path.display(),
2455 "failed to read fuzz branch frontier artifact"
2456 );
2457 return Vec::new();
2458 }
2459 };
2460
2461 if artifact.schema != FUZZ_BRANCH_FRONTIER_SCHEMA || artifact.version != 1 {
2462 warn!(
2463 schema = %artifact.schema,
2464 version = artifact.version,
2465 path = %frontier_path.display(),
2466 "unsupported fuzz branch frontier artifact"
2467 );
2468 return Vec::new();
2469 }
2470 let signature = func.signature();
2471 if artifact.test != signature {
2472 warn!(
2473 artifact_test = %artifact.test,
2474 test = %signature,
2475 path = %frontier_path.display(),
2476 "fuzz branch frontier artifact does not match symbolic target"
2477 );
2478 return Vec::new();
2479 }
2480
2481 let requested_ids = &self.config.symbolic.frontier_ids;
2482 let requested_pcs = &self.config.symbolic.frontier_pcs;
2483 let requested_selectors = &self.config.symbolic.frontier_selectors;
2484 let parsed_selectors = parse_frontier_selectors(requested_selectors, &signature);
2485 let selection_active = !requested_ids.is_empty()
2486 || !requested_pcs.is_empty()
2487 || !requested_selectors.is_empty();
2488 let mut skipped_by_selection = 0usize;
2489 let mut imported_ids = Vec::new();
2490 let mut imported_pcs = Vec::new();
2491 let mut imported_selectors = Vec::new();
2492 let mut imported = Vec::with_capacity(limit.min(artifact.frontiers.len()));
2493 for frontier in artifact.frontiers {
2494 let selector = frontier_selector(&frontier);
2495 if (!requested_ids.is_empty() && !requested_ids.contains(&frontier.id))
2496 || (!requested_pcs.is_empty() && !requested_pcs.contains(&frontier.site.pc))
2497 || (!requested_selectors.is_empty()
2498 && selector.is_none_or(|selector| !parsed_selectors.contains(&selector)))
2499 {
2500 skipped_by_selection += 1;
2501 continue;
2502 }
2503 if imported.len() == limit {
2504 if selection_active {
2505 continue;
2506 }
2507 break;
2508 }
2509 if !matches!(
2510 frontier.site.opcode,
2511 opcode::EQ | opcode::LT | opcode::GT | opcode::SLT | opcode::SGT | opcode::ISZERO
2512 ) {
2513 debug!(
2514 opcode = frontier.site.opcode,
2515 id = frontier.id,
2516 "skipping unsupported fuzz branch frontier opcode"
2517 );
2518 continue;
2519 }
2520 let ([call], 0) = (frontier.sequence.as_slice(), frontier.call_index) else {
2521 debug!(
2522 id = frontier.id,
2523 sequence_len = frontier.sequence.len(),
2524 call_index = frontier.call_index,
2525 "skipping non-stateless fuzz branch frontier"
2526 );
2527 continue;
2528 };
2529 let Some(input) = self.symbolic_corpus_seed_input(func, std::slice::from_ref(call))
2530 else {
2531 debug!(id = frontier.id, "skipping fuzz branch frontier with incompatible call");
2532 continue;
2533 };
2534 let target = SymbolicBranchTarget::new(
2535 frontier.site.address,
2536 frontier.site.pc,
2537 frontier.site.opcode,
2538 frontier.operands.result,
2539 );
2540 imported.push((frontier.id, call.sender, target, input));
2541 imported_ids.push(frontier.id);
2542 imported_pcs.push(frontier.site.pc);
2543 imported_selectors.extend(selector);
2544 }
2545
2546 warn_unimported_frontiers("id", requested_ids, &imported_ids, &signature, &frontier_path);
2547 warn_unimported_frontiers("pc", requested_pcs, &imported_pcs, &signature, &frontier_path);
2548 warn_unimported_frontiers(
2549 "selector",
2550 &parsed_selectors,
2551 &imported_selectors,
2552 &signature,
2553 &frontier_path,
2554 );
2555 if selection_active {
2556 let _ = sh_status!(
2557 "Symbolic frontier selection for {signature}: imported {}, skipped {} by target \
2558 filters (ids: {}; pcs: {}; selectors: {}; limit: {limit})",
2559 imported.len(),
2560 skipped_by_selection,
2561 frontier_filter_display(requested_ids),
2562 frontier_filter_display(requested_pcs),
2563 frontier_filter_display(requested_selectors),
2564 );
2565 }
2566
2567 debug!(
2568 test = %signature,
2569 path = %frontier_path.display(),
2570 imported = imported.len(),
2571 limit,
2572 skipped_by_selection,
2573 requested_ids = ?requested_ids,
2574 requested_pcs = ?requested_pcs,
2575 requested_selectors = ?requested_selectors,
2576 "imported fuzz branch frontiers for targeted symbolic seeding"
2577 );
2578 imported
2579 }
2580
2581 fn import_symbolic_invariant_frontiers(
2582 &self,
2583 invariant_contract: &InvariantContract<'_>,
2584 invariant_config: &InvariantConfig,
2585 ) -> Vec<(FuzzBranchFrontierRecord, Arc<[BasicTxDetails]>)> {
2586 let limit = self.config.symbolic.frontier_limit;
2587 if limit == 0 {
2588 return Vec::new();
2589 }
2590
2591 let Some(frontier_dir) = invariant_config.corpus.frontier_dir.as_ref() else {
2592 let _ = sh_warn!(
2593 "`--symbolic-use-fuzz-frontiers` requires `--invariant-frontier-dir` or \
2594 `invariant.frontier_dir`; running without targeted frontier seeds"
2595 );
2596 return Vec::new();
2597 };
2598 let frontier_path = frontier_dir.join(FUZZ_BRANCH_FRONTIER_FILE);
2599 let artifact = match foundry_common::fs::read_json_file::<FuzzBranchFrontierArtifact>(
2600 &frontier_path,
2601 ) {
2602 Ok(artifact) => artifact,
2603 Err(err) => {
2604 debug!(
2605 %err,
2606 path = %frontier_path.display(),
2607 "failed to read invariant branch frontier artifact"
2608 );
2609 return Vec::new();
2610 }
2611 };
2612 if artifact.schema != STATEFUL_FUZZ_BRANCH_FRONTIER_SCHEMA || artifact.version != 2 {
2613 warn!(
2614 schema = %artifact.schema,
2615 version = artifact.version,
2616 path = %frontier_path.display(),
2617 "unsupported invariant branch frontier artifact"
2618 );
2619 return Vec::new();
2620 }
2621 let signature = invariant_contract.anchor().signature();
2622 if invariant_contract.is_optimization() && artifact.test != signature {
2625 warn!(
2626 artifact_test = %artifact.test,
2627 test = %signature,
2628 path = %frontier_path.display(),
2629 "invariant branch frontier artifact does not match campaign anchor"
2630 );
2631 return Vec::new();
2632 }
2633
2634 let requested_ids = &self.config.symbolic.frontier_ids;
2635 let requested_pcs = &self.config.symbolic.frontier_pcs;
2636 let requested_selectors = &self.config.symbolic.frontier_selectors;
2637 let parsed_selectors = parse_frontier_selectors(requested_selectors, &signature);
2638 let select_frontier_ids = !requested_ids.is_empty();
2639 let select_frontier_pcs = !requested_pcs.is_empty();
2640 let select_frontier_selectors = !requested_selectors.is_empty();
2641
2642 let FuzzBranchFrontierArtifact { sequences, mut frontiers, .. } = artifact;
2643 let sequences =
2644 sequences.into_iter().map(Arc::<[BasicTxDetails]>::from).collect::<Vec<_>>();
2645 let mut observed_results = HashMap::<(Address, usize, u8), u8>::default();
2646 for frontier in &frontiers {
2647 let key = (frontier.site.address, frontier.site.pc, frontier.site.opcode);
2648 let result_bit = if frontier.operands.result { 2 } else { 1 };
2649 *observed_results.entry(key).or_default() |= result_bit;
2650 }
2651 for frontier in &mut frontiers {
2652 frontier.both_results_retained = observed_results
2653 .get(&(frontier.site.address, frontier.site.pc, frontier.site.opcode))
2654 .is_some_and(|results| *results == 3);
2655 }
2656 frontiers.retain(|frontier| {
2657 if select_frontier_ids && !requested_ids.contains(&frontier.id) {
2658 return false;
2659 }
2660 if select_frontier_pcs && !requested_pcs.contains(&frontier.site.pc) {
2661 return false;
2662 }
2663 if !matches!(
2664 frontier.site.opcode,
2665 opcode::EQ | opcode::LT | opcode::GT | opcode::SLT | opcode::SGT | opcode::ISZERO
2666 ) {
2667 return false;
2668 }
2669 let Some(sequence) = frontier.sequence_index.and_then(|index| sequences.get(index))
2670 else {
2671 debug!(id = frontier.id, "skipping invariant frontier with missing sequence");
2672 return false;
2673 };
2674 let Some(call) = sequence.get(frontier.call_index) else {
2675 debug!(
2676 id = frontier.id,
2677 call_index = frontier.call_index,
2678 sequence_len = sequence.len(),
2679 "skipping invariant frontier with invalid call index"
2680 );
2681 return false;
2682 };
2683 if call.warp.is_some_and(|warp| !warp.is_zero())
2684 || call.roll.is_some_and(|roll| !roll.is_zero())
2685 || call.call_details.value.is_some_and(|value| !value.is_zero())
2686 {
2687 return false;
2688 }
2689 let selector = call
2690 .call_details
2691 .calldata
2692 .get(..4)
2693 .and_then(|selector| <[u8; 4]>::try_from(selector).ok())
2694 .map(Selector::from);
2695 !(select_frontier_selectors
2696 && selector.is_none_or(|selector| !parsed_selectors.contains(&selector)))
2697 });
2698 let explicit_selection =
2699 select_frontier_ids || select_frontier_pcs || select_frontier_selectors;
2700 let frontiers = select_stateful_frontiers(frontiers, limit, explicit_selection);
2701
2702 let mut imported = Vec::with_capacity(frontiers.len());
2703 for frontier in frontiers {
2704 let sequence = sequences
2705 .get(frontier.sequence_index.expect("frontier sequence index was validated"))
2706 .expect("frontier sequence was validated");
2707 imported.push((frontier, Arc::clone(sequence)));
2708 }
2709
2710 debug!(
2711 test = %signature,
2712 path = %frontier_path.display(),
2713 imported = imported.len(),
2714 limit,
2715 "imported invariant branch frontiers for targeted symbolic seeding"
2716 );
2717 imported
2718 }
2719
2720 fn symbolic_corpus_seed_input(
2721 &self,
2722 func: &Function,
2723 tx_seq: &[BasicTxDetails],
2724 ) -> Option<SymbolicConcreteInput> {
2725 let [tx] = tx_seq else {
2726 return None;
2727 };
2728 if tx.call_details.target != self.address
2729 || !tx.call_details.value.unwrap_or_default().is_zero()
2730 {
2731 return None;
2732 }
2733 let calldata = &tx.call_details.calldata;
2734 if calldata.get(..4) != Some(func.selector().as_slice()) {
2735 return None;
2736 }
2737 let args = func.abi_decode_input(&calldata[4..]).ok()?;
2738 Some(SymbolicConcreteInput { args, calldata: calldata.clone() })
2739 }
2740
2741 fn run_symbolic_test(mut self, func: &Function) -> TestResult {
2743 if self.prepare_test(func).is_err() {
2744 return self.result;
2745 }
2746
2747 let (corpus_seeds, mut corpus_seed_metadata) = self.import_symbolic_fuzz_corpus(func);
2748 if let Some(metadata) = corpus_seed_metadata.as_mut() {
2749 match SymbolicExecutor::modeled_corpus_seed_indexes(
2750 &self.config.symbolic,
2751 func,
2752 &corpus_seeds,
2753 ) {
2754 Ok(indexes) => {
2755 let mut indexes = indexes.into_iter().peekable();
2756 metadata.used = std::mem::take(&mut metadata.used)
2757 .into_iter()
2758 .enumerate()
2759 .filter_map(|(idx, seed)| indexes.next_if_eq(&idx).map(|_| seed))
2760 .collect();
2761 }
2762 Err(err) => {
2763 debug!(
2764 %err,
2765 test = %func.signature(),
2766 "failed to model imported symbolic corpus seeds"
2767 );
2768 }
2769 }
2770 }
2771 let symbolic_config = self.config.symbolic.clone();
2772 let mut symbolic = SymbolicExecutor::new(symbolic_config.clone());
2773 if self.cr.progress.is_some() && symbolic_config.dump_smt {
2775 symbolic.capture_diagnostics();
2776 }
2777 let result =
2778 symbolic.run(self.symbolic_run_input(func, self.sender, false, corpus_seeds, None));
2779 let portfolio_diagnostics = symbolic.portfolio_diagnostics();
2780 let symbolic_diagnostics = symbolic.take_diagnostics();
2781
2782 let (status, reason, counterexample, symbolic_result) = match result {
2783 SymbolicRunResult::Safe { stats, .. } => {
2784 (TestStatus::Success, None, None, SymbolicResult::pass(&symbolic_config, stats))
2785 }
2786 SymbolicRunResult::Incomplete { kind, reason, stats } => (
2787 TestStatus::Failure,
2788 Some(format!("incomplete symbolic execution ({kind:?}): {reason}")),
2789 None,
2790 SymbolicResult::incomplete(
2791 &symbolic_config,
2792 kind,
2793 reason,
2794 stats,
2795 SymbolicReplayMetadata::not_required(),
2796 SymbolicCallTrace::none(),
2797 None,
2798 ),
2799 ),
2800 SymbolicRunResult::Counterexample { args, calldata, stats } => {
2801 self.replay_symbolic_counterexample(func, args, calldata, stats, &symbolic_config)
2802 }
2803 };
2804 let symbolic_result = match corpus_seed_metadata {
2805 Some(metadata) => symbolic_result.with_corpus_seeds(metadata),
2806 None => symbolic_result,
2807 };
2808 self.result.symbolic_result(status, reason, counterexample, symbolic_result);
2809 self.result.symbolic_portfolio_diagnostics = portfolio_diagnostics;
2810 self.result.symbolic_diagnostics = symbolic_diagnostics;
2811 self.result
2812 }
2813
2814 fn replay_symbolic_counterexample(
2817 &mut self,
2818 func: &Function,
2819 args: Vec<DynSolValue>,
2820 calldata: Bytes,
2821 stats: SymbolicStats,
2822 symbolic_config: &SymbolicConfig,
2823 ) -> (TestStatus, Option<String>, Option<CounterExample>, SymbolicResult) {
2824 let symbolic_counterexample = SymbolicCounterexample::from(
2825 &BaseCounterExample::from_fuzz_call(calldata.clone(), args.clone(), None),
2826 );
2827 let incomplete = |reason: String, replay, call_trace| {
2828 SymbolicResult::incomplete(
2829 symbolic_config,
2830 SymbolicStopReason::Error,
2831 reason,
2832 stats,
2833 replay,
2834 call_trace,
2835 Some(symbolic_counterexample.clone()),
2836 )
2837 };
2838
2839 let (raw, reason) = match self.executor.call(
2840 self.sender,
2841 self.address,
2842 func,
2843 &args,
2844 U256::ZERO,
2845 Some(self.revert_decoder()),
2846 ) {
2847 Ok(res) => (res.raw, None),
2848 Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
2849 Err(EvmError::Skip(reason)) => {
2850 let replay_reason = format!("vm.skip during concrete replay: {reason}");
2851 let symbolic_result = incomplete(
2852 "concrete replay skipped the symbolic counterexample".to_string(),
2853 SymbolicReplayMetadata::skipped(replay_reason),
2854 SymbolicCallTrace::none(),
2855 );
2856 return (TestStatus::Skipped, reason.0, None, symbolic_result);
2857 }
2858 Err(err) => {
2859 let reason = err.to_string();
2860 let symbolic_result = incomplete(
2861 reason.clone(),
2862 SymbolicReplayMetadata::error(reason.clone()),
2863 SymbolicCallTrace::none(),
2864 );
2865 return (TestStatus::Failure, Some(reason), None, symbolic_result);
2866 }
2867 };
2868
2869 let base_counterexample =
2870 BaseCounterExample::from_fuzz_call(calldata, args, raw.traces.clone());
2871 if self.executor.is_raw_call_success(
2872 self.address,
2873 Cow::Borrowed(&raw.state_changeset),
2874 &raw,
2875 false,
2876 ) {
2877 let call_trace = SymbolicCallTrace::test_result_traces(raw.traces.is_some());
2880 self.result.extend(raw);
2881 let reason = "symbolic counterexample did not replay".to_string();
2882 let display_reason = format!(
2883 "incomplete symbolic execution ({:?}): {reason}",
2884 SymbolicStopReason::Error
2885 );
2886 let symbolic_result =
2887 incomplete(reason.clone(), SymbolicReplayMetadata::mismatch(reason), call_trace);
2888 return (TestStatus::Failure, Some(display_reason), None, symbolic_result);
2889 }
2890
2891 let original_call = SymbolicCounterexampleCall::from_base_counterexample(
2892 &base_counterexample,
2893 self.sender,
2894 self.address,
2895 );
2896 let mut final_call = original_call.clone();
2897 let mut final_raw = raw;
2898 let mut final_reason = reason;
2899 let mut minimization = None;
2900 if final_reason.is_some()
2901 && let Some(candidate) = minimize_single_call_counterexample(
2902 func,
2903 &original_call,
2904 self.tcfg.config.invariant.shrink_run_limit as usize,
2905 |candidate| {
2906 self.replay_confirmed_symbolic_single_call(candidate, final_reason.as_deref())
2907 .is_ok()
2908 },
2909 )
2910 {
2911 if candidate.changed() {
2912 match self.replay_confirmed_symbolic_single_call(
2913 &candidate.minimized_call,
2914 final_reason.as_deref(),
2915 ) {
2916 Ok((raw, reason)) => {
2917 final_call = candidate.minimized_call.clone();
2918 final_raw = raw;
2919 final_reason = reason;
2920 minimization = Some(candidate);
2921 }
2922 Err(err) => {
2923 warn!(
2924 %err,
2925 "discarding symbolic counterexample minimization result that no longer replays"
2926 );
2927 }
2928 }
2929 } else {
2930 minimization = Some(candidate);
2931 }
2932 }
2933
2934 let call_trace = SymbolicCallTrace::test_result_traces(final_raw.traces.is_some());
2935 let mut base_counterexample = final_call.to_base_counterexample();
2936 base_counterexample.traces = final_raw.traces.clone();
2937 self.result.extend(final_raw);
2938
2939 let signature = func.signature();
2940 let fail_on_revert = self.config.invariant.fail_on_revert;
2941 let kind = SymbolicCounterexampleArtifactKind::SingleCall;
2942 let mut symbolic_result = SymbolicResult::fail_counterexample(
2943 symbolic_config,
2944 stats,
2945 call_trace,
2946 SymbolicCounterexample::from(&base_counterexample),
2947 );
2948 let minimized_artifact = self.write_symbolic_artifact(
2949 &signature,
2950 &self.symbolic_artifact(
2951 &signature,
2952 kind,
2953 &symbolic_result,
2954 fail_on_revert,
2955 vec![final_call],
2956 ),
2957 );
2958 if let Some(artifact) = minimized_artifact.clone() {
2959 symbolic_result = symbolic_result.with_artifact(artifact);
2960 }
2961 if let Some(minimization) = minimization {
2962 let original_result = SymbolicResult::fail_counterexample(
2963 symbolic_config,
2964 stats,
2965 SymbolicCallTrace::none(),
2966 symbolic_counterexample,
2967 );
2968 let original_artifact = self.write_symbolic_artifact(
2969 &format!("original__{signature}"),
2970 &self.symbolic_artifact(
2971 &signature,
2972 kind,
2973 &original_result,
2974 fail_on_revert,
2975 vec![minimization.original_call.clone()],
2976 ),
2977 );
2978 if let Some((original, minimized)) = original_artifact.zip(minimized_artifact) {
2979 symbolic_result =
2980 symbolic_result.with_minimization(SymbolicCounterexampleMinimization::new(
2981 original,
2982 minimized,
2983 minimization.attempts,
2984 minimization.accepted,
2985 minimization.original_call.calldata.len(),
2986 minimization.minimized_call.calldata.len(),
2987 ));
2988 }
2989 }
2990 (
2991 TestStatus::Failure,
2992 final_reason,
2993 Some(CounterExample::Single(base_counterexample)),
2994 symbolic_result,
2995 )
2996 }
2997
2998 fn run_symbolic_artifact_replay(
3000 mut self,
3001 func: &Function,
3002 invariants: &[&Function],
3003 call_after_invariant: bool,
3004 ) -> TestResult {
3005 if let Err(reason) = self.replay_symbolic_artifact(func, invariants, call_after_invariant) {
3006 self.result.single_fail(Some(reason));
3007 }
3008 self.result
3009 }
3010
3011 fn replay_symbolic_artifact(
3014 &mut self,
3015 func: &Function,
3016 invariants: &[&Function],
3017 call_after_invariant: bool,
3018 ) -> Result<(), String> {
3019 let Some(replay) = &self.cr.mcr.tcfg.symbolic_artifact_replay else {
3020 return Err("missing symbolic artifact replay config".to_string());
3021 };
3022 let artifact = &replay.artifact;
3023 self.apply_function_inline_config(func).map_err(|e| e.to_string())?;
3024
3025 match artifact.kind {
3026 SymbolicCounterexampleArtifactKind::SingleCall => {
3027 if artifact.replay.status != SymbolicReplayStatus::Confirmed {
3028 return Err(format!(
3029 "single-call symbolic artifact replay status must be confirmed, got {:?}",
3030 artifact.replay.status
3031 ));
3032 }
3033 let Some(call) = artifact.calls.first() else {
3034 return Err("symbolic artifact has no calls".to_string());
3035 };
3036 if artifact.calls.len() != 1 {
3037 return Err(
3038 "single-call symbolic artifact must contain exactly one call".to_string()
3039 );
3040 }
3041 if call.target != self.address {
3045 return Err(format!(
3046 "single-call symbolic artifact target {} does not match test contract {}",
3047 call.target, self.address
3048 ));
3049 }
3050 if call.calldata.get(..4).is_none_or(|selector| func.selector() != selector) {
3051 return Err(format!(
3052 "single-call symbolic artifact calldata does not match `{}` selector",
3053 func.signature()
3054 ));
3055 }
3056
3057 if self.prepare_test(func).is_err() {
3058 return Ok(());
3059 }
3060
3061 let counterexample = || CounterExample::Single(call.to_base_counterexample());
3062 let mut executor = self.clone_executor();
3063 let raw = match execute_tx(&mut executor, &call.to_basic_tx_details()) {
3064 Ok(raw) => raw,
3065 Err(err) => {
3066 self.result.counterexample = Some(counterexample());
3067 return Err(err.to_string());
3068 }
3069 };
3070 if executor.is_raw_call_success(
3071 self.address,
3072 Cow::Borrowed(&raw.state_changeset),
3073 &raw,
3074 false,
3075 ) {
3076 self.result.single_result(true, None, raw);
3077 return Ok(());
3078 }
3079 match raw.into_evm_error(Some(self.revert_decoder())) {
3080 EvmError::Execution(err) => {
3081 let reason = if err.reason.is_empty() {
3082 artifact.replay.reason.clone()
3083 } else {
3084 Some(err.reason.clone())
3085 };
3086 self.result.single_result(false, reason, err.raw);
3087 self.result.counterexample = Some(counterexample());
3088 }
3089 EvmError::Skip(reason) => self.result.single_skip(reason),
3090 err => {
3091 self.result.counterexample = Some(counterexample());
3092 return Err(err.to_string());
3093 }
3094 }
3095 }
3096 SymbolicCounterexampleArtifactKind::Sequence => {
3097 let Some(invariant) = invariants.first() else {
3098 return Err(
3099 "sequence symbolic artifact must target an invariant test".to_string()
3100 );
3101 };
3102 if artifact.calls.is_empty() {
3103 return Err("symbolic artifact has no calls".to_string());
3104 }
3105
3106 let calls = artifact
3107 .calls
3108 .iter()
3109 .map(SymbolicCounterexampleCall::to_base_counterexample)
3110 .collect::<Vec<_>>();
3111 let txes = artifact
3112 .calls
3113 .iter()
3114 .map(SymbolicCounterexampleCall::to_basic_tx_details)
3115 .collect::<Vec<_>>();
3116 let setup_contracts = load_contracts(
3117 self.setup.traces.iter().map(|(_, trace)| &trace.arena),
3118 &self.cr.mcr.known_contracts,
3119 );
3120 let mut evm = InvariantExecutor::new_with_fuzz_seed(
3121 self.clone_executor(),
3122 self.invariant_runner(),
3123 self.config.fuzz.seed,
3124 self.config.invariant.clone(),
3125 &setup_contracts,
3126 &self.cr.mcr.known_contracts,
3127 self.cr.num_invariant_campaign_anchors,
3128 );
3129 if let Err(err) = evm.select_contract_artifacts(self.address) {
3130 self.result.invariant_setup_fail(err);
3131 return Ok(());
3132 }
3133 let (sender_filters, targeted) =
3134 match evm.select_contracts_and_senders(self.address) {
3135 Ok(selected) => selected,
3136 Err(err) => {
3137 self.result.invariant_setup_fail(err);
3138 return Ok(());
3139 }
3140 };
3141 let artifact_executor =
3142 match self.clone_executor_with_symbolic_storage(&artifact.storage) {
3143 Ok(executor) => executor,
3144 Err(err) => {
3145 self.result.counterexample =
3146 Some(CounterExample::Sequence(calls.len(), calls));
3147 return Err(err.to_string());
3148 }
3149 };
3150
3151 let dynamic_target_ctx = evm.dynamic_target_ctx();
3152 let mut validation_executor =
3153 targeted.is_updatable.then(|| artifact_executor.clone());
3154 let mut validation_created_contracts = Vec::new();
3155 for (idx, tx) in txes.iter().enumerate() {
3156 let Some(selector) = tx.call_details.calldata.get(..4) else {
3157 return Err(format!(
3158 "sequence symbolic artifact call {} has calldata shorter than a selector",
3159 idx + 1
3160 ));
3161 };
3162 if !targeted.targets().can_replay(tx) {
3163 return Err(format!(
3164 "sequence symbolic artifact call {} targets unknown function {} on {}",
3165 idx + 1,
3166 hex::encode_prefixed(selector),
3167 tx.call_details.target
3168 ));
3169 }
3170 if !sender_filters.allows(tx.sender) {
3171 return Err(format!(
3172 "sequence symbolic artifact call {} uses forbidden sender {}",
3173 idx + 1,
3174 tx.sender
3175 ));
3176 }
3177 if let Some(validation_executor) = validation_executor.as_mut() {
3178 execute_tx_and_register_created(
3179 validation_executor,
3180 tx,
3181 &targeted,
3182 &dynamic_target_ctx,
3183 &mut validation_created_contracts,
3184 )
3185 .map_err(|err| {
3186 format!(
3187 "sequence symbolic artifact call {} failed during target validation: {err}",
3188 idx + 1
3189 )
3190 })?;
3191 }
3192 }
3193
3194 let artifact_failure = artifact.invariant_failure.as_ref();
3195 if matches!(
3196 artifact_failure,
3197 Some(SymbolicInvariantArtifactFailure::Predicate { site: None, .. })
3198 ) {
3199 return Err(
3200 "sequence symbolic artifact does not identify an exact predicate failure site"
3201 .to_string(),
3202 );
3203 }
3204 let is_handler_artifact = matches!(
3205 artifact_failure,
3206 Some(SymbolicInvariantArtifactFailure::Handler { .. })
3207 );
3208 let sequence = (0..txes.len()).collect::<Vec<_>>();
3209 let outcome = match check_sequence(
3210 artifact_executor,
3211 &txes,
3212 &sequence,
3213 self.setup.address,
3214 invariant.selector().to_vec().into(),
3215 CheckSequenceOptions {
3216 accumulate_warp_roll: false,
3220 fail_on_revert: is_handler_artifact
3221 || artifact.replay_semantics.fail_on_revert,
3222 expect_assertion_failure: is_handler_artifact,
3223 call_after_invariant,
3224 rd: Some(self.revert_decoder()),
3225 },
3226 ) {
3227 Ok(outcome) => outcome,
3228 Err(err) => {
3229 self.result.counterexample =
3230 Some(CounterExample::Sequence(calls.len(), calls));
3231 return Err(err.to_string());
3232 }
3233 };
3234 if outcome.success {
3235 self.result.invariant_replay_success(outcome.calls_count, outcome.reverts);
3236 return Ok(());
3237 }
3238 match artifact_failure {
3239 Some(SymbolicInvariantArtifactFailure::Handler {
3240 name,
3241 reverter,
3242 selector,
3243 fingerprint,
3244 }) => {
3245 let expected_site = CheckSequenceFailureSite::SequenceCall {
3246 target: *reverter,
3247 selector: *selector,
3248 fingerprint: *fingerprint,
3249 };
3250 if outcome.failure_site != Some(expected_site) {
3251 return Err(format!(
3252 "sequence symbolic artifact replayed a different handler \
3253 failure site than the stored artifact: expected \
3254 {reverter}::{selector} at {fingerprint}, got {:?}",
3255 outcome.failure_site
3256 ));
3257 }
3258 let handler_name = name.clone().unwrap_or_else(|| {
3259 invariant_handler_failure_name(&setup_contracts, *reverter, *selector)
3260 });
3261 self.result.invariant_result(
3262 invariant_kind(1, outcome.calls_count, outcome.reverts),
3263 InvariantOutcome {
3264 handler_failures: vec![InvariantFailure::Handler {
3265 name: handler_name,
3266 reverter: *reverter,
3267 selector: *selector,
3268 reason: outcome
3269 .reason
3270 .or_else(|| artifact.replay.reason.clone())
3271 .unwrap_or_else(|| {
3272 "symbolic handler counterexample".to_string()
3273 }),
3274 counterexample: Some(CounterExample::Sequence(
3275 calls.len(),
3276 calls,
3277 )),
3278 artifact: Some(SymbolicArtifactRef::new(replay.path.clone())),
3279 }],
3280 ..Default::default()
3281 },
3282 );
3283 }
3284 _ => {
3285 if let Some(SymbolicInvariantArtifactFailure::Predicate { site, .. }) =
3286 artifact_failure
3287 && outcome.failure_site.map(SymbolicInvariantFailureSite::from) != *site
3288 {
3289 return Err(format!(
3290 "sequence symbolic artifact replayed a different failure \
3291 origin than the stored predicate: got {:?}",
3292 outcome.failure_site
3293 ));
3294 }
3295 let signature = invariant.signature();
3296 let invariant_name = match artifact_failure {
3297 Some(SymbolicInvariantArtifactFailure::Predicate { name, .. }) => {
3298 name.as_str()
3299 }
3300 _ => signature.as_str(),
3301 };
3302 self.result.invariant_replay_fail(
3303 outcome,
3304 invariant_name,
3305 artifact.replay.reason.clone(),
3306 calls,
3307 );
3308 }
3309 }
3310 }
3311 }
3312 Ok(())
3313 }
3314
3315 fn try_seed_fuzz_corpus_from_frontiers(&self, func: &Function, fuzz_config: &FuzzConfig) {
3316 if !self.config.symbolic.use_fuzz_frontiers || !func.test_function_kind().is_fuzz_test() {
3317 return;
3318 }
3319 if fuzz_config.corpus.corpus_dir.is_none() {
3320 let _ = sh_warn!(
3321 "`--symbolic-use-fuzz-frontiers` requires `--fuzz-corpus-dir` or \
3322 `fuzz.corpus_dir`; skipping targeted frontier seeding"
3323 );
3324 return;
3325 }
3326
3327 for (id, sender, target, input) in self.import_symbolic_fuzz_frontiers(func, fuzz_config) {
3328 let mut symbolic = SymbolicExecutor::new(self.config.symbolic.clone());
3329 let result = symbolic.run(self.symbolic_run_input(
3330 func,
3331 sender,
3332 true,
3333 vec![input],
3334 Some(target),
3335 ));
3336
3337 let (input, expect_failure) = match result {
3338 SymbolicRunResult::Safe { success_input: Some(input), .. } => (input, false),
3339 SymbolicRunResult::Safe { success_input: None, .. } => {
3340 warn!(
3341 id,
3342 test = %func.signature(),
3343 "targeted symbolic frontier produced no branch-flipping input"
3344 );
3345 continue;
3346 }
3347 SymbolicRunResult::Incomplete { kind, reason, .. } => {
3348 warn!(
3349 id,
3350 ?kind,
3351 %reason,
3352 test = %func.signature(),
3353 "targeted symbolic frontier incomplete"
3354 );
3355 continue;
3356 }
3357 SymbolicRunResult::Counterexample { args, calldata, .. } => {
3358 (SymbolicConcreteInput { args, calldata }, true)
3359 }
3360 };
3361
3362 let replay = self.symbolic_fuzz_seed_replay(sender, &input, fuzz_config);
3363 if replay != Some(!expect_failure) {
3364 warn!(
3365 id,
3366 ?replay,
3367 test = %func.signature(),
3368 "targeted symbolic frontier seed did not replay with the expected outcome"
3369 );
3370 continue;
3371 }
3372
3373 match self.persist_symbolic_fuzz_seed(&fuzz_config.corpus, sender, input.calldata) {
3374 Ok(Some(path)) => {
3375 debug!(
3376 id,
3377 path = %path.display(),
3378 test = %func.signature(),
3379 "persisted targeted symbolic frontier seed"
3380 );
3381 }
3382 Ok(None) => {}
3383 Err(err) => {
3384 warn!(
3385 %err,
3386 id,
3387 test = %func.signature(),
3388 "failed to persist targeted symbolic frontier seed"
3389 );
3390 }
3391 }
3392 }
3393 }
3394
3395 fn invariant_sequence_failure_site(
3396 &self,
3397 invariant_contract: &InvariantContract<'_>,
3398 invariant_idx: usize,
3399 sequence: &[BasicTxDetails],
3400 replay_order: &[usize],
3401 call_after_invariant: bool,
3402 ) -> Option<CheckSequenceFailureSite> {
3403 let policy = invariant_contract.invariant_fns[invariant_idx].1;
3404 let outcome = check_sequence(
3405 self.clone_executor(),
3406 sequence,
3407 replay_order,
3408 invariant_contract.address,
3409 invariant_contract.invariant_calldata(invariant_idx),
3410 CheckSequenceOptions {
3411 accumulate_warp_roll: false,
3412 fail_on_revert: policy,
3413 expect_assertion_failure: false,
3414 call_after_invariant,
3415 rd: Some(self.revert_decoder()),
3416 },
3417 )
3418 .ok()?;
3419 (!outcome.success && outcome.replayed_entirely).then_some(outcome.failure_site).flatten()
3420 }
3421
3422 fn solve_invariants_from_frontier_prefix(
3423 &self,
3424 invariant_contract: &InvariantContract<'_>,
3425 invariant_indexes: &[usize],
3426 prefix_executor: &Executor<FEN>,
3427 target: &SymbolicInvariantTarget,
3428 sender: Address,
3429 prefix: &[BasicTxDetails],
3430 ) -> Vec<(usize, CheckSequenceFailureSite, Vec<BasicTxDetails>)> {
3431 let after_invariant = invariant_contract
3432 .call_after_invariant
3433 .then(|| {
3434 invariant_contract.abi.functions().find(|function| {
3435 function.name == "afterInvariant" && function.inputs.is_empty()
3436 })
3437 })
3438 .flatten();
3439
3440 let invariants = invariant_indexes
3441 .iter()
3442 .map(|&idx| invariant_contract.invariant_fns[idx].0)
3443 .collect::<Vec<_>>();
3444 let mut symbolic = SymbolicExecutor::new(self.config.symbolic.clone());
3445 let result = symbolic.search_invariant_candidates(SymbolicInvariantCandidateInput {
3446 executor: prefix_executor,
3447 invariant_address: invariant_contract.address,
3448 invariants: &invariants,
3449 after_invariant,
3450 target,
3451 handler_sender: sender,
3452 ffi_enabled: self.config.ffi,
3453 });
3454 if let Some(limitation) = &result.limitation {
3455 debug!(
3456 ?limitation.kind,
3457 reason = %limitation.reason,
3458 candidates = result.candidates.len(),
3459 "symbolic invariant frontier candidate search incomplete"
3460 );
3461 }
3462
3463 result
3464 .candidates
3465 .into_iter()
3466 .filter_map(|candidate| {
3467 if !candidate.storage.is_empty() {
3468 return None;
3469 }
3470 let invariant_idx = invariant_indexes[candidate.invariant_idx];
3471 let call = BasicTxDetails {
3472 warp: None,
3473 roll: None,
3474 sender: candidate.step.sender,
3475 call_details: CallDetails {
3476 target: candidate.step.address,
3477 calldata: candidate.step.calldata,
3478 value: None,
3479 },
3480 };
3481 let mut sequence = Vec::with_capacity(prefix.len() + 1);
3482 sequence.extend_from_slice(prefix);
3483 sequence.push(call);
3484 let replay_order = (0..sequence.len()).collect::<Vec<_>>();
3485 let failure_site = self.invariant_sequence_failure_site(
3486 invariant_contract,
3487 invariant_idx,
3488 &sequence,
3489 &replay_order,
3490 after_invariant.is_some(),
3491 )?;
3492 let exact_failure = match failure_site {
3493 CheckSequenceFailureSite::Invariant { selector, .. } => {
3494 selector == invariant_contract.invariant_fns[invariant_idx].0.selector()
3495 }
3496 CheckSequenceFailureSite::AfterInvariant { .. } => true,
3497 CheckSequenceFailureSite::SequenceCall { .. } => false,
3498 };
3499 exact_failure.then_some((invariant_idx, failure_site, sequence))
3500 })
3501 .collect()
3502 }
3503
3504 fn try_seed_invariant_corpus_from_frontiers(
3505 &self,
3506 invariant_contract: &InvariantContract<'_>,
3507 invariant_config: &InvariantConfig,
3508 sender_filters: &SenderFilters,
3509 targeted_contracts: &FuzzRunIdentifiedContracts,
3510 dynamic_target_ctx: &DynamicTargetCtx<'_>,
3511 ) {
3512 if !self.config.symbolic.use_fuzz_frontiers {
3513 return;
3514 }
3515 if invariant_config.corpus.corpus_dir.is_none() {
3516 let _ = sh_warn!(
3517 "`--symbolic-use-fuzz-frontiers` requires `--invariant-corpus-dir` or \
3518 `invariant.corpus_dir`; skipping targeted invariant frontier seeding"
3519 );
3520 return;
3521 }
3522
3523 let mut checked_property_calls = HashSet::<(usize, usize)>::default();
3524 let mut seeded_invariants = HashSet::<usize>::default();
3525 let mut after_invariant_seeded = false;
3526 let fail_on_revert = invariant_contract.invariant_fns.iter().any(|(_, policy)| *policy);
3527 for (frontier, sequence) in
3528 self.import_symbolic_invariant_frontiers(invariant_contract, invariant_config)
3529 {
3530 let id = frontier.id;
3531 let call_index = frontier.call_index;
3532 let Some(call) = sequence.get(call_index) else {
3533 continue;
3534 };
3535 let Some(selector) = call
3536 .call_details
3537 .calldata
3538 .get(..4)
3539 .and_then(|selector| <[u8; 4]>::try_from(selector).ok())
3540 .map(Selector::from)
3541 else {
3542 continue;
3543 };
3544 let mut prefix_executor = self.clone_executor();
3545 let mut created_contracts = Vec::new();
3546 let prefix_targets = FuzzRunIdentifiedContracts::new(
3547 targeted_contracts.targets().clone(),
3548 targeted_contracts.is_updatable,
3549 );
3550 let prefix_result = sequence[..call_index].iter().try_for_each(|prefix_call| {
3551 if !prefix_targets.targets().can_replay(prefix_call)
3552 || !sender_filters.allows(prefix_call.sender)
3553 {
3554 return Err(eyre::eyre!(
3555 "frontier prefix call is not eligible for this campaign"
3556 ));
3557 }
3558 execute_tx_and_register_created(
3559 &mut prefix_executor,
3560 prefix_call,
3561 &prefix_targets,
3562 dynamic_target_ctx,
3563 &mut created_contracts,
3564 )
3565 });
3566 if let Err(err) = prefix_result {
3567 debug!(%err, id, "failed to replay invariant frontier prefix");
3568 continue;
3569 }
3570 if !sender_filters.allows(call.sender) {
3571 debug!(id, sender = %call.sender, "skipping invariant frontier with forbidden sender");
3572 continue;
3573 }
3574 let invariant_target = {
3575 let targets = prefix_targets.targets();
3576 targets.get(&call.call_details.target).and_then(|contract| {
3577 contract.fuzzed_function_by_selector(selector).map(|function| {
3578 SymbolicInvariantTarget {
3579 address: call.call_details.target,
3580 contract_name: Some(contract.identifier.clone()),
3581 function: function.clone(),
3582 }
3583 })
3584 })
3585 };
3586 let Some(invariant_target) = invariant_target else {
3587 debug!(id, selector = %selector, "skipping invariant frontier with unknown target function");
3588 continue;
3589 };
3590 let function = &invariant_target.function;
3591 let Ok(args) = function.abi_decode_input(&call.call_details.calldata[4..]) else {
3592 debug!(id, selector = %selector, "skipping invariant frontier with invalid calldata");
3593 continue;
3594 };
3595
3596 let input =
3597 SymbolicConcreteInput { args, calldata: call.call_details.calldata.clone() };
3598 let property_target_seeded = if invariant_contract.call_after_invariant {
3599 seeded_invariants.contains(&invariant_contract.anchor_idx) || after_invariant_seeded
3600 } else {
3601 seeded_invariants.len() == invariant_contract.invariant_fns.len()
3602 };
3603 if self.config.symbolic.check_invariant_frontiers
3604 && !property_target_seeded
3605 && checked_property_calls.insert((
3606 frontier.sequence_index.expect("frontier sequence index was validated"),
3607 call_index,
3608 ))
3609 {
3610 let mut invariant_indexes = if invariant_contract.call_after_invariant {
3611 vec![invariant_contract.anchor_idx]
3612 } else {
3613 (0..invariant_contract.invariant_fns.len())
3614 .filter(|idx| !seeded_invariants.contains(idx))
3615 .collect::<Vec<_>>()
3616 };
3617 if invariant_indexes.len() > 1 {
3618 let rotation = (checked_property_calls.len() - 1) % invariant_indexes.len();
3619 invariant_indexes.rotate_left(rotation);
3620 }
3621 for (invariant_idx, failure_site, solved_sequence) in self
3622 .solve_invariants_from_frontier_prefix(
3623 invariant_contract,
3624 &invariant_indexes,
3625 &prefix_executor,
3626 &invariant_target,
3627 call.sender,
3628 &sequence[..call_index],
3629 )
3630 {
3631 match persist_corpus_seed(&invariant_config.corpus, solved_sequence) {
3632 Ok(path) => {
3633 match failure_site {
3634 CheckSequenceFailureSite::Invariant { .. } => {
3635 seeded_invariants.insert(invariant_idx);
3636 }
3637 CheckSequenceFailureSite::AfterInvariant { .. } => {
3638 after_invariant_seeded = true;
3639 }
3640 CheckSequenceFailureSite::SequenceCall { .. } => unreachable!(),
3641 }
3642 if let Some(path) = path {
3643 debug!(id, path = %path.display(), "persisted property-directed invariant frontier seed");
3644 }
3645 }
3646 Err(err) => {
3647 warn!(%err, id, "failed to persist property-directed invariant frontier seed");
3648 }
3649 }
3650 }
3651 }
3652
3653 let mut symbolic = SymbolicExecutor::new(self.config.symbolic.clone());
3654 let target = SymbolicBranchTarget::new(
3655 frontier.site.address,
3656 frontier.site.pc,
3657 frontier.site.opcode,
3658 frontier.operands.result,
3659 );
3660 let search = symbolic.search_branch_target(SymbolicRunInput {
3661 executor: &prefix_executor,
3662 target: call.call_details.target,
3663 sender: call.sender,
3664 function,
3665 value: U256::ZERO,
3666 ffi_enabled: self.config.ffi,
3667 collect_success_input: false,
3668 corpus_seeds: vec![input],
3669 branch_target: Some(target),
3670 });
3671 if let SymbolicRunResult::Incomplete { kind, reason, .. } = &search.execution {
3672 debug!(
3673 id,
3674 ?kind,
3675 %reason,
3676 candidates = search.candidates.len(),
3677 "targeted invariant frontier incomplete"
3678 );
3679 } else if search.candidates.is_empty() {
3680 debug!(id, "targeted invariant frontier produced no branch-flipping input");
3681 }
3682
3683 let mut selected_branch_seed = None;
3684 let mut selected_failure_seed = false;
3685 for solved_input in search.candidates {
3686 let mut solved_sequence = sequence[..=call_index].to_vec();
3687 solved_sequence[call_index].call_details.calldata = solved_input.calldata;
3688 let mut replay_executor = prefix_executor.clone();
3689 replay_executor.inspector_mut().collect_evm_cmp_log(true);
3690 let replay_result =
3691 match execute_tx(&mut replay_executor, &solved_sequence[call_index]) {
3692 Ok(result) => result,
3693 Err(err) => {
3694 debug!(%err, id, "failed to replay solved invariant frontier");
3695 continue;
3696 }
3697 };
3698 let comparisons = replay_result.evm_cmp_values.as_deref().unwrap_or_default();
3699 let branch_flipped = frontier_comparison_flipped(
3700 frontier.site,
3701 frontier.operands.result,
3702 comparisons,
3703 );
3704 let assertion_failure =
3705 did_fail_on_assert(&replay_result, &replay_result.state_changeset);
3706 let accepted = replay_result.result.as_ref() != MAGIC_ASSUME
3707 && (!replay_result.reverted || fail_on_revert || assertion_failure);
3708 if !branch_flipped || !accepted {
3709 debug!(
3710 id,
3711 branch_flipped,
3712 reverted = replay_result.reverted,
3713 fail_on_revert,
3714 "solved invariant frontier was not eligible during concrete replay"
3715 );
3716 continue;
3717 }
3718
3719 let replay_order = (0..solved_sequence.len()).collect::<Vec<_>>();
3720 let broken_invariants = (0..invariant_contract.invariant_fns.len())
3721 .filter(|idx| !seeded_invariants.contains(idx))
3722 .filter(|&invariant_idx| {
3723 matches!(
3724 self.invariant_sequence_failure_site(
3725 invariant_contract,
3726 invariant_idx,
3727 &solved_sequence,
3728 &replay_order,
3729 false,
3730 ),
3731 Some(CheckSequenceFailureSite::Invariant { selector, .. })
3732 if selector == invariant_contract.invariant_fns[invariant_idx].0.selector()
3733 )
3734 })
3735 .collect::<Vec<_>>();
3736 let after_invariant_failure = invariant_contract.call_after_invariant
3737 && !after_invariant_seeded
3738 && matches!(
3739 self.invariant_sequence_failure_site(
3740 invariant_contract,
3741 invariant_contract.anchor_idx,
3742 &solved_sequence,
3743 &replay_order,
3744 true,
3745 ),
3746 Some(CheckSequenceFailureSite::AfterInvariant { .. })
3747 );
3748 if !broken_invariants.is_empty() || after_invariant_failure {
3749 match persist_corpus_seed(&invariant_config.corpus, solved_sequence.clone()) {
3750 Ok(path) => {
3751 seeded_invariants.extend(broken_invariants.iter().copied());
3752 after_invariant_seeded |= after_invariant_failure;
3753 if let Some(path) = path {
3754 debug!(
3755 id,
3756 ?broken_invariants,
3757 after_invariant_failure,
3758 path = %path.display(),
3759 "persisted property-breaking branch frontier seed"
3760 );
3761 }
3762 }
3763 Err(err) => {
3764 warn!(%err, id, "failed to persist property-breaking branch frontier seed");
3765 }
3766 }
3767 }
3768
3769 if assertion_failure || replay_result.reverted {
3770 if !selected_failure_seed {
3771 selected_branch_seed = Some(solved_sequence);
3772 selected_failure_seed = true;
3773 }
3774 } else {
3775 selected_branch_seed.get_or_insert(solved_sequence);
3776 }
3777 }
3778 if let Some(sequence) = selected_branch_seed {
3779 match persist_corpus_seed(&invariant_config.corpus, sequence) {
3780 Ok(Some(path)) => {
3781 debug!(id, path = %path.display(), "persisted targeted invariant frontier seed");
3782 }
3783 Ok(None) => {}
3784 Err(err) => {
3785 warn!(%err, id, "failed to persist targeted invariant frontier seed");
3786 }
3787 }
3788 }
3789 }
3790 }
3791
3792 fn try_seed_fuzz_corpus_symbolically(&self, func: &Function, fuzz_config: &FuzzConfig) {
3793 if !self.config.symbolic.seed_corpus || !func.test_function_kind().is_fuzz_test() {
3794 return;
3795 }
3796 if fuzz_config.corpus.corpus_dir.is_none() {
3797 let _ = sh_warn!(
3798 "`--symbolic-seed-corpus` requires `--fuzz-corpus-dir` or `fuzz.corpus_dir`; \
3799 skipping symbolic corpus seeding"
3800 );
3801 return;
3802 }
3803
3804 let mut symbolic = SymbolicExecutor::new(self.config.symbolic.clone());
3805 let result =
3806 symbolic.run(self.symbolic_run_input(func, self.sender, true, Vec::new(), None));
3807
3808 let input = match result {
3809 SymbolicRunResult::Safe { success_input: Some(input), .. } => input,
3810 SymbolicRunResult::Safe { success_input: None, .. } => {
3811 warn!(test = %func.signature(), "symbolic fuzz corpus seeding found no successful input");
3812 return;
3813 }
3814 SymbolicRunResult::Incomplete { kind, reason, .. } => {
3815 warn!(?kind, %reason, test = %func.signature(), "symbolic fuzz corpus seeding incomplete");
3816 return;
3817 }
3818 SymbolicRunResult::Counterexample { .. } => {
3819 warn!(test = %func.signature(), "symbolic fuzz corpus seeding found a counterexample");
3820 return;
3821 }
3822 };
3823
3824 if self.symbolic_fuzz_seed_replay(self.sender, &input, fuzz_config) != Some(true) {
3825 warn!(test = %func.signature(), "symbolic fuzz corpus seed did not pass concrete replay");
3826 return;
3827 }
3828
3829 if let Err(err) =
3830 self.persist_symbolic_fuzz_seed(&fuzz_config.corpus, self.sender, input.calldata)
3831 {
3832 warn!(%err, test = %func.signature(), "failed to persist symbolic fuzz corpus seed");
3833 }
3834 }
3835
3836 fn persist_symbolic_fuzz_seed(
3838 &self,
3839 corpus: &FuzzCorpusConfig,
3840 sender: Address,
3841 calldata: Bytes,
3842 ) -> foundry_common::fs::Result<Option<PathBuf>> {
3843 persist_corpus_seed(
3844 corpus,
3845 vec![BasicTxDetails {
3846 warp: None,
3847 roll: None,
3848 sender,
3849 call_details: CallDetails {
3850 target: self.address,
3851 calldata,
3852 value: Some(U256::ZERO),
3853 },
3854 }],
3855 )
3856 }
3857
3858 fn symbolic_fuzz_seed_replay(
3860 &self,
3861 sender: Address,
3862 input: &SymbolicConcreteInput,
3863 fuzz_config: &FuzzConfig,
3864 ) -> Option<bool> {
3865 let raw = self
3866 .clone_executor()
3867 .call_raw(sender, self.address, input.calldata.clone(), U256::ZERO)
3868 .ok()?;
3869 if raw.result.as_ref() == MAGIC_ASSUME {
3870 return None;
3871 }
3872 Some(
3873 should_ignore_revert(
3874 fuzz_config.fail_on_revert,
3875 self.address,
3876 raw.reverter,
3877 self.executor.inspector().extra_cheatcode_addresses(),
3878 ) || self.executor.is_raw_call_success(
3879 self.address,
3880 Cow::Borrowed(&raw.state_changeset),
3881 &raw,
3882 false,
3883 ),
3884 )
3885 }
3886
3887 fn run_table_test(mut self, func: &Function) -> TestResult {
3896 if self.prepare_test(func).is_err() {
3898 return self.result;
3899 }
3900
3901 let Some(first_param) = func.inputs.first() else {
3903 self.result.single_fail(Some("Table test should have at least one parameter".into()));
3904 return self.result;
3905 };
3906
3907 let Some(first_param_fixtures) =
3908 &self.setup.fuzz_fixtures.param_fixtures(first_param.name())
3909 else {
3910 self.result.single_fail(Some("Table test should have fixtures defined".into()));
3911 return self.result;
3912 };
3913
3914 if first_param_fixtures.is_empty() {
3915 self.result.single_fail(Some("Table test should have at least one fixture".into()));
3916 return self.result;
3917 }
3918
3919 let fixtures_len = first_param_fixtures.len();
3920 let mut table_fixtures = vec![&first_param_fixtures[..]];
3921
3922 for param in &func.inputs[1..] {
3924 let param_name = param.name();
3925 let Some(fixtures) = &self.setup.fuzz_fixtures.param_fixtures(param.name()) else {
3926 self.result.single_fail(Some(format!("No fixture defined for param {param_name}")));
3927 return self.result;
3928 };
3929
3930 if fixtures.len() != fixtures_len {
3931 self.result.single_fail(Some(format!(
3932 "{} fixtures defined for {param_name} (expected {})",
3933 fixtures.len(),
3934 fixtures_len
3935 )));
3936 return self.result;
3937 }
3938
3939 table_fixtures.push(&fixtures[..]);
3940 }
3941
3942 let progress = self.fuzz_progress(&func.name, None, fixtures_len as u32);
3943
3944 let mut result = FuzzTestResult::default();
3945
3946 for i in 0..fixtures_len {
3947 if self.tcfg.early_exit.should_stop() {
3948 return self.result;
3949 }
3950
3951 if let Some(progress) = progress.as_ref() {
3953 progress.inc(1);
3954 }
3955
3956 let args = table_fixtures.iter().map(|row| row[i].clone()).collect_vec();
3957 let Ok((mut raw_call_result, reason)) = self.call_test(func, &args) else {
3958 return self.result;
3959 };
3960
3961 result.gas_by_case.push((raw_call_result.gas_used, raw_call_result.stipend));
3962 result.logs.extend(raw_call_result.logs.clone());
3963 result.labels.extend(raw_call_result.labels.clone());
3964 HitMaps::merge_opt(&mut result.line_coverage, raw_call_result.line_coverage.clone());
3965
3966 let is_success =
3967 self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, false);
3968 if !is_success {
3970 result.counterexample =
3971 Some(CounterExample::Single(BaseCounterExample::from_fuzz_call(
3972 Bytes::from(func.abi_encode_input(&args).unwrap()),
3973 args,
3974 raw_call_result.traces.clone(),
3975 )));
3976 result.reason = reason;
3977 }
3978 if !is_success || i == fixtures_len - 1 {
3981 result.success = is_success;
3982 result.traces = raw_call_result.traces;
3983 result.debug_bytecodes = raw_call_result.debug_bytecodes;
3984 self.result.table_result(result);
3985 return self.result;
3986 }
3987 }
3988
3989 self.result
3990 }
3991
3992 fn run_invariant_test(
3993 mut self,
3994 func: &Function,
3995 invariants: Vec<(&Function, bool)>,
3996 shared_invariant_namespace: bool,
3997 call_after_invariant: bool,
3998 identified_contracts: &ContractsByAddress,
3999 ) -> TestResult {
4000 let fuzz_failure_replay = self.cr.mcr.tcfg.fuzz_failure_replay;
4001 let mut invariant_config = self.config.invariant.clone();
4002 if fuzz_failure_replay {
4003 invariant_config.runs = 0;
4004 }
4005 let invariant_config = &invariant_config;
4006 let is_optimization = is_optimization_invariant(func);
4007 let isolated_campaign =
4008 (is_optimization || !shared_invariant_namespace).then_some(func.name.as_str());
4009
4010 let mut live_invariants = Vec::new();
4011 let mut skipped_predicate_results = Vec::new();
4012 for (invariant, fail_on_revert) in invariants {
4013 if let Some(reason) = self.invariant_skip_reason(invariant) {
4014 skipped_predicate_results.push(InvariantPredicateResult {
4015 name: invariant.name.clone(),
4016 status: TestStatus::Skipped,
4017 reason: reason.0,
4018 });
4019 } else {
4020 live_invariants.push((invariant, fail_on_revert));
4021 }
4022 }
4023
4024 if live_invariants.is_empty() {
4025 let skip_reason = skipped_predicate_results
4026 .iter()
4027 .find(|predicate| predicate.name == func.name)
4028 .and_then(|predicate| predicate.reason.clone());
4029 self.result
4030 .invariant_skip_with_predicates(SkipReason(skip_reason), skipped_predicate_results);
4031 return self.result;
4032 }
4033 let anchor_idx =
4036 live_invariants.iter().position(|(invariant, _)| *invariant == func).unwrap_or(0);
4037
4038 let mut executor = self.clone_executor();
4039 executor.inspector_mut().collect_edge_coverage_with_config(&invariant_config.corpus);
4042 executor
4043 .inspector_mut()
4044 .collect_sancov_edges(invariant_config.corpus.collect_sancov_edges());
4045 executor
4046 .inspector_mut()
4047 .collect_sancov_trace_cmp(invariant_config.corpus.collect_sancov_trace_cmp());
4048 let mut config = invariant_config.clone();
4049 if config.call_override && config.corpus.capture_branch_frontiers() {
4050 let _ = sh_warn!(
4051 "Invariant frontier capture does not support `invariant.call_override`; running \
4052 the campaign without writing frontier artifacts."
4053 );
4054 config.corpus.frontier_dir = None;
4055 }
4056 let execution_profile = self.tcfg.evm_opts.networks.execution_profile_name();
4057 let execution_pass = if self.cr.mcr.tcfg.multi_network.all_override_networks.is_empty() {
4058 "single"
4059 } else if self.cr.mcr.tcfg.multi_network.pass_network.is_some() {
4060 "override"
4061 } else {
4062 "default"
4063 };
4064 let failure_dir = invariant_suite_paths(
4065 &mut config.corpus,
4066 invariant_config.failure_persist_dir.clone().unwrap(),
4067 self.cr.name,
4068 isolated_campaign,
4069 execution_profile,
4070 execution_pass,
4071 );
4072 let resolved_corpus_dir = config.corpus.corpus_dir.clone();
4074
4075 let mut evm = InvariantExecutor::new_with_fuzz_seed(
4076 executor,
4077 self.invariant_runner(),
4078 self.config.fuzz.seed,
4079 config,
4080 identified_contracts,
4081 &self.cr.mcr.known_contracts,
4082 self.cr.num_invariant_campaign_anchors,
4083 );
4084
4085 let predicate_count = live_invariants.len() + skipped_predicate_results.len();
4086 let invariant_contract = InvariantContract::new(
4087 self.address,
4088 self.cr.name,
4089 live_invariants,
4090 anchor_idx,
4091 call_after_invariant,
4092 &self.cr.contract.abi,
4093 );
4094 let anchor = invariant_contract.anchor();
4095 let is_campaign = predicate_count > 1;
4096 let invariant_count = is_campaign.then_some(predicate_count);
4097 let invariant_display_name = if is_campaign {
4098 Cow::Owned(invariant_campaign_display_name(self.cr.name))
4099 } else {
4100 Cow::Borrowed(func.name.as_str())
4101 };
4102
4103 if let Err(e) = evm.select_contract_artifacts(self.address) {
4106 self.result.invariant_setup_fail(e);
4107 return self.result;
4108 }
4109 let (sender_filters, targeted) = match evm.select_contracts_and_senders(self.address) {
4110 Ok(selected) => selected,
4111 Err(e) => {
4112 self.result.invariant_setup_fail(e);
4113 return self.result;
4114 }
4115 };
4116 let current_settings = InvariantSettings::new(
4117 &targeted.targets(),
4118 &sender_filters,
4119 invariant_config.fail_on_revert,
4120 );
4121
4122 let showmap = self.cr.mcr.tcfg.showmap.as_ref();
4123 let minimize = self.cr.mcr.tcfg.fuzz_minimize.as_ref();
4124 if showmap.is_some() || minimize.is_some() {
4125 let dynamic = evm.dynamic_target_ctx();
4126 let replay_target = ShowmapReplayTarget {
4127 stateless: None,
4128 fuzz_fail_on_revert: false,
4129 fuzzed_contracts: Some(&targeted),
4130 invariant_address: Some(self.address),
4131 invariant_fns: &invariant_contract.invariant_fns,
4132 invariant_replay: InvariantReplayOptions {
4133 check_interval: invariant_config.check_interval,
4134 call_after_invariant,
4135 is_optimization,
4136 },
4137 dynamic: Some(&dynamic),
4138 };
4139 if let Some(showmap) = showmap {
4142 let corpus_dir = showmap
4143 .corpus_dir
4144 .clone()
4145 .map(|corpus_dir| {
4146 let target_dir =
4147 invariant_corpus_dir(&corpus_dir, self.cr.name, isolated_campaign);
4148 narrow_generated_corpus_root(corpus_dir, target_dir)
4149 })
4150 .or(resolved_corpus_dir);
4151 return self.run_showmap(func, &func.name, corpus_dir, showmap, replay_target);
4152 }
4153 if let Some(minimize) = minimize {
4154 let target = self.fuzz_minimize_target_id(&invariant_display_name);
4155 replay_fuzz_minimize(
4156 &mut self.result,
4157 minimize,
4158 target,
4159 &evm.executor,
4160 &invariant_config.corpus,
4161 replay_target,
4162 );
4163 return self.result;
4164 }
4165 }
4166
4167 let progress = self.fuzz_progress(
4168 &invariant_display_name,
4169 invariant_config.timeout,
4170 invariant_config.runs,
4171 );
4172 let primary_failure_file = invariant_failure_file(&failure_dir, anchor);
4173
4174 let mut replayed_persisted_invariant = false;
4178 let mut replayed_secondary_failures = Vec::new();
4179 let replay_candidates = invariant_contract
4180 .invariant_fns
4181 .iter()
4182 .copied()
4183 .sorted_by_key(|(invariant, _)| (*invariant == anchor) == fuzz_failure_replay)
4184 .collect::<Vec<_>>();
4185 for (replay_invariant, fail_on_revert) in replay_candidates {
4186 let Some(InvariantPersistedFailure {
4187 mut call_sequence,
4188 assertion_failure,
4189 storage,
4190 failure_site,
4191 ..
4192 }) = persisted_invariant_failure(&failure_dir, replay_invariant, ¤t_settings)
4193 else {
4194 continue;
4195 };
4196 replayed_persisted_invariant = true;
4197 let replay_anchor_idx = invariant_contract
4198 .invariant_fns
4199 .iter()
4200 .position(|(invariant, _)| *invariant == replay_invariant)
4201 .expect("replay anchor must be present in invariant_fns");
4202 let replay_contract = InvariantContract::new(
4203 self.address,
4204 self.cr.name,
4205 invariant_contract.invariant_fns.clone(),
4206 replay_anchor_idx,
4207 call_after_invariant,
4208 &self.cr.contract.abi,
4209 );
4210 let Ok((txes, mut replay)) = self.replay_persisted_call_sequence(
4211 &replay_contract,
4212 &mut call_sequence,
4213 assertion_failure,
4214 &storage,
4215 ) else {
4216 continue;
4217 };
4218 if replay.success {
4219 continue;
4220 }
4221 let Some(confirmed_failure_site) =
4222 replay.failure_site.map(SymbolicInvariantFailureSite::from)
4223 else {
4224 continue;
4225 };
4226 if failure_site.is_some_and(|expected| expected != confirmed_failure_site) {
4227 continue;
4228 }
4229 if replay_invariant != anchor && !fuzz_failure_replay {
4230 let is_revert = match confirmed_failure_site {
4231 SymbolicInvariantFailureSite::Invariant { selector, .. }
4232 if selector == replay_invariant.selector() =>
4233 {
4234 false
4235 }
4236 SymbolicInvariantFailureSite::SequenceCall { .. }
4237 if fail_on_revert && !replay.sequence_assertion_failure =>
4238 {
4239 true
4240 }
4241 _ => continue,
4242 };
4243 replayed_secondary_failures.push((
4244 replay_invariant.name.clone(),
4245 InvariantFuzzError::from_replayed_invariant(
4246 self.address,
4247 replay_invariant,
4248 txes,
4249 replay.reason,
4250 invariant_config,
4251 fail_on_revert,
4252 assertion_failure,
4253 is_revert,
4254 ),
4255 storage,
4256 confirmed_failure_site,
4257 ));
4258 continue;
4259 }
4260 let warn = "Replayed invariant failure from persisted file. \nRun `forge clean` or remove file to ignore failure and to continue invariant test campaign.";
4261 if let Some(progress) = &progress {
4262 progress.set_prefix(format!("{invariant_display_name}\n{warn}\n"));
4263 } else {
4264 let _ = sh_warn!("{warn}");
4265 }
4266
4267 let trace_executor = match self.clone_executor_with_symbolic_storage(&storage) {
4270 Ok(executor) => executor,
4271 Err(err) => {
4272 error!(%err, "Failed to apply symbolic storage for invariant error replay");
4273 self.result.single_fail(Some(err.to_string()));
4274 return self.result;
4275 }
4276 };
4277 match self.replay_error(
4278 invariant_config.clone(),
4279 trace_executor,
4280 &txes,
4281 None,
4282 assertion_failure,
4283 None,
4284 &replay_contract,
4285 replay_invariant,
4286 identified_contracts,
4287 progress.as_ref(),
4288 None,
4289 ) {
4290 Ok(ReplayErrorResult {
4291 counterexample_sequence: sequence, check_result, ..
4292 }) if !sequence.is_empty() => {
4293 call_sequence = sequence;
4294 if let Some(updated) = check_result {
4295 if updated.failure_site.map(SymbolicInvariantFailureSite::from)
4296 != Some(confirmed_failure_site)
4297 {
4298 continue;
4299 }
4300 replay = updated;
4301 }
4302 record_invariant_failure(
4303 &invariant_failure_file(&failure_dir, replay_invariant),
4304 &call_sequence,
4305 ¤t_settings,
4306 assertion_failure,
4307 &storage,
4308 Some(confirmed_failure_site),
4309 );
4310 }
4311 Ok(_) => {}
4312 Err(err) => {
4313 error!(%err, "Failed to replay invariant error");
4314 }
4315 }
4316
4317 self.result.invariant_replay_fail(
4318 replay,
4319 &replay_invariant.name,
4320 None,
4321 call_sequence.clone(),
4322 );
4323 let signature = replay_invariant.signature();
4324 if let Some(artifact) = self.persist_sequence_artifact(
4325 &signature,
4326 &format!("{signature}-replay"),
4327 self.sequence_calls(&call_sequence),
4328 self.config.invariant.fail_on_revert,
4329 &storage,
4330 Some(SymbolicInvariantArtifactFailure::Predicate {
4331 name: replay_invariant.name.clone(),
4332 site: Some(confirmed_failure_site),
4333 }),
4334 ) {
4335 self.result.add_counterexample_artifact(artifact);
4336 }
4337 return self.result;
4338 }
4339
4340 let (mut persisted_handler_failures, mut symbolic_handler_storage) = self
4343 .replay_persisted_handler_failures(&failure_dir.join("handlers"), ¤t_settings);
4344
4345 if fuzz_failure_replay && persisted_handler_failures.is_empty() {
4349 let reason = if replayed_persisted_invariant {
4350 "no persisted invariant failure reproduced for selected invariants".to_string()
4351 } else {
4352 format!("no persisted invariant failure reproduced for {}", anchor.name)
4353 };
4354 self.result.single_skip(SkipReason(Some(reason)));
4355 return self.result;
4356 }
4357
4358 if self.config.symbolic.use_fuzz_frontiers {
4359 let dynamic_target_ctx = evm.dynamic_target_ctx();
4360 let invariant_config = evm.config();
4361 self.try_seed_invariant_corpus_from_frontiers(
4362 &invariant_contract,
4363 &invariant_config,
4364 &sender_filters,
4365 &targeted,
4366 &dynamic_target_ctx,
4367 );
4368 }
4369
4370 if self.config.symbolic.enabled && !is_optimization {
4371 let anchor_fail_on_revert = invariant_contract.invariant_fns[anchor_idx].1;
4372 let after_invariant = call_after_invariant
4373 .then(|| {
4374 self.cr
4375 .contract
4376 .abi
4377 .functions()
4378 .find(|func| func.name == "afterInvariant" && func.inputs.is_empty())
4379 })
4380 .flatten();
4381 let symbolic_targets = targeted
4382 .targets()
4383 .iter()
4384 .flat_map(|(address, contract)| {
4385 let contract_name = Some(contract.identifier.clone());
4386 contract.abi_fuzzed_functions().map(move |function| SymbolicInvariantTarget {
4387 address: *address,
4388 contract_name: contract_name.clone(),
4389 function: function.clone(),
4390 })
4391 })
4392 .collect::<Vec<_>>();
4393 let unsupported_domain_reason = symbolic_invariant_unsupported_domain_reason(
4394 invariant_config,
4395 &sender_filters,
4396 &targeted,
4397 &symbolic_targets,
4398 );
4399
4400 let mut symbolic_invariant_config = invariant_config.clone();
4401 symbolic_invariant_config.fail_on_revert = anchor_fail_on_revert;
4402 let symbolic_config = self.config.symbolic.clone();
4403 let incomplete = |kind, reason: &str, stats, replay| {
4404 SymbolicResult::incomplete(
4405 &symbolic_config,
4406 kind,
4407 reason,
4408 stats,
4409 replay,
4410 SymbolicCallTrace::none(),
4411 None,
4412 )
4413 };
4414 let mut symbolic = SymbolicExecutor::new(symbolic_config.clone());
4415 match symbolic.run_invariant(SymbolicInvariantRunInput {
4416 executor: &evm.executor,
4417 invariant_address: self.address,
4418 sender: self.sender,
4419 invariant: anchor,
4420 after_invariant,
4421 targets: symbolic_targets,
4422 senders: sender_filters.targeted,
4423 excluded_senders: sender_filters.excluded,
4424 depth: symbolic_config.invariant_depth as usize,
4425 check_interval: invariant_config.check_interval,
4426 fail_on_revert: anchor_fail_on_revert,
4427 ffi_enabled: self.config.ffi,
4428 }) {
4429 SymbolicInvariantRunResult::Safe(stats) => {
4430 self.result.record_symbolic(match unsupported_domain_reason {
4431 Some(reason) => incomplete(
4432 SymbolicStopReason::Stuck,
4433 reason,
4434 stats,
4435 SymbolicReplayMetadata::not_required(),
4436 ),
4437 None => SymbolicResult::pass(&symbolic_config, stats),
4438 });
4439 }
4440 SymbolicInvariantRunResult::Incomplete { kind, reason, stats } => {
4441 self.result.record_symbolic(incomplete(
4442 kind,
4443 &reason,
4444 stats,
4445 SymbolicReplayMetadata::not_required(),
4446 ));
4447 }
4448 SymbolicInvariantRunResult::Counterexample {
4449 kind,
4450 sequence,
4451 storage,
4452 stats,
4453 } => 'counterexample: {
4454 let is_handler = matches!(kind, SymbolicInvariantCounterexampleKind::Handler);
4455 let symbolic_calls = symbolic_invariant_counterexample_calls(
4456 &sequence,
4457 identified_contracts,
4458 invariant_config.show_solidity,
4459 );
4460 let check = SequenceReplay {
4461 invariant_config: &symbolic_invariant_config,
4462 invariant_contract: &invariant_contract,
4463 target_invariant: anchor,
4464 assertion_failure: false,
4465 storage: &storage,
4466 };
4467 let replayed = self
4468 .symbolic_sequence_failure(check, &symbolic_calls)
4469 .ok_or("symbolic invariant counterexample did not replay")
4470 .and_then(|failure| {
4471 let handler_site = match failure.failure_site {
4472 Some(CheckSequenceFailureSite::SequenceCall {
4473 target,
4474 selector,
4475 fingerprint,
4476 }) if is_handler => Some((target, selector, fingerprint)),
4477 _ => None,
4478 };
4479 if is_handler && handler_site.is_none() {
4480 return Err("symbolic handler counterexample replayed at a \
4481 non-handler failure site");
4482 }
4483 Ok((failure, handler_site))
4484 });
4485 let (failure, handler_site) = match replayed {
4486 Ok(replayed) => replayed,
4487 Err(reason) => {
4488 self.result.record_symbolic(incomplete(
4489 SymbolicStopReason::Error,
4490 reason,
4491 stats,
4492 SymbolicReplayMetadata::mismatch(reason.to_string()),
4493 ));
4494 break 'counterexample;
4495 }
4496 };
4497
4498 let txes = symbolic_calls
4499 .iter()
4500 .map(SymbolicCounterexampleCall::to_basic_tx_details)
4501 .collect::<Vec<_>>();
4502 let original_sequence_len = txes.len();
4503 let failure_site = failure.failure_site.map(SymbolicInvariantFailureSite::from);
4504 let (artifact_file_name, artifact_failure) = match handler_site {
4505 Some((reverter, selector, fingerprint)) => (
4506 format!("handler-{reverter}-{selector}"),
4507 SymbolicInvariantArtifactFailure::Handler {
4508 name: Some(invariant_handler_failure_name(
4509 identified_contracts,
4510 reverter,
4511 selector,
4512 )),
4513 reverter,
4514 selector,
4515 fingerprint,
4516 },
4517 ),
4518 None => (
4519 anchor.signature(),
4520 SymbolicInvariantArtifactFailure::Predicate {
4521 name: anchor.name.clone(),
4522 site: failure_site,
4523 },
4524 ),
4525 };
4526 let replayed = match self.replay_invariant_error_sequence(
4527 SequenceReplay { assertion_failure: is_handler, ..check },
4528 &txes,
4529 None,
4530 identified_contracts,
4531 ¤t_settings,
4532 SequenceArtifactSpec {
4533 file_name: &artifact_file_name,
4534 fail_on_revert: is_handler || anchor_fail_on_revert,
4535 failure: Some(artifact_failure),
4536 },
4537 progress.as_ref(),
4538 Some((1, 1)),
4539 ) {
4540 Ok(replayed) => replayed,
4541 Err(err) => {
4542 let reason = format!("symbolic invariant replay failed: {err}");
4543 self.result.record_symbolic(incomplete(
4544 SymbolicStopReason::Error,
4545 &reason,
4546 stats,
4547 SymbolicReplayMetadata::error(reason.clone()),
4548 ));
4549 break 'counterexample;
4550 }
4551 };
4552 let ReplayedInvariantSequence {
4553 call_sequence,
4554 artifact,
4555 minimization,
4556 fork_block_number,
4557 } = replayed;
4558 let mut symbolic_result = SymbolicResult::fail_counterexample_sequence(
4559 &symbolic_config,
4560 stats,
4561 SymbolicCallTrace::test_result_traces(!self.result.traces.is_empty()),
4562 );
4563 if let Some(artifact) = artifact.clone() {
4564 symbolic_result = symbolic_result.with_artifact(artifact);
4565 }
4566 if let Some(minimization) = minimization.clone() {
4567 symbolic_result = symbolic_result.with_minimization(minimization);
4568 }
4569 let reason = failure.reason.unwrap_or_else(|| {
4570 if is_handler {
4571 "symbolic handler counterexample".to_string()
4572 } else {
4573 "symbolic invariant counterexample".to_string()
4574 }
4575 });
4576
4577 if let Some((reverter, selector, fingerprint)) = handler_site {
4578 let call_sequence =
4579 call_sequence.iter().map(base_counterexample_to_tx).collect::<Vec<_>>();
4580 symbolic_handler_storage.insert(
4581 (reverter, selector, fingerprint),
4582 SymbolicHandlerReplayStorage {
4583 call_sequence: call_sequence.clone(),
4584 assignments: storage,
4585 },
4586 );
4587 persisted_handler_failures.insert(
4588 (reverter, selector),
4589 InvariantFuzzError::HandlerAssertion(HandlerAssertionFailure {
4590 reverter,
4591 selector,
4592 call_sequence,
4593 original_sequence_len,
4594 revert_reason: reason,
4595 fork_block_number: None,
4596 edge_fingerprint: fingerprint,
4597 }),
4598 );
4599 self.result.record_symbolic(symbolic_result);
4600 break 'counterexample;
4601 }
4602
4603 record_invariant_failure(
4604 &primary_failure_file,
4605 &call_sequence,
4606 ¤t_settings,
4607 false,
4608 &storage,
4609 failure_site,
4610 );
4611 let mut invariant_failures = vec![InvariantFailure::Predicate {
4612 name: anchor.name.clone(),
4613 reason,
4614 counterexample: Some(CounterExample::Sequence(
4615 original_sequence_len,
4616 call_sequence,
4617 )),
4618 artifact,
4619 minimization,
4620 persisted_path: primary_failure_file,
4621 is_anchor: true,
4622 }];
4623 for (invariant, _) in &invariant_contract.invariant_fns {
4624 if let Some((_, error, _, _)) = replayed_secondary_failures
4625 .iter()
4626 .find(|(name, ..)| name == &invariant.name)
4627 && let Some(calls) = failed_invariant_calls(error)
4628 {
4629 invariant_failures.push(InvariantFailure::Predicate {
4630 name: invariant.name.clone(),
4631 reason: error.revert_reason().unwrap_or_default(),
4632 counterexample: Some(CounterExample::Sequence(
4633 calls.len(),
4634 base_counterexamples(
4635 calls,
4636 identified_contracts,
4637 invariant_config.show_solidity,
4638 ),
4639 )),
4640 artifact: None,
4641 minimization: None,
4642 persisted_path: invariant_failure_file(&failure_dir, invariant),
4643 is_anchor: false,
4644 });
4645 }
4646 }
4647 let invariant_predicate_results = if is_campaign {
4648 self.sort_predicate_results(
4649 invariant_failures
4650 .iter()
4651 .map(|failure| InvariantPredicateResult {
4652 name: failure.name().to_string(),
4653 status: TestStatus::Failure,
4654 reason: Some(failure.reason().to_string()),
4655 })
4656 .chain(skipped_predicate_results),
4657 )
4658 } else {
4659 Vec::new()
4660 };
4661 self.result.invariant_result(
4662 invariant_kind(1, failure.calls_count, failure.reverts),
4663 InvariantOutcome {
4664 fork_block_number,
4665 failures: invariant_failures,
4666 predicate_results: invariant_predicate_results,
4667 failure_dir: Some(failure_dir),
4668 invariant_count,
4669 ..Default::default()
4670 },
4671 );
4672 self.result.record_symbolic(symbolic_result);
4673 return self.result;
4674 }
4675 }
4676 }
4677
4678 let mut invariant_result = match evm.invariant_fuzz(
4679 invariant_contract.clone(),
4680 &self.setup.fuzz_fixtures,
4681 self.build_fuzz_state(true, None),
4682 progress.as_ref(),
4683 &self.tcfg.early_exit,
4684 persisted_handler_failures,
4685 ) {
4686 Ok(x) => x,
4687 Err(e) => {
4688 self.result.invariant_setup_fail(e);
4689 return self.result;
4690 }
4691 };
4692 let mut replayed_secondary_metadata = BTreeMap::new();
4693 for (name, failure, storage, failure_site) in replayed_secondary_failures {
4694 if let Entry::Vacant(entry) = invariant_result.errors.entry(name) {
4695 replayed_secondary_metadata.insert(entry.key().clone(), (storage, failure_site));
4696 entry.insert(failure);
4697 }
4698 }
4699 self.result.merge_coverages(invariant_result.line_coverage);
4701
4702 let mut counterexample = None;
4703 let success =
4705 invariant_result.errors.is_empty() && invariant_result.handler_errors.is_empty();
4706 let single_failure =
4707 invariant_result.errors.len() + invariant_result.handler_errors.len() == 1;
4708 let mut fork_block_number = invariant_result.fork_block_number;
4709 let mut invariant_failures = Vec::new();
4710 let mut any_failure_persisted = false;
4711
4712 if success {
4713 if let Some(best_value) = invariant_result.optimization_best_value {
4714 match self.replay_error(
4716 invariant_config.clone(),
4717 self.clone_executor(),
4718 &invariant_result.optimization_best_sequence,
4719 None,
4720 false,
4721 Some(best_value),
4722 &invariant_contract,
4723 anchor,
4724 identified_contracts,
4725 progress.as_ref(),
4726 None,
4727 ) {
4728 Ok(ReplayErrorResult { counterexample_sequence: sequence, .. })
4729 if !sequence.is_empty() =>
4730 {
4731 counterexample = Some(CounterExample::Sequence(
4732 invariant_result.optimization_best_sequence.len(),
4733 sequence,
4734 ));
4735 }
4736 Err(err) => {
4737 error!(%err, "Failed to replay optimization best sequence");
4738 }
4739 _ => {}
4740 }
4741 } else if let Err(err) = replay_run(
4742 &invariant_contract,
4744 anchor,
4745 self.clone_executor(),
4746 &self.cr.mcr.known_contracts,
4747 identified_contracts.clone(),
4748 &mut self.result.logs,
4749 &mut self.result.traces,
4750 &mut self.result.debug_bytecodes,
4751 &mut self.result.line_coverage,
4752 &mut self.result.deprecated_cheatcodes,
4753 &invariant_result.last_run_inputs,
4754 invariant_config.show_solidity,
4755 ) {
4756 error!(%err, "Failed to replay last invariant run");
4757 }
4758 } else {
4759 let total_broken = invariant_result.errors.len();
4762 let mut next_position = 2usize;
4765 let order = std::iter::once(anchor_idx).chain(
4766 (0..invariant_contract.invariant_fns.len()).filter(|idx| *idx != anchor_idx),
4767 );
4768 for idx in order {
4769 let is_anchor = idx == anchor_idx;
4770 let invariant = invariant_contract.invariant_fns[idx].0;
4771 let Some(error) = invariant_result.errors.get(&invariant.name) else {
4772 continue;
4773 };
4774 let persisted_path = invariant_failure_file(&failure_dir, invariant);
4775 let (case_data, calls) = match error {
4776 InvariantFuzzError::BrokenInvariant(case_data)
4777 | InvariantFuzzError::Revert(case_data) => {
4778 (case_data, failed_invariant_calls(error).unwrap_or_default())
4779 }
4780 _ if is_anchor => {
4783 invariant_failures.push(InvariantFailure::Predicate {
4784 name: invariant.name.clone(),
4785 reason: error.revert_reason().unwrap_or_default(),
4786 counterexample: None,
4787 artifact: None,
4788 minimization: None,
4789 persisted_path,
4790 is_anchor,
4791 });
4792 continue;
4793 }
4794 _ => continue,
4795 };
4796 let replayed_metadata = replayed_secondary_metadata.get(&invariant.name);
4797
4798 let replayed = if !is_anchor && self.tcfg.early_exit.should_stop() {
4803 if replayed_metadata.is_none() {
4804 record_invariant_failure(
4805 &persisted_path,
4806 &base_counterexamples(
4807 calls,
4808 identified_contracts,
4809 invariant_config.show_solidity,
4810 ),
4811 ¤t_settings,
4812 case_data.assertion_failure,
4813 &[],
4814 None,
4815 );
4816 }
4817 any_failure_persisted = true;
4818 None
4819 } else {
4820 let position = if is_anchor {
4821 1
4822 } else {
4823 next_position += 1;
4824 next_position - 1
4825 };
4826 let mut replay_config = invariant_config.clone();
4827 if replayed_metadata.is_some() {
4828 replay_config.shrink_run_limit = 0;
4833 }
4834 let (storage, artifact_failure) = match replayed_metadata {
4835 Some((storage, site)) => (
4836 storage.as_slice(),
4837 Some(SymbolicInvariantArtifactFailure::Predicate {
4838 name: invariant.name.clone(),
4839 site: Some(*site),
4840 }),
4841 ),
4842 None => (&[][..], None),
4843 };
4844 let signature = invariant.signature();
4845 match self.replay_invariant_error_sequence(
4846 SequenceReplay {
4847 invariant_config: &replay_config,
4848 invariant_contract: &invariant_contract,
4849 target_invariant: invariant,
4850 assertion_failure: case_data.assertion_failure,
4851 storage,
4852 },
4853 calls,
4854 Some(case_data.inner_sequence.clone()),
4855 identified_contracts,
4856 ¤t_settings,
4857 SequenceArtifactSpec {
4858 file_name: &signature,
4859 fail_on_revert: self.config.invariant.fail_on_revert,
4860 failure: artifact_failure,
4861 },
4862 progress.as_ref(),
4863 Some((position, total_broken)),
4864 ) {
4865 Ok(replayed) if !replayed.call_sequence.is_empty() => {
4866 if single_failure {
4867 fork_block_number =
4868 replayed.fork_block_number.or(fork_block_number);
4869 }
4870 if replayed_metadata.is_none() {
4873 record_invariant_failure(
4874 &persisted_path,
4875 &replayed.call_sequence,
4876 ¤t_settings,
4877 case_data.assertion_failure,
4878 &[],
4879 None,
4880 );
4881 }
4882 any_failure_persisted = true;
4883 Some(replayed)
4884 }
4885 Ok(_) => None,
4886 Err(err) => {
4887 error!(%err, "Failed to replay invariant error");
4888 None
4889 }
4890 }
4891 };
4892 let (counterexample, artifact, minimization) = match replayed {
4893 Some(replayed) => (
4894 Some(CounterExample::Sequence(calls.len(), replayed.call_sequence)),
4895 replayed.artifact,
4896 replayed.minimization,
4897 ),
4898 None => (None, None, None),
4899 };
4900 invariant_failures.push(InvariantFailure::Predicate {
4901 name: invariant.name.clone(),
4902 reason: error.revert_reason().unwrap_or_default(),
4903 counterexample,
4904 artifact,
4905 minimization,
4906 persisted_path,
4907 is_anchor,
4908 });
4909 }
4910 }
4911
4912 let invariant_failure_dir = any_failure_persisted.then(|| failure_dir.clone());
4913 let invariant_predicate_results = if is_campaign {
4914 let failures_by_name = invariant_failures
4915 .iter()
4916 .map(|failure| (failure.name(), failure))
4917 .collect::<BTreeMap<_, _>>();
4918 self.sort_predicate_results(
4919 invariant_contract
4920 .invariant_fns
4921 .iter()
4922 .map(|(invariant, _)| {
4923 let failure = failures_by_name.get(invariant.name.as_str());
4924 InvariantPredicateResult {
4925 name: invariant.name.clone(),
4926 status: if failure.is_some() {
4927 TestStatus::Failure
4928 } else {
4929 TestStatus::Success
4930 },
4931 reason: failure.map(|failure| failure.reason().to_string()),
4932 }
4933 })
4934 .chain(skipped_predicate_results),
4935 )
4936 } else {
4937 Vec::new()
4938 };
4939
4940 let invariant_handler_failures = invariant_result
4946 .handler_errors
4947 .iter()
4948 .sorted_by(|(a, _), (b, _)| a.cmp(b))
4950 .filter_map(|(_, err)| err.as_handler_assertion())
4951 .map(|failure| {
4952 let (reverter, selector) = (failure.reverter, failure.selector);
4953 let name = invariant_handler_failure_name(identified_contracts, reverter, selector);
4954 let symbolic_storage = symbolic_handler_storage
4955 .get(&(reverter, selector, failure.edge_fingerprint))
4956 .filter(|storage| storage.call_sequence == failure.call_sequence)
4957 .map_or(&[][..], |storage| &storage.assignments);
4958 let calls = base_counterexamples(
4959 &failure.call_sequence,
4960 identified_contracts,
4961 invariant_config.show_solidity,
4962 );
4963
4964 if !calls.is_empty() {
4966 record_handler_failure(
4967 &failure_dir,
4968 reverter,
4969 selector,
4970 failure.edge_fingerprint,
4971 &calls,
4972 ¤t_settings,
4973 symbolic_storage,
4974 );
4975 }
4976 let artifact = self.persist_sequence_artifact(
4977 &anchor.signature(),
4978 &format!("handler-{reverter}-{selector}"),
4979 self.sequence_calls(&calls),
4980 true,
4981 symbolic_storage,
4982 Some(SymbolicInvariantArtifactFailure::Handler {
4983 name: Some(name.clone()),
4984 reverter,
4985 selector,
4986 fingerprint: failure.edge_fingerprint,
4987 }),
4988 );
4989 let counterexample = (!calls.is_empty())
4991 .then(|| CounterExample::Sequence(failure.original_sequence_len, calls));
4992
4993 InvariantFailure::Handler {
4994 name,
4995 reverter,
4996 selector,
4997 reason: failure.revert_reason.clone(),
4998 counterexample,
4999 artifact,
5000 }
5001 })
5002 .collect::<Vec<_>>();
5003
5004 self.result.invariant_result(
5005 TestKind::Invariant {
5006 runs: invariant_result.runs,
5007 calls: invariant_result.calls,
5008 reverts: invariant_result.reverts,
5009 workers: invariant_result.workers.max(1),
5010 metrics: invariant_result.metrics,
5011 failed_corpus_replays: invariant_result.failed_corpus_replays,
5012 optimization_best_value: invariant_result.optimization_best_value,
5013 },
5014 InvariantOutcome {
5015 success,
5016 fork_block_number,
5017 failures: invariant_failures,
5018 handler_failures: invariant_handler_failures,
5019 predicate_results: invariant_predicate_results,
5020 failure_dir: invariant_failure_dir,
5021 invariant_count,
5022 counterexample,
5023 gas_report_traces: invariant_result.gas_report_traces,
5024 },
5025 );
5026 self.result
5027 }
5028
5029 fn sort_predicate_results(
5031 &self,
5032 results: impl Iterator<Item = InvariantPredicateResult>,
5033 ) -> Vec<InvariantPredicateResult> {
5034 results
5035 .sorted_by_key(|predicate| {
5036 self.cr
5037 .contract
5038 .abi
5039 .functions()
5040 .position(|func| func.name == predicate.name)
5041 .unwrap_or(usize::MAX)
5042 })
5043 .collect()
5044 }
5045
5046 fn invariant_skip_reason(&self, func: &Function) -> Option<SkipReason> {
5047 match self.executor.call(
5048 self.sender,
5049 self.address,
5050 func,
5051 &[],
5052 U256::ZERO,
5053 Some(self.revert_decoder()),
5054 ) {
5055 Err(EvmError::Skip(reason)) => Some(reason),
5056 _ => None,
5057 }
5058 }
5059
5060 fn run_fuzz_test(mut self, func: &Function) -> TestResult {
5070 if self.prepare_test(func).is_err() {
5072 return self.result;
5073 }
5074
5075 let runner = self.fuzz_runner();
5076 let mut fuzz_config = self.config.fuzz.clone();
5077 let (test_name, legacy_corpus_dir, (failure_dir, failure_file)) =
5078 self.fuzz_test_paths(func, &mut fuzz_config);
5079 let fuzz_input = self.cr.mcr.tcfg.fuzz_input.as_ref();
5080 let is_explicit_target = fuzz_input
5081 .is_some_and(|input| input.contract == self.cr.name && input.test == func.signature());
5082 if is_explicit_target && fuzz_config.run.is_some() {
5083 self.result.fuzz_setup_fail(eyre::eyre!(
5084 "`--fuzz-input-file` cannot be combined with `fuzz.run`"
5085 ));
5086 return self.result;
5087 }
5088
5089 let replay_target = ShowmapReplayTarget {
5090 stateless: Some(StatelessReplayTarget { function: func, address: self.address }),
5091 fuzz_fail_on_revert: fuzz_config.fail_on_revert,
5092 fuzzed_contracts: None,
5093 invariant_address: None,
5094 invariant_fns: &[],
5095 invariant_replay: InvariantReplayOptions::default(),
5096 dynamic: None,
5097 };
5098 if let Some(showmap) = self.cr.mcr.tcfg.showmap.as_ref() {
5101 let corpus_dir = showmap
5102 .corpus_dir
5103 .clone()
5104 .map(|corpus_dir| {
5105 legacy_fuzz_corpus_dir(Some(&corpus_dir), self.cr.name, func, &test_name)
5106 .unwrap_or_else(|| {
5107 let target_dir = corpus_dir
5108 .join(contract_short_name(self.cr.name))
5109 .join(&*test_name);
5110 narrow_generated_corpus_root(corpus_dir, target_dir)
5111 })
5112 })
5113 .or(legacy_corpus_dir)
5114 .or_else(|| fuzz_config.corpus.corpus_dir.clone());
5115 return self.run_showmap(func, &test_name, corpus_dir, showmap, replay_target);
5116 }
5117 if let Some(minimize) = self.cr.mcr.tcfg.fuzz_minimize.as_ref() {
5118 let target = self.fuzz_minimize_target_id(&func.signature());
5119 replay_fuzz_minimize(
5120 &mut self.result,
5121 minimize,
5122 target,
5123 &self.executor,
5124 &fuzz_config.corpus,
5125 replay_target,
5126 );
5127 return self.result;
5128 }
5129
5130 let persisted_failure = if is_explicit_target {
5133 fuzz_input.map(|input| input.failure.as_ref().clone())
5134 } else {
5135 foundry_common::fs::read_json_file::<BaseCounterExample>(&failure_file).ok().or_else(
5136 || {
5137 if test_name == func.name {
5138 return None;
5139 }
5140 let legacy_file = canonicalized(failure_dir.join(&func.name));
5141 let failure =
5142 foundry_common::fs::read_json_file::<BaseCounterExample>(&legacy_file)
5143 .ok()?;
5144 failure
5145 .calldata
5146 .get(..4)
5147 .is_some_and(|selector| func.selector() == selector)
5148 .then_some(failure)
5149 },
5150 )
5151 };
5152 if self.cr.mcr.tcfg.fuzz_failure_replay {
5153 let skip_reason = match &persisted_failure {
5154 None => {
5155 Some(format!("no persisted fuzz failure found at {}", failure_file.display()))
5156 }
5157 Some(failure)
5158 if failure
5159 .calldata
5160 .get(..4)
5161 .is_none_or(|selector| func.selector() != selector) =>
5162 {
5163 Some(format!("persisted fuzz failure selector does not match {}", func.name))
5164 }
5165 Some(_) => None,
5166 };
5167 if let Some(reason) = skip_reason {
5168 self.result.fuzz_result(FuzzTestResult {
5169 skipped: true,
5170 reason: Some(reason),
5171 ..Default::default()
5172 });
5173 return self.result;
5174 }
5175 fuzz_config.corpus.corpus_dir = None;
5176 }
5177
5178 self.try_seed_fuzz_corpus_from_frontiers(func, &fuzz_config);
5179 self.try_seed_fuzz_corpus_symbolically(func, &fuzz_config);
5180
5181 let progress = self.fuzz_progress(
5182 &func.name,
5183 fuzz_config.timeout,
5184 if fuzz_config.run.is_some() { 1 } else { fuzz_config.runs },
5185 );
5186
5187 let state = self.build_fuzz_state(false, Some(func));
5188 let mut executor = self.executor.into_owned();
5189 executor.inspector_mut().collect_edge_coverage_with_config(&fuzz_config.corpus);
5192 executor.inspector_mut().collect_evm_cmp_log(fuzz_config.corpus.collect_evm_cmp_log());
5193 executor.inspector_mut().collect_sancov_edges(fuzz_config.corpus.collect_sancov_edges());
5194 executor
5195 .inspector_mut()
5196 .collect_sancov_trace_cmp(fuzz_config.corpus.collect_sancov_trace_cmp());
5197 let mut fuzzed_executor = FuzzedExecutor::new(
5198 executor,
5199 runner,
5200 self.tcfg.sender,
5201 fuzz_config,
5202 persisted_failure,
5203 legacy_corpus_dir,
5204 );
5205 let result = if self.cr.mcr.tcfg.fuzz_failure_replay {
5206 fuzzed_executor.replay_persisted_failure(
5207 func,
5208 self.address,
5209 &self.cr.mcr.revert_decoder,
5210 )
5211 } else {
5212 fuzzed_executor.fuzz(
5213 func,
5214 &self.setup.fuzz_fixtures,
5215 state,
5216 self.address,
5217 &self.cr.mcr.revert_decoder,
5218 progress.as_ref(),
5219 &self.tcfg.early_exit,
5220 &self.cr.tokio_handle,
5221 )
5222 };
5223 let result = match result {
5224 Ok(result) => result,
5225 Err(e) => {
5226 self.result.fuzz_setup_fail(e);
5227 return self.result;
5228 }
5229 };
5230
5231 if !self.cr.mcr.tcfg.fuzz_failure_replay
5233 && let Some(CounterExample::Single(counterexample)) = &result.counterexample
5234 {
5235 if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) {
5236 error!(%err, "Failed to create fuzz failure dir");
5237 } else if let Err(err) =
5238 foundry_common::fs::write_json_file(&failure_file, counterexample)
5239 {
5240 error!(%err, "Failed to record call sequence");
5241 }
5242 }
5243
5244 self.result.fuzz_result(result);
5245 self.result
5246 }
5247
5248 fn prepare_test(&mut self, func: &Function) -> Result<(), ()> {
5249 let address = self.setup.address;
5250
5251 if self.cr.contract.abi.functions().any(|func| func.name.is_before_test_setup()) {
5253 for calldata in self.executor.call_sol_default(
5254 address,
5255 &ITest::beforeTestSetupCall { testSelector: func.selector() },
5256 ) {
5257 let spec_id: SpecId = self.executor.spec_id().into();
5258 debug!(?calldata, spec=%spec_id, "applying before_test_setup");
5259 let Ok(call_result) = self.executor.to_mut().transact_raw(
5261 self.tcfg.sender,
5262 address,
5263 calldata,
5264 U256::ZERO,
5265 ) else {
5266 self.result.single_fail(None);
5267 return Err(());
5268 };
5269 let reverted = call_result.reverted;
5270 self.result.extend_setup(call_result);
5272 if reverted {
5274 self.result.single_fail(None);
5275 return Err(());
5276 }
5277 }
5278 }
5279 Ok(())
5280 }
5281
5282 fn fuzz_runner(&self) -> TestRunner {
5283 let config = &self.config.fuzz;
5284 fuzzer_with_cases(config.seed, config.runs, config.max_test_rejects)
5285 }
5286
5287 fn run_showmap(
5289 mut self,
5290 func: &Function,
5291 test_name: &str,
5292 corpus_dir: Option<PathBuf>,
5293 showmap: &crate::multi_runner::ShowmapConfig,
5294 target: ShowmapReplayTarget<'_>,
5295 ) -> TestResult {
5296 let Some(corpus_dir) = corpus_dir else {
5297 self.result.replay_skip("no corpus_dir configured for this test");
5298 return self.result;
5299 };
5300
5301 let mut executor = self.clone_executor();
5308 let domain = showmap.domain;
5309 executor.inspector_mut().collect_line_coverage(domain.includes_evm());
5310 executor.inspector_mut().collect_sancov_edges(domain.includes_sancov());
5311
5312 let safe_id = self.cr.name.replace(['/', '\\', ':'], "_");
5319 let safe_fn = test_name.replace(['/', '\\', ':', '(', ')', ',', ' '], "_");
5320 let approach = format!("{}__{safe_id}__{safe_fn}", showmap.approach);
5321 let opts = ShowmapOpts {
5322 out_dir: showmap.out_dir.clone(),
5323 approach,
5324 trial: showmap.trial.clone(),
5325 per_input: showmap.per_input,
5326 domain,
5327 emit_files: showmap.emit_files,
5328 };
5329
5330 let start = std::time::Instant::now();
5331 let result = replay_corpus_to_showmap(&executor, &corpus_dir, target, &opts);
5332 let duration = start.elapsed();
5333 match result {
5334 Ok(stats) => {
5335 if stats.sancov_requested && !stats.sancov_observed && stats.corpus_entries > 0 {
5336 let _ = sh_warn!(
5337 "{}::{}: sancov coverage requested but no hits observed (build is likely not sancov-instrumented)",
5338 self.cr.name,
5339 func.name,
5340 );
5341 }
5342 if stats.unreadable_entries > 0 {
5343 self.result.single_fail(Some(format!(
5344 "failed to read {} corpus entries from {}",
5345 stats.unreadable_entries,
5346 corpus_dir.display()
5347 )));
5348 } else if !showmap.emit_files && stats.corpus_entries == 0 {
5349 self.result.replay_skip(format!(
5350 "replayed 0 corpus entries from {}",
5351 corpus_dir.display()
5352 ));
5353 } else {
5354 self.result.replay_result(
5355 stats.corpus_entries,
5356 stats.showmap_files,
5357 stats.skipped_entries,
5358 duration,
5359 );
5360 }
5361 }
5362 Err(e) => {
5363 self.result.single_fail(Some(e.to_string()));
5364 }
5365 }
5366 self.result
5367 }
5368
5369 fn invariant_runner(&self) -> TestRunner {
5370 let config = &self.config.invariant;
5371 fuzzer_with_cases(self.config.fuzz.seed, config.runs, config.max_assume_rejects)
5372 }
5373
5374 fn clone_executor(&self) -> Executor<FEN> {
5375 self.executor.clone().into_owned()
5376 }
5377
5378 fn clone_executor_with_symbolic_storage(
5379 &self,
5380 storage: &[SymbolicStorageAssignment],
5381 ) -> Result<Executor<FEN>> {
5382 let mut executor = self.clone_executor();
5383 for assignment in storage {
5384 executor.set_storage_slot(assignment.address, assignment.slot, assignment.value)?;
5385 if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut() {
5386 cheats.cache_arbitrary_storage_value(
5387 assignment.address,
5388 assignment.slot,
5389 assignment.value,
5390 );
5391 }
5392 }
5393 Ok(executor)
5394 }
5395
5396 fn build_fuzz_state(&self, invariant: bool, func: Option<&Function>) -> EvmFuzzState {
5397 let config =
5398 if invariant { self.config.invariant.dictionary } else { self.config.fuzz.dictionary };
5399 let has_function_inline_config =
5400 func.is_some_and(|func| self.inline_config.contains_function(self.cr.name, &func.name));
5401 let can_reuse_setup_state = !invariant
5402 && config == self.cr.config.fuzz.dictionary
5403 && !has_function_inline_config
5404 && !self.cr.contract.abi.functions().any(|func| func.name.is_before_test_setup());
5405 if can_reuse_setup_state {
5406 return self
5407 .setup
5408 .fuzz_state
5409 .get_or_init(|| self.build_fuzz_state_uncached(false, config))
5410 .fork();
5411 }
5412
5413 self.build_fuzz_state_uncached(invariant, config)
5414 }
5415
5416 fn build_fuzz_state_uncached(
5417 &self,
5418 invariant: bool,
5419 config: FuzzDictionaryConfig,
5420 ) -> EvmFuzzState {
5421 let literals =
5422 if invariant { &self.cr.mcr.invariant_literals } else { &self.cr.mcr.fuzz_literals };
5423 if let Some(db) = self.executor.backend().active_fork_db() {
5424 EvmFuzzState::new(&self.setup.deployed_libs, db, config, Some(literals))
5425 } else {
5426 let db = self.executor.backend().mem_db();
5427 EvmFuzzState::new(&self.setup.deployed_libs, db, config, Some(literals))
5428 }
5429 }
5430}
5431
5432fn fuzzer_with_cases(seed: Option<U256>, cases: u32, max_global_rejects: u32) -> TestRunner {
5433 let config = proptest::test_runner::Config {
5434 cases,
5435 max_global_rejects,
5436 max_shrink_iters: 0,
5439 ..Default::default()
5440 };
5441
5442 if let Some(seed) = seed {
5443 trace!(target: "forge::test", %seed, "building deterministic fuzzer");
5444 let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
5445 TestRunner::new_with_rng(config, rng)
5446 } else {
5447 trace!(target: "forge::test", "building stochastic fuzzer");
5448 TestRunner::new(config)
5449 }
5450}
5451
5452#[derive(Serialize, Deserialize)]
5454struct InvariantPersistedFailure {
5455 call_sequence: Vec<BaseCounterExample>,
5457 settings: InvariantSettings,
5460 #[serde(default)]
5462 assertion_failure: bool,
5463 #[serde(default, skip_serializing_if = "Vec::is_empty")]
5465 storage: Vec<SymbolicStorageAssignment>,
5466 #[serde(default, skip_serializing_if = "Option::is_none")]
5468 failure_site: Option<SymbolicInvariantFailureSite>,
5469}
5470
5471type HandlerFailureMap = std::collections::HashMap<(Address, Selector), InvariantFuzzError>;
5473type SymbolicHandlerStorageMap = HashMap<(Address, Selector, B256), SymbolicHandlerReplayStorage>;
5475
5476struct SymbolicHandlerReplayStorage {
5478 call_sequence: Vec<BasicTxDetails>,
5479 assignments: Vec<SymbolicStorageAssignment>,
5480}
5481
5482fn persisted_call_sequence(
5485 path: &Path,
5486 current_settings: &InvariantSettings,
5487) -> Option<InvariantPersistedFailure> {
5488 let persisted = foundry_common::fs::read_json_file::<InvariantPersistedFailure>(path).ok()?;
5489 if let Some(diff) = persisted.settings.diff(current_settings) {
5490 let _ = sh_warn!(
5491 "Failure from {path:?} file was ignored because invariant test settings have changed: {diff}"
5492 );
5493 return None;
5494 }
5495 Some(persisted)
5496}
5497
5498fn invariant_failure_file(failure_dir: &Path, invariant: &Function) -> PathBuf {
5500 canonicalized(failure_dir.join("invariants").join(&invariant.name))
5501}
5502
5503fn persisted_invariant_failure(
5505 failure_dir: &Path,
5506 invariant: &Function,
5507 current_settings: &InvariantSettings,
5508) -> Option<InvariantPersistedFailure> {
5509 persisted_call_sequence(&invariant_failure_file(failure_dir, invariant), current_settings)
5510 .or_else(|| {
5511 let legacy_path = canonicalized(failure_dir.join(&invariant.name));
5513 let persisted = persisted_call_sequence(&legacy_path, current_settings)?;
5514 let _ = sh_warn!(
5515 "Using legacy invariant failure cache at {}; new failures will be persisted under {}/invariants.",
5516 legacy_path.display(),
5517 failure_dir.display(),
5518 );
5519 Some(persisted)
5520 })
5521}
5522
5523fn base_counterexamples_to_txes(
5525 call_sequence: &mut [BaseCounterExample],
5526 show_solidity: bool,
5527) -> Vec<BasicTxDetails> {
5528 call_sequence
5529 .iter_mut()
5530 .map(|seq| {
5531 seq.show_solidity = show_solidity;
5532 base_counterexample_to_tx(seq)
5533 })
5534 .collect()
5535}
5536
5537fn base_counterexamples(
5539 calls: &[BasicTxDetails],
5540 identified_contracts: &ContractsByAddress,
5541 show_solidity: bool,
5542) -> Vec<BaseCounterExample> {
5543 calls
5544 .iter()
5545 .map(|tx| {
5546 BaseCounterExample::from_invariant_call(tx, identified_contracts, None, show_solidity)
5547 })
5548 .collect()
5549}
5550
5551fn failed_invariant_calls(error: &InvariantFuzzError) -> Option<&[BasicTxDetails]> {
5553 match error {
5554 InvariantFuzzError::BrokenInvariant(case_data) | InvariantFuzzError::Revert(case_data) => {
5555 let TestError::Fail(_, calls) = &case_data.test_error else {
5556 unreachable!("FailedInvariantCaseData::new always sets TestError::Fail")
5557 };
5558 Some(calls)
5559 }
5560 _ => None,
5561 }
5562}
5563
5564fn base_counterexample_to_tx(seq: &BaseCounterExample) -> BasicTxDetails {
5565 BasicTxDetails {
5566 warp: seq.warp,
5567 roll: seq.roll,
5568 sender: seq.sender.unwrap_or_default(),
5569 call_details: CallDetails {
5570 target: seq.addr.unwrap_or_default(),
5571 calldata: seq.calldata.clone(),
5572 value: seq.value,
5573 },
5574 }
5575}
5576
5577fn symbolic_invariant_counterexample_calls(
5578 steps: &[SymbolicInvariantStep],
5579 identified_contracts: &ContractsByAddress,
5580 show_solidity: bool,
5581) -> Vec<SymbolicCounterexampleCall> {
5582 steps
5583 .iter()
5584 .map(|step| {
5585 let tx = BasicTxDetails {
5586 warp: None,
5587 roll: None,
5588 sender: step.sender,
5589 call_details: CallDetails {
5590 target: step.address,
5591 calldata: step.calldata.clone(),
5592 value: None,
5593 },
5594 };
5595 let counterexample = BaseCounterExample::from_invariant_call(
5596 &tx,
5597 identified_contracts,
5598 None,
5599 show_solidity,
5600 );
5601 SymbolicCounterexampleCall::from_base_counterexample(
5602 &counterexample,
5603 step.sender,
5604 step.address,
5605 )
5606 })
5607 .collect()
5608}
5609
5610fn frontier_selector(frontier: &FuzzBranchFrontierRecord) -> Option<Selector> {
5611 frontier
5612 .sequence
5613 .get(frontier.call_index)
5614 .and_then(|call| call.call_details.calldata.get(..4))
5615 .map(Selector::from_slice)
5616}
5617
5618fn parse_frontier_selectors(selectors: &[String], signature: &str) -> Vec<Selector> {
5619 selectors
5620 .iter()
5621 .filter_map(|selector| {
5622 let parsed = hex::decode(selector.strip_prefix("0x").unwrap_or(selector))
5623 .ok()
5624 .filter(|bytes| bytes.len() == 4)
5625 .map(|bytes| Selector::from_slice(&bytes));
5626 if parsed.is_none() {
5627 let _ = sh_warn!(
5628 "invalid symbolic frontier selector `{selector}` for {signature}; expected \
5629 a 4-byte hex selector like 0x12345678"
5630 );
5631 }
5632 parsed
5633 })
5634 .collect()
5635}
5636
5637fn warn_unimported_frontiers<T: std::fmt::Display + PartialEq>(
5639 label: &str,
5640 requested: &[T],
5641 imported: &[T],
5642 signature: &str,
5643 path: &Path,
5644) {
5645 for value in requested.iter().filter(|value| !imported.contains(value)) {
5646 warn!(
5647 %value,
5648 label,
5649 test = %signature,
5650 path = %path.display(),
5651 "requested fuzz branch frontier was not imported"
5652 );
5653 let _ = sh_warn!(
5654 "requested fuzz branch frontier {label} {value} was not imported for {signature}"
5655 );
5656 }
5657}
5658
5659fn frontier_filter_display<T: std::fmt::Display>(values: &[T]) -> String {
5660 if values.is_empty() { "any".to_string() } else { values.iter().format(", ").to_string() }
5661}
5662
5663fn contract_short_name(contract_name: &str) -> &str {
5665 contract_name.split(':').next_back().unwrap()
5666}
5667
5668fn fuzz_test_path_name<'a>(
5670 abi: &JsonAbi,
5671 func: &'a Function,
5672 config: &FuzzConfig,
5673 contract_name: &str,
5674) -> Cow<'a, str> {
5675 let test_name = format!("{}-{}", func.name, hex::encode(func.selector()));
5676 let overloaded = abi.functions.get(&func.name).is_some_and(|functions| functions.len() > 1);
5677 let contract = contract_short_name(contract_name);
5678 let has_qualified_artifact = config
5679 .failure_persist_dir
5680 .as_ref()
5681 .is_some_and(|dir| dir.join("failures").join(contract).join(&test_name).exists())
5682 || [&config.corpus.corpus_dir, &config.corpus.frontier_dir]
5683 .into_iter()
5684 .flatten()
5685 .any(|dir| dir.join(contract).join(&test_name).exists());
5686
5687 if overloaded || has_qualified_artifact {
5688 Cow::Owned(test_name)
5689 } else {
5690 Cow::Borrowed(&func.name)
5691 }
5692}
5693
5694fn corpus_has_entries(dir: &Path) -> bool {
5696 canonical_replay_dirs(dir).iter().any(|dir| read_corpus_dir(dir).next().is_some())
5697}
5698
5699fn legacy_fuzz_corpus_dir(
5701 root: Option<&Path>,
5702 contract_name: &str,
5703 func: &Function,
5704 test_name: &str,
5705) -> Option<PathBuf> {
5706 if test_name == func.name {
5707 return None;
5708 }
5709 let contract = root?.join(contract_short_name(contract_name));
5710 if corpus_has_entries(&contract.join(test_name)) {
5711 return None;
5712 }
5713 let legacy = contract.join(&func.name);
5714 corpus_has_entries(&legacy).then(|| canonicalized(legacy))
5715}
5716
5717fn test_paths(
5719 corpus_config: &mut FuzzCorpusConfig,
5720 persist_dir: PathBuf,
5721 contract_name: &str,
5722 test_name: &str,
5723) -> (PathBuf, PathBuf) {
5724 let contract = contract_short_name(contract_name);
5725 corpus_config.with_test(contract, test_name);
5727
5728 let failures_dir = canonicalized(persist_dir.join("failures").join(contract));
5729 let failure_file = canonicalized(failures_dir.join(test_name));
5730 (failures_dir, failure_file)
5731}
5732
5733fn invariant_corpus_dir(
5735 root: &Path,
5736 contract_name: &str,
5737 isolated_campaign: Option<&str>,
5738) -> PathBuf {
5739 let dir = root.join(contract_short_name(contract_name));
5740 if let Some(name) = isolated_campaign { dir.join(name) } else { dir }
5741}
5742
5743fn invariant_frontier_dir(
5745 root: &Path,
5746 contract_name: &str,
5747 isolated_campaign: Option<&str>,
5748 execution_profile: &str,
5749 execution_pass: &str,
5750) -> PathBuf {
5751 let contract = stable_hashed_component(contract_short_name(contract_name), contract_name);
5752 let campaign = if let Some(name) = isolated_campaign {
5753 PathBuf::from("isolated").join(sanitize_symbolic_artifact_component(name))
5754 } else {
5755 PathBuf::from("shared")
5756 };
5757 root.join("v2").join(contract).join(execution_profile).join(execution_pass).join(campaign)
5758}
5759
5760fn invariant_suite_paths(
5762 corpus_config: &mut FuzzCorpusConfig,
5763 persist_dir: PathBuf,
5764 contract_name: &str,
5765 isolated_campaign: Option<&str>,
5766 execution_profile: &str,
5767 execution_pass: &str,
5768) -> PathBuf {
5769 if let Some(root) = &corpus_config.corpus_dir {
5770 corpus_config.corpus_dir =
5771 Some(canonicalized(invariant_corpus_dir(root, contract_name, isolated_campaign)));
5772 }
5773 if let Some(root) = &corpus_config.frontier_dir {
5774 corpus_config.frontier_dir = Some(canonicalized(invariant_frontier_dir(
5775 root,
5776 contract_name,
5777 isolated_campaign,
5778 execution_profile,
5779 execution_pass,
5780 )));
5781 }
5782 canonicalized(persist_dir.join("failures").join(contract_short_name(contract_name)))
5783}
5784
5785fn narrow_generated_corpus_root(corpus_dir: PathBuf, target_dir: PathBuf) -> PathBuf {
5787 let target_is_dir =
5788 std::fs::symlink_metadata(&target_dir).is_ok_and(|metadata| metadata.file_type().is_dir());
5789 if target_is_dir { canonicalized(target_dir) } else { corpus_dir }
5790}
5791
5792fn sanitize_symbolic_artifact_component(value: &str) -> String {
5793 let sanitized = value
5794 .chars()
5795 .map(|ch| if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' { ch } else { '_' })
5796 .collect::<String>();
5797 if sanitized.is_empty() { "_".to_string() } else { sanitized }
5798}
5799
5800fn stable_hashed_component(label: &str, identity: &str) -> String {
5801 let hash = keccak256(identity.as_bytes());
5802 let hash = hex::encode(&hash[..16]);
5803 format!("{}-{hash}", sanitize_symbolic_artifact_component(label))
5804}
5805
5806fn symbolic_artifact_file_name(
5807 contract_id: &str,
5808 value: &str,
5809 kind: SymbolicCounterexampleArtifactKind,
5810) -> String {
5811 let identity = format!("{contract_id}\0{value}\0{kind:?}");
5812 format!("{}.json", stable_hashed_component(value, &identity))
5813}
5814
5815fn record_invariant_failure(
5817 failure_file: &Path,
5818 call_sequence: &[BaseCounterExample],
5819 settings: &InvariantSettings,
5820 assertion_failure: bool,
5821 storage: &[SymbolicStorageAssignment],
5822 failure_site: Option<SymbolicInvariantFailureSite>,
5823) {
5824 if let Some(parent) = failure_file.parent()
5825 && let Err(err) = foundry_common::fs::create_dir_all(parent)
5826 {
5827 error!(%err, "Failed to create invariant failure file parent dir");
5828 return;
5829 }
5830
5831 if let Err(err) = foundry_common::fs::write_json_file(
5832 failure_file,
5833 &InvariantPersistedFailure {
5834 call_sequence: call_sequence.to_owned(),
5835 settings: settings.clone(),
5836 assertion_failure,
5837 storage: storage.to_vec(),
5838 failure_site,
5839 },
5840 ) {
5841 error!(%err, "Failed to record call sequence");
5842 }
5843}
5844
5845fn record_handler_failure(
5847 failure_dir: &Path,
5848 reverter: Address,
5849 selector: Selector,
5850 fingerprint: B256,
5851 call_sequence: &[BaseCounterExample],
5852 settings: &InvariantSettings,
5853 storage: &[SymbolicStorageAssignment],
5854) {
5855 let mut buf = [0u8; 24];
5856 buf[..20].copy_from_slice(reverter.as_slice());
5857 buf[20..].copy_from_slice(selector.as_slice());
5858 let file = failure_dir.join("handlers").join(format!("{:x}.json", keccak256(buf)));
5859 record_invariant_failure(
5860 &file,
5861 call_sequence,
5862 settings,
5863 true,
5864 storage,
5865 Some(SymbolicInvariantFailureSite::SequenceCall {
5866 target: reverter,
5867 selector,
5868 fingerprint,
5869 }),
5870 );
5871}
5872
5873fn invariant_handler_failure_name(
5874 identified_contracts: &ContractsByAddress,
5875 reverter: Address,
5876 selector: Selector,
5877) -> String {
5878 identified_contracts
5879 .get(&reverter)
5880 .and_then(|(contract_name, abi)| {
5881 abi.functions()
5882 .find(|f| f.selector() == selector)
5883 .map(|f| format!("{contract_name}::{}", f.name))
5884 })
5885 .unwrap_or_else(|| format!("{reverter}::{selector}"))
5886}
5887
5888fn should_symbolically_import_fuzz_corpus(config: &Config, func: &Function) -> bool {
5889 config.symbolic.use_fuzz_corpus && func.test_function_kind().is_fuzz_test()
5890}
5891
5892pub(crate) fn effective_test_function_kind(
5893 kind: TestFunctionKind,
5894 config: &Config,
5895 func: &Function,
5896) -> TestFunctionKind {
5897 if should_symbolically_import_fuzz_corpus(config, func) {
5898 TestFunctionKind::SymbolicTest
5899 } else {
5900 kind
5901 }
5902}
5903
5904fn symbolic_invariant_unsupported_domain_reason(
5905 invariant_config: &InvariantConfig,
5906 sender_filters: &SenderFilters,
5907 targets: &FuzzRunIdentifiedContracts,
5908 symbolic_targets: &[SymbolicInvariantTarget],
5909) -> Option<&'static str> {
5910 if sender_filters.targeted.is_empty() {
5911 return Some("symbolic invariant execution requires explicit target senders");
5912 }
5913 if invariant_config.has_delay() {
5914 return Some("symbolic invariant execution does not model warp/roll delays");
5915 }
5916 if invariant_config.call_override {
5917 return Some("symbolic invariant execution does not model call override targets");
5918 }
5919 if targets.is_updatable {
5920 return Some("symbolic invariant execution does not model dynamically updatable targets");
5921 }
5922 if invariant_config.corpus.payable_value_weight > 0
5923 && symbolic_targets
5924 .iter()
5925 .any(|target| target.function.state_mutability == StateMutability::Payable)
5926 {
5927 return Some("symbolic invariant execution does not model payable call values");
5928 }
5929 None
5930}
5931
5932fn replay_fuzz_minimize<FEN: FoundryEvmNetwork>(
5934 result: &mut TestResult,
5935 minimize: &FuzzMinimizeConfig,
5936 target: String,
5937 executor: &Executor<FEN>,
5938 corpus: &FuzzCorpusConfig,
5939 replay_target: ShowmapReplayTarget<'_>,
5940) {
5941 let Ok(mut evm_edge_indices_by_target) = minimize.evm_edge_indices.lock() else {
5942 result.single_fail(Some("minimize edge index lock poisoned".to_string()));
5943 return;
5944 };
5945 let evm_edge_indices = evm_edge_indices_by_target
5946 .entry(target.clone())
5947 .or_insert_with(|| Arc::new(Mutex::new(Default::default())))
5948 .clone();
5949 drop(evm_edge_indices_by_target);
5950 let Ok(mut evm_edge_indices) = evm_edge_indices.lock() else {
5951 result.single_fail(Some("minimize edge index lock poisoned".to_string()));
5952 return;
5953 };
5954 match replay_sequence_for_minimization(
5955 executor,
5956 MinimizationReplayInput {
5957 sequence: minimize.input.as_ref(),
5958 evm_edge_indices: &mut evm_edge_indices,
5959 corpus,
5960 stop_at_campaign_end: matches!(minimize.mode, FuzzMinimizeMode::Tmin),
5961 },
5962 replay_target,
5963 ) {
5964 Ok(observation) => {
5965 let replayed = observation.replayed;
5966 let skipped = observation.skipped + observation.unmatched;
5967 let Ok(mut observations) = minimize.observations.lock() else {
5968 result.single_fail(Some("minimize observations lock poisoned".to_string()));
5969 return;
5970 };
5971 observations.push(FuzzMinimizeObservation { target, observation });
5972 result.replay_result(replayed, 0, skipped, std::time::Duration::ZERO);
5973 }
5974 Err(e) => result.single_fail(Some(e.to_string())),
5975 }
5976}