diff --git a/.gitignore b/.gitignore index d6eb9e10..091166da 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,4 @@ package-lock.json **/benchmark_data/ tx-data.txt example.ts - +/data diff --git a/packages/deepbook/sources/book/book.move b/packages/deepbook/sources/book/book.move index 89376624..2b3f9fda 100644 --- a/packages/deepbook/sources/book/book.move +++ b/packages/deepbook/sources/book/book.move @@ -372,6 +372,36 @@ public(package) fun get_level2_range_and_ticks( (price_vec, quantity_vec) } +public(package) fun check_limit_order_params( + self: &Book, + price: u64, + quantity: u64, + expire_timestamp: u64, + timestamp_ms: u64, +): bool { + if (expire_timestamp <= timestamp_ms) { + return false + }; + if (quantity < self.min_size || quantity % self.lot_size != 0) { + return false + }; + if ( + price % self.tick_size != 0 || price < constants::min_price() || price > constants::max_price() + ) { + return false + }; + + true +} + +public(package) fun check_market_order_params(self: &Book, quantity: u64): bool { + if (quantity < self.min_size || quantity % self.lot_size != 0) { + return false + }; + + true +} + public(package) fun get_order(self: &Book, order_id: u128): Order { let order = self.book_side(order_id).borrow(order_id); diff --git a/packages/deepbook/sources/pool.move b/packages/deepbook/sources/pool.move index 5ba82dcd..da518240 100644 --- a/packages/deepbook/sources/pool.move +++ b/packages/deepbook/sources/pool.move @@ -15,6 +15,7 @@ use deepbook::{ DepositCap, WithdrawCap }, + balances, big_vector::BigVector, book::{Self, Book}, constants, @@ -1393,7 +1394,184 @@ public fun locked_balance( (base_quantity, quote_quantity, deep_quantity) } +/// Check if a limit order can be placed based on balance manager balances. +/// Returns true if the balance manager has sufficient balance (accounting for fees) to place the order, false otherwise. +/// Assumes the limit order is a taker order as a worst case scenario. +public fun can_place_limit_order( + self: &Pool, + balance_manager: &BalanceManager, + price: u64, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + expire_timestamp: u64, + clock: &Clock, +): bool { + let whitelist = self.whitelisted(); + let pool_inner = self.load_inner(); + + if ( + !self.check_limit_order_params( + price, + quantity, + expire_timestamp, + clock, + ) + ) { + return false + }; + + let order_deep_price = if (pay_with_deep) { + pool_inner.deep_price.get_order_deep_price(whitelist) + } else { + pool_inner.deep_price.empty_deep_price() + }; + + let quote_quantity = math::mul(quantity, price); + + // Calculate fee quantity using taker fee (worst case for limit orders) + let taker_fee = pool_inner.state.governance().trade_params().taker_fee(); + let fee_balances = order_deep_price.fee_quantity(quantity, quote_quantity, is_bid); + + // Calculate required balances + let mut required_base = 0; + let mut required_quote = 0; + let mut required_deep = 0; + + if (is_bid) { + required_quote = quote_quantity; + if (pay_with_deep) { + required_deep = fee_balances.deep(); + } else { + let fee_quote = math::mul(fee_balances.quote(), taker_fee); + required_quote = required_quote + fee_quote; + }; + } else { + required_base = quantity; + if (pay_with_deep) { + required_deep = fee_balances.deep(); + } else { + let fee_base = math::mul(fee_balances.base(), taker_fee); + required_base = required_base + fee_base; + }; + }; + + // Get current balances from balance manager. Accounts for settled balances. + let settled_balances = if (!self.account_exists(balance_manager)) { + balances::empty() + } else { + self.account(balance_manager).settled_balances() + }; + let available_base = balance_manager.balance() + settled_balances.base(); + let available_quote = balance_manager.balance() + settled_balances.quote(); + let available_deep = balance_manager.balance() + settled_balances.deep(); + + // Check if available balances are sufficient + (available_base >= required_base) && (available_quote >= required_quote) && (available_deep >= required_deep) +} + +/// Check if a market order can be placed based on balance manager balances. +/// Returns true if the balance manager has sufficient balance (accounting for fees) to place the order, false otherwise. +/// Does not account for discounted taker fees +public fun can_place_market_order( + self: &Pool, + balance_manager: &BalanceManager, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + clock: &Clock, +): bool { + // Validate order parameters against pool book params + if (!self.check_market_order_params(quantity)) { + return false + }; + + let mut required_base = 0; + let mut required_deep = 0; + + // Get current balances from balance manager. Accounts for settled balances. + let settled_balances = if (!self.account_exists(balance_manager)) { + balances::empty() + } else { + self.account(balance_manager).settled_balances() + }; + let available_base = balance_manager.balance() + settled_balances.base(); + let available_quote = balance_manager.balance() + settled_balances.quote(); + let available_deep = balance_manager.balance() + settled_balances.deep(); + + if (is_bid) { + // For bid orders: check if available quote can return desired base quantity + // get_quantity_out_input_fee already accounts for fees being deducted from quote + let (base_out, _, deep_required) = if (pay_with_deep) { + self.get_quantity_out(0, available_quote, clock) + } else { + self.get_quantity_out_input_fee(0, available_quote, clock) + }; + + // Not enough quote balance for the base quantity + if (base_out < quantity) { + return false + }; + + if (pay_with_deep) { + required_deep = deep_required; + }; + } else { + // For ask orders: if paying fees in input token (base), need quantity + fees + // get_quantity_out_input_fee accounts for fees, so we need to check if we have enough base + // including fees that will be deducted + let (_, _, deep_required) = if (pay_with_deep) { + self.get_quantity_out(quantity, 0, clock) + } else { + self.get_quantity_out_input_fee(quantity, 0, clock) + }; + + // If paying fees in base asset, need quantity + fees + required_base = if (pay_with_deep) { + quantity + } else { + // Fees are deducted from base, so need more base to account for fees + let (taker_fee, _, _) = self.pool_trade_params(); + let input_fee_rate = math::mul(taker_fee, constants::fee_penalty_multiplier()); + math::mul(quantity, constants::float_scaling() + input_fee_rate) + }; + + if (pay_with_deep) { + required_deep = deep_required; + }; + }; + + // Check if available balances are sufficient + (available_base >= required_base) && (available_deep >= required_deep) +} + +/// Check if a market order can be placed based on pool book params. +/// Returns true if the order parameters are valid, false otherwise. +public fun check_market_order_params( + self: &Pool, + quantity: u64, +): bool { + let pool_inner = self.load_inner(); + pool_inner.book.check_market_order_params(quantity) +} + +/// Check if a limit order can be placed based on pool book params. +/// Returns true if the order parameters are valid, false otherwise. +public fun check_limit_order_params( + self: &Pool, + price: u64, + quantity: u64, + expire_timestamp: u64, + clock: &Clock, +): bool { + let pool_inner = self.load_inner(); + pool_inner + .book + .check_limit_order_params(price, quantity, expire_timestamp, clock.timestamp_ms()) +} + /// Returns the trade params for the pool. +/// Returns (taker_fee, maker_fee, stake_required) public fun pool_trade_params( self: &Pool, ): (u64, u64, u64) { @@ -1431,6 +1609,14 @@ public fun pool_book_params( (tick_size, lot_size, min_size) } +public fun account_exists( + self: &Pool, + balance_manager: &BalanceManager, +): bool { + let self = self.load_inner(); + self.state.account_exists(balance_manager.id()) +} + public fun account( self: &Pool, balance_manager: &BalanceManager, diff --git a/packages/deepbook/tests/balance_manager_tests.move b/packages/deepbook/tests/balance_manager_tests.move index b4561571..c02c900f 100644 --- a/packages/deepbook/tests/balance_manager_tests.move +++ b/packages/deepbook/tests/balance_manager_tests.move @@ -91,7 +91,7 @@ fun test_deposit_as_owner_e() { test.next_tx(alice); { let balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); transfer::public_share_object(balance_manager); }; @@ -120,7 +120,7 @@ fun test_remove_trader_e() { test.next_tx(alice); { let mut balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); let trade_cap = balance_manager.mint_trade_cap(test.ctx()); trade_cap_id = object::id(&trade_cap); transfer::public_transfer(trade_cap, bob); @@ -149,7 +149,7 @@ fun test_deposit_with_removed_trader_e() { test.next_tx(alice); { let mut balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); let trade_cap = balance_manager.mint_trade_cap(test.ctx()); let trade_proof = balance_manager.generate_proof_as_trader( &trade_cap, @@ -199,7 +199,7 @@ fun test_deposit_with_removed_deposit_cap_e() { test.next_tx(alice); { let mut balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); let deposit_cap = balance_manager.mint_deposit_cap(test.ctx()); deposit_cap_id = object::id(&deposit_cap); @@ -277,7 +277,7 @@ fun test_deposit_with_deposit_cap_ok() { test.next_tx(alice); { let mut balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); let deposit_cap = balance_manager.mint_deposit_cap(test.ctx()); balance_manager.deposit_with_cap( @@ -324,7 +324,7 @@ fun test_withdraw_with_removed_withdraw_cap_e() { test.next_tx(alice); { let mut balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); let withdraw_cap = balance_manager.mint_withdraw_cap(test.ctx()); withdraw_cap_id = object::id(&withdraw_cap); balance_manager.deposit( @@ -415,7 +415,7 @@ fun test_withdraw_with_withdraw_cap_ok() { test.next_tx(alice); { let mut balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); let withdraw_cap = balance_manager.mint_withdraw_cap(test.ctx()); balance_manager.deposit( mint_for_testing(1000, test.ctx()), @@ -656,7 +656,7 @@ public(package) fun create_acct_and_share_with_funds( deposit_into_account(&mut balance_manager, amount, test); let trade_cap = balance_manager.mint_trade_cap(test.ctx()); transfer::public_transfer(trade_cap, sender); - let id = object::id(&balance_manager); + let id = balance_manager.id(); transfer::public_share_object(balance_manager); id @@ -718,7 +718,7 @@ public(package) fun create_acct_and_share_with_funds_typed< ); let trade_cap = balance_manager.mint_trade_cap(test.ctx()); transfer::public_transfer(trade_cap, sender); - let id = object::id(&balance_manager); + let id = balance_manager.id(); transfer::public_share_object(balance_manager); id diff --git a/packages/deepbook/tests/pool_tests.move b/packages/deepbook/tests/pool_tests.move index 06b448aa..dd1efc90 100644 --- a/packages/deepbook/tests/pool_tests.move +++ b/packages/deepbook/tests/pool_tests.move @@ -5,7 +5,7 @@ module deepbook::pool_tests; use deepbook::{ - balance_manager::{BalanceManager, TradeCap, DeepBookReferral, DepositCap, WithdrawCap}, + balance_manager::{Self, BalanceManager, TradeCap, DeepBookReferral, DepositCap, WithdrawCap}, balance_manager_tests::{ USDC, USDT, @@ -6308,3 +6308,2043 @@ fun advance_scenario_with_gas_price(test: &mut Scenario, gas_price: u64, timesta let ctx = test.ctx_builder().set_gas_price(gas_price).set_epoch_timestamp(ts); test.next_with_context(ctx); } + +// ============== can_place_market_order tests ============== + +/// Test bid market order with sufficient quote balance and DEEP for fees +#[test] +fun test_can_place_market_order_bid_with_deep_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP fees are required + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + // Place a sell order on the book (so we can buy) + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: bid for 10 SUI with pay_with_deep = true + // Should succeed since we have enough USDC and DEEP + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test bid market order with insufficient quote balance +#[test] +fun test_can_place_market_order_bid_insufficient_quote() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with minimal funds + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Only deposit 1 USDC (not enough to buy 10 SUI at price 2) + bm.deposit( + mint_for_testing(1 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create another balance manager with funds for liquidity + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Place a sell order on the book by Bob + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: try to bid for 10 SUI but only have 1 USDC + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test bid market order with insufficient DEEP for fees (using reference pool setup) +#[test] +fun test_can_place_market_order_bid_insufficient_deep() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with USDC but no DEEP + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + // No DEEP deposited + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create balance manager for Bob with funds for liquidity and reference pool setup + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP is required for fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + BOB, + registry_id, + balance_manager_id_bob, + &mut test, + ); + + // Place a sell order on the book by Bob + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: try to bid for 10 SUI with pay_with_deep but no DEEP + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test ask market order with sufficient base balance and DEEP for fees +#[test] +fun test_can_place_market_order_ask_with_deep_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP fees are required + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + // Place a buy order on the book (so we can sell) + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: 1 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + true, // is_bid = true (buy order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: ask (sell) 10 SUI with pay_with_deep = true + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + true, // pay_with_deep + &clock, + ); + assert!(can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test ask market order with insufficient base balance +#[test] +fun test_can_place_market_order_ask_insufficient_base() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with minimal SUI + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Only deposit 1 SUI (not enough to sell 10 SUI) + bm.deposit( + mint_for_testing(1 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create another balance manager with funds for liquidity + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Place a buy order on the book by Bob + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: 1 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + true, // is_bid = true (buy order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: try to ask (sell) 10 SUI but only have 1 SUI + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test ask market order with insufficient DEEP for fees (using reference pool setup) +#[test] +fun test_can_place_market_order_ask_insufficient_deep() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with SUI but no DEEP + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(100 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + // No DEEP deposited + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create balance manager for Bob with funds for liquidity and reference pool setup + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP is required for fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + BOB, + registry_id, + balance_manager_id_bob, + &mut test, + ); + + // Place a buy order on the book by Bob + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: 1 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + true, // is_bid = true (buy order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: try to ask (sell) 10 SUI with pay_with_deep but no DEEP + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test bid market order paying fees with input token (quote) +#[test] +fun test_can_place_market_order_bid_input_fee_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with liquidity on the book + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Place a sell order on the book + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: bid for 10 SUI with pay_with_deep = false (pay fees in USDC) + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + false, // pay_with_deep = false (fees in quote) + &clock, + ); + assert!(can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test ask market order paying fees with input token (base) +#[test] +fun test_can_place_market_order_ask_input_fee_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with liquidity on the book + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Place a buy order on the book + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: 1 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + true, // is_bid = true (buy order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: ask (sell) 10 SUI with pay_with_deep = false (pay fees in SUI) + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + false, // pay_with_deep = false (fees in base) + &clock, + ); + assert!(can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test ask market order paying fees with input token but insufficient base (need extra for fees) +#[test] +fun test_can_place_market_order_ask_input_fee_insufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with only 9 SUI (clearly not enough to sell 10 SUI + fees) + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Deposit only 9 SUI - clearly not enough to sell 10 SUI when fees are in base + bm.deposit( + mint_for_testing(9 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create another balance manager with funds for liquidity + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Place a buy order on the book by Bob + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: 1 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + true, // is_bid = true (buy order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: try to ask (sell) 10 SUI with pay_with_deep = false + // Should fail because we need 10 SUI + fees, but only have 9 SUI + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + false, // pay_with_deep = false (fees in base) + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test market order with no liquidity on the book +#[test] +fun test_can_place_market_order_no_liquidity() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool WITHOUT any liquidity + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: bid for 10 SUI but no sell orders on book + // get_quantity_out will return 0 base_out since there's no liquidity + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test market order for zero quantity (edge case) +#[test] +fun test_can_place_market_order_zero_quantity() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: zero quantity should return false (fails min_size check) + let can_place = pool.can_place_market_order( + &balance_manager, + 0, // quantity: 0 + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test market order exactly at the limit of available balance +#[test] +fun test_can_place_market_order_bid_exact_balance() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with funds for liquidity + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Place a sell order on the book by Bob at price 1 + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: 1 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + // Create Alice's balance manager with exactly enough USDC to buy 10 SUI at price 1 + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // 10 USDC to buy 10 SUI at price 1 + bm.deposit( + mint_for_testing(10 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + // Enough DEEP for fees + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: bid for exactly 10 SUI with exactly 10 USDC at price 1 + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(can_place); + + // Test: try to bid for 11 SUI (should fail) + let can_place_more = pool.can_place_market_order( + &balance_manager, + 11 * constants::float_scaling(), // quantity: 11 SUI + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(!can_place_more); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +// ============== can_place_limit_order tests ============== + +/// Test bid limit order with sufficient quote balance and DEEP for fees +#[test] +fun test_can_place_limit_order_bid_with_deep_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP fees are required + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: bid for 10 SUI at price 2 with pay_with_deep = true + // Required quote = 10 * 2 = 20 USDC + DEEP fees + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test bid limit order with insufficient quote balance +#[test] +fun test_can_place_limit_order_bid_insufficient_quote() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with minimal funds + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Only deposit 10 USDC (not enough to buy 10 SUI at price 2 = 20 USDC) + bm.deposit( + mint_for_testing(10 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: try to bid for 10 SUI at price 2 but only have 10 USDC (need 20) + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test bid limit order with insufficient DEEP for fees (non-whitelisted pool) +#[test] +fun test_can_place_limit_order_bid_insufficient_deep() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with USDC but no DEEP + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + // No DEEP deposited + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create balance manager for Bob with funds for reference pool setup + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP is required for fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + BOB, + registry_id, + balance_manager_id_bob, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: try to bid for 10 SUI with pay_with_deep but no DEEP + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test ask limit order with sufficient base balance and DEEP for fees +#[test] +fun test_can_place_limit_order_ask_with_deep_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP fees are required + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: ask (sell) 10 SUI at price 2 with pay_with_deep = true + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test ask limit order with insufficient base balance +#[test] +fun test_can_place_limit_order_ask_insufficient_base() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with minimal SUI + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Only deposit 5 SUI (not enough to sell 10 SUI) + bm.deposit( + mint_for_testing(5 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: try to ask (sell) 10 SUI but only have 5 SUI + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test ask limit order with insufficient DEEP for fees (non-whitelisted pool) +#[test] +fun test_can_place_limit_order_ask_insufficient_deep() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with SUI but no DEEP + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(100 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + // No DEEP deposited + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create balance manager for Bob with funds for reference pool setup + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP is required for fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + BOB, + registry_id, + balance_manager_id_bob, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: try to ask (sell) 10 SUI with pay_with_deep but no DEEP + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test bid limit order paying fees with input token (quote) +#[test] +fun test_can_place_limit_order_bid_input_fee_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: bid for 10 SUI at price 2 with pay_with_deep = false (pay fees in USDC) + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + false, // pay_with_deep = false (fees in quote) + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test ask limit order paying fees with input token (base) +#[test] +fun test_can_place_limit_order_ask_input_fee_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: ask (sell) 10 SUI at price 2 with pay_with_deep = false (pay fees in SUI) + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + false, // pay_with_deep = false (fees in base) + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test ask limit order paying fees with input token but insufficient base (need extra for fees) +#[test] +fun test_can_place_limit_order_ask_input_fee_insufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with only 9 SUI (not enough to sell 10 SUI + fees) + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Deposit only 9 SUI - not enough to sell 10 SUI when fees are in base + bm.deposit( + mint_for_testing(9 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: try to ask (sell) 10 SUI with pay_with_deep = false + // Should fail because we need 10 SUI + fees, but only have 9 SUI + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + false, // pay_with_deep = false (fees in base) + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test limit order for zero quantity (edge case) +#[test] +fun test_can_place_limit_order_zero_quantity() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: zero quantity should return false (fails min_size check) + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 0, // quantity: 0 + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test limit order exactly at the limit of available balance +#[test] +fun test_can_place_limit_order_bid_exact_balance() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create Alice's balance manager with exactly enough USDC to bid for 10 SUI at price 2 + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // 20 USDC to buy 10 SUI at price 2 + bm.deposit( + mint_for_testing(20 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + // Enough DEEP for fees + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup pool (whitelisted, so DEEP fees are 0) + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: bid for exactly 10 SUI at price 2 with exactly 20 USDC + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place); + + // Test: try to bid for 11 SUI at price 2 (need 22 USDC, only have 20) + let can_place_more = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 11 * constants::float_scaling(), // quantity: 11 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place_more); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test limit order with different prices +#[test] +fun test_can_place_limit_order_price_variations() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with 100 USDC + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(100 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup pool (whitelisted) + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: bid for 10 SUI at price 5 (need 50 USDC, have 100) + let can_place_low_price = pool.can_place_limit_order( + &balance_manager, + 5 * constants::float_scaling(), // price: 5 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place_low_price); + + // Test: bid for 10 SUI at price 15 (need 150 USDC, only have 100) + let can_place_high_price = pool.can_place_limit_order( + &balance_manager, + 15 * constants::float_scaling(), // price: 15 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place_high_price); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test that fee_penalty_multiplier (1.25) is correctly applied only once +/// For a sell order of 1 SUI with input token fee: +/// required_base = quantity * (1 + fee_penalty_multiplier * taker_fee) +/// = 1 * (1 + 1.25 * 0.001) = 1.00125 SUI +#[test] +fun test_can_place_limit_order_fee_penalty_not_doubled() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Calculate exact required amount: + // taker_fee = 1_000_000 (0.001 or 0.1%) + // fee_penalty_multiplier = 1_250_000_000 (1.25) + // For 1 SUI (1_000_000_000 base units): + // fee_balances.base() = 1_000_000_000 * 1.25 = 1_250_000_000 + // fee_base = 1_250_000_000 * 0.001 = 1_250_000 + // required_base = 1_000_000_000 + 1_250_000 = 1_001_250_000 + let quantity = constants::float_scaling(); // 1 SUI = 1_000_000_000 + let required_with_fee = 1_001_250_000u64; // 1.00125 SUI + + // Create balance manager for setup with lots of funds + let balance_manager_id_setup = create_acct_and_share_with_funds( + OWNER, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Create balance manager with exactly enough (should pass) + test.next_tx(ALICE); + let balance_manager_id_exact; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(required_with_fee, test.ctx()), + test.ctx(), + ); + balance_manager_id_exact = bm.id(); + transfer::public_share_object(bm); + }; + + // Create balance manager with 1 less (should fail) + test.next_tx(BOB); + let balance_manager_id_insufficient; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(required_with_fee - 1, test.ctx()), + test.ctx(), + ); + balance_manager_id_insufficient = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup pool with reference pool to get proper fees (non-whitelisted) + let pool_id = setup_pool_with_default_fees_and_reference_pool( + OWNER, + registry_id, + balance_manager_id_setup, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager_exact = test.take_shared_by_id( + balance_manager_id_exact, + ); + let balance_manager_insufficient = test.take_shared_by_id( + balance_manager_id_insufficient, + ); + let clock = clock::create_for_testing(test.ctx()); + + // Verify taker fee is set correctly + let (taker_fee, _, _) = pool.pool_trade_params(); + assert!(taker_fee == constants::taker_fee()); + + // Test with exactly enough balance - should pass + let can_place_exact = pool.can_place_limit_order( + &balance_manager_exact, + 1 * constants::float_scaling(), // price: 1 USDC per SUI + quantity, // quantity: 1 SUI + false, // is_bid = false (ask/sell) + false, // pay_with_deep = false (fees in base) + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place_exact); + + // Test with 1 unit less - should fail + let can_place_insufficient = pool.can_place_limit_order( + &balance_manager_insufficient, + 1 * constants::float_scaling(), // price: 1 USDC per SUI + quantity, // quantity: 1 SUI + false, // is_bid = false (ask/sell) + false, // pay_with_deep = false (fees in base) + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place_insufficient); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager_exact); + return_shared(balance_manager_insufficient); + }; + + end(test); +} + +/// Test limit order with expired timestamp (should return false even with sufficient balance) +#[test] +fun test_can_place_limit_order_expired_timestamp() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let mut clock = clock::create_for_testing(test.ctx()); + + // Set clock to 1000ms + clock.set_for_testing(1000); + + // Test: sufficient balance but expire_timestamp is in the past (500ms < 1000ms) + // Should return false because the order would be expired + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + 500, // expire_timestamp: 500ms (in the past) + &clock, + ); + assert!(!can_place); + + // Test: same order but with future expire_timestamp should succeed + let can_place_future = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + 2000, // expire_timestamp: 2000ms (in the future) + &clock, + ); + assert!(can_place_future); + + // Test: expire_timestamp exactly at current time should return true + // (order is valid at the moment of expiration) + let can_place_exact = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + 1001, // expire_timestamp: 1001ms (just after current time) + &clock, + ); + assert!(can_place_exact); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test that can_place_limit_order includes settled balances +/// Without settled balances, Alice wouldn't have enough USDC to place a bid. +/// With settled balances from a previous trade, she can place the order. +#[test] +fun test_can_place_limit_order_with_settled_balances() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create Alice's balance manager with only SUI (no USDC) + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Alice has 100 SUI but NO USDC + bm.deposit( + mint_for_testing(100 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create Bob's balance manager with USDC to buy Alice's SUI + test.next_tx(BOB); + let balance_manager_id_bob; + { + let mut bm = balance_manager::new(test.ctx()); + // Bob has USDC to buy SUI + bm.deposit( + mint_for_testing(200 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_bob = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup whitelisted pool (no DEEP fees required for simplicity) + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Alice places a limit sell order: sell 10 SUI at price 2 USDC per SUI + let client_order_id = 1; + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + client_order_id, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (sell/ask) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + // Bob places a market buy order: buy 10 SUI (pays 20 USDC) + // This fills Alice's order, giving Alice 20 USDC in settled balances + place_market_order( + BOB, + pool_id, + balance_manager_id_bob, + 2, + constants::self_matching_allowed(), + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid = true (buy) + true, // pay_with_deep + &mut test, + ); + + // Now test: Alice has 0 direct USDC, but has 20 USDC settled from the trade + // She should be able to place a bid order for 5 SUI at price 2 (needs 10 USDC) + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager_alice = test.take_shared_by_id( + balance_manager_id_alice, + ); + let clock = clock::create_for_testing(test.ctx()); + + // Verify Alice has 0 direct USDC balance + let direct_usdc_balance = balance_manager_alice.balance(); + assert!(direct_usdc_balance == 0); + + // But can_place_limit_order should return true because of settled balances + // Bid for 5 SUI at price 2 = 10 USDC required (she has 20 USDC settled) + let can_place = pool.can_place_limit_order( + &balance_manager_alice, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 5 * constants::float_scaling(), // quantity: 5 SUI + true, // is_bid = true (buy) + true, // pay_with_deep + constants::max_u64(), + &clock, + ); + assert!(can_place); + + // Also verify that without enough settled balance, it would fail + // Bid for 15 SUI at price 2 = 30 USDC required (she only has 20 USDC settled) + let can_place_too_much = pool.can_place_limit_order( + &balance_manager_alice, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 15 * constants::float_scaling(), // quantity: 15 SUI + true, // is_bid = true (buy) + true, // pay_with_deep + constants::max_u64(), + &clock, + ); + assert!(!can_place_too_much); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager_alice); + }; + + end(test); +} + +/// Test limit order with price = 0 (should fail min price check) +#[test] +fun test_can_place_limit_order_price_zero() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: price = 0 should return false (fails min price check) + let can_place = pool.can_place_limit_order( + &balance_manager, + 0, // price: 0 (below min_price) + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test limit order with price = max_u64 (should fail max price check) +#[test] +fun test_can_place_limit_order_price_max_u64() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: price = max_u64 should return false (exceeds max_price) + let can_place = pool.can_place_limit_order( + &balance_manager, + constants::max_u64(), // price: max_u64 (above max_price) + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test that can_place_market_order includes settled balances +/// Without settled balances, Alice wouldn't have enough USDC to place a market bid. +/// With settled balances from a previous trade, she can place the order. +#[test] +fun test_can_place_market_order_with_settled_balances() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create Alice's balance manager with only SUI (no USDC) + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Alice has 100 SUI but NO USDC + bm.deposit( + mint_for_testing(100 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create Bob's balance manager with USDC to buy Alice's SUI + test.next_tx(BOB); + let balance_manager_id_bob; + { + let mut bm = balance_manager::new(test.ctx()); + // Bob has USDC to buy SUI + bm.deposit( + mint_for_testing(200 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_bob = bm.id(); + transfer::public_share_object(bm); + }; + + // Create Carol's balance manager to provide liquidity (sell orders for Alice to buy) + test.next_tx(@0xCCCC); + let balance_manager_id_carol; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(100 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_carol = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup whitelisted pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Alice places a limit sell order: sell 10 SUI at price 2 USDC per SUI + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), + 10 * constants::float_scaling(), + false, // sell + true, + constants::max_u64(), + &mut test, + ); + + // Bob places a market buy order: buy 10 SUI (pays 20 USDC) + // This fills Alice's order, giving Alice 20 USDC in settled balances + place_market_order( + BOB, + pool_id, + balance_manager_id_bob, + 2, + constants::self_matching_allowed(), + 10 * constants::float_scaling(), + true, // buy + true, + &mut test, + ); + + // Carol places sell orders so Alice has liquidity to buy against + place_limit_order( + @0xCCCC, + pool_id, + balance_manager_id_carol, + 3, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), + 50 * constants::float_scaling(), + false, // sell + true, + constants::max_u64(), + &mut test, + ); + + // Now test: Alice has 0 direct USDC, but has 20 USDC settled + // She should be able to place a market bid order + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager_alice = test.take_shared_by_id( + balance_manager_id_alice, + ); + let clock = clock::create_for_testing(test.ctx()); + + // Verify Alice has 0 direct USDC balance + let direct_usdc_balance = balance_manager_alice.balance(); + assert!(direct_usdc_balance == 0); + + // can_place_market_order should return true because of settled balances + // Market bid for 5 SUI (will need ~10 USDC, she has 20 settled) + let can_place = pool.can_place_market_order( + &balance_manager_alice, + 5 * constants::float_scaling(), // quantity: 5 SUI + true, // is_bid = true (buy) + true, // pay_with_deep + &clock, + ); + assert!(can_place); + + // Also verify that without enough settled balance, it would fail + // Market bid for 15 SUI (would need ~30 USDC, she only has 20 settled) + let can_place_too_much = pool.can_place_market_order( + &balance_manager_alice, + 15 * constants::float_scaling(), // quantity: 15 SUI + true, // is_bid = true (buy) + true, // pay_with_deep + &clock, + ); + assert!(!can_place_too_much); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager_alice); + }; + + end(test); +} diff --git a/packages/deepbook_margin/sources/helper/margin_constants.move b/packages/deepbook_margin/sources/helper/margin_constants.move index 6327cf4f..310f7153 100644 --- a/packages/deepbook_margin/sources/helper/margin_constants.move +++ b/packages/deepbook_margin/sources/helper/margin_constants.move @@ -18,6 +18,7 @@ const MAX_PROTOCOL_SPREAD: u64 = 200_000_000; // 20% const MIN_LIQUIDATION_REPAY: u64 = 1000; const MAX_CONF_BPS: u64 = 10_000; // 100% - maximum allowed confidence interval const MAX_EWMA_DIFFERENCE_BPS: u64 = 10_000; // 100% - maximum allowed EWMA price difference +const MAX_CONDITIONAL_ORDERS: u64 = 10; public fun margin_version(): u64 { MARGIN_VERSION @@ -75,6 +76,10 @@ public fun max_ewma_difference_bps(): u64 { MAX_EWMA_DIFFERENCE_BPS } +public fun max_conditional_orders(): u64 { + MAX_CONDITIONAL_ORDERS +} + public fun day_ms(): u64 { DAY_MS } diff --git a/packages/deepbook_margin/sources/helper/oracle.move b/packages/deepbook_margin/sources/helper/oracle.move index 760fda52..06b65d5b 100644 --- a/packages/deepbook_margin/sources/helper/oracle.move +++ b/packages/deepbook_margin/sources/helper/oracle.move @@ -4,6 +4,7 @@ /// Oracle module for margin trading. module deepbook_margin::oracle; +use deepbook::{constants, math}; use deepbook_margin::{margin_constants, margin_registry::MarginRegistry}; use pyth::{price_info::PriceInfoObject, pyth}; use std::type_name::{Self, TypeName}; @@ -16,6 +17,7 @@ const ECurrencyNotSupported: u64 = 2; const EPriceFeedIdMismatch: u64 = 3; const EInvalidPythPriceConf: u64 = 4; const EInvalidOracleConfig: u64 = 5; +const EInvalidPrice: u64 = 6; /// A buffer added to the exponent when doing currency conversions. const BUFFER: u8 = 10; @@ -122,7 +124,54 @@ public(package) fun calculate_usd_currency_amount( target_currency_amount } -// Calculates the amount in target currency based on amount in asset A. +/// Calculates the price of BaseAsset in QuoteAsset. +/// Returns the price accounting for the decimal difference between the two assets. +public(package) fun calculate_price( + registry: &MarginRegistry, + base_price_info_object: &PriceInfoObject, + quote_price_info_object: &PriceInfoObject, + clock: &Clock, +): u64 { + let base_decimals = get_decimals(registry); + let quote_decimals = get_decimals(registry); + + let base_amount = 10u64.pow(base_decimals); + let base_usd_price = calculate_usd_price( + base_price_info_object, + registry, + base_amount, + clock, + ); + + let quote_amount = 10u64.pow(quote_decimals); + let quote_usd_price = calculate_usd_price( + quote_price_info_object, + registry, + quote_amount, + clock, + ); + let price_ratio = math::div(base_usd_price, quote_usd_price); + + if (base_decimals > quote_decimals) { + let decimal_diff = base_decimals - quote_decimals; + let multiplier = 10u128.pow(decimal_diff); + let price = (price_ratio as u128) * multiplier; + assert!(price <= constants::max_price() as u128, EInvalidPrice); + + price as u64 + } else if (quote_decimals > base_decimals) { + let decimal_diff = quote_decimals - base_decimals; + let divisor = 10u128.pow(decimal_diff); + let price = price_ratio as u128 / divisor; + assert!(price <= constants::max_price() as u128, EInvalidPrice); + + price as u64 + } else { + price_ratio + } +} + +/// Calculates the amount in target currency based on amount in asset A. public(package) fun calculate_target_currency( registry: &MarginRegistry, price_info_object_a: &PriceInfoObject, @@ -285,6 +334,10 @@ fun get_config_for_type(registry: &MarginRegistry): CoinTypeData { *config.currencies.get(&payment_type) } +fun get_decimals(registry: &MarginRegistry): u8 { + registry.get_config_for_type().decimals +} + #[test_only] public fun test_conversion_config( target_decimals: u8, diff --git a/packages/deepbook_margin/sources/margin_manager.move b/packages/deepbook_margin/sources/margin_manager.move index fdbe6912..69d5f93d 100644 --- a/packages/deepbook_margin/sources/margin_manager.move +++ b/packages/deepbook_margin/sources/margin_manager.move @@ -15,6 +15,7 @@ use deepbook::{ }, constants, math, + order_info::OrderInfo, pool::Pool, registry::Registry }; @@ -22,7 +23,8 @@ use deepbook_margin::{ margin_constants, margin_pool::MarginPool, margin_registry::MarginRegistry, - oracle::{calculate_target_currency, get_pyth_price} + oracle::{calculate_target_currency, get_pyth_price, calculate_price}, + tpsl::{Self, TakeProfitStopLoss, PendingOrder, Condition, ConditionalOrder} }; use pyth::price_info::PriceInfoObject; use std::{string::String, type_name::{Self, TypeName}}; @@ -44,6 +46,7 @@ const EInvalidManagerForSharing: u64 = 11; const EInvalidDebtAsset: u64 = 12; const ERepayAmountTooLow: u64 = 13; const ERepaySharesTooLow: u64 = 14; +const EPoolNotEnabledForMarginTrading: u64 = 15; // === Structs === /// Witness type for authorizing MarginManager to call protected features of the DeepBook @@ -61,6 +64,7 @@ public struct MarginManager has key { trade_cap: TradeCap, borrowed_base_shares: u64, borrowed_quote_shares: u64, + take_profit_stop_loss: TakeProfitStopLoss, extra_fields: VecMap, } @@ -143,6 +147,164 @@ public struct WithdrawCollateralEvent has copy, drop { timestamp: u64, } +// === Functions - Take Profit Stop Loss === +/// Add a conditional order. +/// Specifies the conditions under which the order is triggered and the pending order to be placed. +public fun add_conditional_order( + self: &mut MarginManager, + pool: &Pool, + base_price_info_object: &PriceInfoObject, + quote_price_info_object: &PriceInfoObject, + registry: &MarginRegistry, + conditional_order_id: u64, + condition: Condition, + pending_order: PendingOrder, + clock: &Clock, + ctx: &mut TxContext, +) { + self.validate_owner(ctx); + let manager_id = self.id(); + assert!(pool.id() == self.deepbook_pool(), EIncorrectDeepBookPool); + self + .take_profit_stop_loss + .add_conditional_order( + pool, + manager_id, + base_price_info_object, + quote_price_info_object, + registry, + conditional_order_id, + condition, + pending_order, + clock, + ); +} + +/// Cancel all conditional orders. +public fun cancel_all_conditional_orders( + self: &mut MarginManager, + clock: &Clock, + ctx: &TxContext, +) { + self.validate_owner(ctx); + let manager_id = self.id(); + let identifiers = self.take_profit_stop_loss.conditional_orders().keys(); + identifiers.do!(|identifier| { + self.take_profit_stop_loss.cancel_conditional_order(manager_id, identifier, clock); + }); +} + +/// Cancel a conditional order. +public fun cancel_conditional_order( + self: &mut MarginManager, + conditional_order_id: u64, + clock: &Clock, + ctx: &TxContext, +) { + self.validate_owner(ctx); + let manager_id = self.id(); + self.take_profit_stop_loss.cancel_conditional_order(manager_id, conditional_order_id, clock); +} + +/// Execute conditional orders and return the order infos. +/// This is a permissionless function that can be called by anyone. +public fun execute_conditional_orders( + self: &mut MarginManager, + pool: &mut Pool, + base_price_info_object: &PriceInfoObject, + quote_price_info_object: &PriceInfoObject, + registry: &MarginRegistry, + max_orders_to_execute: u64, + clock: &Clock, + ctx: &TxContext, +): vector { + assert!(pool.id() == self.deepbook_pool(), EIncorrectDeepBookPool); + let current_price = calculate_price( + registry, + base_price_info_object, + quote_price_info_object, + clock, + ); + + let mut order_infos = vector[]; + let mut executed_identifiers = vector[]; + let mut expired_identifiers = vector[]; + let mut insufficient_funds_identifiers = vector[]; + + let keys = self.take_profit_stop_loss.conditional_orders().keys(); + keys.do!(|identifier| { + if (order_infos.length() >= max_orders_to_execute) return; + let conditional_order = self.conditional_order(identifier); + let condition = conditional_order.condition(); + + let triggered = + (condition.trigger_below_price() && current_price < condition.trigger_price()) || + (!condition.trigger_below_price() && current_price > condition.trigger_price()); + + if (!triggered) return; + + let pending_order = conditional_order.pending_order(); + let can_place = if (pending_order.is_limit_order()) { + pool.can_place_limit_order( + self.balance_manager(), + pending_order.price().destroy_some(), + pending_order.quantity(), + pending_order.is_bid(), + pending_order.pay_with_deep(), + pending_order.expire_timestamp().destroy_some(), + clock, + ) + } else { + pool.can_place_market_order( + self.balance_manager(), + pending_order.quantity(), + pending_order.is_bid(), + pending_order.pay_with_deep(), + clock, + ) + }; + + if (can_place) { + let order_info = self.place_pending_order( + registry, + pool, + &pending_order, + clock, + ctx, + ); + order_infos.push_back(order_info); + executed_identifiers.push_back(identifier); + } else { + if (pending_order.is_limit_order()) { + let expire_timestamp = *pending_order.expire_timestamp().borrow(); + if (expire_timestamp <= clock.timestamp_ms()) { + expired_identifiers.push_back(identifier); + } else { + insufficient_funds_identifiers.push_back(identifier); + } + } else { + insufficient_funds_identifiers.push_back(identifier); + } + } + }); + + let manager_id = self.id(); + insufficient_funds_identifiers.do!(|id| { + self.take_profit_stop_loss.emit_insufficient_funds_event(manager_id, id, clock); + self.take_profit_stop_loss.cancel_conditional_order(manager_id, id, clock); + }); + expired_identifiers.do!(|id| { + self.take_profit_stop_loss.cancel_conditional_order(manager_id, id, clock); + }); + executed_identifiers.do!(|id| { + self + .take_profit_stop_loss + .remove_executed_conditional_order(manager_id, pool.id(), id, clock) + }); + + order_infos +} + // === Public Functions - Margin Manager === /// Creates a new margin manager and shares it. public fun new( @@ -871,6 +1033,19 @@ public fun has_base_debt(self: &MarginManager 0 } +public fun conditional_order_ids( + self: &MarginManager, +): vector { + self.take_profit_stop_loss.conditional_orders().keys() +} + +public fun conditional_order( + self: &MarginManager, + conditional_order_id: u64, +): ConditionalOrder { + *self.take_profit_stop_loss.get_conditional_order(&conditional_order_id) +} + // === Public-Package Functions === /// Unwraps balance manager for trading in deepbook. public(package) fun balance_manager_trading_mut( @@ -882,6 +1057,12 @@ public(package) fun balance_manager_trading_mut( &mut self.balance_manager } +public(package) fun balance_manager_unsafe_mut( + self: &mut MarginManager, +): &mut BalanceManager { + &mut self.balance_manager +} + /// Withdraws settled amounts from the pool permissionlessly. /// Anyone can call this via the pool_proxy to settle balances. public(package) fun withdraw_settled_amounts_permissionless_int( @@ -982,7 +1163,7 @@ fun new_margin_manager( event::emit(MarginManagerCreatedEvent { margin_manager_id, - balance_manager_id: object::id(&balance_manager), + balance_manager_id: balance_manager.id(), deepbook_pool_id: pool.id(), owner, timestamp: clock.timestamp_ms(), @@ -999,6 +1180,7 @@ fun new_margin_manager( trade_cap, borrowed_base_shares: 0, borrowed_quote_shares: 0, + take_profit_stop_loss: tpsl::new(), extra_fields: vec_map::empty(), } } @@ -1113,3 +1295,110 @@ fun assets_in_debt_unit( }; (assets_in_debt_unit, base_asset, quote_asset) } + +fun place_pending_order( + self: &mut MarginManager, + registry: &MarginRegistry, + pool: &mut Pool, + pending_order: &PendingOrder, + clock: &Clock, + ctx: &TxContext, +): OrderInfo { + if (pending_order.is_limit_order()) { + self.place_pending_limit_order( + registry, + pool, + pending_order.client_order_id(), + pending_order.order_type().destroy_some(), + pending_order.self_matching_option(), + pending_order.price().destroy_some(), + pending_order.quantity(), + pending_order.is_bid(), + pending_order.pay_with_deep(), + pending_order.expire_timestamp().destroy_some(), + clock, + ctx, + ) + } else { + self.place_market_order_conditional( + registry, + pool, + pending_order.client_order_id(), + pending_order.self_matching_option(), + pending_order.quantity(), + pending_order.is_bid(), + pending_order.pay_with_deep(), + clock, + ctx, + ) + } +} + +/// Only used for tpsl pending orders. +fun place_pending_limit_order( + self: &mut MarginManager, + registry: &MarginRegistry, + pool: &mut Pool, + client_order_id: u64, + order_type: u8, + self_matching_option: u8, + price: u64, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + expire_timestamp: u64, + clock: &Clock, + ctx: &TxContext, +): OrderInfo { + assert!(self.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = self.trade_proof(ctx); + let balance_manager = self.balance_manager_unsafe_mut(); + assert!(registry.pool_enabled(pool), EPoolNotEnabledForMarginTrading); + + pool.place_limit_order( + balance_manager, + &trade_proof, + client_order_id, + order_type, + self_matching_option, + price, + quantity, + is_bid, + pay_with_deep, + expire_timestamp, + clock, + ctx, + ) +} + +/// Places a market order in the pool. +/// Only used for tpsl pending orders. +fun place_market_order_conditional( + self: &mut MarginManager, + registry: &MarginRegistry, + pool: &mut Pool, + client_order_id: u64, + self_matching_option: u8, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + clock: &Clock, + ctx: &TxContext, +): OrderInfo { + assert!(self.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = self.trade_proof(ctx); + let balance_manager = self.balance_manager_unsafe_mut(); + assert!(registry.pool_enabled(pool), EPoolNotEnabledForMarginTrading); + + pool.place_market_order( + balance_manager, + &trade_proof, + client_order_id, + self_matching_option, + quantity, + is_bid, + pay_with_deep, + clock, + ctx, + ) +} diff --git a/packages/deepbook_margin/sources/tpsl.move b/packages/deepbook_margin/sources/tpsl.move new file mode 100644 index 00000000..ede02986 --- /dev/null +++ b/packages/deepbook_margin/sources/tpsl.move @@ -0,0 +1,340 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module deepbook_margin::tpsl; + +use deepbook::{constants, pool::Pool}; +use deepbook_margin::{margin_constants, margin_registry::MarginRegistry, oracle::calculate_price}; +use pyth::price_info::PriceInfoObject; +use sui::{clock::Clock, event, vec_map::{Self, VecMap}}; + +// === Errors === +const EInvalidCondition: u64 = 1; +const EConditionalOrderNotFound: u64 = 2; +const EMaxConditionalOrdersReached: u64 = 3; +const EInvalidTPSLOrderType: u64 = 4; +const EDuplicateConditionalOrderIdentifier: u64 = 5; +const EInvalidOrderParams: u64 = 6; + +// === Structs === +public struct TakeProfitStopLoss has drop, store { + conditional_orders: VecMap, +} + +public struct ConditionalOrder has copy, drop, store { + condition: Condition, + pending_order: PendingOrder, +} + +public struct Condition has copy, drop, store { + trigger_below_price: bool, + trigger_price: u64, +} + +public struct PendingOrder has copy, drop, store { + is_limit_order: bool, + client_order_id: u64, + order_type: Option, + self_matching_option: u8, + price: Option, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + expire_timestamp: Option, +} + +// === Events === +public struct ConditionalOrderAdded has copy, drop { + manager_id: ID, + conditional_order_id: u64, + conditional_order: ConditionalOrder, + timestamp: u64, +} + +public struct ConditionalOrderCancelled has copy, drop { + manager_id: ID, + conditional_order_id: u64, + conditional_order: ConditionalOrder, + timestamp: u64, +} + +public struct ConditionalOrderExecuted has copy, drop { + manager_id: ID, + pool_id: ID, + conditional_order_id: u64, + conditional_order: ConditionalOrder, + timestamp: u64, +} + +public struct ConditionalOrderInsufficientFunds has copy, drop { + manager_id: ID, + conditional_order_id: u64, + conditional_order: ConditionalOrder, + timestamp: u64, +} + +// === Public Functions === +public fun new_condition(trigger_below_price: bool, trigger_price: u64): Condition { + Condition { + trigger_below_price, + trigger_price, + } +} + +/// Creates a new pending limit order. +/// Order type must be no restriction or immediate or cancel. +public fun new_pending_limit_order( + client_order_id: u64, + order_type: u8, + self_matching_option: u8, + price: u64, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + expire_timestamp: u64, +): PendingOrder { + assert!( + order_type == constants::no_restriction() || order_type == constants::immediate_or_cancel(), + EInvalidTPSLOrderType, + ); + PendingOrder { + is_limit_order: true, + client_order_id, + order_type: option::some(order_type), + self_matching_option, + price: option::some(price), + quantity, + is_bid, + pay_with_deep, + expire_timestamp: option::some(expire_timestamp), + } +} + +public fun new_pending_market_order( + client_order_id: u64, + self_matching_option: u8, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, +): PendingOrder { + PendingOrder { + is_limit_order: false, + client_order_id, + order_type: option::none(), + self_matching_option, + price: option::none(), + quantity, + is_bid, + pay_with_deep, + expire_timestamp: option::none(), + } +} + +// === Read-Only Functions === +public fun conditional_orders(self: &TakeProfitStopLoss): &VecMap { + &self.conditional_orders +} + +public fun get_conditional_order( + self: &TakeProfitStopLoss, + conditional_order_id: &u64, +): &ConditionalOrder { + self.conditional_orders.get(conditional_order_id) +} + +public fun condition(conditional_order: &ConditionalOrder): Condition { + conditional_order.condition +} + +public fun pending_order(conditional_order: &ConditionalOrder): PendingOrder { + conditional_order.pending_order +} + +public fun trigger_below_price(condition: &Condition): bool { + condition.trigger_below_price +} + +public fun trigger_price(condition: &Condition): u64 { + condition.trigger_price +} + +public fun client_order_id(pending_order: &PendingOrder): u64 { + pending_order.client_order_id +} + +public fun order_type(pending_order: &PendingOrder): Option { + pending_order.order_type +} + +public fun self_matching_option(pending_order: &PendingOrder): u8 { + pending_order.self_matching_option +} + +public fun price(pending_order: &PendingOrder): Option { + pending_order.price +} + +public fun quantity(pending_order: &PendingOrder): u64 { + pending_order.quantity +} + +public fun is_bid(pending_order: &PendingOrder): bool { + pending_order.is_bid +} + +public fun pay_with_deep(pending_order: &PendingOrder): bool { + pending_order.pay_with_deep +} + +public fun expire_timestamp(pending_order: &PendingOrder): Option { + pending_order.expire_timestamp +} + +public fun is_limit_order(pending_order: &PendingOrder): bool { + pending_order.is_limit_order +} + +// === public(package) functions === +public(package) fun new(): TakeProfitStopLoss { + TakeProfitStopLoss { + conditional_orders: vec_map::empty(), + } +} + +public(package) fun add_conditional_order( + self: &mut TakeProfitStopLoss, + pool: &Pool, + manager_id: ID, + base_price_info_object: &PriceInfoObject, + quote_price_info_object: &PriceInfoObject, + registry: &MarginRegistry, + conditional_order_id: u64, + condition: Condition, + pending_order: PendingOrder, + clock: &Clock, +) { + if (pending_order.is_limit_order()) { + let price = *pending_order.price.borrow(); + let expire_timestamp = *pending_order.expire_timestamp.borrow(); + assert!( + pool.check_limit_order_params(price, pending_order.quantity, expire_timestamp, clock), + EInvalidOrderParams, + ); + } else { + assert!(pool.check_market_order_params(pending_order.quantity), EInvalidOrderParams); + }; + let current_price = calculate_price( + registry, + base_price_info_object, + quote_price_info_object, + clock, + ); + + let trigger_below_price = condition.trigger_below_price; + let trigger_price = condition.trigger_price; + + // If order is triggered below trigger_price, trigger_price must be lower than current price + // If order is triggered above trigger_price, trigger_price must be higher than current price + assert!( + (trigger_below_price && trigger_price < current_price) || + (!trigger_below_price && trigger_price > current_price), + EInvalidCondition, + ); + + assert!( + self.conditional_orders.length() < margin_constants::max_conditional_orders(), + EMaxConditionalOrdersReached, + ); + assert!( + !self.conditional_orders.contains(&conditional_order_id), + EDuplicateConditionalOrderIdentifier, + ); + + let conditional_order = ConditionalOrder { + condition, + pending_order, + }; + self.conditional_orders.insert(conditional_order_id, conditional_order); + + event::emit(ConditionalOrderAdded { + manager_id, + conditional_order_id, + conditional_order, + timestamp: clock.timestamp_ms(), + }); +} + +public(package) fun cancel_conditional_order( + self: &mut TakeProfitStopLoss, + manager_id: ID, + conditional_order_id: u64, + clock: &Clock, +) { + self.remove_conditional_order( + manager_id, + option::none(), + conditional_order_id, + true, + clock, + ); +} + +public(package) fun remove_executed_conditional_order( + self: &mut TakeProfitStopLoss, + manager_id: ID, + pool_id: ID, + conditional_order_id: u64, + clock: &Clock, +) { + self.remove_conditional_order( + manager_id, + option::some(pool_id), + conditional_order_id, + false, + clock, + ); +} + +public(package) fun remove_conditional_order( + self: &mut TakeProfitStopLoss, + manager_id: ID, + pool_id: Option, + conditional_order_id: u64, + is_cancel: bool, + clock: &Clock, +) { + assert!(self.conditional_orders.contains(&conditional_order_id), EConditionalOrderNotFound); + let (_, conditional_order) = self.conditional_orders.remove(&conditional_order_id); + + if (is_cancel) { + event::emit(ConditionalOrderCancelled { + manager_id, + conditional_order_id, + conditional_order, + timestamp: clock.timestamp_ms(), + }); + } else { + event::emit(ConditionalOrderExecuted { + manager_id, + pool_id: pool_id.destroy_some(), + conditional_order_id, + conditional_order, + timestamp: clock.timestamp_ms(), + }); + }; +} + +public(package) fun emit_insufficient_funds_event( + self: &TakeProfitStopLoss, + manager_id: ID, + conditional_order_id: u64, + clock: &Clock, +) { + let conditional_order = *self.get_conditional_order(&conditional_order_id); + event::emit(ConditionalOrderInsufficientFunds { + manager_id, + conditional_order_id, + conditional_order, + timestamp: clock.timestamp_ms(), + }); +} diff --git a/packages/deepbook_margin/tests/tpsl_tests.move b/packages/deepbook_margin/tests/tpsl_tests.move new file mode 100644 index 00000000..3c2bcdc1 --- /dev/null +++ b/packages/deepbook_margin/tests/tpsl_tests.move @@ -0,0 +1,961 @@ +// // Copyright (c) Mysten Labs, Inc. +// // SPDX-License-Identifier: Apache-2.0 + +// #[test_only] +// module deepbook_margin::tpsl_tests; + +// use deepbook::balance_manager; +// use deepbook::constants; +// use deepbook::pool::Pool; +// use deepbook::registry::Registry; +// use deepbook_margin::margin_manager::{Self, MarginManager}; +// use deepbook_margin::margin_registry::MarginRegistry; +// use deepbook_margin::test_constants::{Self, USDC, USDT}; +// use deepbook_margin::test_helpers::{ +// setup_usdc_usdt_deepbook_margin, +// cleanup_margin_test, +// mint_coin, +// build_demo_usdc_price_info_object, +// build_pyth_price_info_object, +// destroy_2, +// return_shared_2 +// }; +// use deepbook_margin::tpsl; +// use std::unit_test::destroy; +// use sui::test_scenario::return_shared; +// use token::deep::DEEP; + +// // Helper to create a balance manager and place orders on the pool for liquidity +// fun setup_pool_liquidity( +// scenario: &mut sui::test_scenario::Scenario, +// pool_id: ID, +// clock: &sui::clock::Clock, +// base_amount: u64, +// quote_amount: u64, +// ): ID { +// scenario.next_tx(test_constants::user2()); +// let mut pool = scenario.take_shared_by_id>(pool_id); +// let mut balance_manager = balance_manager::new(scenario.ctx()); + +// // Deposit base and quote assets +// balance_manager.deposit(mint_coin(base_amount, scenario.ctx()), scenario.ctx()); +// balance_manager.deposit(mint_coin(quote_amount, scenario.ctx()), scenario.ctx()); +// balance_manager.deposit( +// mint_coin(1000 * test_constants::deep_multiplier(), scenario.ctx()), +// scenario.ctx(), +// ); + +// // Generate trade proof before sharing +// let trade_proof = balance_manager.generate_proof_as_owner(scenario.ctx()); + +// // Place ask order (sell base for quote) +// pool.place_limit_order( +// &mut balance_manager, +// &trade_proof, +// 1, +// constants::no_restriction(), +// constants::self_matching_allowed(), +// 2 * constants::float_scaling(), // price: 2 +// base_amount / 2, // quantity +// false, // is_bid +// false, // pay_with_deep = false +// constants::max_u64(), +// clock, +// scenario.ctx(), +// ); + +// // Place bid order (buy base with quote) +// pool.place_limit_order( +// &mut balance_manager, +// &trade_proof, +// 2, +// constants::no_restriction(), +// constants::self_matching_allowed(), +// 1 * constants::float_scaling(), // price: 1 +// base_amount / 2, // quantity +// true, // is_bid +// false, // pay_with_deep = false +// constants::max_u64(), +// clock, +// scenario.ctx(), +// ); + +// let balance_manager_id = balance_manager.id(); +// transfer::public_share_object(balance_manager); + +// return_shared(pool); +// balance_manager_id +// } + +// // Helper to build price info objects with specific prices +// fun build_usdt_price_info_object_with_price( +// scenario: &mut sui::test_scenario::Scenario, +// price_usd: u64, +// clock: &sui::clock::Clock, +// ): pyth::price_info::PriceInfoObject { +// build_pyth_price_info_object( +// scenario, +// test_constants::usdt_price_feed_id(), +// price_usd * test_constants::pyth_multiplier(), +// 50000, +// test_constants::pyth_decimals(), +// clock.timestamp_ms() / 1000, +// ) +// } + +// #[test] +// fun test_tpsl_trigger_below_limit_order_executed() { +// let ( +// mut scenario, +// clock, +// admin_cap, +// maintainer_cap, +// _usdc_pool_id, +// _usdt_pool_id, +// pool_id, +// registry_id, +// ) = setup_usdc_usdt_deepbook_margin(); + +// // Set up pool liquidity from another user +// setup_pool_liquidity( +// &mut scenario, +// pool_id, +// &clock, +// 10000 * test_constants::usdt_multiplier(), +// 20000 * test_constants::usdc_multiplier(), +// ); + +// scenario.next_tx(test_constants::user1()); +// let mut margin_registry = scenario.take_shared(); +// let pool = scenario.take_shared>(); +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// margin_manager::new( +// &pool, +// &deepbook_registry, +// &mut margin_registry, +// &clock, +// scenario.ctx(), +// ); +// return_shared(deepbook_registry); +// return_shared(pool); + +// scenario.next_tx(test_constants::user1()); +// let mut mm = scenario.take_shared>(); + +// // Initial prices: USDT = $1.00, USDC = $1.00, so price = 1.0 (in float_scaling) +// let usdt_price_high = build_usdt_price_info_object_with_price(&mut scenario, 1, &clock); // $1.00 +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); // $1.00 + +// // Deposit collateral +// mm.deposit( +// &margin_registry, +// &usdt_price_high, +// &usdc_price, +// mint_coin(10000 * test_constants::usdt_multiplier(), scenario.ctx()), +// &clock, +// scenario.ctx(), +// ); + +// // Current price is 1.0 (1 USDT = 1 USDC) +// // Add conditional order: trigger_is_below = true, trigger_price = 0.8 +// // This means: trigger when price drops below 0.8 +// // Condition: trigger_price (0.8) < current_price (1.0) ✓ +// let condition = tpsl::new_condition(true, (8 * constants::float_scaling()) / 10); // 0.8 +// let pending_order = tpsl::new_pending_limit_order( +// 1, // client_order_id +// constants::no_restriction(), +// constants::self_matching_allowed(), +// 1 * constants::float_scaling(), // price +// 100 * test_constants::usdt_multiplier(), // quantity +// true, // is_bid (buy USDT with USDC) +// false, // pay_with_deep +// constants::max_u64(), // expire_timestamp +// ); + +// mm.add_conditional_order( +// &usdt_price_high, +// &usdc_price, +// &margin_registry, +// 1, // conditional_order_identifier +// condition, +// pending_order, +// &clock, +// ); + +// // Verify conditional order was added +// assert!(margin_manager::conditional_orders(&mm).length() == 1); + +// destroy_2!(usdt_price_high, usdc_price); +// return_shared(margin_registry); + +// // Update price to trigger: USDT drops to $0.75, so price = 0.75 < 0.8 trigger +// scenario.next_tx(test_constants::admin()); +// let usdt_price_low = build_usdt_price_info_object_with_price(&mut scenario, 75, &clock); // $0.75 +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// scenario.next_tx(test_constants::user1()); +// let mut pool = scenario.take_shared>(); +// let margin_registry = scenario.take_shared(); + +// // Execute pending orders - should trigger and place order +// let order_infos = mm.execute_pending_orders( +// &mut pool, +// &usdt_price_low, +// &usdc_price, +// &margin_registry, +// &clock, +// scenario.ctx(), +// ); + +// // Verify order was executed +// assert!(order_infos.length() == 1); +// destroy(order_infos[0]); + +// // Verify conditional order was removed +// assert!(margin_manager::conditional_orders(&mm).length() == 0); + +// destroy_2!(usdt_price_low, usdc_price); +// return_shared_2!(mm, pool); + +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// return_shared(deepbook_registry); +// cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +// } + +// #[test] +// fun test_tpsl_trigger_above_limit_order_executed() { +// let ( +// mut scenario, +// clock, +// admin_cap, +// maintainer_cap, +// _usdc_pool_id, +// _usdt_pool_id, +// pool_id, +// registry_id, +// ) = setup_usdc_usdt_deepbook_margin(); + +// setup_pool_liquidity( +// &mut scenario, +// pool_id, +// &clock, +// 10000 * test_constants::usdt_multiplier(), +// 20000 * test_constants::usdc_multiplier(), +// ); + +// scenario.next_tx(test_constants::user1()); +// let mut margin_registry = scenario.take_shared(); +// let pool = scenario.take_shared>(); +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// margin_manager::new( +// &pool, +// &deepbook_registry, +// &mut margin_registry, +// &clock, +// scenario.ctx(), +// ); +// return_shared(deepbook_registry); +// return_shared(pool); + +// scenario.next_tx(test_constants::user1()); +// let mut mm = scenario.take_shared>(); + +// // Initial prices: USDT = $1.00, USDC = $1.00, so price = 1.0 +// let usdt_price_low = build_usdt_price_info_object_with_price(&mut scenario, 1, &clock); // $1.00 +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); // $1.00 + +// mm.deposit( +// &margin_registry, +// &usdt_price_low, +// &usdc_price, +// mint_coin(10000 * test_constants::usdt_multiplier(), scenario.ctx()), +// &clock, +// scenario.ctx(), +// ); + +// // Current price is 1.0 +// // Add conditional order: trigger_is_below = false, trigger_price = 1.2 +// // This means: trigger when price rises above 1.2 +// // Condition: trigger_price (1.2) > current_price (1.0) ✓ +// let condition = tpsl::new_condition(false, (12 * constants::float_scaling()) / 10); // 1.2 +// let pending_order = tpsl::new_pending_limit_order( +// 1, +// constants::no_restriction(), +// constants::self_matching_allowed(), +// 1 * constants::float_scaling(), +// 100 * test_constants::usdt_multiplier(), +// false, // is_bid (sell USDT for USDC) +// false, +// constants::max_u64(), +// ); + +// mm.add_conditional_order( +// &usdt_price_low, +// &usdc_price, +// &margin_registry, +// 1, +// condition, +// pending_order, +// &clock, +// ); + +// assert!(margin_manager::conditional_orders(&mm).length() == 1); + +// destroy_2!(usdt_price_low, usdc_price); +// return_shared(margin_registry); + +// // Update price to trigger: USDT rises to $1.25, so price = 1.25 > 1.2 trigger +// scenario.next_tx(test_constants::admin()); +// let usdt_price_high = build_usdt_price_info_object_with_price(&mut scenario, 125, &clock); // $1.25 +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// scenario.next_tx(test_constants::user1()); +// let mut pool = scenario.take_shared>(); +// let margin_registry = scenario.take_shared(); + +// let order_infos = mm.execute_pending_orders( +// &mut pool, +// &usdt_price_high, +// &usdc_price, +// &margin_registry, +// &clock, +// scenario.ctx(), +// ); + +// assert!(order_infos.length() == 1); +// destroy(order_infos[0]); +// assert!(margin_manager::conditional_orders(&mm).length() == 0); + +// destroy_2!(usdt_price_high, usdc_price); +// return_shared_2!(mm, pool); + +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// return_shared(deepbook_registry); +// cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +// } + +// #[test] +// fun test_tpsl_trigger_below_market_order_executed() { +// let ( +// mut scenario, +// clock, +// admin_cap, +// maintainer_cap, +// _usdc_pool_id, +// _usdt_pool_id, +// pool_id, +// registry_id, +// ) = setup_usdc_usdt_deepbook_margin(); + +// setup_pool_liquidity( +// &mut scenario, +// pool_id, +// &clock, +// 10000 * test_constants::usdt_multiplier(), +// 20000 * test_constants::usdc_multiplier(), +// ); + +// scenario.next_tx(test_constants::user1()); +// let mut margin_registry = scenario.take_shared(); +// let pool = scenario.take_shared>(); +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// margin_manager::new( +// &pool, +// &deepbook_registry, +// &mut margin_registry, +// &clock, +// scenario.ctx(), +// ); +// return_shared(deepbook_registry); +// return_shared(pool); + +// scenario.next_tx(test_constants::user1()); +// let mut mm = scenario.take_shared>(); + +// let usdt_price_high = build_usdt_price_info_object_with_price(&mut scenario, 1, &clock); +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// mm.deposit( +// &margin_registry, +// &usdt_price_high, +// &usdc_price, +// mint_coin(10000 * test_constants::usdt_multiplier(), scenario.ctx()), +// &clock, +// scenario.ctx(), +// ); + +// let condition = tpsl::new_condition(true, (8 * constants::float_scaling()) / 10); +// let pending_order = tpsl::new_pending_market_order( +// 1, +// constants::self_matching_allowed(), +// 100 * test_constants::usdt_multiplier(), +// true, // is_bid +// false, +// ); + +// mm.add_conditional_order( +// &usdt_price_high, +// &usdc_price, +// &margin_registry, +// 1, +// condition, +// pending_order, +// &clock, +// ); + +// assert!(margin_manager::conditional_orders(&mm).length() == 1); + +// destroy_2!(usdt_price_high, usdc_price); +// return_shared(margin_registry); + +// scenario.next_tx(test_constants::admin()); +// let usdt_price_low = build_usdt_price_info_object_with_price(&mut scenario, 75, &clock); +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// scenario.next_tx(test_constants::user1()); +// let mut pool = scenario.take_shared>(); +// let margin_registry = scenario.take_shared(); + +// let order_infos = mm.execute_pending_orders( +// &mut pool, +// &usdt_price_low, +// &usdc_price, +// &margin_registry, +// &clock, +// scenario.ctx(), +// ); + +// assert!(order_infos.length() == 1); +// destroy(order_infos[0]); +// assert!(margin_manager::conditional_orders(&mm).length() == 0); + +// destroy_2!(usdt_price_low, usdc_price); +// return_shared_2!(mm, pool); + +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// return_shared(deepbook_registry); +// cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +// } + +// #[test] +// fun test_tpsl_trigger_above_market_order_executed() { +// let ( +// mut scenario, +// clock, +// admin_cap, +// maintainer_cap, +// _usdc_pool_id, +// _usdt_pool_id, +// pool_id, +// registry_id, +// ) = setup_usdc_usdt_deepbook_margin(); + +// setup_pool_liquidity( +// &mut scenario, +// pool_id, +// &clock, +// 10000 * test_constants::usdt_multiplier(), +// 20000 * test_constants::usdc_multiplier(), +// ); + +// scenario.next_tx(test_constants::user1()); +// let mut margin_registry = scenario.take_shared(); +// let pool = scenario.take_shared>(); +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// margin_manager::new( +// &pool, +// &deepbook_registry, +// &mut margin_registry, +// &clock, +// scenario.ctx(), +// ); +// return_shared(deepbook_registry); +// return_shared(margin_registry); +// return_shared(pool); + +// scenario.next_tx(test_constants::user1()); +// let mut mm = scenario.take_shared>(); + +// let margin_registry = scenario.take_shared(); +// let usdt_price_low = build_usdt_price_info_object_with_price(&mut scenario, 1, &clock); +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// mm.deposit( +// &margin_registry, +// &usdt_price_low, +// &usdc_price, +// mint_coin(10000 * test_constants::usdt_multiplier(), scenario.ctx()), +// &clock, +// scenario.ctx(), +// ); + +// let condition = tpsl::new_condition(false, (12 * constants::float_scaling()) / 10); +// let pending_order = tpsl::new_pending_market_order( +// 1, +// constants::self_matching_allowed(), +// 100 * test_constants::usdt_multiplier(), +// false, // is_bid +// false, +// ); + +// mm.add_conditional_order( +// &usdt_price_low, +// &usdc_price, +// &margin_registry, +// 1, +// condition, +// pending_order, +// &clock, +// ); + +// assert!(margin_manager::conditional_orders(&mm).length() == 1); +// return_shared(margin_registry); + +// destroy_2!(usdt_price_low, usdc_price); + +// scenario.next_tx(test_constants::admin()); +// let usdt_price_high = build_usdt_price_info_object_with_price(&mut scenario, 125, &clock); +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// scenario.next_tx(test_constants::user1()); +// let mut pool = scenario.take_shared>(); +// let margin_registry = scenario.take_shared(); + +// let order_infos = mm.execute_pending_orders( +// &mut pool, +// &usdt_price_high, +// &usdc_price, +// &margin_registry, +// &clock, +// scenario.ctx(), +// ); + +// assert!(order_infos.length() == 1); +// destroy(order_infos[0]); +// assert!(margin_manager::conditional_orders(&mm).length() == 0); + +// destroy_2!(usdt_price_high, usdc_price); +// return_shared_2!(mm, pool); + +// cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +// } + +// #[test] +// fun test_tpsl_trigger_below_limit_order_insufficient_balance() { +// let ( +// mut scenario, +// clock, +// admin_cap, +// maintainer_cap, +// _usdc_pool_id, +// _usdt_pool_id, +// pool_id, +// registry_id, +// ) = setup_usdc_usdt_deepbook_margin(); + +// setup_pool_liquidity( +// &mut scenario, +// pool_id, +// &clock, +// 10000 * test_constants::usdt_multiplier(), +// 20000 * test_constants::usdc_multiplier(), +// ); + +// scenario.next_tx(test_constants::user1()); +// let mut margin_registry = scenario.take_shared(); +// let pool = scenario.take_shared>(); +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// margin_manager::new( +// &pool, +// &deepbook_registry, +// &mut margin_registry, +// &clock, +// scenario.ctx(), +// ); +// return_shared(deepbook_registry); +// return_shared(pool); + +// scenario.next_tx(test_constants::user1()); +// let mut mm = scenario.take_shared>(); + +// let usdt_price_high = build_usdt_price_info_object_with_price(&mut scenario, 1, &clock); +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// // Deposit minimal collateral - not enough for the order +// mm.deposit( +// &margin_registry, +// &usdt_price_high, +// &usdc_price, +// mint_coin(100 * test_constants::usdt_multiplier(), scenario.ctx()), // Small amount +// &clock, +// scenario.ctx(), +// ); + +// let condition = tpsl::new_condition(true, (8 * constants::float_scaling()) / 10); +// let pending_order = tpsl::new_pending_limit_order( +// 1, +// constants::no_restriction(), +// constants::self_matching_allowed(), +// 1 * constants::float_scaling(), +// 10000 * test_constants::usdt_multiplier(), // Large quantity - more than available +// true, +// false, +// constants::max_u64(), +// ); + +// mm.add_conditional_order( +// &usdt_price_high, +// &usdc_price, +// &margin_registry, +// 1, +// condition, +// pending_order, +// &clock, +// ); + +// assert!(margin_manager::conditional_orders(&mm).length() == 1); + +// destroy_2!(usdt_price_high, usdc_price); +// return_shared(margin_registry); + +// // Trigger condition +// scenario.next_tx(test_constants::admin()); +// let usdt_price_low = build_usdt_price_info_object_with_price(&mut scenario, 75, &clock); +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// scenario.next_tx(test_constants::user1()); +// let mut pool = scenario.take_shared>(); +// let margin_registry = scenario.take_shared(); + +// // Execute pending orders - should not place order due to insufficient balance +// let order_infos = mm.execute_pending_orders( +// &mut pool, +// &usdt_price_low, +// &usdc_price, +// &margin_registry, +// &clock, +// scenario.ctx(), +// ); + +// // No orders executed +// assert!(order_infos.length() == 0); + +// // Conditional order should remain +// assert!(margin_manager::conditional_orders(&mm).length() == 1); + +// destroy_2!(usdt_price_low, usdc_price); +// return_shared_2!(mm, pool); + +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// return_shared(deepbook_registry); +// cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +// } + +// #[test] +// fun test_tpsl_trigger_above_limit_order_insufficient_balance() { +// let ( +// mut scenario, +// clock, +// admin_cap, +// maintainer_cap, +// _usdc_pool_id, +// _usdt_pool_id, +// pool_id, +// registry_id, +// ) = setup_usdc_usdt_deepbook_margin(); + +// setup_pool_liquidity( +// &mut scenario, +// pool_id, +// &clock, +// 10000 * test_constants::usdt_multiplier(), +// 20000 * test_constants::usdc_multiplier(), +// ); + +// scenario.next_tx(test_constants::user1()); +// let mut margin_registry = scenario.take_shared(); +// let pool = scenario.take_shared>(); +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// margin_manager::new( +// &pool, +// &deepbook_registry, +// &mut margin_registry, +// &clock, +// scenario.ctx(), +// ); +// return_shared(deepbook_registry); +// return_shared(pool); + +// scenario.next_tx(test_constants::user1()); +// let mut mm = scenario.take_shared>(); + +// let usdt_price_low = build_usdt_price_info_object_with_price(&mut scenario, 1, &clock); +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// mm.deposit( +// &margin_registry, +// &usdt_price_low, +// &usdc_price, +// mint_coin(100 * test_constants::usdt_multiplier(), scenario.ctx()), +// &clock, +// scenario.ctx(), +// ); + +// let condition = tpsl::new_condition(false, (12 * constants::float_scaling()) / 10); +// let pending_order = tpsl::new_pending_limit_order( +// 1, +// constants::no_restriction(), +// constants::self_matching_allowed(), +// 1 * constants::float_scaling(), +// 10000 * test_constants::usdt_multiplier(), // Large quantity +// false, +// false, +// constants::max_u64(), +// ); + +// mm.add_conditional_order( +// &usdt_price_low, +// &usdc_price, +// &margin_registry, +// 1, +// condition, +// pending_order, +// &clock, +// ); + +// assert!(margin_manager::conditional_orders(&mm).length() == 1); + +// destroy_2!(usdt_price_low, usdc_price); +// return_shared(margin_registry); + +// scenario.next_tx(test_constants::admin()); +// let usdt_price_high = build_usdt_price_info_object_with_price(&mut scenario, 125, &clock); +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// scenario.next_tx(test_constants::user1()); +// let mut pool = scenario.take_shared>(); +// let margin_registry = scenario.take_shared(); + +// let order_infos = mm.execute_pending_orders( +// &mut pool, +// &usdt_price_high, +// &usdc_price, +// &margin_registry, +// &clock, +// scenario.ctx(), +// ); + +// assert!(order_infos.length() == 0); +// assert!(margin_manager::conditional_orders(&mm).length() == 1); + +// destroy_2!(usdt_price_high, usdc_price); +// return_shared_2!(mm, pool); + +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// return_shared(deepbook_registry); +// cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +// } + +// #[test] +// fun test_tpsl_trigger_below_market_order_insufficient_balance() { +// let ( +// mut scenario, +// clock, +// admin_cap, +// maintainer_cap, +// _usdc_pool_id, +// _usdt_pool_id, +// pool_id, +// registry_id, +// ) = setup_usdc_usdt_deepbook_margin(); + +// setup_pool_liquidity( +// &mut scenario, +// pool_id, +// &clock, +// 10000 * test_constants::usdt_multiplier(), +// 20000 * test_constants::usdc_multiplier(), +// ); + +// scenario.next_tx(test_constants::user1()); +// let mut margin_registry = scenario.take_shared(); +// let pool = scenario.take_shared>(); +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// margin_manager::new( +// &pool, +// &deepbook_registry, +// &mut margin_registry, +// &clock, +// scenario.ctx(), +// ); +// return_shared(deepbook_registry); +// return_shared(pool); + +// scenario.next_tx(test_constants::user1()); +// let mut mm = scenario.take_shared>(); + +// let usdt_price_high = build_usdt_price_info_object_with_price(&mut scenario, 1, &clock); +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// mm.deposit( +// &margin_registry, +// &usdt_price_high, +// &usdc_price, +// mint_coin(100 * test_constants::usdt_multiplier(), scenario.ctx()), +// &clock, +// scenario.ctx(), +// ); + +// let condition = tpsl::new_condition(true, (8 * constants::float_scaling()) / 10); +// let pending_order = tpsl::new_pending_market_order( +// 1, +// constants::self_matching_allowed(), +// 10000 * test_constants::usdt_multiplier(), // Large quantity +// true, +// false, +// ); + +// mm.add_conditional_order( +// &usdt_price_high, +// &usdc_price, +// &margin_registry, +// 1, +// condition, +// pending_order, +// &clock, +// ); + +// assert!(margin_manager::conditional_orders(&mm).length() == 1); + +// destroy_2!(usdt_price_high, usdc_price); +// return_shared(margin_registry); + +// scenario.next_tx(test_constants::admin()); +// let usdt_price_low = build_usdt_price_info_object_with_price(&mut scenario, 75, &clock); +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// scenario.next_tx(test_constants::user1()); +// let mut pool = scenario.take_shared>(); +// let margin_registry = scenario.take_shared(); + +// let order_infos = mm.execute_pending_orders( +// &mut pool, +// &usdt_price_low, +// &usdc_price, +// &margin_registry, +// &clock, +// scenario.ctx(), +// ); + +// assert!(order_infos.length() == 0); +// assert!(margin_manager::conditional_orders(&mm).length() == 1); + +// destroy_2!(usdt_price_low, usdc_price); +// return_shared_2!(mm, pool); + +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// return_shared(deepbook_registry); +// cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +// } + +// #[test] +// fun test_tpsl_trigger_above_market_order_insufficient_balance() { +// let ( +// mut scenario, +// clock, +// admin_cap, +// maintainer_cap, +// _usdc_pool_id, +// _usdt_pool_id, +// pool_id, +// registry_id, +// ) = setup_usdc_usdt_deepbook_margin(); + +// setup_pool_liquidity( +// &mut scenario, +// pool_id, +// &clock, +// 10000 * test_constants::usdt_multiplier(), +// 20000 * test_constants::usdc_multiplier(), +// ); + +// scenario.next_tx(test_constants::user1()); +// let mut margin_registry = scenario.take_shared(); +// let pool = scenario.take_shared>(); +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// margin_manager::new( +// &pool, +// &deepbook_registry, +// &mut margin_registry, +// &clock, +// scenario.ctx(), +// ); +// return_shared(deepbook_registry); +// return_shared(pool); + +// scenario.next_tx(test_constants::user1()); +// let mut mm = scenario.take_shared>(); + +// let usdt_price_low = build_usdt_price_info_object_with_price(&mut scenario, 1, &clock); +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// mm.deposit( +// &margin_registry, +// &usdt_price_low, +// &usdc_price, +// mint_coin(100 * test_constants::usdt_multiplier(), scenario.ctx()), +// &clock, +// scenario.ctx(), +// ); + +// let condition = tpsl::new_condition(false, (12 * constants::float_scaling()) / 10); +// let pending_order = tpsl::new_pending_market_order( +// 1, +// constants::self_matching_allowed(), +// 10000 * test_constants::usdt_multiplier(), // Large quantity +// false, +// false, +// ); + +// mm.add_conditional_order( +// &usdt_price_low, +// &usdc_price, +// &margin_registry, +// 1, +// condition, +// pending_order, +// &clock, +// ); + +// assert!(margin_manager::conditional_orders(&mm).length() == 1); + +// destroy_2!(usdt_price_low, usdc_price); +// return_shared(margin_registry); + +// scenario.next_tx(test_constants::admin()); +// let usdt_price_high = build_usdt_price_info_object_with_price(&mut scenario, 125, &clock); +// let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + +// scenario.next_tx(test_constants::user1()); +// let mut pool = scenario.take_shared>(); +// let margin_registry = scenario.take_shared(); + +// let order_infos = mm.execute_pending_orders( +// &mut pool, +// &usdt_price_high, +// &usdc_price, +// &margin_registry, +// &clock, +// scenario.ctx(), +// ); + +// assert!(order_infos.length() == 0); +// assert!(margin_manager::conditional_orders(&mm).length() == 1); + +// destroy_2!(usdt_price_high, usdc_price); +// return_shared_2!(mm, pool); + +// let deepbook_registry = scenario.take_shared_by_id(registry_id); +// return_shared(deepbook_registry); +// cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +// }