Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module deepbook_core_account::deepbook_core_account;
use account::{account::{Account, AccountWrapper, Auth}, account_registry::AccountRegistry};
use deepbook::{
account::Account as CoreAccount,
constants,
order::Order,
order_info::OrderInfo,
pool::Pool,
Expand All @@ -18,6 +19,8 @@ use deepbook_core_account::account_data;
use sui::{accumulator::AccumulatorRoot, clock::Clock, vec_set::{Self as vec_set, VecSet}};
use token::deep::DEEP;

const EMarketOrderPriceLimitExceeded: u64 = 0;

/// Return whether this account's embedded manager has DeepBook core pool account data.
public fun pool_account_exists<BaseAsset, QuoteAsset>(
pool: &Pool<BaseAsset, QuoteAsset>,
Expand Down Expand Up @@ -149,7 +152,9 @@ public fun place_limit_order<BaseAsset, QuoteAsset>(
info
}

/// Place a DeepBook market order using account custody.
/// Place a DeepBook market order using account custody. `price_limit` is the
/// maximum average execution price for a bid and the minimum for an ask, in
/// DeepBook's fixed-point price units. Orders with no fills skip the price check.
public fun place_market_order<BaseAsset, QuoteAsset>(
pool: &mut Pool<BaseAsset, QuoteAsset>,
deepbook_registry: &Registry,
Expand All @@ -158,6 +163,7 @@ public fun place_market_order<BaseAsset, QuoteAsset>(
client_order_id: u64,
self_matching_option: u8,
quantity: u64,
price_limit: u64,
is_bid: bool,
pay_with_deep: bool,
root: &AccumulatorRoot,
Expand Down Expand Up @@ -189,6 +195,15 @@ public fun place_market_order<BaseAsset, QuoteAsset>(
clock,
ctx,
);
if (info.executed_quantity() > 0) {
let quote_value =
(info.cumulative_quote_quantity() as u128) * constants::float_scaling_u128();
let limit_value = (info.executed_quantity() as u128) * (price_limit as u128);
assert!(
if (is_bid) quote_value <= limit_value else quote_value >= limit_value,
EMarketOrderPriceLimitExceeded,
);
};
let (base_swept, quote_swept, deep_swept) = account_data::sweep_all<BaseAsset, QuoteAsset>(
d,
ctx,
Expand Down
219 changes: 219 additions & 0 deletions packages/deepbook_core_account/tests/deepbook_core_account_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ const BOB: address = @0xB0B;
const BASE_AMOUNT: u64 = 5_000_000_000;
const RESTING_ASK_CLIENT_ORDER_ID: u64 = 11;
const FILLING_BID_CLIENT_ORDER_ID: u64 = 12;
const MARKET_ORDER_MAKER_CLIENT_ORDER_ID: u64 = 21;
const ACCOUNT_MARKET_ORDER_CLIENT_ORDER_ID: u64 = 22;
const ONE_PRICE_UNIT: u64 = 1;
const PARTIAL_FILL_MULTIPLIER: u64 = 2;
const EUnexpectedSuccess: u64 = 999;
const NO_AVAILABLE_QUANTITY: u64 = 0;
const ZERO_BALANCE: u64 = 0;
const NO_OPEN_ORDER_COUNT: u64 = 0;
const SINGLE_OPEN_ORDER_COUNT: u64 = 1;
Expand Down Expand Up @@ -221,6 +227,48 @@ fun funding_round_trip_sweeps_back_to_account() {
scenario.end();
}

#[test]
fun bid_market_order_at_price_limit_succeeds() {
run_market_order_at_price_limit(
true,
constants::min_size(),
constants::min_size(),
);
}

#[test, expected_failure(abort_code = dca::EMarketOrderPriceLimitExceeded)]
fun bid_market_order_above_price_limit_aborts() {
run_market_order_beyond_price_limit(true)
}

#[test]
fun ask_market_order_at_price_limit_succeeds() {
run_market_order_at_price_limit(
false,
constants::min_size(),
constants::min_size(),
);
}

#[test, expected_failure(abort_code = dca::EMarketOrderPriceLimitExceeded)]
fun ask_market_order_below_price_limit_aborts() {
run_market_order_beyond_price_limit(false)
}

#[test]
fun partial_fill_uses_executed_quantity_for_price_limit() {
run_market_order_at_price_limit(
true,
constants::min_size(),
PARTIAL_FILL_MULTIPLIER * constants::min_size(),
);
}

#[test]
fun no_fill_skips_price_limit_and_preserves_custody() {
run_market_order_at_price_limit(false, NO_AVAILABLE_QUANTITY, constants::min_size());
}

#[test]
fun withdraw_settled_amounts_sweeps_maker_fill_to_account() {
let (mut scenario, registry_id, pool_id, mut wrapper) = setup_whitelisted_account();
Expand Down Expand Up @@ -428,6 +476,142 @@ fun setup_whitelisted_account(): (Scenario, ID, ID, AccountWrapper) {
setup_account_with_pool(true)
}

fun run_market_order_at_price_limit(
is_bid: bool,
available_quantity: u64,
requested_quantity: u64,
) {
let execution_price = constants::float_scaling();
let (mut scenario, registry_id, pool_id, mut wrapper) = if (
available_quantity == NO_AVAILABLE_QUANTITY
) {
let (mut scenario, registry_id, pool_id, wrapper) = setup_whitelisted_account();
authorize_core_app(&mut scenario, registry_id);
(scenario, registry_id, pool_id, wrapper)
} else {
setup_market_order_account(
!is_bid,
execution_price,
available_quantity,
)
};

scenario.next_tx(ALICE);
let registry = scenario.take_shared_by_id<Registry>(registry_id);
let mut pool = scenario.take_shared_by_id<Pool<BASE, QUOTE>>(pool_id);
let root = scenario.take_shared<AccumulatorRoot>();
let clock = scenario.take_shared<Clock>();
let info = dca::place_market_order<BASE, QUOTE>(
&mut pool,
&registry,
&mut wrapper,
account::generate_auth(scenario.ctx()),
ACCOUNT_MARKET_ORDER_CLIENT_ORDER_ID,
constants::self_matching_allowed(),
requested_quantity,
execution_price,
is_bid,
true,
&root,
&clock,
scenario.ctx(),
);
let account = wrapper.load_account();
let expected_base_balance = if (is_bid) {
BASE_AMOUNT + available_quantity
} else {
BASE_AMOUNT - available_quantity
};
let expected_quote_balance = if (is_bid) {
BASE_AMOUNT - available_quantity
} else {
BASE_AMOUNT + available_quantity
};

assert_eq!(info.original_quantity(), requested_quantity);
assert_eq!(info.executed_quantity(), available_quantity);
assert_eq!(info.cumulative_quote_quantity(), available_quantity);
assert_eq!(account.balance<BASE>(&root, &clock), expected_base_balance);
assert_eq!(account.balance<QUOTE>(&root, &clock), expected_quote_balance);
if (available_quantity == NO_AVAILABLE_QUANTITY) {
assert_eq!(account.balance<DEEP>(&root, &clock), BASE_AMOUNT);
};
assert_eq!(account_data::balance_manager_balance<BASE>(account), ZERO_BALANCE);
assert_eq!(account_data::balance_manager_balance<QUOTE>(account), ZERO_BALANCE);
assert_eq!(account_data::balance_manager_balance<DEEP>(account), ZERO_BALANCE);

return_shared(clock);
return_shared(root);
return_shared(pool);
return_shared(registry);
destroy(wrapper);
scenario.end();
}

fun run_market_order_beyond_price_limit(is_bid: bool) {
let execution_price = constants::float_scaling();
let trade_amount = constants::min_size();
let price_limit = if (is_bid) {
execution_price - ONE_PRICE_UNIT
} else {
execution_price + ONE_PRICE_UNIT
};
let (mut scenario, registry_id, pool_id, mut wrapper) = setup_market_order_account(
!is_bid,
execution_price,
trade_amount,
);

scenario.next_tx(ALICE);
let registry = scenario.take_shared_by_id<Registry>(registry_id);
let mut pool = scenario.take_shared_by_id<Pool<BASE, QUOTE>>(pool_id);
let root = scenario.take_shared<AccumulatorRoot>();
let clock = scenario.take_shared<Clock>();
let _ = dca::place_market_order<BASE, QUOTE>(
&mut pool,
&registry,
&mut wrapper,
account::generate_auth(scenario.ctx()),
ACCOUNT_MARKET_ORDER_CLIENT_ORDER_ID,
constants::self_matching_allowed(),
trade_amount,
price_limit,
is_bid,
true,
&root,
&clock,
scenario.ctx(),
);
abort EUnexpectedSuccess
}

fun setup_market_order_account(
maker_is_bid: bool,
maker_price: u64,
maker_quantity: u64,
): (Scenario, ID, ID, AccountWrapper) {
let (mut scenario, registry_id, pool_id, wrapper) = setup_whitelisted_account();
authorize_core_app(&mut scenario, registry_id);
let maker_manager_id = create_balance_manager_with_funds<BASE, QUOTE>(
&mut scenario,
BOB,
BASE_AMOUNT,
);
let maker = place_manager_limit_order<BASE, QUOTE>(
&mut scenario,
BOB,
pool_id,
maker_manager_id,
MARKET_ORDER_MAKER_CLIENT_ORDER_ID,
maker_price,
maker_quantity,
maker_is_bid,
);
assert_eq!(maker.status(), constants::live());
assert!(maker.order_inserted());
(scenario, registry_id, pool_id, wrapper)
}

fun setup_account_with_pool(whitelisted_pool: bool): (Scenario, ID, ID, AccountWrapper) {
let mut scenario = test::begin(ADMIN);

Expand Down Expand Up @@ -504,6 +688,41 @@ fun place_manager_market_order<BaseAsset, QuoteAsset>(
info
}

fun place_manager_limit_order<BaseAsset, QuoteAsset>(
scenario: &mut Scenario,
trader: address,
pool_id: ID,
balance_manager_id: ID,
client_order_id: u64,
price: u64,
quantity: u64,
is_bid: bool,
): OrderInfo {
scenario.next_tx(trader);
let mut pool = scenario.take_shared_by_id<Pool<BaseAsset, QuoteAsset>>(pool_id);
let clock = scenario.take_shared<Clock>();
let mut manager = scenario.take_shared_by_id<BalanceManager>(balance_manager_id);
let proof = manager.generate_proof_as_owner(scenario.ctx());
let info = pool.place_limit_order<BaseAsset, QuoteAsset>(
&mut manager,
&proof,
client_order_id,
constants::no_restriction(),
constants::self_matching_allowed(),
price,
quantity,
is_bid,
true,
constants::max_u64(),
&clock,
scenario.ctx(),
);
return_shared(clock);
return_shared(pool);
return_shared(manager);
info
}

fun authorize_core_app(scenario: &mut Scenario, registry_id: ID) {
scenario.next_tx(ADMIN);
let admin_cap = deepbook::registry::get_admin_cap_for_testing(scenario.ctx());
Expand Down
Loading