Skip to main content

foundry_debugger/
builder.rs

1//! Debugger builder.
2
3use crate::{
4    Debugger, DebuggerLayout, debugger::DebuggerStats, node::flatten_call_trace_with_precompiles,
5};
6use alloy_primitives::{
7    Address, Bytes,
8    map::{AddressHashMap, HashMap},
9};
10use foundry_common::{ContractsByArtifact, get_contract_name, slot_identifier::SlotIdentifier};
11use foundry_evm_core::Breakpoints;
12use foundry_evm_traces::{
13    CallTraceArena, CallTraceDecoder, CallTraceNode, Traces,
14    debug::{ContractSources, DebugTraceIdentifier},
15};
16
17/// Debugger builder.
18#[derive(Debug, Default)]
19#[must_use = "builders do nothing unless you call `build` on them"]
20pub struct DebuggerBuilder {
21    /// Debug traces returned from the EVM execution.
22    trace_arenas: Vec<CallTraceArena>,
23    /// Aggregate stats for the traces passed to the debugger.
24    stats: DebuggerStats,
25    /// Identified contracts.
26    identified_contracts: AddressHashMap<String>,
27    /// Full artifact identifiers for identified contracts.
28    contract_identifiers: AddressHashMap<String>,
29    /// Known local contracts and their compiler metadata.
30    known_contracts: ContractsByArtifact,
31    /// Active precompile labels for the current trace context.
32    precompile_labels: AddressHashMap<String>,
33    /// Map of source files.
34    sources: ContractSources,
35    /// Map of the debugger breakpoints.
36    breakpoints: Breakpoints,
37    /// TUI layout selection.
38    layout: DebuggerLayout,
39}
40
41impl DebuggerBuilder {
42    /// Creates a new debugger builder.
43    #[inline]
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Extends the debug arena.
49    ///
50    /// Internal calls are decoded during [`Self::build`], after resolving each frame's contract.
51    #[inline]
52    pub fn traces(mut self, traces: Traces) -> Self {
53        for (_, arena) in traces {
54            self = self.trace_arena(arena.arena);
55        }
56        self
57    }
58
59    /// Extends the debug arena.
60    ///
61    /// Internal calls are decoded during [`Self::build`], after resolving each frame's contract.
62    #[inline]
63    pub fn trace_arena(mut self, arena: CallTraceArena) -> Self {
64        if let Some(root) = arena.nodes().first() {
65            self.stats.session_trace_gas_used =
66                self.stats.session_trace_gas_used.saturating_add(root.trace.gas_used);
67        }
68        self.stats.session_subcalls =
69            self.stats.session_subcalls.saturating_add(arena.nodes().len().saturating_sub(1));
70        self.trace_arenas.push(arena);
71        self
72    }
73
74    /// Extends the identified contracts from multiple decoders.
75    #[inline]
76    pub fn decoders(mut self, decoders: &[CallTraceDecoder]) -> Self {
77        for decoder in decoders {
78            self = self.decoder(decoder);
79        }
80        self
81    }
82
83    /// Extends the identified contracts from a decoder.
84    #[inline]
85    pub fn decoder(mut self, decoder: &CallTraceDecoder) -> Self {
86        for (address, identifier) in &decoder.contracts {
87            self.identified_contracts.insert(*address, get_contract_name(identifier).to_string());
88            self.contract_identifiers.insert(*address, identifier.clone());
89        }
90        self.precompile_labels.extend(decoder.precompile_labels());
91        self
92    }
93
94    /// Sets known local contracts used to identify storage slots.
95    #[inline]
96    pub fn known_contracts(mut self, known_contracts: &ContractsByArtifact) -> Self {
97        self.known_contracts = known_contracts.clone();
98        self
99    }
100
101    /// Extends the identified contracts.
102    #[inline]
103    pub fn identified_contracts(
104        mut self,
105        identified_contracts: impl IntoIterator<Item = (Address, String)>,
106    ) -> Self {
107        self.identified_contracts.extend(identified_contracts);
108        self
109    }
110
111    /// Sets the sources for the debugger.
112    #[inline]
113    pub fn sources(mut self, sources: ContractSources) -> Self {
114        self.sources = sources;
115        self
116    }
117
118    /// Sets the breakpoints for the debugger.
119    #[inline]
120    pub fn breakpoints(mut self, breakpoints: Breakpoints) -> Self {
121        self.breakpoints = breakpoints;
122        self
123    }
124
125    /// Sets the TUI layout for the debugger.
126    #[inline]
127    pub const fn layout(mut self, layout: DebuggerLayout) -> Self {
128        self.layout = layout;
129        self
130    }
131
132    /// Builds the debugger.
133    #[inline]
134    pub fn build(self) -> Debugger {
135        let Self {
136            trace_arenas,
137            stats,
138            identified_contracts,
139            contract_identifiers,
140            known_contracts,
141            precompile_labels,
142            sources,
143            breakpoints,
144            layout,
145        } = self;
146        let slot_identifiers = contract_identifiers
147            .into_iter()
148            .filter_map(|(address, identifier)| {
149                let (_, contract) =
150                    known_contracts.find_by_name_or_identifier(&identifier).ok().flatten()?;
151                let layout = contract.storage_layout.clone()?;
152                Some((address, SlotIdentifier::new(layout)))
153            })
154            .collect();
155        let mut identified_code = HashMap::default();
156        let mut debug_arena = Vec::new();
157        for mut arena in trace_arenas {
158            let contract_names = arena
159                .nodes_mut()
160                .iter_mut()
161                .map(|node| {
162                    identify_node(
163                        node,
164                        &known_contracts,
165                        &identified_contracts,
166                        &sources,
167                        &mut identified_code,
168                    )
169                })
170                .collect::<Vec<_>>();
171            flatten_call_trace_with_precompiles(
172                arena,
173                &mut debug_arena,
174                &precompile_labels,
175                &contract_names,
176            );
177        }
178        Debugger::new_with_stats(
179            debug_arena,
180            stats,
181            identified_contracts,
182            slot_identifiers,
183            sources,
184            breakpoints,
185            layout,
186        )
187    }
188}
189
190/// Identifies the contract executed by `node` from its recorded bytecode, since an address can
191/// execute different code over time (e.g. after `vm.etch`), and decodes its internal calls.
192fn identify_node(
193    node: &mut CallTraceNode,
194    known_contracts: &ContractsByArtifact,
195    identified_contracts: &AddressHashMap<String>,
196    sources: &ContractSources,
197    identified_code: &mut HashMap<(Address, Bytes), Option<String>>,
198) -> Option<String> {
199    let address = node.trace.address;
200    let address_name = identified_contracts.get(&address);
201    let contract_name = match &node.trace.bytecode {
202        Some(code) if !code.is_empty() && !node.trace.kind.is_any_create() => identified_code
203            .entry((address, code.clone()))
204            .or_insert_with(|| identify_code(known_contracts, address_name, code))
205            .clone(),
206        _ => address_name.cloned(),
207    };
208    if contract_name.as_ref() != address_name
209        && let Some(decoded) = node.trace.decoded.as_mut()
210        && decoded.label.as_ref() == address_name
211    {
212        decoded.label.clone_from(&contract_name);
213    }
214    if let Some(contract_name) = &contract_name
215        && !sources.artifacts_by_name.is_empty()
216    {
217        DebugTraceIdentifier::identify_node_steps_with_sources(node, sources, contract_name);
218    }
219    contract_name
220}
221
222/// Identifies `code` by an exact match against local artifacts. Identities that aren't local
223/// artifacts (e.g. from Etherscan) can't be checked, so they are kept when nothing matches.
224fn identify_code(
225    known_contracts: &ContractsByArtifact,
226    address_name: Option<&String>,
227    code: &[u8],
228) -> Option<String> {
229    // Prefer the address identity only among equally strong runtime matches.
230    if let Some((id, _)) = known_contracts
231        .find_by_deployed_code_exact_preferred(code, |id| address_name == Some(&id.name))
232    {
233        return Some(id.name.clone());
234    }
235    // External identities cannot be checked against local artifacts.
236    address_name.filter(|name| !known_contracts.iter().any(|(id, _)| id.name == **name)).cloned()
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use foundry_evm_traces::{CallKind, CallTrace, CallTraceStep, TraceMemberOrder};
243    use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
244
245    fn step() -> CallTraceStep {
246        CallTraceStep {
247            pc: 0,
248            op: OpCode::STOP,
249            stack: None,
250            push_stack: None,
251            memory: None,
252            returndata: Bytes::new(),
253            gas_remaining: 0,
254            gas_refund_counter: 0,
255            gas_used: 0,
256            gas_cost: 0,
257            state_gas_cost: None,
258            state_gas_reservoir: None,
259            state_gas_spent: 0,
260            storage_change: None,
261            status: Some(InstructionResult::Stop),
262            immediate_bytes: None,
263            decoded: None,
264        }
265    }
266
267    fn trace_arena(gas_used: u64, subcalls: usize) -> CallTraceArena {
268        let mut arena = CallTraceArena::default();
269
270        {
271            let root = &mut arena.nodes_mut()[0];
272            root.trace.steps.push(step());
273            root.trace.gas_limit = 1;
274            root.trace.gas_used = gas_used;
275            root.ordering.push(TraceMemberOrder::Step(0));
276
277            for idx in 1..=subcalls {
278                root.ordering.push(TraceMemberOrder::Call(idx - 1));
279                root.children.push(idx);
280            }
281        }
282
283        for idx in 1..=subcalls {
284            arena.nodes_mut().push(CallTraceNode {
285                parent: Some(0),
286                idx,
287                trace: CallTrace { depth: 1, kind: CallKind::Call, ..Default::default() },
288                ..Default::default()
289            });
290        }
291
292        arena
293    }
294
295    #[test]
296    fn trace_arena_accumulates_stats() {
297        let builder = Debugger::builder().trace_arena(trace_arena(100, 1));
298
299        assert_eq!(builder.stats.session_subcalls, 1);
300        assert_eq!(builder.stats.session_trace_gas_used, 100);
301        assert_eq!(builder.trace_arenas.len(), 1);
302    }
303
304    #[test]
305    fn trace_arena_accumulates_session_stats_across_multiple_arenas() {
306        let builder =
307            Debugger::builder().trace_arena(trace_arena(100, 1)).trace_arena(trace_arena(200, 2));
308
309        assert_eq!(builder.stats.session_subcalls, 3);
310        assert_eq!(builder.stats.session_trace_gas_used, 300);
311        assert_eq!(builder.trace_arenas.len(), 2);
312    }
313}