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