equanetwork-math 0.0.4

The Equa Network program math library
Documentation
//! Combine like-quoted oracle prices (price sources or feeds) into a pair rate.
//!
//! Decoding of oracle accounts (Pyth / Switchboard / Chainlink / stake pool)
//! lives in the on-chain program. This module only does Q64.64 arithmetic.

#[cfg(feature = "wasm")]
use equanetwork_macros::wasm_expose;
use ethnum::U256;

use super::consts::{PER_1M_DENOMINATOR, Q64_ONE};
use super::error::{
    CoreError, ARITHMETIC_OVERFLOW, DIVISION_BY_ZERO, INVALID_MARGIN, INVALID_PRICE,
};
use super::U128;

/// Inclusive Q64.64 interval.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct PriceBand {
    pub lo_q64: u128,
    pub hi_q64: u128,
}

/// Identity Q64.64 price (1.0) used when no price source or feed is configured.
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn default_price_q64() -> U128 {
    U128::from(Q64_ONE)
}

/// Q64.64 ratio `numerator / denominator`.
///
/// Same formula for integer amounts (e.g. stake-pool lamports / supply) and
/// for two Q64.64 prices (`price_in / price_out`).
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn ratio_to_q64(numerator: U128, denominator: U128) -> Result<U128, CoreError> {
    let numerator: u128 = numerator.into();
    let denominator: u128 = denominator.into();
    if denominator == 0 {
        return Err(DIVISION_BY_ZERO);
    }
    if numerator == 0 {
        return Err(INVALID_PRICE);
    }
    let q: U256 = (U256::from(numerator) << 64) / U256::from(denominator);
    let q = u128::try_from(q).map_err(|_| ARITHMETIC_OVERFLOW)?;
    if q == 0 {
        return Err(INVALID_PRICE);
    }
    Ok(U128::from(q))
}

/// Combined pair rate `price_in / price_out` in Q64.64.
///
/// Quote currencies cancel when both inputs are denominated the same way
/// (e.g. two SOL-quoted sources, or two USD-quoted feeds).
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn combine_prices_q64(price_in_q64: U128, price_out_q64: U128) -> Result<U128, CoreError> {
    ratio_to_q64(price_in_q64, price_out_q64)
}

/// `[price * (1 − margin), price * (1 + margin)]` in Q64.64.
pub fn price_band_with_margin(price_q64: U128, margin_per_1m: u32) -> Result<PriceBand, CoreError> {
    let price: u128 = price_q64.into();
    if price == 0 {
        return Err(INVALID_PRICE);
    }
    if margin_per_1m > PER_1M_DENOMINATOR as u32 {
        return Err(INVALID_MARGIN);
    }
    let denom = U256::from(PER_1M_DENOMINATOR as u128);
    let p = U256::from(price);
    let lo_f = U256::from(PER_1M_DENOMINATOR.saturating_sub(margin_per_1m as u64) as u128);
    let hi_f = U256::from(PER_1M_DENOMINATOR.saturating_add(margin_per_1m as u64) as u128);
    let lo = u128::try_from((p * lo_f) / denom).map_err(|_| ARITHMETIC_OVERFLOW)?;
    let hi = u128::try_from((p * hi_f) / denom).map_err(|_| ARITHMETIC_OVERFLOW)?;
    if lo == 0 {
        return Err(INVALID_PRICE);
    }
    Ok(PriceBand {
        lo_q64: lo,
        hi_q64: hi,
    })
}

/// Combined band from two price intervals: `lo = in_lo / out_hi`, `hi = in_hi / out_lo`.
pub fn combine_price_band(
    in_lo_q64: U128,
    in_hi_q64: U128,
    out_lo_q64: U128,
    out_hi_q64: U128,
) -> Result<PriceBand, CoreError> {
    let lo: u128 = combine_prices_q64(in_lo_q64, out_hi_q64)?.into();
    let hi: u128 = combine_prices_q64(in_hi_q64, out_lo_q64)?.into();
    if lo == 0 || hi < lo {
        return Err(INVALID_PRICE);
    }
    Ok(PriceBand {
        lo_q64: lo,
        hi_q64: hi,
    })
}

/// Combined feed band from two `[mid ± conf]` observations.
pub fn combine_feed_band(
    in_mid_q64: U128,
    in_conf_q64: U128,
    out_mid_q64: U128,
    out_conf_q64: U128,
) -> Result<PriceBand, CoreError> {
    let in_mid: u128 = in_mid_q64.into();
    let in_conf: u128 = in_conf_q64.into();
    let out_mid: u128 = out_mid_q64.into();
    let out_conf: u128 = out_conf_q64.into();
    let in_lo = in_mid.saturating_sub(in_conf);
    let in_hi = in_mid.saturating_add(in_conf);
    let out_lo = out_mid.saturating_sub(out_conf);
    let out_hi = out_mid.saturating_add(out_conf);
    if in_lo == 0 || out_lo == 0 {
        return Err(INVALID_PRICE);
    }
    combine_price_band(
        U128::from(in_lo),
        U128::from(in_hi),
        U128::from(out_lo),
        U128::from(out_hi),
    )
}

/// `num_a / den_a <= num_b / den_b` for positive values (widening mul).
fn ratio_leq(num_a: u128, den_a: u128, num_b: u128, den_b: u128) -> bool {
    U256::from(num_a) * U256::from(den_b) <= U256::from(den_a) * U256::from(num_b)
}

