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