use std::cell::RefCell;
use std::rc::Rc;
use alloy::primitives::{Address, Bytes};
use revm::inspector::Inspector;
use revm::interpreter::{CallInputs, CallOutcome, CreateInputs, CreateOutcome, InstructionResult};
use serde::Serialize;
use super::call_end::pair_lifo;
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum FrameOutcome {
Success { gas_used: u64, output: Bytes },
Revert { gas_used: u64, data: Bytes },
Halt { gas_used: u64, reason: String },
}
impl FrameOutcome {
fn from_result(res: &revm::interpreter::InterpreterResult) -> Self {
let gas_used = res.gas.total_gas_spent();
match res.result {
InstructionResult::Revert => Self::Revert {
gas_used,
data: res.output.clone(),
},
InstructionResult::Stop
| InstructionResult::Return
| InstructionResult::SelfDestruct => Self::Success {
gas_used,
output: res.output.clone(),
},
_ => Self::Halt {
gas_used,
reason: format!("{res:?}"),
},
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct CallFrame {
pub depth: usize,
pub caller: Address,
pub target: Address,
pub selector: [u8; 4],
pub gas_limit: u64,
#[serde(default)]
pub data: Bytes,
pub outcome: Option<FrameOutcome>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct CallTrace {
pub frames: Vec<CallFrame>,
}
impl CallTrace {
#[must_use]
pub fn failing_frame(&self) -> Option<&CallFrame> {
self.frames
.iter()
.rev()
.find(|f| !matches!(f.outcome, Some(FrameOutcome::Success { .. }) | None))
}
#[must_use]
pub fn deepest_revert(&self) -> Option<&CallFrame> {
self.frames
.iter()
.rev()
.find(|f| matches!(f.outcome, Some(FrameOutcome::Revert { .. })))
}
#[must_use]
pub fn render_debug(&self) -> String {
use FrameOutcome as FO;
let mut out = String::new();
for f in &self.frames {
let kind = match &f.outcome {
Some(FO::Revert { .. }) => "revert",
Some(FO::Success { .. }) => "ok",
_ => "halt",
};
let gas = f
.outcome
.as_ref()
.map(|o| match o {
FO::Revert { gas_used, .. }
| FO::Halt { gas_used, .. }
| FO::Success { gas_used, .. } => *gas_used,
})
.unwrap_or_default();
for _ in 0..f.depth.saturating_sub(1) {
out.push_str(" ");
}
let reason = match &f.outcome {
Some(FO::Halt { reason, .. }) if !reason.is_empty() => {
format!(" reason={reason}")
}
_ => String::new(),
};
let data = if matches!(kind, "revert" | "halt") {
format!(" data=0x{}", alloy::primitives::hex::encode(&f.data))
} else {
String::new()
};
let _ = std::fmt::Write::write_fmt(
&mut out,
format_args!(
"d{} {}:0x{}:{} gl{} g{}{}{}\n",
f.depth,
f.target,
alloy::primitives::hex::encode(f.selector),
kind,
f.gas_limit,
gas,
reason,
data
),
);
}
out
}
#[must_use]
pub fn reverting_frame_label(&self) -> Option<(&CallFrame, String)> {
self.deepest_revert().map(|f| {
let label = match &f.outcome {
Some(FrameOutcome::Revert { data, .. }) => {
degenbot_decoders::revert::classify_revert(data)
}
_ => String::new(),
};
(f, label)
})
}
}
#[derive(Debug, Default)]
pub(super) struct CallTraceBuffer {
pub frames: Vec<CallFrame>,
pub depth: usize,
}
#[derive(Debug, Clone)]
pub struct CallTraceInspector {
buf: Rc<RefCell<CallTraceBuffer>>,
}
#[derive(Debug, Clone)]
pub struct CallTraceHandle {
buf: Rc<RefCell<CallTraceBuffer>>,
}
impl Default for CallTraceInspector {
fn default() -> Self {
Self {
buf: Rc::new(RefCell::new(CallTraceBuffer::default())),
}
}
}
impl CallTraceInspector {
#[must_use]
pub fn new() -> (Self, CallTraceHandle) {
let buf = Rc::new(RefCell::new(CallTraceBuffer::default()));
(
Self {
buf: Rc::clone(&buf),
},
CallTraceHandle { buf },
)
}
}
impl CallTraceHandle {
#[expect(dead_code)]
fn push_frame(&self, depth: usize, inputs: &CallInputs) {
let selector = selector_of(&inputs.input);
self.buf.borrow_mut().frames.push(CallFrame {
depth,
caller: inputs.caller,
target: inputs.target_address,
selector,
gas_limit: inputs.gas_limit,
data: selector.to_vec().into(),
outcome: None,
});
}
#[must_use]
pub fn take_trace(&self) -> CallTrace {
let mut buf = self.buf.borrow_mut();
let frames = std::mem::take(&mut buf.frames);
buf.depth = 0;
CallTrace { frames }
}
}
fn selector_of(input: &revm::interpreter::CallInput) -> [u8; 4] {
match input {
revm::interpreter::CallInput::Bytes(b) => {
let mut s = [0u8; 4];
if b.len() >= 4 {
s.copy_from_slice(&b[..4]);
} else if !b.is_empty() {
s[..b.len()].copy_from_slice(b);
}
s
}
revm::interpreter::CallInput::SharedBuffer(_) => [0u8; 4],
}
}
impl<CTX, INTR: revm::interpreter::InterpreterTypes> Inspector<CTX, INTR> for CallTraceInspector
where
CTX: revm::context_interface::ContextTr,
{
fn call(&mut self, ctx: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
let data: Bytes = inputs.input.as_bytes(ctx).to_vec().into();
let mut buf = self.buf.borrow_mut();
buf.depth += 1;
let depth = buf.depth;
let frame = CallFrame {
depth,
caller: inputs.caller,
target: inputs.target_address,
selector: selector_of(&inputs.input),
gas_limit: inputs.gas_limit,
data,
outcome: None,
};
buf.frames.push(frame);
None
}
fn call_end(&mut self, _ctx: &mut CTX, _inputs: &CallInputs, outcome: &mut CallOutcome) {
let mut buf = self.buf.borrow_mut();
pair_lifo(&mut buf.frames, FrameOutcome::from_result(&outcome.result));
buf.depth = buf.depth.saturating_sub(1);
}
fn create(&mut self, _ctx: &mut CTX, inputs: &mut CreateInputs) -> Option<CreateOutcome> {
let mut buf = self.buf.borrow_mut();
buf.depth += 1;
let depth = buf.depth;
let frame = CallFrame {
depth,
caller: inputs.caller(),
target: Address::ZERO,
selector: [0u8; 4],
gas_limit: inputs.gas_limit(),
data: Bytes::new(),
outcome: None,
};
buf.frames.push(frame);
None
}
fn create_end(&mut self, _ctx: &mut CTX, _inputs: &CreateInputs, outcome: &mut CreateOutcome) {
let mut buf = self.buf.borrow_mut();
pair_lifo(&mut buf.frames, FrameOutcome::from_result(&outcome.result));
buf.depth = buf.depth.saturating_sub(1);
}
}
#[expect(clippy::unwrap_used)]
#[cfg(test)]
mod tests {
use super::*;
fn success_frame() -> CallFrame {
CallFrame {
depth: 1,
caller: Address::ZERO,
target: Address::ZERO,
selector: [0u8; 4],
gas_limit: 0,
data: Bytes::new(),
outcome: Some(FrameOutcome::Success {
gas_used: 0,
output: Bytes::new(),
}),
}
}
fn revert_frame(data: &[u8]) -> CallFrame {
CallFrame {
depth: 2,
caller: Address::ZERO,
target: Address::repeat_byte(0x20),
selector: [0u8; 4],
gas_limit: 0,
data: Bytes::new(),
outcome: Some(FrameOutcome::Revert {
gas_used: 0,
data: Bytes::copy_from_slice(data),
}),
}
}
#[test]
fn empty_trace_has_no_revert() {
let trace = CallTrace::default();
assert!(trace.deepest_revert().is_none());
assert!(trace.reverting_frame_label().is_none());
}
#[test]
fn render_debug_indents_by_depth_in_call_order() {
let trace = CallTrace {
frames: vec![
CallFrame {
depth: 1,
caller: Address::ZERO,
target: Address::repeat_byte(0xaa),
selector: [0x12, 0x34, 0x56, 0x78],
gas_limit: 0,
data: Bytes::new(),
outcome: Some(FrameOutcome::Success {
gas_used: 1000,
output: Bytes::new(),
}),
},
CallFrame {
depth: 2,
caller: Address::repeat_byte(0xaa),
target: Address::repeat_byte(0xbb),
selector: [0xde, 0xad, 0xbe, 0xef],
gas_limit: 0,
data: Bytes::new(),
outcome: Some(FrameOutcome::Revert {
gas_used: 500,
data: Bytes::new(),
}),
},
],
};
let rendered = trace.render_debug();
assert!(rendered.contains(":0x12345678:ok gl0 g1000"), "{rendered}");
assert!(
rendered.contains(" d2 ") && rendered.contains(":0xdeadbeef:revert gl0 g500"),
"{rendered}"
);
assert!(rendered.ends_with('\n'), "{rendered}");
}
#[test]
fn deepest_revert_picks_deepest_among_ordered_frames() {
let trace = CallTrace {
frames: vec![
revert_frame(&[0xa1]),
success_frame(),
revert_frame(&[0xb2]),
],
};
let label = trace.reverting_frame_label();
assert!(label.is_some());
let (frame, _label) = label.unwrap();
assert_eq!(
frame.outcome,
Some(FrameOutcome::Revert {
gas_used: 0,
data: Bytes::from(vec![0xb2])
})
);
}
}