From a60733559cc23bfcb8f71617b11439daa331c3b5 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Fri, 12 Apr 2024 14:37:25 -0700 Subject: [PATCH] [solana push oracle] Idempotent updates (#1452) * idempotent updates * clippy --- target_chains/solana/Cargo.lock | 1 + .../programs/pyth-push-oracle/Cargo.toml | 1 + .../programs/pyth-push-oracle/src/lib.rs | 57 ++++++++++++++----- .../tests/test_update_price_feed.rs | 36 ++++++------ 4 files changed, 60 insertions(+), 35 deletions(-) diff --git a/target_chains/solana/Cargo.lock b/target_chains/solana/Cargo.lock index a336aba0..bdaa5f46 100644 --- a/target_chains/solana/Cargo.lock +++ b/target_chains/solana/Cargo.lock @@ -3019,6 +3019,7 @@ name = "pyth-push-oracle" version = "0.1.0" dependencies = [ "anchor-lang", + "byteorder", "common-test-utils", "program-simulator", "pyth-solana-receiver", diff --git a/target_chains/solana/programs/pyth-push-oracle/Cargo.toml b/target_chains/solana/programs/pyth-push-oracle/Cargo.toml index b8e6fd86..c1f3ffbb 100644 --- a/target_chains/solana/programs/pyth-push-oracle/Cargo.toml +++ b/target_chains/solana/programs/pyth-push-oracle/Cargo.toml @@ -19,6 +19,7 @@ test-bpf = [] anchor-lang = { workspace = true } pythnet-sdk = { path = "../../../../pythnet/pythnet_sdk" } solana-program = { workspace = true } +byteorder = "1.4.3" pyth-solana-receiver-sdk = { path = "../../pyth_solana_receiver_sdk"} pyth-solana-receiver = { path = "../pyth-solana-receiver", features = ["cpi"]} diff --git a/target_chains/solana/programs/pyth-push-oracle/src/lib.rs b/target_chains/solana/programs/pyth-push-oracle/src/lib.rs index b069fd90..5c0ffae2 100644 --- a/target_chains/solana/programs/pyth-push-oracle/src/lib.rs +++ b/target_chains/solana/programs/pyth-push-oracle/src/lib.rs @@ -9,7 +9,13 @@ use { price_update::PriceUpdateV2, PYTH_PUSH_ORACLE_ID, }, - pythnet_sdk::messages::FeedId, + pythnet_sdk::{ + messages::{ + FeedId, + Message, + }, + wire::from_slice, + }, }; pub mod sdk; @@ -22,6 +28,10 @@ pub enum PushOracleError { UpdatesNotMonotonic, #[msg("Trying to update price feed with the wrong feed id")] PriceFeedMessageMismatch, + #[msg("The message in the update must be a PriceFeedMessage")] + UnsupportedMessageType, + #[msg("Could not deserialize the message in the update")] + DeserializeMessageFailed, } #[program] pub mod pyth_push_oracle { @@ -53,7 +63,7 @@ pub mod pyth_push_oracle { let signer_seeds = &[&seeds[..]]; let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds); - + // Get the timestamp of the price currently stored in the price feed account. let current_timestamp = { if ctx.accounts.price_feed_account.data_is_empty() { 0 @@ -64,20 +74,37 @@ pub mod pyth_push_oracle { price_feed_account.price_message.publish_time } }; - pyth_solana_receiver::cpi::post_update(cpi_context, params)?; - { - let price_feed_account_data = ctx.accounts.price_feed_account.try_borrow_data()?; - let price_feed_account = - PriceUpdateV2::try_deserialize(&mut &price_feed_account_data[..])?; - require!( - price_feed_account.price_message.publish_time > current_timestamp, - PushOracleError::UpdatesNotMonotonic - ); - require!( - price_feed_account.price_message.feed_id == feed_id, - PushOracleError::PriceFeedMessageMismatch - ); + // Get the timestamp of the price in the arguments (that we are trying to put in the account). + // It is a little annoying that we have to redundantly deserialize the message here, but + // it is required to make txs pushing stale prices succeed w/o updating the on-chain price. + // + // Note that we don't do any validity checks on the proof etc. here. If the caller passes an + // invalid message with a newer timestamp, the validity checks will be performed by pyth_solana_receiver. + let message = + from_slice::(params.merkle_price_update.message.as_ref()) + .map_err(|_| PushOracleError::DeserializeMessageFailed)?; + let next_timestamp = match message { + Message::PriceFeedMessage(price_feed_message) => price_feed_message.publish_time, + Message::TwapMessage(_) => { + return err!(PushOracleError::UnsupportedMessageType); + } + }; + + // Only update the price feed if the message contains a newer price. Pushing a stale price + // suceeds without changing the on-chain state. + if next_timestamp > current_timestamp { + pyth_solana_receiver::cpi::post_update(cpi_context, params)?; + { + let price_feed_account_data = ctx.accounts.price_feed_account.try_borrow_data()?; + let price_feed_account = + PriceUpdateV2::try_deserialize(&mut &price_feed_account_data[..])?; + + require!( + price_feed_account.price_message.feed_id == feed_id, + PushOracleError::PriceFeedMessageMismatch + ); + } } Ok(()) } diff --git a/target_chains/solana/programs/pyth-push-oracle/tests/test_update_price_feed.rs b/target_chains/solana/programs/pyth-push-oracle/tests/test_update_price_feed.rs index 60001274..9bc11f17 100644 --- a/target_chains/solana/programs/pyth-push-oracle/tests/test_update_price_feed.rs +++ b/target_chains/solana/programs/pyth-push-oracle/tests/test_update_price_feed.rs @@ -161,26 +161,22 @@ async fn test_update_price_feed() { program_simulator.get_clock().await.unwrap().slot ); - // post another update, outdated - assert_eq!( - program_simulator - .process_ix_with_default_compute_limit( - UpdatePriceFeed::populate( - poster.pubkey(), - encoded_vaa_addresses[0], - DEFAULT_SHARD, - feed_id, - DEFAULT_TREASURY_ID, - merkle_price_updates[1].clone(), - ), - &vec![&poster], - None, - ) - .await - .unwrap_err() - .unwrap(), - into_transaction_error(PushOracleError::UpdatesNotMonotonic) - ); + // post a stale update. The tx succeeds w/o updating on-chain account state. + program_simulator + .process_ix_with_default_compute_limit( + UpdatePriceFeed::populate( + poster.pubkey(), + encoded_vaa_addresses[0], + DEFAULT_SHARD, + feed_id, + DEFAULT_TREASURY_ID, + merkle_price_updates[0].clone(), + ), + &vec![&poster], + None, + ) + .await + .unwrap(); assert_treasury_balance( &mut program_simulator,