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