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::{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) -> Result<Vec<BaseCounterExample>> {
113    match failed_case.test_error {
114        // Don't use at the moment.
115        TestError::Abort(_) => Ok(vec![]),
116        TestError::Fail(_, ref calls) => {
117            // Shrink sequence of failed calls.
118            let calls = shrink_sequence(
119                failed_case,
120                calls,
121                &executor,
122                invariant_contract.call_after_invariant,
123                progress,
124            )?;
125
126            set_up_inner_replay(&mut executor, &failed_case.inner_sequence);
127
128            // Replay calls to get the counterexample and to collect logs, traces and coverage.
129            replay_run(
130                invariant_contract,
131                executor,
132                known_contracts,
133                ided_contracts,
134                logs,
135                traces,
136                line_coverage,
137                deprecated_cheatcodes,
138                &calls,
139                show_solidity,
140            )
141        }
142    }
143}
144
145/// Sets up the calls generated by the internal fuzzer, if they exist.
146fn set_up_inner_replay(executor: &mut Executor, inner_sequence: &[Option<BasicTxDetails>]) {
147    if let Some(fuzzer) = &mut executor.inspector_mut().fuzzer
148        && let Some(call_generator) = &mut fuzzer.call_generator
149    {
150        call_generator.last_sequence = Arc::new(RwLock::new(inner_sequence.to_owned()));
151        call_generator.set_replay(true);
152    }
153}