diff --git a/bin/service-mango-orderbook/src/orderbook_filter.rs b/bin/service-mango-orderbook/src/orderbook_filter.rs index 586879a5e..abbdc0cd8 100644 --- a/bin/service-mango-orderbook/src/orderbook_filter.rs +++ b/bin/service-mango-orderbook/src/orderbook_filter.rs @@ -373,7 +373,7 @@ pub async fn init( { if unchecked_oracle_state .check_confidence_and_maybe_staleness( - &oracle_pk, + &mkt.1.name, &oracle_config.to_oracle_config(), None, // force this to always return a price no matter how stale ) diff --git a/lib/client/src/health_cache.rs b/lib/client/src/health_cache.rs index 160ce0e8b..210609321 100644 --- a/lib/client/src/health_cache.rs +++ b/lib/client/src/health_cache.rs @@ -34,6 +34,7 @@ pub async fn new( begin_perp: active_token_len * 2, begin_serum3: active_token_len * 2 + active_perp_len * 2, staleness_slot: None, + begin_fallback_oracles: metas.len(), // TODO: add support for fallback oracle accounts }; let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); mango_v4::health::new_health_cache(&account.borrow(), &retriever, now_ts) @@ -67,6 +68,7 @@ pub fn new_sync( begin_perp: active_token_len * 2, begin_serum3: active_token_len * 2 + active_perp_len * 2, staleness_slot: None, + begin_fallback_oracles: metas.len(), // TODO: add support for fallback oracle accounts }; let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); mango_v4::health::new_health_cache(&account.borrow(), &retriever, now_ts) diff --git a/programs/mango-v4/src/accounts_ix/stub_oracle_create.rs b/programs/mango-v4/src/accounts_ix/stub_oracle_create.rs index 8787c74d7..8b3cfd2d2 100644 --- a/programs/mango-v4/src/accounts_ix/stub_oracle_create.rs +++ b/programs/mango-v4/src/accounts_ix/stub_oracle_create.rs @@ -12,8 +12,6 @@ pub struct StubOracleCreate<'info> { #[account( init, - seeds = [b"StubOracle".as_ref(), group.key().as_ref(), mint.key().as_ref()], - bump, payer = payer, space = 8 + std::mem::size_of::(), )] diff --git a/programs/mango-v4/src/accounts_ix/token_edit.rs b/programs/mango-v4/src/accounts_ix/token_edit.rs index 5d20d59ca..c11638edc 100644 --- a/programs/mango-v4/src/accounts_ix/token_edit.rs +++ b/programs/mango-v4/src/accounts_ix/token_edit.rs @@ -21,4 +21,9 @@ pub struct TokenEdit<'info> { /// /// CHECK: The oracle can be one of several different account types pub oracle: UncheckedAccount<'info>, + + /// The fallback oracle account is optional and only used when set_fallback_oracle is true. + /// + /// CHECK: The fallback oracle can be one of several different account types + pub fallback_oracle: UncheckedAccount<'info>, } diff --git a/programs/mango-v4/src/accounts_ix/token_register.rs b/programs/mango-v4/src/accounts_ix/token_register.rs index 6c51a7d13..1f91129e8 100644 --- a/programs/mango-v4/src/accounts_ix/token_register.rs +++ b/programs/mango-v4/src/accounts_ix/token_register.rs @@ -51,6 +51,9 @@ pub struct TokenRegister<'info> { /// CHECK: The oracle can be one of several different account types pub oracle: UncheckedAccount<'info>, + /// CHECK: The oracle can be one of several different account types + pub fallback_oracle: UncheckedAccount<'info>, + #[account(mut)] pub payer: Signer<'info>, diff --git a/programs/mango-v4/src/accounts_ix/token_register_trustless.rs b/programs/mango-v4/src/accounts_ix/token_register_trustless.rs index 0bd6130cf..755f02a14 100644 --- a/programs/mango-v4/src/accounts_ix/token_register_trustless.rs +++ b/programs/mango-v4/src/accounts_ix/token_register_trustless.rs @@ -51,6 +51,9 @@ pub struct TokenRegisterTrustless<'info> { /// CHECK: The oracle can be one of several different account types pub oracle: UncheckedAccount<'info>, + /// CHECK: The oracle can be one of several different account types + pub fallback_oracle: UncheckedAccount<'info>, + #[account(mut)] pub payer: Signer<'info>, diff --git a/programs/mango-v4/src/health/account_retriever.rs b/programs/mango-v4/src/health/account_retriever.rs index 7ffd66919..73b7dde2a 100644 --- a/programs/mango-v4/src/health/account_retriever.rs +++ b/programs/mango-v4/src/health/account_retriever.rs @@ -47,6 +47,7 @@ pub trait AccountRetriever { /// 3. PerpMarket accounts, in the order of account.perps.iter_active_accounts() /// 4. PerpMarket oracle accounts, in the order of the perp market accounts /// 5. serum3 OpenOrders accounts, in the order of account.serum3.iter_active() +/// 6. fallback oracle accounts, order and existence of accounts is not guaranteed pub struct FixedOrderAccountRetriever { pub ais: Vec, pub n_banks: usize, @@ -54,6 +55,7 @@ pub struct FixedOrderAccountRetriever { pub begin_perp: usize, pub begin_serum3: usize, pub staleness_slot: Option, + pub begin_fallback_oracles: usize, } pub fn new_fixed_order_account_retriever<'a, 'info>( @@ -66,7 +68,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>( let expected_ais = active_token_len * 2 // banks + oracles + active_perp_len * 2 // PerpMarkets + Oracles + active_serum3_len; // open_orders - require_msg_typed!(ais.len() == expected_ais, MangoError::InvalidHealthAccountCount, + require_msg_typed!(ais.len() >= expected_ais, MangoError::InvalidHealthAccountCount, "received {} accounts but expected {} ({} banks, {} bank oracles, {} perp markets, {} perp oracles, {} serum3 oos)", ais.len(), expected_ais, active_token_len, active_token_len, active_perp_len, active_perp_len, active_serum3_len @@ -79,6 +81,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>( begin_perp: active_token_len * 2, begin_serum3: active_token_len * 2 + active_perp_len * 2, staleness_slot: Some(Clock::get()?.slot), + begin_fallback_oracles: expected_ais, }) } @@ -103,11 +106,6 @@ impl FixedOrderAccountRetriever { Ok(market) } - fn oracle_price_bank(&self, account_index: usize, bank: &Bank) -> Result { - let oracle = &self.ais[account_index]; - bank.oracle_price(oracle, self.staleness_slot) - } - fn oracle_price_perp(&self, account_index: usize, perp_market: &PerpMarket) -> Result { let oracle = &self.ais[account_index]; perp_market.oracle_price(oracle, self.staleness_slot) @@ -134,7 +132,13 @@ impl AccountRetriever for FixedOrderAccountRetriever { })?; let oracle_index = self.n_banks + active_token_position_index; - let oracle_price = self.oracle_price_bank(oracle_index, bank).with_context(|| { + let oracle = &self.ais[oracle_index]; + let fallback_opt = self.ais[self.begin_fallback_oracles..] + .iter() + .find(|ai| ai.key() == &bank.fallback_oracle); + let oracle_price_result = + bank.oracle_price_with_fallback(oracle, fallback_opt, self.staleness_slot); + let oracle_price = oracle_price_result.with_context(|| { format!( "getting oracle for bank with health account index {} and token index {}, passed account {}", bank_account_index, @@ -196,6 +200,7 @@ impl AccountRetriever for FixedOrderAccountRetriever { pub struct ScannedBanksAndOracles<'a, 'info> { banks: Vec>, oracles: Vec>, + fallback_oracles: Vec>, index_map: HashMap, staleness_slot: Option, } @@ -222,7 +227,10 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> { let index = self.bank_index(token_index1)?; let bank = self.banks[index].load_mut_fully_unchecked::()?; let oracle = &self.oracles[index]; - let price = bank.oracle_price(oracle, self.staleness_slot)?; + let fallback_oracle_opt = + fetch_fallback_oracle(&self.fallback_oracles, &bank.fallback_oracle); + let price = + bank.oracle_price_with_fallback(oracle, fallback_oracle_opt, self.staleness_slot)?; return Ok((bank, price, None)); } let index1 = self.bank_index(token_index1)?; @@ -240,8 +248,14 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> { let bank2 = second_bank_part[second - (first + 1)].load_mut_fully_unchecked::()?; let oracle1 = &self.oracles[first]; let oracle2 = &self.oracles[second]; - let price1 = bank1.oracle_price(oracle1, self.staleness_slot)?; - let price2 = bank2.oracle_price(oracle2, self.staleness_slot)?; + let fallback_oracle_opt1 = + fetch_fallback_oracle(&self.fallback_oracles, &bank1.fallback_oracle); + let fallback_oracle_opt2 = + fetch_fallback_oracle(&self.fallback_oracles, &bank2.fallback_oracle); + let price1 = + bank1.oracle_price_with_fallback(oracle1, fallback_oracle_opt1, self.staleness_slot)?; + let price2 = + bank2.oracle_price_with_fallback(oracle2, fallback_oracle_opt2, self.staleness_slot)?; if swap { Ok((bank2, price2, Some((bank1, price1)))) } else { @@ -254,7 +268,11 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> { // The account was already loaded successfully during construction let bank = self.banks[index].load_fully_unchecked::()?; let oracle = &self.oracles[index]; - let price = bank.oracle_price(oracle, self.staleness_slot)?; + let fallback_oracle_opt = + fetch_fallback_oracle(&self.fallback_oracles, &bank.fallback_oracle); + let price = + bank.oracle_price_with_fallback(oracle, fallback_oracle_opt, self.staleness_slot)?; + Ok((bank, price)) } } @@ -265,6 +283,7 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> { /// - an unknown number of PerpMarket accounts /// - the same number of oracles in the same order as the perp markets /// - an unknown number of serum3 OpenOrders accounts +/// - an unknown number of fallback oracle accounts /// and retrieves accounts needed for the health computation by doing a linear /// scan for each request. pub struct ScanningAccountRetriever<'a, 'info> { @@ -350,17 +369,26 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> { let n_perps = perp_index_map.len(); let perp_oracles_start = perps_start + n_perps; let serum3_start = perp_oracles_start + n_perps; + let n_serum3 = ais[serum3_start..] + .iter() + .take_while(|x| { + x.data_len() == std::mem::size_of::() + 12 + && serum3_cpi::has_serum_header(&x.data.borrow()) + }) + .count(); + let fallback_oracles_start = serum3_start + n_serum3; Ok(Self { banks_and_oracles: ScannedBanksAndOracles { banks: AccountInfoRefMut::borrow_slice(&ais[..n_banks])?, oracles: AccountInfoRef::borrow_slice(&ais[n_banks..perps_start])?, + fallback_oracles: AccountInfoRef::borrow_slice(&ais[fallback_oracles_start..])?, index_map: token_index_map, staleness_slot, }, perp_markets: AccountInfoRef::borrow_slice(&ais[perps_start..perp_oracles_start])?, perp_oracles: AccountInfoRef::borrow_slice(&ais[perp_oracles_start..serum3_start])?, - serum3_oos: AccountInfoRef::borrow_slice(&ais[serum3_start..])?, + serum3_oos: AccountInfoRef::borrow_slice(&ais[serum3_start..fallback_oracles_start])?, perp_index_map, }) } @@ -437,6 +465,14 @@ impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> { } } +#[inline(always)] +fn fetch_fallback_oracle<'a, 'info>( + fallback_oracles: &'a Vec>, + fallback_key: &Pubkey, +) -> Option<&'a AccountInfoRef<'a, 'info>> { + fallback_oracles.iter().find(|ai| ai.key() == fallback_key) +} + #[cfg(test)] mod tests { use super::super::test::*; diff --git a/programs/mango-v4/src/instructions/token_edit.rs b/programs/mango-v4/src/instructions/token_edit.rs index abab45d2d..fe6c294e5 100644 --- a/programs/mango-v4/src/instructions/token_edit.rs +++ b/programs/mango-v4/src/instructions/token_edit.rs @@ -8,7 +8,7 @@ use crate::error::MangoError; use crate::state::*; use crate::accounts_ix::*; -use crate::logs::{emit_stack, TokenMetaDataLog}; +use crate::logs::{emit_stack, TokenMetaDataLogV2}; use crate::util::fill_from_str; #[allow(unused_variables)] @@ -49,6 +49,7 @@ pub fn token_edit( maint_weight_shift_asset_target_opt: Option, maint_weight_shift_liab_target_opt: Option, maint_weight_shift_abort: bool, + set_fallback_oracle: bool, ) -> Result<()> { let group = ctx.accounts.group.load()?; @@ -76,6 +77,16 @@ pub fn token_edit( mint_info.oracle = oracle; require_group_admin = true; } + if set_fallback_oracle { + msg!( + "Fallback oracle old {:?}, new {:?}", + bank.fallback_oracle, + ctx.accounts.fallback_oracle.key() + ); + bank.fallback_oracle = ctx.accounts.fallback_oracle.key(); + mint_info.fallback_oracle = ctx.accounts.fallback_oracle.key(); + require_group_admin = true; + } if reset_stable_price { msg!("Stable price reset"); require_keys_eq!(bank.oracle, ctx.accounts.oracle.key()); @@ -456,12 +467,13 @@ pub fn token_edit( let bank = ctx.remaining_accounts.first().unwrap().load_mut::()?; bank.verify()?; - emit_stack(TokenMetaDataLog { + emit_stack(TokenMetaDataLogV2 { mango_group: ctx.accounts.group.key(), mint: mint_info.mint.key(), token_index: bank.token_index, mint_decimals: bank.mint_decimals, oracle: mint_info.oracle.key(), + fallback_oracle: ctx.accounts.fallback_oracle.key(), mint_info: ctx.accounts.mint_info.key(), }); diff --git a/programs/mango-v4/src/instructions/token_register.rs b/programs/mango-v4/src/instructions/token_register.rs index 9ccda45ab..2af080bf6 100644 --- a/programs/mango-v4/src/instructions/token_register.rs +++ b/programs/mango-v4/src/instructions/token_register.rs @@ -6,7 +6,7 @@ use crate::error::*; use crate::state::*; use crate::util::fill_from_str; -use crate::logs::{emit_stack, TokenMetaDataLog}; +use crate::logs::{emit_stack, TokenMetaDataLogV2}; pub const INDEX_START: I80F48 = I80F48::from_bits(1_000_000 * I80F48::ONE.to_bits()); @@ -119,7 +119,8 @@ pub fn token_register( maint_weight_shift_duration_inv: I80F48::ZERO, maint_weight_shift_asset_target: I80F48::ZERO, maint_weight_shift_liab_target: I80F48::ZERO, - reserved: [0; 2008], + fallback_oracle: ctx.accounts.fallback_oracle.key(), + reserved: [0; 1976], }; if let Ok(oracle_price) = @@ -143,19 +144,21 @@ pub fn token_register( banks: Default::default(), vaults: Default::default(), oracle: ctx.accounts.oracle.key(), + fallback_oracle: ctx.accounts.fallback_oracle.key(), registration_time: Clock::get()?.unix_timestamp.try_into().unwrap(), - reserved: [0; 2560], + reserved: [0; 2528], }; mint_info.banks[0] = ctx.accounts.bank.key(); mint_info.vaults[0] = ctx.accounts.vault.key(); - emit_stack(TokenMetaDataLog { + emit_stack(TokenMetaDataLogV2 { mango_group: ctx.accounts.group.key(), mint: ctx.accounts.mint.key(), token_index, mint_decimals: ctx.accounts.mint.decimals, oracle: ctx.accounts.oracle.key(), + fallback_oracle: ctx.accounts.fallback_oracle.key(), mint_info: ctx.accounts.mint_info.key(), }); diff --git a/programs/mango-v4/src/instructions/token_register_trustless.rs b/programs/mango-v4/src/instructions/token_register_trustless.rs index bb412ed34..7ff966c8e 100644 --- a/programs/mango-v4/src/instructions/token_register_trustless.rs +++ b/programs/mango-v4/src/instructions/token_register_trustless.rs @@ -7,7 +7,7 @@ use crate::instructions::INDEX_START; use crate::state::*; use crate::util::fill_from_str; -use crate::logs::{emit_stack, TokenMetaDataLog}; +use crate::logs::{emit_stack, TokenMetaDataLogV2}; use crate::accounts_ix::*; @@ -102,7 +102,8 @@ pub fn token_register_trustless( maint_weight_shift_duration_inv: I80F48::ZERO, maint_weight_shift_asset_target: I80F48::ZERO, maint_weight_shift_liab_target: I80F48::ZERO, - reserved: [0; 2008], + fallback_oracle: ctx.accounts.fallback_oracle.key(), + reserved: [0; 1976], }; if let Ok(oracle_price) = @@ -126,19 +127,21 @@ pub fn token_register_trustless( banks: Default::default(), vaults: Default::default(), oracle: ctx.accounts.oracle.key(), + fallback_oracle: ctx.accounts.fallback_oracle.key(), registration_time: Clock::get()?.unix_timestamp.try_into().unwrap(), - reserved: [0; 2560], + reserved: [0; 2528], }; mint_info.banks[0] = ctx.accounts.bank.key(); mint_info.vaults[0] = ctx.accounts.vault.key(); - emit_stack(TokenMetaDataLog { + emit_stack(TokenMetaDataLogV2 { mango_group: ctx.accounts.group.key(), mint: ctx.accounts.mint.key(), token_index, mint_decimals: ctx.accounts.mint.decimals, oracle: ctx.accounts.oracle.key(), + fallback_oracle: ctx.accounts.fallback_oracle.key(), mint_info: ctx.accounts.mint_info.key(), }); diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 7ed0a2bc4..42de9b5e6 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -181,11 +181,12 @@ 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. - unsafe_oracle_state.check_confidence_and_maybe_staleness( - &bank.oracle, - &bank.oracle_config, - Some(Clock::get()?.slot), - )?; + let slot_opt = Some(Clock::get()?.slot); + unsafe_oracle_state + .check_confidence_and_maybe_staleness(&bank.name(), &bank.oracle_config, slot_opt) + .with_context(|| { + oracle_log_context(&unsafe_oracle_state, &bank.oracle_config, slot_opt) + })?; bank.check_net_borrows(unsafe_oracle_state.price)?; } diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 206ce6483..9c64d41ea 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -234,6 +234,7 @@ pub mod mango_v4 { maint_weight_shift_asset_target_opt: Option, maint_weight_shift_liab_target_opt: Option, maint_weight_shift_abort: bool, + set_fallback_oracle: bool, ) -> Result<()> { #[cfg(feature = "enable-gpl")] instructions::token_edit( @@ -272,6 +273,7 @@ pub mod mango_v4 { maint_weight_shift_asset_target_opt, maint_weight_shift_liab_target_opt, maint_weight_shift_abort, + set_fallback_oracle, )?; Ok(()) } diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index 215da9bba..938abded0 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -450,6 +450,17 @@ pub struct TokenMetaDataLog { pub mint_info: Pubkey, } +#[event] +pub struct TokenMetaDataLogV2 { + pub mango_group: Pubkey, + pub mint: Pubkey, + pub token_index: u16, + pub mint_decimals: u8, + pub oracle: Pubkey, + pub fallback_oracle: Pubkey, + pub mint_info: Pubkey, +} + #[event] pub struct PerpMarketMetaDataLog { pub mango_group: Pubkey, diff --git a/programs/mango-v4/src/serum3_cpi.rs b/programs/mango-v4/src/serum3_cpi.rs index 91ace334f..3ca69b8dc 100644 --- a/programs/mango-v4/src/serum3_cpi.rs +++ b/programs/mango-v4/src/serum3_cpi.rs @@ -1,5 +1,5 @@ use anchor_lang::prelude::*; -use serum_dex::state::{OpenOrders, ToAlignedBytes}; +use serum_dex::state::{OpenOrders, ToAlignedBytes, ACCOUNT_HEAD_PADDING}; use std::cell::{Ref, RefMut}; use std::cmp::min; @@ -49,6 +49,14 @@ fn strip_data_header_mut( })) } +pub fn has_serum_header(data: &[u8]) -> bool { + if data.len() < 5 { + return false; + } + let head = &data[..5]; + head == ACCOUNT_HEAD_PADDING +} + pub fn load_market_state<'a>( market_account: &'a AccountInfo, program_id: &Pubkey, diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 17d250198..219ee4209 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -9,6 +9,7 @@ use anchor_lang::prelude::*; use anchor_spl::token::TokenAccount; use derivative::Derivative; use fixed::types::I80F48; +use oracle::oracle_log_context; use static_assertions::const_assert_eq; use std::mem::size_of; @@ -167,8 +168,10 @@ pub struct Bank { pub maint_weight_shift_asset_target: I80F48, pub maint_weight_shift_liab_target: I80F48, + pub fallback_oracle: Pubkey, + #[derivative(Debug = "ignore")] - pub reserved: [u8; 2008], + pub reserved: [u8; 1976], } const_assert_eq!( size_of::(), @@ -292,7 +295,8 @@ impl Bank { maint_weight_shift_duration_inv: existing_bank.maint_weight_shift_duration_inv, maint_weight_shift_asset_target: existing_bank.maint_weight_shift_asset_target, maint_weight_shift_liab_target: existing_bank.maint_weight_shift_liab_target, - reserved: [0; 2008], + fallback_oracle: existing_bank.oracle, + reserved: [0; 1976], } } @@ -947,12 +951,50 @@ impl Bank { ) -> Result { require_keys_eq!(self.oracle, *oracle_acc.key()); let state = oracle::oracle_state_unchecked(oracle_acc, self.mint_decimals)?; - state.check_confidence_and_maybe_staleness( - &self.oracle, + state + .check_confidence_and_maybe_staleness(&self.name(), &self.oracle_config, staleness_slot) + .with_context(|| oracle_log_context(&state, &self.oracle_config, staleness_slot))?; + Ok(state.price) + } + + /// Tries to return the primary oracle price, and if there is a confidence or staleness issue returns the fallback oracle price. + pub fn oracle_price_with_fallback( + &self, + oracle_acc: &impl KeyedAccountReader, + fallback_oracle_acc_opt: Option<&impl KeyedAccountReader>, + staleness_slot: Option, + ) -> Result { + require_keys_eq!(self.oracle, *oracle_acc.key()); + let primary_state = oracle::oracle_state_unchecked(oracle_acc, self.mint_decimals)?; + let primary_ok = primary_state.check_confidence_and_maybe_staleness( + &self.name(), &self.oracle_config, staleness_slot, - )?; - Ok(state.price) + ); + if primary_ok.is_oracle_error() && fallback_oracle_acc_opt.is_some() { + let fallback_oracle_acc = fallback_oracle_acc_opt.unwrap(); + require_keys_eq!(self.fallback_oracle, *fallback_oracle_acc.key()); + let fallback_state = + oracle::oracle_state_unchecked(fallback_oracle_acc, self.mint_decimals)?; + let fallback_ok = fallback_state.check_confidence_and_maybe_staleness( + &self.name(), + &self.oracle_config, + staleness_slot, + ); + fallback_ok.with_context(|| { + format!( + "{} {}", + oracle_log_context(&primary_state, &self.oracle_config, staleness_slot), + oracle_log_context(&fallback_state, &self.oracle_config, staleness_slot) + ) + })?; + Ok(fallback_state.price) + } else { + primary_ok.with_context(|| { + oracle_log_context(&primary_state, &self.oracle_config, staleness_slot) + })?; + Ok(primary_state.price) + } } pub fn stable_price(&self) -> I80F48 { diff --git a/programs/mango-v4/src/state/mint_info.rs b/programs/mango-v4/src/state/mint_info.rs index 73ae555a8..5f8cc6107 100644 --- a/programs/mango-v4/src/state/mint_info.rs +++ b/programs/mango-v4/src/state/mint_info.rs @@ -33,8 +33,10 @@ pub struct MintInfo { pub registration_time: u64, + pub fallback_oracle: Pubkey, + #[derivative(Debug = "ignore")] - pub reserved: [u8; 2560], + pub reserved: [u8; 2528], } const_assert_eq!( size_of::(), diff --git a/programs/mango-v4/src/state/oracle.rs b/programs/mango-v4/src/state/oracle.rs index 65c483100..5c243ff22 100644 --- a/programs/mango-v4/src/state/oracle.rs +++ b/programs/mango-v4/src/state/oracle.rs @@ -105,19 +105,19 @@ impl OracleState { #[inline] pub fn check_confidence_and_maybe_staleness( &self, - oracle_pk: &Pubkey, + oracle_name: &str, config: &OracleConfig, staleness_slot: Option, ) -> Result<()> { if let Some(now_slot) = staleness_slot { - self.check_staleness(oracle_pk, config, now_slot)?; + self.check_staleness(oracle_name, config, now_slot)?; } - self.check_confidence(oracle_pk, config) + self.check_confidence(oracle_name, config) } pub fn check_staleness( &self, - oracle_pk: &Pubkey, + oracle_name: &str, config: &OracleConfig, now_slot: u64, ) -> Result<()> { @@ -127,27 +127,15 @@ impl OracleState { .saturating_add(config.max_staleness_slots as u64) < now_slot { - msg!( - "Oracle is stale; pubkey {}, price: {}, last_update_slot: {}, now_slot: {}", - oracle_pk, - self.price.to_num::(), - self.last_update_slot, - now_slot, - ); + msg!("Oracle is stale: {}", oracle_name); return Err(MangoError::OracleStale.into()); } Ok(()) } - pub fn check_confidence(&self, oracle_pk: &Pubkey, config: &OracleConfig) -> Result<()> { + pub fn check_confidence(&self, oracle_name: &str, config: &OracleConfig) -> Result<()> { if self.deviation > config.conf_filter * self.price { - msg!( - "Oracle confidence not good enough: pubkey {}, price: {}, deviation: {}, conf_filter: {}", - oracle_pk, - self.price.to_num::(), - self.deviation.to_num::(), - config.conf_filter.to_num::(), - ); + msg!("Oracle confidence not good enough: {}", oracle_name); return Err(MangoError::OracleConfidence.into()); } Ok(()) @@ -228,7 +216,7 @@ fn pyth_get_price( /// Returns the price of one native base token, in native quote tokens /// -/// Example: The for SOL at 40 USDC/SOL it would return 0.04 (the unit is USDC-native/SOL-native) +/// Example: The price for SOL at 40 USDC/SOL it would return 0.04 (the unit is USDC-native/SOL-native) /// /// This currently assumes that quote decimals (i.e. decimals for USD) is 6, like for USDC. /// @@ -332,6 +320,21 @@ pub fn oracle_state_unchecked( }) } +pub fn oracle_log_context( + state: &OracleState, + oracle_config: &OracleConfig, + staleness_slot: Option, +) -> String { + format!( + "price: {}, deviation: {}, last_update_slot: {}, now_slot: {}, conf_filter: {:#?}", + state.price.to_num::(), + state.deviation.to_num::(), + state.last_update_slot, + staleness_slot.unwrap_or_else(|| u64::MAX), + oracle_config.conf_filter.to_num::(), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 1bbf48270..f4e3cf3dc 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -4,10 +4,11 @@ use anchor_lang::prelude::*; use derivative::Derivative; use fixed::types::I80F48; +use oracle::oracle_log_context; use static_assertions::const_assert_eq; use crate::accounts_zerocopy::KeyedAccountReader; -use crate::error::MangoError; +use crate::error::{Contextable, MangoError}; use crate::logs::{emit_stack, PerpUpdateFundingLogV2}; use crate::state::orderbook::Side; use crate::state::{oracle, TokenIndex}; @@ -275,11 +276,9 @@ impl PerpMarket { ) -> Result { require_keys_eq!(self.oracle, *oracle_acc.key()); let state = oracle::oracle_state_unchecked(oracle_acc, self.base_decimals)?; - state.check_confidence_and_maybe_staleness( - &self.oracle, - &self.oracle_config, - staleness_slot, - )?; + state + .check_confidence_and_maybe_staleness(&self.name(), &self.oracle_config, staleness_slot) + .with_context(|| oracle_log_context(&state, &self.oracle_config, staleness_slot))?; Ok(state) } diff --git a/programs/mango-v4/tests/cases/test_basic.rs b/programs/mango-v4/tests/cases/test_basic.rs index 6df657ec9..f3d2a665f 100644 --- a/programs/mango-v4/tests/cases/test_basic.rs +++ b/programs/mango-v4/tests/cases/test_basic.rs @@ -286,6 +286,7 @@ async fn test_basic() -> Result<(), TransportError> { send_tx( solana, StubOracleCloseInstruction { + oracle: tokens[0].oracle, group, mint: bank_data.mint, admin, @@ -459,6 +460,7 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> { group, admin, mint: mints[0].pubkey, + fallback_oracle: Pubkey::default(), options: mango_v4::instruction::TokenEdit { maint_weight_shift_start_opt: Some(start_time + 1000), maint_weight_shift_end_opt: Some(start_time + 2000), @@ -492,6 +494,7 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> { group, admin, mint: mints[0].pubkey, + fallback_oracle: Pubkey::default(), options: mango_v4::instruction::TokenEdit { maint_weight_shift_abort: true, ..token_edit_instruction_default() diff --git a/programs/mango-v4/tests/cases/test_health_compute.rs b/programs/mango-v4/tests/cases/test_health_compute.rs index 35cdef8f9..781ff8395 100644 --- a/programs/mango-v4/tests/cases/test_health_compute.rs +++ b/programs/mango-v4/tests/cases/test_health_compute.rs @@ -1,4 +1,5 @@ use super::*; +use anchor_lang::prelude::AccountMeta; use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; async fn deposit_cu_datapoint( @@ -24,6 +25,31 @@ async fn deposit_cu_datapoint( result.metadata.unwrap().compute_units_consumed } +async fn deposit_cu_fallbacks_datapoint( + solana: &SolanaCookie, + account: Pubkey, + owner: TestKeypair, + token_account: Pubkey, + remaining_accounts: Vec, +) -> u64 { + let result = send_tx_with_extra_accounts( + solana, + TokenDepositInstruction { + amount: 10, + reduce_only: false, + account, + owner, + token_account, + token_authority: owner, + bank_index: 0, + }, + remaining_accounts, + ) + .await + .unwrap(); + result.metadata.unwrap().compute_units_consumed +} + // Try to reach compute limits in health checks by having many different tokens in an account #[tokio::test] async fn test_health_compute_tokens() -> Result<(), TransportError> { @@ -68,7 +94,7 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> { let avg_cu_increase = cu_measurements.windows(2).map(|p| p[1] - p[0]).sum::() / (cu_measurements.len() - 1) as u64; println!("average cu increase: {avg_cu_increase}"); - assert!(avg_cu_increase < 3200); + assert!(avg_cu_increase < 3350); Ok(()) } @@ -107,6 +133,7 @@ async fn test_health_compute_tokens_during_maint_weight_shift() -> Result<(), Tr group, admin, mint: mint.pubkey, + fallback_oracle: Pubkey::default(), options: mango_v4::instruction::TokenEdit { maint_weight_shift_start_opt: Some(now - 1000), maint_weight_shift_end_opt: Some(now + 1000), @@ -137,7 +164,178 @@ async fn test_health_compute_tokens_during_maint_weight_shift() -> Result<(), Tr let avg_cu_increase = cu_measurements.windows(2).map(|p| p[1] - p[0]).sum::() / (cu_measurements.len() - 1) as u64; println!("average cu increase: {avg_cu_increase}"); - assert!(avg_cu_increase < 4200); + assert!(avg_cu_increase < 4300); + + Ok(()) +} + +// Try to reach compute limits in health checks by having many different tokens in an account and using fallback oracles for them +#[tokio::test] +async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportError> { + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(450_000); + let context = test_builder.start_default().await; + let solana = &context.solana.clone(); + + let num_tokens = 8; + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..num_tokens]; + + let mut fallback_oracle_kps = Vec::with_capacity(num_tokens); + for _ in 0..num_tokens { + fallback_oracle_kps.push(TestKeypair::new()); + } + let success_metas: Vec = fallback_oracle_kps + .iter() + .map(|x| AccountMeta { + pubkey: x.pubkey(), + is_signer: false, + is_writable: false, + }) + .collect(); + + let failure_metas = vec![]; + + // + // SETUP: Create a group and an account + // + + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..GroupWithTokensConfig::default() + } + .create(solana) + .await; + + let account = + create_funded_account(&solana, group, owner, 0, &context.users[1], &[], 1000, 0).await; + + let mut success_measurements = vec![]; + let mut failure_measurements = vec![]; + for token_account in &context.users[0].token_accounts[..mints.len()] { + deposit_cu_datapoint(solana, account, owner, *token_account).await; + } + + // + // SETUP: Create and register fallback oracles for each token + // + for (i, _token_account) in context.users[0].token_accounts[..mints.len()] + .iter() + .enumerate() + { + send_tx( + solana, + StubOracleCreate { + oracle: fallback_oracle_kps[i], + group, + mint: mints[i].pubkey, + admin, + payer, + }, + ) + .await + .unwrap(); + + send_tx( + solana, + TokenEdit { + group, + admin, + mint: mints[i].pubkey, + fallback_oracle: fallback_oracle_kps[i].pubkey(), + options: mango_v4::instruction::TokenEdit { + set_fallback_oracle: true, + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + } + + // + // TEST: Progressively make each oracle invalid so that the fallback is used + // + for (i, token_account) in context.users[0].token_accounts[..mints.len()] + .iter() + .enumerate() + { + send_tx( + solana, + StubOracleSetTestInstruction { + oracle: tokens[i].oracle, + group, + mint: mints[i].pubkey, + admin, + price: 1.0, + last_update_slot: 0, + deviation: 100.0, + }, + ) + .await + .unwrap(); + + success_measurements.push( + deposit_cu_fallbacks_datapoint( + solana, + account, + owner, + *token_account, + success_metas.clone(), + ) + .await, + ); + + failure_measurements.push( + deposit_cu_fallbacks_datapoint( + solana, + account, + owner, + *token_account, + failure_metas.clone(), + ) + .await, + ); + } + println!("successful fallbacks:"); + for (i, pair) in success_measurements.windows(2).enumerate() { + println!( + "after adding token {}: {} (+{})", + i, + pair[1], + pair[1] - pair[0] + ); + } + println!("failed fallbacks:"); + for (i, pair) in failure_measurements.windows(2).enumerate() { + println!( + "after adding token {}: {} (+{})", + i, + pair[1], + pair[1] - pair[0] + ); + } + + let avg_success_increase = success_measurements + .windows(2) + .map(|p| p[1] - p[0]) + .sum::() + / (success_measurements.len() - 1) as u64; + + let avg_failure_increase = failure_measurements + .windows(2) + .map(|p| p[1] - p[0]) + .sum::() + / (failure_measurements.len() - 1) as u64; + println!("average success increase: {avg_success_increase}"); + println!("average failure increase: {avg_failure_increase}"); + assert!(avg_success_increase < 2_050); + assert!(avg_success_increase < 18_500); Ok(()) } diff --git a/programs/mango-v4/tests/cases/test_margin_trade.rs b/programs/mango-v4/tests/cases/test_margin_trade.rs index 4c48f0727..7cf2e8f70 100644 --- a/programs/mango-v4/tests/cases/test_margin_trade.rs +++ b/programs/mango-v4/tests/cases/test_margin_trade.rs @@ -244,7 +244,7 @@ async fn test_margin_trade() -> Result<(), BanksClientError> { #[tokio::test] async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> { let mut test_builder = TestContextBuilder::new(); - test_builder.test().set_compute_max_units(100_000); + test_builder.test().set_compute_max_units(150_000); let context = test_builder.start_default().await; let solana = &context.solana.clone(); @@ -278,6 +278,7 @@ async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> { group, admin, mint: tokens[1].mint.pubkey, + fallback_oracle: Pubkey::default(), options: mango_v4::instruction::TokenEdit { flash_loan_swap_fee_rate_opt: Some(swap_fee_rate as f32), ..token_edit_instruction_default() diff --git a/programs/mango-v4/tests/cases/test_perp_settle.rs b/programs/mango-v4/tests/cases/test_perp_settle.rs index 4aca5438f..06aaa6c06 100644 --- a/programs/mango-v4/tests/cases/test_perp_settle.rs +++ b/programs/mango-v4/tests/cases/test_perp_settle.rs @@ -975,6 +975,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { send_tx( solana, StubOracleSetInstruction { + oracle: tokens[1].oracle, group, admin, mint: mints[1].pubkey, diff --git a/programs/mango-v4/tests/cases/test_serum.rs b/programs/mango-v4/tests/cases/test_serum.rs index 9762794db..f136f136c 100644 --- a/programs/mango-v4/tests/cases/test_serum.rs +++ b/programs/mango-v4/tests/cases/test_serum.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] use super::*; +use anchor_lang::prelude::AccountMeta; use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; use mango_v4::serum3_cpi::{load_open_orders_bytes, OpenOrdersSlim}; use std::sync::Arc; @@ -1503,6 +1504,182 @@ async fn test_serum_compute() -> Result<(), TransportError> { Ok(()) } +#[tokio::test] +async fn test_fallback_oracle_serum() -> Result<(), TransportError> { + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(150_000); + let context = test_builder.start_default().await; + let solana = &context.solana.clone(); + + let fallback_oracle_kp = TestKeypair::new(); + let fallback_oracle = fallback_oracle_kp.pubkey(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let payer_token_accounts = &context.users[1].token_accounts[0..3]; + + // + // SETUP: Create a group and an account + // + let deposit_amount = 1_000; + let CommonSetup { + group_with_tokens, + quote_token, + base_token, + mut order_placer, + .. + } = common_setup(&context, deposit_amount).await; + let GroupWithTokens { + group, + admin, + tokens, + .. + } = group_with_tokens; + + // + // SETUP: Create a fallback oracle + // + send_tx( + solana, + StubOracleCreate { + oracle: fallback_oracle_kp, + group, + mint: tokens[2].mint.pubkey, + admin, + payer, + }, + ) + .await + .unwrap(); + + // + // SETUP: Add a fallback oracle + // + send_tx( + solana, + TokenEdit { + group, + admin, + mint: tokens[2].mint.pubkey, + fallback_oracle, + options: mango_v4::instruction::TokenEdit { + set_fallback_oracle: true, + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + + let bank_data: Bank = solana.get_account(tokens[2].bank).await; + assert!(bank_data.fallback_oracle == fallback_oracle); + + // Create some token1 borrows + send_tx( + solana, + TokenWithdrawInstruction { + amount: 1_500, + allow_borrow: true, + account: order_placer.account, + owner, + token_account: payer_token_accounts[2], + bank_index: 0, + }, + ) + .await + .unwrap(); + + // Make oracle invalid by increasing deviation + send_tx( + solana, + StubOracleSetTestInstruction { + oracle: tokens[2].oracle, + group, + mint: tokens[2].mint.pubkey, + admin, + price: 1.0, + last_update_slot: 0, + deviation: 100.0, + }, + ) + .await + .unwrap(); + + // + // TEST: Place a failing order + // + let limit_price = 1.0; + let max_base = 100; + let order_fut = order_placer.try_bid(limit_price, max_base, false).await; + assert_mango_error( + &order_fut, + 6023, + "an oracle does not reach the confidence threshold".to_string(), + ); + + // now send txn with a fallback oracle in the remaining accounts + let fallback_oracle_meta = AccountMeta { + pubkey: fallback_oracle, + is_writable: false, + is_signer: false, + }; + + let client_order_id = order_placer.inc_client_order_id(); + let place_ix = Serum3PlaceOrderInstruction { + side: Serum3Side::Bid, + limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100) + max_base_qty: max_base / 100, // in base lot (100) + // 4 bps taker fees added in + max_native_quote_qty_including_fees: (limit_price * (max_base as f64) * (1.0)).ceil() + as u64, + self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction, + order_type: Serum3OrderType::Limit, + client_order_id, + limit: 10, + account: order_placer.account, + owner: order_placer.owner, + serum_market: order_placer.serum_market, + }; + + let result = send_tx_with_extra_accounts(solana, place_ix, vec![fallback_oracle_meta]) + .await + .unwrap(); + result.result.unwrap(); + + let account_data = get_mango_account(solana, order_placer.account).await; + assert_eq!( + account_data + .token_position_by_raw_index(0) + .unwrap() + .in_use_count, + 1 + ); + assert_eq!( + account_data + .token_position_by_raw_index(1) + .unwrap() + .in_use_count, + 1 + ); + assert_eq!( + account_data + .token_position_by_raw_index(2) + .unwrap() + .in_use_count, + 0 + ); + let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap(); + assert_eq!(serum_orders.base_borrows_without_fee, 0); + assert_eq!(serum_orders.quote_borrows_without_fee, 0); + assert_eq!(serum_orders.base_deposits_reserved, 0); + assert_eq!(serum_orders.quote_deposits_reserved, 100); + + let base_bank = solana.get_account::(base_token.bank).await; + assert_eq!(base_bank.deposits_in_serum, 0); + let quote_bank = solana.get_account::(quote_token.bank).await; + assert_eq!(quote_bank.deposits_in_serum, 100); + Ok(()) +} + struct CommonSetup { group_with_tokens: GroupWithTokens, serum_market_cookie: SpotMarketCookie, diff --git a/programs/mango-v4/tests/cases/test_stale_oracles.rs b/programs/mango-v4/tests/cases/test_stale_oracles.rs index ad14be112..5a4bae9d6 100644 --- a/programs/mango-v4/tests/cases/test_stale_oracles.rs +++ b/programs/mango-v4/tests/cases/test_stale_oracles.rs @@ -1,9 +1,10 @@ use super::*; +use anchor_lang::prelude::AccountMeta; #[tokio::test] async fn test_stale_oracle_deposit_withdraw() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); - test_builder.test().set_compute_max_units(100_000); // bad oracles log a lot + test_builder.test().set_compute_max_units(150_000); // bad oracles log a lot let context = test_builder.start_default().await; let solana = &context.solana.clone(); @@ -17,7 +18,7 @@ async fn test_stale_oracle_deposit_withdraw() -> Result<(), TransportError> { // SETUP: Create a group, account, register tokens // - let mango_setup::GroupWithTokens { group, .. } = mango_setup::GroupWithTokensConfig { + let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { admin, payer, mints: mints.to_vec(), @@ -71,6 +72,7 @@ async fn test_stale_oracle_deposit_withdraw() -> Result<(), TransportError> { send_tx( solana, StubOracleSetTestInstruction { + oracle: tokens[0].oracle, group, mint: mints[0].pubkey, admin, @@ -84,6 +86,7 @@ async fn test_stale_oracle_deposit_withdraw() -> Result<(), TransportError> { send_tx( solana, StubOracleSetTestInstruction { + oracle: tokens[1].oracle, group, mint: mints[1].pubkey, admin, @@ -97,6 +100,7 @@ async fn test_stale_oracle_deposit_withdraw() -> Result<(), TransportError> { send_tx( solana, StubOracleSetTestInstruction { + oracle: tokens[2].oracle, group, mint: mints[2].pubkey, admin, @@ -172,3 +176,143 @@ async fn test_stale_oracle_deposit_withdraw() -> Result<(), TransportError> { Ok(()) } + +#[tokio::test] +async fn test_fallback_oracle_withdraw() -> Result<(), TransportError> { + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(150_000); // bad oracles log a lot + let context = test_builder.start_default().await; + let solana = &context.solana.clone(); + + let fallback_oracle_kp = TestKeypair::new(); + let fallback_oracle = fallback_oracle_kp.pubkey(); + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..3]; + let payer_token_accounts = &context.users[1].token_accounts[0..3]; + + let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..mango_setup::GroupWithTokensConfig::default() + } + .create(solana) + .await; + + // setup fallback_oracle + send_tx( + solana, + StubOracleCreate { + oracle: fallback_oracle_kp, + group, + mint: mints[2].pubkey, + admin, + payer, + }, + ) + .await + .unwrap(); + + // add a fallback oracle + send_tx( + solana, + TokenEdit { + group, + admin, + mint: mints[2].pubkey, + fallback_oracle, + options: mango_v4::instruction::TokenEdit { + set_fallback_oracle: true, + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + + let bank_data: Bank = solana.get_account(tokens[2].bank).await; + assert!(bank_data.fallback_oracle == fallback_oracle); + + // fill vaults, so we can borrow + let _vault_account = create_funded_account( + &solana, + group, + owner, + 2, + &context.users[1], + mints, + 100_000, + 0, + ) + .await; + + // Create account with token3 of deposits + let account = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + &[mints[2]], + 1_000_000, + 0, + ) + .await; + + // Create some token1 borrows + send_tx( + solana, + TokenWithdrawInstruction { + amount: 1, + allow_borrow: true, + account, + owner, + token_account: payer_token_accounts[1], + bank_index: 0, + }, + ) + .await + .unwrap(); + + // Make oracle invalid by increasing deviation + send_tx( + solana, + StubOracleSetTestInstruction { + oracle: tokens[2].oracle, + group, + mint: mints[2].pubkey, + admin, + price: 1.0, + last_update_slot: 0, + deviation: 100.0, + }, + ) + .await + .unwrap(); + + let token_withdraw_ix = TokenWithdrawInstruction { + amount: 1, + allow_borrow: true, + account, + owner, + token_account: payer_token_accounts[2], + bank_index: 0, + }; + + // Verify that withdrawing collateral won't work + assert!(send_tx(solana, token_withdraw_ix.clone(),).await.is_err()); + + // now send txn with a fallback oracle in the remaining accounts + let fallback_oracle_meta = AccountMeta { + pubkey: fallback_oracle, + is_writable: false, + is_signer: false, + }; + send_tx_with_extra_accounts(solana, token_withdraw_ix, vec![fallback_oracle_meta]) + .await + .unwrap(); + + Ok(()) +} diff --git a/programs/mango-v4/tests/cases/test_token_conditional_swap.rs b/programs/mango-v4/tests/cases/test_token_conditional_swap.rs index 62827222d..f0a5c48f7 100644 --- a/programs/mango-v4/tests/cases/test_token_conditional_swap.rs +++ b/programs/mango-v4/tests/cases/test_token_conditional_swap.rs @@ -71,6 +71,7 @@ async fn test_token_conditional_swap_basic() -> Result<(), TransportError> { group, admin, mint: quote_token.mint.pubkey, + fallback_oracle: Pubkey::default(), options: mango_v4::instruction::TokenEdit { token_conditional_swap_taker_fee_rate_opt: Some(0.05), token_conditional_swap_maker_fee_rate_opt: Some(0.1), @@ -388,6 +389,7 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr group, admin, mint: quote_token.mint.pubkey, + fallback_oracle: Pubkey::default(), options: mango_v4::instruction::TokenEdit { token_conditional_swap_taker_fee_rate_opt: Some(0.05), token_conditional_swap_maker_fee_rate_opt: Some(0.1), @@ -648,6 +650,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr group, admin, mint: quote_token.mint.pubkey, + fallback_oracle: Pubkey::default(), options: mango_v4::instruction::TokenEdit { token_conditional_swap_taker_fee_rate_opt: Some(0.05), token_conditional_swap_maker_fee_rate_opt: Some(0.1), diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index a5ac7dae6..cd5bf325f 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -41,14 +41,15 @@ impl ClientAccountLoader for &SolanaCookie { } } -// This fill return a failure if the tx resulted in an error +// This will return a failure if the tx resulted in an error pub async fn send_tx( solana: &SolanaCookie, ix: CI, ) -> std::result::Result { let (accounts, instruction) = ix.to_instruction(solana).await; let signers = ix.signers(); - let instructions = vec![instruction]; + let instructions = vec![instruction.clone()]; + println!("IX IX: {:?}", instruction); let result = solana .process_transaction(&instructions, Some(&signers[..])) .await?; @@ -56,6 +57,21 @@ pub async fn send_tx( Ok(accounts) } +// This will return a failure if the tx resulted in an error +pub async fn send_tx_with_extra_accounts( + solana: &SolanaCookie, + ix: CI, + account_metas: Vec, +) -> std::result::Result { + let (_, mut instruction) = ix.to_instruction(solana).await; + instruction.accounts.extend(account_metas); + let signers = ix.signers(); + let instructions = vec![instruction.clone()]; + solana + .process_transaction(&instructions, Some(&signers[..])) + .await +} + // This will return success even if the tx failed to finish pub async fn send_tx_get_metadata( solana: &SolanaCookie, @@ -428,6 +444,7 @@ pub async fn set_bank_stub_oracle_price( send_tx( solana, StubOracleSetInstruction { + oracle: token.oracle, group, admin, mint: token.mint.pubkey, @@ -722,6 +739,7 @@ impl ClientInstruction for FlashLoanEndInstruction { } } +#[derive(Clone)] pub struct TokenWithdrawInstruction { pub amount: u64, pub allow_borrow: bool, @@ -793,6 +811,7 @@ impl ClientInstruction for TokenWithdrawInstruction { } } +#[derive(Clone)] pub struct TokenDepositInstruction { pub amount: u64, pub reduce_only: bool, @@ -959,6 +978,7 @@ pub struct TokenRegisterInstruction { pub group: Pubkey, pub admin: TestKeypair, pub mint: Pubkey, + pub oracle: Pubkey, pub payer: TestKeypair, } #[async_trait::async_trait(?Send)] @@ -1042,16 +1062,7 @@ impl ClientInstruction for TokenRegisterInstruction { &program_id, ) .0; - // TODO: remove copy pasta of pda derivation, use reference - let oracle = Pubkey::find_program_address( - &[ - b"StubOracle".as_ref(), - self.group.as_ref(), - self.mint.as_ref(), - ], - &program_id, - ) - .0; + let fallback_oracle = Pubkey::default(); let accounts = Self::Accounts { group: self.group, @@ -1060,7 +1071,8 @@ impl ClientInstruction for TokenRegisterInstruction { bank, vault, mint_info, - oracle, + oracle: self.oracle, + fallback_oracle, payer: self.payer.pubkey(), token_program: Token::id(), system_program: System::id(), @@ -1262,6 +1274,7 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit { maint_weight_shift_asset_target_opt: None, maint_weight_shift_liab_target_opt: None, maint_weight_shift_abort: false, + set_fallback_oracle: false, } } @@ -1269,6 +1282,7 @@ pub struct TokenEdit { pub group: Pubkey, pub admin: TestKeypair, pub mint: Pubkey, + pub fallback_oracle: Pubkey, pub options: mango_v4::instruction::TokenEdit, } #[async_trait::async_trait(?Send)] @@ -1297,6 +1311,7 @@ impl ClientInstruction for TokenEdit { admin: self.admin.pubkey(), mint_info: mint_info_key, oracle: mint_info.oracle, + fallback_oracle: self.fallback_oracle, }; let mut instruction = make_instruction(program_id, &accounts, &self.options); @@ -1360,6 +1375,7 @@ impl ClientInstruction for TokenEditWeights { admin: self.admin.pubkey(), mint_info: mint_info_key, oracle: mint_info.oracle, + fallback_oracle: mint_info.fallback_oracle, }; let mut instruction = make_instruction(program_id, &accounts, &instruction); @@ -1416,6 +1432,7 @@ impl ClientInstruction for TokenResetStablePriceModel { admin: self.admin.pubkey(), mint_info: mint_info_key, oracle: mint_info.oracle, + fallback_oracle: mint_info.fallback_oracle, }; let mut instruction = make_instruction(program_id, &accounts, &instruction); @@ -1477,6 +1494,7 @@ impl ClientInstruction for TokenResetNetBorrows { admin: self.admin.pubkey(), mint_info: mint_info_key, oracle: mint_info.oracle, + fallback_oracle: mint_info.fallback_oracle, }; let mut instruction = make_instruction(program_id, &accounts, &instruction); @@ -1535,6 +1553,7 @@ impl ClientInstruction for TokenMakeReduceOnly { admin: self.admin.pubkey(), mint_info: mint_info_key, oracle: mint_info.oracle, + fallback_oracle: mint_info.fallback_oracle, }; let mut instruction = make_instruction(program_id, &accounts, &instruction); @@ -1558,6 +1577,7 @@ pub struct StubOracleSetInstruction { pub group: Pubkey, pub admin: TestKeypair, pub price: f64, + pub oracle: Pubkey, } #[async_trait::async_trait(?Send)] impl ClientInstruction for StubOracleSetInstruction { @@ -1572,19 +1592,9 @@ impl ClientInstruction for StubOracleSetInstruction { let instruction = Self::Instruction { price: I80F48::from_num(self.price), }; - // TODO: remove copy pasta of pda derivation, use reference - let oracle = Pubkey::find_program_address( - &[ - b"StubOracle".as_ref(), - self.group.as_ref(), - self.mint.as_ref(), - ], - &program_id, - ) - .0; let accounts = Self::Accounts { - oracle, + oracle: self.oracle, group: self.group, admin: self.admin.pubkey(), }; @@ -1599,6 +1609,7 @@ impl ClientInstruction for StubOracleSetInstruction { } pub struct StubOracleSetTestInstruction { + pub oracle: Pubkey, pub mint: Pubkey, pub group: Pubkey, pub admin: TestKeypair, @@ -1621,18 +1632,9 @@ impl ClientInstruction for StubOracleSetTestInstruction { last_update_slot: self.last_update_slot, deviation: I80F48::from_num(self.deviation), }; - let oracle = Pubkey::find_program_address( - &[ - b"StubOracle".as_ref(), - self.group.as_ref(), - self.mint.as_ref(), - ], - &program_id, - ) - .0; let accounts = Self::Accounts { - oracle, + oracle: self.oracle, group: self.group, admin: self.admin.pubkey(), }; @@ -1647,10 +1649,11 @@ impl ClientInstruction for StubOracleSetTestInstruction { } pub struct StubOracleCreate { - pub group: Pubkey, - pub mint: Pubkey, + pub oracle: TestKeypair, pub admin: TestKeypair, pub payer: TestKeypair, + pub group: Pubkey, + pub mint: Pubkey, } #[async_trait::async_trait(?Send)] impl ClientInstruction for StubOracleCreate { @@ -1666,19 +1669,9 @@ impl ClientInstruction for StubOracleCreate { price: I80F48::from_num(1.0), }; - let oracle = Pubkey::find_program_address( - &[ - b"StubOracle".as_ref(), - self.group.as_ref(), - self.mint.as_ref(), - ], - &program_id, - ) - .0; - let accounts = Self::Accounts { group: self.group, - oracle, + oracle: self.oracle.pubkey(), mint: self.mint, admin: self.admin.pubkey(), payer: self.payer.pubkey(), @@ -1690,11 +1683,12 @@ impl ClientInstruction for StubOracleCreate { } fn signers(&self) -> Vec { - vec![self.payer, self.admin] + vec![self.payer, self.admin, self.oracle] } } pub struct StubOracleCloseInstruction { + pub oracle: Pubkey, pub group: Pubkey, pub mint: Pubkey, pub admin: TestKeypair, @@ -1712,20 +1706,10 @@ impl ClientInstruction for StubOracleCloseInstruction { let program_id = mango_v4::id(); let instruction = Self::Instruction {}; - let oracle = Pubkey::find_program_address( - &[ - b"StubOracle".as_ref(), - self.group.as_ref(), - self.mint.as_ref(), - ], - &program_id, - ) - .0; - let accounts = Self::Accounts { group: self.group, admin: self.admin.pubkey(), - oracle, + oracle: self.oracle, sol_destination: self.sol_destination, token_program: Token::id(), }; diff --git a/programs/mango-v4/tests/program_test/mango_setup.rs b/programs/mango-v4/tests/program_test/mango_setup.rs index e09a70239..0a722d763 100644 --- a/programs/mango-v4/tests/program_test/mango_setup.rs +++ b/programs/mango-v4/tests/program_test/mango_setup.rs @@ -58,6 +58,7 @@ impl<'a> GroupWithTokensConfig { let create_stub_oracle_accounts = send_tx( solana, StubOracleCreate { + oracle: TestKeypair::new(), group, mint: mint.pubkey, admin, @@ -74,6 +75,7 @@ impl<'a> GroupWithTokensConfig { admin, mint: mint.pubkey, price: 1.0, + oracle, }, ) .await @@ -104,6 +106,7 @@ impl<'a> GroupWithTokensConfig { liquidation_fee: 0.02, group, admin, + oracle, mint: mint.pubkey, payer, min_vault_to_deposits_ratio: 0.2,