implement fallbacks for perp_liq_base_or_positive_pnl
This commit is contained in:
parent
22af31c7ee
commit
20eef53f9c
|
@ -508,7 +508,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
|||
let sol_oracle_index = ais[fallback_oracles_start..]
|
||||
.iter()
|
||||
.position(|o| o.key == &pyth_mainnet_sol_oracle::ID);
|
||||
|
||||
|
||||
Ok(Self {
|
||||
banks_and_oracles: ScannedBanksAndOracles {
|
||||
banks: AccountInfoRefMut::borrow_slice(&ais[..n_banks])?,
|
||||
|
|
|
@ -70,8 +70,16 @@ pub fn perp_liq_base_or_positive_pnl(
|
|||
|
||||
// Get oracle price for market. Price is validated inside
|
||||
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
|
||||
let fallback_opt = if perp_market.fallback_oracle != Pubkey::default() {
|
||||
ctx.remaining_accounts
|
||||
.iter()
|
||||
.find(|a| a.key == &perp_market.fallback_oracle)
|
||||
.map(|k| AccountInfoRef::borrow(k).unwrap())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let oracle_price = perp_market.oracle_price(
|
||||
&OracleAccountInfos::from_reader(oracle_ref),
|
||||
&OracleAccountInfos::from_reader_with_fallback(oracle_ref, fallback_opt.as_ref()),
|
||||
None, // checked in health
|
||||
)?;
|
||||
|
||||
|
|
|
@ -456,7 +456,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
|
|||
/// Copy of the above test with an added fallback oracle + staleness instructions
|
||||
async fn test_liq_perps_bankruptcy_stale_oracle() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(400_000); // PerpLiqNegativePnlOrBankruptcy takes a lot of CU
|
||||
test_builder.test().set_compute_max_units(200_000); // PerpLiqNegativePnlOrBankruptcy takes a lot of CU
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
|
@ -836,7 +836,6 @@ async fn test_liq_perps_bankruptcy_stale_oracle() -> Result<(), TransportError>
|
|||
is_writable: false,
|
||||
is_signer: false,
|
||||
};
|
||||
|
||||
assert!(send_tx_with_extra_accounts(
|
||||
solana,
|
||||
PerpLiqNegativePnlOrBankruptcyInstruction {
|
||||
|
|
|
@ -407,7 +407,7 @@ async fn test_liq_perps_force_cancel_stale_oracle() -> Result<(), TransportError
|
|||
.is_err());
|
||||
|
||||
// can withdraw with fallback
|
||||
send_tx_with_extra_accounts(
|
||||
assert!(send_tx_with_extra_accounts(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
|
@ -420,7 +420,9 @@ async fn test_liq_perps_force_cancel_stale_oracle() -> Result<(), TransportError
|
|||
vec![fallback_oracle_meta.clone()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
.result
|
||||
.is_ok());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use anchor_lang::prelude::AccountMeta;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
|
||||
|
@ -409,3 +410,368 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_liq_perps_positive_pnl_stale_oracle() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(170_000); // PerpLiqBaseOrPositivePnlInstruction takes a lot of CU
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..4];
|
||||
let payer_mint_accounts = &context.users[1].token_accounts[0..4];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let GroupWithTokens {
|
||||
group,
|
||||
tokens,
|
||||
insurance_vault,
|
||||
..
|
||||
} = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
zero_token_is_quote: true,
|
||||
..GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
// fund the insurance vault
|
||||
let insurance_vault_funding = 100;
|
||||
{
|
||||
let mut tx = ClientTransaction::new(solana);
|
||||
tx.add_instruction_direct(
|
||||
spl_token::instruction::transfer(
|
||||
&spl_token::ID,
|
||||
&payer_mint_accounts[0],
|
||||
&insurance_vault,
|
||||
&payer.pubkey(),
|
||||
&[&payer.pubkey()],
|
||||
insurance_vault_funding,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
tx.add_signer(payer);
|
||||
tx.send().await.unwrap();
|
||||
}
|
||||
|
||||
let _quote_token = &tokens[0];
|
||||
let base_token = &tokens[1];
|
||||
let borrow_token = &tokens[2];
|
||||
let settle_token = &tokens[3];
|
||||
|
||||
// deposit some funds, to the vaults aren't empty
|
||||
let liqor = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
250,
|
||||
&context.users[1],
|
||||
mints,
|
||||
10000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// SETUP: Create a perp market
|
||||
//
|
||||
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
|
||||
solana,
|
||||
PerpCreateMarketInstruction {
|
||||
group,
|
||||
admin,
|
||||
payer,
|
||||
perp_market_index: 0,
|
||||
settle_token_index: 3,
|
||||
quote_lot_size: 10,
|
||||
base_lot_size: 100,
|
||||
maint_base_asset_weight: 0.8,
|
||||
init_base_asset_weight: 0.5,
|
||||
maint_base_liab_weight: 1.2,
|
||||
init_base_liab_weight: 1.5,
|
||||
maint_overall_asset_weight: 0.0,
|
||||
init_overall_asset_weight: 0.0,
|
||||
base_liquidation_fee: 0.05,
|
||||
positive_pnl_liquidation_fee: 0.05,
|
||||
maker_fee: 0.0,
|
||||
taker_fee: 0.0,
|
||||
group_insurance_fund: true,
|
||||
settle_pnl_limit_factor: 0.2,
|
||||
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, 10.0).await;
|
||||
let price_lots = {
|
||||
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||
perp_market.native_price_to_lot(I80F48::from(10))
|
||||
};
|
||||
|
||||
//
|
||||
// SETUP: Make an two accounts and deposit some quote and base
|
||||
//
|
||||
let context_ref = &context;
|
||||
let make_account = |idx: u32| async move {
|
||||
let deposit_amount = 10000;
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
idx,
|
||||
&context_ref.users[1],
|
||||
&mints[0..1],
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
account
|
||||
};
|
||||
let account_0 = make_account(0).await;
|
||||
let account_1 = make_account(1).await;
|
||||
|
||||
//
|
||||
// SETUP: Borrow some spot on account_0, so we can later make it liquidatable that way
|
||||
// (actually borrowing 1000.5 due to loan origination!)
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: 1000,
|
||||
allow_borrow: true,
|
||||
account: account_0,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[2],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// SETUP: Trade perps between accounts
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
account: account_0,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Bid,
|
||||
price_lots,
|
||||
max_base_lots: 10,
|
||||
..PerpPlaceOrderInstruction::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
account: account_1,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Ask,
|
||||
price_lots,
|
||||
max_base_lots: 10,
|
||||
..PerpPlaceOrderInstruction::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
PerpConsumeEventsInstruction {
|
||||
perp_market,
|
||||
mango_accounts: vec![account_0, account_1],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// after this order exchange it is changed by
|
||||
// 10*10*100*(0.5-1)*1.4 = -7000 for the long account0
|
||||
// 10*10*100*(1-1.5)*1.4 = -7000 for the short account1
|
||||
// (100 is base lot size)
|
||||
assert_eq!(
|
||||
account_init_health(solana, account_0).await.round(),
|
||||
(10000.0f64 - 1000.5 * 1.4 - 7000.0).round()
|
||||
);
|
||||
assert_eq!(
|
||||
account_init_health(solana, account_1).await.round(),
|
||||
10000.0 - 7000.0
|
||||
);
|
||||
|
||||
//
|
||||
// SETUP: Change the perp oracle to make perp-based health go positive for account_0
|
||||
// perp base value goes to 10*21*100*0.5, exceeding the negative quote
|
||||
// perp uhupnl is 10*21*100*0.5 - 10*10*100 = 500
|
||||
// but health doesn't exceed 10k because of the 0 overall weight
|
||||
//
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, 21.0).await;
|
||||
assert_eq!(
|
||||
account_init_health(solana, account_0).await.round(),
|
||||
(10000.0f64 - 1000.5 * 1.4).round()
|
||||
);
|
||||
|
||||
//
|
||||
// SETUP: Increase the price of the borrow so account_0 becomes liquidatable
|
||||
//
|
||||
set_bank_stub_oracle_price(solana, group, &borrow_token, admin, 10.0).await;
|
||||
assert_eq!(
|
||||
account_init_health(solana, account_0).await.round(),
|
||||
(10000.0f64 - 10.0 * 1000.5 * 1.4).round()
|
||||
);
|
||||
|
||||
//
|
||||
// SETUP: Fallback oracle
|
||||
//
|
||||
let fallback_oracle_kp = TestKeypair::new();
|
||||
let fallback_oracle = fallback_oracle_kp.pubkey();
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleCreate {
|
||||
oracle: fallback_oracle_kp,
|
||||
group,
|
||||
mint: base_token.mint.pubkey,
|
||||
admin,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpAddFallbackOracle {
|
||||
group,
|
||||
admin,
|
||||
perp_market,
|
||||
fallback_oracle,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group,
|
||||
admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
fallback_oracle,
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
set_fallback_oracle: true,
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// SETUP: Change the oracle to be invalid
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetTestInstruction {
|
||||
oracle: base_token.oracle,
|
||||
group,
|
||||
mint: base_token.mint.pubkey,
|
||||
admin,
|
||||
price: 21.0,
|
||||
last_update_slot: 0,
|
||||
deviation: 100.0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// SETUP: Ensure fallback oracle matches default
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetTestInstruction {
|
||||
oracle: fallback_oracle,
|
||||
group,
|
||||
mint: base_token.mint.pubkey,
|
||||
admin,
|
||||
price: 21.0,
|
||||
last_update_slot: 0,
|
||||
deviation: 0.0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: PerpLiqBaseOrPositivePnlInstruction fails with stale oracle
|
||||
//
|
||||
assert!(send_tx(
|
||||
solana,
|
||||
PerpLiqBaseOrPositivePnlInstruction {
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
liqee: account_0,
|
||||
perp_market,
|
||||
max_base_transfer: i64::MAX,
|
||||
max_pnl_transfer: 100,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
//
|
||||
// TEST: PerpLiqBaseOrPositivePnlInstruction succeeds with fallback
|
||||
//
|
||||
let fallback_oracle_meta = AccountMeta {
|
||||
pubkey: fallback_oracle,
|
||||
is_writable: false,
|
||||
is_signer: false,
|
||||
};
|
||||
assert!(send_tx_with_extra_accounts(
|
||||
solana,
|
||||
PerpLiqBaseOrPositivePnlInstruction {
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
liqee: account_0,
|
||||
perp_market,
|
||||
max_base_transfer: i64::MAX,
|
||||
max_pnl_transfer: 100,
|
||||
},
|
||||
vec![fallback_oracle_meta],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.result
|
||||
.is_ok());
|
||||
|
||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 0);
|
||||
assert_eq!(liqor_data.perps[0].quote_position_native(), 100);
|
||||
assert_eq!(
|
||||
account_position(solana, liqor, settle_token.bank).await,
|
||||
10000 - 95
|
||||
);
|
||||
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
|
||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 10);
|
||||
assert_eq!(liqee_data.perps[0].quote_position_native(), -10100);
|
||||
assert_eq!(
|
||||
account_position(solana, account_0, settle_token.bank).await,
|
||||
95
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue