reduce CU cost of fallback oracles

This commit is contained in:
Lou-Kamades 2023-11-28 22:55:19 -06:00
parent 2b0a0e3a3e
commit 59001b3631
8 changed files with 127 additions and 172 deletions

View File

@ -373,7 +373,7 @@ pub async fn init(
{ {
if unchecked_oracle_state if unchecked_oracle_state
.check_confidence_and_maybe_staleness( .check_confidence_and_maybe_staleness(
&oracle_pk, &mkt.1.name,
&oracle_config.to_oracle_config(), &oracle_config.to_oracle_config(),
None, // force this to always return a price no matter how stale None, // force this to always return a price no matter how stale
) )

View File

@ -22,8 +22,8 @@ pub struct TokenEdit<'info> {
/// CHECK: The oracle can be one of several different account types /// CHECK: The oracle can be one of several different account types
pub oracle: UncheckedAccount<'info>, 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>, pub fallback_oracle: UncheckedAccount<'info>,
} }

View File

@ -181,11 +181,12 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
// When borrowing the price has be trustworthy, so we can do a reasonable // When borrowing the price has be trustworthy, so we can do a reasonable
// net borrow check. // net borrow check.
let slot_opt = Some(Clock::get()?.slot);
unsafe_oracle_state.check_confidence_and_maybe_staleness( unsafe_oracle_state.check_confidence_and_maybe_staleness(
&bank.oracle, &bank.name(),
&bank.oracle_config, &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)?; bank.check_net_borrows(unsafe_oracle_state.price)?;
} }

View File

@ -9,6 +9,7 @@ use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount; use anchor_spl::token::TokenAccount;
use derivative::Derivative; use derivative::Derivative;
use fixed::types::I80F48; use fixed::types::I80F48;
use oracle::oracle_log_context;
use static_assertions::const_assert_eq; use static_assertions::const_assert_eq;
use std::mem::size_of; use std::mem::size_of;
@ -949,20 +950,10 @@ impl Bank {
staleness_slot: Option<u64>, staleness_slot: Option<u64>,
) -> Result<I80F48> { ) -> Result<I80F48> {
require_keys_eq!(self.oracle, *oracle_acc.key()); 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<u64>,
) -> Result<I80F48> {
let state = oracle::oracle_state_unchecked(oracle_acc, self.mint_decimals)?; let state = oracle::oracle_state_unchecked(oracle_acc, self.mint_decimals)?;
state.check_confidence_and_maybe_staleness( state
&self.oracle, .check_confidence_and_maybe_staleness(&self.name(), &self.oracle_config, staleness_slot)
&self.oracle_config, .with_context(|| oracle_log_context(&state, &self.oracle_config, staleness_slot))?;
staleness_slot,
)?;
Ok(state.price) Ok(state.price)
} }
@ -973,13 +964,32 @@ impl Bank {
fallback_oracle_acc_opt: Option<&impl KeyedAccountReader>, fallback_oracle_acc_opt: Option<&impl KeyedAccountReader>,
staleness_slot: Option<u64>, staleness_slot: Option<u64>,
) -> Result<I80F48> { ) -> Result<I80F48> {
let primary_price = self.oracle_price(oracle_acc, staleness_slot); require_keys_eq!(self.oracle, *oracle_acc.key());
if primary_price.is_oracle_error() && fallback_oracle_acc_opt.is_some() { 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(); let fallback_oracle_acc = fallback_oracle_acc_opt.unwrap();
require_keys_eq!(self.fallback_oracle, *fallback_oracle_acc.key()); 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 { } else {
primary_price primary_ok.with_context(|| {
oracle_log_context(&primary_state, &self.oracle_config, staleness_slot)
})?;
Ok(primary_state.price)
} }
} }

View File

