Allow token withdraws/deposits even with stale oracles (#646)
This commit is contained in:
parent
a6b6fbbb82
commit
4f810edebc
|
@ -115,6 +115,7 @@ impl MangoError {
|
||||||
|
|
||||||
pub trait IsAnchorErrorWithCode {
|
pub trait IsAnchorErrorWithCode {
|
||||||
fn is_anchor_error_with_code(&self, code: u32) -> bool;
|
fn is_anchor_error_with_code(&self, code: u32) -> bool;
|
||||||
|
fn is_oracle_error(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> IsAnchorErrorWithCode for anchor_lang::Result<T> {
|
impl<T> IsAnchorErrorWithCode for anchor_lang::Result<T> {
|
||||||
|
@ -124,6 +125,15 @@ impl<T> IsAnchorErrorWithCode for anchor_lang::Result<T> {
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn is_oracle_error(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Err(Error::AnchorError(e)) => {
|
||||||
|
e.error_code_number == MangoError::OracleConfidence.error_code()
|
||||||
|
|| e.error_code_number == MangoError::OracleStale.error_code()
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Contextable {
|
pub trait Contextable {
|
||||||
|
|
|
@ -1110,13 +1110,44 @@ pub(crate) fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex
|
||||||
pub fn new_health_cache(
|
pub fn new_health_cache(
|
||||||
account: &MangoAccountRef,
|
account: &MangoAccountRef,
|
||||||
retriever: &impl AccountRetriever,
|
retriever: &impl AccountRetriever,
|
||||||
|
) -> Result<HealthCache> {
|
||||||
|
new_health_cache_impl(account, retriever, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a special HealthCache for an account and its health accounts
|
||||||
|
/// where nonnegative token positions for bad oracles are skipped.
|
||||||
|
///
|
||||||
|
/// This health cache must be used carefully, since it doesn't provide the actual
|
||||||
|
/// account health, just a value that is guaranteed to be less than it.
|
||||||
|
pub fn new_health_cache_skipping_bad_oracles(
|
||||||
|
account: &MangoAccountRef,
|
||||||
|
retriever: &impl AccountRetriever,
|
||||||
|
) -> Result<HealthCache> {
|
||||||
|
new_health_cache_impl(account, retriever, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_health_cache_impl(
|
||||||
|
account: &MangoAccountRef,
|
||||||
|
retriever: &impl AccountRetriever,
|
||||||
|
// If an oracle is stale or inconfident and the health contribution would
|
||||||
|
// not be negative, skip it. This decreases health, but maybe overall it's
|
||||||
|
// still positive?
|
||||||
|
skip_bad_oracles: bool,
|
||||||
) -> Result<HealthCache> {
|
) -> Result<HealthCache> {
|
||||||
// token contribution from token accounts
|
// token contribution from token accounts
|
||||||
let mut token_infos = vec![];
|
let mut token_infos = vec![];
|
||||||
|
|
||||||
for (i, position) in account.active_token_positions().enumerate() {
|
for (i, position) in account.active_token_positions().enumerate() {
|
||||||
let (bank, oracle_price) =
|
let bank_oracle_result =
|
||||||
retriever.bank_and_oracle(&account.fixed.group, i, position.token_index)?;
|
retriever.bank_and_oracle(&account.fixed.group, i, position.token_index);
|
||||||
|
if skip_bad_oracles
|
||||||
|
&& bank_oracle_result.is_oracle_error()
|
||||||
|
&& position.indexed_position >= 0
|
||||||
|
{
|
||||||
|
// Ignore the asset because the oracle is bad, decreasing total health
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (bank, oracle_price) = bank_oracle_result?;
|
||||||
|
|
||||||
let native = position.native(bank);
|
let native = position.native(bank);
|
||||||
let prices = Prices {
|
let prices = Prices {
|
||||||
|
|
|
@ -30,13 +30,13 @@ pub fn perp_place_order(
|
||||||
asks: ctx.accounts.asks.load_mut()?,
|
asks: ctx.accounts.asks.load_mut()?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let oracle_state;
|
let oracle_state = perp_market.oracle_state(
|
||||||
(oracle_price, oracle_state) = perp_market.oracle_price_and_state(
|
|
||||||
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||||
None, // staleness checked in health
|
None, // staleness checked in health
|
||||||
)?;
|
)?;
|
||||||
|
oracle_price = oracle_state.price;
|
||||||
|
|
||||||
perp_market.update_funding_and_stable_price(&book, oracle_price, oracle_state, now_ts)?;
|
perp_market.update_funding_and_stable_price(&book, &oracle_state, now_ts)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||||
|
|
|
@ -14,12 +14,12 @@ pub fn perp_update_funding(ctx: Context<PerpUpdateFunding>) -> Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let now_slot = Clock::get()?.slot;
|
let now_slot = Clock::get()?.slot;
|
||||||
let (oracle_price, oracle_state) = perp_market.oracle_price_and_state(
|
let oracle_state = perp_market.oracle_state(
|
||||||
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||||
Some(now_slot),
|
Some(now_slot),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
perp_market.update_funding_and_stable_price(&book, oracle_price, oracle_state, now_ts)?;
|
perp_market.update_funding_and_stable_price(&book, &oracle_state, now_ts)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ pub fn stub_oracle_create(ctx: Context<StubOracleCreate>, price: I80F48) -> Resu
|
||||||
oracle.group = ctx.accounts.group.key();
|
oracle.group = ctx.accounts.group.key();
|
||||||
oracle.mint = ctx.accounts.mint.key();
|
oracle.mint = ctx.accounts.mint.key();
|
||||||
oracle.price = price;
|
oracle.price = price;
|
||||||
oracle.last_updated = Clock::get()?.unix_timestamp;
|
oracle.last_update_ts = Clock::get()?.unix_timestamp;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,22 @@ use fixed::types::I80F48;
|
||||||
pub fn stub_oracle_set(ctx: Context<StubOracleSet>, price: I80F48) -> Result<()> {
|
pub fn stub_oracle_set(ctx: Context<StubOracleSet>, price: I80F48) -> Result<()> {
|
||||||
let mut oracle = ctx.accounts.oracle.load_mut()?;
|
let mut oracle = ctx.accounts.oracle.load_mut()?;
|
||||||
oracle.price = price;
|
oracle.price = price;
|
||||||
oracle.last_updated = Clock::get()?.unix_timestamp;
|
oracle.last_update_ts = Clock::get()?.unix_timestamp;
|
||||||
|
oracle.last_update_slot = Clock::get()?.slot;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stub_oracle_set_test(
|
||||||
|
ctx: Context<StubOracleSet>,
|
||||||
|
price: I80F48,
|
||||||
|
last_update_slot: u64,
|
||||||
|
deviation: I80F48,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut oracle = ctx.accounts.oracle.load_mut()?;
|
||||||
|
oracle.price = price;
|
||||||
|
oracle.last_update_ts = Clock::get()?.unix_timestamp;
|
||||||
|
oracle.last_update_slot = last_update_slot;
|
||||||
|
oracle.deviation = deviation;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,13 +87,17 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
||||||
token::transfer(self.transfer_ctx(), amount_i80f48.to_num::<u64>())?;
|
token::transfer(self.transfer_ctx(), amount_i80f48.to_num::<u64>())?;
|
||||||
|
|
||||||
let indexed_position = position.indexed_position;
|
let indexed_position = position.indexed_position;
|
||||||
let oracle_price = bank.oracle_price(
|
|
||||||
|
// Get the oracle price, even if stale or unconfident: We want to allow users
|
||||||
|
// to deposit to close borrows or do other fixes even if the oracle is bad.
|
||||||
|
let unsafe_oracle_price = oracle_state_unchecked(
|
||||||
&AccountInfoRef::borrow(self.oracle.as_ref())?,
|
&AccountInfoRef::borrow(self.oracle.as_ref())?,
|
||||||
None, // staleness checked in health
|
bank.mint_decimals,
|
||||||
)?;
|
)?
|
||||||
|
.price;
|
||||||
|
|
||||||
// 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 = (amount_i80f48 * oracle_price).to_num::<i64>();
|
let amount_usd = (amount_i80f48 * unsafe_oracle_price).to_num::<i64>();
|
||||||
account.fixed.net_deposits += amount_usd;
|
account.fixed.net_deposits += amount_usd;
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit!(TokenBalanceLog {
|
||||||
|
@ -110,7 +114,11 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
||||||
// Health computation
|
// Health computation
|
||||||
//
|
//
|
||||||
let retriever = new_fixed_order_account_retriever(remaining_accounts, &account.borrow())?;
|
let retriever = new_fixed_order_account_retriever(remaining_accounts, &account.borrow())?;
|
||||||
let cache = new_health_cache(&account.borrow(), &retriever)?;
|
|
||||||
|
// We only compute health to check if the account leaves the being_liquidated state.
|
||||||
|
// So it's ok to possibly skip token positions for bad oracles and compute a health
|
||||||
|
// value that is too low.
|
||||||
|
let cache = new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever)?;
|
||||||
|
|
||||||
// Since depositing can only increase health, we can skip the usual pre-health computation.
|
// Since depositing can only increase health, we can skip the usual pre-health computation.
|
||||||
// Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated.
|
// Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated.
|
||||||
|
@ -160,7 +168,7 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
||||||
signer: self.token_authority.key(),
|
signer: self.token_authority.key(),
|
||||||
token_index,
|
token_index,
|
||||||
quantity: amount_i80f48.to_num::<u64>(),
|
quantity: amount_i80f48.to_num::<u64>(),
|
||||||
price: oracle_price.to_bits(),
|
price: unsafe_oracle_price.to_bits(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -23,10 +23,19 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||||
let retriever =
|
let retriever =
|
||||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||||
let health_cache =
|
let hc_result =
|
||||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
|
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw health cache");
|
||||||
|
if hc_result.is_oracle_error() {
|
||||||
|
// We allow NOT checking the pre init health. That means later on the health
|
||||||
|
// check will be stricter (post_init > 0, without the post_init >= pre_init option)
|
||||||
|
// Then later we can compute the health while ignoring potential nonnegative
|
||||||
|
// health contributions from tokens with stale oracles.
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let health_cache = hc_result?;
|
||||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||||
Some((health_cache, pre_init_health))
|
Some((health_cache, pre_init_health))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
@ -56,10 +65,11 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
|
|
||||||
let amount_i80f48 = I80F48::from(amount);
|
let amount_i80f48 = I80F48::from(amount);
|
||||||
|
|
||||||
let now_slot = Clock::get()?.slot;
|
// Get the oracle price, even if stale or unconfident: We want to allow users
|
||||||
let oracle_price = bank.oracle_price(
|
// to withdraw deposits (while staying healthy otherwise) if the oracle is bad.
|
||||||
|
let unsafe_oracle_state = oracle_state_unchecked(
|
||||||
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||||
Some(now_slot),
|
bank.mint_decimals,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Update the bank and position
|
// Update the bank and position
|
||||||
|
@ -69,6 +79,10 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
Clock::get()?.unix_timestamp.try_into().unwrap(),
|
Clock::get()?.unix_timestamp.try_into().unwrap(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// Avoid getting in trouble because of the mutable bank account borrow later
|
||||||
|
drop(bank);
|
||||||
|
let bank = ctx.accounts.bank.load()?;
|
||||||
|
|
||||||
// Provide a readable error message in case the vault doesn't have enough tokens
|
// Provide a readable error message in case the vault doesn't have enough tokens
|
||||||
if ctx.accounts.vault.amount < amount {
|
if ctx.accounts.vault.amount < amount {
|
||||||
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
|
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
|
||||||
|
@ -98,15 +112,32 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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 = (amount_i80f48 * oracle_price).to_num::<i64>();
|
let amount_usd = (amount_i80f48 * unsafe_oracle_state.price).to_num::<i64>();
|
||||||
account.fixed.net_deposits -= amount_usd;
|
account.fixed.net_deposits -= amount_usd;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Health check
|
// Health check
|
||||||
//
|
//
|
||||||
|
if !account.fixed.is_in_health_region() {
|
||||||
if let Some((mut health_cache, pre_init_health)) = pre_health_opt {
|
if let Some((mut health_cache, pre_init_health)) = pre_health_opt {
|
||||||
|
// This is the normal case
|
||||||
health_cache.adjust_token_balance(&bank, native_position_after - native_position)?;
|
health_cache.adjust_token_balance(&bank, native_position_after - native_position)?;
|
||||||
account.check_health_post(&health_cache, pre_init_health)?;
|
account.check_health_post(&health_cache, pre_init_health)?;
|
||||||
|
} else {
|
||||||
|
// Some oracle was stale/not confident enough above.
|
||||||
|
//
|
||||||
|
// Try computing health while ignoring nonnegative contributions from bad oracles.
|
||||||
|
// If the health is good enough without those, we can pass.
|
||||||
|
//
|
||||||
|
// Note that this must include the normal pre and post health checks.
|
||||||
|
let retriever =
|
||||||
|
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||||
|
let health_cache = new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever)
|
||||||
|
.context("special post-withdraw health-cache")?;
|
||||||
|
let post_init_health = health_cache.health(HealthType::Init);
|
||||||
|
account.check_health_pre_checks(&health_cache, post_init_health)?;
|
||||||
|
account.check_health_post_checks(I80F48::MAX, post_init_health)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -124,7 +155,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
signer: ctx.accounts.owner.key(),
|
signer: ctx.accounts.owner.key(),
|
||||||
token_index,
|
token_index,
|
||||||
quantity: amount,
|
quantity: amount,
|
||||||
price: oracle_price.to_bits(),
|
price: unsafe_oracle_state.price.to_bits(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if withdraw_result.loan_origination_fee.is_positive() {
|
if withdraw_result.loan_origination_fee.is_positive() {
|
||||||
|
@ -135,7 +166,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
loan_amount: withdraw_result.loan_amount.to_bits(),
|
loan_amount: withdraw_result.loan_amount.to_bits(),
|
||||||
loan_origination_fee: withdraw_result.loan_origination_fee.to_bits(),
|
loan_origination_fee: withdraw_result.loan_origination_fee.to_bits(),
|
||||||
instruction: LoanOriginationFeeInstruction::TokenWithdraw,
|
instruction: LoanOriginationFeeInstruction::TokenWithdraw,
|
||||||
price: Some(oracle_price.to_bits()),
|
price: Some(unsafe_oracle_state.price.to_bits()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +174,15 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
if is_borrow {
|
if is_borrow {
|
||||||
ctx.accounts.vault.reload()?;
|
ctx.accounts.vault.reload()?;
|
||||||
bank.enforce_min_vault_to_deposits_ratio(ctx.accounts.vault.as_ref())?;
|
bank.enforce_min_vault_to_deposits_ratio(ctx.accounts.vault.as_ref())?;
|
||||||
bank.check_net_borrows(oracle_price)?;
|
|
||||||
|
// When borrowing the price has be trustworthy, so we can do a reasonable
|
||||||
|
// net borrow check.
|
||||||
|
unsafe_oracle_state.check_confidence_and_maybe_staleness(
|
||||||
|
&bank.oracle,
|
||||||
|
&bank.oracle_config,
|
||||||
|
Some(Clock::get()?.slot),
|
||||||
|
)?;
|
||||||
|
bank.check_net_borrows(unsafe_oracle_state.price)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -365,6 +365,17 @@ pub mod mango_v4 {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn stub_oracle_set_test(
|
||||||
|
ctx: Context<StubOracleSet>,
|
||||||
|
price: I80F48,
|
||||||
|
last_update_slot: u64,
|
||||||
|
deviation: I80F48,
|
||||||
|
) -> Result<()> {
|
||||||
|
#[cfg(feature = "enable-gpl")]
|
||||||
|
instructions::stub_oracle_set_test(ctx, price, last_update_slot, deviation)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64, reduce_only: bool) -> Result<()> {
|
pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64, reduce_only: bool) -> Result<()> {
|
||||||
#[cfg(feature = "enable-gpl")]
|
#[cfg(feature = "enable-gpl")]
|
||||||
instructions::token_deposit(ctx, amount, reduce_only)?;
|
instructions::token_deposit(ctx, amount, reduce_only)?;
|
||||||
|
|
|
@ -850,14 +850,13 @@ 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());
|
||||||
let (price, _) = oracle::oracle_price_and_state(
|
let state = oracle::oracle_state_unchecked(oracle_acc, self.mint_decimals)?;
|
||||||
oracle_acc,
|
state.check_confidence_and_maybe_staleness(
|
||||||
|
&self.oracle,
|
||||||
&self.oracle_config,
|
&self.oracle_config,
|
||||||
self.mint_decimals,
|
|
||||||
staleness_slot,
|
staleness_slot,
|
||||||
)?;
|
)?;
|
||||||
|
Ok(state.price)
|
||||||
Ok(price)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stable_price(&self) -> I80F48 {
|
pub fn stable_price(&self) -> I80F48 {
|
||||||
|
|
|
@ -1173,7 +1173,15 @@ impl<
|
||||||
pub fn check_health_pre(&mut self, health_cache: &HealthCache) -> Result<I80F48> {
|
pub fn check_health_pre(&mut self, health_cache: &HealthCache) -> Result<I80F48> {
|
||||||
let pre_init_health = health_cache.health(HealthType::Init);
|
let pre_init_health = health_cache.health(HealthType::Init);
|
||||||
msg!("pre_init_health: {}", pre_init_health);
|
msg!("pre_init_health: {}", pre_init_health);
|
||||||
|
self.check_health_pre_checks(health_cache, pre_init_health)?;
|
||||||
|
Ok(pre_init_health)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_health_pre_checks(
|
||||||
|
&mut self,
|
||||||
|
health_cache: &HealthCache,
|
||||||
|
pre_init_health: I80F48,
|
||||||
|
) -> Result<()> {
|
||||||
// We can skip computing LiquidationEnd health if Init health > 0, because
|
// We can skip computing LiquidationEnd health if Init health > 0, because
|
||||||
// LiquidationEnd health >= Init health.
|
// LiquidationEnd health >= Init health.
|
||||||
self.fixed_mut()
|
self.fixed_mut()
|
||||||
|
@ -1187,8 +1195,7 @@ impl<
|
||||||
!self.fixed().being_liquidated(),
|
!self.fixed().being_liquidated(),
|
||||||
MangoError::BeingLiquidated
|
MangoError::BeingLiquidated
|
||||||
);
|
);
|
||||||
|
Ok(())
|
||||||
Ok(pre_init_health)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_health_post(
|
pub fn check_health_post(
|
||||||
|
@ -1198,7 +1205,15 @@ impl<
|
||||||
) -> Result<I80F48> {
|
) -> Result<I80F48> {
|
||||||
let post_init_health = health_cache.health(HealthType::Init);
|
let post_init_health = health_cache.health(HealthType::Init);
|
||||||
msg!("post_init_health: {}", post_init_health);
|
msg!("post_init_health: {}", post_init_health);
|
||||||
|
self.check_health_post_checks(pre_init_health, post_init_health)?;
|
||||||
|
Ok(post_init_health)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_health_post_checks(
|
||||||
|
&mut self,
|
||||||
|
pre_init_health: I80F48,
|
||||||
|
post_init_health: I80F48,
|
||||||
|
) -> Result<()> {
|
||||||
// Accounts that have negative init health may only take actions that don't further
|
// Accounts that have negative init health may only take actions that don't further
|
||||||
// decrease their health.
|
// decrease their health.
|
||||||
// To avoid issues with rounding, we allow accounts to decrease their health by up to
|
// To avoid issues with rounding, we allow accounts to decrease their health by up to
|
||||||
|
@ -1219,7 +1234,7 @@ impl<
|
||||||
post_init_health >= 0 || health_does_not_decrease,
|
post_init_health >= 0 || health_does_not_decrease,
|
||||||
MangoError::HealthMustBePositiveOrIncrease
|
MangoError::HealthMustBePositiveOrIncrease
|
||||||
);
|
);
|
||||||
Ok(post_init_health)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_liquidatable(&mut self, health_cache: &HealthCache) -> Result<CheckLiquidatable> {
|
pub fn check_liquidatable(&mut self, health_cache: &HealthCache) -> Result<CheckLiquidatable> {
|
||||||
|
|
|
@ -83,7 +83,7 @@ impl OracleConfigParams {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, AnchorSerialize, AnchorDeserialize)]
|
#[derive(Clone, Copy, PartialEq, AnchorSerialize, AnchorDeserialize)]
|
||||||
pub enum OracleType {
|
pub enum OracleType {
|
||||||
Pyth,
|
Pyth,
|
||||||
Stub,
|
Stub,
|
||||||
|
@ -92,11 +92,65 @@ pub enum OracleType {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OracleState {
|
pub struct OracleState {
|
||||||
|
pub price: I80F48,
|
||||||
|
pub deviation: I80F48,
|
||||||
pub last_update_slot: u64,
|
pub last_update_slot: u64,
|
||||||
pub confidence: I80F48,
|
|
||||||
pub oracle_type: OracleType,
|
pub oracle_type: OracleType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OracleState {
|
||||||
|
#[inline]
|
||||||
|
pub fn check_confidence_and_maybe_staleness(
|
||||||
|
&self,
|
||||||
|
oracle_pk: &Pubkey,
|
||||||
|
config: &OracleConfig,
|
||||||
|
staleness_slot: Option<u64>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(now_slot) = staleness_slot {
|
||||||
|
self.check_staleness(oracle_pk, config, now_slot)?;
|
||||||
|
}
|
||||||
|
self.check_confidence(oracle_pk, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_staleness(
|
||||||
|
&self,
|
||||||
|
oracle_pk: &Pubkey,
|
||||||
|
config: &OracleConfig,
|
||||||
|
now_slot: u64,
|
||||||
|
) -> Result<()> {
|
||||||
|
if config.max_staleness_slots >= 0
|
||||||
|
&& self
|
||||||
|
.last_update_slot
|
||||||
|
.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,
|
||||||
|
);
|
||||||
|
return Err(MangoError::OracleStale.into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_confidence(&self, oracle_pk: &Pubkey, 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>(),
|
||||||
|
);
|
||||||
|
return Err(MangoError::OracleConfidence.into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[account(zero_copy)]
|
#[account(zero_copy)]
|
||||||
pub struct StubOracle {
|
pub struct StubOracle {
|
||||||
// ABI: Clients rely on this being at offset 8
|
// ABI: Clients rely on this being at offset 8
|
||||||
|
@ -104,10 +158,12 @@ pub struct StubOracle {
|
||||||
// ABI: Clients rely on this being at offset 40
|
// ABI: Clients rely on this being at offset 40
|
||||||
pub mint: Pubkey,
|
pub mint: Pubkey,
|
||||||
pub price: I80F48,
|
pub price: I80F48,
|
||||||
pub last_updated: i64,
|
pub last_update_ts: i64,
|
||||||
pub reserved: [u8; 128],
|
pub last_update_slot: u64,
|
||||||
|
pub deviation: I80F48,
|
||||||
|
pub reserved: [u8; 104],
|
||||||
}
|
}
|
||||||
const_assert_eq!(size_of::<StubOracle>(), 32 + 32 + 16 + 8 + 128);
|
const_assert_eq!(size_of::<StubOracle>(), 32 + 32 + 16 + 8 + 8 + 16 + 104);
|
||||||
const_assert_eq!(size_of::<StubOracle>(), 216);
|
const_assert_eq!(size_of::<StubOracle>(), 216);
|
||||||
const_assert_eq!(size_of::<StubOracle>() % 8, 0);
|
const_assert_eq!(size_of::<StubOracle>() % 8, 0);
|
||||||
|
|
||||||
|
@ -134,22 +190,18 @@ pub fn determine_oracle_type(acc_info: &impl KeyedAccountReader) -> Result<Oracl
|
||||||
Err(MangoError::UnknownOracleType.into())
|
Err(MangoError::UnknownOracleType.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A modified version of PriceAccount::get_price_no_older_than() which
|
/// Get the pyth agg price if it's available, otherwise take the prev price.
|
||||||
/// - doesn't need a Clock instance
|
///
|
||||||
/// - has the staleness check be optional (negative max_staleness)
|
/// Returns the publish slot in addition to the price info.
|
||||||
/// - returns the publish slot of the price
|
///
|
||||||
|
/// Also see pyth's PriceAccount::get_price_no_older_than().
|
||||||
fn pyth_get_price(
|
fn pyth_get_price(
|
||||||
pubkey: &Pubkey,
|
pubkey: &Pubkey,
|
||||||
account: &pyth_sdk_solana::state::PriceAccount,
|
account: &pyth_sdk_solana::state::PriceAccount,
|
||||||
now_slot: u64,
|
) -> (pyth_sdk_solana::Price, u64) {
|
||||||
max_staleness: i64,
|
|
||||||
) -> Result<(pyth_sdk_solana::Price, u64)> {
|
|
||||||
use pyth_sdk_solana::*;
|
use pyth_sdk_solana::*;
|
||||||
if account.agg.status == state::PriceStatus::Trading
|
if account.agg.status == state::PriceStatus::Trading {
|
||||||
&& (max_staleness < 0
|
(
|
||||||
|| account.agg.pub_slot.saturating_add(max_staleness as u64) >= now_slot)
|
|
||||||
{
|
|
||||||
return Ok((
|
|
||||||
Price {
|
Price {
|
||||||
conf: account.agg.conf,
|
conf: account.agg.conf,
|
||||||
expo: account.expo,
|
expo: account.expo,
|
||||||
|
@ -157,11 +209,9 @@ fn pyth_get_price(
|
||||||
publish_time: account.timestamp,
|
publish_time: account.timestamp,
|
||||||
},
|
},
|
||||||
account.agg.pub_slot,
|
account.agg.pub_slot,
|
||||||
));
|
)
|
||||||
}
|
} else {
|
||||||
|
(
|
||||||
if max_staleness < 0 || account.prev_slot.saturating_add(max_staleness as u64) >= now_slot {
|
|
||||||
return Ok((
|
|
||||||
Price {
|
Price {
|
||||||
conf: account.prev_conf,
|
conf: account.prev_conf,
|
||||||
expo: account.expo,
|
expo: account.expo,
|
||||||
|
@ -169,82 +219,62 @@ fn pyth_get_price(
|
||||||
publish_time: account.prev_timestamp,
|
publish_time: account.prev_timestamp,
|
||||||
},
|
},
|
||||||
account.prev_slot,
|
account.prev_slot,
|
||||||
));
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
msg!(
|
|
||||||
"Pyth price too stale; pubkey {} prev price: {} prev slot: {} agg pub slot: {} agg status: {:?}",
|
|
||||||
pubkey,
|
|
||||||
account.prev_price,
|
|
||||||
account.prev_slot,
|
|
||||||
account.agg.pub_slot,
|
|
||||||
account.agg.status,
|
|
||||||
);
|
|
||||||
|
|
||||||
Err(MangoError::OracleStale.into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the price of one native base token, in native quote tokens
|
/// 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 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 (i.e. decimals for USD) is 6, like for USDC.
|
||||||
///
|
///
|
||||||
/// Pass `staleness_slot` = None to skip the staleness check
|
/// The staleness and confidence of the oracle is not checked. Use the functions on
|
||||||
pub fn oracle_price_and_state(
|
/// OracleState to validate them if needed. That's why this function is called _unchecked.
|
||||||
|
pub fn oracle_state_unchecked(
|
||||||
acc_info: &impl KeyedAccountReader,
|
acc_info: &impl KeyedAccountReader,
|
||||||
config: &OracleConfig,
|
|
||||||
base_decimals: u8,
|
base_decimals: u8,
|
||||||
staleness_slot: Option<u64>,
|
) -> Result<OracleState> {
|
||||||
) -> Result<(I80F48, OracleState)> {
|
|
||||||
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 => (
|
OracleType::Stub => {
|
||||||
acc_info.load::<StubOracle>()?.price,
|
let stub = acc_info.load::<StubOracle>()?;
|
||||||
|
let deviation = if stub.deviation == 0 {
|
||||||
|
// allows the confidence check to pass even for negative prices
|
||||||
|
I80F48::MIN
|
||||||
|
} else {
|
||||||
|
stub.deviation
|
||||||
|
};
|
||||||
|
let last_update_slot = if stub.last_update_slot == 0 {
|
||||||
|
// ensure staleness checks will never fail
|
||||||
|
u64::MAX
|
||||||
|
} else {
|
||||||
|
stub.last_update_slot
|
||||||
|
};
|
||||||
OracleState {
|
OracleState {
|
||||||
last_update_slot: 0,
|
price: stub.price,
|
||||||
confidence: I80F48::ZERO,
|
last_update_slot,
|
||||||
|
deviation,
|
||||||
oracle_type: OracleType::Stub,
|
oracle_type: OracleType::Stub,
|
||||||
},
|
}
|
||||||
),
|
}
|
||||||
OracleType::Pyth => {
|
OracleType::Pyth => {
|
||||||
let price_account = pyth_sdk_solana::state::load_price_account(data).unwrap();
|
let price_account = pyth_sdk_solana::state::load_price_account(data).unwrap();
|
||||||
let (price_data, pub_slot) = pyth_get_price(
|
let (price_data, last_update_slot) = pyth_get_price(acc_info.key(), price_account);
|
||||||
acc_info.key(),
|
let raw_price = I80F48::from_num(price_data.price);
|
||||||
price_account,
|
|
||||||
staleness_slot,
|
|
||||||
config.max_staleness_slots,
|
|
||||||
)?;
|
|
||||||
let price = I80F48::from_num(price_data.price);
|
|
||||||
|
|
||||||
// Filter out bad prices
|
|
||||||
if I80F48::from_num(price_data.conf) > (config.conf_filter * price) {
|
|
||||||
msg!(
|
|
||||||
"Pyth conf interval too high; pubkey {} price: {} price_data.conf: {}",
|
|
||||||
acc_info.key(),
|
|
||||||
price.to_num::<f64>(),
|
|
||||||
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
|
|
||||||
// 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"
|
|
||||||
return Err(MangoError::OracleConfidence.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let decimals = (price_account.expo as i8) + QUOTE_DECIMALS - (base_decimals as i8);
|
let decimals = (price_account.expo as i8) + QUOTE_DECIMALS - (base_decimals as i8);
|
||||||
let decimal_adj = power_of_ten(decimals);
|
let decimal_adj = power_of_ten(decimals);
|
||||||
(
|
let price = raw_price * decimal_adj;
|
||||||
price * decimal_adj,
|
require_gte!(price, 0);
|
||||||
OracleState {
|
OracleState {
|
||||||
last_update_slot: pub_slot,
|
price,
|
||||||
confidence: I80F48::from_num(price_data.conf),
|
last_update_slot,
|
||||||
|
deviation: I80F48::from_num(price_data.conf),
|
||||||
oracle_type: OracleType::Pyth,
|
oracle_type: OracleType::Pyth,
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
OracleType::SwitchboardV2 => {
|
OracleType::SwitchboardV2 => {
|
||||||
fn from_foreign_error(e: impl std::fmt::Display) -> Error {
|
fn from_foreign_error(e: impl std::fmt::Display) -> Error {
|
||||||
|
@ -254,93 +284,47 @@ pub fn oracle_price_and_state(
|
||||||
let feed = bytemuck::from_bytes::<AggregatorAccountData>(&data[8..]);
|
let feed = bytemuck::from_bytes::<AggregatorAccountData>(&data[8..]);
|
||||||
let feed_result = feed.get_result().map_err(from_foreign_error)?;
|
let feed_result = feed.get_result().map_err(from_foreign_error)?;
|
||||||
let price_decimal: f64 = feed_result.try_into().map_err(from_foreign_error)?;
|
let price_decimal: f64 = feed_result.try_into().map_err(from_foreign_error)?;
|
||||||
let price = I80F48::from_num(price_decimal);
|
let ui_price = I80F48::from_num(price_decimal);
|
||||||
|
|
||||||
// Filter out bad prices
|
|
||||||
let std_deviation_decimal: f64 = feed
|
let std_deviation_decimal: f64 = feed
|
||||||
.latest_confirmed_round
|
.latest_confirmed_round
|
||||||
.std_deviation
|
.std_deviation
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(from_foreign_error)?;
|
.map_err(from_foreign_error)?;
|
||||||
if I80F48::from_num(std_deviation_decimal) > (config.conf_filter * price) {
|
|
||||||
msg!(
|
|
||||||
"Switchboard v2 std deviation too high; pubkey {} price: {} latest_confirmed_round.std_deviation: {}",
|
|
||||||
acc_info.key(),
|
|
||||||
price.to_num::<f64>(),
|
|
||||||
std_deviation_decimal
|
|
||||||
);
|
|
||||||
return Err(MangoError::OracleConfidence.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// The round_open_slot is an overestimate of the oracle staleness: Reporters will see
|
// The round_open_slot is an underestimate of the last update slot: Reporters will see
|
||||||
// the round opening and only then start executing the price tasks.
|
// the round opening and only then start executing the price tasks.
|
||||||
let round_open_slot = feed.latest_confirmed_round.round_open_slot;
|
let last_update_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 = QUOTE_DECIMALS - (base_decimals as i8);
|
let decimals = QUOTE_DECIMALS - (base_decimals as i8);
|
||||||
let decimal_adj = power_of_ten(decimals);
|
let decimal_adj = power_of_ten(decimals);
|
||||||
(
|
let price = ui_price * decimal_adj;
|
||||||
price * decimal_adj,
|
require_gte!(price, 0);
|
||||||
OracleState {
|
OracleState {
|
||||||
last_update_slot: round_open_slot,
|
price,
|
||||||
confidence: I80F48::from_num(std_deviation_decimal),
|
last_update_slot,
|
||||||
|
deviation: I80F48::from_num(std_deviation_decimal),
|
||||||
oracle_type: OracleType::SwitchboardV2,
|
oracle_type: OracleType::SwitchboardV2,
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
OracleType::SwitchboardV1 => {
|
OracleType::SwitchboardV1 => {
|
||||||
let result = FastRoundResultAccountData::deserialize(data).unwrap();
|
let result = FastRoundResultAccountData::deserialize(data).unwrap();
|
||||||
let price = I80F48::from_num(result.result.result);
|
let ui_price = I80F48::from_num(result.result.result);
|
||||||
|
|
||||||
// Filter out bad prices
|
let deviation =
|
||||||
let min_response = I80F48::from_num(result.result.min_response);
|
I80F48::from_num(result.result.max_response - result.result.min_response);
|
||||||
let max_response = I80F48::from_num(result.result.max_response);
|
let last_update_slot = result.result.round_open_slot;
|
||||||
if (max_response - min_response) > (config.conf_filter * price) {
|
|
||||||
msg!(
|
|
||||||
"Switchboard v1 min-max response gap too wide; pubkey {} price: {} min_response: {} max_response {}",
|
|
||||||
acc_info.key(),
|
|
||||||
price.to_num::<f64>(),
|
|
||||||
min_response,
|
|
||||||
max_response
|
|
||||||
);
|
|
||||||
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 = QUOTE_DECIMALS - (base_decimals as i8);
|
let decimals = QUOTE_DECIMALS - (base_decimals as i8);
|
||||||
let decimal_adj = power_of_ten(decimals);
|
let decimal_adj = power_of_ten(decimals);
|
||||||
(
|
let price = ui_price * decimal_adj;
|
||||||
price * decimal_adj,
|
require_gte!(price, 0);
|
||||||
OracleState {
|
OracleState {
|
||||||
last_update_slot: round_open_slot,
|
price,
|
||||||
confidence: max_response - min_response,
|
last_update_slot,
|
||||||
|
deviation,
|
||||||
oracle_type: OracleType::SwitchboardV1,
|
oracle_type: OracleType::SwitchboardV1,
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -255,29 +255,22 @@ impl PerpMarket {
|
||||||
oracle_acc: &impl KeyedAccountReader,
|
oracle_acc: &impl KeyedAccountReader,
|
||||||
staleness_slot: Option<u64>,
|
staleness_slot: Option<u64>,
|
||||||
) -> Result<I80F48> {
|
) -> Result<I80F48> {
|
||||||
require_keys_eq!(self.oracle, *oracle_acc.key());
|
Ok(self.oracle_state(oracle_acc, staleness_slot)?.price)
|
||||||
let (price, _) = oracle::oracle_price_and_state(
|
|
||||||
oracle_acc,
|
|
||||||
&self.oracle_config,
|
|
||||||
self.base_decimals,
|
|
||||||
staleness_slot,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(price)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn oracle_price_and_state(
|
pub fn oracle_state(
|
||||||
&self,
|
&self,
|
||||||
oracle_acc: &impl KeyedAccountReader,
|
oracle_acc: &impl KeyedAccountReader,
|
||||||
staleness_slot: Option<u64>,
|
staleness_slot: Option<u64>,
|
||||||
) -> Result<(I80F48, OracleState)> {
|
) -> Result<OracleState> {
|
||||||
require_keys_eq!(self.oracle, *oracle_acc.key());
|
require_keys_eq!(self.oracle, *oracle_acc.key());
|
||||||
oracle::oracle_price_and_state(
|
let state = oracle::oracle_state_unchecked(oracle_acc, self.base_decimals)?;
|
||||||
oracle_acc,
|
state.check_confidence_and_maybe_staleness(
|
||||||
|
&self.oracle,
|
||||||
&self.oracle_config,
|
&self.oracle_config,
|
||||||
self.base_decimals,
|
|
||||||
staleness_slot,
|
staleness_slot,
|
||||||
)
|
)?;
|
||||||
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stable_price(&self) -> I80F48 {
|
pub fn stable_price(&self) -> I80F48 {
|
||||||
|
@ -288,15 +281,14 @@ impl PerpMarket {
|
||||||
pub fn update_funding_and_stable_price(
|
pub fn update_funding_and_stable_price(
|
||||||
&mut self,
|
&mut self,
|
||||||
book: &Orderbook,
|
book: &Orderbook,
|
||||||
oracle_price: I80F48,
|
oracle_state: &OracleState,
|
||||||
oracle_state: OracleState,
|
|
||||||
now_ts: u64,
|
now_ts: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if now_ts <= self.funding_last_updated {
|
if now_ts <= self.funding_last_updated {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let index_price = oracle_price;
|
let oracle_price = oracle_state.price;
|
||||||
let oracle_price_lots = self.native_price_to_lot(oracle_price);
|
let oracle_price_lots = self.native_price_to_lot(oracle_price);
|
||||||
|
|
||||||
// Get current book price & compare it to index price
|
// Get current book price & compare it to index price
|
||||||
|
@ -312,7 +304,7 @@ impl PerpMarket {
|
||||||
// calculate mid-market rate
|
// calculate mid-market rate
|
||||||
let mid_price = (bid + ask) / 2;
|
let mid_price = (bid + ask) / 2;
|
||||||
let book_price = self.lot_to_native_price(mid_price);
|
let book_price = self.lot_to_native_price(mid_price);
|
||||||
let diff = book_price / index_price - I80F48::ONE;
|
let diff = book_price / oracle_price - I80F48::ONE;
|
||||||
diff.clamp(self.min_funding, self.max_funding)
|
diff.clamp(self.min_funding, self.max_funding)
|
||||||
}
|
}
|
||||||
(Some(_bid), None) => self.max_funding,
|
(Some(_bid), None) => self.max_funding,
|
||||||
|
@ -332,7 +324,7 @@ impl PerpMarket {
|
||||||
let base_lot_size = I80F48::from_num(self.base_lot_size);
|
let base_lot_size = I80F48::from_num(self.base_lot_size);
|
||||||
|
|
||||||
// The number of native quote that one base lot should pay in funding
|
// The number of native quote that one base lot should pay in funding
|
||||||
let funding_delta = index_price * base_lot_size * funding_rate * time_factor;
|
let funding_delta = oracle_price * base_lot_size * funding_rate * time_factor;
|
||||||
|
|
||||||
self.long_funding += funding_delta;
|
self.long_funding += funding_delta;
|
||||||
self.short_funding += funding_delta;
|
self.short_funding += funding_delta;
|
||||||
|
@ -348,7 +340,7 @@ impl PerpMarket {
|
||||||
short_funding: self.short_funding.to_bits(),
|
short_funding: self.short_funding.to_bits(),
|
||||||
price: oracle_price.to_bits(),
|
price: oracle_price.to_bits(),
|
||||||
oracle_slot: oracle_state.last_update_slot,
|
oracle_slot: oracle_state.last_update_slot,
|
||||||
oracle_confidence: oracle_state.confidence.to_bits(),
|
oracle_confidence: oracle_state.deviation.to_bits(),
|
||||||
oracle_type: oracle_state.oracle_type,
|
oracle_type: oracle_state.oracle_type,
|
||||||
stable_price: self.stable_price().to_bits(),
|
stable_price: self.stable_price().to_bits(),
|
||||||
fees_accrued: self.fees_accrued.to_bits(),
|
fees_accrued: self.fees_accrued.to_bits(),
|
||||||
|
|
|
@ -34,5 +34,6 @@ mod test_perp_settle_fees;
|
||||||
mod test_position_lifetime;
|
mod test_position_lifetime;
|
||||||
mod test_reduce_only;
|
mod test_reduce_only;
|
||||||
mod test_serum;
|
mod test_serum;
|
||||||
|
mod test_stale_oracles;
|
||||||
mod test_token_conditional_swap;
|
mod test_token_conditional_swap;
|
||||||
mod test_token_update_index_and_rate;
|
mod test_token_update_index_and_rate;
|
||||||
|
|
|
@ -385,7 +385,7 @@ async fn test_serum_basics() -> Result<(), TransportError> {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
|
async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
|
||||||
let mut test_builder = TestContextBuilder::new();
|
let mut test_builder = TestContextBuilder::new();
|
||||||
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
|
test_builder.test().set_compute_max_units(100_000); // Serum3PlaceOrder needs 95.1k
|
||||||
let context = test_builder.start_default().await;
|
let context = test_builder.start_default().await;
|
||||||
let solana = &context.solana.clone();
|
let solana = &context.solana.clone();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[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
|
||||||
|
let context = test_builder.start_default().await;
|
||||||
|
let solana = &context.solana.clone();
|
||||||
|
|
||||||
|
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
|
||||||
|
//
|
||||||
|
|
||||||
|
let mango_setup::GroupWithTokens { group, .. } = mango_setup::GroupWithTokensConfig {
|
||||||
|
admin,
|
||||||
|
payer,
|
||||||
|
mints: mints.to_vec(),
|
||||||
|
..mango_setup::GroupWithTokensConfig::default()
|
||||||
|
}
|
||||||
|
.create(solana)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// fill vaults, so we can borrow
|
||||||
|
let _vault_account = create_funded_account(
|
||||||
|
&solana,
|
||||||
|
group,
|
||||||
|
owner,
|
||||||
|
2,
|
||||||
|
&context.users[1],
|
||||||
|
mints,
|
||||||
|
100000,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Create account with token0 deposits
|
||||||
|
let account = create_funded_account(
|
||||||
|
&solana,
|
||||||
|
group,
|
||||||
|
owner,
|
||||||
|
0,
|
||||||
|
&context.users[1],
|
||||||
|
&mints[0..1],
|
||||||
|
100,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Create some token1 borrows
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenWithdrawInstruction {
|
||||||
|
amount: 10,
|
||||||
|
allow_borrow: true,
|
||||||
|
account,
|
||||||
|
owner,
|
||||||
|
token_account: payer_token_accounts[1],
|
||||||
|
bank_index: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Make both oracles invalid by increasing deviation
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
StubOracleSetTestInstruction {
|
||||||
|
group,
|
||||||
|
mint: mints[0].pubkey,
|
||||||
|
admin,
|
||||||
|
price: 1.0,
|
||||||
|
last_update_slot: 0,
|
||||||
|
deviation: 100.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
StubOracleSetTestInstruction {
|
||||||
|
group,
|
||||||
|
mint: mints[1].pubkey,
|
||||||
|
admin,
|
||||||
|
price: 1.0,
|
||||||
|
last_update_slot: 0,
|
||||||
|
deviation: 100.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Verify that creating a new borrow won't work
|
||||||
|
assert!(send_tx(
|
||||||
|
solana,
|
||||||
|
TokenWithdrawInstruction {
|
||||||
|
amount: 1,
|
||||||
|
allow_borrow: true,
|
||||||
|
account,
|
||||||
|
owner,
|
||||||
|
token_account: payer_token_accounts[2],
|
||||||
|
bank_index: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
// Repay token1 borrows
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenDepositInstruction {
|
||||||
|
amount: 11,
|
||||||
|
reduce_only: true,
|
||||||
|
account,
|
||||||
|
owner,
|
||||||
|
token_account: payer_token_accounts[1],
|
||||||
|
token_authority: payer.clone(),
|
||||||
|
bank_index: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Withdraw token0 deposits
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenWithdrawInstruction {
|
||||||
|
amount: 100,
|
||||||
|
allow_borrow: true,
|
||||||
|
account,
|
||||||
|
owner,
|
||||||
|
token_account: payer_token_accounts[0],
|
||||||
|
bank_index: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1368,6 +1368,54 @@ impl ClientInstruction for StubOracleSetInstruction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct StubOracleSetTestInstruction {
|
||||||
|
pub mint: Pubkey,
|
||||||
|
pub group: Pubkey,
|
||||||
|
pub admin: TestKeypair,
|
||||||
|
pub price: f64,
|
||||||
|
pub last_update_slot: u64,
|
||||||
|
pub deviation: f64,
|
||||||
|
}
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ClientInstruction for StubOracleSetTestInstruction {
|
||||||
|
type Accounts = mango_v4::accounts::StubOracleSet;
|
||||||
|
type Instruction = mango_v4::instruction::StubOracleSetTest;
|
||||||
|
|
||||||
|
async fn to_instruction(
|
||||||
|
&self,
|
||||||
|
_loader: impl ClientAccountLoader + 'async_trait,
|
||||||
|
) -> (Self::Accounts, Instruction) {
|
||||||
|
let program_id = mango_v4::id();
|
||||||
|
let instruction = Self::Instruction {
|
||||||
|
price: I80F48::from_num(self.price),
|
||||||
|
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,
|
||||||
|
group: self.group,
|
||||||
|
admin: self.admin.pubkey(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let instruction = make_instruction(program_id, &accounts, &instruction);
|
||||||
|
(accounts, instruction)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signers(&self) -> Vec<TestKeypair> {
|
||||||
|
vec![self.admin]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct StubOracleCreate {
|
pub struct StubOracleCreate {
|
||||||
pub group: Pubkey,
|
pub group: Pubkey,
|
||||||
pub mint: Pubkey,
|
pub mint: Pubkey,
|
||||||
|
|
|
@ -112,7 +112,7 @@ impl TestContextBuilder {
|
||||||
let mut test = ProgramTest::new("mango_v4", mango_v4::id(), processor!(mango_v4::entry));
|
let mut test = ProgramTest::new("mango_v4", mango_v4::id(), processor!(mango_v4::entry));
|
||||||
|
|
||||||
// intentionally set to as tight as possible, to catch potential problems early
|
// intentionally set to as tight as possible, to catch potential problems early
|
||||||
test.set_compute_max_units(75000);
|
test.set_compute_max_units(80000);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
test,
|
test,
|
||||||
|
|
Loading…
Reference in New Issue