Oracle staleness: Check when accessing oracle price
This commit is contained in:
parent
58f7ff2e0e
commit
2ee152f7ea
|
@ -28,6 +28,7 @@ pub fn new(
|
||||||
n_perps: active_perp_len,
|
n_perps: active_perp_len,
|
||||||
begin_perp: active_token_len * 2,
|
begin_perp: active_token_len * 2,
|
||||||
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
||||||
|
staleness_slot: None,
|
||||||
};
|
};
|
||||||
mango_v4::state::new_health_cache(&account.borrow(), &retriever).context("make health cache")
|
mango_v4::state::new_health_cache(&account.borrow(), &retriever).context("make health cache")
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,10 @@ pub fn fetch_top(
|
||||||
let perp_market =
|
let perp_market =
|
||||||
account_fetcher_fetch_anchor_account::<PerpMarket>(account_fetcher, &perp.address)?;
|
account_fetcher_fetch_anchor_account::<PerpMarket>(account_fetcher, &perp.address)?;
|
||||||
let oracle_acc = account_fetcher.fetch_raw_account(&perp_market.oracle)?;
|
let oracle_acc = account_fetcher.fetch_raw_account(&perp_market.oracle)?;
|
||||||
let oracle_price =
|
let oracle_price = perp_market.oracle_price(
|
||||||
perp_market.oracle_price(&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc))?;
|
&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
let accounts =
|
let accounts =
|
||||||
account_fetcher.fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator())?;
|
account_fetcher.fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator())?;
|
||||||
|
|
|
@ -161,10 +161,10 @@ impl<'a> LiquidateHelper<'a> {
|
||||||
let oracle = self
|
let oracle = self
|
||||||
.account_fetcher
|
.account_fetcher
|
||||||
.fetch_raw_account(&perp.market.oracle)?;
|
.fetch_raw_account(&perp.market.oracle)?;
|
||||||
let price = perp.market.oracle_price(&KeyedAccountSharedData::new(
|
let price = perp.market.oracle_price(
|
||||||
perp.market.oracle,
|
&KeyedAccountSharedData::new(perp.market.oracle, oracle.into()),
|
||||||
oracle.into(),
|
None,
|
||||||
))?;
|
)?;
|
||||||
Ok(Some((
|
Ok(Some((
|
||||||
pp.market_index,
|
pp.market_index,
|
||||||
base_lots,
|
base_lots,
|
||||||
|
@ -342,10 +342,10 @@ impl<'a> LiquidateHelper<'a> {
|
||||||
let oracle = self
|
let oracle = self
|
||||||
.account_fetcher
|
.account_fetcher
|
||||||
.fetch_raw_account(&token.mint_info.oracle)?;
|
.fetch_raw_account(&token.mint_info.oracle)?;
|
||||||
let price = bank.oracle_price(&KeyedAccountSharedData::new(
|
let price = bank.oracle_price(
|
||||||
token.mint_info.oracle,
|
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
||||||
oracle.into(),
|
None,
|
||||||
))?;
|
)?;
|
||||||
Ok((
|
Ok((
|
||||||
token_position.token_index,
|
token_position.token_index,
|
||||||
price,
|
price,
|
||||||
|
|
|
@ -45,10 +45,10 @@ impl TokenState {
|
||||||
account_fetcher: &chain_data::AccountFetcher,
|
account_fetcher: &chain_data::AccountFetcher,
|
||||||
) -> anyhow::Result<I80F48> {
|
) -> anyhow::Result<I80F48> {
|
||||||
let oracle = account_fetcher.fetch_raw_account(&token.mint_info.oracle)?;
|
let oracle = account_fetcher.fetch_raw_account(&token.mint_info.oracle)?;
|
||||||
bank.oracle_price(&KeyedAccountSharedData::new(
|
bank.oracle_price(
|
||||||
token.mint_info.oracle,
|
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
||||||
oracle.into(),
|
None,
|
||||||
))
|
)
|
||||||
.map_err_anyhow()
|
.map_err_anyhow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,10 @@ pub enum MangoError {
|
||||||
MaxSettleAmountMustBeGreaterThanZero,
|
MaxSettleAmountMustBeGreaterThanZero,
|
||||||
#[msg("the perp position has open orders or unprocessed fill events")]
|
#[msg("the perp position has open orders or unprocessed fill events")]
|
||||||
HasOpenPerpOrders,
|
HasOpenPerpOrders,
|
||||||
|
#[msg("an oracle does not reach the confidence threshold")]
|
||||||
|
OracleConfidence,
|
||||||
|
#[msg("an oracle is stale")]
|
||||||
|
OracleStale,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Contextable {
|
pub trait Contextable {
|
||||||
|
|
|
@ -82,8 +82,10 @@ pub fn perp_liq_base_position(
|
||||||
let base_lot_size = I80F48::from(perp_market.base_lot_size);
|
let base_lot_size = I80F48::from(perp_market.base_lot_size);
|
||||||
|
|
||||||
// Get oracle price for market. Price is validated inside
|
// Get oracle price for market. Price is validated inside
|
||||||
let oracle_price =
|
let oracle_price = perp_market.oracle_price(
|
||||||
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||||
|
None, // checked in health
|
||||||
|
)?;
|
||||||
let price_per_lot = cm!(base_lot_size * oracle_price);
|
let price_per_lot = cm!(base_lot_size * oracle_price);
|
||||||
|
|
||||||
// Fetch perp positions for accounts, creating for the liqor if needed
|
// Fetch perp positions for accounts, creating for the liqor if needed
|
||||||
|
|
|
@ -50,8 +50,10 @@ pub fn perp_place_order(ctx: Context<PerpPlaceOrder>, order: Order, limit: u8) -
|
||||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||||
let book = ctx.accounts.orderbook.load_mut()?;
|
let book = ctx.accounts.orderbook.load_mut()?;
|
||||||
|
|
||||||
oracle_price =
|
oracle_price = perp_market.oracle_price(
|
||||||
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||||
|
None, // staleness checked in health
|
||||||
|
)?;
|
||||||
|
|
||||||
perp_market.update_funding(&book, oracle_price, now_ts)?;
|
perp_market.update_funding(&book, oracle_price, now_ts)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,10 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get oracle price for market. Price is validated inside
|
// Get oracle price for market. Price is validated inside
|
||||||
let oracle_price =
|
let oracle_price = perp_market.oracle_price(
|
||||||
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||||
|
None, // staleness checked in health
|
||||||
|
)?;
|
||||||
|
|
||||||
// Fetch perp positions for accounts
|
// Fetch perp positions for accounts
|
||||||
let perp_position = account.perp_position_mut(perp_market.perp_market_index)?;
|
let perp_position = account.perp_position_mut(perp_market.perp_market_index)?;
|
||||||
|
|
|
@ -101,8 +101,10 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get oracle price for market. Price is validated inside
|
// Get oracle price for market. Price is validated inside
|
||||||
let oracle_price =
|
let oracle_price = perp_market.oracle_price(
|
||||||
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||||
|
None, // staleness checked in health
|
||||||
|
)?;
|
||||||
|
|
||||||
// Fetch perp positions for accounts
|
// Fetch perp positions for accounts
|
||||||
let a_perp_position = account_a.perp_position_mut(perp_market_index)?;
|
let a_perp_position = account_a.perp_position_mut(perp_market_index)?;
|
||||||
|
|
|
@ -26,8 +26,11 @@ pub fn perp_update_funding(ctx: Context<PerpUpdateFunding>) -> Result<()> {
|
||||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||||
let book = ctx.accounts.orderbook.load_mut()?;
|
let book = ctx.accounts.orderbook.load_mut()?;
|
||||||
|
|
||||||
let oracle_price =
|
let now_slot = Clock::get()?.slot;
|
||||||
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
let oracle_price = perp_market.oracle_price(
|
||||||
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||||
|
Some(now_slot),
|
||||||
|
)?;
|
||||||
|
|
||||||
perp_market.update_funding(&book, oracle_price, now_ts)?;
|
perp_market.update_funding(&book, oracle_price, now_ts)?;
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,10 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
||||||
|
|
||||||
let indexed_position = position.indexed_position;
|
let indexed_position = position.indexed_position;
|
||||||
let bank = self.bank.load()?;
|
let bank = self.bank.load()?;
|
||||||
let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(self.oracle.as_ref())?)?;
|
let oracle_price = bank.oracle_price(
|
||||||
|
&AccountInfoRef::borrow(self.oracle.as_ref())?,
|
||||||
|
None, // staleness checked in health
|
||||||
|
)?;
|
||||||
|
|
||||||
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
|
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
|
||||||
let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::<i64>();
|
let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::<i64>();
|
||||||
|
|
|
@ -78,7 +78,8 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
|
||||||
.load()?
|
.load()?
|
||||||
.verify_banks_ais(ctx.remaining_accounts)?;
|
.verify_banks_ais(ctx.remaining_accounts)?;
|
||||||
|
|
||||||
let now_ts = Clock::get()?.unix_timestamp;
|
let clock = Clock::get()?;
|
||||||
|
let now_ts = clock.unix_timestamp;
|
||||||
|
|
||||||
// compute indexed_total
|
// compute indexed_total
|
||||||
let mut indexed_total_deposits = I80F48::ZERO;
|
let mut indexed_total_deposits = I80F48::ZERO;
|
||||||
|
@ -106,8 +107,10 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
|
||||||
now_ts,
|
now_ts,
|
||||||
);
|
);
|
||||||
|
|
||||||
let price =
|
let price = some_bank.oracle_price(
|
||||||
some_bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||||
|
Some(clock.slot),
|
||||||
|
)?;
|
||||||
emit!(UpdateIndexLog {
|
emit!(UpdateIndexLog {
|
||||||
mango_group: mint_info.group.key(),
|
mango_group: mint_info.group.key(),
|
||||||
token_index: mint_info.token_index,
|
token_index: mint_info.token_index,
|
||||||
|
|
|
@ -122,7 +122,11 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let native_position_after = position.native(&bank);
|
let native_position_after = position.native(&bank);
|
||||||
let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
let now_slot = Clock::get()?.slot;
|
||||||
|
let oracle_price = bank.oracle_price(
|
||||||
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||||
|
Some(now_slot),
|
||||||
|
)?;
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit!(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
|
|
|
@ -583,9 +583,18 @@ impl Bank {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn oracle_price(&self, oracle_acc: &impl KeyedAccountReader) -> Result<I80F48> {
|
pub fn oracle_price(
|
||||||
|
&self,
|
||||||
|
oracle_acc: &impl KeyedAccountReader,
|
||||||
|
staleness_slot: Option<u64>,
|
||||||
|
) -> Result<I80F48> {
|
||||||
require_keys_eq!(self.oracle, *oracle_acc.key());
|
require_keys_eq!(self.oracle, *oracle_acc.key());
|
||||||
oracle::oracle_price(oracle_acc, &self.oracle_config, self.mint_decimals)
|
oracle::oracle_price(
|
||||||
|
oracle_acc,
|
||||||
|
&self.oracle_config,
|
||||||
|
self.mint_decimals,
|
||||||
|
staleness_slot,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,7 @@ pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
|
||||||
pub n_perps: usize,
|
pub n_perps: usize,
|
||||||
pub begin_perp: usize,
|
pub begin_perp: usize,
|
||||||
pub begin_serum3: usize,
|
pub begin_serum3: usize,
|
||||||
|
pub staleness_slot: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_fixed_order_account_retriever<'a, 'info>(
|
pub fn new_fixed_order_account_retriever<'a, 'info>(
|
||||||
|
@ -85,6 +86,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
|
||||||
n_perps: active_perp_len,
|
n_perps: active_perp_len,
|
||||||
begin_perp: cm!(active_token_len * 2),
|
begin_perp: cm!(active_token_len * 2),
|
||||||
begin_serum3: cm!(active_token_len * 2 + active_perp_len * 2),
|
begin_serum3: cm!(active_token_len * 2 + active_perp_len * 2),
|
||||||
|
staleness_slot: Some(Clock::get()?.slot),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,12 +113,12 @@ impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
|
||||||
|
|
||||||
fn oracle_price(&self, account_index: usize, bank: &Bank) -> Result<I80F48> {
|
fn oracle_price(&self, account_index: usize, bank: &Bank) -> Result<I80F48> {
|
||||||
let oracle = &self.ais[cm!(self.n_banks + account_index)];
|
let oracle = &self.ais[cm!(self.n_banks + account_index)];
|
||||||
bank.oracle_price(oracle)
|
bank.oracle_price(oracle, self.staleness_slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn oracle_price_perp(&self, account_index: usize, perp_market: &PerpMarket) -> Result<I80F48> {
|
fn oracle_price_perp(&self, account_index: usize, perp_market: &PerpMarket) -> Result<I80F48> {
|
||||||
let oracle = &self.ais[self.begin_perp + self.n_perps + account_index];
|
let oracle = &self.ais[self.begin_perp + self.n_perps + account_index];
|
||||||
perp_market.oracle_price(oracle)
|
perp_market.oracle_price(oracle, self.staleness_slot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,6 +212,7 @@ pub struct ScanningAccountRetriever<'a, 'info> {
|
||||||
serum3_oos: Vec<AccountInfoRef<'a, 'info>>,
|
serum3_oos: Vec<AccountInfoRef<'a, 'info>>,
|
||||||
token_index_map: HashMap<TokenIndex, usize>,
|
token_index_map: HashMap<TokenIndex, usize>,
|
||||||
perp_index_map: HashMap<PerpMarketIndex, usize>,
|
perp_index_map: HashMap<PerpMarketIndex, usize>,
|
||||||
|
staleness_slot: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns None if `ai` doesn't have the owner or discriminator for T
|
// Returns None if `ai` doesn't have the owner or discriminator for T
|
||||||
|
@ -232,6 +235,14 @@ fn can_load_as<'a, T: ZeroCopy + Owner>(
|
||||||
|
|
||||||
impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
||||||
pub fn new(ais: &'a [AccountInfo<'info>], group: &Pubkey) -> Result<Self> {
|
pub fn new(ais: &'a [AccountInfo<'info>], group: &Pubkey) -> Result<Self> {
|
||||||
|
Self::new_with_staleness(ais, group, Some(Clock::get()?.slot))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_staleness(
|
||||||
|
ais: &'a [AccountInfo<'info>],
|
||||||
|
group: &Pubkey,
|
||||||
|
staleness_slot: Option<u64>,
|
||||||
|
) -> Result<Self> {
|
||||||
// find all Bank accounts
|
// find all Bank accounts
|
||||||
let mut token_index_map = HashMap::with_capacity(ais.len() / 2);
|
let mut token_index_map = HashMap::with_capacity(ais.len() / 2);
|
||||||
ais.iter()
|
ais.iter()
|
||||||
|
@ -283,6 +294,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
||||||
serum3_oos: AccountInfoRef::borrow_slice(&ais[serum3_start..])?,
|
serum3_oos: AccountInfoRef::borrow_slice(&ais[serum3_start..])?,
|
||||||
token_index_map,
|
token_index_map,
|
||||||
perp_index_map,
|
perp_index_map,
|
||||||
|
staleness_slot,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,7 +324,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
||||||
let index = self.bank_index(token_index1)?;
|
let index = self.bank_index(token_index1)?;
|
||||||
let bank = self.banks[index].load_mut_fully_unchecked::<Bank>()?;
|
let bank = self.banks[index].load_mut_fully_unchecked::<Bank>()?;
|
||||||
let oracle = &self.oracles[index];
|
let oracle = &self.oracles[index];
|
||||||
let price = bank.oracle_price(oracle)?;
|
let price = bank.oracle_price(oracle, self.staleness_slot)?;
|
||||||
return Ok((bank, price, None));
|
return Ok((bank, price, None));
|
||||||
}
|
}
|
||||||
let index1 = self.bank_index(token_index1)?;
|
let index1 = self.bank_index(token_index1)?;
|
||||||
|
@ -330,8 +342,8 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
||||||
let bank2 = second_bank_part[second - (first + 1)].load_mut_fully_unchecked::<Bank>()?;
|
let bank2 = second_bank_part[second - (first + 1)].load_mut_fully_unchecked::<Bank>()?;
|
||||||
let oracle1 = &self.oracles[first];
|
let oracle1 = &self.oracles[first];
|
||||||
let oracle2 = &self.oracles[second];
|
let oracle2 = &self.oracles[second];
|
||||||
let price1 = bank1.oracle_price(oracle1)?;
|
let price1 = bank1.oracle_price(oracle1, self.staleness_slot)?;
|
||||||
let price2 = bank2.oracle_price(oracle2)?;
|
let price2 = bank2.oracle_price(oracle2, self.staleness_slot)?;
|
||||||
if swap {
|
if swap {
|
||||||
Ok((bank2, price2, Some((bank1, price1))))
|
Ok((bank2, price2, Some((bank1, price1))))
|
||||||
} else {
|
} else {
|
||||||
|
@ -343,7 +355,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
||||||
let index = self.bank_index(token_index)?;
|
let index = self.bank_index(token_index)?;
|
||||||
let bank = self.banks[index].load_fully_unchecked::<Bank>()?;
|
let bank = self.banks[index].load_fully_unchecked::<Bank>()?;
|
||||||
let oracle = &self.oracles[index];
|
let oracle = &self.oracles[index];
|
||||||
Ok((bank, bank.oracle_price(oracle)?))
|
Ok((bank, bank.oracle_price(oracle, self.staleness_slot)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scanned_perp_market_and_oracle(
|
pub fn scanned_perp_market_and_oracle(
|
||||||
|
@ -353,7 +365,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
||||||
let index = self.perp_market_index(perp_market_index)?;
|
let index = self.perp_market_index(perp_market_index)?;
|
||||||
let perp_market = self.perp_markets[index].load_fully_unchecked::<PerpMarket>()?;
|
let perp_market = self.perp_markets[index].load_fully_unchecked::<PerpMarket>()?;
|
||||||
let oracle_acc = &self.perp_oracles[index];
|
let oracle_acc = &self.perp_oracles[index];
|
||||||
let oracle_price = perp_market.oracle_price(oracle_acc)?;
|
let oracle_price = perp_market.oracle_price(oracle_acc, self.staleness_slot)?;
|
||||||
Ok((perp_market, oracle_price))
|
Ok((perp_market, oracle_price))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1481,7 +1493,7 @@ mod tests {
|
||||||
oo1.as_account_info(),
|
oo1.as_account_info(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let retriever = ScanningAccountRetriever::new(&ais, &group).unwrap();
|
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||||
|
|
||||||
// for bank1/oracle1, including open orders (scenario: bids execute)
|
// for bank1/oracle1, including open orders (scenario: bids execute)
|
||||||
let health1 = (100.0 + 1.0 + 2.0 + (20.0 + 15.0 * 5.0)) * 0.8;
|
let health1 = (100.0 + 1.0 + 2.0 + (20.0 + 15.0 * 5.0)) * 0.8;
|
||||||
|
@ -1532,7 +1544,8 @@ mod tests {
|
||||||
oo1.as_account_info(),
|
oo1.as_account_info(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut retriever = ScanningAccountRetriever::new(&ais, &group).unwrap();
|
let mut retriever =
|
||||||
|
ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||||
|
|
||||||
assert_eq!(retriever.banks.len(), 3);
|
assert_eq!(retriever.banks.len(), 3);
|
||||||
assert_eq!(retriever.token_index_map.len(), 3);
|
assert_eq!(retriever.token_index_map.len(), 3);
|
||||||
|
@ -1678,7 +1691,7 @@ mod tests {
|
||||||
oo2.as_account_info(),
|
oo2.as_account_info(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let retriever = ScanningAccountRetriever::new(&ais, &group).unwrap();
|
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||||
|
|
||||||
assert!(health_eq(
|
assert!(health_eq(
|
||||||
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
||||||
|
@ -2166,7 +2179,7 @@ mod tests {
|
||||||
oracle1_ai,
|
oracle1_ai,
|
||||||
];
|
];
|
||||||
|
|
||||||
let retriever = ScanningAccountRetriever::new(&ais, &group).unwrap();
|
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||||
|
|
||||||
assert!(health_eq(
|
assert!(health_eq(
|
||||||
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
||||||
|
@ -2207,7 +2220,7 @@ mod tests {
|
||||||
oo1.as_account_info(),
|
oo1.as_account_info(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let retriever = ScanningAccountRetriever::new(&ais, &group).unwrap();
|
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||||
let result = retriever.perp_market_and_oracle_price(&group, 0, 9);
|
let result = retriever.perp_market_and_oracle_price(&group, 0, 9);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,33 +131,59 @@ pub fn determine_oracle_type(acc_info: &impl KeyedAccountReader) -> Result<Oracl
|
||||||
/// Example: The for SOL at 40 USDC/SOL it would return 0.04 (the unit is USDC-native/SOL-native)
|
/// Example: The for SOL at 40 USDC/SOL it would return 0.04 (the unit is USDC-native/SOL-native)
|
||||||
///
|
///
|
||||||
/// This currently assumes that quote decimals is 6, like for USDC.
|
/// This currently assumes that quote decimals is 6, like for USDC.
|
||||||
|
///
|
||||||
|
/// Pass `staleness_slot` = None to skip the staleness check
|
||||||
pub fn oracle_price(
|
pub fn oracle_price(
|
||||||
acc_info: &impl KeyedAccountReader,
|
acc_info: &impl KeyedAccountReader,
|
||||||
config: &OracleConfig,
|
config: &OracleConfig,
|
||||||
base_decimals: u8,
|
base_decimals: u8,
|
||||||
|
staleness_slot: Option<u64>,
|
||||||
) -> Result<I80F48> {
|
) -> Result<I80F48> {
|
||||||
let data = &acc_info.data();
|
let data = &acc_info.data();
|
||||||
let oracle_type = determine_oracle_type(acc_info)?;
|
let oracle_type = determine_oracle_type(acc_info)?;
|
||||||
|
let staleness_slot = staleness_slot.unwrap_or(0);
|
||||||
|
|
||||||
Ok(match oracle_type {
|
Ok(match oracle_type {
|
||||||
OracleType::Stub => acc_info.load::<StubOracle>()?.price,
|
OracleType::Stub => acc_info.load::<StubOracle>()?.price,
|
||||||
OracleType::Pyth => {
|
OracleType::Pyth => {
|
||||||
let price_account = pyth_sdk_solana::load_price(data).unwrap();
|
let price_account = pyth_sdk_solana::state::load_price_account(data).unwrap();
|
||||||
let price = I80F48::from_num(price_account.price);
|
let price_data = price_account.to_price();
|
||||||
|
let price = I80F48::from_num(price_data.price);
|
||||||
|
|
||||||
// Filter out bad prices
|
// Filter out bad prices
|
||||||
if I80F48::from_num(price_account.conf) > cm!(config.conf_filter * price) {
|
if I80F48::from_num(price_data.conf) > cm!(config.conf_filter * price) {
|
||||||
msg!(
|
msg!(
|
||||||
"Pyth conf interval too high; pubkey {} price: {} price_account.conf: {}",
|
"Pyth conf interval too high; pubkey {} price: {} price_data.conf: {}",
|
||||||
acc_info.key(),
|
acc_info.key(),
|
||||||
price.to_num::<f64>(),
|
price.to_num::<f64>(),
|
||||||
price_account.conf
|
price_data.conf
|
||||||
);
|
);
|
||||||
|
|
||||||
// future: in v3, we had pricecache, and in case of luna, when there were no updates, we used last known value from cache
|
// future: in v3, we had pricecache, and in case of luna, when there were no updates, we used last known value from cache
|
||||||
// we'll have to add a CachedOracle that is based on one of the oracle types, needs a separate keeper and supports
|
// we'll have to add a CachedOracle that is based on one of the oracle types, needs a separate keeper and supports
|
||||||
// maintaining this "last known good value"
|
// maintaining this "last known good value"
|
||||||
return Err(MangoError::SomeError.into());
|
return Err(MangoError::OracleConfidence.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The aggregation pub slot is the time that the aggregate was computed from published data
|
||||||
|
// that was at most 25 slots old. That means it underestimates the actual staleness, potentially
|
||||||
|
// significantly.
|
||||||
|
let agg_pub_slot = price_account.agg.pub_slot;
|
||||||
|
if config.max_staleness_slots >= 0
|
||||||
|
&& price_account
|
||||||
|
.agg
|
||||||
|
.pub_slot
|
||||||
|
.saturating_add(config.max_staleness_slots as u64)
|
||||||
|
< staleness_slot
|
||||||
|
{
|
||||||
|
msg!(
|
||||||
|
"Pyth price too stale; pubkey {} price: {} pub slot: {}",
|
||||||
|
acc_info.key(),
|
||||||
|
price.to_num::<f64>(),
|
||||||
|
agg_pub_slot,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Err(MangoError::OracleStale.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let decimals = cm!((price_account.expo as i8) + QUOTE_DECIMALS - (base_decimals as i8));
|
let decimals = cm!((price_account.expo as i8) + QUOTE_DECIMALS - (base_decimals as i8));
|
||||||
|
@ -187,7 +213,23 @@ pub fn oracle_price(
|
||||||
price.to_num::<f64>(),
|
price.to_num::<f64>(),
|
||||||
std_deviation_decimal
|
std_deviation_decimal
|
||||||
);
|
);
|
||||||
return Err(MangoError::SomeError.into());
|
return Err(MangoError::OracleConfidence.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The round_open_slot is an overestimate of the oracle staleness: Reporters will see
|
||||||
|
// the round opening and only then start executing the price tasks.
|
||||||
|
let round_open_slot = feed.latest_confirmed_round.round_open_slot;
|
||||||
|
if config.max_staleness_slots >= 0
|
||||||
|
&& round_open_slot.saturating_add(config.max_staleness_slots as u64)
|
||||||
|
< staleness_slot
|
||||||
|
{
|
||||||
|
msg!(
|
||||||
|
"Switchboard v2 price too stale; pubkey {} price: {} latest_confirmed_round.round_open_slot: {}",
|
||||||
|
acc_info.key(),
|
||||||
|
price.to_num::<f64>(),
|
||||||
|
round_open_slot,
|
||||||
|
);
|
||||||
|
return Err(MangoError::OracleConfidence.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let decimals = cm!(QUOTE_DECIMALS - (base_decimals as i8));
|
let decimals = cm!(QUOTE_DECIMALS - (base_decimals as i8));
|
||||||
|
@ -209,7 +251,21 @@ pub fn oracle_price(
|
||||||
min_response,
|
min_response,
|
||||||
max_response
|
max_response
|
||||||
);
|
);
|
||||||
return Err(MangoError::SomeError.into());
|
return Err(MangoError::OracleConfidence.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let round_open_slot = result.result.round_open_slot;
|
||||||
|
if config.max_staleness_slots >= 0
|
||||||
|
&& round_open_slot.saturating_add(config.max_staleness_slots as u64)
|
||||||
|
< staleness_slot
|
||||||
|
{
|
||||||
|
msg!(
|
||||||
|
"Switchboard v1 price too stale; pubkey {} price: {} round_open_slot: {}",
|
||||||
|
acc_info.key(),
|
||||||
|
price.to_num::<f64>(),
|
||||||
|
round_open_slot,
|
||||||
|
);
|
||||||
|
return Err(MangoError::OracleConfidence.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let decimals = cm!(QUOTE_DECIMALS - (base_decimals as i8));
|
let decimals = cm!(QUOTE_DECIMALS - (base_decimals as i8));
|
||||||
|
|
|
@ -139,9 +139,18 @@ impl PerpMarket {
|
||||||
orderbook::new_node_key(side, price_data, self.seq_num)
|
orderbook::new_node_key(side, price_data, self.seq_num)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn oracle_price(&self, oracle_acc: &impl KeyedAccountReader) -> Result<I80F48> {
|
pub fn oracle_price(
|
||||||
|
&self,
|
||||||
|
oracle_acc: &impl KeyedAccountReader,
|
||||||
|
staleness_slot: Option<u64>,
|
||||||
|
) -> Result<I80F48> {
|
||||||
require_keys_eq!(self.oracle, *oracle_acc.key());
|
require_keys_eq!(self.oracle, *oracle_acc.key());
|
||||||
oracle::oracle_price(oracle_acc, &self.oracle_config, self.base_decimals)
|
oracle::oracle_price(
|
||||||
|
oracle_acc,
|
||||||
|
&self.oracle_config,
|
||||||
|
self.base_decimals,
|
||||||
|
staleness_slot,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use current order book price and index price to update the instantaneous funding
|
/// Use current order book price and index price to update the instantaneous funding
|
||||||
|
|
Loading…
Reference in New Issue