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