Merge pull request #223 from blockworks-foundation/ckamm/perp-liq

Perp liquidation: force cancel orders, liq base position
This commit is contained in:
Christian Kamm 2022-09-10 08:11:06 +02:00 committed by GitHub
commit b579096264
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 953 additions and 323 deletions

View File

@ -49,6 +49,8 @@ pub enum MangoError {
PerpPositionDoesNotExist,
#[msg("max settle amount must be greater than zero")]
MaxSettleAmountMustBeGreaterThanZero,
#[msg("the perp position has open orders or unprocessed fill events")]
HasOpenPerpOrders,
}
pub trait Contextable {

View File

@ -20,6 +20,8 @@ pub use perp_consume_events::*;
pub use perp_create_market::*;
pub use perp_deactivate_position::*;
pub use perp_edit_market::*;
pub use perp_liq_base_position::*;
pub use perp_liq_force_cancel_orders::*;
pub use perp_place_order::*;
pub use perp_settle_fees::*;
pub use perp_settle_pnl::*;
@ -67,6 +69,8 @@ mod perp_consume_events;
mod perp_create_market;
mod perp_deactivate_position;
mod perp_edit_market;
mod perp_liq_base_position;
mod perp_liq_force_cancel_orders;
mod perp_place_order;
mod perp_settle_fees;
mod perp_settle_pnl;

View File

@ -0,0 +1,194 @@
use anchor_lang::prelude::*;
use checked_math as cm;
use fixed::types::I80F48;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::state::*;
#[derive(Accounts)]
pub struct PerpLiqBasePosition<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group, has_one = oracle)]
pub perp_market: AccountLoader<'info, PerpMarket>,
/// CHECK: Oracle can have different account types, constrained by address in perp_market
pub oracle: UncheckedAccount<'info>,
#[account(
mut,
has_one = group
// liqor_owner is checked at #1
)]
pub liqor: AccountLoaderDynamic<'info, MangoAccount>,
pub liqor_owner: Signer<'info>,
#[account(mut, has_one = group)]
pub liqee: AccountLoaderDynamic<'info, MangoAccount>,
}
pub fn perp_liq_base_position(
ctx: Context<PerpLiqBasePosition>,
max_base_transfer: i64,
) -> Result<()> {
let group_pk = &ctx.accounts.group.key();
let mut liqor = ctx.accounts.liqor.load_mut()?;
// account constraint #1
require!(
liqor
.fixed
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
MangoError::SomeError
);
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
let mut liqee = ctx.accounts.liqee.load_mut()?;
// Initial liqee health check
let mut liqee_health_cache = {
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
.context("create account retriever")?;
new_health_cache(&liqee.borrow(), &account_retriever)
.context("create liqee health cache")?
};
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
// Once maint_health falls below 0, we want to start liquidating,
// we want to allow liquidation to continue until init_health is positive,
// to prevent constant oscillation between the two states
if liqee.being_liquidated() {
if liqee
.fixed
.maybe_recover_from_being_liquidated(liqee_init_health)
{
msg!("Liqee init_health above zero");
return Ok(());
}
} else {
let maint_health = liqee_health_cache.health(HealthType::Maint);
require!(
maint_health < I80F48::ZERO,
MangoError::HealthMustBeNegative
);
liqee.fixed.set_being_liquidated(true);
}
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let perp_market_index = perp_market.perp_market_index;
let base_lot_size = I80F48::from(perp_market.base_lot_size);
// Get oracle price for market. Price is validated inside
let oracle_price =
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
let price_per_lot = cm!(base_lot_size * oracle_price);
// Fetch perp positions for accounts, creating for the liqor if needed
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
let liqor_perp_position = liqor.ensure_perp_position(perp_market_index)?.0;
let liqee_base_lots = liqee_perp_position.base_position_lots();
require!(
!liqee_perp_position.has_open_orders(),
MangoError::HasOpenPerpOrders
);
// Settle funding
liqee_perp_position.settle_funding(&perp_market);
liqor_perp_position.settle_funding(&perp_market);
// Take over the liqee's base in exchange for quote
require_msg!(liqee_base_lots != 0, "liqee base position is zero");
let (base_transfer, quote_transfer) = if liqee_base_lots > 0 {
require_msg!(
max_base_transfer > 0,
"max_base_transfer must be positive when liqee's base_position is positive"
);
// health gets reduced by `base * price * perp_init_asset_weight`
// and increased by `base * price * (1 - liq_fee) * quote_init_asset_weight`
let quote_asset_weight = I80F48::ONE;
let health_per_lot = cm!(price_per_lot
* (quote_asset_weight - perp_market.init_asset_weight - perp_market.liquidation_fee));
// number of lots to transfer to bring health to zero, rounded up
let base_transfer_for_zero: i64 = cm!(-liqee_init_health / health_per_lot)
.checked_ceil()
.unwrap()
.checked_to_num()
.unwrap();
let base_transfer = base_transfer_for_zero
.min(liqee_base_lots)
.min(max_base_transfer)
.max(0);
let quote_transfer = cm!(-I80F48::from(base_transfer)
* price_per_lot
* (I80F48::ONE - perp_market.liquidation_fee));
(base_transfer, quote_transfer) // base > 0, quote < 0
} else {
// liqee_base_lots < 0
require_msg!(
max_base_transfer < 0,
"max_base_transfer must be negative when liqee's base_position is positive"
);
// health gets increased by `base * price * perp_init_liab_weight`
// and reduced by `base * price * (1 + liq_fee) * quote_init_liab_weight`
let quote_liab_weight = I80F48::ONE;
let health_per_lot = cm!(price_per_lot
* (perp_market.init_liab_weight - quote_liab_weight + perp_market.liquidation_fee));
// (negative) number of lots to transfer to bring health to zero, rounded away from zero
let base_transfer_for_zero: i64 = cm!(liqee_init_health / health_per_lot)
.checked_floor()
.unwrap()
.checked_to_num()
.unwrap();
let base_transfer = base_transfer_for_zero
.max(liqee_base_lots)
.max(max_base_transfer)
.min(0);
let quote_transfer = cm!(-I80F48::from(base_transfer)
* price_per_lot
* (I80F48::ONE + perp_market.liquidation_fee));
(base_transfer, quote_transfer) // base < 0, quote > 0
};
// Execute the transfer. This is essentially a forced trade and updates the
// liqee and liqors entry and break even prices.
liqee_perp_position.change_base_and_quote_positions(
&mut perp_market,
-base_transfer,
-quote_transfer,
);
liqor_perp_position.change_base_and_quote_positions(
&mut perp_market,
base_transfer,
quote_transfer,
);
// Check liqee health again
liqee_health_cache.recompute_perp_info(liqee_perp_position, &perp_market)?;
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
liqee
.fixed
.maybe_recover_from_being_liquidated(liqee_init_health);
drop(perp_market);
// Check liqor's health
if !liqor.fixed.is_in_health_region() {
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
.context("create account retriever end")?;
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
.context("compute liqor health")?;
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
}
Ok(())
}

