riptide-amm-math 2.0.0

The Riptide program math library
Documentation
use ethnum::U256;

#[cfg(feature = "floats")]
use libm::{floor, pow, sqrt};

#[cfg(feature = "wasm")]
use riptide_amm_macros::wasm_expose;

use super::error::{CoreError, AMOUNT_EXCEEDS_MAX_U128, ARITHMETIC_OVERFLOW};

use super::U128;

#[cfg(feature = "floats")]
const Q64_RESOLUTION: f64 = 18446744073709551616.0;

/// Convert a floating point price into a Q64.64 price
/// IMPORTANT: floating point operations can reduce the precision of the result.
/// Make sure to do these operations last and not to use the result for further calculations.
///
/// # Parameters
/// * `price` - The price to convert
/// * `decimals_a` - The number of decimals of the base token
/// * `decimals_b` - The number of decimals of the quote token
///
/// # Returns
/// * `u128` - The Q64.64 price
#[cfg(feature = "floats")]
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn to_q64_64(price: f64, decimals_a: u8, decimals_b: u8) -> U128 {
    let power = pow(10f64, decimals_a as f64 - decimals_b as f64);
    #[allow(clippy::useless_conversion)]
    (floor((price / power) * Q64_RESOLUTION) as u128).into()
}

/// Convert a Q64.64 price into a floating point price
/// IMPORTANT: floating point operations can reduce the precision of the result.
/// Make sure to do these operations last and not to use the result for further calculations.
///
/// # Parameters
/// * `q64_64` - The Q64.64 price to convert
/// * `decimals_a` - The number of decimals of the base token
/// * `decimals_b` - The number of decimals of the quote token
///
/// # Returns
/// * `f64` - The decimal price
#[cfg(feature = "floats")]
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn from_q64_64(price: U128, decimals_a: u8, decimals_b: u8) -> f64 {
    let power = pow(10f64, decimals_a as f64 - decimals_b as f64);
    #[allow(clippy::useless_conversion)]
    let q64_64: u128 = price.into();
    (q64_64 as f64 / Q64_RESOLUTION) * power
}

/// Convert a price into a sqrt priceX64
/// IMPORTANT: floating point operations can reduce the precision of the result.
/// Make sure to do these operations last and not to use the result for further calculations.
///
/// # Parameters
/// * `price` - The price to convert
/// * `decimals_a` - The number of decimals of the base token
/// * `decimals_b` - The number of decimals of the quote token
///
/// # Returns
/// * `u128` - The sqrt priceX64
#[cfg(feature = "floats")]
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn to_sqrt_price(price: f64, decimals_a: u8, decimals_b: u8) -> U128 {
    let power = pow(10f64, decimals_a as f64 - decimals_b as f64);
    #[allow(clippy::useless_conversion)] // `U128` differs under the `wasm` feature.
    (floor(sqrt(price / power) * Q64_RESOLUTION) as u128).into()
}

/// Convert a sqrt priceX64 into a tick index
/// IMPORTANT: floating point operations can reduce the precision of the result.
/// Make sure to do these operations last and not to use the result for further calculations.
///
/// # Parameters
/// * `sqrt_price` - The sqrt priceX64 to convert
/// * `decimals_a` - The number of decimals of the base token
/// * `decimals_b` - The number of decimals of the quote token
///
/// # Returns
/// * `f64` - The decimal price
#[cfg(feature = "floats")]
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn from_sqrt_price(sqrt_price: U128, decimals_a: u8, decimals_b: u8) -> f64 {
    let power = pow(10f64, decimals_a as f64 - decimals_b as f64);
    #[allow(clippy::useless_conversion)] // `U128` differs under the `wasm` feature.
    let sqrt_price: u128 = sqrt_price.into();
    pow(sqrt_price as f64 / Q64_RESOLUTION, 2.0) * power
}

