use ethnum::U256;
use crate::{tick_math::tick_to_sqrt_price_x64, AmmMathError};
pub fn get_amount_0_delta(
sqrt_price_a: u128,
sqrt_price_b: u128,
liquidity: u128,
round_up: bool,
) -> Result<u64, AmmMathError> {
if sqrt_price_a == 0 || sqrt_price_b == 0 {
return Err(AmmMathError::DivisionByZero);
}
let (lower, upper) = if sqrt_price_a <= sqrt_price_b {
(sqrt_price_a, sqrt_price_b)
} else {
(sqrt_price_b, sqrt_price_a)
};
let diff = upper - lower;
if diff == 0 || liquidity == 0 {
return Ok(0);
}
let numerator = U256::from(liquidity) * U256::from(diff) * U256::from(1u128 << 64);
let denominator = U256::from(lower) * U256::from(upper);
let result = if round_up {
(numerator + denominator - U256::from(1u128)) / denominator
} else {
numerator / denominator
};
if result > U256::from(u64::MAX) {
return Err(AmmMathError::Overflow);
}
Ok(result.as_u128() as u64)
}
pub fn get_amount_1_delta(
sqrt_price_a: u128,
sqrt_price_b: u128,
liquidity: u128,
round_up: bool,
) -> Result<u64, AmmMathError> {
let (lower, upper) = if sqrt_price_a <= sqrt_price_b {
(sqrt_price_a, sqrt_price_b)
} else {
(sqrt_price_b, sqrt_price_a)
};
let diff = upper - lower;
if diff == 0 || liquidity == 0 {
return Ok(0);
}
let numerator = U256::from(liquidity) * U256::from(diff);
let denominator = U256::from(1u128 << 64);
let result = if round_up {
(numerator + denominator - U256::from(1u128)) / denominator
} else {
numerator / denominator
};
if result > U256::from(u64::MAX) {
return Err(AmmMathError::Overflow);
}
Ok(result.as_u128() as u64)
}
pub fn token_amounts_from_liquidity(
liquidity: u128,
sqrt_price_x64: u128,
tick_lower: i32,
tick_upper: i32,
round_up: bool,
) -> Result<(u64, u64), AmmMathError> {
let sqrt_price_lower_x64 = tick_to_sqrt_price_x64(tick_lower)?;
let sqrt_price_upper_x64 = tick_to_sqrt_price_x64(tick_upper)?;
token_amounts_from_liquidity_with_sqrt_prices(
liquidity,
sqrt_price_x64,
sqrt_price_lower_x64,
sqrt_price_upper_x64,
round_up,
)
}
pub fn token_amounts_from_liquidity_with_sqrt_prices(
liquidity: u128,
sqrt_price_x64: u128,
sqrt_price_lower_x64: u128,
sqrt_price_upper_x64: u128,
round_up: bool,
) -> Result<(u64, u64), AmmMathError> {
if liquidity == 0 {
return Ok((0, 0));
}
let amount_a;
let amount_b;
if sqrt_price_x64 <= sqrt_price_lower_x64 {
amount_a =
get_amount_0_delta(sqrt_price_lower_x64, sqrt_price_upper_x64, liquidity, round_up)?;
amount_b = 0;
} else if sqrt_price_x64 >= sqrt_price_upper_x64 {
amount_a = 0;
amount_b =
get_amount_1_delta(sqrt_price_lower_x64, sqrt_price_upper_x64, liquidity, round_up)?;
} else {
amount_a = get_amount_0_delta(sqrt_price_x64, sqrt_price_upper_x64, liquidity, round_up)?;
amount_b = get_amount_1_delta(sqrt_price_lower_x64, sqrt_price_x64, liquidity, round_up)?;
}
Ok((amount_a, amount_b))
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub struct TransferFee {
pub fee_bps: u16,
pub max_fee: u64,
}
impl TransferFee {
pub fn new(fee_bps: u16) -> Self {
Self { fee_bps, max_fee: u64::MAX }
}
pub fn new_with_max(fee_bps: u16, max_fee: u64) -> Self {
Self { fee_bps, max_fee }
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub struct TickRange {
pub tick_lower_index: i32,
pub tick_upper_index: i32,
}
pub fn order_tick_indexes(tick_index_1: i32, tick_index_2: i32) -> TickRange {
if tick_index_1 < tick_index_2 {
TickRange { tick_lower_index: tick_index_1, tick_upper_index: tick_index_2 }
} else {
TickRange { tick_lower_index: tick_index_2, tick_upper_index: tick_index_1 }
}
}
pub type QuoteError = &'static str;
pub const ARITHMETIC_OVERFLOW: QuoteError = "Arithmetic over- or underflow";
pub const AMOUNT_EXCEEDS_MAX_U64: QuoteError = "Amount exceeds max u64";
pub const INVALID_TRANSFER_FEE: QuoteError = "Invalid transfer fee";
pub const INVALID_SLIPPAGE_TOLERANCE: QuoteError = "Invalid slippage tolerance";
const BPS_DENOMINATOR: u16 = 10000;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub struct IncreaseLiquidityQuote {
pub liquidity_delta: u128,
pub token_est_a: u64,
pub token_est_b: u64,
pub token_max_a: u64,
pub token_max_b: u64,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub struct DecreaseLiquidityQuote {
pub liquidity_delta: u128,
pub token_est_a: u64,
pub token_est_b: u64,
pub token_min_a: u64,
pub token_min_b: u64,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum PositionStatus {
PriceInRange,
PriceBelowRange,
PriceAboveRange,
Invalid,
}
fn position_status(
current_sqrt_price: u128,
tick_index_1: i32,
tick_index_2: i32,
) -> PositionStatus {
let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
let sqrt_price_lower = tick_to_sqrt_price_x64(tick_range.tick_lower_index).unwrap_or(0);
let sqrt_price_upper = tick_to_sqrt_price_x64(tick_range.tick_upper_index).unwrap_or(u128::MAX);
if tick_index_1 == tick_index_2 {
PositionStatus::Invalid
} else if current_sqrt_price <= sqrt_price_lower {
PositionStatus::PriceBelowRange
} else if current_sqrt_price >= sqrt_price_upper {
PositionStatus::PriceAboveRange
} else {
PositionStatus::PriceInRange
}
}
fn try_apply_transfer_fee(amount: u64, tf: TransferFee) -> Result<u64, QuoteError> {
if tf.fee_bps > BPS_DENOMINATOR {
return Err(INVALID_TRANSFER_FEE);
}
if tf.fee_bps == 0 || amount == 0 {
return Ok(amount);
}
let numerator = (amount as u128).checked_mul(tf.fee_bps as u128).ok_or(ARITHMETIC_OVERFLOW)?;
let raw_fee: u64 = numerator
.div_ceil(BPS_DENOMINATOR as u128)
.try_into()
.map_err(|_| AMOUNT_EXCEEDS_MAX_U64)?;
let fee = raw_fee.min(tf.max_fee);
Ok(amount - fee)
}
fn try_reverse_apply_transfer_fee(amount: u64, tf: TransferFee) -> Result<u64, QuoteError> {
if tf.fee_bps > BPS_DENOMINATOR {
Err(INVALID_TRANSFER_FEE)
} else if tf.fee_bps == 0 {
Ok(amount)
} else if amount == 0 {
Ok(0)
} else if tf.fee_bps == BPS_DENOMINATOR {
amount.checked_add(tf.max_fee).ok_or(AMOUNT_EXCEEDS_MAX_U64)
} else {
let numerator =
(amount as u128).checked_mul(BPS_DENOMINATOR as u128).ok_or(ARITHMETIC_OVERFLOW)?;
let denominator = (BPS_DENOMINATOR as u128) - (tf.fee_bps as u128);
let raw_pre_fee_amount = numerator.div_ceil(denominator);
let fee_amount =
raw_pre_fee_amount.checked_sub(amount as u128).ok_or(AMOUNT_EXCEEDS_MAX_U64)?;
if fee_amount >= tf.max_fee as u128 {
amount.checked_add(tf.max_fee).ok_or(AMOUNT_EXCEEDS_MAX_U64)
} else {
raw_pre_fee_amount.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
}
}
}
fn try_mul_div(
amount: u64,
product: u128,
denominator: u128,
round_up: bool,
) -> Result<u64, QuoteError> {
if amount == 0 || product == 0 {
return Ok(0);
}
let numerator = (amount as u128).checked_mul(product).ok_or(ARITHMETIC_OVERFLOW)?;
let quotient = numerator / denominator;
let remainder = numerator % denominator;
let result = if round_up && remainder != 0 { quotient + 1 } else { quotient };
result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
}
fn try_get_max_amount_with_slippage(amount: u64, slippage_bps: u16) -> Result<u64, QuoteError> {
if slippage_bps > BPS_DENOMINATOR {
return Err(INVALID_SLIPPAGE_TOLERANCE);
}
let product = (BPS_DENOMINATOR as u128) + (slippage_bps as u128);
try_mul_div(amount, product, BPS_DENOMINATOR as u128, true)
}
fn try_get_min_amount_with_slippage(amount: u64, slippage_bps: u16) -> Result<u64, QuoteError> {
if slippage_bps > BPS_DENOMINATOR {
return Err(INVALID_SLIPPAGE_TOLERANCE);
}
let product = (BPS_DENOMINATOR as u128) - (slippage_bps as u128);
try_mul_div(amount, product, BPS_DENOMINATOR as u128, false)
}
fn try_get_token_a_from_liquidity(
liquidity_delta: u128,
sqrt_price_lower: u128,
sqrt_price_upper: u128,
round_up: bool,
) -> Result<u64, QuoteError> {
let sqrt_price_diff =
sqrt_price_upper.checked_sub(sqrt_price_lower).ok_or(ARITHMETIC_OVERFLOW)?;
if sqrt_price_lower == 0 || sqrt_price_upper == 0 {
return Err(ARITHMETIC_OVERFLOW);
}
let numerator: U256 = U256::from(liquidity_delta)
.checked_mul(sqrt_price_diff.into())
.ok_or(ARITHMETIC_OVERFLOW)?
.checked_shl(64)
.ok_or(ARITHMETIC_OVERFLOW)?;
let denominator = U256::from(sqrt_price_upper)
.checked_mul(U256::from(sqrt_price_lower))
.ok_or(ARITHMETIC_OVERFLOW)?;
if denominator == U256::ZERO {
return Err(ARITHMETIC_OVERFLOW);
}
let quotient = numerator / denominator;
let remainder = numerator % denominator;
let result = if round_up && remainder != U256::ZERO { quotient + 1 } else { quotient };
result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
}
fn try_get_token_b_from_liquidity(
liquidity_delta: u128,
sqrt_price_lower: u128,
sqrt_price_upper: u128,
round_up: bool,
) -> Result<u64, QuoteError> {
let sqrt_price_diff =
sqrt_price_upper.checked_sub(sqrt_price_lower).ok_or(ARITHMETIC_OVERFLOW)?;
let product: U256 = U256::from(liquidity_delta)
.checked_mul(sqrt_price_diff.into())
.ok_or(ARITHMETIC_OVERFLOW)?;
let quotient: U256 = product >> 64;
let should_round = round_up && product & U256::from(u64::MAX) > U256::ZERO;
let result = if should_round { quotient + 1 } else { quotient };
result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
}
fn try_get_liquidity_from_a(
token_delta_a: u64,
sqrt_price_lower: u128,
sqrt_price_upper: u128,
) -> Result<u128, QuoteError> {
let sqrt_price_diff =
sqrt_price_upper.checked_sub(sqrt_price_lower).ok_or(ARITHMETIC_OVERFLOW)?;
if sqrt_price_diff == 0 {
return Err(ARITHMETIC_OVERFLOW);
}
let mul: U256 = U256::from(token_delta_a)
.checked_mul(sqrt_price_lower.into())
.ok_or(ARITHMETIC_OVERFLOW)?
.checked_mul(sqrt_price_upper.into())
.ok_or(ARITHMETIC_OVERFLOW)?;
let result: U256 = (mul / U256::from(sqrt_price_diff)) >> 64;
result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
}
fn try_get_liquidity_from_b(
token_delta_b: u64,
sqrt_price_lower: u128,
sqrt_price_upper: u128,
) -> Result<u128, QuoteError> {
let numerator: U256 = U256::from(token_delta_b).checked_shl(64).ok_or(ARITHMETIC_OVERFLOW)?;
let sqrt_price_diff =
sqrt_price_upper.checked_sub(sqrt_price_lower).ok_or(ARITHMETIC_OVERFLOW)?;
if sqrt_price_diff == 0 {
return Err(ARITHMETIC_OVERFLOW);
}
let result = numerator / U256::from(sqrt_price_diff);
result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
}
fn try_get_token_estimates_from_liquidity(
liquidity_delta: u128,
current_sqrt_price: u128,
tick_lower_index: i32,
tick_upper_index: i32,
round_up: bool,
) -> Result<(u64, u64), QuoteError> {
if liquidity_delta == 0 {
return Ok((0, 0));
}
let sqrt_price_lower =
tick_to_sqrt_price_x64(tick_lower_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
let sqrt_price_upper =
tick_to_sqrt_price_x64(tick_upper_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
let status = position_status(current_sqrt_price, tick_lower_index, tick_upper_index);
match status {
PositionStatus::PriceBelowRange => {
let token_a = try_get_token_a_from_liquidity(
liquidity_delta,
sqrt_price_lower,
sqrt_price_upper,
round_up,
)?;
Ok((token_a, 0))
}
PositionStatus::PriceInRange => {
let token_a = try_get_token_a_from_liquidity(
liquidity_delta,
current_sqrt_price,
sqrt_price_upper,
round_up,
)?;
let token_b = try_get_token_b_from_liquidity(
liquidity_delta,
sqrt_price_lower,
current_sqrt_price,
round_up,
)?;
Ok((token_a, token_b))
}
PositionStatus::PriceAboveRange => {
let token_b = try_get_token_b_from_liquidity(
liquidity_delta,
sqrt_price_lower,
sqrt_price_upper,
round_up,
)?;
Ok((0, token_b))
}
PositionStatus::Invalid => Ok((0, 0)),
}
}
pub fn increase_liquidity_quote(
liquidity_delta: u128,
slippage_tolerance_bps: u16,
current_sqrt_price: u128,
tick_index_1: i32,
tick_index_2: i32,
transfer_fee_a: Option<TransferFee>,
transfer_fee_b: Option<TransferFee>,
) -> Result<IncreaseLiquidityQuote, QuoteError> {
if liquidity_delta == 0 {
return Ok(IncreaseLiquidityQuote::default());
}
let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
let (token_est_before_fees_a, token_est_before_fees_b) =
try_get_token_estimates_from_liquidity(
liquidity_delta,
current_sqrt_price,
tick_range.tick_lower_index,
tick_range.tick_upper_index,
true,
)?;
let token_est_a = try_reverse_apply_transfer_fee(
token_est_before_fees_a,
transfer_fee_a.unwrap_or_default(),
)?;
let token_est_b = try_reverse_apply_transfer_fee(
token_est_before_fees_b,
transfer_fee_b.unwrap_or_default(),
)?;
let token_max_a = try_get_max_amount_with_slippage(token_est_a, slippage_tolerance_bps)?;
let token_max_b = try_get_max_amount_with_slippage(token_est_b, slippage_tolerance_bps)?;
Ok(IncreaseLiquidityQuote {
liquidity_delta,
token_est_a,
token_est_b,
token_max_a,
token_max_b,
})
}
pub fn increase_liquidity_quote_a(
token_amount_a: u64,
slippage_tolerance_bps: u16,
current_sqrt_price: u128,
tick_index_1: i32,
tick_index_2: i32,
transfer_fee_a: Option<TransferFee>,
transfer_fee_b: Option<TransferFee>,
) -> Result<IncreaseLiquidityQuote, QuoteError> {
let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
let token_delta_a = try_apply_transfer_fee(token_amount_a, transfer_fee_a.unwrap_or_default())?;
if token_delta_a == 0 {
return Ok(IncreaseLiquidityQuote::default());
}
let sqrt_price_lower =
tick_to_sqrt_price_x64(tick_range.tick_lower_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
let sqrt_price_upper =
tick_to_sqrt_price_x64(tick_range.tick_upper_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
let status = position_status(current_sqrt_price, tick_index_1, tick_index_2);
let liquidity: u128 = match status {
PositionStatus::PriceBelowRange => {
try_get_liquidity_from_a(token_delta_a, sqrt_price_lower, sqrt_price_upper)?
}
PositionStatus::Invalid | PositionStatus::PriceAboveRange => 0,
PositionStatus::PriceInRange => {
try_get_liquidity_from_a(token_delta_a, current_sqrt_price, sqrt_price_upper)?
}
};
increase_liquidity_quote(
liquidity,
slippage_tolerance_bps,
current_sqrt_price,
tick_index_1,
tick_index_2,
transfer_fee_a,
transfer_fee_b,
)
}
pub fn increase_liquidity_quote_b(
token_amount_b: u64,
slippage_tolerance_bps: u16,
current_sqrt_price: u128,
tick_index_1: i32,
tick_index_2: i32,
transfer_fee_a: Option<TransferFee>,
transfer_fee_b: Option<TransferFee>,
) -> Result<IncreaseLiquidityQuote, QuoteError> {
let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
let token_delta_b = try_apply_transfer_fee(token_amount_b, transfer_fee_b.unwrap_or_default())?;
if token_delta_b == 0 {
return Ok(IncreaseLiquidityQuote::default());
}
let sqrt_price_lower =
tick_to_sqrt_price_x64(tick_range.tick_lower_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
let sqrt_price_upper =
tick_to_sqrt_price_x64(tick_range.tick_upper_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
let status = position_status(current_sqrt_price, tick_index_1, tick_index_2);
let liquidity: u128 = match status {
PositionStatus::Invalid | PositionStatus::PriceBelowRange => 0,
PositionStatus::PriceAboveRange => {
try_get_liquidity_from_b(token_delta_b, sqrt_price_lower, sqrt_price_upper)?
}
PositionStatus::PriceInRange => {
try_get_liquidity_from_b(token_delta_b, sqrt_price_lower, current_sqrt_price)?
}
};
increase_liquidity_quote(
liquidity,
slippage_tolerance_bps,
current_sqrt_price,
tick_index_1,
tick_index_2,
transfer_fee_a,
transfer_fee_b,
)
}
pub fn decrease_liquidity_quote(
liquidity_delta: u128,
slippage_tolerance_bps: u16,
current_sqrt_price: u128,
tick_index_1: i32,
tick_index_2: i32,
transfer_fee_a: Option<TransferFee>,
transfer_fee_b: Option<TransferFee>,
) -> Result<DecreaseLiquidityQuote, QuoteError> {
if liquidity_delta == 0 {
return Ok(DecreaseLiquidityQuote::default());
}
let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
let (token_est_before_fees_a, token_est_before_fees_b) =
try_get_token_estimates_from_liquidity(
liquidity_delta,
current_sqrt_price,
tick_range.tick_lower_index,
tick_range.tick_upper_index,
false,
)?;
let token_est_a =
try_apply_transfer_fee(token_est_before_fees_a, transfer_fee_a.unwrap_or_default())?;
let token_est_b =
try_apply_transfer_fee(token_est_before_fees_b, transfer_fee_b.unwrap_or_default())?;
let token_min_a = try_get_min_amount_with_slippage(token_est_a, slippage_tolerance_bps)?;
let token_min_b = try_get_min_amount_with_slippage(token_est_b, slippage_tolerance_bps)?;
Ok(DecreaseLiquidityQuote {
liquidity_delta,
token_est_a,
token_est_b,
token_min_a,
token_min_b,
})
}
pub fn decrease_liquidity_quote_a(
token_amount_a: u64,
slippage_tolerance_bps: u16,
current_sqrt_price: u128,
tick_index_1: i32,
tick_index_2: i32,
transfer_fee_a: Option<TransferFee>,
transfer_fee_b: Option<TransferFee>,
) -> Result<DecreaseLiquidityQuote, QuoteError> {
let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
let token_delta_a =
try_reverse_apply_transfer_fee(token_amount_a, transfer_fee_a.unwrap_or_default())?;
if token_delta_a == 0 {
return Ok(DecreaseLiquidityQuote::default());
}
let sqrt_price_lower =
tick_to_sqrt_price_x64(tick_range.tick_lower_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
let sqrt_price_upper =
tick_to_sqrt_price_x64(tick_range.tick_upper_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
let status = position_status(current_sqrt_price, tick_index_1, tick_index_2);
let liquidity: u128 = match status {
PositionStatus::PriceBelowRange => {
try_get_liquidity_from_a(token_delta_a, sqrt_price_lower, sqrt_price_upper)?
}
PositionStatus::Invalid | PositionStatus::PriceAboveRange => 0,
PositionStatus::PriceInRange => {
try_get_liquidity_from_a(token_delta_a, current_sqrt_price, sqrt_price_upper)?
}
};
decrease_liquidity_quote(
liquidity,
slippage_tolerance_bps,
current_sqrt_price,
tick_index_1,
tick_index_2,
transfer_fee_a,
transfer_fee_b,
)
}
pub fn decrease_liquidity_quote_b(
token_amount_b: u64,
slippage_tolerance_bps: u16,
current_sqrt_price: u128,
tick_index_1: i32,
tick_index_2: i32,
transfer_fee_a: Option<TransferFee>,
transfer_fee_b: Option<TransferFee>,
) -> Result<DecreaseLiquidityQuote, QuoteError> {
let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
let token_delta_b =
try_reverse_apply_transfer_fee(token_amount_b, transfer_fee_b.unwrap_or_default())?;
if token_delta_b == 0 {
return Ok(DecreaseLiquidityQuote::default());
}
let sqrt_price_lower =
tick_to_sqrt_price_x64(tick_range.tick_lower_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
let sqrt_price_upper =
tick_to_sqrt_price_x64(tick_range.tick_upper_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
let status = position_status(current_sqrt_price, tick_index_1, tick_index_2);
let liquidity: u128 = match status {
PositionStatus::Invalid | PositionStatus::PriceBelowRange => 0,
PositionStatus::PriceAboveRange => {
try_get_liquidity_from_b(token_delta_b, sqrt_price_lower, sqrt_price_upper)?
}
PositionStatus::PriceInRange => {
try_get_liquidity_from_b(token_delta_b, sqrt_price_lower, current_sqrt_price)?
}
};
decrease_liquidity_quote(
liquidity,
slippage_tolerance_bps,
current_sqrt_price,
tick_index_1,
tick_index_2,
transfer_fee_a,
transfer_fee_b,
)
}
#[cfg(test)]
mod tests {
use super::*;
const Q64: u128 = 1u128 << 64;
#[test]
fn test_amount_0_delta_basic() {
let price_a = Q64; let price_b = Q64 * 2; let liquidity = 1_000_000u128;
let amount = get_amount_0_delta(price_a, price_b, liquidity, false).unwrap();
assert_eq!(amount, 500_000);
}
#[test]
fn test_amount_1_delta_basic() {
let price_a = Q64; let price_b = Q64 * 2; let liquidity = 1_000_000u128;
let amount = get_amount_1_delta(price_a, price_b, liquidity, false).unwrap();
assert_eq!(amount, 1_000_000);
}
#[test]
fn test_zero_liquidity() {
assert_eq!(get_amount_0_delta(Q64, Q64 * 2, 0, false).unwrap(), 0);
assert_eq!(get_amount_1_delta(Q64, Q64 * 2, 0, false).unwrap(), 0);
}
#[test]
fn test_same_price() {
assert_eq!(get_amount_0_delta(Q64, Q64, 1_000_000, false).unwrap(), 0);
assert_eq!(get_amount_1_delta(Q64, Q64, 1_000_000, false).unwrap(), 0);
}
#[test]
fn test_rounding() {
let price_a = Q64;
let price_b = Q64 * 3;
let liquidity = 100u128;
let floor = get_amount_0_delta(price_a, price_b, liquidity, false).unwrap();
let ceil = get_amount_0_delta(price_a, price_b, liquidity, true).unwrap();
assert!(ceil >= floor);
let floor1 = get_amount_1_delta(price_a, price_b, liquidity, false).unwrap();
let ceil1 = get_amount_1_delta(price_a, price_b, liquidity, true).unwrap();
assert!(ceil1 >= floor1);
}
#[test]
fn test_price_order_invariant() {
let price_a = Q64;
let price_b = Q64 * 2;
let liquidity = 1_000_000u128;
let a0 = get_amount_0_delta(price_a, price_b, liquidity, false).unwrap();
let a0_rev = get_amount_0_delta(price_b, price_a, liquidity, false).unwrap();
assert_eq!(a0, a0_rev);
}
#[test]
fn test_zero_sqrt_price_errors() {
assert!(get_amount_0_delta(0, Q64, 1000, false).is_err());
assert!(get_amount_0_delta(Q64, 0, 1000, false).is_err());
}
#[test]
fn amount_delta_known_values() {
use crate::tick_math::tick_to_sqrt_price_x64;
let sqrt_a = tick_to_sqrt_price_x64(-100).unwrap();
let sqrt_b = tick_to_sqrt_price_x64(100).unwrap();
let liquidity = 1_000_000_000u128;
let amount0 = get_amount_0_delta(sqrt_a, sqrt_b, liquidity, false).unwrap();
let amount1 = get_amount_1_delta(sqrt_a, sqrt_b, liquidity, false).unwrap();
assert!(amount0 > 0 && amount1 > 0);
}
#[test]
fn zero_liquidity_returns_zero() {
use crate::tick_math::tick_to_sqrt_price_x64;
let sqrt_a = tick_to_sqrt_price_x64(0).unwrap();
let sqrt_b = tick_to_sqrt_price_x64(100).unwrap();
assert_eq!(get_amount_0_delta(sqrt_a, sqrt_b, 0, false).unwrap(), 0);
assert_eq!(get_amount_1_delta(sqrt_a, sqrt_b, 0, false).unwrap(), 0);
}
#[test]
fn test_large_liquidity_no_false_overflow() {
let liquidity = 1u128 << 64;
let sqrt_price_a = Q64; let sqrt_price_b = Q64 * 2;
let amount = get_amount_0_delta(sqrt_price_a, sqrt_price_b, liquidity, false).unwrap();
assert_eq!(amount, 1u64 << 63);
let amount_up = get_amount_0_delta(sqrt_price_a, sqrt_price_b, liquidity, true).unwrap();
assert_eq!(amount_up, 1u64 << 63); }
#[test]
fn test_large_liquidity_amount_1_no_false_overflow() {
let liquidity = 1u128 << 64;
let sqrt_price_a = Q64;
let sqrt_price_b = Q64 + (1u128 << 63);
let amount = get_amount_1_delta(sqrt_price_a, sqrt_price_b, liquidity, false).unwrap();
assert_eq!(amount, 1u64 << 63);
}
#[test]
fn test_token_amounts_zero_liquidity() {
let (a, b) = token_amounts_from_liquidity(0, Q64, -100, 100, false).unwrap();
assert_eq!(a, 0);
assert_eq!(b, 0);
}
#[test]
fn test_token_amounts_price_in_range() {
let sqrt_price = tick_to_sqrt_price_x64(0).unwrap();
let liquidity = 1_000_000_000u128;
let (a, b) = token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
assert!(a > 0, "amount_a should be > 0, got {}", a);
assert!(b > 0, "amount_b should be > 0, got {}", b);
}
#[test]
fn test_token_amounts_price_below_range() {
let sqrt_price = tick_to_sqrt_price_x64(-200).unwrap();
let liquidity = 1_000_000_000u128;
let (a, b) = token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
assert!(a > 0, "amount_a should be > 0, got {}", a);
assert_eq!(b, 0, "amount_b should be 0 when price below range");
}
#[test]
fn test_token_amounts_price_above_range() {
let sqrt_price = tick_to_sqrt_price_x64(200).unwrap();
let liquidity = 1_000_000_000u128;
let (a, b) = token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
assert_eq!(a, 0, "amount_a should be 0 when price above range");
assert!(b > 0, "amount_b should be > 0, got {}", b);
}
#[test]
fn test_token_amounts_rounding() {
let sqrt_price = tick_to_sqrt_price_x64(0).unwrap();
let liquidity = 100u128;
let (a_floor, b_floor) =
token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
let (a_ceil, b_ceil) =
token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, true).unwrap();
assert!(a_ceil >= a_floor);
assert!(b_ceil >= b_floor);
}
#[test]
fn test_token_amounts_price_at_lower_boundary() {
let sqrt_price = tick_to_sqrt_price_x64(-100).unwrap();
let liquidity = 1_000_000_000u128;
let (a, b) = token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
assert!(a > 0);
assert_eq!(b, 0);
}
#[test]
fn test_token_amounts_price_at_upper_boundary() {
let sqrt_price = tick_to_sqrt_price_x64(100).unwrap();
let liquidity = 1_000_000_000u128;
let (a, b) = token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
assert_eq!(a, 0);
assert!(b > 0);
}
#[test]
fn test_token_amounts_with_sqrt_prices_variant() {
let sqrt_price = tick_to_sqrt_price_x64(0).unwrap();
let sqrt_lower = tick_to_sqrt_price_x64(-100).unwrap();
let sqrt_upper = tick_to_sqrt_price_x64(100).unwrap();
let liquidity = 1_000_000_000u128;
let (a1, b1) =
token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
let (a2, b2) = token_amounts_from_liquidity_with_sqrt_prices(
liquidity, sqrt_price, sqrt_lower, sqrt_upper, false,
)
.unwrap();
assert_eq!(a1, a2);
assert_eq!(b1, b2);
}
#[test]
fn fuzz_amount_0_delta_no_panic() {
use rand::Rng;
use crate::tick_math::{MAX_SQRT_PRICE, MIN_SQRT_PRICE};
let mut rng = rand::rng();
for _ in 0..1000 {
let sqrt_price_a: u128 = rng.random_range(MIN_SQRT_PRICE..=MAX_SQRT_PRICE);
let sqrt_price_b: u128 = rng.random_range(MIN_SQRT_PRICE..=MAX_SQRT_PRICE);
let liquidity: u128 = rng.random_range(1..=1_000_000_000_000u128);
let round_up = rng.random_range(0u8..=1) == 1;
let _ = get_amount_0_delta(sqrt_price_a, sqrt_price_b, liquidity, round_up);
}
}
#[test]
fn fuzz_amount_1_delta_no_panic() {
use rand::Rng;
use crate::tick_math::{MAX_SQRT_PRICE, MIN_SQRT_PRICE};
let mut rng = rand::rng();
for _ in 0..1000 {
let sqrt_price_a: u128 = rng.random_range(MIN_SQRT_PRICE..=MAX_SQRT_PRICE);
let sqrt_price_b: u128 = rng.random_range(MIN_SQRT_PRICE..=MAX_SQRT_PRICE);
let liquidity: u128 = rng.random_range(1..=1_000_000_000_000u128);
let round_up = rng.random_range(0u8..=1) == 1;
let _ = get_amount_1_delta(sqrt_price_a, sqrt_price_b, liquidity, round_up);
}
}
#[test]
fn fuzz_amount_delta_rounding_direction() {
use rand::Rng;
use crate::tick_math::{MAX_SQRT_PRICE, MIN_SQRT_PRICE};
let mut rng = rand::rng();
for _ in 0..1000 {
let sqrt_price_a: u128 = rng.random_range(MIN_SQRT_PRICE..=MAX_SQRT_PRICE);
let sqrt_price_b: u128 = rng.random_range(MIN_SQRT_PRICE..=MAX_SQRT_PRICE);
let liquidity: u128 = rng.random_range(1..=1_000_000_000u128);
if let (Ok(down_0), Ok(up_0)) = (
get_amount_0_delta(sqrt_price_a, sqrt_price_b, liquidity, false),
get_amount_0_delta(sqrt_price_a, sqrt_price_b, liquidity, true),
) {
assert!(
up_0 >= down_0,
"round_up < round_down for amount_0: a={sqrt_price_a}, b={sqrt_price_b}, \
liq={liquidity}, down={down_0}, up={up_0}"
);
}
if let (Ok(down_1), Ok(up_1)) = (
get_amount_1_delta(sqrt_price_a, sqrt_price_b, liquidity, false),
get_amount_1_delta(sqrt_price_a, sqrt_price_b, liquidity, true),
) {
assert!(
up_1 >= down_1,
"round_up < round_down for amount_1: a={sqrt_price_a}, b={sqrt_price_b}, \
liq={liquidity}, down={down_1}, up={up_1}"
);
}
}
}
#[test]
fn fuzz_token_amounts_from_liquidity_no_panic() {
use rand::Rng;
use crate::tick_math::{MAX_TICK, MIN_TICK};
let mut rng = rand::rng();
for _ in 0..1000 {
let tick_a: i32 = rng.random_range(MIN_TICK..=MAX_TICK);
let tick_b: i32 = rng.random_range(MIN_TICK..=MAX_TICK);
let (tick_lower, tick_upper) =
if tick_a <= tick_b { (tick_a, tick_b) } else { (tick_b, tick_a) };
if tick_lower == tick_upper {
continue;
}
let current_tick: i32 = rng.random_range(MIN_TICK..=MAX_TICK);
let sqrt_price = tick_to_sqrt_price_x64(current_tick).unwrap();
let liquidity: u128 = rng.random_range(0..=1_000_000_000u128);
let round_up = rng.random_range(0u8..=1) == 1;
let _ = token_amounts_from_liquidity(
liquidity, sqrt_price, tick_lower, tick_upper, round_up,
);
}
}
}