diff --git a/programs/mango-v4/src/instructions/perp_create_market.rs b/programs/mango-v4/src/instructions/perp_create_market.rs index e6a4410ba..7b28dfd77 100644 --- a/programs/mango-v4/src/instructions/perp_create_market.rs +++ b/programs/mango-v4/src/instructions/perp_create_market.rs @@ -64,6 +64,7 @@ pub fn perp_create_market( impact_quantity: i64, group_insurance_fund: bool, trusted_market: bool, + fee_penalty: f32, ) -> Result<()> { let mut perp_market = ctx.accounts.perp_market.load_init()?; *perp_market = PerpMarket { @@ -103,7 +104,8 @@ pub fn perp_create_market( padding0: Default::default(), padding1: Default::default(), padding2: Default::default(), - reserved: [0; 112], + fee_penalty, + reserved: [0; 108], }; let mut bids = ctx.accounts.bids.load_init()?; diff --git a/programs/mango-v4/src/instructions/perp_edit_market.rs b/programs/mango-v4/src/instructions/perp_edit_market.rs index 026ac6f9f..3508f603d 100644 --- a/programs/mango-v4/src/instructions/perp_edit_market.rs +++ b/programs/mango-v4/src/instructions/perp_edit_market.rs @@ -35,6 +35,7 @@ pub fn perp_edit_market( impact_quantity_opt: Option, group_insurance_fund_opt: Option, trusted_market_opt: Option, + fee_penalty_opt: Option, ) -> Result<()> { let mut perp_market = ctx.accounts.perp_market.load_mut()?; @@ -91,6 +92,9 @@ pub fn perp_edit_market( if let Some(impact_quantity) = impact_quantity_opt { perp_market.impact_quantity = impact_quantity; } + if let Some(fee_penalty) = fee_penalty_opt { + perp_market.fee_penalty = fee_penalty; + } // unchanged - // long_funding diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 7b58ba6eb..d694a0b1e 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -393,6 +393,7 @@ pub mod mango_v4 { impact_quantity: i64, group_insurance_fund: bool, trusted_market: bool, + fee_penalty: f32, ) -> Result<()> { instructions::perp_create_market( ctx, @@ -414,6 +415,7 @@ pub mod mango_v4 { impact_quantity, group_insurance_fund, trusted_market, + fee_penalty, ) } @@ -435,6 +437,7 @@ pub mod mango_v4 { impact_quantity_opt: Option, group_insurance_fund_opt: Option, trusted_market_opt: Option, + fee_penalty_opt: Option, ) -> Result<()> { instructions::perp_edit_market( ctx, @@ -453,6 +456,7 @@ pub mod mango_v4 { impact_quantity_opt, group_insurance_fund_opt, trusted_market_opt, + fee_penalty_opt, ) } diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index d7cb06c68..32c4924d1 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -400,8 +400,7 @@ pub use account_seeds; #[cfg(test)] mod tests { - use crate::state::{OracleConfig, PerpMarket}; - use anchor_lang::prelude::Pubkey; + use crate::state::PerpMarket; use fixed::types::I80F48; use rand::Rng; @@ -416,52 +415,9 @@ mod tests { pos } - fn create_perp_market() -> PerpMarket { - return PerpMarket { - group: Pubkey::new_unique(), - perp_market_index: 0, - group_insurance_fund: 0, - trusted_market: 0, - name: Default::default(), - oracle: Pubkey::new_unique(), - oracle_config: OracleConfig { - conf_filter: I80F48::ZERO, - }, - bids: Pubkey::new_unique(), - asks: Pubkey::new_unique(), - event_queue: Pubkey::new_unique(), - quote_lot_size: 1, - base_lot_size: 1, - maint_asset_weight: I80F48::from(1), - init_asset_weight: I80F48::from(1), - maint_liab_weight: I80F48::from(1), - init_liab_weight: I80F48::from(1), - liquidation_fee: I80F48::ZERO, - maker_fee: I80F48::ZERO, - taker_fee: I80F48::ZERO, - min_funding: I80F48::ZERO, - max_funding: I80F48::ZERO, - impact_quantity: 0, - long_funding: I80F48::ZERO, - short_funding: I80F48::ZERO, - funding_last_updated: 0, - open_interest: 0, - seq_num: 0, - fees_accrued: I80F48::ZERO, - fees_settled: I80F48::ZERO, - bump: 0, - base_decimals: 0, - reserved: [0; 112], - padding0: Default::default(), - padding1: Default::default(), - padding2: Default::default(), - registration_time: 0, - }; - } - #[test] fn test_quote_entry_long_increasing_from_zero() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(0, 0, 0); // Go long 10 @ 10 pos.change_base_and_quote_positions(&mut market, 10, I80F48::from(-100)); @@ -472,7 +428,7 @@ mod tests { #[test] fn test_quote_entry_short_increasing_from_zero() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(0, 0, 0); // Go short 10 @ 10 pos.change_base_and_quote_positions(&mut market, -10, I80F48::from(100)); @@ -483,7 +439,7 @@ mod tests { #[test] fn test_quote_entry_long_increasing_from_long() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(10, -100, -100); // Go long 10 @ 30 pos.change_base_and_quote_positions(&mut market, 10, I80F48::from(-300)); @@ -494,7 +450,7 @@ mod tests { #[test] fn test_quote_entry_short_increasing_from_short() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(-10, 100, 100); // Go short 10 @ 10 pos.change_base_and_quote_positions(&mut market, -10, I80F48::from(300)); @@ -505,7 +461,7 @@ mod tests { #[test] fn test_quote_entry_long_decreasing_from_short() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(-10, 100, 100); // Go long 5 @ 50 pos.change_base_and_quote_positions(&mut market, 5, I80F48::from(-250)); @@ -516,7 +472,7 @@ mod tests { #[test] fn test_quote_entry_short_decreasing_from_long() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(10, -100, -100); // Go short 5 @ 50 pos.change_base_and_quote_positions(&mut market, -5, I80F48::from(250)); @@ -527,7 +483,7 @@ mod tests { #[test] fn test_quote_entry_long_close_with_short() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(10, -100, -100); // Go short 10 @ 50 pos.change_base_and_quote_positions(&mut market, -10, I80F48::from(250)); @@ -538,7 +494,7 @@ mod tests { #[test] fn test_quote_entry_short_close_with_long() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(-10, 100, 100); // Go long 10 @ 50 pos.change_base_and_quote_positions(&mut market, 10, I80F48::from(-250)); @@ -549,7 +505,7 @@ mod tests { #[test] fn test_quote_entry_long_close_short_with_overflow() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(10, -100, -100); // Go short 15 @ 20 pos.change_base_and_quote_positions(&mut market, -15, I80F48::from(300)); @@ -560,7 +516,7 @@ mod tests { #[test] fn test_quote_entry_short_close_long_with_overflow() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(-10, 100, 100); // Go short 15 @ 20 pos.change_base_and_quote_positions(&mut market, 15, I80F48::from(-300)); @@ -571,7 +527,7 @@ mod tests { #[test] fn test_quote_entry_break_even_price() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(0, 0, 0); // Buy 11 @ 10,000 pos.change_base_and_quote_positions(&mut market, 11, I80F48::from(-11 * 10_000)); @@ -585,7 +541,7 @@ mod tests { #[test] fn test_quote_entry_multiple_and_reversed_changes_return_entry_to_zero() { - let mut market = create_perp_market(); + let mut market = PerpMarket::default_for_tests(); let mut pos = create_perp_position(0, 0, 0); // Generate array of random trades diff --git a/programs/mango-v4/src/state/orderbook/book.rs b/programs/mango-v4/src/state/orderbook/book.rs index dd96b4816..9f05f997b 100644 --- a/programs/mango-v4/src/state/orderbook/book.rs +++ b/programs/mango-v4/src/state/orderbook/book.rs @@ -377,6 +377,11 @@ impl<'a> Book<'a> { apply_fees(market, mango_account, total_quote_lots_taken)?; } + // IOC orders have a fee penalty applied regardless of match + if order_type == OrderType::ImmediateOrCancel { + apply_penalty(market, mango_account)?; + } + Ok(()) } @@ -460,12 +465,24 @@ fn apply_fees( // risks that fees_accrued is settled to 0 before they apply. It going negative // breaks assumptions. // The maker fees apply to the maker's account only when the fill event is consumed. - let maker_fees = taker_quote_native * market.maker_fee; + let maker_fees = cm!(taker_quote_native * market.maker_fee); + + let taker_fees = cm!(taker_quote_native * market.taker_fee); - let taker_fees = taker_quote_native * market.taker_fee; 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; + cm!(market.fees_accrued += taker_fees + maker_fees); + + Ok(()) +} + +/// Applies a fixed penalty fee to the account, and update the market's fees_accrued +fn apply_penalty(market: &mut PerpMarket, mango_account: &mut MangoAccountRefMut) -> Result<()> { + let perp_account = mango_account.perp_position_mut(market.perp_market_index)?; + let fee_penalty = I80F48::from_num(market.fee_penalty); + + perp_account.change_quote_position(-fee_penalty); + cm!(market.fees_accrued += fee_penalty); Ok(()) } diff --git a/programs/mango-v4/src/state/orderbook/mod.rs b/programs/mango-v4/src/state/orderbook/mod.rs index 4fc8ab3fe..9f4161df4 100644 --- a/programs/mango-v4/src/state/orderbook/mod.rs +++ b/programs/mango-v4/src/state/orderbook/mod.rs @@ -375,4 +375,111 @@ mod tests { match_quote - match_quote * market.taker_fee ); } + + #[test] + fn test_fee_penalty_applied_only_on_limit_order() -> Result<()> { + let (mut market, oracle_price, mut event_queue, bids, asks) = test_setup(1000.0); + let mut book = Book { + bids: bids.borrow_mut(), + asks: asks.borrow_mut(), + }; + + let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); + let mut account = MangoAccountValue::from_bytes(&buffer).unwrap(); + let taker_pk = Pubkey::new_unique(); + let now_ts = 1000000; + + market.base_lot_size = 1; + market.quote_lot_size = 1; + market.taker_fee = I80F48::from_num(0.01); + market.fee_penalty = 5.0; + account.ensure_perp_position(market.perp_market_index, 0)?; + + // Passive order + book.new_order( + Side::Ask, + &mut market, + &mut event_queue, + oracle_price, + &mut account.borrow_mut(), + &taker_pk, + 1000, + 2, + i64::MAX, + OrderType::Limit, + 0, + 43, + now_ts, + u8::MAX, + ) + .unwrap(); + + // Partial taker + book.new_order( + Side::Bid, + &mut market, + &mut event_queue, + oracle_price, + &mut account.borrow_mut(), + &taker_pk, + 1000, + 1, + i64::MAX, + OrderType::Limit, + 0, + 43, + now_ts, + u8::MAX, + ) + .unwrap(); + + let pos = account.perp_position(market.perp_market_index)?; + + assert_eq!( + pos.quote_position_native().round(), + I80F48::from_num(-10), + "Regular fees applied on limit order" + ); + + assert_eq!( + market.fees_accrued.round(), + I80F48::from_num(10), + "Fees moved to market" + ); + + // Full taker + book.new_order( + Side::Bid, + &mut market, + &mut event_queue, + oracle_price, + &mut account.borrow_mut(), + &taker_pk, + 1000, + 1, + i64::MAX, + OrderType::ImmediateOrCancel, + 0, + 43, + now_ts, + u8::MAX, + ) + .unwrap(); + + let pos = account.perp_position(market.perp_market_index)?; + + assert_eq!( + pos.quote_position_native().round(), + I80F48::from_num(-25), // -10 - 5 + "Regular fees + fixed penalty applied on IOC order" + ); + + assert_eq!( + market.fees_accrued.round(), + I80F48::from_num(25), // 10 + 5 + "Fees moved to market" + ); + + Ok(()) + } } diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 49a8d2e31..91a621181 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -99,12 +99,30 @@ pub struct PerpMarket { /// Fees settled in native quote currency pub fees_settled: I80F48, - pub reserved: [u8; 112], + pub fee_penalty: f32, + + pub reserved: [u8; 108], } const_assert_eq!( size_of::(), - 32 + 2 + 2 + 4 + 16 + 32 + 16 + 32 * 3 + 8 * 2 + 16 * 12 + 8 * 2 + 8 * 2 + 16 + 2 + 6 + 8 + 112 + 32 + 2 + + 2 + + 4 + + 16 + + 32 + + 16 + + 32 * 3 + + 8 * 2 + + 16 * 12 + + 8 * 2 + + 8 * 2 + + 16 + + 2 + + 6 + + 8 + + 4 + + 108 ); const_assert_eq!(size_of::() % 8, 0); @@ -225,4 +243,49 @@ impl PerpMarket { self.short_funding += socialized_loss; Ok(socialized_loss) } + + /// Creates default market for tests + pub fn default_for_tests() -> PerpMarket { + PerpMarket { + group: Pubkey::new_unique(), + perp_market_index: 0, + name: Default::default(), + oracle: Pubkey::new_unique(), + oracle_config: OracleConfig { + conf_filter: I80F48::ZERO, + }, + bids: Pubkey::new_unique(), + asks: Pubkey::new_unique(), + event_queue: Pubkey::new_unique(), + quote_lot_size: 1, + base_lot_size: 1, + maint_asset_weight: I80F48::from(1), + init_asset_weight: I80F48::from(1), + maint_liab_weight: I80F48::from(1), + init_liab_weight: I80F48::from(1), + liquidation_fee: I80F48::ZERO, + maker_fee: I80F48::ZERO, + taker_fee: I80F48::ZERO, + min_funding: I80F48::ZERO, + max_funding: I80F48::ZERO, + impact_quantity: 0, + long_funding: I80F48::ZERO, + short_funding: I80F48::ZERO, + funding_last_updated: 0, + open_interest: 0, + seq_num: 0, + fees_accrued: I80F48::ZERO, + fees_settled: I80F48::ZERO, + bump: 0, + base_decimals: 0, + reserved: [0; 108], + padding0: Default::default(), + padding1: Default::default(), + padding2: Default::default(), + registration_time: 0, + fee_penalty: 0.0, + trusted_market: 0, + group_insurance_fund: 0, + } + } } diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 49c634e2a..02c148597 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -2143,6 +2143,7 @@ pub struct PerpCreateMarketInstruction { pub taker_fee: f32, pub group_insurance_fund: bool, pub trusted_market: bool, + pub fee_penalty: f32, } impl PerpCreateMarketInstruction { pub async fn with_new_book_and_queue( @@ -2195,6 +2196,7 @@ impl ClientInstruction for PerpCreateMarketInstruction { base_decimals: self.base_decimals, group_insurance_fund: self.group_insurance_fund, trusted_market: self.trusted_market, + fee_penalty: self.fee_penalty, }; let perp_market = Pubkey::find_program_address( diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 316220daa..b091cc613 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -1330,6 +1330,7 @@ export class MangoClient { liquidationFee: number, makerFee: number, takerFee: number, + feePenalty: number, minFunding: number, maxFunding: number, impactQuantity: number, @@ -1364,6 +1365,7 @@ export class MangoClient { new BN(impactQuantity), groupInsuranceFund, trustedMarket, + feePenalty ) .accounts({ group: group.publicKey, @@ -1430,6 +1432,7 @@ export class MangoClient { liquidationFee: number, makerFee: number, takerFee: number, + feePenalty: number, minFunding: number, maxFunding: number, impactQuantity: number, @@ -1459,6 +1462,7 @@ export class MangoClient { new BN(impactQuantity), groupInsuranceFund, trustedMarket, + feePenalty ) .accounts({ group: group.publicKey, diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index f4f4bb827..e5d24c10a 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -2315,6 +2315,10 @@ export type MangoV4 = { { "name": "trustedMarket", "type": "bool" + }, + { + "name": "feePenalty", + "type": "f32" } ] }, @@ -2429,6 +2433,12 @@ export type MangoV4 = { "type": { "option": "bool" } + }, + { + "name": "feePenaltyOpt", + "type": { + "option": "f32" + } } ] }, @@ -4014,12 +4024,16 @@ export type MangoV4 = { "defined": "I80F48" } }, + { + "name": "feePenalty", + "type": "f32" + }, { "name": "reserved", "type": { "array": [ "u8", - 112 + 108 ] } } @@ -8402,6 +8416,10 @@ export const IDL: MangoV4 = { { "name": "trustedMarket", "type": "bool" + }, + { + "name": "feePenalty", + "type": "f32" } ] }, @@ -8516,6 +8534,12 @@ export const IDL: MangoV4 = { "type": { "option": "bool" } + }, + { + "name": "feePenaltyOpt", + "type": { + "option": "f32" + } } ] }, @@ -10101,12 +10125,16 @@ export const IDL: MangoV4 = { "defined": "I80F48" } }, + { + "name": "feePenalty", + "type": "f32" + }, { "name": "reserved", "type": { "array": [ "u8", - 112 + 108 ] } }