gmsol_utils/
oracle.rs

1use num_enum::{IntoPrimitive, TryFromPrimitive};
2
3use crate::{price::Decimal, token_config::TokenConfig, Price};
4
5/// Oracle error.
6#[derive(Debug, thiserror::Error)]
7pub enum OracleError {
8    /// Invalid price feed price.
9    #[error("invalid price feed price: {0}")]
10    InvalidPriceFeedPrice(&'static str),
11}
12
13type OracleResult<T> = std::result::Result<T, OracleError>;
14
15/// Supported Price Provider Kind.
16#[repr(u8)]
17#[derive(
18    Clone,
19    Copy,
20    Default,
21    TryFromPrimitive,
22    IntoPrimitive,
23    PartialEq,
24    Eq,
25    Hash,
26    strum::EnumString,
27    strum::Display,
28)]
29#[strum(serialize_all = "snake_case")]
30#[cfg_attr(feature = "enum-iter", derive(strum::EnumIter))]
31#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
32#[cfg_attr(feature = "clap", clap(rename_all = "snake_case"))]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
35#[cfg_attr(feature = "debug", derive(Debug))]
36#[non_exhaustive]
37pub enum PriceProviderKind {
38    /// Chainlink Data Streams.
39    #[default]
40    ChainlinkDataStreams = 0,
41    /// Pyth Oracle V2.
42    Pyth = 1,
43    /// Chainlink Data Feed.
44    Chainlink = 2,
45    /// Switchboard On-Demand (V3) Data Feed.
46    Switchboard = 3,
47}
48
49/// Convert pyth price value with confidence to [`Price`].
50pub fn pyth_price_with_confidence_to_price(
51    price: i64,
52    confidence: u64,
53    exponent: i32,
54    token_config: &TokenConfig,
55) -> OracleResult<Price> {
56    let mid_price: u64 = price
57        .try_into()
58        .map_err(|_| OracleError::InvalidPriceFeedPrice("mid_price"))?;
59    // Note: No validation of Pyth’s price volatility has been conducted yet.
60    // Exercise caution when choosing Pyth as the primary oracle.
61    let min_price = mid_price
62        .checked_sub(confidence)
63        .ok_or(OracleError::InvalidPriceFeedPrice("min_price"))?;
64    let max_price = mid_price
65        .checked_add(confidence)
66        .ok_or(OracleError::InvalidPriceFeedPrice("max_price"))?;
67    Ok(Price {
68        min: pyth_price_value_to_decimal(min_price, exponent, token_config)?,
69        max: pyth_price_value_to_decimal(max_price, exponent, token_config)?,
70    })
71}
72
73/// Pyth price value to decimal.
74pub fn pyth_price_value_to_decimal(
75    mut value: u64,
76    exponent: i32,
77    token_config: &TokenConfig,
78) -> OracleResult<Decimal> {
79    // actual price == value * 10^exponent
80    // - If `exponent` is not positive, then the `decimals` is set to `-exponent`.
81    // - Otherwise, we should use `value * 10^exponent` as `price` argument, and let `decimals` be `0`.
82    let decimals: u8 = if exponent <= 0 {
83        (-exponent)
84            .try_into()
85            .map_err(|_| OracleError::InvalidPriceFeedPrice("exponent too small"))?
86    } else {
87        let factor = 10u64
88            .checked_pow(exponent as u32)
89            .ok_or(OracleError::InvalidPriceFeedPrice("exponent too big"))?;
90        value = value
91            .checked_mul(factor)
92            .ok_or(OracleError::InvalidPriceFeedPrice("price overflow"))?;
93        0
94    };
95    let price = Decimal::try_from_price(
96        value as u128,
97        decimals,
98        token_config.token_decimals(),
99        token_config.precision(),
100    )
101    .map_err(|_| OracleError::InvalidPriceFeedPrice("converting to Decimal"))?;
102    Ok(price)
103}