use alloy_primitives::{U160, U256};
use rust_decimal::prelude::ToPrimitive;
use rust_decimal_macros::dec;
use crate::{
defi::{
Token,
data::swap::RawSwapData,
tick_map::{
full_math::{DECIMAL_EXPONENT_MAX, FullMath},
sqrt_price_math::{decode_sqrt_price_x96_to_price_tokens_adjusted, price_from_u256},
},
},
enums::OrderSide,
types::{Price, Quantity, fixed::FIXED_PRECISION},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SwapTradeInfo {
pub order_side: OrderSide,
pub quantity_base: Quantity,
pub quantity_quote: Quantity,
pub spot_price: Price,
pub execution_price: Price,
pub is_inverted: bool,
pub spot_price_before: Option<Price>,
}
impl SwapTradeInfo {
pub fn set_spot_price_before(&mut self, price: Price) {
self.spot_price_before = Some(price);
}
pub fn get_price_impact_bps(&self) -> anyhow::Result<u32> {
if let Some(spot_price_before) = self.spot_price_before {
Self::check_spot_price_before(spot_price_before, PriceMetric::Impact)?;
let price_change = self.spot_price - spot_price_before;
let price_impact =
(price_change.as_decimal() / spot_price_before.as_decimal()).abs() * dec!(10_000);
Ok(price_impact.round().to_u32().unwrap_or(0))
} else {
anyhow::bail!("Cannot calculate price impact, the spot price before is not set");
}
}
pub fn get_slippage_bps(&self) -> anyhow::Result<u32> {
if let Some(spot_price_before) = self.spot_price_before {
Self::check_spot_price_before(spot_price_before, PriceMetric::Slippage)?;
let price_change = self.execution_price - spot_price_before;
let slippage =
(price_change.as_decimal() / spot_price_before.as_decimal()).abs() * dec!(10_000);
Ok(slippage.round().to_u32().unwrap_or(0))
} else {
anyhow::bail!("Cannot calculate slippage, the spot price before is not set")
}
}
fn check_spot_price_before(
spot_price_before: Price,
metric: PriceMetric,
) -> anyhow::Result<()> {
let metric = metric.name();
anyhow::ensure!(
!spot_price_before.is_zero(),
"Cannot calculate {metric}, the spot price before is zero"
);
Ok(())
}
}
enum PriceMetric {
Impact,
Slippage,
}
impl PriceMetric {
const fn name(self) -> &'static str {
match self {
Self::Impact => "price impact",
Self::Slippage => "slippage",
}
}
}
#[derive(Debug)]
pub struct SwapTradeInfoCalculator<'a> {
token0: &'a Token,
token1: &'a Token,
pub is_inverted: bool,
raw_swap_data: RawSwapData,
}
impl<'a> SwapTradeInfoCalculator<'a> {
#[must_use]
pub fn new(token0: &'a Token, token1: &'a Token, raw_swap_data: RawSwapData) -> Self {
let is_inverted = token0.get_token_priority() < token1.get_token_priority();
Self {
token0,
token1,
is_inverted,
raw_swap_data,
}
}
#[must_use]
pub fn zero_for_one(&self) -> bool {
self.raw_swap_data.amount0.is_positive()
}
pub fn compute(&self, sqrt_price_x96_before: Option<U160>) -> anyhow::Result<SwapTradeInfo> {
let spot_price_before = if let Some(sqrt_price_x96_before) = sqrt_price_x96_before {
Some(decode_sqrt_price_x96_to_price_tokens_adjusted(
sqrt_price_x96_before,
self.token0.decimals,
self.token1.decimals,
self.is_inverted,
)?)
} else {
None
};
Ok(SwapTradeInfo {
order_side: self.order_side(),
quantity_base: self.quantity_base()?,
quantity_quote: self.quantity_quote()?,
spot_price: self.spot_price()?,
execution_price: self.execution_price()?,
is_inverted: self.is_inverted,
spot_price_before,
})
}
#[must_use]
pub fn order_side(&self) -> OrderSide {
let zero_for_one = self.zero_for_one();
if self.is_inverted {
if zero_for_one {
OrderSide::Buy
} else {
OrderSide::Sell
}
} else {
if zero_for_one {
OrderSide::Sell
} else {
OrderSide::Buy
}
}
}
pub fn quantity_base(&self) -> anyhow::Result<Quantity> {
let (amount, precision) = if self.is_inverted {
(
self.raw_swap_data.amount1.unsigned_abs(),
self.token1.decimals,
)
} else {
(
self.raw_swap_data.amount0.unsigned_abs(),
self.token0.decimals,
)
};
Quantity::from_u256(amount, precision).map_err(Into::into)
}
pub fn quantity_quote(&self) -> anyhow::Result<Quantity> {
let (amount, precision) = if self.is_inverted {
(
self.raw_swap_data.amount0.unsigned_abs(),
self.token0.decimals,
)
} else {
(
self.raw_swap_data.amount1.unsigned_abs(),
self.token1.decimals,
)
};
Quantity::from_u256(amount, precision).map_err(Into::into)
}
fn spot_price(&self) -> anyhow::Result<Price> {
decode_sqrt_price_x96_to_price_tokens_adjusted(
self.raw_swap_data.sqrt_price_x96,
self.token0.decimals,
self.token1.decimals,
self.is_inverted, )
}
fn execution_price(&self) -> anyhow::Result<Price> {
let amount0 = self.raw_swap_data.amount0.unsigned_abs();
let amount1 = self.raw_swap_data.amount1.unsigned_abs();
if amount0.is_zero() || amount1.is_zero() {
anyhow::bail!("Cannot calculate execution price with zero amounts");
}
let (quote_amount, base_amount, quote_decimals, base_decimals) = if self.is_inverted {
(amount0, amount1, self.token0.decimals, self.token1.decimals)
} else {
(amount1, amount0, self.token1.decimals, self.token0.decimals)
};
FullMath::check_decimal_exponent(base_decimals)?;
FullMath::check_decimal_exponent(quote_decimals)?;
let exponent =
i16::from(base_decimals) + i16::from(FIXED_PRECISION) - i16::from(quote_decimals);
let price_raw_u256 = if exponent >= 0 {
let exponent = u8::try_from(exponent)
.map_err(|_| anyhow::anyhow!("Decimal exponent {exponent} exceeds u8 range"))?;
let primary_exponent = exponent.min(DECIMAL_EXPONENT_MAX);
let secondary_exponent = exponent - primary_exponent;
let primary_scalar = FullMath::pow10(primary_exponent)?;
let secondary_scalar = FullMath::pow10(secondary_exponent)?;
FullMath::mul_div_scaled(
quote_amount,
U256::from(1),
base_amount,
&[primary_scalar, secondary_scalar],
)?
} else {
let divisor_exponent = u8::try_from(exponent.unsigned_abs())
.map_err(|_| anyhow::anyhow!("Decimal exponent {exponent} exceeds u8 range"))?;
let divisor = FullMath::pow10(divisor_exponent)?;
(quote_amount / base_amount) / divisor
};
price_from_u256(price_raw_u256)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use alloy_primitives::{I256, U160};
use rstest::{fixture, rstest};
use rust_decimal_macros::dec;
use super::*;
use crate::defi::{
stubs::{usdc, weth},
tick_map::{full_math::Q96_U160, tick_math::MAX_SQRT_RATIO},
};
#[fixture]
fn swap_trade_info() -> SwapTradeInfo {
SwapTradeInfo {
order_side: OrderSide::Buy,
quantity_base: Quantity::from("1"),
quantity_quote: Quantity::from("2"),
spot_price: Price::from_raw(2, FIXED_PRECISION),
execution_price: Price::from_raw(3, FIXED_PRECISION),
is_inverted: true,
spot_price_before: Some(Price::from_raw(1, FIXED_PRECISION)),
}
}
#[rstest]
fn test_get_price_impact_bps_rejects_zero_spot_price_before(
mut swap_trade_info: SwapTradeInfo,
) {
swap_trade_info.spot_price_before = Some(Price::zero(FIXED_PRECISION));
let error = swap_trade_info.get_price_impact_bps().unwrap_err();
assert_eq!(
error.to_string(),
"Cannot calculate price impact, the spot price before is zero"
);
}
#[rstest]
fn test_get_slippage_bps_rejects_zero_spot_price_before(mut swap_trade_info: SwapTradeInfo) {
swap_trade_info.spot_price_before = Some(Price::zero(FIXED_PRECISION));
let error = swap_trade_info.get_slippage_bps().unwrap_err();
assert_eq!(
error.to_string(),
"Cannot calculate slippage, the spot price before is zero"
);
}
#[rstest]
fn test_get_price_impact_bps_accepts_smallest_positive_spot_price_before(
swap_trade_info: SwapTradeInfo,
) {
assert_eq!(swap_trade_info.get_price_impact_bps().unwrap(), 10_000);
}
#[rstest]
fn test_get_slippage_bps_accepts_smallest_positive_spot_price_before(
swap_trade_info: SwapTradeInfo,
) {
assert_eq!(swap_trade_info.get_slippage_bps().unwrap(), 20_000);
}
#[rstest]
fn test_swap_trade_info_calculator_calculations_buy(weth: Token, usdc: Token) {
let raw_data = RawSwapData::new(
I256::from_str("-466341596920355889").unwrap(),
I256::from_str("1656236893").unwrap(),
U160::from_str("4720799958938693700000000").unwrap(),
);
let calculator = SwapTradeInfoCalculator::new(&weth, &usdc, raw_data);
let result = calculator.compute(None).unwrap();
assert!(!calculator.is_inverted);
assert_eq!(result.order_side, OrderSide::Buy);
assert_eq!(
result.quantity_base.as_decimal(),
dec!(0.466341596920355889)
);
assert_eq!(result.quantity_quote.as_decimal(), dec!(1656.236893));
assert_eq!(result.spot_price.as_decimal(), dec!(3550.3570265047994091));
assert_eq!(
result.execution_price.as_decimal(),
dec!(3551.5529902061477063)
);
}
#[rstest]
fn test_swap_trade_info_calculator_calculations_sell(weth: Token, usdc: Token) {
let raw_data = RawSwapData::new(
I256::from_str("193450074461093702").unwrap(),
I256::from_str("-691892530").unwrap(),
U160::from_str("4739235524363817533004858").unwrap(),
);
let calculator = SwapTradeInfoCalculator::new(&weth, &usdc, raw_data);
let result = calculator.compute(None).unwrap();
assert_eq!(result.order_side, OrderSide::Sell);
assert_eq!(
result.quantity_base.as_decimal(),
dec!(0.193450074461093702)
);
assert_eq!(result.quantity_quote.as_decimal(), dec!(691.89253));
assert_eq!(result.spot_price.as_decimal(), dec!(3578.1407251651610105));
assert_eq!(
result.execution_price.as_decimal(),
dec!(3576.5947980503469024)
);
}
#[rstest]
fn test_swap_trade_info_calculator_spot_price_overflow_is_recoverable(
weth: Token,
usdc: Token,
) {
let raw_data = RawSwapData::new(
I256::from_str("1").unwrap(),
I256::from_str("-1").unwrap(),
MAX_SQRT_RATIO - U160::from(1),
);
let calculator = SwapTradeInfoCalculator::new(&weth, &usdc, raw_data);
assert!(calculator.compute(None).is_err());
}
#[rstest]
fn test_execution_price_scales_distinct_decimals_in_both_directions(weth: Token, usdc: Token) {
let normal_data = RawSwapData::new(
I256::from_str("-2000000000000000000").unwrap(),
I256::from_str("5000000").unwrap(),
Q96_U160,
);
let inverted_data = RawSwapData::new(
I256::from_str("5000000").unwrap(),
I256::from_str("-2000000000000000000").unwrap(),
Q96_U160,
);
let normal = SwapTradeInfoCalculator::new(&weth, &usdc, normal_data)
.execution_price()
.unwrap();
let inverted = SwapTradeInfoCalculator::new(&usdc, &weth, inverted_data)
.execution_price()
.unwrap();
let expected = Price::from_raw(25_000_000_000_000_000, FIXED_PRECISION);
assert_eq!(normal, expected);
assert_eq!(inverted, expected);
}
#[rstest]
fn test_execution_price_scales_negative_net_exponent(mut weth: Token, mut usdc: Token) {
weth.decimals = 0;
usdc.decimals = 18;
let raw_data = RawSwapData::new(
I256::from_str("1").unwrap(),
I256::from_str("-100").unwrap(),
Q96_U160,
);
let result = SwapTradeInfoCalculator::new(&weth, &usdc, raw_data)
.execution_price()
.unwrap();
assert_eq!(result, Price::from_raw(1, FIXED_PRECISION));
}
#[rstest]
fn test_execution_price_accepts_largest_decimal_exponent(mut weth: Token, mut usdc: Token) {
weth.decimals = DECIMAL_EXPONENT_MAX;
usdc.decimals = 0;
let raw_data = RawSwapData::new(
I256::from_raw(FullMath::pow10(76).unwrap()),
I256::from_str("-1").unwrap(),
Q96_U160,
);
let result = SwapTradeInfoCalculator::new(&weth, &usdc, raw_data)
.execution_price()
.unwrap();
assert_eq!(
result,
Price::from_raw(100_000_000_000_000_000, FIXED_PRECISION)
);
}
#[rstest]
fn test_execution_price_rejects_first_unsupported_decimal_exponent(
mut weth: Token,
mut usdc: Token,
) {
weth.decimals = DECIMAL_EXPONENT_MAX + 1;
usdc.decimals = 0;
let raw_data = RawSwapData::new(
I256::from_str("1").unwrap(),
I256::from_str("-1").unwrap(),
Q96_U160,
);
let error = SwapTradeInfoCalculator::new(&weth, &usdc, raw_data)
.execution_price()
.unwrap_err();
assert_eq!(
error.to_string(),
"Decimal exponent 78 exceeds supported maximum 77"
);
}
}