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::{ContractsByArtifact, get_contract_name, slot_identifier::SlotIdentifier};
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    /// Full artifact identifiers for identified contracts.
25    contract_identifiers: AddressHashMap<String>,
26    /// Known local contracts and their compiler metadata.
27    known_contracts: ContractsByArtifact,
28    /// Active precompile labels for the current trace context.
29    precompile_labels: AddressHashMap<String>,
30    /// Map of source files.
31    sources: ContractSources,
32    /// Map of the debugger breakpoints.
33    breakpoints: Breakpoints,
34    /// TUI layout selection.
35    layout: DebuggerLayout,
36}
37
38impl DebuggerBuilder {
39    /// Creates a new debugger builder.
40    #[inline]
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Extends the debug arena.
46    #[inline]
47    pub fn traces(mut self, traces: Traces) -> Self {
48        for (_, arena) in traces {
49            self = self.trace_arena(arena.arena);
50        }
51        self
52    }
53
54    /// Extends the debug arena.
55    #[inline]
56    pub fn trace_arena(mut self, arena: CallTraceArena) -> Self {
57        if let Some(root) = arena.nodes().first() {
58            self.stats.session_trace_gas_used =
59                self.stats.session_trace_gas_used.saturating_add(root.trace.gas_used);
60        }
61        self.stats.session_subcalls =
62            self.stats.session_subcalls.saturating_add(arena.nodes().len().saturating_sub(1));
63        self.trace_arenas.push(arena);
64        self
65    }
66
67    /// Extends the identified contracts from multiple decoders.
68    #[inline]
69    pub fn decoders(mut self, decoders: &[CallTraceDecoder]) -> Self {
70        for decoder in decoders {
71            self = self.decoder(decoder);
72        }
73        self
74    }
75
76    /// Extends the identified contracts from a decoder.
77    #[inline]
78    pub fn decoder(mut self, decoder: &CallTraceDecoder) -> Self {
79        for (address, identifier) in &decoder.contracts {
80            self.identified_contracts.insert(*address, get_contract_name(identifier).to_string());
81            self.contract_identifiers.insert(*address, identifier.clone());
82        }
83        self.precompile_labels.extend(decoder.precompile_labels());
84        self
85    }
86
87    /// Sets known local contracts used to identify storage slots.
88    #[inline]
89    pub fn known_contracts(mut self, known_contracts: &ContractsByArtifact) -> Self {
90        self.known_contracts = known_contracts.clone();
91        self
92    }
93
94    /// Extends the identified contracts.
95    #[inline]
96    pub fn identified_contracts(
97        mut self,
98        identified_contracts: impl IntoIterator<Item = (Address, String)>,
99    ) -> Self {
100        self.identified_contracts.extend(identified_contracts);
101        self
102    }
103
104    /// Sets the sources for the debugger.
105    #[inline]
106    pub fn sources(mut self, sources: ContractSources) -> Self {
107        self.sources = sources;
108        self
109    }
110
111    /// Sets the breakpoints for the debugger.
112    #[inline]
113    pub fn breakpoints(mut self, breakpoints: Breakpoints) -> Self {
114        self.breakpoints = breakpoints;
115        self
116    }
117
118    /// Sets the TUI layout for the debugger.
119    #[inline]
120    pub const fn layout(mut self, layout: DebuggerLayout) -> Self {
121        self.layout = layout;
122        self
123    }
124
125    /// Builds the debugger.
126    #[inline]
127    pub fn build(self) -> Debugger {
128        let Self {
129            mut trace_arenas,
130            stats,
131            identified_contracts,
132            contract_identifiers,
133            known_contracts,
134            precompile_labels,
135            sources,
136            breakpoints,
137            layout,
138        } = self;
139        let slot_identifiers = contract_identifiers
140            .into_iter()
141            .filter_map(|(address, identifier)| {
142                let (_, contract) =
143                    known_contracts.find_by_name_or_identifier(&identifier).ok().flatten()?;
144                let layout = contract.storage_layout.clone()?;
145                Some((address, SlotIdentifier::new(layout)))
146            })
147            .collect();
148        identify_internal_calls(&mut trace_arenas, &identified_contracts, &sources);
149        let mut debug_arena = Vec::new();
150        for arena in trace_arenas {
151            flatten_call_trace_with_precompiles(arena, &mut debug_arena, &precompile_labels);
152        }
153        Debugger::new_with_stats(
154            debug_arena,
155            stats,
156            identified_contracts,
157            slot_identifiers,
158            sources,
159            breakpoints,
160            layout,
161        )
162    }
163}
164
165fn identify_internal_calls(
166    trace_arenas: &mut [CallTraceArena],
167    identified_contracts: &AddressHashMap<String>,
168    sources: &ContractSources,
169) {
170    if sources.artifacts_by_name.is_empty() {
171        return;
172    }
173
174    for arena in trace_arenas {
175        for node in arena.nodes_mut() {
176            let Some(contract_name) = identified_contracts.get(&node.trace.address) else {
177                continue;
178            };
179            DebugTraceIdentifier::identify_node_steps_with_sources(node, sources, contract_name);
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use alloy_primitives::Bytes;
188    use foundry_evm_traces::{CallKind, CallTrace, CallTraceNode};
189    use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
190    use revm_inspectors::tracing::types::{CallTraceStep, TraceMemberOrder};
191
192    fn step() -> CallTraceStep {
193        CallTraceStep {
194            pc: 0,
195            op: OpCode::STOP,
196            stack: None,
197            push_stack: None,
198            memory: None,
199            returndata: Bytes::new(),
200            gas_remaining: 0,
201            gas_refund_counter: 0,
202            gas_used: 0,
203            gas_cost: 0,
204            state_gas_cost: None,
205            state_gas_reservoir: None,
206            state_gas_spent: 0,
207            storage_change: None,
208            status: Some(InstructionResult::Stop),
209            immediate_bytes: None,
210            decoded: None,
211        }
212    }
213
214    fn trace_arena(gas_used: u64, subcalls: usize) -> CallTraceArena {
215        let mut arena = CallTraceArena::default();
216
217        {
218            let root = &mut arena.nodes_mut()[0];
219            root.trace.steps.push(step());
220            root.trace.gas_limit = 1;
221            root.trace.gas_used = gas_used;
222            root.ordering.push(TraceMemberOrder::Step(0));
223
224            for idx in 1..=subcalls {
225                root.ordering.push(TraceMemberOrder::Call(idx - 1));
226                root.children.push(idx);
227            }
228        }
229
230        for idx in 1..=subcalls {
231            arena.nodes_mut().push(CallTraceNode {
232                parent: Some(0),
233                idx,
234                trace: CallTrace { depth: 1, kind: CallKind::Call, ..Default::default() },
235                ..Default::default()
236            });
237        }
238
239        arena
240    }
241
242    #[test]
243    fn trace_arena_accumulates_stats() {
244        let builder = DebuggerBuilder::new().trace_arena(trace_arena(100, 1));
245
246        assert_eq!(builder.stats.session_subcalls, 1);
247        assert_eq!(builder.stats.session_trace_gas_used, 100);
248        assert_eq!(builder.trace_arenas.len(), 1);
249    }
250
251    #[test]
252    fn trace_arena_accumulates_session_stats_across_multiple_arenas() {
253        let builder = DebuggerBuilder::new()
254            .trace_arena(trace_arena(100, 1))
255            .trace_arena(trace_arena(200, 2));
256
257        assert_eq!(builder.stats.session_subcalls, 3);
258        assert_eq!(builder.stats.session_trace_gas_used, 300);
259        assert_eq!(builder.trace_arenas.len(), 2);
260    }
261}