diff --git a/protocol/x/clob/e2e/short_term_orders_test.go b/protocol/x/clob/e2e/short_term_orders_test.go index 8147eaab9d..b6bfc6518f 100644 --- a/protocol/x/clob/e2e/short_term_orders_test.go +++ b/protocol/x/clob/e2e/short_term_orders_test.go @@ -1,19 +1,19 @@ package clob_test import ( - "github.com/dydxprotocol/v4-chain/protocol/indexer" "testing" + "github.com/cometbft/cometbft/crypto/tmhash" + "github.com/stretchr/testify/require" "golang.org/x/exp/slices" - "github.com/cometbft/cometbft/crypto/tmhash" "github.com/dydxprotocol/v4-chain/protocol/dtypes" - "github.com/dydxprotocol/v4-chain/protocol/lib" - + "github.com/dydxprotocol/v4-chain/protocol/indexer" indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events" "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" "github.com/dydxprotocol/v4-chain/protocol/indexer/msgsender" "github.com/dydxprotocol/v4-chain/protocol/indexer/off_chain_updates" + "github.com/dydxprotocol/v4-chain/protocol/lib" testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" clobtestutils "github.com/dydxprotocol/v4-chain/protocol/testutil/clob" "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" @@ -21,7 +21,6 @@ import ( assettypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" - "github.com/stretchr/testify/require" ) func TestPlaceOrder(t *testing.T) { @@ -1020,3 +1019,211 @@ func TestShortTermOrderReplacements(t *testing.T) { }) } } + +func TestCancelShortTermOrder(t *testing.T) { + tests := map[string]struct { + firstBlockOrders []clobtypes.MsgPlaceOrder + firstBlockCancels []clobtypes.MsgCancelOrder + secondBlockOrders []clobtypes.MsgPlaceOrder + secondBlockCancels []clobtypes.MsgCancelOrder + + expectedOrderIdsInMemclob map[clobtypes.OrderId]bool + expectedCancelExpirationsInMemclob map[clobtypes.OrderId]uint32 + expectedOrderFillAmounts map[clobtypes.OrderId]uint64 + }{ + "Cancel unfilled short term order": { + firstBlockOrders: []clobtypes.MsgPlaceOrder{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB5, + }, + secondBlockCancels: []clobtypes.MsgCancelOrder{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5, + }, + + expectedOrderIdsInMemclob: map[clobtypes.OrderId]bool{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB5.Order.OrderId: false, + }, + expectedCancelExpirationsInMemclob: map[clobtypes.OrderId]uint32{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5.OrderId: 5, + }, + expectedOrderFillAmounts: map[clobtypes.OrderId]uint64{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB5.Order.OrderId: 0, + }, + }, + "Cancel partially filled short term order in same block": { + firstBlockOrders: []clobtypes.MsgPlaceOrder{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB5, + *clobtypes.NewMsgPlaceOrder(MustScaleOrder( + clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: constants.Bob_Num0, ClientId: 0, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 4, + Subticks: 10, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 20}, + }, + testapp.DefaultGenesis(), + )), + }, + firstBlockCancels: []clobtypes.MsgCancelOrder{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5, + }, + + expectedOrderIdsInMemclob: map[clobtypes.OrderId]bool{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB5.Order.OrderId: false, + }, + expectedCancelExpirationsInMemclob: map[clobtypes.OrderId]uint32{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5.OrderId: 5, + }, + expectedOrderFillAmounts: map[clobtypes.OrderId]uint64{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB5.Order.OrderId: 40, + }, + }, + "Cancel partially filled short term order in next block": { + firstBlockOrders: []clobtypes.MsgPlaceOrder{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB5, + *clobtypes.NewMsgPlaceOrder(MustScaleOrder( + clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: constants.Bob_Num0, ClientId: 0, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 4, + Subticks: 10, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 20}, + }, + testapp.DefaultGenesis(), + )), + }, + secondBlockCancels: []clobtypes.MsgCancelOrder{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5, + }, + + expectedOrderIdsInMemclob: map[clobtypes.OrderId]bool{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB5.Order.OrderId: false, + }, + expectedCancelExpirationsInMemclob: map[clobtypes.OrderId]uint32{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5.OrderId: 5, + }, + expectedOrderFillAmounts: map[clobtypes.OrderId]uint64{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB5.Order.OrderId: 40, + }, + }, + "Cancel succeeds for fully-filled order": { + firstBlockOrders: []clobtypes.MsgPlaceOrder{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB5, + PlaceOrder_Bob_Num0_Id0_Clob0_Sell5_Price10_GTB20, + }, + secondBlockCancels: []clobtypes.MsgCancelOrder{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5, + }, + + expectedOrderIdsInMemclob: map[clobtypes.OrderId]bool{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB5.Order.OrderId: false, + }, + expectedCancelExpirationsInMemclob: map[clobtypes.OrderId]uint32{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5.OrderId: 5, + }, + expectedOrderFillAmounts: map[clobtypes.OrderId]uint64{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB5.Order.OrderId: 50, + }, + }, + "Cancel with GTB < existing order GTB does not remove order from memclob": { + firstBlockOrders: []clobtypes.MsgPlaceOrder{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB20, + }, + secondBlockCancels: []clobtypes.MsgCancelOrder{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5, + }, + + expectedOrderIdsInMemclob: map[clobtypes.OrderId]bool{ + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB20.Order.OrderId: true, + }, + expectedCancelExpirationsInMemclob: map[clobtypes.OrderId]uint32{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5.OrderId: 5, + }, + }, + "Cancel with GTB < existing cancel GTB is not placed on memclob": { + firstBlockCancels: []clobtypes.MsgCancelOrder{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5, + *clobtypes.NewMsgCancelOrderShortTerm( + clobtypes.OrderId{ + SubaccountId: constants.Alice_Num0, + ClientId: 0, + ClobPairId: 0, + }, + 4, + ), + }, + + expectedCancelExpirationsInMemclob: map[clobtypes.OrderId]uint32{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5.OrderId: 5, + }, + }, + "Cancel with GTB > existing cancel GTB is placed on memclob": { + firstBlockCancels: []clobtypes.MsgCancelOrder{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5, + *clobtypes.NewMsgCancelOrderShortTerm( + clobtypes.OrderId{ + SubaccountId: constants.Alice_Num0, + ClientId: 0, + ClobPairId: 0, + }, + 6, + ), + }, + + expectedCancelExpirationsInMemclob: map[clobtypes.OrderId]uint32{ + CancelOrder_Alice_Num0_Id0_Clob0_GTB5.OrderId: 6, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).Build() + ctx := tApp.InitChain() + + // Place first block orders and cancels + for _, order := range tc.firstBlockOrders { + for _, checkTx := range testapp.MustMakeCheckTxsWithClobMsg(ctx, tApp.App, order) { + resp := tApp.CheckTx(checkTx) + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + } + } + for _, cancel := range tc.firstBlockCancels { + for _, checkTx := range testapp.MustMakeCheckTxsWithClobMsg(ctx, tApp.App, cancel) { + tApp.CheckTx(checkTx) + } + } + + // Advance block + ctx = tApp.AdvanceToBlock(2, testapp.AdvanceToBlockOptions{}) + + // Place second block orders and cancels + for _, order := range tc.secondBlockOrders { + for _, checkTx := range testapp.MustMakeCheckTxsWithClobMsg(ctx, tApp.App, order) { + resp := tApp.CheckTx(checkTx) + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + } + } + for _, orderCancel := range tc.secondBlockCancels { + for _, checkTx := range testapp.MustMakeCheckTxsWithClobMsg(ctx, tApp.App, orderCancel) { + resp := tApp.CheckTx(checkTx) + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + } + } + + // Verify expectations + for orderId, shouldHaveOrder := range tc.expectedOrderIdsInMemclob { + _, exists := tApp.App.ClobKeeper.MemClob.GetOrder(ctx, orderId) + require.Equal(t, shouldHaveOrder, exists) + } + for orderId, expectedCancelExpirationBlock := range tc.expectedCancelExpirationsInMemclob { + cancelExpirationBlock, exists := tApp.App.ClobKeeper.MemClob.GetCancelOrder(ctx, orderId) + require.True(t, exists) + require.Equal(t, expectedCancelExpirationBlock, cancelExpirationBlock) + } + for orderId, expectedFillAmount := range tc.expectedOrderFillAmounts { + _, fillAmount, _ := tApp.App.ClobKeeper.GetOrderFillAmount(ctx, orderId) + require.Equal(t, expectedFillAmount, fillAmount.ToUint64()) + } + }) + } +}