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