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