Skip to content

Commit

Permalink
Pyth v2: compare program clock with last price publication time for o…
Browse files Browse the repository at this point in the history
…racle staleness check (#983)

Pyth v2: compare program clock with last price publication time for oracle staleness check
  • Loading branch information
farnyser authored Jul 22, 2024
1 parent 6d9f9b6 commit 0a55f46
Show file tree
Hide file tree
Showing 25 changed files with 212 additions and 116 deletions.
19 changes: 11 additions & 8 deletions bin/cli/src/test_oracles.rs
Original file line number Diff line number Diff line change
@@ -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<()> {
Expand Down Expand Up @@ -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()) {
Expand All @@ -60,19 +61,21 @@ 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);
}
}
}
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);
Expand Down
1 change: 1 addition & 0 deletions bin/liquidator/src/unwrappable_oracle_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 10 additions & 4 deletions lib/client/src/context.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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 {
Expand All @@ -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()));
Expand Down
4 changes: 2 additions & 2 deletions lib/client/src/health_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
55 changes: 34 additions & 21 deletions programs/mango-v4/src/health/account_retriever.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand All @@ -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();

Expand All @@ -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(),
Expand All @@ -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;
Expand All @@ -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();
Expand All @@ -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,
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {}",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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());
Expand Down
8 changes: 6 additions & 2 deletions programs/mango-v4/src/health/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
4 changes: 2 additions & 2 deletions programs/mango-v4/src/instructions/flash_loan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")?
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)),
)?;
}

Expand Down
2 changes: 1 addition & 1 deletion programs/mango-v4/src/instructions/perp_place_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Loading

0 comments on commit 0a55f46

Please sign in to comment.