From e899a0a8c6411db698f7b6ac16650e90d600b04d Mon Sep 17 00:00:00 2001 From: Jonathan Fung Date: Tue, 2 Jan 2024 16:45:57 -0700 Subject: [PATCH 1/6] allow FOK/IOC reduce only orders in validate basic --- protocol/x/clob/types/errors.go | 7 ++++++- protocol/x/clob/types/message_place_order.go | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/protocol/x/clob/types/errors.go b/protocol/x/clob/types/errors.go index a9396f1026..aade943d5d 100644 --- a/protocol/x/clob/types/errors.go +++ b/protocol/x/clob/types/errors.go @@ -201,6 +201,11 @@ var ( 43, "Order has remaining size", ) + ErrReduceOnlyOrderReplacement = errorsmod.Register( + ModuleName, + 44, + "Reduce only order cannot replace a non reduce only order", + ) // Liquidations errors. ErrInvalidLiquidationsConfig = errorsmod.Register( @@ -493,7 +498,7 @@ var ( ErrReduceOnlyDisabled = errorsmod.Register( ModuleName, 9003, - "Reduce-only is currently disabled", + "Reduce-only is currently disabled for non-FOK/IOC orders", ) // Equity tier limit errors. diff --git a/protocol/x/clob/types/message_place_order.go b/protocol/x/clob/types/message_place_order.go index b82f160a94..92623b34a4 100644 --- a/protocol/x/clob/types/message_place_order.go +++ b/protocol/x/clob/types/message_place_order.go @@ -74,8 +74,8 @@ func (msg *MsgPlaceOrder) ValidateBasic() (err error) { return ErrLongTermOrdersCannotRequireImmediateExecution } - if msg.Order.ReduceOnly { - return errorsmod.Wrapf(ErrReduceOnlyDisabled, "reduce-only is temporarily disabled") + if msg.Order.ReduceOnly && !msg.Order.RequiresImmediateExecution() { + return errorsmod.Wrapf(ErrReduceOnlyDisabled, "reduce only orders must be short term IOC or FOK orders") } if msg.Order.Subticks == uint64(0) { From 0d50b633a535cbd36fa4957aa53bc635e9b89942 Mon Sep 17 00:00:00 2001 From: Jonathan Fung Date: Tue, 2 Jan 2024 16:46:30 -0700 Subject: [PATCH 2/6] add validation for disallowing replacement RO orders for existing non-RO orders --- protocol/x/clob/memclob/memclob.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/protocol/x/clob/memclob/memclob.go b/protocol/x/clob/memclob/memclob.go index e68f156e00..677eb10a26 100644 --- a/protocol/x/clob/memclob/memclob.go +++ b/protocol/x/clob/memclob/memclob.go @@ -1352,11 +1352,17 @@ func (m *MemClobPriceTimePriority) validateNewOrder( return types.ErrInvalidReplacement } - // If the order is a reduce-only order, we should ensure it does not increase the subaccount's - // current position size. Note that we intentionally do not validate that the reduce-only order + // If the order is a reduce-only order, we should ensure the full size of the order does not increase the + // subaccount's current position size. Note that we intentionally do not validate that the reduce-only order // does not change the subaccount's position _side_, and that will be validated if the order is matched. // TODO(DEC-1228): use `MustValidateReduceOnlyOrder` and move this to `PerformStatefulOrderValidation`. if order.IsReduceOnly() { + // If there exists an non-reduce only order with the same order id + // cancel the incoming reduce only order by returning an error. + if restingOrderExists && !existingRestingOrder.IsReduceOnly() { + return types.ErrReduceOnlyOrderReplacement + } + existingPositionSize := m.clobKeeper.GetStatePosition(ctx, orderId.SubaccountId, order.GetClobPairId()) orderSize := order.GetBigQuantums() From 889449e5c467a35fa04408b08dd52ea5eecffc21 Mon Sep 17 00:00:00 2001 From: Jonathan Fung Date: Tue, 2 Jan 2024 17:06:11 -0700 Subject: [PATCH 3/6] validate basic tests --- .../x/clob/types/message_place_order_test.go | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/protocol/x/clob/types/message_place_order_test.go b/protocol/x/clob/types/message_place_order_test.go index bf4ad41978..e19929d8a6 100644 --- a/protocol/x/clob/types/message_place_order_test.go +++ b/protocol/x/clob/types/message_place_order_test.go @@ -233,7 +233,7 @@ func TestMsgPlaceOrder_ValidateBasic(t *testing.T) { }, }, }, - "reduce-only disabled": { + "short-term FOK reduce-only success": { msg: MsgPlaceOrder{ Order: Order{ OrderId: OrderId{ @@ -241,6 +241,7 @@ func TestMsgPlaceOrder_ValidateBasic(t *testing.T) { Owner: sample.AccAddress(), Number: uint32(0), }, + OrderFlags: OrderIdFlags_ShortTerm, }, Side: Order_SIDE_BUY, Quantums: uint64(42), @@ -250,6 +251,66 @@ func TestMsgPlaceOrder_ValidateBasic(t *testing.T) { ReduceOnly: true, }, }, + }, + "short-term IOC reduce-only success": { + msg: MsgPlaceOrder{ + Order: Order{ + OrderId: OrderId{ + SubaccountId: satypes.SubaccountId{ + Owner: sample.AccAddress(), + Number: uint32(0), + }, + OrderFlags: OrderIdFlags_ShortTerm, + }, + Side: Order_SIDE_BUY, + Quantums: uint64(42), + GoodTilOneof: &Order_GoodTilBlock{GoodTilBlock: uint32(100)}, + Subticks: uint64(10), + TimeInForce: Order_TIME_IN_FORCE_IOC, + ReduceOnly: true, + }, + }, + }, + "long-term order reduce-only disabled": { + msg: MsgPlaceOrder{ + Order: Order{ + OrderId: OrderId{ + SubaccountId: satypes.SubaccountId{ + Owner: sample.AccAddress(), + Number: uint32(0), + }, + OrderFlags: OrderIdFlags_LongTerm, + }, + Side: Order_SIDE_BUY, + Quantums: uint64(42), + GoodTilOneof: &Order_GoodTilBlockTime{GoodTilBlockTime: uint32(100)}, + Subticks: uint64(10), + TimeInForce: Order_TIME_IN_FORCE_UNSPECIFIED, + ReduceOnly: true, + }, + }, + err: ErrReduceOnlyDisabled, + }, + "long-term reduce-only disabled": { + msg: MsgPlaceOrder{ + Order: Order{ + OrderId: OrderId{ + SubaccountId: satypes.SubaccountId{ + Owner: sample.AccAddress(), + Number: uint32(0), + }, + OrderFlags: OrderIdFlags_Conditional, + }, + Side: Order_SIDE_BUY, + Quantums: uint64(42), + GoodTilOneof: &Order_GoodTilBlockTime{GoodTilBlockTime: uint32(100)}, + Subticks: uint64(10), + TimeInForce: Order_TIME_IN_FORCE_UNSPECIFIED, + ConditionType: Order_CONDITION_TYPE_STOP_LOSS, + ConditionalOrderTriggerSubticks: uint64(8), + ReduceOnly: true, + }, + }, err: ErrReduceOnlyDisabled, }, "conditional: valid": { From a90ae30d6cabc48f16b7fab665422c9c4c8407d8 Mon Sep 17 00:00:00 2001 From: Jonathan Fung Date: Thu, 4 Jan 2024 00:45:55 -0700 Subject: [PATCH 4/6] e2e tests --- protocol/testutil/constants/orders.go | 87 ++++ protocol/testutil/constants/subaccounts.go | 24 ++ .../x/clob/e2e/reduce_only_orders_test.go | 390 ++++++++++++++++++ 3 files changed, 501 insertions(+) create mode 100644 protocol/x/clob/e2e/reduce_only_orders_test.go diff --git a/protocol/testutil/constants/orders.go b/protocol/testutil/constants/orders.go index 8f796e6221..70e3f4c1db 100644 --- a/protocol/testutil/constants/orders.go +++ b/protocol/testutil/constants/orders.go @@ -603,6 +603,20 @@ var ( Subticks: 500_000_000_000, GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, } + Order_Carl_Num0_Id0_Clob0_Buy10_Price500000_GTB20 = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Carl_Num0, ClientId: 0, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 10, + Subticks: 500_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 20}, + } + Order_Carl_Num0_Id0_Clob0_Buy80_Price500000_GTB20 = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Carl_Num0, ClientId: 0, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 80, + Subticks: 500_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 20}, + } Order_Carl_Num0_Id2_Clob0_Sell5_Price10_GTB15 = clobtypes.Order{ OrderId: clobtypes.OrderId{SubaccountId: Carl_Num0, ClientId: 2, ClobPairId: 0}, Side: clobtypes.Order_SIDE_SELL, @@ -1117,6 +1131,79 @@ var ( TimeInForce: clobtypes.Order_TIME_IN_FORCE_FILL_OR_KILL, ReduceOnly: true, } + Order_Alice_Num1_Id1_Clob1_Buy10_Price15_GTB20_FOK_RO = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Alice_Num1, ClientId: 1, ClobPairId: 1}, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 10, + Subticks: 15, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 20}, + TimeInForce: clobtypes.Order_TIME_IN_FORCE_FILL_OR_KILL, + ReduceOnly: true, + } + Order_Alice_Num1_Id1_Clob0_Sell10_Price15_GTB20_FOK_RO = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Alice_Num1, ClientId: 1, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 10, + Subticks: 15, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 20}, + TimeInForce: clobtypes.Order_TIME_IN_FORCE_FILL_OR_KILL, + ReduceOnly: true, + } + Order_Alice_Num1_Id1_Clob0_Buy10_Price15_GTB20_FOK_RO = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Alice_Num1, ClientId: 1, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 10, + Subticks: 15, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 20}, + TimeInForce: clobtypes.Order_TIME_IN_FORCE_FILL_OR_KILL, + ReduceOnly: true, + } + // IOC + RO orders. + Order_Alice_Num1_Id1_Clob1_Sell10_Price15_GTB20_IOC_RO = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Alice_Num1, ClientId: 1, ClobPairId: 1}, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 10, + Subticks: 15, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 20}, + TimeInForce: clobtypes.Order_TIME_IN_FORCE_IOC, + ReduceOnly: true, + } + Order_Alice_Num1_Id1_Clob1_Buy10_Price15_GTB20_IOC_RO = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Alice_Num1, ClientId: 1, ClobPairId: 1}, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 10, + Subticks: 15, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 20}, + TimeInForce: clobtypes.Order_TIME_IN_FORCE_IOC, + ReduceOnly: true, + } + Order_Alice_Num1_Id1_Clob0_Sell10_Price15_GTB20_IOC_RO = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Alice_Num1, ClientId: 1, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 10, + Subticks: 15, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 20}, + TimeInForce: clobtypes.Order_TIME_IN_FORCE_IOC, + ReduceOnly: true, + } + Order_Alice_Num1_Id1_Clob0_Buy10_Price15_GTB20_IOC_RO = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Alice_Num1, ClientId: 1, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 10, + Subticks: 15, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 20}, + TimeInForce: clobtypes.Order_TIME_IN_FORCE_IOC, + ReduceOnly: true, + } + Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Alice_Num1, ClientId: 1, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 15, + Subticks: 500_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 20}, + TimeInForce: clobtypes.Order_TIME_IN_FORCE_IOC, + ReduceOnly: true, + } // Reduce-only orders. Order_Alice_Num1_Id1_Clob0_Sell10_Price15_GTB20_RO = clobtypes.Order{ diff --git a/protocol/testutil/constants/subaccounts.go b/protocol/testutil/constants/subaccounts.go index 832db4057d..4ecc8a392f 100644 --- a/protocol/testutil/constants/subaccounts.go +++ b/protocol/testutil/constants/subaccounts.go @@ -35,6 +35,30 @@ var ( }, PerpetualPositions: []*satypes.PerpetualPosition{}, } + Alice_Num1_1BTC_Short_100_000USD = satypes.Subaccount{ + Id: &Alice_Num1, + AssetPositions: []*satypes.AssetPosition{ + &Usdc_Asset_100_000, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(-100_000_000), // -1 BTC + }, + }, + } + Alice_Num1_1BTC_Long_500_000USD = satypes.Subaccount{ + Id: &Alice_Num1, + AssetPositions: []*satypes.AssetPosition{ + &Usdc_Asset_500_000, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(100_000_000), // +1 BTC + }, + }, + } Bob_Num0_10_000USD = satypes.Subaccount{ Id: &Bob_Num0, AssetPositions: []*satypes.AssetPosition{ diff --git a/protocol/x/clob/e2e/reduce_only_orders_test.go b/protocol/x/clob/e2e/reduce_only_orders_test.go new file mode 100644 index 0000000000..e1000f1b6c --- /dev/null +++ b/protocol/x/clob/e2e/reduce_only_orders_test.go @@ -0,0 +1,390 @@ +package clob_test + +import ( + "testing" + + "github.com/cometbft/cometbft/types" + "github.com/dydxprotocol/v4-chain/protocol/dtypes" + testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" + "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + 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 TestReduceOnlyOrders(t *testing.T) { + tests := map[string]struct { + subaccounts []satypes.Subaccount + ordersForFirstBlock []clobtypes.Order + ordersForSecondBlock []clobtypes.Order + + expectedOrderOnMemClob map[clobtypes.OrderId]bool + expectedOrderFillAmount map[clobtypes.OrderId]uint64 + expectedSubaccounts []satypes.Subaccount + }{ + "IOC Reduce only order partially matches short term order same block, maker order fully filled": { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_100000USD, + constants.Alice_Num1_1BTC_Long_500_000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + MustScaleOrder( + constants.Order_Carl_Num0_Id0_Clob0_Buy10_Price500000_GTB20, + testapp.DefaultGenesis(), + ), + MustScaleOrder( + constants.Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO, + testapp.DefaultGenesis(), + ), + }, + ordersForSecondBlock: []clobtypes.Order{}, + + expectedOrderOnMemClob: map[clobtypes.OrderId]bool{ + constants.Order_Carl_Num0_Id0_Clob0_Buy10_Price500000_GTB20.OrderId: false, + constants.Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO.OrderId: false, + }, + expectedOrderFillAmount: map[clobtypes.OrderId]uint64{ + constants.Order_Carl_Num0_Id0_Clob0_Buy10_Price500000_GTB20.OrderId: 100, + }, + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(95_000_550_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(100), + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + { + Id: &constants.Alice_Num1, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(504_997_500_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(99_999_900), + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + "IOC Reduce only order partially matches short term order second block, maker order fully filled": { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_100000USD, + constants.Alice_Num1_1BTC_Long_500_000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + MustScaleOrder( + constants.Order_Carl_Num0_Id0_Clob0_Buy10_Price500000_GTB20, + testapp.DefaultGenesis(), + ), + }, + ordersForSecondBlock: []clobtypes.Order{ + MustScaleOrder( + constants.Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO, + testapp.DefaultGenesis(), + ), + }, + + expectedOrderOnMemClob: map[clobtypes.OrderId]bool{ + constants.Order_Carl_Num0_Id0_Clob0_Buy10_Price500000_GTB20.OrderId: false, + constants.Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO.OrderId: false, + }, + expectedOrderFillAmount: map[clobtypes.OrderId]uint64{ + constants.Order_Carl_Num0_Id0_Clob0_Buy10_Price500000_GTB20.OrderId: 100, + }, + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(95_000_550_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(100), + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + { + Id: &constants.Alice_Num1, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(504_997_500_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(99_999_900), + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + "IOC Reduce only order partially matches short term order second block, maker order partially filled": { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_100000USD, + constants.Alice_Num1_1BTC_Long_500_000USD, + }, + ordersForFirstBlock: []clobtypes.Order{ + MustScaleOrder( + constants.Order_Carl_Num0_Id0_Clob0_Buy80_Price500000_GTB20, + testapp.DefaultGenesis(), + ), + }, + ordersForSecondBlock: []clobtypes.Order{ + MustScaleOrder( + constants.Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO, + testapp.DefaultGenesis(), + ), + }, + + expectedOrderOnMemClob: map[clobtypes.OrderId]bool{ + constants.Order_Carl_Num0_Id0_Clob0_Buy80_Price500000_GTB20.OrderId: true, + constants.Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO.OrderId: false, + }, + expectedOrderFillAmount: map[clobtypes.OrderId]uint64{ + constants.Order_Carl_Num0_Id0_Clob0_Buy80_Price500000_GTB20.OrderId: 150, + }, + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(9_250_0825_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(150), + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + { + Id: &constants.Alice_Num1, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(507_496_250_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(99_999_850), + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).WithGenesisDocFn(func() (genesis types.GenesisDoc) { + genesis = testapp.DefaultGenesis() + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *satypes.GenesisState) { + genesisState.Subaccounts = tc.subaccounts + }, + ) + return genesis + }).Build() + ctx := tApp.InitChain() + + // Create all orders. + deliverTxsOverride := make([][]byte, 0) + deliverTxsOverride = append( + deliverTxsOverride, + constants.ValidEmptyMsgProposedOperationsTxBytes, + ) + + for _, order := range tc.ordersForFirstBlock { + for _, checkTx := range testapp.MustMakeCheckTxsWithClobMsg( + ctx, + tApp.App, + *clobtypes.NewMsgPlaceOrder(order), + ) { + resp := tApp.CheckTx(checkTx) + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + + if order.IsStatefulOrder() { + deliverTxsOverride = append(deliverTxsOverride, checkTx.Tx) + } + } + } + + // Add an empty premium vote. + deliverTxsOverride = append(deliverTxsOverride, constants.EmptyMsgAddPremiumVotesTxBytes) + + ctx = tApp.AdvanceToBlock(2, testapp.AdvanceToBlockOptions{ + DeliverTxsOverride: deliverTxsOverride, + }) + + // Place orders for second block + for _, order := range tc.ordersForSecondBlock { + for _, checkTx := range testapp.MustMakeCheckTxsWithClobMsg( + ctx, + tApp.App, + *clobtypes.NewMsgPlaceOrder(order), + ) { + resp := tApp.CheckTx(checkTx) + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + + if order.IsStatefulOrder() { + deliverTxsOverride = append(deliverTxsOverride, checkTx.Tx) + } + } + } + + // Verify expectations. + for orderId, exists := range tc.expectedOrderOnMemClob { + _, existsOnMemclob := tApp.App.ClobKeeper.MemClob.GetOrder(ctx, orderId) + // _, found := tApp.App.ClobKeeper.GetLongTermOrderPlacement(ctx, orderId) + require.Equal(t, exists, existsOnMemclob) + } + + for orderId, expectedFillAmount := range tc.expectedOrderFillAmount { + exists, fillAmount, _ := tApp.App.ClobKeeper.GetOrderFillAmount(ctx, orderId) + require.True(t, exists) + require.Equal(t, expectedFillAmount, fillAmount.ToUint64()) + } + + for _, subaccount := range tc.expectedSubaccounts { + actualSubaccount := tApp.App.SubaccountsKeeper.GetSubaccount(ctx, *subaccount.Id) + require.Equal(t, subaccount, actualSubaccount) + } + }) + } +} + +func TestReduceOnlyOrderFailure(t *testing.T) { + tests := map[string]struct { + subaccounts []satypes.Subaccount + orders []clobtypes.Order + errorMsg []string + }{ + "Zero perpetual position subaccount position cannot place sell RO order": { + orders: []clobtypes.Order{ + MustScaleOrder( + constants.Order_Alice_Num1_Id1_Clob1_Sell10_Price15_GTB20_FOK_RO, + testapp.DefaultGenesis(), + ), + }, + errorMsg: []string{ + clobtypes.ErrReduceOnlyWouldIncreasePositionSize.Error(), + }, + }, + "Zero perpetual position subaccount position cannot place buy RO order": { + orders: []clobtypes.Order{ + MustScaleOrder( + constants.Order_Alice_Num1_Id1_Clob1_Buy10_Price15_GTB20_FOK_RO, + testapp.DefaultGenesis(), + ), + }, + errorMsg: []string{ + clobtypes.ErrReduceOnlyWouldIncreasePositionSize.Error(), + }, + }, + "Reduce only order fails to replace non-reduce only order": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num1_1BTC_Short_100_000USD, + }, + orders: []clobtypes.Order{ + // non reduce only order + MustScaleOrder( + constants.Order_Alice_Num1_Id1_Clob1_Sell10_Price15_GTB20, + testapp.DefaultGenesis(), + ), + // reduce only replacement fails + MustScaleOrder( + constants.Order_Alice_Num1_Id1_Clob1_Buy10_Price15_GTB20_FOK_RO, + testapp.DefaultGenesis(), + ), + }, + errorMsg: []string{ + "", + clobtypes.ErrReduceOnlyOrderReplacement.Error(), + }, + }, + "FOK Reduce only order is placed but does not match immediately and is cancelled.": { + subaccounts: []satypes.Subaccount{ + constants.Alice_Num1_1BTC_Short_100_000USD, + }, + orders: []clobtypes.Order{ + MustScaleOrder( + constants.Order_Alice_Num1_Id1_Clob0_Buy10_Price15_GTB20_FOK_RO, + testapp.DefaultGenesis(), + ), + }, + errorMsg: []string{ + clobtypes.ErrFokOrderCouldNotBeFullyFilled.Error(), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).WithGenesisDocFn(func() (genesis types.GenesisDoc) { + genesis = testapp.DefaultGenesis() + if len(tc.subaccounts) > 0 { + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *satypes.GenesisState) { + genesisState.Subaccounts = tc.subaccounts + }, + ) + } + return genesis + }).Build() + ctx := tApp.InitChain() + + for idx, order := range tc.orders { + + for _, checkTx := range testapp.MustMakeCheckTxsWithClobMsg( + ctx, + tApp.App, + *clobtypes.NewMsgPlaceOrder(order), + ) { + resp := tApp.CheckTx(checkTx) + + if tc.errorMsg[idx] == "" { + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + } else { + require.Conditionf(t, resp.IsErr, "Expected CheckTx to error. Response: %+v", resp) + require.Contains( + t, + resp.Log, + tc.errorMsg[idx], + ) + } + } + } + }) + } +} From a7eb6225a3aed9838c27a5a5971d40aea96e9350 Mon Sep 17 00:00:00 2001 From: Jonathan Fung Date: Thu, 4 Jan 2024 17:20:14 -0700 Subject: [PATCH 5/6] lint --- protocol/x/clob/e2e/reduce_only_orders_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/protocol/x/clob/e2e/reduce_only_orders_test.go b/protocol/x/clob/e2e/reduce_only_orders_test.go index e1000f1b6c..0a65e01658 100644 --- a/protocol/x/clob/e2e/reduce_only_orders_test.go +++ b/protocol/x/clob/e2e/reduce_only_orders_test.go @@ -365,7 +365,6 @@ func TestReduceOnlyOrderFailure(t *testing.T) { ctx := tApp.InitChain() for idx, order := range tc.orders { - for _, checkTx := range testapp.MustMakeCheckTxsWithClobMsg( ctx, tApp.App, From aa39d6cf96a69692fa808396d607b501bac51b25 Mon Sep 17 00:00:00 2001 From: Jonathan Fung Date: Thu, 4 Jan 2024 18:25:07 -0700 Subject: [PATCH 6/6] more state assertions --- protocol/x/clob/e2e/reduce_only_orders_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/protocol/x/clob/e2e/reduce_only_orders_test.go b/protocol/x/clob/e2e/reduce_only_orders_test.go index 0a65e01658..cb87af5983 100644 --- a/protocol/x/clob/e2e/reduce_only_orders_test.go +++ b/protocol/x/clob/e2e/reduce_only_orders_test.go @@ -44,7 +44,8 @@ func TestReduceOnlyOrders(t *testing.T) { constants.Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO.OrderId: false, }, expectedOrderFillAmount: map[clobtypes.OrderId]uint64{ - constants.Order_Carl_Num0_Id0_Clob0_Buy10_Price500000_GTB20.OrderId: 100, + constants.Order_Carl_Num0_Id0_Clob0_Buy10_Price500000_GTB20.OrderId: 100, + constants.Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO.OrderId: 100, }, expectedSubaccounts: []satypes.Subaccount{ { @@ -104,7 +105,8 @@ func TestReduceOnlyOrders(t *testing.T) { constants.Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO.OrderId: false, }, expectedOrderFillAmount: map[clobtypes.OrderId]uint64{ - constants.Order_Carl_Num0_Id0_Clob0_Buy10_Price500000_GTB20.OrderId: 100, + constants.Order_Carl_Num0_Id0_Clob0_Buy10_Price500000_GTB20.OrderId: 100, + constants.Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO.OrderId: 100, }, expectedSubaccounts: []satypes.Subaccount{ { @@ -164,7 +166,8 @@ func TestReduceOnlyOrders(t *testing.T) { constants.Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO.OrderId: false, }, expectedOrderFillAmount: map[clobtypes.OrderId]uint64{ - constants.Order_Carl_Num0_Id0_Clob0_Buy80_Price500000_GTB20.OrderId: 150, + constants.Order_Carl_Num0_Id0_Clob0_Buy80_Price500000_GTB20.OrderId: 150, + constants.Order_Alice_Num1_Id1_Clob0_Sell15_Price500000_GTB20_IOC_RO.OrderId: 150, }, expectedSubaccounts: []satypes.Subaccount{ {