#![expect(
clippy::cast_possible_wrap,
clippy::doc_markdown,
clippy::missing_panics_doc,
clippy::must_use_candidate,
clippy::expect_used,
clippy::panic
)]
use std::path::PathBuf;
use hashbrown::{HashMap, HashSet};
use alloy::primitives::{aliases::I256, Address, Bytes, U160, U256};
use degenbot_pools::v4_state::{V4PoolKey, V4PoolState};
use degenbot_pools::v4_storage_slots::{
encode_v4_liquidity_slot, encode_v4_slot0, encode_v4_tick_info_slot, v4_liquidity_slot,
v4_pool_id, v4_pool_state_base_slot, v4_slot0_slot, v4_tick_bitmap_word_slot,
v4_tick_mapping_slot, V4Slot0Parts,
};
use degenbot_simulation::oracle::{self, TxSpec, Verdict};
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../../../")
.canonicalize()
.expect("canonicalize repo root")
}
#[expect(clippy::missing_panics_doc)]
pub fn load_creation_bytecode(file: &str, contract: &str) -> Bytes {
let artifact_path = repo_root()
.join("tier3-oracle")
.join("artifacts")
.join(file)
.join(format!("{contract}.json"));
let raw = std::fs::read_to_string(&artifact_path)
.unwrap_or_else(|_| panic!("missing artifact {}", artifact_path.display()));
let artifact = serde_json::from_str::<serde_json::Value>(&raw).expect("valid artifact json");
let code = artifact["bytecode"]["object"]
.as_str()
.expect("creation bytecode object");
Bytes::from(alloy::hex::decode(code).expect("hex object"))
}
pub fn harness_constructor_args(fee: u32, tick_spacing: i32) -> Vec<u8> {
let mut args = vec![0u8; 64];
args[28..32].copy_from_slice(&fee.to_be_bytes());
args[60..64].copy_from_slice(&tick_spacing.to_be_bytes());
args
}
fn compute_v4_word_from_raw(
tick_data: &HashMap<i32, degenbot_pools::TickInfo>,
tick_spacing: i32,
word_pos: i16,
) -> U256 {
degenbot_pools::v3_storage_slots::compute_v3_tick_bitmap_word_from_raw(
tick_data,
tick_spacing,
word_pos,
)
}
pub fn seed_v4_pool_storage(
evm: &mut oracle::FixtureEvm,
manager: Address,
pool_key: &V4PoolKey,
state: &V4PoolState,
fee: u32,
) {
let pool_id = v4_pool_id(pool_key);
let base = v4_pool_state_base_slot(pool_id);
let mut slots = Vec::new();
slots.push((
v4_slot0_slot(base),
encode_v4_slot0(V4Slot0Parts {
sqrt_price_x96: state.sqrt_price_x96,
tick: state.tick,
protocol_fee: state.protocol_fee,
lp_fee: fee,
}),
));
slots.push((
v4_liquidity_slot(base),
encode_v4_liquidity_slot(state.liquidity),
));
for (tick, info) in &state.tick_data {
slots.push((
v4_tick_mapping_slot(*tick, base),
encode_v4_tick_info_slot(info),
));
}
let mut word_positions: HashSet<i16> = HashSet::new();
for &tick in state.tick_data.keys() {
let compressed = tick.div_euclid(pool_key.tick_spacing);
let word_pos = i16::try_from(compressed >> 8).unwrap_or(0);
word_positions.insert(word_pos);
}
for word_pos in word_positions {
slots.push((
v4_tick_bitmap_word_slot(word_pos, base),
compute_v4_word_from_raw(&state.tick_data, pool_key.tick_spacing, word_pos),
));
}
oracle::seed_slots(evm, manager, &slots);
}
pub fn encode_v4_swap_call(
zero_for_one: bool,
amount_specified: I256,
sqrt_price_limit: U160,
) -> Vec<u8> {
let mut data = oracle::selector("swap(bool,int256,uint160)").to_vec();
let mut buf = vec![0u8; 32];
buf[31] = u8::from(zero_for_one);
data.extend_from_slice(&buf);
data.extend_from_slice(&amount_specified.into_raw().to_be_bytes::<32>());
let mut lim = [0u8; 32];
lim[12..32].copy_from_slice(&sqrt_price_limit.to_be_bytes::<20>());
data.extend_from_slice(&lim);
data
}
pub fn decode_balance_delta(out: &[u8]) -> (U256, U256) {
let mut w32 = [0u8; 32];
w32.copy_from_slice(&out[0..32]);
let packed = U256::from_be_bytes(w32);
let low_mask = U256::from_limbs([u64::MAX, u64::MAX, 0, 0]);
let hi_u128: u128 = ((packed >> 128u32) & low_mask).to::<u128>();
let lo_u128: u128 = (packed & low_mask).to::<u128>();
let i128_to_u256 = |v: i128| -> U256 {
let be = v.to_be_bytes(); let mut arr = [0u8; 32];
arr[0..16].fill(if v < 0 { 0xFF } else { 0x00 });
arr[16..32].copy_from_slice(&be);
U256::from_be_bytes(arr)
};
(
I256::from_raw(i128_to_u256(hi_u128 as i128)).unsigned_abs(),
I256::from_raw(i128_to_u256(lo_u128 as i128)).unsigned_abs(),
)
}
pub struct V4RealSwap {
pub verdict: Verdict,
pub delta: (U256, U256),
pub gas_used: u64,
}
pub fn drive_real_v4_swap(
state: &V4PoolState,
fee: u32,
tick_spacing: i32,
zero_for_one: bool,
amount_specified: I256,
sqrt_price_limit: U160,
gas: u64,
) -> V4RealSwap {
let mut evm = oracle::new_fixture_evm();
oracle::set_disable_nonce_check(&mut evm, true);
oracle::set_code_size_limits(&mut evm, None);
oracle::set_block_gas_limit(&mut evm, std::cmp::max(gas, 30_000_000));
oracle::set_tx_gas_limit_cap(&mut evm, u64::MAX);
let mut init_code =
load_creation_bytecode("V4SwapOracleHarness.sol", "V4SwapOracleHarness").to_vec();
init_code.extend_from_slice(&harness_constructor_args(fee, tick_spacing));
let harness = match oracle::deploy(&mut evm, Bytes::from(init_code), 16_000_000) {
Ok(a) => a,
Err(e) => {
return V4RealSwap {
verdict: Verdict::Halted(format!("harness deploy failed: {e}")),
delta: (U256::ZERO, U256::ZERO),
gas_used: 0,
}
}
};
let cur0 = oracle::read_address(
&mut evm,
harness,
Bytes::from(oracle::selector("currency0()").to_vec()),
2_000_000,
)
.expect("read currency0");
let cur1 = oracle::read_address(
&mut evm,
harness,
Bytes::from(oracle::selector("currency1()").to_vec()),
2_000_000,
)
.expect("read currency1");
let manager = oracle::read_address(
&mut evm,
harness,
Bytes::from(oracle::selector("manager()").to_vec()),
2_000_000,
)
.expect("read manager");
let pool_key = V4PoolKey {
currency0: cur0,
currency1: cur1,
fee,
tick_spacing,
hooks: Address::ZERO,
};
seed_v4_pool_storage(&mut evm, manager, &pool_key, state, fee);
let data = Bytes::from(encode_v4_swap_call(
zero_for_one,
amount_specified,
sqrt_price_limit,
));
let (verdict, gas_used) = oracle::transact_with_gas(
&mut evm,
TxSpec::Call {
to: harness,
data,
gas,
},
);
let delta = match &verdict {
Verdict::Accepted {
output: oracle::Output::Call(b),
..
} => decode_balance_delta(b.as_ref()),
_ => (U256::ZERO, U256::ZERO),
};
V4RealSwap {
verdict,
delta,
gas_used,
}
}