use {
crate::{error::TwammError, math, state},
anchor_lang::prelude::*,
};
const ORACLE_EXPONENT_SCALE: i32 = -9;
const ORACLE_PRICE_SCALE: u64 = 1_000_000_000;
const ORACLE_MAX_PRICE: u64 = (1 << 28) - 1;
#[derive(Copy, Clone, AnchorSerialize, AnchorDeserialize, Debug)]
pub enum OracleType {
None,
Test,
Pyth,
}
impl Default for OracleType {
fn default() -> Self {
Self::None
}
}
#[derive(Copy, Clone, AnchorSerialize, AnchorDeserialize, Debug)]
pub struct OraclePrice {
pub price: u64,
pub exponent: i32,
}
#[account]
#[derive(Default, Debug)]
pub struct TestOracle {
pub price: u64,
pub expo: i32,
pub conf: u64,
pub publish_time: i64,
}
impl TestOracle {
pub const LEN: usize = 8 + std::mem::size_of::<TestOracle>();
}
pub fn get_oracle_price(
oracle_type: OracleType,
oracle_account: &AccountInfo,
max_price_error: f64,
max_price_age_sec: u32,
current_time: i64,
) -> Result<OraclePrice> {
match oracle_type {
OracleType::Test => get_test_price(
oracle_account,
max_price_error,
max_price_age_sec,
current_time,
),
OracleType::Pyth => get_pyth_price(
oracle_account,
max_price_error,
max_price_age_sec,
current_time,
),
_ => err!(TwammError::UnsupportedOracle),
}
}
pub fn get_asset_value_usd(
token_amount: u64,
token_decimals: u8,
oracle_price: &OraclePrice,
) -> Result<f64> {
if token_amount == 0 {
return Ok(0.0);
}
let res = token_amount as f64
* oracle_price.price as f64
* math::checked_powi(
10.0,
math::checked_sub(oracle_price.exponent, token_decimals as i32)?,
)?;
if res.is_finite() {
Ok(res)
} else {
err!(TwammError::MathOverflow)
}
}
pub fn get_test_price(
test_price_info: &AccountInfo,
max_price_error: f64,
max_price_age_sec: u32,
current_time: i64,
) -> Result<OraclePrice> {
require!(
!state::is_empty_account(test_price_info)?,
TwammError::InvalidOracleAccount
);
let oracle_acc = Account::<TestOracle>::try_from(test_price_info)?;
let last_update_age_sec = math::checked_sub(current_time, oracle_acc.publish_time)?;
if last_update_age_sec > max_price_age_sec as i64 {
msg!("Error: Test oracle price is stale");
return err!(TwammError::StaleOraclePrice);
}
if oracle_acc.price == 0
|| math::checked_float_div(oracle_acc.conf as f64, oracle_acc.price as f64)?
> max_price_error
{
msg!("Error: Test oracle price is out of bounds");
return err!(TwammError::InvalidOraclePrice);
}
Ok(OraclePrice {
price: oracle_acc.price,
exponent: oracle_acc.expo,
})
}
pub fn get_pyth_price(
pyth_price_info: &AccountInfo,
max_price_error: f64,
max_price_age_sec: u32,
current_time: i64,
) -> Result<OraclePrice> {
require!(
!state::is_empty_account(pyth_price_info)?,
TwammError::InvalidOracleAccount
);
let price_feed = pyth_sdk_solana::load_price_feed_from_account_info(pyth_price_info)
.map_err(|_| TwammError::InvalidOracleAccount)?;
let pyth_price = price_feed.get_price_unchecked();
let last_update_age_sec = math::checked_sub(current_time, pyth_price.publish_time)?;
if last_update_age_sec > max_price_age_sec as i64 {
msg!("Error: Pyth oracle price is stale");
return err!(TwammError::StaleOraclePrice);
}
if pyth_price.price <= 0
|| math::checked_float_div(pyth_price.conf as f64, pyth_price.price as f64)?
> max_price_error
{
msg!("Error: Pyth oracle price is out of bounds");
return err!(TwammError::InvalidOraclePrice);
}
Ok(OraclePrice {
price: pyth_price.price as u64,
exponent: pyth_price.expo,
})
}
impl OraclePrice {
pub fn new(price: u64, exponent: i32) -> Self {
Self { price, exponent }
}
pub fn new_from_token(amount_and_decimals: (u64, u8)) -> Self {
Self {
price: amount_and_decimals.0,
exponent: -(amount_and_decimals.1 as i32),
}
}
pub fn normalize(&self) -> Result<OraclePrice> {
let mut p = self.price;
let mut e = self.exponent;
while p > ORACLE_MAX_PRICE {
p = math::checked_div(p, 10)?;
e = math::checked_add(e, 1)?;
}
Ok(OraclePrice {
price: p,
exponent: e,
})
}
pub fn checked_div(&self, other: &OraclePrice) -> Result<OraclePrice> {
let base = self.normalize()?;
let other = other.normalize()?;
Ok(OraclePrice {
price: math::checked_div(
math::checked_mul(base.price, ORACLE_PRICE_SCALE)?,
other.price,
)?,
exponent: math::checked_sub(
math::checked_add(base.exponent, ORACLE_EXPONENT_SCALE)?,
other.exponent,
)?,
})
}
pub fn checked_mul(&self, other: &OraclePrice) -> Result<OraclePrice> {
Ok(OraclePrice {
price: math::checked_mul(self.price, other.price)?,
exponent: math::checked_add(self.exponent, other.exponent)?,
})
}
pub fn scale_to_exponent(&self, target_exponent: i32) -> Result<OraclePrice> {
if target_exponent == self.exponent {
return Ok(*self);
}
let delta = math::checked_sub(target_exponent, self.exponent)?;
if delta > 0 {
Ok(OraclePrice {
price: math::checked_div(self.price, math::checked_pow(10, delta as usize)?)?,
exponent: target_exponent,
})
} else {
Ok(OraclePrice {
price: math::checked_mul(self.price, math::checked_pow(10, (-delta) as usize)?)?,
exponent: target_exponent,
})
}
}
pub fn checked_as_f64(&self) -> Result<f64> {
math::checked_float_mul(self.price as f64, math::checked_powi(10.0, self.exponent)?)
}
}