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