diff --git a/protocol/x/clob/memclob/memclob.go b/protocol/x/clob/memclob/memclob.go index 2b3b18c72b..6c9779571f 100644 --- a/protocol/x/clob/memclob/memclob.go +++ b/protocol/x/clob/memclob/memclob.go @@ -810,12 +810,9 @@ func (m *MemClobPriceTimePriority) matchOrder( var matchingErr error - // If the order is post only and it's not the rewind step, then it cannot be filled. + // If the order is post only and crosses the book, // Set the matching error so that the order is canceled. - // TODO(DEC-998): Determine if allowing post-only orders to match in rewind step is valid. - if len(newMakerFills) > 0 && - !order.IsLiquidation() && - order.MustGetOrder().TimeInForce == types.Order_TIME_IN_FORCE_POST_ONLY { + if !order.IsLiquidation() && takerOrderStatus.OrderStatus == types.PostOnlyWouldCrossMakerOrder { matchingErr = types.ErrPostOnlyWouldCrossMakerOrder } @@ -1757,6 +1754,16 @@ func (m *MemClobPriceTimePriority) mustPerformTakerOrderMatching( continue } + // If a valid match has been generated but the taker order is a post only order, + // end the matching loop. Because of this, post-only orders can cause + // undercollateralized maker orders to be removed from the book up to the first valid match. + if takerOrderCrossesMakerOrder && + !newTakerOrder.IsLiquidation() && + newTakerOrder.MustGetOrder().TimeInForce == types.Order_TIME_IN_FORCE_POST_ONLY { + takerOrderStatus.OrderStatus = types.PostOnlyWouldCrossMakerOrder + break + } + // The orders have matched successfully, and the state has been updated. // To mark the orders as matched, perform the following actions: // 1. Deduct `matchedAmount` from the taker order's remaining quantums, and add the matched diff --git a/protocol/x/clob/memclob/memclob_place_order_test.go b/protocol/x/clob/memclob/memclob_place_order_test.go index 51c5a6a250..1b942201ab 100644 --- a/protocol/x/clob/memclob/memclob_place_order_test.go +++ b/protocol/x/clob/memclob/memclob_place_order_test.go @@ -2834,17 +2834,14 @@ func TestPlaceOrder_PostOnly(t *testing.T) { }, }, expectedRemainingAsks: []OrderWithRemainingSize{}, + // Second order is not collat check'd since the first order generates a valid + // match, so the matching loop ends. expectedCollatCheck: []expectedMatch{ { makerOrder: &constants.Order_Bob_Num0_Id11_Clob1_Buy5_Price40_GTB32, takerOrder: &constants.Order_Alice_Num1_Id1_Clob1_Sell10_Price15_GTB20_PO, matchedQuantums: 5, }, - { - makerOrder: &constants.Order_Bob_Num0_Id4_Clob1_Buy20_Price35_GTB22, - takerOrder: &constants.Order_Alice_Num1_Id1_Clob1_Sell10_Price15_GTB20_PO, - matchedQuantums: 5, - }, }, expectedExistingMatches: []expectedMatch{}, expectedOperations: []types.Operation{}, diff --git a/protocol/x/clob/types/orderbook.go b/protocol/x/clob/types/orderbook.go index 9c640308c0..7650c2869b 100644 --- a/protocol/x/clob/types/orderbook.go +++ b/protocol/x/clob/types/orderbook.go @@ -136,6 +136,9 @@ const ( // with either multiple positions in isolated perpetuals or both an isolated and a cross perpetual // position. ViolatesIsolatedSubaccountConstraints + // PostOnlyWouldCrossMakerOrder indicates that matching the post only taker order would cross the + // orderbook, and was therefore canceled. + PostOnlyWouldCrossMakerOrder ) // String returns a string representation of this `OrderStatus` enum.