@ -105,19 +105,19 @@ impl OracleState {
#[inline] #[inline]
pub fn check_confidence_and_maybe_staleness( pub fn check_confidence_and_maybe_staleness(
&self, &self,
oracle_pk: &Pubkey, oracle_name: &str,
config: &OracleConfig, config: &OracleConfig,
staleness_slot: Option<u64>, staleness_slot: Option<u64>,
) -> Result<()> { ) -> Result<()> {
if let Some(now_slot) = staleness_slot { 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( pub fn check_staleness(
&self, &self,
oracle_pk: &Pubkey, oracle_name: &str,
config: &OracleConfig, config: &OracleConfig,
now_slot: u64, now_slot: u64,
) -> Result<()> { ) -> Result<()> {
@ -127,27 +127,15 @@ impl OracleState {
.saturating_add(config.max_staleness_slots as u64) .saturating_add(config.max_staleness_slots as u64)
< now_slot < now_slot
{ {
msg!( msg!("Oracle is stale: {}", oracle_name);
"Oracle is stale; pubkey {}, price: {}, last_update_slot: {}, now_slot: {}",
oracle_pk,
self.price.to_num::<f64>(),
self.last_update_slot,
now_slot,
);
return Err(MangoError::OracleStale.into()); return Err(MangoError::OracleStale.into());
} }
Ok(()) 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 { if self.deviation > config.conf_filter * self.price {
msg!( msg!("Oracle confidence not good enough: {}", oracle_name);
"Oracle confidence not good enough: pubkey {}, price: {}, deviation: {}, conf_filter: {}",
oracle_pk,
self.price.to_num::<f64>(),
self.deviation.to_num::<f64>(),
config.conf_filter.to_num::<f32>(),
);
return Err(MangoError::OracleConfidence.into()); return Err(MangoError::OracleConfidence.into());
} }
Ok(()) Ok(())
@ -332,6 +320,21 @@ pub fn oracle_state_unchecked(
}) })
} }
pub fn oracle_log_context(
state: &OracleState,
oracle_config: &OracleConfig,
staleness_slot: Option<u64>,
) -> String {
format!(
"price: {}, deviation: {}, last_update_slot: {}, now_slot: {}, conf_filter: {:#?}",
state.price.to_num::<f64>(),
state.deviation.to_num::<f64>(),
state.last_update_slot,
staleness_slot.unwrap_or_else(|| u64::MAX),
oracle_config.conf_filter.to_num::<f32>(),
)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -4,10 +4,11 @@ use anchor_lang::prelude::*;
use derivative::Derivative; use derivative::Derivative;
use fixed::types::I80F48; use fixed::types::I80F48;
use oracle::oracle_log_context;
use static_assertions::const_assert_eq; use static_assertions::const_assert_eq;
use crate::accounts_zerocopy::KeyedAccountReader; use crate::accounts_zerocopy::KeyedAccountReader;
use crate::error::MangoError; use crate::error::{Contextable, MangoError};
use crate::logs::{emit_stack, PerpUpdateFundingLogV2}; use crate::logs::{emit_stack, PerpUpdateFundingLogV2};
use crate::state::orderbook::Side; use crate::state::orderbook::Side;
use crate::state::{oracle, TokenIndex}; use crate::state::{oracle, TokenIndex};
@ -275,11 +276,9 @@ impl PerpMarket {
) -> Result<OracleState> { ) -> Result<OracleState> {
require_keys_eq!(self.oracle, *oracle_acc.key()); require_keys_eq!(self.oracle, *oracle_acc.key());
let state = oracle::oracle_state_unchecked(oracle_acc, self.base_decimals)?; let state = oracle::oracle_state_unchecked(oracle_acc, self.base_decimals)?;
state.check_confidence_and_maybe_staleness( state
&self.oracle, .check_confidence_and_maybe_staleness(&self.name(), &self.oracle_config, staleness_slot)
&self.oracle_config, .with_context(|| oracle_log_context(&state, &self.oracle_config, staleness_slot))?;
staleness_slot,
)?;
Ok(state) Ok(state)
} }

View File

@ -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::<u64>() let avg_cu_increase = cu_measurements.windows(2).map(|p| p[1] - p[0]).sum::<u64>()
/ (cu_measurements.len() - 1) as u64; / (cu_measurements.len() - 1) as u64;
println!("average cu increase: {avg_cu_increase}"); println!("average cu increase: {avg_cu_increase}");
assert!(avg_cu_increase < 3230); assert!(avg_cu_increase < 3350);
Ok(()) 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::<u64>() let avg_cu_increase = cu_measurements.windows(2).map(|p| p[1] - p[0]).sum::<u64>()
/ (cu_measurements.len() - 1) as u64; / (cu_measurements.len() - 1) as u64;
println!("average cu increase: {avg_cu_increase}"); println!("average cu increase: {avg_cu_increase}");
assert!(avg_cu_increase < 4200); assert!(avg_cu_increase < 4300);
Ok(()) Ok(())
} }
@ -188,7 +188,7 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr
for _ in 0..num_tokens { for _ in 0..num_tokens {
fallback_oracle_kps.push(TestKeypair::new()); fallback_oracle_kps.push(TestKeypair::new());
} }
let fallback_metas: Vec<AccountMeta> = fallback_oracle_kps let success_metas: Vec<AccountMeta> = fallback_oracle_kps
.iter() .iter()
.map(|x| AccountMeta { .map(|x| AccountMeta {
pubkey: x.pubkey(), pubkey: x.pubkey(),
@ -197,7 +197,7 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr
}) })
.collect(); .collect();
// let fallback_metas = vec![]; let failure_metas = vec![];
// //
// SETUP: Create a group and an account // SETUP: Create a group and an account
@ -215,7 +215,8 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr
let account = let account =
create_funded_account(&solana, group, owner, 0, &context.users[1], &[], 1000, 0).await; 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()] { for token_account in &context.users[0].token_accounts[..mints.len()] {
deposit_cu_datapoint(solana, account, owner, *token_account).await; deposit_cu_datapoint(solana, account, owner, *token_account).await;
} }
@ -279,19 +280,39 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr
.await .await
.unwrap(); .unwrap();
cu_measurements.push( success_measurements.push(
deposit_cu_fallbacks_datapoint( deposit_cu_fallbacks_datapoint(
solana, solana,
account, account,
owner, owner,
*token_account, *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, .await,
); );
} }
println!("successful fallbacks:");
for (i, pair) in cu_measurements.windows(2).enumerate() { 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!( println!(
"after adding token {}: {} (+{})", "after adding token {}: {} (+{})",
i, 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::<u64>() let avg_success_increase = success_measurements
/ (cu_measurements.len() - 1) as u64; .windows(2)
println!("average cu increase: {avg_cu_increase}"); .map(|p| p[1] - p[0])
assert!(avg_cu_increase < 16_600); .sum::<u64>()
/ (success_measurements.len() - 1) as u64;
let avg_failure_increase = failure_measurements
.windows(2)
.map(|p| p[1] - p[0])
.sum::<u64>()
/ (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(()) Ok(())
} }

View File

@ -1491,26 +1491,27 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> {
let fallback_oracle_kp = TestKeypair::new(); let fallback_oracle_kp = TestKeypair::new();
let fallback_oracle = fallback_oracle_kp.pubkey(); let fallback_oracle = fallback_oracle_kp.pubkey();
let admin = TestKeypair::new();
let owner = context.users[0].key; let owner = context.users[0].key;
let payer = context.users[1].key; let payer = context.users[1].key;
let mints = &context.mints[0..3];
let payer_token_accounts = &context.users[1].token_accounts[0..3]; let payer_token_accounts = &context.users[1].token_accounts[0..3];
// //
// SETUP: Create a group and an account // SETUP: Create a group and an account
// //
let deposit_amount = 1_000;
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { let CommonSetup {
group_with_tokens,
quote_token,
base_token,
mut order_placer,
..
} = common_setup(&context, deposit_amount).await;
let GroupWithTokens {
group,
admin, admin,
payer, tokens,
mints: mints.to_vec(), ..
..GroupWithTokensConfig::default() } = group_with_tokens;
}
.create(solana)
.await;
let base_token = &tokens[0];
let quote_token = &tokens[1];
// //
// SETUP: Create a fallback oracle // SETUP: Create a fallback oracle
@ -1520,7 +1521,7 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> {
StubOracleCreate { StubOracleCreate {
oracle: fallback_oracle_kp, oracle: fallback_oracle_kp,
group, group,
mint: mints[2].pubkey, mint: tokens[2].mint.pubkey,
admin, admin,
payer, payer,
}, },
@ -1536,7 +1537,7 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> {
TokenEdit { TokenEdit {
group, group,
admin, admin,
mint: mints[2].pubkey, mint: tokens[2].mint.pubkey,
fallback_oracle, fallback_oracle,
options: mango_v4::instruction::TokenEdit { options: mango_v4::instruction::TokenEdit {
set_fallback_oracle: true, 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; let bank_data: Bank = solana.get_account(tokens[2].bank).await;
assert!(bank_data.fallback_oracle == fallback_oracle); 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 // Create some token1 borrows
send_tx( send_tx(
solana, solana,
TokenWithdrawInstruction { TokenWithdrawInstruction {
amount: 300, amount: 1_500,
allow_borrow: true, allow_borrow: true,
account, account: order_placer.account,
owner, owner,
token_account: payer_token_accounts[2], token_account: payer_token_accounts[2],
bank_index: 0, bank_index: 0,
@ -1593,76 +1566,13 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> {
.await .await
.unwrap(); .unwrap();
//
// SETUP: Create serum market
//
let serum_market_cookie = context
.serum
.list_spot_market(&base_token.mint, &quote_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::<Vec<_>>(),
[(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 // Make oracle invalid by increasing deviation
send_tx( send_tx(
solana, solana,
StubOracleSetTestInstruction { StubOracleSetTestInstruction {
oracle: tokens[2].oracle, oracle: tokens[2].oracle,
group, group,
mint: mints[2].pubkey, mint: tokens[2].mint.pubkey,
admin, admin,
price: 1.0, price: 1.0,
last_update_slot: 0, last_update_slot: 0,
@ -1713,7 +1623,7 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> {
.unwrap(); .unwrap();
result.result.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!( assert_eq!(
account_data account_data
.token_position_by_raw_index(0) .token_position_by_raw_index(0)
@ -1726,14 +1636,14 @@ async fn test_fallback_oracle_serum() -> Result<(), TransportError> {
.token_position_by_raw_index(1) .token_position_by_raw_index(1)
.unwrap() .unwrap()
.in_use_count, .in_use_count,
0 1
); );
assert_eq!( assert_eq!(
account_data account_data
.token_position_by_raw_index(2) .token_position_by_raw_index(2)
.unwrap() .unwrap()
.in_use_count, .in_use_count,
1 0
); );
let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap(); 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.base_borrows_without_fee, 0);