Added perp_settle_pnl instruction

This commit is contained in:
Conj0iner 2022-09-02 00:07:57 +08:00
parent 511db72f8e
commit 97eed4081d
8 changed files with 1006 additions and 12 deletions

View File

@ -39,6 +39,16 @@ pub enum MangoError {
InsufficentBankVaultFunds,
#[msg("account is currently being liquidated")]
BeingLiquidated,
#[msg("invalid bank")]
InvalidBank,
#[msg("account profitability is mismatched")]
ProfitabilityMismatch,
#[msg("cannot settle with self")]
CannotSettleWithSelf,
#[msg("perp position does not exist")]
PerpPositionDoesNotExist,
#[msg("max settle amount must be greater than zero")]
MaxSettleAmountMustBeGreaterThanZero,
}
pub trait Contextable {

View File

@ -20,6 +20,7 @@ pub use perp_consume_events::*;
pub use perp_create_market::*;
pub use perp_edit_market::*;
pub use perp_place_order::*;
pub use perp_settle_pnl::*;
pub use perp_update_funding::*;
pub use serum3_cancel_all_orders::*;
pub use serum3_cancel_order::*;
@ -64,6 +65,7 @@ mod perp_consume_events;
mod perp_create_market;
mod perp_edit_market;
mod perp_place_order;
mod perp_settle_pnl;
mod perp_update_funding;
mod serum3_cancel_all_orders;
mod serum3_cancel_order;

View File

@ -0,0 +1,124 @@
use anchor_lang::prelude::*;
use checked_math as cm;
use fixed::types::I80F48;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::state::compute_health;
use crate::state::new_fixed_order_account_retriever;
use crate::state::Bank;
use crate::state::HealthType;
use crate::state::MangoAccount;
use crate::state::TokenPosition;
use crate::state::QUOTE_TOKEN_INDEX;
use crate::state::{oracle_price, AccountLoaderDynamic, Group, PerpMarket};
#[derive(Accounts)]
pub struct PerpSettlePnl<'info> {
pub group: AccountLoader<'info, Group>,
#[account(has_one = group, has_one = oracle)]
pub perp_market: AccountLoader<'info, PerpMarket>,
// This account MUST be profitable
#[account(mut, has_one = group)]
pub account_a: AccountLoaderDynamic<'info, MangoAccount>,
// This account MUST have a loss
#[account(mut, has_one = group)]
pub account_b: AccountLoaderDynamic<'info, MangoAccount>,
pub oracle: UncheckedAccount<'info>,
#[account(mut, has_one = group)]
pub quote_bank: AccountLoader<'info, Bank>,
}
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>, max_settle_amount: I80F48) -> Result<()> {
// Cannot settle with yourself
require!(
ctx.accounts.account_a.to_account_info().key
!= ctx.accounts.account_b.to_account_info().key,
MangoError::CannotSettleWithSelf
);
// max_settle_amount must greater than zero
require!(
max_settle_amount > 0,
MangoError::MaxSettleAmountMustBeGreaterThanZero
);
let mut account_a = ctx.accounts.account_a.load_mut()?;
let mut account_b = ctx.accounts.account_b.load_mut()?;
let mut bank = ctx.accounts.quote_bank.load_mut()?;
let perp_market = ctx.accounts.perp_market.load()?;
// Verify that the bank is the quote currency bank
require!(
bank.token_index == QUOTE_TOKEN_INDEX,
MangoError::InvalidBank
);
// Get oracle price for market. Price is validated inside
let oracle_price = oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
perp_market.oracle_config.conf_filter,
perp_market.base_token_decimals,
)?;
// Fetch perp positions for accounts
let mut a_perp_position = account_a.perp_position_mut(perp_market.perp_market_index)?;
let mut b_perp_position = account_b.perp_position_mut(perp_market.perp_market_index)?;
// Settle funding before settling any PnL
a_perp_position.settle_funding(&perp_market);
b_perp_position.settle_funding(&perp_market);
// Calculate PnL for each account
let a_base_native = a_perp_position.base_position_native(&perp_market);
let b_base_native = b_perp_position.base_position_native(&perp_market);
let a_pnl: I80F48 = cm!(a_perp_position.quote_position_native + a_base_native * oracle_price);
let b_pnl: I80F48 = cm!(b_perp_position.quote_position_native + b_base_native * oracle_price);
// Account A must be profitable, and B must be unprofitable
// PnL must be opposite signs for there to be a settlement
require!(a_pnl.is_positive(), MangoError::ProfitabilityMismatch);
require!(b_pnl.is_negative(), MangoError::ProfitabilityMismatch);
// Settle for the maximum possible capped to max_settle_amount
let settlement = a_pnl.abs().min(b_pnl.abs()).min(max_settle_amount);
a_perp_position.quote_position_native = cm!(a_perp_position.quote_position_native - settlement);
b_perp_position.quote_position_native = cm!(b_perp_position.quote_position_native + settlement);
// Update the account's net_settled with the new PnL
let settlement_i64 = settlement.checked_to_num::<i64>().unwrap();
account_a.fixed.net_settled = cm!(account_a.fixed.net_settled + settlement_i64);
account_b.fixed.net_settled = cm!(account_b.fixed.net_settled - settlement_i64);
// Transfer token balances
// TODO: Need to guarantee that QUOTE_TOKEN_INDEX token exists at this point. I.E. create it when placing perp order.
let a_token_position = account_a.ensure_token_position(QUOTE_TOKEN_INDEX)?.0;
let b_token_position = account_b.ensure_token_position(QUOTE_TOKEN_INDEX)?.0;
transfer_token_internal(&mut bank, b_token_position, a_token_position, settlement)?;
// Bank is dropped to prevent re-borrow from remaining_accounts
drop(bank);
// Verify that the result of settling did not violate the health of the account that lost money
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account_b.borrow())?;
let health = compute_health(&account_b.borrow(), HealthType::Init, &retriever)?;
require!(health >= 0, MangoError::HealthMustBePositive);
msg!("settled pnl = {}", settlement);
Ok(())
}
fn transfer_token_internal(
bank: &mut Bank,
from_position: &mut TokenPosition,
to_position: &mut TokenPosition,
native_amount: I80F48,
) -> Result<()> {
bank.deposit(to_position, native_amount)?;
bank.withdraw_with_fee(from_position, native_amount)?;
Ok(())
}

