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