wp-solana-amm-math 0.1.1

Protocol-agnostic AMM math for Solana DEX — tick pricing, bin pricing, liquidity math, swap simulation
Documentation
//! Slippage and transfer fee utilities.

use crate::AmmMathError;

/// Apply a transfer fee to an amount (returns the post-fee amount).
///
/// `fee_bps` is in basis points (e.g. 100 = 1%). The fee is capped at
/// `max_fee`. Returns the amount the recipient receives after deduction.
pub fn apply_transfer_fee(amount: u64, fee_bps: u16, max_fee: u64) -> Result<u64, AmmMathError> {
    if fee_bps == 0 || amount == 0 {
        return Ok(amount);
    }
    if fee_bps > 10_000 {
        return Err(AmmMathError::InvalidFeeRate(fee_bps));
    }
    let fee = ((amount as u128) * fee_bps as u128 / 10_000).min(max_fee as u128) as u64;
    amount.checked_sub(fee).ok_or(AmmMathError::Overflow)
}

/// Reverse-apply a transfer fee: given the post-fee amount, compute
/// the pre-fee amount that would have produced it.
///
/// Uses ceiling division so the pre-fee amount is never underestimated.
pub fn reverse_apply_transfer_fee(
    post_fee_amount: u64,
    fee_bps: u16,
    max_fee: u64,
) -> Result<u64, AmmMathError> {
    if fee_bps == 0 || post_fee_amount == 0 {
        return Ok(post_fee_amount);
    }
    if fee_bps >= 10_000 {
        return Err(AmmMathError::InvalidFeeRate(fee_bps));
    }
    let denom = 10_000u128 - fee_bps as u128;
    // ceiling division: pre = ceil(post * 10_000 / denom)
    let pre = (post_fee_amount as u128 * 10_000).div_ceil(denom);
    let fee = pre.saturating_sub(post_fee_amount as u128).min(max_fee as u128);
    Ok((post_fee_amount as u128 + fee) as u64)
}

/// Compute the minimum acceptable amount after slippage.
///
/// `slippage_bps` is in basis points (e.g. 50 = 0.5%).
pub fn min_amount_with_slippage(amount: u64, slippage_bps: u16) -> u64 {
    if slippage_bps >= 10_000 {
        return 0;
    }
    let reduction = (amount as u128) * slippage_bps as u128 / 10_000;
    amount.saturating_sub(reduction as u64)
}

/// Compute the maximum acceptable amount after slippage.
///
/// `slippage_bps` is in basis points (e.g. 50 = 0.5%).
pub fn max_amount_with_slippage(amount: u64, slippage_bps: u16) -> u64 {
    let increase = (amount as u128) * slippage_bps as u128 / 10_000;
    (amount as u128 + increase).min(u64::MAX as u128) as u64
}

/// Compute sqrt price bounds given a slippage tolerance.
///
/// Returns `(lower_bound, upper_bound)` clamped to the valid sqrt
/// price range.
pub fn sqrt_price_slippage_bounds(sqrt_price: u128, slippage_bps: u16) -> (u128, u128) {
    let delta = sqrt_price * slippage_bps as u128 / 10_000;
    let lower = sqrt_price.saturating_sub(delta);
    let upper = sqrt_price.saturating_add(delta).min(crate::tick_math::MAX_SQRT_PRICE);
    (lower.max(crate::tick_math::MIN_SQRT_PRICE), upper)
}

#[cfg(test)]
mod tests {
    use super::*;

    // ---- apply_transfer_fee ----

    #[test]
    fn test_apply_transfer_fee_zero_fee() {
        assert_eq!(apply_transfer_fee(1000, 0, u64::MAX).unwrap(), 1000);
    }

    #[test]
    fn test_apply_transfer_fee_zero_amount() {
        assert_eq!(apply_transfer_fee(0, 100, u64::MAX).unwrap(), 0);
    }

    #[test]
    fn test_apply_transfer_fee_1_percent() {
        // 1% of 10_000 = 100, so post-fee = 9_900
        assert_eq!(apply_transfer_fee(10_000, 100, u64::MAX).unwrap(), 9_900,);
    }

    #[test]
    fn test_apply_transfer_fee_capped() {
        // 1% of 10_000 = 100, but max_fee = 50 => fee = 50
        assert_eq!(apply_transfer_fee(10_000, 100, 50).unwrap(), 9_950,);
    }

    // ---- reverse_apply_transfer_fee ----

    #[test]
    fn test_reverse_apply_roundtrip() {
        let original = 10_000u64;
        let post = apply_transfer_fee(original, 100, u64::MAX).unwrap();
        let recovered = reverse_apply_transfer_fee(post, 100, u64::MAX).unwrap();
        // Ceiling division may overshoot by 1
        assert!(
            recovered == original || recovered == original + 1,
            "recovered={recovered}, original={original}",
        );
    }

    #[test]
    fn test_reverse_apply_zero() {
        assert_eq!(reverse_apply_transfer_fee(0, 100, u64::MAX).unwrap(), 0,);
    }

    #[test]
    fn test_reverse_apply_100_percent_errors() {
        assert_eq!(
            reverse_apply_transfer_fee(100, 10_000, u64::MAX),
            Err(AmmMathError::InvalidFeeRate(10_000)),
        );
    }

    #[test]
    fn test_apply_transfer_fee_invalid_bps() {
        assert_eq!(
            apply_transfer_fee(1000, 10_001, u64::MAX),
            Err(AmmMathError::InvalidFeeRate(10_001)),
        );
    }

    #[test]
    fn test_reverse_apply_transfer_fee_invalid_bps() {
        assert_eq!(
            reverse_apply_transfer_fee(1000, 10_001, u64::MAX),
            Err(AmmMathError::InvalidFeeRate(10_001)),
        );
    }

    // ---- min_amount_with_slippage ----

    #[test]
    fn test_min_amount_slippage_50bps() {
        // 0.5% of 10_000 = 50
        assert_eq!(min_amount_with_slippage(10_000, 50), 9_950);
    }

    #[test]
    fn test_min_amount_slippage_full() {
        assert_eq!(min_amount_with_slippage(10_000, 10_000), 0);
    }

    #[test]
    fn test_min_amount_slippage_zero() {
        assert_eq!(min_amount_with_slippage(10_000, 0), 10_000);
    }

    // ---- max_amount_with_slippage ----

    #[test]
    fn test_max_amount_slippage_50bps() {
        assert_eq!(max_amount_with_slippage(10_000, 50), 10_050);
    }

    #[test]
    fn test_max_amount_slippage_saturates() {
        assert_eq!(max_amount_with_slippage(u64::MAX, 10_000), u64::MAX,);
    }

    // ---- sqrt_price_slippage_bounds ----

    #[test]
    fn test_sqrt_price_bounds_basic() {
        let price = 18446744073709551616u128; // 2^64
        let (lo, hi) = sqrt_price_slippage_bounds(price, 100);
        // 1% delta
        let delta = price / 100;
        assert_eq!(lo, price - delta);
        assert_eq!(hi, price + delta);
    }

    #[test]
    fn test_sqrt_price_bounds_clamped() {
        let (lo, _hi) = sqrt_price_slippage_bounds(crate::tick_math::MIN_SQRT_PRICE, 100);
        assert_eq!(lo, crate::tick_math::MIN_SQRT_PRICE);
        let (_lo, hi) = sqrt_price_slippage_bounds(crate::tick_math::MAX_SQRT_PRICE, 100);
        assert_eq!(hi, crate::tick_math::MAX_SQRT_PRICE);
    }
}