#![cfg_attr(
test,
allow(clippy::unreadable_literal, clippy::decimal_bitwise_operands)
)]
use std::sync::{Mutex, OnceLock};
use alloy::primitives::{Address, B256, U256};
use degenbot_bot::bot_core::{divergence_probe::TrackedSlotProbe, SimAnchorState};
pub const SIM_DIVERGENCE_LOG_ENV: &str = "DEGENBOT_SIM_DIVERGENCE_LOG";
const SIM_DIVERGENCE_LOG_PREFIX: &str = "[sim-divergence]";
static PROBE_ENABLED: OnceLock<bool> = OnceLock::new();
#[cfg(test)]
static TEST_FORCE: std::sync::atomic::AtomicI8 = std::sync::atomic::AtomicI8::new(-1);
fn probe_enabled() -> bool {
#[cfg(test)]
{
let forced = TEST_FORCE.load(std::sync::atomic::Ordering::Acquire);
if forced != -1 {
return forced != 0;
}
}
*PROBE_ENABLED
.get_or_init(|| std::env::var_os(SIM_DIVERGENCE_LOG_ENV).is_some_and(|v| v == "1"))
}
#[cfg(test)]
pub fn force_probe_enabled_for_tests(on: Option<bool>) {
match on {
Some(true) => TEST_FORCE.store(1, std::sync::atomic::Ordering::Release),
Some(false) => TEST_FORCE.store(0, std::sync::atomic::Ordering::Release),
None => TEST_FORCE.store(-1, std::sync::atomic::Ordering::Release),
}
}
#[derive(Debug, Default, Clone)]
pub struct DivergenceTally {
pub slots_compared: u64,
pub divergent_slots: u64,
pub divergent_pairs: u64,
pub divergent_pools: u64,
}
static TALLY: OnceLock<Mutex<DivergenceTallyAccum>> = OnceLock::new();
#[derive(Debug, Default)]
struct DivergenceTallyAccum {
slots_compared: u64,
divergent_slots: u64,
divergent_pairs: std::collections::HashSet<(Address, B256)>,
divergent_pools: std::collections::HashSet<Address>,
}
impl DivergenceTallyAccum {
fn to_tally(&self) -> DivergenceTally {
DivergenceTally {
slots_compared: self.slots_compared,
divergent_slots: self.divergent_slots,
divergent_pairs: self.divergent_pairs.len() as u64,
divergent_pools: self.divergent_pools.len() as u64,
}
}
}
fn tally() -> &'static Mutex<DivergenceTallyAccum> {
TALLY.get_or_init(|| Mutex::new(DivergenceTallyAccum::default()))
}
pub fn reset_divergence_tally() {
if let Some(m) = TALLY.get() {
if let Ok(mut acc) = m.lock() {
*acc = DivergenceTallyAccum::default();
}
}
}
#[must_use]
pub fn divergence_tally_snapshot() -> DivergenceTally {
tally().lock().map(|acc| acc.to_tally()).unwrap_or_default()
}
fn tracked_fields_match(probe: &TrackedSlotProbe, rpc_word: U256) -> bool {
let mask = U256::from_be_bytes(probe.kind.tracked_bit_mask().0);
let engine = U256::from_be_bytes(probe.engine_word.0);
engine == (rpc_word & mask)
}
pub fn observe_storage_read(
anchor: &SimAnchorState,
address: Address,
index: U256,
rpc_value: U256,
) {
if !probe_enabled() {
return;
}
let Some(probe) = anchor.probe_tracked_storage_slot(address, index) else {
return;
};
let matched = tracked_fields_match(&probe, rpc_value);
record_observation(address, index, &probe, matched);
if matched {
return;
}
tracing::info!(
pool_addr = %format!("{address:?}"),
slot = %index,
kind = ?probe.kind,
engine = %hex_padded(probe.engine_word),
rpc = %hex_padded_u256(rpc_value),
update_block = probe.update_block,
"{SIM_DIVERGENCE_LOG_PREFIX}"
);
}
fn record_observation(address: Address, index: U256, _probe: &TrackedSlotProbe, matched: bool) {
let Ok(mut acc) = tally().lock() else { return };
acc.slots_compared += 1;
if !matched {
acc.divergent_slots += 1;
let slot_bytes: [u8; 32] = index.to_be_bytes();
acc.divergent_pairs
.insert((address, B256::from(slot_bytes)));
acc.divergent_pools.insert(address);
}
}
fn hex_padded(word: B256) -> String {
use alloy::hex;
hex::encode(word.0)
}
fn hex_padded_u256(word: U256) -> String {
use alloy::hex;
hex::encode(word.to_be_bytes::<32>())
}
pub fn dump_divergence_summary() {
if !probe_enabled() {
return;
}
let tally = divergence_tally_snapshot();
tracing::info!(
slots_compared = tally.slots_compared,
divergent_slots = tally.divergent_slots,
divergent_pairs = tally.divergent_pairs,
divergent_pools = tally.divergent_pools,
"{SIM_DIVERGENCE_LOG_PREFIX} summary"
);
}
#[expect(clippy::unwrap_used, clippy::expect_used, clippy::cast_sign_loss)]
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::evm::BotStateDb;
use alloy::primitives::{address, Address, B256, U256};
use degenbot_bot::bot_core::{
divergence_probe::TrackedSlotProbe, BotState, RegisterV3PoolParams, TrackedSlotKind,
};
use revm::database_interface::DatabaseRef;
use revm::primitives::{StorageKey, StorageValue, B256 as RevmB256};
use revm::state::AccountInfo;
const V3_ADDR: Address = address!("888888875ce34e0b60a4a79bb5bc5d34b7e5fab4");
#[derive(Default)]
struct FixedStorageDb {
slots: std::collections::HashMap<(Address, U256), U256>,
}
impl DatabaseRef for FixedStorageDb {
type Error = std::convert::Infallible;
fn basic_ref(&self, _address: Address) -> Result<Option<AccountInfo>, Self::Error> {
Ok(None)
}
fn storage_ref(
&self,
address: Address,
index: StorageKey,
) -> Result<StorageValue, Self::Error> {
Ok(self
.slots
.get(&(address, index))
.copied()
.unwrap_or(StorageValue::ZERO))
}
fn code_by_hash_ref(
&self,
_code_hash: RevmB256,
) -> Result<revm::bytecode::Bytecode, Self::Error> {
Ok(revm::bytecode::Bytecode::default())
}
fn block_hash_ref(&self, _number: u64) -> Result<RevmB256, Self::Error> {
Ok(RevmB256::ZERO)
}
}
fn v3_pool(sqrt: U256, liquidity: u128, tick: i32, update_block: u64) -> BotState {
let mut core = BotState::new();
let params = RegisterV3PoolParams {
address: V3_ADDR,
token0: Address::ZERO,
token1: Address::from([0xa0; 20]),
fee: 3000,
tick_spacing: 60,
factory: Address::ZERO,
sqrt_price_x96: sqrt,
liquidity,
tick,
tick_data: std::collections::HashMap::new(),
update_block,
coverage: degenbot_bot::solvers::arb_engine::PoolTickCoverage::Sparse,
fetcher: None,
..Default::default()
};
core.register_v3_pool(¶ms).expect("V3 registration");
core
}
fn rpc_slot0_with_tick(sqrt: U256, tick: i32) -> U256 {
let sqrt_masked = sqrt & U256::from_limbs([u64::MAX, u64::MAX, 0xffff_ffff, 0]);
let tick_u = (tick as u32) & 0x00ff_ffff;
sqrt_masked | (U256::from(tick_u) << 160u32)
}
#[test]
fn tracked_fields_match_when_masked_fields_equal() {
let sqrt = U256::from(1u128) << 96;
let engine_tick_u = (-5010i32 as u32) & 0x00ff_ffff;
let engine_word: U256 = (sqrt & U256::from_limbs([u64::MAX, u64::MAX, 0xffff_ffff, 0]))
| (U256::from(engine_tick_u) << 160u32);
let probe = TrackedSlotProbe {
kind: TrackedSlotKind::V3Slot0,
engine_word: B256::from(engine_word.to_be_bytes::<32>()),
update_block: 0,
};
let rpc = engine_word | (U256::from(0xdead_beefu64) << 184u32);
assert!(
tracked_fields_match(&probe, rpc),
"tracked fields match when sqrtPrice+tick agree (high garbage masked out)"
);
}
#[test]
fn tracked_fields_detect_tick_divergence() {
let sqrt = U256::from(1u128) << 96;
let engine_tick_u = (-5010i32 as u32) & 0x00ff_ffff;
let probe = TrackedSlotProbe {
kind: TrackedSlotKind::V3Slot0,
engine_word: B256::from(
({
let w: U256 = (sqrt & U256::from_limbs([u64::MAX, u64::MAX, 0xffff_ffff, 0]))
| (U256::from(engine_tick_u) << 160u32);
w
})
.to_be_bytes::<32>(),
),
update_block: 0,
};
let rpc = (sqrt & U256::from_limbs([u64::MAX, u64::MAX, 0xffff_ffff, 0]))
| (U256::from(5010u32 & 0xffffff) << 160u32);
assert!(
!tracked_fields_match(&probe, rpc),
"tick divergence (engine -5010 vs rpc +5010) is flagged"
);
}
#[test]
fn storage_ref_returns_rpc_value_unchanged_when_probe_on_or_off() {
let core = v3_pool(U256::from(1u128) << 96, 1_000_000, -5010, 18_000_000);
let anchor = SimAnchorState::snapshot(&core);
let rpc_word = rpc_slot0_with_tick(U256::from(1u128) << 96, 5010);
let mut db = FixedStorageDb::default();
db.slots.insert((V3_ADDR, U256::ZERO), rpc_word);
let bot_db = BotStateDb::new(&anchor, db);
let got = bot_db
.storage_ref(V3_ADDR, U256::ZERO)
.expect("storage_ref ok");
assert_eq!(
got, rpc_word,
"rpc value returned unchanged (probe off path)"
);
}
static TALLY_TEST_GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn observe_logs_divergence_when_engine_lags_rpc_and_tally_accumulates() {
let _g = TALLY_TEST_GUARD.lock().unwrap();
reset_divergence_tally();
let core = v3_pool(U256::from(1u128) << 96, 1_000_000, -5010, 18_000_000);
let anchor = SimAnchorState::snapshot(&core);
let rpc_word = rpc_slot0_with_tick(U256::from(1u128) << 96, 5010);
force_probe_enabled_for_tests(Some(true));
observe_storage_read(&anchor, V3_ADDR, U256::ZERO, rpc_word);
let tally = divergence_tally_snapshot();
assert_eq!(tally.slots_compared, 1, "one tracked slot compared");
assert_eq!(tally.divergent_slots, 1, "the tick diverged");
assert_eq!(tally.divergent_pools, 1, "one distinct pool");
force_probe_enabled_for_tests(None);
}
#[test]
fn observe_never_complains_when_engine_matches_rpc() {
let _g = TALLY_TEST_GUARD.lock().unwrap();
reset_divergence_tally();
let sqrt = U256::from(1u128) << 96;
let core = v3_pool(sqrt, 1_000_000, -5010, 18_000_000);
let anchor = SimAnchorState::snapshot(&core);
let rpc_word = rpc_slot0_with_tick(sqrt, -5010);
force_probe_enabled_for_tests(Some(true));
observe_storage_read(&anchor, V3_ADDR, U256::ZERO, rpc_word);
let tally = divergence_tally_snapshot();
assert_eq!(tally.slots_compared, 1, "compared once");
assert_eq!(tally.divergent_slots, 0, "matched → not flagged");
force_probe_enabled_for_tests(None);
}
#[test]
fn observe_ignores_untracked_slot() {
let _g = TALLY_TEST_GUARD.lock().unwrap();
reset_divergence_tally();
let core = v3_pool(U256::from(1u128) << 96, 1_000_000, 0, 18_000_000);
let anchor = SimAnchorState::snapshot(&core);
force_probe_enabled_for_tests(Some(true));
observe_storage_read(
&anchor,
V3_ADDR,
U256::from(1u64),
U256::from(0xdeadbeefu64),
);
let tally = divergence_tally_snapshot();
assert_eq!(tally.slots_compared, 0, "untracked slot never compared");
force_probe_enabled_for_tests(None);
}
}