pyth_solana_receiver_sdk_legacy/
price_update.rs

1pub use pythnet_sdk::messages::{
2    FeedId,
3    PriceFeedMessage,
4};
5use {
6    crate::{
7        check,
8        error::GetPriceError,
9    },
10    anchor_lang::prelude::{
11        borsh::BorshSchema,
12        *,
13    },
14    solana_program::pubkey::Pubkey,
15};
16
17
18/// Pyth price updates are bridged to all blockchains via Wormhole.
19/// Using the price updates on another chain requires verifying the signatures of the Wormhole guardians.
20/// The usual process is to check the signatures for two thirds of the total number of guardians, but this can be cumbersome on Solana because of the transaction size limits,
21/// so we also allow for partial verification.
22///
23/// This enum represents how much a price update has been verified:
24/// - If `Full`, we have verified the signatures for two thirds of the current guardians.
25/// - If `Partial`, only `num_signatures` guardian signatures have been checked.
26///
27/// # Warning
28/// Using partially verified price updates is dangerous, as it lowers the threshold of guardians that need to collude to produce a malicious price update.
29#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, BorshSchema, Debug)]
30pub enum VerificationLevel {
31    Partial { num_signatures: u8 },
32    Full,
33}
34
35impl VerificationLevel {
36    /// Compare two `VerificationLevel`.
37    /// `Full` is always greater than `Partial`, and `Partial` with more signatures is greater than `Partial` with fewer signatures.
38    pub fn gte(&self, other: VerificationLevel) -> bool {
39        match self {
40            VerificationLevel::Full => true,
41            VerificationLevel::Partial { num_signatures } => match other {
42                VerificationLevel::Full => false,
43                VerificationLevel::Partial {
44                    num_signatures: other_num_signatures,
45                } => *num_signatures >= other_num_signatures,
46            },
47        }
48    }
49}
50
51/// A price update account. This account is used by the Pyth Receiver program to store a verified price update from a Pyth price feed.
52/// It contains:
53/// - `write_authority`: The write authority for this account. This authority can close this account to reclaim rent or update the account to contain a different price update.
54/// - `verification_level`: The [`VerificationLevel`] of this price update. This represents how many Wormhole guardian signatures have been verified for this price update.
55/// - `price_message`: The actual price update.
56/// - `posted_slot`: The slot at which this price update was posted.
57#[account]
58#[derive(BorshSchema)]
59pub struct PriceUpdateV2 {
60    pub write_authority:    Pubkey,
61    pub verification_level: VerificationLevel,
62    pub price_message:      PriceFeedMessage,
63    pub posted_slot:        u64,
64}
65
66impl PriceUpdateV2 {
67    pub const LEN: usize = 8 + 32 + 2 + 32 + 8 + 8 + 4 + 8 + 8 + 8 + 8 + 8;
68}
69
70/// A Pyth price.
71/// The actual price is `(price ± conf)* 10^exponent`. `publish_time` may be used to check the recency of the price.
72#[derive(PartialEq, Debug, Clone, Copy)]
73pub struct Price {
74    pub price:        i64,
75    pub conf:         u64,
76    pub exponent:     i32,
77    pub publish_time: i64,
78}
79
80impl PriceUpdateV2 {
81    /// Get a `Price` from a `PriceUpdateV2` account for a given `FeedId`.
82    ///
83    /// # Warning
84    /// This function does not check :
85    /// - How recent the price is
86    /// - Whether the price update has been verified
87    ///
88    /// It is therefore unsafe to use this function without any extra checks, as it allows for the possibility of using unverified or outdated price updates.
89    pub fn get_price_unchecked(
90        &self,
91        feed_id: &FeedId,
92    ) -> std::result::Result<Price, GetPriceError> {
93        check!(
94            self.price_message.feed_id == *feed_id,
95            GetPriceError::MismatchedFeedId
96        );
97        Ok(Price {
98            price:        self.price_message.price,
99            conf:         self.price_message.conf,
100            exponent:     self.price_message.exponent,
101            publish_time: self.price_message.publish_time,
102        })
103    }
104
105    /// Get a `Price` from a `PriceUpdateV2` account for a given `FeedId` no older than `maximum_age` with customizable verification level.
106    ///
107    /// # Warning
108    /// Lowering the verification level from `Full` to `Partial` increases the risk of using a malicious price update.
109    /// Please read the documentation for [`VerificationLevel`] for more information.
110    ///
111    /// # Example
112    /// ```
113    /// use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, VerificationLevel, PriceUpdateV2};
114    /// use anchor_lang::prelude::*;
115    ///
116    /// const MAXIMUM_AGE : u64 = 30;
117    /// const FEED_ID: &str = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; // SOL/USD
118    ///
119    /// #[derive(Accounts)]
120    /// #[instruction(amount_in_usd : u64)]
121    /// pub struct ReadPriceAccount<'info> {
122    ///     pub price_update: Account<'info, PriceUpdateV2>,
123    /// }
124    ///
125    /// pub fn read_price_account(ctx : Context<ReadPriceAccount>) -> Result<()> {
126    ///     let price_update = &mut ctx.accounts.price_update;
127    ///     let price = price_update.get_price_no_older_than_with_custom_verification_level(&Clock::get()?, MAXIMUM_AGE, &get_feed_id_from_hex(FEED_ID)?, VerificationLevel::Partial{num_signatures: 5})?;
128    ///     Ok(())
129    /// }
130    ///```
131    pub fn get_price_no_older_than_with_custom_verification_level(
132        &self,
133        clock: &Clock,
134        maximum_age: u64,
135        feed_id: &FeedId,
136        verification_level: VerificationLevel,
137    ) -> std::result::Result<Price, GetPriceError> {
138        check!(
139            self.verification_level.gte(verification_level),
140            GetPriceError::InsufficientVerificationLevel
141        );
142        let price = self.get_price_unchecked(feed_id)?;
143        check!(
144            price
145                .publish_time
146                .saturating_add(maximum_age.try_into().unwrap())
147                >= clock.unix_timestamp,
148            GetPriceError::PriceTooOld
149        );
150        Ok(price)
151    }
152
153    /// Get a `Price` from a `PriceUpdateV2` account for a given `FeedId` no older than `maximum_age` with `Full` verification.
154    ///
155    /// # Example
156    /// ```
157    /// use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, PriceUpdateV2};
158    /// use anchor_lang::prelude::*;
159    ///
160    /// const MAXIMUM_AGE : u64 = 30;
161    /// const FEED_ID: &str = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; // SOL/USD
162    ///
163    /// #[derive(Accounts)]
164    /// #[instruction(amount_in_usd : u64)]
165    /// pub struct ReadPriceAccount<'info> {
166    ///     pub price_update: Account<'info, PriceUpdateV2>,
167    /// }
168    ///
169    /// pub fn read_price_account(ctx : Context<ReadPriceAccount>) -> Result<()> {
170    ///     let price_update = &mut ctx.accounts.price_update;
171    ///     let price = price_update.get_price_no_older_than(&Clock::get()?, MAXIMUM_AGE, &get_feed_id_from_hex(FEED_ID)?)?;
172    ///     Ok(())
173    /// }
174    ///```
175    pub fn get_price_no_older_than(
176        &self,
177        clock: &Clock,
178        maximum_age: u64,
179        feed_id: &FeedId,
180    ) -> std::result::Result<Price, GetPriceError> {
181        self.get_price_no_older_than_with_custom_verification_level(
182            clock,
183            maximum_age,
184            feed_id,
185            VerificationLevel::Full,
186        )
187    }
188}
189
190/// Get a `FeedId` from a hex string.
191///
192/// Price feed ids are a 32 byte unique identifier for each price feed in the Pyth network.
193/// They are sometimes represented as a 64 character hex string (with or without a 0x prefix).
194///
195/// # Example
196///
197/// ```
198/// use pyth_solana_receiver_sdk::price_update::get_feed_id_from_hex;
199/// let feed_id = get_feed_id_from_hex("0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d").unwrap();
200/// ```
201pub fn get_feed_id_from_hex(input: &str) -> std::result::Result<FeedId, GetPriceError> {
202    let mut feed_id: FeedId = [0; 32];
203    match input.len() {
204        66 => feed_id.copy_from_slice(
205            &hex::decode(&input[2..]).map_err(|_| GetPriceError::FeedIdNonHexCharacter)?,
206        ),
207        64 => feed_id.copy_from_slice(
208            &hex::decode(input).map_err(|_| GetPriceError::FeedIdNonHexCharacter)?,
209        ),
210        _ => return Err(GetPriceError::FeedIdMustBe32Bytes),
211    }
212    Ok(feed_id)
213}
214
215#[cfg(test)]
216pub mod tests {
217    use {
218        crate::{
219            error::GetPriceError,
220            price_update::{
221                Price,
222                PriceUpdateV2,
223                VerificationLevel,
224            },
225        },
226        anchor_lang::Discriminator,
227        pythnet_sdk::messages::PriceFeedMessage,
228        solana_program::{
229            borsh0_10,
230            clock::Clock,
231            pubkey::Pubkey,
232        },
233    };
234
235    #[test]
236    fn check_size() {
237        assert!(
238            PriceUpdateV2::discriminator().len() + borsh0_10::get_packed_len::<PriceUpdateV2>()
239                == PriceUpdateV2::LEN
240        );
241    }
242
243    #[test]
244    fn gte() {
245        assert!(VerificationLevel::Full.gte(VerificationLevel::Full));
246        assert!(VerificationLevel::Full.gte(VerificationLevel::Partial {
247            num_signatures: 255,
248        }));
249        assert!(VerificationLevel::Partial { num_signatures: 8 }
250            .gte(VerificationLevel::Partial { num_signatures: 8 }));
251        assert!(!VerificationLevel::Partial { num_signatures: 8 }.gte(VerificationLevel::Full));
252        assert!(!VerificationLevel::Partial { num_signatures: 8 }
253            .gte(VerificationLevel::Partial { num_signatures: 9 }));
254    }
255
256    #[test]
257    fn get_feed_id_from_hex() {
258        let feed_id = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";
259        let expected_feed_id = [
260            239, 13, 139, 111, 218, 44, 235, 164, 29, 161, 93, 64, 149, 209, 218, 57, 42, 13, 47,
261            142, 208, 198, 199, 188, 15, 76, 250, 200, 194, 128, 181, 109,
262        ];
263        assert_eq!(super::get_feed_id_from_hex(feed_id), Ok(expected_feed_id));
264        assert_eq!(
265            super::get_feed_id_from_hex(&feed_id[2..]),
266            Ok(expected_feed_id)
267        );
268
269        assert_eq!(
270            super::get_feed_id_from_hex(&feed_id[..64]),
271            Err(GetPriceError::FeedIdNonHexCharacter)
272        );
273
274        assert_eq!(
275            super::get_feed_id_from_hex(
276                "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b5"
277            ),
278            Err(GetPriceError::FeedIdMustBe32Bytes)
279        );
280    }
281
282    #[test]
283    fn get_price() {
284        let expected_price = Price {
285            price:        1,
286            conf:         2,
287            exponent:     3,
288            publish_time: 900,
289        };
290
291        let feed_id = [0; 32];
292        let mismatched_feed_id = [1; 32];
293        let mock_clock = Clock {
294            unix_timestamp: 1000,
295            ..Default::default()
296        };
297
298        let price_update_unverified = PriceUpdateV2 {
299            write_authority:    Pubkey::new_unique(),
300            verification_level: VerificationLevel::Partial { num_signatures: 0 },
301            price_message:      PriceFeedMessage {
302                feed_id,
303                ema_conf: 0,
304                ema_price: 0,
305                price: 1,
306                conf: 2,
307                exponent: 3,
308                prev_publish_time: 899,
309                publish_time: 900,
310            },
311            posted_slot:        0,
312        };
313
314        let price_update_partially_verified = PriceUpdateV2 {
315            write_authority:    Pubkey::new_unique(),
316            verification_level: VerificationLevel::Partial { num_signatures: 5 },
317            price_message:      PriceFeedMessage {
318                feed_id,
319                ema_conf: 0,
320                ema_price: 0,
321                price: 1,
322                conf: 2,
323                exponent: 3,
324                prev_publish_time: 899,
325                publish_time: 900,
326            },
327            posted_slot:        0,
328        };
329
330        let price_update_fully_verified = PriceUpdateV2 {
331            write_authority:    Pubkey::new_unique(),
332            verification_level: VerificationLevel::Full,
333            price_message:      PriceFeedMessage {
334                feed_id,
335                ema_conf: 0,
336                ema_price: 0,
337                price: 1,
338                conf: 2,
339                exponent: 3,
340                prev_publish_time: 899,
341                publish_time: 900,
342            },
343            posted_slot:        0,
344        };
345
346
347        assert_eq!(
348            price_update_unverified.get_price_unchecked(&feed_id),
349            Ok(expected_price)
350        );
351        assert_eq!(
352            price_update_partially_verified.get_price_unchecked(&feed_id),
353            Ok(expected_price)
354        );
355        assert_eq!(
356            price_update_fully_verified.get_price_unchecked(&feed_id),
357            Ok(expected_price)
358        );
359
360        assert_eq!(
361            price_update_unverified.get_price_no_older_than_with_custom_verification_level(
362                &mock_clock,
363                100,
364                &feed_id,
365                VerificationLevel::Partial { num_signatures: 5 }
366            ),
367            Err(GetPriceError::InsufficientVerificationLevel)
368        );
369        assert_eq!(
370            price_update_partially_verified.get_price_no_older_than_with_custom_verification_level(
371                &mock_clock,
372                100,
373                &feed_id,
374                VerificationLevel::Partial { num_signatures: 5 }
375            ),
376            Ok(expected_price)
377        );
378        assert_eq!(
379            price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
380                &mock_clock,
381                100,
382                &feed_id,
383                VerificationLevel::Partial { num_signatures: 5 }
384            ),
385            Ok(expected_price)
386        );
387
388        assert_eq!(
389            price_update_unverified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
390            Err(GetPriceError::InsufficientVerificationLevel)
391        );
392        assert_eq!(
393            price_update_partially_verified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
394            Err(GetPriceError::InsufficientVerificationLevel)
395        );
396        assert_eq!(
397            price_update_fully_verified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
398            Ok(expected_price)
399        );
400
401        // Reduce maximum_age
402        assert_eq!(
403            price_update_unverified.get_price_no_older_than_with_custom_verification_level(
404                &mock_clock,
405                10,
406                &feed_id,
407                VerificationLevel::Partial { num_signatures: 5 }
408            ),
409            Err(GetPriceError::InsufficientVerificationLevel)
410        );
411        assert_eq!(
412            price_update_partially_verified.get_price_no_older_than_with_custom_verification_level(
413                &mock_clock,
414                10,
415                &feed_id,
416                VerificationLevel::Partial { num_signatures: 5 }
417            ),
418            Err(GetPriceError::PriceTooOld)
419        );
420        assert_eq!(
421            price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
422                &mock_clock,
423                10,
424                &feed_id,
425                VerificationLevel::Partial { num_signatures: 5 }
426            ),
427            Err(GetPriceError::PriceTooOld)
428        );
429
430        assert_eq!(
431            price_update_unverified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
432            Err(GetPriceError::InsufficientVerificationLevel)
433        );
434        assert_eq!(
435            price_update_partially_verified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
436            Err(GetPriceError::InsufficientVerificationLevel)
437        );
438        assert_eq!(
439            price_update_fully_verified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
440            Err(GetPriceError::PriceTooOld)
441        );
442
443        // Mismatched feed id
444        assert_eq!(
445            price_update_fully_verified.get_price_unchecked(&mismatched_feed_id),
446            Err(GetPriceError::MismatchedFeedId)
447        );
448        assert_eq!(
449            price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
450                &mock_clock,
451                100,
452                &mismatched_feed_id,
453                VerificationLevel::Partial { num_signatures: 5 }
454            ),
455            Err(GetPriceError::MismatchedFeedId)
456        );
457        assert_eq!(
458            price_update_fully_verified.get_price_no_older_than(
459                &mock_clock,
460                100,
461                &mismatched_feed_id,
462            ),
463            Err(GetPriceError::MismatchedFeedId)
464        );
465    }
466}