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    corpus::persist_campaign_optimization,
5    invariant::shrink::{
6        CheckSequenceOutcome, ShrinkProgress, shrink_sequence, shrink_sequence_value,
7    },
8};
9use alloy_dyn_abi::JsonAbiExt;
10use alloy_json_abi::Function;
11use alloy_primitives::{
12    Address, Bytes, I256, Log,
13    map::{AddressHashMap, HashMap},
14};
15use eyre::Result;
16use foundry_common::{ContractsByAddress, ContractsByArtifact};
17use foundry_config::InvariantConfig;
18use foundry_evm_core::{decode::RevertDecoder, evm::FoundryEvmNetwork};
19use foundry_evm_coverage::HitMaps;
20use foundry_evm_fuzz::{BaseCounterExample, BasicTxDetails, invariant::InvariantContract};
21use foundry_evm_traces::{TraceKind, TraceRequirements, Traces, load_contracts};
22use indicatif::ProgressBar;
23use parking_lot::RwLock;
24use std::sync::Arc;
25
26pub struct ReplayErrorResult {
27    pub counterexample_sequence: Vec<BaseCounterExample>,
28    pub check_result: Option<CheckSequenceOutcome>,
29    pub fork_block_number: Option<u64>,
30    /// Optimization value of the shrunk sequence, which can exceed the campaign best.
31    pub optimization_value: Option<I256>,
32}
33
34/// Replays a call sequence for collecting logs and traces.
35/// Returns counterexample to be used when the call sequence is a failed scenario.
36#[expect(clippy::too_many_arguments)]
37pub fn replay_run<FEN: FoundryEvmNetwork>(
38    invariant_contract: &InvariantContract<'_>,
39    target_invariant: &Function,
40    mut executor: Executor<FEN>,
41    known_contracts: &ContractsByArtifact,
42    mut ided_contracts: ContractsByAddress,
43    logs: &mut Vec<Log>,
44    traces: &mut Traces,
45    debug_bytecodes: &mut AddressHashMap<Bytes>,
46    line_coverage: &mut Option<HitMaps>,
47    deprecated_cheatcodes: &mut HashMap<&'static str, Option<&'static str>>,
48    inputs: &[BasicTxDetails],
49    show_solidity: bool,
50) -> Result<ReplayErrorResult> {
51    // We want traces for a failed case.
52    if executor.inspector().tracer.is_none() {
53        executor.set_trace_requirements(TraceRequirements::none().with_calls(true));
54    }
55
56    let mut counterexample_sequence = vec![];
57
58    // Replay each call from the sequence, collect logs, traces and coverage.
59    for tx in inputs {
60        let mut call_result = execute_tx(&mut executor, tx)?;
61        logs.append(&mut call_result.logs);
62        debug_bytecodes.extend(std::mem::take(&mut call_result.debug_bytecodes));
63        traces.push((TraceKind::Execution, call_result.traces.clone().unwrap()));
64        HitMaps::merge_opt(line_coverage, call_result.line_coverage.take());
65
66        // Commit state changes to persist across calls in the sequence.
67        executor.commit(&mut call_result);
68
69        // Identify newly generated contracts, if they exist.
70        ided_contracts
71            .extend(load_contracts(call_result.traces.iter().map(|a| &a.arena), known_contracts));
72
73        // Create counter example to be used in failed case.
74        counterexample_sequence.push(BaseCounterExample::from_invariant_call(
75            tx,
76            &ided_contracts,
77            call_result.traces,
78            show_solidity,
79        ));
80    }
81
82    // Replay invariant to collect logs and traces.
83    // We do this only once at the end of the replayed sequence.
84    // Checking after each call doesn't add valuable info for passing scenario
85    // (invariant call result is always success) nor for failed scenarios
86    // (invariant call result is always success until the last call that breaks it).
87    let (invariant_result, invariant_success) = call_invariant_function(
88        &executor,
89        invariant_contract.address,
90        target_invariant.abi_encode_input(&[])?.into(),
91    )?;
92    let fork_block_number = invariant_result.fork_block_number;
93    debug_bytecodes.extend(invariant_result.debug_bytecodes);
94    traces.push((TraceKind::Execution, invariant_result.traces.clone().unwrap()));
95    logs.extend(invariant_result.logs);
96    deprecated_cheatcodes.extend(
97        invariant_result
98            .cheatcodes
99            .as_ref()
100            .map_or_else(Default::default, |cheats| cheats.deprecated.clone()),
101    );
102
103    // Collect after invariant logs and traces.
104    if invariant_contract.call_after_invariant && invariant_success {
105        let (after_invariant_result, _) =
106            call_after_invariant_function(&executor, invariant_contract.address)?;
107        debug_bytecodes.extend(after_invariant_result.debug_bytecodes);
108        traces.push((TraceKind::Execution, after_invariant_result.traces.clone().unwrap()));
109        logs.extend(after_invariant_result.logs);
110    }
111
112    Ok(ReplayErrorResult {
113        counterexample_sequence,
114        check_result: None,
115        fork_block_number,
116        optimization_value: None,
117    })
118}
119
120/// Replays and shrinks a call sequence, collecting logs and traces.
121///
122/// For check mode (target_value=None): shrinks to find shortest failing sequence.
123/// For optimization mode (target_value=Some): shrinks to find shortest sequence producing target.
124/// `targets` are the campaign's fuzz targets, used to score optimization candidates.
125#[expect(clippy::too_many_arguments)]
126pub fn replay_error<FEN: FoundryEvmNetwork>(
127    config: InvariantConfig,
128    mut executor: Executor<FEN>,
129    calls: &[BasicTxDetails],
130    inner_sequence: Option<Vec<Option<BasicTxDetails>>>,
131    expect_assertion_failure: bool,
132    rd: Option<&RevertDecoder>,
133    target_value: Option<I256>,
134    targets: &[Address],
135    invariant_contract: &InvariantContract<'_>,
136    target_invariant: &Function,
137    known_contracts: &ContractsByArtifact,
138    ided_contracts: ContractsByAddress,
139    logs: &mut Vec<Log>,
140    traces: &mut Traces,
141    debug_bytecodes: &mut AddressHashMap<Bytes>,
142    line_coverage: &mut Option<HitMaps>,
143    deprecated_cheatcodes: &mut HashMap<&'static str, Option<&'static str>>,
144    progress: Option<&ProgressBar>,
145    early_exit: &EarlyExit,
146    position: Option<(usize, usize)>,
147) -> Result<ReplayErrorResult> {
148    // Multi-invariant runs include `[i/N]` in the shrink progress message so users see how many
149    // shrinkers are queued behind the current one.
150    let shrink_progress = ShrinkProgress::new(
151        &config,
152        progress,
153        &target_invariant.name,
154        position,
155        Some(&ided_contracts),
156        config.show_solidity,
157    );
158
159    // Every shrink attempt would append its steps to the shared step table, so only the replay of
160    // the shrunk sequence records them.
161    let recorder = executor.inspector_mut().debug_recorder.take();
162    let (calls, check_result, optimization_value) = if let Some(target) = target_value {
163        let (calls, value) = shrink_sequence_value(
164            &config,
165            invariant_contract,
166            target_invariant,
167            calls,
168            &executor,
169            target,
170            targets,
171            &shrink_progress,
172            early_exit,
173        )?;
174        (calls, None, Some(value))
175    } else {
176        let shrunk = shrink_sequence(
177            &config,
178            invariant_contract,
179            target_invariant,
180            calls,
181            expect_assertion_failure,
182            &executor,
183            rd,
184            &shrink_progress,
185            early_exit,
186        )?;
187        (shrunk.calls, shrunk.result, None)
188    };
189    executor.inspector_mut().debug_recorder = recorder;
190
191    if let Some(sequence) = inner_sequence {
192        set_up_inner_replay(&mut executor, &sequence);
193    }
194
195    let mut replay = replay_run(
196        invariant_contract,
197        target_invariant,
198        executor,
199        known_contracts,
200        ided_contracts,
201        logs,
202        traces,
203        debug_bytecodes,
204        line_coverage,
205        deprecated_cheatcodes,
206        &calls,
207        config.show_solidity,
208    )?;
209
210    persist_campaign_optimization(&config.corpus, optimization_value, &calls);
211    replay.check_result = check_result;
212    replay.optimization_value = optimization_value;
213    Ok(replay)
214}
215
216/// Sets up the calls generated by the internal fuzzer, if they exist.
217fn set_up_inner_replay<FEN: FoundryEvmNetwork>(
218    executor: &mut Executor<FEN>,
219    inner_sequence: &[Option<BasicTxDetails>],
220) {
221    if let Some(fuzzer) = &mut executor.inspector_mut().fuzzer
222        && let Some(call_generator) = &mut fuzzer.call_generator
223    {
224        call_generator.last_sequence = Arc::new(RwLock::new(inner_sequence.to_owned()));
225        call_generator.set_replay(true);
226    }
227}