1use crate::{
4 MultiContractRunner, TestFilter,
5 coverage::HitMaps,
6 fuzz::{BaseCounterExample, FuzzTestResult},
7 multi_runner::{TestContract, TestRunnerConfig},
8 progress::{TestsProgress, start_fuzz_progress},
9 result::{
10 InvariantFailure, InvariantPredicateResult, SuiteResult, TestResult, TestSetup, TestStatus,
11 invariant_campaign_display_name,
12 },
13};
14use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
15use alloy_json_abi::{Function, JsonAbi};
16use alloy_primitives::{Address, Bytes, Selector, U256, address, map::HashMap};
17use eyre::Result;
18use foundry_common::{TestFunctionExt, TestFunctionKind, contracts::ContractsByAddress};
19use foundry_compilers::utils::canonicalized;
20use foundry_config::{Config, FuzzCorpusConfig, InlineConfig, InvariantConfig};
21use foundry_evm::{
22 constants::CALLER,
23 core::evm::FoundryEvmNetwork,
24 decode::{RevertDecoder, SkipReason},
25 executors::{
26 CallResult, EvmError, Executor, ITest, RawCallResult, ShowmapOpts,
27 fuzz::FuzzedExecutor,
28 invariant::{
29 CheckSequenceOptions, HandlerAssertionFailure, InvariantExecutor, InvariantFuzzError,
30 check_sequence, replay_error, replay_handler_failure_sequence, replay_run,
31 },
32 replay_corpus_to_showmap,
33 },
34 fuzz::{
35 BasicTxDetails, CallDetails, CounterExample, FuzzFixtures, fixture_name,
36 invariant::{InvariantContract, InvariantSettings, is_optimization_invariant},
37 strategies::EvmFuzzState,
38 },
39 revm::primitives::hardfork::SpecId,
40 traces::{TraceKind, TraceMode, load_contracts},
41};
42use foundry_evm_networks::NetworkVariant;
43use foundry_evm_symbolic::{SymbolicExecutor, SymbolicRunInput, SymbolicRunResult};
44use itertools::Itertools;
45use proptest::test_runner::{RngAlgorithm, TestError, TestRng, TestRunner};
46use rayon::prelude::*;
47use serde::{Deserialize, Serialize};
48use std::{
49 borrow::Cow,
50 cmp::min,
51 collections::{BTreeMap, BTreeSet},
52 ops::Deref,
53 path::{Path, PathBuf},
54 sync::Arc,
55 time::Instant,
56};
57use tokio::signal;
58use tracing::Span;
59
60pub const LIBRARY_DEPLOYER: Address = address!("0x1F95D37F27EA0dEA9C252FC09D5A6eaA97647353");
66
67pub(crate) fn is_symbolic_entrypoint(func: &Function) -> bool {
68 func.name.starts_with("check") || func.name.starts_with("prove")
69}
70
71pub(crate) struct InvariantCampaignScope<'a> {
72 pub config: &'a Config,
73 pub inline_config: &'a InlineConfig,
74 pub contract_name: &'a str,
75 pub all_override_networks: &'a [NetworkVariant],
76 pub pass_network: Option<&'a NetworkVariant>,
77}
78
79struct InvariantCampaignSelection<'a> {
80 matched_boolean_invariant_fns: Vec<&'a Function>,
81 merge_boolean_suite: bool,
82 boolean_suite_anchor: Option<&'a Function>,
83 optimization_anchors: usize,
84}
85
86impl InvariantCampaignSelection<'_> {
87 const fn anchor_count(&self) -> usize {
88 self.optimization_anchors
89 + if self.matched_boolean_invariant_fns.is_empty() {
90 0
91 } else if self.merge_boolean_suite {
92 1
93 } else {
94 self.matched_boolean_invariant_fns.len()
95 }
96 }
97}
98
99pub(crate) fn count_runnable_invariant_campaign_anchors(
100 abi: &JsonAbi,
101 filter: &dyn TestFilter,
102 scope: InvariantCampaignScope<'_>,
103) -> usize {
104 let invariant_fns = abi.functions().filter(|func| func.is_invariant_test()).collect::<Vec<_>>();
105 if invariant_fns.iter().any(|func| !func.inputs.is_empty()) {
106 return 0;
107 }
108
109 let functions = abi
110 .functions()
111 .filter(|func| filter.matches_test_function(func))
112 .filter(|func| {
113 function_matches_network_pass(
114 scope.all_override_networks,
115 scope.pass_network,
116 scope.inline_config.network_for(
117 &scope.config.profile,
118 scope.contract_name,
119 &func.name,
120 ),
121 )
122 })
123 .collect::<Vec<_>>();
124
125 select_invariant_campaigns(
126 &invariant_fns,
127 &functions,
128 scope.config,
129 scope.inline_config,
130 scope.contract_name,
131 )
132 .anchor_count()
133}
134
135fn function_matches_network_pass(
136 all_override_networks: &[NetworkVariant],
137 pass_network: Option<&NetworkVariant>,
138 func_network: Option<NetworkVariant>,
139) -> bool {
140 if all_override_networks.is_empty() {
141 return true;
142 }
143 match pass_network {
144 None => func_network.is_none_or(|network| !all_override_networks.contains(&network)),
145 Some(target) => func_network.as_ref() == Some(target),
146 }
147}
148
149fn inline_config_for(
150 config: &Config,
151 inline_config: &InlineConfig,
152 contract_name: &str,
153 func: Option<&Function>,
154) -> Result<Config> {
155 let function = func.map(|f| f.name.as_str()).unwrap_or("");
156 Ok(config.merge_inline_provider(inline_config.provide(contract_name, function))?)
157}
158
159fn invariant_suite_configs_match(
160 config: &Config,
161 inline_config: &InlineConfig,
162 contract_name: &str,
163 funcs: &[&Function],
164) -> bool {
165 let Some((anchor, rest)) = funcs.split_first() else {
166 return true;
167 };
168 let anchor_config = match inline_config_for(config, inline_config, contract_name, Some(anchor))
169 {
170 Ok(config) => config.invariant,
171 Err(_) => return false,
172 };
173 rest.iter().all(|func| {
174 inline_config_for(config, inline_config, contract_name, Some(func))
175 .map(|config| config.invariant == anchor_config)
176 .unwrap_or(false)
177 })
178}
179
180fn select_invariant_campaigns<'a>(
181 invariant_fns: &[&'a Function],
182 functions: &[&'a Function],
183 config: &Config,
184 inline_config: &InlineConfig,
185 contract_name: &str,
186) -> InvariantCampaignSelection<'a> {
187 let boolean_invariant_fns =
188 invariant_fns.iter().copied().filter(|func| !is_optimization_invariant(func));
189 let matched_boolean_invariant_fns = functions
190 .iter()
191 .copied()
192 .filter(|func| func.is_invariant_test() && !is_optimization_invariant(func))
193 .collect::<Vec<_>>();
194 let optimization_anchors = functions
195 .iter()
196 .filter(|func| func.is_invariant_test() && is_optimization_invariant(func))
197 .count();
198
199 let canonical_boolean_anchor = boolean_invariant_fns.into_iter().next();
204 let merge_boolean_suite = !matched_boolean_invariant_fns.is_empty()
205 && invariant_suite_configs_match(
206 config,
207 inline_config,
208 contract_name,
209 &matched_boolean_invariant_fns,
210 );
211 let boolean_suite_anchor = merge_boolean_suite
212 .then(|| {
213 canonical_boolean_anchor
214 .filter(|anchor| matched_boolean_invariant_fns.contains(anchor))
215 .or_else(|| matched_boolean_invariant_fns.first().copied())
216 })
217 .flatten();
218
219 InvariantCampaignSelection {
220 matched_boolean_invariant_fns,
221 merge_boolean_suite,
222 boolean_suite_anchor,
223 optimization_anchors,
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use foundry_common::EmptyTestFilter;
231 use foundry_config::NatSpec;
232
233 const CONTRACT_NAME: &str = "src/Test.t.sol:InvariantTest";
234
235 fn count_anchors(abi: &JsonAbi, inline_config: &InlineConfig) -> usize {
236 let config = Config::default();
237 count_runnable_invariant_campaign_anchors(
238 abi,
239 &EmptyTestFilter::default(),
240 InvariantCampaignScope {
241 config: &config,
242 inline_config,
243 contract_name: CONTRACT_NAME,
244 all_override_networks: &[],
245 pass_network: None,
246 },
247 )
248 }
249
250 #[test]
251 fn runnable_campaign_anchor_count_merges_boolean_suite_and_counts_optimizations() {
252 let abi = JsonAbi::parse([
253 "function invariantOne() external",
254 "function invariantTwo() external",
255 "function invariantOptimizeA() external returns (int256)",
256 "function invariantOptimizeB() external returns (int256)",
257 ])
258 .unwrap();
259
260 assert_eq!(count_anchors(&abi, &InlineConfig::new()), 3);
261 }
262
263 #[test]
264 fn runnable_campaign_anchor_count_splits_boolean_suite_when_configs_differ() {
265 let abi = JsonAbi::parse([
266 "function invariantOne() external",
267 "function invariantTwo() external",
268 ])
269 .unwrap();
270 let mut inline_config = InlineConfig::new();
271 inline_config
272 .insert(&NatSpec {
273 contract: CONTRACT_NAME.to_string(),
274 function: Some("invariantTwo".to_string()),
275 line: "1:1".to_string(),
276 docs: "forge-config: default.invariant.depth = 1".to_string(),
277 })
278 .unwrap();
279
280 assert_eq!(count_anchors(&abi, &inline_config), 2);
281 }
282
283 #[test]
284 fn runnable_campaign_anchor_count_respects_network_pass() {
285 let abi = JsonAbi::parse(["function invariantTempoOnly() external"]).unwrap();
286 let mut inline_config = InlineConfig::new();
287 inline_config
288 .insert(&NatSpec {
289 contract: CONTRACT_NAME.to_string(),
290 function: Some("invariantTempoOnly".to_string()),
291 line: "1:1".to_string(),
292 docs: r#"forge-config: default.networks.network = "tempo""#.to_string(),
293 })
294 .unwrap();
295 let config = Config::default();
296 let override_networks = [NetworkVariant::Tempo];
297
298 let default_pass = count_runnable_invariant_campaign_anchors(
299 &abi,
300 &EmptyTestFilter::default(),
301 InvariantCampaignScope {
302 config: &config,
303 inline_config: &inline_config,
304 contract_name: CONTRACT_NAME,
305 all_override_networks: &override_networks,
306 pass_network: None,
307 },
308 );
309 let tempo_pass = count_runnable_invariant_campaign_anchors(
310 &abi,
311 &EmptyTestFilter::default(),
312 InvariantCampaignScope {
313 config: &config,
314 inline_config: &inline_config,
315 contract_name: CONTRACT_NAME,
316 all_override_networks: &override_networks,
317 pass_network: Some(&NetworkVariant::Tempo),
318 },
319 );
320
321 assert_eq!(default_pass, 0);
322 assert_eq!(tempo_pass, 1);
323 }
324}
325
326pub struct ContractRunner<'a, FEN: FoundryEvmNetwork> {
328 name: &'a str,
330 contract: &'a TestContract,
332 executor: Executor<FEN>,
334 progress: Option<&'a TestsProgress>,
336 tokio_handle: tokio::runtime::Handle,
338 span: tracing::Span,
340 tcfg: Cow<'a, TestRunnerConfig<FEN>>,
342 mcr: &'a MultiContractRunner<FEN>,
344 num_invariant_campaign_anchors: usize,
346}
347
348pub(crate) struct ContractRunnerContext<'a> {
349 pub(crate) progress: Option<&'a TestsProgress>,
350 pub(crate) tokio_handle: tokio::runtime::Handle,
351 pub(crate) num_invariant_campaign_anchors: usize,
352}
353
354impl<'a, FEN: FoundryEvmNetwork> Deref for ContractRunner<'a, FEN> {
355 type Target = Cow<'a, TestRunnerConfig<FEN>>;
356
357 #[inline(always)]
358 fn deref(&self) -> &Self::Target {
359 &self.tcfg
360 }
361}
362
363impl<'a, FEN: FoundryEvmNetwork> ContractRunner<'a, FEN> {
364 pub(crate) fn new(
365 name: &'a str,
366 contract: &'a TestContract,
367 executor: Executor<FEN>,
368 span: Span,
369 mcr: &'a MultiContractRunner<FEN>,
370 context: ContractRunnerContext<'a>,
371 ) -> Self {
372 Self {
373 name,
374 contract,
375 executor,
376 progress: context.progress,
377 tokio_handle: context.tokio_handle,
378 span,
379 tcfg: Cow::Borrowed(&mcr.tcfg),
380 mcr,
381 num_invariant_campaign_anchors: context.num_invariant_campaign_anchors,
382 }
383 }
384
385 fn function_matches_network_pass(&self, func: &Function) -> bool {
392 function_matches_network_pass(
393 &self.mcr.tcfg.multi_network.all_override_networks,
394 self.mcr.tcfg.multi_network.pass_network.as_ref(),
395 self.mcr.inline_config.network_for(&self.tcfg.config.profile, self.name, &func.name),
396 )
397 }
398
399 pub fn setup(&mut self, call_setup: bool) -> TestSetup {
402 self._setup(call_setup).unwrap_or_else(|err| {
403 if err.to_string().contains("skipped") {
404 TestSetup::skipped(err.to_string())
405 } else {
406 TestSetup::failed(err.to_string())
407 }
408 })
409 }
410
411 fn _setup(&mut self, call_setup: bool) -> Result<TestSetup> {
412 trace!(call_setup, "setting up");
413
414 self.apply_contract_inline_config()?;
415
416 self.executor.set_balance(self.sender, U256::MAX)?;
418 self.executor.set_balance(CALLER, U256::MAX)?;
419
420 self.executor.set_nonce(self.sender, 1)?;
422
423 self.executor.set_balance(LIBRARY_DEPLOYER, U256::MAX)?;
425
426 let mut result = TestSetup::default();
427 for code in &self.mcr.libs_to_deploy {
428 let deploy_result = self.executor.deploy(
429 LIBRARY_DEPLOYER,
430 code.clone(),
431 U256::ZERO,
432 Some(&self.mcr.revert_decoder),
433 );
434
435 if let Ok(deployed) = &deploy_result {
437 result.deployed_libs.push(deployed.address);
438 }
439
440 let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
441 result.extend(raw, TraceKind::Deployment);
442 if reason.is_some() {
443 debug!(?reason, "deployment of library failed");
444 result.reason = reason;
445 return Ok(result);
446 }
447 }
448
449 let address = self.sender.create(self.executor.get_nonce(self.sender)?);
450 result.address = address;
451
452 self.executor.set_balance(address, self.initial_balance())?;
455
456 let deploy_result = self.executor.deploy(
458 self.sender,
459 self.contract.bytecode.clone(),
460 U256::ZERO,
461 Some(&self.mcr.revert_decoder),
462 );
463
464 result.deployment_failure = deploy_result.is_err();
465
466 if let Ok(dr) = &deploy_result {
467 debug_assert_eq!(dr.address, address);
468 }
469 let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
470 result.extend(raw, TraceKind::Deployment);
471 if reason.is_some() {
472 debug!(?reason, "deployment of test contract failed");
473 result.reason = reason;
474 return Ok(result);
475 }
476
477 self.executor.set_balance(self.sender, self.initial_balance())?;
479 self.executor.set_balance(CALLER, self.initial_balance())?;
480 self.executor.set_balance(LIBRARY_DEPLOYER, self.initial_balance())?;
481
482 self.executor.deploy_create2_deployer()?;
483
484 if call_setup {
486 trace!("calling setUp");
487 let res = self.executor.setup(None, address, Some(&self.mcr.revert_decoder));
488 let (raw, reason) = RawCallResult::from_evm_result(res)?;
489 result.extend(raw, TraceKind::Setup);
490 result.reason = reason;
491 }
492
493 result.fuzz_fixtures = self.fuzz_fixtures(address);
494
495 Ok(result)
496 }
497
498 fn initial_balance(&self) -> U256 {
499 self.evm_opts.initial_balance
500 }
501
502 fn apply_contract_inline_config(&mut self) -> Result<()> {
504 if self.inline_config.contains_contract(self.name) {
505 let new_config = Arc::new(self.inline_config(None)?);
506 self.tcfg.to_mut().reconfigure_with(new_config);
507 let prev_tracer = self.executor.inspector_mut().tracer.take();
508 self.tcfg.configure_executor(&mut self.executor);
509 self.executor.inspector_mut().tracer = prev_tracer;
511 }
512 Ok(())
513 }
514
515 fn inline_config(&self, func: Option<&Function>) -> Result<Config> {
517 inline_config_for(&self.config, &self.mcr.inline_config, self.name, func)
518 }
519
520 fn fuzz_fixtures(&mut self, address: Address) -> FuzzFixtures {
536 let mut fixtures = HashMap::default();
537 let fixture_functions = self.contract.abi.functions().filter(|func| func.is_fixture());
538 for func in fixture_functions {
539 if func.inputs.is_empty() {
540 if let Ok(CallResult { raw: _, decoded_result }) =
542 self.executor.call(CALLER, address, func, &[], U256::ZERO, None)
543 {
544 fixtures.insert(fixture_name(func.name.clone()), decoded_result);
545 }
546 } else {
547 let mut vals = Vec::new();
550 let mut index = 0;
551 loop {
552 if let Ok(CallResult { raw: _, decoded_result }) = self.executor.call(
553 CALLER,
554 address,
555 func,
556 &[DynSolValue::Uint(U256::from(index), 256)],
557 U256::ZERO,
558 None,
559 ) {
560 vals.push(decoded_result);
561 } else {
562 break;
565 }
566 index += 1;
567 }
568 fixtures.insert(fixture_name(func.name.clone()), DynSolValue::Array(vals));
569 };
570 }
571 FuzzFixtures::new(fixtures)
572 }
573
574 pub fn run_tests(mut self, filter: &dyn TestFilter) -> SuiteResult {
576 let start = Instant::now();
577 let mut warnings = Vec::new();
578
579 let setup_fns: Vec<_> =
581 self.contract.abi.functions().filter(|func| func.name.is_setup()).collect();
582 let call_setup = setup_fns.len() == 1 && setup_fns[0].name == "setUp";
583 for &setup_fn in &setup_fns {
585 if setup_fn.name != "setUp" {
586 warnings.push(format!(
587 "Found invalid setup function \"{}\" did you mean \"setUp()\"?",
588 setup_fn.signature()
589 ));
590 }
591 }
592
593 if setup_fns.len() > 1 {
595 self.tcfg.early_exit.record_failure();
599 return SuiteResult::new(
600 start.elapsed(),
601 [("setUp()".to_string(), TestResult::fail("multiple setUp functions".to_string()))]
602 .into(),
603 warnings,
604 );
605 }
606
607 let after_invariant_fns: Vec<_> =
609 self.contract.abi.functions().filter(|func| func.name.is_after_invariant()).collect();
610 if after_invariant_fns.len() > 1 {
611 self.tcfg.early_exit.record_failure();
613 return SuiteResult::new(
614 start.elapsed(),
615 [(
616 "afterInvariant()".to_string(),
617 TestResult::fail("multiple afterInvariant functions".to_string()),
618 )]
619 .into(),
620 warnings,
621 );
622 }
623 let call_after_invariant = after_invariant_fns.first().is_some_and(|after_invariant_fn| {
624 let match_sig = after_invariant_fn.name == "afterInvariant";
625 if !match_sig {
626 warnings.push(format!(
627 "Found invalid afterInvariant function \"{}\" did you mean \"afterInvariant()\"?",
628 after_invariant_fn.signature()
629 ));
630 }
631 match_sig
632 });
633
634 let invariant_fns: Vec<_> =
635 self.contract.abi.functions().filter(|func| func.is_invariant_test()).collect();
636
637 let invalid_invariants: Vec<_> = invariant_fns
642 .iter()
643 .filter(|f| !f.inputs.is_empty())
644 .map(|f| {
645 (
646 f.signature(),
647 TestResult::fail(format!(
648 "invariant `{}` must take no parameters",
649 f.signature()
650 )),
651 )
652 })
653 .collect();
654 if !invalid_invariants.is_empty() {
655 self.tcfg.early_exit.record_failure();
656 return SuiteResult::new(
657 start.elapsed(),
658 invalid_invariants.into_iter().collect(),
659 warnings,
660 );
661 }
662
663 let has_invariants = !invariant_fns.is_empty();
668
669 let should_override_setup_tracing =
670 !self.tcfg.debug && (self.executor.inspector().tracer.is_some() || has_invariants);
671
672 let prev_tracer = should_override_setup_tracing.then(|| {
673 let prev_tracer = self.executor.inspector_mut().tracer.take();
674 self.executor.set_tracing(TraceMode::Call);
675 prev_tracer
676 });
677
678 let setup_time = Instant::now();
679 let setup = self.setup(call_setup);
680 debug!("finished setting up in {:?}", setup_time.elapsed());
681
682 if let Some(prev_tracer) = prev_tracer {
683 self.executor.inspector_mut().tracer = prev_tracer;
684 }
685
686 if setup.reason.is_some() {
687 let fail_msg = if setup.deployment_failure {
689 "constructor()".to_string()
690 } else {
691 "setUp()".to_string()
692 };
693 self.tcfg.early_exit.record_failure();
694 return SuiteResult::new(
695 start.elapsed(),
696 [(fail_msg, TestResult::setup_result(setup))].into(),
697 warnings,
698 );
699 }
700
701 let find_timer = Instant::now();
704 let symbolic_enabled = self.config.symbolic.enabled;
705 let functions = self
706 .contract
707 .abi
708 .functions()
709 .filter(|func| {
710 if symbolic_enabled && is_symbolic_entrypoint(func) {
711 filter.matches_test(&func.signature())
712 } else {
713 filter.matches_test_function_in_contract(self.name, func)
714 }
715 })
716 .filter(|func| self.function_matches_network_pass(func))
717 .collect::<Vec<_>>();
718 debug!(
719 "Found {} test functions out of {} in {:?}",
720 functions.len(),
721 self.contract.abi.functions().count(),
722 find_timer.elapsed(),
723 );
724
725 let identified_contracts = has_invariants.then(|| {
726 load_contracts(setup.traces.iter().map(|(_, t)| &t.arena), &self.mcr.known_contracts)
727 });
728
729 let test_fail_functions =
730 functions.iter().filter(|func| func.test_function_kind().is_any_test_fail());
731 if test_fail_functions.clone().next().is_some() {
732 let fail = || {
733 TestResult::fail("`testFail*` has been removed. Consider changing to test_Revert[If|When]_Condition and expecting a revert".to_string())
734 };
735 let test_results = test_fail_functions.map(|func| (func.signature(), fail())).collect();
736 self.tcfg.early_exit.record_failure();
737 return SuiteResult::new(start.elapsed(), test_results, warnings);
738 }
739
740 let early_exit = &self.tcfg.early_exit;
741
742 if self.progress.is_some() {
743 let interrupt = early_exit.clone();
744 self.tokio_handle.spawn(async move {
745 signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
746 interrupt.record_ctrl_c();
747 });
748 }
749
750 let invariant_campaigns = select_invariant_campaigns(
751 &invariant_fns,
752 &functions,
753 &self.config,
754 &self.mcr.inline_config,
755 self.name,
756 );
757 let InvariantCampaignSelection {
758 matched_boolean_invariant_fns,
759 merge_boolean_suite: merge_invariant_suite,
760 boolean_suite_anchor: invariant_suite_anchor,
761 optimization_anchors: _,
762 } = invariant_campaigns;
763
764 let test_results = functions
765 .par_iter()
766 .filter_map(|&func| {
767 if early_exit.should_stop() {
769 return None;
770 }
771 let invariants: &[&Function] = if func.is_invariant_test() {
774 if is_optimization_invariant(func) {
775 std::slice::from_ref(&func)
776 } else if merge_invariant_suite {
777 if invariant_suite_anchor != Some(func) {
779 return None;
780 }
781 matched_boolean_invariant_fns.as_slice()
782 } else {
783 std::slice::from_ref(&func)
784 }
785 } else {
786 invariant_fns.as_slice()
787 };
788
789 if func.is_invariant_test() && invariants.is_empty() {
791 return None;
792 }
793
794 let start = Instant::now();
795
796 let _guard = self.tokio_handle.enter();
797
798 let _guard;
799 let current_span = tracing::Span::current();
800 if current_span.is_none() || current_span.id() != self.span.id() {
801 _guard = self.span.enter();
802 }
803
804 let sig = func.signature();
805 let kind = if self.config.symbolic.enabled && is_symbolic_entrypoint(func) {
806 TestFunctionKind::SymbolicTest
807 } else {
808 func.test_function_kind()
809 };
810
811 let _guard = debug_span!(
812 "test",
813 %kind,
814 name = %if enabled!(tracing::Level::TRACE) { &sig } else { &func.name },
815 )
816 .entered();
817
818 let mut res = FunctionRunner::new(&self, &setup).run(
819 func,
820 invariants,
821 kind,
822 call_after_invariant,
823 identified_contracts.as_ref(),
824 );
825 res.duration = start.elapsed();
826
827 if res.status.is_failure() {
829 early_exit.record_failure();
830 }
831
832 Some((sig, res))
833 })
834 .collect::<BTreeMap<_, _>>();
835
836 let duration = start.elapsed();
837 SuiteResult::new(duration, test_results, warnings)
838 }
839}
840
841struct FunctionRunner<'a, FEN: FoundryEvmNetwork> {
843 tcfg: Cow<'a, TestRunnerConfig<FEN>>,
845 executor: Cow<'a, Executor<FEN>>,
847 cr: &'a ContractRunner<'a, FEN>,
849 address: Address,
851 setup: &'a TestSetup,
853 result: TestResult,
855}
856
857impl<'a, FEN: FoundryEvmNetwork> Deref for FunctionRunner<'a, FEN> {
858 type Target = Cow<'a, TestRunnerConfig<FEN>>;
859
860 #[inline(always)]
861 fn deref(&self) -> &Self::Target {
862 &self.tcfg
863 }
864}
865
866impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> {
867 fn new(cr: &'a ContractRunner<'a, FEN>, setup: &'a TestSetup) -> Self {
868 Self {
869 tcfg: match &cr.tcfg {
870 Cow::Borrowed(tcfg) => Cow::Borrowed(tcfg),
871 Cow::Owned(tcfg) => Cow::Owned(tcfg.clone()),
872 },
873 executor: Cow::Borrowed(&cr.executor),
874 cr,
875 address: setup.address,
876 setup,
877 result: TestResult::new(setup),
878 }
879 }
880
881 const fn revert_decoder(&self) -> &'a RevertDecoder {
882 &self.cr.mcr.revert_decoder
883 }
884
885 fn should_defer_symbolic_diagnostics(&self) -> bool {
887 self.cr.progress.is_some() && self.config.symbolic.dump_smt
888 }
889
890 fn apply_function_inline_config(&mut self, func: &Function) -> Result<()> {
892 if self.inline_config.contains_function(self.cr.name, &func.name) {
893 let new_config = Arc::new(self.cr.inline_config(Some(func))?);
894 self.tcfg.to_mut().reconfigure_with(new_config);
895 self.tcfg.configure_executor(self.executor.to_mut());
896 }
897 Ok(())
898 }
899
900 fn run(
901 mut self,
902 func: &Function,
903 invariants: &[&Function],
904 kind: TestFunctionKind,
905 call_after_invariant: bool,
906 identified_contracts: Option<&ContractsByAddress>,
907 ) -> TestResult {
908 if let Err(e) = self.apply_function_inline_config(func) {
909 self.result.single_fail(Some(e.to_string()));
910 return self.result;
911 }
912
913 if self.cr.mcr.tcfg.showmap.is_some()
915 && matches!(kind, TestFunctionKind::UnitTest { .. } | TestFunctionKind::TableTest)
916 {
917 self.result.replay_skip("not runnable in showmap mode");
918 return self.result;
919 }
920
921 match kind {
922 TestFunctionKind::UnitTest { .. } => self.run_unit_test(func),
923 TestFunctionKind::FuzzTest { .. } => self.run_fuzz_test(func),
924 TestFunctionKind::TableTest => self.run_table_test(func),
925 TestFunctionKind::SymbolicTest => self.run_symbolic_test(func),
926 TestFunctionKind::InvariantTest => {
927 let fail_on_revert_for = |f: &Function| {
928 if self.inline_config.contains_function(self.cr.name, &f.name)
929 && let Ok(config) = self.cr.inline_config(Some(f))
930 {
931 return config.invariant.fail_on_revert;
932 }
933 self.config.invariant.fail_on_revert
934 };
935 let invariant_fns: Vec<_> =
936 invariants.iter().copied().map(|f| (f, fail_on_revert_for(f))).collect();
937 self.run_invariant_test(
938 func,
939 invariant_fns,
940 call_after_invariant,
941 identified_contracts.unwrap(),
942 )
943 }
944 _ => unreachable!(),
945 }
946 }
947
948 fn run_unit_test(mut self, func: &Function) -> TestResult {
957 if self.prepare_test(func).is_err() {
959 return self.result;
960 }
961
962 let (mut raw_call_result, reason) = match self.executor.call(
964 self.sender,
965 self.address,
966 func,
967 &[],
968 U256::ZERO,
969 Some(self.revert_decoder()),
970 ) {
971 Ok(res) => (res.raw, None),
972 Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
973 Err(EvmError::Skip(reason)) => {
974 self.result.single_skip(reason);
975 return self.result;
976 }
977 Err(err) => {
978 self.result.single_fail(Some(err.to_string()));
979 return self.result;
980 }
981 };
982
983 let success =
984 self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, false);
985 self.result.single_result(success, reason, raw_call_result);
986 self.result
987 }
988
989 fn run_symbolic_test(mut self, func: &Function) -> TestResult {
991 if self.prepare_test(func).is_err() {
992 return self.result;
993 }
994
995 let mut symbolic = SymbolicExecutor::new(self.config.symbolic.clone());
996 if self.should_defer_symbolic_diagnostics() {
997 symbolic.capture_diagnostics();
998 }
999 let result = symbolic.run(SymbolicRunInput {
1000 executor: self.executor.as_ref(),
1001 target: self.address,
1002 sender: self.sender,
1003 function: func,
1004 value: U256::ZERO,
1005 ffi_enabled: self.config.ffi,
1006 });
1007 let portfolio_diagnostics = symbolic.portfolio_diagnostics();
1008 let symbolic_diagnostics = symbolic.take_diagnostics();
1009
1010 match result {
1011 SymbolicRunResult::Safe(stats) => {
1012 self.result.symbolic_result(true, None, None, stats);
1013 }
1014 SymbolicRunResult::Incomplete { kind, reason, stats } => {
1015 self.result.symbolic_result(
1016 false,
1017 Some(format!("incomplete symbolic execution ({kind:?}): {reason}")),
1018 None,
1019 stats,
1020 );
1021 }
1022 SymbolicRunResult::Counterexample { args, calldata, stats } => {
1023 let (mut raw_call_result, reason) = match self.executor.call(
1024 self.sender,
1025 self.address,
1026 func,
1027 &args,
1028 U256::ZERO,
1029 Some(self.revert_decoder()),
1030 ) {
1031 Ok(res) => (res.raw, None),
1032 Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
1033 Err(EvmError::Skip(reason)) => {
1034 self.result.single_skip(reason);
1035 self.result.symbolic_portfolio_diagnostics = portfolio_diagnostics;
1036 self.result.symbolic_diagnostics = symbolic_diagnostics;
1037 return self.result;
1038 }
1039 Err(err) => {
1040 self.result.symbolic_result(false, Some(err.to_string()), None, stats);
1041 self.result.symbolic_portfolio_diagnostics = portfolio_diagnostics;
1042 self.result.symbolic_diagnostics = symbolic_diagnostics;
1043 return self.result;
1044 }
1045 };
1046
1047 let success = self.executor.is_raw_call_mut_success(
1048 self.address,
1049 &mut raw_call_result,
1050 false,
1051 );
1052 let counterexample = CounterExample::Single(BaseCounterExample::from_fuzz_call(
1053 calldata,
1054 args,
1055 raw_call_result.traces.clone(),
1056 ));
1057 self.result.extend(raw_call_result);
1058 self.result.symbolic_result(
1059 false,
1060 if success {
1061 Some("symbolic counterexample did not replay".to_string())
1062 } else {
1063 reason
1064 },
1065 Some(counterexample),
1066 stats,
1067 );
1068 }
1069 }
1070
1071 self.result.symbolic_portfolio_diagnostics = portfolio_diagnostics;
1072 self.result.symbolic_diagnostics = symbolic_diagnostics;
1073 self.result
1074 }
1075
1076 fn run_table_test(mut self, func: &Function) -> TestResult {
1085 if self.prepare_test(func).is_err() {
1087 return self.result;
1088 }
1089
1090 let Some(first_param) = func.inputs.first() else {
1092 self.result.single_fail(Some("Table test should have at least one parameter".into()));
1093 return self.result;
1094 };
1095
1096 let Some(first_param_fixtures) =
1097 &self.setup.fuzz_fixtures.param_fixtures(first_param.name())
1098 else {
1099 self.result.single_fail(Some("Table test should have fixtures defined".into()));
1100 return self.result;
1101 };
1102
1103 if first_param_fixtures.is_empty() {
1104 self.result.single_fail(Some("Table test should have at least one fixture".into()));
1105 return self.result;
1106 }
1107
1108 let fixtures_len = first_param_fixtures.len();
1109 let mut table_fixtures = vec![&first_param_fixtures[..]];
1110
1111 for param in &func.inputs[1..] {
1113 let param_name = param.name();
1114 let Some(fixtures) = &self.setup.fuzz_fixtures.param_fixtures(param.name()) else {
1115 self.result.single_fail(Some(format!("No fixture defined for param {param_name}")));
1116 return self.result;
1117 };
1118
1119 if fixtures.len() != fixtures_len {
1120 self.result.single_fail(Some(format!(
1121 "{} fixtures defined for {param_name} (expected {})",
1122 fixtures.len(),
1123 fixtures_len
1124 )));
1125 return self.result;
1126 }
1127
1128 table_fixtures.push(&fixtures[..]);
1129 }
1130
1131 let progress = start_fuzz_progress(
1132 self.cr.progress,
1133 self.cr.name,
1134 &func.name,
1135 None,
1136 fixtures_len as u32,
1137 );
1138
1139 let mut result = FuzzTestResult::default();
1140
1141 for i in 0..fixtures_len {
1142 if self.tcfg.early_exit.should_stop() {
1143 return self.result;
1144 }
1145
1146 if let Some(progress) = progress.as_ref() {
1148 progress.inc(1);
1149 }
1150
1151 let args = table_fixtures.iter().map(|row| row[i].clone()).collect_vec();
1152 let (mut raw_call_result, reason) = match self.executor.call(
1153 self.sender,
1154 self.address,
1155 func,
1156 &args,
1157 U256::ZERO,
1158 Some(self.revert_decoder()),
1159 ) {
1160 Ok(res) => (res.raw, None),
1161 Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
1162 Err(EvmError::Skip(reason)) => {
1163 self.result.single_skip(reason);
1164 return self.result;
1165 }
1166 Err(err) => {
1167 self.result.single_fail(Some(err.to_string()));
1168 return self.result;
1169 }
1170 };
1171
1172 result.gas_by_case.push((raw_call_result.gas_used, raw_call_result.stipend));
1173 result.logs.extend(raw_call_result.logs.clone());
1174 result.labels.extend(raw_call_result.labels.clone());
1175 HitMaps::merge_opt(&mut result.line_coverage, raw_call_result.line_coverage.clone());
1176
1177 let is_success =
1178 self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, false);
1179 if !is_success {
1181 result.counterexample =
1182 Some(CounterExample::Single(BaseCounterExample::from_fuzz_call(
1183 Bytes::from(func.abi_encode_input(&args).unwrap()),
1184 args,
1185 raw_call_result.traces.clone(),
1186 )));
1187 result.reason = reason;
1188 result.traces = raw_call_result.traces;
1189 result.debug_bytecodes = raw_call_result.debug_bytecodes;
1190 self.result.table_result(result);
1191 return self.result;
1192 }
1193
1194 if i == fixtures_len - 1 {
1197 result.success = true;
1198 result.traces = raw_call_result.traces;
1199 result.debug_bytecodes = raw_call_result.debug_bytecodes;
1200 self.result.table_result(result);
1201 return self.result;
1202 }
1203 }
1204
1205 self.result
1206 }
1207
1208 fn run_invariant_test(
1209 mut self,
1210 func: &Function,
1211 invariants: Vec<(&Function, bool)>,
1212 call_after_invariant: bool,
1213 identified_contracts: &ContractsByAddress,
1214 ) -> TestResult {
1215 let runner = self.invariant_runner();
1216 let invariant_config = self.config.invariant.clone();
1217 let invariant_config = &invariant_config;
1218 let is_optimization = is_optimization_invariant(func);
1219
1220 let mut live_invariants = Vec::new();
1221 let mut skipped_predicate_results = Vec::new();
1222 for (invariant, fail_on_revert) in invariants {
1223 if let Some(reason) = self.invariant_skip_reason(invariant) {
1224 skipped_predicate_results.push(InvariantPredicateResult {
1225 name: invariant.name.clone(),
1226 status: TestStatus::Skipped,
1227 reason: reason.0,
1228 });
1229 } else {
1230 live_invariants.push((invariant, fail_on_revert));
1231 }
1232 }
1233
1234 if live_invariants.is_empty() {
1235 let skip_reason = skipped_predicate_results
1236 .iter()
1237 .find(|predicate| predicate.name == func.name)
1238 .and_then(|predicate| predicate.reason.clone());
1239 self.result
1240 .invariant_skip_with_predicates(SkipReason(skip_reason), skipped_predicate_results);
1241 return self.result;
1242 }
1243 let campaign_anchor = live_invariants
1244 .iter()
1245 .find(|(invariant_fn, _)| *invariant_fn == func)
1246 .map(|(invariant_fn, _)| *invariant_fn)
1247 .unwrap_or_else(|| live_invariants[0].0);
1248
1249 let mut executor = self.clone_executor();
1250 executor.inspector_mut().collect_edge_coverage_with_config(&invariant_config.corpus);
1253 executor.inspector_mut().collect_evm_cmp_log(invariant_config.corpus.collect_evm_cmp_log());
1254 executor
1255 .inspector_mut()
1256 .collect_sancov_edges(invariant_config.corpus.collect_sancov_edges());
1257 executor
1258 .inspector_mut()
1259 .collect_sancov_trace_cmp(invariant_config.corpus.collect_sancov_trace_cmp());
1260 let mut config = invariant_config.clone();
1261 let failure_dir = invariant_suite_paths(
1262 &mut config.corpus,
1263 invariant_config.failure_persist_dir.clone().unwrap(),
1264 self.cr.name,
1265 func.name.as_str(),
1266 is_optimization,
1267 );
1268 let resolved_corpus_dir = config.corpus.corpus_dir.clone();
1270
1271 let mut evm = InvariantExecutor::new_with_fuzz_seed(
1272 executor,
1273 runner,
1274 self.config.fuzz.seed,
1275 config,
1276 identified_contracts,
1277 &self.cr.mcr.known_contracts,
1278 self.cr.num_invariant_campaign_anchors,
1279 );
1280
1281 if let Some(showmap) = self.cr.mcr.tcfg.showmap.clone() {
1284 let corpus_dir = showmap.corpus_dir.clone().or(resolved_corpus_dir);
1285
1286 if let Err(e) = evm.select_contract_artifacts(self.address) {
1288 self.result.invariant_setup_fail(e);
1289 return self.result;
1290 }
1291 let targeted = match evm.select_contracts_and_senders(self.address) {
1292 Ok((_, t)) => t,
1293 Err(e) => {
1294 self.result.invariant_setup_fail(e);
1295 return self.result;
1296 }
1297 };
1298 let dynamic = evm.dynamic_target_ctx();
1299 return self.run_showmap(
1300 func,
1301 corpus_dir,
1302 &showmap,
1303 None,
1304 Some(&targeted),
1305 Some(&dynamic),
1306 );
1307 }
1308
1309 let current_settings = match evm.compute_settings(self.address) {
1312 Ok(s) => s,
1313 Err(e) => {
1314 self.result.invariant_setup_fail(e);
1315 return self.result;
1316 }
1317 };
1318 let persisted_invariants = if is_optimization {
1321 BTreeSet::new()
1322 } else {
1323 live_invariants
1324 .iter()
1325 .filter(|(invariant_fn, _)| *invariant_fn != campaign_anchor)
1326 .filter_map(|(invariant_fn, _)| {
1327 persisted_invariant_failure(&failure_dir, invariant_fn, ¤t_settings)
1328 .is_some()
1329 .then_some(invariant_fn.name.as_str())
1330 })
1331 .collect::<BTreeSet<_>>()
1332 };
1333 if !is_optimization {
1338 let persisted_skipped: Vec<&str> = live_invariants
1339 .iter()
1340 .filter(|(invariant_fn, _)| {
1341 *invariant_fn != campaign_anchor
1342 && persisted_invariants.contains(invariant_fn.name.as_str())
1343 })
1344 .map(|(invariant_fn, _)| invariant_fn.name.as_str())
1345 .collect();
1346 if !persisted_skipped.is_empty() {
1347 let _ = sh_warn!(
1348 "{}: {} invariant(s) skipped due to persisted failures: {}. \
1349 Run `forge clean` or delete files in {} to re-include.",
1350 self.cr.name,
1351 persisted_skipped.len(),
1352 persisted_skipped.join(", "),
1353 failure_dir.display(),
1354 );
1355 }
1356 }
1357 let invariant_fns: Vec<(&Function, bool)> = live_invariants
1362 .into_iter()
1363 .filter(|(invariant_fn, _)| {
1364 *invariant_fn == campaign_anchor
1365 || (!is_optimization
1366 && !persisted_invariants.contains(invariant_fn.name.as_str()))
1367 })
1368 .collect();
1369 let anchor_idx = invariant_fns
1370 .iter()
1371 .position(|(invariant_fn, _)| *invariant_fn == campaign_anchor)
1372 .expect("campaign anchor must be present in invariant_fns");
1373 let predicate_count = invariant_fns.len() + skipped_predicate_results.len();
1374 let invariant_contract = InvariantContract::new(
1375 self.address,
1376 self.cr.name,
1377 invariant_fns,
1378 anchor_idx,
1379 call_after_invariant,
1380 &self.cr.contract.abi,
1381 );
1382 let show_solidity = invariant_config.show_solidity;
1383 let is_campaign = predicate_count > 1;
1384 let invariant_count = is_campaign.then_some(predicate_count);
1385 let invariant_display_name = if is_campaign {
1386 Cow::Owned(invariant_campaign_display_name(self.cr.name))
1387 } else {
1388 Cow::Borrowed(func.name.as_str())
1389 };
1390
1391 let progress = start_fuzz_progress(
1392 self.cr.progress,
1393 self.cr.name,
1394 invariant_display_name.as_ref(),
1395 invariant_config.timeout,
1396 invariant_config.runs,
1397 );
1398
1399 let replay_ctx = ReplayContext {
1400 invariant_contract: &invariant_contract,
1401 invariant_config,
1402 revert_decoder: self.revert_decoder(),
1403 show_solidity,
1404 };
1405
1406 let primary_failure_file =
1408 invariant_failure_file(&failure_dir, invariant_contract.anchor());
1409 let persisted_primary = persisted_invariant_failure(
1410 &failure_dir,
1411 invariant_contract.anchor(),
1412 ¤t_settings,
1413 );
1414 if let Some(InvariantPersistedFailure { mut call_sequence, assertion_failure, .. }) =
1415 persisted_primary
1416 {
1417 let (txes, replay) = replay_persisted_call_sequence(
1418 &replay_ctx,
1419 self.clone_executor(),
1420 &mut call_sequence,
1421 assertion_failure,
1422 );
1423 if let Ok((success, replayed_entirely, replay_reason)) = replay
1424 && !success
1425 {
1426 let warn =
1427 "Replayed invariant failure from persisted file. \nRun `forge clean` or remove file to ignore failure and to continue invariant test campaign."
1428 .to_string();
1429
1430 if let Some(ref progress) = progress {
1431 progress.set_prefix(format!("{invariant_display_name}\n{warn}\n"));
1432 } else {
1433 let _ = sh_warn!("{warn}");
1434 }
1435
1436 match replay_error(
1439 evm.config(),
1440 self.clone_executor(),
1441 &txes,
1442 None,
1443 assertion_failure,
1444 None, &invariant_contract,
1446 invariant_contract.anchor(),
1447 &self.cr.mcr.known_contracts,
1448 identified_contracts.clone(),
1449 &mut self.result.logs,
1450 &mut self.result.traces,
1451 &mut self.result.debug_bytecodes,
1452 &mut self.result.line_coverage,
1453 &mut self.result.deprecated_cheatcodes,
1454 progress.as_ref(),
1455 &self.tcfg.early_exit,
1456 None, ) {
1458 Ok(replayed_call_sequence) if !replayed_call_sequence.is_empty() => {
1459 call_sequence = replayed_call_sequence;
1460 record_invariant_failure(
1462 failure_dir.as_path(),
1463 primary_failure_file.as_path(),
1464 &call_sequence,
1465 ¤t_settings,
1466 assertion_failure,
1467 );
1468 }
1469 Ok(_) => {}
1470 Err(err) => {
1471 error!(%err, "Failed to replay invariant error");
1472 }
1473 }
1474
1475 self.result.invariant_replay_fail(
1476 replayed_entirely,
1477 &invariant_contract.anchor().name,
1478 replay_reason,
1479 call_sequence,
1480 );
1481 return self.result;
1482 }
1483 }
1484
1485 let persisted_handler_failures = replay_persisted_handler_failures(
1488 &failure_dir.join("handlers"),
1489 ¤t_settings,
1490 self.clone_executor(),
1491 &replay_ctx,
1492 );
1493
1494 let invariant_result = match evm.invariant_fuzz(
1495 invariant_contract.clone(),
1496 &self.setup.fuzz_fixtures,
1497 self.build_fuzz_state(true),
1498 progress.as_ref(),
1499 &self.tcfg.early_exit,
1500 persisted_handler_failures,
1501 ) {
1502 Ok(x) => x,
1503 Err(e) => {
1504 self.result.invariant_setup_fail(e);
1505 return self.result;
1506 }
1507 };
1508 self.result.merge_coverages(invariant_result.line_coverage);
1510
1511 let mut counterexample = None;
1512 let success =
1514 invariant_result.errors.is_empty() && invariant_result.handler_errors.is_empty();
1515 let mut invariant_failures: Vec<InvariantFailure> = vec![];
1516 let mut any_failure_persisted = false;
1517
1518 if success {
1519 if let Some(best_value) = invariant_result.optimization_best_value {
1520 match replay_error(
1522 evm.config(),
1523 self.clone_executor(),
1524 &invariant_result.optimization_best_sequence,
1525 None,
1526 false,
1527 Some(best_value),
1528 &invariant_contract,
1529 invariant_contract.anchor(),
1530 &self.cr.mcr.known_contracts,
1531 identified_contracts.clone(),
1532 &mut self.result.logs,
1533 &mut self.result.traces,
1534 &mut self.result.debug_bytecodes,
1535 &mut self.result.line_coverage,
1536 &mut self.result.deprecated_cheatcodes,
1537 progress.as_ref(),
1538 &self.tcfg.early_exit,
1539 None, ) {
1541 Ok(best_sequence) if !best_sequence.is_empty() => {
1542 counterexample = Some(CounterExample::Sequence(
1543 invariant_result.optimization_best_sequence.len(),
1544 best_sequence,
1545 ));
1546 }
1547 Err(err) => {
1548 error!(%err, "Failed to replay optimization best sequence");
1549 }
1550 _ => {}
1551 }
1552 } else {
1553 if let Err(err) = replay_run(
1555 &invariant_contract,
1556 invariant_contract.anchor(),
1557 self.clone_executor(),
1558 &self.cr.mcr.known_contracts,
1559 identified_contracts.clone(),
1560 &mut self.result.logs,
1561 &mut self.result.traces,
1562 &mut self.result.debug_bytecodes,
1563 &mut self.result.line_coverage,
1564 &mut self.result.deprecated_cheatcodes,
1565 &invariant_result.last_run_inputs,
1566 show_solidity,
1567 ) {
1568 error!(%err, "Failed to replay last invariant run");
1569 }
1570 }
1571 } else {
1572 let total_broken = invariant_result.errors.len();
1576 if let Some(error) = invariant_result.errors.get(&invariant_contract.anchor().name) {
1581 let anchor_counterexample = match error {
1582 InvariantFuzzError::BrokenInvariant(case_data)
1583 | InvariantFuzzError::Revert(case_data) => {
1584 let TestError::Fail(_, ref calls) = case_data.test_error else {
1585 unreachable!("FailedInvariantCaseData::new always sets TestError::Fail")
1586 };
1587 match replay_error(
1588 evm.config(),
1589 self.clone_executor(),
1590 calls,
1591 Some(case_data.inner_sequence.clone()),
1592 case_data.assertion_failure,
1593 None, &invariant_contract,
1595 invariant_contract.anchor(),
1596 &self.cr.mcr.known_contracts,
1597 identified_contracts.clone(),
1598 &mut self.result.logs,
1599 &mut self.result.traces,
1600 &mut self.result.debug_bytecodes,
1601 &mut self.result.line_coverage,
1602 &mut self.result.deprecated_cheatcodes,
1603 progress.as_ref(),
1604 &self.tcfg.early_exit,
1605 Some((1, total_broken)),
1606 ) {
1607 Ok(call_sequence) if !call_sequence.is_empty() => {
1608 record_invariant_failure(
1609 failure_dir.as_path(),
1610 primary_failure_file.as_path(),
1611 &call_sequence,
1612 ¤t_settings,
1613 case_data.assertion_failure,
1614 );
1615 any_failure_persisted = true;
1616 Some(CounterExample::Sequence(calls.len(), call_sequence))
1617 }
1618 Ok(_) => None,
1619 Err(err) => {
1620 error!(%err, "Failed to replay invariant error");
1621 None
1622 }
1623 }
1624 }
1625 InvariantFuzzError::MaxAssumeRejects(_) => None,
1626 InvariantFuzzError::HandlerAssertion(_) => None,
1628 };
1629 invariant_failures.push(InvariantFailure::Predicate {
1630 name: invariant_contract.anchor().name.clone(),
1631 reason: error.revert_reason().unwrap_or_default(),
1632 counterexample: anchor_counterexample,
1633 persisted_path: primary_failure_file,
1634 is_anchor: true,
1635 });
1636 }
1637
1638 let mut next_position = 2usize;
1647 for (idx, (invariant, _)) in invariant_contract.invariant_fns.iter().enumerate() {
1649 if idx == invariant_contract.anchor_idx {
1650 continue;
1651 }
1652
1653 let persisted_failure = invariant_failure_file(&failure_dir, invariant);
1659 if !persisted_invariants.contains(invariant.name.as_str())
1660 && let Some(error) = invariant_result.errors.get(&invariant.name)
1661 && let InvariantFuzzError::BrokenInvariant(case_data)
1662 | InvariantFuzzError::Revert(case_data) = error
1663 && let TestError::Fail(_, ref calls) = case_data.test_error
1664 {
1665 let original_seq_len = calls.len();
1666 let secondary_counterexample = if self.tcfg.early_exit.should_stop() {
1671 let unshrunk_sequence = calls
1672 .iter()
1673 .map(|tx| {
1674 BaseCounterExample::from_invariant_call(
1675 tx,
1676 identified_contracts,
1677 None,
1678 invariant_config.show_solidity,
1679 )
1680 })
1681 .collect::<Vec<_>>();
1682 record_invariant_failure(
1683 failure_dir.as_path(),
1684 persisted_failure.as_path(),
1685 &unshrunk_sequence,
1686 ¤t_settings,
1687 case_data.assertion_failure,
1688 );
1689 any_failure_persisted = true;
1690 None
1691 } else {
1692 let position = next_position;
1693 next_position += 1;
1694 match replay_error(
1695 invariant_config.clone(),
1696 self.clone_executor(),
1697 calls,
1698 Some(case_data.inner_sequence.clone()),
1699 case_data.assertion_failure,
1700 None, &invariant_contract,
1702 invariant,
1703 &self.cr.mcr.known_contracts,
1704 identified_contracts.clone(),
1705 &mut self.result.logs,
1706 &mut self.result.traces,
1707 &mut self.result.debug_bytecodes,
1708 &mut self.result.line_coverage,
1709 &mut self.result.deprecated_cheatcodes,
1710 progress.as_ref(),
1711 &self.tcfg.early_exit,
1712 Some((position, total_broken)),
1713 ) {
1714 Ok(call_sequence) if !call_sequence.is_empty() => {
1715 record_invariant_failure(
1716 failure_dir.as_path(),
1717 persisted_failure.as_path(),
1718 &call_sequence,
1719 ¤t_settings,
1720 case_data.assertion_failure,
1721 );
1722 any_failure_persisted = true;
1723 Some(CounterExample::Sequence(original_seq_len, call_sequence))
1724 }
1725 Ok(_) => None,
1726 Err(err) => {
1727 error!(%err, "Failed to replay invariant error");
1728 None
1729 }
1730 }
1731 };
1732 invariant_failures.push(InvariantFailure::Predicate {
1733 name: invariant.name.clone(),
1734 reason: error.revert_reason().unwrap_or_default(),
1735 counterexample: secondary_counterexample,
1736 persisted_path: persisted_failure.clone(),
1737 is_anchor: false,
1738 });
1739 }
1740 }
1741 }
1742
1743 let invariant_failure_dir = any_failure_persisted.then(|| failure_dir.clone());
1744 let invariant_predicate_results = if is_campaign {
1745 let failures_by_name = invariant_failures
1746 .iter()
1747 .map(|failure| (failure.name(), failure))
1748 .collect::<BTreeMap<_, _>>();
1749 invariant_contract
1750 .invariant_fns
1751 .iter()
1752 .map(|(invariant, _)| {
1753 if let Some(failure) = failures_by_name.get(invariant.name.as_str()) {
1754 InvariantPredicateResult {
1755 name: invariant.name.clone(),
1756 status: TestStatus::Failure,
1757 reason: Some(failure.reason().to_string()),
1758 }
1759 } else {
1760 InvariantPredicateResult {
1761 name: invariant.name.clone(),
1762 status: TestStatus::Success,
1763 reason: None,
1764 }
1765 }
1766 })
1767 .chain(skipped_predicate_results)
1768 .sorted_by_key(|predicate| {
1769 self.cr
1770 .contract
1771 .abi
1772 .functions()
1773 .position(|func| func.name == predicate.name)
1774 .unwrap_or(usize::MAX)
1775 })
1776 .collect()
1777 } else {
1778 Vec::new()
1779 };
1780
1781 let identified_contracts_ro = identified_contracts;
1787 let invariant_handler_failures = invariant_result
1788 .handler_errors
1789 .iter()
1790 .sorted_by(|(ka, _), (kb, _)| {
1791 ka.cmp(kb)
1793 })
1794 .filter_map(|(site, err)| err.as_handler_assertion().map(|f| (site, f)))
1795 .map(|(_site, failure)| {
1796 let reverter = failure.reverter;
1797 let selector = failure.selector;
1798 let resolved_name = identified_contracts_ro
1800 .get(&reverter)
1801 .and_then(|(contract_name, abi)| {
1802 abi.functions()
1803 .find(|f| f.selector() == selector)
1804 .map(|f| format!("{contract_name}::{}", f.name))
1805 })
1806 .unwrap_or_else(|| format!("{reverter}::{selector}"));
1807
1808 let counterexample_calls = failure
1809 .call_sequence
1810 .iter()
1811 .map(|tx| {
1812 BaseCounterExample::from_invariant_call(
1813 tx,
1814 identified_contracts_ro,
1815 None,
1816 invariant_config.show_solidity,
1817 )
1818 })
1819 .collect::<Vec<_>>();
1820
1821 if !counterexample_calls.is_empty() {
1823 record_handler_failure(
1824 failure_dir.as_path(),
1825 reverter,
1826 selector,
1827 &counterexample_calls,
1828 ¤t_settings,
1829 );
1830 }
1831
1832 let counterexample = if counterexample_calls.is_empty() {
1833 None
1834 } else {
1835 Some(CounterExample::Sequence(
1837 failure.original_sequence_len,
1838 counterexample_calls,
1839 ))
1840 };
1841
1842 InvariantFailure::Handler {
1843 name: resolved_name,
1844 reverter,
1845 selector,
1846 reason: failure.revert_reason.clone(),
1847 counterexample,
1848 }
1849 })
1850 .collect::<Vec<_>>();
1851
1852 self.result.invariant_result(
1853 invariant_result.gas_report_traces,
1854 success,
1855 invariant_failures,
1856 invariant_predicate_results,
1857 invariant_failure_dir,
1858 invariant_count,
1859 invariant_handler_failures,
1860 counterexample,
1861 invariant_result.runs,
1862 invariant_result.calls,
1863 invariant_result.reverts,
1864 invariant_result.metrics,
1865 invariant_result.failed_corpus_replays,
1866 invariant_result.workers,
1867 invariant_result.optimization_best_value,
1868 );
1869 self.result
1870 }
1871
1872 fn invariant_skip_reason(&self, func: &Function) -> Option<SkipReason> {
1873 match self.executor.call(
1874 self.sender,
1875 self.address,
1876 func,
1877 &[],
1878 U256::ZERO,
1879 Some(self.revert_decoder()),
1880 ) {
1881 Err(EvmError::Skip(reason)) => Some(reason),
1882 _ => None,
1883 }
1884 }
1885
1886 fn run_fuzz_test(mut self, func: &Function) -> TestResult {
1896 if self.prepare_test(func).is_err() {
1898 return self.result;
1899 }
1900
1901 let runner = self.fuzz_runner();
1902 let mut fuzz_config = self.config.fuzz.clone();
1903 let (failure_dir, failure_file) = test_paths(
1904 &mut fuzz_config.corpus,
1905 fuzz_config.failure_persist_dir.clone().unwrap(),
1906 self.cr.name,
1907 &func.name,
1908 );
1909
1910 if let Some(showmap) = self.cr.mcr.tcfg.showmap.clone() {
1913 let corpus_dir =
1914 showmap.corpus_dir.clone().or_else(|| fuzz_config.corpus.corpus_dir.clone());
1915 return self.run_showmap(func, corpus_dir, &showmap, Some(func), None, None);
1916 }
1917
1918 let progress = start_fuzz_progress(
1919 self.cr.progress,
1920 self.cr.name,
1921 &func.name,
1922 fuzz_config.timeout,
1923 if fuzz_config.run.is_some() { 1 } else { fuzz_config.runs },
1924 );
1925
1926 let state = self.build_fuzz_state(false);
1927 let mut executor = self.executor.into_owned();
1928 executor.inspector_mut().collect_edge_coverage_with_config(&fuzz_config.corpus);
1931 executor.inspector_mut().collect_evm_cmp_log(fuzz_config.corpus.collect_evm_cmp_log());
1932 executor.inspector_mut().collect_sancov_edges(fuzz_config.corpus.collect_sancov_edges());
1933 executor
1934 .inspector_mut()
1935 .collect_sancov_trace_cmp(fuzz_config.corpus.collect_sancov_trace_cmp());
1936 let persisted_failure =
1938 foundry_common::fs::read_json_file::<BaseCounterExample>(failure_file.as_path()).ok();
1939 let mut fuzzed_executor =
1941 FuzzedExecutor::new(executor, runner, self.tcfg.sender, fuzz_config, persisted_failure);
1942 let result = match fuzzed_executor.fuzz(
1943 func,
1944 &self.setup.fuzz_fixtures,
1945 state,
1946 self.address,
1947 &self.cr.mcr.revert_decoder,
1948 progress.as_ref(),
1949 &self.tcfg.early_exit,
1950 &self.cr.tokio_handle,
1951 ) {
1952 Ok(x) => x,
1953 Err(e) => {
1954 self.result.fuzz_setup_fail(e);
1955 return self.result;
1956 }
1957 };
1958
1959 if let Some(CounterExample::Single(counterexample)) = &result.counterexample {
1961 if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) {
1962 error!(%err, "Failed to create fuzz failure dir");
1963 } else if let Err(err) =
1964 foundry_common::fs::write_json_file(failure_file.as_path(), counterexample)
1965 {
1966 error!(%err, "Failed to record call sequence");
1967 }
1968 }
1969
1970 self.result.fuzz_result(result);
1971 self.result
1972 }
1973
1974 fn prepare_test(&mut self, func: &Function) -> Result<(), ()> {
1984 let address = self.setup.address;
1985
1986 if self.cr.contract.abi.functions().any(|func| func.name.is_before_test_setup()) {
1988 for calldata in self.executor.call_sol_default(
1989 address,
1990 &ITest::beforeTestSetupCall { testSelector: func.selector() },
1991 ) {
1992 let spec_id: SpecId = self.executor.spec_id().into();
1993 debug!(?calldata, spec=%spec_id, "applying before_test_setup");
1994 match self.executor.to_mut().transact_raw(
1996 self.tcfg.sender,
1997 address,
1998 calldata,
1999 U256::ZERO,
2000 ) {
2001 Ok(call_result) => {
2002 let reverted = call_result.reverted;
2003
2004 self.result.extend(call_result);
2006
2007 if reverted {
2009 self.result.single_fail(None);
2010 return Err(());
2011 }
2012 }
2013 Err(_) => {
2014 self.result.single_fail(None);
2015 return Err(());
2016 }
2017 }
2018 }
2019 }
2020 Ok(())
2021 }
2022
2023 fn fuzz_runner(&self) -> TestRunner {
2024 let config = &self.config.fuzz;
2025 fuzzer_with_cases(config.seed, config.runs, config.max_test_rejects)
2026 }
2027
2028 fn run_showmap(
2030 mut self,
2031 func: &Function,
2032 corpus_dir: Option<PathBuf>,
2033 showmap: &crate::multi_runner::ShowmapConfig,
2034 fuzzed_function: Option<&Function>,
2035 fuzzed_contracts: Option<&foundry_evm::fuzz::invariant::FuzzRunIdentifiedContracts>,
2036 dynamic: Option<&foundry_evm::executors::DynamicTargetCtx<'_>>,
2037 ) -> TestResult {
2038 let Some(corpus_dir) = corpus_dir else {
2039 self.result.replay_skip("no corpus_dir configured for this test");
2040 return self.result;
2041 };
2042
2043 let mut executor = self.clone_executor();
2050 let domain = showmap.domain;
2051 executor.inspector_mut().collect_line_coverage(domain.includes_evm());
2052 executor.inspector_mut().collect_sancov_edges(domain.includes_sancov());
2053
2054 let safe_id = self.cr.name.replace(['/', '\\', ':'], "_");
2059 let approach = if fuzzed_contracts.is_some() {
2060 format!("{}__{safe_id}", showmap.approach)
2061 } else {
2062 let safe_fn = func.name.replace(['/', '\\', ':', '(', ')', ',', ' '], "_");
2063 format!("{}__{safe_id}__{safe_fn}", showmap.approach)
2064 };
2065 let opts = ShowmapOpts {
2066 out_dir: showmap.out_dir.clone(),
2067 approach,
2068 trial: showmap.trial.clone(),
2069 per_input: showmap.per_input,
2070 domain,
2071 };
2072
2073 let start = std::time::Instant::now();
2074 let result = replay_corpus_to_showmap(
2075 &executor,
2076 &corpus_dir,
2077 fuzzed_function,
2078 fuzzed_contracts,
2079 dynamic,
2080 &opts,
2081 );
2082 let duration = start.elapsed();
2083 match result {
2084 Ok(stats) => {
2085 if stats.sancov_requested && !stats.sancov_observed && stats.corpus_entries > 0 {
2086 let _ = sh_warn!(
2087 "{}::{}: sancov coverage requested but no hits observed (build is likely not sancov-instrumented)",
2088 self.cr.name,
2089 func.name,
2090 );
2091 }
2092 self.result.replay_result(
2093 stats.corpus_entries,
2094 stats.showmap_files,
2095 stats.skipped_entries,
2096 duration,
2097 );
2098 }
2099 Err(e) => {
2100 self.result.single_fail(Some(e.to_string()));
2101 }
2102 }
2103 self.result
2104 }
2105
2106 fn invariant_runner(&self) -> TestRunner {
2107 let config = &self.config.invariant;
2108 fuzzer_with_cases(self.config.fuzz.seed, config.runs, config.max_assume_rejects)
2109 }
2110
2111 fn clone_executor(&self) -> Executor<FEN> {
2112 self.executor.clone().into_owned()
2113 }
2114
2115 fn build_fuzz_state(&self, invariant: bool) -> EvmFuzzState {
2116 let config =
2117 if invariant { self.config.invariant.dictionary } else { self.config.fuzz.dictionary };
2118 if let Some(db) = self.executor.backend().active_fork_db() {
2119 EvmFuzzState::new(
2120 &self.setup.deployed_libs,
2121 db,
2122 config,
2123 Some(&self.cr.mcr.fuzz_literals),
2124 )
2125 } else {
2126 let db = self.executor.backend().mem_db();
2127 EvmFuzzState::new(
2128 &self.setup.deployed_libs,
2129 db,
2130 config,
2131 Some(&self.cr.mcr.fuzz_literals),
2132 )
2133 }
2134 }
2135}
2136
2137fn fuzzer_with_cases(seed: Option<U256>, cases: u32, max_global_rejects: u32) -> TestRunner {
2138 let config = proptest::test_runner::Config {
2139 cases,
2140 max_global_rejects,
2141 max_shrink_iters: 0,
2144 ..Default::default()
2145 };
2146
2147 if let Some(seed) = seed {
2148 trace!(target: "forge::test", %seed, "building deterministic fuzzer");
2149 let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
2150 TestRunner::new_with_rng(config, rng)
2151 } else {
2152 trace!(target: "forge::test", "building stochastic fuzzer");
2153 TestRunner::new(config)
2154 }
2155}
2156
2157#[derive(Serialize, Deserialize)]
2159struct InvariantPersistedFailure {
2160 call_sequence: Vec<BaseCounterExample>,
2162 settings: InvariantSettings,
2165 #[serde(default)]
2167 assertion_failure: bool,
2168}
2169
2170type CheckSequenceResult = eyre::Result<(bool, bool, Option<String>)>;
2172
2173struct ReplayContext<'a> {
2175 invariant_contract: &'a InvariantContract<'a>,
2176 invariant_config: &'a InvariantConfig,
2177 revert_decoder: &'a RevertDecoder,
2178 show_solidity: bool,
2179}
2180
2181fn persisted_call_sequence(
2184 path: &Path,
2185 current_settings: &InvariantSettings,
2186) -> Option<InvariantPersistedFailure> {
2187 foundry_common::fs::read_json_file::<InvariantPersistedFailure>(path).ok().and_then(
2188 |persisted_failure| {
2189 if let Some(diff) = persisted_failure.settings.diff(current_settings) {
2190 let _ = sh_warn!(
2191 "Failure from {:?} file was ignored because invariant test settings have changed: {}",
2192 path,
2193 diff
2194 );
2195 return None;
2196 }
2197 Some(persisted_failure)
2198 },
2199 )
2200}
2201
2202fn invariant_failure_file(failure_dir: &Path, invariant: &Function) -> PathBuf {
2204 canonicalized(failure_dir.join("invariants").join(&invariant.name))
2205}
2206
2207fn legacy_invariant_failure_file(failure_dir: &Path, invariant: &Function) -> PathBuf {
2209 canonicalized(failure_dir.join(&invariant.name))
2210}
2211
2212fn persisted_invariant_failure(
2214 failure_dir: &Path,
2215 invariant: &Function,
2216 current_settings: &InvariantSettings,
2217) -> Option<InvariantPersistedFailure> {
2218 persisted_call_sequence(invariant_failure_file(failure_dir, invariant).as_path(), current_settings)
2219 .or_else(|| {
2220 let legacy_path = legacy_invariant_failure_file(failure_dir, invariant);
2221 let persisted = persisted_call_sequence(legacy_path.as_path(), current_settings)?;
2222 let _ = sh_warn!(
2223 "Using legacy invariant failure cache at {}; new failures will be persisted under {}/invariants.",
2224 legacy_path.display(),
2225 failure_dir.display(),
2226 );
2227 Some(persisted)
2228 })
2229}
2230
2231fn base_counterexamples_to_txes(
2233 ctx: &ReplayContext<'_>,
2234 call_sequence: &mut [BaseCounterExample],
2235) -> Vec<BasicTxDetails> {
2236 call_sequence
2237 .iter_mut()
2238 .map(|seq| {
2239 seq.show_solidity = ctx.show_solidity;
2240 BasicTxDetails {
2241 warp: seq.warp,
2242 roll: seq.roll,
2243 sender: seq.sender.unwrap_or_default(),
2244 call_details: CallDetails {
2245 target: seq.addr.unwrap_or_default(),
2246 calldata: seq.calldata.clone(),
2247 value: seq.value,
2248 },
2249 }
2250 })
2251 .collect()
2252}
2253
2254fn replay_persisted_call_sequence<FEN: FoundryEvmNetwork>(
2257 ctx: &ReplayContext<'_>,
2258 executor: Executor<FEN>,
2259 call_sequence: &mut [BaseCounterExample],
2260 expect_assertion_failure: bool,
2261) -> (Vec<BasicTxDetails>, CheckSequenceResult) {
2262 let txes = base_counterexamples_to_txes(ctx, call_sequence);
2263 let result = check_sequence(
2264 executor,
2265 &txes,
2266 (0..min(txes.len(), ctx.invariant_config.depth as usize)).collect(),
2267 ctx.invariant_contract.address,
2268 ctx.invariant_contract.anchor().selector().to_vec().into(),
2269 CheckSequenceOptions {
2270 accumulate_warp_roll: ctx.invariant_config.has_delay(),
2271 fail_on_revert: ctx.invariant_config.fail_on_revert,
2272 expect_assertion_failure,
2273 call_after_invariant: ctx.invariant_contract.call_after_invariant,
2274 rd: Some(ctx.revert_decoder),
2275 },
2276 );
2277 (txes, result)
2278}
2279
2280fn test_paths(
2282 corpus_config: &mut FuzzCorpusConfig,
2283 persist_dir: PathBuf,
2284 contract_name: &str,
2285 test_name: &str,
2286) -> (PathBuf, PathBuf) {
2287 let contract = contract_name.split(':').next_back().unwrap();
2288 corpus_config.with_test(contract, test_name);
2290
2291 let failures_dir = canonicalized(persist_dir.join("failures").join(contract));
2292 let failure_file = canonicalized(failures_dir.join(test_name));
2293 (failures_dir, failure_file)
2294}
2295
2296fn invariant_suite_paths(
2298 corpus_config: &mut FuzzCorpusConfig,
2299 persist_dir: PathBuf,
2300 contract_name: &str,
2301 invariant_name: &str,
2302 is_optimization: bool,
2303) -> PathBuf {
2304 let failure_dir = invariant_failure_dir(persist_dir, contract_name);
2305 let contract = invariant_contract_name(contract_name);
2306 if let Some(corpus_dir) = &corpus_config.corpus_dir {
2307 let mut corpus_dir = corpus_dir.join(contract);
2308 if is_optimization {
2309 corpus_dir = corpus_dir.join(invariant_name);
2310 }
2311 corpus_config.corpus_dir = Some(canonicalized(corpus_dir));
2312 }
2313
2314 failure_dir
2315}
2316
2317fn invariant_failure_dir(persist_dir: PathBuf, contract_name: &str) -> PathBuf {
2319 canonicalized(persist_dir.join("failures").join(invariant_contract_name(contract_name)))
2320}
2321
2322fn invariant_contract_name(contract_name: &str) -> &str {
2324 contract_name.split(':').next_back().unwrap()
2325}
2326
2327fn record_invariant_failure(
2329 failure_dir: &Path,
2330 failure_file: &Path,
2331 call_sequence: &[BaseCounterExample],
2332 settings: &InvariantSettings,
2333 assertion_failure: bool,
2334) {
2335 if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) {
2336 error!(%err, "Failed to create invariant failure dir");
2337 return;
2338 }
2339 if let Some(parent) = failure_file.parent()
2340 && let Err(err) = foundry_common::fs::create_dir_all(parent)
2341 {
2342 error!(%err, "Failed to create invariant failure file parent dir");
2343 return;
2344 }
2345
2346 if let Err(err) = foundry_common::fs::write_json_file(
2347 failure_file,
2348 &InvariantPersistedFailure {
2349 call_sequence: call_sequence.to_owned(),
2350 settings: settings.clone(),
2351 assertion_failure,
2352 },
2353 ) {
2354 error!(%err, "Failed to record call sequence");
2355 }
2356}
2357
2358fn record_handler_failure(
2361 failure_dir: &Path,
2362 reverter: Address,
2363 selector: Selector,
2364 call_sequence: &[BaseCounterExample],
2365 settings: &InvariantSettings,
2366) {
2367 let handlers_dir = failure_dir.join("handlers");
2368 if let Err(err) = foundry_common::fs::create_dir_all(&handlers_dir) {
2369 error!(%err, "Failed to create handler failure dir");
2370 return;
2371 }
2372 let mut buf = [0u8; 24];
2373 buf[..20].copy_from_slice(reverter.as_slice());
2374 buf[20..].copy_from_slice(selector.as_slice());
2375 let site_hash = alloy_primitives::keccak256(buf);
2376 let file = handlers_dir.join(format!("{site_hash:x}.json"));
2377 record_invariant_failure(&handlers_dir, &file, call_sequence, settings, true);
2378}
2379
2380fn replay_persisted_handler_failures<FEN: FoundryEvmNetwork>(
2384 handlers_dir: &Path,
2385 current_settings: &InvariantSettings,
2386 executor: Executor<FEN>,
2387 ctx: &ReplayContext<'_>,
2388) -> std::collections::HashMap<(Address, Selector), InvariantFuzzError> {
2389 let mut replayed: std::collections::HashMap<(Address, Selector), InvariantFuzzError> =
2390 std::collections::HashMap::new();
2391 let entries = match std::fs::read_dir(handlers_dir) {
2392 Ok(e) => e,
2393 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return replayed,
2394 Err(err) => {
2395 error!(%err, "Failed to read handler failure dir");
2396 return replayed;
2397 }
2398 };
2399 for entry in entries.flatten() {
2400 let path = entry.path();
2401 if path.extension().and_then(|s| s.to_str()) != Some("json") {
2402 continue;
2403 }
2404 let Some(persisted) = persisted_call_sequence(&path, current_settings) else {
2405 continue;
2406 };
2407 let mut call_sequence = persisted.call_sequence;
2408 if call_sequence.is_empty() {
2409 let _ = std::fs::remove_file(&path);
2410 continue;
2411 }
2412 let txes = base_counterexamples_to_txes(ctx, &mut call_sequence);
2413 let Some(last) = txes.last() else {
2415 let _ = std::fs::remove_file(&path);
2416 continue;
2417 };
2418 let expected_target = last.call_details.target;
2419 let expected_selector_bytes: [u8; 4] =
2420 last.call_details.calldata.get(..4).and_then(|s| s.try_into().ok()).unwrap_or_default();
2421 let expected_site = (expected_target, Selector::from(expected_selector_bytes));
2422 let sequence: Vec<usize> =
2423 (0..min(txes.len(), ctx.invariant_config.depth as usize)).collect();
2424 let outcome = replay_handler_failure_sequence(
2425 executor.clone(),
2426 &txes,
2427 sequence,
2428 ctx.invariant_config.has_delay(),
2429 Some(ctx.revert_decoder),
2430 );
2431 match outcome {
2432 Ok(outcome) if outcome.anchor_asserted => {
2433 let _ = sh_warn!(
2434 "Replayed handler-side assertion bug from {path:?}. \nRun `forge clean` or remove file to ignore."
2435 );
2436 let failure = HandlerAssertionFailure::from_replayed_sequence(
2437 txes,
2438 outcome.anchor_fingerprint,
2439 outcome.revert_reason.unwrap_or_default(),
2440 );
2441 let already_shorter = replayed
2444 .get(&expected_site)
2445 .and_then(InvariantFuzzError::as_handler_assertion)
2446 .is_some_and(|existing| {
2447 existing.call_sequence.len() <= failure.call_sequence.len()
2448 });
2449 if !already_shorter {
2450 replayed.insert(expected_site, InvariantFuzzError::HandlerAssertion(failure));
2451 }
2452 }
2453 Ok(_) => {
2455 let _ = std::fs::remove_file(&path);
2456 }
2457 Err(err) => {
2458 error!(%err, "Failed to replay handler-side assertion bug");
2459 }
2460 }
2461 }
2462 replayed
2463}