foundry_evm/executors/invariant/
replay.rs

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