/// Combined synthetic oracle band must lie inside the combined execution-price
/// band.
///
/// Execution is the point `source_in / source_out`, expanded by the stricter of
/// the two vaults' `price_margin_per_1m`. Oracle is
/// `(feed_in ± conf_in) / (feed_out ∓ conf_out)`. Quote currencies cancel when
/// both sources share a denomination and both feeds share a (possibly
/// different) denomination.
#[allow(clippy::too_many_arguments)]
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn assert_synthetic_oracle_within_execution(
    source_in_q64: U128,
    source_out_q64: U128,
    feed_in_mid_q64: U128,
    feed_in_conf_q64: U128,
    feed_out_mid_q64: U128,
    feed_out_conf_q64: U128,
    margin_in_per_1m: u32,
    margin_out_per_1m: u32,
) -> Result<(), CoreError> {
    let source_in: u128 = source_in_q64.into();
    let source_out: u128 = source_out_q64.into();
    let feed_in_mid: u128 = feed_in_mid_q64.into();
    let feed_in_conf: u128 = feed_in_conf_q64.into();
    let feed_out_mid: u128 = feed_out_mid_q64.into();
    let feed_out_conf: u128 = feed_out_conf_q64.into();

    if source_in == 0 || source_out == 0 {
        return Err(INVALID_PRICE);
    }
    let margin_per_1m = core::cmp::min(margin_in_per_1m, margin_out_per_1m);
    let source_band = price_band_with_margin(U128::from(source_in), margin_per_1m)?;

    let feed_in_lo = feed_in_mid.saturating_sub(feed_in_conf);
    let feed_in_hi = feed_in_mid.saturating_add(feed_in_conf);
    let feed_out_lo = feed_out_mid.saturating_sub(feed_out_conf);
    let feed_out_hi = feed_out_mid.saturating_add(feed_out_conf);
    if feed_in_lo == 0 || feed_out_lo == 0 {
        return Err(INVALID_PRICE);
    }

    // exec_lo = source_in_lo / source_out, oracle_lo = feed_in_lo / feed_out_hi
    // exec_hi = source_in_hi / source_out, oracle_hi = feed_in_hi / feed_out_lo
    if !ratio_leq(source_band.lo_q64, source_out, feed_in_lo, feed_out_hi)
        || !ratio_leq(feed_in_hi, feed_out_lo, source_band.hi_q64, source_out)
    {
        return Err(INVALID_MARGIN);
    }
    Ok(())
}

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

    #[test]
    fn combine_prices_cancels_quote_currency() {
        let exec: u128 = combine_prices_q64(U128::from(Q64_ONE * 2), U128::from(Q64_ONE))
            .unwrap()
            .into();
        let oracle: u128 = combine_prices_q64(U128::from(Q64_ONE * 200), U128::from(Q64_ONE * 100))
            .unwrap()
            .into();
        assert_eq!(exec, oracle);
        assert_eq!(exec, Q64_ONE * 2);
    }

    #[test]
    fn ratio_to_q64_from_integer_amounts() {
        let price: u128 = ratio_to_q64(U128::from(2u128), U128::from(1u128))
            .unwrap()
            .into();
        assert_eq!(price, Q64_ONE * 2);
    }

    #[test]
    fn synthetic_oracle_matches_execution() {
        assert!(assert_synthetic_oracle_within_execution(
            U128::from(Q64_ONE),
            U128::from(Q64_ONE),
            U128::from(Q64_ONE),
            U128::from(0u128),
            U128::from(Q64_ONE),
            U128::from(0u128),
            5_000,
            5_000
        )
        .is_ok());
        // 1.04 / 1.00 sits inside execution ± 5%.
        let near = Q64_ONE + Q64_ONE / 25;
        assert!(assert_synthetic_oracle_within_execution(
            U128::from(Q64_ONE),
            U128::from(Q64_ONE),
            U128::from(near),
            U128::from(0u128),
            U128::from(Q64_ONE),
            U128::from(0u128),
            50_000,
            50_000
        )
        .is_ok());
        // Combined 1.10 / 1.00 is outside execution 1.0 ± 5%.
        let far = Q64_ONE + Q64_ONE / 10;
        assert!(assert_synthetic_oracle_within_execution(
            U128::from(Q64_ONE),
            U128::from(Q64_ONE),
            U128::from(far),
            U128::from(0u128),
            U128::from(Q64_ONE),
            U128::from(0u128),
            50_000,
            50_000
        )
        .is_err());
        // Wide conf that escapes the combined execution band fails.
        let conf = Q64_ONE / 50;
        assert!(assert_synthetic_oracle_within_execution(
            U128::from(Q64_ONE),
            U128::from(Q64_ONE),
            U128::from(Q64_ONE),
            U128::from(conf),
            U128::from(Q64_ONE),
            U128::from(0u128),
            5_000,
            5_000
        )
        .is_err());
    }

    #[test]
    fn mixed_quote_currencies_cancel_in_combined_ratio() {
        // source_in XYZ/SOL = 2, source_out unset = 1 → execution 2
        // feed_in XYZ/USD = 200, feed_out ABC/USD = 100 → oracle 2
        let source_in = Q64_ONE * 2;
        let feed_in = Q64_ONE * 200;
        let feed_out = Q64_ONE * 100;
        assert!(assert_synthetic_oracle_within_execution(
            U128::from(source_in),
            U128::from(Q64_ONE),
            U128::from(feed_in),
            U128::from(0u128),
            U128::from(feed_out),
            U128::from(0u128),
            5_000,
            5_000
        )
        .is_ok());
        // Oracle 220/100 = 2.2 vs execution 2 is outside margin.
        let feed_in_far = Q64_ONE * 220;
        assert!(assert_synthetic_oracle_within_execution(
            U128::from(source_in),
            U128::from(Q64_ONE),
            U128::from(feed_in_far),
            U128::from(0u128),
            U128::from(feed_out),
            U128::from(0u128),
            5_000,
            5_000
        )
        .is_err());
    }
}