1use anchor_lang::prelude::{Pubkey, Result};
2use anchor_lang::solana_program::pubkey;
3use fix::prelude::*;
4use fix::typenum::{Integer, Z0};
5use pyth_solana_receiver_sdk::price_update::{
6 FeedId, PriceUpdateV2, VerificationLevel,
7};
8
9use crate::error::CoreError::{
10 PythOracleConfidence, PythOracleExponent, PythOracleNegativePrice,
11 PythOracleNegativeTime, PythOracleOutdated, PythOraclePriceRange,
12 PythOracleSlotInvalid, PythOracleVerificationLevel,
13};
14use crate::solana_clock::SolanaClock;
15
16pub const SOL_USD: FeedId = [
17 239, 13, 139, 111, 218, 44, 235, 164, 29, 161, 93, 64, 149, 209, 218, 57, 42,
18 13, 47, 142, 208, 198, 199, 188, 15, 76, 250, 200, 194, 128, 181, 109,
19];
20
21pub const SOL_USD_PYTH_FEED: Pubkey =
22 pubkey!("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE");
23
24#[derive(Copy, Clone)]
25pub struct OracleConfig<Exp> {
26 pub interval_secs: u64,
27 pub conf_tolerance: UFix64<Exp>,
28}
29
30impl<Exp> OracleConfig<Exp> {
31 #[must_use]
32 pub fn new(
33 interval_secs: u64,
34 conf_tolerance: UFix64<Exp>,
35 ) -> OracleConfig<Exp> {
36 OracleConfig {
37 interval_secs,
38 conf_tolerance,
39 }
40 }
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct PriceRange<Exp: Integer> {
47 pub lower: UFix64<Exp>,
48 pub upper: UFix64<Exp>,
49}
50
51impl<Exp: Integer> PriceRange<Exp> {
52 pub fn from_conf(
57 price: UFix64<Exp>,
58 conf: UFix64<Exp>,
59 ) -> Result<PriceRange<Exp>> {
60 let (lower, upper) = price
61 .checked_sub(&conf)
62 .zip(price.checked_add(&conf))
63 .ok_or(PythOraclePriceRange)?;
64 Ok(Self::new(lower, upper))
65 }
66
67 #[must_use]
69 pub fn one(price: UFix64<Exp>) -> PriceRange<Exp> {
70 Self::new(price, price)
71 }
72
73 #[must_use]
75 pub fn new(lower: UFix64<Exp>, upper: UFix64<Exp>) -> PriceRange<Exp> {
76 PriceRange { lower, upper }
77 }
78}
79
80fn validate_conf<Exp>(
83 price: UFix64<Exp>,
84 conf: UFix64<Exp>,
85 tolerance: UFix64<Exp>,
86) -> Result<UFix64<Exp>>
87where
88 UFix64<Exp>: FixExt,
89{
90 conf
91 .mul_div_floor(UFix64::one(), price)
92 .filter(|diff| diff.le(&tolerance))
93 .map(|_| conf)
94 .ok_or(PythOracleConfidence.into())
95}
96
97fn validate_publish_time(
100 publish_time: i64,
101 oracle_interval: u64,
102 clock_time: i64,
103) -> Result<()> {
104 let (publish_time, clock_time) =
105 if publish_time.is_positive() && clock_time.is_positive() {
106 Ok((publish_time.unsigned_abs(), clock_time.unsigned_abs()))
107 } else {
108 Err(PythOracleNegativeTime)
109 }?;
110 if publish_time.saturating_add(oracle_interval) >= clock_time {
111 Ok(())
112 } else {
113 Err(PythOracleOutdated.into())
114 }
115}
116
117fn slot_interval(oracle_interval_secs: u64) -> Option<u64> {
119 let time: UFix64<N2> = UFix64::<Z0>::new(oracle_interval_secs).convert();
120 let slot_time = UFix64::<N2>::new(40); time.checked_div(&slot_time).map(|i| i.bits)
122}
123
124fn validate_posted_slot(
126 posted_slot: u64,
127 oracle_interval_secs: u64,
128 current_slot: u64,
129) -> Result<()> {
130 current_slot
131 .checked_sub(posted_slot)
132 .zip(slot_interval(oracle_interval_secs))
133 .filter(|(delta, slot_interval)| *delta <= *slot_interval)
134 .ok_or(PythOracleSlotInvalid.into())
135 .map(|_| ())
136}
137
138fn validate_price<Exp: Integer>(price: i64, exp: i32) -> Result<UFix64<Exp>> {
141 if Exp::to_i32() != exp {
142 Err(PythOracleExponent.into())
143 } else if price <= 0 {
144 Err(PythOracleNegativePrice.into())
145 } else {
146 Ok(UFix64::new(price.unsigned_abs()))
147 }
148}
149
150fn validate_verification_level(level: VerificationLevel) -> Result<()> {
152 if level == VerificationLevel::Full {
153 Ok(())
154 } else {
155 Err(PythOracleVerificationLevel.into())
156 }
157}
158
159pub fn query_pyth_price<Exp: Integer, C: SolanaClock>(
161 clock: &C,
162 oracle: &PriceUpdateV2,
163 OracleConfig {
164 interval_secs,
165 conf_tolerance,
166 }: OracleConfig<Exp>,
167) -> Result<PriceRange<Exp>>
168where
169 UFix64<Exp>: FixExt,
170{
171 validate_verification_level(oracle.verification_level)?;
173 validate_publish_time(
174 oracle.price_message.publish_time,
175 interval_secs,
176 clock.unix_timestamp(),
177 )?;
178 validate_posted_slot(oracle.posted_slot, interval_secs, clock.slot())?;
179
180 let spot_price =
182 validate_price(oracle.price_message.price, oracle.price_message.exponent)?;
183 let spot_conf = validate_conf(
184 spot_price,
185 UFix64::new(oracle.price_message.conf),
186 conf_tolerance,
187 )?;
188 PriceRange::from_conf(spot_price, spot_conf)
189}
190
191#[cfg(test)]
192mod tests {
193 use fix::typenum::N8;
194 use proptest::prelude::*;
195
196 use super::*;
197
198 const INTERVAL_SECS: u64 = 60;
199
200 proptest! {
201 #[test]
202 fn validate_price_pos(price in i64::arbitrary()) {
203 prop_assume!(price > 0);
204 let out = validate_price::<N8>(price, -8)?;
205 prop_assert_eq!(out, UFix64::new(price.unsigned_abs()));
206 }
207
208 #[test]
209 fn validate_price_neg(price in i64::arbitrary(), exp in i32::arbitrary()) {
210 prop_assume!(price < 0 || exp != -8);
211 let out = validate_price::<N8>(price, exp);
212 prop_assert!(out.is_err());
213 }
214
215 #[test]
216 fn validate_publish_time_neg(
217 publish_time in i64::arbitrary(),
218 time in i64::arbitrary()
219 ) {
220 let out = validate_publish_time(publish_time, INTERVAL_SECS, time);
221 if publish_time.is_negative() || time.is_negative() {
222 prop_assert_eq!(out, Err(PythOracleNegativeTime.into()));
223 } else if publish_time.unsigned_abs() + INTERVAL_SECS < time.unsigned_abs() {
224 prop_assert_eq!(out, Err(PythOracleOutdated.into()));
225 } else {
226 prop_assert!(out.is_ok());
227 }
228 }
229
230 #[allow(clippy::cast_possible_wrap)]
231 #[test]
232 fn validate_publish_time_pos(
233 publish_time in i64::arbitrary(),
234 offset in 0..INTERVAL_SECS as i64,
235 ) {
236 prop_assume!(publish_time.is_positive());
237 let out = validate_publish_time(publish_time, INTERVAL_SECS, publish_time + offset);
238 prop_assert!(out.is_ok());
239 }
240 }
241
242 #[test]
243 fn slot_interval_precise() {
244 let out = slot_interval(60);
246 assert_eq!(out, Some(150));
247 }
248
249 #[test]
250 fn slot_interval_lossy() {
251 let out = slot_interval(1);
253 assert_eq!(out, Some(2));
254 }
255
256 #[test]
257 fn validate_confidence_pos() {
258 let price = UFix64::<N8>::new(14_640_110_937);
259 let conf = UFix64::<N8>::new(9_463_582);
260 let tolerance = UFix64::<N8>::new(200_000);
261 let out = validate_conf(price, conf, tolerance);
262 assert!(out.is_ok());
263 }
264}