use crate::data::{ExactInParams, ExactOutParams, PoolState, Quote};
use crate::hook_options::has_swap_permissions as native_has_swap_permissions;
use alloy_primitives::Address;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum QuoteError {
#[error("input currency does not match pool currencies")]
UnknownCurrency,
#[error("pool has swap-permission hooks at {hooks}; unsupported by standard CL quote math")]
HookedPoolUnsupported { hooks: Address },
#[error("pool uses dynamic fee flag; unsupported (dynamic-fee pools require hooks)")]
DynamicFeeUnsupported,
#[error("v3 family quote error: {0}")]
Delegate(String),
}
const DYNAMIC_FEE_FLAG: u32 = 0x800000;
pub fn exact_in(state: &PoolState, params: &ExactInParams) -> Result<Quote, QuoteError> {
let hooks = state.pool_key.hooks;
if has_swap_permissions(hooks) {
return Err(QuoteError::HookedPoolUnsupported { hooks });
}
let fee: u32 = state.pool_key.fee.to::<u32>();
if fee == DYNAMIC_FEE_FLAG {
return Err(QuoteError::DynamicFeeUnsupported);
}
let v3_state = as_v3_pool_state(state, fee);
let v3_params = as_v3_exact_in_params(params);
let v3_quote = wp_evm_v3_core::quote::exact_in_with_fee_fn(&v3_state, &v3_params, move |_| fee)
.map_err(|e| QuoteError::Delegate(format!("{e:?}")))?;
Ok(Quote {
amount_in: v3_quote.amount_in,
amount_out: v3_quote.amount_out,
sqrt_price_x96_after: v3_quote.sqrt_price_x96_after,
price_impact_bps: v3_quote.price_impact_bps,
effective_fee_pips: fee,
})
}
pub fn exact_out(state: &PoolState, params: &ExactOutParams) -> Result<Quote, QuoteError> {
let hooks = state.pool_key.hooks;
if has_swap_permissions(hooks) {
return Err(QuoteError::HookedPoolUnsupported { hooks });
}
let fee: u32 = state.pool_key.fee.to::<u32>();
if fee == DYNAMIC_FEE_FLAG {
return Err(QuoteError::DynamicFeeUnsupported);
}
let v3_state = as_v3_pool_state(state, fee);
let v3_params = wp_evm_v3_core::data::ExactOutParams {
token_in: params.currency_in,
token_out: params.currency_out,
amount_out: params.amount_out,
recipient: params.recipient,
};
let v3_quote =
wp_evm_v3_core::quote::exact_out_with_fee_fn(&v3_state, &v3_params, move |_| fee)
.map_err(|e| QuoteError::Delegate(format!("{e:?}")))?;
Ok(Quote {
amount_in: v3_quote.amount_in,
amount_out: v3_quote.amount_out,
sqrt_price_x96_after: v3_quote.sqrt_price_x96_after,
price_impact_bps: v3_quote.price_impact_bps,
effective_fee_pips: fee,
})
}
fn as_v3_pool_state(state: &PoolState, fee: u32) -> wp_evm_v3_core::data::PoolState {
let tick_spacing_i32: i32 = state.pool_key.tickSpacing.as_i32();
wp_evm_v3_core::data::PoolState {
token0: state.pool_key.currency0,
token1: state.pool_key.currency1,
fee,
tick_spacing: tick_spacing_i32,
sqrt_price_x96: state.sqrt_price_x96,
liquidity: state.liquidity,
tick: state.tick,
ticks: state
.ticks
.iter()
.map(|t| wp_evm_v3_core::data::TickInfo {
tick: t.tick,
liquidity_net: t.liquidity_net,
liquidity_gross: t.liquidity_gross,
})
.collect(),
}
}
fn as_v3_exact_in_params(p: &ExactInParams) -> wp_evm_v3_core::data::ExactInParams {
wp_evm_v3_core::data::ExactInParams {
token_in: p.currency_in,
token_out: p.currency_out,
amount_in: p.amount_in,
recipient: p.recipient,
}
}
fn has_swap_permissions(hooks: Address) -> bool {
native_has_swap_permissions(hooks)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::{PoolKey, TickInfo};
use alloy_primitives::{address, aliases::I24, aliases::U24, b256, U256};
fn hookless_usdc_weth_fixture() -> PoolState {
let sqrt_price_x96 = U256::from_str_radix("3543191142285914205922034323214", 10).unwrap();
let pool_key = PoolKey {
currency0: address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), currency1: address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), fee: U24::from(3000u32),
tickSpacing: I24::try_from(60i32).unwrap(),
hooks: Address::ZERO, };
PoolState {
pool_key,
pool_id: b256!("0000000000000000000000000000000000000000000000000000000000000001"),
sqrt_price_x96,
tick: 76012,
liquidity: 2_000_000_000_000_000_000_000u128,
protocol_fee: 0,
lp_fee: 3000,
ticks: vec![
TickInfo {
tick: 74940,
liquidity_net: 1_000_000_000_000_000_000_000i128,
liquidity_gross: 1_000_000_000_000_000_000_000u128,
},
TickInfo {
tick: 75960,
liquidity_net: 1_000_000_000_000_000_000_000i128,
liquidity_gross: 1_000_000_000_000_000_000_000u128,
},
TickInfo {
tick: 76020,
liquidity_net: -2_000_000_000_000_000_000_000i128,
liquidity_gross: 2_000_000_000_000_000_000_000u128,
},
],
}
}
fn hooked_pool_fixture() -> PoolState {
let mut state = hookless_usdc_weth_fixture();
state.pool_key.hooks = address!("0000000000000000000000000000000000000080");
state
}
#[test]
fn exact_in_hookless_pool_delegates_successfully() {
let s = hookless_usdc_weth_fixture();
let p = ExactInParams {
currency_in: s.pool_key.currency0,
currency_out: s.pool_key.currency1,
amount_in: U256::from(1_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
};
let q = exact_in(&s, &p).expect("hookless quote ok");
assert!(q.amount_out > U256::ZERO);
assert_eq!(q.amount_in, p.amount_in);
assert_eq!(q.effective_fee_pips, 3000);
}
#[test]
fn exact_in_hooked_pool_rejected() {
let s = hooked_pool_fixture();
let p = ExactInParams {
currency_in: s.pool_key.currency0,
currency_out: s.pool_key.currency1,
amount_in: U256::from(1_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
};
let err = exact_in(&s, &p).expect_err("hooked pool should be rejected");
assert!(matches!(err, QuoteError::HookedPoolUnsupported { .. }));
}
#[test]
fn has_swap_permissions_detects_swap_flags() {
assert!(!has_swap_permissions(Address::ZERO));
assert!(!has_swap_permissions(address!("0000000000000000000000000000000000000001")));
assert!(has_swap_permissions(address!("0000000000000000000000000000000000000080")));
assert!(has_swap_permissions(address!("0000000000000000000000000000000000000040")));
assert!(has_swap_permissions(address!("0000000000000000000000000000000000000008")));
assert!(has_swap_permissions(address!("0000000000000000000000000000000000000004")));
assert!(has_swap_permissions(address!("00000000000000000000000000000000000000CC")));
}
#[test]
fn exact_in_dynamic_fee_pool_rejected() {
let mut s = hookless_usdc_weth_fixture();
s.pool_key.fee = U24::from(0x800000u32);
let p = ExactInParams {
currency_in: s.pool_key.currency0,
currency_out: s.pool_key.currency1,
amount_in: U256::from(1_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
};
let err = exact_in(&s, &p).expect_err("dynamic fee pool should be rejected");
assert!(matches!(err, QuoteError::DynamicFeeUnsupported));
}
#[test]
fn exact_out_dynamic_fee_pool_rejected() {
let mut s = hookless_usdc_weth_fixture();
s.pool_key.fee = U24::from(0x800000u32);
let p = ExactOutParams {
currency_in: s.pool_key.currency0,
currency_out: s.pool_key.currency1,
amount_out: U256::from(500_000_000_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
};
let err = exact_out(&s, &p).expect_err("dynamic fee pool should be rejected");
assert!(matches!(err, QuoteError::DynamicFeeUnsupported));
}
#[test]
fn exact_in_unknown_currency_delegates_to_v3_error() {
let s = hookless_usdc_weth_fixture();
let bogus = address!("000000000000000000000000000000000000dead");
let p = ExactInParams {
currency_in: bogus,
currency_out: s.pool_key.currency1,
amount_in: U256::from(1_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
};
let err = exact_in(&s, &p).expect_err("unknown currency should be rejected");
assert!(matches!(err, QuoteError::Delegate(_)), "expected Delegate error, got {:?}", err);
}
#[test]
fn exact_out_hooked_pool_rejected() {
let mut s = hookless_usdc_weth_fixture();
s.pool_key.hooks = address!("0000000000000000000000000000000000000080");
let p = ExactOutParams {
currency_in: s.pool_key.currency0,
currency_out: s.pool_key.currency1,
amount_out: U256::from(500_000_000_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
};
let err = exact_out(&s, &p).expect_err("hooked pool should be rejected");
assert!(matches!(err, QuoteError::HookedPoolUnsupported { .. }));
}
#[test]
fn exact_in_before_swap_returns_delta_only_hook_rejected() {
let mut s = hookless_usdc_weth_fixture();
s.pool_key.hooks = address!("0000000000000000000000000000000000000008");
let p = ExactInParams {
currency_in: s.pool_key.currency0,
currency_out: s.pool_key.currency1,
amount_in: U256::from(1_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
};
let err = exact_in(&s, &p).expect_err("delta-returning hook should be rejected");
assert!(matches!(err, QuoteError::HookedPoolUnsupported { .. }));
}
#[test]
fn exact_out_unknown_currency_delegates_to_v3_error() {
let s = hookless_usdc_weth_fixture();
let bogus = address!("000000000000000000000000000000000000dead");
let p = ExactOutParams {
currency_in: bogus,
currency_out: s.pool_key.currency1,
amount_out: U256::from(500_000_000_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
};
let err = exact_out(&s, &p).expect_err("unknown currency should be rejected");
assert!(matches!(err, QuoteError::Delegate(_)), "expected Delegate error, got {:?}", err);
}
}