Skip to main content

foundry_debugger/
builder.rs

1//! Debugger builder.
2
3use crate::soldb::{SoldbDebugger, write_bundle};
4use alloy_primitives::{
5    Address, Bytes,
6    map::{AddressHashMap, HashMap},
7};
8use eyre::Result;
9use foundry_common::{ContractsByArtifact, get_contract_name};
10use foundry_evm_core::Breakpoints;
11use foundry_evm_traces::{
12    CallTraceArena, CallTraceDecoder, CallTraceNode, Traces,
13    debug::{ContractSources, DebugRecording},
14};
15use std::path::Path;
16
17/// Debugger builder.
18#[derive(Debug, Default)]
19#[must_use = "builders do nothing unless you call `run` on them"]
20pub struct DebuggerBuilder {
21    /// Debug traces returned from the EVM execution.
22    trace_arenas: Vec<CallTraceArena>,
23    /// The steps a debug recorder recorded for each trace arena, if one ran.
24    debug_steps: Vec<Option<DebugRecording>>,
25    /// Identified contracts.
26    identified_contracts: AddressHashMap<String>,
27    /// Known local contracts and their compiler metadata.
28    known_contracts: ContractsByArtifact,
29    /// Map of source files.
30    sources: ContractSources,
31    /// Map of the debugger breakpoints.
32    breakpoints: Breakpoints,
33}
34
35impl DebuggerBuilder {
36    /// Creates a new debugger builder.
37    #[inline]
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Extends the debug arena.
43    #[inline]
44    pub fn traces(mut self, traces: Traces) -> Self {
45        for (_, arena) in traces {
46            self = self.trace_arena(arena.arena);
47            *self.debug_steps.last_mut().unwrap() = arena.debug_steps;
48        }
49        self
50    }
51
52    /// Extends the debug arena.
53    #[inline]
54    pub fn trace_arena(mut self, arena: CallTraceArena) -> Self {
55        self.trace_arenas.push(arena);
56        self.debug_steps.push(None);
57        self
58    }
59
60    /// Extends the identified contracts from multiple decoders.
61    #[inline]
62    pub fn decoders(mut self, decoders: &[CallTraceDecoder]) -> Self {
63        for decoder in decoders {
64            self = self.decoder(decoder);
65        }
66        self
67    }
68
69    /// Extends the identified contracts from a decoder.
70    #[inline]
71    pub fn decoder(mut self, decoder: &CallTraceDecoder) -> Self {
72        for (address, identifier) in &decoder.contracts {
73            self.identified_contracts.insert(*address, get_contract_name(identifier).to_string());
74        }
75        self
76    }
77
78    /// Sets known local contracts and their compiler metadata.
79    #[inline]
80    pub fn known_contracts(mut self, known_contracts: &ContractsByArtifact) -> Self {
81        self.known_contracts = known_contracts.clone();
82        self
83    }
84
85    /// Extends the identified contracts.
86    #[inline]
87    pub fn identified_contracts(
88        mut self,
89        identified_contracts: impl IntoIterator<Item = (Address, String)>,
90    ) -> Self {
91        self.identified_contracts.extend(identified_contracts);
92        self
93    }
94
95    /// Sets the sources for the debugger.
96    #[inline]
97    pub fn sources(mut self, sources: ContractSources) -> Self {
98        self.sources = sources;
99        self
100    }
101
102    /// Sets the breakpoints for the debugger.
103    #[inline]
104    pub fn breakpoints(mut self, breakpoints: Breakpoints) -> Self {
105        self.breakpoints = breakpoints;
106        self
107    }
108
109    /// Opens SolDB, or writes a bundle for its CLI, profiler, and DAP server.
110    ///
111    /// Every trace arena belongs to the same session, including deployment and setup.
112    pub fn run(self, dump: Option<&Path>) -> Result<()> {
113        let Self {
114            trace_arenas,
115            debug_steps,
116            identified_contracts,
117            known_contracts,
118            sources,
119            breakpoints,
120        } = self;
121        // The steps come from the recorder when it ran for every arena, else from the tracer.
122        let recordings = if debug_steps.iter().all(Option::is_some) {
123            debug_steps.into_iter().flatten().collect()
124        } else {
125            eyre::ensure!(
126                debug_steps.iter().all(Option::is_none),
127                "only some debug traces have recorded steps"
128            );
129            Vec::new()
130        };
131        let mut identified_code = HashMap::default();
132        let contract_names = trace_arenas
133            .iter()
134            .map(|arena| {
135                arena
136                    .nodes()
137                    .iter()
138                    .map(|node| {
139                        node_contract_name(
140                            node,
141                            &known_contracts,
142                            &identified_contracts,
143                            &mut identified_code,
144                        )
145                    })
146                    .collect::<Vec<_>>()
147            })
148            .collect::<Vec<_>>();
149        match dump {
150            Some(dir) => write_bundle(
151                dir,
152                trace_arenas,
153                recordings,
154                &contract_names,
155                &sources,
156                &known_contracts,
157            ),
158            None => SoldbDebugger::new(
159                trace_arenas,
160                recordings,
161                &contract_names,
162                &sources,
163                &known_contracts,
164                &breakpoints,
165            )?
166            .try_run(),
167        }
168    }
169}
170
171/// Identifies the contract executed by `node` from its recorded bytecode, since an address can
172/// execute different code over time (e.g. after `vm.etch`).
173fn node_contract_name(
174    node: &CallTraceNode,
175    known_contracts: &ContractsByArtifact,
176    identified_contracts: &AddressHashMap<String>,
177    identified_code: &mut HashMap<(Address, Bytes), Option<String>>,
178) -> Option<String> {
179    let address = node.trace.address;
180    let address_name = identified_contracts.get(&address);
181    match &node.trace.bytecode {
182        Some(code) if !code.is_empty() && !node.trace.kind.is_any_create() => identified_code
183            .entry((address, code.clone()))
184            .or_insert_with(|| identify_code(known_contracts, address_name, code))
185            .clone(),
186        _ => address_name.cloned(),
187    }
188}
189
190/// Identifies `code` by an exact match against local artifacts. Identities that aren't local
191/// artifacts (e.g. from Etherscan) can't be checked, so they are kept when nothing matches.
192fn identify_code(
193    known_contracts: &ContractsByArtifact,
194    address_name: Option<&String>,
195    code: &[u8],
196) -> Option<String> {
197    // Prefer the address identity only among equally strong runtime matches.
198    if let Some((id, _)) = known_contracts
199        .find_by_deployed_code_exact_preferred(code, |id| address_name == Some(&id.name))
200    {
201        return Some(id.name.clone());
202    }
203    // External identities cannot be checked against local artifacts.
204    address_name.filter(|name| !known_contracts.iter().any(|(id, _)| id.name == **name)).cloned()
205}