foundry_evm/executors/invariant/
replay.rs

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