From 4cc13db633ea3616e6e334f23575cd822b6046b3 Mon Sep 17 00:00:00 2001 From: Ayush Suresh Date: Mon, 7 Jul 2025 13:55:20 -0500 Subject: [PATCH 01/16] removed extra get fee function --- .../stylus/contracts/pyth-receiver/src/lib.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index 029ecbcf88..7f479b558c 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -260,22 +260,18 @@ impl PythReceiver { U256::from(num_updates).saturating_mul(self.single_update_fee_in_wei.get()) } - pub fn get_update_fee(&self, _update_data: Vec>) -> U256 { - U256::from(0u8) - } - pub fn get_twap_update_fee(&self, _update_data: Vec>) -> U256 { U256::from(0u8) } pub fn parse_price_feed_updates( &mut self, - _update_data: Vec>, - _price_ids: Vec<[u8; 32]>, - _min_publish_time: u64, - _max_publish_time: u64, + update_data: Vec>, + price_ids: Vec<[u8; 32]>, + min_publish_time: u64, + max_publish_time: u64, ) -> Vec { - Vec::new() + parse_price_feed_updates_with_config(update_data, price_ids, min_publish_time, max_publish_time, false, false, false) } pub fn parse_price_feed_updates_with_config( From 088d0d8dd07f4b28d2821c8158b777ed71e32e50 Mon Sep 17 00:00:00 2001 From: Ayush Suresh Date: Mon, 7 Jul 2025 17:52:34 -0500 Subject: [PATCH 02/16] setting up internal parse fxn --- .../stylus/contracts/pyth-receiver/src/lib.rs | 127 ++++++++++++------ 1 file changed, 83 insertions(+), 44 deletions(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index 7f479b558c..f172fa3872 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -151,7 +151,7 @@ impl PythReceiver { #[payable] pub fn update_price_feeds(&mut self, update_data: Vec) -> Result<(), PythReceiverError> { - self.update_price_feeds_internal(update_data)?; + self.update_price_feeds_internal(update_data, Vec::new(), 0, 0, false)?; Ok(()) } @@ -164,7 +164,70 @@ impl PythReceiver { // dummy implementation } - fn update_price_feeds_internal(&mut self, update_data: Vec) -> Result<(), PythReceiverError> { + fn update_price_feeds_internal(&mut self, update_data: Vec, price_ids: Vec<[u8; 32]>, min_publish_time: u64, max_publish_time: u64, unique: bool) -> Result<(), PythReceiverError> { + let price_returns= self.parse_price_feed_updates(update_data, price_ids.clone(), min_publish_time, max_publish_time)?; + for i in 0..price_ids.clone().len() { + let cur_price_id = &price_ids[i]; + let cur_price_return = &price_returns[i]; + let price_id_fb : FixedBytes<32> = FixedBytes::from(cur_price_id); + let mut recent_price_info = self.latest_price_info.setter(price_id_fb); + + if recent_price_info.publish_time.get() < cur_price_return.0 + || recent_price_info.price.get() == I64::ZERO { + recent_price_info.publish_time.set(cur_price_return.0); + recent_price_info.expo.set(cur_price_return.1); + recent_price_info.price.set(cur_price_return.2); + recent_price_info.conf.set(cur_price_return.3); + recent_price_info.ema_price.set(cur_price_return.4); + recent_price_info.ema_conf.set(cur_price_return.5); + } + } + + Ok(()) + } + + fn get_total_fee(&self, num_updates: u8) -> U256 { + U256::from(num_updates).saturating_mul(self.single_update_fee_in_wei.get()) + } + + pub fn get_twap_update_fee(&self, _update_data: Vec>) -> U256 { + U256::from(0u8) + } + + pub fn parse_price_feed_updates( + &mut self, + update_data: Vec, + price_ids: Vec<[u8; 32]>, + min_publish_time: u64, + max_publish_time: u64, + ) -> Result, PythReceiverError> { + let price_feeds = self.parse_price_feed_updates_with_config(update_data, price_ids, min_publish_time, max_publish_time, false, false, false); + price_feeds + } + + pub fn parse_price_feed_updates_with_config( + &mut self, + update_data: Vec, + price_ids: Vec<[u8; 32]>, + min_allowed_publish_time: u64, + max_allowed_publish_time: u64, + check_uniqueness: bool, + check_update_data_is_minimal: bool, + store_updates_if_fresh: bool, + ) -> Result, PythReceiverError> { + + } + + fn parse_price_feed_updates_internal( + &mut self, + update_data: Vec, + price_ids: Vec<[u8; 32]>, + min_allowed_publish_time: u64, + max_allowed_publish_time: u64, + check_uniqueness: bool, + check_update_data_is_minimal: bool, + store_updates_if_fresh: bool, + ) -> Result, PythReceiverError> { let update_data_array: &[u8] = &update_data; // Check the first 4 bytes of the update_data_array for the magic header if update_data_array.len() < 4 { @@ -180,6 +243,8 @@ impl PythReceiver { let update_data = AccumulatorUpdateData::try_from_slice(&update_data_array).map_err(|_| PythReceiverError::InvalidAccumulatorMessage)?; + let mut price_feeds: Vec = Vec::new(); + match update_data.proof { Proof::WormholeMerkle { vaa, updates } => { let wormhole: IWormholeContract = IWormholeContract::new(self.wormhole.get()); @@ -233,17 +298,22 @@ impl PythReceiver { let price_id_fb : FixedBytes<32> = FixedBytes::from(price_feed_message.feed_id); let mut recent_price_info = self.latest_price_info.setter(price_id_fb); - if recent_price_info.publish_time.get() < U64::from(price_feed_message.publish_time) - || recent_price_info.price.get() == I64::ZERO { - recent_price_info.publish_time.set(U64::from(price_feed_message.publish_time)); - recent_price_info.price.set(I64::from_le_bytes(price_feed_message.price.to_le_bytes())); - recent_price_info.conf.set(U64::from(price_feed_message.conf)); - recent_price_info.expo.set(I32::from_le_bytes(price_feed_message.exponent.to_le_bytes())); - recent_price_info.ema_price.set(I64::from_le_bytes(price_feed_message.ema_price.to_le_bytes())); - recent_price_info.ema_conf.set(U64::from(price_feed_message.ema_conf)); + let price_info_return = ( + recent_price_info.publish_time.get(), + recent_price_info.expo.get(), + recent_price_info.price.get(), + recent_price_info.conf.get(), + recent_price_info.ema_price.get(), + recent_price_info.ema_conf.get(), + ); + + // Find the index of the price_id in the input price_ids vector + if let Some(idx) = price_ids.iter().position(|id| *id == price_feed_message.feed_id) { + if price_feeds.len() <= idx { + price_feeds.resize(idx + 1, Default::default()); + } + price_feeds[idx] = price_info_return; } - - }, _ => { return Err(PythReceiverError::InvalidAccumulatorMessageType); @@ -253,38 +323,7 @@ impl PythReceiver { } }; - Ok(()) - } - - fn get_total_fee(&self, num_updates: u8) -> U256 { - U256::from(num_updates).saturating_mul(self.single_update_fee_in_wei.get()) - } - - pub fn get_twap_update_fee(&self, _update_data: Vec>) -> U256 { - U256::from(0u8) - } - - pub fn parse_price_feed_updates( - &mut self, - update_data: Vec>, - price_ids: Vec<[u8; 32]>, - min_publish_time: u64, - max_publish_time: u64, - ) -> Vec { - parse_price_feed_updates_with_config(update_data, price_ids, min_publish_time, max_publish_time, false, false, false) - } - - pub fn parse_price_feed_updates_with_config( - &mut self, - _update_data: Vec>, - _price_ids: Vec<[u8; 32]>, - _min_allowed_publish_time: u64, - _max_allowed_publish_time: u64, - _check_uniqueness: bool, - _check_update_data_is_minimal: bool, - _store_updates_if_fresh: bool, - ) -> (Vec, Vec) { - (Vec::new(), Vec::new()) + Ok(price_feeds) } pub fn parse_twap_price_feed_updates( From 82783ecc489a29a329eb9b0d9e38ba5f0693396b Mon Sep 17 00:00:00 2001 From: Ayush Suresh Date: Mon, 7 Jul 2025 17:55:10 -0500 Subject: [PATCH 03/16] removed price ids input --- target_chains/stylus/contracts/pyth-receiver/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index f172fa3872..730b0344c2 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -221,7 +221,6 @@ impl PythReceiver { fn parse_price_feed_updates_internal( &mut self, update_data: Vec, - price_ids: Vec<[u8; 32]>, min_allowed_publish_time: u64, max_allowed_publish_time: u64, check_uniqueness: bool, From 9453d7b4b7a3acec96be826b96f84d9a0ec18801 Mon Sep 17 00:00:00 2001 From: Ayush Suresh Date: Mon, 7 Jul 2025 17:55:20 -0500 Subject: [PATCH 04/16] removed price ids input 2 --- .../stylus/contracts/pyth-receiver/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index 730b0344c2..bd94e87561 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -307,12 +307,12 @@ impl PythReceiver { ); // Find the index of the price_id in the input price_ids vector - if let Some(idx) = price_ids.iter().position(|id| *id == price_feed_message.feed_id) { - if price_feeds.len() <= idx { - price_feeds.resize(idx + 1, Default::default()); - } - price_feeds[idx] = price_info_return; - } + // if let Some(idx) = price_ids.iter().position(|id| *id == price_feed_message.feed_id) { + // if price_feeds.len() <= idx { + // price_feeds.resize(idx + 1, Default::default()); + // } + // price_feeds[idx] = price_info_return; + // } }, _ => { return Err(PythReceiverError::InvalidAccumulatorMessageType); From 22ebbd304d6745d6bf19b179bfff7eb45ad060b0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 23:07:49 +0000 Subject: [PATCH 05/16] feat: modify parse functions to return HashMap internally and matching vector indices - Modified parse_price_feed_updates_internal to build a BTreeMap internally mapping price IDs to PriceInfoReturn - Updated parse_price_feed_updates_with_config to return a vector with matching indices to input price_ids - Added price_ids parameter to parse_price_feed_updates_internal function - Maintained existing error handling and validation logic Co-Authored-By: ayush.suresh@dourolabs.xyz --- .../stylus/contracts/pyth-receiver/src/lib.rs | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index bd94e87561..6f4bd5eea9 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -12,7 +12,7 @@ mod integration_tests; #[cfg(test)] mod test_data; -use alloc::vec::Vec; +use alloc::{vec::Vec, collections::BTreeMap}; use stylus_sdk::{alloy_primitives::{U16, U32, U256, U64, I32, I64, FixedBytes, Address}, prelude::*, storage::{StorageAddress, StorageVec, StorageMap, StorageUint, StorageBool, StorageU256, StorageU16, StorageFixedBytes}, @@ -215,12 +215,21 @@ impl PythReceiver { check_update_data_is_minimal: bool, store_updates_if_fresh: bool, ) -> Result, PythReceiverError> { - + self.parse_price_feed_updates_internal( + update_data, + price_ids, + min_allowed_publish_time, + max_allowed_publish_time, + check_uniqueness, + check_update_data_is_minimal, + store_updates_if_fresh, + ) } fn parse_price_feed_updates_internal( &mut self, update_data: Vec, + price_ids: Vec<[u8; 32]>, min_allowed_publish_time: u64, max_allowed_publish_time: u64, check_uniqueness: bool, @@ -242,14 +251,14 @@ impl PythReceiver { let update_data = AccumulatorUpdateData::try_from_slice(&update_data_array).map_err(|_| PythReceiverError::InvalidAccumulatorMessage)?; - let mut price_feeds: Vec = Vec::new(); - + let mut price_feeds: BTreeMap<[u8; 32], PriceInfoReturn> = BTreeMap::new(); + match update_data.proof { Proof::WormholeMerkle { vaa, updates } => { let wormhole: IWormholeContract = IWormholeContract::new(self.wormhole.get()); let config = Call::new(); wormhole.parse_and_verify_vm(config, Vec::from(vaa.clone())).map_err(|_| PythReceiverError::InvalidWormholeMessage)?; - + let vaa = Vaa::read(&mut Vec::from(vaa.clone()).as_slice()).map_err(|_| PythReceiverError::VaaVerificationFailed)?; let cur_emitter_address: &[u8; 32] = vaa @@ -275,7 +284,7 @@ impl PythReceiver { let total_fee = self.get_total_fee(num_updates); let value = self.vm().msg_value(); - + if value < total_fee { return Err(PythReceiverError::InsufficientFee); } @@ -306,13 +315,7 @@ impl PythReceiver { recent_price_info.ema_conf.get(), ); - // Find the index of the price_id in the input price_ids vector - // if let Some(idx) = price_ids.iter().position(|id| *id == price_feed_message.feed_id) { - // if price_feeds.len() <= idx { - // price_feeds.resize(idx + 1, Default::default()); - // } - // price_feeds[idx] = price_info_return; - // } + price_feeds.insert(price_feed_message.feed_id, price_info_return); }, _ => { return Err(PythReceiverError::InvalidAccumulatorMessageType); @@ -322,7 +325,17 @@ impl PythReceiver { } }; - Ok(price_feeds) + let mut result: Vec = Vec::with_capacity(price_ids.len()); + + for price_id in price_ids { + if let Some(price_info) = price_feeds.get(&price_id) { + result.push(*price_info); + } else { + result.push((U64::from(0), I32::from_be_bytes([0, 0, 0, 0]), I64::from_be_bytes([0, 0, 0, 0, 0, 0, 0, 0]), U64::from(0), I64::from_be_bytes([0, 0, 0, 0, 0, 0, 0, 0]), U64::from(0))); + } + } + + Ok(result) } pub fn parse_twap_price_feed_updates( From ce8a378a96c25d19ccea29ca60bad8bd64c462db Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 23:20:29 +0000 Subject: [PATCH 06/16] feat: modify parse functions to return map internally and handle array ordering - parse_price_feed_updates_internal now returns Vec<([u8; 32], PriceInfoReturn)> without taking price_ids parameter - parse_price_feed_updates_with_config converts internal result to BTreeMap for efficient lookup and returns ordered vector - This enables update_price_feeds functions to use the dictionary directly as requested Co-Authored-By: ayush.suresh@dourolabs.xyz --- .../stylus/contracts/pyth-receiver/src/lib.rs | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index 6f4bd5eea9..8f5d2d4845 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -215,27 +215,40 @@ impl PythReceiver { check_update_data_is_minimal: bool, store_updates_if_fresh: bool, ) -> Result, PythReceiverError> { - self.parse_price_feed_updates_internal( + let price_pairs = self.parse_price_feed_updates_internal( update_data, - price_ids, min_allowed_publish_time, max_allowed_publish_time, check_uniqueness, check_update_data_is_minimal, store_updates_if_fresh, - ) + )?; + + let price_map: BTreeMap<[u8; 32], PriceInfoReturn> = price_pairs.into_iter().collect(); + + // Create a vector with the same length as price_ids + let mut result: Vec = Vec::with_capacity(price_ids.len()); + + for price_id in price_ids { + if let Some(price_info) = price_map.get(&price_id) { + result.push(*price_info); + } else { + result.push((U64::from(0), I32::from_be_bytes([0, 0, 0, 0]), I64::from_be_bytes([0, 0, 0, 0, 0, 0, 0, 0]), U64::from(0), I64::from_be_bytes([0, 0, 0, 0, 0, 0, 0, 0]), U64::from(0))); + } + } + + Ok(result) } fn parse_price_feed_updates_internal( &mut self, update_data: Vec, - price_ids: Vec<[u8; 32]>, min_allowed_publish_time: u64, max_allowed_publish_time: u64, check_uniqueness: bool, check_update_data_is_minimal: bool, store_updates_if_fresh: bool, - ) -> Result, PythReceiverError> { + ) -> Result, PythReceiverError> { let update_data_array: &[u8] = &update_data; // Check the first 4 bytes of the update_data_array for the magic header if update_data_array.len() < 4 { @@ -325,17 +338,7 @@ impl PythReceiver { } }; - let mut result: Vec = Vec::with_capacity(price_ids.len()); - - for price_id in price_ids { - if let Some(price_info) = price_feeds.get(&price_id) { - result.push(*price_info); - } else { - result.push((U64::from(0), I32::from_be_bytes([0, 0, 0, 0]), I64::from_be_bytes([0, 0, 0, 0, 0, 0, 0, 0]), U64::from(0), I64::from_be_bytes([0, 0, 0, 0, 0, 0, 0, 0]), U64::from(0))); - } - } - - Ok(result) + Ok(price_feeds.into_iter().collect()) } pub fn parse_twap_price_feed_updates( From bc242c75b233ba261aa259a1818fb6ce087b2ab0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 23:27:06 +0000 Subject: [PATCH 07/16] fix: parse_price_feed_updates_internal now processes incoming price feed data - Extract price data from price_feed_message instead of reading from storage - Fixes test failures where price data wasn't being processed correctly - Maintains separation of concerns between parsing and storage updates Co-Authored-By: ayush.suresh@dourolabs.xyz --- .../stylus/contracts/pyth-receiver/src/lib.rs | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index 8f5d2d4845..cd09e8ca1c 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -164,22 +164,28 @@ impl PythReceiver { // dummy implementation } - fn update_price_feeds_internal(&mut self, update_data: Vec, price_ids: Vec<[u8; 32]>, min_publish_time: u64, max_publish_time: u64, unique: bool) -> Result<(), PythReceiverError> { - let price_returns= self.parse_price_feed_updates(update_data, price_ids.clone(), min_publish_time, max_publish_time)?; - for i in 0..price_ids.clone().len() { - let cur_price_id = &price_ids[i]; - let cur_price_return = &price_returns[i]; - let price_id_fb : FixedBytes<32> = FixedBytes::from(cur_price_id); + fn update_price_feeds_internal(&mut self, update_data: Vec, _price_ids: Vec<[u8; 32]>, min_publish_time: u64, max_publish_time: u64, _unique: bool) -> Result<(), PythReceiverError> { + let price_pairs = self.parse_price_feed_updates_internal( + update_data, + min_publish_time, + max_publish_time, + false, // check_uniqueness + false, // check_update_data_is_minimal + true, // store_updates_if_fresh + )?; + + for (price_id, price_return) in price_pairs { + let price_id_fb: FixedBytes<32> = FixedBytes::from(price_id); let mut recent_price_info = self.latest_price_info.setter(price_id_fb); - if recent_price_info.publish_time.get() < cur_price_return.0 + if recent_price_info.publish_time.get() < price_return.0 || recent_price_info.price.get() == I64::ZERO { - recent_price_info.publish_time.set(cur_price_return.0); - recent_price_info.expo.set(cur_price_return.1); - recent_price_info.price.set(cur_price_return.2); - recent_price_info.conf.set(cur_price_return.3); - recent_price_info.ema_price.set(cur_price_return.4); - recent_price_info.ema_conf.set(cur_price_return.5); + recent_price_info.publish_time.set(price_return.0); + recent_price_info.expo.set(price_return.1); + recent_price_info.price.set(price_return.2); + recent_price_info.conf.set(price_return.3); + recent_price_info.ema_price.set(price_return.4); + recent_price_info.ema_conf.set(price_return.5); } } @@ -316,16 +322,13 @@ impl PythReceiver { match msg { Message::PriceFeedMessage(price_feed_message) => { - let price_id_fb : FixedBytes<32> = FixedBytes::from(price_feed_message.feed_id); - let mut recent_price_info = self.latest_price_info.setter(price_id_fb); - let price_info_return = ( - recent_price_info.publish_time.get(), - recent_price_info.expo.get(), - recent_price_info.price.get(), - recent_price_info.conf.get(), - recent_price_info.ema_price.get(), - recent_price_info.ema_conf.get(), + U64::from(price_feed_message.publish_time), + I32::from_be_bytes(price_feed_message.exponent.to_be_bytes()), + I64::from_be_bytes(price_feed_message.price.to_be_bytes()), + U64::from(price_feed_message.conf), + I64::from_be_bytes(price_feed_message.ema_price.to_be_bytes()), + U64::from(price_feed_message.ema_conf), ); price_feeds.insert(price_feed_message.feed_id, price_info_return); From 31a8bbb2779fe42a52d334a10b10d975e050c0ad Mon Sep 17 00:00:00 2001 From: Ayush Suresh Date: Mon, 7 Jul 2025 18:33:56 -0500 Subject: [PATCH 08/16] formatted --- .../contracts/pyth-receiver/src/error.rs | 6 +- .../pyth-receiver/src/integration_tests.rs | 352 +++++++++++++----- .../stylus/contracts/pyth-receiver/src/lib.rs | 136 ++++--- .../contracts/pyth-receiver/src/structs.rs | 20 +- .../contracts/pyth-receiver/src/test_data.rs | 278 +++++++++++--- 5 files changed, 593 insertions(+), 199 deletions(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/error.rs b/target_chains/stylus/contracts/pyth-receiver/src/error.rs index a949250b15..b20cb5284d 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/error.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/error.rs @@ -16,7 +16,7 @@ pub enum PythReceiverError { InvalidAccumulatorMessageType, InsufficientFee, InvalidEmitterAddress, - TooManyUpdates + TooManyUpdates, } impl core::fmt::Debug for PythReceiverError { @@ -37,12 +37,12 @@ impl From for Vec { PythReceiverError::InvalidAccumulatorMessage => 7, PythReceiverError::InvalidMerkleRoot => 8, PythReceiverError::InvalidMerklePath => 9, - PythReceiverError::InvalidUnknownSource => 10, + PythReceiverError::InvalidUnknownSource => 10, PythReceiverError::NewPriceUnavailable => 11, PythReceiverError::InvalidAccumulatorMessageType => 12, PythReceiverError::InsufficientFee => 13, PythReceiverError::InvalidEmitterAddress => 14, - PythReceiverError::TooManyUpdates => 15 + PythReceiverError::TooManyUpdates => 15, }] } } diff --git a/target_chains/stylus/contracts/pyth-receiver/src/integration_tests.rs b/target_chains/stylus/contracts/pyth-receiver/src/integration_tests.rs index cdb9ce5cf4..a2c12b1cc1 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/integration_tests.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/integration_tests.rs @@ -1,14 +1,15 @@ #[cfg(test)] mod test { - use crate::PythReceiver; - use crate::test_data; use crate::error::PythReceiverError; - use alloy_primitives::{address, U256, Address, U64, I64, I32}; + use crate::test_data; + use crate::PythReceiver; + use alloy_primitives::{address, Address, I32, I64, U256, U64}; use motsu::prelude::*; use wormhole_contract::WormholeContract; const TEST_PRICE_ID: [u8; 32] = [ - 0xe6, 0x2d, 0xf6, 0xc8, 0xb4, 0xa8, 0x5f, 0xe1, 0xa6, 0x7d, 0xb4, 0x4d, 0xc1, 0x2d, 0xe5, 0xdb, - 0x33, 0x0f, 0x7a, 0xc6, 0x6b, 0x72, 0xdc, 0x65, 0x8a, 0xfe, 0xdf, 0x0f, 0x4a, 0x41, 0x5b, 0x43 + 0xe6, 0x2d, 0xf6, 0xc8, 0xb4, 0xa8, 0x5f, 0xe1, 0xa6, 0x7d, 0xb4, 0x4d, 0xc1, 0x2d, 0xe5, + 0xdb, 0x33, 0x0f, 0x7a, 0xc6, 0x6b, 0x72, 0xdc, 0x65, 0x8a, 0xfe, 0xdf, 0x0f, 0x4a, 0x41, + 0x5b, 0x43, ]; const TEST_PUBLISH_TIME: u64 = 1751563000; const TEST_PRICE: i64 = 10967241867779; @@ -19,8 +20,9 @@ mod test { const PYTHNET_CHAIN_ID: u16 = 26; const PYTHNET_EMITTER_ADDRESS: [u8; 32] = [ - 0xe1, 0x01, 0xfa, 0xed, 0xac, 0x58, 0x51, 0xe3, 0x2b, 0x9b, 0x23, 0xb5, 0xf9, 0x41, 0x1a, 0x8c, - 0x2b, 0xac, 0x4a, 0xae, 0x3e, 0xd4, 0xdd, 0x7b, 0x81, 0x1d, 0xd1, 0xa7, 0x2e, 0xa4, 0xaa, 0x71 + 0xe1, 0x01, 0xfa, 0xed, 0xac, 0x58, 0x51, 0xe3, 0x2b, 0x9b, 0x23, 0xb5, 0xf9, 0x41, 0x1a, + 0x8c, 0x2b, 0xac, 0x4a, 0xae, 0x3e, 0xd4, 0xdd, 0x7b, 0x81, 0x1d, 0xd1, 0xa7, 0x2e, 0xa4, + 0xaa, 0x71, ]; const CHAIN_ID: u16 = 60051; @@ -53,10 +55,24 @@ mod test { } #[motsu::test] - fn e2e_valid_test(pyth_contract: Contract, wormhole_contract: Contract, alice: Address) { + fn e2e_valid_test( + pyth_contract: Contract, + wormhole_contract: Contract, + alice: Address, + ) { let guardians = current_guardians(); - let governance_contract = Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); - wormhole_contract.sender(alice).initialize(guardians, 4, CHAIN_ID, GOVERNANCE_CHAIN_ID, governance_contract).unwrap(); + let governance_contract = + Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); + wormhole_contract + .sender(alice) + .initialize( + guardians, + 4, + CHAIN_ID, + GOVERNANCE_CHAIN_ID, + governance_contract, + ) + .unwrap(); // let result = wormhole_contract.sender(alice).store_gs(4, current_guardians(), 0); let single_update_fee = U256::from(100u64); @@ -81,33 +97,50 @@ mod test { governance_initial_sequence, data, ); - + alice.fund(U256::from(200)); - + let update_data = test_data::good_update1(); - let result = pyth_contract.sender_and_value(alice, U256::from(100)).update_price_feeds(update_data); + let result = pyth_contract + .sender_and_value(alice, U256::from(100)) + .update_price_feeds(update_data); assert!(result.is_ok()); let price_result = pyth_contract.sender(alice).get_price_unsafe(TEST_PRICE_ID); assert!(price_result.is_ok()); - assert_eq!(price_result.unwrap(), ( - U64::from(TEST_PUBLISH_TIME), - I32::from_le_bytes(TEST_EXPO.to_le_bytes()), - I64::from_le_bytes(TEST_PRICE.to_le_bytes()), - U64::from(TEST_CONF), - I64::from_le_bytes(TEST_EMA_PRICE.to_le_bytes()), - U64::from(TEST_EMA_CONF) - )); - - + assert_eq!( + price_result.unwrap(), + ( + U64::from(TEST_PUBLISH_TIME), + I32::from_le_bytes(TEST_EXPO.to_le_bytes()), + I64::from_le_bytes(TEST_PRICE.to_le_bytes()), + U64::from(TEST_CONF), + I64::from_le_bytes(TEST_EMA_PRICE.to_le_bytes()), + U64::from(TEST_EMA_CONF) + ) + ); } #[motsu::test] - fn test_update_price_feed_insufficient_fee(pyth_contract: Contract, wormhole_contract: Contract, alice: Address) { + fn test_update_price_feed_insufficient_fee( + pyth_contract: Contract, + wormhole_contract: Contract, + alice: Address, + ) { let guardians = current_guardians(); - let governance_contract = Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); - wormhole_contract.sender(alice).initialize(guardians, 4, CHAIN_ID, GOVERNANCE_CHAIN_ID, governance_contract).unwrap(); + let governance_contract = + Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); + wormhole_contract + .sender(alice) + .initialize( + guardians, + 4, + CHAIN_ID, + GOVERNANCE_CHAIN_ID, + governance_contract, + ) + .unwrap(); // let result = wormhole_contract.sender(alice).store_gs(4, current_guardians(), 0); let single_update_fee = U256::from(100u64); @@ -132,22 +165,37 @@ mod test { governance_initial_sequence, data, ); - + alice.fund(U256::from(50)); - + let update_data = test_data::good_update1(); - let result = pyth_contract.sender_and_value(alice, U256::from(50)).update_price_feeds(update_data); + let result = pyth_contract + .sender_and_value(alice, U256::from(50)) + .update_price_feeds(update_data); assert!(result.is_err()); assert_eq!(result.unwrap_err(), PythReceiverError::InsufficientFee); - } #[motsu::test] - fn test_get_price_after_multiple_updates(pyth_contract: Contract, wormhole_contract: Contract, alice: Address) { + fn test_get_price_after_multiple_updates( + pyth_contract: Contract, + wormhole_contract: Contract, + alice: Address, + ) { let guardians = current_guardians(); - let governance_contract = Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); - wormhole_contract.sender(alice).initialize(guardians, 4, CHAIN_ID, GOVERNANCE_CHAIN_ID, governance_contract).unwrap(); + let governance_contract = + Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); + wormhole_contract + .sender(alice) + .initialize( + guardians, + 4, + CHAIN_ID, + GOVERNANCE_CHAIN_ID, + governance_contract, + ) + .unwrap(); let single_update_fee = U256::from(100u64); let valid_time_period = U256::from(3600u64); @@ -175,30 +223,51 @@ mod test { alice.fund(U256::from(200)); let update_data1 = test_data::good_update1(); - let result1 = pyth_contract.sender_and_value(alice, U256::from(100)).update_price_feeds(update_data1); + let result1 = pyth_contract + .sender_and_value(alice, U256::from(100)) + .update_price_feeds(update_data1); assert!(result1.is_ok()); let update_data2 = test_data::good_update2(); - let result2 = pyth_contract.sender_and_value(alice, U256::from(100)).update_price_feeds(update_data2); + let result2 = pyth_contract + .sender_and_value(alice, U256::from(100)) + .update_price_feeds(update_data2); assert!(result2.is_ok()); let price_result = pyth_contract.sender(alice).get_price_unsafe(TEST_PRICE_ID); assert!(price_result.is_ok()); - assert_eq!(price_result.unwrap(), ( - U64::from(1751573860u64), - I32::from_le_bytes((-8i32).to_le_bytes()), - I64::from_le_bytes(10985663592646i64.to_le_bytes()), - U64::from(4569386330u64), - I64::from_le_bytes(10977795800000i64.to_le_bytes()), - U64::from(3919318300u64) - )); + assert_eq!( + price_result.unwrap(), + ( + U64::from(1751573860u64), + I32::from_le_bytes((-8i32).to_le_bytes()), + I64::from_le_bytes(10985663592646i64.to_le_bytes()), + U64::from(4569386330u64), + I64::from_le_bytes(10977795800000i64.to_le_bytes()), + U64::from(3919318300u64) + ) + ); } #[motsu::test] - fn test_get_price_unavailable_no_update(pyth_contract: Contract, wormhole_contract: Contract, alice: Address) { + fn test_get_price_unavailable_no_update( + pyth_contract: Contract, + wormhole_contract: Contract, + alice: Address, + ) { let guardians = current_guardians(); - let governance_contract = Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); - wormhole_contract.sender(alice).initialize(guardians, 4, CHAIN_ID, GOVERNANCE_CHAIN_ID, governance_contract).unwrap(); + let governance_contract = + Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); + wormhole_contract + .sender(alice) + .initialize( + guardians, + 4, + CHAIN_ID, + GOVERNANCE_CHAIN_ID, + governance_contract, + ) + .unwrap(); let single_update_fee = U256::from(100u64); let valid_time_period = U256::from(3600u64); @@ -225,14 +294,31 @@ mod test { let price_result = pyth_contract.sender(alice).get_price_unsafe(TEST_PRICE_ID); assert!(price_result.is_err()); - assert_eq!(price_result.unwrap_err(), PythReceiverError::PriceUnavailable); + assert_eq!( + price_result.unwrap_err(), + PythReceiverError::PriceUnavailable + ); } #[motsu::test] - fn test_get_price_no_older_than_unavailable_random_id(pyth_contract: Contract, wormhole_contract: Contract, alice: Address) { + fn test_get_price_no_older_than_unavailable_random_id( + pyth_contract: Contract, + wormhole_contract: Contract, + alice: Address, + ) { let guardians = current_guardians(); - let governance_contract = Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); - wormhole_contract.sender(alice).initialize(guardians, 4, CHAIN_ID, GOVERNANCE_CHAIN_ID, governance_contract).unwrap(); + let governance_contract = + Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); + wormhole_contract + .sender(alice) + .initialize( + guardians, + 4, + CHAIN_ID, + GOVERNANCE_CHAIN_ID, + governance_contract, + ) + .unwrap(); let single_update_fee = U256::from(100u64); let valid_time_period = U256::from(3600u64); @@ -258,20 +344,40 @@ mod test { ); let random_id: [u8; 32] = [ - 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, - 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0 + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, + 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, + 0x9a, 0xbc, 0xde, 0xf0, ]; - let price_result = pyth_contract.sender(alice).get_price_no_older_than(random_id, 3600); + let price_result = pyth_contract + .sender(alice) + .get_price_no_older_than(random_id, 3600); assert!(price_result.is_err()); - assert_eq!(price_result.unwrap_err(), PythReceiverError::PriceUnavailable); + assert_eq!( + price_result.unwrap_err(), + PythReceiverError::PriceUnavailable + ); } #[motsu::test] - fn test_get_price_no_older_than_valid_max_age(pyth_contract: Contract, wormhole_contract: Contract, alice: Address) { + fn test_get_price_no_older_than_valid_max_age( + pyth_contract: Contract, + wormhole_contract: Contract, + alice: Address, + ) { let guardians = current_guardians(); - let governance_contract = Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); - wormhole_contract.sender(alice).initialize(guardians, 4, CHAIN_ID, GOVERNANCE_CHAIN_ID, governance_contract).unwrap(); + let governance_contract = + Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); + wormhole_contract + .sender(alice) + .initialize( + guardians, + 4, + CHAIN_ID, + GOVERNANCE_CHAIN_ID, + governance_contract, + ) + .unwrap(); let single_update_fee = U256::from(100u64); let valid_time_period = U256::from(3600u64); @@ -299,26 +405,47 @@ mod test { alice.fund(U256::from(200)); let update_data = test_data::good_update2(); - let result = pyth_contract.sender_and_value(alice, U256::from(100)).update_price_feeds(update_data); + let result = pyth_contract + .sender_and_value(alice, U256::from(100)) + .update_price_feeds(update_data); assert!(result.is_ok()); - let price_result = pyth_contract.sender(alice).get_price_no_older_than(TEST_PRICE_ID, u64::MAX); + let price_result = pyth_contract + .sender(alice) + .get_price_no_older_than(TEST_PRICE_ID, u64::MAX); assert!(price_result.is_ok()); - assert_eq!(price_result.unwrap(), ( - U64::from(1751573860u64), - I32::from_le_bytes((-8i32).to_le_bytes()), - I64::from_le_bytes(10985663592646i64.to_le_bytes()), - U64::from(4569386330u64), - I64::from_le_bytes(10977795800000i64.to_le_bytes()), - U64::from(3919318300u64) - )); + assert_eq!( + price_result.unwrap(), + ( + U64::from(1751573860u64), + I32::from_le_bytes((-8i32).to_le_bytes()), + I64::from_le_bytes(10985663592646i64.to_le_bytes()), + U64::from(4569386330u64), + I64::from_le_bytes(10977795800000i64.to_le_bytes()), + U64::from(3919318300u64) + ) + ); } #[motsu::test] - fn test_get_price_no_older_than_too_old(pyth_contract: Contract, wormhole_contract: Contract, alice: Address) { + fn test_get_price_no_older_than_too_old( + pyth_contract: Contract, + wormhole_contract: Contract, + alice: Address, + ) { let guardians = current_guardians(); - let governance_contract = Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); - wormhole_contract.sender(alice).initialize(guardians, 4, CHAIN_ID, GOVERNANCE_CHAIN_ID, governance_contract).unwrap(); + let governance_contract = + Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); + wormhole_contract + .sender(alice) + .initialize( + guardians, + 4, + CHAIN_ID, + GOVERNANCE_CHAIN_ID, + governance_contract, + ) + .unwrap(); let single_update_fee = U256::from(100u64); let valid_time_period = U256::from(3600u64); @@ -345,19 +472,40 @@ mod test { alice.fund(U256::from(200)); let update_data = test_data::good_update2(); - let result = pyth_contract.sender_and_value(alice, U256::from(100)).update_price_feeds(update_data); + let result = pyth_contract + .sender_and_value(alice, U256::from(100)) + .update_price_feeds(update_data); assert!(result.is_ok()); - let price_result = pyth_contract.sender(alice).get_price_no_older_than(TEST_PRICE_ID, 1); + let price_result = pyth_contract + .sender(alice) + .get_price_no_older_than(TEST_PRICE_ID, 1); assert!(price_result.is_err()); - assert_eq!(price_result.unwrap_err(), PythReceiverError::NewPriceUnavailable); + assert_eq!( + price_result.unwrap_err(), + PythReceiverError::NewPriceUnavailable + ); } #[motsu::test] - fn test_multiple_updates_both_ids(pyth_contract: Contract, wormhole_contract: Contract, alice: Address) { + fn test_multiple_updates_both_ids( + pyth_contract: Contract, + wormhole_contract: Contract, + alice: Address, + ) { let guardians = current_guardians(); - let governance_contract = Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); - wormhole_contract.sender(alice).initialize(guardians, 4, CHAIN_ID, GOVERNANCE_CHAIN_ID, governance_contract).unwrap(); + let governance_contract = + Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); + wormhole_contract + .sender(alice) + .initialize( + guardians, + 4, + CHAIN_ID, + GOVERNANCE_CHAIN_ID, + governance_contract, + ) + .unwrap(); let single_update_fee = U256::from(100u64); let valid_time_period = U256::from(3600u64); @@ -385,38 +533,48 @@ mod test { alice.fund(U256::from(200)); let update_data = test_data::multiple_updates(); - let result = pyth_contract.sender_and_value(alice, U256::from(200)).update_price_feeds(update_data); + let result = pyth_contract + .sender_and_value(alice, U256::from(200)) + .update_price_feeds(update_data); assert!(result.is_ok()); let first_id: [u8; 32] = [ - 0xe6, 0x2d, 0xf6, 0xc8, 0xb4, 0xa8, 0x5f, 0xe1, 0xa6, 0x7d, 0xb4, 0x4d, 0xc1, 0x2d, 0xe5, 0xdb, - 0x33, 0x0f, 0x7a, 0xc6, 0x6b, 0x72, 0xdc, 0x65, 0x8a, 0xfe, 0xdf, 0x0f, 0x4a, 0x41, 0x5b, 0x43 + 0xe6, 0x2d, 0xf6, 0xc8, 0xb4, 0xa8, 0x5f, 0xe1, 0xa6, 0x7d, 0xb4, 0x4d, 0xc1, 0x2d, + 0xe5, 0xdb, 0x33, 0x0f, 0x7a, 0xc6, 0x6b, 0x72, 0xdc, 0x65, 0x8a, 0xfe, 0xdf, 0x0f, + 0x4a, 0x41, 0x5b, 0x43, ]; let second_id: [u8; 32] = [ - 0xff, 0x61, 0x49, 0x1a, 0x93, 0x11, 0x12, 0xdd, 0xf1, 0xbd, 0x81, 0x47, 0xcd, 0x1b, 0x64, 0x13, - 0x75, 0xf7, 0x9f, 0x58, 0x25, 0x12, 0x6d, 0x66, 0x54, 0x80, 0x87, 0x46, 0x34, 0xfd, 0x0a, 0xce + 0xff, 0x61, 0x49, 0x1a, 0x93, 0x11, 0x12, 0xdd, 0xf1, 0xbd, 0x81, 0x47, 0xcd, 0x1b, + 0x64, 0x13, 0x75, 0xf7, 0x9f, 0x58, 0x25, 0x12, 0x6d, 0x66, 0x54, 0x80, 0x87, 0x46, + 0x34, 0xfd, 0x0a, 0xce, ]; let first_price_result = pyth_contract.sender(alice).get_price_unsafe(first_id); assert!(first_price_result.is_ok()); - assert_eq!(first_price_result.unwrap(), ( - U64::from(1751573123u64), - I32::from_le_bytes((-8i32).to_le_bytes()), - I64::from_le_bytes(10990356724259i64.to_le_bytes()), - U64::from(3891724259u64), - I64::from_le_bytes(10974970400000i64.to_le_bytes()), - U64::from(3918344000u64) - )); + assert_eq!( + first_price_result.unwrap(), + ( + U64::from(1751573123u64), + I32::from_le_bytes((-8i32).to_le_bytes()), + I64::from_le_bytes(10990356724259i64.to_le_bytes()), + U64::from(3891724259u64), + I64::from_le_bytes(10974970400000i64.to_le_bytes()), + U64::from(3918344000u64) + ) + ); let second_price_result = pyth_contract.sender(alice).get_price_unsafe(second_id); assert!(second_price_result.is_ok()); - assert_eq!(second_price_result.unwrap(), ( - U64::from(1751573123u64), - I32::from_le_bytes((-8i32).to_le_bytes()), - I64::from_le_bytes(258906787480i64.to_le_bytes()), - U64::from(158498649u64), - I64::from_le_bytes(258597182000i64.to_le_bytes()), - U64::from(131285914u64) - )); + assert_eq!( + second_price_result.unwrap(), + ( + U64::from(1751573123u64), + I32::from_le_bytes((-8i32).to_le_bytes()), + I64::from_le_bytes(258906787480i64.to_le_bytes()), + U64::from(158498649u64), + I64::from_le_bytes(258597182000i64.to_le_bytes()), + U64::from(131285914u64) + ) + ); } } diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index cd09e8ca1c..1349312ecd 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -5,31 +5,38 @@ #[macro_use] extern crate alloc; -mod structs; mod error; #[cfg(test)] mod integration_tests; +mod structs; #[cfg(test)] mod test_data; -use alloc::{vec::Vec, collections::BTreeMap}; -use stylus_sdk::{alloy_primitives::{U16, U32, U256, U64, I32, I64, FixedBytes, Address}, - prelude::*, - storage::{StorageAddress, StorageVec, StorageMap, StorageUint, StorageBool, StorageU256, StorageU16, StorageFixedBytes}, - call::Call}; +use alloc::{collections::BTreeMap, vec::Vec}; +use stylus_sdk::{ + alloy_primitives::{Address, FixedBytes, I32, I64, U16, U256, U32, U64}, + call::Call, + prelude::*, + storage::{ + StorageAddress, StorageBool, StorageFixedBytes, StorageMap, StorageU16, StorageU256, + StorageUint, StorageVec, + }, +}; -use structs::{PriceInfoReturn, PriceInfoStorage, DataSourceStorage, DataSource}; -use error::{PythReceiverError}; -use pythnet_sdk::{wire::{from_slice, v1::{ - AccumulatorUpdateData, Proof, +use error::PythReceiverError; +use pythnet_sdk::{ + accumulators::merkle::{MerklePath, MerkleRoot}, + hashers::keccak256_160::Keccak160, + messages::Message, + wire::{ + from_slice, + v1::{ + AccumulatorUpdateData, Proof, WormholeMessage, WormholePayload, PYTHNET_ACCUMULATOR_UPDATE_MAGIC, - WormholeMessage, WormholePayload, }, }, - accumulators::{merkle::{MerklePath, MerkleRoot}}, - hashers::keccak256_160::Keccak160, - messages::Message }; +use structs::{DataSource, DataSourceStorage, PriceInfoReturn, PriceInfoStorage}; use wormhole_vaas::{Readable, Vaa, Writeable}; const ACCUMULATOR_WORMHOLE_MAGIC: u32 = 0x41555756; @@ -61,19 +68,31 @@ pub struct PythReceiver { #[public] impl PythReceiver { - pub fn initialize(&mut self, wormhole: Address, single_update_fee_in_wei: U256, valid_time_period_seconds: U256, - data_source_emitter_chain_ids: Vec, data_source_emitter_addresses: Vec<[u8; 32]>, - governance_emitter_chain_id: u16, governance_emitter_address: [u8; 32], - governance_initial_sequence: u64, _data: Vec) { + pub fn initialize( + &mut self, + wormhole: Address, + single_update_fee_in_wei: U256, + valid_time_period_seconds: U256, + data_source_emitter_chain_ids: Vec, + data_source_emitter_addresses: Vec<[u8; 32]>, + governance_emitter_chain_id: u16, + governance_emitter_address: [u8; 32], + governance_initial_sequence: u64, + _data: Vec, + ) { self.wormhole.set(wormhole); self.single_update_fee_in_wei.set(single_update_fee_in_wei); - self.valid_time_period_seconds.set(valid_time_period_seconds); + self.valid_time_period_seconds + .set(valid_time_period_seconds); - self.governance_data_source_chain_id.set(U16::from(governance_emitter_chain_id)); - self.governance_data_source_emitter_address.set(FixedBytes::<32>::from(governance_emitter_address)); + self.governance_data_source_chain_id + .set(U16::from(governance_emitter_chain_id)); + self.governance_data_source_emitter_address + .set(FixedBytes::<32>::from(governance_emitter_address)); // Initialize other fields - self.last_executed_governance_sequence.set(U64::from(governance_initial_sequence)); + self.last_executed_governance_sequence + .set(U64::from(governance_initial_sequence)); self.governance_data_source_index.set(U32::ZERO); for (i, chain_id) in data_source_emitter_chain_ids.iter().enumerate() { @@ -89,10 +108,7 @@ impl PythReceiver { emitter_address: emitter_address, }; - - self.is_valid_data_source - .setter(data_source_key) - .set(true); + self.is_valid_data_source.setter(data_source_key).set(true); } } @@ -115,7 +131,11 @@ impl PythReceiver { )) } - pub fn get_price_no_older_than(&self, id: [u8; 32], age: u64) -> Result { + pub fn get_price_no_older_than( + &self, + id: [u8; 32], + age: u64, + ) -> Result { let price_info = self.get_price_unsafe(id)?; if !self.is_no_older_than(price_info.0, age) { return Err(PythReceiverError::NewPriceUnavailable); @@ -141,7 +161,11 @@ impl PythReceiver { )) } - pub fn get_ema_price_no_older_than(&self, id: [u8; 32], age: u64) -> Result { + pub fn get_ema_price_no_older_than( + &self, + id: [u8; 32], + age: u64, + ) -> Result { let price_info = self.get_ema_price_unsafe(id)?; if !self.is_no_older_than(price_info.0, age) { return Err(PythReceiverError::NewPriceUnavailable); @@ -164,7 +188,14 @@ impl PythReceiver { // dummy implementation } - fn update_price_feeds_internal(&mut self, update_data: Vec, _price_ids: Vec<[u8; 32]>, min_publish_time: u64, max_publish_time: u64, _unique: bool) -> Result<(), PythReceiverError> { + fn update_price_feeds_internal( + &mut self, + update_data: Vec, + _price_ids: Vec<[u8; 32]>, + min_publish_time: u64, + max_publish_time: u64, + _unique: bool, + ) -> Result<(), PythReceiverError> { let price_pairs = self.parse_price_feed_updates_internal( update_data, min_publish_time, @@ -179,7 +210,8 @@ impl PythReceiver { let mut recent_price_info = self.latest_price_info.setter(price_id_fb); if recent_price_info.publish_time.get() < price_return.0 - || recent_price_info.price.get() == I64::ZERO { + || recent_price_info.price.get() == I64::ZERO + { recent_price_info.publish_time.set(price_return.0); recent_price_info.expo.set(price_return.1); recent_price_info.price.set(price_return.2); @@ -207,7 +239,15 @@ impl PythReceiver { min_publish_time: u64, max_publish_time: u64, ) -> Result, PythReceiverError> { - let price_feeds = self.parse_price_feed_updates_with_config(update_data, price_ids, min_publish_time, max_publish_time, false, false, false); + let price_feeds = self.parse_price_feed_updates_with_config( + update_data, + price_ids, + min_publish_time, + max_publish_time, + false, + false, + false, + ); price_feeds } @@ -232,14 +272,20 @@ impl PythReceiver { let price_map: BTreeMap<[u8; 32], PriceInfoReturn> = price_pairs.into_iter().collect(); - // Create a vector with the same length as price_ids let mut result: Vec = Vec::with_capacity(price_ids.len()); for price_id in price_ids { if let Some(price_info) = price_map.get(&price_id) { result.push(*price_info); } else { - result.push((U64::from(0), I32::from_be_bytes([0, 0, 0, 0]), I64::from_be_bytes([0, 0, 0, 0, 0, 0, 0, 0]), U64::from(0), I64::from_be_bytes([0, 0, 0, 0, 0, 0, 0, 0]), U64::from(0))); + result.push(( + U64::from(0), + I32::from_be_bytes([0, 0, 0, 0]), + I64::from_be_bytes([0, 0, 0, 0, 0, 0, 0, 0]), + U64::from(0), + I64::from_be_bytes([0, 0, 0, 0, 0, 0, 0, 0]), + U64::from(0), + )); } } @@ -268,7 +314,8 @@ impl PythReceiver { return Err(PythReceiverError::InvalidAccumulatorMessage); } - let update_data = AccumulatorUpdateData::try_from_slice(&update_data_array).map_err(|_| PythReceiverError::InvalidAccumulatorMessage)?; + let update_data = AccumulatorUpdateData::try_from_slice(&update_data_array) + .map_err(|_| PythReceiverError::InvalidAccumulatorMessage)?; let mut price_feeds: BTreeMap<[u8; 32], PriceInfoReturn> = BTreeMap::new(); @@ -276,9 +323,12 @@ impl PythReceiver { Proof::WormholeMerkle { vaa, updates } => { let wormhole: IWormholeContract = IWormholeContract::new(self.wormhole.get()); let config = Call::new(); - wormhole.parse_and_verify_vm(config, Vec::from(vaa.clone())).map_err(|_| PythReceiverError::InvalidWormholeMessage)?; + wormhole + .parse_and_verify_vm(config, Vec::from(vaa.clone())) + .map_err(|_| PythReceiverError::InvalidWormholeMessage)?; - let vaa = Vaa::read(&mut Vec::from(vaa.clone()).as_slice()).map_err(|_| PythReceiverError::VaaVerificationFailed)?; + let vaa = Vaa::read(&mut Vec::from(vaa.clone()).as_slice()) + .map_err(|_| PythReceiverError::VaaVerificationFailed)?; let cur_emitter_address: &[u8; 32] = vaa .body @@ -298,7 +348,8 @@ impl PythReceiver { let root_digest: MerkleRoot = parse_wormhole_proof(vaa)?; - let num_updates = u8::try_from(updates.len()).map_err(|_| PythReceiverError::TooManyUpdates)?; + let num_updates = + u8::try_from(updates.len()).map_err(|_| PythReceiverError::TooManyUpdates)?; let total_fee = self.get_total_fee(num_updates); @@ -309,7 +360,6 @@ impl PythReceiver { } for update in updates { - let message_vec = Vec::from(update.message); let proof: MerklePath = update.proof; @@ -332,10 +382,10 @@ impl PythReceiver { ); price_feeds.insert(price_feed_message.feed_id, price_info_return); - }, + } _ => { return Err(PythReceiverError::InvalidAccumulatorMessageType); - }, + } } } } @@ -364,7 +414,9 @@ impl PythReceiver { #[inline] fn is_no_older_than(&self, publish_time: U64, max_age: u64) -> bool { - self.get_current_timestamp().saturating_sub(publish_time.to::()) <= max_age + self.get_current_timestamp() + .saturating_sub(publish_time.to::()) + <= max_age } // Stylus doesn't provide a way to mock up the testing timestamp @@ -383,7 +435,7 @@ impl PythReceiver { fn parse_wormhole_proof(vaa: Vaa) -> Result, PythReceiverError> { let message = WormholeMessage::try_from_bytes(vaa.body.payload.to_vec()) - .map_err(|_| PythReceiverError::PriceUnavailable)?; + .map_err(|_| PythReceiverError::PriceUnavailable)?; let root: MerkleRoot = MerkleRoot::new(match message.payload { WormholePayload::Merkle(merkle_root) => merkle_root.root, }); diff --git a/target_chains/stylus/contracts/pyth-receiver/src/structs.rs b/target_chains/stylus/contracts/pyth-receiver/src/structs.rs index c2d915b282..722eb7312a 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/structs.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/structs.rs @@ -1,13 +1,11 @@ -use alloc::{vec::Vec, boxed::Box, format}; +use alloc::{boxed::Box, format, vec::Vec}; use pythnet_sdk::wire::to_vec; use serde::Serialize; +use stylus_sdk::alloy_primitives::{keccak256, FixedBytes, B256, I32, I64, U16, U256, U64}; use stylus_sdk::{ prelude::*, - storage::{ - StorageU64, StorageI32, StorageI64, StorageU16, StorageFixedBytes, StorageKey - }, + storage::{StorageFixedBytes, StorageI32, StorageI64, StorageKey, StorageU16, StorageU64}, }; -use stylus_sdk::alloy_primitives::{U16, FixedBytes,U64, I32, I64, B256, U256, keccak256}; #[derive(Serialize)] struct SerializableDataSource { @@ -16,7 +14,10 @@ struct SerializableDataSource { emitter_address: [u8; 32], } -fn serialize_data_source_to_bytes(chain_id: u16, emitter_address: &[u8; 32]) -> Result<[u8; 34], Box> { +fn serialize_data_source_to_bytes( + chain_id: u16, + emitter_address: &[u8; 32], +) -> Result<[u8; 34], Box> { let data_source = SerializableDataSource { chain_id, emitter_address: *emitter_address, @@ -127,7 +128,7 @@ pub type PriceInfoReturn = (U64, I32, I64, U64, I64, U64); #[cfg(test)] mod tests { use super::*; - use stylus_sdk::alloy_primitives::{U16, FixedBytes}; + use stylus_sdk::alloy_primitives::{FixedBytes, U16}; #[test] fn test_data_source_serialization_compatibility() { @@ -146,6 +147,9 @@ mod tests { let actual_bytes = serialize_data_source_to_bytes(chain_id, &emitter_address) .expect("Serialization should succeed"); - assert_eq!(actual_bytes, expected_bytes, "Serialization should produce identical bytes"); + assert_eq!( + actual_bytes, expected_bytes, + "Serialization should produce identical bytes" + ); } } diff --git a/target_chains/stylus/contracts/pyth-receiver/src/test_data.rs b/target_chains/stylus/contracts/pyth-receiver/src/test_data.rs index 6bb82aa097..0faa5fa93f 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/test_data.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/test_data.rs @@ -3,14 +3,14 @@ use hex::FromHex; fn from_cairo_byte_array_data(data: &[U256], num_last_bytes: usize) -> Vec { let mut result = Vec::new(); - + if data.is_empty() { return result; } - + for (i, &item) in data.iter().enumerate() { let buf = item.to_be_bytes::<32>(); - + if i == data.len() - 1 { if num_last_bytes > 0 { let start_idx = 32 - num_last_bytes; @@ -20,7 +20,7 @@ fn from_cairo_byte_array_data(data: &[U256], num_last_bytes: usize) -> Vec { result.extend_from_slice(&buf[1..]); } } - + result } @@ -44,36 +44,156 @@ pub fn multiple_updates() -> Vec { pub fn good_vm1() -> Vec { let bytes = [ - U256::from_str_radix("1766847066033410293701000231337210964058791470455465385734308943533652138", 10).unwrap(), - U256::from_str_radix("250126301534699068413432844632573953364878937343368310395142095034982913232", 10).unwrap(), - U256::from_str_radix("374780571002258088211231890250917843593951765403462661483498298003400611238", 10).unwrap(), - U256::from_str_radix("23190137343211334092589308306056431640588154666326612124726174150537328574", 10).unwrap(), - U256::from_str_radix("238750269065878649216923353030193912502813798896051725498208457553032584635", 10).unwrap(), - U256::from_str_radix("29844190303057534696518006438077948796328243878877072296680853158289181326", 10).unwrap(), - U256::from_str_radix("106329507856770018708432343978518079724691760719405501795955774399597471533", 10).unwrap(), - U256::from_str_radix("50779865592261858016477142415230454208001695486195806892438697217059319645", 10).unwrap(), - U256::from_str_radix("448669871976126446102256476358498380455807705600424321390063431836375575318", 10).unwrap(), - U256::from_str_radix("115682669871397824853706713833773246708114483862317474710603223566297521279", 10).unwrap(), - U256::from_str_radix("301634766618012930739391408723909107532790832406455099966028276947414082504", 10).unwrap(), - U256::from_str_radix("104473166230846104217366042152018649207811514257244625711402436055500423094", 10).unwrap(), - U256::from_str_radix("64445621634231668761998815864645440965239569561546522651415024970517905416", 10).unwrap(), - U256::from_str_radix("192317190225976528694195501079591384434869624408066864018183189813956862386", 10).unwrap(), - U256::from_str_radix("289982656017597431343118552054719821766658675456661448685110903889153449006", 10).unwrap(), - U256::from_str_radix("218840601196095059731241556733624112758673153548932709011933806481899680620", 10).unwrap(), - U256::from_str_radix("430933799927481265070475198137531816946660368757134588278434352703899277070", 10).unwrap(), - U256::from_str_radix("69322998883710289192076494057541346430050879299268159627180404869988632073", 10).unwrap(), - U256::from_str_radix("23862615839737051269352321086490452186237833007444069999578906611768140646", 10).unwrap(), - U256::from_str_radix("444634264607471510688862284107804392707600799506487897206707262445172121289", 10).unwrap(), - U256::from_str_radix("438038196736233160320436150616293672539386464061037100698335568417587662951", 10).unwrap(), - U256::from_str_radix("4682255185797880874381673193118803274635247527626050223938224759013169366", 10).unwrap(), - U256::from_str_radix("337620725992972686809095065321563509600769533202700218393281926304544120094", 10).unwrap(), - U256::from_str_radix("106657917096532484607371891267699639824731774168349872862335217581425289654", 10).unwrap(), - U256::from_str_radix("71240348385993236445536577509595968468284689483611375124653855125285401592", 10).unwrap(), - U256::from_str_radix("347603391821038175842934311068097986460257977131947418186118379296987051086", 10).unwrap(), - U256::from_str_radix("414263571545410645948841360836383289766662078574048514890988877286444618669", 10).unwrap(), - U256::from_str_radix("250301638008739107522011802538487063969565433276260914336890309092111026583", 10).unwrap(), - U256::from_str_radix("43192785595291340058788190601908070333310658291317702311902081", 10).unwrap(), - U256::from_str_radix("52685537088250779930155363779405986390839624071318818148325576008719597568", 10).unwrap(), + U256::from_str_radix( + "1766847066033410293701000231337210964058791470455465385734308943533652138", + 10, + ) + .unwrap(), + U256::from_str_radix( + "250126301534699068413432844632573953364878937343368310395142095034982913232", + 10, + ) + .unwrap(), + U256::from_str_radix( + "374780571002258088211231890250917843593951765403462661483498298003400611238", + 10, + ) + .unwrap(), + U256::from_str_radix( + "23190137343211334092589308306056431640588154666326612124726174150537328574", + 10, + ) + .unwrap(), + U256::from_str_radix( + "238750269065878649216923353030193912502813798896051725498208457553032584635", + 10, + ) + .unwrap(), + U256::from_str_radix( + "29844190303057534696518006438077948796328243878877072296680853158289181326", + 10, + ) + .unwrap(), + U256::from_str_radix( + "106329507856770018708432343978518079724691760719405501795955774399597471533", + 10, + ) + .unwrap(), + U256::from_str_radix( + "50779865592261858016477142415230454208001695486195806892438697217059319645", + 10, + ) + .unwrap(), + U256::from_str_radix( + "448669871976126446102256476358498380455807705600424321390063431836375575318", + 10, + ) + .unwrap(), + U256::from_str_radix( + "115682669871397824853706713833773246708114483862317474710603223566297521279", + 10, + ) + .unwrap(), + U256::from_str_radix( + "301634766618012930739391408723909107532790832406455099966028276947414082504", + 10, + ) + .unwrap(), + U256::from_str_radix( + "104473166230846104217366042152018649207811514257244625711402436055500423094", + 10, + ) + .unwrap(), + U256::from_str_radix( + "64445621634231668761998815864645440965239569561546522651415024970517905416", + 10, + ) + .unwrap(), + U256::from_str_radix( + "192317190225976528694195501079591384434869624408066864018183189813956862386", + 10, + ) + .unwrap(), + U256::from_str_radix( + "289982656017597431343118552054719821766658675456661448685110903889153449006", + 10, + ) + .unwrap(), + U256::from_str_radix( + "218840601196095059731241556733624112758673153548932709011933806481899680620", + 10, + ) + .unwrap(), + U256::from_str_radix( + "430933799927481265070475198137531816946660368757134588278434352703899277070", + 10, + ) + .unwrap(), + U256::from_str_radix( + "69322998883710289192076494057541346430050879299268159627180404869988632073", + 10, + ) + .unwrap(), + U256::from_str_radix( + "23862615839737051269352321086490452186237833007444069999578906611768140646", + 10, + ) + .unwrap(), + U256::from_str_radix( + "444634264607471510688862284107804392707600799506487897206707262445172121289", + 10, + ) + .unwrap(), + U256::from_str_radix( + "438038196736233160320436150616293672539386464061037100698335568417587662951", + 10, + ) + .unwrap(), + U256::from_str_radix( + "4682255185797880874381673193118803274635247527626050223938224759013169366", + 10, + ) + .unwrap(), + U256::from_str_radix( + "337620725992972686809095065321563509600769533202700218393281926304544120094", + 10, + ) + .unwrap(), + U256::from_str_radix( + "106657917096532484607371891267699639824731774168349872862335217581425289654", + 10, + ) + .unwrap(), + U256::from_str_radix( + "71240348385993236445536577509595968468284689483611375124653855125285401592", + 10, + ) + .unwrap(), + U256::from_str_radix( + "347603391821038175842934311068097986460257977131947418186118379296987051086", + 10, + ) + .unwrap(), + U256::from_str_radix( + "414263571545410645948841360836383289766662078574048514890988877286444618669", + 10, + ) + .unwrap(), + U256::from_str_radix( + "250301638008739107522011802538487063969565433276260914336890309092111026583", + 10, + ) + .unwrap(), + U256::from_str_radix( + "43192785595291340058788190601908070333310658291317702311902081", + 10, + ) + .unwrap(), + U256::from_str_radix( + "52685537088250779930155363779405986390839624071318818148325576008719597568", + 10, + ) + .unwrap(), U256::from_str_radix("14615204155786886573933667335033405822686404253588533", 10).unwrap(), ]; from_cairo_byte_array_data(&bytes, 22) @@ -81,21 +201,81 @@ pub fn good_vm1() -> Vec { pub fn test_price_update1() -> Vec { let bytes = [ - U256::from_str_radix("141887862745809943100421399774809552050876420277163116849842965275903806689", 10).unwrap(), - U256::from_str_radix("210740906737592158039211995620336526131859667363627655742687286503264782608", 10).unwrap(), - U256::from_str_radix("437230063624699337579360546580839669896712252828825008570863758867641146081", 10).unwrap(), - U256::from_str_radix("3498691308882995183871222184377409432186747119716981166996399082193594993", 10).unwrap(), - U256::from_str_radix("1390200166945919815453709407753165121175395927094647129599868236", 10).unwrap(), - U256::from_str_radix("222819573728193325268644030206737371345667885599602384508424089704440116301", 10).unwrap(), - U256::from_str_radix("341318259000017461738706238280879290398059773267212529438772847337449455616", 10).unwrap(), - U256::from_str_radix("1275126645346645395843037504005879519843596923369759718556759844520336145", 10).unwrap(), - U256::from_str_radix("363528783578153760894082184744116718493621815898909809604883433584616420886", 10).unwrap(), - U256::from_str_radix("301537311768214106147206781423041990995720118715322906821301413003463484347", 10).unwrap(), - U256::from_str_radix("83150006264761451992768264969047148434524798781124754530141755679159432208", 10).unwrap(), - U256::from_str_radix("96387772316726941183358990094337324283641753573556594738287498821253761827", 10).unwrap(), - U256::from_str_radix("395908154570808692326126405856049827157095768069251211022053821585519235652", 10).unwrap(), - U256::from_str_radix("87135893730137265929093180553063146337041045646221968026289709394440932141", 10).unwrap(), - U256::from_str_radix("245333243912241114598596888050489286502591033459250287888834", 10).unwrap(), + U256::from_str_radix( + "141887862745809943100421399774809552050876420277163116849842965275903806689", + 10, + ) + .unwrap(), + U256::from_str_radix( + "210740906737592158039211995620336526131859667363627655742687286503264782608", + 10, + ) + .unwrap(), + U256::from_str_radix( + "437230063624699337579360546580839669896712252828825008570863758867641146081", + 10, + ) + .unwrap(), + U256::from_str_radix( + "3498691308882995183871222184377409432186747119716981166996399082193594993", + 10, + ) + .unwrap(), + U256::from_str_radix( + "1390200166945919815453709407753165121175395927094647129599868236", + 10, + ) + .unwrap(), + U256::from_str_radix( + "222819573728193325268644030206737371345667885599602384508424089704440116301", + 10, + ) + .unwrap(), + U256::from_str_radix( + "341318259000017461738706238280879290398059773267212529438772847337449455616", + 10, + ) + .unwrap(), + U256::from_str_radix( + "1275126645346645395843037504005879519843596923369759718556759844520336145", + 10, + ) + .unwrap(), + U256::from_str_radix( + "363528783578153760894082184744116718493621815898909809604883433584616420886", + 10, + ) + .unwrap(), + U256::from_str_radix( + "301537311768214106147206781423041990995720118715322906821301413003463484347", + 10, + ) + .unwrap(), + U256::from_str_radix( + "83150006264761451992768264969047148434524798781124754530141755679159432208", + 10, + ) + .unwrap(), + U256::from_str_radix( + "96387772316726941183358990094337324283641753573556594738287498821253761827", + 10, + ) + .unwrap(), + U256::from_str_radix( + "395908154570808692326126405856049827157095768069251211022053821585519235652", + 10, + ) + .unwrap(), + U256::from_str_radix( + "87135893730137265929093180553063146337041045646221968026289709394440932141", + 10, + ) + .unwrap(), + U256::from_str_radix( + "245333243912241114598596888050489286502591033459250287888834", + 10, + ) + .unwrap(), ]; from_cairo_byte_array_data(&bytes, 25) } From 9e459271f1c16b3b16cc9976eda1afdefa81bb41 Mon Sep 17 00:00:00 2001 From: Ayush Suresh Date: Tue, 8 Jul 2025 15:32:07 -0500 Subject: [PATCH 09/16] getting publish time filtering working --- .../contracts/pyth-receiver/src/error.rs | 2 + .../pyth-receiver/src/integration_tests.rs | 10 ++++ .../stylus/contracts/pyth-receiver/src/lib.rs | 46 +++++++++++-------- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/error.rs b/target_chains/stylus/contracts/pyth-receiver/src/error.rs index b20cb5284d..5f0bb2ecf6 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/error.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/error.rs @@ -17,6 +17,7 @@ pub enum PythReceiverError { InsufficientFee, InvalidEmitterAddress, TooManyUpdates, + PriceFeedNotFoundWithinRange, } impl core::fmt::Debug for PythReceiverError { @@ -43,6 +44,7 @@ impl From for Vec { PythReceiverError::InsufficientFee => 13, PythReceiverError::InvalidEmitterAddress => 14, PythReceiverError::TooManyUpdates => 15, + PythReceiverError::PriceFeedNotFoundWithinRange => 16, }] } } diff --git a/target_chains/stylus/contracts/pyth-receiver/src/integration_tests.rs b/target_chains/stylus/contracts/pyth-receiver/src/integration_tests.rs index 9e6131146b..cc17c457c2 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/integration_tests.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/integration_tests.rs @@ -369,4 +369,14 @@ mod test { ) ); } + + #[motsu::test] + fn test_multiple_updates_same_id_updates_latest( + pyth_contract: Contract, + wormhole_contract: Contract, + alice: Address, + ) { + pyth_wormhole_init(&pyth_contract, &wormhole_contract, &alice); + alice.fund(U256::from(200)); + } } diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index 38f30f0544..d661892201 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -191,7 +191,7 @@ impl PythReceiver { min_publish_time: u64, max_publish_time: u64, _unique: bool, - ) -> Result<(), PythReceiverError> { + ) -> Result, PythReceiverError> { let price_pairs = self.parse_price_feed_updates_internal( update_data, min_publish_time, @@ -201,7 +201,7 @@ impl PythReceiver { true, // store_updates_if_fresh )?; - for (price_id, price_return) in price_pairs { + for (price_id, price_return) in price_pairs.clone() { let price_id_fb: FixedBytes<32> = FixedBytes::from(price_id); let mut recent_price_info = self.latest_price_info.setter(price_id_fb); @@ -217,7 +217,7 @@ impl PythReceiver { } } - Ok(()) + Ok(price_pairs) } fn get_update_fee(&self, update_data: Vec) -> Result { @@ -266,14 +266,29 @@ impl PythReceiver { check_update_data_is_minimal: bool, store_updates_if_fresh: bool, ) -> Result, PythReceiverError> { - let price_pairs = self.parse_price_feed_updates_internal( - update_data, - min_allowed_publish_time, - max_allowed_publish_time, - check_uniqueness, - check_update_data_is_minimal, - store_updates_if_fresh, - )?; + let price_pairs; + if store_updates_if_fresh { + price_pairs = self.update_price_feeds_internal( + update_data, + price_ids.clone(), + min_allowed_publish_time, + max_allowed_publish_time, + check_uniqueness, + )?; + } else { + price_pairs = self.parse_price_feed_updates_internal( + update_data, + min_allowed_publish_time, + max_allowed_publish_time, + check_uniqueness, + check_update_data_is_minimal, + store_updates_if_fresh, + )?; + } + + if check_update_data_is_minimal && price_ids.len() != price_pairs.len() { + return Err(PythReceiverError::InvalidUpdateData); + } let price_map: BTreeMap<[u8; 32], PriceInfoReturn> = price_pairs.into_iter().collect(); @@ -283,14 +298,7 @@ impl PythReceiver { if let Some(price_info) = price_map.get(&price_id) { result.push(*price_info); } else { - result.push(( - U64::from(0), - I32::from_be_bytes([0, 0, 0, 0]), - I64::from_be_bytes([0, 0, 0, 0, 0, 0, 0, 0]), - U64::from(0), - I64::from_be_bytes([0, 0, 0, 0, 0, 0, 0, 0]), - U64::from(0), - )); + return Err(PythReceiverError::PriceFeedNotFoundWithinRange); } } From 3e39a40f5e49078623589e830710b49a7d53d39a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:32:17 +0000 Subject: [PATCH 10/16] feat: add publish time validation in parse_price_feed_updates_internal - Add validation to check if publish times are within [min_allowed_publish_time, max_allowed_publish_time] range - Return PythReceiverError::PriceFeedNotFoundWithinRange (error code 16) when publish times are outside allowed range - Fix type casting from u64 to i64 for proper comparison with price_feed_message.publish_time Co-Authored-By: ayush.suresh@dourolabs.xyz --- target_chains/stylus/contracts/pyth-receiver/src/lib.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index d661892201..d1ca7bf38c 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -382,8 +382,15 @@ impl PythReceiver { match msg { Message::PriceFeedMessage(price_feed_message) => { + let publish_time = price_feed_message.publish_time; + + if (min_allowed_publish_time > 0 && publish_time < min_allowed_publish_time as i64) || + (max_allowed_publish_time > 0 && publish_time > max_allowed_publish_time as i64) { + return Err(PythReceiverError::PriceFeedNotFoundWithinRange); + } + let price_info_return = ( - U64::from(price_feed_message.publish_time), + U64::from(publish_time), I32::from_be_bytes(price_feed_message.exponent.to_be_bytes()), I64::from_be_bytes(price_feed_message.price.to_be_bytes()), U64::from(price_feed_message.conf), From 55658af0465162c06ec5c8beb3b4f400648b827a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 22:04:32 +0000 Subject: [PATCH 11/16] feat: implement check_uniqueness validation in parse_price_feed_updates_internal - Add uniqueness checking logic when check_uniqueness parameter is true - Require minAllowedPublishTime > prevPublishTime for uniqueness validation - Return PythReceiverError::PriceFeedNotFoundWithinRange when uniqueness check fails - Retrieve previous publish time from latest_price_info storage Co-Authored-By: ayush.suresh@dourolabs.xyz --- .../stylus/contracts/pyth-receiver/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index d1ca7bf38c..ac3f08114c 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -389,6 +389,16 @@ impl PythReceiver { return Err(PythReceiverError::PriceFeedNotFoundWithinRange); } + if check_uniqueness { + let price_id_fb = FixedBytes::<32>::from(price_feed_message.feed_id); + let prev_price_info = self.latest_price_info.get(price_id_fb); + let prev_publish_time = prev_price_info.publish_time.get().to::(); + + if prev_publish_time > 0 && min_allowed_publish_time <= prev_publish_time { + return Err(PythReceiverError::PriceFeedNotFoundWithinRange); + } + } + let price_info_return = ( U64::from(publish_time), I32::from_be_bytes(price_feed_message.exponent.to_be_bytes()), From 47d535abc05b5c2033faa24088d106007156726b Mon Sep 17 00:00:00 2001 From: Ayush Suresh Date: Tue, 8 Jul 2025 17:26:53 -0500 Subject: [PATCH 12/16] removed extraneous parameters --- .../stylus/contracts/pyth-receiver/src/lib.rs | 6 ------ .../stylus/contracts/wormhole/src/lib.rs | 16 ++++++++-------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index ac3f08114c..7e2dbb048b 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -197,8 +197,6 @@ impl PythReceiver { min_publish_time, max_publish_time, false, // check_uniqueness - false, // check_update_data_is_minimal - true, // store_updates_if_fresh )?; for (price_id, price_return) in price_pairs.clone() { @@ -281,8 +279,6 @@ impl PythReceiver { min_allowed_publish_time, max_allowed_publish_time, check_uniqueness, - check_update_data_is_minimal, - store_updates_if_fresh, )?; } @@ -311,8 +307,6 @@ impl PythReceiver { min_allowed_publish_time: u64, max_allowed_publish_time: u64, check_uniqueness: bool, - check_update_data_is_minimal: bool, - store_updates_if_fresh: bool, ) -> Result, PythReceiverError> { let update_data_array: &[u8] = &update_data; // Check the first 4 bytes of the update_data_array for the magic header diff --git a/target_chains/stylus/contracts/wormhole/src/lib.rs b/target_chains/stylus/contracts/wormhole/src/lib.rs index 5b0aea296d..240f988fb0 100644 --- a/target_chains/stylus/contracts/wormhole/src/lib.rs +++ b/target_chains/stylus/contracts/wormhole/src/lib.rs @@ -500,7 +500,7 @@ mod tests { use core::str::FromStr; use k256::ecdsa::SigningKey; use stylus_sdk::alloy_primitives::keccak256; - + #[cfg(test)] use base64::engine::general_purpose; #[cfg(test)] @@ -543,7 +543,7 @@ mod tests { 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, ] } - + #[cfg(test)] fn current_guardians() -> Vec
{ vec![ @@ -634,7 +634,7 @@ mod tests { contract.initialize(guardians, 1, CHAIN_ID, GOVERNANCE_CHAIN_ID, governance_contract).unwrap(); contract } - + #[cfg(test)] fn deploy_with_current_mainnet_guardians() -> WormholeContract { let mut contract = WormholeContract::default(); @@ -802,7 +802,7 @@ mod tests { #[motsu::test] fn test_verification_multiple_guardian_sets() { let mut contract = deploy_with_current_mainnet_guardians(); - + let store_result = contract.store_gs(4, current_guardians(), 0); if let Err(_) = store_result { panic!("Error deploying multiple guardian sets"); @@ -816,7 +816,7 @@ mod tests { #[motsu::test] fn test_verification_incorrect_guardian_set() { let mut contract = deploy_with_current_mainnet_guardians(); - + let store_result = contract.store_gs(4, mock_guardian_set13(), 0); if let Err(_) = store_result { panic!("Error deploying guardian set"); @@ -1147,7 +1147,7 @@ mod tests { let mut contract = WormholeContract::default(); let guardians = current_guardians(); let governance_contract = Address::from_slice(&GOVERNANCE_CONTRACT.to_be_bytes::<32>()[12..32]); - + let result = contract.initialize(guardians.clone(), 4, CHAIN_ID, GOVERNANCE_CHAIN_ID, governance_contract); assert!(result.is_ok(), "Contract initialization should succeed"); } @@ -1222,5 +1222,5 @@ mod tests { assert!(result2.is_ok()); } - -} \ No newline at end of file + +} From 9e144fff4eea43ea29ce1d167c51470aff61c719 Mon Sep 17 00:00:00 2001 From: Ayush Suresh Date: Wed, 9 Jul 2025 17:26:20 -0500 Subject: [PATCH 13/16] implemented parse_price_feed_updates_unique --- .../contracts/pyth-receiver/src/error.rs | 2 + .../stylus/contracts/pyth-receiver/src/lib.rs | 50 +++++++++++++++---- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/error.rs b/target_chains/stylus/contracts/pyth-receiver/src/error.rs index 5f0bb2ecf6..4b68178d80 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/error.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/error.rs @@ -18,6 +18,7 @@ pub enum PythReceiverError { InvalidEmitterAddress, TooManyUpdates, PriceFeedNotFoundWithinRange, + NoFreshUpdate, } impl core::fmt::Debug for PythReceiverError { @@ -45,6 +46,7 @@ impl From for Vec { PythReceiverError::InvalidEmitterAddress => 14, PythReceiverError::TooManyUpdates => 15, PythReceiverError::PriceFeedNotFoundWithinRange => 16, + PythReceiverError::NoFreshUpdate => 17, }] } } diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index 32a8f7e859..cb75a513aa 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -193,11 +193,30 @@ impl PythReceiver { pub fn update_price_feeds_if_necessary( &mut self, - _update_data: Vec>, - _price_ids: Vec<[u8; 32]>, - _publish_times: Vec, - ) { - // dummy implementation + update_data: Vec>, + price_ids: Vec<[u8; 32]>, + publish_times: Vec, + ) -> Result<(), PythReceiverError> { + if (price_ids.len() != publish_times.len()) + || (price_ids.is_empty() && publish_times.is_empty()) + { + return Err(PythReceiverError::InvalidUpdateData); + } + + for i in 0..price_ids.len() { + if (self.latest_price_info_publish_time(price_ids[i]) < publish_times[i]) { + self.update_price_feeds(update_data.clone())?; + return Ok(()); + } + } + + return Err(PythReceiverError::NoFreshUpdate); + } + + fn latest_price_info_publish_time(&self, price_id: [u8; 32]) -> u64 { + let price_id_fb: FixedBytes<32> = FixedBytes::from(price_id); + let recent_price_info = self.latest_price_info.get(price_id_fb); + recent_price_info.publish_time.get().to::() } fn update_price_feeds_internal( @@ -449,12 +468,21 @@ impl PythReceiver { pub fn parse_price_feed_updates_unique( &mut self, - _update_data: Vec>, - _price_ids: Vec<[u8; 32]>, - _min_publish_time: u64, - _max_publish_time: u64, - ) -> Vec { - Vec::new() + update_data: Vec>, + price_ids: Vec<[u8; 32]>, + min_publish_time: u64, + max_publish_time: u64, + ) -> Result, PythReceiverError> { + let price_feeds = self.parse_price_feed_updates_with_config( + update_data, + price_ids, + min_publish_time, + max_publish_time, + true, + false, + false, + ); + price_feeds } fn is_no_older_than(&self, publish_time: U64, max_age: u64) -> bool { From 6897096914b9edc37529c01e8228d19dad9a3c44 Mon Sep 17 00:00:00 2001 From: Ayush Suresh Date: Wed, 9 Jul 2025 17:52:18 -0500 Subject: [PATCH 14/16] pushing work for now --- .../stylus/contracts/pyth-receiver/src/lib.rs | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index cb75a513aa..7bc41e7e96 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -300,7 +300,7 @@ impl PythReceiver { pub fn parse_price_feed_updates_with_config( &mut self, - update_data: Vec, + update_data: Vec>, price_ids: Vec<[u8; 32]>, min_allowed_publish_time: u64, max_allowed_publish_time: u64, @@ -308,22 +308,24 @@ impl PythReceiver { check_update_data_is_minimal: bool, store_updates_if_fresh: bool, ) -> Result, PythReceiverError> { - let price_pairs; - if store_updates_if_fresh { - price_pairs = self.update_price_feeds_internal( - update_data, - price_ids.clone(), - min_allowed_publish_time, - max_allowed_publish_time, - check_uniqueness, - )?; - } else { - price_pairs = self.parse_price_feed_updates_internal( - update_data, - min_allowed_publish_time, - max_allowed_publish_time, - check_uniqueness, - )?; + let all_parsed_price_pairs = Vec::new(); + for data in &update_data { + if store_updates_if_fresh { + all_parsed_price_pairs.extend(self.update_price_feeds_internal( + data, + price_ids.clone(), + min_allowed_publish_time, + max_allowed_publish_time, + check_uniqueness, + )?); + } else { + all_parsed_price_pairs.extend(self.parse_price_feed_updates_internal( + data, + min_allowed_publish_time, + max_allowed_publish_time, + check_uniqueness, + )?); + } } if check_update_data_is_minimal && price_ids.len() != price_pairs.len() { From 2c77720df3a611955c46efaac9f14aa5cd99b8a2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 22:59:07 +0000 Subject: [PATCH 15/16] fix: modify parse_price_feed_updates_with_config to preserve existing price values - Fixed compilation errors by making all_parsed_price_pairs mutable - Modified logic after first for loop to iterate through price pairs and preserve existing values - Updated parse_price_feed_updates to wrap single update_data in vector - All tests passing Co-Authored-By: ayush.suresh@dourolabs.xyz --- .../stylus/contracts/pyth-receiver/src/lib.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index 7bc41e7e96..85297d66d8 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -287,7 +287,7 @@ impl PythReceiver { max_publish_time: u64, ) -> Result, PythReceiverError> { let price_feeds = self.parse_price_feed_updates_with_config( - update_data, + vec![update_data], price_ids, min_publish_time, max_publish_time, @@ -308,11 +308,11 @@ impl PythReceiver { check_update_data_is_minimal: bool, store_updates_if_fresh: bool, ) -> Result, PythReceiverError> { - let all_parsed_price_pairs = Vec::new(); + let mut all_parsed_price_pairs = Vec::new(); for data in &update_data { if store_updates_if_fresh { all_parsed_price_pairs.extend(self.update_price_feeds_internal( - data, + data.clone(), price_ids.clone(), min_allowed_publish_time, max_allowed_publish_time, @@ -320,7 +320,7 @@ impl PythReceiver { )?); } else { all_parsed_price_pairs.extend(self.parse_price_feed_updates_internal( - data, + data.clone(), min_allowed_publish_time, max_allowed_publish_time, check_uniqueness, @@ -328,13 +328,18 @@ impl PythReceiver { } } - if check_update_data_is_minimal && price_ids.len() != price_pairs.len() { + if check_update_data_is_minimal && all_parsed_price_pairs.len() != price_ids.len() { return Err(PythReceiverError::InvalidUpdateData); } - let price_map: BTreeMap<[u8; 32], PriceInfoReturn> = price_pairs.into_iter().collect(); - let mut result: Vec = Vec::with_capacity(price_ids.len()); + let mut price_map: BTreeMap<[u8; 32], PriceInfoReturn> = BTreeMap::new(); + + for (price_id, price_info) in all_parsed_price_pairs { + if !price_map.contains_key(&price_id) { + price_map.insert(price_id, price_info); + } + } for price_id in price_ids { if let Some(price_info) = price_map.get(&price_id) { From 99998e4cf6d1279355a24fb4756bfd7663fe9dd3 Mon Sep 17 00:00:00 2001 From: Ayush Suresh Date: Fri, 11 Jul 2025 15:44:59 -0500 Subject: [PATCH 16/16] removed parentheses --- target_chains/stylus/contracts/pyth-receiver/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index 43f907d166..8fd7c270a0 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -203,7 +203,7 @@ impl PythReceiver { } for i in 0..price_ids.len() { - if (self.latest_price_info_publish_time(price_ids[i]) < publish_times[i]) { + if self.latest_price_info_publish_time(price_ids[i]) < publish_times[i] { self.update_price_feeds(update_data.clone())?; return Ok(()); }