@@ -87,10 +87,11 @@ impl TwapUpdate {
8787 /// # Warning
8888 /// This function does not check :
8989 /// - How recent the price is
90+ /// - If the TWAP's window size is expected
9091 /// - Whether the price update has been verified
9192 ///
9293 /// It is therefore unsafe to use this function without any extra checks,
93- /// as it allows for the possibility of using unverified or outdated price updates.
94+ /// as it allows for the possibility of using unverified, outdated, or unexpected price updates.
9495 pub fn get_twap_unchecked (
9596 & self ,
9697 feed_id : & FeedId ,
@@ -101,35 +102,42 @@ impl TwapUpdate {
101102 ) ;
102103 Ok ( self . twap )
103104 }
104-
105- /// Get a `TwapPrice` from a `TwapUpdate` account for a given `FeedId` no older than `maximum_age` .
105+ /// Get a `TwapPrice` from a `TwapUpdate` account for a given `FeedId` no older than `maximum_age` with a specific window size.
106+ /// The window size check includes a tolerance of ±1 second to account for Solana block time variations .
106107 ///
107108 /// # Example
108109 /// ```
109110 /// use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, TwapUpdate};
110111 /// use anchor_lang::prelude::*;
111112 ///
112- /// const MAXIMUM_AGE : u64 = 30;
113+ /// const MAXIMUM_AGE: u64 = 30;
114+ /// const WINDOW_SECONDS: u64 = 300; // 5-minute TWAP
113115 /// const FEED_ID: &str = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; // SOL/USD
114116 ///
115117 /// #[derive(Accounts)]
116118 /// pub struct ReadTwapAccount<'info> {
117119 /// pub twap_update: Account<'info, TwapUpdate>,
118120 /// }
119121 ///
120- /// pub fn read_twap_account(ctx : Context<ReadTwapAccount>) -> Result<()> {
122+ /// pub fn read_twap_account(ctx: Context<ReadTwapAccount>) -> Result<()> {
121123 /// let twap_update = &ctx.accounts.twap_update;
122- /// let twap = twap_update.get_twap_no_older_than(&Clock::get()?, MAXIMUM_AGE, &get_feed_id_from_hex(FEED_ID)?)?;
124+ /// let twap = twap_update.get_twap_no_older_than(
125+ /// &Clock::get()?,
126+ /// MAXIMUM_AGE,
127+ /// WINDOW_SECONDS,
128+ /// &get_feed_id_from_hex(FEED_ID)?
129+ /// )?;
123130 /// Ok(())
124131 /// }
125132 /// ```
126133 pub fn get_twap_no_older_than (
127134 & self ,
128135 clock : & Clock ,
129136 maximum_age : u64 ,
137+ window_seconds : u64 ,
130138 feed_id : & FeedId ,
131139 ) -> std:: result:: Result < TwapPrice , GetPriceError > {
132- // Ensure the update isn't outdated
140+ // Ensure the update is isn't outdated
133141 let twap_price = self . get_twap_unchecked ( feed_id) ?;
134142 check ! (
135143 twap_price
@@ -138,6 +146,16 @@ impl TwapUpdate {
138146 >= clock. unix_timestamp,
139147 GetPriceError :: PriceTooOld
140148 ) ;
149+
150+ // Ensure the twap window size is as expected
151+ // Allow for +/- 1 second tolerance to account for the imprecision introduced by Solana block times
152+ const TOLERANCE : i64 = 1 ;
153+ let actual_window = twap_price. end_time . saturating_sub ( twap_price. start_time ) ;
154+ check ! (
155+ ( actual_window - i64 :: try_from( window_seconds) . unwrap( ) ) . abs( ) <= TOLERANCE ,
156+ GetPriceError :: InvalidWindowSize
157+ ) ;
158+
141159 Ok ( twap_price)
142160 }
143161}
@@ -543,13 +561,12 @@ pub mod tests {
543561 Err ( GetPriceError :: MismatchedFeedId )
544562 ) ;
545563 }
546-
547564 #[ test]
548565 fn test_get_twap_no_older_than ( ) {
549566 let expected_twap = TwapPrice {
550567 feed_id : [ 0 ; 32 ] ,
551568 start_time : 800 ,
552- end_time : 900 ,
569+ end_time : 900 , // Window size is 100 seconds (900 - 800)
553570 price : 1 ,
554571 conf : 2 ,
555572 exponent : -3 ,
@@ -571,15 +588,39 @@ pub mod tests {
571588 // Test unchecked access
572589 assert_eq ! ( update. get_twap_unchecked( & feed_id) , Ok ( expected_twap) ) ;
573590
574- // Test with age check
591+ // Test with correct window size (100 seconds)
592+ assert_eq ! (
593+ update. get_twap_no_older_than( & mock_clock, 100 , 100 , & feed_id) ,
594+ Ok ( expected_twap)
595+ ) ;
596+
597+ // Test with window size within tolerance (+1 second)
575598 assert_eq ! (
576- update. get_twap_no_older_than( & mock_clock, 100 , & feed_id) ,
599+ update. get_twap_no_older_than( & mock_clock, 100 , 101 , & feed_id) ,
577600 Ok ( expected_twap)
578601 ) ;
579602
603+ // Test with window size within tolerance (-1 second)
604+ assert_eq ! (
605+ update. get_twap_no_older_than( & mock_clock, 100 , 99 , & feed_id) ,
606+ Ok ( expected_twap)
607+ ) ;
608+
609+ // Test with incorrect window size (outside tolerance)
610+ assert_eq ! (
611+ update. get_twap_no_older_than( & mock_clock, 100 , 103 , & feed_id) ,
612+ Err ( GetPriceError :: InvalidWindowSize )
613+ ) ;
614+
615+ // Test with incorrect window size (outside tolerance)
616+ assert_eq ! (
617+ update. get_twap_no_older_than( & mock_clock, 100 , 97 , & feed_id) ,
618+ Err ( GetPriceError :: InvalidWindowSize )
619+ ) ;
620+
580621 // Test with reduced maximum age
581622 assert_eq ! (
582- update. get_twap_no_older_than( & mock_clock, 10 , & feed_id) ,
623+ update. get_twap_no_older_than( & mock_clock, 10 , 100 , & feed_id) ,
583624 Err ( GetPriceError :: PriceTooOld )
584625 ) ;
585626
@@ -589,7 +630,7 @@ pub mod tests {
589630 Err ( GetPriceError :: MismatchedFeedId )
590631 ) ;
591632 assert_eq ! (
592- update. get_twap_no_older_than( & mock_clock, 100 , & mismatched_feed_id) ,
633+ update. get_twap_no_older_than( & mock_clock, 100 , 100 , & mismatched_feed_id) ,
593634 Err ( GetPriceError :: MismatchedFeedId )
594635 ) ;
595636 }
0 commit comments