View File

@ -497,6 +497,9 @@ pub mod mango_v4 {
instructions::perp_update_funding(ctx)
}
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>, max_settle_amount: I80F48) -> Result<()> {
instructions::perp_settle_pnl(ctx, max_settle_amount)
}
// TODO
// perp_force_cancel_order
@ -504,7 +507,7 @@ pub mod mango_v4 {
// liquidate_token_and_perp
// liquidate_perp_and_perp
// settle_* - settle_funds, settle_pnl, settle_fees
// settle_* - settle_funds, settle_fees
// resolve_banktruptcy

View File

@ -690,6 +690,18 @@ impl<
get_helper_mut(self.dynamic_mut(), offset)
}
pub fn perp_position_mut(
&mut self,
market_index: PerpMarketIndex,
) -> Result<&mut PerpPosition> {
let raw_index_opt = self
.all_perp_positions()
.position(|p| p.is_active_for_market(market_index));
raw_index_opt
.map(|raw_index| self.perp_position_mut_by_raw_index(raw_index))
.ok_or_else(|| error!(MangoError::PerpPositionDoesNotExist))
}
pub fn ensure_perp_position(
&mut self,
perp_market_index: PerpMarketIndex,
@ -1205,6 +1217,17 @@ mod tests {
assert_eq!(pos.market_index, 42);
}
{
let pos_res = account.perp_position_mut(1);
assert!(pos_res.is_ok());
assert_eq!(pos_res.unwrap().market_index, 1)
}
{
let pos_res = account.perp_position_mut(99);
assert!(pos_res.is_err());
}
{
account.deactivate_perp_position(1);
@ -1228,16 +1251,5 @@ mod tests {
assert!(account.perp_position(8).is_ok());
assert!(account.perp_position(42).is_ok());
assert_eq!(account.active_perp_positions().count(), 2);
/*{
let (pos, raw) = account.perp_position_mut(42).unwrap();
assert_eq!(pos.perp_index, 42);
assert_eq!(raw, 2);
}
{
let (pos, raw) = account.perp_position_mut(8).unwrap();
assert_eq!(pos.perp_index, 8);
assert_eq!(raw, 1);
}*/
}
}

View File

@ -235,6 +235,11 @@ impl PerpPosition {
self.market_index == market_index
}
// Return base position in native units for a perp market
pub fn base_position_native(&self, market: &PerpMarket) -> I80F48 {
I80F48::from(cm!(self.base_position_lots * market.base_lot_size))
}
/// This assumes settle_funding was already called
pub fn change_base_position(&mut self, perp_market: &mut PerpMarket, base_change: i64) {
let start = self.base_position_lots;

View File

@ -2481,6 +2481,61 @@ 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: I80F48,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpSettlePnlInstruction {
type Accounts = mango_v4::accounts::PerpSettlePnl;
type Instruction = mango_v4::instruction::PerpSettlePnl;
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_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
.load_mango_account(&self.account_b)
.await
.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account_b,
None,
false,
Some(perp_market.perp_market_index),
)
.await;
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas);
(accounts, instruction)
}
fn signers(&self) -> Vec<&Keypair> {
vec![]
}
}
pub struct BenchmarkInstruction {}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for BenchmarkInstruction {

View File

@ -0,0 +1,783 @@
#![cfg(all(feature = "test-bpf"))]
use anchor_lang::prelude::ErrorCode;
use fixed::types::I80F48;
use mango_v4::{error::MangoError, state::*};
use program_test::*;
use solana_program::instruction::InstructionError;
use solana_program_test::*;
use solana_sdk::{signature::Keypair, transaction::TransactionError, transport::TransportError};
mod program_test;
#[tokio::test]
async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = &Keypair::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];
let initial_token_deposit = 10_000;
//
// SETUP: Create a group and an account
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
}
.create(solana)
.await;
let account_0 = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
let account_1 = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
//
// SETUP: Deposit user funds
//
{
let deposit_amount = initial_token_deposit;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_0,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_0,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
{
let deposit_amount = initial_token_deposit;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_1,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_1,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
//
// TEST: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket {
perp_market,
asks,
bids,
event_queue,
..
} = 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,
init_asset_weight: 0.95,
maint_liab_weight: 1.025,
init_liab_weight: 1.05,
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,
},
)
.await
.unwrap();
//
// TEST: Create another perp market
//
let mango_v4::accounts::PerpCreateMarket {
perp_market: perp_market_2,
..
} = send_tx(
solana,
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,
init_asset_weight: 0.95,
maint_liab_weight: 1.025,
init_liab_weight: 1.05,
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,
},
)
.await
.unwrap();
let price_lots = {
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
perp_market.native_price_to_lot(I80F48::from(1000))
};
// Set the initial oracle price
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[0].pubkey,
payer,
price: "1000.0",
},
)
.await
.unwrap();
//
// Place orders and create a position
//
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_1,
perp_market,
asks,
bids,
event_queue,
oracle: tokens[0].oracle,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
group,
perp_market,
event_queue,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
{
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_0.perps[0].base_position_lots, 1);
assert_eq!(mango_account_1.perps[0].base_position_lots, -1);
assert_eq!(
mango_account_0.perps[0].quote_position_native.round(),
-100_020
);
assert_eq!(mango_account_1.perps[0].quote_position_native, 100_000);
}
// Bank must be valid for quote currency
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: I80F48::MAX,
},
)
.await;
assert_mango_error(
&result,
MangoError::InvalidBank.into(),
"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: I80F48::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: I80F48::MAX,
},
)
.await;
assert_mango_error(
&result,
MangoError::CannotSettleWithSelf.into(),
"Cannot settle with yourself".to_string(),
);
// Cannot settle position that does not exist
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: I80F48::MAX,
},
)
.await;
assert_mango_error(
&result,
MangoError::PerpPositionDoesNotExist.into(),
"Cannot settle a position that does not exist".to_string(),
);
// max_settle_amount must be greater than zero
for max_amnt in vec![I80F48::ZERO, I80F48::from(-100)] {
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: max_amnt,
},
)
.await;
assert_mango_error(
&result,
MangoError::MaxSettleAmountMustBeGreaterThanZero.into(),
"max_settle_amount must be greater than zero".to_string(),
);
}
// TODO: Test funding settlement
{
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
assert_eq!(
mango_account_0.tokens[0].native(&bank).round(),
initial_token_deposit,
"account 0 has expected amount of tokens"
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
initial_token_deposit,
"account 1 has expected amount of tokens"
);
}
// Try and settle with high price
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[0].pubkey,
payer,
price: "1200.0",
},
)
.await
.unwrap();
// Account a must be the profitable one
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: I80F48::MAX,
},
)
.await;
assert_mango_error(
&result,
MangoError::ProfitabilityMismatch.into(),
"Account a must be the profitable one".to_string(),
);
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: I80F48::MAX,
},
)
.await;
assert_mango_error(
&result,
MangoError::HealthMustBePositive.into(),
"Health of losing account must be positive to settle".to_string(),
);
// Change the oracle to a more reasonable price
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[0].pubkey,
payer,
price: "1005.0",
},
)
.await
.unwrap();
let expected_pnl_0 = I80F48::from(480); // Less due to fees
let expected_pnl_1 = I80F48::from(-500);
{
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
get_pnl_native(&mango_account_0.perps[0], &perp_market, I80F48::from(1005)).round(),
expected_pnl_0
);
assert_eq!(
get_pnl_native(&mango_account_1.perps[0], &perp_market, I80F48::from(1005)),
expected_pnl_1
);
}
solana.advance_clock().await;
// Partially execute the settle
let partial_settle_amount = I80F48::from(200);
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,
},
)
.await
.unwrap();
{
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(
mango_account_0.perps[0].base_position_lots, 1,
"base position unchanged for account 0"
);
assert_eq!(
mango_account_1.perps[0].base_position_lots, -1,
"base position unchanged for account 1"
);
assert_eq!(
mango_account_0.perps[0].quote_position_native.round(),
I80F48::from(-100_020) - partial_settle_amount,
"quote position reduced for profitable position by max_settle_amount"
);
assert_eq!(
mango_account_1.perps[0].quote_position_native.round(),
I80F48::from(100_000) + partial_settle_amount,
"quote position increased for losing position by opposite of first account"
);
assert_eq!(
mango_account_0.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit) + partial_settle_amount,
"account 0 token native position increased (profit) by max_settle_amount"
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit) - partial_settle_amount,
"account 1 token native position decreased (loss) by max_settle_amount"
);
assert_eq!(
mango_account_0.net_settled, partial_settle_amount,
"net_settled on account 0 updated with profit from settlement"
);
assert_eq!(
mango_account_1.net_settled, -partial_settle_amount,
"net_settled on account 1 updated with loss from settlement"
);
}
solana.advance_clock().await;
// Fully execute the settle
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: I80F48::MAX,
},
)
.await
.unwrap();
{
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(
mango_account_0.perps[0].base_position_lots, 1,
"base position unchanged for account 0"
);
assert_eq!(
mango_account_1.perps[0].base_position_lots, -1,
"base position unchanged for account 1"
);
assert_eq!(
mango_account_0.perps[0].quote_position_native.round(),
I80F48::from(-100_020) - expected_pnl_0,
"quote position reduced for profitable position"
);
assert_eq!(
mango_account_1.perps[0].quote_position_native.round(),
I80F48::from(100_000) + expected_pnl_0,
"quote position increased for losing position by opposite of first account"
);
assert_eq!(
mango_account_0.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit) + expected_pnl_0,
"account 0 token native position increased (profit)"
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit) - expected_pnl_0,
"account 1 token native position decreased (loss)"
);
assert_eq!(
mango_account_0.net_settled, expected_pnl_0,
"net_settled on account 0 updated with profit from settlement"
);
assert_eq!(
mango_account_1.net_settled, -expected_pnl_0,
"net_settled on account 1 updated with loss from settlement"
);
}
solana.advance_clock().await;
// Change the oracle to a reasonable price in other direction
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[0].pubkey,
payer,
price: "995.0",
},
)
.await
.unwrap();
let expected_pnl_0 = I80F48::from(-1000);
let expected_pnl_1 = I80F48::from(980);
{
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
get_pnl_native(&mango_account_0.perps[0], &perp_market, I80F48::from(995)).round(),
expected_pnl_0
);
assert_eq!(
get_pnl_native(&mango_account_1.perps[0], &perp_market, I80F48::from(995)).round(),
expected_pnl_1
);
}
solana.advance_clock().await;
// Fully execute the settle
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: I80F48::MAX,
},
)
.await
.unwrap();
{
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(
mango_account_0.perps[0].base_position_lots, 1,
"base position unchanged for account 0"
);
assert_eq!(
mango_account_1.perps[0].base_position_lots, -1,
"base position unchanged for account 1"
);
assert_eq!(
mango_account_0.perps[0].quote_position_native.round(),
I80F48::from(-100_500) + expected_pnl_1,
"quote position increased for losing position"
);
assert_eq!(
mango_account_1.perps[0].quote_position_native.round(),
I80F48::from(100_480) - expected_pnl_1,
"quote position reduced for losing position by opposite of first account"
);
// 480 was previous settlement
assert_eq!(
mango_account_0.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit + 480) - expected_pnl_1,
"account 0 token native position decreased (loss)"
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit - 480) + expected_pnl_1,
"account 1 token native position increased (profit)"
);
assert_eq!(
mango_account_0.net_settled,
I80F48::from(480) - expected_pnl_1,
"net_settled on account 0 updated with loss from settlement"
);
assert_eq!(
mango_account_1.net_settled,
I80F48::from(-480) + expected_pnl_1,
"net_settled on account 1 updated with profit from settlement"
);
}
Ok(())
}
fn get_pnl_native(
perp_position: &PerpPosition,
perp_market: &PerpMarket,
oracle_price: I80F48,
) -> I80F48 {
let contract_size = perp_market.base_lot_size;
let new_quote_pos =
I80F48::from_num(-perp_position.base_position_lots * contract_size) * oracle_price;
perp_position.quote_position_native - new_quote_pos
}
fn assert_mango_error<T>(result: &Result<T, TransportError>, expected_error: u32, comment: String) {
match result {
Ok(_) => assert!(false, "No error returned"),
Err(TransportError::TransactionError(tx_err)) => match tx_err {
TransactionError::InstructionError(_, err) => match err {
InstructionError::Custom(err_num) => {
assert_eq!(*err_num, expected_error, "{}", comment);
}
_ => assert!(false, "Not a mango error"),
},
_ => assert!(false, "Not a mango error"),
},
_ => assert!(false, "Not a mango error"),
}
}