use std::cell::RefCell;
use std::rc::Rc;
use alloy::primitives::{Address, Log, I256, U256};
use alloy::rpc::types::Log as RpcLog;
use degenbot_decoders::{
v2_swap_decoder::{decode_v2_swap_log, V2_SWAP_TOPIC},
v3_swap_decoder::{decode_v3_swap_log, V3_SWAP_TOPIC},
v4_swap_decoder::{decode_v4_swap_log, V4_SWAP_TOPIC},
};
use revm::handler::FrameResult;
use revm::inspector::Inspector;
use revm::interpreter::{FrameInput, Interpreter};
use serde::Serialize;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SwapFamily {
V2,
V3,
V4,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct CapturedSwap {
pub emitter: Address,
pub family: SwapFamily,
pub amount0: I256,
pub amount1: I256,
pub sqrt_price_x96: U256,
pub liquidity: U256,
pub tick: i32,
}
impl CapturedSwap {
#[must_use]
pub fn from_log(log: &Log) -> Option<Self> {
let topic0 = log.topics().first()?;
let rpc_log = RpcLog {
inner: log.clone(),
block_hash: None,
block_number: None,
block_timestamp: None,
transaction_hash: None,
transaction_index: None,
log_index: None,
removed: false,
};
if *topic0 == V2_SWAP_TOPIC {
let ev = decode_v2_swap_log(&rpc_log)?;
let amount0 = u256_to_signed_delta(ev.amount0_out, ev.amount0_in)?;
let amount1 = u256_to_signed_delta(ev.amount1_out, ev.amount1_in)?;
Some(Self {
emitter: ev.pool_address,
family: SwapFamily::V2,
amount0,
amount1,
sqrt_price_x96: U256::ZERO,
liquidity: U256::ZERO,
tick: 0,
})
} else if *topic0 == V3_SWAP_TOPIC {
let ev = decode_v3_swap_log(&rpc_log)?;
Some(Self {
emitter: ev.pool_address,
family: SwapFamily::V3,
amount0: ev.amount0,
amount1: ev.amount1,
sqrt_price_x96: ev.sqrt_price_x96,
liquidity: U256::from(ev.liquidity),
tick: ev.tick,
})
} else if *topic0 == V4_SWAP_TOPIC {
let ev = decode_v4_swap_log(&rpc_log)?;
Some(Self {
emitter: Address::ZERO, family: SwapFamily::V4,
amount0: ev.amount0,
amount1: ev.amount1,
sqrt_price_x96: ev.sqrt_price_x96,
liquidity: U256::from(ev.liquidity),
tick: ev.tick,
})
} else {
None
}
}
#[must_use]
pub fn from_log_with_emitter(log: &Log) -> Option<Self> {
let mut swap = Self::from_log(log)?;
if swap.family == SwapFamily::V4 {
swap.emitter = log.address;
}
Some(swap)
}
}
#[must_use]
fn u256_to_signed_delta(amount_out: U256, amount_in: U256) -> Option<I256> {
let out = I256::try_from(amount_out).ok()?;
let inm = I256::try_from(amount_in).ok()?;
Some(out - inm)
}
#[derive(Debug, Default)]
pub(super) struct SwapEventBuffer {
pub swaps: Vec<CapturedSwap>,
pub reverted_swaps: Vec<CapturedSwap>,
frame_stack: Vec<Vec<CapturedSwap>>,
pub log_full_count: usize,
}
#[derive(Debug, Clone)]
pub struct SwapEventCaptureInspector {
buf: Rc<RefCell<SwapEventBuffer>>,
}
#[derive(Debug, Clone)]
pub struct SwapEventCaptureHandle {
buf: Rc<RefCell<SwapEventBuffer>>,
}
impl Default for SwapEventCaptureInspector {
fn default() -> Self {
Self {
buf: Rc::new(RefCell::new(SwapEventBuffer::default())),
}
}
}
impl SwapEventCaptureInspector {
#[must_use]
pub fn new() -> (Self, SwapEventCaptureHandle) {
let buf = Rc::new(RefCell::new(SwapEventBuffer::default()));
(
Self {
buf: Rc::clone(&buf),
},
SwapEventCaptureHandle { buf },
)
}
fn begin_frame(&self) {
self.buf.borrow_mut().frame_stack.push(Vec::new());
}
fn capture_swap_log(&self, log: &Log) {
let Some(swap) = CapturedSwap::from_log_with_emitter(log) else {
return;
};
let mut buf = self.buf.borrow_mut();
match buf.frame_stack.last_mut() {
Some(frame) => frame.push(swap),
None => buf.swaps.push(swap),
}
}
fn end_frame_committed(&self) {
let mut buf = self.buf.borrow_mut();
let Some(frame_swaps) = buf.frame_stack.pop() else {
return;
};
match buf.frame_stack.last_mut() {
Some(parent) => parent.extend(frame_swaps),
None => buf.swaps.extend(frame_swaps),
}
}
fn end_frame_reverted(&self) {
let mut buf = self.buf.borrow_mut();
if let Some(frame_swaps) = buf.frame_stack.pop() {
buf.reverted_swaps.extend(frame_swaps);
}
}
}
impl SwapEventCaptureHandle {
#[must_use]
pub fn take_swaps(&self) -> Vec<CapturedSwap> {
let mut buf = self.buf.borrow_mut();
let swaps = std::mem::take(&mut buf.swaps);
buf.frame_stack.clear();
buf.log_full_count = 0;
swaps
}
#[must_use]
pub fn take_reverted_swaps(&self) -> Vec<CapturedSwap> {
let mut buf = self.buf.borrow_mut();
std::mem::take(&mut buf.reverted_swaps)
}
#[must_use]
pub fn log_full_count(&self) -> usize {
self.buf.borrow().log_full_count
}
}
impl<CTX, INTR: revm::interpreter::InterpreterTypes> Inspector<CTX, INTR>
for SwapEventCaptureInspector
{
fn log_full(&mut self, _interp: &mut Interpreter<INTR>, _ctx: &mut CTX, log: Log) {
self.buf.borrow_mut().log_full_count += 1;
self.capture_swap_log(&log);
}
fn frame_start(
&mut self,
_context: &mut CTX,
_frame_input: &mut FrameInput,
) -> Option<FrameResult> {
self.begin_frame();
None
}
fn frame_end(
&mut self,
_context: &mut CTX,
_frame_input: &FrameInput,
frame_result: &mut FrameResult,
) {
let committed = match frame_result {
FrameResult::Call(outcome) => outcome.result.result.is_ok(),
FrameResult::Create(outcome) => outcome.result.result.is_ok(),
};
if committed {
self.end_frame_committed();
} else {
self.end_frame_reverted();
}
}
}
#[expect(clippy::unwrap_used, clippy::expect_used)]
#[cfg(test)]
mod tests {
use super::*;
use alloy::primitives::{Bytes, B256};
use degenbot_decoders::v2_swap_decoder::V2_SWAP_TOPIC;
fn v2_swap_log(
pool: Address,
sender: Address,
to: Address,
amount0_in: U256,
amount1_in: U256,
amount0_out: U256,
amount1_out: U256,
) -> Log {
let mut data = Vec::with_capacity(128);
data.extend_from_slice(&amount0_in.to_be_bytes::<32>());
data.extend_from_slice(&amount1_in.to_be_bytes::<32>());
data.extend_from_slice(&amount0_out.to_be_bytes::<32>());
data.extend_from_slice(&amount1_out.to_be_bytes::<32>());
Log::new_unchecked(
pool,
vec![V2_SWAP_TOPIC, sender.into_word(), to.into_word()],
Bytes::from(data),
)
}
#[test]
fn non_swap_log_returns_none() {
let log = Log::new_unchecked(
Address::repeat_byte(0x42),
vec![B256::repeat_byte(0xab)],
Bytes::new(),
);
assert!(CapturedSwap::from_log(&log).is_none());
}
#[test]
fn v2_swap_log_decodes_to_captured_swap_with_amounts() {
let pool = Address::repeat_byte(0x42);
let sender = Address::repeat_byte(0x11);
let to = Address::repeat_byte(0x22);
let amount0_in = U256::from(1_000_000_000_000_000_000_u64);
let amount1_out = U256::from(3_000_000_000_u64);
let log = v2_swap_log(
pool,
sender,
to,
amount0_in,
U256::ZERO,
U256::ZERO,
amount1_out,
);
let swap = CapturedSwap::from_log_with_emitter(&log).expect("V2 Swap decodes");
assert_eq!(swap.emitter, pool);
assert_eq!(swap.family, SwapFamily::V2);
assert_eq!(
swap.amount0,
I256::try_from(-1_000_000_000_000_000_000_i128).unwrap(),
"amount0 = out - in = -1e18 (token0 paid in)"
);
assert_eq!(
swap.amount1,
I256::try_from(3_000_000_000_i128).unwrap(),
"amount1 = out - in = +3000e6 (token1 received)"
);
assert_eq!(swap.sqrt_price_x96, U256::ZERO);
assert_eq!(swap.tick, 0);
}
#[test]
fn v2_swap_log_reverse_direction_negative_amount1() {
let pool = Address::repeat_byte(0x42);
let sender = Address::repeat_byte(0x11);
let to = Address::repeat_byte(0x22);
let log = v2_swap_log(
pool,
sender,
to,
U256::ZERO,
U256::from(500_u64),
U256::from(2_u64),
U256::ZERO,
);
let swap = CapturedSwap::from_log_with_emitter(&log).expect("V2 Swap decodes");
assert_eq!(swap.amount0, I256::try_from(2_i128).unwrap(), "+2 received");
assert_eq!(
swap.amount1,
I256::try_from(-500_i128).unwrap(),
"-500 paid in"
);
}
#[test]
fn empty_log_with_no_topics() {
let log = Log::new_unchecked(Address::repeat_byte(0x42), vec![], Bytes::new());
assert_eq!(CapturedSwap::from_log(&log), None);
}
#[test]
fn v2_swap_topic_is_known_constant() {
assert_eq!(
V2_SWAP_TOPIC,
alloy::primitives::b256!(
"0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822"
)
);
}
fn capture_log(pool: Address) -> Log {
v2_swap_log(
pool,
Address::repeat_byte(0x11),
Address::repeat_byte(0x22),
U256::ZERO,
U256::ZERO,
U256::ZERO,
U256::ZERO,
)
}
#[test]
fn reverted_subframe_swap_log_is_dropped_not_captured() {
let (insp, handle) = SwapEventCaptureInspector::new();
insp.begin_frame();
insp.capture_swap_log(&capture_log(Address::repeat_byte(0xA1)));
insp.begin_frame();
insp.capture_swap_log(&capture_log(Address::repeat_byte(0xA2)));
insp.end_frame_reverted();
insp.end_frame_committed();
let captured = handle.take_swaps();
assert_eq!(captured.len(), 1, "reverted sub-frame swap must be dropped");
assert_eq!(
captured[0].emitter,
Address::repeat_byte(0xA1),
"only the root-frame committed swap remains"
);
}
#[test]
fn committed_subframe_swap_log_is_merged_into_parent() {
let (insp, handle) = SwapEventCaptureInspector::new();
insp.begin_frame();
insp.capture_swap_log(&capture_log(Address::repeat_byte(0xB1)));
insp.begin_frame();
insp.capture_swap_log(&capture_log(Address::repeat_byte(0xB2)));
insp.end_frame_committed();
insp.capture_swap_log(&capture_log(Address::repeat_byte(0xB3)));
insp.end_frame_committed();
let captured = handle.take_swaps();
assert_eq!(captured.len(), 3, "all three committed swaps kept");
}
#[test]
fn nested_reverted_subframe_drops_only_inner_logs() {
let (insp, handle) = SwapEventCaptureInspector::new();
insp.begin_frame();
insp.capture_swap_log(&capture_log(Address::repeat_byte(0xC1)));
insp.begin_frame(); insp.capture_swap_log(&capture_log(Address::repeat_byte(0xC2)));
insp.begin_frame(); insp.capture_swap_log(&capture_log(Address::repeat_byte(0xC3)));
insp.end_frame_reverted();
insp.end_frame_committed();
insp.end_frame_committed();
let captured = handle.take_swaps();
assert_eq!(captured.len(), 2, "inner reverted logs dropped, outer kept");
}
#[test]
fn reverted_root_frame_drops_all_logs_from_committed_set() {
let (insp, handle) = SwapEventCaptureInspector::new();
insp.begin_frame();
insp.capture_swap_log(&capture_log(Address::repeat_byte(0xD1)));
insp.end_frame_reverted();
assert!(
handle.take_swaps().is_empty(),
"a reverted root frame drops all swaps from the committed set"
);
let reverted = handle.take_reverted_swaps();
assert_eq!(
reverted.len(),
1,
"the reverted swap is preserved separately"
);
assert_eq!(reverted[0].emitter, Address::repeat_byte(0xD1));
}
#[test]
fn reverted_unlock_inner_swaps_capturable_separately() {
let (insp, handle) = SwapEventCaptureInspector::new();
insp.begin_frame();
insp.begin_frame();
insp.begin_frame();
insp.capture_swap_log(&capture_log(Address::repeat_byte(0xA1)));
insp.end_frame_committed(); insp.begin_frame();
insp.capture_swap_log(&capture_log(Address::repeat_byte(0xA2)));
insp.end_frame_committed(); insp.end_frame_reverted();
insp.end_frame_committed(); assert!(
handle.take_swaps().is_empty(),
"the reverted unlock drops its inner swaps from the committed set"
);
let reverted = handle.take_reverted_swaps();
assert_eq!(
reverted.len(),
2,
"both inner swaps (V4 + V3) preserved in reverted_swaps"
);
assert_eq!(reverted[0].emitter, Address::repeat_byte(0xA1));
assert_eq!(reverted[1].emitter, Address::repeat_byte(0xA2));
}
}