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/// Builds a folded stack trace from a call trace arena.
8pub fn build(arena: &CallTraceArena, isolate: bool) -> Vec<String> {
9    let mut fst = EvmFoldedStackTraceBuilder::new(isolate);
10    fst.process_call_node(arena.nodes(), 0);
11    fst.build()
12}
13
14/// Wrapper for building a folded stack trace using EVM call trace node.
15pub struct EvmFoldedStackTraceBuilder {
16    /// Trace produced in isolate mode, meaning refund needs to be reversed at the depth=1
17    /// frame for consistent gas values.
18    isolate: bool,
19    /// Raw folded stack trace builder.
20    fst: FoldedStackTraceBuilder,
21}
22
23impl EvmFoldedStackTraceBuilder {
24    pub fn new(isolate: bool) -> Self {
25        Self { isolate, fst: FoldedStackTraceBuilder::default() }
26    }
27
28    /// Returns the folded stack trace.
29    pub fn build(self) -> Vec<String> {
30        self.fst.build()
31    }
32
33    /// Creates an entry for a EVM CALL in the folded stack trace. This method recursively processes
34    /// all the children nodes of the call node and at the end it exits.
35    pub fn process_call_node(&mut self, nodes: &[CallTraceNode], idx: usize) {
36        let node = &nodes[idx];
37
38        let func_name = if node.trace.kind.is_any_create() {
39            let contract_name = node
40                .trace
41                .decoded
42                .as_ref()
43                .and_then(|dc| dc.label.as_deref())
44                .unwrap_or("Contract");
45            format!("new {contract_name}")
46        } else {
47            let selector = node
48                .selector()
49                .map(|selector| selector.encode_hex_with_prefix())
50                .unwrap_or_else(|| "fallback".to_string());
51            let signature = node
52                .trace
53                .decoded
54                .as_ref()
55                .and_then(|dc| dc.call_data.as_ref())
56                .map(|dc| &dc.signature)
57                .unwrap_or(&selector);
58
59            if let Some(label) = node.trace.decoded.as_ref().and_then(|dc| dc.label.as_ref()) {
60                format!("{label}.{signature}")
61            } else {
62                signature.clone()
63            }
64        };
65
66        let mut gas_used = node.trace.gas_used;
67        let max_refund_adjust_depth = if self.isolate { 1 } else { 0 };
68        if node.trace.depth <= max_refund_adjust_depth {
69            gas_used += node.trace.gas_refund_counter;
70        }
71
72        self.fst.enter(func_name, gas_used);
73
74        // Track internal function step exits to do in this call context.
75        let mut step_exits = vec![];
76
77        // Process children nodes.
78        for order in &node.ordering {
79            match order {
80                TraceMemberOrder::Call(child_idx) => {
81                    let child_node_idx = node.children[*child_idx];
82                    self.process_call_node(nodes, child_node_idx);
83                }
84                TraceMemberOrder::Step(step_idx) => {
85                    self.exit_previous_steps(&mut step_exits, *step_idx);
86                    self.process_step(&node.trace.steps, *step_idx, &mut step_exits)
87                }
88                TraceMemberOrder::Log(_) => {}
89            }
90        }
91
92        // Exit pending internal function calls if any.
93        for _ in 0..step_exits.len() {
94            self.fst.exit();
95        }
96
97        // Exit from this call context in the folded stack trace.
98        self.fst.exit();
99    }
100
101    /// Creates an entry for an internal function call in the folded stack trace. This method only
102    /// enters the function in the folded stack trace, we cannot exit since we need to exit at a
103    /// future step. Hence, we keep track of the step end index in the `step_exits`.
104    fn process_step(
105        &mut self,
106        steps: &[CallTraceStep],
107        step_idx: usize,
108        step_exits: &mut Vec<usize>,
109    ) {
110        let step = &steps[step_idx];
111        if let Some(decoded_step) = &step.decoded {
112            match decoded_step.as_ref() {
113                DecodedTraceStep::InternalCall(decoded_internal_call, step_end_idx) => {
114                    let gas_used = step.gas_remaining - steps[*step_end_idx].gas_remaining;
115                    self.fst.enter(decoded_internal_call.func_name.clone(), gas_used);
116                    step_exits.push(*step_end_idx);
117                }
118                DecodedTraceStep::Line(_) => {}
119            }
120        }
121    }
122
123    /// Exits all the previous internal calls that should end before starting step_idx.
124    fn exit_previous_steps(&mut self, step_exits: &mut Vec<usize>, step_idx: usize) {
125        let initial_length = step_exits.len();
126        step_exits.retain(|&number| number > step_idx);
127
128        let num_exits = initial_length - step_exits.len();
129        for _ in 0..num_exits {
130            self.fst.exit();
131        }
132    }
133}
134
135/// Helps to translate a function enter-exit flow into a folded stack trace.
136///
137/// Example:
138/// ```solidity
139/// function top() { child_a(); child_b() } // consumes 500 gas
140/// function child_a() {} // consumes 100 gas
141/// function child_b() {} // consumes 200 gas
142/// ```
143///
144/// For execution of the `top` function looks like:
145/// 1. enter `top`
146/// 2. enter `child_a`
147/// 3. exit `child_a`
148/// 4. enter `child_b`
149/// 5. exit `child_b`
150/// 6. exit `top`
151///
152/// The translated folded stack trace lines look like:
153/// 1. top
154/// 2. top;child_a
155/// 3. top;child_b
156///
157/// Including the gas consumed by the function by itself.
158/// 1. top 200 // 500 - 100 - 200
159/// 2. top;child_a 100
160/// 3. top;child_b 200
161#[derive(Debug, Default)]
162pub struct FoldedStackTraceBuilder {
163    /// Trace entries.
164    traces: Vec<TraceEntry>,
165    /// Number of exits to be done before entering a new function.
166    exits: usize,
167}
168
169#[derive(Debug, Default)]
170struct TraceEntry {
171    /// Names of all functions in the call stack of this trace.
172    names: Vec<String>,
173    /// Gas consumed by this function, not including refunds.
174    gas: u64,
175}
176
177impl FoldedStackTraceBuilder {
178    /// Enter execution of a function call that consumes `gas`.
179    pub fn enter(&mut self, label: String, gas: u64) {
180        let mut names = self.traces.last().map(|entry| entry.names.clone()).unwrap_or_default();
181
182        while self.exits > 0 {
183            names.pop();
184            self.exits -= 1;
185        }
186
187        names.push(label);
188        self.traces.push(TraceEntry { names, gas });
189    }
190
191    /// Exit execution of a function call.
192    pub fn exit(&mut self) {
193        self.exits += 1;
194    }
195
196    /// Returns folded stack trace.
197    pub fn build(mut self) -> Vec<String> {
198        self.subtract_children();
199        self.build_without_subtraction()
200    }
201
202    /// Internal method to build the folded stack trace without subtracting gas consumed by
203    /// the children function calls.
204    pub fn build_without_subtraction(&mut self) -> Vec<String> {
205        let mut lines = Vec::new();
206        for TraceEntry { names, gas } in &self.traces {
207            lines.push(format!("{} {}", names.join(";"), gas));
208        }
209        lines
210    }
211
212    /// Subtracts gas consumed by the children function calls from the parent function calls.
213    fn subtract_children(&mut self) {
214        // Iterate over each trace to find the children and subtract their values from the parents.
215        for i in 0..self.traces.len() {
216            let (left, right) = self.traces.split_at_mut(i);
217            let TraceEntry { names, gas } = &right[0];
218            if names.len() > 1 {
219                let parent_trace_to_match = &names[..names.len() - 1];
220                for parent in left.iter_mut().rev() {
221                    if parent.names == parent_trace_to_match {
222                        parent.gas -= gas;
223                        break;
224                    }
225                }
226            }
227        }
228    }
229}
230
231mod tests {
232    #[test]
233    fn test_fst_1() {
234        let mut trace = super::FoldedStackTraceBuilder::default();
235        trace.enter("top".to_string(), 500);
236        trace.enter("child_a".to_string(), 100);
237        trace.exit();
238        trace.enter("child_b".to_string(), 200);
239
240        assert_eq!(
241            trace.build_without_subtraction(),
242            vec![
243                "top 500", //
244                "top;child_a 100",
245                "top;child_b 200",
246            ]
247        );
248        assert_eq!(
249            trace.build(),
250            vec![
251                "top 200", // 500 - 100 - 200
252                "top;child_a 100",
253                "top;child_b 200",
254            ]
255        );
256    }
257
258    #[test]
259    fn test_fst_2() {
260        let mut trace = super::FoldedStackTraceBuilder::default();
261        trace.enter("top".to_string(), 500);
262        trace.enter("child_a".to_string(), 300);
263        trace.enter("child_b".to_string(), 100);
264        trace.exit();
265        trace.exit();
266        trace.enter("child_c".to_string(), 100);
267
268        assert_eq!(
269            trace.build_without_subtraction(),
270            vec![
271                "top 500", //
272                "top;child_a 300",
273                "top;child_a;child_b 100",
274                "top;child_c 100",
275            ]
276        );
277
278        assert_eq!(
279            trace.build(),
280            vec![
281                "top 100",         // 500 - 300 - 100
282                "top;child_a 200", // 300 - 100
283                "top;child_a;child_b 100",
284                "top;child_c 100",
285            ]
286        );
287    }
288
289    #[test]
290    fn test_fst_3() {
291        let mut trace = super::FoldedStackTraceBuilder::default();
292        trace.enter("top".to_string(), 1700);
293        trace.enter("child_a".to_string(), 500);
294        trace.exit();
295        trace.enter("child_b".to_string(), 500);
296        trace.enter("child_c".to_string(), 500);
297        trace.exit();
298        trace.exit();
299        trace.exit();
300        trace.enter("top2".to_string(), 1700);
301
302        assert_eq!(
303            trace.build_without_subtraction(),
304            vec![
305                "top 1700", //
306                "top;child_a 500",
307                "top;child_b 500",
308                "top;child_b;child_c 500",
309                "top2 1700",
310            ]
311        );
312
313        assert_eq!(
314            trace.build(),
315            vec![
316                "top 700", //
317                "top;child_a 500",
318                "top;child_b 0",
319                "top;child_b;child_c 500",
320                "top2 1700",
321            ]
322        );
323    }
324}