diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index 7b3d7d18d..333b056e7 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -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; diff --git a/programs/mango-v4/src/instructions/perp_create_market.rs b/programs/mango-v4/src/instructions/perp_create_market.rs index d5c76fb16..212425837 100644 --- a/programs/mango-v4/src/instructions/perp_create_market.rs +++ b/programs/mango-v4/src/instructions/perp_create_market.rs @@ -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, diff --git a/programs/mango-v4/src/instructions/perp_settle_fees.rs b/programs/mango-v4/src/instructions/perp_settle_fees.rs new file mode 100644 index 000000000..8eddb0ab4 --- /dev/null +++ b/programs/mango-v4/src/instructions/perp_settle_fees.rs @@ -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, 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::().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(()) +} diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 2a05f52b2..6d51cd168 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -498,6 +498,10 @@ pub mod mango_v4 { pub fn perp_settle_pnl(ctx: Context, max_settle_amount: I80F48) -> Result<()> { instructions::perp_settle_pnl(ctx, max_settle_amount) } + + pub fn perp_settle_fees(ctx: Context, 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 diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 46447327c..d1471f24a 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -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], diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 010ddf435..8c426a36e 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -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::(), - 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::() % 8, 0); diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 63ec9e95a..6c7f98f5d 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -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 { diff --git a/programs/mango-v4/tests/program_test/utils.rs b/programs/mango-v4/tests/program_test/utils.rs index c053b6065..1d24f0060 100644 --- a/programs/mango-v4/tests/program_test/utils.rs +++ b/programs/mango-v4/tests/program_test/utils.rs @@ -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 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( + result: &Result, + 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"), + } +} diff --git a/programs/mango-v4/tests/test_perp_settle.rs b/programs/mango-v4/tests/test_perp_settle.rs index 27a72f807..9702ab374 100644 --- a/programs/mango-v4/tests/test_perp_settle.rs +++ b/programs/mango-v4/tests/test_perp_settle.rs @@ -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(result: &Result, 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"), - } -} diff --git a/programs/mango-v4/tests/test_perp_settle_fees.rs b/programs/mango-v4/tests/test_perp_settle_fees.rs new file mode 100644 index 000000000..00dfa4ff3 --- /dev/null +++ b/programs/mango-v4/tests/test_perp_settle_fees.rs @@ -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::(&mango_v4::id()) + .await, + bids: context + .solana + .create_account_for_type::(&mango_v4::id()) + .await, + event_queue: { + context + .solana + .create_account_for_type::(&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::(&mango_v4::id()) + .await, + bids: context + .solana + .create_account_for_type::(&mango_v4::id()) + .await, + event_queue: { + context + .solana + .create_account_for_type::(&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::(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::(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::(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::(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::(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::(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::(tokens[0].bank).await; + let mango_account_1 = solana.get_account::(account_1).await; + let perp_market = solana.get_account::(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::(tokens[0].bank).await; + let mango_account_1 = solana.get_account::(account_1).await; + let perp_market = solana.get_account::(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(()) +}