From 6288f2dd7df3b336c45aa1842f8f216aca3424a7 Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Fri, 17 Nov 2023 13:08:25 -0600 Subject: [PATCH 01/11] add fallback oracles to bank and mintinfo --- programs/mango-v4/src/accounts_ix/token_edit.rs | 5 +++++ .../mango-v4/src/accounts_ix/token_register.rs | 3 +++ .../src/accounts_ix/token_register_trustless.rs | 3 +++ programs/mango-v4/src/instructions/token_edit.rs | 16 ++++++++++++++-- .../mango-v4/src/instructions/token_register.rs | 11 +++++++---- .../src/instructions/token_register_trustless.rs | 11 +++++++---- programs/mango-v4/src/lib.rs | 2 ++ programs/mango-v4/src/logs.rs | 11 +++++++++++ programs/mango-v4/src/state/bank.rs | 7 +++++-- programs/mango-v4/src/state/mint_info.rs | 4 +++- 10 files changed, 60 insertions(+), 13 deletions(-) diff --git a/programs/mango-v4/src/accounts_ix/token_edit.rs b/programs/mango-v4/src/accounts_ix/token_edit.rs index 5d20d59ca..d4e6efdf6 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 oracle account is optional and only used when reset_stable_price is set. + /// + /// CHECK: The 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/instructions/token_edit.rs b/programs/mango-v4/src/instructions/token_edit.rs index abab45d2d..80e3b28fa 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, + fallback_oracle_opt: Option, ) -> Result<()> { let group = ctx.accounts.group.load()?; @@ -76,6 +77,16 @@ pub fn token_edit( mint_info.oracle = oracle; require_group_admin = true; } + if let Some(fallback_oracle) = fallback_oracle_opt { + msg!( + "Fallback oracle old {:?}, new {:?}", + bank.fallback_oracle, + fallback_oracle + ); + bank.fallback_oracle = fallback_oracle; + mint_info.fallback_oracle = fallback_oracle; + 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..28858a023 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..77cab62db 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/lib.rs b/programs/mango-v4/src/lib.rs index 477466497..8120d0823 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, + fallback_oracle_opt: Option, ) -> 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, + fallback_oracle_opt, )?; 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/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 17d250198..334c8944f 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -167,8 +167,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 +294,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, // bongo + reserved: [0; 1976], } } 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::(), From c22db3be0cf35fc9ad6fea04569b594091176dd9 Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Sun, 19 Nov 2023 18:01:24 -0600 Subject: [PATCH 02/11] test: stubOracle is not a PDA (allow multiple with same mint) --- .../src/accounts_ix/stub_oracle_create.rs | 2 - .../src/instructions/stub_oracle_create.rs | 5 +- .../mango-v4/tests/cases/test_perp_settle.rs | 1 + .../tests/cases/test_stale_oracles.rs | 6 +- .../tests/program_test/mango_client.rs | 69 ++++++------------- .../tests/program_test/mango_setup.rs | 3 + 6 files changed, 34 insertions(+), 52 deletions(-) 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/instructions/stub_oracle_create.rs b/programs/mango-v4/src/instructions/stub_oracle_create.rs index 05f5ab0af..211c4108a 100644 --- a/programs/mango-v4/src/instructions/stub_oracle_create.rs +++ b/programs/mango-v4/src/instructions/stub_oracle_create.rs @@ -3,7 +3,10 @@ use fixed::types::I80F48; use crate::accounts_ix::*; -pub fn stub_oracle_create(ctx: Context, price: I80F48) -> Result<()> { +pub fn stub_oracle_create( + ctx: Context, + price: I80F48 +) -> Result<()> { let mut oracle = ctx.accounts.oracle.load_init()?; oracle.group = ctx.accounts.group.key(); oracle.mint = ctx.accounts.mint.key(); diff --git a/programs/mango-v4/tests/cases/test_perp_settle.rs b/programs/mango-v4/tests/cases/test_perp_settle.rs index 4aca5438f..ae7d3ceb7 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[0].oracle, group, admin, mint: mints[1].pubkey, diff --git a/programs/mango-v4/tests/cases/test_stale_oracles.rs b/programs/mango-v4/tests/cases/test_stale_oracles.rs index ad14be112..115a1e2d0 100644 --- a/programs/mango-v4/tests/cases/test_stale_oracles.rs +++ b/programs/mango-v4/tests/cases/test_stale_oracles.rs @@ -17,7 +17,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 +71,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 +85,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 +99,7 @@ async fn test_stale_oracle_deposit_withdraw() -> Result<(), TransportError> { send_tx( solana, StubOracleSetTestInstruction { + oracle: tokens[2].oracle, group, mint: mints[2].pubkey, admin, @@ -171,4 +174,3 @@ async fn test_stale_oracle_deposit_withdraw() -> Result<(), TransportError> { .unwrap(); Ok(()) -} diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 80d77307b..e52b384f7 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -48,7 +48,8 @@ pub async fn send_tx( ) -> 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?; @@ -428,6 +429,7 @@ pub async fn set_bank_stub_oracle_price( send_tx( solana, StubOracleSetInstruction { + oracle: token.oracle, group, admin, mint: token.mint.pubkey, @@ -959,6 +961,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 +1045,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 +1054,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 +1257,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, + fallback_oracle_opt: None, } } @@ -1297,6 +1293,7 @@ impl ClientInstruction for TokenEdit { 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, &self.options); @@ -1360,6 +1357,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 +1414,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 +1476,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 +1535,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 +1559,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 +1574,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 +1591,7 @@ impl ClientInstruction for StubOracleSetInstruction { } pub struct StubOracleSetTestInstruction { + pub oracle: Pubkey, pub mint: Pubkey, pub group: Pubkey, pub admin: TestKeypair, @@ -1621,18 +1614,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 +1631,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 +1651,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,7 +1665,7 @@ impl ClientInstruction for StubOracleCreate { } fn signers(&self) -> Vec { - vec![self.payer, self.admin] + vec![self.payer, self.admin, self.oracle] } } 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, From d4017e603852bdce649ceb68b76153ba902dae92 Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Mon, 20 Nov 2023 15:00:36 -0600 Subject: [PATCH 03/11] FixedOrderAccountRetriever uses fallback oracles --- .../mango-v4/src/health/account_retriever.rs | 23 +++++++++++------ .../src/instructions/stub_oracle_create.rs | 5 +--- programs/mango-v4/src/state/bank.rs | 25 +++++++++++++++++++ 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/programs/mango-v4/src/health/account_retriever.rs b/programs/mango-v4/src/health/account_retriever.rs index 7ffd66919..31b47cdb0 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 fallback_oracle_ais: Vec, } pub fn new_fixed_order_account_retriever<'a, 'info>( @@ -66,19 +68,22 @@ 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 ); + let fixed_ais = AccountInfoRef::borrow_slice(&ais[..expected_ais])?; + let fallback_oracle_ais = AccountInfoRef::borrow_slice(&ais[expected_ais..])?; Ok(FixedOrderAccountRetriever { - ais: AccountInfoRef::borrow_slice(ais)?, + ais: fixed_ais, n_banks: active_token_len, n_perps: active_perp_len, begin_perp: active_token_len * 2, begin_serum3: active_token_len * 2 + active_perp_len * 2, staleness_slot: Some(Clock::get()?.slot), + fallback_oracle_ais, }) } @@ -103,11 +108,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 +134,14 @@ 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 + .fallback_oracle_ais + .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, diff --git a/programs/mango-v4/src/instructions/stub_oracle_create.rs b/programs/mango-v4/src/instructions/stub_oracle_create.rs index 211c4108a..05f5ab0af 100644 --- a/programs/mango-v4/src/instructions/stub_oracle_create.rs +++ b/programs/mango-v4/src/instructions/stub_oracle_create.rs @@ -3,10 +3,7 @@ use fixed::types::I80F48; use crate::accounts_ix::*; -pub fn stub_oracle_create( - ctx: Context, - price: I80F48 -) -> Result<()> { +pub fn stub_oracle_create(ctx: Context, price: I80F48) -> Result<()> { let mut oracle = ctx.accounts.oracle.load_init()?; oracle.group = ctx.accounts.group.key(); oracle.mint = ctx.accounts.mint.key(); diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 334c8944f..8c7196b94 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -949,6 +949,14 @@ impl Bank { staleness_slot: Option, ) -> Result { require_keys_eq!(self.oracle, *oracle_acc.key()); + self.oracle_price_inner(oracle_acc, staleness_slot) + } + + fn oracle_price_inner( + &self, + oracle_acc: &impl KeyedAccountReader, + staleness_slot: Option, + ) -> Result { let state = oracle::oracle_state_unchecked(oracle_acc, self.mint_decimals)?; state.check_confidence_and_maybe_staleness( &self.oracle, @@ -958,6 +966,23 @@ impl Bank { 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 { + let primary_price = self.oracle_price(oracle_acc, staleness_slot); + if primary_price.is_ok() || fallback_oracle_acc_opt.is_none() { + primary_price + } else { + let fallback_oracle_acc = fallback_oracle_acc_opt.unwrap(); + require_keys_eq!(self.fallback_oracle, *fallback_oracle_acc.key()); + self.oracle_price_inner(fallback_oracle_acc, staleness_slot) + } + } + pub fn stable_price(&self) -> I80F48 { I80F48::from_num(self.stable_price_model.stable_price) } From 20261bf73413fb6395d706929fafd405b75dadb2 Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Mon, 20 Nov 2023 15:00:57 -0600 Subject: [PATCH 04/11] test: add fallback oracle withdrawal test --- programs/mango-v4/src/state/oracle.rs | 2 +- programs/mango-v4/tests/cases/test_basic.rs | 1 + .../tests/cases/test_health_compute.rs | 2 +- .../mango-v4/tests/cases/test_margin_trade.rs | 2 +- .../mango-v4/tests/cases/test_perp_settle.rs | 2 +- .../tests/cases/test_stale_oracles.rs | 147 +++++++++++++++++- .../tests/program_test/mango_client.rs | 34 ++-- 7 files changed, 173 insertions(+), 17 deletions(-) diff --git a/programs/mango-v4/src/state/oracle.rs b/programs/mango-v4/src/state/oracle.rs index 65c483100..a1ed4c35e 100644 --- a/programs/mango-v4/src/state/oracle.rs +++ b/programs/mango-v4/src/state/oracle.rs @@ -228,7 +228,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. /// diff --git a/programs/mango-v4/tests/cases/test_basic.rs b/programs/mango-v4/tests/cases/test_basic.rs index 6df657ec9..ade4f6658 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, diff --git a/programs/mango-v4/tests/cases/test_health_compute.rs b/programs/mango-v4/tests/cases/test_health_compute.rs index 35cdef8f9..f9c84c381 100644 --- a/programs/mango-v4/tests/cases/test_health_compute.rs +++ b/programs/mango-v4/tests/cases/test_health_compute.rs @@ -68,7 +68,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 < 3230); 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..663f06337 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(); diff --git a/programs/mango-v4/tests/cases/test_perp_settle.rs b/programs/mango-v4/tests/cases/test_perp_settle.rs index ae7d3ceb7..06aaa6c06 100644 --- a/programs/mango-v4/tests/cases/test_perp_settle.rs +++ b/programs/mango-v4/tests/cases/test_perp_settle.rs @@ -975,7 +975,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { send_tx( solana, StubOracleSetInstruction { - oracle: tokens[0].oracle, + oracle: tokens[1].oracle, group, admin, mint: mints[1].pubkey, diff --git a/programs/mango-v4/tests/cases/test_stale_oracles.rs b/programs/mango-v4/tests/cases/test_stale_oracles.rs index 115a1e2d0..66da66b6b 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(); @@ -174,3 +175,147 @@ async fn test_stale_oracle_deposit_withdraw() -> Result<(), TransportError> { .unwrap(); 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]; + + // + // SETUP: Create a group, account, register tokens + //Foracle_ + + 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, + options: mango_v4::instruction::TokenEdit { + fallback_oracle_opt: Some(fallback_oracle), + ..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/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index e52b384f7..a1fbfac25 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -41,7 +41,7 @@ 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, @@ -57,6 +57,23 @@ 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 (accounts, mut instruction) = ix.to_instruction(solana).await; + instruction.accounts.extend(account_metas); + let signers = ix.signers(); + let instructions = vec![instruction.clone()]; + let result = solana + .process_transaction(&instructions, Some(&signers[..])) + .await?; + result.result?; + Ok(accounts) +} + // This will return success even if the tx failed to finish pub async fn send_tx_get_metadata( solana: &SolanaCookie, @@ -724,6 +741,7 @@ impl ClientInstruction for FlashLoanEndInstruction { } } +#[derive(Clone)] pub struct TokenWithdrawInstruction { pub amount: u64, pub allow_borrow: bool, @@ -795,6 +813,7 @@ impl ClientInstruction for TokenWithdrawInstruction { } } +#[derive(Clone)] pub struct TokenDepositInstruction { pub amount: u64, pub reduce_only: bool, @@ -1670,6 +1689,7 @@ impl ClientInstruction for StubOracleCreate { } pub struct StubOracleCloseInstruction { + pub oracle: Pubkey, pub group: Pubkey, pub mint: Pubkey, pub admin: TestKeypair, @@ -1687,20 +1707,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(), }; From a0a47af6a45ccdf717aa309d56378fe0ac9c23a8 Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Tue, 21 Nov 2023 13:40:18 -0600 Subject: [PATCH 05/11] ScanningAccountRetriever uses fallback oracles --- lib/client/src/health_cache.rs | 2 + .../mango-v4/src/health/account_retriever.rs | 40 ++++++++++++++----- .../mango-v4/src/instructions/token_edit.rs | 10 ++--- programs/mango-v4/src/lib.rs | 4 +- programs/mango-v4/src/state/bank.rs | 2 +- programs/mango-v4/tests/cases/test_basic.rs | 2 + .../tests/cases/test_health_compute.rs | 1 + .../mango-v4/tests/cases/test_margin_trade.rs | 1 + .../tests/cases/test_stale_oracles.rs | 7 +--- .../cases/test_token_conditional_swap.rs | 3 ++ .../tests/program_test/mango_client.rs | 5 ++- 11 files changed, 51 insertions(+), 26 deletions(-) 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/health/account_retriever.rs b/programs/mango-v4/src/health/account_retriever.rs index 31b47cdb0..02a036f1f 100644 --- a/programs/mango-v4/src/health/account_retriever.rs +++ b/programs/mango-v4/src/health/account_retriever.rs @@ -55,7 +55,7 @@ pub struct FixedOrderAccountRetriever { pub begin_perp: usize, pub begin_serum3: usize, pub staleness_slot: Option, - pub fallback_oracle_ais: Vec, + pub begin_fallback_oracles: usize, } pub fn new_fixed_order_account_retriever<'a, 'info>( @@ -73,17 +73,15 @@ pub fn new_fixed_order_account_retriever<'a, 'info>( ais.len(), expected_ais, active_token_len, active_token_len, active_perp_len, active_perp_len, active_serum3_len ); - let fixed_ais = AccountInfoRef::borrow_slice(&ais[..expected_ais])?; - let fallback_oracle_ais = AccountInfoRef::borrow_slice(&ais[expected_ais..])?; Ok(FixedOrderAccountRetriever { - ais: fixed_ais, + ais: AccountInfoRef::borrow_slice(ais)?, n_banks: active_token_len, n_perps: active_perp_len, begin_perp: active_token_len * 2, begin_serum3: active_token_len * 2 + active_perp_len * 2, staleness_slot: Some(Clock::get()?.slot), - fallback_oracle_ais, + begin_fallback_oracles: expected_ais, }) } @@ -135,8 +133,7 @@ impl AccountRetriever for FixedOrderAccountRetriever { let oracle_index = self.n_banks + active_token_position_index; let oracle = &self.ais[oracle_index]; - let fallback_opt = self - .fallback_oracle_ais + let fallback_opt = self.ais[self.begin_fallback_oracles..] .iter() .find(|ai| ai.key() == &bank.fallback_oracle); let oracle_price_result = @@ -203,6 +200,7 @@ impl AccountRetriever for FixedOrderAccountRetriever { pub struct ScannedBanksAndOracles<'a, 'info> { banks: Vec>, oracles: Vec>, + fallback_oracles: HashMap>, index_map: HashMap, staleness_slot: Option, } @@ -229,7 +227,9 @@ 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 = self.fallback_oracles.get(&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)?; @@ -247,8 +247,12 @@ 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 = self.fallback_oracles.get(&bank1.fallback_oracle); + let fallback_oracle_opt2 = self.fallback_oracles.get(&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 { @@ -261,7 +265,10 @@ 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 = self.fallback_oracles.get(&bank.fallback_oracle); + let price = + bank.oracle_price_with_fallback(oracle, fallback_oracle_opt, self.staleness_slot)?; + Ok((bank, price)) } } @@ -272,6 +279,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> { @@ -357,11 +365,21 @@ 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() + .filter(|x| serum3_cpi::load_open_orders_ref(x).is_ok()) + .count(); + let fallback_oracles_start = serum3_start + n_serum3; + let fallback_oracles: HashMap = ais[fallback_oracles_start..] + .into_iter() + .map(|ai| (ai.key(), AccountInfoRef::borrow(ai).unwrap())) + .collect(); Ok(Self { banks_and_oracles: ScannedBanksAndOracles { banks: AccountInfoRefMut::borrow_slice(&ais[..n_banks])?, oracles: AccountInfoRef::borrow_slice(&ais[n_banks..perps_start])?, + fallback_oracles, index_map: token_index_map, staleness_slot, }, diff --git a/programs/mango-v4/src/instructions/token_edit.rs b/programs/mango-v4/src/instructions/token_edit.rs index 80e3b28fa..d00f4b41e 100644 --- a/programs/mango-v4/src/instructions/token_edit.rs +++ b/programs/mango-v4/src/instructions/token_edit.rs @@ -49,7 +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, - fallback_oracle_opt: Option, + set_fallback_oracle: bool, ) -> Result<()> { let group = ctx.accounts.group.load()?; @@ -77,14 +77,14 @@ pub fn token_edit( mint_info.oracle = oracle; require_group_admin = true; } - if let Some(fallback_oracle) = fallback_oracle_opt { + if set_fallback_oracle { msg!( "Fallback oracle old {:?}, new {:?}", bank.fallback_oracle, - fallback_oracle + ctx.accounts.fallback_oracle.key() ); - bank.fallback_oracle = fallback_oracle; - mint_info.fallback_oracle = fallback_oracle; + 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 { diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 8120d0823..535bd91ca 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -234,7 +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, - fallback_oracle_opt: Option, + set_fallback_oracle: bool, ) -> Result<()> { #[cfg(feature = "enable-gpl")] instructions::token_edit( @@ -273,7 +273,7 @@ pub mod mango_v4 { maint_weight_shift_asset_target_opt, maint_weight_shift_liab_target_opt, maint_weight_shift_abort, - fallback_oracle_opt, + set_fallback_oracle, )?; Ok(()) } diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 8c7196b94..d9d75ab60 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -294,7 +294,7 @@ 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, - fallback_oracle: existing_bank.oracle, // bongo + fallback_oracle: existing_bank.oracle, reserved: [0; 1976], } } diff --git a/programs/mango-v4/tests/cases/test_basic.rs b/programs/mango-v4/tests/cases/test_basic.rs index ade4f6658..f3d2a665f 100644 --- a/programs/mango-v4/tests/cases/test_basic.rs +++ b/programs/mango-v4/tests/cases/test_basic.rs @@ -460,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), @@ -493,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 f9c84c381..5612018a9 100644 --- a/programs/mango-v4/tests/cases/test_health_compute.rs +++ b/programs/mango-v4/tests/cases/test_health_compute.rs @@ -107,6 +107,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), diff --git a/programs/mango-v4/tests/cases/test_margin_trade.rs b/programs/mango-v4/tests/cases/test_margin_trade.rs index 663f06337..7cf2e8f70 100644 --- a/programs/mango-v4/tests/cases/test_margin_trade.rs +++ b/programs/mango-v4/tests/cases/test_margin_trade.rs @@ -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_stale_oracles.rs b/programs/mango-v4/tests/cases/test_stale_oracles.rs index 66da66b6b..5a4bae9d6 100644 --- a/programs/mango-v4/tests/cases/test_stale_oracles.rs +++ b/programs/mango-v4/tests/cases/test_stale_oracles.rs @@ -192,10 +192,6 @@ async fn test_fallback_oracle_withdraw() -> Result<(), TransportError> { let mints = &context.mints[0..3]; let payer_token_accounts = &context.users[1].token_accounts[0..3]; - // - // SETUP: Create a group, account, register tokens - //Foracle_ - let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { admin, payer, @@ -226,8 +222,9 @@ async fn test_fallback_oracle_withdraw() -> Result<(), TransportError> { group, admin, mint: mints[2].pubkey, + fallback_oracle, options: mango_v4::instruction::TokenEdit { - fallback_oracle_opt: Some(fallback_oracle), + set_fallback_oracle: true, ..token_edit_instruction_default() }, }, 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 a1fbfac25..866a8bce9 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -1276,7 +1276,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, - fallback_oracle_opt: None, + set_fallback_oracle: false, } } @@ -1284,6 +1284,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)] @@ -1312,7 +1313,7 @@ impl ClientInstruction for TokenEdit { admin: self.admin.pubkey(), mint_info: mint_info_key, oracle: mint_info.oracle, - fallback_oracle: mint_info.fallback_oracle, + fallback_oracle: self.fallback_oracle, }; let mut instruction = make_instruction(program_id, &accounts, &self.options); From aed2afbde44d72af40788112b41de537f173ffcb Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Wed, 22 Nov 2023 11:42:32 -0600 Subject: [PATCH 06/11] ensure fallbacks are only used for stale or unconfident oracles --- programs/mango-v4/src/instructions/token_edit.rs | 2 +- programs/mango-v4/src/instructions/token_register.rs | 2 +- .../mango-v4/src/instructions/token_register_trustless.rs | 2 +- programs/mango-v4/src/state/bank.rs | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/programs/mango-v4/src/instructions/token_edit.rs b/programs/mango-v4/src/instructions/token_edit.rs index d00f4b41e..fe6c294e5 100644 --- a/programs/mango-v4/src/instructions/token_edit.rs +++ b/programs/mango-v4/src/instructions/token_edit.rs @@ -467,7 +467,7 @@ pub fn token_edit( let bank = ctx.remaining_accounts.first().unwrap().load_mut::()?; bank.verify()?; - emit_stack!(TokenMetaDataLogV2 { + emit_stack(TokenMetaDataLogV2 { mango_group: ctx.accounts.group.key(), mint: mint_info.mint.key(), token_index: bank.token_index, diff --git a/programs/mango-v4/src/instructions/token_register.rs b/programs/mango-v4/src/instructions/token_register.rs index 28858a023..2af080bf6 100644 --- a/programs/mango-v4/src/instructions/token_register.rs +++ b/programs/mango-v4/src/instructions/token_register.rs @@ -152,7 +152,7 @@ pub fn token_register( mint_info.banks[0] = ctx.accounts.bank.key(); mint_info.vaults[0] = ctx.accounts.vault.key(); - emit_stack!(TokenMetaDataLogV2 { + emit_stack(TokenMetaDataLogV2 { mango_group: ctx.accounts.group.key(), mint: ctx.accounts.mint.key(), token_index, diff --git a/programs/mango-v4/src/instructions/token_register_trustless.rs b/programs/mango-v4/src/instructions/token_register_trustless.rs index 77cab62db..7ff966c8e 100644 --- a/programs/mango-v4/src/instructions/token_register_trustless.rs +++ b/programs/mango-v4/src/instructions/token_register_trustless.rs @@ -135,7 +135,7 @@ pub fn token_register_trustless( mint_info.banks[0] = ctx.accounts.bank.key(); mint_info.vaults[0] = ctx.accounts.vault.key(); - emit_stack!(TokenMetaDataLogV2 { + emit_stack(TokenMetaDataLogV2 { mango_group: ctx.accounts.group.key(), mint: ctx.accounts.mint.key(), token_index, diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index d9d75ab60..872fa8128 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -974,12 +974,12 @@ impl Bank { staleness_slot: Option, ) -> Result { let primary_price = self.oracle_price(oracle_acc, staleness_slot); - if primary_price.is_ok() || fallback_oracle_acc_opt.is_none() { - primary_price - } else { + if primary_price.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()); self.oracle_price_inner(fallback_oracle_acc, staleness_slot) + } else { + primary_price } } From 2b0a0e3a3e21181fc1eae3d74b7439ba949e14ce Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Wed, 22 Nov 2023 16:40:48 -0600 Subject: [PATCH 07/11] add fallback CU and serum tests --- .../tests/cases/test_health_compute.rs | 165 +++++++++++ programs/mango-v4/tests/cases/test_serum.rs | 267 ++++++++++++++++++ .../tests/program_test/mango_client.rs | 10 +- 3 files changed, 436 insertions(+), 6 deletions(-) diff --git a/programs/mango-v4/tests/cases/test_health_compute.rs b/programs/mango-v4/tests/cases/test_health_compute.rs index 5612018a9..f9d529741 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> { @@ -143,6 +169,145 @@ async fn test_health_compute_tokens_during_maint_weight_shift() -> Result<(), Tr 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 fallback_metas: Vec = fallback_oracle_kps + .iter() + .map(|x| AccountMeta { + pubkey: x.pubkey(), + is_signer: false, + is_writable: false, + }) + .collect(); + + // let fallback_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 cu_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(); + + cu_measurements.push( + deposit_cu_fallbacks_datapoint( + solana, + account, + owner, + *token_account, + fallback_metas.clone(), + ) + .await, + ); + } + + for (i, pair) in cu_measurements.windows(2).enumerate() { + println!( + "after adding token {}: {} (+{})", + i, + pair[1], + pair[1] - pair[0] + ); + } + + 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 < 16_600); + + Ok(()) +} + // Try to reach compute limits in health checks by having many serum markets in an account #[tokio::test] async fn test_health_compute_serum() -> Result<(), TransportError> { diff --git a/programs/mango-v4/tests/cases/test_serum.rs b/programs/mango-v4/tests/cases/test_serum.rs index 9ad854515..751232016 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; @@ -1481,6 +1482,272 @@ 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 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]; + + // + // SETUP: Create a group and an account + // + + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..GroupWithTokensConfig::default() + } + .create(solana) + .await; + let base_token = &tokens[0]; + let quote_token = &tokens[1]; + + // + // SETUP: Create a fallback oracle + // + send_tx( + solana, + StubOracleCreate { + oracle: fallback_oracle_kp, + group, + mint: mints[2].pubkey, + admin, + payer, + }, + ) + .await + .unwrap(); + + // + // SETUP: 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; + + // + // SETUP: Create account + // + let account = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + &[mints[1]], + 1_000, + 0, + ) + .await; + + // Create some token1 borrows + send_tx( + solana, + TokenWithdrawInstruction { + amount: 300, + allow_borrow: true, + account, + owner, + token_account: payer_token_accounts[2], + bank_index: 0, + }, + ) + .await + .unwrap(); + + // + // SETUP: Create serum market + // + let serum_market_cookie = context + .serum + .list_spot_market(&base_token.mint, "e_token.mint) + .await; + + // + // TEST: Register a serum market + // + let serum_market = send_tx( + solana, + Serum3RegisterMarketInstruction { + group, + admin, + serum_program: context.serum.program_id, + serum_market_external: serum_market_cookie.market, + market_index: 0, + base_bank: base_token.bank, + quote_bank: quote_token.bank, + payer, + }, + ) + .await + .unwrap() + .serum_market; + + // + // TEST: Create an open orders account + // + let open_orders = send_tx( + solana, + Serum3CreateOpenOrdersInstruction { + account, + serum_market, + owner, + payer, + }, + ) + .await + .unwrap() + .open_orders; + + let account_data = get_mango_account(solana, account).await; + assert_eq!( + account_data + .active_serum3_orders() + .map(|v| (v.open_orders, v.market_index)) + .collect::>(), + [(open_orders, 0)] + ); + + let mut order_placer = SerumOrderPlacer { + solana: solana.clone(), + serum: context.serum.clone(), + account, + owner: owner.clone(), + serum_market, + open_orders, + next_client_order_id: 0, + }; + + // 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(); + + // + // 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, 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, + 0 + ); + assert_eq!( + account_data + .token_position_by_raw_index(2) + .unwrap() + .in_use_count, + 1 + ); + 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/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 866a8bce9..0334d299c 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -62,16 +62,14 @@ pub async fn send_tx_with_extra_accounts( solana: &SolanaCookie, ix: CI, account_metas: Vec, -) -> std::result::Result { - let (accounts, mut instruction) = ix.to_instruction(solana).await; +) -> 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()]; - let result = solana + solana .process_transaction(&instructions, Some(&signers[..])) - .await?; - result.result?; - Ok(accounts) + .await } // This will return success even if the tx failed to finish From 59001b363102286cf69da23ef4f653bf3773f29e Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Tue, 28 Nov 2023 22:55:19 -0600 Subject: [PATCH 08/11] reduce CU cost of fallback oracles --- .../src/orderbook_filter.rs | 2 +- .../mango-v4/src/accounts_ix/token_edit.rs | 4 +- .../src/instructions/token_withdraw.rs | 7 +- programs/mango-v4/src/state/bank.rs | 44 +++--- programs/mango-v4/src/state/oracle.rs | 41 +++--- programs/mango-v4/src/state/perp_market.rs | 11 +- .../tests/cases/test_health_compute.rs | 58 ++++++-- programs/mango-v4/tests/cases/test_serum.rs | 132 +++--------------- 8 files changed, 127 insertions(+), 172 deletions(-) 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/programs/mango-v4/src/accounts_ix/token_edit.rs b/programs/mango-v4/src/accounts_ix/token_edit.rs index d4e6efdf6..c11638edc 100644 --- a/programs/mango-v4/src/accounts_ix/token_edit.rs +++ b/programs/mango-v4/src/accounts_ix/token_edit.rs @@ -22,8 +22,8 @@ pub struct TokenEdit<'info> { /// CHECK: The oracle can be one of several different account types pub oracle: UncheckedAccount<'info>, - /// The oracle account is optional and only used when reset_stable_price is set. + /// The fallback oracle account is optional and only used when set_fallback_oracle is true. /// - /// CHECK: The oracle can be one of several different account types + /// CHECK: The fallback oracle can be one of several different account types pub fallback_oracle: UncheckedAccount<'info>, } diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 7ed0a2bc4..d352bb469 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. + let slot_opt = Some(Clock::get()?.slot); unsafe_oracle_state.check_confidence_and_maybe_staleness( - &bank.oracle, + &bank.name(), &bank.oracle_config, - Some(Clock::get()?.slot), - )?; + 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/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 872fa8128..95db2737d 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; @@ -949,20 +950,10 @@ impl Bank { staleness_slot: Option, ) -> Result { require_keys_eq!(self.oracle, *oracle_acc.key()); - self.oracle_price_inner(oracle_acc, staleness_slot) - } - - fn oracle_price_inner( - &self, - oracle_acc: &impl KeyedAccountReader, - staleness_slot: Option, - ) -> Result { let state = oracle::oracle_state_unchecked(oracle_acc, self.mint_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.price) } @@ -973,13 +964,32 @@ impl Bank { fallback_oracle_acc_opt: Option<&impl KeyedAccountReader>, staleness_slot: Option, ) -> Result { - let primary_price = self.oracle_price(oracle_acc, staleness_slot); - if primary_price.is_oracle_error() && fallback_oracle_acc_opt.is_some() { + 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, + ); + 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()); - self.oracle_price_inner(fallback_oracle_acc, staleness_slot) + 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(|| { + oracle_log_context(&fallback_state, &self.oracle_config, staleness_slot) + })?; + Ok(fallback_state.price) } else { - primary_price + primary_ok.with_context(|| { + oracle_log_context(&primary_state, &self.oracle_config, staleness_slot) + })?; + Ok(primary_state.price) } } diff --git a/programs/mango-v4/src/state/oracle.rs b/programs/mango-v4/src/state/oracle.rs index a1ed4c35e..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(()) @@ -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_health_compute.rs b/programs/mango-v4/tests/cases/test_health_compute.rs index f9d529741..781ff8395 100644 --- a/programs/mango-v4/tests/cases/test_health_compute.rs +++ b/programs/mango-v4/tests/cases/test_health_compute.rs @@ -94,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 < 3230); + assert!(avg_cu_increase < 3350); Ok(()) } @@ -164,7 +164,7 @@ 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(()) } @@ -188,7 +188,7 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr for _ in 0..num_tokens { fallback_oracle_kps.push(TestKeypair::new()); } - let fallback_metas: Vec = fallback_oracle_kps + let success_metas: Vec = fallback_oracle_kps .iter() .map(|x| AccountMeta { pubkey: x.pubkey(), @@ -197,7 +197,7 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr }) .collect(); - // let fallback_metas = vec![]; + let failure_metas = vec![]; // // SETUP: Create a group and an account @@ -215,7 +215,8 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr let account = create_funded_account(&solana, group, owner, 0, &context.users[1], &[], 1000, 0).await; - let mut cu_measurements = vec![]; + 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; } @@ -279,19 +280,39 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr .await .unwrap(); - cu_measurements.push( + success_measurements.push( deposit_cu_fallbacks_datapoint( solana, account, owner, *token_account, - fallback_metas.clone(), + success_metas.clone(), + ) + .await, + ); + + failure_measurements.push( + deposit_cu_fallbacks_datapoint( + solana, + account, + owner, + *token_account, + failure_metas.clone(), ) .await, ); } - - for (i, pair) in cu_measurements.windows(2).enumerate() { + 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, @@ -300,10 +321,21 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr ); } - 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 < 16_600); + 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_serum.rs b/programs/mango-v4/tests/cases/test_serum.rs index 751232016..21799e25e 100644 --- a/programs/mango-v4/tests/cases/test_serum.rs +++ b/programs/mango-v4/tests/cases/test_serum.rs @@ -1491,26 +1491,27 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> { 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]; // // SETUP: Create a group and an account // - - let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { + 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, - payer, - mints: mints.to_vec(), - ..GroupWithTokensConfig::default() - } - .create(solana) - .await; - let base_token = &tokens[0]; - let quote_token = &tokens[1]; + tokens, + .. + } = group_with_tokens; // // SETUP: Create a fallback oracle @@ -1520,7 +1521,7 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> { StubOracleCreate { oracle: fallback_oracle_kp, group, - mint: mints[2].pubkey, + mint: tokens[2].mint.pubkey, admin, payer, }, @@ -1536,7 +1537,7 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> { TokenEdit { group, admin, - mint: mints[2].pubkey, + mint: tokens[2].mint.pubkey, fallback_oracle, options: mango_v4::instruction::TokenEdit { set_fallback_oracle: true, @@ -1550,41 +1551,13 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> { 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; - - // - // SETUP: Create account - // - let account = create_funded_account( - &solana, - group, - owner, - 0, - &context.users[1], - &[mints[1]], - 1_000, - 0, - ) - .await; - // Create some token1 borrows send_tx( solana, TokenWithdrawInstruction { - amount: 300, + amount: 1_500, allow_borrow: true, - account, + account: order_placer.account, owner, token_account: payer_token_accounts[2], bank_index: 0, @@ -1593,76 +1566,13 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> { .await .unwrap(); - // - // SETUP: Create serum market - // - let serum_market_cookie = context - .serum - .list_spot_market(&base_token.mint, "e_token.mint) - .await; - - // - // TEST: Register a serum market - // - let serum_market = send_tx( - solana, - Serum3RegisterMarketInstruction { - group, - admin, - serum_program: context.serum.program_id, - serum_market_external: serum_market_cookie.market, - market_index: 0, - base_bank: base_token.bank, - quote_bank: quote_token.bank, - payer, - }, - ) - .await - .unwrap() - .serum_market; - - // - // TEST: Create an open orders account - // - let open_orders = send_tx( - solana, - Serum3CreateOpenOrdersInstruction { - account, - serum_market, - owner, - payer, - }, - ) - .await - .unwrap() - .open_orders; - - let account_data = get_mango_account(solana, account).await; - assert_eq!( - account_data - .active_serum3_orders() - .map(|v| (v.open_orders, v.market_index)) - .collect::>(), - [(open_orders, 0)] - ); - - let mut order_placer = SerumOrderPlacer { - solana: solana.clone(), - serum: context.serum.clone(), - account, - owner: owner.clone(), - serum_market, - open_orders, - next_client_order_id: 0, - }; - // Make oracle invalid by increasing deviation send_tx( solana, StubOracleSetTestInstruction { oracle: tokens[2].oracle, group, - mint: mints[2].pubkey, + mint: tokens[2].mint.pubkey, admin, price: 1.0, last_update_slot: 0, @@ -1713,7 +1623,7 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> { .unwrap(); result.result.unwrap(); - let account_data = get_mango_account(solana, account).await; + let account_data = get_mango_account(solana, order_placer.account).await; assert_eq!( account_data .token_position_by_raw_index(0) @@ -1726,14 +1636,14 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> { .token_position_by_raw_index(1) .unwrap() .in_use_count, - 0 + 1 ); assert_eq!( account_data .token_position_by_raw_index(2) .unwrap() .in_use_count, - 1 + 0 ); let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap(); assert_eq!(serum_orders.base_borrows_without_fee, 0); From b19678f87424af617c7e25a7d08b649d8f24e3cd Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Wed, 29 Nov 2023 01:03:48 -0600 Subject: [PATCH 09/11] use a Vec for fallbacks in ScannedBanksAndOracles --- .../mango-v4/src/health/account_retriever.rs | 32 ++++++++++++------- .../src/instructions/token_withdraw.rs | 10 +++--- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/programs/mango-v4/src/health/account_retriever.rs b/programs/mango-v4/src/health/account_retriever.rs index 02a036f1f..686fdc3fe 100644 --- a/programs/mango-v4/src/health/account_retriever.rs +++ b/programs/mango-v4/src/health/account_retriever.rs @@ -200,7 +200,7 @@ impl AccountRetriever for FixedOrderAccountRetriever { pub struct ScannedBanksAndOracles<'a, 'info> { banks: Vec>, oracles: Vec>, - fallback_oracles: HashMap>, + fallback_oracles: Vec>, index_map: HashMap, staleness_slot: Option, } @@ -227,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 fallback_oracle_opt = self.fallback_oracles.get(&bank.fallback_oracle); + let fallback_oracle_opt = self + .fallback_oracles + .iter() + .find(|ai| ai.key() == &bank.fallback_oracle); let price = bank.oracle_price_with_fallback(oracle, fallback_oracle_opt, self.staleness_slot)?; return Ok((bank, price, None)); @@ -247,8 +250,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 fallback_oracle_opt1 = self.fallback_oracles.get(&bank1.fallback_oracle); - let fallback_oracle_opt2 = self.fallback_oracles.get(&bank2.fallback_oracle); + let fallback_oracle_opt1 = self + .fallback_oracles + .iter() + .find(|ai| ai.key() == &bank1.fallback_oracle); + let fallback_oracle_opt2 = self + .fallback_oracles + .iter() + .find(|ai| ai.key() == &bank2.fallback_oracle); let price1 = bank1.oracle_price_with_fallback(oracle1, fallback_oracle_opt1, self.staleness_slot)?; let price2 = @@ -265,7 +274,10 @@ 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 fallback_oracle_opt = self.fallback_oracles.get(&bank.fallback_oracle); + let fallback_oracle_opt = self + .fallback_oracles + .iter() + .find(|ai| ai.key() == &bank.fallback_oracle); let price = bank.oracle_price_with_fallback(oracle, fallback_oracle_opt, self.staleness_slot)?; @@ -367,25 +379,21 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> { let serum3_start = perp_oracles_start + n_perps; let n_serum3 = ais[serum3_start..] .iter() - .filter(|x| serum3_cpi::load_open_orders_ref(x).is_ok()) + .filter(|x| x.data_len() == std::mem::size_of::() + 12) .count(); let fallback_oracles_start = serum3_start + n_serum3; - let fallback_oracles: HashMap = ais[fallback_oracles_start..] - .into_iter() - .map(|ai| (ai.key(), AccountInfoRef::borrow(ai).unwrap())) - .collect(); Ok(Self { banks_and_oracles: ScannedBanksAndOracles { banks: AccountInfoRefMut::borrow_slice(&ais[..n_banks])?, oracles: AccountInfoRef::borrow_slice(&ais[n_banks..perps_start])?, - fallback_oracles, + 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, }) } diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index d352bb469..42de9b5e6 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -182,11 +182,11 @@ 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); - 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))?; + 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)?; } From ab9d3c37b45197f159f1393b5da0f1ee316a9467 Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Thu, 30 Nov 2023 11:03:24 -0600 Subject: [PATCH 10/11] check serum OO headers in ScanningAccountRetriever --- programs/mango-v4/src/health/account_retriever.rs | 5 ++++- programs/mango-v4/src/serum3_cpi.rs | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/programs/mango-v4/src/health/account_retriever.rs b/programs/mango-v4/src/health/account_retriever.rs index 686fdc3fe..5c9a3a6e8 100644 --- a/programs/mango-v4/src/health/account_retriever.rs +++ b/programs/mango-v4/src/health/account_retriever.rs @@ -379,7 +379,10 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> { let serum3_start = perp_oracles_start + n_perps; let n_serum3 = ais[serum3_start..] .iter() - .filter(|x| x.data_len() == std::mem::size_of::() + 12) + .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; diff --git a/programs/mango-v4/src/serum3_cpi.rs b/programs/mango-v4/src/serum3_cpi.rs index 3084c5585..d5694d64f 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, From 25aa422e2dccf3654dcc190366b8ac08cd324fb3 Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Fri, 1 Dec 2023 12:44:35 -0600 Subject: [PATCH 11/11] also log primary oracle when fallback errors --- .../mango-v4/src/health/account_retriever.rs | 32 +++++++++---------- programs/mango-v4/src/state/bank.rs | 6 +++- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/programs/mango-v4/src/health/account_retriever.rs b/programs/mango-v4/src/health/account_retriever.rs index 5c9a3a6e8..73b7dde2a 100644 --- a/programs/mango-v4/src/health/account_retriever.rs +++ b/programs/mango-v4/src/health/account_retriever.rs @@ -227,10 +227,8 @@ 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 fallback_oracle_opt = self - .fallback_oracles - .iter() - .find(|ai| ai.key() == &bank.fallback_oracle); + 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)); @@ -250,14 +248,10 @@ 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 fallback_oracle_opt1 = self - .fallback_oracles - .iter() - .find(|ai| ai.key() == &bank1.fallback_oracle); - let fallback_oracle_opt2 = self - .fallback_oracles - .iter() - .find(|ai| ai.key() == &bank2.fallback_oracle); + 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 = @@ -274,10 +268,8 @@ 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 fallback_oracle_opt = self - .fallback_oracles - .iter() - .find(|ai| ai.key() == &bank.fallback_oracle); + 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)?; @@ -473,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/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 95db2737d..219ee4209 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -982,7 +982,11 @@ impl Bank { staleness_slot, ); fallback_ok.with_context(|| { - oracle_log_context(&fallback_state, &self.oracle_config, staleness_slot) + 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 {