Skip to main content

foundry_cheatcodes/evm/
record_debug_step.rs

1use alloy_primitives::{Bytes, U256};
2use foundry_evm_core::buffer::{BufferKind, get_buffer_accesses};
3use foundry_evm_traces::{
4    CallTraceArena, CallTraceNode, CallTraceStep, RecordedMemory, TraceMemberOrder,
5};
6use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
7use spec::Vm::DebugStep;
8
9// Context for a CallTraceStep, includes depth and contract address.
10pub(crate) struct CallTraceCtx<'a> {
11    pub node: &'a CallTraceNode,
12    pub step: &'a CallTraceStep,
13}
14
15// Do a depth first traverse of the nodes and steps and return steps
16// that are after `node_start_idx`
17pub(crate) fn flatten_call_trace<'a>(
18    root: usize,
19    arena: &'a CallTraceArena,
20    node_start_idx: usize,
21) -> Vec<CallTraceCtx<'a>> {
22    let mut steps = Vec::new();
23    let mut record_started = false;
24
25    // Start the recursion from the root node
26    recursive_flatten_call_trace(root, arena, node_start_idx, &mut record_started, &mut steps);
27    steps
28}
29
30// Inner recursive function to process nodes.
31// This implementation directly mutates `record_started` and `flatten_steps`.
32// So the recursive call can change the `record_started` flag even for the parent
33// unfinished processing, and append steps to the `flatten_steps` as the final result.
34fn recursive_flatten_call_trace<'a>(
35    node_idx: usize,
36    arena: &'a CallTraceArena,
37    node_start_idx: usize,
38    record_started: &mut bool,
39    flatten_steps: &mut Vec<CallTraceCtx<'a>>,
40) {
41    // Once node_idx exceeds node_start_idx, start recording steps
42    // for all the recursive processing.
43    if !*record_started && node_idx >= node_start_idx {
44        *record_started = true;
45    }
46
47    let node = &arena.nodes()[node_idx];
48
49    for order in &node.ordering {
50        match order {
51            TraceMemberOrder::Step(step_idx) if *record_started => {
52                let step = &node.trace.steps[*step_idx];
53                flatten_steps.push(CallTraceCtx { node, step });
54            }
55            TraceMemberOrder::Call(call_idx) => {
56                let child_node_idx = node.children[*call_idx];
57                recursive_flatten_call_trace(
58                    child_node_idx,
59                    arena,
60                    node_start_idx,
61                    record_started,
62                    flatten_steps,
63                );
64            }
65            _ => {}
66        }
67    }
68}
69
70// Function to convert CallTraceStep to DebugStep
71pub(crate) fn convert_call_trace_ctx_to_debug_step(ctx: &CallTraceCtx) -> DebugStep {
72    let opcode = ctx.step.op.get();
73    let stack = get_stack_inputs_for_opcode(opcode, ctx.step.stack.as_deref());
74
75    let memory =
76        get_memory_input_for_opcode(opcode, ctx.step.stack.as_deref(), ctx.step.memory.as_ref());
77
78    let is_out_of_gas = matches!(
79        ctx.step.status,
80        Some(
81            InstructionResult::OutOfGas
82                | InstructionResult::MemoryOOG
83                | InstructionResult::MemoryLimitOOG
84                | InstructionResult::PrecompileOOG
85                | InstructionResult::InvalidOperandOOG
86        )
87    );
88
89    let depth = ctx.node.trace.depth as u64 + 1;
90    let contract_addr = ctx.node.execution_address();
91
92    DebugStep {
93        stack,
94        memoryInput: memory,
95        opcode: ctx.step.op.get(),
96        depth,
97        isOutOfGas: is_out_of_gas,
98        contractAddr: contract_addr,
99    }
100}
101
102// The expected `stack` here is from the trace stack, where the top of the stack
103// is the last value of the vector
104fn get_memory_input_for_opcode(
105    opcode: u8,
106    stack: Option<&[U256]>,
107    memory: Option<&RecordedMemory>,
108) -> Bytes {
109    let mut memory_input = Bytes::new();
110    let Some(stack_data) = stack else { return memory_input };
111    let Some(memory_data) = memory else { return memory_input };
112
113    if let Some(accesses) = get_buffer_accesses(opcode, stack_data)
114        && let Some((BufferKind::Memory, access)) = accesses.read
115    {
116        memory_input = get_slice_from_memory(memory_data.as_bytes(), access.offset, access.len);
117    };
118
119    memory_input
120}
121
122// The expected `stack` here is from the trace stack, where the top of the stack
123// is the last value of the vector.
124//
125// A step that fails with a stack underflow is recorded with fewer items than its opcode
126// consumes, so only the items that are actually present are returned.
127fn get_stack_inputs_for_opcode(opcode: u8, stack: Option<&[U256]>) -> Vec<U256> {
128    let Some(op) = OpCode::new(opcode) else { return Vec::new() };
129    let Some(stack_data) = stack else { return Vec::new() };
130
131    let stack_input_size = (op.inputs() as usize).min(stack_data.len());
132    stack_data.iter().rev().take(stack_input_size).copied().collect()
133}
134
135fn get_slice_from_memory(memory: &Bytes, start_index: usize, size: usize) -> Bytes {
136    let memory_len = memory.len();
137
138    let end_bound = start_index + size;
139
140    // Return the bytes if data is within the range.
141    if start_index < memory_len && end_bound <= memory_len {
142        return memory.slice(start_index..end_bound);
143    }
144
145    // Pad zero bytes if attempting to load memory partially out of range.
146    if start_index < memory_len && end_bound > memory_len {
147        let mut result = memory.slice(start_index..memory_len).to_vec();
148        result.resize(size, 0u8);
149        return Bytes::from(result);
150    }
151
152    // Return empty bytes with the size if not in range at all.
153    Bytes::from(vec![0u8; size])
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use revm::bytecode::opcode;
160
161    #[test]
162    fn stack_inputs_are_taken_from_the_top() {
163        let stack = [U256::ONE, U256::from(2), U256::from(3)];
164        assert_eq!(
165            get_stack_inputs_for_opcode(opcode::ADD, Some(&stack)),
166            vec![U256::from(3), U256::from(2)]
167        );
168    }
169
170    #[test]
171    fn stack_inputs_are_truncated_on_underflow() {
172        assert_eq!(get_stack_inputs_for_opcode(opcode::ADD, Some(&[])), Vec::<U256>::new());
173        assert_eq!(
174            get_stack_inputs_for_opcode(opcode::ADD, Some(&[U256::from(7)])),
175            vec![U256::from(7)]
176        );
177    }
178}