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::{InvariantFailure, SuiteResult, TestResult, TestSetup},
10};
11use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
12use alloy_json_abi::Function;
13use alloy_primitives::{Address, Bytes, Selector, U256, address, map::HashMap};
14use eyre::Result;
15use foundry_common::{TestFunctionExt, TestFunctionKind, contracts::ContractsByAddress};
16use foundry_compilers::utils::canonicalized;
17use foundry_config::{Config, FuzzCorpusConfig, InvariantConfig};
18use foundry_evm::{
19 constants::CALLER,
20 core::evm::FoundryEvmNetwork,
21 decode::RevertDecoder,
22 executors::{
23 CallResult, EvmError, Executor, ITest, RawCallResult,
24 fuzz::FuzzedExecutor,
25 invariant::{
26 CheckSequenceOptions, HandlerAssertionFailure, InvariantExecutor, InvariantFuzzError,
27 check_sequence, replay_error, replay_handler_failure_sequence, replay_run,
28 },
29 },
30 fuzz::{
31 BasicTxDetails, CallDetails, CounterExample, FuzzFixtures, fixture_name,
32 invariant::{InvariantContract, InvariantSettings, is_optimization_invariant},
33 strategies::EvmFuzzState,
34 },
35 revm::primitives::hardfork::SpecId,
36 traces::{TraceKind, TraceMode, load_contracts},
37};
38use itertools::Itertools;
39use proptest::test_runner::{RngAlgorithm, TestError, TestRng, TestRunner};
40use rayon::prelude::*;
41use serde::{Deserialize, Serialize};
42use std::{
43 borrow::Cow,
44 cmp::min,
45 collections::BTreeMap,
46 ops::Deref,
47 path::{Path, PathBuf},
48 sync::Arc,
49 time::Instant,
50};
51use tokio::signal;
52use tracing::Span;
53
54pub const LIBRARY_DEPLOYER: Address = address!("0x1F95D37F27EA0dEA9C252FC09D5A6eaA97647353");
60
61pub struct ContractRunner<'a, FEN: FoundryEvmNetwork> {
63 name: &'a str,
65 contract: &'a TestContract,
67 executor: Executor<FEN>,
69 progress: Option<&'a TestsProgress>,
71 tokio_handle: &'a tokio::runtime::Handle,
73 span: tracing::Span,
75 tcfg: Cow<'a, TestRunnerConfig<FEN>>,
77 mcr: &'a MultiContractRunner<FEN>,
79}
80
81impl<'a, FEN: FoundryEvmNetwork> Deref for ContractRunner<'a, FEN> {
82 type Target = Cow<'a, TestRunnerConfig<FEN>>;
83
84 #[inline(always)]
85 fn deref(&self) -> &Self::Target {
86 &self.tcfg
87 }
88}
89
90impl<'a, FEN: FoundryEvmNetwork> ContractRunner<'a, FEN> {
91 pub const fn new(
92 name: &'a str,
93 contract: &'a TestContract,
94 executor: Executor<FEN>,
95 progress: Option<&'a TestsProgress>,
96 tokio_handle: &'a tokio::runtime::Handle,
97 span: Span,
98 mcr: &'a MultiContractRunner<FEN>,
99 ) -> Self {
100 Self {
101 name,
102 contract,
103 executor,
104 progress,
105 tokio_handle,
106 span,
107 tcfg: Cow::Borrowed(&mcr.tcfg),
108 mcr,
109 }
110 }
111
112 fn function_matches_network_pass(&self, func: &Function) -> bool {
119 let multi = &self.mcr.tcfg.multi_network;
120 if multi.all_override_networks.is_empty() {
121 return true;
122 }
123 let profile = &self.tcfg.config.profile;
124 let func_network = self.mcr.inline_config.network_for(profile, self.name, &func.name);
125 match &multi.pass_network {
126 None => func_network.is_none_or(|n| !multi.all_override_networks.contains(&n)),
127 Some(target) => func_network.as_ref() == Some(target),
128 }
129 }
130
131 pub fn setup(&mut self, call_setup: bool) -> TestSetup {
134 self._setup(call_setup).unwrap_or_else(|err| {
135 if err.to_string().contains("skipped") {
136 TestSetup::skipped(err.to_string())
137 } else {
138 TestSetup::failed(err.to_string())
139 }
140 })
141 }
142
143 fn _setup(&mut self, call_setup: bool) -> Result<TestSetup> {
144 trace!(call_setup, "setting up");
145
146 self.apply_contract_inline_config()?;
147
148 self.executor.set_balance(self.sender, U256::MAX)?;
150 self.executor.set_balance(CALLER, U256::MAX)?;
151
152 self.executor.set_nonce(self.sender, 1)?;
154
155 self.executor.set_balance(LIBRARY_DEPLOYER, U256::MAX)?;
157
158 let mut result = TestSetup::default();
159 for code in &self.mcr.libs_to_deploy {
160 let deploy_result = self.executor.deploy(
161 LIBRARY_DEPLOYER,
162 code.clone(),
163 U256::ZERO,
164 Some(&self.mcr.revert_decoder),
165 );
166
167 if let Ok(deployed) = &deploy_result {
169 result.deployed_libs.push(deployed.address);
170 }
171
172 let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
173 result.extend(raw, TraceKind::Deployment);
174 if reason.is_some() {
175 debug!(?reason, "deployment of library failed");
176 result.reason = reason;
177 return Ok(result);
178 }
179 }
180
181 let address = self.sender.create(self.executor.get_nonce(self.sender)?);
182 result.address = address;
183
184 self.executor.set_balance(address, self.initial_balance())?;
187
188 let deploy_result = self.executor.deploy(
190 self.sender,
191 self.contract.bytecode.clone(),
192 U256::ZERO,
193 Some(&self.mcr.revert_decoder),
194 );
195
196 result.deployment_failure = deploy_result.is_err();
197
198 if let Ok(dr) = &deploy_result {
199 debug_assert_eq!(dr.address, address);
200 }
201 let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?;
202 result.extend(raw, TraceKind::Deployment);
203 if reason.is_some() {
204 debug!(?reason, "deployment of test contract failed");
205 result.reason = reason;
206 return Ok(result);
207 }
208
209 self.executor.set_balance(self.sender, self.initial_balance())?;
211 self.executor.set_balance(CALLER, self.initial_balance())?;
212 self.executor.set_balance(LIBRARY_DEPLOYER, self.initial_balance())?;
213
214 self.executor.deploy_create2_deployer()?;
215
216 if call_setup {
218 trace!("calling setUp");
219 let res = self.executor.setup(None, address, Some(&self.mcr.revert_decoder));
220 let (raw, reason) = RawCallResult::from_evm_result(res)?;
221 result.extend(raw, TraceKind::Setup);
222 result.reason = reason;
223 }
224
225 result.fuzz_fixtures = self.fuzz_fixtures(address);
226
227 Ok(result)
228 }
229
230 fn initial_balance(&self) -> U256 {
231 self.evm_opts.initial_balance
232 }
233
234 fn apply_contract_inline_config(&mut self) -> Result<()> {
236 if self.inline_config.contains_contract(self.name) {
237 let new_config = Arc::new(self.inline_config(None)?);
238 self.tcfg.to_mut().reconfigure_with(new_config);
239 let prev_tracer = self.executor.inspector_mut().tracer.take();
240 self.tcfg.configure_executor(&mut self.executor);
241 self.executor.inspector_mut().tracer = prev_tracer;
243 }
244 Ok(())
245 }
246
247 fn inline_config(&self, func: Option<&Function>) -> Result<Config> {
249 let function = func.map(|f| f.name.as_str()).unwrap_or("");
250 let config = self
251 .config
252 .merge_inline_provider(self.mcr.inline_config.provide(self.name, function))?;
253 Ok(config)
254 }
255
256 fn fuzz_fixtures(&mut self, address: Address) -> FuzzFixtures {
272 let mut fixtures = HashMap::default();
273 let fixture_functions = self.contract.abi.functions().filter(|func| func.is_fixture());
274 for func in fixture_functions {
275 if func.inputs.is_empty() {
276 if let Ok(CallResult { raw: _, decoded_result }) =
278 self.executor.call(CALLER, address, func, &[], U256::ZERO, None)
279 {
280 fixtures.insert(fixture_name(func.name.clone()), decoded_result);
281 }
282 } else {
283 let mut vals = Vec::new();
286 let mut index = 0;
287 loop {
288 if let Ok(CallResult { raw: _, decoded_result }) = self.executor.call(
289 CALLER,
290 address,
291 func,
292 &[DynSolValue::Uint(U256::from(index), 256)],
293 U256::ZERO,
294 None,
295 ) {
296 vals.push(decoded_result);
297 } else {
298 break;
301 }
302 index += 1;
303 }
304 fixtures.insert(fixture_name(func.name.clone()), DynSolValue::Array(vals));
305 };
306 }
307 FuzzFixtures::new(fixtures)
308 }
309
310 pub fn run_tests(mut self, filter: &dyn TestFilter) -> SuiteResult {
312 let start = Instant::now();
313 let mut warnings = Vec::new();
314
315 let setup_fns: Vec<_> =
317 self.contract.abi.functions().filter(|func| func.name.is_setup()).collect();
318 let call_setup = setup_fns.len() == 1 && setup_fns[0].name == "setUp";
319 for &setup_fn in &setup_fns {
321 if setup_fn.name != "setUp" {
322 warnings.push(format!(
323 "Found invalid setup function \"{}\" did you mean \"setUp()\"?",
324 setup_fn.signature()
325 ));
326 }
327 }
328
329 if setup_fns.len() > 1 {
331 return SuiteResult::new(
332 start.elapsed(),
333 [("setUp()".to_string(), TestResult::fail("multiple setUp functions".to_string()))]
334 .into(),
335 warnings,
336 );
337 }
338
339 let after_invariant_fns: Vec<_> =
341 self.contract.abi.functions().filter(|func| func.name.is_after_invariant()).collect();
342 if after_invariant_fns.len() > 1 {
343 return SuiteResult::new(
345 start.elapsed(),
346 [(
347 "afterInvariant()".to_string(),
348 TestResult::fail("multiple afterInvariant functions".to_string()),
349 )]
350 .into(),
351 warnings,
352 );
353 }
354 let call_after_invariant = after_invariant_fns.first().is_some_and(|after_invariant_fn| {
355 let match_sig = after_invariant_fn.name == "afterInvariant";
356 if !match_sig {
357 warnings.push(format!(
358 "Found invalid afterInvariant function \"{}\" did you mean \"afterInvariant()\"?",
359 after_invariant_fn.signature()
360 ));
361 }
362 match_sig
363 });
364
365 let invariant_fns: Vec<_> =
368 self.contract.abi.functions().filter(|func| func.is_invariant_test()).collect();
369
370 let invalid_invariants: Vec<_> = invariant_fns
375 .iter()
376 .filter(|f| !f.inputs.is_empty())
377 .map(|f| {
378 (
379 f.signature(),
380 TestResult::fail(format!(
381 "invariant `{}` must take no parameters",
382 f.signature()
383 )),
384 )
385 })
386 .collect();
387 if !invalid_invariants.is_empty() {
388 return SuiteResult::new(
389 start.elapsed(),
390 invalid_invariants.into_iter().collect(),
391 warnings,
392 );
393 }
394
395 let has_invariants = !invariant_fns.is_empty();
396
397 let prev_tracer = self.executor.inspector_mut().tracer.take();
398 if prev_tracer.is_some() || has_invariants {
399 self.executor.set_tracing(TraceMode::Call);
400 }
401
402 let setup_time = Instant::now();
403 let setup = self.setup(call_setup);
404 debug!("finished setting up in {:?}", setup_time.elapsed());
405
406 self.executor.inspector_mut().tracer = prev_tracer;
407
408 if setup.reason.is_some() {
409 let fail_msg = if setup.deployment_failure {
411 "constructor()".to_string()
412 } else {
413 "setUp()".to_string()
414 };
415 return SuiteResult::new(
416 start.elapsed(),
417 [(fail_msg, TestResult::setup_result(setup))].into(),
418 warnings,
419 );
420 }
421
422 let find_timer = Instant::now();
425 let functions = self
426 .contract
427 .abi
428 .functions()
429 .filter(|func| filter.matches_test_function(func))
430 .filter(|func| self.function_matches_network_pass(func))
431 .collect::<Vec<_>>();
432 debug!(
433 "Found {} test functions out of {} in {:?}",
434 functions.len(),
435 self.contract.abi.functions().count(),
436 find_timer.elapsed(),
437 );
438
439 let identified_contracts = has_invariants.then(|| {
440 load_contracts(setup.traces.iter().map(|(_, t)| &t.arena), &self.mcr.known_contracts)
441 });
442
443 let test_fail_functions =
444 functions.iter().filter(|func| func.test_function_kind().is_any_test_fail());
445 if test_fail_functions.clone().next().is_some() {
446 let fail = || {
447 TestResult::fail("`testFail*` has been removed. Consider changing to test_Revert[If|When]_Condition and expecting a revert".to_string())
448 };
449 let test_results = test_fail_functions.map(|func| (func.signature(), fail())).collect();
450 return SuiteResult::new(start.elapsed(), test_results, warnings);
451 }
452
453 let early_exit = &self.tcfg.early_exit;
454
455 if self.progress.is_some() {
456 let interrupt = early_exit.clone();
457 self.tokio_handle.spawn(async move {
458 signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
459 interrupt.record_ctrl_c();
460 });
461 }
462
463 let test_results = functions
464 .par_iter()
465 .filter_map(|&func| {
466 if early_exit.should_stop() {
468 return None;
469 }
470
471 let start = Instant::now();
472
473 let _guard = self.tokio_handle.enter();
474
475 let _guard;
476 let current_span = tracing::Span::current();
477 if current_span.is_none() || current_span.id() != self.span.id() {
478 _guard = self.span.enter();
479 }
480
481 let sig = func.signature();
482 let kind = func.test_function_kind();
483
484 let _guard = debug_span!(
485 "test",
486 %kind,
487 name = %if enabled!(tracing::Level::TRACE) { &sig } else { &func.name },
488 )
489 .entered();
490
491 let mut res = FunctionRunner::new(&self, &setup).run(
492 func,
493 invariant_fns.clone(),
494 kind,
495 call_after_invariant,
496 identified_contracts.as_ref(),
497 );
498 res.duration = start.elapsed();
499
500 if res.status.is_failure() {
502 early_exit.record_failure();
503 }
504
505 Some((sig, res))
506 })
507 .collect::<BTreeMap<_, _>>();
508
509 let duration = start.elapsed();
510 SuiteResult::new(duration, test_results, warnings)
511 }
512}
513
514struct FunctionRunner<'a, FEN: FoundryEvmNetwork> {
516 tcfg: Cow<'a, TestRunnerConfig<FEN>>,
518 executor: Cow<'a, Executor<FEN>>,
520 cr: &'a ContractRunner<'a, FEN>,
522 address: Address,
524 setup: &'a TestSetup,
526 result: TestResult,
528}
529
530impl<'a, FEN: FoundryEvmNetwork> Deref for FunctionRunner<'a, FEN> {
531 type Target = Cow<'a, TestRunnerConfig<FEN>>;
532
533 #[inline(always)]
534 fn deref(&self) -> &Self::Target {
535 &self.tcfg
536 }
537}
538
539impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> {
540 fn new(cr: &'a ContractRunner<'a, FEN>, setup: &'a TestSetup) -> Self {
541 Self {
542 tcfg: match &cr.tcfg {
543 Cow::Borrowed(tcfg) => Cow::Borrowed(tcfg),
544 Cow::Owned(tcfg) => Cow::Owned(tcfg.clone()),
545 },
546 executor: Cow::Borrowed(&cr.executor),
547 cr,
548 address: setup.address,
549 setup,
550 result: TestResult::new(setup),
551 }
552 }
553
554 const fn revert_decoder(&self) -> &'a RevertDecoder {
555 &self.cr.mcr.revert_decoder
556 }
557
558 fn apply_function_inline_config(&mut self, func: &Function) -> Result<()> {
560 if self.inline_config.contains_function(self.cr.name, &func.name) {
561 let new_config = Arc::new(self.cr.inline_config(Some(func))?);
562 self.tcfg.to_mut().reconfigure_with(new_config);
563 self.tcfg.configure_executor(self.executor.to_mut());
564 }
565 Ok(())
566 }
567
568 fn run(
569 mut self,
570 func: &Function,
571 invariants: Vec<&Function>,
572 kind: TestFunctionKind,
573 call_after_invariant: bool,
574 identified_contracts: Option<&ContractsByAddress>,
575 ) -> TestResult {
576 let fail_on_revert_for = |f: &Function| {
577 if self.inline_config.contains_function(self.cr.name, &f.name)
578 && let Ok(config) = self.cr.inline_config(Some(f))
579 {
580 return config.invariant.fail_on_revert;
581 }
582 self.config.invariant.fail_on_revert
583 };
584 let invariant_fns: Vec<_> =
585 invariants.into_iter().map(|f| (f, fail_on_revert_for(f))).collect();
586
587 if let Err(e) = self.apply_function_inline_config(func) {
588 self.result.single_fail(Some(e.to_string()));
589 return self.result;
590 }
591
592 match kind {
593 TestFunctionKind::UnitTest { .. } => self.run_unit_test(func),
594 TestFunctionKind::FuzzTest { .. } => self.run_fuzz_test(func),
595 TestFunctionKind::TableTest => self.run_table_test(func),
596 TestFunctionKind::InvariantTest => self.run_invariant_test(
597 func,
598 invariant_fns,
599 call_after_invariant,
600 identified_contracts.unwrap(),
601 ),
602 _ => unreachable!(),
603 }
604 }
605
606 fn run_unit_test(mut self, func: &Function) -> TestResult {
615 if self.prepare_test(func).is_err() {
617 return self.result;
618 }
619
620 let (mut raw_call_result, reason) = match self.executor.call(
622 self.sender,
623 self.address,
624 func,
625 &[],
626 U256::ZERO,
627 Some(self.revert_decoder()),
628 ) {
629 Ok(res) => (res.raw, None),
630 Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
631 Err(EvmError::Skip(reason)) => {
632 self.result.single_skip(reason);
633 return self.result;
634 }
635 Err(err) => {
636 self.result.single_fail(Some(err.to_string()));
637 return self.result;
638 }
639 };
640
641 let success =
642 self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, false);
643 self.result.single_result(success, reason, raw_call_result);
644 self.result
645 }
646
647 fn run_table_test(mut self, func: &Function) -> TestResult {
656 if self.prepare_test(func).is_err() {
658 return self.result;
659 }
660
661 let Some(first_param) = func.inputs.first() else {
663 self.result.single_fail(Some("Table test should have at least one parameter".into()));
664 return self.result;
665 };
666
667 let Some(first_param_fixtures) =
668 &self.setup.fuzz_fixtures.param_fixtures(first_param.name())
669 else {
670 self.result.single_fail(Some("Table test should have fixtures defined".into()));
671 return self.result;
672 };
673
674 if first_param_fixtures.is_empty() {
675 self.result.single_fail(Some("Table test should have at least one fixture".into()));
676 return self.result;
677 }
678
679 let fixtures_len = first_param_fixtures.len();
680 let mut table_fixtures = vec![&first_param_fixtures[..]];
681
682 for param in &func.inputs[1..] {
684 let param_name = param.name();
685 let Some(fixtures) = &self.setup.fuzz_fixtures.param_fixtures(param.name()) else {
686 self.result.single_fail(Some(format!("No fixture defined for param {param_name}")));
687 return self.result;
688 };
689
690 if fixtures.len() != fixtures_len {
691 self.result.single_fail(Some(format!(
692 "{} fixtures defined for {param_name} (expected {})",
693 fixtures.len(),
694 fixtures_len
695 )));
696 return self.result;
697 }
698
699 table_fixtures.push(&fixtures[..]);
700 }
701
702 let progress = start_fuzz_progress(
703 self.cr.progress,
704 self.cr.name,
705 &func.name,
706 None,
707 fixtures_len as u32,
708 );
709
710 let mut result = FuzzTestResult::default();
711
712 for i in 0..fixtures_len {
713 if self.tcfg.early_exit.should_stop() {
714 return self.result;
715 }
716
717 if let Some(progress) = progress.as_ref() {
719 progress.inc(1);
720 }
721
722 let args = table_fixtures.iter().map(|row| row[i].clone()).collect_vec();
723 let (mut raw_call_result, reason) = match self.executor.call(
724 self.sender,
725 self.address,
726 func,
727 &args,
728 U256::ZERO,
729 Some(self.revert_decoder()),
730 ) {
731 Ok(res) => (res.raw, None),
732 Err(EvmError::Execution(err)) => (err.raw, Some(err.reason)),
733 Err(EvmError::Skip(reason)) => {
734 self.result.single_skip(reason);
735 return self.result;
736 }
737 Err(err) => {
738 self.result.single_fail(Some(err.to_string()));
739 return self.result;
740 }
741 };
742
743 result.gas_by_case.push((raw_call_result.gas_used, raw_call_result.stipend));
744 result.logs.extend(raw_call_result.logs.clone());
745 result.labels.extend(raw_call_result.labels.clone());
746 HitMaps::merge_opt(&mut result.line_coverage, raw_call_result.line_coverage.clone());
747
748 let is_success =
749 self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, false);
750 if !is_success {
752 result.counterexample =
753 Some(CounterExample::Single(BaseCounterExample::from_fuzz_call(
754 Bytes::from(func.abi_encode_input(&args).unwrap()),
755 args,
756 raw_call_result.traces.clone(),
757 )));
758 result.reason = reason;
759 result.traces = raw_call_result.traces;
760 self.result.table_result(result);
761 return self.result;
762 }
763
764 if i == fixtures_len - 1 {
767 result.success = true;
768 result.traces = raw_call_result.traces;
769 self.result.table_result(result);
770 return self.result;
771 }
772 }
773
774 self.result
775 }
776
777 fn run_invariant_test(
778 mut self,
779 func: &Function,
780 invariants: Vec<(&Function, bool)>,
781 call_after_invariant: bool,
782 identified_contracts: &ContractsByAddress,
783 ) -> TestResult {
784 if let Err(EvmError::Skip(reason)) = self.executor.call(
786 self.sender,
787 self.address,
788 func,
789 &[],
790 U256::ZERO,
791 Some(self.revert_decoder()),
792 ) {
793 self.result.invariant_skip(reason);
794 return self.result;
795 };
796
797 let runner = self.invariant_runner();
798 let invariant_config = self.config.invariant.clone();
799 let invariant_config = &invariant_config;
800
801 let mut executor = self.clone_executor();
802 executor
805 .inspector_mut()
806 .collect_edge_coverage(invariant_config.corpus.collect_evm_edge_coverage());
807 executor.inspector_mut().collect_evm_cmp_log(invariant_config.corpus.collect_evm_cmp_log());
808 executor
809 .inspector_mut()
810 .collect_sancov_edges(invariant_config.corpus.collect_sancov_edges());
811 executor
812 .inspector_mut()
813 .collect_sancov_trace_cmp(invariant_config.corpus.collect_sancov_trace_cmp());
814 let mut config = invariant_config.clone();
815 let (failure_dir, failure_file) = test_paths(
816 &mut config.corpus,
817 invariant_config.failure_persist_dir.clone().unwrap(),
818 self.cr.name,
819 &func.name,
820 );
821
822 let mut evm = InvariantExecutor::new(
823 executor,
824 runner,
825 config,
826 identified_contracts,
827 &self.cr.mcr.known_contracts,
828 );
829 let is_optimization = is_optimization_invariant(func);
833 if is_optimization && invariant_config.assert_all {
838 let dropped: Vec<&str> = invariants
839 .iter()
840 .filter(|(invariant_fn, _)| *invariant_fn != func)
841 .map(|(invariant_fn, _)| invariant_fn.name.as_str())
842 .collect();
843 if !dropped.is_empty() {
844 let _ = sh_warn!(
845 "{}: assert_all is on but {} is an optimization invariant; \
846 {} boolean invariant(s) skipped: {}. \
847 Move them to a separate contract to run them.",
848 self.cr.name,
849 func.name,
850 dropped.len(),
851 dropped.join(", "),
852 );
853 }
854 }
855 let current_settings = match evm.compute_settings(self.address) {
858 Ok(s) => s,
859 Err(e) => {
860 self.result.invariant_setup_fail(e);
861 return self.result;
862 }
863 };
864 let secondary_has_compatible_persisted = |invariant_fn: &Function| {
867 persisted_call_sequence(
868 canonicalized(failure_dir.join(invariant_fn.name.clone())).as_path(),
869 ¤t_settings,
870 )
871 .is_some()
872 };
873 if !is_optimization && invariant_config.assert_all {
878 let persisted_skipped: Vec<&str> = invariants
879 .iter()
880 .filter(|(invariant_fn, _)| {
881 *invariant_fn != func && secondary_has_compatible_persisted(invariant_fn)
882 })
883 .map(|(invariant_fn, _)| invariant_fn.name.as_str())
884 .collect();
885 if !persisted_skipped.is_empty() {
886 let _ = sh_warn!(
887 "{}: {} invariant(s) skipped due to persisted failures: {}. \
888 Run `forge clean` or delete files in {} to re-include.",
889 self.cr.name,
890 persisted_skipped.len(),
891 persisted_skipped.join(", "),
892 failure_dir.display(),
893 );
894 }
895 }
896 let invariant_fns: Vec<(&Function, bool)> = invariants
901 .into_iter()
902 .filter(|(invariant_fn, _)| {
903 *invariant_fn == func
904 || (!is_optimization
905 && invariant_config.assert_all
906 && !secondary_has_compatible_persisted(invariant_fn))
907 })
908 .collect();
909 let anchor_idx = invariant_fns
910 .iter()
911 .position(|(invariant_fn, _)| *invariant_fn == func)
912 .expect("anchor must be present in invariant_fns");
913 let invariant_contract = InvariantContract::new(
914 self.address,
915 self.cr.name,
916 invariant_fns,
917 anchor_idx,
918 call_after_invariant,
919 &self.cr.contract.abi,
920 );
921 let show_solidity = invariant_config.show_solidity;
922
923 let progress = start_fuzz_progress(
924 self.cr.progress,
925 self.cr.name,
926 &func.name,
927 invariant_config.timeout,
928 invariant_config.runs,
929 );
930
931 let replay_ctx = ReplayContext {
932 invariant_contract: &invariant_contract,
933 invariant_config,
934 revert_decoder: self.revert_decoder(),
935 show_solidity,
936 };
937
938 if let Some(InvariantPersistedFailure { mut call_sequence, assertion_failure, .. }) =
940 persisted_call_sequence(failure_file.as_path(), ¤t_settings)
941 {
942 let (txes, replay) = replay_persisted_call_sequence(
943 &replay_ctx,
944 self.clone_executor(),
945 &mut call_sequence,
946 assertion_failure,
947 );
948 if let Ok((success, replayed_entirely, replay_reason)) = replay
949 && !success
950 {
951 let warn = format!(
952 "Replayed invariant failure from {:?} file. \nRun `forge clean` or remove file to ignore failure and to continue invariant test campaign.",
953 failure_file.as_path()
954 );
955
956 if let Some(ref progress) = progress {
957 progress.set_prefix(format!("{}\n{warn}\n", func.name));
958 } else {
959 let _ = sh_warn!("{warn}");
960 }
961
962 match replay_error(
965 evm.config(),
966 self.clone_executor(),
967 &txes,
968 None,
969 assertion_failure,
970 None, &invariant_contract,
972 invariant_contract.anchor(),
973 &self.cr.mcr.known_contracts,
974 identified_contracts.clone(),
975 &mut self.result.logs,
976 &mut self.result.traces,
977 &mut self.result.line_coverage,
978 &mut self.result.deprecated_cheatcodes,
979 progress.as_ref(),
980 &self.tcfg.early_exit,
981 None, ) {
983 Ok(replayed_call_sequence) if !replayed_call_sequence.is_empty() => {
984 call_sequence = replayed_call_sequence;
985 record_invariant_failure(
987 failure_dir.as_path(),
988 failure_file.as_path(),
989 &call_sequence,
990 ¤t_settings,
991 assertion_failure,
992 );
993 }
994 Ok(_) => {}
995 Err(err) => {
996 error!(%err, "Failed to replay invariant error");
997 }
998 }
999
1000 self.result.invariant_replay_fail(
1001 replayed_entirely,
1002 &invariant_contract.anchor().name,
1003 replay_reason,
1004 call_sequence,
1005 );
1006 return self.result;
1007 }
1008 }
1009
1010 let persisted_handler_failures = replay_persisted_handler_failures(
1013 &failure_dir.join("handlers"),
1014 ¤t_settings,
1015 self.clone_executor(),
1016 &replay_ctx,
1017 );
1018
1019 let invariant_result = match evm.invariant_fuzz(
1020 invariant_contract.clone(),
1021 &self.setup.fuzz_fixtures,
1022 self.build_fuzz_state(true),
1023 progress.as_ref(),
1024 &self.tcfg.early_exit,
1025 persisted_handler_failures,
1026 ) {
1027 Ok(x) => x,
1028 Err(e) => {
1029 self.result.invariant_setup_fail(e);
1030 return self.result;
1031 }
1032 };
1033 self.result.merge_coverages(invariant_result.line_coverage);
1035
1036 let mut counterexample = None;
1037 let success =
1039 invariant_result.errors.is_empty() && invariant_result.handler_errors.is_empty();
1040 let mut invariant_failures: Vec<InvariantFailure> = vec![];
1041 let mut any_failure_persisted = false;
1042
1043 if success {
1044 if let Some(best_value) = invariant_result.optimization_best_value {
1045 match replay_error(
1047 evm.config(),
1048 self.clone_executor(),
1049 &invariant_result.optimization_best_sequence,
1050 None,
1051 false,
1052 Some(best_value),
1053 &invariant_contract,
1054 invariant_contract.anchor(),
1055 &self.cr.mcr.known_contracts,
1056 identified_contracts.clone(),
1057 &mut self.result.logs,
1058 &mut self.result.traces,
1059 &mut self.result.line_coverage,
1060 &mut self.result.deprecated_cheatcodes,
1061 progress.as_ref(),
1062 &self.tcfg.early_exit,
1063 None, ) {
1065 Ok(best_sequence) if !best_sequence.is_empty() => {
1066 counterexample = Some(CounterExample::Sequence(
1067 invariant_result.optimization_best_sequence.len(),
1068 best_sequence,
1069 ));
1070 }
1071 Err(err) => {
1072 error!(%err, "Failed to replay optimization best sequence");
1073 }
1074 _ => {}
1075 }
1076 } else {
1077 if let Err(err) = replay_run(
1079 &invariant_contract,
1080 invariant_contract.anchor(),
1081 self.clone_executor(),
1082 &self.cr.mcr.known_contracts,
1083 identified_contracts.clone(),
1084 &mut self.result.logs,
1085 &mut self.result.traces,
1086 &mut self.result.line_coverage,
1087 &mut self.result.deprecated_cheatcodes,
1088 &invariant_result.last_run_inputs,
1089 show_solidity,
1090 ) {
1091 error!(%err, "Failed to replay last invariant run");
1092 }
1093 }
1094 } else {
1095 let total_broken = invariant_result.errors.len();
1099 if let Some(error) = invariant_result.errors.get(&invariant_contract.anchor().name) {
1104 let anchor_counterexample = match error {
1105 InvariantFuzzError::BrokenInvariant(case_data)
1106 | InvariantFuzzError::Revert(case_data) => {
1107 let TestError::Fail(_, ref calls) = case_data.test_error else {
1108 unreachable!("FailedInvariantCaseData::new always sets TestError::Fail")
1109 };
1110 match replay_error(
1111 evm.config(),
1112 self.clone_executor(),
1113 calls,
1114 Some(case_data.inner_sequence.clone()),
1115 case_data.assertion_failure,
1116 None, &invariant_contract,
1118 invariant_contract.anchor(),
1119 &self.cr.mcr.known_contracts,
1120 identified_contracts.clone(),
1121 &mut self.result.logs,
1122 &mut self.result.traces,
1123 &mut self.result.line_coverage,
1124 &mut self.result.deprecated_cheatcodes,
1125 progress.as_ref(),
1126 &self.tcfg.early_exit,
1127 Some((1, total_broken)),
1128 ) {
1129 Ok(call_sequence) if !call_sequence.is_empty() => {
1130 record_invariant_failure(
1131 failure_dir.as_path(),
1132 failure_file.as_path(),
1133 &call_sequence,
1134 ¤t_settings,
1135 case_data.assertion_failure,
1136 );
1137 any_failure_persisted = true;
1138 Some(CounterExample::Sequence(calls.len(), call_sequence))
1139 }
1140 Ok(_) => None,
1141 Err(err) => {
1142 error!(%err, "Failed to replay invariant error");
1143 None
1144 }
1145 }
1146 }
1147 InvariantFuzzError::MaxAssumeRejects(_) => None,
1148 InvariantFuzzError::HandlerAssertion(_) => None,
1150 };
1151 invariant_failures.push(InvariantFailure::Predicate {
1152 name: invariant_contract.anchor().name.clone(),
1153 reason: error.revert_reason().unwrap_or_default(),
1154 counterexample: anchor_counterexample,
1155 persisted_path: failure_file,
1156 is_anchor: true,
1157 });
1158 }
1159
1160 let mut next_position = 2usize;
1169 for (idx, (invariant, _)) in invariant_contract.invariant_fns.iter().enumerate() {
1171 if idx == invariant_contract.anchor_idx {
1172 continue;
1173 }
1174
1175 let persisted_failure = canonicalized(failure_dir.join(invariant.name.clone()));
1181 if !secondary_has_compatible_persisted(invariant)
1182 && let Some(error) = invariant_result.errors.get(&invariant.name)
1183 && let InvariantFuzzError::BrokenInvariant(case_data)
1184 | InvariantFuzzError::Revert(case_data) = error
1185 && let TestError::Fail(_, ref calls) = case_data.test_error
1186 {
1187 let original_seq_len = calls.len();
1188 let secondary_counterexample = if self.tcfg.early_exit.should_stop() {
1193 let unshrunk_sequence = calls
1194 .iter()
1195 .map(|tx| {
1196 BaseCounterExample::from_invariant_call(
1197 tx,
1198 identified_contracts,
1199 None,
1200 invariant_config.show_solidity,
1201 )
1202 })
1203 .collect::<Vec<_>>();
1204 record_invariant_failure(
1205 failure_dir.as_path(),
1206 persisted_failure.as_path(),
1207 &unshrunk_sequence,
1208 ¤t_settings,
1209 case_data.assertion_failure,
1210 );
1211 any_failure_persisted = true;
1212 None
1213 } else {
1214 let position = next_position;
1215 next_position += 1;
1216 match replay_error(
1217 invariant_config.clone(),
1218 self.clone_executor(),
1219 calls,
1220 Some(case_data.inner_sequence.clone()),
1221 case_data.assertion_failure,
1222 None, &invariant_contract,
1224 invariant,
1225 &self.cr.mcr.known_contracts,
1226 identified_contracts.clone(),
1227 &mut self.result.logs,
1228 &mut self.result.traces,
1229 &mut self.result.line_coverage,
1230 &mut self.result.deprecated_cheatcodes,
1231 progress.as_ref(),
1232 &self.tcfg.early_exit,
1233 Some((position, total_broken)),
1234 ) {
1235 Ok(call_sequence) if !call_sequence.is_empty() => {
1236 record_invariant_failure(
1237 failure_dir.as_path(),
1238 persisted_failure.as_path(),
1239 &call_sequence,
1240 ¤t_settings,
1241 case_data.assertion_failure,
1242 );
1243 any_failure_persisted = true;
1244 Some(CounterExample::Sequence(original_seq_len, call_sequence))
1245 }
1246 Ok(_) => None,
1247 Err(err) => {
1248 error!(%err, "Failed to replay invariant error");
1249 None
1250 }
1251 }
1252 };
1253 invariant_failures.push(InvariantFailure::Predicate {
1254 name: invariant.name.clone(),
1255 reason: error.revert_reason().unwrap_or_default(),
1256 counterexample: secondary_counterexample,
1257 persisted_path: persisted_failure.clone(),
1258 is_anchor: false,
1259 });
1260 }
1261 }
1262 }
1263
1264 let invariant_failure_dir = any_failure_persisted.then(|| failure_dir.clone());
1265 let assert_all_invariant_count = (invariant_config.assert_all
1269 && invariant_contract.invariant_fns.len() > 1)
1270 .then_some(invariant_contract.invariant_fns.len());
1271
1272 let identified_contracts_ro = identified_contracts;
1278 let invariant_handler_failures = invariant_result
1279 .handler_errors
1280 .iter()
1281 .sorted_by(|(ka, _), (kb, _)| {
1282 ka.cmp(kb)
1284 })
1285 .filter_map(|(site, err)| err.as_handler_assertion().map(|f| (site, f)))
1286 .map(|(_site, failure)| {
1287 let reverter = failure.reverter;
1288 let selector = failure.selector;
1289 let resolved_name = identified_contracts_ro
1291 .get(&reverter)
1292 .and_then(|(contract_name, abi)| {
1293 abi.functions()
1294 .find(|f| f.selector() == selector)
1295 .map(|f| format!("{contract_name}::{}", f.name))
1296 })
1297 .unwrap_or_else(|| format!("{reverter}::{selector}"));
1298
1299 let counterexample_calls = failure
1300 .call_sequence
1301 .iter()
1302 .map(|tx| {
1303 BaseCounterExample::from_invariant_call(
1304 tx,
1305 identified_contracts_ro,
1306 None,
1307 invariant_config.show_solidity,
1308 )
1309 })
1310 .collect::<Vec<_>>();
1311
1312 if !counterexample_calls.is_empty() {
1314 record_handler_failure(
1315 failure_dir.as_path(),
1316 reverter,
1317 selector,
1318 &counterexample_calls,
1319 ¤t_settings,
1320 );
1321 }
1322
1323 let counterexample = if counterexample_calls.is_empty() {
1324 None
1325 } else {
1326 Some(CounterExample::Sequence(
1328 failure.original_sequence_len,
1329 counterexample_calls,
1330 ))
1331 };
1332
1333 InvariantFailure::Handler {
1334 name: resolved_name,
1335 reverter,
1336 selector,
1337 reason: failure.revert_reason.clone(),
1338 counterexample,
1339 }
1340 })
1341 .collect::<Vec<_>>();
1342
1343 self.result.invariant_result(
1344 invariant_result.gas_report_traces,
1345 success,
1346 invariant_failures,
1347 invariant_failure_dir,
1348 assert_all_invariant_count,
1349 invariant_handler_failures,
1350 counterexample,
1351 invariant_result.cases,
1352 invariant_result.reverts,
1353 invariant_result.metrics,
1354 invariant_result.failed_corpus_replays,
1355 invariant_result.optimization_best_value,
1356 );
1357 self.result
1358 }
1359
1360 fn run_fuzz_test(mut self, func: &Function) -> TestResult {
1370 if self.prepare_test(func).is_err() {
1372 return self.result;
1373 }
1374
1375 let runner = self.fuzz_runner();
1376 let mut fuzz_config = self.config.fuzz.clone();
1377 let (failure_dir, failure_file) = test_paths(
1378 &mut fuzz_config.corpus,
1379 fuzz_config.failure_persist_dir.clone().unwrap(),
1380 self.cr.name,
1381 &func.name,
1382 );
1383
1384 let progress = start_fuzz_progress(
1385 self.cr.progress,
1386 self.cr.name,
1387 &func.name,
1388 fuzz_config.timeout,
1389 if fuzz_config.run.is_some() { 1 } else { fuzz_config.runs },
1390 );
1391
1392 let state = self.build_fuzz_state(false);
1393 let mut executor = self.executor.into_owned();
1394 executor
1397 .inspector_mut()
1398 .collect_edge_coverage(fuzz_config.corpus.collect_evm_edge_coverage());
1399 executor.inspector_mut().collect_evm_cmp_log(fuzz_config.corpus.collect_evm_cmp_log());
1400 executor.inspector_mut().collect_sancov_edges(fuzz_config.corpus.collect_sancov_edges());
1401 executor
1402 .inspector_mut()
1403 .collect_sancov_trace_cmp(fuzz_config.corpus.collect_sancov_trace_cmp());
1404 let persisted_failure =
1406 foundry_common::fs::read_json_file::<BaseCounterExample>(failure_file.as_path()).ok();
1407 let mut fuzzed_executor =
1409 FuzzedExecutor::new(executor, runner, self.tcfg.sender, fuzz_config, persisted_failure);
1410 let result = match fuzzed_executor.fuzz(
1411 func,
1412 &self.setup.fuzz_fixtures,
1413 state,
1414 self.address,
1415 &self.cr.mcr.revert_decoder,
1416 progress.as_ref(),
1417 &self.tcfg.early_exit,
1418 self.cr.tokio_handle,
1419 ) {
1420 Ok(x) => x,
1421 Err(e) => {
1422 self.result.fuzz_setup_fail(e);
1423 return self.result;
1424 }
1425 };
1426
1427 if let Some(CounterExample::Single(counterexample)) = &result.counterexample {
1429 if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) {
1430 error!(%err, "Failed to create fuzz failure dir");
1431 } else if let Err(err) =
1432 foundry_common::fs::write_json_file(failure_file.as_path(), counterexample)
1433 {
1434 error!(%err, "Failed to record call sequence");
1435 }
1436 }
1437
1438 self.result.fuzz_result(result);
1439 self.result
1440 }
1441
1442 fn prepare_test(&mut self, func: &Function) -> Result<(), ()> {
1452 let address = self.setup.address;
1453
1454 if self.cr.contract.abi.functions().any(|func| func.name.is_before_test_setup()) {
1456 for calldata in self.executor.call_sol_default(
1457 address,
1458 &ITest::beforeTestSetupCall { testSelector: func.selector() },
1459 ) {
1460 let spec_id: SpecId = self.executor.spec_id().into();
1461 debug!(?calldata, spec=%spec_id, "applying before_test_setup");
1462 match self.executor.to_mut().transact_raw(
1464 self.tcfg.sender,
1465 address,
1466 calldata,
1467 U256::ZERO,
1468 ) {
1469 Ok(call_result) => {
1470 let reverted = call_result.reverted;
1471
1472 self.result.extend(call_result);
1474
1475 if reverted {
1477 self.result.single_fail(None);
1478 return Err(());
1479 }
1480 }
1481 Err(_) => {
1482 self.result.single_fail(None);
1483 return Err(());
1484 }
1485 }
1486 }
1487 }
1488 Ok(())
1489 }
1490
1491 fn fuzz_runner(&self) -> TestRunner {
1492 let config = &self.config.fuzz;
1493 fuzzer_with_cases(config.seed, config.runs, config.max_test_rejects)
1494 }
1495
1496 fn invariant_runner(&self) -> TestRunner {
1497 let config = &self.config.invariant;
1498 fuzzer_with_cases(self.config.fuzz.seed, config.runs, config.max_assume_rejects)
1499 }
1500
1501 fn clone_executor(&self) -> Executor<FEN> {
1502 self.executor.clone().into_owned()
1503 }
1504
1505 fn build_fuzz_state(&self, invariant: bool) -> EvmFuzzState {
1506 let config =
1507 if invariant { self.config.invariant.dictionary } else { self.config.fuzz.dictionary };
1508 if let Some(db) = self.executor.backend().active_fork_db() {
1509 EvmFuzzState::new(
1510 &self.setup.deployed_libs,
1511 db,
1512 config,
1513 Some(&self.cr.mcr.fuzz_literals),
1514 )
1515 } else {
1516 let db = self.executor.backend().mem_db();
1517 EvmFuzzState::new(
1518 &self.setup.deployed_libs,
1519 db,
1520 config,
1521 Some(&self.cr.mcr.fuzz_literals),
1522 )
1523 }
1524 }
1525}
1526
1527fn fuzzer_with_cases(seed: Option<U256>, cases: u32, max_global_rejects: u32) -> TestRunner {
1528 let config = proptest::test_runner::Config {
1529 cases,
1530 max_global_rejects,
1531 max_shrink_iters: 0,
1534 ..Default::default()
1535 };
1536
1537 if let Some(seed) = seed {
1538 trace!(target: "forge::test", %seed, "building deterministic fuzzer");
1539 let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
1540 TestRunner::new_with_rng(config, rng)
1541 } else {
1542 trace!(target: "forge::test", "building stochastic fuzzer");
1543 TestRunner::new(config)
1544 }
1545}
1546
1547#[derive(Serialize, Deserialize)]
1549struct InvariantPersistedFailure {
1550 call_sequence: Vec<BaseCounterExample>,
1552 settings: InvariantSettings,
1555 #[serde(default)]
1557 assertion_failure: bool,
1558}
1559
1560type CheckSequenceResult = eyre::Result<(bool, bool, Option<String>)>;
1562
1563struct ReplayContext<'a> {
1565 invariant_contract: &'a InvariantContract<'a>,
1566 invariant_config: &'a InvariantConfig,
1567 revert_decoder: &'a RevertDecoder,
1568 show_solidity: bool,
1569}
1570
1571fn persisted_call_sequence(
1574 path: &Path,
1575 current_settings: &InvariantSettings,
1576) -> Option<InvariantPersistedFailure> {
1577 foundry_common::fs::read_json_file::<InvariantPersistedFailure>(path).ok().and_then(
1578 |persisted_failure| {
1579 if let Some(diff) = persisted_failure.settings.diff(current_settings) {
1580 let _ = sh_warn!(
1581 "Failure from {:?} file was ignored because invariant test settings have changed: {}",
1582 path,
1583 diff
1584 );
1585 return None;
1586 }
1587 Some(persisted_failure)
1588 },
1589 )
1590}
1591
1592fn base_counterexamples_to_txes(
1594 ctx: &ReplayContext<'_>,
1595 call_sequence: &mut [BaseCounterExample],
1596) -> Vec<BasicTxDetails> {
1597 call_sequence
1598 .iter_mut()
1599 .map(|seq| {
1600 seq.show_solidity = ctx.show_solidity;
1601 BasicTxDetails {
1602 warp: seq.warp,
1603 roll: seq.roll,
1604 sender: seq.sender.unwrap_or_default(),
1605 call_details: CallDetails {
1606 target: seq.addr.unwrap_or_default(),
1607 calldata: seq.calldata.clone(),
1608 value: seq.value,
1609 },
1610 }
1611 })
1612 .collect()
1613}
1614
1615fn replay_persisted_call_sequence<FEN: FoundryEvmNetwork>(
1618 ctx: &ReplayContext<'_>,
1619 executor: Executor<FEN>,
1620 call_sequence: &mut [BaseCounterExample],
1621 expect_assertion_failure: bool,
1622) -> (Vec<BasicTxDetails>, CheckSequenceResult) {
1623 let txes = base_counterexamples_to_txes(ctx, call_sequence);
1624 let result = check_sequence(
1625 executor,
1626 &txes,
1627 (0..min(txes.len(), ctx.invariant_config.depth as usize)).collect(),
1628 ctx.invariant_contract.address,
1629 ctx.invariant_contract.anchor().selector().to_vec().into(),
1630 CheckSequenceOptions {
1631 accumulate_warp_roll: ctx.invariant_config.has_delay(),
1632 fail_on_revert: ctx.invariant_config.fail_on_revert,
1633 expect_assertion_failure,
1634 call_after_invariant: ctx.invariant_contract.call_after_invariant,
1635 rd: Some(ctx.revert_decoder),
1636 },
1637 );
1638 (txes, result)
1639}
1640
1641fn test_paths(
1643 corpus_config: &mut FuzzCorpusConfig,
1644 persist_dir: PathBuf,
1645 contract_name: &str,
1646 test_name: &str,
1647) -> (PathBuf, PathBuf) {
1648 let contract = contract_name.split(':').next_back().unwrap();
1649 corpus_config.with_test(contract, test_name);
1651
1652 let failures_dir = canonicalized(persist_dir.join("failures").join(contract));
1653 let failure_file = canonicalized(failures_dir.join(test_name));
1654 (failures_dir, failure_file)
1655}
1656
1657fn record_invariant_failure(
1659 failure_dir: &Path,
1660 failure_file: &Path,
1661 call_sequence: &[BaseCounterExample],
1662 settings: &InvariantSettings,
1663 assertion_failure: bool,
1664) {
1665 if let Err(err) = foundry_common::fs::create_dir_all(failure_dir) {
1666 error!(%err, "Failed to create invariant failure dir");
1667 return;
1668 }
1669
1670 if let Err(err) = foundry_common::fs::write_json_file(
1671 failure_file,
1672 &InvariantPersistedFailure {
1673 call_sequence: call_sequence.to_owned(),
1674 settings: settings.clone(),
1675 assertion_failure,
1676 },
1677 ) {
1678 error!(%err, "Failed to record call sequence");
1679 }
1680}
1681
1682fn record_handler_failure(
1685 failure_dir: &Path,
1686 reverter: Address,
1687 selector: Selector,
1688 call_sequence: &[BaseCounterExample],
1689 settings: &InvariantSettings,
1690) {
1691 let handlers_dir = failure_dir.join("handlers");
1692 if let Err(err) = foundry_common::fs::create_dir_all(&handlers_dir) {
1693 error!(%err, "Failed to create handler failure dir");
1694 return;
1695 }
1696 let mut buf = [0u8; 24];
1697 buf[..20].copy_from_slice(reverter.as_slice());
1698 buf[20..].copy_from_slice(selector.as_slice());
1699 let site_hash = alloy_primitives::keccak256(buf);
1700 let file = handlers_dir.join(format!("{site_hash:x}.json"));
1701 record_invariant_failure(&handlers_dir, &file, call_sequence, settings, true);
1702}
1703
1704fn replay_persisted_handler_failures<FEN: FoundryEvmNetwork>(
1708 handlers_dir: &Path,
1709 current_settings: &InvariantSettings,
1710 executor: Executor<FEN>,
1711 ctx: &ReplayContext<'_>,
1712) -> std::collections::HashMap<(Address, Selector), InvariantFuzzError> {
1713 let mut replayed: std::collections::HashMap<(Address, Selector), InvariantFuzzError> =
1714 std::collections::HashMap::new();
1715 let entries = match std::fs::read_dir(handlers_dir) {
1716 Ok(e) => e,
1717 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return replayed,
1718 Err(err) => {
1719 error!(%err, "Failed to read handler failure dir");
1720 return replayed;
1721 }
1722 };
1723 for entry in entries.flatten() {
1724 let path = entry.path();
1725 if path.extension().and_then(|s| s.to_str()) != Some("json") {
1726 continue;
1727 }
1728 let Some(persisted) = persisted_call_sequence(&path, current_settings) else {
1729 continue;
1730 };
1731 let mut call_sequence = persisted.call_sequence;
1732 if call_sequence.is_empty() {
1733 let _ = std::fs::remove_file(&path);
1734 continue;
1735 }
1736 let txes = base_counterexamples_to_txes(ctx, &mut call_sequence);
1737 let Some(last) = txes.last() else {
1739 let _ = std::fs::remove_file(&path);
1740 continue;
1741 };
1742 let expected_target = last.call_details.target;
1743 let expected_selector_bytes: [u8; 4] =
1744 last.call_details.calldata.get(..4).and_then(|s| s.try_into().ok()).unwrap_or_default();
1745 let expected_site = (expected_target, Selector::from(expected_selector_bytes));
1746 let sequence: Vec<usize> =
1747 (0..min(txes.len(), ctx.invariant_config.depth as usize)).collect();
1748 let outcome = replay_handler_failure_sequence(
1749 executor.clone(),
1750 &txes,
1751 sequence,
1752 ctx.invariant_config.has_delay(),
1753 Some(ctx.revert_decoder),
1754 );
1755 match outcome {
1756 Ok(outcome) if outcome.anchor_asserted => {
1757 let _ = sh_warn!(
1758 "Replayed handler-side assertion bug from {path:?}. \nRun `forge clean` or remove file to ignore."
1759 );
1760 let failure = HandlerAssertionFailure::from_replayed_sequence(
1761 txes,
1762 outcome.anchor_fingerprint,
1763 outcome.revert_reason.unwrap_or_default(),
1764 );
1765 let already_shorter = replayed
1768 .get(&expected_site)
1769 .and_then(InvariantFuzzError::as_handler_assertion)
1770 .is_some_and(|existing| {
1771 existing.call_sequence.len() <= failure.call_sequence.len()
1772 });
1773 if !already_shorter {
1774 replayed.insert(expected_site, InvariantFuzzError::HandlerAssertion(failure));
1775 }
1776 }
1777 Ok(_) => {
1779 let _ = std::fs::remove_file(&path);
1780 }
1781 Err(err) => {
1782 error!(%err, "Failed to replay handler-side assertion bug");
1783 }
1784 }
1785 }
1786 replayed
1787}