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