From 39437e0601324d601866e9c6b6dcdeed6e53f8f0 Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Thu, 2 Jan 2025 19:18:47 -0800 Subject: [PATCH] fix: add window size check to get_twap_no_older_than --- target_chains/solana/Cargo.lock | 4 +- .../programs/pyth-solana-receiver/Cargo.toml | 2 +- .../pyth_solana_receiver_sdk/Cargo.toml | 2 +- .../pyth_solana_receiver_sdk/src/error.rs | 2 + .../src/price_update.rs | 67 +++++++++++++++---- 5 files changed, 60 insertions(+), 17 deletions(-) diff --git a/target_chains/solana/Cargo.lock b/target_chains/solana/Cargo.lock index 69564db24d..8329623e6a 100644 --- a/target_chains/solana/Cargo.lock +++ b/target_chains/solana/Cargo.lock @@ -3065,7 +3065,7 @@ dependencies = [ [[package]] name = "pyth-solana-receiver" -version = "0.2.0" +version = "0.2.1" dependencies = [ "anchor-lang", "byteorder", @@ -3107,7 +3107,7 @@ dependencies = [ [[package]] name = "pyth-solana-receiver-sdk" -version = "0.4.0" +version = "0.5.0" dependencies = [ "anchor-lang", "hex", diff --git a/target_chains/solana/programs/pyth-solana-receiver/Cargo.toml b/target_chains/solana/programs/pyth-solana-receiver/Cargo.toml index 376248d1e5..e79b2ec763 100644 --- a/target_chains/solana/programs/pyth-solana-receiver/Cargo.toml +++ b/target_chains/solana/programs/pyth-solana-receiver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyth-solana-receiver" -version = "0.2.0" +version = "0.2.1" description = "Created with Anchor" edition = "2021" diff --git a/target_chains/solana/pyth_solana_receiver_sdk/Cargo.toml b/target_chains/solana/pyth_solana_receiver_sdk/Cargo.toml index dd21a59d47..2d907f0598 100644 --- a/target_chains/solana/pyth_solana_receiver_sdk/Cargo.toml +++ b/target_chains/solana/pyth_solana_receiver_sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyth-solana-receiver-sdk" -version = "0.4.0" +version = "0.5.0" description = "SDK for the Pyth Solana Receiver program" authors = ["Pyth Data Association"] repository = "https://github.com/pyth-network/pyth-crosschain" diff --git a/target_chains/solana/pyth_solana_receiver_sdk/src/error.rs b/target_chains/solana/pyth_solana_receiver_sdk/src/error.rs index 2e5d2722a9..7ee27df61a 100644 --- a/target_chains/solana/pyth_solana_receiver_sdk/src/error.rs +++ b/target_chains/solana/pyth_solana_receiver_sdk/src/error.rs @@ -5,6 +5,8 @@ use anchor_lang::error_code; pub enum GetPriceError { #[msg("This price feed update's age exceeds the requested maximum age")] PriceTooOld = 10000, // Big number to avoid conflicts with the SDK user's program error codes + #[msg("This TWAP update's window size is invalid")] + InvalidWindowSize, #[msg("The price feed update doesn't match the requested feed id")] MismatchedFeedId, #[msg("This price feed update has a lower verification level than the one requested")] diff --git a/target_chains/solana/pyth_solana_receiver_sdk/src/price_update.rs b/target_chains/solana/pyth_solana_receiver_sdk/src/price_update.rs index 79741b66cb..66b306a0b5 100644 --- a/target_chains/solana/pyth_solana_receiver_sdk/src/price_update.rs +++ b/target_chains/solana/pyth_solana_receiver_sdk/src/price_update.rs @@ -87,10 +87,11 @@ impl TwapUpdate { /// # Warning /// This function does not check : /// - How recent the price is + /// - If the TWAP's window size is expected /// - Whether the price update has been verified /// /// 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. + /// as it allows for the possibility of using unverified, outdated, or unexpected price updates. pub fn get_twap_unchecked( &self, feed_id: &FeedId, @@ -101,15 +102,16 @@ impl TwapUpdate { ); Ok(self.twap) } - - /// Get a `TwapPrice` from a `TwapUpdate` account for a given `FeedId` no older than `maximum_age`. + /// Get a `TwapPrice` from a `TwapUpdate` account for a given `FeedId` no older than `maximum_age` with a specific window size. + /// The window size check includes a tolerance of ±1 second to account for Solana block time variations. /// /// # Example /// ``` /// use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, TwapUpdate}; /// use anchor_lang::prelude::*; /// - /// const MAXIMUM_AGE : u64 = 30; + /// const MAXIMUM_AGE: u64 = 30; + /// const WINDOW_SECONDS: u64 = 300; // 5-minute TWAP /// const FEED_ID: &str = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; // SOL/USD /// /// #[derive(Accounts)] @@ -117,9 +119,14 @@ impl TwapUpdate { /// pub twap_update: Account<'info, TwapUpdate>, /// } /// - /// pub fn read_twap_account(ctx : Context) -> Result<()> { + /// pub fn read_twap_account(ctx: Context) -> Result<()> { /// let twap_update = &ctx.accounts.twap_update; - /// let twap = twap_update.get_twap_no_older_than(&Clock::get()?, MAXIMUM_AGE, &get_feed_id_from_hex(FEED_ID)?)?; + /// let twap = twap_update.get_twap_no_older_than( + /// &Clock::get()?, + /// MAXIMUM_AGE, + /// WINDOW_SECONDS, + /// &get_feed_id_from_hex(FEED_ID)? + /// )?; /// Ok(()) /// } /// ``` @@ -127,9 +134,10 @@ impl TwapUpdate { &self, clock: &Clock, maximum_age: u64, + window_seconds: u64, feed_id: &FeedId, ) -> std::result::Result { - // Ensure the update isn't outdated + // Ensure the update is isn't outdated let twap_price = self.get_twap_unchecked(feed_id)?; check!( twap_price @@ -138,6 +146,16 @@ impl TwapUpdate { >= clock.unix_timestamp, GetPriceError::PriceTooOld ); + + // Ensure the twap window size is as expected + // Allow for +/- 1 second tolerance to account for the imprecision introduced by Solana block times + const TOLERANCE: i64 = 1; + let actual_window = twap_price.end_time.saturating_sub(twap_price.start_time); + check!( + (actual_window - i64::try_from(window_seconds).unwrap()).abs() <= TOLERANCE, + GetPriceError::InvalidWindowSize + ); + Ok(twap_price) } } @@ -543,13 +561,12 @@ pub mod tests { Err(GetPriceError::MismatchedFeedId) ); } - #[test] fn test_get_twap_no_older_than() { let expected_twap = TwapPrice { feed_id: [0; 32], start_time: 800, - end_time: 900, + end_time: 900, // Window size is 100 seconds (900 - 800) price: 1, conf: 2, exponent: -3, @@ -571,15 +588,39 @@ pub mod tests { // Test unchecked access assert_eq!(update.get_twap_unchecked(&feed_id), Ok(expected_twap)); - // Test with age check + // Test with correct window size (100 seconds) + assert_eq!( + update.get_twap_no_older_than(&mock_clock, 100, 100, &feed_id), + Ok(expected_twap) + ); + + // Test with window size within tolerance (+1 second) assert_eq!( - update.get_twap_no_older_than(&mock_clock, 100, &feed_id), + update.get_twap_no_older_than(&mock_clock, 100, 101, &feed_id), Ok(expected_twap) ); + // Test with window size within tolerance (-1 second) + assert_eq!( + update.get_twap_no_older_than(&mock_clock, 100, 99, &feed_id), + Ok(expected_twap) + ); + + // Test with incorrect window size (outside tolerance) + assert_eq!( + update.get_twap_no_older_than(&mock_clock, 100, 103, &feed_id), + Err(GetPriceError::InvalidWindowSize) + ); + + // Test with incorrect window size (outside tolerance) + assert_eq!( + update.get_twap_no_older_than(&mock_clock, 100, 97, &feed_id), + Err(GetPriceError::InvalidWindowSize) + ); + // Test with reduced maximum age assert_eq!( - update.get_twap_no_older_than(&mock_clock, 10, &feed_id), + update.get_twap_no_older_than(&mock_clock, 10, 100, &feed_id), Err(GetPriceError::PriceTooOld) ); @@ -589,7 +630,7 @@ pub mod tests { Err(GetPriceError::MismatchedFeedId) ); assert_eq!( - update.get_twap_no_older_than(&mock_clock, 100, &mismatched_feed_id), + update.get_twap_no_older_than(&mock_clock, 100, 100, &mismatched_feed_id), Err(GetPriceError::MismatchedFeedId) ); }