use crate::{
data::{ExactInParams, ExactOutParams, PoolState, Quote},
swap,
};
use alloy_primitives::{I256, U256};
use thiserror::Error;
use wp_evm_amm_math::AmmMathError;
#[derive(Error, Debug)]
pub enum QuoteError {
#[error("input token does not match pool")]
UnknownToken,
#[error("amm math error: {0}")]
Math(#[from] AmmMathError),
#[error("pool has insufficient liquidity to fulfil the requested swap")]
InsufficientLiquidity,
#[error("quote pipeline internal invariant violated: {0}")]
Internal(&'static str),
}
impl From<swap::SwapError> for QuoteError {
fn from(e: swap::SwapError) -> Self {
match e {
swap::SwapError::Math(m) => QuoteError::Math(m),
swap::SwapError::InsufficientLiquidity => QuoteError::InsufficientLiquidity,
swap::SwapError::ZeroAmount => {
QuoteError::Internal("swap returned ZeroAmount after quote-level guard")
}
swap::SwapError::InvalidPriceLimit => {
QuoteError::Internal("swap returned InvalidPriceLimit with quote-supplied sentinel")
}
swap::SwapError::Internal(msg) => QuoteError::Internal(msg),
}
}
}
pub fn exact_in(state: &PoolState, params: &ExactInParams) -> Result<Quote, QuoteError> {
exact_in_with_fee_fn(state, params, |s| s.fee)
}
pub fn exact_in_with_fee_fn<F>(
state: &PoolState,
params: &ExactInParams,
fee_fn: F,
) -> Result<Quote, QuoteError>
where
F: Fn(&PoolState) -> u32,
{
let zero_for_one = params.token_in == state.token0;
if !zero_for_one && params.token_in != state.token1 {
return Err(QuoteError::UnknownToken);
}
if params.amount_in.is_zero() {
return Ok(Quote {
amount_in: U256::ZERO,
amount_out: U256::ZERO,
sqrt_price_x96_after: state.sqrt_price_x96,
price_impact_bps: 0,
});
}
let effective_fee = fee_fn(state);
let amount_specified = I256::try_from(params.amount_in)
.map_err(|_| QuoteError::Math(AmmMathError::MulDivOverflow))?;
let sqrt_price_limit_x96 = if zero_for_one {
swap::min_sqrt_ratio_plus_one()
} else {
swap::max_sqrt_ratio_minus_one()
};
let result =
swap::swap(state, zero_for_one, amount_specified, sqrt_price_limit_x96, effective_fee)?;
if result.amount_in != params.amount_in {
return Err(QuoteError::InsufficientLiquidity);
}
Ok(Quote {
amount_in: result.amount_in,
amount_out: result.amount_out,
sqrt_price_x96_after: result.sqrt_price_x96_after,
price_impact_bps: compute_price_impact_bps(
state.sqrt_price_x96,
result.sqrt_price_x96_after,
),
})
}
pub fn exact_out(state: &PoolState, params: &ExactOutParams) -> Result<Quote, QuoteError> {
exact_out_with_fee_fn(state, params, |s| s.fee)
}
pub fn exact_out_with_fee_fn<F>(
state: &PoolState,
params: &ExactOutParams,
fee_fn: F,
) -> Result<Quote, QuoteError>
where
F: Fn(&PoolState) -> u32,
{
let zero_for_one = params.token_in == state.token0;
if !zero_for_one && params.token_in != state.token1 {
return Err(QuoteError::UnknownToken);
}
if params.amount_out.is_zero() {
return Ok(Quote {
amount_in: U256::ZERO,
amount_out: U256::ZERO,
sqrt_price_x96_after: state.sqrt_price_x96,
price_impact_bps: 0,
});
}
let effective_fee = fee_fn(state);
let amount_out_i = I256::try_from(params.amount_out)
.map_err(|_| QuoteError::Math(AmmMathError::MulDivOverflow))?;
let amount_specified =
amount_out_i.checked_neg().ok_or(QuoteError::Math(AmmMathError::MulDivOverflow))?;
let sqrt_price_limit_x96 = if zero_for_one {
swap::min_sqrt_ratio_plus_one()
} else {
swap::max_sqrt_ratio_minus_one()
};
let result =
swap::swap(state, zero_for_one, amount_specified, sqrt_price_limit_x96, effective_fee)?;
if result.amount_out != params.amount_out {
return Err(QuoteError::InsufficientLiquidity);
}
Ok(Quote {
amount_in: result.amount_in,
amount_out: result.amount_out,
sqrt_price_x96_after: result.sqrt_price_x96_after,
price_impact_bps: compute_price_impact_bps(
state.sqrt_price_x96,
result.sqrt_price_x96_after,
),
})
}
fn compute_price_impact_bps(before: U256, after: U256) -> u16 {
if before == U256::ZERO {
return 0;
}
let (num, denom) =
if after > before { (after - before, before) } else { (before - after, before) };
let bps = (num * U256::from(10_000u64)) / denom;
bps.saturating_to::<u16>()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::{ExactInParams, ExactOutParams, PoolState, TickInfo};
use alloy_primitives::{address, U256};
fn fixture_usdc_weth_03() -> PoolState {
let sqrt_price_x96: U256 =
U256::from_str_radix("3543191142285914205922034323214", 10).unwrap();
PoolState {
token0: address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), token1: address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), fee: 3000,
tick_spacing: 60,
sqrt_price_x96,
liquidity: 2_000_000_000_000_000_000_000u128, tick: 76012, 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,
},
],
}
}
#[test]
fn exact_in_one_usdc_for_weth_within_tick() {
let s = fixture_usdc_weth_03();
let p = ExactInParams {
token_in: s.token0,
token_out: s.token1,
amount_in: U256::from(1_000_000u64), recipient: address!("0x0000000000000000000000000000000000000099"),
};
let q = exact_in(&s, &p).expect("quote ok");
assert!(q.amount_out > U256::ZERO);
assert!(q.amount_out < U256::from(1_000_000_000_000_000u64));
assert_eq!(q.amount_in, p.amount_in);
}
#[test]
fn exact_in_rejects_unknown_token() {
let s = fixture_usdc_weth_03();
let bogus_token = address!("0x000000000000000000000000000000000000dead");
let p = ExactInParams {
token_in: bogus_token,
token_out: s.token1,
amount_in: U256::from(1_000_000u64),
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let err = exact_in(&s, &p).expect_err("should reject unknown token");
assert!(matches!(err, QuoteError::UnknownToken));
}
#[test]
fn exact_in_reverse_direction_weth_for_usdc() {
let s = fixture_usdc_weth_03();
let p = ExactInParams {
token_in: s.token1, token_out: s.token0, amount_in: U256::from(1_000_000_000_000_000u64), recipient: address!("0x0000000000000000000000000000000000000099"),
};
let q = exact_in(&s, &p).expect("reverse quote ok");
assert!(
q.amount_out > U256::from(480_000_000_000u64),
"amount_out too low: {}",
q.amount_out
);
assert!(
q.amount_out < U256::from(515_000_000_000u64),
"amount_out too high: {}",
q.amount_out
);
assert_eq!(q.amount_in, p.amount_in);
}
#[test]
fn exact_in_large_swap_crosses_tick() {
let s = fixture_usdc_weth_03();
let p = ExactInParams {
token_in: s.token0, token_out: s.token1, amount_in: U256::from(1_000_000_000_000_000_000u64), recipient: address!("0x0000000000000000000000000000000000000099"),
};
let q = exact_in(&s, &p).expect("large swap ok");
assert!(q.amount_out > U256::ZERO, "amount_out should be > 0");
let sqrt_at_75960 = U256::from_str_radix("3533845506420911390540068078527", 10).unwrap();
assert!(
q.sqrt_price_x96_after < sqrt_at_75960,
"expected sqrt_price to cross below tick 75960 (sqrt {}), got {}",
sqrt_at_75960,
q.sqrt_price_x96_after
);
assert_eq!(q.amount_in, p.amount_in);
}
#[test]
fn exact_out_round_trip_against_exact_in() {
let s = fixture_usdc_weth_03();
let p_in = ExactInParams {
token_in: s.token0,
token_out: s.token1,
amount_in: U256::from(1_000_000u64),
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let q_in = exact_in(&s, &p_in).unwrap();
let p_out = ExactOutParams {
token_in: s.token0,
token_out: s.token1,
amount_out: q_in.amount_out,
recipient: p_in.recipient,
};
let q_out = exact_out(&s, &p_out).unwrap();
let diff = if q_out.amount_in > p_in.amount_in {
q_out.amount_in - p_in.amount_in
} else {
p_in.amount_in - q_out.amount_in
};
assert!(diff <= U256::from(1_000u64), "round-trip diff = {}", diff);
}
#[test]
fn exact_in_reports_price_impact() {
let s = fixture_usdc_weth_03();
let big = ExactInParams {
token_in: s.token0,
token_out: s.token1,
amount_in: U256::from(1_000_000_000_000_000_000u64), recipient: address!("0x0000000000000000000000000000000000000099"),
};
let q = exact_in(&s, &big).unwrap();
assert!(q.price_impact_bps > 0, "expected nonzero price impact, got 0");
}
#[test]
fn exact_in_small_swap_has_minimal_price_impact() {
let s = fixture_usdc_weth_03();
let p = ExactInParams {
token_in: s.token0,
token_out: s.token1,
amount_in: U256::from(1_000u64), recipient: address!("0x0000000000000000000000000000000000000099"),
};
let q = exact_in(&s, &p).unwrap();
assert!(q.price_impact_bps < 100, "expected tiny impact, got {}", q.price_impact_bps);
}
#[test]
fn exact_out_rejects_unknown_token() {
let s = fixture_usdc_weth_03();
let bogus = address!("0x000000000000000000000000000000000000dead");
let p = ExactOutParams {
token_in: bogus,
token_out: s.token1,
amount_out: U256::from(1_000_000_000_000_000u64),
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let err = exact_out(&s, &p).expect_err("should reject unknown token");
assert!(matches!(err, QuoteError::UnknownToken));
}
#[test]
fn exact_in_with_fee_fn_uses_injected_fee() {
let s = fixture_usdc_weth_03();
let p = ExactInParams {
token_in: s.token0,
token_out: s.token1,
amount_in: U256::from(1_000u64),
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let q_normal = exact_in(&s, &p).expect("normal quote ok");
let q_low_fee = exact_in_with_fee_fn(&s, &p, |_| 100).expect("low-fee quote ok");
assert!(
q_low_fee.amount_out > q_normal.amount_out,
"low-fee output should exceed normal output: {} vs {}",
q_low_fee.amount_out,
q_normal.amount_out
);
}
#[test]
fn exact_in_token_in_equals_token_out_returns_unknown_token() {
let s = fixture_usdc_weth_03();
let bogus = address!("0x000000000000000000000000000000000000dead");
let p = ExactInParams {
token_in: bogus,
token_out: bogus, amount_in: U256::from(1_000_000u64),
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let err = exact_in(&s, &p).expect_err("should reject token not in pool");
assert!(matches!(err, QuoteError::UnknownToken));
}
#[test]
fn exact_out_token_in_equals_token_out_returns_unknown_token() {
let s = fixture_usdc_weth_03();
let bogus = address!("0x000000000000000000000000000000000000dead");
let p = ExactOutParams {
token_in: bogus,
token_out: bogus,
amount_out: U256::from(1_000_000u64),
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let err = exact_out(&s, &p).expect_err("should reject token not in pool");
assert!(matches!(err, QuoteError::UnknownToken));
}
#[test]
fn exact_in_very_large_amount_returns_insufficient_liquidity() {
let s = fixture_usdc_weth_03();
let p = ExactInParams {
token_in: s.token0,
token_out: s.token1,
amount_in: U256::from_str_radix("10000000000000000000000000000000000000000", 10)
.unwrap(), recipient: address!("0x0000000000000000000000000000000000000099"),
};
let err = exact_in(&s, &p).expect_err("should error on oversized amount");
assert!(
matches!(err, QuoteError::InsufficientLiquidity),
"expected InsufficientLiquidity, got {:?}",
err
);
}
#[test]
fn exact_out_very_large_amount_returns_insufficient_liquidity() {
let s = fixture_usdc_weth_03();
let p = ExactOutParams {
token_in: s.token0,
token_out: s.token1,
amount_out: U256::from_str_radix("10000000000000000000000000000000000000000", 10)
.unwrap(), recipient: address!("0x0000000000000000000000000000000000000099"),
};
let err = exact_out(&s, &p).expect_err("should error on oversized output request");
assert!(
matches!(err, QuoteError::InsufficientLiquidity),
"expected InsufficientLiquidity, got {:?}",
err
);
}
#[test]
fn exact_out_zero_amount_returns_zero() {
let s = fixture_usdc_weth_03();
let p = ExactOutParams {
token_in: s.token0,
token_out: s.token1,
amount_out: U256::ZERO,
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let q = exact_out(&s, &p).expect("zero-amount exact_out should not error");
assert_eq!(q.amount_in, U256::ZERO);
assert_eq!(q.amount_out, U256::ZERO);
assert_eq!(q.sqrt_price_x96_after, s.sqrt_price_x96);
assert_eq!(q.price_impact_bps, 0);
}
#[test]
fn exact_in_zero_amount_returns_zero_out() {
let s = fixture_usdc_weth_03();
let p = ExactInParams {
token_in: s.token0,
token_out: s.token1,
amount_in: U256::ZERO,
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let q = exact_in(&s, &p).expect("zero-amount swap should not error");
assert_eq!(q.amount_out, U256::ZERO, "zero-in should produce zero-out");
assert_eq!(q.amount_in, U256::ZERO);
}
#[test]
fn exact_in_zero_amount_reverse_direction_returns_zero_out() {
let s = fixture_usdc_weth_03();
let p = ExactInParams {
token_in: s.token1,
token_out: s.token0,
amount_in: U256::ZERO,
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let q = exact_in(&s, &p).expect("reverse-direction zero-input should not error");
assert_eq!(q.amount_in, U256::ZERO);
assert_eq!(q.amount_out, U256::ZERO);
assert_eq!(q.sqrt_price_x96_after, s.sqrt_price_x96);
}
#[test]
fn exact_out_zero_amount_reverse_direction_returns_zero() {
let s = fixture_usdc_weth_03();
let p = ExactOutParams {
token_in: s.token1,
token_out: s.token0,
amount_out: U256::ZERO,
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let q = exact_out(&s, &p).expect("reverse-direction zero-output should not error");
assert_eq!(q.amount_in, U256::ZERO);
assert_eq!(q.amount_out, U256::ZERO);
assert_eq!(q.sqrt_price_x96_after, s.sqrt_price_x96);
}
}