Skip to main content

equanetwork_math/
oracle.rs

1use ethnum::U256;
2
3use super::error::{CoreError, AMOUNT_EXCEEDS_MAX_U128, ARITHMETIC_OVERFLOW};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct OraclePrice {
7    pub price: u128,
8    // Confidence in price as a u128 confidence interval (lower is more confident)
9    pub confidence: u128,
10    pub timestamp: i64,
11}
12
13impl OraclePrice {
14    pub fn try_from_raw(
15        price: i64,
16        confidence: u64,
17        exponent: i32,
18        timestamp: i64,
19    ) -> Result<OraclePrice, CoreError> {
20        Self::try_from_scaled(
21            i128::from(price),
22            u128::from(confidence),
23            exponent,
24            timestamp,
25        )
26    }
27
28    /// Convert a signed mantissa (Pyth i64, Switchboard/Chainlink i128) into Q64.64.
29    pub fn try_from_scaled(
30        price: i128,
31        confidence: u128,
32        exponent: i32,
33        timestamp: i64,
34    ) -> Result<OraclePrice, CoreError> {
35        if price <= 0 {
36            return Err(ARITHMETIC_OVERFLOW);
37        }
38        let price = mantissa_to_q64(price as u128, exponent)?;
39        if price == 0 {
40            return Err(ARITHMETIC_OVERFLOW);
41        }
42        let confidence = mantissa_to_q64(confidence, exponent)?;
43        Ok(OraclePrice {
44            price,
45            confidence,
46            timestamp,
47        })
48    }
49
50    /// Pair rate `in / out` in atomic B/A Q64.64, using the confidence bounds
51    /// that minimize output tokens per input token, then scaling by
52    /// `10^(decimals_out - decimals_in)` with floor division.
53    pub fn combine(
54        price_in: &OraclePrice,
55        price_out: &OraclePrice,
56        decimals_in: u8,
57        decimals_out: u8,
58    ) -> Result<u128, CoreError> {
59        let in_lo = price_in.price.saturating_sub(price_in.confidence);
60        let out_hi = price_out.price.saturating_add(price_out.confidence);
61        if in_lo == 0 || out_hi == 0 {
62            return Err(ARITHMETIC_OVERFLOW);
63        }
64
65        let ui_rate: u128 = U256::from(in_lo)
66            .checked_shl(64)
67            .ok_or(ARITHMETIC_OVERFLOW)?
68            .checked_div(U256::from(out_hi))
69            .ok_or(ARITHMETIC_OVERFLOW)?
70            .try_into()
71            .map_err(|_| AMOUNT_EXCEEDS_MAX_U128)?;
72        if ui_rate == 0 {
73            return Err(ARITHMETIC_OVERFLOW);
74        }
75
76        let scaled = if decimals_out >= decimals_in {
77            let scale = 10u128
78                .checked_pow((decimals_out - decimals_in) as u32)
79                .ok_or(ARITHMETIC_OVERFLOW)?;
80            ui_rate.checked_mul(scale).ok_or(ARITHMETIC_OVERFLOW)?
81        } else {
82            let scale = 10u128
83                .checked_pow((decimals_in - decimals_out) as u32)
84                .ok_or(ARITHMETIC_OVERFLOW)?;
85            ui_rate / scale
86        };
87        if scaled == 0 {
88            return Err(ARITHMETIC_OVERFLOW);
89        }
90        Ok(scaled)
91    }
92}
93
94fn mantissa_to_q64(mantissa: u128, exponent: i32) -> Result<u128, CoreError> {
95    let scale = 10u128
96        .checked_pow(exponent.unsigned_abs())
97        .ok_or(ARITHMETIC_OVERFLOW)?;
98    let shifted = U256::from(mantissa)
99        .checked_shl(64)
100        .ok_or(ARITHMETIC_OVERFLOW)?;
101    let result = if exponent >= 0 {
102        shifted
103            .checked_mul(U256::from(scale))
104            .ok_or(ARITHMETIC_OVERFLOW)?
105    } else {
106        shifted
107            .checked_div(U256::from(scale))
108            .ok_or(ARITHMETIC_OVERFLOW)?
109    };
110    result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U128)
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use rstest::rstest;
117
118    fn price(mid: u128, conf: u128) -> OraclePrice {
119        OraclePrice {
120            price: mid,
121            confidence: conf,
122            timestamp: 0,
123        }
124    }
125
126    #[rstest]
127    #[case(100_000_000, 0, -8, 1_700_000_000, Ok((1u128 << 64, 0)))]
128    #[case(200_000_000, 1_000_000, -8, 42, Ok((2 << 64, (1u128 << 64) / 100)))]
129    #[case(1, 0, 0, 0, Ok((1u128 << 64, 0)))]
130    #[case(1, 0, 1, 0, Ok((10 << 64, 0)))]
131    #[case(1, 1, -1, 7, Ok(((1u128 << 64) / 10, (1u128 << 64) / 10)))]
132    #[case(0, 0, -8, 0, Err(ARITHMETIC_OVERFLOW))]
133    #[case(-1, 0, -8, 0, Err(ARITHMETIC_OVERFLOW))]
134    #[case(1, 0, -20, 0, Err(ARITHMETIC_OVERFLOW))]
135    fn test_try_from_raw(
136        #[case] raw_price: i64,
137        #[case] raw_confidence: u64,
138        #[case] exponent: i32,
139        #[case] timestamp: i64,
140        #[case] expected: Result<(u128, u128), CoreError>,
141    ) {
142        let result = OraclePrice::try_from_raw(raw_price, raw_confidence, exponent, timestamp);
143        match (result, expected) {
144            (Ok(obs), Ok((price, confidence))) => {
145                let got_price: u128 = obs.price.into();
146                let got_confidence: u128 = obs.confidence.into();
147                assert_eq!(got_price, price);
148                assert_eq!(got_confidence, confidence);
149                assert_eq!(obs.timestamp, timestamp);
150            }
151            (Err(err), Err(expected_err)) => assert_eq!(err, expected_err),
152            (result, expected) => panic!("unexpected result {result:?}, expected {expected:?}"),
153        }
154    }
155
156    #[test]
157    fn try_from_scaled_switchboard_precision() {
158        // 1.0 with Switchboard On-Demand PRECISION=18 must fit via U256.
159        let obs = OraclePrice::try_from_scaled(1_000_000_000_000_000_000, 0, -18, 7).unwrap();
160        assert_eq!(obs.price, 1u128 << 64);
161        assert_eq!(obs.confidence, 0);
162        assert_eq!(obs.timestamp, 7);
163    }
164
165    #[rstest]
166    #[case(1u128 << 64, 0, 1u128 << 64, 0, 6, 6, Ok(1u128 << 64))]
167    #[case(2 << 64, 0, 1u128 << 64, 0, 6, 6, Ok(2 << 64))]
168    #[case(1u128 << 64, 0, 2 << 64, 0, 9, 9, Ok((1u128 << 64) / 2))]
169    #[case(200 << 64, 0, 100 << 64, 0, 0, 0, Ok(2 << 64))]
170    #[case(1u128 << 64, (1u128 << 64) / 16, 1u128 << 64, (1u128 << 64) / 16, 6, 6, Ok((15 << 64) / 17))]
171    #[case(1u128 << 64, 1u128 << 64, 1u128 << 64, 0, 6, 6, Err(ARITHMETIC_OVERFLOW))]
172    #[case(0, 0, 1u128 << 64, 0, 6, 6, Err(ARITHMETIC_OVERFLOW))]
173    #[case(1u128 << 64, 0, 0, 0, 6, 6, Err(ARITHMETIC_OVERFLOW))]
174    #[case(1u128 << 64, 0, 1u128 << 64, 0, 6, 9, Ok((1u128 << 64) * 1000))]
175    #[case(1u128 << 64, 0, 1u128 << 64, 0, 9, 6, Ok((1u128 << 64) / 1000))]
176    fn test_combine(
177        #[case] in_price: u128,
178        #[case] in_conf: u128,
179        #[case] out_price: u128,
180        #[case] out_conf: u128,
181        #[case] decimals_in: u8,
182        #[case] decimals_out: u8,
183        #[case] expected: Result<u128, CoreError>,
184    ) {
185        let result = OraclePrice::combine(
186            &price(in_price, in_conf),
187            &price(out_price, out_conf),
188            decimals_in,
189            decimals_out,
190        );
191        assert_eq!(result, expected);
192    }
193
194    #[test]
195    fn combine_floor_not_ceil_when_downscaling() {
196        let one = 1u128 << 64;
197        let floored = OraclePrice::combine(&price(one, 0), &price(one, 0), 9, 6).unwrap();
198        assert_eq!(floored, one / 1000);
199        assert_ne!(floored, one.div_ceil(1000));
200    }
201
202    #[test]
203    fn combine_uses_worst_bound_for_the_user() {
204        let mid =
205            OraclePrice::combine(&price(1u128 << 64, 0), &price(1u128 << 64, 0), 6, 6).unwrap();
206        let conf = (1u128 << 64) / 16;
207        let conservative =
208            OraclePrice::combine(&price(1u128 << 64, conf), &price(1u128 << 64, conf), 6, 6)
209                .unwrap();
210        let optimistic_in =
211            OraclePrice::combine(&price(1u128 << 64, 0), &price(1u128 << 64, conf), 6, 6).unwrap();
212        let optimistic_out =
213            OraclePrice::combine(&price(1u128 << 64, conf), &price(1u128 << 64, 0), 6, 6).unwrap();
214
215        assert!(conservative < mid);
216        assert_eq!(conservative, (15 << 64) / 17);
217        assert_eq!(optimistic_in, (1u128 << 64) * 16 / 17);
218        assert_eq!(optimistic_out, (1u128 << 64) * 15 / 16);
219        assert!(conservative < optimistic_in);
220        assert!(conservative < optimistic_out);
221    }
222}