From 5dd3bae108a4c30aac23f9bdf0b298118411bef7 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz <125316911+jakob-dydx@users.noreply.github.com> Date: Wed, 20 Dec 2023 12:28:10 -0800 Subject: [PATCH] [CLOB-1017] final settlement deleveraging step in PrepareCheckState (#848) * add functionality in ProcessDeleveraging to allow settlement at oracle price instead of bankruptcy price * pull deltaQuoteQuantums out of ProcessDeleveraging * fix lint * fix issue with test * pr nits * update getDeleveragingQuoteQuantumsDelta helper * initial implementation of final settlement deleveraging * add delivertx logic for deleveraging match * update to have DeliverTx calculate price based off of IsFinalSettlement flag instead of calculating implicitly based off of clob pair status, and update indexer deleveraging event so we can send isFinalSettlement flag * update delivertx validation * allow final settlement subaccounts to be deleveraged by regular deleveraging flow * remove superfluous comment * update err name * fix import formatting * lint * begin adding tests * update logic so that isFinalSettlement flag on operation is used * re-use helper function * pr nits, redefine CanDeleverageSubaccount * update tests for CanDeleverageSubaccount * split up PCS steps 6 and 7 to be for liquidations and then for deleveraging * update to use new subaccountOpenPositionInfo type * update tests * pr nits * lint * formatting * nits and tests * more nits and comment updates * [CLOB-1021] final settlement DeliverTx validation to block trading (#834) * add missing indexer constants * update proto formatting * fix indexer test * set up ground work for allowing only deleveraging events for final settlement markets * test trading is blocked in process proposer operations for final settlement * update comments * add DeliverTx tests for process operations final settlement deleveraging operations * pr nits * nit * merge from upstream * format --- protocol/indexer/events/deleveraging.go | 14 +- protocol/indexer/events/deleveraging_test.go | 4 +- protocol/lib/metrics/constants.go | 213 ++++++------ protocol/lib/metrics/metric_keys.go | 1 + protocol/mocks/MemClob.go | 83 +++-- protocol/mocks/MemClobKeeper.go | 102 ++++-- protocol/testutil/memclob/keeper.go | 5 +- protocol/x/clob/abci.go | 22 +- .../clob/e2e/liquidation_deleveraging_test.go | 95 +++++- protocol/x/clob/keeper/clob_pair.go | 49 +++ protocol/x/clob/keeper/deleveraging.go | 210 ++++++++---- protocol/x/clob/keeper/deleveraging_test.go | 103 +++++- protocol/x/clob/keeper/liquidations.go | 70 ++-- protocol/x/clob/keeper/liquidations_test.go | 76 +++++ protocol/x/clob/keeper/process_operations.go | 50 ++- .../x/clob/keeper/process_operations_test.go | 312 ++++++++++++++++++ protocol/x/clob/memclob/memclob.go | 3 + protocol/x/clob/types/clob_pair.go | 4 + protocol/x/clob/types/clob_pair_test.go | 4 + protocol/x/clob/types/errors.go | 10 + protocol/x/clob/types/internal_operation.go | 8 +- protocol/x/clob/types/mem_clob_keeper.go | 4 +- protocol/x/clob/types/memclob.go | 1 + .../x/clob/types/operations_to_propose.go | 2 + .../clob/types/operations_to_propose_test.go | 4 + 25 files changed, 1164 insertions(+), 285 deletions(-) diff --git a/protocol/indexer/events/deleveraging.go b/protocol/indexer/events/deleveraging.go index 7e67f0ea0b..a7a9b4feed 100644 --- a/protocol/indexer/events/deleveraging.go +++ b/protocol/indexer/events/deleveraging.go @@ -14,15 +14,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/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/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 d35ddd2f7e..ff9fc7b936 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( @@ -601,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 trading is blocked. + 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 diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index 928e79cb49..c8c89c85cd 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,17 @@ 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 { + if !shouldDeleverageAtBankruptcyPrice && !shouldDeleverageAtOraclePrice { metrics.IncrCounter( metrics.ClobPrepareCheckStateCannotDeleverageSubaccount, 1, @@ -62,7 +66,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 +147,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 +165,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 + clobPairId, err := k.GetClobPairIdForPerpetual(ctx, perpetualId) + if err != nil { + return false, false, err + } + clobPair := k.mustGetClobPair(ctx, clobPairId) + + // 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 + // 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 +215,7 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( liquidatedSubaccountId satypes.SubaccountId, perpetualId uint32, deltaQuantumsTotal *big.Int, + isFinalSettlement bool, ) ( fills []types.MatchPerpetualDeleveraging_Fill, deltaQuantumsRemaining *big.Int, @@ -229,11 +251,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 +264,8 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( ctx, perpetualId, liquidatedSubaccountId, - deltaQuantums, + deltaBaseQuantums, + isFinalSettlement, ) if err != nil { liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) @@ -251,9 +274,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 +288,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 +337,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 +374,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 +532,88 @@ 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 func() { + 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/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/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 1e7368d6a2..5be94c81e2 100644 --- a/protocol/x/clob/types/clob_pair.go +++ b/protocol/x/clob/types/clob_pair.go @@ -33,6 +33,10 @@ func IsSupportedClobPairStatus(clobPairStatus ClobPair_Status) bool { // 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 } diff --git a/protocol/x/clob/types/clob_pair_test.go b/protocol/x/clob/types/clob_pair_test.go index 9065372d1c..8312f771dc 100644 --- a/protocol/x/clob/types/clob_pair_test.go +++ b/protocol/x/clob/types/clob_pair_test.go @@ -86,6 +86,10 @@ 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 toClobPairStatus == fromClobPairStatus { + continue + } + switch fromClobPairStatus { case int32(types.ClobPair_STATUS_INITIALIZING): { 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{