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
.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
)

View File

@ -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>,
}

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
// 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)?;
}

View File

@ -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<u64>,
) -> Result<I80F48> {
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)?;
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<u64>,
) -> Result<I80F48> {
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)
}
}

View File

@ -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<u64>,
) -> 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::<f64>(),
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::<f64>(),
self.deviation.to_num::<f64>(),
config.conf_filter.to_num::<f32>(),
);
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<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)]
mod tests {
use super::*;

View File

@ -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<OracleState> {
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)
}

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>()
/ (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::<u64>()
/ (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<AccountMeta> = fallback_oracle_kps
let success_metas: Vec<AccountMeta> = 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::<u64>()
/ (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::<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(())
}

View File

@ -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, &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
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);