diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index 333b056e7..bb83769ed 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -18,6 +18,7 @@ pub use perp_cancel_order_by_client_order_id::*; pub use perp_close_market::*; pub use perp_consume_events::*; pub use perp_create_market::*; +pub use perp_deactivate_position::*; pub use perp_edit_market::*; pub use perp_place_order::*; pub use perp_settle_fees::*; @@ -64,6 +65,7 @@ mod perp_cancel_order_by_client_order_id; mod perp_close_market; mod perp_consume_events; mod perp_create_market; +mod perp_deactivate_position; mod perp_edit_market; mod perp_place_order; mod perp_settle_fees; diff --git a/programs/mango-v4/src/instructions/perp_deactivate_position.rs b/programs/mango-v4/src/instructions/perp_deactivate_position.rs new file mode 100644 index 000000000..c46fb866a --- /dev/null +++ b/programs/mango-v4/src/instructions/perp_deactivate_position.rs @@ -0,0 +1,61 @@ +use anchor_lang::prelude::*; + +use crate::error::*; +use crate::state::*; +use crate::util::checked_math as cm; + +#[derive(Accounts)] +pub struct PerpDeactivatePosition<'info> { + pub group: AccountLoader<'info, Group>, + + #[account( + mut, + has_one = group + // owner is checked at #1 + )] + pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub owner: Signer<'info>, + + #[account(has_one = group)] + pub perp_market: AccountLoader<'info, PerpMarket>, +} + +pub fn perp_deactivate_position(ctx: Context) -> Result<()> { + let mut account = ctx.accounts.account.load_mut()?; + // account constraint #1 + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + + let perp_market = ctx.accounts.perp_market.load()?; + let perp_position = account.perp_position_mut(perp_market.perp_market_index)?; + + // Is the perp position closable? + perp_position.settle_funding(&perp_market); + require_msg!( + perp_position.base_position_lots() == 0, + "perp position still has base lots" + ); + // No dusting needed because we're able to use settle_pnl to get this to 0. + require_msg!( + perp_position.quote_position_native() == 0, + "perp position still has quote position" + ); + require_msg!( + perp_position.bids_base_lots == 0 && perp_position.asks_base_lots == 0, + "perp position still has open orders" + ); + require_msg!( + perp_position.taker_base_lots == 0 && perp_position.taker_quote_lots == 0, + "perp position still has events on event queue" + ); + + account.deactivate_perp_position(perp_market.perp_market_index)?; + + // Reduce the in-use-count of the settlement token + let mut token_position = account.token_position_mut(QUOTE_TOKEN_INDEX)?.0; + cm!(token_position.in_use_count -= 1); + + Ok(()) +} diff --git a/programs/mango-v4/src/instructions/perp_place_order.rs b/programs/mango-v4/src/instructions/perp_place_order.rs index 4d2164e72..ebd5674ec 100644 --- a/programs/mango-v4/src/instructions/perp_place_order.rs +++ b/programs/mango-v4/src/instructions/perp_place_order.rs @@ -5,8 +5,9 @@ use crate::error::*; use crate::state::MangoAccount; use crate::state::{ new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, Book, BookSide, - EventQueue, Group, OrderType, PerpMarket, Side, + EventQueue, Group, OrderType, PerpMarket, Side, QUOTE_TOKEN_INDEX, }; +use crate::util::checked_math as cm; #[derive(Accounts)] pub struct PerpPlaceOrder<'info> { @@ -83,11 +84,21 @@ pub fn perp_place_order( let account_pk = ctx.accounts.account.key(); - let perp_market_index = { - let perp_market = ctx.accounts.perp_market.load()?; - perp_market.perp_market_index - }; - let (_, perp_position_raw_index) = account.ensure_perp_position(perp_market_index)?; + let perp_market_index = ctx.accounts.perp_market.load()?.perp_market_index; + + // + // Create the perp position if needed + // + if !account + .active_perp_positions() + .any(|p| p.is_active_for_market(perp_market_index)) + { + account.ensure_perp_position(perp_market_index)?; + + // Require that the token position for the settlement token is retained + let mut token_position = account.ensure_token_position(QUOTE_TOKEN_INDEX)?.0; + cm!(token_position.in_use_count += 1); + } // // Pre-health computation, _after_ perp position is created @@ -151,7 +162,7 @@ pub fn perp_place_order( // Health check // if let Some((mut health_cache, pre_health)) = pre_health_opt { - let perp_position = account.perp_position_by_raw_index(perp_position_raw_index); + let perp_position = account.perp_position(perp_market_index)?; health_cache.recompute_perp_info(perp_position, &perp_market)?; account.check_health_post(&health_cache, pre_health)?; } diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 6d51cd168..29c0f81a3 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -439,6 +439,10 @@ pub mod mango_v4 { // TODO perp_change_perp_market_params + pub fn perp_deactivate_position(ctx: Context) -> Result<()> { + instructions::perp_deactivate_position(ctx) + } + #[allow(clippy::too_many_arguments)] pub fn perp_place_order( ctx: Context, diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index b385bd858..2bc40bf1a 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -724,8 +724,9 @@ impl< } } - pub fn deactivate_perp_position(&mut self, raw_index: usize) { - self.perp_position_mut_by_raw_index(raw_index).market_index = PerpMarketIndex::MAX; + pub fn deactivate_perp_position(&mut self, perp_market_index: PerpMarketIndex) -> Result<()> { + self.perp_position_mut(perp_market_index)?.market_index = PerpMarketIndex::MAX; + Ok(()) } pub fn add_perp_order( @@ -734,8 +735,7 @@ impl< side: Side, order: &LeafNode, ) -> Result<()> { - // TODO: pass in the PerpPosition, currently has a creation side-effect - let mut perp_account = self.ensure_perp_position(perp_market_index).unwrap().0; + let mut perp_account = self.perp_position_mut(perp_market_index)?; match side { Side::Bid => { cm!(perp_account.bids_base_lots += order.quantity); @@ -755,13 +755,12 @@ impl< } pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> { - // TODO: pass in the PerpPosition, currently has a creation side-effect { let oo = self.perp_order_mut_by_raw_index(slot); require_neq!(oo.order_market, FREE_ORDER_SLOT); let order_side = oo.order_side; let perp_market_index = oo.order_market; - let perp_account = self.ensure_perp_position(perp_market_index).unwrap().0; + let perp_account = self.perp_position_mut(perp_market_index)?; // accounting match order_side { @@ -789,8 +788,7 @@ impl< perp_market: &mut PerpMarket, fill: &FillEvent, ) -> Result<()> { - // TODO: pass in the PerpPosition, currently has a creation side-effect - let pa = self.ensure_perp_position(perp_market_index).unwrap().0; + let pa = self.perp_position_mut(perp_market_index)?; pa.settle_funding(perp_market); let side = fill.taker_side.invert_side(); @@ -824,8 +822,7 @@ impl< perp_market: &mut PerpMarket, fill: &FillEvent, ) -> Result<()> { - // TODO: pass in the PerpPosition, currently has a creation side-effect - let pa = self.ensure_perp_position(perp_market_index).unwrap().0; + let pa = self.perp_position_mut(perp_market_index)?; pa.settle_funding(perp_market); let (base_change, quote_change) = fill.base_quote_change(fill.taker_side); @@ -1188,7 +1185,7 @@ mod tests { fn test_perp_positions() { let mut account = make_test_account(); assert!(account.perp_position(1).is_err()); - //assert!(account.perp_position_mut(3).is_err()); + assert!(account.perp_position_mut(3).is_err()); assert_eq!( account.perp_position_by_raw_index(0).market_index, PerpMarketIndex::MAX @@ -1222,7 +1219,7 @@ mod tests { } { - account.deactivate_perp_position(1); + assert!(account.deactivate_perp_position(7).is_ok()); let (pos, raw) = account.ensure_perp_position(42).unwrap(); assert_eq!(raw, 2); @@ -1234,13 +1231,13 @@ mod tests { } assert_eq!(account.active_perp_positions().count(), 3); - account.deactivate_perp_position(0); + assert!(account.deactivate_perp_position(1).is_ok()); assert_eq!( account.perp_position_by_raw_index(0).market_index, PerpMarketIndex::MAX ); assert!(account.perp_position(1).is_err()); - //assert!(account.perp_position_mut(1).is_err()); + assert!(account.perp_position_mut(1).is_err()); assert!(account.perp_position(8).is_ok()); assert!(account.perp_position(42).is_ok()); assert_eq!(account.active_perp_positions().count(), 2); diff --git a/programs/mango-v4/src/state/orderbook/book.rs b/programs/mango-v4/src/state/orderbook/book.rs index f64043b31..dd96b4816 100644 --- a/programs/mango-v4/src/state/orderbook/book.rs +++ b/programs/mango-v4/src/state/orderbook/book.rs @@ -263,9 +263,7 @@ impl<'a> Book<'a> { // Record the taker trade in the account already, even though it will only be // realized when the fill event gets executed - let perp_account = mango_account - .ensure_perp_position(market.perp_market_index)? - .0; + let perp_account = mango_account.perp_position_mut(market.perp_market_index)?; perp_account.add_taker_trade(side, match_base_lots, match_quote_lots); let fill = FillEvent::new( @@ -465,9 +463,7 @@ fn apply_fees( let maker_fees = taker_quote_native * market.maker_fee; let taker_fees = taker_quote_native * market.taker_fee; - let perp_account = mango_account - .ensure_perp_position(market.perp_market_index)? - .0; + let perp_account = mango_account.perp_position_mut(market.perp_market_index)?; perp_account.change_quote_position(-taker_fees); market.fees_accrued += taker_fees + maker_fees; diff --git a/programs/mango-v4/src/state/orderbook/mod.rs b/programs/mango-v4/src/state/orderbook/mod.rs index ca6d0d798..54c52a38d 100644 --- a/programs/mango-v4/src/state/orderbook/mod.rs +++ b/programs/mango-v4/src/state/orderbook/mod.rs @@ -103,6 +103,9 @@ mod tests { |book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 { let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); let mut account = MangoAccountValue::from_bytes(&buffer).unwrap(); + account + .ensure_perp_position(perp_market.perp_market_index) + .unwrap(); let quantity = 1; let tif = 100; @@ -199,6 +202,12 @@ mod tests { let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); let mut maker = MangoAccountValue::from_bytes(&buffer).unwrap(); let mut taker = MangoAccountValue::from_bytes(&buffer).unwrap(); + maker + .ensure_perp_position(market.perp_market_index) + .unwrap(); + taker + .ensure_perp_position(market.perp_market_index) + .unwrap(); let maker_pk = Pubkey::new_unique(); let taker_pk = Pubkey::new_unique(); diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 3c7796c19..3b4182ceb 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -2220,6 +2220,39 @@ impl ClientInstruction for PerpCloseMarketInstruction { } } +pub struct PerpDeactivatePositionInstruction { + pub account: Pubkey, + pub perp_market: Pubkey, + pub owner: TestKeypair, +} +#[async_trait::async_trait(?Send)] +impl ClientInstruction for PerpDeactivatePositionInstruction { + type Accounts = mango_v4::accounts::PerpDeactivatePosition; + type Instruction = mango_v4::instruction::PerpDeactivatePosition; + async fn to_instruction( + &self, + account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap(); + + let instruction = Self::Instruction {}; + let accounts = Self::Accounts { + group: perp_market.group, + account: self.account, + perp_market: self.perp_market, + owner: self.owner.pubkey(), + }; + + let instruction = make_instruction(program_id, &accounts, instruction); + (accounts, instruction) + } + + fn signers(&self) -> Vec { + vec![self.owner] + } +} + pub struct PerpPlaceOrderInstruction { pub group: Pubkey, pub account: Pubkey, diff --git a/programs/mango-v4/tests/program_test/utils.rs b/programs/mango-v4/tests/program_test/utils.rs index 24f8d50c0..5f4ccb748 100644 --- a/programs/mango-v4/tests/program_test/utils.rs +++ b/programs/mango-v4/tests/program_test/utils.rs @@ -111,3 +111,19 @@ pub fn assert_mango_error( _ => assert!(false, "Not a mango error"), } } + +pub fn assert_equal_fixed_f64(value: I80F48, expected: f64, max_error: f64) -> bool { + let ok = (value.to_num::() - expected).abs() < max_error; + if !ok { + println!("comparison failed: value: {value}, expected: {expected}"); + } + ok +} + +pub fn assert_equal_f64_f64(value: f64, expected: f64, max_error: f64) -> bool { + let ok = (value - expected).abs() < max_error; + if !ok { + println!("comparison failed: value: {value}, expected: {expected}"); + } + ok +} diff --git a/programs/mango-v4/tests/test_margin_trade.rs b/programs/mango-v4/tests/test_margin_trade.rs index 5e25b78cc..aca8e4fb7 100644 --- a/programs/mango-v4/tests/test_margin_trade.rs +++ b/programs/mango-v4/tests/test_margin_trade.rs @@ -23,7 +23,7 @@ async fn test_margin_trade() -> Result<(), BanksClientError> { let loan_origination_fee = 0.0005; // higher resolution that the loan_origination_fee for one token - let balance_f64eq = |a: f64, b: f64| (a - b).abs() < 0.0001; + let balance_f64eq = |a: f64, b: f64| utils::assert_equal_f64_f64(a, b, 0.0001); // // SETUP: Create a group, account, register a token (mint0) diff --git a/programs/mango-v4/tests/test_perp.rs b/programs/mango-v4/tests/test_perp.rs index f10bd6918..0c5631ada 100644 --- a/programs/mango-v4/tests/test_perp.rs +++ b/programs/mango-v4/tests/test_perp.rs @@ -1,6 +1,7 @@ #![cfg(all(feature = "test-bpf"))] use anchor_lang::prelude::Pubkey; +use fixed::types::I80F48; use fixed_macro::types::I80F48; use mango_v4::state::*; use program_test::*; @@ -8,6 +9,7 @@ use solana_program_test::*; use solana_sdk::transport::TransportError; use mango_setup::*; +use utils::assert_equal_fixed_f64 as assert_equal; mod program_test; @@ -97,8 +99,8 @@ async fn test_perp() -> Result<(), TransportError> { maint_liab_weight: 1.025, init_liab_weight: 1.05, liquidation_fee: 0.012, - maker_fee: 0.0002, - taker_fee: 0.000, + maker_fee: -0.0001, + taker_fee: 0.0002, }, ) .await @@ -342,12 +344,172 @@ async fn test_perp() -> Result<(), TransportError> { let mango_account_0 = solana.get_account::(account_0).await; assert_eq!(mango_account_0.perps[0].base_position_lots(), 1); - assert!(mango_account_0.perps[0].quote_position_native() < -100.019); + assert!(assert_equal( + mango_account_0.perps[0].quote_position_native(), + -99.99, + 0.001 + )); 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); + assert!(assert_equal( + mango_account_1.perps[0].quote_position_native(), + 99.98, + 0.001 + )); + // + // TEST: closing perp positions + // + + // Can't close yet, active positions + assert!(send_tx( + solana, + PerpDeactivatePositionInstruction { + account: account_0, + perp_market, + owner, + }, + ) + .await + .is_err()); + solana.advance_by_slots(1).await; + + // Trade again to bring base_position_lots to 0 + send_tx( + solana, + PerpPlaceOrderInstruction { + group, + account: account_0, + 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: 7, + }, + ) + .await + .unwrap(); + check_prev_instruction_post_health(&solana, account_0).await; + + send_tx( + solana, + PerpPlaceOrderInstruction { + group, + account: account_1, + 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: 8, + }, + ) + .await + .unwrap(); + check_prev_instruction_post_health(&solana, account_1).await; + + 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(), 0); + assert!(assert_equal( + mango_account_0.perps[0].quote_position_native(), + 0.02, + 0.001 + )); + + let mango_account_1 = solana.get_account::(account_1).await; + assert_eq!(mango_account_1.perps[0].base_position_lots(), 0); + assert!(assert_equal( + mango_account_1.perps[0].quote_position_native(), + -0.04, + 0.001 + )); + + // settle pnl and fees to bring quote_position_native fully to 0 + 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(); + 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 mango_account_0 = solana.get_account::(account_0).await; + assert_eq!(mango_account_0.perps[0].quote_position_native(), 0); + + // Now closing works! + send_tx( + solana, + PerpDeactivatePositionInstruction { + account: account_0, + perp_market, + owner, + }, + ) + .await + .unwrap(); + send_tx( + solana, + PerpDeactivatePositionInstruction { + account: account_1, + perp_market, + owner, + }, + ) + .await + .unwrap(); + + let mango_account_0 = solana.get_account::(account_0).await; + assert_eq!(mango_account_0.perps[0].market_index, PerpMarketIndex::MAX); + let mango_account_1 = solana.get_account::(account_1).await; + assert_eq!(mango_account_1.perps[0].market_index, PerpMarketIndex::MAX); + + // + // TEST: market closing (testing only) + // send_tx( solana, PerpCloseMarketInstruction { diff --git a/programs/mango-v4/tests/test_token_update_index_and_rate.rs b/programs/mango-v4/tests/test_token_update_index_and_rate.rs index 7ad9a3a9e..ddd65908d 100644 --- a/programs/mango-v4/tests/test_token_update_index_and_rate.rs +++ b/programs/mango-v4/tests/test_token_update_index_and_rate.rs @@ -6,6 +6,7 @@ use solana_sdk::transport::TransportError; use mango_setup::*; use program_test::*; +use utils::assert_equal_fixed_f64 as assert_equal; mod program_test; @@ -86,28 +87,22 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> { let interest_change = 5000.0 * (dynamic_rate + loan_fee_rate) * diff_ts / year; let fee_change = 5000.0 * loan_fee_rate * diff_ts / year; - assert!( - (bank_after.native_borrows().to_num::() - - bank_before.native_borrows().to_num::() - - interest_change) - .abs() - < 0.1 - ); - assert!( - (bank_after.native_deposits().to_num::() - - bank_before.native_deposits().to_num::() - - interest_change) - .abs() - < 0.1 - ); - assert!( - (bank_after.collected_fees_native.to_num::() - - bank_before.collected_fees_native.to_num::() - - fee_change) - .abs() - < 0.1 - ); - assert!((bank_after.avg_utilization.to_num::() - utilization).abs() < 0.01); + assert!(assert_equal( + bank_after.native_borrows() - bank_before.native_borrows(), + interest_change, + 0.1 + )); + assert!(assert_equal( + bank_after.native_deposits() - bank_before.native_deposits(), + interest_change, + 0.1 + )); + assert!(assert_equal( + bank_after.collected_fees_native - bank_before.collected_fees_native, + fee_change, + 0.1 + )); + assert!(assert_equal(bank_after.avg_utilization, utilization, 0.01)); Ok(()) }