Pyth v2: compare program clock with last price publication time for oracle staleness check (#983)

Pyth v2: compare program clock with last price publication time for oracle staleness check
This commit is contained in:
Serge Farny 2024-07-22 14:41:03 +01:00 committed by GitHub
parent 6d9f9b664c
commit 0a55f46efb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 212 additions and 116 deletions

View File

@ -1,11 +1,11 @@
use std::collections::HashMap;
use itertools::Itertools;
use mango_v4::accounts_zerocopy::KeyedAccount;
use mango_v4::state::OracleAccountInfos;
use mango_v4_client::{Client, MangoGroupContext};
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::*;
pub async fn run(client: &Client, group: Pubkey) -> anyhow::Result<()> {
@ -44,6 +44,7 @@ pub async fn run(client: &Client, group: Pubkey) -> anyhow::Result<()> {
}
let response = response.unwrap();
let slot = response.context.slot;
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let accounts = response.value;
for (pubkey, account_opt) in oracles.iter().zip(accounts.into_iter()) {
@ -60,9 +61,10 @@ pub async fn run(client: &Client, group: Pubkey) -> anyhow::Result<()> {
let perp_opt = perp_markets.get(pubkey);
let mut price = None;
if let Some(bank) = bank_opt {
match bank
.oracle_price(&OracleAccountInfos::from_reader(&keyed_account), Some(slot))
{
match bank.oracle_price(
&OracleAccountInfos::from_reader(&keyed_account),
Some((now, slot)),
) {
Ok(p) => price = Some(p),
Err(e) => {
error!("could not read bank oracle {}: {e:?}", keyed_account.key);
@ -70,9 +72,10 @@ pub async fn run(client: &Client, group: Pubkey) -> anyhow::Result<()> {
}
}
if let Some(perp) = perp_opt {
match perp
.oracle_price(&OracleAccountInfos::from_reader(&keyed_account), Some(slot))
{
match perp.oracle_price(
&OracleAccountInfos::from_reader(&keyed_account),
Some((now, slot)),
) {
Ok(p) => price = Some(p),
Err(e) => {
error!("could not read perp oracle {}: {e:?}", keyed_account.key);

View File

@ -74,6 +74,7 @@ mod tests {
price: Default::default(),
deviation: Default::default(),
last_update_slot: 0,
last_update_time: None,
oracle_type: OracleType::Pyth,
},
&OracleConfig {

View File

@ -1,6 +1,6 @@
use std::collections::HashMap;
use anchor_client::ClientError;
use std::collections::HashMap;
use std::time::SystemTime;
use anchor_lang::__private::bytemuck;
@ -669,6 +669,10 @@ impl MangoGroupContext {
.fetch_multiple_accounts(&oracle_keys)
.await?;
let now_slot = account_fetcher.get_slot().await?;
let now_ts = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("system time after epoch start")
.as_secs();
let mut stale_oracles_with_fallbacks = vec![];
for (key, acc) in oracle_accounts {
@ -677,8 +681,10 @@ impl MangoGroupContext {
&OracleAccountInfos::from_reader(&KeyedAccountSharedData::new(key, acc)),
token.decimals,
)?;
let oracle_is_valid = state
.check_confidence_and_maybe_staleness(&token.oracle_config, Some(now_slot));
let oracle_is_valid = state.check_confidence_and_maybe_staleness(
&token.oracle_config,
Some((now_ts, now_slot)),
);
if oracle_is_valid.is_err() && token.fallback_context.key != Pubkey::default() {
stale_oracles_with_fallbacks
.push((token.oracle, token.fallback_context.clone()));

View File

@ -43,7 +43,7 @@ pub async fn new(
n_perps: active_perp_len,
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len * 2,
staleness_slot: None,
now: None,
begin_fallback_oracles: metas.len(),
usdc_oracle_index: metas
.iter()
@ -88,7 +88,7 @@ pub fn new_sync(
n_perps: active_perp_len,
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len * 2,
staleness_slot: None,
now: None,
begin_fallback_oracles: metas.len(),
usdc_oracle_index: None,
sol_oracle_index: None,

View File

@ -61,7 +61,7 @@ pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
pub n_perps: usize,
pub begin_perp: usize,
pub begin_serum3: usize,
pub staleness_slot: Option<u64>,
pub now: Option<(u64, u64)>,
pub begin_fallback_oracles: usize,
pub usdc_oracle_index: Option<usize>,
pub sol_oracle_index: Option<usize>,
@ -74,7 +74,7 @@ pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
pub fn new_fixed_order_account_retriever<'a, 'info>(
ais: &'a [AccountInfo<'info>],
account: &MangoAccountRef,
now_slot: u64,
now: (u64, u64),
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
let active_token_len = account.active_token_positions().count();
@ -83,7 +83,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
ai.load::<Bank>()?;
}
new_fixed_order_account_retriever_inner(ais, account, now_slot, active_token_len)
new_fixed_order_account_retriever_inner(ais, account, now, active_token_len)
}
/// A FixedOrderAccountRetriever with n_banks <= active_token_positions().count(),
@ -94,7 +94,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
pub fn new_fixed_order_account_retriever_with_optional_banks<'a, 'info>(
ais: &'a [AccountInfo<'info>],
account: &MangoAccountRef,
now_slot: u64,
now: (u64, u64),
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
// Scan for the number of banks provided
let mut n_banks = 0;
@ -110,13 +110,13 @@ pub fn new_fixed_order_account_retriever_with_optional_banks<'a, 'info>(
let active_token_len = account.active_token_positions().count();
require_gte!(active_token_len, n_banks);
new_fixed_order_account_retriever_inner(ais, account, now_slot, n_banks)
new_fixed_order_account_retriever_inner(ais, account, now, n_banks)
}
pub fn new_fixed_order_account_retriever_inner<'a, 'info>(
ais: &'a [AccountInfo<'info>],
account: &MangoAccountRef,
now_slot: u64,
now: (u64, u64),
n_banks: usize,
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
let active_serum3_len = account.active_serum3_orders().count();
@ -142,7 +142,7 @@ pub fn new_fixed_order_account_retriever_inner<'a, 'info>(
n_perps: active_perp_len,
begin_perp: n_banks * 2,
begin_serum3: n_banks * 2 + active_perp_len * 2,
staleness_slot: Some(now_slot),
now: Some(now),
begin_fallback_oracles: expected_ais,
usdc_oracle_index,
sol_oracle_index,
@ -190,7 +190,7 @@ impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
fn oracle_price_perp(&self, account_index: usize, perp_market: &PerpMarket) -> Result<I80F48> {
let oracle = &self.ais[account_index];
let oracle_acc_infos = OracleAccountInfos::from_reader(oracle);
perp_market.oracle_price(&oracle_acc_infos, self.staleness_slot)
perp_market.oracle_price(&oracle_acc_infos, self.now)
}
#[inline(always)]
@ -234,7 +234,7 @@ impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
let oracle_index = self.n_banks + bank_account_index;
let oracle_acc_infos = &self.create_oracle_infos(oracle_index, &bank.fallback_oracle);
let oracle_price_result = bank.oracle_price(oracle_acc_infos, self.staleness_slot);
let oracle_price_result = bank.oracle_price(oracle_acc_infos, self.now);
let oracle_price = oracle_price_result.with_context(|| {
format!(
"getting oracle for bank with health account index {} and token index {}, passed account {}",
@ -299,7 +299,7 @@ pub struct ScannedBanksAndOracles<'a, 'info> {
oracles: Vec<AccountInfoRef<'a, 'info>>,
fallback_oracles: Vec<AccountInfoRef<'a, 'info>>,
index_map: HashMap<TokenIndex, usize>,
staleness_slot: Option<u64>,
staleness_slot: Option<(u64, u64)>,
/// index in fallback_oracles
usd_oracle_index: Option<usize>,
/// index in fallback_oracles
@ -432,13 +432,17 @@ fn can_load_as<'a, T: ZeroCopy + Owner>(
impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
pub fn new(ais: &'a [AccountInfo<'info>], group: &Pubkey) -> Result<Self> {
Self::new_with_staleness(ais, group, Some(Clock::get()?.slot))
Self::new_with_staleness(
ais,
group,
Some(Clock::get().map(|c| (c.unix_timestamp as u64, c.slot as u64))?),
)
}
pub fn new_with_staleness(
ais: &'a [AccountInfo<'info>],
group: &Pubkey,
staleness_slot: Option<u64>,
staleness_slot: Option<(u64, u64)>,
) -> Result<Self> {
// find all Bank accounts
let mut token_index_map = HashMap::with_capacity(ais.len() / 2);
@ -755,8 +759,11 @@ mod tests {
perp1.as_account_info(),
oracle2_clone.as_account_info(),
];
let retriever =
new_fixed_order_account_retriever_with_optional_banks(&ais, &account.borrow(), 0)
let retriever = new_fixed_order_account_retriever_with_optional_banks(
&ais,
&account.borrow(),
(0, 0),
)
.unwrap();
assert_eq!(retriever.available_banks(), Ok(vec![10, 20, 30]));
@ -785,8 +792,11 @@ mod tests {
perp1.as_account_info(),
oracle2_clone.as_account_info(),
];
let retriever =
new_fixed_order_account_retriever_with_optional_banks(&ais, &account.borrow(), 0)
let retriever = new_fixed_order_account_retriever_with_optional_banks(
&ais,
&account.borrow(),
(0, 0),
)
.unwrap();
assert_eq!(retriever.available_banks(), Ok(vec![10, 30]));
@ -806,8 +816,11 @@ mod tests {
// skip all
{
let ais = vec![perp1.as_account_info(), oracle2_clone.as_account_info()];
let retriever =
new_fixed_order_account_retriever_with_optional_banks(&ais, &account.borrow(), 0)
let retriever = new_fixed_order_account_retriever_with_optional_banks(
&ais,
&account.borrow(),
(0, 0),
)
.unwrap();
assert_eq!(retriever.available_banks(), Ok(vec![]));

View File

@ -96,7 +96,11 @@ pub fn compute_health_from_fixed_accounts(
ais: &[AccountInfo],
now_ts: u64,
) -> Result<I80F48> {
let retriever = new_fixed_order_account_retriever(ais, account, Clock::get()?.slot)?;
let retriever = new_fixed_order_account_retriever(
ais,
account,
Clock::get().map(|c| (c.unix_timestamp as u64, c.slot as u64))?,
)?;
Ok(new_health_cache(account, &retriever, now_ts)?.health(health_type))
}
@ -2007,7 +2011,7 @@ mod tests {
let retriever = new_fixed_order_account_retriever_with_optional_banks(
&ais,
&account.borrow(),
0,
(0, 0),
)
.unwrap();
new_health_cache_skipping_missing_banks_and_bad_oracles(

View File

@ -41,14 +41,14 @@ pub fn account_buyback_fees_with_mngo(
let mngo_oracle_ref = &AccountInfoRef::borrow(&ctx.accounts.mngo_oracle.as_ref())?;
let mngo_oracle_price = mngo_bank.oracle_price(
&OracleAccountInfos::from_reader(mngo_oracle_ref),
Some(slot),
Some((now_ts, slot)),
)?;
let mngo_asset_price = mngo_oracle_price.min(mngo_bank.stable_price());
let fees_oracle_ref = &AccountInfoRef::borrow(&ctx.accounts.fees_oracle.as_ref())?;
let fees_oracle_price = fees_bank.oracle_price(
&OracleAccountInfos::from_reader(fees_oracle_ref),
Some(slot),
Some((now_ts, slot)),
)?;
let fees_liab_price = fees_oracle_price.max(fees_bank.stable_price());

View File

@ -394,7 +394,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
let retriever = new_fixed_order_account_retriever_with_optional_banks(
health_ais,
&account.borrow(),
now_slot,
(now_ts, now_slot),
)?;
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
@ -523,7 +523,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
let retriever = new_fixed_order_account_retriever_with_optional_banks(
health_ais,
&account.borrow(),
now_slot,
(now_ts, now_slot),
)?;
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),

View File

@ -33,10 +33,13 @@ pub fn perp_force_close_position(ctx: Context<PerpForceClosePosition>) -> Result
.base_position_lots()
.min(account_b_perp_position.base_position_lots().abs())
.max(0);
let now_slot = Clock::get()?.slot;
let clock = Clock::get()?;
let (now_ts, now_slot) = (clock.unix_timestamp as u64, clock.slot);
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let oracle_price =
perp_market.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), Some(now_slot))?;
let oracle_price = perp_market.oracle_price(
&OracleAccountInfos::from_reader(oracle_ref),
Some((now_ts, now_slot)),
)?;
let quote_transfer = I80F48::from(base_transfer * perp_market.base_lot_size) * oracle_price;
account_a_perp_position.record_trade(&mut perp_market, -base_transfer, quote_transfer);

View File

@ -14,8 +14,11 @@ pub fn perp_liq_force_cancel_orders(
let (now_ts, now_slot) = clock_now();
let mut health_cache = {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
let retriever = new_fixed_order_account_retriever(
ctx.remaining_accounts,
&account.borrow(),
(now_ts, now_slot),
)?;
new_health_cache(&account.borrow(), &retriever, now_ts).context("create health cache")?
};

View File

@ -34,14 +34,16 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
perp_market_index = perp_market.perp_market_index;
settle_token_index = perp_market.settle_token_index;
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
perp_oracle_price = perp_market
.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), Some(now_slot))?;
perp_oracle_price = perp_market.oracle_price(
&OracleAccountInfos::from_reader(oracle_ref),
Some((now_ts, now_slot)),
)?;
let settle_bank = ctx.accounts.settle_bank.load()?;
let settle_oracle_ref = &AccountInfoRef::borrow(ctx.accounts.settle_oracle.as_ref())?;
settle_token_oracle_price = settle_bank.oracle_price(
&OracleAccountInfos::from_reader(settle_oracle_ref),
Some(now_slot),
Some((now_ts, now_slot)),
)?;
drop(settle_bank); // could be the same as insurance_bank
@ -51,7 +53,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
// the liqee isn't guaranteed to have an insurance fund token position.
insurance_token_oracle_price = insurance_bank.oracle_price(
&OracleAccountInfos::from_reader(insurance_oracle_ref),
Some(now_slot),
Some((now_ts, now_slot)),
)?;
}

View File

@ -70,7 +70,7 @@ pub fn perp_place_order(
let retriever = new_fixed_order_account_retriever_with_optional_banks(
ctx.remaining_accounts,
&account.borrow(),
now_slot,
(now_ts, now_slot),
)?;
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),

View File

@ -125,8 +125,11 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
// Verify that the result of settling did not violate the health of the account that lost money
let (now_ts, now_slot) = clock_now();
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
let retriever = new_fixed_order_account_retriever(
ctx.remaining_accounts,
&account.borrow(),
(now_ts, now_slot),
)?;
let health = compute_health(&account.borrow(), HealthType::Init, &retriever, now_ts)?;
require!(health >= 0, MangoError::HealthMustBePositive);

View File

@ -15,8 +15,10 @@ pub fn perp_update_funding(ctx: Context<PerpUpdateFunding>) -> Result<()> {
let now_slot = Clock::get()?.slot;
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let oracle_state =
perp_market.oracle_state(&OracleAccountInfos::from_reader(oracle_ref), Some(now_slot))?;
let oracle_state = perp_market.oracle_state(
&OracleAccountInfos::from_reader(oracle_ref),
Some((now_ts, now_slot)),
)?;
perp_market.update_funding_and_stable_price(&book, &oracle_state, now_ts)?;

View File

@ -58,8 +58,11 @@ pub fn serum3_liq_force_cancel_orders(
//
let mut health_cache = {
let mut account = ctx.accounts.account.load_full_mut()?;
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
let retriever = new_fixed_order_account_retriever(
ctx.remaining_accounts,
&account.borrow(),
(now_ts, now_slot),
)?;
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
.context("create health cache")?;

View File

@ -82,7 +82,7 @@ pub fn serum3_place_order(
let retriever = new_fixed_order_account_retriever_with_optional_banks(
ctx.remaining_accounts,
&account.borrow(),
now_slot,
(now_ts, now_slot),
)?;
let mut health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
@ -610,7 +610,7 @@ pub fn apply_settle_changes(
let quote_oracle_ref = &AccountInfoRef::borrow(quote_oracle_ai)?;
let quote_oracle_price = quote_bank.oracle_price(
&OracleAccountInfos::from_reader(quote_oracle_ref),
Some(clock.slot),
Some((now_ts, clock.slot)),
)?;
let quote_asset_price = quote_oracle_price.min(quote_bank.stable_price());
account

View File

@ -188,7 +188,7 @@ pub fn charge_loan_origination_fees(
let ai_ref = &AccountInfoRef::borrow(ai)?;
base_bank.oracle_price(
&OracleAccountInfos::from_reader(ai_ref),
Some(Clock::get()?.slot),
Some(Clock::get().map(|c| (c.unix_timestamp as u64, c.slot as u64))?),
)
})
.transpose()?;
@ -228,7 +228,7 @@ pub fn charge_loan_origination_fees(
let ai_ref = &AccountInfoRef::borrow(ai)?;
quote_bank.oracle_price(
&OracleAccountInfos::from_reader(ai_ref),
Some(Clock::get()?.slot),
Some(Clock::get().map(|c| (c.unix_timestamp as u64, c.slot as u64))?),
)
})
.transpose()?;

View File

@ -68,8 +68,11 @@ where
let time_scaling = I80F48::from(charge_seconds) * inv_seconds_per_day;
let health_cache = {
let retriever =
new_fixed_order_account_retriever(remaining_accounts, &account.borrow(), now_slot)?;
let retriever = new_fixed_order_account_retriever(
remaining_accounts,
&account.borrow(),
(now_ts, now_slot),
)?;
new_health_cache(&account.borrow(), &retriever, now_ts)?
};

View File

@ -124,7 +124,7 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
let retriever = new_fixed_order_account_retriever_with_optional_banks(
remaining_accounts,
&account.borrow(),
now_slot,
(now_ts, now_slot),
)?;
// We only compute health to check if the account leaves the being_liquidated state.
@ -208,12 +208,15 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64, reduce_only: bool)
// Activating a new token position requires that the oracle is in a good state.
// Otherwise users could abuse oracle staleness to delay liquidation.
if !token_position_exists {
let now_slot = Clock::get()?.slot;
let (now_ts, now_slot) =
Clock::get().map(|c| (c.unix_timestamp as u64, c.slot as u64))?;
let bank = ctx.accounts.bank.load()?;
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let oracle_result =
bank.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), Some(now_slot));
let oracle_result = bank.oracle_price(
&OracleAccountInfos::from_reader(oracle_ref),
Some((now_ts, now_slot)),
);
if let Err(e) = oracle_result {
msg!("oracle must be valid when creating a new token position");
return Err(e);

View File

@ -79,7 +79,7 @@ pub fn token_update_index_and_rate(
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let price = some_bank.oracle_price(
&OracleAccountInfos::from_reader(oracle_ref),
Some(clock.slot),
Some((clock.unix_timestamp as u64, clock.slot)),
);
// Early exit if oracle is invalid

View File

@ -31,7 +31,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
let retriever = new_fixed_order_account_retriever_with_optional_banks(
ctx.remaining_accounts,
&account.borrow(),
now_slot,
(now_ts, now_slot),
)?;
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
@ -217,15 +217,15 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
// When borrowing the price has be trustworthy, so we can do a reasonable
// net borrow check.
let slot_opt = Some(Clock::get()?.slot);
let now_opt = Some(Clock::get().map(|c| (c.unix_timestamp as u64, c.slot as u64))?);
unsafe_oracle_state
.check_confidence_and_maybe_staleness(&bank.oracle_config, slot_opt)
.check_confidence_and_maybe_staleness(&bank.oracle_config, now_opt)
.with_context(|| {
oracle_log_context(
bank.name(),
&unsafe_oracle_state,
&bank.oracle_config,
slot_opt,
now_opt,
)
})?;
bank.check_net_borrows(unsafe_oracle_state.price)?;

View File

@ -1184,45 +1184,30 @@ impl Bank {
pub fn oracle_price<T: KeyedAccountReader>(
&self,
oracle_acc_infos: &OracleAccountInfos<T>,
staleness_slot: Option<u64>,
now: Option<(u64, u64)>, // (now_ts, now_slot)
) -> Result<I80F48> {
require_keys_eq!(self.oracle, *oracle_acc_infos.oracle.key());
let primary_state = oracle::oracle_state_unchecked(oracle_acc_infos, self.mint_decimals)?;
let primary_ok =
primary_state.check_confidence_and_maybe_staleness(&self.oracle_config, staleness_slot);
primary_state.check_confidence_and_maybe_staleness(&self.oracle_config, now);
if primary_ok.is_oracle_error() && oracle_acc_infos.fallback_opt.is_some() {
let fallback_oracle_acc = oracle_acc_infos.fallback_opt.unwrap();
require_keys_eq!(self.fallback_oracle, *fallback_oracle_acc.key());
let fallback_state =
oracle::fallback_oracle_state_unchecked(&oracle_acc_infos, self.mint_decimals)?;
let fallback_ok = fallback_state
.check_confidence_and_maybe_staleness(&self.oracle_config, staleness_slot);
let fallback_ok =
fallback_state.check_confidence_and_maybe_staleness(&self.oracle_config, now);
fallback_ok.with_context(|| {
format!(
"{} {}",
oracle_log_context(
self.name(),
&primary_state,
&self.oracle_config,
staleness_slot
),
oracle_log_context(
self.name(),
&fallback_state,
&self.oracle_config,
staleness_slot
)
oracle_log_context(self.name(), &primary_state, &self.oracle_config, now),
oracle_log_context(self.name(), &fallback_state, &self.oracle_config, now)
)
})?;
Ok(fallback_state.price)
} else {
primary_ok.with_context(|| {
oracle_log_context(
self.name(),
&primary_state,
&self.oracle_config,
staleness_slot,
)
oracle_log_context(self.name(), &primary_state, &self.oracle_config, now)
})?;
Ok(primary_state.price)
}

View File

@ -1,5 +1,6 @@
use std::mem::size_of;
use super::{load_raydium_pool_state, orca_mainnet_whirlpool, raydium_mainnet};
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::state::load_orca_pool_state;
@ -13,8 +14,6 @@ use switchboard_on_demand::PullFeedAccountData;
use switchboard_program::FastRoundResultAccountData;
use switchboard_v2::AggregatorAccountData;
use super::{load_raydium_pool_state, orca_mainnet_whirlpool, raydium_mainnet};
const DECIMAL_CONSTANT_ZERO_INDEX: i8 = 12;
const DECIMAL_CONSTANTS: [I80F48; 25] = [
I80F48::from_bits((1 << 48) / 10i128.pow(12u32)),
@ -134,6 +133,7 @@ pub struct OracleState {
pub price: I80F48,
pub deviation: I80F48,
pub last_update_slot: u64,
pub last_update_time: Option<u64>,
pub oracle_type: OracleType,
}
@ -142,23 +142,41 @@ impl OracleState {
pub fn check_confidence_and_maybe_staleness(
&self,
config: &OracleConfig,
staleness_slot: Option<u64>,
now: Option<(u64, u64)>, // (now_ts, now_slot)
) -> Result<()> {
if let Some(now_slot) = staleness_slot {
self.check_staleness(config, now_slot)?;
if let Some((now_ts, now_slot)) = now {
self.check_staleness(config, now_slot, now_ts)?;
}
self.check_confidence(config)
}
pub fn check_staleness(&self, config: &OracleConfig, now_slot: u64) -> Result<()> {
if config.max_staleness_slots >= 0
&& self
pub fn check_staleness(&self, config: &OracleConfig, now_slot: u64, now_ts: u64) -> Result<()> {
if config.max_staleness_slots < 0 {
return Ok(());
}
if self
.last_update_slot
.saturating_add(config.max_staleness_slots as u64)
< now_slot
{
return Err(MangoError::OracleStale.into());
}
if self.last_update_time.is_some() {
let current_time_in_msecs = now_ts * 1000;
let last_update_time_in_msecs = self.last_update_time.unwrap() * 1000;
let max_acceptable_update_age_in_ms = (config.max_staleness_slots as u64) * 450;
let oldest_acceptable_time =
current_time_in_msecs.saturating_sub(max_acceptable_update_age_in_ms);
if last_update_time_in_msecs < oldest_acceptable_time {
msg!("Oracle stale (using time fallback method: current time: {} vs published time: {})", current_time_in_msecs, last_update_time_in_msecs);
return Err(MangoError::OracleStale.into());
}
}
Ok(())
}
@ -291,6 +309,7 @@ pub fn get_pyth_state(
last_update_slot,
deviation,
oracle_type: OracleType::Pyth,
last_update_time: None,
})
}
@ -313,12 +332,15 @@ pub fn get_pyth_on_demand_state(
let deviation = I80F48::from_num(price_account.price_message.conf) * decimal_adj;
let last_update_slot = price_account.posted_slot;
let price_timestamp = price_account.price_message.publish_time;
require_gte!(price, 0);
Ok(OracleState {
price,
last_update_slot,
deviation,
oracle_type: OracleType::PythV2,
last_update_time: Some(price_timestamp as u64),
})
}
@ -398,6 +420,7 @@ fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
last_update_slot,
deviation,
oracle_type: OracleType::Stub,
last_update_time: None,
}
}
OracleType::Pyth => get_pyth_state(oracle_info, base_decimals)?,
@ -430,6 +453,7 @@ fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
last_update_slot,
deviation,
oracle_type: OracleType::SwitchboardV2,
last_update_time: None,
}
}
OracleType::SwitchboardV1 => {
@ -450,6 +474,7 @@ fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
last_update_slot,
deviation,
oracle_type: OracleType::SwitchboardV1,
last_update_time: None,
}
}
OracleType::SwitchboardOnDemand => {
@ -479,6 +504,7 @@ fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
last_update_slot,
deviation,
oracle_type: OracleType::SwitchboardOnDemand,
last_update_time: None,
}
}
OracleType::OrcaCLMM => {
@ -491,6 +517,7 @@ fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
last_update_slot: quote_oracle_state.last_update_slot,
deviation: quote_oracle_state.deviation,
oracle_type: OracleType::OrcaCLMM,
last_update_time: None,
}
}
OracleType::RaydiumCLMM => {
@ -503,6 +530,7 @@ fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
last_update_slot: quote_oracle_state.last_update_slot,
deviation: quote_oracle_state.deviation,
oracle_type: OracleType::RaydiumCLMM,
last_update_time: None,
}
}
})
@ -512,15 +540,15 @@ pub fn oracle_log_context(
name: &str,
state: &OracleState,
oracle_config: &OracleConfig,
staleness_slot: Option<u64>,
now: Option<(u64, u64)>,
) -> String {
format!(
"name: {}, price: {}, deviation: {}, last_update_slot: {}, now_slot: {}, conf_filter: {:#?}",
"name: {}, price: {}, deviation: {}, last_update_slot: {}, now: {:?}, conf_filter: {:#?}",
name,
state.price.to_num::<f64>(),
state.deviation.to_num::<f64>(),
state.last_update_slot,
staleness_slot.unwrap_or_else(|| u64::MAX),
now.unwrap_or_else(|| (u64::MAX, u64::MAX)),
oracle_config.conf_filter.to_num::<f32>(),
)
}
@ -861,4 +889,40 @@ mod tests {
}
Ok(())
}
#[test]
fn use_time_for_max_staleness_check() {
let fixtures = vec![
(100_000, 100_000, false),
(100_000, 50_000, true),
(100_000, 150_000, false),
(100_000, 100_000 - 44, false),
(100_000, 100_000 - 46, true),
(100_000, 100_000 + 45, false),
(100_000, 100_000 + 300, false),
];
let config = OracleConfig {
conf_filter: Default::default(),
max_staleness_slots: 100,
reserved: [0; 72],
};
for (now_ts, publish_ts, expect_error) in fixtures {
let now_slot = 0;
let state = OracleState {
price: Default::default(),
deviation: Default::default(),
last_update_slot: now_slot,
last_update_time: Some(publish_ts),
oracle_type: OracleType::Pyth,
};
println!("test case: {}, {} => {}", now_ts, publish_ts, expect_error);
assert_eq!(
expect_error,
state.check_staleness(&config, now_slot, now_ts).is_err()
);
}
}
}

View File

@ -277,23 +277,21 @@ impl PerpMarket {
pub fn oracle_price<T: KeyedAccountReader>(
&self,
oracle_acc_infos: &OracleAccountInfos<T>,
staleness_slot: Option<u64>,
now: Option<(u64, u64)>,
) -> Result<I80F48> {
Ok(self.oracle_state(oracle_acc_infos, staleness_slot)?.price)
Ok(self.oracle_state(oracle_acc_infos, now)?.price)
}
pub fn oracle_state<T: KeyedAccountReader>(
&self,
oracle_acc_infos: &OracleAccountInfos<T>,
staleness_slot: Option<u64>,
now: Option<(u64, u64)>,
) -> Result<OracleState> {
require_keys_eq!(self.oracle, *oracle_acc_infos.oracle.key());
let state = oracle::oracle_state_unchecked(oracle_acc_infos, self.base_decimals)?;
state
.check_confidence_and_maybe_staleness(&self.oracle_config, staleness_slot)
.with_context(|| {
oracle_log_context(self.name(), &state, &self.oracle_config, staleness_slot)
})?;
.check_confidence_and_maybe_staleness(&self.oracle_config, now)
.with_context(|| oracle_log_context(self.name(), &state, &self.oracle_config, now))?;
Ok(state)
}

View File

@ -335,7 +335,7 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr
println!("average success increase: {avg_success_increase}");
println!("average failure increase: {avg_failure_increase}");
assert!(avg_success_increase < 2_050);
assert!(avg_failure_increase < 19_500);
assert!(avg_failure_increase < 19_900);
Ok(())
}