foundry_evm_traces/backtrace/
mod.rs1use crate::{CallTrace, SparsedTraceArena};
4use alloy_primitives::{Address, Bytes, map::HashMap};
5use foundry_compilers::{
6 Artifact, ArtifactId, ProjectCompileOutput,
7 artifacts::{ConfigurableContractArtifact, Libraries, sourcemap::SourceMap},
8};
9use std::{fmt, path::PathBuf};
10use yansi::Paint;
11
12mod source_map;
13use source_map::load_build_sources;
14pub use source_map::{PcSourceMapper, SourceData};
15
16#[derive(Debug, Clone)]
21struct LinkedLib {
22 path: PathBuf,
24 name: String,
26 address: Address,
28}
29
30pub struct BacktraceBuilder<'a> {
33 linked_libraries: Vec<LinkedLib>,
35 output: &'a ProjectCompileOutput,
37 root: PathBuf,
39 disable_source_locs: bool,
43 build_sources_cache: HashMap<String, Vec<(PathBuf, String)>>,
49}
50
51impl<'a> BacktraceBuilder<'a> {
52 pub fn new(
54 output: &'a ProjectCompileOutput,
55 root: PathBuf,
56 linked_libraries: Option<Libraries>,
57 disable_source_locs: bool,
58 ) -> Self {
59 let linked_libs = linked_libraries
60 .map(|libs| {
61 libs.libs
62 .iter()
63 .flat_map(|(path, libs_map)| {
64 libs_map.iter().map(move |(name, addr_str)| (path, name, addr_str))
65 })
66 .filter_map(|(path, name, addr_str)| {
67 addr_str.parse().ok().map(|address| LinkedLib {
68 path: path.clone(),
69 name: name.clone(),
70 address,
71 })
72 })
73 .collect()
74 })
75 .unwrap_or_default();
76
77 Self {
78 linked_libraries: linked_libs,
79 output,
80 root,
81 disable_source_locs,
82 build_sources_cache: HashMap::default(),
83 }
84 }
85
86 pub fn from_traces(&mut self, arena: &SparsedTraceArena) -> Backtrace<'_> {
88 let artifacts_by_address = self.resolve_addresses(arena);
90 for (artifact_id, _) in artifacts_by_address.values() {
91 let build_id = &artifact_id.build_id;
92 if !self.build_sources_cache.contains_key(build_id)
93 && let Some(sources) = load_build_sources(build_id, self.output, &self.root)
94 {
95 self.build_sources_cache.insert(build_id.clone(), sources);
96 }
97 }
98
99 Backtrace::new(
100 artifacts_by_address,
101 &self.build_sources_cache,
102 self.linked_libraries.clone(),
103 self.disable_source_locs,
104 arena,
105 )
106 }
107
108 fn resolve_addresses(
111 &self,
112 arena: &SparsedTraceArena,
113 ) -> HashMap<Address, (ArtifactId, SourceData)> {
114 let mut artifacts_by_address = HashMap::default();
115
116 let label_to_address = arena
118 .nodes()
119 .iter()
120 .filter_map(|node| {
121 if let Some(decoded) = &node.trace.decoded
122 && let Some(label) = &decoded.label
123 {
124 return Some((label.as_str(), node.trace.address));
125 }
126 None
127 })
128 .collect::<HashMap<_, _>>();
129
130 let linked_lib_targets = self
132 .linked_libraries
133 .iter()
134 .map(|lib| (format!("{}:{}", lib.path.display(), lib.name), lib.address))
135 .collect::<HashMap<_, _>>();
136
137 let get_source = |artifact: &ConfigurableContractArtifact| -> Option<(SourceMap, Bytes)> {
138 let source_map = artifact.get_source_map_deployed()?.ok()?;
139 let deployed_bytecode = artifact.get_deployed_bytecode_bytes()?.into_owned();
140
141 if deployed_bytecode.is_empty() {
142 return None;
143 }
144
145 Some((source_map, deployed_bytecode))
146 };
147
148 for (artifact_id, artifact) in self.output.artifact_ids() {
149 if let Some(address) = label_to_address.get(artifact_id.name.as_str())
151 && let Some((source_map, bytecode)) = get_source(artifact)
152 {
153 artifacts_by_address
155 .insert(*address, (artifact_id.clone(), SourceData { source_map, bytecode }));
156 } else if let Some(&lib_address) =
157 linked_lib_targets.get(&artifact_id.identifier()).or_else(|| {
159 let id = artifact_id
160 .clone()
161 .with_stripped_file_prefixes(&self.root)
162 .identifier();
163 linked_lib_targets.get(&id)
164 })
165 && let Some((source_map, bytecode)) = get_source(artifact)
166 {
167 artifacts_by_address
169 .insert(lib_address, (artifact_id, SourceData { source_map, bytecode }));
170 }
171 }
172
173 artifacts_by_address
174 }
175}
176
177#[non_exhaustive]
186pub struct Backtrace<'a> {
187 frames: Vec<BacktraceFrame>,
189 pc_mappers: HashMap<Address, PcSourceMapper<'a>>,
191 linked_libraries: Vec<LinkedLib>,
193 disable_source_locs: bool,
197}
198
199impl<'a> Backtrace<'a> {
200 fn new(
202 artifacts_by_address: HashMap<Address, (ArtifactId, SourceData)>,
203 build_sources: &'a HashMap<String, Vec<(PathBuf, String)>>,
204 linked_libraries: Vec<LinkedLib>,
205 disable_source_locs: bool,
206 arena: &SparsedTraceArena,
207 ) -> Self {
208 let mut pc_mappers = HashMap::default();
209
210 if !disable_source_locs {
212 for (addr, (artifact_id, source_data)) in artifacts_by_address {
213 if let Some(sources) = build_sources.get(&artifact_id.build_id) {
214 let mapper = PcSourceMapper::new(source_data, sources);
215 pc_mappers.insert(addr, mapper);
216 }
217 }
218 }
219
220 let mut backtrace =
221 Self { frames: Vec::new(), pc_mappers, linked_libraries, disable_source_locs };
222
223 backtrace.extract_frames(arena);
224
225 backtrace
226 }
227
228 fn extract_frames(&mut self, arena: &SparsedTraceArena) {
230 let resolved_arena = &arena.arena;
231
232 if resolved_arena.nodes().is_empty() {
233 return;
234 }
235
236 let mut current_idx = None;
238 let mut max_depth = 0;
239
240 for (idx, node) in resolved_arena.nodes().iter().enumerate() {
241 if !node.trace.success && node.trace.depth >= max_depth {
242 max_depth = node.trace.depth;
243 current_idx = Some(idx);
244 }
245 }
246
247 if current_idx.is_none() {
248 return;
249 }
250
251 while let Some(idx) = current_idx {
253 let node = &resolved_arena.nodes()[idx];
254 let trace = &node.trace;
255
256 if let Some(frame) = self.create_frame(trace) {
257 self.frames.push(frame);
258 }
259
260 current_idx = node.parent;
261 }
262 }
263
264 fn create_frame(&self, trace: &CallTrace) -> Option<BacktraceFrame> {
266 let contract_address = trace.address;
267 let mut frame = BacktraceFrame::new(contract_address);
268
269 if !self.disable_source_locs
271 && let Some(source_location) = trace.steps.last().and_then(|last_step| {
272 self.pc_mappers.get(&contract_address).and_then(|m| m.map_pc(last_step.pc))
273 })
274 {
275 frame = frame
276 .with_source_location(
277 source_location.file,
278 source_location.line,
279 source_location.column,
280 )
281 .with_byte_offset(source_location.offset);
282 }
283
284 if let Some(decoded) = &trace.decoded {
285 if let Some(label) = &decoded.label {
286 frame = frame.with_contract_name(label.clone());
287 } else if let Some(lib) =
288 self.linked_libraries.iter().find(|l| l.address == contract_address)
289 {
290 frame = frame.with_contract_name(lib.name.clone());
291 }
292
293 if let Some(call_data) = &decoded.call_data {
294 let sig = &call_data.signature;
295 let func_name =
296 if let Some(paren_pos) = sig.find('(') { &sig[..paren_pos] } else { sig };
297 frame = frame.with_function_name(func_name.to_string());
298 }
299 }
300
301 Some(frame)
302 }
303
304 pub fn is_empty(&self) -> bool {
306 self.frames.is_empty()
307 }
308}
309
310impl fmt::Display for Backtrace<'_> {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 if self.frames.is_empty() {
313 return Ok(());
314 }
315
316 writeln!(f, "{}", Paint::yellow("Backtrace:"))?;
317
318 for frame in &self.frames {
319 write!(f, " ")?;
320 write!(f, "at ")?;
321 writeln!(f, "{frame}")?;
322 }
323
324 Ok(())
325 }
326}
327
328#[derive(Debug, Clone)]
330struct BacktraceFrame {
331 pub contract_address: Address,
333 pub contract_name: Option<String>,
335 pub function_name: Option<String>,
337 pub file: Option<PathBuf>,
339 pub line: Option<usize>,
341 pub column: Option<usize>,
343 pub byte_offset: Option<usize>,
345}
346
347impl BacktraceFrame {
348 fn new(contract_address: Address) -> Self {
350 Self {
351 contract_address,
352 contract_name: None,
353 function_name: None,
354 file: None,
355 line: None,
356 column: None,
357 byte_offset: None,
358 }
359 }
360
361 fn with_contract_name(mut self, name: String) -> Self {
363 self.contract_name = Some(name);
364 self
365 }
366
367 fn with_function_name(mut self, name: String) -> Self {
369 self.function_name = Some(name);
370 self
371 }
372
373 fn with_source_location(mut self, file: PathBuf, line: usize, column: usize) -> Self {
375 self.file = Some(file);
376 self.line = Some(line);
377 self.column = Some(column);
378 self
379 }
380
381 fn with_byte_offset(mut self, offset: usize) -> Self {
383 self.byte_offset = Some(offset);
384 self
385 }
386}
387
388impl fmt::Display for BacktraceFrame {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 let mut result = String::new();
392
393 result.push_str(self.contract_name.as_ref().unwrap_or(&self.contract_address.to_string()));
395
396 result.push_str(&self.function_name.as_ref().map_or(String::new(), |f| format!(".{f}")));
398
399 if let Some(file) = &self.file {
400 result.push_str(" (");
401 result.push_str(&file.display().to_string());
402 }
403
404 if let Some(line) = self.line {
405 result.push(':');
406 result.push_str(&line.to_string());
407
408 result.push(':');
409 result.push_str(&self.column.as_ref().map_or("0".to_string(), |c| c.to_string()));
410 }
411
412 if self.file.is_some() || self.line.is_some() {
414 result.push(')');
415 }
416
417 write!(f, "{result}")
418 }
419}