View File

@ -0,0 +1,87 @@
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use crate::error::*;
use crate::state::*;
#[derive(Accounts)]
pub struct PerpLiqForceCancelOrders<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
#[account(
mut,
has_one = group,
has_one = bids,
has_one = asks
)]
pub perp_market: AccountLoader<'info, PerpMarket>,
#[account(mut)]
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
pub bids: AccountLoader<'info, BookSide>,
}
pub fn perp_liq_force_cancel_orders(
ctx: Context<PerpLiqForceCancelOrders>,
limit: u8,
) -> Result<()> {
let mut account = ctx.accounts.account.load_mut()?;
//
// Check liqee health if liquidation is allowed
//
let mut health_cache = {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health_cache =
new_health_cache(&account.borrow(), &retriever).context("create health cache")?;
if account.being_liquidated() {
let init_health = health_cache.health(HealthType::Init);
if account
.fixed
.maybe_recover_from_being_liquidated(init_health)
{
msg!("Liqee init_health above zero");
return Ok(());
}
} else {
let maint_health = health_cache.health(HealthType::Maint);
require!(
maint_health < I80F48::ZERO,
MangoError::HealthMustBeNegative
);
account.fixed.set_being_liquidated(true);
}
health_cache
};
//
// Cancel orders
//
{
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let mut book = Book::new(bids, asks);
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;
let perp_position = account.perp_position(perp_market.perp_market_index)?;
health_cache.recompute_perp_info(perp_position, &perp_market)?;
}
//
// Health check at the end
//
let init_health = health_cache.health(HealthType::Init);
account
.fixed
.maybe_recover_from_being_liquidated(init_health);
Ok(())
}

