foundry_evm_traces/
folded_stack_trace.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
use alloy_primitives::hex::ToHexExt;
use revm_inspectors::tracing::{
    types::{CallTraceNode, CallTraceStep, DecodedTraceStep, TraceMemberOrder},
    CallTraceArena,
};

/// Builds a folded stack trace from a call trace arena.
pub fn build(arena: &CallTraceArena) -> Vec<String> {
    let mut fst = EvmFoldedStackTraceBuilder::default();
    fst.process_call_node(arena.nodes(), 0);
    fst.build()
}

/// Wrapper for building a folded stack trace using EVM call trace node.
#[derive(Default)]
pub struct EvmFoldedStackTraceBuilder {
    /// Raw folded stack trace builder.
    fst: FoldedStackTraceBuilder,
}

impl EvmFoldedStackTraceBuilder {
    /// Returns the folded stack trace.
    pub fn build(self) -> Vec<String> {
        self.fst.build()
    }

    /// Creates an entry for a EVM CALL in the folded stack trace. This method recursively processes
    /// all the children nodes of the call node and at the end it exits.
    pub fn process_call_node(&mut self, nodes: &[CallTraceNode], idx: usize) {
        let node = &nodes[idx];

        let func_name = if node.trace.kind.is_any_create() {
            let contract_name = node.trace.decoded.label.as_deref().unwrap_or("Contract");
            format!("new {contract_name}")
        } else {
            let selector = node
                .selector()
                .map(|selector| selector.encode_hex_with_prefix())
                .unwrap_or_else(|| "fallback".to_string());
            let signature =
                node.trace.decoded.call_data.as_ref().map(|dc| &dc.signature).unwrap_or(&selector);

            if let Some(label) = &node.trace.decoded.label {
                format!("{label}.{signature}")
            } else {
                signature.clone()
            }
        };

        self.fst.enter(func_name, node.trace.gas_used as i64);

        // Track internal function step exits to do in this call context.
        let mut step_exits = vec![];

        // Process children nodes.
        for order in &node.ordering {
            match order {
                TraceMemberOrder::Call(child_idx) => {
                    let child_node_idx = node.children[*child_idx];
                    self.process_call_node(nodes, child_node_idx);
                }
                TraceMemberOrder::Step(step_idx) => {
                    self.exit_previous_steps(&mut step_exits, *step_idx);
                    self.process_step(&node.trace.steps, *step_idx, &mut step_exits)
                }
                TraceMemberOrder::Log(_) => {}
            }
        }

        // Exit pending internal function calls if any.
        for _ in 0..step_exits.len() {
            self.fst.exit();
        }

        // Exit from this call context in the folded stack trace.
        self.fst.exit();
    }

    /// Creates an entry for an internal function call in the folded stack trace. This method only
    /// enters the function in the folded stack trace, we cannot exit since we need to exit at a
    /// future step. Hence, we keep track of the step end index in the `step_exits`.
    fn process_step(
        &mut self,
        steps: &[CallTraceStep],
        step_idx: usize,
        step_exits: &mut Vec<usize>,
    ) {
        let step = &steps[step_idx];
        if let Some(decoded_step) = &step.decoded {
            match decoded_step {
                DecodedTraceStep::InternalCall(decoded_internal_call, step_end_idx) => {
                    let gas_used = steps[*step_end_idx].gas_used.saturating_sub(step.gas_used);
                    self.fst.enter(decoded_internal_call.func_name.clone(), gas_used as i64);
                    step_exits.push(*step_end_idx);
                }
                DecodedTraceStep::Line(_) => {}
            }
        }
    }

    /// Exits all the previous internal calls that should end before starting step_idx.
    fn exit_previous_steps(&mut self, step_exits: &mut Vec<usize>, step_idx: usize) {
        let initial_length = step_exits.len();
        step_exits.retain(|&number| number > step_idx);

        let num_exits = initial_length - step_exits.len();
        for _ in 0..num_exits {
            self.fst.exit();
        }
    }
}

/// Helps to translate a function enter-exit flow into a folded stack trace.
///
/// Example:
/// ```solidity
/// function top() { child_a(); child_b() } // consumes 500 gas
/// function child_a() {} // consumes 100 gas
/// function child_b() {} // consumes 200 gas
/// ```
///
/// For execution of the `top` function looks like:
/// 1. enter `top`
/// 2. enter `child_a`
/// 3. exit `child_a`
/// 4. enter `child_b`
/// 5. exit `child_b`
/// 6. exit `top`
///
/// The translated folded stack trace lines look like:
/// 1. top
/// 2. top;child_a
/// 3. top;child_b
///
/// Including the gas consumed by the function by itself.
/// 1. top 200 // 500 - 100 - 200
/// 2. top;child_a 100
/// 3. top;child_b 200
#[derive(Debug, Default)]
pub struct FoldedStackTraceBuilder {
    /// Trace entries.
    traces: Vec<TraceEntry>,
    /// Number of exits to be done before entering a new function.
    exits: usize,
}

