use std::collections::HashMap;
use alloy::primitives::{Address, U256};
pub const SENTINEL_PM: u8 = 0xFC;
pub const SENTINEL_SELF: u8 = 0xFD;
pub const SENTINEL_WETH: u8 = 0xFE;
pub const SENTINEL_NATIVE: u8 = 0xFF;
pub const SENTINEL_THRESHOLD: u8 = 0xFC;
pub const MAX_INDEXED_ADDRESSES: usize = 32;
pub const NATIVE_ADDRESS: Address = Address::ZERO;
pub const V4_FEE_ENCODER_MAX: u32 = 0x1_0000;
pub const CMD_SET_ADDRESS: u8 = 0x00;
pub const CMD_ERC20_TRANSFER: u8 = 0x10;
pub const CMD_ERC20_XFER_BALANCE: u8 = 0x11;
pub const CMD_WETH_DEPOSIT: u8 = 0x12;
pub const CMD_WETH_WITHDRAW: u8 = 0x13;
pub const CMD_WETH_DEPOSIT_ALL: u8 = 0x14;
pub const CMD_WETH_WITHDRAW_ALL: u8 = 0x15;
pub const CMD_SEND_ETH: u8 = 0x16;
pub const CMD_SEND_ETH_ALL: u8 = 0x17;
pub const CMD_V2_SWAP_COMPACT: u8 = 0x20;
pub const CMD_V2_SWAP_CALC: u8 = 0x21;
pub const CMD_V2_SWAP_DIRECT: u8 = 0x22;
pub const CMD_V3_SWAP_COMPACT: u8 = 0x30;
pub const CMD_V3_SWAP_DELTA: u8 = 0x31;
pub const CMD_V4_SWAP_COMPACT: u8 = 0x40;
pub const CMD_V4_SWAP_DYNAMIC: u8 = 0x41;
pub const CMD_V4_BATCH: u8 = 0x42;
pub const CMD_V4_BATCH_OPEN_WETH: u8 = 0x43;
pub const CMD_V4_UNLOCK: u8 = 0x50;
pub const CMD_V4_TAKE: u8 = 0x51;
pub const CMD_V4_TAKE_COMPACT: u8 = 0x52;
pub const CMD_V4_TAKE_DELTA: u8 = 0x53;
pub const CMD_V4_SYNC: u8 = 0x54;
pub const CMD_V4_SETTLE: u8 = 0x55;
pub const CMD_V4_SETTLE_DELTA: u8 = 0x56;
pub const CMD_V4_SETTLE_ALL: u8 = 0x57;
pub const CMD_V4_MINT_COMPACT: u8 = 0x58;
pub const CMD_V4_BURN_COMPACT: u8 = 0x59;
pub const BEGIN_EXECUTION: u8 = 0xFF;
const UINT96_BOUND: u128 = 1u128 << 96;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EncoderError {
Uint96Overflow(u128),
ForwardDataTooLong(usize),
AddressTableFull,
TooManyV4BatchSwaps(usize),
}
impl std::fmt::Display for EncoderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Uint96Overflow(v) => write!(f, "uint96 amount {v} overflows the 12-byte field"),
Self::ForwardDataTooLong(n) => {
write!(f, "forward_data length {n} exceeds the 255-byte cap")
}
Self::AddressTableFull => write!(
f,
"address table full (max {MAX_INDEXED_ADDRESSES} entries)"
),
Self::TooManyV4BatchSwaps(n) => {
write!(f, "V4_BATCH max 8 swaps, got {n}")
}
}
}
}
impl std::error::Error for EncoderError {}
fn push_u8(out: &mut Vec<u8>, v: u8) {
out.push(v);
}
fn push_u16(out: &mut Vec<u8>, v: u16) {
out.extend_from_slice(&v.to_be_bytes());
}
fn push_i16(out: &mut Vec<u8>, v: i16) {
out.extend_from_slice(&v.to_be_bytes());
}
fn push_u96(out: &mut Vec<u8>, v: u128) -> Result<(), EncoderError> {
if v >= UINT96_BOUND {
return Err(EncoderError::Uint96Overflow(v));
}
let b = v.to_be_bytes();
out.extend_from_slice(&b[4..]); Ok(())
}
fn push_u256(out: &mut Vec<u8>, v: U256) {
out.extend_from_slice(&v.to_be_bytes::<32>());
}
fn push_forward_data(out: &mut Vec<u8>, data: &[u8]) -> Result<(), EncoderError> {
let len_u8 =
u8::try_from(data.len()).map_err(|_| EncoderError::ForwardDataTooLong(data.len()))?;
out.push(len_u8);
out.extend_from_slice(data);
Ok(())
}
pub use crate::config::{pack_config, pack_expected_balance};
#[derive(Debug, Default)]
pub struct AddressTable {
addresses: Vec<Address>,
index_map: HashMap<Address, u8>,
sentinel_map: HashMap<Address, u8>,
}
impl AddressTable {
#[must_use]
pub fn new() -> Self {
let mut sentinel_map = HashMap::new();
sentinel_map.insert(NATIVE_ADDRESS, SENTINEL_NATIVE);
sentinel_map.insert(Address::ZERO, SENTINEL_NATIVE);
Self {
addresses: Vec::new(),
index_map: HashMap::new(),
sentinel_map,
}
}
#[must_use]
pub fn with_sentinels(
weth: Option<Address>,
executor: Option<Address>,
pool_manager: Option<Address>,
) -> Self {
let mut table = Self::new();
if let Some(pm) = pool_manager {
table.sentinel_map.insert(pm, SENTINEL_PM);
}
if let Some(self_) = executor {
table.sentinel_map.insert(self_, SENTINEL_SELF);
}
if let Some(weth) = weth {
table.sentinel_map.insert(weth, SENTINEL_WETH);
}
table
}
pub fn add(&mut self, addr: Address) -> Result<u8, EncoderError> {
if let Some(&idx) = self.sentinel_map.get(&addr) {
return Ok(idx);
}
if let Some(&idx) = self.index_map.get(&addr) {
return Ok(idx);
}
let idx = self.addresses.len();
if idx >= MAX_INDEXED_ADDRESSES {
return Err(EncoderError::AddressTableFull);
}
let idx = u8::try_from(idx).unwrap_or(u8::MAX);
self.addresses.push(addr);
self.index_map.insert(addr, idx);
Ok(idx)
}
#[must_use]
pub fn index_of(&self, addr: Address) -> Option<u8> {
if let Some(&idx) = self.sentinel_map.get(&addr) {
Some(idx)
} else {
self.index_map.get(&addr).copied()
}
}
#[must_use]
pub fn contains(&self, addr: Address) -> bool {
self.sentinel_map.contains_key(&addr) || self.index_map.contains_key(&addr)
}
#[must_use]
pub fn addresses(&self) -> &[Address] {
&self.addresses
}
}
#[must_use]
pub fn enc_set_address(addr: Address) -> Vec<u8> {
let mut out = Vec::with_capacity(21);
out.push(CMD_SET_ADDRESS);
out.extend_from_slice(addr.as_slice());
out
}
#[must_use]
pub fn enc_set_addresses(address_table: &AddressTable) -> Vec<u8> {
let mut out = Vec::with_capacity(address_table.addresses().len() * 21);
for &addr in address_table.addresses() {
out.extend_from_slice(&enc_set_address(addr));
}
out
}
#[must_use]
pub fn enc_preamble(address_table: &AddressTable) -> Vec<u8> {
let mut out = enc_set_addresses(address_table);
out.push(BEGIN_EXECUTION);
out
}
pub fn enc_erc20_transfer(
token_idx: u8,
recipient_idx: u8,
amount: u128,
) -> Result<Vec<u8>, EncoderError> {
let mut out = Vec::with_capacity(15);
out.push(CMD_ERC20_TRANSFER);
push_u8(&mut out, token_idx);
push_u8(&mut out, recipient_idx);
push_u96(&mut out, amount)?;
Ok(out)
}
#[must_use]
pub fn enc_erc20_xfer_balance(token_idx: u8, recipient_idx: u8) -> Vec<u8> {
vec![CMD_ERC20_XFER_BALANCE, token_idx, recipient_idx]
}
#[must_use]
pub fn enc_weth_deposit(amount: U256) -> Vec<u8> {
let mut out = Vec::with_capacity(33);
out.push(CMD_WETH_DEPOSIT);
push_u256(&mut out, amount);
out
}
#[must_use]
pub fn enc_weth_withdraw(amount: U256) -> Vec<u8> {
let mut out = Vec::with_capacity(33);
out.push(CMD_WETH_WITHDRAW);
push_u256(&mut out, amount);
out
}
#[must_use]
pub fn enc_weth_deposit_all() -> Vec<u8> {
vec![CMD_WETH_DEPOSIT_ALL]
}
#[must_use]
pub fn enc_weth_withdraw_all() -> Vec<u8> {
vec![CMD_WETH_WITHDRAW_ALL]
}
pub fn enc_send_eth(recipient_idx: u8, amount: u128) -> Result<Vec<u8>, EncoderError> {
let mut out = Vec::with_capacity(14);
out.push(CMD_SEND_ETH);
push_u8(&mut out, recipient_idx);
push_u96(&mut out, amount)?;
Ok(out)
}
#[must_use]
pub fn enc_send_eth_all(recipient_idx: u8) -> Vec<u8> {
vec![CMD_SEND_ETH_ALL, recipient_idx]
}
pub fn enc_v2_swap_compact(
pool_idx: u8,
zfo: bool,
amount_out: u128,
recipient_idx: u8,
fee: u16,
forward_data: &[u8],
) -> Result<Vec<u8>, EncoderError> {
let mut out = Vec::with_capacity(19 + forward_data.len());
out.push(CMD_V2_SWAP_COMPACT);
push_u8(&mut out, pool_idx);
push_u8(&mut out, u8::from(zfo));
push_u96(&mut out, amount_out)?;
push_u8(&mut out, recipient_idx);
push_u16(&mut out, fee);
push_forward_data(&mut out, forward_data)?;
Ok(out)
}
#[must_use]
pub fn enc_v2_swap_calc(pool_idx: u8, zfo: bool, recipient_idx: u8, fee: u16) -> Vec<u8> {
let mut out = Vec::with_capacity(6);
out.push(CMD_V2_SWAP_CALC);
push_u8(&mut out, pool_idx);
push_u8(&mut out, u8::from(zfo));
push_u8(&mut out, recipient_idx);
push_u16(&mut out, fee);
out
}
pub fn enc_v2_swap_direct(
pool_idx: u8,
zfo: bool,
amount_out: u128,
recipient_idx: u8,
) -> Result<Vec<u8>, EncoderError> {
let mut out = Vec::with_capacity(16);
out.push(CMD_V2_SWAP_DIRECT);
push_u8(&mut out, pool_idx);
push_u8(&mut out, u8::from(zfo));
push_u96(&mut out, amount_out)?;
push_u8(&mut out, recipient_idx);
Ok(out)
}
pub fn enc_v3_swap_compact(
pool_idx: u8,
zfo: bool,
amount_specified: u128,
recipient_idx: u8,
forward_data: &[u8],
) -> Result<Vec<u8>, EncoderError> {
let mut out = Vec::with_capacity(17 + forward_data.len());
out.push(CMD_V3_SWAP_COMPACT);
push_u8(&mut out, pool_idx);
push_u8(&mut out, u8::from(zfo));
push_u96(&mut out, amount_specified)?;
push_u8(&mut out, recipient_idx);
push_forward_data(&mut out, forward_data)?;
Ok(out)
}
#[must_use]
pub fn enc_v3_swap_delta(pool_idx: u8, zfo: bool, recipient_idx: u8) -> Vec<u8> {
let mut out = Vec::with_capacity(4);
out.push(CMD_V3_SWAP_DELTA);
push_u8(&mut out, pool_idx);
push_u8(&mut out, u8::from(zfo));
push_u8(&mut out, recipient_idx);
out
}
pub fn enc_v4_swap_compact(
c0_idx: u8,
c1_idx: u8,
fee: u16,
tick_spacing: i16,
hooks_idx: u8,
zfo: bool,
amount_u96: u128,
) -> Result<Vec<u8>, EncoderError> {
let mut out = Vec::with_capacity(21);
out.push(CMD_V4_SWAP_COMPACT);
push_u8(&mut out, c0_idx);
push_u8(&mut out, c1_idx);
push_u16(&mut out, fee);
push_i16(&mut out, tick_spacing);
push_u8(&mut out, hooks_idx);
push_u8(&mut out, u8::from(zfo));
push_u96(&mut out, amount_u96)?;
Ok(out)
}
#[must_use]
pub fn enc_v4_swap_dynamic(
c0_idx: u8,
c1_idx: u8,
fee: u16,
tick_spacing: i16,
hooks_idx: u8,
zfo: bool,
) -> Vec<u8> {
let mut out = Vec::with_capacity(9);
out.push(CMD_V4_SWAP_DYNAMIC);
push_u8(&mut out, c0_idx);
push_u8(&mut out, c1_idx);
push_u16(&mut out, fee);
push_i16(&mut out, tick_spacing);
push_u8(&mut out, hooks_idx);
push_u8(&mut out, u8::from(zfo));
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct V4BatchEntry {
pub c0_idx: u8,
pub c1_idx: u8,
pub fee: u16,
pub tick_spacing: i16,
pub hooks_idx: u8,
pub zfo: bool,
pub amount_u96: u128,
}
pub fn enc_v4_batch(swaps: &[V4BatchEntry]) -> Result<Vec<u8>, EncoderError> {
v4_batch_stream(CMD_V4_BATCH, swaps)
}
pub fn enc_v4_batch_open_weth(swaps: &[V4BatchEntry]) -> Result<Vec<u8>, EncoderError> {
v4_batch_stream(CMD_V4_BATCH_OPEN_WETH, swaps)
}
fn v4_batch_stream(cmd: u8, swaps: &[V4BatchEntry]) -> Result<Vec<u8>, EncoderError> {
if swaps.len() > 8 {
return Err(EncoderError::TooManyV4BatchSwaps(swaps.len()));
}
let mut out = Vec::with_capacity(2 + swaps.len() * 20);
out.push(cmd);
push_u8(&mut out, u8::try_from(swaps.len()).unwrap_or(u8::MAX));
for s in swaps {
push_u8(&mut out, s.c0_idx);
push_u8(&mut out, s.c1_idx);
push_u16(&mut out, s.fee);
push_i16(&mut out, s.tick_spacing);
push_u8(&mut out, s.hooks_idx);
push_u8(&mut out, u8::from(s.zfo));
push_u96(&mut out, s.amount_u96)?;
}
Ok(out)
}
pub fn enc_v4_unlock(forward_data: &[u8]) -> Result<Vec<u8>, EncoderError> {
let mut out = Vec::with_capacity(2 + forward_data.len());
out.push(CMD_V4_UNLOCK);
push_forward_data(&mut out, forward_data)?;
Ok(out)
}
#[must_use]
pub fn enc_v4_take(currency_idx: u8, recipient_idx: u8, amount: U256) -> Vec<u8> {
let mut out = Vec::with_capacity(35);
out.push(CMD_V4_TAKE);
push_u8(&mut out, currency_idx);
push_u8(&mut out, recipient_idx);
push_u256(&mut out, amount);
out
}
pub fn enc_v4_take_compact(
currency_idx: u8,
recipient_idx: u8,
amount_u96: u128,
) -> Result<Vec<u8>, EncoderError> {
let mut out = Vec::with_capacity(15);
out.push(CMD_V4_TAKE_COMPACT);
push_u8(&mut out, currency_idx);
push_u8(&mut out, recipient_idx);
push_u96(&mut out, amount_u96)?;
Ok(out)
}
#[must_use]
pub fn enc_v4_take_delta(currency_idx: u8, recipient_idx: u8) -> Vec<u8> {
vec![CMD_V4_TAKE_DELTA, currency_idx, recipient_idx]
}
#[must_use]
pub fn enc_v4_sync(currency_idx: u8) -> Vec<u8> {
vec![CMD_V4_SYNC, currency_idx]
}
#[must_use]
pub fn enc_v4_settle() -> Vec<u8> {
vec![CMD_V4_SETTLE]
}
#[must_use]
pub fn enc_v4_settle_delta(currency_idx: u8) -> Vec<u8> {
vec![CMD_V4_SETTLE_DELTA, currency_idx]
}
#[must_use]
pub fn enc_v4_settle_all() -> Vec<u8> {
vec![CMD_V4_SETTLE_ALL]
}
pub fn enc_v4_mint_compact(
currency_idx: u8,
recipient_idx: u8,
amount_u96: u128,
) -> Result<Vec<u8>, EncoderError> {
let mut out = Vec::with_capacity(15);
out.push(CMD_V4_MINT_COMPACT);
push_u8(&mut out, currency_idx);
push_u8(&mut out, recipient_idx);
push_u96(&mut out, amount_u96)?;
Ok(out)
}
pub fn enc_v4_burn_compact(currency_idx: u8, amount_u96: u128) -> Result<Vec<u8>, EncoderError> {
let mut out = Vec::with_capacity(14);
out.push(CMD_V4_BURN_COMPACT);
push_u8(&mut out, currency_idx);
push_u96(&mut out, amount_u96)?;
Ok(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct V4PoolKey {
pub currency0: Address,
pub currency1: Address,
pub fee: u32,
pub tick_spacing: i32,
pub hooks: Address,
}
#[must_use]
pub fn make_pool_key(
currency0: Address,
currency1: Address,
fee: u32,
tick_spacing: i32,
hooks: Address,
) -> V4PoolKey {
let (c0, c1) = if currency0 <= currency1 {
(currency0, currency1)
} else {
(currency1, currency0)
};
V4PoolKey {
currency0: c0,
currency1: c1,
fee,
tick_spacing,
hooks,
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used, clippy::cast_possible_truncation)]
mod tests {
use super::*;
use alloy::primitives::address;
#[test]
fn uint96_max_is_accepted_overflow_is_rejected() {
let max = u128::MAX >> 32;
assert_eq!(max, (1u128 << 96) - 1);
assert!(enc_erc20_transfer(1, 2, max).is_ok());
assert_eq!(
enc_erc20_transfer(1, 2, 1u128 << 96).unwrap_err(),
EncoderError::Uint96Overflow(1u128 << 96)
);
}
#[test]
fn address_table_sentinels_resolve_without_adding() {
let pm = address!("000000000004444c5dc75cB358380D2e3dE08A90");
let exec = address!("DeAd0000000000000000000000000000000000Be");
let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let mut table = AddressTable::with_sentinels(Some(weth), Some(exec), Some(pm));
assert_eq!(table.add(weth).unwrap(), SENTINEL_WETH);
assert_eq!(table.add(pm).unwrap(), SENTINEL_PM);
assert_eq!(table.add(exec).unwrap(), SENTINEL_SELF);
assert_eq!(table.add(Address::ZERO).unwrap(), SENTINEL_NATIVE);
assert!(table.addresses().is_empty());
}
#[test]
fn address_table_dedups_insertion_order() {
let usdc = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
let wbtc = address!("2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599");
let mut table = AddressTable::new();
assert_eq!(table.add(usdc).unwrap(), 0);
assert_eq!(table.add(wbtc).unwrap(), 1);
assert_eq!(table.add(usdc).unwrap(), 0);
assert_eq!(table.add(wbtc).unwrap(), 1);
assert_eq!(table.addresses(), &[usdc, wbtc]);
assert_eq!(table.index_of(usdc), Some(0));
assert_eq!(table.index_of(wbtc), Some(1));
assert!(table
.index_of(address!("DeAd000000000000000000000000000000000001"))
.is_none());
}
#[test]
fn address_table_cap_rejects_beyond_32() {
let mut table = AddressTable::new();
for i in 0u8..MAX_INDEXED_ADDRESSES as u8 {
let addr = Address::with_last_byte(i + 1);
assert_eq!(table.add(addr).unwrap(), i);
}
let extra = Address::with_last_byte(0xAA);
assert_eq!(
table.add(extra).unwrap_err(),
EncoderError::AddressTableFull
);
assert_eq!(table.addresses().len(), MAX_INDEXED_ADDRESSES);
}
#[test]
fn make_pool_key_sorts_and_is_symmetric() {
use proptest::prelude::*;
proptest!(|(a in 0u64..u64::MAX, b in 0u64..u64::MAX)| {
let ca = Address::with_last_byte((a & 0xFF) as u8);
let cb = Address::with_last_byte((b & 0xFF) as u8);
let k13 = make_pool_key(ca, cb, 3000, 60, Address::ZERO);
let k31 = make_pool_key(cb, ca, 3000, 60, Address::ZERO);
prop_assert_eq!(k13, k31);
prop_assert!(k13.currency0 <= k13.currency1);
});
}
#[test]
fn property_address_table_membership_stable_under_reorder() {
use proptest::prelude::*;
proptest!(|(a in 0u64..256, b in 0u64..256, c in 0u64..256)| {
let addrs: [Address; 3] = [
Address::with_last_byte(a as u8),
Address::with_last_byte(b as u8),
Address::with_last_byte(c as u8),
];
let mut t1 = AddressTable::new();
let mut t2 = AddressTable::new();
for a_ in &addrs { t1.add(*a_).ok(); }
for a_ in addrs.iter().rev() { t2.add(*a_).ok(); }
for a_ in &addrs {
prop_assert_eq!(t1.contains(*a_), t2.contains(*a_));
}
});
}
}