From e232cb05fa2e9f8b93aee9f88ab7d76c8e140432 Mon Sep 17 00:00:00 2001 From: Roy Li Date: Wed, 12 Jun 2024 13:32:48 -0400 Subject: [PATCH] Revert "Remove collateralization-check for unmatched orders (backport #1587) (#1671)" This reverts commit 1e4fa08d886d10e2cb0efe83a30b75b7dc1bc7c6. --- protocol/mocks/MemClobKeeper.go | 30 +++ protocol/testutil/memclob/keeper.go | 11 + protocol/x/clob/e2e/order_removal_test.go | 50 ++++ protocol/x/clob/keeper/orders_test.go | 144 ++++++++++ protocol/x/clob/memclob/memclob.go | 87 ++++++ .../clob/memclob/memclob_cancel_order_test.go | 253 +++++++++++++++++- .../memclob_get_order_filled_amount_test.go | 3 + .../memclob_place_order_long_term_test.go | 163 ++++++++++- .../clob/memclob/memclob_place_order_test.go | 35 +++ ...emclob_purge_invalid_memclob_state_test.go | 24 +- .../clob/memclob/memclob_remove_order_test.go | 2 + protocol/x/clob/memclob/memclob_test_util.go | 20 ++ protocol/x/clob/types/mem_clob_keeper.go | 8 + 13 files changed, 807 insertions(+), 23 deletions(-) diff --git a/protocol/mocks/MemClobKeeper.go b/protocol/mocks/MemClobKeeper.go index d574acd60b..12eb88d42e 100644 --- a/protocol/mocks/MemClobKeeper.go +++ b/protocol/mocks/MemClobKeeper.go @@ -24,6 +24,36 @@ type MemClobKeeper struct { mock.Mock } +// AddOrderToOrderbookSubaccountUpdatesCheck provides a mock function with given fields: ctx, clobPairId, subaccountOpenOrders +func (_m *MemClobKeeper) AddOrderToOrderbookSubaccountUpdatesCheck(ctx types.Context, clobPairId clobtypes.ClobPairId, subaccountOpenOrders map[subaccountstypes.SubaccountId][]clobtypes.PendingOpenOrder) (bool, map[subaccountstypes.SubaccountId]subaccountstypes.UpdateResult) { + ret := _m.Called(ctx, clobPairId, subaccountOpenOrders) + + if len(ret) == 0 { + panic("no return value specified for AddOrderToOrderbookSubaccountUpdatesCheck") + } + + var r0 bool + var r1 map[subaccountstypes.SubaccountId]subaccountstypes.UpdateResult + if rf, ok := ret.Get(0).(func(types.Context, clobtypes.ClobPairId, map[subaccountstypes.SubaccountId][]clobtypes.PendingOpenOrder) (bool, map[subaccountstypes.SubaccountId]subaccountstypes.UpdateResult)); ok { + return rf(ctx, clobPairId, subaccountOpenOrders) + } + if rf, ok := ret.Get(0).(func(types.Context, clobtypes.ClobPairId, map[subaccountstypes.SubaccountId][]clobtypes.PendingOpenOrder) bool); ok { + r0 = rf(ctx, clobPairId, subaccountOpenOrders) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(types.Context, clobtypes.ClobPairId, map[subaccountstypes.SubaccountId][]clobtypes.PendingOpenOrder) map[subaccountstypes.SubaccountId]subaccountstypes.UpdateResult); ok { + r1 = rf(ctx, clobPairId, subaccountOpenOrders) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(map[subaccountstypes.SubaccountId]subaccountstypes.UpdateResult) + } + } + + return r0, r1 +} + // AddPreexistingStatefulOrder provides a mock function with given fields: ctx, order, memclob func (_m *MemClobKeeper) AddPreexistingStatefulOrder(ctx types.Context, order *clobtypes.Order, memclob clobtypes.MemClob) (subaccountstypes.BaseQuantums, clobtypes.OrderStatus, *clobtypes.OffchainUpdates, error) { ret := _m.Called(ctx, order, memclob) diff --git a/protocol/testutil/memclob/keeper.go b/protocol/testutil/memclob/keeper.go index 900bfe157e..4ded29afdb 100644 --- a/protocol/testutil/memclob/keeper.go +++ b/protocol/testutil/memclob/keeper.go @@ -284,6 +284,17 @@ func (f *FakeMemClobKeeper) GetStatefulOrdersTimeSlice( return orderIds } +func (f *FakeMemClobKeeper) AddOrderToOrderbookSubaccountUpdatesCheck( + ctx sdk.Context, + clobPairId types.ClobPairId, + subaccountOpenOrders map[satypes.SubaccountId][]types.PendingOpenOrder, +) ( + success bool, + successPerUpdate map[satypes.SubaccountId]satypes.UpdateResult, +) { + return f.collatCheckFn(subaccountOpenOrders) +} + func (f *FakeMemClobKeeper) addFakePositionSize( ctx sdk.Context, clobPairId types.ClobPairId, diff --git a/protocol/x/clob/e2e/order_removal_test.go b/protocol/x/clob/e2e/order_removal_test.go index c3e9ecc804..fe887f774c 100644 --- a/protocol/x/clob/e2e/order_removal_test.go +++ b/protocol/x/clob/e2e/order_removal_test.go @@ -183,6 +183,35 @@ func TestConditionalOrderRemoval(t *testing.T) { // TODO(CORE-858): Re-enable determinism checks once non-determinism issue is found and resolved. disableNonDeterminismChecks: true, }, + "under-collateralized conditional taker when adding to book is removed": { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_100000USD, + constants.Dave_Num0_10000USD, + }, + orders: []clobtypes.Order{ + constants.LongTermOrder_Carl_Num0_Id0_Clob0_Buy1BTC_Price49500_GTBT10, + // Does not cross with best bid. + constants.ConditionalOrder_Dave_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_SL_50003, + }, + withdrawal: &sendingtypes.MsgWithdrawFromSubaccount{ + Sender: constants.Dave_Num0, + Recipient: constants.DaveAccAddress.String(), + AssetId: constants.Usdc.Id, + Quantums: 10_000_000_000, + }, + priceUpdate: &prices.MsgUpdateMarketPrices{ + MarketPriceUpdates: []*prices.MsgUpdateMarketPrices_MarketPrice{ + prices.NewMarketPriceUpdate(0, 5_000_250_000), + }, + }, + + expectedOrderRemovals: []bool{ + false, + true, // taker order fails add-to-orderbook collateralization check + }, + // TODO(CORE-858): Re-enable determinism checks once non-determinism issue is found and resolved. + disableNonDeterminismChecks: true, + }, "under-collateralized conditional maker is removed": { subaccounts: []satypes.Subaccount{ constants.Carl_Num0_10000USD, @@ -779,6 +808,27 @@ func TestOrderRemoval(t *testing.T) { // TODO(CORE-858): Re-enable determinism checks once non-determinism issue is found and resolved. disableNonDeterminismChecks: true, }, + "under-collateralized taker when adding to book is removed": { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_10000USD, + constants.Dave_Num0_10000USD, + }, + firstOrder: constants.LongTermOrder_Carl_Num0_Id0_Clob0_Buy1BTC_Price49500_GTBT10, + // Does not cross with best bid. + secondOrder: constants.LongTermOrder_Dave_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10, + + withdrawal: &sendingtypes.MsgWithdrawFromSubaccount{ + Sender: constants.Dave_Num0, + Recipient: constants.DaveAccAddress.String(), + AssetId: constants.Usdc.Id, + Quantums: 10_000_000_000, + }, + + expectedFirstOrderRemoved: false, + expectedSecondOrderRemoved: true, // taker order fails add-to-orderbook collateralization check + // TODO(CORE-858): Re-enable determinism checks once non-determinism issue is found and resolved. + disableNonDeterminismChecks: true, + }, "under-collateralized maker is removed": { subaccounts: []satypes.Subaccount{ constants.Carl_Num0_10000USD, diff --git a/protocol/x/clob/keeper/orders_test.go b/protocol/x/clob/keeper/orders_test.go index e3b27261f3..c01b27aaba 100644 --- a/protocol/x/clob/keeper/orders_test.go +++ b/protocol/x/clob/keeper/orders_test.go @@ -158,6 +158,29 @@ func TestPlaceShortTermOrder(t *testing.T) { constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(0), }, }, + "Cannot place an order on the orderbook if the account would be undercollateralized": { + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_SmallMarginRequirement, + constants.EthUsd_20PercentInitial_10PercentMaintenance, + }, + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_599USD, + }, + clobs: []types.ClobPair{ + constants.ClobPair_Btc, + constants.ClobPair_Eth, + }, + feeParams: constants.PerpetualFeeParams, + + order: constants.Order_Carl_Num0_Id3_Clob1_Buy1ETH_Price3000, + + expectedOrderStatus: types.Undercollateralized, + expectedFilledSize: 0, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, + }, "Can place an order on the orderbook if the subaccount is right at the initial margin ratio": { perpetuals: []perptypes.Perpetual{ constants.BtcUsd_100PercentMarginRequirement, @@ -179,6 +202,28 @@ func TestPlaceShortTermOrder(t *testing.T) { constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), }, }, + "Cannot place an order on the orderbook if the account would be undercollateralized due to fees paid": { + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_100PercentMarginRequirement, + }, + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short, + }, + clobs: []types.ClobPair{ + // Exact same set-up as the previous test, except the clob pair has fees. + constants.ClobPair_Btc, + }, + feeParams: constants.PerpetualFeeParams, + + order: constants.Order_Carl_Num0_Id0_Clob0_Buy10QtBTC_Price100000QuoteQt, + + expectedOrderStatus: types.Undercollateralized, + expectedFilledSize: 0, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, + }, "Can place an order on the orderbook if the account would be collateralized due to rebate": { perpetuals: []perptypes.Perpetual{ constants.BtcUsd_100PercentMarginRequirement, @@ -349,6 +394,73 @@ func TestPlaceShortTermOrder(t *testing.T) { constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), }, }, + // This is a regression test for an issue whereby orders that had been previously matched were being checked for + // collateralization as if the subticks of the order were `0`. This resulted in always using `0` + // `bigFillQuoteQuantums` for the order when performing collateralization checks during `PlaceOrder`. + // This meant that previous buy orders in the match queue could only ever increase collateralization + // of the subaccount. + // Context: https://dydx-team.slack.com/archives/C03SLFHC3L7/p1668105457456389 + `Regression: New order should be undercollateralized when adding to the orderbook when previous fills make it + undercollateralized`: { + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_100PercentMarginRequirement, + }, + subaccounts: []satypes.Subaccount{ + constants.Carl_Num1_500USD, + constants.Carl_Num0_10000USD, + }, + clobs: []types.ClobPair{ + constants.ClobPair_Btc, + }, + existingOrders: []types.Order{ + // The maker subaccount places an order which is a maker order to buy $500 worth of BTC. + // The subaccount has a balance of $500 worth of USDC, and the perpetual has a 100% margin requirement. + // This order does not match, and is placed on the book as a maker order. + constants.Order_Carl_Num1_Id0_Clob0_Buy1kQtBTC_Price50000, + // The taker subaccount places an order which fully fills the previous order. + constants.Order_Carl_Num0_Id0_Clob0_Sell1kQtBTC_Price50000, + }, + feeParams: constants.PerpetualFeeParamsNoFee, + // The maker subaccount places a second order identical to the first. + // This should fail, because the maker subaccount currently has a balance of $0 USDC, and a perpetual of size + // 0.01 BTC ($500), and the perpetual has a 100% margin requirement. + order: constants.Order_Carl_Num1_Id1_Clob0_Buy1kQtBTC_Price50000, + expectedOrderStatus: types.Undercollateralized, + }, + `Regression: New order should be undercollateralized when matching when previous fills make it + undercollateralized`: { + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_100PercentMarginRequirement, + }, + subaccounts: []satypes.Subaccount{ + constants.Carl_Num1_500USD, + constants.Carl_Num0_10000USD, + }, + clobs: []types.ClobPair{ + constants.ClobPair_Btc, + }, + existingOrders: []types.Order{ + // The maker subaccount places an order which is a maker order to buy $500 worth of BTC. + // The subaccount has a balance of $500 worth of USDC, and the perpetual has a 100% margin requirement. + // This order does not match, and is placed on the book as a maker order. + constants.Order_Carl_Num1_Id0_Clob0_Buy1kQtBTC_Price50000, + // The taker subaccount places an order which fully fills the previous order. + constants.Order_Carl_Num0_Id0_Clob0_Sell1kQtBTC_Price50000, + // Match queue is now empty. + // The subaccount from the above order now places an order which is added to the book. + constants.Order_Carl_Num0_Id1_Clob0_Sell1kQtBTC_Price50000, + }, + feeParams: constants.PerpetualFeeParamsNoFee, + // The maker subaccount places a second order identical to the first. + // This should fail, because the maker during matching, because subaccount currently has a balance of $0 USDC, + // and a perpetual of size 0.01 BTC ($500), and the perpetual has a 100% margin requirement. + order: constants.Order_Carl_Num1_Id1_Clob0_Buy1kQtBTC_Price50000, + expectedOrderStatus: types.Undercollateralized, + expectedOpenInterests: map[uint32]*big.Int{ + // 1 BTC + 0.01 BTC filled + constants.BtcUsd_100PercentMarginRequirement.Params.Id: big.NewInt(101_000_000), + }, + }, `New order should be undercollateralized when matching when previous fills make it undercollateralized when using maker orders subticks, but would be collateralized if using taker order subticks`: { perpetuals: []perptypes.Perpetual{ @@ -487,6 +599,22 @@ func TestPlaceShortTermOrder(t *testing.T) { expectedFilledSize: 0, expectedMultiStoreWrites: []string{}, }, + `Subaccount cannot place buy order due to a failed collateralization check with its maker price but would + pass if using the oracle price`: { + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_50PercentInitial_40PercentMaintenance, + }, + subaccounts: []satypes.Subaccount{constants.Carl_Num0_100000USD}, + clobs: []types.ClobPair{ + constants.ClobPair_Btc, + }, + existingOrders: []types.Order{}, + feeParams: constants.PerpetualFeeParamsNoFee, + order: constants.Order_Carl_Num0_Id0_Clob0_Buy1BTC_Price500000_GTB10, + expectedOrderStatus: types.Undercollateralized, + expectedFilledSize: 0, + expectedMultiStoreWrites: []string{}, + }, `Subaccount placed buy order passes collateralization check when using the maker price`: { perpetuals: []perptypes.Perpetual{ constants.BtcUsd_50PercentInitial_40PercentMaintenance, @@ -524,6 +652,22 @@ func TestPlaceShortTermOrder(t *testing.T) { expectedFilledSize: 0, expectedMultiStoreWrites: []string{}, }, + `Subaccount cannot place sell order due to a failed collateralization check with its maker price but would + pass if using the oracle price`: { + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_50PercentInitial_40PercentMaintenance, + }, + subaccounts: []satypes.Subaccount{constants.Carl_Num0_50000USD}, + clobs: []types.ClobPair{ + constants.ClobPair_Btc, + }, + existingOrders: []types.Order{}, + feeParams: constants.PerpetualFeeParamsNoFee, + order: constants.Order_Carl_Num0_Id0_Clob0_Sell1BTC_Price5000_GTB10, + expectedOrderStatus: types.Undercollateralized, + expectedFilledSize: 0, + expectedMultiStoreWrites: []string{}, + }, `Subaccount placed sell order passes collateralization check when using the maker price`: { perpetuals: []perptypes.Perpetual{ constants.BtcUsd_50PercentInitial_40PercentMaintenance, diff --git a/protocol/x/clob/memclob/memclob.go b/protocol/x/clob/memclob/memclob.go index 61fe391f85..82f21d7857 100644 --- a/protocol/x/clob/memclob/memclob.go +++ b/protocol/x/clob/memclob/memclob.go @@ -638,6 +638,40 @@ func (m *MemClobPriceTimePriority) PlaceOrder( return orderSizeOptimisticallyFilledFromMatchingQuantums, orderStatus, offchainUpdates, nil } + // The taker order has unfilled size which will be added to the orderbook as a maker order. + // Verify the maker order can be added to the orderbook by performing the add-to-orderbook + // subaccount updates check. + addOrderOrderStatus := m.addOrderToOrderbookSubaccountUpdatesCheck( + ctx, + order, + ) + + // If the add order to orderbook subaccount updates check failed, we cannot add the order to the orderbook. + if !addOrderOrderStatus.IsSuccess() { + if m.generateOffchainUpdates { + // Send an off-chain update message indicating the order should be removed from the orderbook + // on the Indexer. + if message, success := off_chain_updates.CreateOrderRemoveMessage( + ctx, + order.OrderId, + addOrderOrderStatus, + nil, + ocutypes.OrderRemoveV1_ORDER_REMOVAL_STATUS_BEST_EFFORT_CANCELED, + ); success { + offchainUpdates.AddRemoveMessage(order.OrderId, message) + } + } + + // remove stateful orders which fail collateralization check while being added to orderbook + if order.IsStatefulOrder() && !m.operationsToPropose.IsOrderRemovalInOperationsQueue(order.OrderId) { + m.operationsToPropose.MustAddOrderRemovalToOperationsQueue( + order.OrderId, + types.OrderRemoval_REMOVAL_REASON_UNDERCOLLATERALIZED, + ) + } + return orderSizeOptimisticallyFilledFromMatchingQuantums, addOrderOrderStatus, offchainUpdates, nil + } + // If this is a Short-Term order and it's not in the operations queue, add the TX bytes to the // operations to propose. if order.IsShortTermOrder() && @@ -1454,6 +1488,59 @@ func (m *MemClobPriceTimePriority) validateNewOrder( return nil } +// addOrderToOrderbookSubaccountUpdatesCheck will perform a check to verify that the subaccount updates +// if the new maker order were to be fully filled are valid. +// It returns the result of this subaccount updates check. If the check returns an error, it will return +// the error so that it can be surfaced to the client. +// +// This function will assume that all prior order validation has passed, including the pre-requisite validation of +// `validateNewOrder` and the actual validation performed within `validateNewOrder`. +// Note that this is a loose check, mainly for the purposes of spam mitigation. We perform an additional +// check on the subaccount updates for orders when we attempt to match them. +func (m *MemClobPriceTimePriority) addOrderToOrderbookSubaccountUpdatesCheck( + ctx sdk.Context, + order types.Order, +) types.OrderStatus { + defer telemetry.ModuleMeasureSince( + types.ModuleName, + time.Now(), + metrics.PlaceOrder, + metrics.Memclob, + metrics.AddToOrderbookCollateralizationCheck, + metrics.Latency, + ) + + orderId := order.OrderId + subaccountId := orderId.SubaccountId + + // For the collateralization check, use the remaining amount of the order that is resting on the book. + remainingAmount, hasRemainingAmount := m.GetOrderRemainingAmount(ctx, order) + if !hasRemainingAmount { + panic(fmt.Sprintf("addOrderToOrderbookSubaccountUpdatesCheck: order has no remaining amount %v", order)) + } + + pendingOpenOrder := types.PendingOpenOrder{ + RemainingQuantums: remainingAmount, + IsBuy: order.IsBuy(), + Subticks: order.GetOrderSubticks(), + ClobPairId: order.GetClobPairId(), + } + + // Temporarily construct the subaccountOpenOrders with a single PendingOpenOrder. + subaccountOpenOrders := make(map[satypes.SubaccountId][]types.PendingOpenOrder) + subaccountOpenOrders[subaccountId] = []types.PendingOpenOrder{pendingOpenOrder} + + // TODO(DEC-1896): AddOrderToOrderbookSubaccountUpdatesCheck should accept a single PendingOpenOrder as a + // parameter rather than the subaccountOpenOrders map. + _, successPerSubaccountUpdate := m.clobKeeper.AddOrderToOrderbookSubaccountUpdatesCheck( + ctx, + order.GetClobPairId(), + subaccountOpenOrders, + ) + + return updateResultToOrderStatus(successPerSubaccountUpdate[subaccountId]) +} + // mustAddOrderToOrderbook will add the order to the resting orderbook. // This function will assume that all order validation has already been done. // If `forceToFrontOfLevel` is true, places the order at the head of the level, diff --git a/protocol/x/clob/memclob/memclob_cancel_order_test.go b/protocol/x/clob/memclob/memclob_cancel_order_test.go index c6cd1b0fed..6dc1bee2c7 100644 --- a/protocol/x/clob/memclob/memclob_cancel_order_test.go +++ b/protocol/x/clob/memclob/memclob_cancel_order_test.go @@ -433,6 +433,21 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { }, collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{ 0: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num1: { + { + RemainingQuantums: 30, + IsBuy: true, + Subticks: 50, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num1: satypes.Success, + }, + }, + 1: { CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ constants.Alice_Num0: { { @@ -524,6 +539,21 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { }, collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{ 0: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num1: { + { + RemainingQuantums: 30, + IsBuy: true, + Subticks: 50, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num0: satypes.Success, + }, + }, + 1: { CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ constants.Alice_Num0: { { @@ -612,7 +642,23 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { ), ), }, - collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{}, + collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{ + 0: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num1: { + { + RemainingQuantums: 30, + IsBuy: true, + Subticks: 50, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num1: satypes.Success, + }, + }, + }, expectedOperations: []types.Operation{}, expectedInternalOperations: []types.InternalOperation{}, @@ -636,6 +682,21 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { }, collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{ 0: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num1: { + { + RemainingQuantums: 30, + IsBuy: true, + Subticks: 50, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num1: satypes.Success, + }, + }, + 1: { CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ constants.Alice_Num1: { { @@ -659,6 +720,21 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { constants.Bob_Num0: satypes.Success, }, }, + 2: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Bob_Num0: { + { + RemainingQuantums: 5, + IsBuy: false, + Subticks: 35, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Bob_Num0: satypes.Success, + }, + }, }, expectedOperations: []types.Operation{ @@ -731,6 +807,21 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { }, collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{ 0: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num0: { + { + RemainingQuantums: 5, + IsBuy: false, + Subticks: 10, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num0: satypes.Success, + }, + }, + 1: { CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ constants.Alice_Num0: { { @@ -754,7 +845,37 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { constants.Alice_Num1: satypes.Success, }, }, - 1: { + 2: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num1: { + { + RemainingQuantums: 25, + IsBuy: true, + Subticks: 50, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num1: satypes.Success, + }, + }, + 3: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num0: { + { + RemainingQuantums: 25, + IsBuy: false, + Subticks: 15, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Bob_Num0: satypes.Success, + }, + }, + 4: { CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ constants.Alice_Num0: { { @@ -778,7 +899,22 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { constants.Alice_Num1: satypes.Success, }, }, - 2: { + 5: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num1: { + { + RemainingQuantums: 10, + IsBuy: true, + Subticks: 45, + ClobPairId: 1, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num1: satypes.Success, + }, + }, + 6: { CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ constants.Alice_Num0: { { @@ -943,6 +1079,21 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { }, collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{ 0: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num1: { + { + RemainingQuantums: 30, + IsBuy: true, + Subticks: 50, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num0: satypes.Success, + }, + }, + 1: { CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ constants.Alice_Num0: { { @@ -966,6 +1117,21 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { constants.Alice_Num1: satypes.Success, }, }, + 2: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num1: { + { + RemainingQuantums: 45, + IsBuy: true, + Subticks: 50, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num1: satypes.Success, + }, + }, }, expectedOperations: []types.Operation{ @@ -1036,7 +1202,24 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { ), }, collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{ + // Collateralization checks for first order placement. 0: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num1: { + { + RemainingQuantums: 30, + IsBuy: true, + Subticks: 50, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num0: satypes.Success, + }, + }, + // Collateralization checks for second order placement. + 1: { CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ constants.Alice_Num0: { { @@ -1060,7 +1243,24 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { constants.Alice_Num1: satypes.Success, }, }, - 1: { + // Collateralization checks for third order placement. + 2: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Bob_Num0: { + { + RemainingQuantums: 20, + IsBuy: false, + Subticks: 10, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Bob_Num0: satypes.Success, + }, + }, + // Collateralization checks for fourth order placement. + 3: { CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ constants.Alice_Num1: { { @@ -1084,6 +1284,21 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { constants.Bob_Num0: satypes.Success, }, }, + 4: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num1: { + { + RemainingQuantums: 25, + IsBuy: true, + Subticks: 50, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num1: satypes.Success, + }, + }, }, expectedOperations: []types.Operation{ @@ -1171,6 +1386,21 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { }, collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{ 0: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num1: { + { + RemainingQuantums: 30, + IsBuy: true, + Subticks: 50, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num1: satypes.Success, + }, + }, + 1: { CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ constants.Alice_Num0: { { @@ -1194,6 +1424,21 @@ func TestCancelOrder_OperationsQueue(t *testing.T) { constants.Alice_Num1: satypes.Success, }, }, + 2: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num1: { + { + RemainingQuantums: 45, + IsBuy: true, + Subticks: 50, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num1: satypes.Success, + }, + }, }, expectedOperations: []types.Operation{ diff --git a/protocol/x/clob/memclob/memclob_get_order_filled_amount_test.go b/protocol/x/clob/memclob/memclob_get_order_filled_amount_test.go index 6781044e05..c0a1502591 100644 --- a/protocol/x/clob/memclob/memclob_get_order_filled_amount_test.go +++ b/protocol/x/clob/memclob/memclob_get_order_filled_amount_test.go @@ -46,6 +46,9 @@ func TestGetOrderFilledAmount(t *testing.T) { ClientId: 0, } + memClobKeeper.On("AddOrderToOrderbookSubaccountUpdatesCheck", mock.Anything, mock.Anything). + Return(true, make(map[satypes.SubaccountId]satypes.UpdateResult)) + memClobKeeper.On("GetStatePosition", mock.Anything, mock.Anything, mock.Anything). Return(big.NewInt(0)) diff --git a/protocol/x/clob/memclob/memclob_place_order_long_term_test.go b/protocol/x/clob/memclob/memclob_place_order_long_term_test.go index 45a0472624..df31dfb750 100644 --- a/protocol/x/clob/memclob/memclob_place_order_long_term_test.go +++ b/protocol/x/clob/memclob/memclob_place_order_long_term_test.go @@ -32,8 +32,24 @@ func TestPlaceOrder_LongTerm(t *testing.T) { expectedErr error }{ "Can place a valid Long-Term buy order on an empty orderbook": { - placedMatchableOrders: []types.MatchableOrder{}, - collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{}, + placedMatchableOrders: []types.MatchableOrder{}, + collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{ + 0: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num0: { + { + RemainingQuantums: constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15.GetBaseQuantums(), + IsBuy: constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15.IsBuy(), + Subticks: constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15.GetOrderSubticks(), + ClobPairId: constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15.GetClobPairId(), + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num0: satypes.Success, + }, + }, + }, order: constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15, @@ -83,6 +99,21 @@ func TestPlaceOrder_LongTerm(t *testing.T) { constants.Bob_Num0: satypes.Success, }, }, + 1: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Bob_Num0: { + { + RemainingQuantums: 15, + IsBuy: true, + Subticks: 30, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Bob_Num0: satypes.Success, + }, + }, }, order: constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10, @@ -442,6 +473,98 @@ func TestPlaceOrder_LongTerm(t *testing.T) { expectedRemainingBids: []OrderWithRemainingSize{}, expectedRemainingAsks: []OrderWithRemainingSize{}, }, + `A Long-Term sell order can partially match with a Long-Term buy order, fail collateralization + checks when adding to orderbook, and all existing matches are considered valid`: { + placedMatchableOrders: []types.MatchableOrder{ + &constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10, + }, + collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{ + 0: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num0: { + { + RemainingQuantums: 25, + IsBuy: false, + Subticks: 30, + ClobPairId: 0, + }, + }, + constants.Bob_Num0: { + { + RemainingQuantums: 25, + IsBuy: true, + Subticks: 30, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num0: satypes.Success, + constants.Bob_Num0: satypes.Success, + }, + }, + 1: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num0: { + { + RemainingQuantums: 40, + IsBuy: false, + Subticks: 10, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num0: satypes.NewlyUndercollateralized, + }, + }, + }, + + order: constants.LongTermOrder_Alice_Num0_Id2_Clob0_Sell65_Price10_GTBT25, + + expectedFilledSize: 25, + expectedOrderStatus: types.Undercollateralized, + expectedOperations: []types.Operation{ + clobtest.NewOrderPlacementOperation( + constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10, + ), + clobtest.NewOrderPlacementOperation( + constants.LongTermOrder_Alice_Num0_Id2_Clob0_Sell65_Price10_GTBT25, + ), + clobtest.NewMatchOperation( + &constants.LongTermOrder_Alice_Num0_Id2_Clob0_Sell65_Price10_GTBT25, + []types.MakerFill{ + { + MakerOrderId: constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10.OrderId, + FillAmount: 25, + }, + }, + ), + }, + expectedInternalOperations: []types.InternalOperation{ + types.NewPreexistingStatefulOrderPlacementInternalOperation( + constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10, + ), + types.NewPreexistingStatefulOrderPlacementInternalOperation( + constants.LongTermOrder_Alice_Num0_Id2_Clob0_Sell65_Price10_GTBT25, + ), + types.NewMatchOrdersInternalOperation( + constants.LongTermOrder_Alice_Num0_Id2_Clob0_Sell65_Price10_GTBT25, + []types.MakerFill{ + { + MakerOrderId: constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10.OrderId, + FillAmount: 25, + }, + }, + ), + types.NewOrderRemovalInternalOperation( + constants.LongTermOrder_Alice_Num0_Id2_Clob0_Sell65_Price10_GTBT25.OrderId, + types.OrderRemoval_REMOVAL_REASON_UNDERCOLLATERALIZED, + ), + }, + expectedRemainingBids: []OrderWithRemainingSize{}, + expectedRemainingAsks: []OrderWithRemainingSize{}, + }, `A Long-Term post-only sell order can partially match with a Long-Term buy order, all existing matches are reverted and it's not added to pendingStatefulOrders`: { placedMatchableOrders: []types.MatchableOrder{ @@ -503,7 +626,23 @@ func TestPlaceOrder_LongTerm(t *testing.T) { placedMatchableOrders: []types.MatchableOrder{ &constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15, }, - collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{}, + collateralizationCheck: map[int]testutil_memclob.CollateralizationCheck{ + 0: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num0: { + { + RemainingQuantums: constants.LongTermOrder_Alice_Num0_Id2_Clob0_Sell65_Price10_GTBT25.GetBaseQuantums(), + IsBuy: constants.LongTermOrder_Alice_Num0_Id2_Clob0_Sell65_Price10_GTBT25.IsBuy(), + Subticks: constants.LongTermOrder_Alice_Num0_Id2_Clob0_Sell65_Price10_GTBT25.GetOrderSubticks(), + ClobPairId: constants.LongTermOrder_Alice_Num0_Id2_Clob0_Sell65_Price10_GTBT25.GetClobPairId(), + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num0: satypes.Success, + }, + }, + }, order: constants.LongTermOrder_Alice_Num0_Id2_Clob0_Sell65_Price10_GTBT25, @@ -567,7 +706,23 @@ func TestPlaceOrder_PreexistingStatefulOrder(t *testing.T) { ctx, _, _ := sdktest.NewSdkContextWithMultistore() ctx = ctx.WithIsCheckTx(true) longTermOrder := constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15 - collateralizationCheck := map[int]testutil_memclob.CollateralizationCheck{} + collateralizationCheck := map[int]testutil_memclob.CollateralizationCheck{ + 0: { + CollatCheck: map[satypes.SubaccountId][]types.PendingOpenOrder{ + constants.Alice_Num0: { + { + RemainingQuantums: 5, + IsBuy: true, + Subticks: 10, + ClobPairId: 0, + }, + }, + }, + Result: map[satypes.SubaccountId]satypes.UpdateResult{ + constants.Alice_Num0: satypes.Success, + }, + }, + } memclob, fakeMemClobKeeper, expectedNumCollateralizationChecks, numCollateralChecks := simplePlaceOrderTestSetup( t, ctx, diff --git a/protocol/x/clob/memclob/memclob_place_order_test.go b/protocol/x/clob/memclob/memclob_place_order_test.go index cb6957a64b..89cd408d7f 100644 --- a/protocol/x/clob/memclob/memclob_place_order_test.go +++ b/protocol/x/clob/memclob/memclob_place_order_test.go @@ -177,6 +177,24 @@ func TestPlaceOrder_AddOrderToOrderbook(t *testing.T) { expectedOrderStatus: types.Success, expectedToReplaceOrder: false, }, + "Placing an order that causes the account to be undercollateralized fails": { + existingOrders: []types.MatchableOrder{}, + collateralizationCheck: satypes.NewlyUndercollateralized, + + order: constants.Order_Bob_Num0_Id1_Clob1_Sell11_Price16_GTB20, + + expectedOrderStatus: types.Undercollateralized, + expectedToReplaceOrder: false, + }, + "Placing an order that throws an error from the collateralization check fails": { + existingOrders: []types.MatchableOrder{}, + collateralizationCheck: satypes.UpdateCausedError, + + order: constants.Order_Bob_Num0_Id1_Clob1_Sell11_Price16_GTB20, + + expectedOrderStatus: types.InternalError, + expectedToReplaceOrder: false, + }, "Replacing an order fails if GoodTilBlock is lower than existing order": { existingOrders: []types.MatchableOrder{ &constants.Order_Bob_Num0_Id1_Clob1_Sell11_Price16_GTB20, @@ -258,6 +276,18 @@ func TestPlaceOrder_AddOrderToOrderbook(t *testing.T) { expectedOrderStatus: types.Success, expectedToReplaceOrder: true, }, + `Old order is removed from the book if GoodTilBlock is greater than existing order, the order + passes initial validation, and new replacement order fails collateralization checks`: { + existingOrders: []types.MatchableOrder{ + &constants.Order_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB15, + }, + + order: constants.Order_Alice_Num0_Id0_Clob0_Buy6_Price10_GTB20, + + collateralizationCheck: satypes.NewlyUndercollateralized, + expectedOrderStatus: types.Undercollateralized, + expectedToReplaceOrder: true, + }, `Replacing an order succeeds and old order is skipped during matching if GoodTilBlock is greater than existing order and the new replacement order is on the opposite side of the existing order`: { existingOrders: []types.MatchableOrder{ @@ -2792,6 +2822,8 @@ func TestAddOrderToOrderbook_ErrorPlaceNewFullyFilledOrder(t *testing.T) { memclob.SetClobKeeper(&memClobKeeper) memclob.CreateOrderbook(ctx, constants.ClobPair_Btc) + memClobKeeper.On("AddOrderToOrderbookSubaccountUpdatesCheck", mock.Anything, mock.Anything, mock.Anything). + Return(true, make(map[satypes.SubaccountId]satypes.UpdateResult)) memClobKeeper.On("GetStatePosition", mock.Anything, mock.Anything, mock.Anything). Return(big.NewInt(0)) memClobKeeper.On("ValidateSubaccountEquityTierLimitForNewOrder", mock.Anything, mock.Anything). @@ -2824,6 +2856,9 @@ func TestAddOrderToOrderbook_PanicsIfFullyFilled(t *testing.T) { orderId := order.OrderId quantums := order.GetBaseQuantums() + memClobKeeper.On("AddOrderToOrderbookSubaccountUpdatesCheck", mock.Anything, mock.Anything, mock.Anything). + Return(true, make(map[satypes.SubaccountId]satypes.UpdateResult)) + memClobKeeper.On("GetStatePosition", mock.Anything, mock.Anything, mock.Anything). Return(big.NewInt(0)) diff --git a/protocol/x/clob/memclob/memclob_purge_invalid_memclob_state_test.go b/protocol/x/clob/memclob/memclob_purge_invalid_memclob_state_test.go index b4a36009a6..f60bd10b92 100644 --- a/protocol/x/clob/memclob/memclob_purge_invalid_memclob_state_test.go +++ b/protocol/x/clob/memclob/memclob_purge_invalid_memclob_state_test.go @@ -260,12 +260,19 @@ func TestPurgeInvalidMemclobState(t *testing.T) { case *types.Operation_ShortTermOrderPlacement: order := operation.GetShortTermOrderPlacement().Order orderId := order.OrderId - // Mock out calls to GetOrderFillAmount during test setup. + // Mock out the first 4 calls to GetOrderFillAmount, which is called during test setup. mockMemClobKeeper.On("GetOrderFillAmount", mock.Anything, orderId).Return( false, satypes.BaseQuantums(0), uint32(0), - ) + ).Times(5) + mockMemClobKeeper.On("AddOrderToOrderbookSubaccountUpdatesCheck", mock.Anything, mock.Anything, mock.Anything). + Return(true, make(map[satypes.SubaccountId]satypes.UpdateResult)).Once() + + // Mock out all remaining calls to GetOrderFillAmount, which is called in + // `memclob.PurgeInvalidMemclobState` and during test assertions. + fillAmount, exists := tc.newOrderFillAmounts[orderId] + mockMemClobKeeper.On("GetOrderFillAmount", mock.Anything, orderId).Return(exists, fillAmount, uint32(5)) } } @@ -286,19 +293,6 @@ func TestPurgeInvalidMemclobState(t *testing.T) { mockMemClobKeeper, ) - for _, operation := range tc.placedOperations { - switch operation.Operation.(type) { - case *types.Operation_ShortTermOrderPlacement: - order := operation.GetShortTermOrderPlacement().Order - orderId := order.OrderId - // Mock out all remaining calls to GetOrderFillAmount, which is called in - // `memclob.PurgeInvalidMemclobState` and during test assertions. - fillAmount, exists := tc.newOrderFillAmounts[orderId] - mockMemClobKeeper.On("GetOrderFillAmount", mock.Anything, orderId).Unset() - mockMemClobKeeper.On("GetOrderFillAmount", mock.Anything, orderId).Return(exists, fillAmount, uint32(5)) - } - } - // Run the test. ctx = ctx.WithBlockHeight(10) offchainUpdates := types.NewOffchainUpdates() diff --git a/protocol/x/clob/memclob/memclob_remove_order_test.go b/protocol/x/clob/memclob/memclob_remove_order_test.go index 2fc23aed45..44084b614e 100644 --- a/protocol/x/clob/memclob/memclob_remove_order_test.go +++ b/protocol/x/clob/memclob/memclob_remove_order_test.go @@ -327,6 +327,8 @@ func TestRemoveOrderIfFilled(t *testing.T) { memclob := NewMemClobPriceTimePriority(false) memclob.SetClobKeeper(&memClobKeeper) + memClobKeeper.On("AddOrderToOrderbookSubaccountUpdatesCheck", mock.Anything, mock.Anything, mock.Anything). + Return(true, make(map[satypes.SubaccountId]satypes.UpdateResult)) memClobKeeper.On("ValidateSubaccountEquityTierLimitForNewOrder", mock.Anything, mock.Anything).Return(nil) memClobKeeper.On("SendOrderbookUpdates", mock.Anything, mock.Anything, mock.Anything).Return().Maybe() diff --git a/protocol/x/clob/memclob/memclob_test_util.go b/protocol/x/clob/memclob/memclob_test_util.go index 5c6d58bc3d..c06041029a 100644 --- a/protocol/x/clob/memclob/memclob_test_util.go +++ b/protocol/x/clob/memclob/memclob_test_util.go @@ -102,6 +102,26 @@ func createCollatCheckExpectationsFromPendingMatches( expectedCollatChecks[i] = expectedPendingMatchesForCollatCheck } + expectedMatchingCollateralizationChecks := len(expectedPendingMatches) + + // If this is not a liquidation and taker order size will be added to the book, populate the + // expected parameters of the collateralization check for adding an order to the orderbook. + if !order.IsLiquidation() && addToOrderbookSize > 0 { + orderbookPendingMatches := []types.PendingOpenOrder{ + { + RemainingQuantums: addToOrderbookSize, + IsBuy: order.IsBuy(), + Subticks: order.GetOrderSubticks(), + ClobPairId: clobPairId, + }, + } + + expectedCollatChecks[expectedMatchingCollateralizationChecks] = + map[satypes.SubaccountId][]types.PendingOpenOrder{ + order.GetSubaccountId(): orderbookPendingMatches, + } + } + return expectedCollatChecks } diff --git a/protocol/x/clob/types/mem_clob_keeper.go b/protocol/x/clob/types/mem_clob_keeper.go index 168687d599..a4d135d37c 100644 --- a/protocol/x/clob/types/mem_clob_keeper.go +++ b/protocol/x/clob/types/mem_clob_keeper.go @@ -29,6 +29,14 @@ type MemClobKeeper interface { offchainUpdates *OffchainUpdates, err error, ) + AddOrderToOrderbookSubaccountUpdatesCheck( + ctx sdk.Context, + clobPairId ClobPairId, + subaccountOpenOrders map[satypes.SubaccountId][]PendingOpenOrder, + ) ( + success bool, + successPerUpdate map[satypes.SubaccountId]satypes.UpdateResult, + ) CanDeleverageSubaccount( ctx sdk.Context, subaccountId satypes.SubaccountId,