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:
Christian Kamm 2022-11-24 11:55:22 +01:00 committed by GitHub
parent 748334d674
commit c276353289
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 728 additions and 355 deletions

View File

@ -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();

View File

@ -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(),

View File

@ -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()?;

View File

@ -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(())
}

View File

@ -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<()> {

View File

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

View File

@ -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(),

View File

@ -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(),

View File

@ -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();
}
}

View File

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

View File

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

View File

@ -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]

View File

@ -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
));
}
}

View File

@ -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;

View File

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

View File

@ -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);
}
}

View File

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

View File

@ -72,8 +72,7 @@ impl<'a> GroupWithTokensConfig {
group,
admin,
mint: mint.pubkey,
payer,
price: "1.0",
price: 1.0,
},
)
.await

View File

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

View File

@ -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(

View File

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

View File

@ -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 {

View File

@ -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;

View File

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