foundry_evm_traces/backtrace/
mod.rs

1//! Solidity stack trace support for test failures.
2
3use 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/// Linked library information for backtrace resolution.
17///
18/// Contains the path, name, and deployed address of a linked library
19/// to enable proper frame resolution in backtraces.
20#[derive(Debug, Clone)]
21struct LinkedLib {
22    /// The source file path of the library
23    path: PathBuf,
24    /// The name of the library contract
25    name: String,
26    /// The deployed address of the library
27    address: Address,
28}
29
30/// Holds a reference to [`ProjectCompileOutput`] to fetch artifacts and sources for backtrace
31/// generation.
32pub struct BacktraceBuilder<'a> {
33    /// Linked libraries from configuration
34    linked_libraries: Vec<LinkedLib>,
35    /// Reference to project output for on-demand source loading
36    output: &'a ProjectCompileOutput,
37    /// Project root
38    root: PathBuf,
39    /// Disable source locations
40    ///
41    /// Source locations will be inaccurately reported if the files have been compiled with via-ir
42    disable_source_locs: bool,
43    /// Sources grouped by [`ArtifactId::build_id`] to avoid re-reading files for artifacts from
44    /// the same build
45    ///
46    /// The source [`Vec`] is indexed by the compiler source ID, and contains the source path and
47    /// source content.
48    build_sources_cache: HashMap<String, Vec<(PathBuf, String)>>,
49}
50
51impl<'a> BacktraceBuilder<'a> {
52    /// Instantiates a backtrace builder from a [`ProjectCompileOutput`].
53    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    /// Generates a backtrace from a [`SparsedTraceArena`].
87    pub fn from_traces(&mut self, arena: &SparsedTraceArena) -> Backtrace<'_> {
88        // Resolve addresses to artifacts using trace labels and linked libraries
89        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    /// Resolves contract addresses to [`ArtifactId`] and their [`SourceData`] from trace labels and
109    /// linked libraries.
110    fn resolve_addresses(
111        &self,
112        arena: &SparsedTraceArena,
113    ) -> HashMap<Address, (ArtifactId, SourceData)> {
114        let mut artifacts_by_address = HashMap::default();
115
116        // Collect all labels from traces first
117        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        // Build linked library target IDs
131        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            // Match and insert artifacts using trace labels
150            if let Some(address) = label_to_address.get(artifact_id.name.as_str())
151                && let Some((source_map, bytecode)) = get_source(artifact)
152            {
153                // Match and insert artifacts using trace labels
154                artifacts_by_address
155                    .insert(*address, (artifact_id.clone(), SourceData { source_map, bytecode }));
156            } else if let Some(&lib_address) =
157                // Match and insert the linked library artifacts
158                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                // Insert linked libraries
168                artifacts_by_address
169                    .insert(lib_address, (artifact_id, SourceData { source_map, bytecode }));
170            }
171        }
172
173        artifacts_by_address
174    }
175}
176
177/// A Solidity stack trace for a test failure.
178///
179/// Generates a backtrace from a [`SparsedTraceArena`] by leveraging source maps and bytecode.
180///
181/// It uses the program counter (PC) from the traces to map to a specific source location for the
182/// call.
183///
184/// Each step/call in the backtrace is classified as a BacktraceFrame
185#[non_exhaustive]
186pub struct Backtrace<'a> {
187    /// The frames of the backtrace, from innermost (where the revert happened) to outermost.
188    frames: Vec<BacktraceFrame>,
189    /// Map from address to PcSourceMapper
190    pc_mappers: HashMap<Address, PcSourceMapper<'a>>,
191    /// Linked libraries from configuration
192    linked_libraries: Vec<LinkedLib>,
193    /// Disable pinpointing source locations in files
194    ///
195    /// Should be disabled when via-ir is enabled
196    disable_source_locs: bool,
197}
198
199impl<'a> Backtrace<'a> {
200    /// Creates a backtrace from collected artifacts and sources.
201    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        // Build PC source mappers for each contract
211        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    /// Extracts backtrace frames from a trace arena.
229    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        // Find the deepest failed node (where the actual revert happened)
237        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        // Build the call stack by walking from the deepest node back to root
252        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    /// Creates a frame from a call trace.
265    fn create_frame(&self, trace: &CallTrace) -> Option<BacktraceFrame> {
266        let contract_address = trace.address;
267        let mut frame = BacktraceFrame::new(contract_address);
268
269        // Try to get source location from PC mapper
270        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    /// Returns true if the backtrace is empty.
305    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/// A single frame in a backtrace.
329#[derive(Debug, Clone)]
330struct BacktraceFrame {
331    /// The contract address where this frame is executing.
332    pub contract_address: Address,
333    /// The contract name, if known.
334    pub contract_name: Option<String>,
335    /// The function name, if known.
336    pub function_name: Option<String>,
337    /// The source file path.
338    pub file: Option<PathBuf>,
339    /// The line number in the source file.
340    pub line: Option<usize>,
341    /// The column number in the source file.
342    pub column: Option<usize>,
343    /// The byte offset in the source file.
344    pub byte_offset: Option<usize>,
345}
346
347impl BacktraceFrame {
348    /// Creates a new backtrace frame.
349    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    /// Sets the contract name.
362    fn with_contract_name(mut self, name: String) -> Self {
363        self.contract_name = Some(name);
364        self
365    }
366
367    /// Sets the function name.
368    fn with_function_name(mut self, name: String) -> Self {
369        self.function_name = Some(name);
370        self
371    }
372
373    /// Sets the source location.
374    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    /// Sets the byte offset.
382    fn with_byte_offset(mut self, offset: usize) -> Self {
383        self.byte_offset = Some(offset);
384        self
385    }
386}
387
388// Format: <CONTRACT_NAME>.<FUNCTION_NAME> (FILE:LINE:COL)
389impl fmt::Display for BacktraceFrame {
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        let mut result = String::new();
392
393        // No contract name, show address
394        result.push_str(self.contract_name.as_ref().unwrap_or(&self.contract_address.to_string()));
395
396        // Add function name if available
397        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        // Add location in parentheses if available
413        if self.file.is_some() || self.line.is_some() {
414            result.push(')');
415        }
416
417        write!(f, "{result}")
418    }
419}