#[derive(Debug, Default)]
struct TraceEntry {
    /// Names of all functions in the call stack of this trace.
    names: Vec<String>,
    /// Gas consumed by this function, allowed to be negative due to refunds.
    gas: i64,
}

impl FoldedStackTraceBuilder {
    /// Enter execution of a function call that consumes `gas`.
    pub fn enter(&mut self, label: String, gas: i64) {
        let mut names = self.traces.last().map(|entry| entry.names.clone()).unwrap_or_default();

        while self.exits > 0 {
            names.pop();
            self.exits -= 1;
        }

        names.push(label);
        self.traces.push(TraceEntry { names, gas });
    }

    /// Exit execution of a function call.
    pub fn exit(&mut self) {
        self.exits += 1;
    }

    /// Returns folded stack trace.
    pub fn build(mut self) -> Vec<String> {
        self.subtract_children();
        self.build_without_subtraction()
    }

    /// Internal method to build the folded stack trace without subtracting gas consumed by
    /// the children function calls.
    fn build_without_subtraction(&mut self) -> Vec<String> {
        let mut lines = Vec::new();
        for TraceEntry { names, gas } in self.traces.iter() {
            lines.push(format!("{} {}", names.join(";"), gas));
        }
        lines
    }

    /// Subtracts gas consumed by the children function calls from the parent function calls.
    fn subtract_children(&mut self) {
        // Iterate over each trace to find the children and subtract their values from the parents.
        for i in 0..self.traces.len() {
            let (left, right) = self.traces.split_at_mut(i);
            let TraceEntry { names, gas } = &right[0];
            if names.len() > 1 {
                let parent_trace_to_match = &names[..names.len() - 1];
                for parent in left.iter_mut().rev() {
                    if parent.names == parent_trace_to_match {
                        parent.gas -= gas;
                        break;
                    }
                }
            }
        }
    }
}

mod tests {
    #[test]
    fn test_fst_1() {
        let mut trace = super::FoldedStackTraceBuilder::default();
        trace.enter("top".to_string(), 500);
        trace.enter("child_a".to_string(), 100);
        trace.exit();
        trace.enter("child_b".to_string(), 200);

        assert_eq!(
            trace.build_without_subtraction(),
            vec![
                "top 500", //
                "top;child_a 100",
                "top;child_b 200",
            ]
        );
        assert_eq!(
            trace.build(),
            vec![
                "top 200", // 500 - 100 - 200
                "top;child_a 100",
                "top;child_b 200",
            ]
        );
    }

    #[test]
    fn test_fst_2() {
        let mut trace = super::FoldedStackTraceBuilder::default();
        trace.enter("top".to_string(), 500);
        trace.enter("child_a".to_string(), 300);
        trace.enter("child_b".to_string(), 100);
        trace.exit();
        trace.exit();
        trace.enter("child_c".to_string(), 100);

        assert_eq!(
            trace.build_without_subtraction(),
            vec![
                "top 500", //
                "top;child_a 300",
                "top;child_a;child_b 100",
                "top;child_c 100",
            ]
        );

        assert_eq!(
            trace.build(),
            vec![
                "top 100",         // 500 - 300 - 100
                "top;child_a 200", // 300 - 100
                "top;child_a;child_b 100",
                "top;child_c 100",
            ]
        );
    }

    #[test]
    fn test_fst_3() {
        let mut trace = super::FoldedStackTraceBuilder::default();
        trace.enter("top".to_string(), 1700);
        trace.enter("child_a".to_string(), 500);
        trace.exit();
        trace.enter("child_b".to_string(), 500);
        trace.enter("child_c".to_string(), 500);
        trace.exit();
        trace.exit();
        trace.exit();
        trace.enter("top2".to_string(), 1700);

        assert_eq!(
            trace.build_without_subtraction(),
            vec![
                "top 1700", //
                "top;child_a 500",
                "top;child_b 500",
                "top;child_b;child_c 500",
                "top2 1700",
            ]
        );

        assert_eq!(
            trace.build(),
            vec![
                "top 700", //
                "top;child_a 500",
                "top;child_b 0",
                "top;child_b;child_c 500",
                "top2 1700",
            ]
        );
    }
}