diff --git a/Cargo.lock b/Cargo.lock index 15f7d02d90f..5b5cd8229e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5136,12 +5136,14 @@ dependencies = [ "arrayref", "assert_matches", "base64 0.13.1", + "borsh 0.10.3", "bytemuck", "log", "num-derive 0.3.3", "num-traits", "proptest", "pyth-sdk-solana", + "pyth-solana-receiver-sdk", "rand 0.8.5", "serde", "serde_yaml 0.8.26", diff --git a/token-lending/program/Cargo.toml b/token-lending/program/Cargo.toml index 2783bafff41..95e8f567b18 100644 --- a/token-lending/program/Cargo.toml +++ b/token-lending/program/Cargo.toml @@ -12,15 +12,15 @@ no-entrypoint = [] test-bpf = [] [dependencies] +bytemuck = "1.5.1" pyth-sdk-solana = "0.8.0" +pyth-solana-receiver-sdk = "0.3.0" solana-program = "=1.16.20" -spl-token = { version = "3.3.0", features=["no-entrypoint"] } solend-sdk = { path = "../sdk" } +spl-token = { version = "3.3.0", features=["no-entrypoint"] } static_assertions = "1.1.0" switchboard-program = "0.2.0" switchboard-v2 = "0.1.3" -bytemuck = "1.5.1" -pyth-solana-receiver-sdk = "0.3.0" [dev-dependencies] assert_matches = "1.5.0" diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 7aaa9860c07..1fc2d699d62 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -34,7 +34,8 @@ use solana_program::{ use solend_sdk::{ math::SaturatingSub, oracles::{ - get_oracle_type, get_pyth_price_unchecked, validate_pyth_price_account_info, OracleType, + get_oracle_type, get_pyth_price_unchecked, get_pyth_pull_price_unchecked, + validate_pyth_price_account_info, OracleType, }, state::{LendingMarketMetadata, RateLimiter, RateLimiterConfig, ReserveType}, }; @@ -497,7 +498,10 @@ fn validate_extra_oracle( match get_oracle_type(extra_oracle_info)? { OracleType::Pyth => { - validate_pyth_price_account_info(lending_market, extra_oracle_info)?; + validate_pyth_price_account_info(extra_oracle_info)?; + } + OracleType::PythPull => { + validate_pyth_price_account_info(extra_oracle_info)?; } OracleType::Switchboard => { validate_switchboard_keys(lending_market, extra_oracle_info)?; @@ -573,6 +577,9 @@ fn _refresh_reserve<'a>( match get_oracle_type(extra_oracle_account_info)? { OracleType::Pyth => Some(get_pyth_price_unchecked(extra_oracle_account_info)?), + OracleType::PythPull => { + Some(get_pyth_pull_price_unchecked(extra_oracle_account_info)?) + } OracleType::Switchboard => Some(get_switchboard_price_v2( extra_oracle_account_info, clock, diff --git a/token-lending/sdk/Cargo.toml b/token-lending/sdk/Cargo.toml index 857ac1ff821..380c675ea44 100644 --- a/token-lending/sdk/Cargo.toml +++ b/token-lending/sdk/Cargo.toml @@ -9,10 +9,12 @@ edition = "2018" [dependencies] arrayref = "0.3.6" +borsh = "0.10.3" bytemuck = "1.5.1" num-derive = "0.3" num-traits = "0.2" pyth-sdk-solana = "0.8.0" +pyth-solana-receiver-sdk = "0.3.0" solana-program = ">=1.9" spl-token = { version = "3.2.0", features=["no-entrypoint"] } static_assertions = "1.1.0" diff --git a/token-lending/sdk/src/lib.rs b/token-lending/sdk/src/lib.rs index c5b1933b186..5553caf7751 100644 --- a/token-lending/sdk/src/lib.rs +++ b/token-lending/sdk/src/lib.rs @@ -42,3 +42,8 @@ pub mod switchboard_v2_devnet { pub mod pyth_mainnet { solana_program::declare_id!("FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH"); } + +/// Mainnet program id for pyth +pub mod pyth_pull_mainnet { + solana_program::declare_id!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ"); +} \ No newline at end of file diff --git a/token-lending/sdk/src/oracles.rs b/token-lending/sdk/src/oracles.rs index aaeb7c53973..7865881c8c9 100644 --- a/token-lending/sdk/src/oracles.rs +++ b/token-lending/sdk/src/oracles.rs @@ -3,25 +3,37 @@ use crate::{ self as solend_program, error::LendingError, math::{Decimal, TryDiv, TryMul}, - pyth_mainnet, + pyth_mainnet, pyth_pull_mainnet, solana_program, state::LendingMarket, switchboard_v2_mainnet, }; + +use borsh::BorshDeserialize; use pyth_sdk_solana::Price; -// use pyth_sdk_solana; +use pyth_solana_receiver_sdk::price_update::{self, PriceUpdateV2, VerificationLevel}; use solana_program::{ account_info::AccountInfo, msg, program_error::ProgramError, sysvar::clock::Clock, }; -use std::{convert::TryInto, result::Result}; +use std::{ + convert::{TryFrom, TryInto}, + result::Result, +}; + +const PYTH_CONFIDENCE_RATIO: u64 = 10; +const STALE_AFTER_SLOTS_ELAPSED: u64 = 240; // roughly 2 min +const STALE_AFTER_SECONDS_ELAPSED: u64 = 120; // roughly 2 min pub enum OracleType { Pyth, Switchboard, + PythPull, } pub fn get_oracle_type(extra_oracle_info: &AccountInfo) -> Result { if *extra_oracle_info.owner == pyth_mainnet::id() { return Ok(OracleType::Pyth); + } else if *extra_oracle_info.owner == pyth_pull_mainnet::id() { + return Ok(OracleType::PythPull); } else if *extra_oracle_info.owner == switchboard_v2_mainnet::id() { return Ok(OracleType::Switchboard); } @@ -34,11 +46,8 @@ pub fn get_oracle_type(extra_oracle_info: &AccountInfo) -> Result Result<(), ProgramError> { - if *pyth_price_info.owner != lending_market.oracle_program_id { +pub fn validate_pyth_price_account_info(pyth_price_info: &AccountInfo) -> Result<(), ProgramError> { + if *pyth_price_info.owner != pyth_mainnet::id() { msg!("pyth price account is not owned by pyth program"); return Err(ProgramError::IncorrectProgramId); } @@ -52,6 +61,21 @@ pub fn validate_pyth_price_account_info( Ok(()) } +pub fn validate_pyth_pull_price_account_info( + pyth_price_info: &AccountInfo, +) -> Result<(), ProgramError> { + if *pyth_price_info.owner != pyth_pull_mainnet::id() { + msg!("pyth price account is not owned by pyth program"); + return Err(ProgramError::IncorrectProgramId); + } + let data = &pyth_price_info.data.borrow(); + let _price_feed_account: PriceUpdateV2 = PriceUpdateV2::try_from_slice(data).map_err(|e| { + msg!("Couldn't load price feed from account info: {:?}", e); + LendingError::InvalidOracleConfig + })?; + Ok(()) +} + /// get pyth price without caring about staleness or variance. only used pub fn get_pyth_price_unchecked(pyth_price_info: &AccountInfo) -> Result { if *pyth_price_info.key == solend_program::NULL_PUBKEY { @@ -69,13 +93,31 @@ pub fn get_pyth_price_unchecked(pyth_price_info: &AccountInfo) -> Result Result { + if *pyth_price_info.owner != pyth_pull_mainnet::id() { + msg!("pyth price account is not owned by pyth program"); + return Err(ProgramError::IncorrectProgramId); + } + let data = &pyth_price_info.data.borrow(); + let price_feed_account: PriceUpdateV2 = PriceUpdateV2::try_from_slice(data).map_err(|e| { + msg!("Couldn't load price feed from account info: {:?}", e); + LendingError::InvalidOracleConfig + })?; + + let price = price_feed_account.get_price_unchecked(&price_feed_account.price_message.feed_id).map_err(|e| { + msg!("Couldn't load price feed from account info: {:?}", e); + LendingError::InvalidOracleConfig + })?; + pyth_pull_price_to_decimal(&price) +} + + pub fn get_pyth_price( pyth_price_info: &AccountInfo, clock: &Clock, ) -> Result<(Decimal, Decimal), ProgramError> { - const PYTH_CONFIDENCE_RATIO: u64 = 10; - const STALE_AFTER_SLOTS_ELAPSED: u64 = 240; // roughly 2 min - if *pyth_price_info.key == solend_program::NULL_PUBKEY { return Err(LendingError::NullOracleConfig.into()); } @@ -124,7 +166,64 @@ pub fn get_pyth_price( Ok((market_price?, ema_price)) } -fn pyth_price_to_decimal(pyth_price: &Price) -> Result { +pub fn get_pyth_pull_price( + pyth_price_info: &AccountInfo, + clock: &Clock, +) -> Result<(Decimal, Decimal), ProgramError> { + if *pyth_price_info.key == solend_program::NULL_PUBKEY { + return Err(LendingError::NullOracleConfig.into()); + } + + let data = &pyth_price_info.data.borrow(); + let price_feed_account: PriceUpdateV2 = PriceUpdateV2::try_from_slice(data).map_err(|e| { + msg!("Couldn't load price feed from account info: {:?}", e); + LendingError::InvalidOracleConfig + })?; + + let pyth_price = price_feed_account.get_price_no_older_than_with_custom_verification_level( + clock, + STALE_AFTER_SECONDS_ELAPSED, // MAXIMUM_AGE, // this should be filtered by the caller + &price_feed_account.price_message.feed_id, + VerificationLevel::Full, // All our prices and the sponsored feeds are full verified + ).map_err(|e| { + msg!("Pyth oracle price is likey too stale! error: {:?}", e); + LendingError::InvalidOracleConfig + })?; + + let price: u64 = pyth_price.price.try_into().map_err(|_| { + msg!("Oracle price cannot be negative"); + LendingError::InvalidOracleConfig + })?; + + // Perhaps confidence_ratio should exist as a per reserve config + // 100/confidence_ratio = maximum size of confidence range as a percent of price + // confidence_ratio of 10 filters out pyth prices with conf > 10% of price + if pyth_price.conf.saturating_mul(PYTH_CONFIDENCE_RATIO) > price { + msg!( + "Oracle price confidence is too wide. price: {}, conf: {}", + price, + pyth_price.conf, + ); + return Err(LendingError::InvalidOracleConfig.into()); + } + + let market_price = pyth_pull_price_to_decimal(&pyth_price)?; + + let ema_price = { + let ema_price = pyth_solana_receiver_sdk::price_update::Price{ + price: price_feed_account.price_message.ema_price, + conf: price_feed_account.price_message.ema_conf, + exponent: price_feed_account.price_message.exponent, + publish_time: price_feed_account.price_message.publish_time, + }; + pyth_pull_price_to_decimal(&ema_price)? + }; + + Ok((market_price, ema_price)) +} + + +fn pyth_price_to_decimal(pyth_price: &pyth_sdk_solana::Price) -> Result { let price: u64 = pyth_price.price.try_into().map_err(|_| { msg!("Oracle price cannot be negative"); LendingError::InvalidOracleConfig @@ -153,6 +252,36 @@ fn pyth_price_to_decimal(pyth_price: &Price) -> Result { } } + +fn pyth_pull_price_to_decimal(pyth_price: &pyth_solana_receiver_sdk::price_update::Price) -> Result { + let price: u64 = pyth_price.price.try_into().map_err(|_| { + msg!("Oracle price cannot be negative"); + LendingError::InvalidOracleConfig + })?; + + if pyth_price.exponent >= 0 { + let exponent = pyth_price + .exponent + .try_into() + .map_err(|_| LendingError::MathOverflow)?; + let zeros = 10u64 + .checked_pow(exponent) + .ok_or(LendingError::MathOverflow)?; + Decimal::from(price).try_mul(zeros) + } else { + let exponent = pyth_price + .exponent + .checked_abs() + .ok_or(LendingError::MathOverflow)? + .try_into() + .map_err(|_| LendingError::MathOverflow)?; + let decimals = 10u64 + .checked_pow(exponent) + .ok_or(LendingError::MathOverflow)?; + Decimal::from(price).try_div(decimals) + } +} + #[cfg(test)] mod test { use super::*;