View File

@ -506,12 +506,26 @@ pub mod mango_v4 {
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) -> Result<()> {
instructions::perp_settle_fees(ctx, max_settle_amount)
}
pub fn perp_liq_base_position(
ctx: Context<PerpLiqBasePosition>,
max_base_transfer: i64,
) -> Result<()> {
instructions::perp_liq_base_position(ctx, max_base_transfer)
}
pub fn perp_liq_force_cancel_orders(
ctx: Context<PerpLiqForceCancelOrders>,
limit: u8,
) -> Result<()> {
instructions::perp_liq_force_cancel_orders(ctx, limit)
}
// TODO
// perp_force_cancel_order
// liquidate_token_and_perp
// liquidate_perp_and_perp
// settle_* - settle_funds

View File

@ -326,6 +326,14 @@ impl PerpPosition {
cm!(self.quote_position_native += quote_change_native);
}
/// Does the perp position have any open orders or fill events?
pub fn has_open_orders(&self) -> bool {
self.asks_base_lots != 0
|| self.bids_base_lots != 0
|| self.taker_base_lots != 0
|| self.taker_quote_lots != 0
}
/// Calculate the average entry price of the position
pub fn avg_entry_price(&self) -> I80F48 {
if self.base_position_lots == 0 {

View File

@ -2097,6 +2097,7 @@ impl ClientInstruction for LiqTokenBankruptcyInstruction {
}
}
#[derive(Default)]
pub struct PerpCreateMarketInstruction {
pub group: Pubkey,
pub admin: TestKeypair,
@ -2118,6 +2119,28 @@ pub struct PerpCreateMarketInstruction {
pub maker_fee: f32,
pub taker_fee: f32,
}
impl PerpCreateMarketInstruction {
pub async fn with_new_book_and_queue(
solana: &SolanaCookie,
base: &crate::mango_setup::Token,
) -> Self {
PerpCreateMarketInstruction {
asks: solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
bids: solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
event_queue: solana
.create_account_for_type::<EventQueue>(&mango_v4::id())
.await,
oracle: base.oracle,
base_token_index: base.index,
base_token_decimals: base.mint.decimals,
..PerpCreateMarketInstruction::default()
}
}
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpCreateMarketInstruction {
type Accounts = mango_v4::accounts::PerpCreateMarket;
@ -2254,13 +2277,8 @@ impl ClientInstruction for PerpDeactivatePositionInstruction {
}
pub struct PerpPlaceOrderInstruction {
pub group: Pubkey,
pub account: Pubkey,
pub perp_market: Pubkey,
pub asks: Pubkey,
pub bids: Pubkey,
pub event_queue: Pubkey,
pub oracle: Pubkey,
pub owner: TestKeypair,
pub side: Side,
pub price_lots: i64,
@ -2287,16 +2305,6 @@ impl ClientInstruction for PerpPlaceOrderInstruction {
expiry_timestamp: 0,
limit: 1,
};
let accounts = Self::Accounts {
group: self.group,
account: self.account,
perp_market: self.perp_market,
asks: self.asks,
bids: self.bids,
event_queue: self.event_queue,
oracle: self.oracle,
owner: self.owner.pubkey(),
};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let account = account_loader
@ -2312,6 +2320,16 @@ impl ClientInstruction for PerpPlaceOrderInstruction {
)
.await;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
perp_market: self.perp_market,
asks: perp_market.asks,
bids: perp_market.bids,
event_queue: perp_market.event_queue,
oracle: perp_market.oracle,
owner: self.owner.pubkey(),
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas);
@ -2402,11 +2420,8 @@ impl ClientInstruction for PerpCancelOrderByClientOrderIdInstruction {
}
pub struct PerpCancelAllOrdersInstruction {
pub group: Pubkey,
pub account: Pubkey,
pub perp_market: Pubkey,
pub asks: Pubkey,
pub bids: Pubkey,
pub owner: TestKeypair,
}
#[async_trait::async_trait(?Send)]
@ -2415,16 +2430,17 @@ impl ClientInstruction for PerpCancelAllOrdersInstruction {
type Instruction = mango_v4::instruction::PerpCancelAllOrders;
async fn to_instruction(
&self,
_loader: impl ClientAccountLoader + 'async_trait,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction { limit: 5 };
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let accounts = Self::Accounts {
group: self.group,
group: perp_market.group,
account: self.account,
perp_market: self.perp_market,
asks: self.asks,
bids: self.bids,
asks: perp_market.asks,
bids: perp_market.bids,
owner: self.owner.pubkey(),
};
@ -2438,9 +2454,7 @@ impl ClientInstruction for PerpCancelAllOrdersInstruction {
}
pub struct PerpConsumeEventsInstruction {
pub group: Pubkey,
pub perp_market: Pubkey,
pub event_queue: Pubkey,
pub mango_accounts: Vec<Pubkey>,
}
#[async_trait::async_trait(?Send)]
@ -2449,14 +2463,16 @@ impl ClientInstruction for PerpConsumeEventsInstruction {
type Instruction = mango_v4::instruction::PerpConsumeEvents;
async fn to_instruction(
&self,
_loader: impl ClientAccountLoader + 'async_trait,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction { limit: 10 };
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let accounts = Self::Accounts {
group: self.group,
group: perp_market.group,
perp_market: self.perp_market,
event_queue: self.event_queue,
event_queue: perp_market.event_queue,
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
@ -2511,11 +2527,9 @@ impl ClientInstruction for PerpUpdateFundingInstruction {
}
pub struct PerpSettlePnlInstruction {
pub group: Pubkey,
pub account_a: Pubkey,
pub account_b: Pubkey,
pub perp_market: Pubkey,
pub oracle: Pubkey,
pub quote_bank: Pubkey,
pub max_settle_amount: u64,
}
@ -2531,14 +2545,6 @@ impl ClientInstruction for PerpSettlePnlInstruction {
let instruction = Self::Instruction {
max_settle_amount: self.max_settle_amount,
};
let accounts = Self::Accounts {
group: self.group,
perp_market: self.perp_market,
account_a: self.account_a,
account_b: self.account_b,
oracle: self.oracle,
quote_bank: self.quote_bank,
};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let account_b = account_loader
@ -2554,6 +2560,15 @@ impl ClientInstruction for PerpSettlePnlInstruction {
)
.await;
let accounts = Self::Accounts {
group: perp_market.group,
perp_market: self.perp_market,
account_a: self.account_a,
account_b: self.account_b,
oracle: perp_market.oracle,
quote_bank: self.quote_bank,
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas);
@ -2566,10 +2581,8 @@ impl ClientInstruction for PerpSettlePnlInstruction {
}
pub struct PerpSettleFeesInstruction {
pub group: Pubkey,
pub account: Pubkey,
pub perp_market: Pubkey,
pub oracle: Pubkey,
pub quote_bank: Pubkey,
pub max_settle_amount: u64,
}
@ -2585,13 +2598,6 @@ impl ClientInstruction for PerpSettleFeesInstruction {
let instruction = Self::Instruction {
max_settle_amount: self.max_settle_amount,
};
let accounts = Self::Accounts {
group: self.group,
perp_market: self.perp_market,
account: self.account,
oracle: self.oracle,
quote_bank: self.quote_bank,
};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let account = account_loader
@ -2607,6 +2613,13 @@ impl ClientInstruction for PerpSettleFeesInstruction {
)
.await;
let accounts = Self::Accounts {
group: perp_market.group,
perp_market: self.perp_market,
account: self.account,
oracle: perp_market.oracle,
quote_bank: self.quote_bank,
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas);
@ -2618,6 +2631,112 @@ impl ClientInstruction for PerpSettleFeesInstruction {
}
}
pub struct PerpLiqForceCancelOrdersInstruction {
pub account: Pubkey,
pub perp_market: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpLiqForceCancelOrdersInstruction {
type Accounts = mango_v4::accounts::PerpLiqForceCancelOrders;
type Instruction = mango_v4::instruction::PerpLiqForceCancelOrders;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction { limit: 10 };
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
None,
false,
None,
)
.await;
let accounts = Self::Accounts {
group: account.fixed.group,
perp_market: self.perp_market,
account: self.account,
bids: perp_market.bids,
asks: perp_market.asks,
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![]
}
}
pub struct PerpLiqBasePositionInstruction {
pub liqor: Pubkey,
pub liqor_owner: TestKeypair,
pub liqee: Pubkey,
pub perp_market: Pubkey,
pub max_base_transfer: i64,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpLiqBasePositionInstruction {
type Accounts = mango_v4::accounts::PerpLiqBasePosition;
type Instruction = mango_v4::instruction::PerpLiqBasePosition;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
max_base_transfer: self.max_base_transfer,
};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let liqor = account_loader
.load_mango_account(&self.liqor)
.await
.unwrap();
let liqee = account_loader
.load_mango_account(&self.liqee)
.await
.unwrap();
let health_check_metas = derive_liquidation_remaining_account_metas(
&account_loader,
&liqee,
&liqor,
TokenIndex::MAX,
0,
TokenIndex::MAX,
0,
)
.await;
let accounts = Self::Accounts {
group: liqor.fixed.group,
perp_market: self.perp_market,
oracle: perp_market.oracle,
liqor: self.liqor,
liqor_owner: self.liqor_owner.pubkey(),
liqee: self.liqee,
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.liqor_owner]
}
}
pub struct BenchmarkInstruction {}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for BenchmarkInstruction {

View File

@ -197,7 +197,6 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
//
// SETUP: Create perp markets
//
let quote_token = &tokens[0];
let mut perp_markets = vec![];
for (perp_market_index, token) in tokens[1..].iter().enumerate() {
let mango_v4::accounts::PerpCreateMarket {
@ -211,25 +210,8 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
PerpCreateMarketInstruction {
group,
admin,
oracle: token.oracle,
asks: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
bids: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
event_queue: {
context
.solana
.create_account_for_type::<EventQueue>(&mango_v4::id())
.await
},
payer,
perp_market_index: perp_market_index as PerpMarketIndex,
base_token_index: quote_token.index,
base_token_decimals: quote_token.mint.decimals,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.975,
@ -239,6 +221,11 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,
// HACK: Currently the base_token_index token needs to be active on the account.
// Using token[0] for each market allows us to have multiple perp positions with
// just a single token position.
base_token_index: tokens[0].index,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &token).await
},
)
.await
@ -255,18 +242,13 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
//
// TEST: Create a perp order for each market
//
for (i, &(perp_market, asks, bids, event_queue)) in perp_markets.iter().enumerate() {
for (i, &(perp_market, _asks, _bids, _event_queue)) in perp_markets.iter().enumerate() {
println!("adding market {}", i);
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[i + 1].oracle,
owner,
side: Side::Bid,
price_lots,

View File

@ -0,0 +1,465 @@
#![cfg(feature = "test-bpf")]
use fixed::types::I80F48;
use solana_program_test::*;
use solana_sdk::transport::TransportError;
use mango_v4::state::*;
use program_test::*;
use mango_setup::*;
mod program_test;
use utils::assert_equal_fixed_f64 as assert_equal;
#[tokio::test]
async fn test_liq_perps_force_cancel() -> Result<(), TransportError> {
let test_builder = TestContextBuilder::new();
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..2];
let payer_mint_accounts = &context.users[1].token_accounts[0..2];
//
// SETUP: Create a group and an account to fill the vaults
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
}
.create(solana)
.await;
//let quote_token = &tokens[0];
let base_token = &tokens[1];
// deposit some funds, to the vaults aren't empty
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 10000, 0).await;
//
// TEST: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
payer,
perp_market_index: 0,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.8,
init_asset_weight: 0.6,
maint_liab_weight: 1.2,
init_liab_weight: 1.4,
liquidation_fee: 0.05,
maker_fee: 0.0,
taker_fee: 0.0,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await
},
)
.await
.unwrap();
let price_lots = {
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
perp_market.native_price_to_lot(I80F48::ONE)
};
//
// SETUP: Make an account and deposit some quote and base
//
let deposit_amount = 1000;
let account = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
&mints[0..1],
deposit_amount,
0,
)
.await;
send_tx(
solana,
TokenDepositInstruction {
amount: 1,
account,
token_account: payer_mint_accounts[1],
token_authority: payer,
bank_index: 0,
},
)
.await
.unwrap();
//
// SETUP: Place a perp order
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account,
perp_market,
owner,
side: Side::Ask,
price_lots,
// health was 1000 * 0.6 = 600; this order is -14*100*(1.4-1) = -560
max_base_lots: 14,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
)
.await
.unwrap();
//
// 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();
// verify health is bad: can't withdraw
assert!(send_tx(
solana,
TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account,
owner,
token_account: payer_mint_accounts[1],
bank_index: 0,
}
)
.await
.is_err());
//
// TEST: force cancel orders, making the account healthy again
//
send_tx(
solana,
PerpLiqForceCancelOrdersInstruction {
account,
perp_market,
},
)
.await
.unwrap();
// can withdraw again
send_tx(
solana,
TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account,
owner,
token_account: payer_mint_accounts[1],
bank_index: 0,
},
)
.await
.unwrap();
Ok(())
}
#[tokio::test]
async fn test_liq_perps_base_position() -> Result<(), TransportError> {
let test_builder = TestContextBuilder::new();
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..2];
let payer_mint_accounts = &context.users[1].token_accounts[0..2];
//
// SETUP: Create a group and an account to fill the vaults
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
}
.create(solana)
.await;
//let quote_token = &tokens[0];
let base_token = &tokens[1];
// 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;
//
// TEST: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
payer,
perp_market_index: 0,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.8,
init_asset_weight: 0.6,
maint_liab_weight: 1.2,
init_liab_weight: 1.4,
liquidation_fee: 0.05,
maker_fee: 0.0,
taker_fee: 0.0,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await
},
)
.await
.unwrap();
let price_lots = {
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
perp_market.native_price_to_lot(I80F48::ONE)
};
//
// 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 = 1000;
let account = create_funded_account(
&solana,
group,
owner,
idx,
&context_ref.users[1],
&mints[0..1],
deposit_amount,
0,
)
.await;
send_tx(
solana,
TokenDepositInstruction {
amount: 1,
account,
token_account: payer_mint_accounts[1],
token_authority: payer,
bank_index: 0,
},
)
.await
.unwrap();
account
};
let account_0 = make_account(0).await;
let account_1 = make_account(1).await;
//
// SETUP: Trade perps between accounts
//
// health was 1000 * 0.6 = 600 before
// after this order it is -14*100*(1.4-1) = -560 for the short
// and 14*100*(0.6-1) = -560 for the long
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 14,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 14,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
//
// 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();
// verify health is bad: can't withdraw
assert!(send_tx(
solana,
TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account: account_0,
owner,
token_account: payer_mint_accounts[1],
bank_index: 0,
}
)
.await
.is_err());
//
// TEST: Liquidate base position
//
send_tx(
solana,
PerpLiqBasePositionInstruction {
liqor,
liqor_owner: owner,
liqee: account_0,
perp_market,
max_base_transfer: 10,
},
)
.await
.unwrap();
let liq_amount = 10.0 * 100.0 * 0.5 * (1.0 - 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 10);
assert!(assert_equal(
liqor_data.perps[0].quote_position_native(),
-liq_amount,
0.1
));
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 4);
assert!(assert_equal(
liqee_data.perps[0].quote_position_native(),
-14.0 * 100.0 + liq_amount,
0.1
));
//
// 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();
// verify health is bad: can't withdraw
assert!(send_tx(
solana,
TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account: account_1,
owner,
token_account: payer_mint_accounts[1],
bank_index: 0,
}
)
.await
.is_err());
//
// TEST: Liquidate base position
//
send_tx(
solana,
PerpLiqBasePositionInstruction {
liqor,
liqor_owner: owner,
liqee: account_1,
perp_market,
max_base_transfer: i64::MIN,
},
)
.await
.unwrap();
let liq_amount_2 = 14.0 * 100.0 * 2.0 * (1.0 + 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 10 - 14);
assert!(assert_equal(
liqor_data.perps[0].quote_position_native(),
-liq_amount + liq_amount_2,
0.1
));
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
assert!(assert_equal(
liqee_data.perps[0].quote_position_native(),
14.0 * 100.0 - liq_amount_2,
0.1
));
Ok(())
}

