foundry_evm_traces/debug/
mod.rs

1mod sources;
2use crate::CallTraceNode;
3use alloy_dyn_abi::{
4    parser::{Parameters, Storage},
5    DynSolType, DynSolValue, Specifier,
6};
7use alloy_primitives::U256;
8use foundry_common::fmt::format_token;
9use foundry_compilers::artifacts::sourcemap::{Jump, SourceElement};
10use revm::interpreter::OpCode;
11use revm_inspectors::tracing::types::{CallTraceStep, DecodedInternalCall, DecodedTraceStep};
12pub use sources::{ArtifactData, ContractSources, SourceData};
13
14#[derive(Clone, Debug)]
15pub struct DebugTraceIdentifier {
16    /// Source map of contract sources
17    contracts_sources: ContractSources,
18}
19
20impl DebugTraceIdentifier {
21    pub fn new(contracts_sources: ContractSources) -> Self {
22        Self { contracts_sources }
23    }
24
25    /// Identifies internal function invocations in a given [CallTraceNode].
26    ///
27    /// Accepts the node itself and identified name of the contract which node corresponds to.
28    pub fn identify_node_steps(&self, node: &mut CallTraceNode, contract_name: &str) {
29        DebugStepsWalker::new(node, &self.contracts_sources, contract_name).walk();
30    }
31}
32
33/// Walks through the [CallTraceStep]s attempting to match JUMPs to internal functions.
34///
35/// This is done by looking up jump kinds in the source maps. The structure of internal function
36/// call always looks like this:
37///     - JUMP
38///     - JUMPDEST
39///     ... function steps ...
40///     - JUMP
41///     - JUMPDEST
42///
43/// The assumption we rely on is that first JUMP into function will be marked as [Jump::In] in
44/// source map, and second JUMP out of the function will be marked as [Jump::Out].
45///
46/// Also, we rely on JUMPDEST after first JUMP pointing to the source location of the body of
47/// function which was entered. We pass this source part to [parse_function_from_loc] to extract the
48/// function name.
49///
50/// When we find a [Jump::In] and identify the function name, we push it to the stack.
51///
52/// When we find a [Jump::Out] we try to find a matching [Jump::In] in the stack. A match is found
53/// when source location of the JUMP-in matches the source location of final JUMPDEST (this would be
54/// the location of the function invocation), or when source location of first JUMODEST matches the
55/// source location of the JUMP-out (this would be the location of function body).
56///
57/// When a match is found, all items which were pushed after the matched function are removed. There
58/// is a lot of such items due to source maps getting malformed during optimization.
59struct DebugStepsWalker<'a> {
60    node: &'a mut CallTraceNode,
61    current_step: usize,
62    stack: Vec<(String, usize)>,
63    sources: &'a ContractSources,
64    contract_name: &'a str,
65}
66
67impl<'a> DebugStepsWalker<'a> {
68    pub fn new(
69        node: &'a mut CallTraceNode,
70        sources: &'a ContractSources,
71        contract_name: &'a str,
72    ) -> Self {
73        Self { node, current_step: 0, stack: Vec::new(), sources, contract_name }
74    }
75
76    fn current_step(&self) -> &CallTraceStep {
77        &self.node.trace.steps[self.current_step]
78    }
79
80    fn src_map(&self, step: usize) -> Option<(SourceElement, &SourceData)> {
81        self.sources.find_source_mapping(
82            self.contract_name,
83            self.node.trace.steps[step].pc as u32,
84            self.node.trace.kind.is_any_create(),
85        )
86    }
87
88    fn prev_src_map(&self) -> Option<(SourceElement, &SourceData)> {
89        if self.current_step == 0 {
90            return None;
91        }
92
93        self.src_map(self.current_step - 1)
94    }
95
96    fn current_src_map(&self) -> Option<(SourceElement, &SourceData)> {
97        self.src_map(self.current_step)
98    }
99
100    fn is_same_loc(&self, step: usize, other: usize) -> bool {
101        let Some((loc, _)) = self.src_map(step) else {
102            return false;
103        };
104        let Some((other_loc, _)) = self.src_map(other) else {
105            return false;
106        };
107
108        loc.offset() == other_loc.offset() &&
109            loc.length() == other_loc.length() &&
110            loc.index() == other_loc.index()
111    }
112
113    /// Invoked when current step is a JUMPDEST preceded by a JUMP marked as [Jump::In].
114    fn jump_in(&mut self) {
115        // This usually means that this is a jump into the external function which is an
116        // entrypoint for the current frame. We don't want to include this to avoid
117        // duplicating traces.
118        if self.is_same_loc(self.current_step, self.current_step - 1) {
119            return;
120        }
121
122        let Some((source_element, source)) = self.current_src_map() else {
123            return;
124        };
125
126        if let Some(name) = parse_function_from_loc(source, &source_element) {
127            self.stack.push((name, self.current_step - 1));
128        }
129    }
130
131    /// Invoked when current step is a JUMPDEST preceded by a JUMP marked as [Jump::Out].
132    fn jump_out(&mut self) {
133        let Some((i, _)) = self.stack.iter().enumerate().rfind(|(_, (_, step_idx))| {
134            self.is_same_loc(*step_idx, self.current_step) ||
135                self.is_same_loc(step_idx + 1, self.current_step - 1)
136        }) else {
137            return
138        };
139        // We've found a match, remove all records between start and end, those
140        // are considered invalid.
141        let (func_name, start_idx) = self.stack.split_off(i).swap_remove(0);
142
143        // Try to decode function inputs and outputs from the stack and memory.
144        let (inputs, outputs) = self
145            .src_map(start_idx + 1)
146            .map(|(source_element, source)| {
147                let start = source_element.offset() as usize;
148                let end = start + source_element.length() as usize;
149                let fn_definition = source.source[start..end].replace('\n', "");
150                let (inputs, outputs) = parse_types(&fn_definition);
151
152                (
153                    inputs.and_then(|t| {
154                        try_decode_args_from_step(&t, &self.node.trace.steps[start_idx + 1])
155                    }),
156                    outputs.and_then(|t| try_decode_args_from_step(&t, self.current_step())),
157                )
158            })
159            .unwrap_or_default();
160
161        self.node.trace.steps[start_idx].decoded = Some(DecodedTraceStep::InternalCall(
162            DecodedInternalCall { func_name, args: inputs, return_data: outputs },
163            self.current_step,
164        ));
165    }
166
167    fn process(&mut self) {
168        // We are only interested in JUMPs.
169        if self.current_step().op != OpCode::JUMP && self.current_step().op != OpCode::JUMPDEST {
170            return;
171        }
172
173        let Some((prev_source_element, _)) = self.prev_src_map() else {
174            return;
175        };
176
177        match prev_source_element.jump() {
178            Jump::In => self.jump_in(),
179            Jump::Out => self.jump_out(),
180            _ => {}
181        };
182    }
183
184    fn step(&mut self) {
185        self.process();
186        self.current_step += 1;
187    }
188
189    pub fn walk(mut self) {
190        while self.current_step < self.node.trace.steps.len() {
191            self.step();
192        }
193    }
194}
195
196/// Tries to parse the function name from the source code and detect the contract name which
197/// contains the given function.
198///
199/// Returns string in the format `Contract::function`.
200fn parse_function_from_loc(source: &SourceData, loc: &SourceElement) -> Option<String> {
201    let start = loc.offset() as usize;
202    let end = start + loc.length() as usize;
203    let source_part = &source.source[start..end];
204    if !source_part.starts_with("function") {
205        return None;
206    }
207    let function_name = source_part.split_once("function")?.1.split('(').next()?.trim();
208    let contract_name = source.find_contract_name(start, end)?;
209
210    Some(format!("{contract_name}::{function_name}"))
211}
212
213/// Parses function input and output types into [Parameters].
214fn parse_types(source: &str) -> (Option<Parameters<'_>>, Option<Parameters<'_>>) {
215    let inputs = source.find('(').and_then(|params_start| {
216        let params_end = params_start + source[params_start..].find(')')?;
217        Parameters::parse(&source[params_start..params_end + 1]).ok()
218    });
219    let outputs = source.find("returns").and_then(|returns_start| {
220        let return_params_start = returns_start + source[returns_start..].find('(')?;
221        let return_params_end = return_params_start + source[return_params_start..].find(')')?;
222        Parameters::parse(&source[return_params_start..return_params_end + 1]).ok()
223    });
224
225    (inputs, outputs)
226}
227
228/// Given [Parameters] and [CallTraceStep], tries to decode parameters by using stack and memory.
229fn try_decode_args_from_step(args: &Parameters<'_>, step: &CallTraceStep) -> Option<Vec<String>> {
230    let params = &args.params;
231
232    if params.is_empty() {
233        return Some(vec![]);
234    }
235
236    let types = params.iter().map(|p| p.resolve().ok().map(|t| (t, p.storage))).collect::<Vec<_>>();
237
238    let stack = step.stack.as_ref()?;
239
240    if stack.len() < types.len() {
241        return None;
242    }
243
244    let inputs = &stack[stack.len() - types.len()..];
245
246    let decoded = inputs
247        .iter()
248        .zip(types.iter())
249        .map(|(input, type_and_storage)| {
250            type_and_storage
251                .as_ref()
252                .and_then(|(type_, storage)| {
253                    match (type_, storage) {
254                        // HACK: alloy parser treats user-defined types as uint8: https://github.com/alloy-rs/core/pull/386
255                        //
256                        // filter out `uint8` params which are marked as storage or memory as this
257                        // is not possible in Solidity and means that type is user-defined
258                        (DynSolType::Uint(8), Some(Storage::Memory | Storage::Storage)) => None,
259                        (_, Some(Storage::Memory)) => decode_from_memory(
260                            type_,
261                            step.memory.as_ref()?.as_bytes(),
262                            input.try_into().ok()?,
263                        ),
264                        // Read other types from stack
265                        _ => type_.abi_decode(&input.to_be_bytes::<32>()).ok(),
266                    }
267                })
268                .as_ref()
269                .map(format_token)
270                .unwrap_or_else(|| "<unknown>".to_string())
271        })
272        .collect();
273
274    Some(decoded)
275}
276
277/// Decodes given [DynSolType] from memory.
278fn decode_from_memory(ty: &DynSolType, memory: &[u8], location: usize) -> Option<DynSolValue> {
279    let first_word = memory.get(location..location + 32)?;
280
281    match ty {
282        // For `string` and `bytes` layout is a word with length followed by the data
283        DynSolType::String | DynSolType::Bytes => {
284            let length: usize = U256::from_be_slice(first_word).try_into().ok()?;
285            let data = memory.get(location + 32..location + 32 + length)?;
286
287            match ty {
288                DynSolType::Bytes => Some(DynSolValue::Bytes(data.to_vec())),
289                DynSolType::String => {
290                    Some(DynSolValue::String(String::from_utf8_lossy(data).to_string()))
291                }
292                _ => unreachable!(),
293            }
294        }
295        // Dynamic arrays are encoded as a word with length followed by words with elements
296        // Fixed arrays are encoded as words with elements
297        DynSolType::Array(inner) | DynSolType::FixedArray(inner, _) => {
298            let (length, start) = match ty {
299                DynSolType::FixedArray(_, length) => (*length, location),
300                DynSolType::Array(_) => {
301                    (U256::from_be_slice(first_word).try_into().ok()?, location + 32)
302                }
303                _ => unreachable!(),
304            };
305            let mut decoded = Vec::with_capacity(length);
306
307            for i in 0..length {
308                let offset = start + i * 32;
309                let location = match inner.as_ref() {
310                    // Arrays of variable length types are arrays of pointers to the values
311                    DynSolType::String | DynSolType::Bytes | DynSolType::Array(_) => {
312                        U256::from_be_slice(memory.get(offset..offset + 32)?).try_into().ok()?
313                    }
314                    _ => offset,
315                };
316
317                decoded.push(decode_from_memory(inner, memory, location)?);
318            }
319
320            Some(DynSolValue::Array(decoded))
321        }
322        _ => ty.abi_decode(first_word).ok(),
323    }
324}