Skip to main content

cast/
debug.rs

1use alloy_primitives::{Bytes, map::AddressHashMap};
2use foundry_cli::utils::{TraceResult, print_traces};
3use foundry_common::{ContractsByArtifactBuilder, compile::ProjectCompiler, shell};
4use foundry_compilers::artifacts::output_selection::ContractOutputSelection;
5use foundry_config::{Config, FoundryHardfork, TracingConfig};
6use foundry_debugger::Debugger;
7use foundry_evm::{
8    opts::ForkEndpointIdentity,
9    traces::{
10        CallTraceDecoderBuilder, DebugTraceIdentifier, TraceContext,
11        debug::ContractSources,
12        erc8021::Attribution,
13        identifier::{SignaturesIdentifier, TraceIdentifiers},
14    },
15};
16use foundry_evm_networks::NetworkVariant;
17use itertools::Itertools;
18
19pub(crate) fn select_remote_trace_hardfork(
20    configured: Option<FoundryHardfork>,
21    endpoint: Option<FoundryHardfork>,
22    network: NetworkVariant,
23) -> Option<FoundryHardfork> {
24    let namespace = network.hardfork_namespace();
25    configured
26        .filter(|hardfork| hardfork.namespace() == namespace)
27        .or_else(|| endpoint.filter(|hardfork| hardfork.namespace() == namespace))
28}
29
30/// Resolves the hardfork used to decode a trace executed by the remote node. A configured
31/// hardfork is an explicit override; otherwise an Anvil endpoint's exact execution hardfork is
32/// honored before consulting the source chain's schedule at `block_timestamp`.
33pub(crate) fn resolve_remote_trace_hardfork(
34    configured: Option<FoundryHardfork>,
35    endpoint: &ForkEndpointIdentity,
36    block_timestamp: Option<u64>,
37) -> Option<FoundryHardfork> {
38    select_remote_trace_hardfork(configured, endpoint.hardfork, endpoint.network).or_else(|| {
39        block_timestamp.and_then(|timestamp| {
40            FoundryHardfork::from_chain_and_timestamp(endpoint.source_chain_id, timestamp)
41        })
42    })
43}
44
45pub(crate) fn ensure_remote_trace_context_unchanged(
46    before: &ForkEndpointIdentity,
47    after: &ForkEndpointIdentity,
48) -> eyre::Result<()> {
49    if before != after {
50        eyre::bail!(
51            "the RPC endpoint changed execution context while the remote trace was being \
52             collected; retry the command"
53        );
54    }
55    Ok(())
56}
57
58/// labels the traces, conditionally prints them or opens the debugger
59pub(crate) async fn handle_traces(
60    mut result: TraceResult,
61    config: &Config,
62    context: TraceContext,
63    contracts_bytecode: &AddressHashMap<Bytes>,
64    tracing: &TracingConfig,
65    with_local_artifacts: bool,
66    debug: bool,
67) -> eyre::Result<()> {
68    let (known_contracts, mut sources) = if with_local_artifacts {
69        // Status prose goes to stderr so `--json` output on stdout stays machine-readable.
70        let _ = sh_status!("Compiling project to generate artifacts");
71        let mut config = config.clone();
72        if debug && !config.extra_output.contains(&ContractOutputSelection::StorageLayout) {
73            config.extra_output.push(ContractOutputSelection::StorageLayout);
74        }
75        let project = config.project()?;
76        let compiler = ProjectCompiler::new();
77        let output = compiler.compile(&project)?;
78        (
79            Some(
80                ContractsByArtifactBuilder::new(
81                    output.artifact_ids().map(|(id, artifact)| (id, artifact.into())),
82                )
83                .with_storage_layouts(output.artifact_ids().filter_map(|(id, artifact)| {
84                    artifact.storage_layout.as_ref().map(|layout| (id, layout.clone()))
85                }))
86                .build(),
87            ),
88            ContractSources::from_project_output(&output, project.root(), None)?,
89        )
90    } else {
91        (None, ContractSources::default())
92    };
93
94    let mut builder = CallTraceDecoderBuilder::new()
95        .with_tracing_config(tracing)
96        .with_signature_identifier(SignaturesIdentifier::from_config(config)?)
97        .with_networks(context.networks())
98        .with_chain_id(Some(context.chain().id()))
99        .with_hardfork(context.hardfork());
100    let mut identifier = TraceIdentifiers::new().with_external(config, Some(context.chain()))?;
101    if let Some(contracts) = &known_contracts {
102        builder = builder.with_known_contracts(contracts);
103        identifier = identifier.with_local_and_bytecodes(contracts, contracts_bytecode);
104    }
105
106    let mut decoder = builder.build();
107
108    for (_, trace) in result.traces.as_deref_mut().unwrap_or_default() {
109        decoder.identify(trace, &mut identifier);
110    }
111
112    if tracing.decode_internal || debug {
113        if let Some(ref etherscan_identifier) = identifier.external {
114            sources.merge(etherscan_identifier.get_compiled_contracts().await?);
115        }
116
117        if debug {
118            let mut builder = Debugger::builder()
119                .traces(result.traces.expect("missing traces"))
120                .decoder(&decoder)
121                .sources(sources);
122            if let Some(known_contracts) = &known_contracts {
123                builder = builder.known_contracts(known_contracts);
124            }
125            let mut debugger = builder.build();
126            debugger.try_run_tui()?;
127            return Ok(());
128        }
129
130        decoder.debug_identifier = Some(DebugTraceIdentifier::new(sources));
131    }
132
133    // Collected before printing, which may prune the calls that carry them, e.g. ERC-4337 user
134    // operations.
135    let attributions = result
136        .traces
137        .iter()
138        .flatten()
139        .flat_map(|(_, arena)| arena.nodes())
140        .filter_map(|node| Attribution::decode(&node.trace.data))
141        .unique()
142        .collect::<Vec<_>>();
143
144    print_traces(
145        &mut result,
146        &decoder,
147        tracing.verbosity > 0,
148        tracing.verbosity > 4,
149        tracing.trace_depth,
150    )
151    .await?;
152
153    if !shell::is_json() {
154        for attribution in attributions {
155            sh_println!("ERC-8021 attribution: {attribution}")?;
156        }
157    }
158
159    Ok(())
160}
161
162#[cfg(all(test, feature = "monad"))]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn remote_trace_hardfork_ignores_cross_network_override() {
168        let ethereum = FoundryHardfork::Ethereum(foundry_evm::hardforks::EthereumHardfork::Cancun);
169        let monad_eight = FoundryHardfork::Monad(foundry_evm::hardforks::MonadHardfork::MonadEight);
170        let monad_nine = FoundryHardfork::Monad(foundry_evm::hardforks::MonadHardfork::MonadNine);
171
172        assert_eq!(
173            select_remote_trace_hardfork(Some(ethereum), Some(monad_nine), NetworkVariant::Monad),
174            Some(monad_nine)
175        );
176        assert_eq!(
177            select_remote_trace_hardfork(
178                Some(monad_eight),
179                Some(monad_nine),
180                NetworkVariant::Monad
181            ),
182            Some(monad_eight)
183        );
184    }
185}