1use 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#[derive(Debug, Default)]
19#[must_use = "builders do nothing unless you call `build` on them"]
20pub struct DebuggerBuilder {
21 trace_arenas: Vec<CallTraceArena>,
23 stats: DebuggerStats,
25 identified_contracts: AddressHashMap<String>,
27 contract_identifiers: AddressHashMap<String>,
29 known_contracts: ContractsByArtifact,
31 precompile_labels: AddressHashMap<String>,
33 sources: ContractSources,
35 breakpoints: Breakpoints,
37 layout: DebuggerLayout,
39}
40
41impl DebuggerBuilder {
42 #[inline]
44 pub fn new() -> Self {
45 Self::default()
46 }
47
48 #[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 #[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 #[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 #[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 #[inline]
96 pub fn known_contracts(mut self, known_contracts: &ContractsByArtifact) -> Self {
97 self.known_contracts = known_contracts.clone();
98 self
99 }
100
101 #[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 #[inline]
113 pub fn sources(mut self, sources: ContractSources) -> Self {
114 self.sources = sources;
115 self
116 }
117
118 #[inline]
120 pub fn breakpoints(mut self, breakpoints: Breakpoints) -> Self {
121 self.breakpoints = breakpoints;
122 self
123 }
124
125 #[inline]
127 pub const fn layout(mut self, layout: DebuggerLayout) -> Self {
128 self.layout = layout;
129 self
130 }
131
132 #[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
190fn 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
222fn identify_code(
225 known_contracts: &ContractsByArtifact,
226 address_name: Option<&String>,
227 code: &[u8],
228) -> Option<String> {
229 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 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}