Skip to content

Commit

Permalink
impementing pyth pull oracle
Browse files Browse the repository at this point in the history
  • Loading branch information
nope-finance committed Jun 14, 2024
1 parent 6a30199 commit a6703de
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 17 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions token-lending/program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 9 additions & 2 deletions token-lending/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions token-lending/sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions token-lending/sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
153 changes: 141 additions & 12 deletions token-lending/sdk/src/oracles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OracleType, ProgramError> {
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);
}
Expand All @@ -34,11 +46,8 @@ pub fn get_oracle_type(extra_oracle_info: &AccountInfo) -> Result<OracleType, Pr
Err(LendingError::InvalidOracleConfig.into())
}

pub fn validate_pyth_price_account_info(
lending_market: &LendingMarket,
pyth_price_info: &AccountInfo,
) -> 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);
}
Expand All @@ -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<Decimal, ProgramError> {
if *pyth_price_info.key == solend_program::NULL_PUBKEY {
Expand All @@ -69,13 +93,31 @@ pub fn get_pyth_price_unchecked(pyth_price_info: &AccountInfo) -> Result<Decimal
pyth_price_to_decimal(&price)
}

pub fn get_pyth_pull_price_unchecked(
pyth_price_info: &AccountInfo,
) -> Result<Decimal, 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
})?;

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());
}
Expand Down Expand Up @@ -124,7 +166,64 @@ pub fn get_pyth_price(
Ok((market_price?, ema_price))
}

fn pyth_price_to_decimal(pyth_price: &Price) -> Result<Decimal, ProgramError> {
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<Decimal, ProgramError> {
let price: u64 = pyth_price.price.try_into().map_err(|_| {
msg!("Oracle price cannot be negative");
LendingError::InvalidOracleConfig
Expand Down Expand Up @@ -153,6 +252,36 @@ fn pyth_price_to_decimal(pyth_price: &Price) -> Result<Decimal, ProgramError> {
}
}


fn pyth_pull_price_to_decimal(pyth_price: &pyth_solana_receiver_sdk::price_update::Price) -> Result<Decimal, ProgramError> {
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::*;
Expand Down

0 comments on commit a6703de

Please sign in to comment.