Skip to main content

foundry_evm_traces/
folded_stack_trace.rs

1use alloy_primitives::hex::ToHexExt;
2use revm_inspectors::tracing::{
3    CallTraceArena,
4    types::{CallTraceNode, CallTraceStep, DecodedTraceStep, TraceMemberOrder},
5};
6
7/// Wrapper for building a folded stack trace using EVM call trace node.
8#[derive(Default)]
9pub struct EvmFoldedStackTraceBuilder {
10    /// Trace produced in isolate mode, meaning refund needs to be reversed at the depth=1
11    /// frame for consistent gas values.
12    isolate: bool,
13    /// Raw folded stack trace builder.
14    fst: FoldedStackTraceBuilder,
15}
16
17impl EvmFoldedStackTraceBuilder {
18    pub fn new(isolate: bool) -> Self {
19        Self { isolate, fst: FoldedStackTraceBuilder::default() }
20    }
21
22    /// Returns the folded stack trace as formatted strings.
23    pub fn build(self) -> Vec<String> {
24        self.fst.build()
25    }
26
27    /// Creates an entry for an EVM CALL in the folded stack trace. This method recursively
28    /// processes all the children nodes of the call node and at the end it exits.
29    pub fn process_call_node(&mut self, nodes: &[CallTraceNode], idx: usize) {
30        let node = &nodes[idx];
31
32        let func_name = if node.trace.kind.is_any_create() {
33            let contract_name = node
34                .trace
35                .decoded
36                .as_ref()
37                .and_then(|dc| dc.label.as_deref())
38                .unwrap_or("Contract");
39            format!("new {contract_name}")
40        } else {
41            let selector = node
42                .selector()
43                .map(|selector| selector.encode_hex_with_prefix())
44                .unwrap_or_else(|| "fallback".to_string());
45            let signature = node
46                .trace
47                .decoded
48                .as_ref()
49                .and_then(|dc| dc.call_data.as_ref())
50                .map(|dc| &dc.signature)
51                .unwrap_or(&selector);
52
53            if let Some(label) = node.trace.decoded.as_ref().and_then(|dc| dc.label.as_ref()) {
54                format!("{label}.{signature}")
55            } else {
56                signature.clone()
57            }
58        };
59
60        let mut gas_used = node.trace.gas_used;
61        let max_refund_adjust_depth = if self.isolate { 1 } else { 0 };
62        if node.trace.depth <= max_refund_adjust_depth {
63            gas_used += node.trace.gas_refund_counter;
64        }
65
66        self.fst.enter(func_name, gas_used);
67
68        // Track internal function step exits to do in this call context.
69        let mut step_exits = vec![];
70
71        // Process children nodes.
72        for order in &node.ordering {
73            match order {
74                TraceMemberOrder::Call(child_idx) => {
75                    let child_node_idx = node.children[*child_idx];
76                    self.process_call_node(nodes, child_node_idx);
77                }
78                TraceMemberOrder::Step(step_idx) => {
79                    self.exit_previous_steps(&mut step_exits, *step_idx);
80                    self.process_step(&node.trace.steps, *step_idx, &mut step_exits)
81                }
82                TraceMemberOrder::Log(_) => {}
83            }
84        }
85
86        // Exit pending internal function calls if any.
87        for _ in 0..step_exits.len() {
88            self.fst.exit();
89        }
90
91        // Exit from this call context in the folded stack trace.
92        self.fst.exit();
93    }
94
95    /// Creates an entry for an internal function call in the folded stack trace. This method only
96    /// enters the function in the folded stack trace, we cannot exit since we need to exit at a
97    /// future step. Hence, we keep track of the step end index in the `step_exits`.
98    fn process_step(
99        &mut self,
100        steps: &[CallTraceStep],
101        step_idx: usize,
102        step_exits: &mut Vec<usize>,
103    ) {
104        let step = &steps[step_idx];
105        if let Some(decoded_step) = &step.decoded {
106            match decoded_step.as_ref() {
107                DecodedTraceStep::InternalCall(decoded_internal_call, step_end_idx) => {
108                    // Gas metering resets can increase the remaining gas across an internal call.
109                    let gas_used =
110                        step.gas_remaining.saturating_sub(steps[*step_end_idx].gas_remaining);
111                    self.fst.enter(decoded_internal_call.func_name.clone(), gas_used);
112                    step_exits.push(*step_end_idx);
113                }
114                DecodedTraceStep::Line(_) => {}
115            }
116        }
117    }
118
119    /// Exits all the previous internal calls that should end before starting step_idx.
120    fn exit_previous_steps(&mut self, step_exits: &mut Vec<usize>, step_idx: usize) {
121        let initial_length = step_exits.len();
122        step_exits.retain(|&number| number > step_idx);
123
124        let num_exits = initial_length - step_exits.len();
125        for _ in 0..num_exits {
126            self.fst.exit();
127        }
128    }
129}
130
131/// Helps to translate a function enter-exit flow into a folded stack trace.
132///
133/// Example:
134/// ```solidity
135/// function top() { child_a(); child_b() } // consumes 500 gas
136/// function child_a() {} // consumes 100 gas
137/// function child_b() {} // consumes 200 gas
138/// ```
139///
140/// For execution of the `top` function looks like:
141/// 1. enter `top`
142/// 2. enter `child_a`
143/// 3. exit `child_a`
144/// 4. enter `child_b`
145/// 5. exit `child_b`
146/// 6. exit `top`
147///
148/// The translated folded stack trace lines look like:
149/// 1. top
150/// 2. top;child_a
151/// 3. top;child_b
152///
153/// Including the gas consumed by the function by itself.
154/// 1. top 200 // 500 - 100 - 200
155/// 2. top;child_a 100
156/// 3. top;child_b 200
157#[derive(Debug, Default)]
158pub struct FoldedStackTraceBuilder {
159    /// Trace entries.
160    traces: Vec<TraceEntry>,
161    /// Number of exits to be done before entering a new function.
162    exits: usize,
163}
164
165/// A single entry in a folded stack trace.
166#[derive(Debug, Default)]
167struct TraceEntry {
168    /// Names of all functions in the call stack of this trace.
169    names: Vec<String>,
170    /// Gas consumed by this function, not including refunds.
171    gas: u64,
172}
173
174impl FoldedStackTraceBuilder {
175    /// Enter execution of a function call that consumes `gas`.
176    pub fn enter(&mut self, label: String, gas: u64) {
177        let mut names = self.traces.last().map(|entry| entry.names.clone()).unwrap_or_default();
178
179        while self.exits > 0 {
180            names.pop();
181            self.exits -= 1;
182        }
183
184        names.push(label);
185        self.traces.push(TraceEntry { names, gas });
186    }
187
188    /// Exit execution of a function call.
189    pub const fn exit(&mut self) {
190        self.exits += 1;
191    }
192
193    /// Returns folded stack trace as formatted strings.
194    pub fn build(mut self) -> Vec<String> {
195        self.subtract_children();
196        self.traces.iter().map(|e| format!("{} {}", e.names.join(";"), e.gas)).collect()
197    }
198
199    /// Internal method to build the folded stack trace without subtracting gas consumed by
200    /// the children function calls.
201    pub fn build_without_subtraction(&self) -> Vec<String> {
202        self.traces.iter().map(|e| format!("{} {}", e.names.join(";"), e.gas)).collect()
203    }
204
205    /// Subtracts gas consumed by the children function calls from the parent function calls.
206    fn subtract_children(&mut self) {
207        // Iterate over each trace to find the children and subtract their values from the parents.
208        for i in 0..self.traces.len() {
209            let (left, right) = self.traces.split_at_mut(i);
210            let TraceEntry { names, gas } = &right[0];
211            if names.len() > 1 {
212                let parent_trace_to_match = &names[..names.len() - 1];
213                for parent in left.iter_mut().rev() {
214                    if parent.names == parent_trace_to_match {
215                        parent.gas = parent.gas.saturating_sub(*gas);
216                        break;
217                    }
218                }
219            }
220        }
221    }
222}
223
224/// Builds a folded stack trace from a call trace arena.
225pub fn build(arena: &CallTraceArena, isolate: bool) -> Vec<String> {
226    let mut fst = EvmFoldedStackTraceBuilder::new(isolate);
227    if !arena.nodes().is_empty() {
228        fst.process_call_node(arena.nodes(), 0);
229    }
230    fst.build()
231}
232
233#[cfg(test)]
234mod tests {
235    use alloy_primitives::{Bytes, U256};
236    use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
237    use revm_inspectors::tracing::{
238        CallTraceArena,
239        types::{CallTraceStep, DecodedInternalCall, DecodedTraceStep, TraceMemberOrder},
240    };
241
242    fn trace_step(gas_remaining: u64) -> CallTraceStep {
243        CallTraceStep {
244            pc: 0,
245            op: OpCode::STOP,
246            stack: Some(Vec::<U256>::new().into_boxed_slice()),
247            push_stack: None,
248            memory: None,
249            returndata: Bytes::new(),
250            gas_remaining,
251            gas_refund_counter: 0,
252            gas_used: 0,
253            gas_cost: 0,
254            state_gas_cost: None,
255            state_gas_reservoir: None,
256            state_gas_spent: 0,
257            storage_change: None,
258            status: Some(InstructionResult::Stop),
259            immediate_bytes: None,
260            decoded: None,
261        }
262    }
263
264    #[test]
265    fn test_fst_1() {
266        let mut trace = super::FoldedStackTraceBuilder::default();
267        trace.enter("top".to_string(), 500);
268        trace.enter("child_a".to_string(), 100);
269        trace.exit();
270        trace.enter("child_b".to_string(), 200);
271
272        assert_eq!(
273            trace.build_without_subtraction(),
274            vec![
275                "top 500", //
276                "top;child_a 100",
277                "top;child_b 200",
278            ]
279        );
280        assert_eq!(
281            trace.build(),
282            vec![
283                "top 200", // 500 - 100 - 200
284                "top;child_a 100",
285                "top;child_b 200",
286            ]
287        );
288    }
289
290    #[test]
291    fn test_fst_2() {
292        let mut trace = super::FoldedStackTraceBuilder::default();
293        trace.enter("top".to_string(), 500);
294        trace.enter("child_a".to_string(), 300);
295        trace.enter("child_b".to_string(), 100);
296        trace.exit();
297        trace.exit();
298        trace.enter("child_c".to_string(), 100);
299
300        assert_eq!(
301            trace.build_without_subtraction(),
302            vec![
303                "top 500", //
304                "top;child_a 300",
305                "top;child_a;child_b 100",
306                "top;child_c 100",
307            ]
308        );
309
310        assert_eq!(
311            trace.build(),
312            vec![
313                "top 100",         // 500 - 300 - 100
314                "top;child_a 200", // 300 - 100
315                "top;child_a;child_b 100",
316                "top;child_c 100",
317            ]
318        );
319    }
320
321    #[test]
322    fn test_fst_3() {
323        let mut trace = super::FoldedStackTraceBuilder::default();
324        trace.enter("top".to_string(), 1700);
325        trace.enter("child_a".to_string(), 500);
326        trace.exit();
327        trace.enter("child_b".to_string(), 500);
328        trace.enter("child_c".to_string(), 500);
329        trace.exit();
330        trace.exit();
331        trace.exit();
332        trace.enter("top2".to_string(), 1700);
333
334        assert_eq!(
335            trace.build_without_subtraction(),
336            vec![
337                "top 1700", //
338                "top;child_a 500",
339                "top;child_b 500",
340                "top;child_b;child_c 500",
341                "top2 1700",
342            ]
343        );
344
345        assert_eq!(
346            trace.build(),
347            vec![
348                "top 700", //
349                "top;child_a 500",
350                "top;child_b 0",
351                "top;child_b;child_c 500",
352                "top2 1700",
353            ]
354        );
355    }
356
357    #[test]
358    fn folded_stack_trace_saturates_parent_gas() {
359        let mut trace = super::FoldedStackTraceBuilder::default();
360        trace.enter("top".to_string(), 100);
361        trace.enter("child".to_string(), 150);
362
363        assert_eq!(trace.build(), vec!["top 0", "top;child 150"]);
364    }
365
366    #[test]
367    fn folded_stack_trace_keeps_precise_internal_function_names() {
368        let mut arena = CallTraceArena::default();
369        let root = &mut arena.nodes_mut()[0];
370        root.trace.gas_used = 100;
371        root.trace.gas_limit = 100;
372        root.trace.steps = vec![trace_step(100), trace_step(70)];
373        root.trace.steps[0].decoded = Some(Box::new(DecodedTraceStep::InternalCall(
374            DecodedInternalCall {
375                func_name: "DebugVarsTest::foo(uint256)".to_string(),
376                args: Some(vec!["42".to_string()]),
377                return_data: Some(vec!["43".to_string()]),
378            },
379            1,
380        )));
381        root.ordering = vec![TraceMemberOrder::Step(0), TraceMemberOrder::Step(1)];
382
383        assert_eq!(
384            super::build(&arena, false),
385            vec!["fallback 70", "fallback;DebugVarsTest::foo(uint256) 30",]
386        );
387    }
388
389    #[test]
390    fn folded_stack_trace_saturates_internal_call_gas_on_reset_metering() {
391        // Model an internal call whose gas meter resets before returning.
392        let mut arena = CallTraceArena::default();
393        let root = &mut arena.nodes_mut()[0];
394        root.trace.gas_used = 100;
395        root.trace.gas_limit = 100;
396        root.trace.steps = vec![trace_step(70), trace_step(100)];
397        root.trace.steps[0].decoded = Some(Box::new(DecodedTraceStep::InternalCall(
398            DecodedInternalCall {
399                func_name: "DebugVarsTest::resetsGas()".to_string(),
400                args: None,
401                return_data: None,
402            },
403            1,
404        )));
405        root.ordering = vec![TraceMemberOrder::Step(0), TraceMemberOrder::Step(1)];
406
407        assert_eq!(
408            super::build(&arena, false),
409            vec!["fallback 100", "fallback;DebugVarsTest::resetsGas() 0",]
410        );
411    }
412}