Implement a stable_price on banks and perp markets (#303)
It is tracked in the StablePriceModel and updated on TokenUpdateIndexAndRate and PerpUpdateFunding instructions. The stable price is used in health computations.
This commit is contained in:
parent
748334d674
commit
c276353289
|
@ -1,6 +1,7 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::accounts_zerocopy::AccountInfoRef;
|
||||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
use crate::util::fill_from_str;
|
||||
|
@ -122,9 +123,16 @@ pub fn perp_create_market(
|
|||
settle_fee_flat,
|
||||
settle_fee_amount_threshold,
|
||||
settle_fee_fraction_low_health,
|
||||
reserved: [0; 2244],
|
||||
stable_price_model: StablePriceModel::default(),
|
||||
reserved: [0; 1956],
|
||||
};
|
||||
|
||||
let oracle_price =
|
||||
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)?;
|
||||
perp_market
|
||||
.stable_price_model
|
||||
.reset_to_price(oracle_price.to_num(), now_ts);
|
||||
|
||||
let mut orderbook = ctx.accounts.orderbook.load_init()?;
|
||||
orderbook.init();
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::state::*;
|
||||
use crate::{accounts_zerocopy::AccountInfoRef, state::*};
|
||||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
|
@ -17,6 +17,9 @@ pub struct PerpEditMarket<'info> {
|
|||
has_one = group
|
||||
)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
|
||||
/// CHECK: The oracle can be one of several different account types
|
||||
pub oracle: UncheckedAccount<'info>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -41,6 +44,9 @@ pub fn perp_edit_market(
|
|||
settle_fee_flat_opt: Option<f32>,
|
||||
settle_fee_amount_threshold_opt: Option<f32>,
|
||||
settle_fee_fraction_low_health_opt: Option<f32>,
|
||||
stable_price_delay_interval_seconds_opt: Option<u32>,
|
||||
stable_price_delay_growth_limit_opt: Option<f32>,
|
||||
stable_price_growth_limit_opt: Option<f32>,
|
||||
) -> Result<()> {
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
|
||||
|
@ -51,12 +57,20 @@ pub fn perp_edit_market(
|
|||
// name
|
||||
// group
|
||||
|
||||
if let Some(oracle) = oracle_opt {
|
||||
perp_market.oracle = oracle;
|
||||
}
|
||||
if let Some(oracle_config) = oracle_config_opt {
|
||||
perp_market.oracle_config = oracle_config.to_oracle_config();
|
||||
};
|
||||
if let Some(oracle) = oracle_opt {
|
||||
perp_market.oracle = oracle;
|
||||
|
||||
require_keys_eq!(oracle, ctx.accounts.oracle.key());
|
||||
let oracle_price = perp_market
|
||||
.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)?;
|
||||
perp_market.stable_price_model.reset_to_price(
|
||||
oracle_price.to_num(),
|
||||
Clock::get()?.unix_timestamp.try_into().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// unchanged -
|
||||
// bids
|
||||
|
@ -137,6 +151,17 @@ pub fn perp_edit_market(
|
|||
perp_market.settle_fee_fraction_low_health = settle_fee_fraction_low_health;
|
||||
}
|
||||
|
||||
if let Some(stable_price_delay_interval_seconds) = stable_price_delay_interval_seconds_opt {
|
||||
// Updating this makes the old delay values slightly inconsistent
|
||||
perp_market.stable_price_model.delay_interval_seconds = stable_price_delay_interval_seconds;
|
||||
}
|
||||
if let Some(stable_price_delay_growth_limit) = stable_price_delay_growth_limit_opt {
|
||||
perp_market.stable_price_model.delay_growth_limit = stable_price_delay_growth_limit;
|
||||
}
|
||||
if let Some(stable_price_growth_limit) = stable_price_growth_limit_opt {
|
||||
perp_market.stable_price_model.stable_growth_limit = stable_price_growth_limit;
|
||||
}
|
||||
|
||||
emit!(PerpMarketMetaDataLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
perp_market: ctx.accounts.perp_market.key(),
|
||||
|
|
|
@ -55,7 +55,7 @@ pub fn perp_place_order(ctx: Context<PerpPlaceOrder>, order: Order, limit: u8) -
|
|||
None, // staleness checked in health
|
||||
)?;
|
||||
|
||||
perp_market.update_funding(&book, oracle_price, now_ts)?;
|
||||
perp_market.update_funding_and_stable_price(&book, oracle_price, now_ts)?;
|
||||
}
|
||||
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
|
|
|
@ -32,7 +32,7 @@ pub fn perp_update_funding(ctx: Context<PerpUpdateFunding>) -> Result<()> {
|
|||
Some(now_slot),
|
||||
)?;
|
||||
|
||||
perp_market.update_funding(&book, oracle_price, now_ts)?;
|
||||
perp_market.update_funding_and_stable_price(&book, oracle_price, now_ts)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -17,9 +17,6 @@ pub struct StubOracleSet<'info> {
|
|||
has_one = group
|
||||
)]
|
||||
pub oracle: AccountLoader<'info, StubOracle>,
|
||||
|
||||
#[account(mut)]
|
||||
pub payer: Signer<'info>,
|
||||
}
|
||||
|
||||
pub fn stub_oracle_set(ctx: Context<StubOracleSet>, price: I80F48) -> Result<()> {
|
||||
|
|
|
@ -3,7 +3,7 @@ use anchor_lang::prelude::*;
|
|||
use fixed::types::I80F48;
|
||||
|
||||
use super::InterestRateParams;
|
||||
use crate::accounts_zerocopy::LoadMutZeroCopyRef;
|
||||
use crate::accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef};
|
||||
|
||||
use crate::state::*;
|
||||
|
||||
|
@ -26,6 +26,9 @@ pub struct TokenEdit<'info> {
|
|||
has_one = group
|
||||
)]
|
||||
pub mint_info: AccountLoader<'info, MintInfo>,
|
||||
|
||||
/// CHECK: The oracle can be one of several different account types
|
||||
pub oracle: UncheckedAccount<'info>,
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
|
@ -43,6 +46,9 @@ pub fn token_edit(
|
|||
maint_liab_weight_opt: Option<f32>,
|
||||
init_liab_weight_opt: Option<f32>,
|
||||
liquidation_fee_opt: Option<f32>,
|
||||
stable_price_delay_interval_seconds_opt: Option<u32>,
|
||||
stable_price_delay_growth_limit_opt: Option<f32>,
|
||||
stable_price_growth_limit_opt: Option<f32>,
|
||||
) -> Result<()> {
|
||||
let mut mint_info = ctx.accounts.mint_info.load_mut()?;
|
||||
mint_info.verify_banks_ais(ctx.remaining_accounts)?;
|
||||
|
@ -59,14 +65,22 @@ pub fn token_edit(
|
|||
// mint
|
||||
// vault
|
||||
|
||||
if let Some(oracle) = oracle_opt {
|
||||
bank.oracle = oracle;
|
||||
mint_info.oracle = oracle;
|
||||
}
|
||||
if let Some(oracle_config) = oracle_config_opt.as_ref() {
|
||||
bank.oracle_config = oracle_config.to_oracle_config();
|
||||
bank.oracle_conf_filter = bank.oracle_config.conf_filter;
|
||||
};
|
||||
if let Some(oracle) = oracle_opt {
|
||||
bank.oracle = oracle;
|
||||
mint_info.oracle = oracle;
|
||||
|
||||
require_keys_eq!(oracle, ctx.accounts.oracle.key());
|
||||
let oracle_price =
|
||||
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)?;
|
||||
bank.stable_price_model.reset_to_price(
|
||||
oracle_price.to_num(),
|
||||
Clock::get()?.unix_timestamp.try_into().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(group_insurance_fund) = group_insurance_fund_opt {
|
||||
mint_info.group_insurance_fund = if group_insurance_fund { 1 } else { 0 };
|
||||
|
@ -117,6 +131,17 @@ pub fn token_edit(
|
|||
bank.liquidation_fee = I80F48::from_num(liquidation_fee);
|
||||
}
|
||||
|
||||
if let Some(stable_price_delay_interval_seconds) = stable_price_delay_interval_seconds_opt {
|
||||
// Updating this makes the old delay values slightly inconsistent
|
||||
bank.stable_price_model.delay_interval_seconds = stable_price_delay_interval_seconds;
|
||||
}
|
||||
if let Some(stable_price_delay_growth_limit) = stable_price_delay_growth_limit_opt {
|
||||
bank.stable_price_model.delay_growth_limit = stable_price_delay_growth_limit;
|
||||
}
|
||||
if let Some(stable_price_growth_limit) = stable_price_growth_limit_opt {
|
||||
bank.stable_price_model.stable_growth_limit = stable_price_growth_limit;
|
||||
}
|
||||
|
||||
// unchanged -
|
||||
// dust
|
||||
// flash_loan_token_account_initial
|
||||
|
|
|
@ -3,6 +3,7 @@ use anchor_spl::token::{Mint, Token, TokenAccount};
|
|||
use fixed::types::I80F48;
|
||||
use fixed_macro::types::I80F48;
|
||||
|
||||
use crate::accounts_zerocopy::AccountInfoRef;
|
||||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
use crate::util::fill_from_str;
|
||||
|
@ -98,6 +99,8 @@ pub fn token_register(
|
|||
);
|
||||
}
|
||||
|
||||
let now_ts = Clock::get()?.unix_timestamp;
|
||||
|
||||
let mut bank = ctx.accounts.bank.load_init()?;
|
||||
*bank = Bank {
|
||||
group: ctx.accounts.group.key(),
|
||||
|
@ -111,8 +114,8 @@ pub fn token_register(
|
|||
cached_indexed_total_borrows: I80F48::ZERO,
|
||||
indexed_deposits: I80F48::ZERO,
|
||||
indexed_borrows: I80F48::ZERO,
|
||||
index_last_updated: Clock::get()?.unix_timestamp,
|
||||
bank_rate_last_updated: Clock::get()?.unix_timestamp,
|
||||
index_last_updated: now_ts,
|
||||
bank_rate_last_updated: now_ts,
|
||||
// TODO: add a require! verifying relation between the parameters
|
||||
avg_utilization: I80F48::ZERO,
|
||||
adjustment_factor: I80F48::from_num(interest_rate_params.adjustment_factor),
|
||||
|
@ -138,10 +141,16 @@ pub fn token_register(
|
|||
bank_num: 0,
|
||||
oracle_conf_filter: oracle_config.to_oracle_config().conf_filter,
|
||||
oracle_config: oracle_config.to_oracle_config(),
|
||||
reserved: [0; 2464],
|
||||
stable_price_model: StablePriceModel::default(),
|
||||
reserved: [0; 2176],
|
||||
};
|
||||
require_gt!(bank.max_rate, MINIMUM_MAX_RATE);
|
||||
|
||||
let oracle_price =
|
||||
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)?;
|
||||
bank.stable_price_model
|
||||
.reset_to_price(oracle_price.to_num(), now_ts.try_into().unwrap());
|
||||
|
||||
let mut mint_info = ctx.accounts.mint_info.load_init()?;
|
||||
*mint_info = MintInfo {
|
||||
group: ctx.accounts.group.key(),
|
||||
|
|
|
@ -2,6 +2,7 @@ use anchor_lang::prelude::*;
|
|||
use anchor_spl::token::{Mint, Token, TokenAccount};
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::accounts_zerocopy::AccountInfoRef;
|
||||
use crate::error::*;
|
||||
use crate::instructions::INDEX_START;
|
||||
use crate::state::*;
|
||||
|
@ -71,6 +72,8 @@ pub fn token_register_trustless(
|
|||
) -> Result<()> {
|
||||
require_neq!(token_index, 0);
|
||||
|
||||
let now_ts = Clock::get()?.unix_timestamp;
|
||||
|
||||
let mut bank = ctx.accounts.bank.load_init()?;
|
||||
*bank = Bank {
|
||||
group: ctx.accounts.group.key(),
|
||||
|
@ -84,8 +87,8 @@ pub fn token_register_trustless(
|
|||
cached_indexed_total_borrows: I80F48::ZERO,
|
||||
indexed_deposits: I80F48::ZERO,
|
||||
indexed_borrows: I80F48::ZERO,
|
||||
index_last_updated: Clock::get()?.unix_timestamp,
|
||||
bank_rate_last_updated: Clock::get()?.unix_timestamp,
|
||||
index_last_updated: now_ts,
|
||||
bank_rate_last_updated: now_ts,
|
||||
avg_utilization: I80F48::ZERO,
|
||||
// 10% daily adjustment at 0% or 100% utilization
|
||||
adjustment_factor: I80F48::from_num(0.004),
|
||||
|
@ -115,10 +118,16 @@ pub fn token_register_trustless(
|
|||
max_staleness_slots: -1,
|
||||
reserved: [0; 72],
|
||||
},
|
||||
reserved: [0; 2464],
|
||||
stable_price_model: StablePriceModel::default(),
|
||||
reserved: [0; 2176],
|
||||
};
|
||||
require_gt!(bank.max_rate, MINIMUM_MAX_RATE);
|
||||
|
||||
let oracle_price =
|
||||
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)?;
|
||||
bank.stable_price_model
|
||||
.reset_to_price(oracle_price.to_num(), now_ts.try_into().unwrap());
|
||||
|
||||
let mut mint_info = ctx.accounts.mint_info.load_init()?;
|
||||
*mint_info = MintInfo {
|
||||
group: ctx.accounts.group.key(),
|
||||
|
|
|
@ -91,6 +91,7 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
|
|||
}
|
||||
|
||||
// compute and set latest index and average utilization on each bank
|
||||
// also update moving average prices
|
||||
{
|
||||
let mut some_bank = ctx.remaining_accounts[0].load_mut::<Bank>()?;
|
||||
|
||||
|
@ -111,6 +112,12 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
|
|||
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||
Some(clock.slot),
|
||||
)?;
|
||||
|
||||
some_bank
|
||||
.stable_price_model
|
||||
.update(now_ts as u64, price.to_num());
|
||||
let stable_price_model = some_bank.stable_price_model.clone();
|
||||
|
||||
emit!(UpdateIndexLog {
|
||||
mango_group: mint_info.group.key(),
|
||||
token_index: mint_info.token_index,
|
||||
|
@ -118,6 +125,7 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
|
|||
borrow_index: borrow_index.to_bits(),
|
||||
avg_utilization: new_avg_utilization.to_bits(),
|
||||
price: price.to_bits(),
|
||||
stable_price: some_bank.stable_price().to_bits(),
|
||||
collected_fees: some_bank.collected_fees_native.to_bits(),
|
||||
loan_fee_rate: some_bank.loan_fee_rate.to_bits(),
|
||||
total_deposits: cm!(deposit_index * indexed_total_deposits).to_bits(),
|
||||
|
@ -150,6 +158,8 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
|
|||
// inside OracleConfig.
|
||||
// TODO: remove once fully migrated to OracleConfig
|
||||
bank.oracle_config.conf_filter = bank.oracle_conf_filter;
|
||||
|
||||
bank.stable_price_model = stable_price_model.clone();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -112,6 +112,9 @@ pub mod mango_v4 {
|
|||
maint_liab_weight_opt: Option<f32>,
|
||||
init_liab_weight_opt: Option<f32>,
|
||||
liquidation_fee_opt: Option<f32>,
|
||||
stable_price_delay_interval_seconds_opt: Option<u32>,
|
||||
stable_price_delay_growth_limit_opt: Option<f32>,
|
||||
stable_price_growth_limit_opt: Option<f32>,
|
||||
) -> Result<()> {
|
||||
instructions::token_edit(
|
||||
ctx,
|
||||
|
@ -126,6 +129,9 @@ pub mod mango_v4 {
|
|||
maint_liab_weight_opt,
|
||||
init_liab_weight_opt,
|
||||
liquidation_fee_opt,
|
||||
stable_price_delay_interval_seconds_opt,
|
||||
stable_price_delay_growth_limit_opt,
|
||||
stable_price_growth_limit_opt,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -456,6 +462,9 @@ pub mod mango_v4 {
|
|||
settle_fee_flat_opt: Option<f32>,
|
||||
settle_fee_amount_threshold_opt: Option<f32>,
|
||||
settle_fee_fraction_low_health_opt: Option<f32>,
|
||||
stable_price_delay_interval_seconds_opt: Option<u32>,
|
||||
stable_price_delay_growth_limit_opt: Option<f32>,
|
||||
stable_price_growth_limit_opt: Option<f32>,
|
||||
) -> Result<()> {
|
||||
instructions::perp_edit_market(
|
||||
ctx,
|
||||
|
@ -478,6 +487,9 @@ pub mod mango_v4 {
|
|||
settle_fee_flat_opt,
|
||||
settle_fee_amount_threshold_opt,
|
||||
settle_fee_fraction_low_health_opt,
|
||||
stable_price_delay_interval_seconds_opt,
|
||||
stable_price_delay_growth_limit_opt,
|
||||
stable_price_growth_limit_opt,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -122,6 +122,7 @@ pub struct PerpUpdateFundingLog {
|
|||
pub long_funding: i128,
|
||||
pub short_funding: i128,
|
||||
pub price: i128,
|
||||
pub stable_price: i128,
|
||||
pub fees_accrued: i128,
|
||||
pub open_interest: i64,
|
||||
}
|
||||
|
@ -134,6 +135,7 @@ pub struct UpdateIndexLog {
|
|||
pub borrow_index: i128, // I80F48
|
||||
pub avg_utilization: i128, // I80F48
|
||||
pub price: i128, // I80F48
|
||||
pub stable_price: i128, // I80F48
|
||||
pub collected_fees: i128, // I80F48
|
||||
pub loan_fee_rate: i128, // I80F48
|
||||
pub total_borrows: i128,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::{OracleConfig, TokenIndex, TokenPosition};
|
||||
use crate::accounts_zerocopy::KeyedAccountReader;
|
||||
use crate::state::oracle;
|
||||
use crate::state::{oracle, StablePriceModel};
|
||||
use crate::util;
|
||||
use crate::util::checked_math as cm;
|
||||
use anchor_lang::prelude::*;
|
||||
|
@ -104,13 +104,12 @@ pub struct Bank {
|
|||
|
||||
pub oracle_config: OracleConfig,
|
||||
|
||||
pub stable_price_model: StablePriceModel,
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub reserved: [u8; 2464],
|
||||
pub reserved: [u8; 2176],
|
||||
}
|
||||
const_assert_eq!(
|
||||
size_of::<Bank>(),
|
||||
32 + 16 + 32 * 3 + 16 + 16 * 6 + 8 * 2 + 16 * 16 + 8 * 2 + 2 + 1 + 1 + 4 + 96 + 2464
|
||||
);
|
||||
const_assert_eq!(size_of::<Bank>(), 3112);
|
||||
const_assert_eq!(size_of::<Bank>() % 8, 0);
|
||||
|
||||
impl Bank {
|
||||
|
@ -163,7 +162,8 @@ impl Bank {
|
|||
token_index: existing_bank.token_index,
|
||||
mint_decimals: existing_bank.mint_decimals,
|
||||
oracle_config: existing_bank.oracle_config.clone(),
|
||||
reserved: [0; 2464],
|
||||
stable_price_model: StablePriceModel::default(),
|
||||
reserved: [0; 2176],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -596,6 +596,10 @@ impl Bank {
|
|||
staleness_slot,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn stable_price(&self) -> I80F48 {
|
||||
I80F48::from_num(self.stable_price_model.stable_price)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
|
|
@ -30,8 +30,8 @@ pub struct Prices {
|
|||
/// The current oracle price
|
||||
pub oracle: I80F48, // native/native
|
||||
|
||||
/// A "safe" price, some sort of average over time that is harder to manipulate
|
||||
pub safe: I80F48, // native/native
|
||||
/// A "stable" price, provided by StablePriceModel
|
||||
pub stable: I80F48, // native/native
|
||||
}
|
||||
|
||||
impl Prices {
|
||||
|
@ -39,7 +39,7 @@ impl Prices {
|
|||
pub fn new_single_price(price: I80F48) -> Self {
|
||||
Self {
|
||||
oracle: price,
|
||||
safe: price,
|
||||
stable: price,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ impl Prices {
|
|||
if health_type == HealthType::Maint {
|
||||
self.oracle
|
||||
} else {
|
||||
self.oracle.max(self.safe)
|
||||
self.oracle.max(self.stable)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ impl Prices {
|
|||
if health_type == HealthType::Maint {
|
||||
self.oracle
|
||||
} else {
|
||||
self.oracle.min(self.safe)
|
||||
self.oracle.min(self.stable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1349,7 +1349,7 @@ pub fn new_health_cache(
|
|||
init_liab_weight: bank.init_liab_weight,
|
||||
prices: Prices {
|
||||
oracle: oracle_price,
|
||||
safe: oracle_price,
|
||||
stable: bank.stable_price(),
|
||||
},
|
||||
balance_native: native,
|
||||
});
|
||||
|
@ -1398,7 +1398,7 @@ pub fn new_health_cache(
|
|||
perp_market,
|
||||
Prices {
|
||||
oracle: oracle_price,
|
||||
safe: oracle_price,
|
||||
stable: perp_market.stable_price(),
|
||||
},
|
||||
)?);
|
||||
}
|
||||
|
@ -1528,12 +1528,14 @@ mod tests {
|
|||
bank.data().init_liab_weight = I80F48::from_num(1.0 + init_weights);
|
||||
bank.data().maint_asset_weight = I80F48::from_num(1.0 - maint_weights);
|
||||
bank.data().maint_liab_weight = I80F48::from_num(1.0 + maint_weights);
|
||||
bank.data().stable_price_model.reset_to_price(price, 0);
|
||||
(bank, oracle)
|
||||
}
|
||||
|
||||
fn mock_perp_market(
|
||||
group: Pubkey,
|
||||
oracle: Pubkey,
|
||||
price: f64,
|
||||
market_index: PerpMarketIndex,
|
||||
init_weights: f64,
|
||||
maint_weights: f64,
|
||||
|
@ -1548,6 +1550,7 @@ mod tests {
|
|||
pm.data().maint_liab_weight = I80F48::from_num(1.0 + maint_weights);
|
||||
pm.data().quote_lot_size = 100;
|
||||
pm.data().base_lot_size = 10;
|
||||
pm.data().stable_price_model.reset_to_price(price, 0);
|
||||
pm
|
||||
}
|
||||
|
||||
|
@ -1596,7 +1599,7 @@ mod tests {
|
|||
oo1.data().native_coin_free = 3;
|
||||
oo1.data().referrer_rebates_accrued = 2;
|
||||
|
||||
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 9, 0.2, 0.1);
|
||||
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 5.0, 9, 0.2, 0.1);
|
||||
let perpaccount = account.ensure_perp_position(9, 1).unwrap().0;
|
||||
perpaccount.change_base_and_quote_positions(perp1.data(), 3, -I80F48::from(310u16));
|
||||
perpaccount.bids_base_lots = 7;
|
||||
|
@ -1648,8 +1651,8 @@ mod tests {
|
|||
let oo1key = oo1.pubkey;
|
||||
oo1.data().native_pc_total = 20;
|
||||
|
||||
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 9, 0.2, 0.1);
|
||||
let mut perp2 = mock_perp_market(group, oracle1.pubkey, 8, 0.2, 0.1);
|
||||
let mut perp1 = mock_perp_market(group, oracle2.pubkey, oracle2_price, 9, 0.2, 0.1);
|
||||
let mut perp2 = mock_perp_market(group, oracle1.pubkey, oracle1_price, 8, 0.2, 0.1);
|
||||
|
||||
let oracle1_account_info = oracle1.as_account_info();
|
||||
let oracle2_account_info = oracle2.as_account_info();
|
||||
|
@ -1790,7 +1793,7 @@ mod tests {
|
|||
oo2.data().native_pc_total = testcase.oo_1_3.0;
|
||||
oo2.data().native_coin_total = testcase.oo_1_3.1;
|
||||
|
||||
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 9, 0.2, 0.1);
|
||||
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 5.0, 9, 0.2, 0.1);
|
||||
let perpaccount = account.ensure_perp_position(9, 1).unwrap().0;
|
||||
perpaccount.change_base_and_quote_positions(
|
||||
perp1.data(),
|
||||
|
@ -2285,7 +2288,7 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let mut perp1 = mock_perp_market(group, oracle1.pubkey, 9, 0.2, 0.1);
|
||||
let mut perp1 = mock_perp_market(group, oracle1.pubkey, 1.0, 9, 0.2, 0.1);
|
||||
perp1.data().long_funding = I80F48::from_num(10.1);
|
||||
let perpaccount = account.ensure_perp_position(9, 1).unwrap().0;
|
||||
perpaccount.change_base_and_quote_positions(perp1.data(), 10, I80F48::from(-110));
|
||||
|
@ -2323,8 +2326,8 @@ mod tests {
|
|||
|
||||
let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
|
||||
|
||||
let mut perp1 = mock_perp_market(group, oracle1.pubkey, 9, 0.2, 0.1);
|
||||
let mut perp2 = mock_perp_market(group, oracle2.pubkey, 8, 0.2, 0.1);
|
||||
let mut perp1 = mock_perp_market(group, oracle1.pubkey, 1.0, 9, 0.2, 0.1);
|
||||
let mut perp2 = mock_perp_market(group, oracle2.pubkey, 5.0, 8, 0.2, 0.1);
|
||||
|
||||
let oracle1_account_info = oracle1.as_account_info();
|
||||
let oracle2_account_info = oracle2.as_account_info();
|
||||
|
@ -2344,4 +2347,73 @@ mod tests {
|
|||
let result = retriever.perp_market_and_oracle_price(&group, 0, 9);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_health_stable_price_token() {
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
let buffer2 = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account2 = MangoAccountValue::from_bytes(&buffer2).unwrap();
|
||||
let buffer3 = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account3 = MangoAccountValue::from_bytes(&buffer3).unwrap();
|
||||
|
||||
let group = Pubkey::new_unique();
|
||||
|
||||
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1);
|
||||
bank1.data().stable_price_model.stable_price = 0.5;
|
||||
bank1
|
||||
.data()
|
||||
.change_without_fee(
|
||||
account.ensure_token_position(1).unwrap().0,
|
||||
I80F48::from(100),
|
||||
)
|
||||
.unwrap();
|
||||
bank1
|
||||
.data()
|
||||
.change_without_fee(
|
||||
account2.ensure_token_position(1).unwrap().0,
|
||||
I80F48::from(-100),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut perp1 = mock_perp_market(group, oracle1.pubkey, 1.0, 9, 0.2, 0.1);
|
||||
perp1.data().stable_price_model.stable_price = 0.5;
|
||||
let perpaccount = account3.ensure_perp_position(9, 1).unwrap().0;
|
||||
perpaccount.change_base_and_quote_positions(perp1.data(), 10, I80F48::from(-100));
|
||||
|
||||
let oracle1_ai = oracle1.as_account_info();
|
||||
let ais = vec![
|
||||
bank1.as_account_info(),
|
||||
oracle1_ai.clone(),
|
||||
perp1.as_account_info(),
|
||||
oracle1_ai,
|
||||
];
|
||||
|
||||
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||
|
||||
assert!(health_eq(
|
||||
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
||||
0.8 * 0.5 * 100.0
|
||||
));
|
||||
assert!(health_eq(
|
||||
compute_health(&account.borrow(), HealthType::Maint, &retriever).unwrap(),
|
||||
0.9 * 1.0 * 100.0
|
||||
));
|
||||
assert!(health_eq(
|
||||
compute_health(&account2.borrow(), HealthType::Init, &retriever).unwrap(),
|
||||
-1.2 * 1.0 * 100.0
|
||||
));
|
||||
assert!(health_eq(
|
||||
compute_health(&account2.borrow(), HealthType::Maint, &retriever).unwrap(),
|
||||
-1.1 * 1.0 * 100.0
|
||||
));
|
||||
assert!(health_eq(
|
||||
compute_health(&account3.borrow(), HealthType::Init, &retriever).unwrap(),
|
||||
0.8 * 0.5 * 10.0 * 10.0 - 100.0
|
||||
));
|
||||
assert!(health_eq(
|
||||
compute_health(&account3.borrow(), HealthType::Maint, &retriever).unwrap(),
|
||||
0.9 * 1.0 * 10.0 * 10.0 - 100.0
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ pub use oracle::*;
|
|||
pub use orderbook::*;
|
||||
pub use perp_market::*;
|
||||
pub use serum3_market::*;
|
||||
pub use stable_price::*;
|
||||
|
||||
mod bank;
|
||||
mod dynamic_account;
|
||||
|
@ -23,3 +24,4 @@ mod oracle;
|
|||
mod orderbook;
|
||||
mod perp_market;
|
||||
mod serum3_market;
|
||||
mod stable_price;
|
||||
|
|
|
@ -6,12 +6,12 @@ use fixed::types::I80F48;
|
|||
use static_assertions::const_assert_eq;
|
||||
|
||||
use crate::accounts_zerocopy::KeyedAccountReader;
|
||||
use crate::logs::PerpUpdateFundingLog;
|
||||
use crate::state::orderbook::Side;
|
||||
use crate::state::{oracle, TokenIndex};
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
use super::{orderbook, OracleConfig, Orderbook, DAY_I80F48};
|
||||
use crate::logs::PerpUpdateFundingLog;
|
||||
use super::{orderbook, OracleConfig, Orderbook, StablePriceModel, DAY_I80F48};
|
||||
|
||||
pub type PerpMarketIndex = u16;
|
||||
|
||||
|
@ -109,7 +109,9 @@ pub struct PerpMarket {
|
|||
/// Fraction of pnl to pay out as fee if +pnl account has low health.
|
||||
pub settle_fee_fraction_low_health: f32,
|
||||
|
||||
pub reserved: [u8; 2244],
|
||||
pub stable_price_model: StablePriceModel,
|
||||
|
||||
pub reserved: [u8; 1956],
|
||||
}
|
||||
|
||||
const_assert_eq!(size_of::<PerpMarket>(), 2784);
|
||||
|
@ -153,8 +155,12 @@ impl PerpMarket {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn stable_price(&self) -> I80F48 {
|
||||
I80F48::from_num(self.stable_price_model.stable_price)
|
||||
}
|
||||
|
||||
/// Use current order book price and index price to update the instantaneous funding
|
||||
pub fn update_funding(
|
||||
pub fn update_funding_and_stable_price(
|
||||
&mut self,
|
||||
book: &Orderbook,
|
||||
oracle_price: I80F48,
|
||||
|
@ -193,12 +199,16 @@ impl PerpMarket {
|
|||
self.short_funding += funding_delta;
|
||||
self.funding_last_updated = now_ts;
|
||||
|
||||
self.stable_price_model
|
||||
.update(now_ts, oracle_price.to_num());
|
||||
|
||||
emit!(PerpUpdateFundingLog {
|
||||
mango_group: self.group,
|
||||
market_index: self.perp_market_index,
|
||||
long_funding: self.long_funding.to_bits(),
|
||||
short_funding: self.long_funding.to_bits(),
|
||||
price: oracle_price.to_bits(),
|
||||
stable_price: self.stable_price().to_bits(),
|
||||
fees_accrued: self.fees_accrued.to_bits(),
|
||||
open_interest: self.open_interest,
|
||||
});
|
||||
|
@ -293,7 +303,7 @@ impl PerpMarket {
|
|||
fees_settled: I80F48::ZERO,
|
||||
bump: 0,
|
||||
base_decimals: 0,
|
||||
reserved: [0; 2244],
|
||||
reserved: [0; 1956],
|
||||
padding1: Default::default(),
|
||||
padding2: Default::default(),
|
||||
registration_time: 0,
|
||||
|
@ -303,6 +313,7 @@ impl PerpMarket {
|
|||
settle_fee_flat: 0.0,
|
||||
settle_fee_amount_threshold: 0.0,
|
||||
settle_fee_fraction_low_health: 0.0,
|
||||
stable_price_model: StablePriceModel::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,288 @@
|
|||
use crate::util::checked_math as cm;
|
||||
use anchor_lang::prelude::*;
|
||||
use derivative::Derivative;
|
||||
use static_assertions::const_assert_eq;
|
||||
use std::mem::size_of;
|
||||
|
||||
/// Maintains a "stable_price" based on the oracle price.
|
||||
///
|
||||
/// The stable price follows the oracle price, but its relative rate of
|
||||
/// change is limited (to `stable_growth_limit`) and futher reduced if
|
||||
/// the oracle price is far from the `delay_price`.
|
||||
///
|
||||
/// Conceptually the `delay_price` is itself a time delayed
|
||||
/// (`24 * delay_interval_seconds`, assume 24h) and relative rate of change limited
|
||||
/// function of the oracle price. It is implemented as averaging the oracle
|
||||
/// price over every `delay_interval_seconds` (assume 1h) and then applying the
|
||||
/// `delay_growth_limit` between intervals.
|
||||
#[zero_copy]
|
||||
#[derive(Derivative, Debug)]
|
||||
pub struct StablePriceModel {
|
||||
/// Current stable price to use in health
|
||||
pub stable_price: f64,
|
||||
|
||||
pub last_update_timestamp: u64,
|
||||
|
||||
/// Stored delay_price for each delay_interval.
|
||||
/// If we want the delay_price to be 24h delayed, we would store one for each hour.
|
||||
/// This is used in a cyclical way: We use the maximally-delayed value at delay_interval_index
|
||||
/// and once enough time passes to move to the next delay interval, that gets overwritten and
|
||||
/// we use the next one.
|
||||
pub delay_prices: [f64; 24],
|
||||
|
||||
/// The delay price is based on an average over each delay_interval. The contributions
|
||||
/// to the average are summed up here.
|
||||
pub delay_accumulator_price: f64,
|
||||
|
||||
/// Accumulating the total time for the above average.
|
||||
pub delay_accumulator_time: u32,
|
||||
|
||||
/// Length of a delay_interval
|
||||
pub delay_interval_seconds: u32,
|
||||
|
||||
/// Maximal relative difference between two delay_price in consecutive intervals.
|
||||
pub delay_growth_limit: f32,
|
||||
|
||||
/// Maximal per-second relative difference of the stable price.
|
||||
/// It gets further reduced if stable and delay price disagree.
|
||||
pub stable_growth_limit: f32,
|
||||
|
||||
/// The delay_interval_index that update() was last called on.
|
||||
pub last_delay_interval_index: u8,
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub padding: [u8; 7],
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub reserved: [u8; 48],
|
||||
}
|
||||
const_assert_eq!(size_of::<StablePriceModel>(), 288);
|
||||
const_assert_eq!(size_of::<StablePriceModel>() % 8, 0);
|
||||
|
||||
impl Default for StablePriceModel {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stable_price: 0.0,
|
||||
last_update_timestamp: 0,
|
||||
delay_prices: [0.0; 24],
|
||||
delay_accumulator_price: 0.0,
|
||||
delay_accumulator_time: 0,
|
||||
delay_interval_seconds: 60 * 60, // 1h, for a total delay of 24h
|
||||
delay_growth_limit: 0.06, // 6% per hour, 400% per day
|
||||
stable_growth_limit: 0.0003, // 0.03% per second, 293% in 1h if updated every 10s, 281% in 1h if updated every 5min
|
||||
last_delay_interval_index: 0,
|
||||
padding: Default::default(),
|
||||
reserved: [0; 48],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StablePriceModel {
|
||||
pub fn reset_to_price(&mut self, oracle_price: f64, now_ts: u64) {
|
||||
self.stable_price = oracle_price;
|
||||
self.delay_prices = [oracle_price; 24];
|
||||
self.delay_accumulator_price = 0.0;
|
||||
self.delay_accumulator_time = 0;
|
||||
self.last_update_timestamp = now_ts;
|
||||
}
|
||||
|
||||
pub fn delay_interval_index(&self, timestamp: u64) -> u8 {
|
||||
((timestamp / self.delay_interval_seconds as u64) % self.delay_prices.len() as u64) as u8
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn growth_clamped(target: f64, prev: f64, growth_limit: f64) -> f64 {
|
||||
let max = prev * (1.0 + growth_limit);
|
||||
// for the lower bound, we technically should divide by (1 + growth_limit), but
|
||||
// the error is small when growth_limit is small and this saves a division
|
||||
let min = prev * (1.0 - growth_limit);
|
||||
target.clamp(min, max)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, now_ts: u64, oracle_price: f64) {
|
||||
let dt = now_ts.saturating_sub(self.last_update_timestamp);
|
||||
// Hardcoded. Requiring a minimum time between updates reduces the possible difference
|
||||
// between frequent updates and infrequent ones.
|
||||
// Limiting the max dt prevents very strong updates if update() hasn't been
|
||||
// called for hours.
|
||||
let min_dt = 10;
|
||||
let max_dt = 10 * 60; // 10 min
|
||||
if dt < min_dt {
|
||||
return;
|
||||
}
|
||||
// did we wrap around all delay intervals?
|
||||
let full_delay_passed =
|
||||
dt > self.delay_prices.len() as u64 * self.delay_interval_seconds as u64;
|
||||
let dt_limited = dt.min(max_dt) as f64;
|
||||
self.last_update_timestamp = now_ts;
|
||||
|
||||
//
|
||||
// Update delay price
|
||||
//
|
||||
cm!(self.delay_accumulator_time += dt as u32);
|
||||
self.delay_accumulator_price += oracle_price * dt_limited;
|
||||
|
||||
let delay_interval_index = self.delay_interval_index(now_ts);
|
||||
if delay_interval_index != self.last_delay_interval_index {
|
||||
// last_delay_interval_index points to the most delayed price, which we will
|
||||
// overwrite with a new delay price
|
||||
let new_delay_price = {
|
||||
// Get the previous new delay_price.
|
||||
let prev = if self.last_delay_interval_index == 0 {
|
||||
self.delay_prices[self.delay_prices.len() - 1]
|
||||
} else {
|
||||
self.delay_prices[self.last_delay_interval_index as usize - 1]
|
||||
};
|
||||
let avg = self.delay_accumulator_price / (self.delay_accumulator_time as f64);
|
||||
Self::growth_clamped(avg, prev, self.delay_growth_limit as f64)
|
||||
};
|
||||
|
||||
// Store the new delay price, accounting for skipped intervals
|
||||
if full_delay_passed {
|
||||
self.delay_prices.fill(new_delay_price);
|
||||
} else if delay_interval_index > self.last_delay_interval_index {
|
||||
self.delay_prices
|
||||
[self.last_delay_interval_index as usize..delay_interval_index as usize]
|
||||
.fill(new_delay_price);
|
||||
} else {
|
||||
self.delay_prices[self.last_delay_interval_index as usize..].fill(new_delay_price);
|
||||
self.delay_prices[..delay_interval_index as usize].fill(new_delay_price);
|
||||
}
|
||||
|
||||
self.delay_accumulator_price = 0.0;
|
||||
self.delay_accumulator_time = 0;
|
||||
self.last_delay_interval_index = delay_interval_index;
|
||||
}
|
||||
|
||||
let delay_price = self.delay_prices[delay_interval_index as usize];
|
||||
|
||||
//
|
||||
// Update stable price
|
||||
//
|
||||
self.stable_price = {
|
||||
let prev_stable_price = self.stable_price;
|
||||
let fraction = if delay_price >= prev_stable_price {
|
||||
prev_stable_price / delay_price
|
||||
} else {
|
||||
delay_price / prev_stable_price
|
||||
};
|
||||
let growth_limit = (self.stable_growth_limit as f64) * fraction * fraction * dt_limited;
|
||||
Self::growth_clamped(oracle_price, prev_stable_price, growth_limit)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn run_and_print(
|
||||
model: &mut StablePriceModel,
|
||||
start: u64,
|
||||
dt: u64,
|
||||
steps: u64,
|
||||
price: fn(u64) -> f64,
|
||||
) -> u64 {
|
||||
println!("step,timestamp,stable_price,delay_price");
|
||||
for i in 0..steps {
|
||||
let time = start + dt * (i + 1);
|
||||
model.update(time, price(time));
|
||||
println!(
|
||||
"{i},{time},{},{}",
|
||||
model.stable_price, model.delay_prices[model.last_delay_interval_index as usize]
|
||||
);
|
||||
}
|
||||
start + dt * steps
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stable_price_10x() {
|
||||
let mut model = StablePriceModel::default();
|
||||
model.reset_to_price(1.0, 0);
|
||||
|
||||
let mut t;
|
||||
t = run_and_print(&mut model, 0, 60, 60, |_| 10.0);
|
||||
assert!((model.stable_price - 1.8).abs() < 0.1);
|
||||
assert_eq!(model.delay_prices[1..], [1.0; 23]);
|
||||
assert!((model.delay_prices[0] - 1.06).abs() < 0.01);
|
||||
assert_eq!(model.last_delay_interval_index, 1);
|
||||
assert_eq!(model.delay_accumulator_time, 0);
|
||||
assert_eq!(model.delay_accumulator_price, 0.0);
|
||||
|
||||
t = run_and_print(&mut model, t, 10, 6 * 60, |_| 10.0);
|
||||
assert!((model.stable_price - 2.3).abs() < 0.1);
|
||||
assert_eq!(model.delay_prices[2..], [1.0; 22]);
|
||||
assert!((model.delay_prices[0] - 1.06).abs() < 0.01);
|
||||
assert!((model.delay_prices[1] - 1.06 * 1.06).abs() < 0.01);
|
||||
assert_eq!(model.last_delay_interval_index, 2);
|
||||
assert_eq!(model.delay_accumulator_time, 0);
|
||||
assert_eq!(model.delay_accumulator_price, 0.0);
|
||||
|
||||
// check delay price wraparound (go to 25h since start)
|
||||
t = run_and_print(&mut model, t, 300, 12 * 23, |_| 10.0);
|
||||
assert!((model.stable_price - 7.4).abs() < 0.1);
|
||||
assert!(model.delay_prices[0] > model.delay_prices[23]);
|
||||
assert!(model.delay_prices[23] > model.delay_prices[22]);
|
||||
assert!(model.delay_prices[1] < model.delay_prices[0]);
|
||||
assert!(model.delay_prices[1] < model.delay_prices[2]);
|
||||
assert_eq!(model.last_delay_interval_index, 1);
|
||||
|
||||
println!("{t}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stable_price_characteristics_upwards() {
|
||||
let mut model = StablePriceModel::default();
|
||||
|
||||
model.reset_to_price(1.0, 0);
|
||||
|
||||
let mut last = 1;
|
||||
for i in 0..100000 {
|
||||
model.update(60 * (i + 1), 1000.0);
|
||||
let now = model.stable_price as i32;
|
||||
if now > last {
|
||||
last = now;
|
||||
println!("reached {now}x after {i} steps, {} hours", i as f64 / 60.0);
|
||||
if now == 10 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stable_price_characteristics_downwards() {
|
||||
let mut model = StablePriceModel::default();
|
||||
let init = 10000.0;
|
||||
model.reset_to_price(init, 0);
|
||||
|
||||
let mut last = 1;
|
||||
for i in 0..100000 {
|
||||
model.update(60 * (i + 1), 0.0);
|
||||
let now = (init / model.stable_price) as i32;
|
||||
if now > last {
|
||||
last = now;
|
||||
println!(
|
||||
"reached 1/{now}x after {i} steps, {} hours",
|
||||
i as f64 / 60.0
|
||||
);
|
||||
if now == 10 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stable_price_average() {
|
||||
let mut model = StablePriceModel {
|
||||
delay_growth_limit: 10.00,
|
||||
..StablePriceModel::default()
|
||||
};
|
||||
model.reset_to_price(1.0, 0);
|
||||
|
||||
run_and_print(&mut model, 0, 60, 60, |t| if t > 1800 { 2.0 } else { 1.0 });
|
||||
println!("{}", model.delay_prices[0]);
|
||||
assert!((model.delay_prices[0] - 1.5).abs() < 0.01);
|
||||
}
|
||||
}
|
|
@ -13,7 +13,6 @@ use solana_program::instruction::Instruction;
|
|||
use solana_program_test::BanksClientError;
|
||||
use solana_sdk::instruction;
|
||||
use solana_sdk::transport::TransportError;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::solana::SolanaCookie;
|
||||
|
@ -361,6 +360,58 @@ pub async fn check_prev_instruction_post_health(solana: &SolanaCookie, account:
|
|||
assert_eq!(health_data.init_health.to_num::<f64>(), post_health);
|
||||
}
|
||||
|
||||
pub async fn set_bank_stub_oracle_price(
|
||||
solana: &SolanaCookie,
|
||||
group: Pubkey,
|
||||
token: &super::mango_setup::Token,
|
||||
admin: TestKeypair,
|
||||
price: f64,
|
||||
) {
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: token.mint.pubkey,
|
||||
price,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenResetStablePriceModel {
|
||||
group,
|
||||
admin,
|
||||
mint: token.mint.pubkey,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn set_perp_stub_oracle_price(
|
||||
solana: &SolanaCookie,
|
||||
group: Pubkey,
|
||||
perp_market: Pubkey,
|
||||
token: &super::mango_setup::Token,
|
||||
admin: TestKeypair,
|
||||
price: f64,
|
||||
) {
|
||||
set_bank_stub_oracle_price(solana, group, token, admin, price).await;
|
||||
send_tx(
|
||||
solana,
|
||||
PerpResetStablePriceModel {
|
||||
group,
|
||||
admin,
|
||||
perp_market,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
//
|
||||
// a struct for each instruction along with its
|
||||
// ClientInstruction impl
|
||||
|
@ -976,12 +1027,78 @@ impl ClientInstruction for TokenDeregisterInstruction {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct TokenResetStablePriceModel {
|
||||
pub group: Pubkey,
|
||||
pub admin: TestKeypair,
|
||||
pub mint: Pubkey,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for TokenResetStablePriceModel {
|
||||
type Accounts = mango_v4::accounts::TokenEdit;
|
||||
type Instruction = mango_v4::instruction::TokenEdit;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
|
||||
let mint_info_key = Pubkey::find_program_address(
|
||||
&[
|
||||
b"MintInfo".as_ref(),
|
||||
self.group.as_ref(),
|
||||
self.mint.as_ref(),
|
||||
],
|
||||
&program_id,
|
||||
)
|
||||
.0;
|
||||
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
|
||||
|
||||
let instruction = Self::Instruction {
|
||||
oracle_opt: Some(mint_info.oracle),
|
||||
oracle_config_opt: None,
|
||||
group_insurance_fund_opt: None,
|
||||
interest_rate_params_opt: None,
|
||||
loan_fee_rate_opt: None,
|
||||
loan_origination_fee_rate_opt: None,
|
||||
maint_asset_weight_opt: None,
|
||||
init_asset_weight_opt: None,
|
||||
maint_liab_weight_opt: None,
|
||||
init_liab_weight_opt: None,
|
||||
liquidation_fee_opt: None,
|
||||
stable_price_delay_interval_seconds_opt: None,
|
||||
stable_price_delay_growth_limit_opt: None,
|
||||
stable_price_growth_limit_opt: None,
|
||||
};
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: self.group,
|
||||
admin: self.admin.pubkey(),
|
||||
mint_info: mint_info_key,
|
||||
oracle: mint_info.oracle,
|
||||
};
|
||||
|
||||
let mut instruction = make_instruction(program_id, &accounts, instruction);
|
||||
instruction
|
||||
.accounts
|
||||
.extend(mint_info.banks().iter().map(|&k| AccountMeta {
|
||||
pubkey: k,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
}));
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<TestKeypair> {
|
||||
vec![self.admin]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StubOracleSetInstruction {
|
||||
pub mint: Pubkey,
|
||||
pub group: Pubkey,
|
||||
pub admin: TestKeypair,
|
||||
pub payer: TestKeypair,
|
||||
pub price: &'static str,
|
||||
pub price: f64,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for StubOracleSetInstruction {
|
||||
|
@ -994,7 +1111,7 @@ impl ClientInstruction for StubOracleSetInstruction {
|
|||
) -> (Self::Accounts, Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {
|
||||
price: I80F48::from_str(self.price).unwrap(),
|
||||
price: I80F48::from_num(self.price),
|
||||
};
|
||||
// TODO: remove copy pasta of pda derivation, use reference
|
||||
let oracle = Pubkey::find_program_address(
|
||||
|
@ -1011,7 +1128,6 @@ impl ClientInstruction for StubOracleSetInstruction {
|
|||
oracle,
|
||||
group: self.group,
|
||||
admin: self.admin.pubkey(),
|
||||
payer: self.payer.pubkey(),
|
||||
};
|
||||
|
||||
let instruction = make_instruction(program_id, &accounts, instruction);
|
||||
|
@ -1019,7 +1135,7 @@ impl ClientInstruction for StubOracleSetInstruction {
|
|||
}
|
||||
|
||||
fn signers(&self) -> Vec<TestKeypair> {
|
||||
vec![self.payer, self.admin]
|
||||
vec![self.admin]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2310,6 +2426,65 @@ impl ClientInstruction for PerpCreateMarketInstruction {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct PerpResetStablePriceModel {
|
||||
pub group: Pubkey,
|
||||
pub admin: TestKeypair,
|
||||
pub perp_market: Pubkey,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for PerpResetStablePriceModel {
|
||||
type Accounts = mango_v4::accounts::PerpEditMarket;
|
||||
type Instruction = mango_v4::instruction::PerpEditMarket;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
|
||||
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||
|
||||
let instruction = Self::Instruction {
|
||||
oracle_opt: Some(perp_market.oracle),
|
||||
oracle_config_opt: None,
|
||||
base_decimals_opt: None,
|
||||
maint_asset_weight_opt: None,
|
||||
init_asset_weight_opt: None,
|
||||
maint_liab_weight_opt: None,
|
||||
init_liab_weight_opt: None,
|
||||
liquidation_fee_opt: None,
|
||||
maker_fee_opt: None,
|
||||
taker_fee_opt: None,
|
||||
min_funding_opt: None,
|
||||
max_funding_opt: None,
|
||||
impact_quantity_opt: None,
|
||||
group_insurance_fund_opt: None,
|
||||
trusted_market_opt: None,
|
||||
fee_penalty_opt: None,
|
||||
settle_fee_flat_opt: None,
|
||||
settle_fee_amount_threshold_opt: None,
|
||||
settle_fee_fraction_low_health_opt: None,
|
||||
stable_price_delay_interval_seconds_opt: None,
|
||||
stable_price_delay_growth_limit_opt: None,
|
||||
stable_price_growth_limit_opt: None,
|
||||
};
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: self.group,
|
||||
admin: self.admin.pubkey(),
|
||||
perp_market: self.perp_market,
|
||||
oracle: perp_market.oracle,
|
||||
};
|
||||
|
||||
let instruction = make_instruction(program_id, &accounts, instruction);
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<TestKeypair> {
|
||||
vec![self.admin]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PerpCloseMarketInstruction {
|
||||
pub admin: TestKeypair,
|
||||
pub perp_market: Pubkey,
|
||||
|
|
|
@ -72,8 +72,7 @@ impl<'a> GroupWithTokensConfig {
|
|||
group,
|
||||
admin,
|
||||
mint: mint.pubkey,
|
||||
payer,
|
||||
price: "1.0",
|
||||
price: 1.0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -164,18 +164,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
|
|||
//
|
||||
// SETUP: Change the oracle to make health go very negative
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: borrow_token1.mint.pubkey,
|
||||
payer,
|
||||
price: "20.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, borrow_token1, admin, 20.0).await;
|
||||
|
||||
//
|
||||
// SETUP: liquidate all the collateral against borrow1
|
||||
|
@ -478,18 +467,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
|
|||
//
|
||||
// SETUP: Change the oracle to make health go very negative
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: borrow_token2.mint.pubkey,
|
||||
payer,
|
||||
price: "20.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, borrow_token2, admin, 20.0).await;
|
||||
|
||||
//
|
||||
// SETUP: liquidate all the collateral against borrow2
|
||||
|
|
|
@ -126,18 +126,7 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> {
|
|||
//
|
||||
// SETUP: Change the oracle to make health go negative
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
payer,
|
||||
price: "10.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, base_token, admin, 10.0).await;
|
||||
|
||||
// verify health is bad: can't withdraw
|
||||
assert!(send_tx(
|
||||
|
@ -372,18 +361,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
|||
//
|
||||
// SETUP: Change the oracle to make health go negative for account_0
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
payer,
|
||||
price: "0.5",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, base_token, admin, 0.5).await;
|
||||
|
||||
// verify health is bad: can't withdraw
|
||||
assert!(send_tx(
|
||||
|
@ -435,18 +413,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
|||
//
|
||||
// SETUP: Change the oracle to make health go negative for account_1
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
payer,
|
||||
price: "2.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, base_token, admin, 2.0).await;
|
||||
|
||||
// verify health is bad: can't withdraw
|
||||
assert!(send_tx(
|
||||
|
|
|
@ -122,18 +122,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
//
|
||||
// TEST: Change the oracle to make health go negative
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
payer,
|
||||
price: "10.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, base_token, admin, 10.0).await;
|
||||
|
||||
// can't withdraw
|
||||
assert!(send_tx(
|
||||
|
@ -324,18 +313,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
//
|
||||
// SETUP: Change the oracle to make health go negative
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: borrow_token1.mint.pubkey,
|
||||
payer,
|
||||
price: "2.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, borrow_token1, admin, 2.0).await;
|
||||
|
||||
//
|
||||
// TEST: liquidate borrow2 against too little collateral2
|
||||
|
@ -465,18 +443,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
//
|
||||
|
||||
// Setup: make collateral really valueable, remove nearly all of it
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: collateral_token1.mint.pubkey,
|
||||
payer,
|
||||
price: "100000.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, collateral_token1, admin, 100000.0).await;
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
|
@ -493,18 +460,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
// Setup: reduce collateral value to trigger liquidatability
|
||||
// We have -93 borrows, so -93*2*1.4 = -260.4 health from that
|
||||
// And 1-2 collateral, so max 2*0.6*X health; say X=150 for max 180 health
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: collateral_token1.mint.pubkey,
|
||||
payer,
|
||||
price: "150.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, collateral_token1, admin, 150.0).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
|
|
|
@ -706,18 +706,7 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> {
|
|||
.unwrap();
|
||||
|
||||
// TEST: Change the oracle, now the ask matches
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
payer,
|
||||
price: "1.002",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[0], admin, 1.002).await;
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
|
@ -745,18 +734,7 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> {
|
|||
assert_no_perp_orders(solana, account_0).await;
|
||||
|
||||
// restore the oracle to default
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
payer,
|
||||
price: "1.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[0], admin, 1.0).await;
|
||||
|
||||
//
|
||||
// TEST: order is cancelled when the price exceeds the peg limit
|
||||
|
@ -779,18 +757,7 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> {
|
|||
.unwrap();
|
||||
|
||||
// order is still matchable when exactly at the peg limit
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
payer,
|
||||
price: "1.003",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[0], admin, 1.003).await;
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
|
@ -819,18 +786,7 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> {
|
|||
.is_err());
|
||||
|
||||
// but once the adjusted price is > the peg limit, it's gone
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
payer,
|
||||
price: "1.004",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[0], admin, 1.004).await;
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
|
|
|
@ -120,18 +120,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
|
|||
};
|
||||
|
||||
// Set the initial oracle price
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[1].pubkey,
|
||||
payer,
|
||||
price: "1000.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1000.0).await;
|
||||
|
||||
//
|
||||
// Place orders and create a position
|
||||
|
@ -270,18 +259,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
|
|||
}
|
||||
|
||||
// Try and settle with high price
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[1].pubkey,
|
||||
payer,
|
||||
price: "1200.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1200.0).await;
|
||||
|
||||
// Account a must be the profitable one
|
||||
let result = send_tx(
|
||||
|
@ -304,18 +282,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
|
|||
);
|
||||
|
||||
// Change the oracle to a more reasonable price
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[1].pubkey,
|
||||
payer,
|
||||
price: "1005.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1005.0).await;
|
||||
|
||||
let expected_pnl_0 = I80F48::from(480); // Less due to fees
|
||||
let expected_pnl_1 = I80F48::from(-500);
|
||||
|
@ -335,18 +302,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
|
|||
}
|
||||
|
||||
// Change the oracle to a very high price, such that the pnl exceeds the account funding
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[1].pubkey,
|
||||
payer,
|
||||
price: "1500.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1500.0).await;
|
||||
|
||||
let expected_pnl_0 = I80F48::from(50000 - 20);
|
||||
let expected_pnl_1 = I80F48::from(-50000);
|
||||
|
@ -431,18 +387,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
|
|||
}
|
||||
|
||||
// Change the oracle to a reasonable price in other direction
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[1].pubkey,
|
||||
payer,
|
||||
price: "995.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 995.0).await;
|
||||
|
||||
let expected_pnl_0 = I80F48::from(-8520);
|
||||
let expected_pnl_1 = I80F48::from(8500);
|
||||
|
@ -647,18 +592,7 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
|
|||
};
|
||||
|
||||
// Set the initial oracle price
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[1].pubkey,
|
||||
payer,
|
||||
price: "1000.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1000.0).await;
|
||||
|
||||
//
|
||||
// SETUP: Create a perp base position
|
||||
|
@ -721,18 +655,7 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
|
|||
//
|
||||
// TEST: Settle (health is high)
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[1].pubkey,
|
||||
payer,
|
||||
price: "1050.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1050.0).await;
|
||||
|
||||
let expected_pnl = 5000;
|
||||
|
||||
|
@ -795,34 +718,12 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[2].pubkey,
|
||||
payer,
|
||||
price: "10700.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, &tokens[2], admin, 10700.0).await;
|
||||
|
||||
//
|
||||
// TEST: Settle (health is low)
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[1].pubkey,
|
||||
payer,
|
||||
price: "1100.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1100.0).await;
|
||||
|
||||
let expected_pnl = 5000;
|
||||
|
||||
|
|
|
@ -195,18 +195,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
|
|||
};
|
||||
|
||||
// Set the initial oracle price
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
payer,
|
||||
price: "1000.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 1000.0).await;
|
||||
|
||||
//
|
||||
// Place orders and create a position
|
||||
|
@ -335,18 +324,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
|
|||
}
|
||||
|
||||
// Try and settle with high price
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
payer,
|
||||
price: "1200.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 1200.0).await;
|
||||
|
||||
// Account must have a loss
|
||||
let result = send_tx(
|
||||
|
@ -387,18 +365,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
|
|||
// );
|
||||
|
||||
// Change the oracle to a more reasonable price
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
payer,
|
||||
price: "1005.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 1005.0).await;
|
||||
|
||||
let expected_pnl_0 = I80F48::from(480); // Less due to fees
|
||||
let expected_pnl_1 = I80F48::from(-500);
|
||||
|
|
Loading…
Reference in New Issue