diff --git a/.vscode/settings.json b/.vscode/settings.json index 799a8ad..6a09a16 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "bytemuck", + "pyth", "sablier", "Zeroable" ] diff --git a/Cargo.toml b/Cargo.toml index cc4db14..313c0b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,5 @@ bytemuck = "1.19.0" anyhow = "1.0.91" solana-program = "~2.0.10" num-traits = "0.2.19" -log = "0.4.22" \ No newline at end of file +log = "0.4.22" +borsh = "1.5.1" diff --git a/src/lib.rs b/src/lib.rs index 294eafa..0e27783 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,9 @@ pub use { pub mod liquidation_price; pub mod math; +pub mod oracle_price; pub mod pda; +pub mod pyth; pub mod types; declare_id!("13gDzEXCdocbj8iAiqrScGo47NiSuYENGsRqi3SEAwet"); diff --git a/src/oracle_price.rs b/src/oracle_price.rs new file mode 100644 index 0000000..57616f3 --- /dev/null +++ b/src/oracle_price.rs @@ -0,0 +1,80 @@ +use { + crate::{math, pyth::PriceUpdateV2, Cortex}, + anyhow::{anyhow, Result}, +}; + +// pub const ORACLE_EXPONENT_SCALE: i32 = -9; +pub const ORACLE_PRICE_SCALE: u128 = 1_000_000_000; +pub const ORACLE_MAX_PRICE: u64 = (1 << 28) - 1; + +// In BPS +pub const MAX_PRICE_ERROR: u16 = 300; + +#[derive(Copy, Clone, Eq, PartialEq, Default, Debug)] +pub struct OraclePrice { + pub price: u64, + pub exponent: i32, + pub confidence: u64, +} + +impl OraclePrice { + pub fn new(price: u64, exponent: i32, conf: u64) -> Self { + Self { + price, + exponent, + confidence: conf, + } + } + + pub fn low(&self) -> Self { + Self { + price: self.price - self.confidence, + exponent: self.exponent, + confidence: 0, + } + } + + pub fn high(&self) -> Self { + Self { + price: self.price + self.confidence, + exponent: self.exponent, + confidence: 0, + } + } + + pub fn new_from_pyth_price_update_v2(price_update_v2: &PriceUpdateV2) -> Result { + let pyth_price = price_update_v2.price_message; + + // Check for maximum confidence + { + let confidence_bps: u64 = math::checked_as_u64(math::checked_ceil_div::( + pyth_price.conf as u128 * Cortex::BPS_POWER, + pyth_price.price as u128, + )?)?; + + if pyth_price.price <= 0 || confidence_bps > MAX_PRICE_ERROR as u64 { + return Err(anyhow!("Pyth price is out of bounds")); + } + } + + OraclePrice { + // price is i64 and > 0 per check above + price: pyth_price.price as u64, + exponent: pyth_price.exponent, + confidence: pyth_price.conf, + } + .scale_to_exponent(-(Cortex::PRICE_DECIMALS as i32)) + } + + pub fn scale_to_exponent(&self, target_exponent: i32) -> Result { + if target_exponent == self.exponent { + return Ok(*self); + } + + Ok(OraclePrice { + price: math::scale_to_exponent(self.price, self.exponent, target_exponent)?, + exponent: target_exponent, + confidence: self.confidence, + }) + } +} diff --git a/src/pyth.rs b/src/pyth.rs new file mode 100644 index 0000000..7779a7f --- /dev/null +++ b/src/pyth.rs @@ -0,0 +1,57 @@ +use { + anchor_lang::prelude::Pubkey, + borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, +}; + +#[derive(BorshSerialize, BorshDeserialize, BorshSchema, Copy, Clone, PartialEq, Debug)] +pub enum VerificationLevel { + Partial { num_signatures: u8 }, + Full, +} + +/// Id of a feed producing the message. One feed produces one or more messages. +pub type FeedId = [u8; 32]; + +#[repr(C)] +#[derive(Debug, Copy, Clone, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)] +pub struct PriceFeedMessage { + pub feed_id: FeedId, + pub price: i64, + pub conf: u64, + pub exponent: i32, + /// The timestamp of this price update in seconds + pub publish_time: i64, + /// The timestamp of the previous price update. This field is intended to allow users to + /// identify the single unique price update for any moment in time: + /// for any time t, the unique update is the one such that prev_publish_time < t <= publish_time. + /// + /// Note that there may not be such an update while we are migrating to the new message-sending logic, + /// as some price updates on pythnet may not be sent to other chains (because the message-sending + /// logic may not have triggered). We can solve this problem by making the message-sending mandatory + /// (which we can do once publishers have migrated over). + /// + /// Additionally, this field may be equal to publish_time if the message is sent on a slot where + /// where the aggregation was unsuccesful. This problem will go away once all publishers have + /// migrated over to a recent version of pyth-agent. + pub prev_publish_time: i64, + pub ema_price: i64, + pub ema_conf: u64, +} + +/// A price update account. This account is used by the Pyth Receiver program to store a verified price update from a Pyth price feed. +/// It contains: +/// - `write_authority`: The write authority for this account. This authority can close this account to reclaim rent or update the account to contain a different price update. +/// - `verification_level`: The [`VerificationLevel`] of this price update. This represents how many Wormhole guardian signatures have been verified for this price update. +/// - `price_message`: The actual price update. +/// - `posted_slot`: The slot at which this price update was posted. +#[derive(BorshSchema, BorshSerialize, BorshDeserialize)] +pub struct PriceUpdateV2 { + pub write_authority: Pubkey, + pub verification_level: VerificationLevel, + pub price_message: PriceFeedMessage, + pub posted_slot: u64, +} + +impl PriceUpdateV2 { + pub const LEN: usize = 8 + 32 + 2 + 32 + 8 + 8 + 4 + 8 + 8 + 8 + 8 + 8; +}