Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions target_chains/solana/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pyth-solana-receiver"
version = "0.2.0"
version = "0.2.1"
description = "Created with Anchor"
edition = "2021"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,18 +575,14 @@ fn calculate_twap(start_msg: &TwapMessage, end_msg: &TwapMessage) -> Result<(i64

// Calculate down_slots_ratio as an integer between 0 and 1_000_000
// A value of 1_000_000 means all slots were missed and 0 means no slots were missed.
let total_slots = end_msg
.publish_slot
.checked_sub(start_msg.publish_slot)
.ok_or(ReceiverError::TwapCalculationOverflow)?;
let total_down_slots = end_msg
.num_down_slots
.checked_sub(start_msg.num_down_slots)
.ok_or(ReceiverError::TwapCalculationOverflow)?;
let down_slots_ratio = total_down_slots
.checked_mul(1_000_000)
.ok_or(ReceiverError::TwapCalculationOverflow)?
.checked_div(total_slots)
.checked_div(slot_diff)
.ok_or(ReceiverError::TwapCalculationOverflow)?;
// down_slots_ratio is a number in [0, 1_000_000], so we only need 32 unsigned bits
let down_slots_ratio =
Expand Down
2 changes: 1 addition & 1 deletion target_chains/solana/pyth_solana_receiver_sdk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 2 additions & 0 deletions target_chains/solana/pyth_solana_receiver_sdk/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
65 changes: 53 additions & 12 deletions target_chains/solana/pyth_solana_receiver_sdk/src/price_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -101,32 +102,39 @@ 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)]
/// pub struct ReadTwapAccount<'info> {
/// pub twap_update: Account<'info, TwapUpdate>,
/// }
///
/// pub fn read_twap_account(ctx : Context<ReadTwapAccount>) -> Result<()> {
/// pub fn read_twap_account(ctx: Context<ReadTwapAccount>) -> 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(())
/// }
/// ```
pub fn get_twap_no_older_than(
&self,
clock: &Clock,
maximum_age: u64,
window_seconds: u64,
feed_id: &FeedId,
) -> std::result::Result<TwapPrice, GetPriceError> {
// Ensure the update isn't outdated
Expand All @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get why you need TOLERANCE here. There should be at least one VAA per second with 400 milliseconds blocktimes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding was that there's a VAA roughly every 400ms. That means that if I request a 1 second TWAP (t0=1000 and t1=2000), the start VAA might have timestamp 800 and the end VAA might have timestamp 2000. That would result in a 1200ms window. If i had a strict check for window size, it would fail since 1200 != 1000. This is why i added the tolerance... but now i'm noticing that the start_time and end_time precision is in seconds and not milliseconds, so this shouldn't matter! Will go ahead and remove the tolerance. Does this check out?

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)
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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)
);

Expand All @@ -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)
);
}
Expand Down
Loading