View File

@ -72,25 +72,8 @@ async fn test_perp() -> Result<(), TransportError> {
PerpCreateMarketInstruction {
group,
admin,
oracle: tokens[0].oracle,
asks: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
bids: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
event_queue: {
context
.solana
.create_account_for_type::<EventQueue>(&mango_v4::id())
.await
},
payer,
perp_market_index: 0,
base_token_index: tokens[0].index,
base_token_decimals: tokens[0].mint.decimals,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.975,
@ -100,6 +83,7 @@ async fn test_perp() -> Result<(), TransportError> {
liquidation_fee: 0.012,
maker_fee: -0.0001,
taker_fee: 0.0002,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
},
)
.await
@ -116,13 +100,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Bid,
price_lots,
@ -163,13 +142,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Bid,
price_lots,
@ -205,13 +179,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Bid,
price_lots,
@ -226,13 +195,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Bid,
price_lots,
@ -247,13 +211,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Bid,
price_lots,
@ -269,11 +228,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpCancelAllOrdersInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
owner,
},
)
@ -288,13 +244,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Bid,
price_lots,
@ -310,13 +261,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_1,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Ask,
price_lots,
@ -332,9 +278,7 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpConsumeEventsInstruction {
group,
perp_market,
event_queue,
mango_accounts: vec![account_0, account_1],
},
)
@ -378,13 +322,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Ask,
price_lots,
@ -400,13 +339,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_1,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Bid,
price_lots,
@ -422,9 +356,7 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpConsumeEventsInstruction {
group,
perp_market,
event_queue,
mango_accounts: vec![account_0, account_1],
},
)
@ -451,11 +383,9 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpSettlePnlInstruction {
group,
account_a: account_0,
account_b: account_1,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
@ -465,10 +395,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_1,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},

