diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 1b8e1b86bde..9eb5549bd68 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -33,7 +33,9 @@ use solana_program::{ }, }; use solend_sdk::{ - oracles::{get_oracle_type, validate_pyth_price_account_info, OracleType}, + oracles::{ + get_oracle_type, get_pyth_price_unchecked, validate_pyth_price_account_info, OracleType, + }, state::{LendingMarketMetadata, RateLimiter, RateLimiterConfig, ReserveType}, }; use solend_sdk::{switchboard_v2_devnet, switchboard_v2_mainnet}; @@ -578,10 +580,12 @@ fn _refresh_reserve<'a>( } match get_oracle_type(extra_oracle_account_info)? { - OracleType::Pyth => Some(get_pyth_price(extra_oracle_account_info, clock)?.0), - OracleType::Switchboard => { - Some(get_switchboard_price_v2(extra_oracle_account_info, clock)?) - } + OracleType::Pyth => Some(get_pyth_price_unchecked(extra_oracle_account_info)?), + OracleType::Switchboard => Some(get_switchboard_price_v2( + extra_oracle_account_info, + clock, + false, + )?), } } None => { @@ -3074,7 +3078,7 @@ fn get_switchboard_price( if switchboard_feed_info.owner == &switchboard_v2_mainnet::id() || switchboard_feed_info.owner == &switchboard_v2_devnet::id() { - return get_switchboard_price_v2(switchboard_feed_info, clock); + return get_switchboard_price_v2(switchboard_feed_info, clock, true); } let account_buf = switchboard_feed_info.try_borrow_data()?; @@ -3113,6 +3117,7 @@ fn get_switchboard_price( fn get_switchboard_price_v2( switchboard_feed_info: &AccountInfo, clock: &Clock, + check_staleness: bool, ) -> Result { const STALE_AFTER_SLOTS_ELAPSED: u64 = 240; let data = &switchboard_feed_info.try_borrow_data()?; @@ -3122,7 +3127,7 @@ fn get_switchboard_price_v2( .slot .checked_sub(feed.latest_confirmed_round.round_open_slot) .ok_or(LendingError::MathOverflow)?; - if slots_elapsed >= STALE_AFTER_SLOTS_ELAPSED { + if check_staleness && slots_elapsed >= STALE_AFTER_SLOTS_ELAPSED { msg!("Switchboard oracle price is stale"); return Err(LendingError::InvalidOracleConfig.into()); } diff --git a/token-lending/program/tests/refresh_reserve.rs b/token-lending/program/tests/refresh_reserve.rs index 8710d038d5f..9e16c0ff22a 100644 --- a/token-lending/program/tests/refresh_reserve.rs +++ b/token-lending/program/tests/refresh_reserve.rs @@ -293,6 +293,20 @@ async fn test_success_pyth_price_stale_switchboard_valid() { wsol_reserve_post.account.liquidity.smoothed_market_price, Decimal::from(11u64) ); + + test.advance_clock_by_slots(241).await; + let err = lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + err, + TransactionError::InstructionError( + 1, + InstructionError::Custom(LendingError::InvalidOracleConfig as u32) + ) + ); } #[tokio::test] @@ -676,19 +690,11 @@ async fn test_use_extra_oracle_bad_cases() { let mut msol_reserve = test.load_account::(reserves[0].pubkey).await; - // this should fail because the extra oracle is stale - let err = lending_market + // this no longer fails because the extra oracle is not checked for staleness/variance + lending_market .refresh_reserve(&mut test, &msol_reserve) .await - .unwrap_err() .unwrap(); - assert_eq!( - err, - TransactionError::InstructionError( - 1, - InstructionError::Custom(LendingError::InvalidOracleConfig as u32) - ) - ); msol_reserve.account.config.extra_oracle_pubkey = Some(msol_reserve.account.liquidity.pyth_oracle_pubkey); diff --git a/token-lending/sdk/src/oracles.rs b/token-lending/sdk/src/oracles.rs index 7688a5feb82..aaeb7c53973 100644 --- a/token-lending/sdk/src/oracles.rs +++ b/token-lending/sdk/src/oracles.rs @@ -52,6 +52,23 @@ pub fn validate_pyth_price_account_info( 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 { + return Err(LendingError::NullOracleConfig.into()); + } + + let data = &pyth_price_info.try_borrow_data()?; + let price_account = pyth_sdk_solana::state::load_price_account(data).map_err(|e| { + msg!("Couldn't load price feed from account info: {:?}", e); + LendingError::InvalidOracleConfig + })?; + + let price_feed = price_account.to_price_feed(pyth_price_info.key); + let price = price_feed.get_price_unchecked(); + pyth_price_to_decimal(&price) +} + pub fn get_pyth_price( pyth_price_info: &AccountInfo, clock: &Clock, @@ -453,4 +470,47 @@ mod test { ); } } + + #[test] + fn pyth_price_unchecked_test_cases() { + let mut price_account = PriceAccount { + magic: MAGIC, + ver: VERSION_2, + atype: AccountType::Price as u32, + ptype: PriceType::Price, + expo: 1, + timestamp: 1, + ema_price: Rational { + val: 11, + numer: 110, + denom: 10, + }, + agg: PriceInfo { + price: 200, + conf: 40, + status: PriceStatus::Trading, + corp_act: CorpAction::NoCorpAct, + pub_slot: 0, + }, + ..PriceAccount::default() + }; + + let mut lamports = 20; + let pubkey = Pubkey::new_unique(); + let account_info = AccountInfo::new( + &pubkey, + false, + false, + &mut lamports, + bytes_of_mut(&mut price_account), + &pubkey, + false, + 0, + ); + + assert_eq!( + get_pyth_price_unchecked(&account_info), + Ok(Decimal::from(2000_u64)) + ); + } }