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::{Address, map::AddressHashMap};
7use foundry_common::get_contract_name;
8use foundry_evm_core::Breakpoints;
9use foundry_evm_traces::{
10    CallTraceArena, CallTraceDecoder, Traces,
11    debug::{ContractSources, DebugTraceIdentifier},
12};
13
14/// Debugger builder.
15#[derive(Debug, Default)]
16#[must_use = "builders do nothing unless you call `build` on them"]
17pub struct DebuggerBuilder {
18    /// Debug traces returned from the EVM execution.
19    trace_arenas: Vec<CallTraceArena>,
20    /// Aggregate stats for the traces passed to the debugger.
21    stats: DebuggerStats,
22    /// Identified contracts.
23    identified_contracts: AddressHashMap<String>,
24    /// Active precompile labels for the current trace context.
25    precompile_labels: AddressHashMap<String>,
26    /// Map of source files.
27    sources: ContractSources,
28    /// Map of the debugger breakpoints.
29    breakpoints: Breakpoints,
30    /// TUI layout selection.
31    layout: DebuggerLayout,
32}
33
34impl DebuggerBuilder {
35    /// Creates a new debugger builder.
36    #[inline]
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Extends the debug arena.
42    #[inline]
43    pub fn traces(mut self, traces: Traces) -> Self {
44        for (_, arena) in traces {
45            self = self.trace_arena(arena.arena);
46        }
47        self
48    }
49
50    /// Extends the debug arena.
51    #[inline]
52    pub fn trace_arena(mut self, arena: CallTraceArena) -> Self {
53        if let Some(root) = arena.nodes().first() {
54            self.stats.session_trace_gas_used =
55                self.stats.session_trace_gas_used.saturating_add(root.trace.gas_used);
56        }
57        self.stats.session_subcalls =
58            self.stats.session_subcalls.saturating_add(arena.nodes().len().saturating_sub(1));
59        self.trace_arenas.push(arena);
60        self
61    }
62
63    /// Extends the identified contracts from multiple decoders.
64    #[inline]
65    pub fn decoders(mut self, decoders: &[CallTraceDecoder]) -> Self {
66        for decoder in decoders {
67            self = self.decoder(decoder);
68        }
69        self
70    }
71
72    /// Extends the identified contracts from a decoder.
73    #[inline]
74    pub fn decoder(mut self, decoder: &CallTraceDecoder) -> Self {
75        let c = decoder.contracts.iter().map(|(k, v)| (*k, get_contract_name(v).to_string()));
76        self.identified_contracts.extend(c);
77        self.precompile_labels.extend(decoder.precompile_labels());
78        self
79    }
80
81    /// Extends the identified contracts.
82    #[inline]
83    pub fn identified_contracts(
84        mut self,
85        identified_contracts: impl IntoIterator<Item = (Address, String)>,
86    ) -> Self {
87        self.identified_contracts.extend(identified_contracts);
88        self
89    }
90
91    /// Sets the sources for the debugger.
92    #[inline]
93    pub fn sources(mut self, sources: ContractSources) -> Self {
94        self.sources = sources;
95        self
96    }
97
98    /// Sets the breakpoints for the debugger.
99    #[inline]
100    pub fn breakpoints(mut self, breakpoints: Breakpoints) -> Self {
101        self.breakpoints = breakpoints;
102        self
103    }
104
105    /// Sets the TUI layout for the debugger.
106    #[inline]
107    pub const fn layout(mut self, layout: DebuggerLayout) -> Self {
108        self.layout = layout;
109        self
110    }
111
112    /// Builds the debugger.
113    #[inline]
114    pub fn build(self) -> Debugger {
115        let Self {
116            mut trace_arenas,
117            stats,
118            identified_contracts,
119            precompile_labels,
120            sources,
121            breakpoints,
122            layout,
123        } = self;
124        identify_internal_calls(&mut trace_arenas, &identified_contracts, &sources);
125        let mut debug_arena = Vec::new();
126        for arena in trace_arenas {
127            flatten_call_trace_with_precompiles(arena, &mut debug_arena, &precompile_labels);
128        }
129        Debugger::new_with_stats(
130            debug_arena,
131            stats,
132            identified_contracts,
133            sources,
134            breakpoints,
135            layout,
136        )
137    }
138}
139
140fn identify_internal_calls(
141    trace_arenas: &mut [CallTraceArena],
142    identified_contracts: &AddressHashMap<String>,
143    sources: &ContractSources,
144) {
145    if sources.artifacts_by_name.is_empty() {
146        return;
147    }
148
149    for arena in trace_arenas {
150        for node in arena.nodes_mut() {
151            let Some(contract_name) = identified_contracts.get(&node.trace.address) else {
152                continue;
153            };
154            DebugTraceIdentifier::identify_node_steps_with_sources(node, sources, contract_name);
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use alloy_primitives::Bytes;
163    use foundry_evm_traces::{CallKind, CallTrace, CallTraceNode};
164    use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
165    use revm_inspectors::tracing::types::{CallTraceStep, TraceMemberOrder};
166
167    fn step() -> CallTraceStep {
168        CallTraceStep {
169            pc: 0,
170            op: OpCode::STOP,
171            stack: None,
172            push_stack: None,
173            memory: None,
174            returndata: Bytes::new(),
175            gas_remaining: 0,
176            gas_refund_counter: 0,
177            gas_used: 0,
178            gas_cost: 0,
179            storage_change: None,
180            status: Some(InstructionResult::Stop),
181            immediate_bytes: None,
182            decoded: None,
183        }
184    }
185
186    fn trace_arena(gas_used: u64, subcalls: usize) -> CallTraceArena {
187        let mut arena = CallTraceArena::default();
188
189        {
190            let root = &mut arena.nodes_mut()[0];
191            root.trace.steps.push(step());
192            root.trace.gas_limit = 1;
193            root.trace.gas_used = gas_used;
194            root.ordering.push(TraceMemberOrder::Step(0));
195
196            for idx in 1..=subcalls {
197                root.ordering.push(TraceMemberOrder::Call(idx - 1));
198                root.children.push(idx);
199            }
200        }
201
202        for idx in 1..=subcalls {
203            arena.nodes_mut().push(CallTraceNode {
204                parent: Some(0),
205                idx,
206                trace: CallTrace { depth: 1, kind: CallKind::Call, ..Default::default() },
207                ..Default::default()
208            });
209        }
210
211        arena
212    }
213
214    #[test]
215    fn trace_arena_accumulates_stats() {
216        let builder = DebuggerBuilder::new().trace_arena(trace_arena(100, 1));
217
218        assert_eq!(builder.stats.session_subcalls, 1);
219        assert_eq!(builder.stats.session_trace_gas_used, 100);
220        assert_eq!(builder.trace_arenas.len(), 1);
221    }
222
223    #[test]
224    fn trace_arena_accumulates_session_stats_across_multiple_arenas() {
225        let builder = DebuggerBuilder::new()
226            .trace_arena(trace_arena(100, 1))
227            .trace_arena(trace_arena(200, 2));
228
229        assert_eq!(builder.stats.session_subcalls, 3);
230        assert_eq!(builder.stats.session_trace_gas_used, 300);
231        assert_eq!(builder.trace_arenas.len(), 2);
232    }
233}