From 5833ec7c7aa417237656bbc6384aa51247c9d295 Mon Sep 17 00:00:00 2001 From: bathord Date: Sat, 14 Mar 2026 18:00:09 +0300 Subject: [PATCH 01/11] Add MAX_REFERRAL_FEE_RATE & FEE_PRECISION_MULTIPLE constants --- packages/deepbook/sources/helper/constants.move | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/deepbook/sources/helper/constants.move b/packages/deepbook/sources/helper/constants.move index 677844437..bd4072b36 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_FEE_RATE: u64 = 2_000_000; // 0.2% (20 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_fee_rate(): u64 { + MAX_REFERRAL_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 } From 27e18a78348906a754620d79fe98d9f5a96f0c87 Mon Sep 17 00:00:00 2001 From: bathord Date: Sat, 14 Mar 2026 18:01:00 +0300 Subject: [PATCH 02/11] Add ReferralFeeConfig allowing to set independent referral fee rate --- packages/deepbook/sources/pool.move | 77 ++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/packages/deepbook/sources/pool.move b/packages/deepbook/sources/pool.move index 34cb63cea..c4e7a655c 100644 --- a/packages/deepbook/sources/pool.move +++ b/packages/deepbook/sources/pool.move @@ -66,6 +66,7 @@ const EInvalidEWMAAlpha: u64 = 17; const EInvalidZScoreThreshold: u64 = 18; const EInvalidAdditionalTakerFee: u64 = 19; const EWrongPoolReferral: u64 = 20; +const EInvalidReferralFeeRate: u64 = 21; // === Structs === public struct Pool has key { @@ -107,6 +108,14 @@ public struct DeepBurned has copy, drop, deep_burned: u64, } +/// The configuration for the referral fee rate. +public struct ReferralFeeConfig has store { + fee_rate: u64, +} + +/// The key for the referral fee configuration. +public struct ReferralFeeConfigKey(ID) has copy, drop, store; + public struct ReferralRewards has store { multiplier: u64, base: Balance, @@ -926,6 +935,29 @@ public fun update_pool_referral_multiplier( referral_rewards.multiplier = multiplier; } +/// Update the fee rate for the referral. +public fun update_pool_referral_fee_rate( + self: &mut Pool, + referral: &DeepBookPoolReferral, + 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!(fee_rate <= constants::max_referral_fee_rate(), EInvalidReferralFeeRate); + assert!(fee_rate % constants::fee_precision_multiple() == 0, EInvalidReferralFeeRate); + + let referral_id = object::id(referral); + let key = ReferralFeeConfigKey(referral_id); + if (self.id.exists_(key)) { + let config: &mut ReferralFeeConfig = self.id.borrow_mut(key); + config.fee_rate = fee_rate; + } else { + self.id.add(key, ReferralFeeConfig { fee_rate }); + } +} + #[deprecated(note = b"This function is deprecated, use `claim_pool_referral_rewards` instead.")] public fun claim_referral_rewards( _self: &mut Pool, @@ -1939,32 +1971,61 @@ 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 mut volume_fee = 0; + if (self.id.exists_(ReferralFeeConfigKey(referral_id))) { + let config: &ReferralFeeConfig = self.id.borrow(ReferralFeeConfigKey(referral_id)); + let volume = if (order_info.fee_is_deep()) { + let deep_quantity = order_info + .order_deep_price() + .deep_quantity_u128( + order_info.executed_quantity() as u128, + order_info.cumulative_quote_quantity() as u128, + ); + + // Cap the volume at MAX_U64 to prevent the transaction from aborting due to overflow + // during the cast. MAX_U64 DEEP is far greater than the total supply. + if (deep_quantity > (constants::max_u64() as u128)) { + constants::max_u64() + } else { + deep_quantity as u64 + } + } else if (order_info.is_bid()) { + order_info.cumulative_quote_quantity() + } else { + order_info.executed_quantity() + }; + volume_fee = math::mul(volume, config.fee_rate); + }; + 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) { + let multiplier_fee = math::mul(order_info.paid_fees(), referral_multiplier); + + let total_referral_fee = multiplier_fee + volume_fee; + if (total_referral_fee == 0) { return }; + let mut base_fee = 0; let mut quote_fee = 0; let mut deep_fee = 0; if (order_info.fee_is_deep()) { referral_rewards .deep - .join(balance_manager.withdraw_with_proof(trade_proof, referral_fee, false)); - deep_fee = referral_fee; + .join(balance_manager.withdraw_with_proof(trade_proof, total_referral_fee, false)); + deep_fee = total_referral_fee; } else if (!order_info.is_bid()) { referral_rewards .base - .join(balance_manager.withdraw_with_proof(trade_proof, referral_fee, false)); - base_fee = referral_fee; + .join(balance_manager.withdraw_with_proof(trade_proof, total_referral_fee, false)); + base_fee = total_referral_fee; } else { referral_rewards .quote - .join(balance_manager.withdraw_with_proof(trade_proof, referral_fee, false)); - quote_fee = referral_fee; + .join(balance_manager.withdraw_with_proof(trade_proof, total_referral_fee, false)); + quote_fee = total_referral_fee; }; event::emit(ReferralFeeEvent { From ec47e249db34cbc07f3f3b79ca13a2c149100c42 Mon Sep 17 00:00:00 2001 From: bathord Date: Sat, 14 Mar 2026 18:03:04 +0300 Subject: [PATCH 03/11] Add tests for referral fee config --- packages/deepbook/tests/pool_tests.move | 983 ++++++++++++++++++++++++ 1 file changed, 983 insertions(+) diff --git a/packages/deepbook/tests/pool_tests.move b/packages/deepbook/tests/pool_tests.move index 3f0c4646f..2fca4b21a 100644 --- a/packages/deepbook/tests/pool_tests.move +++ b/packages/deepbook/tests/pool_tests.move @@ -9822,3 +9822,986 @@ 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_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_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_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 pool_referral_combined_fees() { + 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); + // Mint referral with 50% multiplier (5000 bps = 50%) + // 0.5 * float_scaling = 500_000_000 + referral_id = pool.mint_referral(500_000_000, test.ctx()); + return_shared(pool); + }; + + // Set 10 bps volume 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_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 Buy order from BOB (taker) + // 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, // 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) + // We expect combined fee > volume fee (0.2 USDC = 200_000_000). + assert!(quote > 200_000_000, 0); + + assert_eq!(base, 0); + assert_eq!(deep, 0); + + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test] +fun pool_referral_combined_fees_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(500_000_000, test.ctx()); // 50% multiplier + return_shared(pool); + }; + + // Set 10 bps volume 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_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 Sell order from BOB (taker) + // Sell 100 SUI at market price ($1) + // Volume = 100 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) + // We expect combined fee > volume fee (0.1 SUI = 100_000_000). + assert!(base > 100_000_000, 0); + + assert_eq!(quote, 0); + assert_eq!(deep, 0); + + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test] +fun pool_referral_combined_fees_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(500_000_000, test.ctx()); // 50% multiplier + return_shared(pool); + }; + + // Set 10 bps volume 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_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 Buy order from BOB (taker), paying in DEEP + // 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) + 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 is in DEEP + // We expect combined fee > 0 + assert!(deep > 0, 0); + + assert_eq!(base, 0); + assert_eq!(quote, 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_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_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 10 bps fee rate + let fee_rate_1 = 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_fee_rate(&referral, fee_rate_1, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + // Update to 20 bps fee rate + let fee_rate_2 = 2_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_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 20 bps + 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.002 = 0.4 USDC = 400_000_000 + assert_eq!(quote, 400_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_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_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 21 bps fee rate (Max is 20 bps = 2_000_000) + let fee_rate = 2_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_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 = 1_000_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_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_fee_rate(&referral, fee_rate, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + end(test); +} From 2dd19497e97269dcc7edb890fa6940b08469a94f Mon Sep 17 00:00:00 2001 From: bathord Date: Sun, 22 Mar 2026 09:46:57 +0300 Subject: [PATCH 04/11] Add comments to update_pool_referral_fee_rate --- packages/deepbook/sources/pool.move | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/deepbook/sources/pool.move b/packages/deepbook/sources/pool.move index c4e7a655c..f7fb84a40 100644 --- a/packages/deepbook/sources/pool.move +++ b/packages/deepbook/sources/pool.move @@ -935,7 +935,8 @@ public fun update_pool_referral_multiplier( referral_rewards.multiplier = multiplier; } -/// Update the fee rate for the referral. +/// Update the referral fee rate, attaching a `ReferralFeeConfig` dynamic field if one doesn't +/// exist yet. public fun update_pool_referral_fee_rate( self: &mut Pool, referral: &DeepBookPoolReferral, @@ -951,9 +952,11 @@ public fun update_pool_referral_fee_rate( let referral_id = object::id(referral); let key = ReferralFeeConfigKey(referral_id); if (self.id.exists_(key)) { + // Update existing config let config: &mut ReferralFeeConfig = self.id.borrow_mut(key); config.fee_rate = fee_rate; } else { + // Attach new config self.id.add(key, ReferralFeeConfig { fee_rate }); } } From 48beb43c6ab086bc28159ee6e7cf2bc53dc40552 Mon Sep 17 00:00:00 2001 From: bathord Date: Sun, 22 Mar 2026 09:47:52 +0300 Subject: [PATCH 05/11] Upd ReferralFeeConfig comment --- packages/deepbook/sources/pool.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/deepbook/sources/pool.move b/packages/deepbook/sources/pool.move index f7fb84a40..39c4fe9ef 100644 --- a/packages/deepbook/sources/pool.move +++ b/packages/deepbook/sources/pool.move @@ -108,7 +108,7 @@ public struct DeepBurned has copy, drop, deep_burned: u64, } -/// The configuration for the referral fee rate. +/// The configuration for the referral fee rate in bps. public struct ReferralFeeConfig has store { fee_rate: u64, } From e7cf19a462b028a80fe1395c030dbd20c2a9469a Mon Sep 17 00:00:00 2001 From: bathord Date: Sun, 22 Mar 2026 09:49:17 +0300 Subject: [PATCH 06/11] Upd MAX_REFERRAL_FEE_RATE to 10 bps --- packages/deepbook/sources/helper/constants.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/deepbook/sources/helper/constants.move b/packages/deepbook/sources/helper/constants.move index bd4072b36..aca34f575 100644 --- a/packages/deepbook/sources/helper/constants.move +++ b/packages/deepbook/sources/helper/constants.move @@ -17,7 +17,7 @@ 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_FEE_RATE: u64 = 2_000_000; // 0.2% (20 bps) +const MAX_REFERRAL_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; From 61d61afd4bc3da7bbcd5ae53aab96864e2aa01b9 Mon Sep 17 00:00:00 2001 From: bathord Date: Sun, 22 Mar 2026 10:41:07 +0300 Subject: [PATCH 07/11] Upd referral fee modes to be mutually exclusive --- packages/deepbook/sources/pool.move | 64 ++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/packages/deepbook/sources/pool.move b/packages/deepbook/sources/pool.move index 39c4fe9ef..6ee62d57b 100644 --- a/packages/deepbook/sources/pool.move +++ b/packages/deepbook/sources/pool.move @@ -67,6 +67,7 @@ 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 { @@ -917,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 fee rate in bps is absent or 0. public fun update_pool_referral_multiplier( self: &mut Pool, referral: &DeepBookPoolReferral, @@ -928,7 +929,15 @@ 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 = ReferralFeeConfigKey(referral_id); + if (self.id.exists_(bps_config_key)) { + // Referral fee rate in bps must be 0 + let config: &ReferralFeeConfig = self.id.borrow(bps_config_key); + assert!(config.fee_rate == 0, EConflictingReferralFee); + }; + let referral_rewards: &mut ReferralRewards = self .id .borrow_mut(referral_id); @@ -936,7 +945,7 @@ public fun update_pool_referral_multiplier( } /// Update the referral fee rate, attaching a `ReferralFeeConfig` dynamic field if one doesn't -/// exist yet. +/// exist yet. Only allowed when the referral fee multiplier is 0. public fun update_pool_referral_fee_rate( self: &mut Pool, referral: &DeepBookPoolReferral, @@ -950,6 +959,10 @@ public fun update_pool_referral_fee_rate( assert!(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 = ReferralFeeConfigKey(referral_id); if (self.id.exists_(key)) { // Update existing config @@ -1965,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, @@ -1974,9 +1988,17 @@ 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 mut volume_fee = 0; - if (self.id.exists_(ReferralFeeConfigKey(referral_id))) { - let config: &ReferralFeeConfig = self.id.borrow(ReferralFeeConfigKey(referral_id)); + + let bps_config_key = ReferralFeeConfigKey(referral_id); + let bps_fee_rate = if (self.id.exists_(bps_config_key)) { + let config: &ReferralFeeConfig = self.id.borrow(bps_config_key); + config.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()) { let deep_quantity = order_info .order_deep_price() @@ -1997,38 +2019,40 @@ fun process_referral_fees( } else { order_info.executed_quantity() }; - volume_fee = math::mul(volume, config.fee_rate); + + 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 multiplier_fee = math::mul(order_info.paid_fees(), referral_multiplier); - - let total_referral_fee = multiplier_fee + volume_fee; - if (total_referral_fee == 0) { - return - }; - let mut base_fee = 0; let mut quote_fee = 0; let mut deep_fee = 0; if (order_info.fee_is_deep()) { referral_rewards .deep - .join(balance_manager.withdraw_with_proof(trade_proof, total_referral_fee, false)); - deep_fee = total_referral_fee; + .join(balance_manager.withdraw_with_proof(trade_proof, referral_fee, false)); + deep_fee = referral_fee; } else if (!order_info.is_bid()) { referral_rewards .base - .join(balance_manager.withdraw_with_proof(trade_proof, total_referral_fee, false)); - base_fee = total_referral_fee; + .join(balance_manager.withdraw_with_proof(trade_proof, referral_fee, false)); + base_fee = referral_fee; } else { referral_rewards .quote - .join(balance_manager.withdraw_with_proof(trade_proof, total_referral_fee, false)); - quote_fee = total_referral_fee; + .join(balance_manager.withdraw_with_proof(trade_proof, referral_fee, false)); + quote_fee = referral_fee; }; event::emit(ReferralFeeEvent { From b60efd9806d7514f5b106ddbbff87f09fe84b6cd Mon Sep 17 00:00:00 2001 From: bathord Date: Sun, 22 Mar 2026 15:05:30 +0300 Subject: [PATCH 08/11] Cast deep quantity to u64 without capping --- packages/deepbook/sources/pool.move | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/deepbook/sources/pool.move b/packages/deepbook/sources/pool.move index 6ee62d57b..cd9bd9f65 100644 --- a/packages/deepbook/sources/pool.move +++ b/packages/deepbook/sources/pool.move @@ -2000,20 +2000,12 @@ fun process_referral_fees( 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()) { - let deep_quantity = order_info + order_info .order_deep_price() .deep_quantity_u128( order_info.executed_quantity() as u128, order_info.cumulative_quote_quantity() as u128, - ); - - // Cap the volume at MAX_U64 to prevent the transaction from aborting due to overflow - // during the cast. MAX_U64 DEEP is far greater than the total supply. - if (deep_quantity > (constants::max_u64() as u128)) { - constants::max_u64() - } else { - deep_quantity as u64 - } + ) as u64 } else if (order_info.is_bid()) { order_info.cumulative_quote_quantity() } else { From fd47a376c45c12e90374afcb3ecdf692d98618e4 Mon Sep 17 00:00:00 2001 From: bathord Date: Sun, 22 Mar 2026 15:38:19 +0300 Subject: [PATCH 09/11] MAX_REFERRAL_FEE_RATE -> MAX_REFERRAL_TAKER_FEE_RATE --- packages/deepbook/sources/helper/constants.move | 6 +++--- packages/deepbook/sources/pool.move | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/deepbook/sources/helper/constants.move b/packages/deepbook/sources/helper/constants.move index aca34f575..58419eca5 100644 --- a/packages/deepbook/sources/helper/constants.move +++ b/packages/deepbook/sources/helper/constants.move @@ -17,7 +17,7 @@ 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_FEE_RATE: u64 = 1_000_000; // 0.1% (10 bps) +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; @@ -266,8 +266,8 @@ public fun referral_max_multiplier(): u64 { REFERRAL_MAX_MULTIPLIER } -public fun max_referral_fee_rate(): u64 { - MAX_REFERRAL_FEE_RATE +public fun max_referral_taker_fee_rate(): u64 { + MAX_REFERRAL_TAKER_FEE_RATE } public fun referral_multiplier(): u64 { diff --git a/packages/deepbook/sources/pool.move b/packages/deepbook/sources/pool.move index cd9bd9f65..4a0695f0d 100644 --- a/packages/deepbook/sources/pool.move +++ b/packages/deepbook/sources/pool.move @@ -955,7 +955,7 @@ public fun update_pool_referral_fee_rate( let _ = self.load_inner(); referral.assert_referral_owner(ctx); assert!(referral.balance_manager_referral_pool_id() == self.id(), EWrongPoolReferral); - assert!(fee_rate <= constants::max_referral_fee_rate(), EInvalidReferralFeeRate); + assert!(fee_rate <= constants::max_referral_taker_fee_rate(), EInvalidReferralFeeRate); assert!(fee_rate % constants::fee_precision_multiple() == 0, EInvalidReferralFeeRate); let referral_id = object::id(referral); From a4ca20a45ce93824076b33dcc5eb011d4e6ad238 Mon Sep 17 00:00:00 2001 From: bathord Date: Sun, 22 Mar 2026 16:02:31 +0300 Subject: [PATCH 10/11] Fee -> Taker fee --- packages/deepbook/sources/pool.move | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/deepbook/sources/pool.move b/packages/deepbook/sources/pool.move index 4a0695f0d..bb5ed0969 100644 --- a/packages/deepbook/sources/pool.move +++ b/packages/deepbook/sources/pool.move @@ -109,13 +109,13 @@ public struct DeepBurned has copy, drop, deep_burned: u64, } -/// The configuration for the referral fee rate in bps. -public struct ReferralFeeConfig has store { - fee_rate: 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 fee configuration. -public struct ReferralFeeConfigKey(ID) has copy, drop, store; +/// The key for the referral taker fee configuration. +public struct ReferralTakerFeeConfigKey(ID) has copy, drop, store; public struct ReferralRewards has store { multiplier: u64, @@ -918,7 +918,7 @@ public fun update_deepbook_referral_multiplier( abort } -/// Update the multiplier for the referral. Only allowed when the referral fee rate in bps is absent or 0. +/// 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, @@ -931,11 +931,11 @@ public fun update_pool_referral_multiplier( assert!(multiplier % constants::referral_multiplier() == 0, EInvalidReferralMultiplier); let referral_id = object::id(referral); - let bps_config_key = ReferralFeeConfigKey(referral_id); + let bps_config_key = ReferralTakerFeeConfigKey(referral_id); if (self.id.exists_(bps_config_key)) { - // Referral fee rate in bps must be 0 - let config: &ReferralFeeConfig = self.id.borrow(bps_config_key); - assert!(config.fee_rate == 0, EConflictingReferralFee); + // 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 @@ -944,33 +944,33 @@ public fun update_pool_referral_multiplier( referral_rewards.multiplier = multiplier; } -/// Update the referral fee rate, attaching a `ReferralFeeConfig` dynamic field if one doesn't +/// 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_fee_rate( +public fun update_pool_referral_taker_fee_rate( self: &mut Pool, referral: &DeepBookPoolReferral, - fee_rate: u64, + 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!(fee_rate <= constants::max_referral_taker_fee_rate(), EInvalidReferralFeeRate); - assert!(fee_rate % constants::fee_precision_multiple() == 0, EInvalidReferralFeeRate); + 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 = ReferralFeeConfigKey(referral_id); + let key = ReferralTakerFeeConfigKey(referral_id); if (self.id.exists_(key)) { // Update existing config - let config: &mut ReferralFeeConfig = self.id.borrow_mut(key); - config.fee_rate = fee_rate; + let config: &mut ReferralTakerFeeConfig = self.id.borrow_mut(key); + config.taker_fee_rate = taker_fee_rate; } else { // Attach new config - self.id.add(key, ReferralFeeConfig { fee_rate }); + self.id.add(key, ReferralTakerFeeConfig { taker_fee_rate }); } } @@ -1989,10 +1989,10 @@ fun process_referral_fees( if (referral_id.is_some()) { let referral_id = referral_id.destroy_some(); - let bps_config_key = ReferralFeeConfigKey(referral_id); + let bps_config_key = ReferralTakerFeeConfigKey(referral_id); let bps_fee_rate = if (self.id.exists_(bps_config_key)) { - let config: &ReferralFeeConfig = self.id.borrow(bps_config_key); - config.fee_rate + let config: &ReferralTakerFeeConfig = self.id.borrow(bps_config_key); + config.taker_fee_rate } else { 0 }; From eb25e08fa5e54d00fb33899eb5da349dd03f197a Mon Sep 17 00:00:00 2001 From: bathord Date: Sun, 22 Mar 2026 16:24:21 +0300 Subject: [PATCH 11/11] Upd referral tests --- packages/deepbook/tests/pool_tests.move | 552 +++++++++++++----------- 1 file changed, 294 insertions(+), 258 deletions(-) diff --git a/packages/deepbook/tests/pool_tests.move b/packages/deepbook/tests/pool_tests.move index 2fca4b21a..4dcb24bd6 100644 --- a/packages/deepbook/tests/pool_tests.move +++ b/packages/deepbook/tests/pool_tests.move @@ -9842,7 +9842,7 @@ fun pool_referral_fee_rate_ok() { { let mut pool = test.take_shared_by_id>(pool_id); let referral = test.take_shared_by_id(referral_id); - pool.update_pool_referral_fee_rate(&referral, fee_rate, test.ctx()); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); return_shared(referral); return_shared(pool); }; @@ -9922,7 +9922,7 @@ fun pool_referral_fee_rate_base_asset() { { let mut pool = test.take_shared_by_id>(pool_id); let referral = test.take_shared_by_id(referral_id); - pool.update_pool_referral_fee_rate(&referral, fee_rate, test.ctx()); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); return_shared(referral); return_shared(pool); }; @@ -10001,7 +10001,7 @@ fun pool_referral_fee_rate_deep() { { let mut pool = test.take_shared_by_id>(pool_id); let referral = test.take_shared_by_id(referral_id); - pool.update_pool_referral_fee_rate(&referral, fee_rate, test.ctx()); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); return_shared(referral); return_shared(pool); }; @@ -10061,242 +10061,6 @@ fun pool_referral_fee_rate_deep() { end(test); } -#[test] -fun pool_referral_combined_fees() { - 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); - // Mint referral with 50% multiplier (5000 bps = 50%) - // 0.5 * float_scaling = 500_000_000 - referral_id = pool.mint_referral(500_000_000, test.ctx()); - return_shared(pool); - }; - - // Set 10 bps volume 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_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 Buy order from BOB (taker) - // 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, // 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) - // We expect combined fee > volume fee (0.2 USDC = 200_000_000). - assert!(quote > 200_000_000, 0); - - assert_eq!(base, 0); - assert_eq!(deep, 0); - - return_shared(referral); - return_shared(pool); - }; - - end(test); -} - -#[test] -fun pool_referral_combined_fees_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(500_000_000, test.ctx()); // 50% multiplier - return_shared(pool); - }; - - // Set 10 bps volume 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_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 Sell order from BOB (taker) - // Sell 100 SUI at market price ($1) - // Volume = 100 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) - // We expect combined fee > volume fee (0.1 SUI = 100_000_000). - assert!(base > 100_000_000, 0); - - assert_eq!(quote, 0); - assert_eq!(deep, 0); - - return_shared(referral); - return_shared(pool); - }; - - end(test); -} - -#[test] -fun pool_referral_combined_fees_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(500_000_000, test.ctx()); // 50% multiplier - return_shared(pool); - }; - - // Set 10 bps volume 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_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 Buy order from BOB (taker), paying in DEEP - // 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) - 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 is in DEEP - // We expect combined fee > 0 - assert!(deep > 0, 0); - - assert_eq!(base, 0); - assert_eq!(quote, 0); - - return_shared(referral); - return_shared(pool); - }; - - end(test); -} - #[test] fun whitelisted_pool_referral_fee() { let mut test = begin(OWNER); @@ -10353,7 +10117,7 @@ fun whitelisted_pool_referral_fee() { { let mut pool = test.take_shared_by_id>(pool_id); let referral = test.take_shared_by_id(referral_id); - pool.update_pool_referral_fee_rate(&referral, fee_rate, test.ctx()); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); return_shared(referral); return_shared(pool); }; @@ -10430,7 +10194,7 @@ fun pool_referral_fee_zero() { { let mut pool = test.take_shared_by_id>(pool_id); let referral = test.take_shared_by_id(referral_id); - pool.update_pool_referral_fee_rate(&referral, fee_rate, test.ctx()); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); return_shared(referral); return_shared(pool); }; @@ -10496,24 +10260,24 @@ fun pool_referral_fee_update() { return_shared(pool); }; - // Set 10 bps fee rate - let fee_rate_1 = 1_000_000; + // 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_fee_rate(&referral, fee_rate_1, test.ctx()); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate_1, test.ctx()); return_shared(referral); return_shared(pool); }; - // Update to 20 bps fee rate - let fee_rate_2 = 2_000_000; + // 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_fee_rate(&referral, fee_rate_2, test.ctx()); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate_2, test.ctx()); return_shared(referral); return_shared(pool); }; @@ -10549,7 +10313,7 @@ fun pool_referral_fee_update() { &mut test, ); - // Check referral rewards - should reflect 20 bps + // Check referral rewards - should reflect 10 bps (max) test.next_tx(ALICE); { let pool = test.take_shared_by_id>(pool_id); @@ -10557,8 +10321,8 @@ fun pool_referral_fee_update() { let (_, quote, _) = pool.get_pool_referral_balances(&referral); // Volume = 200 USDC - // Fee = 200 * 0.002 = 0.4 USDC = 400_000_000 - assert_eq!(quote, 400_000_000); + // Fee = 200 * 0.001 = 0.2 USDC = 200_000_000 + assert_eq!(quote, 200_000_000); return_shared(referral); return_shared(pool); @@ -10585,7 +10349,7 @@ fun pool_referral_fee_maker_no_fill() { { let mut pool = test.take_shared_by_id>(pool_id); let referral = test.take_shared_by_id(referral_id); - pool.update_pool_referral_fee_rate(&referral, fee_rate, test.ctx()); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); return_shared(referral); return_shared(pool); }; @@ -10663,7 +10427,7 @@ fun pool_referral_fee_maker_partial_fill() { { let mut pool = test.take_shared_by_id>(pool_id); let referral = test.take_shared_by_id(referral_id); - pool.update_pool_referral_fee_rate(&referral, fee_rate, test.ctx()); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); return_shared(referral); return_shared(pool); }; @@ -10742,13 +10506,13 @@ fun pool_referral_fee_max_exceeded() { return_shared(pool); }; - // Set 21 bps fee rate (Max is 20 bps = 2_000_000) - let fee_rate = 2_100_000; + // 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_fee_rate(&referral, fee_rate, test.ctx()); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); return_shared(referral); return_shared(pool); }; @@ -10769,12 +10533,12 @@ fun pool_referral_fee_invalid_precision() { // Set fee rate not multiple of 1000 (0.01 bps) // 1_000_001 - let fee_rate = 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_fee_rate(&referral, fee_rate, test.ctx()); + pool.update_pool_referral_taker_fee_rate(&referral, fee_rate, test.ctx()); return_shared(referral); return_shared(pool); }; @@ -10799,9 +10563,281 @@ fun pool_referral_fee_unauthorized_update() { { let mut pool = test.take_shared_by_id>(pool_id); let referral = test.take_shared_by_id(referral_id); - pool.update_pool_referral_fee_rate(&referral, fee_rate, test.ctx()); + 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); +}