Skip to main content

foundry_evm/executors/invariant/
replay.rs

1use super::{call_after_invariant_function, call_invariant_function, execute_tx};
2use crate::executors::{
3    EarlyExit, Executor,
4    invariant::shrink::{shrink_sequence, shrink_sequence_value},
5};
6use alloy_dyn_abi::JsonAbiExt;
7use alloy_primitives::{I256, Log, map::HashMap};
8use eyre::Result;
9use foundry_common::{ContractsByAddress, ContractsByArtifact};
10use foundry_config::InvariantConfig;
11use foundry_evm_coverage::HitMaps;
12use foundry_evm_fuzz::{BaseCounterExample, BasicTxDetails, invariant::InvariantContract};
13use foundry_evm_traces::{TraceKind, TraceMode, Traces, load_contracts};
14use indicatif::ProgressBar;
15use parking_lot::RwLock;
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 mut call_result = execute_tx(&mut executor, tx)?;
43        logs.extend(call_result.logs.clone());
44        traces.push((TraceKind::Execution, call_result.traces.clone().unwrap()));
45        HitMaps::merge_opt(line_coverage, call_result.line_coverage.clone());
46
47        // Commit state changes to persist across calls in the sequence.
48        executor.commit(&mut call_result);
49
50        // Identify newly generated contracts, if they exist.
51        ided_contracts
52            .extend(load_contracts(call_result.traces.iter().map(|a| &a.arena), known_contracts));
53
54        // Create counter example to be used in failed case.
55        counterexample_sequence.push(BaseCounterExample::from_invariant_call(
56            tx,
57            &ided_contracts,
58            call_result.traces,
59            show_solidity,
60        ));
61    }
62
63    // Replay invariant to collect logs and traces.
64    // We do this only once at the end of the replayed sequence.
65    // Checking after each call doesn't add valuable info for passing scenario
66    // (invariant call result is always success) nor for failed scenarios
67    // (invariant call result is always success until the last call that breaks it).
68    let (invariant_result, invariant_success) = call_invariant_function(
69        &executor,
70        invariant_contract.address,
71        invariant_contract.invariant_function.abi_encode_input(&[])?.into(),
72    )?;
73    traces.push((TraceKind::Execution, invariant_result.traces.clone().unwrap()));
74    logs.extend(invariant_result.logs);
75    deprecated_cheatcodes.extend(
76        invariant_result
77            .cheatcodes
78            .as_ref()
79            .map_or_else(Default::default, |cheats| cheats.deprecated.clone()),
80    );
81
82    // Collect after invariant logs and traces.
83    if invariant_contract.call_after_invariant && invariant_success {
84        let (after_invariant_result, _) =
85            call_after_invariant_function(&executor, invariant_contract.address)?;
86        traces.push((TraceKind::Execution, after_invariant_result.traces.clone().unwrap()));
87        logs.extend(after_invariant_result.logs);
88    }
89
90    Ok(counterexample_sequence)
91}
92
93/// Replays and shrinks a call sequence, collecting logs and traces.
94///
95/// For check mode (target_value=None): shrinks to find shortest failing sequence.
96/// For optimization mode (target_value=Some): shrinks to find shortest sequence producing target.
97#[expect(clippy::too_many_arguments)]
98pub fn replay_error(
99    config: InvariantConfig,
100    mut executor: Executor,
101    calls: &[BasicTxDetails],
102    inner_sequence: Option<Vec<Option<BasicTxDetails>>>,
103    target_value: Option<I256>,
104    invariant_contract: &InvariantContract<'_>,
105    known_contracts: &ContractsByArtifact,
106    ided_contracts: ContractsByAddress,
107    logs: &mut Vec<Log>,
108    traces: &mut Traces,
109    line_coverage: &mut Option<HitMaps>,
110    deprecated_cheatcodes: &mut HashMap<&'static str, Option<&'static str>>,
111    progress: Option<&ProgressBar>,
112    early_exit: &EarlyExit,
113) -> Result<Vec<BaseCounterExample>> {
114    let calls = if let Some(target) = target_value {
115        shrink_sequence_value(
116            &config,
117            invariant_contract,
118            calls,
119            &executor,
120            target,
121            progress,
122            early_exit,
123        )?
124    } else {
125        shrink_sequence(&config, invariant_contract, calls, &executor, progress, early_exit)?
126    };
127
128    if let Some(sequence) = inner_sequence {
129        set_up_inner_replay(&mut executor, &sequence);
130    }
131
132    replay_run(
133        invariant_contract,
134        executor,
135        known_contracts,
136        ided_contracts,
137        logs,
138        traces,
139        line_coverage,
140        deprecated_cheatcodes,
141        &calls,
142        config.show_solidity,
143    )
144}
145
146/// Sets up the calls generated by the internal fuzzer, if they exist.
147fn set_up_inner_replay(executor: &mut Executor, inner_sequence: &[Option<BasicTxDetails>]) {
148    if let Some(fuzzer) = &mut executor.inspector_mut().fuzzer
149        && let Some(call_generator) = &mut fuzzer.call_generator
150    {
151        call_generator.last_sequence = Arc::new(RwLock::new(inner_sequence.to_owned()));
152        call_generator.set_replay(true);
153    }
154}