Skip to main content

foundry_evm/executors/invariant/
replay.rs

1use super::{call_after_invariant_function, call_invariant_function, execute_tx};
2use crate::executors::{
3    EarlyExit, Executor,
4    invariant::shrink::{shrink_sequence, shrink_sequence_value},
5};
6use alloy_dyn_abi::JsonAbiExt;
7use alloy_primitives::{I256, Log, map::HashMap};
8use eyre::Result;
9use foundry_common::{ContractsByAddress, ContractsByArtifact};
10use foundry_config::InvariantConfig;
11use foundry_evm_core::evm::FoundryEvmNetwork;
12use foundry_evm_coverage::HitMaps;
13use foundry_evm_fuzz::{BaseCounterExample, BasicTxDetails, invariant::InvariantContract};
14use foundry_evm_traces::{TraceKind, TraceMode, Traces, load_contracts};
15use indicatif::ProgressBar;
16use parking_lot::RwLock;
17use std::sync::Arc;
18
19/// Replays a call sequence for collecting logs and traces.
20/// Returns counterexample to be used when the call sequence is a failed scenario.
21#[expect(clippy::too_many_arguments)]
22pub fn replay_run<FEN: FoundryEvmNetwork>(
23    invariant_contract: &InvariantContract<'_>,
24    mut executor: Executor<FEN>,
25    known_contracts: &ContractsByArtifact,
26    mut ided_contracts: ContractsByAddress,
27    logs: &mut Vec<Log>,
28    traces: &mut Traces,
29    line_coverage: &mut Option<HitMaps>,
30    deprecated_cheatcodes: &mut HashMap<&'static str, Option<&'static str>>,
31    inputs: &[BasicTxDetails],
32    show_solidity: bool,
33) -> Result<Vec<BaseCounterExample>> {
34    // We want traces for a failed case.
35    if executor.inspector().tracer.is_none() {
36        executor.set_tracing(TraceMode::Call);
37    }
38
39    let mut counterexample_sequence = vec![];
40
41    // Replay each call from the sequence, collect logs, traces and coverage.
42    for tx in inputs {
43        let mut call_result = execute_tx(&mut executor, tx)?;
44        logs.extend(call_result.logs.clone());
45        traces.push((TraceKind::Execution, call_result.traces.clone().unwrap()));
46        HitMaps::merge_opt(line_coverage, call_result.line_coverage.clone());
47
48        // Commit state changes to persist across calls in the sequence.
49        executor.commit(&mut call_result);
50
51        // Identify newly generated contracts, if they exist.
52        ided_contracts
53            .extend(load_contracts(call_result.traces.iter().map(|a| &a.arena), known_contracts));
54
55        // Create counter example to be used in failed case.
56        counterexample_sequence.push(BaseCounterExample::from_invariant_call(
57            tx,
58            &ided_contracts,
59            call_result.traces,
60            show_solidity,
61        ));
62    }
63
64    // Replay invariant to collect logs and traces.
65    // We do this only once at the end of the replayed sequence.
66    // Checking after each call doesn't add valuable info for passing scenario
67    // (invariant call result is always success) nor for failed scenarios
68    // (invariant call result is always success until the last call that breaks it).
69    let (invariant_result, invariant_success) = call_invariant_function(
70        &executor,
71        invariant_contract.address,
72        invariant_contract.invariant_function.abi_encode_input(&[])?.into(),
73    )?;
74    traces.push((TraceKind::Execution, invariant_result.traces.clone().unwrap()));
75    logs.extend(invariant_result.logs);
76    deprecated_cheatcodes.extend(
77        invariant_result
78            .cheatcodes
79            .as_ref()
80            .map_or_else(Default::default, |cheats| cheats.deprecated.clone()),
81    );
82
83    // Collect after invariant logs and traces.
84    if invariant_contract.call_after_invariant && invariant_success {
85        let (after_invariant_result, _) =
86            call_after_invariant_function(&executor, invariant_contract.address)?;
87        traces.push((TraceKind::Execution, after_invariant_result.traces.clone().unwrap()));
88        logs.extend(after_invariant_result.logs);
89    }
90
91    Ok(counterexample_sequence)
92}
93
94/// Replays and shrinks a call sequence, collecting logs and traces.
95///
96/// For check mode (target_value=None): shrinks to find shortest failing sequence.
97/// For optimization mode (target_value=Some): shrinks to find shortest sequence producing target.
98#[expect(clippy::too_many_arguments)]
99pub fn replay_error<FEN: FoundryEvmNetwork>(
100    config: InvariantConfig,
101    mut executor: Executor<FEN>,
102    calls: &[BasicTxDetails],
103    inner_sequence: Option<Vec<Option<BasicTxDetails>>>,
104    target_value: Option<I256>,
105    invariant_contract: &InvariantContract<'_>,
106    known_contracts: &ContractsByArtifact,
107    ided_contracts: ContractsByAddress,
108    logs: &mut Vec<Log>,
109    traces: &mut Traces,
110    line_coverage: &mut Option<HitMaps>,
111    deprecated_cheatcodes: &mut HashMap<&'static str, Option<&'static str>>,
112    progress: Option<&ProgressBar>,
113    early_exit: &EarlyExit,
114) -> Result<Vec<BaseCounterExample>> {
115    let calls = if let Some(target) = target_value {
116        shrink_sequence_value(
117            &config,
118            invariant_contract,
119            calls,
120            &executor,
121            target,
122            progress,
123            early_exit,
124        )?
125    } else {
126        shrink_sequence(&config, invariant_contract, calls, &executor, progress, early_exit)?
127    };
128
129    if let Some(sequence) = inner_sequence {
130        set_up_inner_replay(&mut executor, &sequence);
131    }
132
133    replay_run(
134        invariant_contract,
135        executor,
136        known_contracts,
137        ided_contracts,
138        logs,
139        traces,
140        line_coverage,
141        deprecated_cheatcodes,
142        &calls,
143        config.show_solidity,
144    )
145}
146
147/// Sets up the calls generated by the internal fuzzer, if they exist.
148fn set_up_inner_replay<FEN: FoundryEvmNetwork>(
149    executor: &mut Executor<FEN>,
150    inner_sequence: &[Option<BasicTxDetails>],
151) {
152    if let Some(fuzzer) = &mut executor.inspector_mut().fuzzer
153        && let Some(call_generator) = &mut fuzzer.call_generator
154    {
155        call_generator.last_sequence = Arc::new(RwLock::new(inner_sequence.to_owned()));
156        call_generator.set_replay(true);
157    }
158}