/// Invert a price from A/B to B/A, works for both Q64.64 and SqrtPrice
///
/// # Parameters
/// * `price` - The Q64.64 or SqrtPrice to invert
///
/// # Returns
/// * `U128` - The inverted price
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn invert_price(price: U128) -> Result<U128, CoreError> {
    let result: u128 = U256::from(1u128)
        .checked_shl(128)
        .ok_or(ARITHMETIC_OVERFLOW)?
        .checked_div(price.into())
        .ok_or(ARITHMETIC_OVERFLOW)?
        .try_into()
        .map_err(|_| AMOUNT_EXCEEDS_MAX_U128)?;

    Ok(U128::from(result))
}

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

    #[cfg(feature = "floats")]
    #[rstest]
    #[case(1.0, 9, 9, 1 << 64)]
    #[case(1000.0, 9, 6, 1 << 64)]
    #[case(0.001, 6, 9, 1 << 64)]
    #[case(1.0, 6, 6, 1 << 64)]
    #[case(0.0, 6, 6, 0)]
    fn test_to_q64_64(
        #[case] price: f64,
        #[case] decimals_a: u8,
        #[case] decimals_b: u8,
        #[case] expected_q64_64: u128,
    ) {
        let q64_64 = to_q64_64(price, decimals_a, decimals_b);
        assert_eq!(q64_64, U128::from(expected_q64_64));
    }

    #[cfg(feature = "floats")]
    #[rstest]
    #[case(1 << 64, 9, 9, 1.0)]
    #[case(1 << 64, 9, 6, 1000.0)]
    #[case(1 << 64, 6, 9, 0.001)]
    #[case(1 << 64, 6, 6, 1.0)]
    #[case(0, 6, 6, 0.0)]
    fn test_from_q64_64(
        #[case] q64_64: u128,
        #[case] decimals_a: u8,
        #[case] decimals_b: u8,
        #[case] expected_price: f64,
    ) {
        #[allow(clippy::useless_conversion)] // `U128` differs under the `wasm` feature.
        let price = from_q64_64(q64_64.into(), decimals_a, decimals_b);
        assert_eq!(price, expected_price);
    }

    #[rstest]
    #[case(1.0, 9, 9, 1 << 64)]
    #[case(1000.0, 9, 6, 1 << 64)]
    #[case(0.001, 6, 9, 1 << 64)]
    #[case(1.0, 6, 6, 1 << 64)]
    #[case(0.0, 6, 6, 0)]
    #[cfg(feature = "floats")]
    fn test_to_sqrt_price(
        #[case] price: f64,
        #[case] decimals_a: u8,
        #[case] decimals_b: u8,
        #[case] expected_sqrt_price: u128,
    ) {
        let sqrt_price = to_sqrt_price(price, decimals_a, decimals_b);
        assert_eq!(sqrt_price, U128::from(expected_sqrt_price));
    }

    #[rstest]
    #[case(1 << 64, 9, 9, 1.0)]
    #[case(1 << 64, 9, 6, 1000.0)]
    #[case(1 << 64, 6, 9, 0.001)]
    #[case(1 << 64, 6, 6, 1.0)]
    #[case(0, 6, 6, 0.0)]
    #[cfg(feature = "floats")]
    fn test_from_sqrt_price(
        #[case] sqrt_price: u128,
        #[case] decimals_a: u8,
        #[case] decimals_b: u8,
        #[case] expected_price: f64,
    ) {
        #[allow(clippy::useless_conversion)] // `U128` differs under the `wasm` feature.
        let price = from_sqrt_price(sqrt_price.into(), decimals_a, decimals_b);
        assert_eq!(price, expected_price);
    }

    #[rstest]
    #[case(1 << 64, Ok(1 << 64))]
    #[case(2 << 64, Ok((1 << 64) / 2))]
    #[case(4 << 64, Ok((1 << 64) / 4))]
    #[case((1 << 64) / 2, Ok(2 << 64))]
    #[case((1 << 64) / 4, Ok(4 << 64))]
    #[case(0, Err(ARITHMETIC_OVERFLOW))]
    fn test_invert_price(#[case] price: u128, #[case] expected: Result<u128, CoreError>) {
        let result = invert_price(U128::from(price));
        #[allow(clippy::useless_conversion)]
        let expected = expected.map(U128::from);
        assert_eq!(result, expected);
    }
}