View File

@ -1,6 +1,5 @@
#![cfg(all(feature = "test-bpf"))]
use anchor_lang::prelude::ErrorCode;
use fixed::types::I80F48;
use mango_v4::{error::MangoError, state::*};
use program_test::*;
@ -134,36 +133,13 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
//
// TEST: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket {
perp_market,
asks,
bids,
event_queue,
..
} = send_tx(
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
oracle: tokens[0].oracle,
asks: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
bids: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
event_queue: {
context
.solana
.create_account_for_type::<EventQueue>(&mango_v4::id())
.await
},
payer,
perp_market_index: 0,
base_token_index: tokens[0].index,
base_token_decimals: tokens[0].mint.decimals,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.975,
@ -173,6 +149,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
},
)
.await
@ -189,25 +166,8 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
PerpCreateMarketInstruction {
group,
admin,
oracle: tokens[1].oracle,
asks: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
bids: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
event_queue: {
context
.solana
.create_account_for_type::<EventQueue>(&mango_v4::id())
.await
},
payer,
perp_market_index: 1,
base_token_index: tokens[1].index,
base_token_decimals: tokens[1].mint.decimals,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.975,
@ -217,6 +177,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
},
)
.await
@ -247,13 +208,8 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Bid,
price_lots,
@ -268,13 +224,8 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_1,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Ask,
price_lots,
@ -289,9 +240,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
send_tx(
solana,
PerpConsumeEventsInstruction {
group,
perp_market,
event_queue,
mango_accounts: vec![account_0, account_1],
},
)
@ -315,11 +264,9 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let result = send_tx(
solana,
PerpSettlePnlInstruction {
group,
account_a: account_1,
account_b: account_0,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[1].bank,
max_settle_amount: u64::MAX,
},
@ -332,36 +279,13 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
"Bank must be valid for quote currency".to_string(),
);
// Oracle must be valid for the perp market
let result = send_tx(
solana,
PerpSettlePnlInstruction {
group,
account_a: account_1,
account_b: account_0,
perp_market,
oracle: tokens[1].oracle, // Using oracle for token 1 not 0
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
)
.await;
assert_mango_error(
&result,
ErrorCode::ConstraintHasOne.into(),
"Oracle must be valid for perp market".to_string(),
);
// Cannot settle with yourself
let result = send_tx(
solana,
PerpSettlePnlInstruction {
group,
account_a: account_0,
account_b: account_0,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
@ -378,11 +302,9 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let result = send_tx(
solana,
PerpSettlePnlInstruction {
group,
account_a: account_0,
account_b: account_1,
perp_market: perp_market_2,
oracle: tokens[1].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
@ -399,11 +321,9 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let result = send_tx(
solana,
PerpSettlePnlInstruction {
group,
account_a: account_0,
account_b: account_1,
perp_market: perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: 0,
},
@ -452,11 +372,9 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let result = send_tx(
solana,
PerpSettlePnlInstruction {
group,
account_a: account_1,
account_b: account_0,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
@ -472,11 +390,9 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let result = send_tx(
solana,
PerpSettlePnlInstruction {
group,
account_a: account_0,
account_b: account_1,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
@ -525,11 +441,9 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
send_tx(
solana,
PerpSettlePnlInstruction {
group,
account_a: account_0,
account_b: account_1,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: partial_settle_amount,
},
@ -590,11 +504,9 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
send_tx(
solana,
PerpSettlePnlInstruction {
group,
account_a: account_0,
account_b: account_1,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
@ -685,11 +597,9 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
send_tx(
solana,
PerpSettlePnlInstruction {
group,
account_a: account_1,
account_b: account_0,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},

View File

@ -1,6 +1,5 @@
#![cfg(all(feature = "test-bpf"))]
use anchor_lang::prelude::ErrorCode;
use fixed::types::I80F48;
use mango_v4::{error::MangoError, state::*};
use program_test::*;
@ -134,36 +133,13 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
//
// TEST: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket {
perp_market,
asks,
bids,
event_queue,
..
} = send_tx(
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
oracle: tokens[0].oracle,
asks: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
bids: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
event_queue: {
context
.solana
.create_account_for_type::<EventQueue>(&mango_v4::id())
.await
},
payer,
perp_market_index: 0,
base_token_index: tokens[0].index,
base_token_decimals: tokens[0].mint.decimals,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.975,
@ -173,6 +149,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
},
)
.await
@ -189,25 +166,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
PerpCreateMarketInstruction {
group,
admin,
oracle: tokens[1].oracle,
asks: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
bids: context
.solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
event_queue: {
context
.solana
.create_account_for_type::<EventQueue>(&mango_v4::id())
.await
},
payer,
perp_market_index: 1,
base_token_index: tokens[1].index,
base_token_decimals: tokens[1].mint.decimals,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.975,
@ -217,6 +177,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
},
)
.await
@ -247,13 +208,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Bid,
price_lots,
@ -268,13 +224,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_1,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Ask,
price_lots,
@ -289,9 +240,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
send_tx(
solana,
PerpConsumeEventsInstruction {
group,
perp_market,
event_queue,
mango_accounts: vec![account_0, account_1],
},
)
@ -313,10 +262,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
let result = send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_0,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[1].bank,
max_settle_amount: u64::MAX,
},
@ -329,34 +276,12 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
"Bank must be valid for quote currency".to_string(),
);
// Oracle must be valid for the perp market
let result = send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_0,
perp_market,
oracle: tokens[1].oracle, // Using oracle for token 1 not 0
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
)
.await;
assert_mango_error(
&result,
ErrorCode::ConstraintHasOne.into(),
"Oracle must be valid for perp market".to_string(),
);
// Cannot settle position that does not exist
let result = send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_1,
perp_market: perp_market_2,
oracle: tokens[1].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
@ -373,10 +298,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
let result = send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_1,
perp_market: perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: 0,
},
@ -423,10 +346,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
let result = send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_0,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
@ -509,10 +430,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_1,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: partial_settle_amount,
},
@ -565,10 +484,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_1,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},