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 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 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 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 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}