#![expect(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::sync::Arc;
use alloy::primitives::{address, Address, Bytes, B256, U256};
use degenbot_pools::slot_layout::{
cl_liquidity_tracked_word, cl_slot0_tracked_word, decode_tick_word, decode_v2_reserves_word,
pack_v2_reserves_word, tick_mapping_slot_at_base, tick_word_of, V2ReservesParts,
};
use degenbot_pools::v3_storage_slots::decode_v3_slot0;
use degenbot_pools::v4_storage_slots::{
encode_v4_liquidity_slot, encode_v4_slot0, v4_liquidity_slot, v4_pool_state_base_slot,
v4_slot0_slot, v4_tick_mapping_slot, V4Slot0Parts,
};
use degenbot_pools::ClSlotLayout;
use degenbot_simulation::sim::evm::frame_replay::{
ReplayStatus, ReplayableTx, ScratchBlock, ScratchEvm,
};
use degenbot_simulation::sim::evm::journal_pools::{
extract_pool_post_states, PoolFamily, PoolPostKind, PoolPostState, TouchedTickWord,
TypedPoolPost, V4PoolDescriptor, V4PoolSet,
};
use revm::bytecode::Bytecode;
use revm::database::CacheDB;
use revm::database_interface::EmptyDB;
use revm::state::AccountInfo;
const SENDER: Address = address!("0x1111111111111111111111111111111111111111");
const POOL: Address = address!("0x2222222222222222222222222222222222222222");
const STRANGER: Address = address!("0x3333333333333333333333333333333333333333");
const FIRST_BLOCK: u64 = 2050;
const TIMESTAMP: u64 = 1_780_000_000;
const BASE_FEE_GWEI: u128 = 1_000_000_000;
const PAIRS: usize = 3;
fn multi_store_code() -> Vec<u8> {
let mut code = Vec::new();
for k in 0..PAIRS {
let s = 32 * k;
let v = 32 * (k + PAIRS);
code.extend_from_slice(&[
0x60,
u8::try_from(v).unwrap(),
0x35,
0x60,
u8::try_from(s).unwrap(),
0x35,
0x55,
]);
}
code
}
fn block_env() -> ScratchBlock {
ScratchBlock {
number: FIRST_BLOCK,
timestamp: TIMESTAMP,
base_fee_next: BASE_FEE_GWEI,
}
}
type TestExt = CacheDB<EmptyDB>;
fn scratch() -> ScratchEvm<TestExt> {
let mut db = CacheDB::new(EmptyDB::default());
for (addr, code) in [(POOL, multi_store_code()), (STRANGER, multi_store_code())] {
db.insert_account_info(
addr,
AccountInfo {
balance: U256::from(1_000_000_000_000_000_000u64),
nonce: 0,
code: Some(Bytecode::new_raw(Bytes::copy_from_slice(&code))),
..Default::default()
},
);
}
db.insert_account_info(
SENDER,
AccountInfo {
balance: U256::from(1_000_000_000_000_000_000u64),
nonce: 7,
..Default::default()
},
);
ScratchEvm::new(db, block_env())
}
fn tx(nonce: u64, pairs: &[(U256, U256)]) -> ReplayableTx {
let pad: U256 = U256::from(1u128) << 248;
let pad_be = pad.to_be_bytes::<32>();
let mut data = Vec::with_capacity(64 * PAIRS);
for k in 0..PAIRS {
match pairs.get(k) {
Some((slot, _)) => data.extend_from_slice(&slot.to_be_bytes::<32>()),
None => data.extend_from_slice(&pad_be),
}
}
for k in 0..PAIRS {
match pairs.get(k) {
Some((_, value)) => data.extend_from_slice(&value.to_be_bytes::<32>()),
None => data.extend_from_slice(&pad_be),
}
}
ReplayableTx {
from: SENDER,
to: Some(POOL),
value: U256::ZERO,
data: Bytes::from(data),
gas_limit: 300_000,
max_fee_per_gas: BASE_FEE_GWEI,
max_priority_fee_per_gas: 0,
nonce,
}
}
fn tx_to(to: Address, nonce: u64, pairs: &[(U256, U256)]) -> ReplayableTx {
ReplayableTx {
to: Some(to),
..tx(nonce, pairs)
}
}
fn relay_code(pool: Address) -> Vec<u8> {
let mut code = Vec::new();
code.extend_from_slice(&[0x36, 0x60, 0x00, 0x60, 0x00, 0x37]); code.extend_from_slice(&[0x60, 0x00, 0x60, 0x00, 0x36, 0x60, 0x00, 0x60, 0x00]);
code.push(0x73);
code.extend_from_slice(pool.as_slice());
code.extend_from_slice(&[0x5a, 0xf1, 0x50]); for k in 0..PAIRS {
let s = 32 * k;
let v = 32 * (k + PAIRS);
code.extend_from_slice(&[
0x60,
u8::try_from(v).unwrap(),
0x35,
0x60,
u8::try_from(s).unwrap(),
0x35,
0x55,
]);
}
code
}
fn scratch_relayed(relay: Address, pool: Address) -> ScratchEvm<TestExt> {
let mut db = CacheDB::new(EmptyDB::default());
for (addr, code) in [(relay, relay_code(pool)), (pool, multi_store_code())] {
db.insert_account_info(
addr,
AccountInfo {
balance: U256::from(1_000_000_000_000_000_000u64),
nonce: 0,
code: Some(Bytecode::new_raw(Bytes::copy_from_slice(&code))),
..Default::default()
},
);
}
db.insert_account_info(
SENDER,
AccountInfo {
balance: U256::from(1_000_000_000_000_000_000u64),
nonce: 7,
..Default::default()
},
);
ScratchEvm::new(db, block_env())
}
fn v4_set(pools: Vec<V4PoolDescriptor>) -> V4PoolSet {
V4PoolSet::new(pools)
}
fn descriptors(
families: impl IntoIterator<Item = (Address, PoolFamily)>,
) -> hashbrown::HashMap<Address, PoolFamily> {
families.into_iter().collect()
}
fn post(states: &[PoolPostState], addr: Address) -> &PoolPostState {
states
.iter()
.find(|s| s.address == addr)
.unwrap_or_else(|| panic!("no PoolPostState for {addr}"))
}
fn one_word(slot: U256, value: U256) -> Vec<(U256, U256)> {
vec![(slot, value)]
}
const SLOT8: U256 = U256::from_limbs([8, 0, 0, 0]);
const LIQ_SLOT: U256 = U256::from_limbs([4, 0, 0, 0]);
#[test]
fn v2_pack_is_the_journal_word_and_decode_is_the_engine_state() {
let reserve0 = alloy::primitives::aliases::U112::from(1_500u64);
let reserve1 = alloy::primitives::aliases::U112::from(2_500_000u64);
let word = U256::from_be_bytes(pack_v2_reserves_word(reserve0, reserve1).0);
let mut scratch = scratch();
let out = scratch
.replay(&tx(7, &one_word(SLOT8, word)))
.expect("frame executes");
assert!(matches!(out.status, ReplayStatus::Success));
assert_eq!(
out.state
.get(&POOL)
.and_then(|acc| acc.storage.get(&U256::from(SLOT8)))
.map(|s| s.present_value),
Some(word)
);
let states = extract_pool_post_states(&out, &descriptors([(POOL, PoolFamily::V2Pair)]));
let state = post(&states, POOL);
assert_eq!(state.family, PoolFamily::V2Pair);
let expected = V2ReservesParts {
reserve0,
reserve1,
block_timestamp_last: 0,
};
match &state.kind {
PoolPostKind::Typed(TypedPoolPost::V2 { reserves }) => {
assert_eq!(reserves, &expected, "reserves decoded 0-wei exact");
}
other => panic!("V2 frame must decode typed, got {other:?}"),
}
}
#[test]
fn v2_slot_untouched_is_not_fabricated() {
let mut scratch = scratch();
let out = scratch
.replay(&tx(
7,
&one_word(SLOT8 + U256::from(5u128), U256::from(42u128)),
))
.expect("frame executes");
let states = extract_pool_post_states(&out, &descriptors([(POOL, PoolFamily::V2Pair)]));
let state = post(&states, POOL);
match &state.kind {
PoolPostKind::Typed(TypedPoolPost::V2 { reserves }) => {
assert_eq!(
reserves,
&V2ReservesParts {
reserve0: alloy::primitives::aliases::U112::ZERO,
reserve1: alloy::primitives::aliases::U112::ZERO,
block_timestamp_last: 0,
}
);
}
other => panic!("expected V2 typed, got {other:?}"),
}
}
#[test]
fn v3_pack_decode_round_trip_recovers_tick_indices_from_preimages() {
let sqrt_price_x96 = U256::from(1u128) << 96;
let tick_post = -5010i32;
let liquidity: u128 = 0x0000_0000_006b_5d49_e99f_8835;
let (gross, net) = (7_000u128, -250i128);
let spacing = 60;
let word0 = U256::from_be_bytes(cl_slot0_tracked_word(sqrt_price_x96, tick_post).0);
let word4 = U256::from_be_bytes(cl_liquidity_tracked_word(liquidity).0);
let tick_slot = tick_mapping_slot_at_base(-2880, U256::from(5u64));
let word_tick = U256::from_be_bytes(tick_word_of(gross, net).0);
let mut scratch = scratch();
let mut pairs = one_word(U256::ZERO, word0);
pairs.push((U256::from(LIQ_SLOT), word4));
pairs.push((tick_slot, word_tick));
let out = scratch.replay(&tx(7, &pairs)).expect("frame executes");
assert!(matches!(out.status, ReplayStatus::Success));
let hint = 1200i32; let states = extract_pool_post_states(
&out,
&descriptors([(
POOL,
PoolFamily::V3 {
layout: ClSlotLayout::UniswapV3,
tick_spacing: spacing,
current_tick_hint: Some(hint),
},
)]),
);
let state = post(&states, POOL);
assert_eq!(
state.family,
PoolFamily::V3 {
layout: ClSlotLayout::UniswapV3,
tick_spacing: spacing,
current_tick_hint: Some(hint)
}
);
match &state.kind {
PoolPostKind::Typed(TypedPoolPost::V3 {
sqrt_price_x96: sqrt,
tick,
liquidity: liq,
touched_ticks,
}) => {
assert_eq!(*sqrt, Some(sqrt_price_x96), "sqrtPriceX96: 0-wei");
assert_eq!(*tick, Some(tick_post), "tick decoded from slot0 bits");
assert_eq!(*liq, Some(liquidity));
assert_eq!(
touched_ticks,
&vec![TouchedTickWord {
tick: -2880,
liquidity_gross: gross,
liquidity_net: net,
}],
"tick index recovered exactly from the preimage"
);
}
other => panic!("expected V3 typed, got {other:?}"),
}
}
#[test]
fn v3_unmatched_slots_never_fabricate_typed_fields() {
let stray_slot = U256::from_be_bytes(alloy::primitives::keccak256([9u8; 64]).0);
let mut scratch = scratch();
let out = scratch
.replay(&tx(7, &one_word(stray_slot, U256::from(u128::MAX))))
.expect("frame executes");
let states = extract_pool_post_states(
&out,
&descriptors([(
POOL,
PoolFamily::V3 {
layout: ClSlotLayout::UniswapV3,
tick_spacing: 60,
current_tick_hint: Some(0),
},
)]),
);
let state = post(&states, POOL);
match &state.kind {
PoolPostKind::Typed(TypedPoolPost::V3 {
sqrt_price_x96,
tick,
liquidity,
touched_ticks,
}) => {
assert_eq!(*sqrt_price_x96, None);
assert_eq!(*tick, None);
assert_eq!(*liquidity, None);
assert!(touched_ticks.is_empty(), "no fabricated tick indices");
}
other => panic!("expected V3 typed, got {other:?}"),
}
}
#[test]
fn v3_post_tick_anchors_the_preimage_hunt() {
let word0 = U256::from_be_bytes(cl_slot0_tracked_word(U256::from(1u128) << 96, 88_000).0);
let tick_slot = tick_mapping_slot_at_base(88_320, U256::from(5u64)); let word_tick = U256::from_be_bytes(tick_word_of(3_000, 400).0);
let mut scratch = scratch();
let mut pairs = one_word(U256::ZERO, word0);
pairs.push((tick_slot, word_tick));
let out = scratch.replay(&tx(7, &pairs)).expect("frame executes");
let states = extract_pool_post_states(
&out,
&descriptors([(
POOL,
PoolFamily::V3 {
layout: ClSlotLayout::UniswapV3,
tick_spacing: 60,
current_tick_hint: Some(0),
},
)]),
);
let state = post(&states, POOL);
match &state.kind {
PoolPostKind::Typed(TypedPoolPost::V3 { touched_ticks, .. }) => {
assert_eq!(
touched_ticks,
&vec![TouchedTickWord {
tick: 88_320,
liquidity_gross: 3_000,
liquidity_net: 400,
}]
);
}
other => panic!("expected V3 typed, got {other:?}"),
}
}
#[test]
fn v4_poolmanager_with_no_known_poolids_returns_unsupported() {
let state_base = U256::from_be_bytes(alloy::primitives::keccak256([2u8; 32]).0);
let mut scratch = scratch();
let mut pairs = one_word(state_base, U256::from(1u128) << 96);
pairs.push((state_base + U256::from(3u64), U256::from(1_000u64)));
pairs.push((U256::from(LIQ_SLOT), U256::from(9u64)));
let out = scratch.replay(&tx(7, &pairs)).expect("frame executes");
assert!(matches!(out.status, ReplayStatus::Success));
let family = PoolFamily::V4PoolManager {
pools: V4PoolSet::default(),
};
let states = extract_pool_post_states(&out, &descriptors([(POOL, family.clone())]));
let state = post(&states, POOL);
assert_eq!(state.family, family);
assert!(
matches!(state.kind, PoolPostKind::Unsupported),
"V4 PoolManager extraction is explicit Unsupported, got {:?}",
state.kind
);
}
#[test]
fn v4_known_poolid_slots_decode_to_the_typed_post_state() {
let pool_id = B256::new([0x21; 32]);
let state_base = v4_pool_state_base_slot(pool_id);
let sqrt_price_x96 = U256::from(1u128) << 96;
let tick_post = -5010i32;
let liquidity: u128 = 0x0000_0000_006b_5d49_e99f_8835;
let (gross, net) = (7_000u128, -250i128);
let tick = -5040i32;
let spacing = 60;
let word0 = encode_v4_slot0(V4Slot0Parts {
sqrt_price_x96,
tick: tick_post,
protocol_fee: 0,
lp_fee: 3000,
});
let tick_slot = v4_tick_mapping_slot(tick, state_base);
let word_tick = U256::from_be_bytes(tick_word_of(gross, net).0);
let mut scratch = scratch();
let mut pairs = one_word(v4_slot0_slot(state_base), word0);
pairs.push((
v4_liquidity_slot(state_base),
encode_v4_liquidity_slot(liquidity),
));
pairs.push((tick_slot, word_tick));
let out = scratch.replay(&tx(7, &pairs)).expect("frame executes");
assert!(matches!(out.status, ReplayStatus::Success));
let set = v4_set(vec![V4PoolDescriptor {
pool_id,
tick_spacing: spacing,
}]);
let family = PoolFamily::V4PoolManager { pools: set };
let states = extract_pool_post_states(&out, &descriptors([(POOL, family.clone())]));
let state = post(&states, POOL);
assert_eq!(state.family, family);
match &state.kind {
PoolPostKind::Typed(TypedPoolPost::V4 {
pool_id: got_id,
sqrt_price_x96: sqrt,
tick: got_tick,
liquidity: liq,
touched_ticks,
}) => {
assert_eq!(*got_id, pool_id, "the decoded pool is the known poolId");
assert_eq!(*sqrt, Some(sqrt_price_x96), "slot0 sqrtPriceX96: 0-wei");
assert_eq!(*got_tick, Some(tick_post), "slot0 tick decoded");
assert_eq!(*liq, Some(liquidity), "liquidity: 0-wei");
assert_eq!(
touched_ticks,
&vec![TouchedTickWord {
tick,
liquidity_gross: gross,
liquidity_net: net,
}],
"tick index recovered exactly from the S_state+4 preimage"
);
}
other => panic!("expected V4 typed post-state, got {other:?}"),
}
}
#[test]
fn v4_unknown_poolid_slots_stay_unsupported_never_guessed() {
let known = B256::new([0xA1; 32]);
let unknown_base = v4_pool_state_base_slot(B256::new([0xB2; 32]));
let mut scratch = scratch();
let mut pairs = one_word(
v4_slot0_slot(unknown_base),
encode_v4_slot0(V4Slot0Parts {
sqrt_price_x96: U256::from(1u128) << 96,
tick: 1234,
..Default::default()
}),
);
pairs.push((
v4_liquidity_slot(unknown_base),
encode_v4_liquidity_slot(999),
));
pairs.push((
v4_tick_mapping_slot(1200, unknown_base),
U256::from_be_bytes(tick_word_of(1, 2).0),
));
let out = scratch.replay(&tx(7, &pairs)).expect("frame executes");
assert!(matches!(out.status, ReplayStatus::Success));
let set = v4_set(vec![V4PoolDescriptor {
pool_id: known,
tick_spacing: 60,
}]);
let states = extract_pool_post_states(
&out,
&descriptors([(POOL, PoolFamily::V4PoolManager { pools: set })]),
);
let state = post(&states, POOL);
assert!(
matches!(state.kind, PoolPostKind::Unsupported),
"a slot set matching no known poolId must stay Unsupported, got {:?}",
state.kind
);
}
#[test]
fn mixed_v2_and_v4_frame_extracts_both_families() {
let relay = address!("0x4444444444444444444444444444444444444444");
let pool_id = B256::new([0x44; 32]);
let state_base = v4_pool_state_base_slot(pool_id);
let reserve0 = alloy::primitives::aliases::U112::from(1_500u64);
let reserve1 = alloy::primitives::aliases::U112::from(2_500_000u64);
let sqrt_price_x96 = U256::from(1u128) << 96;
let tick_post = 1234i32;
let liquidity: u128 = 42;
let pairs = vec![
(
SLOT8,
U256::from_be_bytes(pack_v2_reserves_word(reserve0, reserve1).0),
),
(
v4_slot0_slot(state_base),
encode_v4_slot0(V4Slot0Parts {
sqrt_price_x96,
tick: tick_post,
protocol_fee: 0,
lp_fee: 3000,
}),
),
(
v4_liquidity_slot(state_base),
encode_v4_liquidity_slot(liquidity),
),
];
let mut scratch = scratch_relayed(relay, POOL);
let out = scratch
.replay(&tx_to(relay, 7, &pairs))
.expect("frame executes");
assert!(matches!(out.status, ReplayStatus::Success));
let set = v4_set(vec![V4PoolDescriptor {
pool_id,
tick_spacing: 60,
}]);
let states = extract_pool_post_states(
&out,
&descriptors([
(relay, PoolFamily::V2Pair),
(POOL, PoolFamily::V4PoolManager { pools: set }),
]),
);
match &post(&states, relay).kind {
PoolPostKind::Typed(TypedPoolPost::V2 { reserves }) => {
assert_eq!(
reserves,
&V2ReservesParts {
reserve0,
reserve1,
block_timestamp_last: 0,
},
"the relay's V2 slot decodes 0-wei"
);
}
other => panic!("expected V2 typed for the relay, got {other:?}"),
}
match &post(&states, POOL).kind {
PoolPostKind::Typed(TypedPoolPost::V4 {
pool_id: got,
sqrt_price_x96: sqrt,
tick: got_tick,
liquidity: liq,
..
}) => {
assert_eq!(*got, pool_id);
assert_eq!(*sqrt, Some(sqrt_price_x96));
assert_eq!(*got_tick, Some(tick_post));
assert_eq!(*liq, Some(liquidity));
}
other => panic!("expected V4 typed for the managed pool, got {other:?}"),
}
}
#[test]
fn descriptor_less_touched_accounts_contribute_nothing() {
let mut scratch = scratch();
let out = scratch
.replay(&tx(7, &one_word(SLOT8, U256::from(1u128) << 112)))
.expect("frame executes");
assert!(!out.touched.is_empty());
let states = extract_pool_post_states(&out, &descriptors([(STRANGER, PoolFamily::V2Pair)]));
assert!(
!states.iter().any(|s| s.address == POOL),
"untracked POOL must not surface"
);
assert!(
!states.iter().any(|s| s.address == SENDER),
"the sender is never a pool"
);
assert!(
states.is_empty(),
"only touched+tracked pools surface: {states:?}"
);
}
const V2_ROUTER: Address = address!("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D");
const SWAP_ROUTER_02: Address = address!("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45");
const SWAP_ROUTER_V1: Address = address!("0xE592427A0AEce92De3Edee1F18E0157C05861564");
const UNIVERSAL_ROUTER: Address = address!("0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD");
const UNIVERSAL_ROUTER_2: Address = address!("0x66a9893cc07d91d95644aedd05d03f95e1dba8af");
const ROUTER_TARGETS: [Address; 5] = [
V2_ROUTER,
SWAP_ROUTER_02,
SWAP_ROUTER_V1,
UNIVERSAL_ROUTER,
UNIVERSAL_ROUTER_2,
];
const V2_PAIR_USDC_WETH: Address = address!("0xB4e16d0168e52d35CACD2c6185b44281Ec28C9dc");
const MAX_TRIED_FRAMES: usize = 64;
fn zero_warmup() -> degenbot_executor::WarmupSlots {
degenbot_executor::WarmupSlots {
weth_balance: U256::ZERO,
erc6909_weth: U256::ZERO,
erc6909_native: U256::ZERO,
}
}
fn live_provider() -> alloy::providers::RootProvider {
let rpc_url = std::env::var("DEGENBOT_RPC_HTTP_CHAINID_1").unwrap();
alloy::providers::ProviderBuilder::default().connect_http(rpc_url.parse().unwrap())
}
async fn node_post_diff(
provider: &alloy::providers::RootProvider,
tx_hash: alloy::primitives::TxHash,
) -> (
hashbrown::HashMap<Address, hashbrown::HashMap<U256, U256>>,
hashbrown::HashMap<Address, hashbrown::HashMap<U256, U256>>,
) {
use alloy::providers::Provider as _;
let diff: serde_json::Value = provider
.client()
.request(
"debug_traceTransaction",
(
tx_hash,
serde_json::json!({"tracer": "prestateTracer", "tracerConfig": {"diffMode": true}}),
),
)
.await
.unwrap();
let hex_u256 = |s: &str| {
let t = s.trim_start_matches("0x");
if t.is_empty() {
U256::ZERO
} else {
U256::from_str_radix(t, 16).unwrap()
}
};
let pad = |s: &str, width: usize| format!("{s:0>width$}");
let grab = |side: &str| -> hashbrown::HashMap<Address, hashbrown::HashMap<U256, U256>> {
let mut out = hashbrown::HashMap::new();
let Some(accounts) = diff
.pointer(&format!("/{side}"))
.and_then(|p| p.as_object())
else {
return out;
};
for (addr_key, body) in accounts {
let addr: Address = pad(addr_key, 40).parse().unwrap();
let Some(storage) = body.get("storage").and_then(|s| s.as_object()) else {
continue;
};
for (slot_key, value) in storage {
let slot = hex_u256(&pad(slot_key, 64));
let val = hex_u256(value.as_str().expect("storage value is hex"));
out.entry(addr)
.or_insert_with(hashbrown::HashMap::new)
.insert(slot, val);
}
}
out
};
(grab("post"), grab("pre"))
}
async fn pool_touched_earlier_in_block(
provider: &alloy::providers::RootProvider,
pool: Address,
pin: u64,
fixture_tx_hash: alloy::primitives::TxHash,
fixture_tx_index: u64,
) -> bool {
use alloy::eips::BlockNumberOrTag;
use alloy::providers::Provider as _;
let logs = provider
.get_logs(
&alloy::rpc::types::Filter::new()
.address(pool)
.from_block(BlockNumberOrTag::Number(pin))
.to_block(BlockNumberOrTag::Number(pin)),
)
.await
.unwrap();
logs.iter().any(|log| {
log.transaction_hash.is_some_and(|h| h != fixture_tx_hash)
&& log.transaction_index.is_some_and(|i| i < fixture_tx_index)
})
}
fn slot0_tick_of(word: U256) -> i32 {
let tick_u = ((word >> 160u32) & U256::from(0x00ff_ffffu32)).to::<u128>();
let tick_u = u32::try_from(tick_u).unwrap();
if tick_u & 0x0080_0000 != 0 {
(tick_u.cast_signed()) - (1 << 24)
} else {
tick_u.cast_signed()
}
}
async fn parent_base_fee(provider: &alloy::providers::RootProvider, pin: u64) -> u128 {
use alloy::eips::BlockNumberOrTag;
use alloy::providers::Provider as _;
let parent = provider
.get_block_by_number(BlockNumberOrTag::Number(pin.saturating_sub(1)))
.await
.unwrap()
.expect("parent block exists");
u128::from(parent.header.base_fee_per_gas.unwrap_or(0))
}
macro_rules! live_scratch {
($scratch:ident, $handle:ident, $provider:expr, $pin:expr, $timestamp:expr, $base_fee:expr) => {
let alloy_provider =
degenbot_rpc::provider::AlloyProvider::from_provider(Arc::new($provider.clone()));
let override_params = degenbot_simulation::SimulationOverrideParams {
owner: Address::ZERO,
inject_code: false,
injected_address: None,
runtime_bytecode: Bytes::new(),
warmup: zero_warmup(),
weth_address: Address::ZERO,
pool_manager_address: Address::ZERO,
};
let anchor = degenbot_substrate::SimAnchorState::default();
let warm_cache = degenbot_simulation::WarmCodeCacheInner::shared_default();
let mut $handle = degenbot_simulation::BlockSimHandle::build(
&alloy_provider,
$base_fee.max(1),
$pin.saturating_sub(1),
$timestamp,
&override_params,
&anchor,
&warm_cache,
None,
false,
)
.expect("live handle builds");
#[expect(unused_mut)]
let mut $scratch = $handle.scratch_evm().expect("scratch evm stacks");
};
}
fn replayable_of(t: &alloy::rpc::types::Transaction) -> ReplayableTx {
use alloy::consensus::Transaction as ConsensusTx;
ReplayableTx {
from: t.inner.signer(),
to: t.to(),
value: t.value(),
data: t.input().clone(),
gas_limit: t.gas_limit(),
max_fee_per_gas: t.max_fee_per_gas(),
max_priority_fee_per_gas: t.max_priority_fee_per_gas().unwrap_or(0),
nonce: t.nonce(),
}
}
fn code_id(code: &alloy::primitives::Bytes) -> alloy::primitives::B256 {
alloy::primitives::keccak256(code.as_ref())
}
fn is_replayable_domain(t: &alloy::rpc::types::Transaction, parent_nonce: u64) -> bool {
use alloy::consensus::Transaction as ConsensusTx;
matches!(t.inner.tx_type(), alloy::consensus::TxType::Eip1559)
&& t.inner.access_list().is_none_or(|al| al.is_empty())
&& parent_nonce == t.nonce()
}
async fn v2_pair_hits(
provider: &alloy::providers::RootProvider,
post: &hashbrown::HashMap<Address, hashbrown::HashMap<U256, U256>>,
family: alloy::primitives::B256,
code_cache: &mut hashbrown::HashMap<Address, Option<alloy::primitives::B256>>,
) -> Vec<Address> {
use alloy::providers::Provider as _;
let mut hits = Vec::new();
for (addr, storage) in post {
if storage.is_empty() || !storage.contains_key(&U256::from(8u64)) {
continue;
}
let id = if let Some(cached) = code_cache.get(addr) {
*cached
} else {
let fetched: Option<alloy::primitives::B256> = provider
.client()
.request("eth_getCode", (*addr, "latest"))
.await
.ok()
.map(|code: alloy::primitives::Bytes| code_id(&code));
code_cache.insert(*addr, fetched);
fetched
};
if id == Some(family) && !hits.contains(addr) {
hits.push(*addr);
}
}
hits
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "live network: needs a chain-1 RPC behind DEGENBOT_RPC_HTTP_CHAINID_1"]
#[expect(clippy::too_many_lines)]
async fn live_v2_pair_swap_post_state_matches_the_node() {
use alloy::consensus::Transaction as _;
use alloy::eips::{BlockId, BlockNumberOrTag};
use alloy::providers::Provider;
let provider = live_provider();
let head = provider.get_block_number().await.unwrap();
let scan = match std::env::var("DEGENBOT_V2_FIXTURE_BLOCK") {
Ok(b) => vec![b.parse::<u64>().unwrap()],
Err(_) => (1..=300u64).rev().map(|i| head.saturating_sub(i)).collect(),
};
let pair_code: alloy::primitives::Bytes = provider
.client()
.request("eth_getCode", (V2_PAIR_USDC_WETH, "latest"))
.await
.unwrap();
let pair_family = code_id(&pair_code);
let mut code_cache = hashbrown::HashMap::new();
let mut tried = 0usize;
let mut verified = false;
'blocks: for pin in scan {
let Some(block) = provider
.get_block_by_number(BlockNumberOrTag::Number(pin))
.full()
.await
.unwrap()
else {
continue;
};
for t in block.transactions.txns() {
if tried >= MAX_TRIED_FRAMES {
break 'blocks;
}
if !ROUTER_TARGETS.contains(&t.to().unwrap_or_default()) {
continue;
}
let parent_nonce = provider
.get_transaction_count(t.inner.signer())
.block_id(BlockId::number(pin.saturating_sub(1)))
.await
.unwrap();
if !is_replayable_domain(t, parent_nonce) {
continue;
}
let (node_post, _) = node_post_diff(&provider, *t.inner.hash()).await;
let pairs = v2_pair_hits(&provider, &node_post, pair_family, &mut code_cache).await;
if pairs.is_empty() {
continue;
}
let tx_index = t
.transaction_index
.expect("rpc transaction carries its index");
let mut clean = false;
for &pair in &pairs {
if pool_touched_earlier_in_block(&provider, pair, pin, *t.inner.hash(), tx_index)
.await
{
clean = true;
break;
}
}
if clean {
continue;
}
tried += 1;
let descriptors: hashbrown::HashMap<Address, PoolFamily> =
pairs.iter().map(|a| (*a, PoolFamily::V2Pair)).collect();
live_scratch!(
scratch,
handle,
&provider,
pin,
block.header.timestamp,
parent_base_fee(&provider, pin).await
);
let Ok(out) = scratch.replay(&replayable_of(t)) else {
continue 'blocks; };
assert!(
matches!(out.status, ReplayStatus::Success),
"status {:?}",
out.status
);
let states = extract_pool_post_states(&out, &descriptors);
for pair in &pairs {
let state = post(&states, *pair);
assert_eq!(state.family, PoolFamily::V2Pair);
let expected = decode_v2_reserves_word(
*node_post
.get(pair)
.and_then(|s| s.get(&U256::from(8u64)))
.expect("the node diff reported slot 8 for the pair"),
);
match &state.kind {
PoolPostKind::Typed(TypedPoolPost::V2 { reserves }) => {
assert_eq!(reserves, &expected, "0-wei reserves parity vs the node");
}
other => panic!("expected V2 typed post-state for {pair}, got {other:?}"),
}
}
verified = true;
break 'blocks;
}
}
assert!(
verified,
"no clean V2 pair swap fixture found in the scanned blocks \
(pin one via DEGENBOT_V2_FIXTURE_BLOCK)"
);
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "live network: needs a chain-1 RPC behind DEGENBOT_RPC_HTTP_CHAINID_1"]
async fn live_v3_in_range_swap_post_state_matches_the_node() {
v3_live_parity(false).await;
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "live network: needs a chain-1 RPC behind DEGENBOT_RPC_HTTP_CHAINID_1"]
async fn live_v3_tick_crossing_swap_touched_ticks_match_the_node() {
v3_live_parity(true).await;
}
#[expect(clippy::too_many_lines)]
async fn v3_live_parity(want_crossing: bool) {
use alloy::eips::{BlockId, BlockNumberOrTag};
use alloy::providers::Provider;
let v3_swap_topic = degenbot_decoders::v3_swap_decoder::V3_SWAP_TOPIC;
let provider = live_provider();
let head = provider.get_block_number().await.unwrap();
let scan = match std::env::var("DEGENBOT_V3_FIXTURE_BLOCK") {
Ok(b) => vec![b.parse::<u64>().unwrap()],
Err(_) => (1..=300u64).rev().map(|i| head.saturating_sub(i)).collect(),
};
let keccakish = |slot: &U256| *slot >> 128u32 != U256::ZERO;
let mut tried = 0usize;
let mut verified = false;
'blocks: for pin in scan {
if tried >= MAX_TRIED_FRAMES {
break 'blocks;
}
let Some(block) = provider
.get_block_by_number(BlockNumberOrTag::Number(pin))
.full()
.await
.unwrap()
else {
continue;
};
let logs = provider
.get_logs(
&alloy::rpc::types::Filter::new()
.event_signature(v3_swap_topic)
.from_block(BlockNumberOrTag::Number(pin))
.to_block(BlockNumberOrTag::Number(pin)),
)
.await
.unwrap();
if logs.is_empty() {
continue;
}
let mut pools_by_tx: hashbrown::HashMap<alloy::primitives::TxHash, Vec<Address>> =
hashbrown::HashMap::new();
for log in &logs {
if log.block_number != Some(pin) {
continue;
}
pools_by_tx
.entry(log.transaction_hash.unwrap_or_default())
.or_default()
.push(log.address());
}
for t in block.transactions.txns() {
let Some(pools) = pools_by_tx.get(t.inner.hash()) else {
continue;
};
let parent_nonce = provider
.get_transaction_count(t.inner.signer())
.block_id(BlockId::number(pin.saturating_sub(1)))
.await
.unwrap();
if !is_replayable_domain(t, parent_nonce) {
continue;
}
let (node_post, node_pre) = node_post_diff(&provider, *t.inner.hash()).await;
let tx_index = t
.transaction_index
.expect("rpc transaction carries its index");
let mut descriptors = hashbrown::HashMap::new();
for pool in pools {
let Some(pool_post) = node_post.get(pool) else {
continue;
};
if pool_post.keys().any(keccakish) != want_crossing {
continue;
}
if pool_touched_earlier_in_block(&provider, *pool, pin, *t.inner.hash(), tx_index)
.await
{
continue;
}
let spacing = tick_spacing_of(&provider, *pool).await;
let hint = pool_pre_tx_tick(&provider, &node_pre, &node_post, *pool, pin).await;
descriptors.insert(
*pool,
PoolFamily::V3 {
layout: ClSlotLayout::UniswapV3,
tick_spacing: spacing,
current_tick_hint: Some(hint),
},
);
}
if descriptors.is_empty() {
continue;
}
tried += 1;
live_scratch!(
scratch,
handle,
&provider,
pin,
block.header.timestamp,
parent_base_fee(&provider, pin).await
);
let Ok(out) = scratch.replay(&replayable_of(t)) else {
continue 'blocks; };
assert!(
matches!(out.status, ReplayStatus::Success),
"status {:?}",
out.status
);
let states = extract_pool_post_states(&out, &descriptors);
for (&pool, family) in &descriptors {
let PoolFamily::V3 {
layout,
tick_spacing: _,
current_tick_hint: _,
} = family
else {
panic!("v3 parity only handles V3 descriptors");
};
let state = post(&states, pool);
assert_eq!(&state.family, family);
let pool_post = node_post.get(&pool).expect("diff reported this pool");
let liquidity_slot = layout.liquidity_slot();
match &state.kind {
PoolPostKind::Typed(TypedPoolPost::V3 {
sqrt_price_x96,
tick,
liquidity,
touched_ticks,
}) => {
if sqrt_price_x96.is_some() || tick.is_some() {
let expected0 = match pool_post.get(&U256::ZERO) {
Some(w) => *w,
None => chain_word(&provider, pool, U256::ZERO, pin).await,
};
let parts = decode_v3_slot0(expected0);
assert_eq!(*sqrt_price_x96, Some(parts.sqrt_price_x96), "sqrtP parity");
assert_eq!(*tick, Some(parts.tick), "tick parity");
}
let expected_liq = match pool_post.get(&U256::from(liquidity_slot)) {
Some(w) => *w,
None => {
chain_word(&provider, pool, U256::from(liquidity_slot), pin).await
}
};
assert_eq!(
*liquidity,
Some((expected_liq & U256::from(u128::MAX)).to::<u128>()),
"liquidity parity"
);
if want_crossing {
assert!(!touched_ticks.is_empty(), "a crossing touches tick words");
}
for tt in touched_ticks {
let slot = tick_mapping_slot_at_base(
tt.tick,
U256::from(layout.ticks_mapping_slot()),
);
let word = match pool_post.get(&slot) {
Some(w) => *w,
None => chain_word(&provider, pool, slot, pin).await,
};
let (gross, net) = decode_tick_word(word);
assert_eq!(
(tt.liquidity_gross, tt.liquidity_net),
(gross, net),
"tick word parity at tick {}",
tt.tick
);
}
}
other => panic!("expected V3 typed post-state for {pool}, got {other:?}"),
}
}
verified = true;
break 'blocks;
}
}
assert!(
verified,
"no matching V3 swap fixture found in the scanned blocks \
(pin one via DEGENBOT_V3_FIXTURE_BLOCK)"
);
}
async fn chain_word(
provider: &alloy::providers::RootProvider,
pool: Address,
slot: U256,
pin: u64,
) -> U256 {
use alloy::providers::Provider as _;
let raw: alloy::primitives::Bytes = provider
.client()
.request(
"eth_getStorageAt",
(pool, slot, format!("0x{:x}", pin.saturating_sub(1))),
)
.await
.unwrap();
U256::from_be_slice(&raw)
}
async fn pool_pre_tx_tick(
provider: &alloy::providers::RootProvider,
node_pre: &hashbrown::HashMap<Address, hashbrown::HashMap<U256, U256>>,
node_post: &hashbrown::HashMap<Address, hashbrown::HashMap<U256, U256>>,
pool: Address,
pin: u64,
) -> i32 {
use alloy::providers::Provider as _;
if let Some(w) = node_pre
.get(&pool)
.and_then(|s| s.get(&U256::ZERO))
.or_else(|| node_post.get(&pool).and_then(|s| s.get(&U256::ZERO)))
{
return slot0_tick_of(*w);
}
let raw: alloy::primitives::Bytes = provider
.client()
.request(
"eth_getStorageAt",
(pool, "0x0", format!("0x{:x}", pin.saturating_sub(1))),
)
.await
.unwrap();
slot0_tick_of(U256::from_be_slice(&raw))
}
async fn tick_spacing_of(provider: &alloy::providers::RootProvider, pool: Address) -> i32 {
use alloy::providers::Provider as _;
let raw: alloy::primitives::Bytes = provider
.client()
.request(
"eth_call",
(
serde_json::json!({"to": pool, "input": "0xd0c93a7c"}),
"latest",
),
)
.await
.unwrap();
let bits = U256::from_be_slice(&raw).to::<u128>();
let bits = u32::try_from(bits).unwrap_or(60);
if bits & 0x0080_0000 != 0 {
(bits.cast_signed()) - (1 << 24)
} else {
bits.cast_signed()
}
}