Merge pull request #204 from blockworks-foundation/cj/settle_fees
Add perp_settle_fees instruction
This commit is contained in:
commit
2693a20e16
|
@ -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_fees::*;
|
||||
pub use perp_settle_pnl::*;
|
||||
pub use perp_update_funding::*;
|
||||
pub use serum3_cancel_all_orders::*;
|
||||
|
@ -65,6 +66,7 @@ mod perp_consume_events;
|
|||
mod perp_create_market;
|
||||
mod perp_edit_market;
|
||||
mod perp_place_order;
|
||||
mod perp_settle_fees;
|
||||
mod perp_settle_pnl;
|
||||
mod perp_update_funding;
|
||||
mod serum3_cancel_all_orders;
|
||||
|
|
|
@ -91,6 +91,7 @@ pub fn perp_create_market(
|
|||
open_interest: 0,
|
||||
seq_num: 0,
|
||||
fees_accrued: I80F48::ZERO,
|
||||
fees_settled: I80F48::ZERO,
|
||||
// Why optional - Perp could be based purely on an oracle
|
||||
bump: *ctx.bumps.get("perp_market").ok_or(MangoError::SomeError)?,
|
||||
base_token_decimals,
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
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::QUOTE_TOKEN_INDEX;
|
||||
use crate::state::{oracle_price, AccountLoaderDynamic, Group, PerpMarket};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct PerpSettleFees<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(mut, has_one = group, has_one = oracle)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
|
||||
// This account MUST have a loss
|
||||
#[account(mut, has_one = group)]
|
||||
pub account: AccountLoaderDynamic<'info, MangoAccount>,
|
||||
|
||||
pub oracle: UncheckedAccount<'info>,
|
||||
|
||||
#[account(mut, has_one = group)]
|
||||
pub quote_bank: AccountLoader<'info, Bank>,
|
||||
}
|
||||
|
||||
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: I80F48) -> Result<()> {
|
||||
// max_settle_amount must greater than zero
|
||||
require!(
|
||||
max_settle_amount > 0,
|
||||
MangoError::MaxSettleAmountMustBeGreaterThanZero
|
||||
);
|
||||
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let mut bank = ctx.accounts.quote_bank.load_mut()?;
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
|
||||
// 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 perp_position = account.perp_position_mut(perp_market.perp_market_index)?;
|
||||
|
||||
// Settle funding before settling any PnL
|
||||
perp_position.settle_funding(&perp_market);
|
||||
|
||||
// Calculate PnL for each account
|
||||
let base_native = perp_position.base_position_native(&perp_market);
|
||||
let pnl: I80F48 = cm!(perp_position.quote_position_native + base_native * oracle_price);
|
||||
|
||||
// Account perp position must have a loss to be able to settle against the fee account
|
||||
require!(pnl.is_negative(), MangoError::ProfitabilityMismatch);
|
||||
require!(
|
||||
perp_market.fees_accrued.is_positive(),
|
||||
MangoError::ProfitabilityMismatch
|
||||
);
|
||||
|
||||
// Settle for the maximum possible capped to max_settle_amount
|
||||
let settlement = pnl
|
||||
.abs()
|
||||
.min(perp_market.fees_accrued.abs())
|
||||
.min(max_settle_amount);
|
||||
perp_position.quote_position_native = cm!(perp_position.quote_position_native + settlement);
|
||||
perp_market.fees_accrued = cm!(perp_market.fees_accrued - settlement);
|
||||
|
||||
// Update the account's net_settled with the new PnL
|
||||
let settlement_i64 = settlement.round().checked_to_num::<i64>().unwrap();
|
||||
account.fixed.net_settled = cm!(account.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 token_position = account.ensure_token_position(QUOTE_TOKEN_INDEX)?.0;
|
||||
bank.withdraw_with_fee(token_position, settlement)?;
|
||||
// Update the settled balance on the market itself
|
||||
perp_market.fees_settled = cm!(perp_market.fees_settled + settlement);
|
||||
|
||||
// Bank & perp_market are dropped to prevent re-borrow from remaining_accounts
|
||||
drop(bank);
|
||||
drop(perp_market);
|
||||
|
||||
// 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.borrow())?;
|
||||
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
|
||||
require!(health >= 0, MangoError::HealthMustBePositive);
|
||||
|
||||
msg!("settled fees = {}", settlement);
|
||||
Ok(())
|
||||
}
|
|
@ -498,6 +498,10 @@ pub mod mango_v4 {
|
|||
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>, max_settle_amount: I80F48) -> Result<()> {
|
||||
instructions::perp_settle_pnl(ctx, max_settle_amount)
|
||||
}
|
||||
|
||||
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: I80F48) -> Result<()> {
|
||||
instructions::perp_settle_fees(ctx, max_settle_amount)
|
||||
}
|
||||
// TODO
|
||||
|
||||
// perp_force_cancel_order
|
||||
|
@ -505,7 +509,7 @@ pub mod mango_v4 {
|
|||
// liquidate_token_and_perp
|
||||
// liquidate_perp_and_perp
|
||||
|
||||
// settle_* - settle_funds, settle_fees
|
||||
// settle_* - settle_funds
|
||||
|
||||
// resolve_banktruptcy
|
||||
|
||||
|
|
|
@ -420,6 +420,7 @@ mod tests {
|
|||
open_interest: 0,
|
||||
seq_num: 0,
|
||||
fees_accrued: I80F48::ZERO,
|
||||
fees_settled: I80F48::ZERO,
|
||||
bump: 0,
|
||||
base_token_decimals: 0,
|
||||
reserved: [0; 128],
|
||||
|
|
|
@ -75,6 +75,9 @@ pub struct PerpMarket {
|
|||
/// Fees accrued in native quote currency
|
||||
pub fees_accrued: I80F48,
|
||||
|
||||
/// Fees settled in native quote currency
|
||||
pub fees_settled: I80F48,
|
||||
|
||||
/// Liquidity mining metadata
|
||||
/// pub liquidity_mining_info: LiquidityMiningInfo,
|
||||
|
||||
|
@ -95,7 +98,7 @@ pub struct PerpMarket {
|
|||
|
||||
const_assert_eq!(
|
||||
size_of::<PerpMarket>(),
|
||||
32 + 2 + 2 + 4 + 16 + 32 + 16 + 32 * 3 + 8 * 2 + 16 * 11 + 8 * 2 + 8 * 2 + 16 + 2 + 6 + 8 + 128
|
||||
32 + 2 + 2 + 4 + 16 + 32 + 16 + 32 * 3 + 8 * 2 + 16 * 12 + 8 * 2 + 8 * 2 + 16 + 2 + 6 + 8 + 128
|
||||
);
|
||||
const_assert_eq!(size_of::<PerpMarket>() % 8, 0);
|
||||
|
||||
|
|
|
@ -2536,6 +2536,59 @@ 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: I80F48,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for PerpSettleFeesInstruction {
|
||||
type Accounts = mango_v4::accounts::PerpSettleFees;
|
||||
type Instruction = mango_v4::instruction::PerpSettleFees;
|
||||
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: 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
|
||||
.load_mango_account(&self.account)
|
||||
.await
|
||||
.unwrap();
|
||||
let health_check_metas = derive_health_check_remaining_account_metas(
|
||||
&account_loader,
|
||||
&account,
|
||||
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 {
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use bytemuck::{bytes_of, Contiguous};
|
||||
use fixed::types::I80F48;
|
||||
use mango_v4::state::{PerpMarket, PerpPosition};
|
||||
use solana_program::instruction::InstructionError;
|
||||
use solana_program::program_error::ProgramError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::transaction::TransactionError;
|
||||
use solana_sdk::transport::TransportError;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub fn gen_signer_seeds<'a>(nonce: &'a u64, acc_pk: &'a Pubkey) -> [&'a [u8]; 2] {
|
||||
|
@ -75,3 +80,34 @@ impl From<Keypair> for TestKeypair {
|
|||
Self(k)
|
||||
}
|
||||
}
|
||||
|
||||
pub 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
|
||||
}
|
||||
|
||||
pub 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"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -754,30 +754,3 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
|
|||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,623 @@
|
|||
#![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_test::*;
|
||||
use solana_sdk::{signature::Keypair, transport::TransportError};
|
||||
|
||||
mod program_test;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_perp_settle_fees() -> 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;
|
||||
assert_eq!(mango_account_0.perps[0].base_position_lots, 1);
|
||||
assert_eq!(
|
||||
mango_account_0.perps[0].quote_position_native.round(),
|
||||
-100_020
|
||||
);
|
||||
|
||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(mango_account_1.perps[0].base_position_lots, -1);
|
||||
assert_eq!(mango_account_1.perps[0].quote_position_native, 100_000);
|
||||
|
||||
// Bank must be valid for quote currency
|
||||
let result = send_tx(
|
||||
solana,
|
||||
PerpSettleFeesInstruction {
|
||||
group,
|
||||
account: 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,
|
||||
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: I80F48::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: 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,
|
||||
PerpSettleFeesInstruction {
|
||||
group,
|
||||
account: 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 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 must have a loss
|
||||
let result = send_tx(
|
||||
solana,
|
||||
PerpSettleFeesInstruction {
|
||||
group,
|
||||
account: 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 must be unprofitable".to_string(),
|
||||
);
|
||||
|
||||
// TODO: Difficult to test health due to fees being so small. Need alternative
|
||||
// let result = send_tx(
|
||||
// solana,
|
||||
// PerpSettleFeesInstruction {
|
||||
// group,
|
||||
// account: 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 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;
|
||||
|
||||
// Check the fees accrued
|
||||
let initial_fees = I80F48::from(20);
|
||||
{
|
||||
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||
assert_eq!(
|
||||
perp_market.fees_accrued.round(),
|
||||
initial_fees,
|
||||
"Fees from trading have been accrued"
|
||||
);
|
||||
assert_eq!(
|
||||
perp_market.fees_settled.round(),
|
||||
0,
|
||||
"No fees have been settled yet"
|
||||
);
|
||||
}
|
||||
|
||||
// Partially execute the settle
|
||||
let partial_settle_amount = I80F48::from(10);
|
||||
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,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||
|
||||
assert_eq!(
|
||||
mango_account_1.perps[0].base_position_lots, -1,
|
||||
"base position unchanged for account 1"
|
||||
);
|
||||
|
||||
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 fee 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_1.net_settled, -partial_settle_amount,
|
||||
"net_settled on account 1 updated with loss from settlement"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
perp_market.fees_accrued.round(),
|
||||
initial_fees - partial_settle_amount,
|
||||
"Fees accrued have been reduced by partial settle"
|
||||
);
|
||||
assert_eq!(
|
||||
perp_market.fees_settled.round(),
|
||||
partial_settle_amount,
|
||||
"Fees have been partially settled"
|
||||
);
|
||||
}
|
||||
|
||||
solana.advance_clock().await;
|
||||
|
||||
// Fully execute the settle
|
||||
send_tx(
|
||||
solana,
|
||||
PerpSettleFeesInstruction {
|
||||
group,
|
||||
account: 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_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||
|
||||
assert_eq!(
|
||||
mango_account_1.perps[0].base_position_lots, -1,
|
||||
"base position unchanged for account 1"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
mango_account_1.perps[0].quote_position_native.round(),
|
||||
I80F48::from(100_000) + initial_fees,
|
||||
"quote position increased for losing position by fees settled"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
mango_account_1.tokens[0].native(&bank).round(),
|
||||
I80F48::from(initial_token_deposit) - initial_fees,
|
||||
"account 1 token native position decreased (loss)"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
mango_account_1.net_settled, -initial_fees,
|
||||
"net_settled on account 1 updated with loss from settlement"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
perp_market.fees_accrued.round(),
|
||||
0,
|
||||
"Fees accrued have been reduced to zero"
|
||||
);
|
||||
assert_eq!(
|
||||
perp_market.fees_settled.round(),
|
||||
initial_fees,
|
||||
"Fees have been fully settled"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue