Skip to main content

foundry_evm_traces/debug/
mod.rs

1mod sources;
2use crate::CallTraceNode;
3use alloy_dyn_abi::{
4    DynSolType, DynSolValue, Specifier,
5    parser::{Parameters, Storage},
6};
7use alloy_primitives::U256;
8use foundry_common::fmt::format_token;
9use foundry_compilers::artifacts::sourcemap::{Jump, SourceElement};
10use revm::bytecode::opcode::OpCode;
11use revm_inspectors::tracing::types::{CallTraceStep, DecodedInternalCall, DecodedTraceStep};
12pub use sources::{ArtifactData, ContractSources, DebugSourceScope, DebugVariable, 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 const 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        Self::identify_node_steps_with_sources(node, &self.contracts_sources, contract_name);
30    }
31
32    /// Identifies internal function invocations without taking ownership of source metadata.
33    pub fn identify_node_steps_with_sources(
34        node: &mut CallTraceNode,
35        sources: &ContractSources,
36        contract_name: &str,
37    ) {
38        DebugStepsWalker::new(node, sources, contract_name).walk();
39    }
40}
41
42/// Walks through the [CallTraceStep]s attempting to match JUMPs to internal functions.
43///
44/// This is done by looking up jump kinds in the source maps. The structure of internal function
45/// call always looks like this:
46///     - JUMP
47///     - JUMPDEST
48///     ... function steps ...
49///     - JUMP
50///     - JUMPDEST
51///
52/// The assumption we rely on is that first JUMP into function will be marked as [Jump::In] in
53/// source map, and second JUMP out of the function will be marked as [Jump::Out].
54///
55/// Also, we rely on JUMPDEST after first JUMP pointing to the source location of the body of
56/// function which was entered. We pass this source part to [parse_function_from_loc] to extract the
57/// function name.
58///
59/// When we find a [Jump::In] and identify the function name, we push it to the stack.
60///
61/// When we find a [Jump::Out] we try to find a matching [Jump::In] in the stack. A match is found
62/// when source location of the JUMP-in matches the source location of final JUMPDEST (this would be
63/// the location of the function invocation), or when source location of first JUMODEST matches the
64/// source location of the JUMP-out (this would be the location of function body).
65///
66/// When a match is found, all items which were pushed after the matched function are removed. There
67/// is a lot of such items due to source maps getting malformed during optimization.
68struct DebugStepsWalker<'a> {
69    node: &'a mut CallTraceNode,
70    current_step: usize,
71    stack: Vec<(String, usize)>,
72    sources: &'a ContractSources,
73    contract_name: &'a str,
74}
75
76impl<'a> DebugStepsWalker<'a> {
77    pub const fn new(
78        node: &'a mut CallTraceNode,
79        sources: &'a ContractSources,
80        contract_name: &'a str,
81    ) -> Self {
82        Self { node, current_step: 0, stack: Vec::new(), sources, contract_name }
83    }
84
85    fn current_step(&self) -> &CallTraceStep {
86        &self.node.trace.steps[self.current_step]
87    }
88
89    fn src_map(&self, step: usize) -> Option<(SourceElement, &SourceData)> {
90        self.sources.find_source_mapping(
91            self.contract_name,
92            self.node.trace.steps[step].pc as u32,
93            self.node.trace.kind.is_any_create(),
94        )
95    }
96
97    fn prev_src_map(&self) -> Option<(SourceElement, &SourceData)> {
98        if self.current_step == 0 {
99            return None;
100        }
101
102        self.src_map(self.current_step - 1)
103    }
104
105    fn current_src_map(&self) -> Option<(SourceElement, &SourceData)> {
106        self.src_map(self.current_step)
107    }
108
109    fn is_same_loc(&self, step: usize, other: usize) -> bool {
110        let Some((loc, _)) = self.src_map(step) else {
111            return false;
112        };
113        let Some((other_loc, _)) = self.src_map(other) else {
114            return false;
115        };
116
117        loc.offset() == other_loc.offset()
118            && loc.length() == other_loc.length()
119            && loc.index() == other_loc.index()
120    }
121
122    /// Invoked when current step is a JUMPDEST preceded by a JUMP marked as [Jump::In].
123    fn jump_in(&mut self) {
124        // This usually means that this is a jump into the external function which is an
125        // entrypoint for the current frame. We don't want to include this to avoid
126        // duplicating traces.
127        if self.is_same_loc(self.current_step, self.current_step - 1) {
128            return;
129        }
130
131        let Some((source_element, source)) = self.current_src_map() else {
132            return;
133        };
134
135        if let Some(name) = parse_function_from_loc(source, &source_element) {
136            self.stack.push((name, self.current_step - 1));
137        }
138    }
139
140    /// Invoked when current step is a JUMPDEST preceded by a JUMP marked as [Jump::Out].
141    fn jump_out(&mut self) {
142        let Some((i, _)) = self.stack.iter().enumerate().rfind(|(_, (_, step_idx))| {
143            self.is_same_loc(*step_idx, self.current_step)
144                || self.is_same_loc(step_idx + 1, self.current_step - 1)
145        }) else {
146            return;
147        };
148        // We've found a match, remove all records between start and end, those
149        // are considered invalid.
150        let (func_name, start_idx) = self.stack.split_off(i).swap_remove(0);
151
152        // Try to decode function inputs and outputs from the stack and memory.
153        let (inputs, outputs) = self
154            .src_map(start_idx + 1)
155            .and_then(|(source_element, source)| {
156                let start = source_element.offset() as usize;
157                let (fn_definition, _) =
158                    source_span(&source.source, start, source_element.length() as usize)?;
159                let fn_definition = fn_definition.replace('\n', "");
160                let (inputs, outputs) = parse_types(&fn_definition);
161
162                Some((
163                    inputs.and_then(|t| {
164                        decode_step_parameters(
165                            &t,
166                            &self.node.trace.steps[start_idx + 1],
167                            Some(self.node.trace.data.as_ref()),
168                        )
169                    }),
170                    outputs.and_then(|t| decode_step_parameters(&t, self.current_step(), None)),
171                ))
172            })
173            .unwrap_or_default();
174
175        self.node.trace.steps[start_idx].decoded = Some(Box::new(DecodedTraceStep::InternalCall(
176            DecodedInternalCall { func_name, args: inputs, return_data: outputs },
177            self.current_step,
178        )));
179    }
180
181    fn process(&mut self) {
182        // We are only interested in JUMPs.
183        if self.current_step().op != OpCode::JUMP && self.current_step().op != OpCode::JUMPDEST {
184            return;
185        }
186
187        let Some((prev_source_element, _)) = self.prev_src_map() else {
188            return;
189        };
190
191        match prev_source_element.jump() {
192            Jump::In => self.jump_in(),
193            Jump::Out => self.jump_out(),
194            _ => {}
195        };
196    }
197
198    fn step(&mut self) {
199        self.process();
200        self.current_step += 1;
201    }
202
203    pub fn walk(mut self) {
204        while self.current_step < self.node.trace.steps.len() {
205            self.step();
206        }
207    }
208}
209
210/// Tries to parse the function name from the source code and detect the contract name which
211/// contains the given function.
212///
213/// Returns a string in the format `Contract::function(types)` when parameters can be resolved,
214/// falling back to `Contract::function`.
215fn parse_function_from_loc(source: &SourceData, loc: &SourceElement) -> Option<String> {
216    let start = loc.offset() as usize;
217    let (source_part, end) = source_span(&source.source, start, loc.length() as usize)?;
218
219    if !source_part.starts_with("function") {
220        return None;
221    }
222    let function_name = source_part.split_once("function")?.1.split('(').next()?.trim();
223    let contract_name = source.find_contract_name(start, end)?;
224
225    Some(internal_function_identifier(contract_name, function_name, source_part))
226}
227
228fn internal_function_identifier(
229    contract_name: &str,
230    function_name: &str,
231    source_part: &str,
232) -> String {
233    let signature = canonical_function_signature(function_name, source_part)
234        .unwrap_or_else(|| function_name.to_string());
235    format!("{contract_name}::{signature}")
236}
237
238fn canonical_function_signature(function_name: &str, source_part: &str) -> Option<String> {
239    let source_part = source_part.replace('\n', "");
240    let (inputs, _) = parse_types(&source_part);
241    let inputs = inputs?;
242    let types =
243        inputs.params.iter().map(|param| param.resolve().ok()).collect::<Option<Vec<_>>>()?;
244    Some(function_signature(function_name, &types))
245}
246
247/// Formats an ABI-style function signature from a name and canonical parameter types.
248pub fn function_signature(function_name: &str, types: &[DynSolType]) -> String {
249    let mut signature = String::new();
250    signature.push_str(function_name);
251    signature.push('(');
252    for (i, ty) in types.iter().enumerate() {
253        if i > 0 {
254            signature.push(',');
255        }
256        signature.push_str(&ty.sol_type_name());
257    }
258    signature.push(')');
259    signature
260}
261
262fn source_span(source: &str, start: usize, len: usize) -> Option<(&str, usize)> {
263    let end = start.checked_add(len)?;
264
265    Some((source.get(start..end)?, end))
266}
267
268/// Parses function input and output types into [Parameters].
269fn parse_types(source: &str) -> (Option<Parameters<'_>>, Option<Parameters<'_>>) {
270    let inputs = source.find('(').and_then(|params_start| {
271        let params_end = params_start + source[params_start..].find(')')?;
272        Parameters::parse(&source[params_start..params_end + 1]).ok()
273    });
274    let outputs = source.find("returns").and_then(|returns_start| {
275        let return_params_start = returns_start + source[returns_start..].find('(')?;
276        let return_params_end = return_params_start + source[return_params_start..].find(')')?;
277        Parameters::parse(&source[return_params_start..return_params_end + 1]).ok()
278    });
279
280    (inputs, outputs)
281}
282
283/// Given [Parameters] and [CallTraceStep], tries to decode parameters by using stack, memory, and
284/// call data.
285pub fn decode_step_parameters(
286    args: &Parameters<'_>,
287    step: &CallTraceStep,
288    calldata: Option<&[u8]>,
289) -> Option<Vec<String>> {
290    let params = &args.params;
291
292    if params.is_empty() {
293        return Some(vec![]);
294    }
295
296    let types = params
297        .iter()
298        .map(|p| {
299            p.resolve().ok().map(|t| {
300                let slots = stack_slots(&t, p.storage);
301                (t, p.storage, slots)
302            })
303        })
304        .collect::<Vec<_>>();
305
306    let stack = step.stack.as_ref()?;
307    let stack_slots =
308        types.iter().map(|type_| type_.as_ref().map_or(1, |(_, _, slots)| *slots)).sum::<usize>();
309
310    if stack.len() < stack_slots {
311        return None;
312    }
313
314    let inputs = &stack[stack.len() - stack_slots..];
315    let memory = step.memory.as_ref().map(|memory| memory.as_bytes().as_ref());
316    let mut input_idx = 0;
317    let mut decoded = Vec::with_capacity(types.len());
318
319    for type_and_storage in &types {
320        let Some((type_, storage, slots)) = type_and_storage.as_ref() else {
321            input_idx += 1;
322            decoded.push("<unknown>".to_string());
323            continue;
324        };
325        let input = &inputs[input_idx..input_idx + *slots];
326        input_idx += *slots;
327
328        decoded.push(
329            decode_parameter(type_, *storage, input, memory, calldata)
330                .as_ref()
331                .map(format_token)
332                .unwrap_or_else(|| "<unknown>".to_string()),
333        );
334    }
335
336    Some(decoded)
337}
338
339const fn stack_slots(ty: &DynSolType, storage: Option<Storage>) -> usize {
340    match (ty, storage) {
341        (
342            DynSolType::String | DynSolType::Bytes | DynSolType::Array(_),
343            Some(Storage::Calldata),
344        ) => 2,
345        _ => 1,
346    }
347}
348
349fn decode_parameter(
350    ty: &DynSolType,
351    storage: Option<Storage>,
352    stack_words: &[U256],
353    memory: Option<&[u8]>,
354    calldata: Option<&[u8]>,
355) -> Option<DynSolValue> {
356    let input = stack_words.first()?;
357
358    match (ty, storage) {
359        // HACK: alloy parser treats user-defined types as uint8: https://github.com/alloy-rs/core/pull/386
360        //
361        // filter out `uint8` params which are marked as storage, memory, or calldata as this
362        // is not possible in Solidity and means that type is user-defined
363        (DynSolType::Uint(8), Some(Storage::Memory | Storage::Storage | Storage::Calldata)) => None,
364        (_, Some(Storage::Storage)) => None,
365        (_, Some(Storage::Memory)) => decode_from_memory(ty, memory?, input.try_into().ok()?),
366        (_, Some(Storage::Calldata)) => decode_from_calldata(ty, calldata?, stack_words),
367        // Read other types from stack
368        _ => ty.abi_decode(&input.to_be_bytes::<32>()).ok(),
369    }
370}
371
372fn decode_from_calldata(
373    ty: &DynSolType,
374    calldata: &[u8],
375    stack_words: &[U256],
376) -> Option<DynSolValue> {
377    let offset: usize = stack_words.first()?.try_into().ok()?;
378
379    match ty {
380        // For calldata `string` and `bytes`, Solidity keeps the byte offset and length on stack.
381        DynSolType::String | DynSolType::Bytes => {
382            let length: usize = stack_words.get(1)?.try_into().ok()?;
383            let data = memory_range(calldata, offset, length)?;
384
385            match ty {
386                DynSolType::Bytes => Some(DynSolValue::Bytes(data.to_vec())),
387                DynSolType::String => {
388                    Some(DynSolValue::String(String::from_utf8_lossy(data).to_string()))
389                }
390                _ => unreachable!(),
391            }
392        }
393        _ => None,
394    }
395}
396
397/// Decodes given [DynSolType] from memory.
398fn decode_from_memory(ty: &DynSolType, memory: &[u8], location: usize) -> Option<DynSolValue> {
399    let first_word = memory_range(memory, location, 32)?;
400
401    match ty {
402        // For `string` and `bytes` layout is a word with length followed by the data
403        DynSolType::String | DynSolType::Bytes => {
404            let length: usize = U256::from_be_slice(first_word).try_into().ok()?;
405            let data = memory_range(memory, location.checked_add(32)?, length)?;
406
407            match ty {
408                DynSolType::Bytes => Some(DynSolValue::Bytes(data.to_vec())),
409                DynSolType::String => {
410                    Some(DynSolValue::String(String::from_utf8_lossy(data).to_string()))
411                }
412                _ => unreachable!(),
413            }
414        }
415        // Dynamic arrays are encoded as a word with length followed by words with elements
416        // Fixed arrays are encoded as words with elements
417        DynSolType::Array(inner) | DynSolType::FixedArray(inner, _) => {
418            let (length, start) = match ty {
419                DynSolType::FixedArray(_, length) => (*length, location),
420                DynSolType::Array(_) => {
421                    (U256::from_be_slice(first_word).try_into().ok()?, location.checked_add(32)?)
422                }
423                _ => unreachable!(),
424            };
425            memory_range(memory, start, length.checked_mul(32)?)?;
426            let mut decoded = Vec::with_capacity(length);
427
428            for i in 0..length {
429                let offset = start.checked_add(i.checked_mul(32)?)?;
430                let location = match inner.as_ref() {
431                    // Arrays of variable length types are arrays of pointers to the values
432                    DynSolType::String | DynSolType::Bytes | DynSolType::Array(_) => {
433                        U256::from_be_slice(memory_range(memory, offset, 32)?).try_into().ok()?
434                    }
435                    _ => offset,
436                };
437
438                decoded.push(decode_from_memory(inner, memory, location)?);
439            }
440
441            Some(DynSolValue::Array(decoded))
442        }
443        _ => ty.abi_decode(first_word).ok(),
444    }
445}
446
447fn memory_range(memory: &[u8], start: usize, len: usize) -> Option<&[u8]> {
448    memory.get(start..start.checked_add(len)?)
449}
450
451#[cfg(test)]
452mod tests {
453    use super::{
454        decode_from_memory, decode_step_parameters, internal_function_identifier, source_span,
455    };
456    use alloy_dyn_abi::{DynSolType, parser::Parameters};
457    use alloy_primitives::{Bytes, U256};
458    use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
459    use revm_inspectors::tracing::types::CallTraceStep;
460
461    fn trace_step(stack: Vec<U256>) -> CallTraceStep {
462        CallTraceStep {
463            pc: 0,
464            op: OpCode::STOP,
465            stack: Some(stack.into_boxed_slice()),
466            push_stack: None,
467            memory: None,
468            returndata: Bytes::new(),
469            gas_remaining: 0,
470            gas_refund_counter: 0,
471            gas_used: 0,
472            gas_cost: 0,
473            storage_change: None,
474            status: Some(InstructionResult::Stop),
475            immediate_bytes: None,
476            decoded: None,
477        }
478    }
479
480    #[test]
481    fn source_span_returns_none_for_invalid_ranges() {
482        assert_eq!(source_span("abcdef", 2, 3), Some(("cde", 5)));
483        assert_eq!(source_span("abcdef", 7, 1), None);
484        assert_eq!(source_span("abcdef", usize::MAX, 1), None);
485    }
486
487    #[test]
488    fn internal_function_identifier_includes_canonical_signature() {
489        assert_eq!(
490            internal_function_identifier(
491                "DebugMe",
492                "foo",
493                "function foo(uint256 amount, bool ok) internal returns (uint256) {",
494            ),
495            "DebugMe::foo(uint256,bool)"
496        );
497    }
498
499    #[test]
500    fn decode_from_memory_rejects_overflow_location() {
501        assert_eq!(decode_from_memory(&DynSolType::Bytes, &[0; 64], usize::MAX), None);
502    }
503
504    #[test]
505    fn decode_from_memory_rejects_oversized_dynamic_array_length() {
506        let memory = U256::from(1_000_000).to_be_bytes::<32>();
507        let ty = DynSolType::Array(Box::new(DynSolType::Uint(256)));
508
509        assert_eq!(decode_from_memory(&ty, &memory, 0), None);
510    }
511
512    #[test]
513    fn decode_step_parameters_marks_storage_params_unknown() {
514        let params = Parameters::parse("(uint256[] storage values)").unwrap();
515        let step = trace_step(vec![U256::from(5)]);
516
517        assert_eq!(
518            decode_step_parameters(&params, &step, None),
519            Some(vec!["<unknown>".to_string()])
520        );
521    }
522
523    #[test]
524    fn decode_step_parameters_aligns_static_arg_before_calldata_bytes() {
525        let params = Parameters::parse("(bytes32 digest, bytes calldata signature)").unwrap();
526        let digest = U256::from(0x1234);
527        let offset = 0x44;
528        let mut calldata = vec![0; offset];
529        calldata.extend_from_slice(&[0x11, 0x22, 0x33]);
530        let step = trace_step(vec![digest, U256::from(offset), U256::from(3)]);
531
532        assert_eq!(
533            decode_step_parameters(&params, &step, Some(&calldata)),
534            Some(vec![
535                "0x0000000000000000000000000000000000000000000000000000000000001234".to_string(),
536                "0x112233".to_string(),
537            ])
538        );
539    }
540
541    #[test]
542    fn decode_step_parameters_marks_calldata_bytes_unknown_without_calldata() {
543        let params = Parameters::parse("(bytes calldata signature)").unwrap();
544        let step = trace_step(vec![U256::from(0x44), U256::from(3)]);
545
546        assert_eq!(
547            decode_step_parameters(&params, &step, None),
548            Some(vec!["<unknown>".to_string()])
549        );
550    }
551
552    #[test]
553    fn decode_step_parameters_aligns_static_arg_after_unsupported_calldata_array() {
554        let params = Parameters::parse("(uint256[] calldata values, bytes32 digest)").unwrap();
555        let digest = U256::from(0x1234);
556        let step = trace_step(vec![U256::from(0x44), U256::from(2), digest]);
557
558        assert_eq!(
559            decode_step_parameters(&params, &step, Some(&[])),
560            Some(vec![
561                "<unknown>".to_string(),
562                "0x0000000000000000000000000000000000000000000000000000000000001234".to_string(),
563            ])
564        );
565    }
566}