Skip to main content

cast/
debug.rs

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