Skip to content

Commit c716a93

Browse files
fix(pyth-solana-receiver): improve perf and security (#2222)
* refactor: reuse slots_diff calculation * fix: add window size check to get_twap_no_older_than * nit: fix comment * fix: remove tolerance in window size check * Update target_chains/solana/pyth_solana_receiver_sdk/src/price_update.rs Co-authored-by: guibescos <[email protected]> --------- Co-authored-by: guibescos <[email protected]>
1 parent 98c80f6 commit c716a93

File tree

6 files changed

+45
-21
lines changed

6 files changed

+45
-21
lines changed

target_chains/solana/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

target_chains/solana/programs/pyth-solana-receiver/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyth-solana-receiver"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
description = "Created with Anchor"
55
edition = "2021"
66

target_chains/solana/programs/pyth-solana-receiver/src/lib.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -575,18 +575,14 @@ fn calculate_twap(start_msg: &TwapMessage, end_msg: &TwapMessage) -> Result<(i64
575575

576576
// Calculate down_slots_ratio as an integer between 0 and 1_000_000
577577
// A value of 1_000_000 means all slots were missed and 0 means no slots were missed.
578-
let total_slots = end_msg
579-
.publish_slot
580-
.checked_sub(start_msg.publish_slot)
581-
.ok_or(ReceiverError::TwapCalculationOverflow)?;
582578
let total_down_slots = end_msg
583579
.num_down_slots
584580
.checked_sub(start_msg.num_down_slots)
585581
.ok_or(ReceiverError::TwapCalculationOverflow)?;
586582
let down_slots_ratio = total_down_slots
587583
.checked_mul(1_000_000)
588584
.ok_or(ReceiverError::TwapCalculationOverflow)?
589-
.checked_div(total_slots)
585+
.checked_div(slot_diff)
590586
.ok_or(ReceiverError::TwapCalculationOverflow)?;
591587
// down_slots_ratio is a number in [0, 1_000_000], so we only need 32 unsigned bits
592588
let down_slots_ratio =

target_chains/solana/pyth_solana_receiver_sdk/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyth-solana-receiver-sdk"
3-
version = "0.4.0"
3+
version = "0.5.0"
44
description = "SDK for the Pyth Solana Receiver program"
55
authors = ["Pyth Data Association"]
66
repository = "https://github.com/pyth-network/pyth-crosschain"

target_chains/solana/pyth_solana_receiver_sdk/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use anchor_lang::error_code;
55
pub enum GetPriceError {
66
#[msg("This price feed update's age exceeds the requested maximum age")]
77
PriceTooOld = 10000, // Big number to avoid conflicts with the SDK user's program error codes
8+
#[msg("This TWAP update's window size is invalid")]
9+
InvalidWindowSize,
810
#[msg("The price feed update doesn't match the requested feed id")]
911
MismatchedFeedId,
1012
#[msg("This price feed update has a lower verification level than the one requested")]

target_chains/solana/pyth_solana_receiver_sdk/src/price_update.rs

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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 arbitrary window length twap updates.
9495
pub fn get_twap_unchecked(
9596
&self,
9697
feed_id: &FeedId,
@@ -101,32 +102,38 @@ 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.
106106
///
107107
/// # Example
108108
/// ```
109109
/// use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, TwapUpdate};
110110
/// use anchor_lang::prelude::*;
111111
///
112-
/// const MAXIMUM_AGE : u64 = 30;
112+
/// const MAXIMUM_AGE: u64 = 30;
113+
/// const WINDOW_SECONDS: u64 = 300; // 5-minute TWAP
113114
/// const FEED_ID: &str = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; // SOL/USD
114115
///
115116
/// #[derive(Accounts)]
116117
/// pub struct ReadTwapAccount<'info> {
117118
/// pub twap_update: Account<'info, TwapUpdate>,
118119
/// }
119120
///
120-
/// pub fn read_twap_account(ctx : Context<ReadTwapAccount>) -> Result<()> {
121+
/// pub fn read_twap_account(ctx: Context<ReadTwapAccount>) -> Result<()> {
121122
/// 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)?)?;
123+
/// let twap = twap_update.get_twap_no_older_than(
124+
/// &Clock::get()?,
125+
/// MAXIMUM_AGE,
126+
/// WINDOW_SECONDS,
127+
/// &get_feed_id_from_hex(FEED_ID)?
128+
/// )?;
123129
/// Ok(())
124130
/// }
125131
/// ```
126132
pub fn get_twap_no_older_than(
127133
&self,
128134
clock: &Clock,
129135
maximum_age: u64,
136+
window_seconds: u64,
130137
feed_id: &FeedId,
131138
) -> std::result::Result<TwapPrice, GetPriceError> {
132139
// Ensure the update isn't outdated
@@ -138,6 +145,14 @@ impl TwapUpdate {
138145
>= clock.unix_timestamp,
139146
GetPriceError::PriceTooOld
140147
);
148+
149+
// Ensure the twap window size is as expected
150+
let actual_window = twap_price.end_time.saturating_sub(twap_price.start_time);
151+
check!(
152+
actual_window == i64::try_from(window_seconds).unwrap(),
153+
GetPriceError::InvalidWindowSize
154+
);
155+
141156
Ok(twap_price)
142157
}
143158
}
@@ -543,13 +558,12 @@ pub mod tests {
543558
Err(GetPriceError::MismatchedFeedId)
544559
);
545560
}
546-
547561
#[test]
548562
fn test_get_twap_no_older_than() {
549563
let expected_twap = TwapPrice {
550564
feed_id: [0; 32],
551565
start_time: 800,
552-
end_time: 900,
566+
end_time: 900, // Window size is 100 seconds (900 - 800)
553567
price: 1,
554568
conf: 2,
555569
exponent: -3,
@@ -571,15 +585,27 @@ pub mod tests {
571585
// Test unchecked access
572586
assert_eq!(update.get_twap_unchecked(&feed_id), Ok(expected_twap));
573587

574-
// Test with age check
588+
// Test with correct window size (100 seconds)
575589
assert_eq!(
576-
update.get_twap_no_older_than(&mock_clock, 100, &feed_id),
590+
update.get_twap_no_older_than(&mock_clock, 100, 100, &feed_id),
577591
Ok(expected_twap)
578592
);
579593

594+
// Test with incorrect window size
595+
assert_eq!(
596+
update.get_twap_no_older_than(&mock_clock, 100, 101, &feed_id),
597+
Err(GetPriceError::InvalidWindowSize)
598+
);
599+
600+
// Test with incorrect window size
601+
assert_eq!(
602+
update.get_twap_no_older_than(&mock_clock, 100, 99, &feed_id),
603+
Err(GetPriceError::InvalidWindowSize)
604+
);
605+
580606
// Test with reduced maximum age
581607
assert_eq!(
582-
update.get_twap_no_older_than(&mock_clock, 10, &feed_id),
608+
update.get_twap_no_older_than(&mock_clock, 10, 100, &feed_id),
583609
Err(GetPriceError::PriceTooOld)
584610
);
585611

@@ -589,7 +615,7 @@ pub mod tests {
589615
Err(GetPriceError::MismatchedFeedId)
590616
);
591617
assert_eq!(
592-
update.get_twap_no_older_than(&mock_clock, 100, &mismatched_feed_id),
618+
update.get_twap_no_older_than(&mock_clock, 100, 100, &mismatched_feed_id),
593619
Err(GetPriceError::MismatchedFeedId)
594620
);
595621
}

0 commit comments

Comments
 (0)