diff --git a/bin/cli/src/test_oracles.rs b/bin/cli/src/test_oracles.rs index 5b07bed05..d2a5afe2b 100644 --- a/bin/cli/src/test_oracles.rs +++ b/bin/cli/src/test_oracles.rs @@ -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); diff --git a/bin/liquidator/src/unwrappable_oracle_error.rs b/bin/liquidator/src/unwrappable_oracle_error.rs index f27340eea..0ab05fc38 100644 --- a/bin/liquidator/src/unwrappable_oracle_error.rs +++ b/bin/liquidator/src/unwrappable_oracle_error.rs @@ -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 { diff --git a/lib/client/src/context.rs b/lib/client/src/context.rs index 185555a04..81ee7bf88 100644 --- a/lib/client/src/context.rs +++ b/lib/client/src/context.rs @@ -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())); diff --git a/lib/client/src/health_cache.rs b/lib/client/src/health_cache.rs index 47a176f54..8f666c0af 100644 --- a/lib/client/src/health_cache.rs +++ b/lib/client/src/health_cache.rs @@ -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, diff --git a/programs/mango-v4/src/health/account_retriever.rs b/programs/mango-v4/src/health/account_retriever.rs index c88764def..763111218 100644 --- a/programs/mango-v4/src/health/account_retriever.rs +++ b/programs/mango-v4/src/health/account_retriever.rs @@ -61,7 +61,7 @@ pub struct FixedOrderAccountRetriever { pub n_perps: usize, pub begin_perp: usize, pub begin_serum3: usize, - pub staleness_slot: Option, + pub now: Option<(u64, u64)>, pub begin_fallback_oracles: usize, pub usdc_oracle_index: Option, pub sol_oracle_index: Option, @@ -74,7 +74,7 @@ pub struct FixedOrderAccountRetriever { pub fn new_fixed_order_account_retriever<'a, 'info>( ais: &'a [AccountInfo<'info>], account: &MangoAccountRef, - now_slot: u64, + now: (u64, u64), ) -> Result>> { let active_token_len = account.active_token_positions().count(); @@ -83,7 +83,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>( ai.load::()?; } - 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>> { // 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>> { 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 FixedOrderAccountRetriever { fn oracle_price_perp(&self, account_index: usize, perp_market: &PerpMarket) -> Result { 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 AccountRetriever for FixedOrderAccountRetriever { 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>, fallback_oracles: Vec>, index_map: HashMap, - staleness_slot: Option, + staleness_slot: Option<(u64, u64)>, /// index in fallback_oracles usd_oracle_index: Option, /// 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::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, + staleness_slot: Option<(u64, u64)>, ) -> Result { // 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()); diff --git a/programs/mango-v4/src/health/cache.rs b/programs/mango-v4/src/health/cache.rs index 21f8cf289..5c92525b8 100644 --- a/programs/mango-v4/src/health/cache.rs +++ b/programs/mango-v4/src/health/cache.rs @@ -96,7 +96,11 @@ pub fn compute_health_from_fixed_accounts( ais: &[AccountInfo], now_ts: u64, ) -> Result { - 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( diff --git a/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs b/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs index f876fd470..75c7d24cd 100644 --- a/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs +++ b/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs @@ -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()); diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index 09defac67..1d6705999 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -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(), diff --git a/programs/mango-v4/src/instructions/perp_force_close_position.rs b/programs/mango-v4/src/instructions/perp_force_close_position.rs index 494136dc0..1f4aa13bb 100644 --- a/programs/mango-v4/src/instructions/perp_force_close_position.rs +++ b/programs/mango-v4/src/instructions/perp_force_close_position.rs @@ -33,10 +33,13 @@ pub fn perp_force_close_position(ctx: Context) -> 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); diff --git a/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs index 37bfdc6d5..8d4637e36 100644 --- a/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs @@ -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")? }; diff --git a/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_bankruptcy.rs b/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_bankruptcy.rs index 49b8416f9..604310a33 100644 --- a/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_bankruptcy.rs @@ -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)), )?; } diff --git a/programs/mango-v4/src/instructions/perp_place_order.rs b/programs/mango-v4/src/instructions/perp_place_order.rs index 36e3b9579..6d96687d7 100644 --- a/programs/mango-v4/src/instructions/perp_place_order.rs +++ b/programs/mango-v4/src/instructions/perp_place_order.rs @@ -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(), diff --git a/programs/mango-v4/src/instructions/perp_settle_fees.rs b/programs/mango-v4/src/instructions/perp_settle_fees.rs index 627f484c5..87449f55d 100644 --- a/programs/mango-v4/src/instructions/perp_settle_fees.rs +++ b/programs/mango-v4/src/instructions/perp_settle_fees.rs @@ -125,8 +125,11 @@ pub fn perp_settle_fees(ctx: Context, 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); diff --git a/programs/mango-v4/src/instructions/perp_update_funding.rs b/programs/mango-v4/src/instructions/perp_update_funding.rs index 890864c22..a83eacba0 100644 --- a/programs/mango-v4/src/instructions/perp_update_funding.rs +++ b/programs/mango-v4/src/instructions/perp_update_funding.rs @@ -15,8 +15,10 @@ pub fn perp_update_funding(ctx: Context) -> 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)?; diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index 5a508b183..269698c77 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -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")?; diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 80d43fb18..b51daf73d 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -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 diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index 761b43d72..2689895fc 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -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()?; diff --git a/programs/mango-v4/src/instructions/token_charge_collateral_fees.rs b/programs/mango-v4/src/instructions/token_charge_collateral_fees.rs index 25c0f8be8..acf435b05 100644 --- a/programs/mango-v4/src/instructions/token_charge_collateral_fees.rs +++ b/programs/mango-v4/src/instructions/token_charge_collateral_fees.rs @@ -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)? }; diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index 1155cd7d9..8acf4d4d9 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -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, 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); diff --git a/programs/mango-v4/src/instructions/token_update_index_and_rate.rs b/programs/mango-v4/src/instructions/token_update_index_and_rate.rs index 07c17f834..c2ba89b32 100644 --- a/programs/mango-v4/src/instructions/token_update_index_and_rate.rs +++ b/programs/mango-v4/src/instructions/token_update_index_and_rate.rs @@ -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 diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index cf0dfbc36..eb3161c9e 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -31,7 +31,7 @@ pub fn token_withdraw(ctx: Context, 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, 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)?; diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index cfc6aea3d..40d889bbb 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -1184,45 +1184,30 @@ impl Bank { pub fn oracle_price( &self, oracle_acc_infos: &OracleAccountInfos, - staleness_slot: Option, + now: Option<(u64, u64)>, // (now_ts, now_slot) ) -> Result { 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) } diff --git a/programs/mango-v4/src/state/oracle.rs b/programs/mango-v4/src/state/oracle.rs index e434efb55..c7e24c772 100644 --- a/programs/mango-v4/src/state/oracle.rs +++ b/programs/mango-v4/src/state/oracle.rs @@ -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, pub oracle_type: OracleType, } @@ -142,23 +142,41 @@ impl OracleState { pub fn check_confidence_and_maybe_staleness( &self, config: &OracleConfig, - staleness_slot: Option, + 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( 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( last_update_slot, deviation, oracle_type: OracleType::SwitchboardV2, + last_update_time: None, } } OracleType::SwitchboardV1 => { @@ -450,6 +474,7 @@ fn oracle_state_unchecked_inner( last_update_slot, deviation, oracle_type: OracleType::SwitchboardV1, + last_update_time: None, } } OracleType::SwitchboardOnDemand => { @@ -479,6 +504,7 @@ fn oracle_state_unchecked_inner( last_update_slot, deviation, oracle_type: OracleType::SwitchboardOnDemand, + last_update_time: None, } } OracleType::OrcaCLMM => { @@ -491,6 +517,7 @@ fn oracle_state_unchecked_inner( 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( 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, + 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::(), state.deviation.to_num::(), 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::(), ) } @@ -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() + ); + } + } } diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 2b1c795a3..0c295049e 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -277,23 +277,21 @@ impl PerpMarket { pub fn oracle_price( &self, oracle_acc_infos: &OracleAccountInfos, - staleness_slot: Option, + now: Option<(u64, u64)>, ) -> Result { - Ok(self.oracle_state(oracle_acc_infos, staleness_slot)?.price) + Ok(self.oracle_state(oracle_acc_infos, now)?.price) } pub fn oracle_state( &self, oracle_acc_infos: &OracleAccountInfos, - staleness_slot: Option, + now: Option<(u64, u64)>, ) -> Result { 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) } diff --git a/programs/mango-v4/tests/cases/test_health_compute.rs b/programs/mango-v4/tests/cases/test_health_compute.rs index f72eaba49..297ed95a1 100644 --- a/programs/mango-v4/tests/cases/test_health_compute.rs +++ b/programs/mango-v4/tests/cases/test_health_compute.rs @@ -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(()) }