pub mod price_map;
mod feed;
pub mod switchboard;
pub mod chainlink;
pub mod pyth;
pub mod validator;
pub mod time;
use std::ops::Deref;
use crate::{
states::{TokenMapAccess, TokenMapLoader},
CoreError, CoreResult,
};
use anchor_lang::prelude::*;
use self::price_map::PriceMap;
use super::{HasMarketMeta, Seed, Store, TokenConfig, TokenMapHeader, TokenMapRef};
pub use self::{
chainlink::Chainlink,
feed::{PriceFeed, PriceFeedPrice},
pyth::Pyth,
switchboard::Switchboard,
time::{ValidateOracleTime, ValidateOracleTimeExt},
validator::PriceValidator,
};
pub use gmsol_utils::oracle::PriceProviderKind;
const MAX_FLAGS: usize = 8;
#[repr(u8)]
#[non_exhaustive]
#[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive)]
enum OracleFlag {
Cleared,
}
gmsol_utils::flags!(OracleFlag, MAX_FLAGS, u8);
#[account(zero_copy)]
#[cfg_attr(feature = "debug", derive(derive_more::Debug))]
pub struct Oracle {
version: u8,
#[cfg_attr(feature = "debug", debug(skip))]
padding_0: [u8; 7],
pub store: Pubkey,
pub(crate) authority: Pubkey,
min_oracle_ts: i64,
max_oracle_ts: i64,
min_oracle_slot: u64,
primary: PriceMap,
flags: OracleFlagContainer,
#[cfg_attr(feature = "debug", debug(skip))]
padding_1: [u8; 3],
#[cfg_attr(feature = "debug", debug(skip))]
reserved: [u8; 256],
}
impl gmsol_utils::InitSpace for Oracle {
const INIT_SPACE: usize = std::mem::size_of::<Self>();
}
impl Seed for Oracle {
const SEED: &'static [u8] = b"oracle";
}
impl Oracle {
pub(crate) fn init(&mut self, store: Pubkey, authority: Pubkey) {
self.clear_all_prices();
self.store = store;
self.authority = authority;
}
pub fn is_cleared(&self) -> bool {
self.flags.get_flag(OracleFlag::Cleared)
}
pub(crate) fn set_prices_from_remaining_accounts<'info>(
&mut self,
mut validator: PriceValidator,
map: &TokenMapRef,
tokens: &[Pubkey],
remaining_accounts: &'info [AccountInfo<'info>],
chainlink: Option<&Program<'info, Chainlink>>,
) -> Result<()> {
require!(self.is_cleared(), CoreError::PricesAreAlreadySet);
require!(self.primary.is_empty(), CoreError::PricesAreAlreadySet);
require!(
tokens.len() <= PriceMap::MAX_TOKENS,
CoreError::ExceedMaxLengthLimit
);
require!(
tokens.len() <= remaining_accounts.len(),
ErrorCode::AccountNotEnoughKeys
);
for (idx, token) in tokens.iter().enumerate() {
let feed = &remaining_accounts[idx];
let token_config = map.get(token).ok_or_else(|| error!(CoreError::NotFound))?;
require!(token_config.is_enabled(), CoreError::TokenConfigDisabled);
let oracle_price = OraclePrice::parse_from_feed_account(
validator.clock(),
token_config,
chainlink,
feed,
)?;
validator.validate_one(
token_config,
&oracle_price.provider,
oracle_price.oracle_ts,
oracle_price.oracle_slot,
&oracle_price.price,
)?;
self.primary
.set(token, oracle_price.price, token_config.is_synthetic())?;
}
self.update_oracle_ts_and_slot(validator)?;
Ok(())
}
pub fn min_oracle_slot(&self) -> Option<u64> {
if self.is_cleared() {
None
} else {
Some(self.min_oracle_slot)
}
}
pub fn min_oracle_ts(&self) -> i64 {
self.min_oracle_ts
}
pub fn max_oracle_ts(&self) -> i64 {
self.max_oracle_ts
}
fn update_oracle_ts_and_slot(&mut self, mut validator: PriceValidator) -> Result<()> {
validator.merge_range(
self.min_oracle_slot(),
self.min_oracle_ts,
self.max_oracle_ts,
);
if let Some((min_slot, min_ts, max_ts)) = validator.finish()? {
self.min_oracle_slot = min_slot;
self.min_oracle_ts = min_ts;
self.max_oracle_ts = max_ts;
self.flags.set_flag(OracleFlag::Cleared, false);
}
Ok(())
}
pub(crate) fn clear_all_prices(&mut self) {
self.primary.clear();
self.min_oracle_ts = i64::MAX;
self.max_oracle_ts = i64::MIN;
self.min_oracle_slot = u64::MAX;
self.flags.set_flag(OracleFlag::Cleared, true);
}
#[inline(never)]
pub(crate) fn with_prices<'info, T>(
&mut self,
store: &AccountLoader<'info, Store>,
token_map: &AccountLoader<'info, TokenMapHeader>,
tokens: &[Pubkey],
remaining_accounts: &'info [AccountInfo<'info>],
chainlink: Option<&Program<'info, Chainlink>>,
f: impl FnOnce(&mut Self, &'info [AccountInfo<'info>]) -> Result<T>,
) -> Result<T> {
let validator = PriceValidator::try_from(store.load()?.deref())?;
require_gte!(
remaining_accounts.len(),
tokens.len(),
CoreError::NotEnoughTokenFeeds,
);
let feeds = &remaining_accounts[..tokens.len()];
let remaining_accounts = &remaining_accounts[tokens.len()..];
let res = {
let token_map = token_map.load_token_map()?;
self.set_prices_from_remaining_accounts(validator, &token_map, tokens, feeds, chainlink)
};
match res {
Ok(()) => {
let output = f(self, remaining_accounts);
self.clear_all_prices();
output
}
Err(err) => {
self.clear_all_prices();
Err(err)
}
}
}
pub(crate) fn validate_time(&self, target: &impl ValidateOracleTime) -> CoreResult<()> {
if self.max_oracle_ts < self.min_oracle_ts {
msg!("min = {}, max = {}", self.min_oracle_ts, self.max_oracle_ts);
return Err(CoreError::InvalidOracleTimestampsRange);
}
target.validate_min_oracle_slot(self)?;
target.validate_min_oracle_ts(self)?;
target.validate_max_oracle_ts(self)?;
Ok(())
}
pub fn get_primary_price(
&self,
token: &Pubkey,
allow_synthetic: bool,
) -> Result<gmsol_model::price::Price<u128>> {
let price = self
.primary
.get(token)
.ok_or_else(|| error!(CoreError::MissingOraclePrice))?;
if !allow_synthetic {
require!(
!price.is_synthetic(),
CoreError::SyntheticTokenPriceIsNotAllowed
);
}
Ok(gmsol_model::price::Price {
min: price.min().to_unit_price(),
max: price.max().to_unit_price(),
})
}
pub(crate) fn market_prices(
&self,
market: &impl HasMarketMeta,
) -> Result<gmsol_model::price::Prices<u128>> {
let meta = market.market_meta();
let prices = gmsol_model::price::Prices {
index_token_price: self.get_primary_price(&meta.index_token_mint, true)?,
long_token_price: self.get_primary_price(&meta.long_token_mint, false)?,
short_token_price: self.get_primary_price(&meta.short_token_mint, false)?,
};
Ok(prices)
}
}
fn from_program_id(program_id: &Pubkey) -> Option<PriceProviderKind> {
if *program_id == Chainlink::id() {
Some(PriceProviderKind::Chainlink)
} else if *program_id == Pyth::id() {
Some(PriceProviderKind::Pyth)
} else if *program_id == Switchboard::id() {
Some(PriceProviderKind::Switchboard)
} else {
None
}
}
struct OraclePrice {
provider: PriceProviderKind,
oracle_slot: u64,
oracle_ts: i64,
price: gmsol_utils::Price,
}
impl OraclePrice {
fn parse_from_feed_account<'info>(
clock: &Clock,
token_config: &TokenConfig,
chainlink: Option<&Program<'info, Chainlink>>,
account: &'info AccountInfo<'info>,
) -> Result<Self> {
let (provider, parsed) = match from_program_id(account.owner) {
Some(provider) => (provider, None),
None if *account.owner == crate::ID => {
let loader = AccountLoader::<'info, PriceFeed>::try_from(account)?;
let feed = loader.load()?;
let kind = feed.provider()?;
(kind, Some(feed.check_and_get_price(clock, token_config)?))
}
None => return Err(error!(CoreError::InvalidPriceFeedAccount)),
};
require_eq!(
token_config.expected_provider().map_err(CoreError::from)?,
provider
);
let feed_id = token_config.get_feed(&provider).map_err(CoreError::from)?;
let (oracle_slot, oracle_ts, price) = match provider {
PriceProviderKind::ChainlinkDataStreams => {
parsed.ok_or_else(|| error!(CoreError::Internal))?
}
PriceProviderKind::Pyth => {
let (oracle_slot, oracle_ts, price) =
Pyth::check_and_get_price(clock, token_config, account, &feed_id)?;
(oracle_slot, oracle_ts, price)
}
PriceProviderKind::Chainlink => {
require_keys_eq!(feed_id, account.key(), CoreError::InvalidPriceFeedAccount);
let program =
chainlink.ok_or_else(|| error!(CoreError::ChainlinkProgramIsRequired))?;
let (oracle_slot, oracle_ts, price) = Chainlink::check_and_get_chainlink_price(
clock,
program,
token_config,
account,
)?;
(oracle_slot, oracle_ts, price)
}
PriceProviderKind::Switchboard => {
require_keys_eq!(feed_id, account.key(), CoreError::InvalidPriceFeedAccount);
Switchboard::check_and_get_price(clock, token_config, account)?
}
kind => {
msg!("Unsupported price provider: {}", kind);
return err!(CoreError::Unimplemented);
}
};
Ok(Self {
provider,
oracle_slot,
oracle_ts,
price,
})
}
}