Skip to main content

foundry_evm_traces/debug/
mod.rs

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