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:
parent
6d9f9b664c
commit
0a55f46efb
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,9 +759,12 @@ 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)
|
||||
.unwrap();
|
||||
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]));
|
||||
|
||||
let (i, bank) = retriever.bank(&group, 0, 10).unwrap();
|
||||
|
@ -785,9 +792,12 @@ 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)
|
||||
.unwrap();
|
||||
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]));
|
||||
|
||||
let (i, bank) = retriever.bank(&group, 0, 10).unwrap();
|
||||
|
@ -806,9 +816,12 @@ 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)
|
||||
.unwrap();
|
||||
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
||||
&ais,
|
||||
&account.borrow(),
|
||||
(0, 0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(retriever.available_banks(), Ok(vec![]));
|
||||
|
||||
assert!(retriever.bank(&group, 0, 10).is_err());
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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")?
|
||||
};
|
||||
|
||||
|
|
|
@ -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)),
|
||||
)?;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)?;
|
||||
|
||||
|
|
|
@ -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")?;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()?;
|
||||
|
|
|
@ -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)?
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
.last_update_slot
|
||||
.saturating_add(config.max_staleness_slots as u64)
|
||||
< now_slot
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue