Skip to main content

foundry_evm_traces/
lib.rs

1//! # foundry-evm-traces
2//!
3//! EVM trace identifying and decoding.
4
5#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[macro_use]
9extern crate foundry_common;
10
11#[macro_use]
12extern crate tracing;
13
14use foundry_common::{
15    contracts::{ContractsByAddress, ContractsByArtifact},
16    shell,
17};
18use revm::bytecode::opcode::OpCode;
19use revm_inspectors::tracing::{
20    OpcodeFilter,
21    types::{DecodedTraceStep, TraceMemberOrder},
22};
23use serde::{Deserialize, Serialize};
24use std::{
25    borrow::Cow,
26    collections::{BTreeMap, BTreeSet},
27    ops::{Deref, DerefMut},
28};
29
30use alloy_primitives::{U256, map::HashMap};
31use tempo_contracts::precompiles::TIP20_CHANNEL_RESERVE_ADDRESS;
32
33pub use revm_inspectors::tracing::{
34    CallTraceArena, FourByteInspector, GethTraceBuilder, ParityTraceBuilder, StackSnapshotType,
35    TraceWriter, TracingInspector, TracingInspectorConfig,
36    types::{
37        CallKind, CallLog, CallTrace, CallTraceNode, DecodedCallData, DecodedCallLog,
38        DecodedCallTrace,
39    },
40};
41
42/// Call trace address identifiers.
43///
44/// Identifiers figure out what ABIs and labels belong to all the addresses of the trace.
45pub mod identifier;
46use identifier::LocalTraceIdentifier;
47
48mod decoder;
49pub use decoder::{CallTraceDecoder, CallTraceDecoderBuilder};
50
51pub mod debug;
52pub use debug::DebugTraceIdentifier;
53
54pub mod folded_stack_trace;
55
56pub mod backtrace;
57
58pub type Traces = Vec<(TraceKind, SparsedTraceArena)>;
59
60/// Trace arena keeping track of ignored trace items.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct SparsedTraceArena {
63    /// Full trace arena.
64    #[serde(flatten)]
65    pub arena: CallTraceArena,
66    /// Ranges of trace steps to ignore in format (start_node, start_step) -> (end_node, end_step).
67    /// See `foundry_cheatcodes::utils::IgnoredTraces` for more information.
68    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
69    pub ignored: HashMap<(usize, usize), (usize, usize)>,
70}
71
72impl SparsedTraceArena {
73    /// Goes over entire trace arena and removes ignored trace items.
74    fn resolve_arena(&self) -> Cow<'_, CallTraceArena> {
75        if self.ignored.is_empty() {
76            Cow::Borrowed(&self.arena)
77        } else {
78            let mut arena = self.arena.clone();
79
80            fn clear_node(
81                nodes: &mut [CallTraceNode],
82                node_idx: usize,
83                ignored: &HashMap<(usize, usize), (usize, usize)>,
84                cur_ignore_end: &mut Option<(usize, usize)>,
85            ) {
86                // Prepend an additional None item to the ordering to handle the beginning of the
87                // trace.
88                let items = std::iter::once(None)
89                    .chain(nodes[node_idx].ordering.clone().into_iter().map(Some))
90                    .enumerate();
91
92                let mut internal_calls = Vec::new();
93                let mut items_to_remove = BTreeSet::new();
94                for (item_idx, item) in items {
95                    if let Some(end_node) = ignored.get(&(node_idx, item_idx)) {
96                        *cur_ignore_end = Some(*end_node);
97                    }
98
99                    let mut remove = cur_ignore_end.is_some() & item.is_some();
100
101                    match item {
102                        // we only remove calls if they did not start/pause tracing
103                        Some(TraceMemberOrder::Call(child_idx)) => {
104                            clear_node(
105                                nodes,
106                                nodes[node_idx].children[child_idx],
107                                ignored,
108                                cur_ignore_end,
109                            );
110                            remove &= cur_ignore_end.is_some();
111                        }
112                        // we only remove decoded internal calls if they did not start/pause tracing
113                        Some(TraceMemberOrder::Step(step_idx)) => {
114                            // If this is an internal call beginning, track it in `internal_calls`
115                            if let Some(decoded) = &nodes[node_idx].trace.steps[step_idx].decoded
116                                && let DecodedTraceStep::InternalCall(_, end_step_idx) = &**decoded
117                            {
118                                internal_calls.push((item_idx, remove, *end_step_idx));
119                                // we decide if we should remove it later
120                                remove = false;
121                            }
122                            // Handle ends of internal calls
123                            internal_calls.retain(|(start_item_idx, remove_start, end_idx)| {
124                                if *end_idx != step_idx {
125                                    return true;
126                                }
127                                // only remove start if end should be removed as well
128                                if *remove_start && remove {
129                                    items_to_remove.insert(*start_item_idx);
130                                } else {
131                                    remove = false;
132                                }
133
134                                false
135                            });
136                        }
137                        _ => {}
138                    }
139
140                    if remove {
141                        items_to_remove.insert(item_idx);
142                    }
143
144                    if let Some((end_node, end_step_idx)) = cur_ignore_end
145                        && node_idx == *end_node
146                        && item_idx == *end_step_idx
147                    {
148                        *cur_ignore_end = None;
149                    }
150                }
151
152                for (offset, item_idx) in items_to_remove.into_iter().enumerate() {
153                    nodes[node_idx].ordering.remove(item_idx - offset - 1);
154                }
155            }
156
157            clear_node(arena.nodes_mut(), 0, &self.ignored, &mut None);
158
159            Cow::Owned(arena)
160        }
161    }
162}
163
164impl Deref for SparsedTraceArena {
165    type Target = CallTraceArena;
166
167    fn deref(&self) -> &Self::Target {
168        &self.arena
169    }
170}
171
172impl DerefMut for SparsedTraceArena {
173    fn deref_mut(&mut self) -> &mut Self::Target {
174        &mut self.arena
175    }
176}
177
178/// Decode a collection of call traces.
179///
180/// The traces will be decoded using the given decoder, if possible.
181pub async fn decode_trace_arena(arena: &mut CallTraceArena, decoder: &CallTraceDecoder) {
182    decoder.prefetch_signatures(arena.nodes()).await;
183    decoder.populate_traces(arena.nodes_mut()).await;
184}
185
186/// Render a collection of call traces to a string.
187pub fn render_trace_arena(arena: &SparsedTraceArena) -> String {
188    render_trace_arena_inner(arena, false, false)
189}
190
191/// Prunes trace depth if depth is provided as an argument
192pub fn prune_trace_depth(arena: &mut CallTraceArena, depth: usize) {
193    for node in arena.nodes_mut() {
194        if node.trace.depth >= depth {
195            node.ordering.clear();
196        }
197    }
198}
199
200/// Render a collection of call traces to a string optionally including contract creation bytecodes
201/// and in JSON format.
202pub fn render_trace_arena_inner(
203    arena: &SparsedTraceArena,
204    with_bytecodes: bool,
205    with_storage_changes: bool,
206) -> String {
207    if shell::is_json() {
208        return serde_json::to_string(&arena.resolve_arena()).expect("Failed to serialize traces");
209    }
210
211    let resolved = arena.resolve_arena();
212    let mut w = TraceWriter::new(Vec::<u8>::new())
213        .color_cheatcodes(true)
214        .use_colors(convert_color_choice(shell::color_choice()))
215        .write_bytecodes(with_bytecodes)
216        .with_storage_changes(with_storage_changes);
217    w.write_arena(&resolved).expect("Failed to write traces");
218    let mut rendered =
219        String::from_utf8(w.into_writer()).expect("trace writer wrote invalid UTF-8");
220    if with_storage_changes {
221        append_tempo_channel_storage_decodes(&mut rendered, &resolved);
222    }
223    rendered
224}
225
226fn append_tempo_channel_storage_decodes(rendered: &mut String, arena: &CallTraceArena) {
227    let decoded_changes = arena
228        .nodes()
229        .iter()
230        .filter(|node| node.trace.address == TIP20_CHANNEL_RESERVE_ADDRESS)
231        .flat_map(compact_channel_storage_changes)
232        .collect::<Vec<_>>();
233
234    if decoded_changes.is_empty() {
235        return;
236    }
237
238    if !rendered.ends_with('\n') {
239        rendered.push('\n');
240    }
241    rendered.push_str("Decoded TIP20ChannelReserve storage:\n");
242    for (slot, before, after) in decoded_changes {
243        rendered.push_str(&format!(
244            "  @ {}: {} -> {}\n",
245            format_storage_word(slot),
246            format_channel_state(before),
247            format_channel_state(after),
248        ));
249    }
250}
251
252fn compact_channel_storage_changes(node: &CallTraceNode) -> Vec<(U256, U256, U256)> {
253    let mut changes_map = BTreeMap::new();
254    for step in &node.trace.steps {
255        if let Some(change) = &step.storage_change
256            && change.had_value.is_some()
257        {
258            let (_first, last) = changes_map.entry(change.key).or_insert((&**change, &**change));
259            *last = &**change;
260        }
261    }
262
263    changes_map
264        .into_iter()
265        .filter_map(|(key, (first, last))| {
266            let before = first.had_value.unwrap_or_default();
267            let after = last.value;
268            (before != after).then_some((key, before, after))
269        })
270        .collect()
271}
272
273fn format_channel_state(value: U256) -> String {
274    let (settled, deposit, close_requested_at) = decode_channel_state(value);
275    format!("{{settled: {settled}, deposit: {deposit}, closeRequestedAt: {close_requested_at}}}")
276}
277
278fn decode_channel_state(value: U256) -> (U256, U256, u32) {
279    let mask96 = (U256::from(1) << 96) - U256::from(1);
280    let mask32 = (U256::from(1) << 32) - U256::from(1);
281    let settled: U256 = value & mask96;
282    let deposit: U256 = (value >> 96usize) & mask96;
283    let close_requested_at_word: U256 = (value >> 192usize) & mask32;
284    let close_requested_at = close_requested_at_word.to::<u32>();
285    (settled, deposit, close_requested_at)
286}
287
288fn format_storage_word(value: U256) -> String {
289    if value < U256::from(1_000_000u64) { value.to_string() } else { format!("0x{value:x}") }
290}
291
292const fn convert_color_choice(choice: shell::ColorChoice) -> revm_inspectors::ColorChoice {
293    match choice {
294        shell::ColorChoice::Auto => revm_inspectors::ColorChoice::Auto,
295        shell::ColorChoice::Always => revm_inspectors::ColorChoice::Always,
296        shell::ColorChoice::Never => revm_inspectors::ColorChoice::Never,
297    }
298}
299
300/// Specifies the kind of trace.
301#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
302pub enum TraceKind {
303    Deployment,
304    Setup,
305    Execution,
306}
307
308impl TraceKind {
309    /// Returns `true` if the trace kind is [`Deployment`].
310    ///
311    /// [`Deployment`]: TraceKind::Deployment
312    #[must_use]
313    pub const fn is_deployment(self) -> bool {
314        matches!(self, Self::Deployment)
315    }
316
317    /// Returns `true` if the trace kind is [`Setup`].
318    ///
319    /// [`Setup`]: TraceKind::Setup
320    #[must_use]
321    pub const fn is_setup(self) -> bool {
322        matches!(self, Self::Setup)
323    }
324
325    /// Returns `true` if the trace kind is [`Execution`].
326    ///
327    /// [`Execution`]: TraceKind::Execution
328    #[must_use]
329    pub const fn is_execution(self) -> bool {
330        matches!(self, Self::Execution)
331    }
332}
333
334/// Given a list of traces and artifacts, it returns a map connecting address to abi
335pub fn load_contracts<'a>(
336    traces: impl IntoIterator<Item = &'a CallTraceArena>,
337    known_contracts: &ContractsByArtifact,
338) -> ContractsByAddress {
339    let mut local_identifier = LocalTraceIdentifier::new(known_contracts);
340    let decoder = CallTraceDecoder::new();
341    let mut contracts = ContractsByAddress::new();
342    for trace in traces {
343        for address in decoder.identify_addresses(trace, &mut local_identifier) {
344            if let (Some(contract), Some(abi)) = (address.contract, address.abi) {
345                contracts.insert(address.address, (contract, abi.into_owned()));
346            }
347        }
348    }
349    contracts
350}
351
352/// Different kinds of internal functions tracing.
353#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
354pub enum InternalTraceMode {
355    #[default]
356    None,
357    /// Traces internal functions without decoding inputs/outputs from memory.
358    Simple,
359    /// Same as `Simple`, but also tracks memory snapshots.
360    Full,
361}
362
363impl From<InternalTraceMode> for TraceMode {
364    fn from(mode: InternalTraceMode) -> Self {
365        match mode {
366            InternalTraceMode::None => Self::None,
367            InternalTraceMode::Simple => Self::JumpSimple,
368            InternalTraceMode::Full => Self::Jump,
369        }
370    }
371}
372
373// Different kinds of traces used by different foundry components.
374#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
375pub enum TraceMode {
376    /// Disabled tracing.
377    #[default]
378    None,
379    /// Simple call trace, no steps tracing required.
380    Call,
381    /// Call trace with steps tracing for JUMP and JUMPDEST opcodes.
382    ///
383    /// Does not enable tracking memory or stack snapshots.
384    Steps,
385    /// Call trace with tracing for JUMP and JUMPDEST opcode steps.
386    ///
387    /// Used for internal functions identification. Does not track memory snapshots.
388    JumpSimple,
389    /// Call trace with tracing for JUMP and JUMPDEST opcode steps.
390    ///
391    /// Same as `JumpSimple`, but tracks memory snapshots as well.
392    Jump,
393    /// Call trace with complete steps tracing.
394    ///
395    /// Used by debugger.
396    Debug,
397    /// Step trace with storage change recording.
398    ///
399    /// Records JUMP/JUMPDEST steps (like `Steps`) plus storage diffs on SLOAD/SSTORE.
400    /// Does not enable memory/stack snapshots or unfiltered opcode recording.
401    RecordStateDiff,
402}
403
404impl TraceMode {
405    pub const fn is_none(self) -> bool {
406        matches!(self, Self::None)
407    }
408
409    pub const fn is_call(self) -> bool {
410        matches!(self, Self::Call)
411    }
412
413    pub const fn is_steps(self) -> bool {
414        matches!(self, Self::Steps)
415    }
416
417    pub const fn is_jump_simple(self) -> bool {
418        matches!(self, Self::JumpSimple)
419    }
420
421    pub const fn is_jump(self) -> bool {
422        matches!(self, Self::Jump)
423    }
424
425    pub const fn record_state_diff(self) -> bool {
426        matches!(self, Self::RecordStateDiff)
427    }
428
429    pub const fn is_debug(self) -> bool {
430        matches!(self, Self::Debug)
431    }
432
433    pub fn with_debug(self, yes: bool) -> Self {
434        if yes { std::cmp::max(self, Self::Debug) } else { self }
435    }
436
437    pub fn with_decode_internal(self, mode: InternalTraceMode) -> Self {
438        std::cmp::max(self, mode.into())
439    }
440
441    pub fn with_state_changes(self, yes: bool) -> Self {
442        if yes { std::cmp::max(self, Self::RecordStateDiff) } else { self }
443    }
444
445    pub fn with_verbosity(self, verbosity: u8) -> Self {
446        match verbosity {
447            0..3 => self,
448            3..=4 => std::cmp::max(self, Self::Call),
449            // Enable step recording and state diff recording when verbosity is 5 or higher.
450            // This includes backtraces (JUMP/JUMPDEST steps) and storage changes.
451            _ => std::cmp::max(self, Self::RecordStateDiff),
452        }
453    }
454
455    pub fn into_config(self) -> Option<TracingInspectorConfig> {
456        if self.is_none() {
457            None
458        } else {
459            // RecordStateDiff is Steps + state diff recording, not Debug + state diff.
460            // It should not enable memory/stack snapshots.
461            // State diff recording requires all opcodes (no filter) since it needs
462            // SLOAD/SSTORE steps, not just JUMP/JUMPDEST.
463            let effective = if self.record_state_diff() { Self::Steps } else { self };
464            TracingInspectorConfig {
465                record_steps: self >= Self::Steps,
466                record_memory_snapshots: effective >= Self::Jump,
467                record_stack_snapshots: if effective > Self::Steps {
468                    StackSnapshotType::Full
469                } else {
470                    StackSnapshotType::None
471                },
472                record_logs: true,
473                record_state_diff: self.record_state_diff(),
474                record_returndata_snapshots: effective.is_debug(),
475                // State diff needs all opcodes recorded to capture SLOAD/SSTORE.
476                record_opcodes_filter: if self.record_state_diff() {
477                    None
478                } else {
479                    (effective.is_steps() || effective.is_jump() || effective.is_jump_simple())
480                        .then(|| {
481                            OpcodeFilter::new().enabled(OpCode::JUMP).enabled(OpCode::JUMPDEST)
482                        })
483                },
484                exclude_precompile_calls: false,
485                record_immediate_bytes: effective.is_debug(),
486            }
487            .into()
488        }
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495
496    #[test]
497    fn decodes_tip1034_packed_channel_state() {
498        let settled = U256::from(123u64);
499        let deposit = U256::from(456u64);
500        let close_requested_at = U256::from(1_780_495_200u64);
501        let packed = settled | (deposit << 96usize) | (close_requested_at << 192usize);
502
503        assert_eq!(decode_channel_state(packed), (settled, deposit, 1_780_495_200));
504        assert_eq!(
505            format_channel_state(packed),
506            "{settled: 123, deposit: 456, closeRequestedAt: 1780495200}"
507        );
508    }
509
510    // -- TraceMode::with_verbosity level tests --
511
512    #[test]
513    fn verbosity_0_through_2_is_noop() {
514        for v in 0..=2 {
515            assert_eq!(TraceMode::None.with_verbosity(v), TraceMode::None, "v={v}");
516            assert_eq!(TraceMode::Call.with_verbosity(v), TraceMode::Call, "v={v}");
517            assert_eq!(TraceMode::Debug.with_verbosity(v), TraceMode::Debug, "v={v}");
518        }
519    }
520
521    #[test]
522    fn verbosity_3_and_4_raises_to_call() {
523        for v in 3..=4 {
524            assert_eq!(TraceMode::None.with_verbosity(v), TraceMode::Call, "v={v}");
525            // Already above Call — must not downgrade.
526            assert_eq!(TraceMode::Debug.with_verbosity(v), TraceMode::Debug, "v={v}");
527            assert_eq!(
528                TraceMode::RecordStateDiff.with_verbosity(v),
529                TraceMode::RecordStateDiff,
530                "v={v}"
531            );
532        }
533    }
534
535    #[test]
536    fn verbosity_5_raises_to_record_state_diff() {
537        assert_eq!(TraceMode::None.with_verbosity(5), TraceMode::RecordStateDiff);
538        assert_eq!(TraceMode::Call.with_verbosity(5), TraceMode::RecordStateDiff);
539        assert_eq!(TraceMode::Steps.with_verbosity(5), TraceMode::RecordStateDiff);
540        assert_eq!(TraceMode::Debug.with_verbosity(5), TraceMode::RecordStateDiff);
541        // Already at the top — stays the same.
542        assert_eq!(TraceMode::RecordStateDiff.with_verbosity(5), TraceMode::RecordStateDiff);
543    }
544
545    // -- into_config at each verbosity level --
546
547    #[test]
548    fn config_at_verbosity_0_is_none() {
549        let mode = TraceMode::None.with_verbosity(0);
550        assert!(mode.into_config().is_none());
551    }
552
553    #[test]
554    fn config_at_verbosity_3_records_calls_only() {
555        let cfg = TraceMode::None.with_verbosity(3).into_config().unwrap();
556        assert!(!cfg.record_steps, "verbosity 3 should not record steps");
557        assert!(!cfg.record_state_diff, "verbosity 3 should not record state diff");
558        assert!(cfg.record_logs, "verbosity 3 should record logs");
559    }
560
561    #[test]
562    fn config_at_verbosity_5_records_steps_and_state_diff() {
563        let cfg = TraceMode::None.with_verbosity(5).into_config().unwrap();
564        assert!(cfg.record_steps, "verbosity 5 must record steps for backtraces");
565        assert!(cfg.record_state_diff, "verbosity 5 must record state diff");
566        assert!(cfg.record_logs, "verbosity 5 must record logs");
567        // RecordStateDiff should NOT enable expensive debug-level features.
568        assert!(!cfg.record_memory_snapshots, "verbosity 5 should not record memory snapshots");
569        assert_eq!(
570            cfg.record_stack_snapshots,
571            StackSnapshotType::None,
572            "verbosity 5 should not record stack snapshots"
573        );
574        // State diff requires all opcodes to capture SLOAD/SSTORE, so no filter.
575        assert!(
576            cfg.record_opcodes_filter.is_none(),
577            "verbosity 5 needs unfiltered opcodes for state diff"
578        );
579    }
580
581    #[test]
582    fn config_debug_mode_unchanged() {
583        // Debug mode must still enable full recording for the debugger.
584        let cfg = TraceMode::Debug.into_config().unwrap();
585        assert!(cfg.record_steps);
586        assert!(cfg.record_memory_snapshots, "Debug must record memory snapshots");
587        assert_eq!(
588            cfg.record_stack_snapshots,
589            StackSnapshotType::Full,
590            "Debug must record full stack snapshots"
591        );
592        assert!(cfg.record_returndata_snapshots, "Debug must record returndata");
593        assert!(cfg.record_immediate_bytes, "Debug must record immediate bytes");
594        assert!(cfg.record_opcodes_filter.is_none(), "Debug must record all opcodes (no filter)");
595        assert!(!cfg.record_state_diff, "Debug alone should not record state diff");
596    }
597}