From 59001b363102286cf69da23ef4f653bf3773f29e Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Tue, 28 Nov 2023 22:55:19 -0600 Subject: [PATCH] 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);