foundry_evm_traces/
folded_stack_trace.rs

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