diff --git a/protocol/app/ante_whitebox_test.go b/protocol/app/ante_whitebox_test.go index 03d0b67f35..4effb1619c 100644 --- a/protocol/app/ante_whitebox_test.go +++ b/protocol/app/ante_whitebox_test.go @@ -1,10 +1,11 @@ package app import ( - "github.com/dydxprotocol/v4-chain/protocol/lib" "reflect" "testing" + "github.com/dydxprotocol/v4-chain/protocol/lib" + delaymsgmoduletypes "github.com/dydxprotocol/v4-chain/protocol/x/delaymsg/types" "github.com/dydxprotocol/v4-chain/protocol/x/clob/rate_limit" @@ -15,6 +16,7 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" feegrantkeeper "github.com/cosmos/cosmos-sdk/x/feegrant/keeper" + liquidationtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/liquidations" libante "github.com/dydxprotocol/v4-chain/protocol/lib/ante" clobante "github.com/dydxprotocol/v4-chain/protocol/x/clob/ante" "github.com/dydxprotocol/v4-chain/protocol/x/clob/flags" @@ -72,6 +74,7 @@ func newTestHandlerOptions() HandlerOptions { flags.GetDefaultClobFlags(), rate_limit.NewNoOpRateLimiter[*types.MsgPlaceOrder](), rate_limit.NewNoOpRateLimiter[*types.MsgCancelOrder](), + liquidationtypes.NewDaemonLiquidationInfo(), ) return HandlerOptions{ HandlerOptions: ante.HandlerOptions{ diff --git a/protocol/app/app.go b/protocol/app/app.go index 8af600e5f8..cb3e38c93f 100644 --- a/protocol/app/app.go +++ b/protocol/app/app.go @@ -903,6 +903,7 @@ func New( clobFlags, rate_limit.NewPanicRateLimiter[*clobmoduletypes.MsgPlaceOrder](), rate_limit.NewPanicRateLimiter[*clobmoduletypes.MsgCancelOrder](), + daemonLiquidationInfo, ) clobModule := clobmodule.NewAppModule( appCodec, @@ -910,7 +911,6 @@ func New( app.AccountKeeper, app.BankKeeper, app.SubaccountsKeeper, - daemonLiquidationInfo, ) app.PerpetualsKeeper.SetClobKeeper(app.ClobKeeper) diff --git a/protocol/daemons/server/types/liquidations/daemon_liquidation_info.go b/protocol/daemons/server/types/liquidations/daemon_liquidation_info.go index 68f887abf7..dea31851d3 100644 --- a/protocol/daemons/server/types/liquidations/daemon_liquidation_info.go +++ b/protocol/daemons/server/types/liquidations/daemon_liquidation_info.go @@ -97,21 +97,37 @@ func (ls *DaemonLiquidationInfo) UpdateSubaccountsWithPositions( } } -// GetSubaccountsWithPositions returns the list of subaccount ids with open positions. -func (ls *DaemonLiquidationInfo) GetSubaccountsWithPositions() map[uint32]*clobtypes.SubaccountOpenPositionInfo { +// GetSubaccountsWithOpenPositions returns the list of subaccount ids with open positions for a perpetual. +func (ls *DaemonLiquidationInfo) GetSubaccountsWithOpenPositions( + perpetualId uint32, +) []satypes.SubaccountId { ls.Lock() defer ls.Unlock() - result := make(map[uint32]*clobtypes.SubaccountOpenPositionInfo) - for perpetualId, info := range ls.subaccountsWithPositions { - clone := &clobtypes.SubaccountOpenPositionInfo{ - PerpetualId: perpetualId, - SubaccountsWithLongPosition: make([]satypes.SubaccountId, len(info.SubaccountsWithLongPosition)), - SubaccountsWithShortPosition: make([]satypes.SubaccountId, len(info.SubaccountsWithShortPosition)), + result := make([]satypes.SubaccountId, 0) + if info, ok := ls.subaccountsWithPositions[perpetualId]; ok { + result = append(result, info.SubaccountsWithLongPosition...) + result = append(result, info.SubaccountsWithShortPosition...) + } + return result +} + +// GetSubaccountsWithOpenPositionsOnSide returns the list of subaccount ids with open positions +// on a specific side for a perpetual. +func (ls *DaemonLiquidationInfo) GetSubaccountsWithOpenPositionsOnSide( + perpetualId uint32, + isLong bool, +) []satypes.SubaccountId { + ls.Lock() + defer ls.Unlock() + + result := make([]satypes.SubaccountId, 0) + if info, ok := ls.subaccountsWithPositions[perpetualId]; ok { + if isLong { + result = append(result, info.SubaccountsWithLongPosition...) + } else { + result = append(result, info.SubaccountsWithShortPosition...) } - copy(clone.SubaccountsWithLongPosition, info.SubaccountsWithLongPosition) - copy(clone.SubaccountsWithShortPosition, info.SubaccountsWithShortPosition) - result[perpetualId] = clone } return result } diff --git a/protocol/daemons/server/types/liquidations/daemon_liquidation_info_test.go b/protocol/daemons/server/types/liquidations/daemon_liquidation_info_test.go index e699d28473..8d390663fe 100644 --- a/protocol/daemons/server/types/liquidations/daemon_liquidation_info_test.go +++ b/protocol/daemons/server/types/liquidations/daemon_liquidation_info_test.go @@ -14,7 +14,7 @@ func TestNewDaemonLiquidationInfo(t *testing.T) { ls := liquidationstypes.NewDaemonLiquidationInfo() require.Empty(t, ls.GetLiquidatableSubaccountIds()) require.Empty(t, ls.GetNegativeTncSubaccountIds()) - require.Empty(t, ls.GetSubaccountsWithPositions()) + require.Empty(t, ls.GetSubaccountsWithOpenPositions(0)) } func TestLiquidatableSubaccountIds_Multiple_Reads(t *testing.T) { @@ -62,12 +62,13 @@ func TestSubaccountsWithOpenPositions_Multiple_Reads(t *testing.T) { input := []clobtypes.SubaccountOpenPositionInfo{info} ls.UpdateSubaccountsWithPositions(input) - expected := map[uint32]*clobtypes.SubaccountOpenPositionInfo{ - 0: &info, + expected := []satypes.SubaccountId{ + constants.Alice_Num1, + constants.Bob_Num0, } - require.Equal(t, expected, ls.GetSubaccountsWithPositions()) - require.Equal(t, expected, ls.GetSubaccountsWithPositions()) - require.Equal(t, expected, ls.GetSubaccountsWithPositions()) + require.Equal(t, expected, ls.GetSubaccountsWithOpenPositions(0)) + require.Equal(t, expected, ls.GetSubaccountsWithOpenPositions(0)) + require.Equal(t, expected, ls.GetSubaccountsWithOpenPositions(0)) } func TestLiquidatableSubaccountIds_Multiple_Writes(t *testing.T) { @@ -118,7 +119,7 @@ func TestNegativeTncSubaccounts_Multiple_Writes(t *testing.T) { func TestSubaccountsWithOpenPositions_Multiple_Writes(t *testing.T) { ls := liquidationstypes.NewDaemonLiquidationInfo() - require.Empty(t, ls.GetSubaccountsWithPositions()) + require.Empty(t, ls.GetSubaccountsWithOpenPositions(0)) info := clobtypes.SubaccountOpenPositionInfo{ PerpetualId: 0, @@ -132,10 +133,11 @@ func TestSubaccountsWithOpenPositions_Multiple_Writes(t *testing.T) { input := []clobtypes.SubaccountOpenPositionInfo{info} ls.UpdateSubaccountsWithPositions(input) - expected := map[uint32]*clobtypes.SubaccountOpenPositionInfo{ - 0: &info, + expected := []satypes.SubaccountId{ + constants.Alice_Num1, + constants.Bob_Num0, } - require.Equal(t, expected, ls.GetSubaccountsWithPositions()) + require.Equal(t, expected, ls.GetSubaccountsWithOpenPositions(0)) info2 := clobtypes.SubaccountOpenPositionInfo{ PerpetualId: 0, @@ -149,10 +151,11 @@ func TestSubaccountsWithOpenPositions_Multiple_Writes(t *testing.T) { input2 := []clobtypes.SubaccountOpenPositionInfo{info2} ls.UpdateSubaccountsWithPositions(input2) - expected = map[uint32]*clobtypes.SubaccountOpenPositionInfo{ - 0: &info2, + expected = []satypes.SubaccountId{ + constants.Carl_Num0, + constants.Dave_Num0, } - require.Equal(t, expected, ls.GetSubaccountsWithPositions()) + require.Equal(t, expected, ls.GetSubaccountsWithOpenPositions(0)) info3 := clobtypes.SubaccountOpenPositionInfo{ PerpetualId: 0, @@ -166,10 +169,11 @@ func TestSubaccountsWithOpenPositions_Multiple_Writes(t *testing.T) { input3 := []clobtypes.SubaccountOpenPositionInfo{info3} ls.UpdateSubaccountsWithPositions(input3) - expected = map[uint32]*clobtypes.SubaccountOpenPositionInfo{ - 0: &info3, + expected = []satypes.SubaccountId{ + constants.Dave_Num1, + constants.Alice_Num1, } - require.Equal(t, expected, ls.GetSubaccountsWithPositions()) + require.Equal(t, expected, ls.GetSubaccountsWithOpenPositions(0)) } func TestLiquidatableSubaccountIds_Empty_Update(t *testing.T) { @@ -204,7 +208,7 @@ func TestNegativeTnc_Empty_Update(t *testing.T) { func TestSubaccountsWithOpenPosition_Empty_Update(t *testing.T) { ls := liquidationstypes.NewDaemonLiquidationInfo() - require.Empty(t, ls.GetSubaccountsWithPositions()) + require.Empty(t, ls.GetSubaccountsWithOpenPositions(0)) info := clobtypes.SubaccountOpenPositionInfo{ PerpetualId: 0, @@ -217,12 +221,13 @@ func TestSubaccountsWithOpenPosition_Empty_Update(t *testing.T) { } input := []clobtypes.SubaccountOpenPositionInfo{info} ls.UpdateSubaccountsWithPositions(input) - expected := map[uint32]*clobtypes.SubaccountOpenPositionInfo{ - 0: &info, + expected := []satypes.SubaccountId{ + constants.Alice_Num1, + constants.Bob_Num0, } - require.Equal(t, expected, ls.GetSubaccountsWithPositions()) + require.Equal(t, expected, ls.GetSubaccountsWithOpenPositions(0)) input2 := []clobtypes.SubaccountOpenPositionInfo{} ls.UpdateSubaccountsWithPositions(input2) - require.Empty(t, ls.GetSubaccountsWithPositions()) + require.Empty(t, ls.GetSubaccountsWithOpenPositions(0)) } diff --git a/protocol/testutil/clob/open_positions.go b/protocol/testutil/clob/open_positions.go new file mode 100644 index 0000000000..66c7f25e71 --- /dev/null +++ b/protocol/testutil/clob/open_positions.go @@ -0,0 +1,42 @@ +package clob + +import ( + clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" +) + +func GetOpenPositionsFromSubaccounts( + subaccounts []satypes.Subaccount, +) []clobtypes.SubaccountOpenPositionInfo { + positionMap := make(map[uint32]*clobtypes.SubaccountOpenPositionInfo) + for _, subaccount := range subaccounts { + for _, position := range subaccount.PerpetualPositions { + info, ok := positionMap[position.PerpetualId] + if !ok { + info = &clobtypes.SubaccountOpenPositionInfo{ + PerpetualId: position.PerpetualId, + SubaccountsWithLongPosition: make([]satypes.SubaccountId, 0), + SubaccountsWithShortPosition: make([]satypes.SubaccountId, 0), + } + positionMap[position.PerpetualId] = info + } + if position.GetIsLong() { + info.SubaccountsWithLongPosition = append( + info.SubaccountsWithLongPosition, + *subaccount.Id, + ) + } else { + info.SubaccountsWithShortPosition = append( + info.SubaccountsWithShortPosition, + *subaccount.Id, + ) + } + } + } + + positionSlice := make([]clobtypes.SubaccountOpenPositionInfo, 0) + for _, info := range positionMap { + positionSlice = append(positionSlice, *info) + } + return positionSlice +} diff --git a/protocol/testutil/keeper/clob.go b/protocol/testutil/keeper/clob.go index 1681e0fed9..b6aed9ce9c 100644 --- a/protocol/testutil/keeper/clob.go +++ b/protocol/testutil/keeper/clob.go @@ -23,6 +23,7 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + liquidationtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/liquidations" asskeeper "github.com/dydxprotocol/v4-chain/protocol/x/assets/keeper" blocktimekeeper "github.com/dydxprotocol/v4-chain/protocol/x/blocktime/keeper" "github.com/dydxprotocol/v4-chain/protocol/x/clob/keeper" @@ -217,6 +218,7 @@ func createClobKeeper( flags.GetDefaultClobFlags(), rate_limit.NewNoOpRateLimiter[*types.MsgPlaceOrder](), rate_limit.NewNoOpRateLimiter[*types.MsgCancelOrder](), + liquidationtypes.NewDaemonLiquidationInfo(), ) k.SetAnteHandler(constants.EmptyAnteHandler) diff --git a/protocol/x/clob/abci.go b/protocol/x/clob/abci.go index 9739e24160..b65caa1899 100644 --- a/protocol/x/clob/abci.go +++ b/protocol/x/clob/abci.go @@ -6,7 +6,6 @@ import ( "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" - liquidationtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/liquidations" indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events" "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" indexershared "github.com/dydxprotocol/v4-chain/protocol/indexer/shared" @@ -117,7 +116,6 @@ func EndBlocker( func PrepareCheckState( ctx sdk.Context, keeper *keeper.Keeper, - daemonLiquidationInfo *liquidationtypes.DaemonLiquidationInfo, ) { // Get the events generated from processing the matches in the latest block. processProposerMatchesEvents := keeper.GetProcessProposerMatchesEvents(ctx) @@ -197,20 +195,16 @@ func PrepareCheckState( } // 6. Get all potentially liquidatable subaccount IDs and attempt to liquidate them. - liquidatableSubaccountIds := daemonLiquidationInfo.GetLiquidatableSubaccountIds() + liquidatableSubaccountIds := keeper.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, - )..., + keeper.GetSubaccountsWithPositionsInFinalSettlementMarkets(ctx)..., ) // 7. Deleverage subaccounts. diff --git a/protocol/x/clob/abci_test.go b/protocol/x/clob/abci_test.go index a5df03a616..8f962a1ee5 100644 --- a/protocol/x/clob/abci_test.go +++ b/protocol/x/clob/abci_test.go @@ -25,7 +25,6 @@ import ( "github.com/cosmos/cosmos-sdk/store/prefix" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" - liquidationtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/liquidations" "github.com/dydxprotocol/v4-chain/protocol/mocks" keepertest "github.com/dydxprotocol/v4-chain/protocol/testutil/keeper" blocktimetypes "github.com/dydxprotocol/v4-chain/protocol/x/blocktime/types" @@ -1099,7 +1098,6 @@ func TestPrepareCheckState_WithProcessProposerMatchesEventsWithBadBlockHeight(t clob.PrepareCheckState( ks.Ctx.WithBlockHeight(int64(blockHeight+1)), ks.ClobKeeper, - liquidationtypes.NewDaemonLiquidationInfo(), ) }) } @@ -1126,7 +1124,6 @@ func TestCommitBlocker_WithProcessProposerMatchesEventsWithBadBlockHeight(t *tes clob.PrepareCheckState( ks.Ctx.WithBlockHeight(int64(blockHeight+1)), ks.ClobKeeper, - liquidationtypes.NewDaemonLiquidationInfo(), ) }) } @@ -1475,14 +1472,12 @@ func TestPrepareCheckState(t *testing.T) { } // Set the liquidatable subaccount IDs. - liquidatableSubaccountIds := liquidationtypes.NewDaemonLiquidationInfo() - liquidatableSubaccountIds.UpdateLiquidatableSubaccountIds(tc.liquidatableSubaccounts) + ks.ClobKeeper.DaemonLiquidationInfo.UpdateLiquidatableSubaccountIds(tc.liquidatableSubaccounts) // Run the test. clob.PrepareCheckState( ctx, ks.ClobKeeper, - liquidatableSubaccountIds, ) // Verify test expectations. diff --git a/protocol/x/clob/e2e/liquidation_deleveraging_test.go b/protocol/x/clob/e2e/liquidation_deleveraging_test.go index ac9de31a7a..167ff50fd8 100644 --- a/protocol/x/clob/e2e/liquidation_deleveraging_test.go +++ b/protocol/x/clob/e2e/liquidation_deleveraging_test.go @@ -10,6 +10,7 @@ import ( "github.com/cometbft/cometbft/types" testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" + clobtest "github.com/dydxprotocol/v4-chain/protocol/testutil/clob" "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" assettypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" @@ -645,7 +646,6 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { // Parameters. placedMatchableOrders []clobtypes.MatchableOrder liquidatableSubaccountIds []satypes.SubaccountId - subaccountPositionInfo []clobtypes.SubaccountOpenPositionInfo // Configuration. liquidationConfig clobtypes.LiquidationsConfig @@ -1046,17 +1046,6 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { 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 @@ -1092,17 +1081,6 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { 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, @@ -1216,7 +1194,7 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { _, err := tApp.App.Server.LiquidateSubaccounts(ctx, &api.LiquidateSubaccountsRequest{ LiquidatableSubaccountIds: tc.liquidatableSubaccountIds, - SubaccountOpenPositionInfo: tc.subaccountPositionInfo, + SubaccountOpenPositionInfo: clobtest.GetOpenPositionsFromSubaccounts(tc.subaccounts), }) require.NoError(t, err) diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index f1b0e4b1c1..2e4123dfff 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -233,122 +233,137 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( deltaQuantumsRemaining = new(big.Int).Set(deltaQuantumsTotal) fills = make([]types.MatchPerpetualDeleveraging_Fill, 0) - k.subaccountsKeeper.ForEachSubaccountRandomStart( - ctx, - func(offsettingSubaccount satypes.Subaccount) (finished bool) { - // Iterate at most `MaxDeleveragingSubaccountsToIterate` subaccounts. - if numSubaccountsIterated >= k.Flags.MaxDeleveragingSubaccountsToIterate { - return true - } - - numSubaccountsIterated++ - offsettingPosition, _ := offsettingSubaccount.GetPerpetualPositionForId(perpetualId) - bigOffsettingPositionQuantums := offsettingPosition.GetBigQuantums() - - // Skip subaccounts that do not have a position in the opposite direction as the liquidated subaccount. - if deltaQuantumsRemaining.Sign() != bigOffsettingPositionQuantums.Sign() { - numSubaccountsWithNoOpenPositionOnOppositeSide++ - return false - } - - // TODO(DEC-1495): Determine max amount to offset per offsetting subaccount. - var deltaBaseQuantums *big.Int - if deltaQuantumsRemaining.CmpAbs(bigOffsettingPositionQuantums) > 0 { - deltaBaseQuantums = new(big.Int).Set(bigOffsettingPositionQuantums) - } else { - deltaBaseQuantums = new(big.Int).Set(deltaQuantumsRemaining) - } - - // Fetch delta quote quantums. Calculated at bankruptcy price for standard - // deleveraging and at oracle price for final settlement deleveraging. - deltaQuoteQuantums, err := k.getDeleveragingQuoteQuantumsDelta( - ctx, - perpetualId, - liquidatedSubaccountId, - deltaBaseQuantums, - isFinalSettlement, + // Find subaccounts with open positions on the opposite side of the liquidated subaccount. + isDeleveragingLong := deltaQuantumsTotal.Sign() == -1 + subaccountsWithOpenPositions := k.DaemonLiquidationInfo.GetSubaccountsWithOpenPositionsOnSide( + perpetualId, + !isDeleveragingLong, + ) + + numSubaccounts := len(subaccountsWithOpenPositions) + if numSubaccounts == 0 { + liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) + k.Logger(ctx).Error( + "Failed to find subaccounts with open positions on opposite side of liquidated subaccount", + "blockHeight", ctx.BlockHeight(), + "perpetualId", perpetualId, + "deltaQuantumsTotal", deltaQuantumsTotal, + "liquidatedSubaccount", liquidatedSubaccount, + ) + return fills, deltaQuantumsRemaining + } + + // Start from a random subaccount. + pseudoRand := k.GetPseudoRand(ctx) + indexOffset := pseudoRand.Intn(numSubaccounts) + + // Iterate at most `MaxDeleveragingSubaccountsToIterate` subaccounts. + numSubaccountsToIterate := lib.Min(numSubaccounts, int(k.Flags.MaxDeleveragingSubaccountsToIterate)) + + for i := 0; i < numSubaccountsToIterate && deltaQuantumsRemaining.Sign() != 0; i++ { + index := (i + indexOffset) % numSubaccounts + subaccountId := subaccountsWithOpenPositions[index] + + numSubaccountsIterated++ + offsettingSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, subaccountId) + offsettingPosition, _ := offsettingSubaccount.GetPerpetualPositionForId(perpetualId) + bigOffsettingPositionQuantums := offsettingPosition.GetBigQuantums() + + // Skip subaccounts that do not have a position in the opposite direction as the liquidated subaccount. + if deltaQuantumsRemaining.Sign() != bigOffsettingPositionQuantums.Sign() { + numSubaccountsWithNoOpenPositionOnOppositeSide++ + continue + } + + // TODO(DEC-1495): Determine max amount to offset per offsetting subaccount. + var deltaBaseQuantums *big.Int + if deltaQuantumsRemaining.CmpAbs(bigOffsettingPositionQuantums) > 0 { + deltaBaseQuantums = new(big.Int).Set(bigOffsettingPositionQuantums) + } else { + deltaBaseQuantums = new(big.Int).Set(deltaQuantumsRemaining) + } + + // Fetch delta quote quantums. Calculated at bankruptcy price for standard + // deleveraging and at oracle price for final settlement deleveraging. + deltaQuoteQuantums, err := k.getDeleveragingQuoteQuantumsDelta( + ctx, + perpetualId, + liquidatedSubaccountId, + deltaBaseQuantums, + isFinalSettlement, + ) + if err != nil { + liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) + k.Logger(ctx).Error( + "Encountered error when getting quote quantums for deleveraging", + "error", err, + "blockHeight", ctx.BlockHeight(), + "perpetualId", perpetualId, + "deltaBaseQuantums", deltaBaseQuantums, + "liquidatedSubaccount", liquidatedSubaccount, + "offsettingSubaccount", offsettingSubaccount, ) - if err != nil { - liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) - k.Logger(ctx).Error( - "Encountered error when getting quote quantums for deleveraging", - "error", err, - "blockHeight", ctx.BlockHeight(), - "perpetualId", perpetualId, - "deltaBaseQuantums", deltaBaseQuantums, - "liquidatedSubaccount", liquidatedSubaccount, - "offsettingSubaccount", offsettingSubaccount, - "isFinalSettlement", isFinalSettlement, - ) - return false - } - - // Try to process the deleveraging operation for both subaccounts. - if err := k.ProcessDeleveraging( + continue + } + + // Try to process the deleveraging operation for both subaccounts. + if err := k.ProcessDeleveraging( + ctx, + liquidatedSubaccountId, + *offsettingSubaccount.Id, + perpetualId, + deltaBaseQuantums, + deltaQuoteQuantums, + ); err == nil { + // Update the remaining liquidatable quantums. + deltaQuantumsRemaining.Sub(deltaQuantumsRemaining, deltaBaseQuantums) + fills = append(fills, types.MatchPerpetualDeleveraging_Fill{ + OffsettingSubaccountId: *offsettingSubaccount.Id, + 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, - liquidatedSubaccountId, - *offsettingSubaccount.Id, - perpetualId, - deltaBaseQuantums, - deltaQuoteQuantums, - ); err == nil { - // Update the remaining liquidatable quantums. - deltaQuantumsRemaining = new(big.Int).Sub( - deltaQuantumsRemaining, - deltaBaseQuantums, - ) - fills = append(fills, types.MatchPerpetualDeleveraging_Fill{ - OffsettingSubaccountId: *offsettingSubaccount.Id, - 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, - ), + 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( - "Invalid perpetual position size delta when processing deleveraging. error: %v", - err, - ), - ) - } else { - // If an error is returned, it's likely because the subaccounts' bankruptcy prices do not overlap. - // TODO(CLOB-75): Support deleveraging subaccounts with non overlapping bankruptcy prices. - liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) - offsettingSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, *offsettingSubaccount.Id) - k.Logger(ctx).Debug( - "Encountered error when processing deleveraging", - "error", err, - "blockHeight", ctx.BlockHeight(), - "checkTx", ctx.IsCheckTx(), - "perpetualId", perpetualId, - "deltaQuantums", deltaBaseQuantums, - "liquidatedSubaccount", liquidatedSubaccount, - "offsettingSubaccount", offsettingSubaccount, - ) - numSubaccountsWithNonOverlappingBankruptcyPrices++ - } - - return deltaQuantumsRemaining.Sign() == 0 - }, - k.GetPseudoRand(ctx), - ) + ), + ) + } else if errors.Is(err, types.ErrInvalidPerpetualPositionSizeDelta) { + panic( + fmt.Sprintf( + "Invalid perpetual position size delta when processing deleveraging. error: %v", + err, + ), + ) + } else { + // If an error is returned, it's likely because the subaccounts' bankruptcy prices do not overlap. + // TODO(CLOB-75): Support deleveraging subaccounts with non overlapping bankruptcy prices. + liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) + offsettingSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, *offsettingSubaccount.Id) + k.Logger(ctx).Debug( + "Encountered error when processing deleveraging", + "error", err, + "blockHeight", ctx.BlockHeight(), + "checkTx", ctx.IsCheckTx(), + "perpetualId", perpetualId, + "deltaBaseQuantums", deltaBaseQuantums, + "liquidatedSubaccount", liquidatedSubaccount, + "offsettingSubaccount", offsettingSubaccount, + ) + numSubaccountsWithNonOverlappingBankruptcyPrices++ + } + } labels := []metrics.Label{ metrics.GetLabelForIntValue(metrics.PerpetualId, int(perpetualId)), @@ -542,7 +557,6 @@ func (k Keeper) ProcessDeleveraging( // 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(), @@ -557,19 +571,10 @@ func (k Keeper) GetSubaccountsWithPositionsInFinalSettlementMarkets( } 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 { + subaccountsWithPosition := k.DaemonLiquidationInfo.GetSubaccountsWithOpenPositions( + finalSettlementPerpetualId, + ) + for _, subaccountId := range subaccountsWithPosition { subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ SubaccountId: subaccountId, PerpetualId: finalSettlementPerpetualId, diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index c19ebe1eb2..49541a00f5 100644 --- a/protocol/x/clob/keeper/deleveraging_test.go +++ b/protocol/x/clob/keeper/deleveraging_test.go @@ -15,6 +15,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/dydxprotocol/v4-chain/protocol/dtypes" "github.com/dydxprotocol/v4-chain/protocol/mocks" + clobtest "github.com/dydxprotocol/v4-chain/protocol/testutil/clob" "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" keepertest "github.com/dydxprotocol/v4-chain/protocol/testutil/keeper" assettypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" @@ -799,6 +800,8 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { ).Return() } + positions := clobtest.GetOpenPositionsFromSubaccounts(tc.subaccounts) + ks.ClobKeeper.DaemonLiquidationInfo.UpdateSubaccountsWithPositions(positions) fills, deltaQuantumsRemaining := ks.ClobKeeper.OffsetSubaccountPerpetualPosition( ks.Ctx, tc.liquidatedSubaccountId, diff --git a/protocol/x/clob/keeper/keeper.go b/protocol/x/clob/keeper/keeper.go index dfe9f3f44d..ac4f3e959b 100644 --- a/protocol/x/clob/keeper/keeper.go +++ b/protocol/x/clob/keeper/keeper.go @@ -17,6 +17,7 @@ import ( "github.com/cosmos/cosmos-sdk/store/prefix" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + liquidationtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/liquidations" flags "github.com/dydxprotocol/v4-chain/protocol/x/clob/flags" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" ) @@ -56,6 +57,8 @@ type ( placeOrderRateLimiter rate_limit.RateLimiter[*types.MsgPlaceOrder] cancelOrderRateLimiter rate_limit.RateLimiter[*types.MsgCancelOrder] + + DaemonLiquidationInfo *liquidationtypes.DaemonLiquidationInfo } ) @@ -84,6 +87,7 @@ func NewKeeper( clobFlags flags.ClobFlags, placeOrderRateLimiter rate_limit.RateLimiter[*types.MsgPlaceOrder], cancelOrderRateLimiter rate_limit.RateLimiter[*types.MsgCancelOrder], + daemonLiquidationInfo *liquidationtypes.DaemonLiquidationInfo, ) *Keeper { keeper := &Keeper{ cdc: cdc, @@ -113,6 +117,7 @@ func NewKeeper( Flags: clobFlags, placeOrderRateLimiter: placeOrderRateLimiter, cancelOrderRateLimiter: cancelOrderRateLimiter, + DaemonLiquidationInfo: daemonLiquidationInfo, } // Provide the keeper to the MemClob. diff --git a/protocol/x/clob/keeper/liquidations_test.go b/protocol/x/clob/keeper/liquidations_test.go index a6e6efb63d..c98abf036c 100644 --- a/protocol/x/clob/keeper/liquidations_test.go +++ b/protocol/x/clob/keeper/liquidations_test.go @@ -2003,6 +2003,10 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { ks.SubaccountsKeeper.SetSubaccount(ctx, s) } + ks.ClobKeeper.DaemonLiquidationInfo.UpdateSubaccountsWithPositions( + clobtest.GetOpenPositionsFromSubaccounts(tc.subaccounts), + ) + for marketId, oraclePrice := range tc.marketIdToOraclePriceOverride { err := ks.PricesKeeper.UpdateMarketPrices( ctx, diff --git a/protocol/x/clob/module.go b/protocol/x/clob/module.go index 867fcb899c..38f9811023 100644 --- a/protocol/x/clob/module.go +++ b/protocol/x/clob/module.go @@ -18,7 +18,6 @@ import ( "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - liquidationtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/liquidations" "github.com/dydxprotocol/v4-chain/protocol/x/clob/client/cli" "github.com/dydxprotocol/v4-chain/protocol/x/clob/keeper" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" @@ -104,11 +103,10 @@ func (AppModuleBasic) GetQueryCmd() *cobra.Command { type AppModule struct { AppModuleBasic - keeper *keeper.Keeper - accountKeeper types.AccountKeeper - bankKeeper types.BankKeeper - subaccountsKeeper types.SubaccountsKeeper - daemonLiquidationInfo *liquidationtypes.DaemonLiquidationInfo + keeper *keeper.Keeper + accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper + subaccountsKeeper types.SubaccountsKeeper } func NewAppModule( @@ -117,15 +115,13 @@ func NewAppModule( accountKeeper types.AccountKeeper, bankKeeper types.BankKeeper, subaccountsKeeper types.SubaccountsKeeper, - daemonLiquidationInfo *liquidationtypes.DaemonLiquidationInfo, ) AppModule { return AppModule{ - AppModuleBasic: NewAppModuleBasic(cdc), - keeper: keeper, - accountKeeper: accountKeeper, - bankKeeper: bankKeeper, - subaccountsKeeper: subaccountsKeeper, - daemonLiquidationInfo: daemonLiquidationInfo, + AppModuleBasic: NewAppModuleBasic(cdc), + keeper: keeper, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + subaccountsKeeper: subaccountsKeeper, } } @@ -194,6 +190,5 @@ func (am AppModule) Commit(ctx sdk.Context) { PrepareCheckState( ctx, am.keeper, - am.daemonLiquidationInfo, ) } diff --git a/protocol/x/clob/module_test.go b/protocol/x/clob/module_test.go index 5bcd88da94..e806a6c921 100644 --- a/protocol/x/clob/module_test.go +++ b/protocol/x/clob/module_test.go @@ -21,7 +21,6 @@ import ( "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx" - liquidations_types "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/liquidations" "github.com/dydxprotocol/v4-chain/protocol/mocks" "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" "github.com/dydxprotocol/v4-chain/protocol/testutil/keeper" @@ -103,7 +102,6 @@ func createAppModuleWithKeeper(t *testing.T) ( nil, nil, nil, - liquidations_types.NewDaemonLiquidationInfo(), ), ks.ClobKeeper, ks.PricesKeeper, ks.PerpetualsKeeper, ks.Ctx, mockIndexerEventManager } diff --git a/protocol/x/clob/types/expected_keepers.go b/protocol/x/clob/types/expected_keepers.go index 4bcc0e0cb5..b17a7497eb 100644 --- a/protocol/x/clob/types/expected_keepers.go +++ b/protocol/x/clob/types/expected_keepers.go @@ -42,11 +42,6 @@ type SubaccountsKeeper interface { ) ( list []satypes.Subaccount, ) - ForEachSubaccountRandomStart( - ctx sdk.Context, - callback func(satypes.Subaccount) (finished bool), - rand *rand.Rand, - ) GetRandomSubaccount( ctx sdk.Context, rand *rand.Rand, diff --git a/protocol/x/subaccounts/keeper/subaccount.go b/protocol/x/subaccounts/keeper/subaccount.go index 4716ad3072..a6fff13edd 100644 --- a/protocol/x/subaccounts/keeper/subaccount.go +++ b/protocol/x/subaccounts/keeper/subaccount.go @@ -113,49 +113,6 @@ func (k Keeper) ForEachSubaccount(ctx sdk.Context, callback func(types.Subaccoun } } -// ForEachSubaccountRandomStart performs a callback across all subaccounts. -// The callback function should return a boolean if we should end iteration or not. -// Note that this function starts at a random subaccount using the passed in `rand` -// and iterates from there. `rand` should be seeded for determinism if used in ways -// that affect consensus. -// TODO(CLOB-823): improve how random bytes are selected since bytes distribution -// might not be uniform. -func (k Keeper) ForEachSubaccountRandomStart( - ctx sdk.Context, - callback func(types.Subaccount) (finished bool), - rand *rand.Rand, -) { - store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.SubaccountKeyPrefix)) - prefix, err := k.getRandomBytes(ctx, rand) - if err != nil { - return - } - - // Iterate over subaccounts from the random prefix (inclusive) to the end. - prefixStartIterator := store.Iterator(prefix, nil) - defer prefixStartIterator.Close() - for ; prefixStartIterator.Valid(); prefixStartIterator.Next() { - var subaccount types.Subaccount - k.cdc.MustUnmarshal(prefixStartIterator.Value(), &subaccount) - done := callback(subaccount) - if done { - return - } - } - - // Iterator over subaccounts from the start to the random prefix (exclusive). - prefixEndIterator := store.Iterator(nil, prefix) - defer prefixEndIterator.Close() - for ; prefixEndIterator.Valid(); prefixEndIterator.Next() { - var subaccount types.Subaccount - k.cdc.MustUnmarshal(prefixEndIterator.Value(), &subaccount) - done := callback(subaccount) - if done { - return - } - } -} - // GetRandomSubaccount returns a random subaccount. Will return an error if there are no subaccounts. func (k Keeper) GetRandomSubaccount(ctx sdk.Context, rand *rand.Rand) (types.Subaccount, error) { store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.SubaccountKeyPrefix)) diff --git a/protocol/x/subaccounts/keeper/subaccount_test.go b/protocol/x/subaccounts/keeper/subaccount_test.go index ae31a40f31..36e1c468a3 100644 --- a/protocol/x/subaccounts/keeper/subaccount_test.go +++ b/protocol/x/subaccounts/keeper/subaccount_test.go @@ -3,7 +3,6 @@ package keeper_test import ( "math" "math/big" - "math/rand" "strconv" "testing" @@ -206,78 +205,6 @@ func TestForEachSubaccount(t *testing.T) { } } -func TestForEachSubaccountRandomStart(t *testing.T) { - tests := map[string]struct { - numSubaccountsInState int - iterationCount int - }{ - "No subaccounts in state": { - numSubaccountsInState: 0, - iterationCount: 0, - }, - "one subaccount in state, one iteration": { - numSubaccountsInState: 1, - iterationCount: 1, - }, - "two subaccount in state, one iteration": { - numSubaccountsInState: 2, - iterationCount: 1, - }, - "ten subaccount in state, one iteration": { - numSubaccountsInState: 10, - iterationCount: 1, - }, - "ten subaccount in state, partial iteration": { - numSubaccountsInState: 10, - iterationCount: 8, - }, - "ten subaccount in state, full iteration": { - numSubaccountsInState: 10, - iterationCount: 10, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - rand := rand.New(rand.NewSource(53)) - ctx, keeper, _, _, _, _, _, _ := testutil.SubaccountsKeepers(t, true) - _ = createNSubaccount(keeper, ctx, tc.numSubaccountsInState, big.NewInt(1_000)) - collectedSubaccounts := make([]types.Subaccount, 0) - i := 0 - keeper.ForEachSubaccountRandomStart( - ctx, - func(subaccount types.Subaccount) bool { - i++ - collectedSubaccounts = append(collectedSubaccounts, subaccount) - return i == tc.iterationCount - }, - rand, - ) - - require.Len(t, collectedSubaccounts, tc.iterationCount) - - if tc.iterationCount > 0 { - subaccounts := keeper.GetAllSubaccount(ctx) - - offset := 0 - for i, subaccount := range subaccounts { - if *subaccount.Id == *collectedSubaccounts[0].Id { - offset = i - break - } - } - - for i := 0; i < tc.iterationCount; i++ { - require.Equal( - t, - subaccounts[(i+offset)%(tc.numSubaccountsInState)], - collectedSubaccounts[i], - ) - } - } - }) - } -} - func TestUpdateSubaccounts(t *testing.T) { // default subaccount id, the first subaccount id generated when calling createNSubaccount defaultSubaccountId := types.SubaccountId{