diff --git a/packages/deepbook/sources/helper/constants.move b/packages/deepbook/sources/helper/constants.move index 677844437..58419eca5 100644 --- a/packages/deepbook/sources/helper/constants.move +++ b/packages/deepbook/sources/helper/constants.move @@ -17,7 +17,9 @@ const DEEP_UNIT: u64 = 1_000_000; const FEE_PENALTY_MULTIPLIER: u64 = 1_250_000_000; // 25% more than normal const EWMA_DF_KEY: vector = b"ewma"; const REFERRAL_MAX_MULTIPLIER: u64 = 2_000_000_000; // 2x multiplier +const MAX_REFERRAL_TAKER_FEE_RATE: u64 = 1_000_000; // 0.1% (10 bps) const REFERRAL_MULTIPLIER: u64 = 100_000_000; // 0.1x multiplier +const FEE_PRECISION_MULTIPLE: u64 = 1_000; // 0.01 bps const MAX_BALANCE_MANAGERS: u64 = 100; const DEFAULT_EWMA_ALPHA: u64 = 10_000_000; // 1% smoothing factor. at 3 TPS ~ one minute alpha @@ -264,10 +266,18 @@ public fun referral_max_multiplier(): u64 { REFERRAL_MAX_MULTIPLIER } +public fun max_referral_taker_fee_rate(): u64 { + MAX_REFERRAL_TAKER_FEE_RATE +} + public fun referral_multiplier(): u64 { REFERRAL_MULTIPLIER } +public fun fee_precision_multiple(): u64 { + FEE_PRECISION_MULTIPLE +} + public fun max_balance_managers(): u64 { MAX_BALANCE_MANAGERS } diff --git a/packages/deepbook/sources/pool.move b/packages/deepbook/sources/pool.move index 34cb63cea..bb5ed0969 100644 --- a/packages/deepbook/sources/pool.move +++ b/packages/deepbook/sources/pool.move @@ -66,6 +66,8 @@ const EInvalidEWMAAlpha: u64 = 17; const EInvalidZScoreThreshold: u64 = 18; const EInvalidAdditionalTakerFee: u64 = 19; const EWrongPoolReferral: u64 = 20; +const EInvalidReferralFeeRate: u64 = 21; +const EConflictingReferralFee: u64 = 22; // === Structs === public struct Pool has key { @@ -107,6 +109,14 @@ public struct DeepBurned has copy, drop, deep_burned: u64, } +/// The configuration for the referral taker fee rate in bps. +public struct ReferralTakerFeeConfig has store { + taker_fee_rate: u64, +} + +/// The key for the referral taker fee configuration. +public struct ReferralTakerFeeConfigKey(ID) has copy, drop, store; + public struct ReferralRewards has store { multiplier: u64, base: Balance, @@ -908,7 +918,7 @@ public fun update_deepbook_referral_multiplier( abort } -/// Update the multiplier for the referral. +/// Update the multiplier for the referral. Only allowed when the referral taker fee rate in bps is absent or 0. public fun update_pool_referral_multiplier( self: &mut Pool, referral: &DeepBookPoolReferral, @@ -919,13 +929,51 @@ public fun update_pool_referral_multiplier( referral.assert_referral_owner(ctx); assert!(multiplier <= constants::referral_max_multiplier(), EInvalidReferralMultiplier); assert!(multiplier % constants::referral_multiplier() == 0, EInvalidReferralMultiplier); + let referral_id = object::id(referral); + let bps_config_key = ReferralTakerFeeConfigKey(referral_id); + if (self.id.exists_(bps_config_key)) { + // Referral taker fee rate in bps must be 0 + let config: &ReferralTakerFeeConfig = self.id.borrow(bps_config_key); + assert!(config.taker_fee_rate == 0, EConflictingReferralFee); + }; + let referral_rewards: &mut ReferralRewards = self .id .borrow_mut(referral_id); referral_rewards.multiplier = multiplier; } +/// Update the referral taker fee rate, attaching a `ReferralTakerFeeConfig` dynamic field if one doesn't +/// exist yet. Only allowed when the referral fee multiplier is 0. +public fun update_pool_referral_taker_fee_rate( + self: &mut Pool, + referral: &DeepBookPoolReferral, + taker_fee_rate: u64, + ctx: &TxContext, +) { + let _ = self.load_inner(); + referral.assert_referral_owner(ctx); + assert!(referral.balance_manager_referral_pool_id() == self.id(), EWrongPoolReferral); + assert!(taker_fee_rate <= constants::max_referral_taker_fee_rate(), EInvalidReferralFeeRate); + assert!(taker_fee_rate % constants::fee_precision_multiple() == 0, EInvalidReferralFeeRate); + + let referral_id = object::id(referral); + // Referral fee multiplier must be 0 + let rewards: &ReferralRewards = self.id.borrow(referral_id); + assert!(rewards.multiplier == 0, EConflictingReferralFee); + + let key = ReferralTakerFeeConfigKey(referral_id); + if (self.id.exists_(key)) { + // Update existing config + let config: &mut ReferralTakerFeeConfig = self.id.borrow_mut(key); + config.taker_fee_rate = taker_fee_rate; + } else { + // Attach new config + self.id.add(key, ReferralTakerFeeConfig { taker_fee_rate }); + } +} + #[deprecated(note = b"This function is deprecated, use `claim_pool_referral_rewards` instead.")] public fun claim_referral_rewards( _self: &mut Pool, @@ -1930,6 +1978,7 @@ fun place_order_int( order_info } +// Process referral fee: exactly one of multiplier or bps mode is active at a time fun process_referral_fees( self: &mut Pool, order_info: &OrderInfo, @@ -1939,14 +1988,45 @@ fun process_referral_fees( let referral_id = balance_manager.get_balance_manager_referral_id(self.id()); if (referral_id.is_some()) { let referral_id = referral_id.destroy_some(); + + let bps_config_key = ReferralTakerFeeConfigKey(referral_id); + let bps_fee_rate = if (self.id.exists_(bps_config_key)) { + let config: &ReferralTakerFeeConfig = self.id.borrow(bps_config_key); + config.taker_fee_rate + } else { + 0 + }; + + let referral_fee = if (bps_fee_rate > 0) { + // BPS mode: fee is a fraction of the traded volume + let volume = if (order_info.fee_is_deep()) { + order_info + .order_deep_price() + .deep_quantity_u128( + order_info.executed_quantity() as u128, + order_info.cumulative_quote_quantity() as u128, + ) as u64 + } else if (order_info.is_bid()) { + order_info.cumulative_quote_quantity() + } else { + order_info.executed_quantity() + }; + + math::mul(volume, bps_fee_rate) + } else { + // Multiplier mode: fee is a fraction of the fees paid by the trader + let rewards: &ReferralRewards = self.id.borrow(referral_id); + let multiplier = rewards.multiplier; + if (multiplier == 0) { return }; // Both bps fee rate and multiplier are 0, no referral fee + math::mul(order_info.paid_fees(), multiplier) + }; + + // Covers: zero paid_fees in multiplier mode (e.g. whitelisted pool), or bps fee is too small. + if (referral_fee == 0) { return }; + let referral_rewards: &mut ReferralRewards = self .id .borrow_mut(referral_id); - let referral_multiplier = referral_rewards.multiplier; - let referral_fee = math::mul(order_info.paid_fees(), referral_multiplier); - if (referral_fee == 0) { - return - }; let mut base_fee = 0; let mut quote_fee = 0; let mut deep_fee = 0; diff --git a/packages/deepbook/tests/pool_tests.move b/packages/deepbook/tests/pool_tests.move index 3f0c4646f..4dcb24bd6 100644 --- a/packages/deepbook/tests/pool_tests.move +++ b/packages/deepbook/tests/pool_tests.move @@ -9822,3 +9822,1022 @@ fun test_get_quantity_out_ask_min_size_enforced() { end(test); } + +#[test] +fun pool_referral_fee_rate_ok() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); // 0 multiplier + return_shared(pool); + }; + + // Set 10 bps fee rate (1000 * 1000 = 1_000_000, which is 0.1% of 10^9) + let fee_rate = 1_000_000; + + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + // Set the referral for BOB (the taker) + let balance_manager_id_bob = create_acct_and_share_with_funds_typed( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + test.next_tx(BOB); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_bob); + let referral = test.take_shared_by_id(referral_id); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + balance_manager.set_balance_manager_referral(&referral, &trade_cap); + + transfer::public_transfer(trade_cap, BOB); + return_shared(referral); + return_shared(balance_manager); + }; + + // Place a market order from BOB (taker) + // Buy 100 SUI at market price (approx $2/SUI from setup_everything) + // Volume = 100 SUI * $2 = 200 USDC + // Fee = 200 USDC * 0.1% = 0.2 USDC + let quantity = 100 * constants::float_scaling(); + + place_market_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::self_matching_allowed(), + quantity, + true, // is_bid (buy) + false, // pay_with_deep = false (pay in quote/base) + &mut test, + ); + + // Check referral rewards + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + + // Fee is in Quote (USDC) because it's a Buy order paying in input (Quote) + // Volume = 200 USDC (100 qty * 2 price) + // Fee = 200 * 0.001 = 0.2 USDC = 200_000_000 (scaled) + assert_eq!(quote, 200_000_000); + assert_eq!(base, 0); + assert_eq!(deep, 0); + + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test] +fun pool_referral_fee_rate_base_asset() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); // 0 multiplier + return_shared(pool); + }; + + // Set 10 bps fee rate + let fee_rate = 1_000_000; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + // Set the referral for BOB (the taker) + let balance_manager_id_bob = create_acct_and_share_with_funds_typed( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + test.next_tx(BOB); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_bob); + let referral = test.take_shared_by_id(referral_id); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + balance_manager.set_balance_manager_referral(&referral, &trade_cap); + transfer::public_transfer(trade_cap, BOB); + return_shared(referral); + return_shared(balance_manager); + }; + + // Place a market Sell order from BOB (taker) + // Sell 100 SUI at market price (approx $1/SUI best bid from setup_everything) + // Volume = 100 SUI + // Fee = 100 SUI * 0.1% = 0.1 SUI + let quantity = 100 * constants::float_scaling(); + + place_market_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::self_matching_allowed(), + quantity, + false, // is_bid = false (Sell) + false, // pay_with_deep = false + &mut test, + ); + + // Check referral rewards + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + + // Fee is in Base (SUI) because it's a Sell order paying in input (Base) + // Volume = 100 SUI + // Fee = 100 * 0.001 = 0.1 SUI = 100_000_000 (scaled) + assert_eq!(base, 100_000_000); + assert_eq!(quote, 0); + assert_eq!(deep, 0); + + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test] +fun pool_referral_fee_rate_deep() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); // 0 multiplier + return_shared(pool); + }; + + // Set 10 bps fee rate + let fee_rate = 1_000_000; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + // Set the referral for BOB (the taker) + let balance_manager_id_bob = create_acct_and_share_with_funds_typed( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + test.next_tx(BOB); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_bob); + let referral = test.take_shared_by_id(referral_id); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + balance_manager.set_balance_manager_referral(&referral, &trade_cap); + transfer::public_transfer(trade_cap, BOB); + return_shared(referral); + return_shared(balance_manager); + }; + + // Place a market Buy order from BOB (taker), paying in DEEP + // Buy 100 SUI at market price ($2) + // Volume = 200 USDC + // Fee = 200 USDC * 0.1% = 0.2 USDC value + // Converted to DEEP + let quantity = 100 * constants::float_scaling(); + + place_market_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::self_matching_allowed(), + quantity, + true, // is_bid (buy) + true, // pay_with_deep = true + &mut test, + ); + + // Check referral rewards + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + + // Fee should be in DEEP. + assert_eq!(base, 0); + assert_eq!(quote, 0); + assert!(deep > 0, 0); + + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test] +fun whitelisted_pool_referral_fee() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + let balance_manager_id_alice = create_acct_and_share_with_funds_typed( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Create whitelisted pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, // whitelisted_pool = true + false, // stable_pool = false + &mut test, + ); + + // Alice places limit orders to provide liquidity + let client_order_id = 1; + let order_type = constants::no_restriction(); + let price = 2 * constants::float_scaling(); // Sell SUI at 2 USDC + let quantity = 1000 * constants::float_scaling(); + let expire_timestamp = constants::max_u64(); + + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + client_order_id, + order_type, + constants::self_matching_allowed(), + price, + quantity, + false, // is_bid = false (Ask/Sell) + true, // pay_with_deep (irrelevant for whitelisted but required arg) + expire_timestamp, + &mut test, + ); + + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); // 0 multiplier + return_shared(pool); + }; + + // Set 10 bps fee rate + let fee_rate = 1_000_000; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + // Setup Bob + let balance_manager_id_bob = create_acct_and_share_with_funds_typed( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + test.next_tx(BOB); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_bob); + let referral = test.take_shared_by_id(referral_id); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + balance_manager.set_balance_manager_referral(&referral, &trade_cap); + transfer::public_transfer(trade_cap, BOB); + return_shared(referral); + return_shared(balance_manager); + }; + + // Bob places market order (Buy SUI with USDC) + // 100 SUI * 2 USDC = 200 USDC volume + let quantity = 100 * constants::float_scaling(); + + place_market_order( + BOB, + pool_id, + balance_manager_id_bob, + 2, + constants::self_matching_allowed(), + quantity, + true, // is_bid (buy) + false, // pay_with_deep = false + &mut test, + ); + + // Check referral rewards + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + + // Fee is in Quote (USDC) + // Volume = 200 USDC + // Referral Fee = 200 * 0.001 = 0.2 USDC = 200_000_000 + assert_eq!(quote, 200_000_000); + assert_eq!(base, 0); + assert_eq!(deep, 0); + + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test] +fun pool_referral_fee_zero() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); + return_shared(pool); + }; + + // Set 0 bps fee rate + let fee_rate = 0; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + // Set the referral for BOB + let balance_manager_id_bob = create_acct_and_share_with_funds_typed( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + test.next_tx(BOB); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_bob); + let referral = test.take_shared_by_id(referral_id); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + balance_manager.set_balance_manager_referral(&referral, &trade_cap); + transfer::public_transfer(trade_cap, BOB); + return_shared(referral); + return_shared(balance_manager); + }; + + // Place a market order + let quantity = 100 * constants::float_scaling(); + place_market_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::self_matching_allowed(), + quantity, + true, + false, + &mut test, + ); + + // Check referral rewards - should be 0 + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + + assert_eq!(quote, 0); + assert_eq!(base, 0); + assert_eq!(deep, 0); + + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test] +fun pool_referral_fee_update() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); + return_shared(pool); + }; + + // Set 5 bps fee rate + let fee_rate_1 = 500_000; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate_1, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + // Update to 10 bps fee rate (max) + let fee_rate_2 = 1_000_000; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate_2, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + // Set referral for BOB and trade + let balance_manager_id_bob = create_acct_and_share_with_funds_typed( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + test.next_tx(BOB); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_bob); + let referral = test.take_shared_by_id(referral_id); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + balance_manager.set_balance_manager_referral(&referral, &trade_cap); + transfer::public_transfer(trade_cap, BOB); + return_shared(referral); + return_shared(balance_manager); + }; + + // Trade + let quantity = 100 * constants::float_scaling(); + place_market_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::self_matching_allowed(), + quantity, + true, + false, + &mut test, + ); + + // Check referral rewards - should reflect 10 bps (max) + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (_, quote, _) = pool.get_pool_referral_balances(&referral); + + // Volume = 200 USDC + // Fee = 200 * 0.001 = 0.2 USDC = 200_000_000 + assert_eq!(quote, 200_000_000); + + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test] +fun pool_referral_fee_maker_no_fill() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); + return_shared(pool); + }; + + // Set 10 bps fee rate + let fee_rate = 1_000_000; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + // Set referral for BOB + let balance_manager_id_bob = create_acct_and_share_with_funds_typed( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + test.next_tx(BOB); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_bob); + let referral = test.take_shared_by_id(referral_id); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + balance_manager.set_balance_manager_referral(&referral, &trade_cap); + transfer::public_transfer(trade_cap, BOB); + return_shared(referral); + return_shared(balance_manager); + }; + + // Place a LIMIT order that rests on the book (Maker) + // Current best ask is $2. Place bid at $1.5. + let price = 1500000000; // 1.5 + let quantity = 100 * constants::float_scaling(); + + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + price, + quantity, + true, // is_bid (buy) + false, // pay_with_deep = false + constants::max_u64(), + &mut test, + ); + + // Check referral rewards - should be 0 as it's a maker order (no fill) + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + + assert_eq!(quote, 0); + assert_eq!(base, 0); + assert_eq!(deep, 0); + + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test] +fun pool_referral_fee_maker_partial_fill() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); + return_shared(pool); + }; + + // Set 10 bps fee rate + let fee_rate = 1_000_000; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + // Set referral for BOB + let balance_manager_id_bob = create_acct_and_share_with_funds_typed( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + test.next_tx(BOB); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_bob); + let referral = test.take_shared_by_id(referral_id); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + balance_manager.set_balance_manager_referral(&referral, &trade_cap); + transfer::public_transfer(trade_cap, BOB); + return_shared(referral); + return_shared(balance_manager); + }; + + // Place a LIMIT order that crosses the spread (Taker) but only partially fills + // Setup: Best Ask is $2 (1000 qty). + // Bob places Buy Limit at $2 for 1500 qty. + // Matches 1000 at $2 (Taker). + // Remaining 500 sits at $2 (Maker). + + let quantity = 1500 * constants::float_scaling(); + let price = 2 * constants::float_scaling(); + + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + price, + quantity, + true, // is_bid (buy) + false, // pay_with_deep = false + constants::max_u64(), + &mut test, + ); + + // Check referral rewards + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + + // Fee is in Quote (USDC) + // Volume = 2000 USDC (1000 filled * 2) + // Fee = 2000 * 0.001 = 2 USDC = 2_000_000_000 + assert_eq!(quote, 2_000_000_000); + assert_eq!(base, 0); + assert_eq!(deep, 0); + + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test, expected_failure(abort_code = ::deepbook::pool::EInvalidReferralFeeRate)] +fun pool_referral_fee_max_exceeded() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); + return_shared(pool); + }; + + // Set 11 bps fee rate (Max is 10 bps = 1_000_000) + let fee_rate = 1_100_000; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + end(test); +} + +#[test, expected_failure(abort_code = ::deepbook::pool::EInvalidReferralFeeRate)] +fun pool_referral_fee_invalid_precision() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); + return_shared(pool); + }; + + // Set fee rate not multiple of 1000 (0.01 bps) + // 1_000_001 + let fee_rate = 100_001; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + end(test); +} + +#[test, expected_failure(abort_code = ::deepbook::balance_manager::EInvalidReferralOwner)] +fun pool_referral_fee_unauthorized_update() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); + return_shared(pool); + }; + + // Try to update fee rate by BOB (not owner) + let fee_rate = 1_000_000; + test.next_tx(BOB); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + end(test); +} + +#[test, expected_failure(abort_code = ::deepbook::pool::EWrongPoolReferral)] +fun pool_referral_fee_wrong_pool() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create two pools with different type pairs + let pool_a_id = setup_pool_with_default_fees( + OWNER, + registry_id, + false, + false, + &mut test, + ); + let pool_b_id = setup_pool_with_default_fees( + OWNER, + registry_id, + false, + false, + &mut test, + ); + + // Mint referral from pool A — it is bound to pool A's ID + let referral_id; + test.next_tx(ALICE); + { + let mut pool_a = test.take_shared_by_id>(pool_a_id); + referral_id = pool_a.mint_referral(0, test.ctx()); + return_shared(pool_a); + }; + + // Try to use pool A's referral with pool B → should abort + test.next_tx(ALICE); + { + let mut pool_b = test.take_shared_by_id>(pool_b_id); + let referral = test.take_shared_by_id(referral_id); + pool_b.update_pool_referral_taker_fee_rate(&referral, 1_000_000, test.ctx()); + return_shared(referral); + return_shared(pool_b); + }; + end(test); +} + +// === Mutual exclusivity tests === + +#[test, expected_failure(abort_code = ::deepbook::pool::EConflictingReferralFee)] +fun pool_referral_conflict_bps_blocked_by_multiplier() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(500_000_000, test.ctx()); // 0.5x multiplier active + return_shared(pool); + }; + + // Try to set bps fee rate while multiplier is active → should abort + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, 1_000_000, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + end(test); +} + +#[test, expected_failure(abort_code = ::deepbook::pool::EConflictingReferralFee)] +fun pool_referral_conflict_multiplier_blocked_by_bps() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); + return_shared(pool); + }; + + // Set bps fee rate + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, 1_000_000, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + // Try to set multiplier while bps fee rate is active → should abort + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_multiplier(&referral, 500_000_000, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + end(test); +} + +#[test] +fun pool_referral_switch_multiplier_to_bps() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(500_000_000, test.ctx()); // start in multiplier mode + return_shared(pool); + }; + + // Zero out multiplier, then activate bps mode + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_multiplier(&referral, 0, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, 1_000_000, test.ctx()); // 10 bps + return_shared(referral); + return_shared(pool); + }; + + // Set referral for BOB and trade + let balance_manager_id_bob = create_acct_and_share_with_funds_typed( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + test.next_tx(BOB); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_bob); + let referral = test.take_shared_by_id(referral_id); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + balance_manager.set_balance_manager_referral(&referral, &trade_cap); + transfer::public_transfer(trade_cap, BOB); + return_shared(referral); + return_shared(balance_manager); + }; + + // Buy 100 SUI at market price ($2), volume = 200 USDC + let quantity = 100 * constants::float_scaling(); + place_market_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::self_matching_allowed(), + quantity, + true, // is_bid (buy) + false, + &mut test, + ); + + // Verify bps fee was collected: 200 USDC * 0.001 = 0.2 USDC = 200_000_000 + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + + assert_eq!(quote, 200_000_000); + assert_eq!(base, 0); + assert_eq!(deep, 0); + + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test] +fun pool_referral_switch_bps_to_multiplier() { + // Also verifies that process_referral_fees correctly uses multiplier mode when + // the bps config exists but fee_rate is 0 (the "config exists but zeroed" edge case). + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(0, test.ctx()); + return_shared(pool); + }; + + // Set bps fee rate, then zero it out — config now exists with fee_rate=0 + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, 1_000_000, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_taker_fee_rate(&referral, 0, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + // Activate multiplier mode + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_multiplier(&referral, 500_000_000, test.ctx()); // 0.5x + return_shared(referral); + return_shared(pool); + }; + + // Set referral for BOB and trade + let balance_manager_id_bob = create_acct_and_share_with_funds_typed( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + test.next_tx(BOB); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_bob); + let referral = test.take_shared_by_id(referral_id); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + balance_manager.set_balance_manager_referral(&referral, &trade_cap); + transfer::public_transfer(trade_cap, BOB); + return_shared(referral); + return_shared(balance_manager); + }; + + // Buy 100 SUI at market price ($2) + let quantity = 100 * constants::float_scaling(); + place_market_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::self_matching_allowed(), + quantity, + true, // is_bid (buy) + false, + &mut test, + ); + + // Verify multiplier fee was collected despite bps config existing with fee_rate=0 + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + + assert!(quote > 0, 0); + assert_eq!(base, 0); + assert_eq!(deep, 0); + + return_shared(referral); + return_shared(pool); + }; + + end(test); +}