diff --git a/indexer/packages/v4-protos/src/codegen/dydxprotocol/indexer/protocol/v1/clob.ts b/indexer/packages/v4-protos/src/codegen/dydxprotocol/indexer/protocol/v1/clob.ts index 92995bc19d..09c933e0d9 100644 --- a/indexer/packages/v4-protos/src/codegen/dydxprotocol/indexer/protocol/v1/clob.ts +++ b/indexer/packages/v4-protos/src/codegen/dydxprotocol/indexer/protocol/v1/clob.ts @@ -311,8 +311,9 @@ export enum ClobPairStatus { /** * CLOB_PAIR_STATUS_FINAL_SETTLEMENT - CLOB_PAIR_STATUS_FINAL_SETTLEMENT represents a clob pair that has been * deactivated. Clob pairs in this state do not accept new orders and trading - * is blocked. All open positions are closed by the protocol when the clob - * pair gains this status. + * is blocked. All open positions are closed and open stateful orders canceled + * by the protocol when the clob pair transitions to this status. All + * short-term orders are left to expire. */ CLOB_PAIR_STATUS_FINAL_SETTLEMENT = 6, UNRECOGNIZED = -1, @@ -360,8 +361,9 @@ export enum ClobPairStatusSDKType { /** * CLOB_PAIR_STATUS_FINAL_SETTLEMENT - CLOB_PAIR_STATUS_FINAL_SETTLEMENT represents a clob pair that has been * deactivated. Clob pairs in this state do not accept new orders and trading - * is blocked. All open positions are closed by the protocol when the clob - * pair gains this status. + * is blocked. All open positions are closed and open stateful orders canceled + * by the protocol when the clob pair transitions to this status. All + * short-term orders are left to expire. */ CLOB_PAIR_STATUS_FINAL_SETTLEMENT = 6, UNRECOGNIZED = -1, diff --git a/proto/dydxprotocol/indexer/protocol/v1/clob.proto b/proto/dydxprotocol/indexer/protocol/v1/clob.proto index b7b52b0a69..b061dae39b 100644 --- a/proto/dydxprotocol/indexer/protocol/v1/clob.proto +++ b/proto/dydxprotocol/indexer/protocol/v1/clob.proto @@ -175,7 +175,8 @@ enum ClobPairStatus { CLOB_PAIR_STATUS_INITIALIZING = 5; // CLOB_PAIR_STATUS_FINAL_SETTLEMENT represents a clob pair that has been // deactivated. Clob pairs in this state do not accept new orders and trading - // is blocked. All open positions are closed by the protocol when the clob - // pair gains this status. + // is blocked. All open positions are closed and open stateful orders canceled + // by the protocol when the clob pair transitions to this status. All + // short-term orders are left to expire. CLOB_PAIR_STATUS_FINAL_SETTLEMENT = 6; } diff --git a/protocol/indexer/events/deleveraging.go b/protocol/indexer/events/deleveraging.go index 7e67f0ea0b..b846e42001 100644 --- a/protocol/indexer/events/deleveraging.go +++ b/protocol/indexer/events/deleveraging.go @@ -7,6 +7,10 @@ import ( // NewDeleveragingEvent creates a DeleveragingEvent representing a deleveraging // where a liquidated subaccount's position is offset by another subaccount. +// Due to the support of final settlement deleveraging matches, sometimes the +// liquidatedSubaccountId is not actually an account that is liquidatable. More +// specifically, it may be a well-collateralized subaccount with an open position +// in a market with the final settlement status. func NewDeleveragingEvent( liquidatedSubaccountId satypes.SubaccountId, offsettingSubaccountId satypes.SubaccountId, @@ -14,15 +18,17 @@ func NewDeleveragingEvent( fillAmount satypes.BaseQuantums, price satypes.BaseQuantums, isBuy bool, + isFinalSettlement bool, ) *DeleveragingEventV1 { indexerLiquidatedSubaccountId := v1.SubaccountIdToIndexerSubaccountId(liquidatedSubaccountId) indexerOffsettingSubaccountId := v1.SubaccountIdToIndexerSubaccountId(offsettingSubaccountId) return &DeleveragingEventV1{ - Liquidated: indexerLiquidatedSubaccountId, - Offsetting: indexerOffsettingSubaccountId, - PerpetualId: perpetualId, - FillAmount: fillAmount.ToUint64(), - Price: price.ToUint64(), - IsBuy: isBuy, + Liquidated: indexerLiquidatedSubaccountId, + Offsetting: indexerOffsettingSubaccountId, + PerpetualId: perpetualId, + FillAmount: fillAmount.ToUint64(), + Price: price.ToUint64(), + IsBuy: isBuy, + IsFinalSettlement: isFinalSettlement, } } diff --git a/protocol/indexer/events/deleveraging_test.go b/protocol/indexer/events/deleveraging_test.go index 3578cc6b51..036d664906 100644 --- a/protocol/indexer/events/deleveraging_test.go +++ b/protocol/indexer/events/deleveraging_test.go @@ -1,9 +1,10 @@ package events_test import ( - satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" "testing" + satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" + "github.com/dydxprotocol/v4-chain/protocol/indexer/events" v1 "github.com/dydxprotocol/v4-chain/protocol/indexer/protocol/v1" "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" @@ -26,6 +27,7 @@ func TestNewDeleveragingEvent_Success(t *testing.T) { fillAmount, price, isBuy, + false, ) indexerLiquidatedSubaccountId := v1.SubaccountIdToIndexerSubaccountId(liquidatedSubaccountId) indexerOffsettingSubaccountId := v1.SubaccountIdToIndexerSubaccountId(offsettingSubaccountId) diff --git a/protocol/indexer/protocol/v1/clob.pb.go b/protocol/indexer/protocol/v1/clob.pb.go index 6e17da3336..1c4958743c 100644 --- a/protocol/indexer/protocol/v1/clob.pb.go +++ b/protocol/indexer/protocol/v1/clob.pb.go @@ -49,8 +49,9 @@ const ( ClobPairStatus_CLOB_PAIR_STATUS_INITIALIZING ClobPairStatus = 5 // CLOB_PAIR_STATUS_FINAL_SETTLEMENT represents a clob pair that has been // deactivated. Clob pairs in this state do not accept new orders and trading - // is blocked. All open positions are closed by the protocol when the clob - // pair gains this status. + // is blocked. All open positions are closed and open stateful orders canceled + // by the protocol when the clob pair transitions to this status. All + // short-term orders are left to expire. ClobPairStatus_CLOB_PAIR_STATUS_FINAL_SETTLEMENT ClobPairStatus = 6 ) diff --git a/protocol/lib/metrics/constants.go b/protocol/lib/metrics/constants.go index ee3f61a02e..5acaea443c 100644 --- a/protocol/lib/metrics/constants.go +++ b/protocol/lib/metrics/constants.go @@ -88,112 +88,113 @@ const ( RecognizedEventInfo = "recognized_event_info" // CLOB. - AddPerpetualFillAmount = "add_perpetual_fill_amount" - BaseQuantums = "base_quantums" - BestAsk = "best_ask" - BestAskClobPair = "best_ask_clob_pair" - BestBid = "best_bid" - BestBidClobPair = "best_bid_clob_pair" - Buy = "buy" - CancelOrder = "cancel_order" - CancelOrderAccounts = "cancel_order_accounts" - CancelShortTermOrder = "cancel_short_term_order" - CancelStatefulOrder = "cancel_stateful_order" - ClobPairId = "clob_pair_id" - ClobLiquidateSubaccountsAgainstOrderbook = "liquidate_subaccounts_against_orderbook" - LiquidateSubaccounts_GetLiquidations = "liquidate_subaccounts_against_orderbook_get_liquidations" - LiquidateSubaccounts_PlaceLiquidations = "liquidate_subaccounts_against_orderbook_place_liquidations" - LiquidateSubaccounts_Deleverage = "liquidate_subaccounts_against_orderbook_deleverage" - CollateralizationCheck = "place_order_collateralization_check" - CollateralizationCheckFailed = "collateralization_check_failed" - CollateralizationCheckSubaccounts = "collateralization_check_subaccounts" - Conditional = "conditional" - ConditionalOrderTriggered = "conditional_order_triggered" - ConditionalOrderUntriggered = "conditional_order_untriggered" - ConvertToUpdates = "convert_to_updates" - CreateClobPair = "create_clob_pair" - Expired = "expired" - FullyFilled = "fully_filled" - GetFillQuoteQuantums = "get_fill_quote_quantums" - Hydrate = "hydrate" - IsLong = "is_long" - IterateOverPendingMatches = "iterate_over_pending_matches" - MemClobReplayOperations = "memclob_replay_operations" - MemClobPurgeInvalidState = "memclob_purge_invalid_state" - NumConditionalOrderRemovals = "num_conditional_order_removals" - NumFills = "num_fills" - NumLongTermOrderRemovals = "num_long_term_order_removals" - NumMatchPerpDeleveragingOperations = "num_match_perp_deleveraging_operations" - NumMatchPerpLiquidationsOperations = "num_match_perp_liquidations_operations" - NumMatchStatefulOrders = "num_match_stateful_orders" - NumMatchTakerOrders = "num_match_taker_orders" - NumMatchedOrdersInOperationsQueue = "num_matched_orders_in_operations_queue" - NumMatchedConditionalOrders = "num_match_conditional_orders" - NumMatchedLiquidationOrders = "num_match_liquidation_orders" - NumMatchedLongTermOrders = "num_match_long_term_orders" - NumMatchedShortTermOrders = "num_match_short_term_orders" - NumOffsettingSubaccountsForDeleveraging = "num_offsetting_subaccounts_for_deleveraging" - NumProposedOperations = "num_proposed_operations" - NumShortTermOrderTxBytes = "num_short_term_order_tx_bytes" - NumUniqueSubaccountsDeleveraged = "num_unique_subaccounts_deleveraged" - NumUniqueSubaccountsLiquidated = "num_unique_subaccounts_liquidated" - NumUniqueSubaccountsOffsettingDeleveraged = "num_unique_subaccounts_offsetting_deleveraged" - OffsettingSubaccountPerpetualPosition = "offsetting_subaccount_perpetual_position" - OperationsQueueLength = "operations_queue_length" - OrderConflictsWithClobPairStatus = "order_conflicts_with_clob_pair_status" - OrderFlag = "order_flag" - OrderSide = "order_side" - OrderId = "order_id" - PartiallyFilled = "partially_filled" - PlaceConditionalOrdersFromLastBlock = "place_conditional_orders_from_last_block" - PlaceLongTermOrdersFromLastBlock = "place_long_term_orders_from_last_block" - PlaceOrder = "place_order" - PlaceOrderAccounts = "place_order_accounts" - PlaceStatefulOrder = "place_stateful_order" - ProcessMatches = "process_matches" - ProcessOperations = "process_operations" - ProposedOperations = "proposed_operations" - Proposer = "proposer" - ProposerConsAddress = "proposer_cons_addr" - QuoteQuantums = "quote_quantums" - RateLimit = "rate_limit" - ReduceOnly = "reduce_only" - RemovalReason = "removal_reason" - RemoveAndClearOperationsQueue = "remove_and_clear_operations_queue" - ReplayOperations = "replay_operations" - SortLiquidationOrders = "sort_liquidation_orders" - SendCancelOrderOffchainUpdates = "send_cancel_order_offchain_updates" - SendPlaceOrderOffchainUpdates = "send_place_order_offchain_updates" - SendPlacePerpetualLiquidationOffchainUpdates = "send_perpetual_liquidation_offchain_updates" - SendPrepareCheckStateOffchainUpdates = "send_prepare_check_state_offchain_updates" - SendProcessProposerMatchesOffchainUpdates = "send_process_proposer_matches_offchain_updates" - SendProposedOperationsOffchainUpdates = "send_proposed_operations_offchain_updates" - SendPurgeOffchainUpdates = "send_purge_offchain_updates" - SendUncrossOffchainUpdates = "send_uncross_offchain_updates" - Sell = "sell" - ShortTermOrder = "short_term_order" - SkipOrderRemovalAfterPlacement = "skip_order_removal_after_placement" - StatefulCancellationMsgHandlerFailure = "stateful_cancellation_msg_handler_failure" - StatefulCancellationMsgHandlerSuccess = "stateful_cancellation_msg_handler_success" - StatefulOrder = "stateful_order" - StatefulOrderAlreadyRemoved = "stateful_order_already_removed" - StatefulOrderMsgHandlerSuccess = "stateful_order_msg_handler_success" - StatefulOrderRemoved = "stateful_order_removed" - Status = "status" - SubaccountPendingMatches = "subaccount_pending_matches" - TimeInForce = "time_in_force" - TotalOrdersInClob = "total_orders_in_clob" - TotalQuoteQuantums = "total_quote_quantums" - Unfilled = "unfilled" - UnfilledLiquidationOrders = "unfilled_liquidation_orders" - UnknownPlaceOrders = "unknown_place_orders" - UnverifiedStatefulOrderRemoval = "unverified_stateful_order_removal" - UpdateBlockRateLimitConfiguration = "update_block_rate_limit_configuration" - UpdateClobPair = "update_clob_pair" - UpdateEquityTierLimitConfiguration = "update_equity_tier_limit_configuration" - UpdateLiquidationsConfig = "update_liquidations_config" - ValidateMatches = "validate_matches" - ValidateOrder = "validate_order" + AddPerpetualFillAmount = "add_perpetual_fill_amount" + BaseQuantums = "base_quantums" + BestAsk = "best_ask" + BestAskClobPair = "best_ask_clob_pair" + BestBid = "best_bid" + BestBidClobPair = "best_bid_clob_pair" + Buy = "buy" + CancelOrder = "cancel_order" + CancelOrderAccounts = "cancel_order_accounts" + CancelShortTermOrder = "cancel_short_term_order" + CancelStatefulOrder = "cancel_stateful_order" + ClobPairId = "clob_pair_id" + ClobLiquidateSubaccountsAgainstOrderbook = "liquidate_subaccounts_against_orderbook" + ClobGetSubaccountsWithPositionsInFinalSettlementMarkets = "get_subaccounts_with_positions_in_final_settlement_markets" + LiquidateSubaccounts_GetLiquidations = "liquidate_subaccounts_against_orderbook_get_liquidations" + LiquidateSubaccounts_PlaceLiquidations = "liquidate_subaccounts_against_orderbook_place_liquidations" + LiquidateSubaccounts_Deleverage = "liquidate_subaccounts_against_orderbook_deleverage" + CollateralizationCheck = "place_order_collateralization_check" + CollateralizationCheckFailed = "collateralization_check_failed" + CollateralizationCheckSubaccounts = "collateralization_check_subaccounts" + Conditional = "conditional" + ConditionalOrderTriggered = "conditional_order_triggered" + ConditionalOrderUntriggered = "conditional_order_untriggered" + ConvertToUpdates = "convert_to_updates" + CreateClobPair = "create_clob_pair" + Expired = "expired" + FullyFilled = "fully_filled" + GetFillQuoteQuantums = "get_fill_quote_quantums" + Hydrate = "hydrate" + IsLong = "is_long" + IterateOverPendingMatches = "iterate_over_pending_matches" + MemClobReplayOperations = "memclob_replay_operations" + MemClobPurgeInvalidState = "memclob_purge_invalid_state" + NumConditionalOrderRemovals = "num_conditional_order_removals" + NumFills = "num_fills" + NumLongTermOrderRemovals = "num_long_term_order_removals" + NumMatchPerpDeleveragingOperations = "num_match_perp_deleveraging_operations" + NumMatchPerpLiquidationsOperations = "num_match_perp_liquidations_operations" + NumMatchStatefulOrders = "num_match_stateful_orders" + NumMatchTakerOrders = "num_match_taker_orders" + NumMatchedOrdersInOperationsQueue = "num_matched_orders_in_operations_queue" + NumMatchedConditionalOrders = "num_match_conditional_orders" + NumMatchedLiquidationOrders = "num_match_liquidation_orders" + NumMatchedLongTermOrders = "num_match_long_term_orders" + NumMatchedShortTermOrders = "num_match_short_term_orders" + NumOffsettingSubaccountsForDeleveraging = "num_offsetting_subaccounts_for_deleveraging" + NumProposedOperations = "num_proposed_operations" + NumShortTermOrderTxBytes = "num_short_term_order_tx_bytes" + NumUniqueSubaccountsDeleveraged = "num_unique_subaccounts_deleveraged" + NumUniqueSubaccountsLiquidated = "num_unique_subaccounts_liquidated" + NumUniqueSubaccountsOffsettingDeleveraged = "num_unique_subaccounts_offsetting_deleveraged" + OffsettingSubaccountPerpetualPosition = "offsetting_subaccount_perpetual_position" + OperationsQueueLength = "operations_queue_length" + OrderConflictsWithClobPairStatus = "order_conflicts_with_clob_pair_status" + OrderFlag = "order_flag" + OrderSide = "order_side" + OrderId = "order_id" + PartiallyFilled = "partially_filled" + PlaceConditionalOrdersFromLastBlock = "place_conditional_orders_from_last_block" + PlaceLongTermOrdersFromLastBlock = "place_long_term_orders_from_last_block" + PlaceOrder = "place_order" + PlaceOrderAccounts = "place_order_accounts" + PlaceStatefulOrder = "place_stateful_order" + ProcessMatches = "process_matches" + ProcessOperations = "process_operations" + ProposedOperations = "proposed_operations" + Proposer = "proposer" + ProposerConsAddress = "proposer_cons_addr" + QuoteQuantums = "quote_quantums" + RateLimit = "rate_limit" + ReduceOnly = "reduce_only" + RemovalReason = "removal_reason" + RemoveAndClearOperationsQueue = "remove_and_clear_operations_queue" + ReplayOperations = "replay_operations" + SortLiquidationOrders = "sort_liquidation_orders" + SendCancelOrderOffchainUpdates = "send_cancel_order_offchain_updates" + SendPlaceOrderOffchainUpdates = "send_place_order_offchain_updates" + SendPlacePerpetualLiquidationOffchainUpdates = "send_perpetual_liquidation_offchain_updates" + SendPrepareCheckStateOffchainUpdates = "send_prepare_check_state_offchain_updates" + SendProcessProposerMatchesOffchainUpdates = "send_process_proposer_matches_offchain_updates" + SendProposedOperationsOffchainUpdates = "send_proposed_operations_offchain_updates" + SendPurgeOffchainUpdates = "send_purge_offchain_updates" + SendUncrossOffchainUpdates = "send_uncross_offchain_updates" + Sell = "sell" + ShortTermOrder = "short_term_order" + SkipOrderRemovalAfterPlacement = "skip_order_removal_after_placement" + StatefulCancellationMsgHandlerFailure = "stateful_cancellation_msg_handler_failure" + StatefulCancellationMsgHandlerSuccess = "stateful_cancellation_msg_handler_success" + StatefulOrder = "stateful_order" + StatefulOrderAlreadyRemoved = "stateful_order_already_removed" + StatefulOrderMsgHandlerSuccess = "stateful_order_msg_handler_success" + StatefulOrderRemoved = "stateful_order_removed" + Status = "status" + SubaccountPendingMatches = "subaccount_pending_matches" + TimeInForce = "time_in_force" + TotalOrdersInClob = "total_orders_in_clob" + TotalQuoteQuantums = "total_quote_quantums" + Unfilled = "unfilled" + UnfilledLiquidationOrders = "unfilled_liquidation_orders" + UnknownPlaceOrders = "unknown_place_orders" + UnverifiedStatefulOrderRemoval = "unverified_stateful_order_removal" + UpdateBlockRateLimitConfiguration = "update_block_rate_limit_configuration" + UpdateClobPair = "update_clob_pair" + UpdateEquityTierLimitConfiguration = "update_equity_tier_limit_configuration" + UpdateLiquidationsConfig = "update_liquidations_config" + ValidateMatches = "validate_matches" + ValidateOrder = "validate_order" // MemCLOB. AddedToOrderBook = "added_to_orderbook" diff --git a/protocol/lib/metrics/metric_keys.go b/protocol/lib/metrics/metric_keys.go index f0cedcf014..ac5329a76c 100644 --- a/protocol/lib/metrics/metric_keys.go +++ b/protocol/lib/metrics/metric_keys.go @@ -32,6 +32,7 @@ const ( ClobDeleveragingNonOverlappingBankrupcyPricesCount = "clob_deleveraging_non_overlapping_bankruptcy_prices_count" ClobDeleveragingNoOpenPositionOnOppositeSideCount = "clob_deleveraging_no_open_position_on_opposite_side_count" ClobDeleverageSubaccountFilledQuoteQuantums = "clob_deleverage_subaccount_filled_quote_quantums" + ClobSubaccountsWithFinalSettlementPositionsCount = "clob_subaccounts_with_final_settlement_positions_count" LiquidationsLiquidatableSubaccountIdsCount = "liquidations_liquidatable_subaccount_ids_count" LiquidationsPercentFilledDistribution = "liquidations_percent_filled_distribution" LiquidationsPlacePerpetualLiquidationQuoteQuantumsDistribution = "liquidations_place_perpetual_liquidation_quote_quantums_distribution" diff --git a/protocol/mocks/MemClob.go b/protocol/mocks/MemClob.go index 6bb77b08cd..fe1c624734 100644 --- a/protocol/mocks/MemClob.go +++ b/protocol/mocks/MemClob.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.26.1. DO NOT EDIT. package mocks @@ -25,6 +25,10 @@ func (_m *MemClob) CancelOrder(ctx types.Context, msgCancelOrder *clobtypes.MsgC ret := _m.Called(ctx, msgCancelOrder) var r0 *clobtypes.OffchainUpdates + var r1 error + if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MsgCancelOrder) (*clobtypes.OffchainUpdates, error)); ok { + return rf(ctx, msgCancelOrder) + } if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MsgCancelOrder) *clobtypes.OffchainUpdates); ok { r0 = rf(ctx, msgCancelOrder) } else { @@ -33,7 +37,6 @@ func (_m *MemClob) CancelOrder(ctx types.Context, msgCancelOrder *clobtypes.MsgC } } - var r1 error if rf, ok := ret.Get(1).(func(types.Context, *clobtypes.MsgCancelOrder) error); ok { r1 = rf(ctx, msgCancelOrder) } else { @@ -62,22 +65,25 @@ func (_m *MemClob) CreateOrderbook(ctx types.Context, clobPair clobtypes.ClobPai _m.Called(ctx, clobPair) } -// DeleverageSubaccount provides a mock function with given fields: ctx, subaccountId, perpetualId, deltaQuantums -func (_m *MemClob) DeleverageSubaccount(ctx types.Context, subaccountId subaccountstypes.SubaccountId, perpetualId uint32, deltaQuantums *big.Int) (*big.Int, error) { - ret := _m.Called(ctx, subaccountId, perpetualId, deltaQuantums) +// DeleverageSubaccount provides a mock function with given fields: ctx, subaccountId, perpetualId, deltaQuantums, isFinalSettlement +func (_m *MemClob) DeleverageSubaccount(ctx types.Context, subaccountId subaccountstypes.SubaccountId, perpetualId uint32, deltaQuantums *big.Int, isFinalSettlement bool) (*big.Int, error) { + ret := _m.Called(ctx, subaccountId, perpetualId, deltaQuantums, isFinalSettlement) var r0 *big.Int - if rf, ok := ret.Get(0).(func(types.Context, subaccountstypes.SubaccountId, uint32, *big.Int) *big.Int); ok { - r0 = rf(ctx, subaccountId, perpetualId, deltaQuantums) + var r1 error + if rf, ok := ret.Get(0).(func(types.Context, subaccountstypes.SubaccountId, uint32, *big.Int, bool) (*big.Int, error)); ok { + return rf(ctx, subaccountId, perpetualId, deltaQuantums, isFinalSettlement) + } + if rf, ok := ret.Get(0).(func(types.Context, subaccountstypes.SubaccountId, uint32, *big.Int, bool) *big.Int); ok { + r0 = rf(ctx, subaccountId, perpetualId, deltaQuantums, isFinalSettlement) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*big.Int) } } - var r1 error - if rf, ok := ret.Get(1).(func(types.Context, subaccountstypes.SubaccountId, uint32, *big.Int) error); ok { - r1 = rf(ctx, subaccountId, perpetualId, deltaQuantums) + if rf, ok := ret.Get(1).(func(types.Context, subaccountstypes.SubaccountId, uint32, *big.Int, bool) error); ok { + r1 = rf(ctx, subaccountId, perpetualId, deltaQuantums, isFinalSettlement) } else { r1 = ret.Error(1) } @@ -90,13 +96,16 @@ func (_m *MemClob) GetCancelOrder(ctx types.Context, orderId clobtypes.OrderId) ret := _m.Called(ctx, orderId) var r0 uint32 + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context, clobtypes.OrderId) (uint32, bool)); ok { + return rf(ctx, orderId) + } if rf, ok := ret.Get(0).(func(types.Context, clobtypes.OrderId) uint32); ok { r0 = rf(ctx, orderId) } else { r0 = ret.Get(0).(uint32) } - var r1 bool if rf, ok := ret.Get(1).(func(types.Context, clobtypes.OrderId) bool); ok { r1 = rf(ctx, orderId) } else { @@ -111,27 +120,30 @@ func (_m *MemClob) GetMidPrice(ctx types.Context, clobPairId clobtypes.ClobPairI ret := _m.Called(ctx, clobPairId) var r0 clobtypes.Subticks + var r1 clobtypes.Order + var r2 clobtypes.Order + var r3 bool + if rf, ok := ret.Get(0).(func(types.Context, clobtypes.ClobPairId) (clobtypes.Subticks, clobtypes.Order, clobtypes.Order, bool)); ok { + return rf(ctx, clobPairId) + } if rf, ok := ret.Get(0).(func(types.Context, clobtypes.ClobPairId) clobtypes.Subticks); ok { r0 = rf(ctx, clobPairId) } else { r0 = ret.Get(0).(clobtypes.Subticks) } - var r1 clobtypes.Order if rf, ok := ret.Get(1).(func(types.Context, clobtypes.ClobPairId) clobtypes.Order); ok { r1 = rf(ctx, clobPairId) } else { r1 = ret.Get(1).(clobtypes.Order) } - var r2 clobtypes.Order if rf, ok := ret.Get(2).(func(types.Context, clobtypes.ClobPairId) clobtypes.Order); ok { r2 = rf(ctx, clobPairId) } else { r2 = ret.Get(2).(clobtypes.Order) } - var r3 bool if rf, ok := ret.Get(3).(func(types.Context, clobtypes.ClobPairId) bool); ok { r3 = rf(ctx, clobPairId) } else { @@ -162,6 +174,10 @@ func (_m *MemClob) GetOperationsToReplay(ctx types.Context) ([]clobtypes.Interna ret := _m.Called(ctx) var r0 []clobtypes.InternalOperation + var r1 map[clobtypes.OrderHash][]byte + if rf, ok := ret.Get(0).(func(types.Context) ([]clobtypes.InternalOperation, map[clobtypes.OrderHash][]byte)); ok { + return rf(ctx) + } if rf, ok := ret.Get(0).(func(types.Context) []clobtypes.InternalOperation); ok { r0 = rf(ctx) } else { @@ -170,7 +186,6 @@ func (_m *MemClob) GetOperationsToReplay(ctx types.Context) ([]clobtypes.Interna } } - var r1 map[clobtypes.OrderHash][]byte if rf, ok := ret.Get(1).(func(types.Context) map[clobtypes.OrderHash][]byte); ok { r1 = rf(ctx) } else { @@ -187,13 +202,16 @@ func (_m *MemClob) GetOrder(ctx types.Context, orderId clobtypes.OrderId) (clobt ret := _m.Called(ctx, orderId) var r0 clobtypes.Order + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context, clobtypes.OrderId) (clobtypes.Order, bool)); ok { + return rf(ctx, orderId) + } if rf, ok := ret.Get(0).(func(types.Context, clobtypes.OrderId) clobtypes.Order); ok { r0 = rf(ctx, orderId) } else { r0 = ret.Get(0).(clobtypes.Order) } - var r1 bool if rf, ok := ret.Get(1).(func(types.Context, clobtypes.OrderId) bool); ok { r1 = rf(ctx, orderId) } else { @@ -222,13 +240,16 @@ func (_m *MemClob) GetOrderRemainingAmount(ctx types.Context, order clobtypes.Or ret := _m.Called(ctx, order) var r0 subaccountstypes.BaseQuantums + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context, clobtypes.Order) (subaccountstypes.BaseQuantums, bool)); ok { + return rf(ctx, order) + } if rf, ok := ret.Get(0).(func(types.Context, clobtypes.Order) subaccountstypes.BaseQuantums); ok { r0 = rf(ctx, order) } else { r0 = ret.Get(0).(subaccountstypes.BaseQuantums) } - var r1 bool if rf, ok := ret.Get(1).(func(types.Context, clobtypes.Order) bool); ok { r1 = rf(ctx, order) } else { @@ -243,13 +264,16 @@ func (_m *MemClob) GetPricePremium(ctx types.Context, clobPair clobtypes.ClobPai ret := _m.Called(ctx, clobPair, params) var r0 int32 + var r1 error + if rf, ok := ret.Get(0).(func(types.Context, clobtypes.ClobPair, perpetualstypes.GetPricePremiumParams) (int32, error)); ok { + return rf(ctx, clobPair, params) + } if rf, ok := ret.Get(0).(func(types.Context, clobtypes.ClobPair, perpetualstypes.GetPricePremiumParams) int32); ok { r0 = rf(ctx, clobPair, params) } else { r0 = ret.Get(0).(int32) } - var r1 error if rf, ok := ret.Get(1).(func(types.Context, clobtypes.ClobPair, perpetualstypes.GetPricePremiumParams) error); ok { r1 = rf(ctx, clobPair, params) } else { @@ -264,6 +288,10 @@ func (_m *MemClob) GetSubaccountOrders(ctx types.Context, clobPairId clobtypes.C ret := _m.Called(ctx, clobPairId, subaccountId, side) var r0 []clobtypes.Order + var r1 error + if rf, ok := ret.Get(0).(func(types.Context, clobtypes.ClobPairId, subaccountstypes.SubaccountId, clobtypes.Order_Side) ([]clobtypes.Order, error)); ok { + return rf(ctx, clobPairId, subaccountId, side) + } if rf, ok := ret.Get(0).(func(types.Context, clobtypes.ClobPairId, subaccountstypes.SubaccountId, clobtypes.Order_Side) []clobtypes.Order); ok { r0 = rf(ctx, clobPairId, subaccountId, side) } else { @@ -272,7 +300,6 @@ func (_m *MemClob) GetSubaccountOrders(ctx types.Context, clobPairId clobtypes.C } } - var r1 error if rf, ok := ret.Get(1).(func(types.Context, clobtypes.ClobPairId, subaccountstypes.SubaccountId, clobtypes.Order_Side) error); ok { r1 = rf(ctx, clobPairId, subaccountId, side) } else { @@ -287,20 +314,24 @@ func (_m *MemClob) PlaceOrder(ctx types.Context, order clobtypes.Order) (subacco ret := _m.Called(ctx, order) var r0 subaccountstypes.BaseQuantums + var r1 clobtypes.OrderStatus + var r2 *clobtypes.OffchainUpdates + var r3 error + if rf, ok := ret.Get(0).(func(types.Context, clobtypes.Order) (subaccountstypes.BaseQuantums, clobtypes.OrderStatus, *clobtypes.OffchainUpdates, error)); ok { + return rf(ctx, order) + } if rf, ok := ret.Get(0).(func(types.Context, clobtypes.Order) subaccountstypes.BaseQuantums); ok { r0 = rf(ctx, order) } else { r0 = ret.Get(0).(subaccountstypes.BaseQuantums) } - var r1 clobtypes.OrderStatus if rf, ok := ret.Get(1).(func(types.Context, clobtypes.Order) clobtypes.OrderStatus); ok { r1 = rf(ctx, order) } else { r1 = ret.Get(1).(clobtypes.OrderStatus) } - var r2 *clobtypes.OffchainUpdates if rf, ok := ret.Get(2).(func(types.Context, clobtypes.Order) *clobtypes.OffchainUpdates); ok { r2 = rf(ctx, order) } else { @@ -309,7 +340,6 @@ func (_m *MemClob) PlaceOrder(ctx types.Context, order clobtypes.Order) (subacco } } - var r3 error if rf, ok := ret.Get(3).(func(types.Context, clobtypes.Order) error); ok { r3 = rf(ctx, order) } else { @@ -324,20 +354,24 @@ func (_m *MemClob) PlacePerpetualLiquidation(ctx types.Context, liquidationOrder ret := _m.Called(ctx, liquidationOrder) var r0 subaccountstypes.BaseQuantums + var r1 clobtypes.OrderStatus + var r2 *clobtypes.OffchainUpdates + var r3 error + if rf, ok := ret.Get(0).(func(types.Context, clobtypes.LiquidationOrder) (subaccountstypes.BaseQuantums, clobtypes.OrderStatus, *clobtypes.OffchainUpdates, error)); ok { + return rf(ctx, liquidationOrder) + } if rf, ok := ret.Get(0).(func(types.Context, clobtypes.LiquidationOrder) subaccountstypes.BaseQuantums); ok { r0 = rf(ctx, liquidationOrder) } else { r0 = ret.Get(0).(subaccountstypes.BaseQuantums) } - var r1 clobtypes.OrderStatus if rf, ok := ret.Get(1).(func(types.Context, clobtypes.LiquidationOrder) clobtypes.OrderStatus); ok { r1 = rf(ctx, liquidationOrder) } else { r1 = ret.Get(1).(clobtypes.OrderStatus) } - var r2 *clobtypes.OffchainUpdates if rf, ok := ret.Get(2).(func(types.Context, clobtypes.LiquidationOrder) *clobtypes.OffchainUpdates); ok { r2 = rf(ctx, liquidationOrder) } else { @@ -346,7 +380,6 @@ func (_m *MemClob) PlacePerpetualLiquidation(ctx types.Context, liquidationOrder } } - var r3 error if rf, ok := ret.Get(3).(func(types.Context, clobtypes.LiquidationOrder) error); ok { r3 = rf(ctx, liquidationOrder) } else { diff --git a/protocol/mocks/MemClobKeeper.go b/protocol/mocks/MemClobKeeper.go index 88ffde9f52..5139e4b877 100644 --- a/protocol/mocks/MemClobKeeper.go +++ b/protocol/mocks/MemClobKeeper.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.26.1. DO NOT EDIT. package mocks @@ -29,13 +29,16 @@ func (_m *MemClobKeeper) AddOrderToOrderbookCollatCheck(ctx types.Context, clobP ret := _m.Called(ctx, clobPairId, subaccountOpenOrders) 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) } - var r1 map[subaccountstypes.SubaccountId]subaccountstypes.UpdateResult 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 { @@ -52,20 +55,24 @@ func (_m *MemClobKeeper) AddPreexistingStatefulOrder(ctx types.Context, order *c ret := _m.Called(ctx, order, memclob) var r0 subaccountstypes.BaseQuantums + var r1 clobtypes.OrderStatus + var r2 *clobtypes.OffchainUpdates + var r3 error + if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.Order, clobtypes.MemClob) (subaccountstypes.BaseQuantums, clobtypes.OrderStatus, *clobtypes.OffchainUpdates, error)); ok { + return rf(ctx, order, memclob) + } if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.Order, clobtypes.MemClob) subaccountstypes.BaseQuantums); ok { r0 = rf(ctx, order, memclob) } else { r0 = ret.Get(0).(subaccountstypes.BaseQuantums) } - var r1 clobtypes.OrderStatus if rf, ok := ret.Get(1).(func(types.Context, *clobtypes.Order, clobtypes.MemClob) clobtypes.OrderStatus); ok { r1 = rf(ctx, order, memclob) } else { r1 = ret.Get(1).(clobtypes.OrderStatus) } - var r2 *clobtypes.OffchainUpdates if rf, ok := ret.Get(2).(func(types.Context, *clobtypes.Order, clobtypes.MemClob) *clobtypes.OffchainUpdates); ok { r2 = rf(ctx, order, memclob) } else { @@ -74,7 +81,6 @@ func (_m *MemClobKeeper) AddPreexistingStatefulOrder(ctx types.Context, order *c } } - var r3 error if rf, ok := ret.Get(3).(func(types.Context, *clobtypes.Order, clobtypes.MemClob) error); ok { r3 = rf(ctx, order, memclob) } else { @@ -84,25 +90,35 @@ func (_m *MemClobKeeper) AddPreexistingStatefulOrder(ctx types.Context, order *c return r0, r1, r2, r3 } -// CanDeleverageSubaccount provides a mock function with given fields: ctx, subaccountId -func (_m *MemClobKeeper) CanDeleverageSubaccount(ctx types.Context, subaccountId subaccountstypes.SubaccountId) (bool, error) { - ret := _m.Called(ctx, subaccountId) +// CanDeleverageSubaccount provides a mock function with given fields: ctx, subaccountId, perpetualId +func (_m *MemClobKeeper) CanDeleverageSubaccount(ctx types.Context, subaccountId subaccountstypes.SubaccountId, perpetualId uint32) (bool, bool, error) { + ret := _m.Called(ctx, subaccountId, perpetualId) var r0 bool - if rf, ok := ret.Get(0).(func(types.Context, subaccountstypes.SubaccountId) bool); ok { - r0 = rf(ctx, subaccountId) + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(types.Context, subaccountstypes.SubaccountId, uint32) (bool, bool, error)); ok { + return rf(ctx, subaccountId, perpetualId) + } + if rf, ok := ret.Get(0).(func(types.Context, subaccountstypes.SubaccountId, uint32) bool); ok { + r0 = rf(ctx, subaccountId, perpetualId) } else { r0 = ret.Get(0).(bool) } - var r1 error - if rf, ok := ret.Get(1).(func(types.Context, subaccountstypes.SubaccountId) error); ok { - r1 = rf(ctx, subaccountId) + if rf, ok := ret.Get(1).(func(types.Context, subaccountstypes.SubaccountId, uint32) bool); ok { + r1 = rf(ctx, subaccountId, perpetualId) } else { - r1 = ret.Error(1) + r1 = ret.Get(1).(bool) } - return r0, r1 + if rf, ok := ret.Get(2).(func(types.Context, subaccountstypes.SubaccountId, uint32) error); ok { + r2 = rf(ctx, subaccountId, perpetualId) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 } // CancelShortTermOrder provides a mock function with given fields: ctx, msgCancelOrder @@ -140,13 +156,16 @@ func (_m *MemClobKeeper) GetLongTermOrderPlacement(ctx types.Context, orderId cl ret := _m.Called(ctx, orderId) var r0 clobtypes.LongTermOrderPlacement + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context, clobtypes.OrderId) (clobtypes.LongTermOrderPlacement, bool)); ok { + return rf(ctx, orderId) + } if rf, ok := ret.Get(0).(func(types.Context, clobtypes.OrderId) clobtypes.LongTermOrderPlacement); ok { r0 = rf(ctx, orderId) } else { r0 = ret.Get(0).(clobtypes.LongTermOrderPlacement) } - var r1 bool if rf, ok := ret.Get(1).(func(types.Context, clobtypes.OrderId) bool); ok { r1 = rf(ctx, orderId) } else { @@ -161,20 +180,23 @@ func (_m *MemClobKeeper) GetOrderFillAmount(ctx types.Context, orderId clobtypes ret := _m.Called(ctx, orderId) var r0 bool + var r1 subaccountstypes.BaseQuantums + var r2 uint32 + if rf, ok := ret.Get(0).(func(types.Context, clobtypes.OrderId) (bool, subaccountstypes.BaseQuantums, uint32)); ok { + return rf(ctx, orderId) + } if rf, ok := ret.Get(0).(func(types.Context, clobtypes.OrderId) bool); ok { r0 = rf(ctx, orderId) } else { r0 = ret.Get(0).(bool) } - var r1 subaccountstypes.BaseQuantums if rf, ok := ret.Get(1).(func(types.Context, clobtypes.OrderId) subaccountstypes.BaseQuantums); ok { r1 = rf(ctx, orderId) } else { r1 = ret.Get(1).(subaccountstypes.BaseQuantums) } - var r2 uint32 if rf, ok := ret.Get(2).(func(types.Context, clobtypes.OrderId) uint32); ok { r2 = rf(ctx, orderId) } else { @@ -205,13 +227,16 @@ func (_m *MemClobKeeper) IsLiquidatable(ctx types.Context, subaccountId subaccou ret := _m.Called(ctx, subaccountId) var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(types.Context, subaccountstypes.SubaccountId) (bool, error)); ok { + return rf(ctx, subaccountId) + } if rf, ok := ret.Get(0).(func(types.Context, subaccountstypes.SubaccountId) bool); ok { r0 = rf(ctx, subaccountId) } else { r0 = ret.Get(0).(bool) } - var r1 error if rf, ok := ret.Get(1).(func(types.Context, subaccountstypes.SubaccountId) error); ok { r1 = rf(ctx, subaccountId) } else { @@ -242,22 +267,25 @@ func (_m *MemClobKeeper) MustAddOrderToStatefulOrdersTimeSlice(ctx types.Context _m.Called(ctx, goodTilBlockTime, orderId) } -// OffsetSubaccountPerpetualPosition provides a mock function with given fields: ctx, liquidatedSubaccountId, perpetualId, deltaQuantumsTotal -func (_m *MemClobKeeper) OffsetSubaccountPerpetualPosition(ctx types.Context, liquidatedSubaccountId subaccountstypes.SubaccountId, perpetualId uint32, deltaQuantumsTotal *big.Int) ([]clobtypes.MatchPerpetualDeleveraging_Fill, *big.Int) { - ret := _m.Called(ctx, liquidatedSubaccountId, perpetualId, deltaQuantumsTotal) +// OffsetSubaccountPerpetualPosition provides a mock function with given fields: ctx, liquidatedSubaccountId, perpetualId, deltaQuantumsTotal, isFinalSettlement +func (_m *MemClobKeeper) OffsetSubaccountPerpetualPosition(ctx types.Context, liquidatedSubaccountId subaccountstypes.SubaccountId, perpetualId uint32, deltaQuantumsTotal *big.Int, isFinalSettlement bool) ([]clobtypes.MatchPerpetualDeleveraging_Fill, *big.Int) { + ret := _m.Called(ctx, liquidatedSubaccountId, perpetualId, deltaQuantumsTotal, isFinalSettlement) var r0 []clobtypes.MatchPerpetualDeleveraging_Fill - if rf, ok := ret.Get(0).(func(types.Context, subaccountstypes.SubaccountId, uint32, *big.Int) []clobtypes.MatchPerpetualDeleveraging_Fill); ok { - r0 = rf(ctx, liquidatedSubaccountId, perpetualId, deltaQuantumsTotal) + var r1 *big.Int + if rf, ok := ret.Get(0).(func(types.Context, subaccountstypes.SubaccountId, uint32, *big.Int, bool) ([]clobtypes.MatchPerpetualDeleveraging_Fill, *big.Int)); ok { + return rf(ctx, liquidatedSubaccountId, perpetualId, deltaQuantumsTotal, isFinalSettlement) + } + if rf, ok := ret.Get(0).(func(types.Context, subaccountstypes.SubaccountId, uint32, *big.Int, bool) []clobtypes.MatchPerpetualDeleveraging_Fill); ok { + r0 = rf(ctx, liquidatedSubaccountId, perpetualId, deltaQuantumsTotal, isFinalSettlement) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]clobtypes.MatchPerpetualDeleveraging_Fill) } } - var r1 *big.Int - if rf, ok := ret.Get(1).(func(types.Context, subaccountstypes.SubaccountId, uint32, *big.Int) *big.Int); ok { - r1 = rf(ctx, liquidatedSubaccountId, perpetualId, deltaQuantumsTotal) + if rf, ok := ret.Get(1).(func(types.Context, subaccountstypes.SubaccountId, uint32, *big.Int, bool) *big.Int); ok { + r1 = rf(ctx, liquidatedSubaccountId, perpetualId, deltaQuantumsTotal, isFinalSettlement) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*big.Int) @@ -272,27 +300,31 @@ func (_m *MemClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders * ret := _m.Called(ctx, matchWithOrders) var r0 bool + var r1 subaccountstypes.UpdateResult + var r2 subaccountstypes.UpdateResult + var r3 *clobtypes.OffchainUpdates + var r4 error + if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *clobtypes.OffchainUpdates, error)); ok { + return rf(ctx, matchWithOrders) + } if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders) bool); ok { r0 = rf(ctx, matchWithOrders) } else { r0 = ret.Get(0).(bool) } - var r1 subaccountstypes.UpdateResult if rf, ok := ret.Get(1).(func(types.Context, *clobtypes.MatchWithOrders) subaccountstypes.UpdateResult); ok { r1 = rf(ctx, matchWithOrders) } else { r1 = ret.Get(1).(subaccountstypes.UpdateResult) } - var r2 subaccountstypes.UpdateResult if rf, ok := ret.Get(2).(func(types.Context, *clobtypes.MatchWithOrders) subaccountstypes.UpdateResult); ok { r2 = rf(ctx, matchWithOrders) } else { r2 = ret.Get(2).(subaccountstypes.UpdateResult) } - var r3 *clobtypes.OffchainUpdates if rf, ok := ret.Get(3).(func(types.Context, *clobtypes.MatchWithOrders) *clobtypes.OffchainUpdates); ok { r3 = rf(ctx, matchWithOrders) } else { @@ -301,7 +333,6 @@ func (_m *MemClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders * } } - var r4 error if rf, ok := ret.Get(4).(func(types.Context, *clobtypes.MatchWithOrders) error); ok { r4 = rf(ctx, matchWithOrders) } else { @@ -316,20 +347,24 @@ func (_m *MemClobKeeper) ReplayPlaceOrder(ctx types.Context, msg *clobtypes.MsgP ret := _m.Called(ctx, msg) var r0 subaccountstypes.BaseQuantums + var r1 clobtypes.OrderStatus + var r2 *clobtypes.OffchainUpdates + var r3 error + if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MsgPlaceOrder) (subaccountstypes.BaseQuantums, clobtypes.OrderStatus, *clobtypes.OffchainUpdates, error)); ok { + return rf(ctx, msg) + } if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MsgPlaceOrder) subaccountstypes.BaseQuantums); ok { r0 = rf(ctx, msg) } else { r0 = ret.Get(0).(subaccountstypes.BaseQuantums) } - var r1 clobtypes.OrderStatus if rf, ok := ret.Get(1).(func(types.Context, *clobtypes.MsgPlaceOrder) clobtypes.OrderStatus); ok { r1 = rf(ctx, msg) } else { r1 = ret.Get(1).(clobtypes.OrderStatus) } - var r2 *clobtypes.OffchainUpdates if rf, ok := ret.Get(2).(func(types.Context, *clobtypes.MsgPlaceOrder) *clobtypes.OffchainUpdates); ok { r2 = rf(ctx, msg) } else { @@ -338,7 +373,6 @@ func (_m *MemClobKeeper) ReplayPlaceOrder(ctx types.Context, msg *clobtypes.MsgP } } - var r3 error if rf, ok := ret.Get(3).(func(types.Context, *clobtypes.MsgPlaceOrder) error); ok { r3 = rf(ctx, msg) } else { diff --git a/protocol/testutil/constants/clob_pair.go b/protocol/testutil/constants/clob_pair.go index f1fc88945a..10b42e79bf 100644 --- a/protocol/testutil/constants/clob_pair.go +++ b/protocol/testutil/constants/clob_pair.go @@ -103,6 +103,18 @@ var ( QuantumConversionExponent: -8, Status: clobtypes.ClobPair_STATUS_INITIALIZING, } + ClobPair_Btc_Final_Settlement = clobtypes.ClobPair{ + Id: 0, + Metadata: &clobtypes.ClobPair_PerpetualClobMetadata{ + PerpetualClobMetadata: &clobtypes.PerpetualClobMetadata{ + PerpetualId: 0, + }, + }, + StepBaseQuantums: 5, + SubticksPerTick: 5, + QuantumConversionExponent: -8, + Status: clobtypes.ClobPair_STATUS_FINAL_SETTLEMENT, + } ClobPair_Btc_Paused = clobtypes.ClobPair{ Id: 0, Metadata: &clobtypes.ClobPair_PerpetualClobMetadata{ diff --git a/protocol/testutil/memclob/keeper.go b/protocol/testutil/memclob/keeper.go index 5508cd8edc..3cc9df6377 100644 --- a/protocol/testutil/memclob/keeper.go +++ b/protocol/testutil/memclob/keeper.go @@ -108,11 +108,13 @@ func (f *FakeMemClobKeeper) CancelShortTermOrder( func (f *FakeMemClobKeeper) CanDeleverageSubaccount( ctx sdk.Context, msg satypes.SubaccountId, + perpetualId uint32, ) ( + bool, bool, error, ) { - return f.subaccountsToDeleverage[msg], nil + return f.subaccountsToDeleverage[msg], false, nil } // Commit simulates `checkState.Commit()`. @@ -463,6 +465,7 @@ func (f *FakeMemClobKeeper) OffsetSubaccountPerpetualPosition( liquidatedSubaccountId satypes.SubaccountId, perpetualId uint32, deltaQuantumsTotal *big.Int, + isFinalSettlement bool, ) ( fills []types.MatchPerpetualDeleveraging_Fill, deltaQuantumsRemaining *big.Int, diff --git a/protocol/x/clob/abci.go b/protocol/x/clob/abci.go index f266ecc732..9739e24160 100644 --- a/protocol/x/clob/abci.go +++ b/protocol/x/clob/abci.go @@ -197,8 +197,26 @@ func PrepareCheckState( } // 6. Get all potentially liquidatable subaccount IDs and attempt to liquidate them. - subaccountIds := daemonLiquidationInfo.GetLiquidatableSubaccountIds() - if err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, subaccountIds); err != nil { + liquidatableSubaccountIds := daemonLiquidationInfo.GetLiquidatableSubaccountIds() + subaccountsToDeleverage, err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, liquidatableSubaccountIds) + if err != nil { + panic(err) + } + subaccountPositionInfo := daemonLiquidationInfo.GetSubaccountsWithPositions() + // Add subaccounts with open positions in final settlement markets to the slice of subaccounts/perps + // to be deleveraged. + subaccountsToDeleverage = append( + subaccountsToDeleverage, + keeper.GetSubaccountsWithPositionsInFinalSettlementMarkets( + ctx, + subaccountPositionInfo, + )..., + ) + + // 7. Deleverage subaccounts. + // TODO(CLOB-1052) - decouple steps 6 and 7 by using DaemonLiquidationInfo.NegativeTncSubaccounts + // as the input for this function. + if err := keeper.DeleverageSubaccounts(ctx, subaccountsToDeleverage); err != nil { panic(err) } diff --git a/protocol/x/clob/e2e/liquidation_deleveraging_test.go b/protocol/x/clob/e2e/liquidation_deleveraging_test.go index abded06f7b..ac9de31a7a 100644 --- a/protocol/x/clob/e2e/liquidation_deleveraging_test.go +++ b/protocol/x/clob/e2e/liquidation_deleveraging_test.go @@ -645,6 +645,7 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { // Parameters. placedMatchableOrders []clobtypes.MatchableOrder liquidatableSubaccountIds []satypes.SubaccountId + subaccountPositionInfo []clobtypes.SubaccountOpenPositionInfo // Configuration. liquidationConfig clobtypes.LiquidationsConfig @@ -1040,6 +1041,97 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { }, }, }, + `Deleveraging occurs at bankruptcy price for negative TNC subaccount with open position in final settlement market`: { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_50499USD, + constants.Dave_Num0_1BTC_Long_50000USD, + }, + subaccountPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { + PerpetualId: constants.BtcUsd_20PercentInitial_10PercentMaintenance.GetId(), + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Dave_Num0, + }, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Carl_Num0, + }, + }, + }, + + marketIdToOraclePriceOverride: map[uint32]uint64{ + constants.BtcUsd.MarketId: 5_050_000_000, // $50,500 / BTC + }, + // Account should be deleveraged regardless of whether or not the liquidations engine returns this subaccount + // in the list of liquidatable subaccounts. Pass empty list to confirm this. + liquidatableSubaccountIds: []satypes.SubaccountId{}, + liquidationConfig: constants.LiquidationsConfig_FillablePrice_Max_Smmr, + liquidityTiers: constants.LiquidityTiers, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + clobPairs: []clobtypes.ClobPair{constants.ClobPair_Btc_Final_Settlement}, + + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + }, + { + Id: &constants.Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(50_000_000_000 + 50_499_000_000), + }, + }, + }, + }, + }, + `Deleveraging occurs at oracle price for non-negative TNC subaccounts + with open positions in final settlement market`: { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_100000USD, + constants.Dave_Num0_1BTC_Long_50000USD, + }, + subaccountPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { + PerpetualId: constants.BtcUsd_20PercentInitial_10PercentMaintenance.GetId(), + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Dave_Num0, + }, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Carl_Num0, + }, + }, + }, + liquidatableSubaccountIds: []satypes.SubaccountId{}, + liquidationConfig: constants.LiquidationsConfig_FillablePrice_Max_Smmr, + liquidityTiers: constants.LiquidityTiers, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + clobPairs: []clobtypes.ClobPair{constants.ClobPair_Btc_Final_Settlement}, + + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(100_000_000_000 - 50_000_000_000), + }, + }, + }, + { + Id: &constants.Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(50_000_000_000 + 50_000_000_000), + }, + }, + }, + }, + }, } for name, tc := range tests { @@ -1123,7 +1215,8 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { } _, err := tApp.App.Server.LiquidateSubaccounts(ctx, &api.LiquidateSubaccountsRequest{ - LiquidatableSubaccountIds: tc.liquidatableSubaccountIds, + LiquidatableSubaccountIds: tc.liquidatableSubaccountIds, + SubaccountOpenPositionInfo: tc.subaccountPositionInfo, }) require.NoError(t, err) diff --git a/protocol/x/clob/keeper/clob_pair.go b/protocol/x/clob/keeper/clob_pair.go index 22b2e6e331..4af9d4a436 100644 --- a/protocol/x/clob/keeper/clob_pair.go +++ b/protocol/x/clob/keeper/clob_pair.go @@ -304,6 +304,42 @@ func (k Keeper) GetAllClobPairs(ctx sdk.Context) (list []types.ClobPair) { return } +// validateLiquidationAgainstClobPairStatus returns an error if placing the provided +// liquidation order would conflict with the clob pair's current status. +func (k Keeper) validateLiquidationAgainstClobPairStatus( + ctx sdk.Context, + liquidationOrder types.LiquidationOrder, +) error { + clobPair, found := k.GetClobPair(ctx, liquidationOrder.GetClobPairId()) + if !found { + return errorsmod.Wrapf( + types.ErrInvalidClob, + "Clob %v is not a valid clob", + liquidationOrder.GetClobPairId(), + ) + } + + if !types.IsSupportedClobPairStatus(clobPair.Status) { + panic( + fmt.Sprintf( + "validateLiquidationAgainstClobPairStatus: clob pair status %v is not supported", + clobPair.Status, + ), + ) + } + + if clobPair.Status != types.ClobPair_STATUS_ACTIVE { + return errorsmod.Wrapf( + types.ErrLiquidationConflictsWithClobPairStatus, + "Liquidation order %+v cannot be placed for clob pair with status %+v", + liquidationOrder, + clobPair.Status, + ) + } + + return nil +} + // validateOrderAgainstClobPairStatus returns an error if placing the provided // order would conflict with the clob pair's current status. func (k Keeper) validateOrderAgainstClobPairStatus( @@ -373,6 +409,13 @@ func (k Keeper) validateOrderAgainstClobPairStatus( clobPair.Status, ) } + case types.ClobPair_STATUS_FINAL_SETTLEMENT: + return errorsmod.Wrapf( + types.ErrOrderConflictsWithClobPairStatus, + "Order %+v disallowed, trading is disabled for clob pair with status %+v", + order, + clobPair.Status, + ) } return nil @@ -468,7 +511,7 @@ func (k Keeper) UpdateClobPair( oldStatus := oldClobPair.Status newStatus := clobPair.Status - if oldStatus != newStatus && !types.IsSupportedClobPairStatusTransition(oldStatus, newStatus) { + if !types.IsSupportedClobPairStatusTransition(oldStatus, newStatus) { return errorsmod.Wrapf( types.ErrInvalidClobPairStatusTransition, "Cannot transition from status %+v to status %+v", @@ -499,6 +542,11 @@ func (k Keeper) UpdateClobPair( ), ) + // If newly transitioning to final settlement, enter final settlement. + if newStatus == types.ClobPair_STATUS_FINAL_SETTLEMENT && oldStatus != newStatus { + k.mustTransitionToFinalSettlement(ctx, clobPair.GetClobPairId()) + } + return nil } @@ -589,6 +637,19 @@ func (k Keeper) validateInternalOperationAgainstClobPairStatus( clobPairId, types.ClobPair_STATUS_INITIALIZING, ) + case types.ClobPair_STATUS_FINAL_SETTLEMENT: + // Only allow deleveraging events. This allows the protocol to close out open + // positions in the market. All other operations are not allowed. + if match := internalOperation.GetMatch(); match != nil && match.GetMatchPerpetualDeleveraging() != nil { + return nil + } + return errorsmod.Wrapf( + types.ErrOperationConflictsWithClobPairStatus, + "Operation %s invalid for ClobPair with id %d with status %s", + internalOperation.GetInternalOperationTextString(), + clobPairId, + types.ClobPair_STATUS_FINAL_SETTLEMENT, + ) } return nil @@ -609,7 +670,7 @@ func (k Keeper) IsPerpetualClobPairActive( if !found { return false, errorsmod.Wrapf( types.ErrInvalidClob, - "GetPerpetualClobPairStatus: did not find clob pair with id = %d", + "IsPerpetualClobPairActive: did not find clob pair with id = %d", clobPairId, ) } diff --git a/protocol/x/clob/keeper/clob_pair_test.go b/protocol/x/clob/keeper/clob_pair_test.go index 449af51299..3005f4346f 100644 --- a/protocol/x/clob/keeper/clob_pair_test.go +++ b/protocol/x/clob/keeper/clob_pair_test.go @@ -7,6 +7,7 @@ import ( indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events" "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" + indexershared "github.com/dydxprotocol/v4-chain/protocol/indexer/shared" "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/cosmos/cosmos-sdk/codec" @@ -19,6 +20,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/testutil/nullify" perptest "github.com/dydxprotocol/v4-chain/protocol/testutil/perpetuals" pricestest "github.com/dydxprotocol/v4-chain/protocol/testutil/prices" + "github.com/dydxprotocol/v4-chain/protocol/x/clob/keeper" "github.com/dydxprotocol/v4-chain/protocol/x/clob/memclob" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals" @@ -597,6 +599,121 @@ func TestClobPairGetAll(t *testing.T) { ) } +func TestUpdateClobPair_FinalSettlement(t *testing.T) { + memClob := memclob.NewMemClobPriceTimePriority(false) + mockIndexerEventManager := &mocks.IndexerEventManager{} + ks := keepertest.NewClobKeepersTestContext(t, memClob, &mocks.BankKeeper{}, mockIndexerEventManager) + prices.InitGenesis(ks.Ctx, *ks.PricesKeeper, constants.Prices_DefaultGenesisState) + perpetuals.InitGenesis(ks.Ctx, *ks.PerpetualsKeeper, constants.Perpetuals_DefaultGenesisState) + + clobPair := constants.ClobPair_Btc + mockIndexerEventManager.On("AddTxnEvent", + ks.Ctx, + indexerevents.SubtypePerpetualMarket, + indexerevents.PerpetualMarketEventVersion, + indexer_manager.GetBytes( + indexerevents.NewPerpetualMarketCreateEvent( + 0, + 0, + constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.Ticker, + constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.MarketId, + types.ClobPair_STATUS_ACTIVE, + clobPair.QuantumConversionExponent, + constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.AtomicResolution, + clobPair.SubticksPerTick, + clobPair.StepBaseQuantums, + constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.LiquidityTier, + ), + ), + ).Once().Return() + + _, err := ks.ClobKeeper.CreatePerpetualClobPair( + ks.Ctx, + clobPair.Id, + clobtest.MustPerpetualId(clobPair), + satypes.BaseQuantums(clobPair.StepBaseQuantums), + clobPair.QuantumConversionExponent, + clobPair.SubticksPerTick, + types.ClobPair_STATUS_ACTIVE, + ) + require.NoError(t, err) + + mockIndexerEventManager.On("AddTxnEvent", + ks.Ctx, + indexerevents.SubtypeUpdateClobPair, + indexerevents.UpdateClobPairEventVersion, + indexer_manager.GetBytes( + indexerevents.NewUpdateClobPairEvent( + clobPair.GetClobPairId(), + types.ClobPair_STATUS_FINAL_SETTLEMENT, + clobPair.QuantumConversionExponent, + types.SubticksPerTick(clobPair.GetSubticksPerTick()), + satypes.BaseQuantums(clobPair.GetStepBaseQuantums()), + ), + ), + ).Once().Return() + + statefulOrders := []types.Order{ + constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy100_Price10_GTBT15, + constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10, + constants.LongTermOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTBT5, // different clob pair + } + for _, order := range statefulOrders { + ks.ClobKeeper.SetLongTermOrderPlacement(ks.Ctx, order, 5) + ks.ClobKeeper.MustAddOrderToStatefulOrdersTimeSlice( + ks.Ctx, + order.MustGetUnixGoodTilBlockTime(), + order.GetOrderId(), + ) + + if order.OrderId.ClobPairId == 0 { + mockIndexerEventManager.On("AddTxnEvent", + ks.Ctx, + indexerevents.SubtypeStatefulOrder, + indexerevents.StatefulOrderEventVersion, + indexer_manager.GetBytes( + indexerevents.NewStatefulOrderRemovalEvent( + order.OrderId, + indexershared.OrderRemovalReason_ORDER_REMOVAL_REASON_FINAL_SETTLEMENT, + ), + ), + ).Once().Return() + } + } + + ks.ClobKeeper.UntriggeredConditionalOrders = map[types.ClobPairId]*keeper.UntriggeredConditionalOrders{ + 0: {}, // leaving blank, orders here don't matter in particular since we clear the whole key + } + + clobPair.Status = types.ClobPair_STATUS_FINAL_SETTLEMENT + err = ks.ClobKeeper.UpdateClobPair(ks.Ctx, clobPair) + require.NoError(t, err) + + // Verify indexer expectations. + mockIndexerEventManager.AssertExpectations(t) + + // Verify stateful orders are removed from state. + for _, order := range statefulOrders { + _, found := ks.ClobKeeper.GetLongTermOrderPlacement(ks.Ctx, order.OrderId) + require.Equal(t, order.OrderId.ClobPairId != 0, found) + } + + // Verify ProcessProposerMatchesEvents.RemovedStatefulOrderIds is populated correctly. + ppme := ks.ClobKeeper.GetProcessProposerMatchesEvents(ks.Ctx) + require.Equal( + t, + []types.OrderId{ + constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy100_Price10_GTBT15.OrderId, + constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10.OrderId, + }, + ppme.RemovedStatefulOrderIds, + ) + + // Verify UntriggeredConditionalOrders is cleared. + _, found := ks.ClobKeeper.UntriggeredConditionalOrders[0] + require.False(t, found) +} + func TestUpdateClobPair(t *testing.T) { testCases := map[string]struct { setup func(t *testing.T, ks keepertest.ClobKeepersTestContext, manager *mocks.IndexerEventManager) diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index 928e79cb49..f1b0e4b1c1 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -6,12 +6,12 @@ import ( "math/big" "time" - indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events" - "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" - errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" + indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events" + "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" assettypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" @@ -33,13 +33,18 @@ func (k Keeper) MaybeDeleverageSubaccount( ) { lib.AssertCheckTxMode(ctx) - canPerformDeleveraging, err := k.CanDeleverageSubaccount(ctx, subaccountId) + shouldDeleverageAtBankruptcyPrice, shouldDeleverageAtOraclePrice, err := k.CanDeleverageSubaccount( + ctx, + subaccountId, + perpetualId, + ) if err != nil { return new(big.Int), err } - // Early return to skip deleveraging if the subaccount can't be deleveraged. - if !canPerformDeleveraging { + // Early return to skip deleveraging if the subaccount doesn't have negative equity or a position in a final + // settlement market. + if !shouldDeleverageAtBankruptcyPrice && !shouldDeleverageAtOraclePrice { metrics.IncrCounter( metrics.ClobPrepareCheckStateCannotDeleverageSubaccount, 1, @@ -62,7 +67,13 @@ func (k Keeper) MaybeDeleverageSubaccount( } deltaQuantums := new(big.Int).Neg(position.GetBigQuantums()) - quantumsDeleveraged, err = k.MemClob.DeleverageSubaccount(ctx, subaccountId, perpetualId, deltaQuantums) + quantumsDeleveraged, err = k.MemClob.DeleverageSubaccount( + ctx, + subaccountId, + perpetualId, + deltaQuantums, + shouldDeleverageAtOraclePrice, + ) labels := []metrics.Label{ metrics.GetLabelForIntValue(metrics.PerpetualId, int(perpetualId)), @@ -137,13 +148,16 @@ func (k Keeper) GetInsuranceFundBalance( } // CanDeleverageSubaccount returns true if a subaccount can be deleveraged. -// Specifically, this function returns true if both of the following are true: -// - The subaccount's total net collateral is negative. -// This function returns an error if `GetNetCollateralAndMarginRequirements` returns an error. +// This function returns two booleans, shouldDeleverageAtBankruptcyPrice and shouldDeleverageAtOraclePrice. +// - shouldDeleverageAtBankruptcyPrice is true if the subaccount has negative TNC. +// - shouldDeleverageAtOraclePrice is true if the subaccount has non-negative TNC and the market is in final settlement. +// This function returns an error if `GetNetCollateralAndMarginRequirements` returns an error or if there is +// an error when fetching the clob pair for the provided perpetual. func (k Keeper) CanDeleverageSubaccount( ctx sdk.Context, subaccountId satypes.SubaccountId, -) (bool, error) { + perpetualId uint32, +) (shouldDeleverageAtBankruptcyPrice bool, shouldDeleverageAtOraclePrice bool, err error) { bigNetCollateral, _, _, @@ -152,16 +166,24 @@ func (k Keeper) CanDeleverageSubaccount( satypes.Update{SubaccountId: subaccountId}, ) if err != nil { - return false, err + return false, false, err } - // Deleveraging cannot be performed if the subaccounts net collateral is non-negative. - if bigNetCollateral.Sign() >= 0 { - return false, nil + // Negative TNC, deleverage at bankruptcy price. + if bigNetCollateral.Sign() == -1 { + return true, false, nil } - // The subaccount's total net collateral is negative, so deleveraging can be performed. - return true, nil + clobPairId, err := k.GetClobPairIdForPerpetual(ctx, perpetualId) + if err != nil { + return false, false, err + } + clobPair := k.mustGetClobPair(ctx, clobPairId) + + // Non-negative TNC, deleverage at oracle price if market is in final settlement. Deleveraging at oracle price + // is always a valid state transition when TNC is non-negative. This is because the TNC/TMMR ratio is improving; + // TNC is staying constant while TMMR is decreasing. + return false, clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT, nil } // IsValidInsuranceFundDelta returns true if the insurance fund has enough funds to cover the insurance @@ -194,6 +216,7 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( liquidatedSubaccountId satypes.SubaccountId, perpetualId uint32, deltaQuantumsTotal *big.Int, + isFinalSettlement bool, ) ( fills []types.MatchPerpetualDeleveraging_Fill, deltaQuantumsRemaining *big.Int, @@ -229,11 +252,11 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( } // TODO(DEC-1495): Determine max amount to offset per offsetting subaccount. - var deltaQuantums *big.Int + var deltaBaseQuantums *big.Int if deltaQuantumsRemaining.CmpAbs(bigOffsettingPositionQuantums) > 0 { - deltaQuantums = new(big.Int).Set(bigOffsettingPositionQuantums) + deltaBaseQuantums = new(big.Int).Set(bigOffsettingPositionQuantums) } else { - deltaQuantums = new(big.Int).Set(deltaQuantumsRemaining) + deltaBaseQuantums = new(big.Int).Set(deltaQuantumsRemaining) } // Fetch delta quote quantums. Calculated at bankruptcy price for standard @@ -242,7 +265,8 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( ctx, perpetualId, liquidatedSubaccountId, - deltaQuantums, + deltaBaseQuantums, + isFinalSettlement, ) if err != nil { liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) @@ -251,9 +275,10 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( "error", err, "blockHeight", ctx.BlockHeight(), "perpetualId", perpetualId, - "deltaQuantums", deltaQuantums, + "deltaBaseQuantums", deltaBaseQuantums, "liquidatedSubaccount", liquidatedSubaccount, "offsettingSubaccount", offsettingSubaccount, + "isFinalSettlement", isFinalSettlement, ) return false } @@ -264,18 +289,37 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( liquidatedSubaccountId, *offsettingSubaccount.Id, perpetualId, - deltaQuantums, + deltaBaseQuantums, deltaQuoteQuantums, ); err == nil { // Update the remaining liquidatable quantums. deltaQuantumsRemaining = new(big.Int).Sub( deltaQuantumsRemaining, - deltaQuantums, + deltaBaseQuantums, ) fills = append(fills, types.MatchPerpetualDeleveraging_Fill{ OffsettingSubaccountId: *offsettingSubaccount.Id, - FillAmount: new(big.Int).Abs(deltaQuantums).Uint64(), + FillAmount: new(big.Int).Abs(deltaBaseQuantums).Uint64(), }) + + // Send on-chain update for the deleveraging. The events are stored in a TransientStore which should be rolled-back + // if the branched state is discarded, so batching is not necessary. + k.GetIndexerEventManager().AddTxnEvent( + ctx, + indexerevents.SubtypeDeleveraging, + indexerevents.DeleveragingEventVersion, + indexer_manager.GetBytes( + indexerevents.NewDeleveragingEvent( + liquidatedSubaccountId, + *offsettingSubaccount.Id, + perpetualId, + satypes.BaseQuantums(new(big.Int).Abs(deltaBaseQuantums).Uint64()), + satypes.BaseQuantums(deltaQuoteQuantums.Uint64()), + deltaBaseQuantums.Sign() > 0, + isFinalSettlement, + ), + ), + ) } else if errors.Is(err, types.ErrInvalidPerpetualPositionSizeDelta) { panic( fmt.Sprintf( @@ -294,12 +338,13 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( "blockHeight", ctx.BlockHeight(), "checkTx", ctx.IsCheckTx(), "perpetualId", perpetualId, - "deltaQuantums", deltaQuantums, + "deltaQuantums", deltaBaseQuantums, "liquidatedSubaccount", liquidatedSubaccount, "offsettingSubaccount", offsettingSubaccount, ) numSubaccountsWithNonOverlappingBankruptcyPrices++ } + return deltaQuantumsRemaining.Sign() == 0 }, k.GetPseudoRand(ctx), @@ -330,28 +375,17 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( // getDeleveragingQuoteQuantums returns the quote quantums delta to apply to a deleveraging operation. // This returns the bankruptcy price for standard deleveraging operations, and the oracle price for -// final settlement deleveraging operations. The type of deleveraging event is determined by the -// collaterlization status of the subaccount (negative/non-negative TNC) as well as the clob pair -// status for the specified perpetual. +// final settlement deleveraging operations. func (k Keeper) getDeleveragingQuoteQuantumsDelta( ctx sdk.Context, perpetualId uint32, subaccountId satypes.SubaccountId, deltaQuantums *big.Int, -) (*big.Int, error) { - clobPair := k.mustGetClobPairForPerpetualId(ctx, perpetualId) - isFinalSettlement := clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT - + isFinalSettlement bool, +) (deltaQuoteQuantums *big.Int, err error) { // If market is in final settlement and the subaccount has non-negative TNC, use the oracle price. if isFinalSettlement { - hasNegativeTnc, err := k.CanDeleverageSubaccount(ctx, subaccountId) - if err != nil { - return new(big.Int), err - } - - if !hasNegativeTnc { - return k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaQuantums) - } + return k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, new(big.Int).Neg(deltaQuantums)) } // For standard deleveraging, use the bankruptcy price. @@ -499,23 +533,86 @@ func (k Keeper) ProcessDeleveraging( ), ) - // Send on-chain update for the deleveraging. The events are stored in a TransientStore which should be rolled-back - // if the branched state is discarded, so batching is not necessary. - k.GetIndexerEventManager().AddTxnEvent( - ctx, - indexerevents.SubtypeDeleveraging, - indexerevents.DeleveragingEventVersion, - indexer_manager.GetBytes( - indexerevents.NewDeleveragingEvent( - liquidatedSubaccountId, - offsettingSubaccountId, - perpetualId, - satypes.BaseQuantums(new(big.Int).Abs(deltaBaseQuantums).Uint64()), - satypes.BaseQuantums(deltaQuoteQuantums.Uint64()), - deltaBaseQuantums.Sign() > 0, - ), - ), + return nil +} + +// GetSubaccountsWithPositionsInFinalSettlementMarkets uses the subaccountOpenPositionInfo returned from the +// liquidations daemon to fetch subaccounts with open positions in final settlement markets. These subaccounts +// will be deleveraged in either at the oracle price if non-negative TNC or at bankruptcy price if negative TNC. This +// function is called in PrepareCheckState during the deleveraging step. +func (k Keeper) GetSubaccountsWithPositionsInFinalSettlementMarkets( + ctx sdk.Context, + subaccountOpenPositionInfo map[uint32]*types.SubaccountOpenPositionInfo, +) (subaccountsToDeleverage []subaccountToDeleverage) { + defer telemetry.MeasureSince( + time.Now(), + types.ModuleName, + metrics.ClobGetSubaccountsWithPositionsInFinalSettlementMarkets, + metrics.Latency, ) + for _, clobPair := range k.GetAllClobPairs(ctx) { + if clobPair.Status != types.ClobPair_STATUS_FINAL_SETTLEMENT { + continue + } + + finalSettlementPerpetualId := clobPair.MustGetPerpetualId() + positionInfo, found := subaccountOpenPositionInfo[finalSettlementPerpetualId] + if !found { + // No open positions in the market. + continue + } + + for _, subaccountId := range positionInfo.SubaccountsWithLongPosition { + subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ + SubaccountId: subaccountId, + PerpetualId: finalSettlementPerpetualId, + }) + } + for _, subaccountId := range positionInfo.SubaccountsWithShortPosition { + subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ + SubaccountId: subaccountId, + PerpetualId: finalSettlementPerpetualId, + }) + } + } + + metrics.AddSample( + metrics.ClobSubaccountsWithFinalSettlementPositionsCount, + float32(len(subaccountsToDeleverage)), + ) + + return subaccountsToDeleverage +} + +// DeleverageSubaccounts deleverages a slice of subaccounts paired with a perpetual position to deleverage with. +// Returns an error if a deleveraging attempt returns an error. +func (k Keeper) DeleverageSubaccounts( + ctx sdk.Context, + subaccountsToDeleverage []subaccountToDeleverage, +) error { + defer telemetry.MeasureSince( + time.Now(), + types.ModuleName, + metrics.LiquidateSubaccounts_Deleverage, + metrics.Latency, + ) + + // For each unfilled liquidation, attempt to deleverage the subaccount. + for i := 0; i < int(k.Flags.MaxDeleveragingAttemptsPerBlock) && i < len(subaccountsToDeleverage); i++ { + subaccountId := subaccountsToDeleverage[i].SubaccountId + perpetualId := subaccountsToDeleverage[i].PerpetualId + _, err := k.MaybeDeleverageSubaccount(ctx, subaccountId, perpetualId) + if err != nil { + k.Logger(ctx).Error( + "Failed to deleverage subaccount.", + "subaccount", subaccountId, + "perpetualId", perpetualId, + "error", err, + ) + return err + } + } + return nil } diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index 788b352d9f..c19ebe1eb2 100644 --- a/protocol/x/clob/keeper/deleveraging_test.go +++ b/protocol/x/clob/keeper/deleveraging_test.go @@ -218,9 +218,11 @@ func TestCanDeleverageSubaccount(t *testing.T) { insuranceFundBalance *big.Int subaccount satypes.Subaccount marketIdToOraclePriceOverride map[uint32]uint64 + clobPairs []types.ClobPair // Expectations. - expectedCanDeleverageSubaccount bool + expectedShouldDeleverageAtBankruptcyPrice bool + expectedShouldDeleverageAtOraclePrice bool }{ `Cannot deleverage when subaccount has positive TNC`: { liquidationConfig: constants.LiquidationsConfig_No_Limit, @@ -229,8 +231,12 @@ func TestCanDeleverageSubaccount(t *testing.T) { marketIdToOraclePriceOverride: map[uint32]uint64{ constants.BtcUsd.MarketId: 5_000_000_000, // $50,000 / BTC }, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc, + }, - expectedCanDeleverageSubaccount: false, + expectedShouldDeleverageAtBankruptcyPrice: false, + expectedShouldDeleverageAtOraclePrice: false, }, `Cannot deleverage when subaccount has zero TNC`: { liquidationConfig: constants.LiquidationsConfig_No_Limit, @@ -239,8 +245,12 @@ func TestCanDeleverageSubaccount(t *testing.T) { marketIdToOraclePriceOverride: map[uint32]uint64{ constants.BtcUsd.MarketId: 5_499_000_000, // $54,999 / BTC }, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc, + }, - expectedCanDeleverageSubaccount: false, + expectedShouldDeleverageAtBankruptcyPrice: false, + expectedShouldDeleverageAtOraclePrice: false, }, `Can deleverage when subaccount has negative TNC`: { liquidationConfig: constants.LiquidationsConfig_No_Limit, @@ -249,8 +259,40 @@ func TestCanDeleverageSubaccount(t *testing.T) { marketIdToOraclePriceOverride: map[uint32]uint64{ constants.BtcUsd.MarketId: 5_500_000_000, // $55,000 / BTC }, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc, + }, + + expectedShouldDeleverageAtBankruptcyPrice: true, + expectedShouldDeleverageAtOraclePrice: false, + }, + `Can deleverage when subaccount has negative TNC and clob pair has status FINAL_SETTLEMENT`: { + liquidationConfig: constants.LiquidationsConfig_No_Limit, + insuranceFundBalance: big.NewInt(10_000_000_000), // $10,000 + subaccount: constants.Carl_Num0_1BTC_Short_54999USD, + marketIdToOraclePriceOverride: map[uint32]uint64{ + constants.BtcUsd.MarketId: 5_500_000_000, // $55,000 / BTC + }, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, - expectedCanDeleverageSubaccount: true, + expectedShouldDeleverageAtBankruptcyPrice: true, + expectedShouldDeleverageAtOraclePrice: false, + }, + `Can final settle deleverage when subaccount has positive TNC and clob pair has status FINAL_SETTLEMENT`: { + liquidationConfig: constants.LiquidationsConfig_No_Limit, + insuranceFundBalance: big.NewInt(10_000_000_001), // $10,000.000001 + subaccount: constants.Carl_Num0_1BTC_Short_54999USD, + marketIdToOraclePriceOverride: map[uint32]uint64{ + constants.BtcUsd.MarketId: 5_000_000_000, // $50,000 / BTC + }, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, + + expectedShouldDeleverageAtBankruptcyPrice: false, + expectedShouldDeleverageAtOraclePrice: true, }, } @@ -259,7 +301,8 @@ func TestCanDeleverageSubaccount(t *testing.T) { // Setup keeper state. memClob := memclob.NewMemClobPriceTimePriority(false) bankMock := &mocks.BankKeeper{} - ks := keepertest.NewClobKeepersTestContext(t, memClob, bankMock, &mocks.IndexerEventManager{}) + mockIndexerEventManager := &mocks.IndexerEventManager{} + ks := keepertest.NewClobKeepersTestContext(t, memClob, bankMock, mockIndexerEventManager) err := keepertest.CreateUsdcAsset(ks.Ctx, ks.AssetsKeeper) require.NoError(t, err) @@ -313,17 +356,56 @@ func TestCanDeleverageSubaccount(t *testing.T) { require.NoError(t, err) } + for i, clobPair := range tc.clobPairs { + mockIndexerEventManager.On("AddTxnEvent", + ks.Ctx, + indexerevents.SubtypePerpetualMarket, + indexerevents.PerpetualMarketEventVersion, + indexer_manager.GetBytes( + indexerevents.NewPerpetualMarketCreateEvent( + clobPair.MustGetPerpetualId(), + clobPair.Id, + perpetuals[i].Params.Ticker, + perpetuals[i].Params.MarketId, + clobPair.Status, + clobPair.QuantumConversionExponent, + perpetuals[i].Params.AtomicResolution, + clobPair.SubticksPerTick, + clobPair.StepBaseQuantums, + perpetuals[i].Params.LiquidityTier, + ), + ), + ).Once().Return() + + _, err = ks.ClobKeeper.CreatePerpetualClobPair( + ks.Ctx, + clobPair.Id, + clobPair.MustGetPerpetualId(), + satypes.BaseQuantums(clobPair.StepBaseQuantums), + clobPair.QuantumConversionExponent, + clobPair.SubticksPerTick, + clobPair.Status, + ) + require.NoError(t, err) + } + ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.subaccount) - canDeleverageSubaccount, err := ks.ClobKeeper.CanDeleverageSubaccount( + shouldDeleverageAtBankruptcyPrice, shouldDeleverageAtOraclePrice, err := ks.ClobKeeper.CanDeleverageSubaccount( ks.Ctx, *tc.subaccount.Id, + 0, ) require.NoError(t, err) require.Equal( t, - tc.expectedCanDeleverageSubaccount, - canDeleverageSubaccount, + tc.expectedShouldDeleverageAtBankruptcyPrice, + shouldDeleverageAtBankruptcyPrice, + ) + require.Equal( + t, + tc.expectedShouldDeleverageAtOraclePrice, + shouldDeleverageAtOraclePrice, ) }) } @@ -711,6 +793,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { satypes.BaseQuantums(fill.FillAmount), satypes.BaseQuantums(bankruptcyPriceQuoteQuantums.Uint64()), tc.deltaQuantums.Sign() > 0, + false, ), ), ).Return() @@ -721,6 +804,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { tc.liquidatedSubaccountId, tc.perpetualId, tc.deltaQuantums, + false, // TODO, add tests where final settlement is true ) require.Equal(t, tc.expectedFills, fills) require.True(t, tc.expectedQuantumsRemaining.Cmp(deltaQuantumsRemaining) == 0) @@ -1126,6 +1210,7 @@ func TestProcessDeleveraging(t *testing.T) { satypes.BaseQuantums(new(big.Int).Abs(tc.deltaQuantums).Uint64()), satypes.BaseQuantums(bankruptcyPriceQuoteQuantums.Uint64()), tc.deltaQuantums.Sign() > 0, + false, ), ), ).Return() @@ -1334,6 +1419,7 @@ func TestProcessDeleveragingAtOraclePrice(t *testing.T) { satypes.BaseQuantums(new(big.Int).Abs(tc.deltaQuantums).Uint64()), satypes.BaseQuantums(fillPriceQuoteQuantums.Uint64()), tc.deltaQuantums.Sign() > 0, + false, ), ), ).Return() @@ -1493,6 +1579,7 @@ func TestProcessDeleveraging_Rounding(t *testing.T) { satypes.BaseQuantums(new(big.Int).Abs(tc.deltaQuantums).Uint64()), satypes.BaseQuantums(bankruptcyPriceQuoteQuantums.Uint64()), tc.deltaQuantums.Sign() > 0, + false, ), ), ).Return() diff --git a/protocol/x/clob/keeper/final_settlement.go b/protocol/x/clob/keeper/final_settlement.go new file mode 100644 index 0000000000..4e1468ff73 --- /dev/null +++ b/protocol/x/clob/keeper/final_settlement.go @@ -0,0 +1,73 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events" + "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" + indexershared "github.com/dydxprotocol/v4-chain/protocol/indexer/shared" + "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" +) + +// mustTransitionToFinalSettlement holds logic executed when a market transitions to FINAL_SETTLEMENT status. +// This function will forcefully cancel all stateful open orders for the clob pair. +func (k Keeper) mustTransitionToFinalSettlement(ctx sdk.Context, clobPairId types.ClobPairId) { + // Forcefully cancel all stateful orders from state for this clob pair. + k.mustCancelStatefulOrdersForFinalSettlement(ctx, clobPairId) + + // Delete untriggered conditional orders for this clob pair from memory. + delete(k.UntriggeredConditionalOrders, clobPairId) +} + +// mustCancelStatefulOrdersForFinalSettlement forcefully cancels all stateful orders +// for the provided ClobPair. These orders will be removed from the memclob in PrepareCheckState. +func (k Keeper) mustCancelStatefulOrdersForFinalSettlement(ctx sdk.Context, clobPairId types.ClobPairId) { + statefulOrders := k.GetAllStatefulOrders(ctx) + processProposerMatchesEvents := k.GetProcessProposerMatchesEvents(ctx) + + // This logic is executed in EndBlocker and should not panic. This would be unexpected, + // but if it happens we would rather recover and continue if an order fails to be removed from state + // rather than halt the chain. + removeStatefulOrderWithoutPanicing := func(ctx sdk.Context, orderId types.OrderId) { + defer func() { + if r := recover(); r != nil { + k.Logger(ctx).Error( + "mustCancelStatefulOrdersForFinalSettlement: Failed to remove stateful order with OrderId %+v: %v", + orderId, + r, + ) + } + }() + k.MustRemoveStatefulOrder(ctx, orderId) + } + + // TODO(CLOB-1053): Iterate over stateful orders for only specified clob pair + for _, order := range statefulOrders { + if order.GetClobPairId() != clobPairId { + continue + } + + // Remove from state, recovering from panic if necessary + removeStatefulOrderWithoutPanicing(ctx, order.OrderId) + + // Append to RemovedStatefulOrderIds so this order gets removed + // from the memclob in PrepareCheckState during the PurgeInvalidMemclobState step + processProposerMatchesEvents.RemovedStatefulOrderIds = append( + processProposerMatchesEvents.RemovedStatefulOrderIds, + order.OrderId, + ) + + k.GetIndexerEventManager().AddTxnEvent( + ctx, + indexerevents.SubtypeStatefulOrder, + indexerevents.StatefulOrderEventVersion, + indexer_manager.GetBytes( + indexerevents.NewStatefulOrderRemovalEvent( + order.OrderId, + indexershared.OrderRemovalReason_ORDER_REMOVAL_REASON_FINAL_SETTLEMENT, + ), + ), + ) + } + + k.MustSetProcessProposerMatchesEvents(ctx, processProposerMatchesEvents) +} diff --git a/protocol/x/clob/keeper/liquidations.go b/protocol/x/clob/keeper/liquidations.go index 6c2136c0fb..f185c89165 100644 --- a/protocol/x/clob/keeper/liquidations.go +++ b/protocol/x/clob/keeper/liquidations.go @@ -18,13 +18,26 @@ import ( satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) +// subaccountToDeleverage is a struct containing a subaccount ID and perpetual ID to deleverage. +// This struct is used as a return type for the LiquidateSubaccountsAgainstOrderbook and +// GetSubaccountsWithOpenPositionsInFinalSettlementMarkets called in PrepareCheckState. +type subaccountToDeleverage struct { + SubaccountId satypes.SubaccountId + PerpetualId uint32 +} + // LiquidateSubaccountsAgainstOrderbook takes a list of subaccount IDs and liquidates them against // the orderbook. It will liquidate as many subaccounts as possible up to the maximum number of -// liquidations per block. Subaccounts are selected with a pseudo-randomly generated offset. +// liquidations per block. Subaccounts are selected with a pseudo-randomly generated offset. A slice +// of subaccounts to deleverage is returned from this function, derived from liquidation orders that +// failed to fill. func (k Keeper) LiquidateSubaccountsAgainstOrderbook( ctx sdk.Context, subaccountIds []satypes.SubaccountId, -) error { +) ( + subaccountsToDeleverage []subaccountToDeleverage, + err error, +) { lib.AssertCheckTxMode(ctx) metrics.AddSample( @@ -35,7 +48,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( // Early return if there are 0 subaccounts to liquidate. numSubaccounts := len(subaccountIds) if numSubaccounts == 0 { - return nil + return nil, nil } defer telemetry.MeasureSince( @@ -68,7 +81,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( } // Return unexpected errors. - return err + return nil, err } liquidationOrders = append(liquidationOrders, *liquidationOrder) @@ -93,7 +106,6 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( // Attempt to place each liquidation order and perform deleveraging if necessary. startPlaceLiquidationOrders := time.Now() - unfilledLiquidations := make([]types.LiquidationOrder, 0) for _, subaccountId := range subaccountIdsToLiquidate { // Generate a new liquidation order with the appropriate order size from the sorted subaccount ids. liquidationOrder, err := k.MaybeGetLiquidationOrder(ctx, subaccountId) @@ -105,21 +117,26 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( } // Return unexpected errors. - return err + return nil, err } optimisticallyFilledQuantums, _, err := k.PlacePerpetualLiquidation(ctx, *liquidationOrder) - if err != nil { + // Exception for liquidation which conflicts with clob pair status. This is expected for liquidations generated + // for subaccounts with open positions in final settlement markets. + if err != nil && !errors.Is(err, types.ErrLiquidationConflictsWithClobPairStatus) { k.Logger(ctx).Error( "Failed to liquidate subaccount", "liquidationOrder", *liquidationOrder, "error", err, ) - return err + return nil, err } if optimisticallyFilledQuantums == 0 { - unfilledLiquidations = append(unfilledLiquidations, *liquidationOrder) + subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ + SubaccountId: liquidationOrder.GetSubaccountId(), + PerpetualId: liquidationOrder.MustGetLiquidatedPerpetualId(), + }) } } telemetry.MeasureSince( @@ -129,33 +146,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( metrics.Latency, ) - // For each unfilled liquidation, attempt to deleverage the subaccount. - startDeleverageSubaccounts := time.Now() - for i := 0; i < int(k.Flags.MaxDeleveragingAttemptsPerBlock) && i < len(unfilledLiquidations); i++ { - liquidationOrder := unfilledLiquidations[i] - - subaccountId := liquidationOrder.GetSubaccountId() - perpetualId := liquidationOrder.MustGetLiquidatedPerpetualId() - - _, err := k.MaybeDeleverageSubaccount(ctx, subaccountId, perpetualId) - if err != nil { - k.Logger(ctx).Error( - "Failed to deleverage subaccount.", - "subaccount", subaccountId, - "perpetualId", perpetualId, - "error", err, - ) - return err - } - } - telemetry.MeasureSince( - startDeleverageSubaccounts, - types.ModuleName, - metrics.LiquidateSubaccounts_Deleverage, - metrics.Latency, - ) - - return nil + return subaccountsToDeleverage, nil } // MaybeGetLiquidationOrder takes a subaccount ID and returns a liquidation order that can be used to @@ -241,7 +232,8 @@ func (k Keeper) GetLiquidationOrderForPerpetual( } // PlacePerpetualLiquidation places an IOC liquidation order onto the book that results in fills of type -// `PerpetualLiquidation`. +// `PerpetualLiquidation`. This function will return an error if attempting to place a liquidation order +// in a non-active market. func (k Keeper) PlacePerpetualLiquidation( ctx sdk.Context, liquidationOrder types.LiquidationOrder, @@ -258,6 +250,10 @@ func (k Keeper) PlacePerpetualLiquidation( metrics.PlacePerpetualLiquidation, ) + if err := k.validateLiquidationAgainstClobPairStatus(ctx, liquidationOrder); err != nil { + return 0, 0, err + } + orderSizeOptimisticallyFilledFromMatchingQuantums, orderStatus, offchainUpdates, diff --git a/protocol/x/clob/keeper/liquidations_test.go b/protocol/x/clob/keeper/liquidations_test.go index bcaed32fd3..a6e6efb63d 100644 --- a/protocol/x/clob/keeper/liquidations_test.go +++ b/protocol/x/clob/keeper/liquidations_test.go @@ -343,6 +343,82 @@ func TestPlacePerpetualLiquidation(t *testing.T) { } } +func TestPlacePerpetualLiquidation_validateLiquidationAgainstClobPairStatus(t *testing.T) { + tests := map[string]struct { + status types.ClobPair_Status + + expectedError error + }{ + "Cannot liquidate in initializing state": { + status: types.ClobPair_STATUS_INITIALIZING, + + expectedError: types.ErrLiquidationConflictsWithClobPairStatus, + }, + "Can liquidate in active state": { + status: types.ClobPair_STATUS_ACTIVE, + }, + "Cannot liquidate in final settlement state": { + status: types.ClobPair_STATUS_FINAL_SETTLEMENT, + + expectedError: types.ErrLiquidationConflictsWithClobPairStatus, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + memClob := memclob.NewMemClobPriceTimePriority(false) + mockBankKeeper := &mocks.BankKeeper{} + ks := keepertest.NewClobKeepersTestContext(t, memClob, mockBankKeeper, indexer_manager.NewIndexerEventManagerNoop()) + ctx := ks.Ctx.WithIsCheckTx(true) + + // Create the default markets. + keepertest.CreateTestMarkets(t, ks.Ctx, ks.PricesKeeper) + + // Create liquidity tiers. + keepertest.CreateTestLiquidityTiers(t, ks.Ctx, ks.PerpetualsKeeper) + + err := keepertest.CreateUsdcAsset(ks.Ctx, ks.AssetsKeeper) + require.NoError(t, err) + + perpetuals := []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + } + for _, p := range perpetuals { + _, err := ks.PerpetualsKeeper.CreatePerpetual( + ctx, + p.Params.Id, + p.Params.Ticker, + p.Params.MarketId, + p.Params.AtomicResolution, + p.Params.DefaultFundingPpm, + p.Params.LiquidityTier, + ) + require.NoError(t, err) + } + + clobPair := constants.ClobPair_Btc + _, err = ks.ClobKeeper.CreatePerpetualClobPair( + ctx, + clobPair.Id, + clobtest.MustPerpetualId(clobPair), + satypes.BaseQuantums(clobPair.StepBaseQuantums), + clobPair.QuantumConversionExponent, + clobPair.SubticksPerTick, + tc.status, + ) + require.NoError(t, err) + + _, _, err = ks.ClobKeeper.PlacePerpetualLiquidation( + ctx, + constants.LiquidationOrder_Dave_Num0_Clob0_Sell1BTC_Price50000, + ) + if tc.expectedError != nil { + require.ErrorIs(t, err, tc.expectedError) + } + }) + } +} + func TestPlacePerpetualLiquidation_PreexistingLiquidation(t *testing.T) { tests := map[string]struct { // State. diff --git a/protocol/x/clob/keeper/orders_test.go b/protocol/x/clob/keeper/orders_test.go index a9f77d58af..5948c6562a 100644 --- a/protocol/x/clob/keeper/orders_test.go +++ b/protocol/x/clob/keeper/orders_test.go @@ -1670,6 +1670,27 @@ func TestPerformStatefulOrderValidation(t *testing.T) { TimeInForce: types.Order_TIME_IN_FORCE_POST_ONLY, }, }, + "Fails with short-term order and ClobPair_Status of FINAL_SETTLEMENT": { + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, + order: constants.Order_Alice_Num0_Id0_Clob0_Buy10_Price10_GTB16, + expectedErr: "trading is disabled for clob pair", + }, + "Fails with long-term order and ClobPair_Status of FINAL_SETTLEMENT": { + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, + order: constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy100_Price10_GTBT15, + expectedErr: "trading is disabled for clob pair", + }, + "Fails with conditional order and ClobPair_Status of FINAL_SETTLEMENT": { + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, + order: constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10_SL_50001, + expectedErr: "trading is disabled for clob pair", + }, } for name, tc := range tests { diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index 56f2d16649..1c7c5bab0e 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -610,7 +610,10 @@ func (k Keeper) PersistMatchLiquidationToState( // PersistMatchDeleveragingToState writes a MatchPerpetualDeleveraging object to state. // This function returns an error if: -// - CanDeleverageSubaccount returns false, indicating the subaccount failed deleveraging validation. +// - CanDeleverageSubaccount returns false for both boolean return values, indicating the +// subaccount failed deleveraging validation. +// - The IsFinalSettlement flag on the operation does not match the expected value based on collateralization +// and market status. // - OffsetSubaccountPerpetualPosition returns an error. // - The generated fills do not match the fills in the Operations object. // TODO(CLOB-654) Verify deleveraging is triggered by unmatched liquidation orders and for the correct amount. @@ -619,9 +622,15 @@ func (k Keeper) PersistMatchDeleveragingToState( matchDeleveraging *types.MatchPerpetualDeleveraging, ) error { liquidatedSubaccountId := matchDeleveraging.GetLiquidated() + perpetualId := matchDeleveraging.GetPerpetualId() // Validate that the provided subaccount can be deleveraged. - if canDeleverageSubaccount, err := k.CanDeleverageSubaccount(ctx, liquidatedSubaccountId); err != nil { + shouldDeleverageAtBankruptcyPrice, shouldDeleverageAtOraclePrice, err := k.CanDeleverageSubaccount( + ctx, + liquidatedSubaccountId, + perpetualId, + ) + if err != nil { panic( fmt.Sprintf( "PersistMatchDeleveragingToState: Failed to determine if subaccount can be deleveraged. "+ @@ -630,7 +639,9 @@ func (k Keeper) PersistMatchDeleveragingToState( err, ), ) - } else if !canDeleverageSubaccount { + } + + if !shouldDeleverageAtBankruptcyPrice && !shouldDeleverageAtOraclePrice { // TODO(CLOB-853): Add more verbose error logging about why deleveraging failed validation. return errorsmod.Wrapf( types.ErrInvalidDeleveragedSubaccount, @@ -639,7 +650,18 @@ func (k Keeper) PersistMatchDeleveragingToState( ) } - perpetualId := matchDeleveraging.GetPerpetualId() + if matchDeleveraging.IsFinalSettlement != shouldDeleverageAtOraclePrice { + // Throw error if the isFinalSettlement flag does not match the expected value. This prevents misuse or lack + // of use of the isFinalSettlement flag. The isFinalSettlement flag should be set to true if-and-only-if the + // subaccount has non-negative TNC and the market is in final settlement. Otherwise, it must be false. + return errorsmod.Wrapf( + types.ErrDeleveragingIsFinalSettlementFlagMismatch, + "MatchPerpetualDeleveraging %+v has isFinalSettlement flag (%v), expected (%v)", + matchDeleveraging, + matchDeleveraging.IsFinalSettlement, + shouldDeleverageAtOraclePrice, + ) + } liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) position, exists := liquidatedSubaccount.GetPerpetualPositionForId(perpetualId) @@ -664,6 +686,7 @@ func (k Keeper) PersistMatchDeleveragingToState( perpetualId, liquidatedSubaccountId, deltaBaseQuantums, + matchDeleveraging.IsFinalSettlement, ) if err != nil { return err @@ -689,6 +712,25 @@ func (k Keeper) PersistMatchDeleveragingToState( err, ) } + + // Send on-chain update for the deleveraging. The events are stored in a TransientStore which should be rolled-back + // if the branched state is discarded, so batching is not necessary. + k.GetIndexerEventManager().AddTxnEvent( + ctx, + indexerevents.SubtypeDeleveraging, + indexerevents.DeleveragingEventVersion, + indexer_manager.GetBytes( + indexerevents.NewDeleveragingEvent( + liquidatedSubaccountId, + fill.OffsettingSubaccountId, + perpetualId, + satypes.BaseQuantums(new(big.Int).Abs(deltaBaseQuantums).Uint64()), + satypes.BaseQuantums(deltaQuoteQuantums.Uint64()), + deltaBaseQuantums.Sign() > 0, + matchDeleveraging.IsFinalSettlement, + ), + ), + ) } return nil diff --git a/protocol/x/clob/keeper/process_operations_test.go b/protocol/x/clob/keeper/process_operations_test.go index 4e31a3c8a4..f617bcb8af 100644 --- a/protocol/x/clob/keeper/process_operations_test.go +++ b/protocol/x/clob/keeper/process_operations_test.go @@ -1426,6 +1426,318 @@ func TestProcessProposerOperations(t *testing.T) { }, expectedError: types.ErrInvalidOrderRemoval, }, + "Fails with order removal for market in final settlement": { + perpetuals: []*perptypes.Perpetual{ + &constants.BtcUsd_100PercentMarginRequirement, + }, + perpetualFeeParams: &constants.PerpetualFeeParams, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, + rawOperations: []types.OperationRaw{ + clobtest.NewOrderRemovalOperationRaw( + constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10.OrderId, + types.OrderRemoval_REMOVAL_REASON_INVALID_SELF_TRADE, + ), + }, + expectedError: types.ErrOperationConflictsWithClobPairStatus, + }, + "Fails with short-term order placement for market in final settlement": { + perpetuals: []*perptypes.Perpetual{ + &constants.BtcUsd_100PercentMarginRequirement, + }, + perpetualFeeParams: &constants.PerpetualFeeParams, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, + rawOperations: []types.OperationRaw{ + clobtest.NewShortTermOrderPlacementOperationRaw( + constants.Order_Dave_Num0_Id1_Clob0_Sell025BTC_Price50000_GTB11, + ), + }, + expectedError: types.ErrOperationConflictsWithClobPairStatus, + }, + "Fails with ClobMatch_MatchOrders for market in final settlement": { + perpetuals: []*perptypes.Perpetual{ + &constants.BtcUsd_100PercentMarginRequirement, + }, + perpetualFeeParams: &constants.PerpetualFeeParams, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, + rawOperations: []types.OperationRaw{ + clobtest.NewMatchOperationRaw( + &constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10, + []types.MakerFill{ + { + FillAmount: 10, + MakerOrderId: constants.LongTermOrder_Alice_Num0_Id1_Clob0_Sell20_Price10_GTBT10.OrderId, + }, + }, + ), + }, + expectedError: types.ErrOperationConflictsWithClobPairStatus, + }, + // Liquidations are disallowed for markets in final settlement because they may result + // in a position increasing in size. This is not allowed for markets in final settlement. + "Fails with ClobMatch_MatchPerpetualLiquidation for market in final settlement": { + perpetuals: []*perptypes.Perpetual{ + &constants.BtcUsd_100PercentMarginRequirement, + }, + perpetualFeeParams: &constants.PerpetualFeeParams, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, + rawOperations: []types.OperationRaw{ + clobtest.NewMatchOperationRawFromPerpetualLiquidation( + types.MatchPerpetualLiquidation{ + Liquidated: constants.Alice_Num0, + ClobPairId: 0, + PerpetualId: 0, + TotalSize: 10, + IsBuy: false, + Fills: []types.MakerFill{ + { + FillAmount: 10, + MakerOrderId: constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10.OrderId, + }, + }, + }, + ), + }, + expectedError: types.ErrOperationConflictsWithClobPairStatus, + }, + // Deleveraging is allowed for markets in final settlement to close out all open positions. A deleveraging + // event with IsFinalSettlement set to false represents a negative TNC subaccount in the market getting deleveraged. + "Succeeds with ClobMatch_MatchPerpetualDeleveraging, IsFinalSettlement is false for market in final settlement": { + perpetuals: []*perptypes.Perpetual{ + &constants.BtcUsd_100PercentMarginRequirement, + }, + perpetualFeeParams: &constants.PerpetualFeeParams, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, + subaccounts: []satypes.Subaccount{ + // liquidatable: MMR = $5000, TNC = $499 + constants.Carl_Num0_1BTC_Short_50499USD, + constants.Dave_Num0_1BTC_Long_50000USD, + }, + marketIdToOraclePriceOverride: map[uint32]uint64{ + constants.BtcUsd.MarketId: 5_050_000_000, // $50,500 / BTC + }, + rawOperations: []types.OperationRaw{ + clobtest.NewMatchOperationRawFromPerpetualDeleveragingLiquidation( + types.MatchPerpetualDeleveraging{ + Liquidated: constants.Carl_Num0, + PerpetualId: 0, + Fills: []types.MatchPerpetualDeleveraging_Fill{ + { + OffsettingSubaccountId: constants.Dave_Num0, + FillAmount: 100_000_000, + }, + }, + }, + ), + }, + expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{ + BlockHeight: blockHeight, + }, + expectedQuoteBalances: map[satypes.SubaccountId]int64{ + constants.Carl_Num0: 0, + constants.Dave_Num0: constants.Usdc_Asset_100_499.GetBigQuantums().Int64(), + }, + expectedPerpetualPositions: map[satypes.SubaccountId][]*satypes.PerpetualPosition{ + constants.Carl_Num0: {}, + constants.Dave_Num0: {}, + }, + }, + // Deleveraging is allowed for markets in final settlement to close out all open positions. A deleveraging + // event with IsFinalSettlement set to true represents a non-negative TNC subaccount having its position closed + // at the oracle price against other subaccounts with open positions on the opposing side of the book. + "Succeeds with ClobMatch_MatchPerpetualDeleveraging, IsFinalSettlement is true for market in final settlement": { + perpetuals: []*perptypes.Perpetual{ + &constants.BtcUsd_100PercentMarginRequirement, + }, + perpetualFeeParams: &constants.PerpetualFeeParams, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, + subaccounts: []satypes.Subaccount{ + // both well-collateralized + constants.Carl_Num0_1BTC_Short_100000USD, + constants.Dave_Num0_1BTC_Long_50000USD, + }, + rawOperations: []types.OperationRaw{ + clobtest.NewMatchOperationRawFromPerpetualDeleveragingLiquidation( + types.MatchPerpetualDeleveraging{ + Liquidated: constants.Carl_Num0, + PerpetualId: 0, + Fills: []types.MatchPerpetualDeleveraging_Fill{ + { + OffsettingSubaccountId: constants.Dave_Num0, + FillAmount: 100_000_000, + }, + }, + IsFinalSettlement: true, + }, + ), + }, + expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{ + BlockHeight: blockHeight, + }, + expectedQuoteBalances: map[satypes.SubaccountId]int64{ + constants.Carl_Num0: constants.Usdc_Asset_50_000.GetBigQuantums().Int64(), + constants.Dave_Num0: constants.Usdc_Asset_100_000.GetBigQuantums().Int64(), + }, + expectedPerpetualPositions: map[satypes.SubaccountId][]*satypes.PerpetualPosition{ + constants.Carl_Num0: {}, + constants.Dave_Num0: {}, + }, + }, + // This throws an error because the CanDeleverageSubaccount function will return false for + // shouldFinalSettlePosition, but the IsFinalSettlement flag is set to true. + `Fails with ClobMatch_MatchPerpetualDeleveraging for negative TNC subaccount, + IsFinalSettlement is true for market not in final settlement`: { + perpetuals: []*perptypes.Perpetual{ + &constants.BtcUsd_100PercentMarginRequirement, + }, + perpetualFeeParams: &constants.PerpetualFeeParams, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc, + }, + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_49999USD, + constants.Dave_Num0_1BTC_Long_50000USD, + }, + rawOperations: []types.OperationRaw{ + clobtest.NewMatchOperationRawFromPerpetualDeleveragingLiquidation( + types.MatchPerpetualDeleveraging{ + Liquidated: constants.Carl_Num0, + PerpetualId: 0, + Fills: []types.MatchPerpetualDeleveraging_Fill{ + { + OffsettingSubaccountId: constants.Dave_Num0, + FillAmount: 100_000_000, + }, + }, + IsFinalSettlement: true, + }, + ), + }, + expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{ + BlockHeight: blockHeight, + }, + expectedError: types.ErrDeleveragingIsFinalSettlementFlagMismatch, + }, + // This test will fail because the CanDeleverageSubaccount function will return false for + // shouldFinalSettlePosition, but the IsFinalSettlement flag is set to true. Negative TNC subaccounts + // should never be deleveraged using final settlement (oracle price), and instead should be deleveraged + // using the bankruptcy price. + `Fails with ClobMatch_MatchPerpetualDeleveraging for negative TNC subaccount, + IsFinalSettlement is true for market in final settlement`: { + perpetuals: []*perptypes.Perpetual{ + &constants.BtcUsd_100PercentMarginRequirement, + }, + perpetualFeeParams: &constants.PerpetualFeeParams, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, + subaccounts: []satypes.Subaccount{ + // liquidatable: MMR = $5000, TNC = $499 + constants.Carl_Num0_1BTC_Short_50499USD, + constants.Dave_Num0_1BTC_Long_50000USD, + }, + marketIdToOraclePriceOverride: map[uint32]uint64{ + constants.BtcUsd.MarketId: 5_050_000_000, // $50,500 / BTC + }, + rawOperations: []types.OperationRaw{ + clobtest.NewMatchOperationRawFromPerpetualDeleveragingLiquidation( + types.MatchPerpetualDeleveraging{ + Liquidated: constants.Carl_Num0, + PerpetualId: 0, + Fills: []types.MatchPerpetualDeleveraging_Fill{ + { + OffsettingSubaccountId: constants.Dave_Num0, + FillAmount: 100_000_000, + }, + }, + IsFinalSettlement: true, + }, + ), + }, + expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{ + BlockHeight: blockHeight, + }, + expectedError: types.ErrDeleveragingIsFinalSettlementFlagMismatch, + }, + // This test will fail because the CanDeleverageSubaccount function will return false for + // a non-negative TNC subaccount in a market not in final settlement. + `Fails with ClobMatch_MatchPerpetualDeleveraging for non-negative TNC subaccount, + IsFinalSettlement is true for market not in final settlement`: { + perpetuals: []*perptypes.Perpetual{ + &constants.BtcUsd_100PercentMarginRequirement, + }, + perpetualFeeParams: &constants.PerpetualFeeParams, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc, + }, + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_100000USD, + constants.Dave_Num0_1BTC_Long_50000USD, + }, + rawOperations: []types.OperationRaw{ + clobtest.NewMatchOperationRawFromPerpetualDeleveragingLiquidation( + types.MatchPerpetualDeleveraging{ + Liquidated: constants.Carl_Num0, + PerpetualId: 0, + Fills: []types.MatchPerpetualDeleveraging_Fill{ + { + OffsettingSubaccountId: constants.Dave_Num0, + FillAmount: 100_000_000, + }, + }, + IsFinalSettlement: true, + }, + ), + }, + expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{ + BlockHeight: blockHeight, + }, + expectedError: types.ErrInvalidDeleveragedSubaccount, + }, + `Fails with ClobMatch_MatchPerpetualDeleveraging for non-negative TNC subaccount, + IsFinalSettlement is false for market in final settlement`: { + perpetuals: []*perptypes.Perpetual{ + &constants.BtcUsd_100PercentMarginRequirement, + }, + perpetualFeeParams: &constants.PerpetualFeeParams, + clobPairs: []types.ClobPair{ + constants.ClobPair_Btc_Final_Settlement, + }, + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_100000USD, + constants.Dave_Num0_1BTC_Long_50000USD, + }, + rawOperations: []types.OperationRaw{ + clobtest.NewMatchOperationRawFromPerpetualDeleveragingLiquidation( + types.MatchPerpetualDeleveraging{ + Liquidated: constants.Carl_Num0, + PerpetualId: 0, + Fills: []types.MatchPerpetualDeleveraging_Fill{ + { + OffsettingSubaccountId: constants.Dave_Num0, + FillAmount: 100_000_000, + }, + }, + IsFinalSettlement: false, + }, + ), + }, + expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{ + BlockHeight: blockHeight, + }, + expectedError: types.ErrDeleveragingIsFinalSettlementFlagMismatch, + }, } for name, tc := range tests { diff --git a/protocol/x/clob/memclob/memclob.go b/protocol/x/clob/memclob/memclob.go index e81f1ae0d4..e68f156e00 100644 --- a/protocol/x/clob/memclob/memclob.go +++ b/protocol/x/clob/memclob/memclob.go @@ -694,6 +694,7 @@ func (m *MemClobPriceTimePriority) DeleverageSubaccount( subaccountId satypes.SubaccountId, perpetualId uint32, deltaQuantums *big.Int, + isFinalSettlement bool, ) ( quantumsDeleveraged *big.Int, err error, @@ -705,6 +706,7 @@ func (m *MemClobPriceTimePriority) DeleverageSubaccount( subaccountId, perpetualId, deltaQuantums, + isFinalSettlement, ) if len(fills) > 0 { @@ -712,6 +714,7 @@ func (m *MemClobPriceTimePriority) DeleverageSubaccount( subaccountId, perpetualId, fills, + isFinalSettlement, ) } diff --git a/protocol/x/clob/types/clob_pair.go b/protocol/x/clob/types/clob_pair.go index 1376b524ab..5be94c81e2 100644 --- a/protocol/x/clob/types/clob_pair.go +++ b/protocol/x/clob/types/clob_pair.go @@ -10,9 +10,15 @@ import ( // may be transitioned to from this state. Note the keys of this map may be // a subset of the types defined in the proto for ClobPair_Status. var SupportedClobPairStatusTransitions = map[ClobPair_Status]map[ClobPair_Status]struct{}{ - ClobPair_STATUS_ACTIVE: {}, + ClobPair_STATUS_ACTIVE: { + ClobPair_STATUS_FINAL_SETTLEMENT: struct{}{}, + }, ClobPair_STATUS_INITIALIZING: { - ClobPair_STATUS_ACTIVE: struct{}{}, + ClobPair_STATUS_ACTIVE: struct{}{}, + ClobPair_STATUS_FINAL_SETTLEMENT: struct{}{}, + }, + ClobPair_STATUS_FINAL_SETTLEMENT: { + ClobPair_STATUS_INITIALIZING: struct{}{}, }, } @@ -25,7 +31,16 @@ func IsSupportedClobPairStatus(clobPairStatus ClobPair_Status) bool { // IsSupportedClobPairStatusTransition returns true if it is considered valid to transition from // the first provided ClobPair_Status to the second provided ClobPair_Status. Else, returns false. +// Transitions from a ClobPair_Status to itself are considered valid. func IsSupportedClobPairStatusTransition(from ClobPair_Status, to ClobPair_Status) bool { + if !IsSupportedClobPairStatus(from) || !IsSupportedClobPairStatus(to) { + return false + } + + if from == to { + return true + } + _, exists := SupportedClobPairStatusTransitions[from][to] return exists } diff --git a/protocol/x/clob/types/clob_pair_test.go b/protocol/x/clob/types/clob_pair_test.go index 8050c51f90..a365164bbe 100644 --- a/protocol/x/clob/types/clob_pair_test.go +++ b/protocol/x/clob/types/clob_pair_test.go @@ -42,6 +42,7 @@ func TestIsSupportedClobPairStatus_Supported(t *testing.T) { // these are the only two supported statuses require.True(t, types.IsSupportedClobPairStatus(types.ClobPair_STATUS_ACTIVE)) require.True(t, types.IsSupportedClobPairStatus(types.ClobPair_STATUS_INITIALIZING)) + require.True(t, types.IsSupportedClobPairStatus(types.ClobPair_STATUS_FINAL_SETTLEMENT)) } func TestIsSupportedClobPairStatus_Unsupported(t *testing.T) { @@ -56,10 +57,18 @@ func TestIsSupportedClobPairStatus_Unsupported(t *testing.T) { } func TestIsSupportedClobPairStatusTransition_Supported(t *testing.T) { - // only supported transition require.True(t, types.IsSupportedClobPairStatusTransition( types.ClobPair_STATUS_INITIALIZING, types.ClobPair_STATUS_ACTIVE, )) + require.True(t, types.IsSupportedClobPairStatusTransition( + types.ClobPair_STATUS_INITIALIZING, types.ClobPair_STATUS_FINAL_SETTLEMENT, + )) + require.True(t, types.IsSupportedClobPairStatusTransition( + types.ClobPair_STATUS_ACTIVE, types.ClobPair_STATUS_FINAL_SETTLEMENT, + )) + require.True(t, types.IsSupportedClobPairStatusTransition( + types.ClobPair_STATUS_FINAL_SETTLEMENT, types.ClobPair_STATUS_INITIALIZING, + )) } func TestIsSupportedClobPairStatusTransition_Unsupported(t *testing.T) { @@ -77,10 +86,58 @@ func TestIsSupportedClobPairStatusTransition_Unsupported(t *testing.T) { // iterate over all permutations of clob pair statuses for _, fromClobPairStatus := range types.ClobPair_Status_value { for _, toClobPairStatus := range types.ClobPair_Status_value { - if fromClobPairStatus == int32(types.ClobPair_STATUS_INITIALIZING) && - toClobPairStatus == int32(types.ClobPair_STATUS_ACTIVE) { - continue - } else { + switch fromClobPairStatus { + case int32(types.ClobPair_STATUS_INITIALIZING): + { + switch toClobPairStatus { + case int32(types.ClobPair_STATUS_ACTIVE): + fallthrough + case int32(types.ClobPair_STATUS_FINAL_SETTLEMENT): + continue + default: + require.Equal( + t, + toClobPairStatus == fromClobPairStatus, + types.IsSupportedClobPairStatusTransition( + types.ClobPair_Status(fromClobPairStatus), + types.ClobPair_Status(toClobPairStatus), + ), + ) + } + } + case int32(types.ClobPair_STATUS_ACTIVE): + { + switch toClobPairStatus { + case int32(types.ClobPair_STATUS_FINAL_SETTLEMENT): + continue + default: + require.Equal( + t, + toClobPairStatus == fromClobPairStatus, + types.IsSupportedClobPairStatusTransition( + types.ClobPair_Status(fromClobPairStatus), + types.ClobPair_Status(toClobPairStatus), + ), + ) + } + } + case int32(types.ClobPair_STATUS_FINAL_SETTLEMENT): + { + switch toClobPairStatus { + case int32(types.ClobPair_STATUS_INITIALIZING): + continue + default: + require.Equal( + t, + toClobPairStatus == fromClobPairStatus, + types.IsSupportedClobPairStatusTransition( + types.ClobPair_Status(fromClobPairStatus), + types.ClobPair_Status(toClobPairStatus), + ), + ) + } + } + default: require.False( t, types.IsSupportedClobPairStatusTransition( diff --git a/protocol/x/clob/types/errors.go b/protocol/x/clob/types/errors.go index abb80ab27b..a9396f1026 100644 --- a/protocol/x/clob/types/errors.go +++ b/protocol/x/clob/types/errors.go @@ -308,6 +308,16 @@ var ( 1020, "Position cannot be fully offset", ) + ErrDeleveragingIsFinalSettlementFlagMismatch = errorsmod.Register( + ModuleName, + 1021, + "Deleveraging match has incorrect value for isFinalSettlement flag", + ) + ErrLiquidationConflictsWithClobPairStatus = errorsmod.Register( + ModuleName, + 1022, + "Liquidation conflicts with ClobPair status", + ) // Advanced order type errors. ErrFokOrderCouldNotBeFullyFilled = errorsmod.Register( diff --git a/protocol/x/clob/types/internal_operation.go b/protocol/x/clob/types/internal_operation.go index 4d93a6021b..db46bb0059 100644 --- a/protocol/x/clob/types/internal_operation.go +++ b/protocol/x/clob/types/internal_operation.go @@ -112,6 +112,7 @@ func NewMatchPerpetualDeleveragingInternalOperation( liquidatedSubaccountId satypes.SubaccountId, perpetualId uint32, fills []MatchPerpetualDeleveraging_Fill, + isFinalSettlement bool, ) InternalOperation { if len(fills) == 0 { panic( @@ -129,9 +130,10 @@ func NewMatchPerpetualDeleveragingInternalOperation( Match: &ClobMatch{ Match: &ClobMatch_MatchPerpetualDeleveraging{ MatchPerpetualDeleveraging: &MatchPerpetualDeleveraging{ - Liquidated: liquidatedSubaccountId, - PerpetualId: perpetualId, - Fills: fills, + Liquidated: liquidatedSubaccountId, + PerpetualId: perpetualId, + Fills: fills, + IsFinalSettlement: isFinalSettlement, }, }, }, diff --git a/protocol/x/clob/types/mem_clob_keeper.go b/protocol/x/clob/types/mem_clob_keeper.go index 1562a2c33f..381c5870d1 100644 --- a/protocol/x/clob/types/mem_clob_keeper.go +++ b/protocol/x/clob/types/mem_clob_keeper.go @@ -40,7 +40,8 @@ type MemClobKeeper interface { CanDeleverageSubaccount( ctx sdk.Context, subaccountId satypes.SubaccountId, - ) (bool, error) + perpetualId uint32, + ) (bool, bool, error) GetStatePosition( ctx sdk.Context, subaccountId satypes.SubaccountId, @@ -90,6 +91,7 @@ type MemClobKeeper interface { liquidatedSubaccountId satypes.SubaccountId, perpetualId uint32, deltaQuantumsTotal *big.Int, + isFinalSettlement bool, ) ( fills []MatchPerpetualDeleveraging_Fill, deltaQuantumsRemaining *big.Int, diff --git a/protocol/x/clob/types/memclob.go b/protocol/x/clob/types/memclob.go index 352c741787..a90aa4f68c 100644 --- a/protocol/x/clob/types/memclob.go +++ b/protocol/x/clob/types/memclob.go @@ -90,6 +90,7 @@ type MemClob interface { subaccountId satypes.SubaccountId, perpetualId uint32, deltaQuantums *big.Int, + isFinalSettlement bool, ) ( quantumsDeleveraged *big.Int, err error, diff --git a/protocol/x/clob/types/operations_to_propose.go b/protocol/x/clob/types/operations_to_propose.go index fb8fb44dab..59e0bbc645 100644 --- a/protocol/x/clob/types/operations_to_propose.go +++ b/protocol/x/clob/types/operations_to_propose.go @@ -247,6 +247,7 @@ func (o *OperationsToPropose) MustAddDeleveragingToOperationsQueue( liquidatedSubaccountId satypes.SubaccountId, perpetualId uint32, fills []MatchPerpetualDeleveraging_Fill, + isFinalSettlement bool, ) { if len(fills) == 0 { panic( @@ -306,6 +307,7 @@ func (o *OperationsToPropose) MustAddDeleveragingToOperationsQueue( liquidatedSubaccountId, perpetualId, fills, + isFinalSettlement, ), ) } diff --git a/protocol/x/clob/types/operations_to_propose_test.go b/protocol/x/clob/types/operations_to_propose_test.go index 56a1e1dddd..14b4a54f49 100644 --- a/protocol/x/clob/types/operations_to_propose_test.go +++ b/protocol/x/clob/types/operations_to_propose_test.go @@ -611,6 +611,7 @@ func TestMustAddDeleveraingToOperationsQueue(t *testing.T) { FillAmount: 5, }, }, + false, ) } @@ -714,6 +715,7 @@ func TestMustAddDeleveragingToOperationsQueue_Panics(t *testing.T) { tc.liquidatedSubaccountId, tc.perpetualId, tc.fills, + false, ) }, ) @@ -915,6 +917,7 @@ func TestGetOperationsToReplay_Success(t *testing.T) { FillAmount: 10, }, }, + false, ) }, expectedOperations: []types.InternalOperation{ @@ -1103,6 +1106,7 @@ func TestGetOperationsToPropose_Success(t *testing.T) { FillAmount: 10, }, }, + false, ) }, expectedOperations: []types.OperationRaw{