From 64598c868be7464644a86e924d60211a9a1318fb Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Mon, 4 Dec 2023 15:24:50 -0800 Subject: [PATCH 01/31] add functionality in ProcessDeleveraging to allow settlement at oracle price instead of bankruptcy price --- protocol/x/clob/keeper/deleveraging.go | 42 ++-- protocol/x/clob/keeper/deleveraging_test.go | 211 ++++++++++++++++++- protocol/x/clob/keeper/process_operations.go | 1 + 3 files changed, 237 insertions(+), 17 deletions(-) diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index 50e055e3ab..2f1e08269d 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -3,11 +3,12 @@ package keeper import ( "errors" "fmt" - indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events" - "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" "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" gometrics "github.com/armon/go-metrics" @@ -241,6 +242,7 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( *offsettingSubaccount.Id, perpetualId, deltaQuantums, + false, ); err == nil { // Update the remaining liquidatable quantums. deltaQuantumsRemaining = new(big.Int).Sub( @@ -323,6 +325,7 @@ func (k Keeper) ProcessDeleveraging( offsettingSubaccountId satypes.SubaccountId, perpetualId uint32, deltaQuantums *big.Int, + fillPriceIsOraclePrice bool, ) ( err error, ) { @@ -354,20 +357,29 @@ func (k Keeper) ProcessDeleveraging( ) } - // Calculate the bankruptcy price of the liquidated position. This is the price at which both positions - // are closed. - bankruptcyPriceQuoteQuantums, err := k.GetBankruptcyPriceInQuoteQuantums( - ctx, - liquidatedSubaccountId, - perpetualId, - deltaQuantums, - ) - if err != nil { - return err + fillPriceDeltaQuoteQuantums := new(big.Int) + if fillPriceIsOraclePrice { // Flow used for final settlement deleveraging events + fillPriceDeltaQuoteQuantums, err = k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaQuantums) + fillPriceDeltaQuoteQuantums.Neg(fillPriceDeltaQuoteQuantums) + if err != nil { + return err + } + } else { // Regular deleveraging flow + // Calculate the bankruptcy price of the liquidated position. This is the price at which both positions + // are closed. + fillPriceDeltaQuoteQuantums, err = k.GetBankruptcyPriceInQuoteQuantums( + ctx, + liquidatedSubaccountId, + perpetualId, + deltaQuantums, + ) + if err != nil { + return err + } } - deleveragedSubaccountQuoteBalanceDelta := bankruptcyPriceQuoteQuantums - offsettingSubaccountQuoteBalanceDelta := new(big.Int).Neg(bankruptcyPriceQuoteQuantums) + deleveragedSubaccountQuoteBalanceDelta := fillPriceDeltaQuoteQuantums + offsettingSubaccountQuoteBalanceDelta := new(big.Int).Neg(fillPriceDeltaQuoteQuantums) deleveragedSubaccountPerpetualQuantumsDelta := deltaQuantums offsettingSubaccountPerpetualQuantumsDelta := new(big.Int).Neg(deltaQuantums) @@ -465,7 +477,7 @@ func (k Keeper) ProcessDeleveraging( offsettingSubaccountId, perpetualId, satypes.BaseQuantums(new(big.Int).Abs(deltaQuantums).Uint64()), - satypes.BaseQuantums(bankruptcyPriceQuoteQuantums.Uint64()), + satypes.BaseQuantums(fillPriceDeltaQuoteQuantums.Uint64()), deltaQuantums.Sign() > 0, ), ), diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index 0f1fd8c15a..3d1906d5ca 100644 --- a/protocol/x/clob/keeper/deleveraging_test.go +++ b/protocol/x/clob/keeper/deleveraging_test.go @@ -2,13 +2,14 @@ package keeper_test import ( "errors" - indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events" - "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" "math" "math/big" "testing" "time" + indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events" + "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" @@ -1095,6 +1096,211 @@ func TestProcessDeleveraging(t *testing.T) { *tc.offsettingSubaccount.GetId(), uint32(0), tc.deltaQuantums, + false, + ) + if tc.expectedErr == nil { + require.NoError(t, err) + + actualLiquidated := ks.SubaccountsKeeper.GetSubaccount(ks.Ctx, *tc.liquidatedSubaccount.GetId()) + require.Equal( + t, + tc.expectedLiquidatedSubaccount, + actualLiquidated, + ) + + actualOffsetting := ks.SubaccountsKeeper.GetSubaccount(ks.Ctx, *tc.offsettingSubaccount.GetId()) + require.Equal( + t, + tc.expectedOffsettingSubaccount, + actualOffsetting, + ) + } else { + require.ErrorContains(t, err, tc.expectedErr.Error()) + } + }) + } +} + +func TestProcessDeleveragingAtOraclePrice(t *testing.T) { + tests := map[string]struct { + // Setup. + liquidatedSubaccount satypes.Subaccount + offsettingSubaccount satypes.Subaccount + deltaQuantums *big.Int + + // Expectations. + expectedLiquidatedSubaccount satypes.Subaccount + expectedOffsettingSubaccount satypes.Subaccount + expectedErr error + }{ + "Liquidated: well-collateralized, offsetting: well-collateralized": { + liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_100000USD, + offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50000USD, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC + + expectedLiquidatedSubaccount: satypes.Subaccount{ + Id: &constants.Carl_Num0, + AssetPositions: keepertest.CreateUsdcAssetPosition( + big.NewInt(100_000_000_000 - 50_000_000_000), + ), + }, + expectedOffsettingSubaccount: satypes.Subaccount{ + Id: &constants.Dave_Num0, + AssetPositions: keepertest.CreateUsdcAssetPosition( + big.NewInt(50_000_000_000 + 50_000_000_000), + ), + }, + }, + "Liquidated: well-collateralized, offsetting: under-collateralized, TNC > 0": { + liquidatedSubaccount: constants.Dave_Num0_1BTC_Long_50000USD, + offsettingSubaccount: constants.Carl_Num0_1BTC_Short_54999USD, + deltaQuantums: big.NewInt(-100_000_000), // 1 BTC + + expectedLiquidatedSubaccount: satypes.Subaccount{ + Id: &constants.Dave_Num0, + AssetPositions: keepertest.CreateUsdcAssetPosition( + big.NewInt(50_000_000_000 + 50_000_000_000), + ), + }, + expectedOffsettingSubaccount: satypes.Subaccount{ + Id: &constants.Carl_Num0, + AssetPositions: keepertest.CreateUsdcAssetPosition( + big.NewInt(54_999_000_000 - 50_000_000_000), + ), + }, + }, + "Liquidated: well-collateralized, offsetting: under-collateralized, TNC == 0": { + liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_100000USD, + offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50000USD_Short, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC + + expectedLiquidatedSubaccount: satypes.Subaccount{ + Id: &constants.Carl_Num0, + AssetPositions: keepertest.CreateUsdcAssetPosition( + big.NewInt(100_000_000_000 - 50_000_000_000), + ), + }, + expectedOffsettingSubaccount: satypes.Subaccount{ + Id: &constants.Dave_Num0, + }, + }, + "Liquidated: well-collateralized, offsetting: under-collateralized, TNC < 0": { + liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_100000USD, + offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50001USD_Short, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC + + // Negative TNC account closing at oracle price is an invalid state transition. + expectedErr: satypes.ErrFailedToUpdateSubaccounts, + }, + "Liquidated: under-collateralized, TNC > 0, offsetting: well-collateralized": { + liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_54999USD, + offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50000USD, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC + + expectedLiquidatedSubaccount: satypes.Subaccount{ + Id: &constants.Carl_Num0, + AssetPositions: keepertest.CreateUsdcAssetPosition( + big.NewInt(54_999_000_000 - 50_000_000_000), + ), + }, + expectedOffsettingSubaccount: satypes.Subaccount{ + Id: &constants.Dave_Num0, + AssetPositions: keepertest.CreateUsdcAssetPosition( + big.NewInt(50_000_000_000 + 50_000_000_000), + ), + }, + }, + "Liquidated: under-collateralized, TNC == 0, offsetting: under-collateralized, TNC < 0": { + liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_50000USD, + offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50001USD_Short, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC + + // Negative TNC account closing at oracle price is an invalid state transition. + expectedErr: satypes.ErrFailedToUpdateSubaccounts, + }, + "Liquidated: under-collateralized, TNC < 0, offsetting: under-collateralized, TNC > 0": { + liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_49999USD, + offsettingSubaccount: constants.Dave_Num0_1BTC_Long_45001USD_Short, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC + + // Negative TNC account closing at oracle price is an invalid state transition. + expectedErr: satypes.ErrFailedToUpdateSubaccounts, + }, + "Liquidated: under-collateralized, TNC < 0, offsetting: well-collateralized": { + liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_49999USD, + offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50000USD, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC + + // Negative TNC account closing at oracle price is an invalid state transition. + expectedErr: satypes.ErrFailedToUpdateSubaccounts, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + memClob := memclob.NewMemClobPriceTimePriority(false) + mockIndexerEventManager := &mocks.IndexerEventManager{} + ks := keepertest.NewClobKeepersTestContext(t, memClob, &mocks.BankKeeper{}, mockIndexerEventManager) + + // 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) + + for _, p := range []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + constants.EthUsd_20PercentInitial_10PercentMaintenance, + } { + _, err := ks.PerpetualsKeeper.CreatePerpetual( + ks.Ctx, + p.Params.Id, + p.Params.Ticker, + p.Params.MarketId, + p.Params.AtomicResolution, + p.Params.DefaultFundingPpm, + p.Params.LiquidityTier, + ) + require.NoError(t, err) + } + + ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.liquidatedSubaccount) + ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.offsettingSubaccount) + + if tc.expectedErr == nil { + fillPriceQuoteQuantums, err := ks.PerpetualsKeeper.GetNetNotional( + ks.Ctx, + uint32(0), + tc.deltaQuantums, + ) + fillPriceQuoteQuantums.Neg(fillPriceQuoteQuantums) + require.NoError(t, err) + mockIndexerEventManager.On("AddTxnEvent", + ks.Ctx, + indexerevents.SubtypeDeleveraging, + indexerevents.DeleveragingEventVersion, + indexer_manager.GetBytes( + indexerevents.NewDeleveragingEvent( + *tc.liquidatedSubaccount.GetId(), + *tc.offsettingSubaccount.GetId(), + uint32(0), + satypes.BaseQuantums(new(big.Int).Abs(tc.deltaQuantums).Uint64()), + satypes.BaseQuantums(fillPriceQuoteQuantums.Uint64()), + tc.deltaQuantums.Sign() > 0, + ), + ), + ).Return() + } + err = ks.ClobKeeper.ProcessDeleveraging( + ks.Ctx, + *tc.liquidatedSubaccount.GetId(), + *tc.offsettingSubaccount.GetId(), + uint32(0), + tc.deltaQuantums, + true, ) if tc.expectedErr == nil { require.NoError(t, err) @@ -1252,6 +1458,7 @@ func TestProcessDeleveraging_Rounding(t *testing.T) { *tc.offsettingSubaccount.GetId(), uint32(0), tc.deltaQuantums, + false, ) if tc.expectedErr == nil { require.NoError(t, err) diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index ef464f4268..2e4af66313 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -665,6 +665,7 @@ func (k Keeper) PersistMatchDeleveragingToState( fill.OffsettingSubaccountId, perpetualId, deltaQuantums, + false, ); err != nil { return errorsmod.Wrapf( types.ErrInvalidDeleveragingFill, From b2e6b2e42a37b1e61199ff7ebd54eced01e2db26 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Tue, 5 Dec 2023 15:32:18 -0800 Subject: [PATCH 02/31] pull deltaQuoteQuantums out of ProcessDeleveraging --- protocol/x/clob/keeper/deleveraging.go | 107 ++++++++++++------- protocol/x/clob/keeper/deleveraging_test.go | 95 +++++++++++----- protocol/x/clob/keeper/process_operations.go | 27 +++-- 3 files changed, 156 insertions(+), 73 deletions(-) diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index ec03bcd429..e5f3b2450e 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -236,6 +236,29 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( deltaQuantums = 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, + deltaQuantums, + ) + if err != nil { + liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) + k.Logger(ctx).Debug( + "Encountered error when getting quote quantums for deleveraging", + "error", err, + "blockHeight", ctx.BlockHeight(), + "checkTx", ctx.IsCheckTx(), + "perpetualId", perpetualId, + "deltaQuantums", deltaQuantums, + "liquidatedSubaccount", liquidatedSubaccount, + "offsettingSubaccount", offsettingSubaccount, + ) + return false + } + // Try to process the deleveraging operation for both subaccounts. if err := k.ProcessDeleveraging( ctx, @@ -243,7 +266,7 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( *offsettingSubaccount.Id, perpetualId, deltaQuantums, - false, + deltaQuoteQuantums, ); err == nil { // Update the remaining liquidatable quantums. deltaQuantumsRemaining = new(big.Int).Sub( @@ -306,6 +329,33 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( return fills, deltaQuantumsRemaining } +// 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 +// clob pair status of the clob pair associated with the provided perpetual. +func (k Keeper) getDeleveragingQuoteQuantumsDelta( + ctx sdk.Context, + perpetualId uint32, + subaccountId satypes.SubaccountId, + deltaQuantums *big.Int, +) (deltaQuoteQuantums *big.Int, err error) { + clobPair := k.mustGetClobPairForPerpetualId(ctx, perpetualId) + isFinalSettlement := clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT + + if isFinalSettlement { + deltaQuoteQuantums, err = k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaQuantums) + } else { + deltaQuoteQuantums, err = k.GetBankruptcyPriceInQuoteQuantums( + ctx, + subaccountId, + perpetualId, + deltaQuantums, + ) + } + + return deltaQuoteQuantums, err +} + // ProcessDeleveraging processes a deleveraging operation by closing both the liquidated subaccount's // position and the offsetting subaccount's position at the bankruptcy price of the _liquidated_ position. // This function takes a `deltaQuantums` argument, which is the delta with respect to the liquidated subaccount's @@ -313,7 +363,7 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( // is successfully written to state. // // This function returns an error if: -// - `deltaQuantums` is not valid with respect to either of the subaccounts. +// - `deltaBaseQuantums` is not valid with respect to either of the subaccounts. // - `GetBankruptcyPriceInQuoteQuantums` returns an error. // - subaccount updates cannot be applied when the bankruptcy prices of both subaccounts don't overlap. func (k Keeper) ProcessDeleveraging( @@ -321,8 +371,8 @@ func (k Keeper) ProcessDeleveraging( liquidatedSubaccountId satypes.SubaccountId, offsettingSubaccountId satypes.SubaccountId, perpetualId uint32, - deltaQuantums *big.Int, - fillPriceIsOraclePrice bool, + deltaBaseQuantums *big.Int, + deltaQuoteQuantums *big.Int, ) ( err error, ) { @@ -340,45 +390,24 @@ func (k Keeper) ProcessDeleveraging( // by checking that `deltaQuantums` is on the opposite side of the liquidated position side, // the same side as the offsetting subaccount position side, and the magnitude of `deltaQuantums` // is not larger than both positions. - if liquidatedPositionQuantums.Sign()*deltaQuantums.Sign() != -1 || - liquidatedPositionQuantums.CmpAbs(deltaQuantums) == -1 || - offsettingPositionQuantums.Sign()*deltaQuantums.Sign() != 1 || - offsettingPositionQuantums.CmpAbs(deltaQuantums) == -1 { + if liquidatedPositionQuantums.Sign()*deltaBaseQuantums.Sign() != -1 || + liquidatedPositionQuantums.CmpAbs(deltaBaseQuantums) == -1 || + offsettingPositionQuantums.Sign()*deltaBaseQuantums.Sign() != 1 || + offsettingPositionQuantums.CmpAbs(deltaBaseQuantums) == -1 { return errorsmod.Wrapf( types.ErrInvalidPerpetualPositionSizeDelta, "ProcessDeleveraging: liquidated = (%s), offsetting = (%s), perpetual id = (%d), deltaQuantums = (%+v)", lib.MaybeGetJsonString(liquidatedSubaccount), lib.MaybeGetJsonString(offsettingSubaccount), perpetualId, - deltaQuantums, + deltaBaseQuantums, ) } - fillPriceDeltaQuoteQuantums := new(big.Int) - if fillPriceIsOraclePrice { // Flow used for final settlement deleveraging events - fillPriceDeltaQuoteQuantums, err = k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaQuantums) - fillPriceDeltaQuoteQuantums.Neg(fillPriceDeltaQuoteQuantums) - if err != nil { - return err - } - } else { // Regular deleveraging flow - // Calculate the bankruptcy price of the liquidated position. This is the price at which both positions - // are closed. - fillPriceDeltaQuoteQuantums, err = k.GetBankruptcyPriceInQuoteQuantums( - ctx, - liquidatedSubaccountId, - perpetualId, - deltaQuantums, - ) - if err != nil { - return err - } - } - - deleveragedSubaccountQuoteBalanceDelta := fillPriceDeltaQuoteQuantums - offsettingSubaccountQuoteBalanceDelta := new(big.Int).Neg(fillPriceDeltaQuoteQuantums) - deleveragedSubaccountPerpetualQuantumsDelta := deltaQuantums - offsettingSubaccountPerpetualQuantumsDelta := new(big.Int).Neg(deltaQuantums) + deleveragedSubaccountQuoteBalanceDelta := deltaQuoteQuantums + offsettingSubaccountQuoteBalanceDelta := new(big.Int).Neg(deltaQuoteQuantums) + deleveragedSubaccountPerpetualQuantumsDelta := deltaBaseQuantums + offsettingSubaccountPerpetualQuantumsDelta := new(big.Int).Neg(deltaBaseQuantums) updates := []satypes.Update{ // Liquidated subaccount update. @@ -430,12 +459,12 @@ func (k Keeper) ProcessDeleveraging( if deleveragedQuoteQuantums, err := k.perpetualsKeeper.GetNetCollateral( ctx, perpetualId, - new(big.Int).Abs(deltaQuantums), + new(big.Int).Abs(deltaBaseQuantums), ); err == nil { labels := []metrics.Label{ metrics.GetLabelForIntValue(metrics.PerpetualId, int(perpetualId)), metrics.GetLabelForBoolValue(metrics.CheckTx, ctx.IsCheckTx()), - metrics.GetLabelForBoolValue(metrics.IsLong, deltaQuantums.Sign() == -1), + metrics.GetLabelForBoolValue(metrics.IsLong, deltaBaseQuantums.Sign() == -1), } metrics.AddSampleWithLabels( @@ -474,9 +503,9 @@ func (k Keeper) ProcessDeleveraging( liquidatedSubaccountId, offsettingSubaccountId, perpetualId, - satypes.BaseQuantums(new(big.Int).Abs(deltaQuantums).Uint64()), - satypes.BaseQuantums(fillPriceDeltaQuoteQuantums.Uint64()), - deltaQuantums.Sign() > 0, + satypes.BaseQuantums(new(big.Int).Abs(deltaBaseQuantums).Uint64()), + satypes.BaseQuantums(deltaQuoteQuantums.Uint64()), + deltaBaseQuantums.Sign() > 0, ), ), ) diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index 3d1906d5ca..564e45ecb2 100644 --- a/protocol/x/clob/keeper/deleveraging_test.go +++ b/protocol/x/clob/keeper/deleveraging_test.go @@ -625,10 +625,11 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { err := keepertest.CreateUsdcAsset(ks.Ctx, ks.AssetsKeeper) require.NoError(t, err) - for _, p := range []perptypes.Perpetual{ + perps := []perptypes.Perpetual{ constants.BtcUsd_100PercentMarginRequirement, constants.EthUsd_100PercentMarginRequirement, - } { + } + for _, p := range perps { _, err := ks.PerpetualsKeeper.CreatePerpetual( ks.Ctx, p.Params.Id, @@ -641,6 +642,42 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { require.NoError(t, err) } + clobPairs := []types.ClobPair{ + constants.ClobPair_Btc, + constants.ClobPair_Eth, + } + for i, clobPair := range clobPairs { + mockIndexerEventManager.On("AddTxnEvent", + ks.Ctx, + indexerevents.SubtypePerpetualMarket, + indexerevents.PerpetualMarketEventVersion, + indexer_manager.GetBytes( + indexerevents.NewPerpetualMarketCreateEvent( + clobPair.MustGetPerpetualId(), + clobPair.Id, + perps[i].Params.Ticker, + perps[i].Params.MarketId, + clobPair.Status, + clobPair.QuantumConversionExponent, + perps[i].Params.AtomicResolution, + clobPair.SubticksPerTick, + clobPair.StepBaseQuantums, + perps[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, + ) + } + for _, subaccount := range tc.subaccounts { ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, subaccount) } @@ -1066,14 +1103,15 @@ func TestProcessDeleveraging(t *testing.T) { ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.liquidatedSubaccount) ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.offsettingSubaccount) + bankruptcyPriceQuoteQuantums, err := ks.ClobKeeper.GetBankruptcyPriceInQuoteQuantums( + ks.Ctx, + *tc.liquidatedSubaccount.GetId(), + uint32(0), + tc.deltaQuantums, + ) + require.NoError(t, err) + if tc.expectedErr == nil { - bankruptcyPriceQuoteQuantums, err := ks.ClobKeeper.GetBankruptcyPriceInQuoteQuantums( - ks.Ctx, - *tc.liquidatedSubaccount.GetId(), - uint32(0), - tc.deltaQuantums, - ) - require.NoError(t, err) mockIndexerEventManager.On("AddTxnEvent", ks.Ctx, indexerevents.SubtypeDeleveraging, @@ -1096,7 +1134,7 @@ func TestProcessDeleveraging(t *testing.T) { *tc.offsettingSubaccount.GetId(), uint32(0), tc.deltaQuantums, - false, + bankruptcyPriceQuoteQuantums, ) if tc.expectedErr == nil { require.NoError(t, err) @@ -1121,6 +1159,9 @@ func TestProcessDeleveraging(t *testing.T) { } } +// Note that final settlement matches piggyback off of the deleveraging operation. Because of this +// the pair of subaccounts offsetting each other are still referred to as "liquidated subaccount" and +// "offsetting subaccount" in the test cases below. func TestProcessDeleveragingAtOraclePrice(t *testing.T) { tests := map[string]struct { // Setup. @@ -1270,14 +1311,15 @@ func TestProcessDeleveragingAtOraclePrice(t *testing.T) { ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.liquidatedSubaccount) ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.offsettingSubaccount) + fillPriceQuoteQuantums, err := ks.PerpetualsKeeper.GetNetNotional( + ks.Ctx, + uint32(0), + tc.deltaQuantums, + ) + fillPriceQuoteQuantums.Neg(fillPriceQuoteQuantums) + require.NoError(t, err) + if tc.expectedErr == nil { - fillPriceQuoteQuantums, err := ks.PerpetualsKeeper.GetNetNotional( - ks.Ctx, - uint32(0), - tc.deltaQuantums, - ) - fillPriceQuoteQuantums.Neg(fillPriceQuoteQuantums) - require.NoError(t, err) mockIndexerEventManager.On("AddTxnEvent", ks.Ctx, indexerevents.SubtypeDeleveraging, @@ -1300,7 +1342,7 @@ func TestProcessDeleveragingAtOraclePrice(t *testing.T) { *tc.offsettingSubaccount.GetId(), uint32(0), tc.deltaQuantums, - true, + fillPriceQuoteQuantums, ) if tc.expectedErr == nil { require.NoError(t, err) @@ -1428,14 +1470,15 @@ func TestProcessDeleveraging_Rounding(t *testing.T) { ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.liquidatedSubaccount) ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.offsettingSubaccount) + bankruptcyPriceQuoteQuantums, err := ks.ClobKeeper.GetBankruptcyPriceInQuoteQuantums( + ks.Ctx, + *tc.liquidatedSubaccount.GetId(), + uint32(0), + tc.deltaQuantums, + ) + require.NoError(t, err) + if tc.expectedErr == nil { - bankruptcyPriceQuoteQuantums, err := ks.ClobKeeper.GetBankruptcyPriceInQuoteQuantums( - ks.Ctx, - *tc.liquidatedSubaccount.GetId(), - uint32(0), - tc.deltaQuantums, - ) - require.NoError(t, err) mockIndexerEventManager.On("AddTxnEvent", ks.Ctx, indexerevents.SubtypeDeleveraging, @@ -1458,7 +1501,7 @@ func TestProcessDeleveraging_Rounding(t *testing.T) { *tc.offsettingSubaccount.GetId(), uint32(0), tc.deltaQuantums, - false, + bankruptcyPriceQuoteQuantums, ) if tc.expectedErr == nil { require.NoError(t, err) diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index 2e4af66313..56f2d16649 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -651,12 +651,22 @@ func (k Keeper) PersistMatchDeleveragingToState( perpetualId, ) } - deltaQuantumsIsNegative := position.GetIsLong() + deltaBaseQuantumsIsNegative := position.GetIsLong() for _, fill := range matchDeleveraging.GetFills() { - deltaQuantums := new(big.Int).SetUint64(fill.FillAmount) - if deltaQuantumsIsNegative { - deltaQuantums.Neg(deltaQuantums) + deltaBaseQuantums := new(big.Int).SetUint64(fill.FillAmount) + if deltaBaseQuantumsIsNegative { + deltaBaseQuantums.Neg(deltaBaseQuantums) + } + + deltaQuoteQuantums, err := k.getDeleveragingQuoteQuantumsDelta( + ctx, + perpetualId, + liquidatedSubaccountId, + deltaBaseQuantums, + ) + if err != nil { + return err } if err := k.ProcessDeleveraging( @@ -664,17 +674,18 @@ func (k Keeper) PersistMatchDeleveragingToState( liquidatedSubaccountId, fill.OffsettingSubaccountId, perpetualId, - deltaQuantums, - false, + deltaBaseQuantums, + deltaQuoteQuantums, ); err != nil { return errorsmod.Wrapf( types.ErrInvalidDeleveragingFill, "Failed to process deleveraging fill: %+v. liquidatedSubaccountId: %+v, "+ - "perpetualId: %v, deltaQuantums: %v, error: %v", + "perpetualId: %v, deltaBaseQuantums: %v, deltaQuoteQuantums: %v, error: %v", fill, liquidatedSubaccountId, perpetualId, - deltaQuantums, + deltaBaseQuantums, + deltaQuoteQuantums, err, ) } From f81ad942642ab83cec77308ac6b4829e3b320d69 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Tue, 5 Dec 2023 15:42:35 -0800 Subject: [PATCH 03/31] fix lint --- protocol/x/clob/keeper/deleveraging_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index 564e45ecb2..a6f0166301 100644 --- a/protocol/x/clob/keeper/deleveraging_test.go +++ b/protocol/x/clob/keeper/deleveraging_test.go @@ -676,6 +676,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { clobPair.SubticksPerTick, clobPair.Status, ) + require.NoError(t, err) } for _, subaccount := range tc.subaccounts { From 25bcb315b84cdefe29294d5f4edbec1debc834b1 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Tue, 5 Dec 2023 16:32:49 -0800 Subject: [PATCH 04/31] fix issue with test --- protocol/x/clob/keeper/deleveraging_test.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index a6f0166301..788b352d9f 100644 --- a/protocol/x/clob/keeper/deleveraging_test.go +++ b/protocol/x/clob/keeper/deleveraging_test.go @@ -1104,15 +1104,16 @@ func TestProcessDeleveraging(t *testing.T) { ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.liquidatedSubaccount) ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.offsettingSubaccount) - bankruptcyPriceQuoteQuantums, err := ks.ClobKeeper.GetBankruptcyPriceInQuoteQuantums( - ks.Ctx, - *tc.liquidatedSubaccount.GetId(), - uint32(0), - tc.deltaQuantums, - ) - require.NoError(t, err) - + bankruptcyPriceQuoteQuantums := new(big.Int) if tc.expectedErr == nil { + bankruptcyPriceQuoteQuantums, err = ks.ClobKeeper.GetBankruptcyPriceInQuoteQuantums( + ks.Ctx, + *tc.liquidatedSubaccount.GetId(), + uint32(0), + tc.deltaQuantums, + ) + require.NoError(t, err) + mockIndexerEventManager.On("AddTxnEvent", ks.Ctx, indexerevents.SubtypeDeleveraging, From 64503ff6e3745be836c4a24119a2a1fafc0bcdd9 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Wed, 6 Dec 2023 10:55:08 -0800 Subject: [PATCH 05/31] pr nits --- protocol/x/clob/keeper/deleveraging.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index e5f3b2450e..ea406957af 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -246,11 +246,10 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( ) if err != nil { liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) - k.Logger(ctx).Debug( + k.Logger(ctx).Error( "Encountered error when getting quote quantums for deleveraging", "error", err, "blockHeight", ctx.BlockHeight(), - "checkTx", ctx.IsCheckTx(), "perpetualId", perpetualId, "deltaQuantums", deltaQuantums, "liquidatedSubaccount", liquidatedSubaccount, @@ -338,22 +337,20 @@ func (k Keeper) getDeleveragingQuoteQuantumsDelta( perpetualId uint32, subaccountId satypes.SubaccountId, deltaQuantums *big.Int, -) (deltaQuoteQuantums *big.Int, err error) { +) (*big.Int, error) { clobPair := k.mustGetClobPairForPerpetualId(ctx, perpetualId) isFinalSettlement := clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT if isFinalSettlement { - deltaQuoteQuantums, err = k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaQuantums) - } else { - deltaQuoteQuantums, err = k.GetBankruptcyPriceInQuoteQuantums( - ctx, - subaccountId, - perpetualId, - deltaQuantums, - ) + return k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaQuantums) } - return deltaQuoteQuantums, err + return k.GetBankruptcyPriceInQuoteQuantums( + ctx, + subaccountId, + perpetualId, + deltaQuantums, + ) } // ProcessDeleveraging processes a deleveraging operation by closing both the liquidated subaccount's From b469e2eff28e2ddb2052bb81ed3cf959c4ce91e5 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Wed, 6 Dec 2023 12:07:04 -0800 Subject: [PATCH 06/31] update getDeleveragingQuoteQuantumsDelta helper --- protocol/x/clob/keeper/deleveraging.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index ea406957af..928e79cb49 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -331,7 +331,8 @@ 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 -// clob pair status of the clob pair associated with the provided perpetual. +// collaterlization status of the subaccount (negative/non-negative TNC) as well as the clob pair +// status for the specified perpetual. func (k Keeper) getDeleveragingQuoteQuantumsDelta( ctx sdk.Context, perpetualId uint32, @@ -341,10 +342,19 @@ func (k Keeper) getDeleveragingQuoteQuantumsDelta( clobPair := k.mustGetClobPairForPerpetualId(ctx, perpetualId) isFinalSettlement := clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT + // If market is in final settlement and the subaccount has non-negative TNC, use the oracle price. if isFinalSettlement { - return k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaQuantums) + hasNegativeTnc, err := k.CanDeleverageSubaccount(ctx, subaccountId) + if err != nil { + return new(big.Int), err + } + + if !hasNegativeTnc { + return k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaQuantums) + } } + // For standard deleveraging, use the bankruptcy price. return k.GetBankruptcyPriceInQuoteQuantums( ctx, subaccountId, From 01e35c20ed96997c934ef2ce05d42faac1f097ce Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Wed, 6 Dec 2023 13:45:28 -0800 Subject: [PATCH 07/31] initial implementation of final settlement deleveraging --- protocol/x/clob/abci.go | 10 ++++- protocol/x/clob/keeper/deleveraging.go | 57 +++++++++++++++++++++++++- protocol/x/clob/keeper/liquidations.go | 29 +++++++++---- 3 files changed, 85 insertions(+), 11 deletions(-) diff --git a/protocol/x/clob/abci.go b/protocol/x/clob/abci.go index b22303d983..7e5e81e375 100644 --- a/protocol/x/clob/abci.go +++ b/protocol/x/clob/abci.go @@ -14,6 +14,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" "github.com/dydxprotocol/v4-chain/protocol/x/clob/keeper" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + subaccounttypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) // BeginBlocker executes all ABCI BeginBlock logic respective to the clob module. @@ -198,7 +199,14 @@ func PrepareCheckState( // 6. Get all potentially liquidatable subaccount IDs and attempt to liquidate them. subaccountIds := liquidatableSubaccountIds.GetSubaccountIds() - if err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, subaccountIds); err != nil { + numDeleveragingAttempts, err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, subaccountIds) + if err != nil { + panic(err) + } + + // 7. Deleverage subaccounts with open positions in final settlement markets. + subaccountOpenPositionInfo := make(map[uint32]map[bool]map[subaccounttypes.SubaccountId]struct{}) + if err := keeper.DeleverageSubaccountsInFinalSettlementMarkets(ctx, subaccountOpenPositionInfo, numDeleveragingAttempts); err != nil { panic(err) } diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index 928e79cb49..3f021361fe 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -33,13 +33,21 @@ func (k Keeper) MaybeDeleverageSubaccount( ) { lib.AssertCheckTxMode(ctx) + clobPairId, err := k.GetClobPairIdForPerpetual(ctx, perpetualId) + if err != nil { + return new(big.Int), err + } + clobPair := k.mustGetClobPair(ctx, clobPairId) + canPerformDeleveraging, err := k.CanDeleverageSubaccount(ctx, subaccountId) if err != nil { return new(big.Int), err } - // Early return to skip deleveraging if the subaccount can't be deleveraged. - if !canPerformDeleveraging { + // Early return to skip deleveraging if the subaccount can't be deleveraged. Make an exception + // for markets in final settlement, since final settlement deleveraging is allowed even for + // subaccounts with non-negative TNC. + if clobPair.Status != types.ClobPair_STATUS_FINAL_SETTLEMENT && !canPerformDeleveraging { metrics.IncrCounter( metrics.ClobPrepareCheckStateCannotDeleverageSubaccount, 1, @@ -519,3 +527,48 @@ func (k Keeper) ProcessDeleveraging( return nil } + +// DeleverageSubaccountsInFinalSettlementMarkets uses the subaccountOpenPositionInfo returned from the +// liquidations daemon to deleverage subaccounts with open positions in final settlement markets. Note +// this function will deleverage both negative TNC and non-negative TNC subaccounts. The deleveraging code +// uses the bankruptcy price for the former and the oracle price for the latter. +func (k Keeper) DeleverageSubaccountsInFinalSettlementMarkets( + ctx sdk.Context, + subaccountOpenPositionInfo map[uint32]map[bool]map[satypes.SubaccountId]struct{}, + numDeleveragingAttempts int, +) error { + // Gather perpetualIds for perpetuals whose clob pairs are in final settlement. + finalSettlementPerpetualIds := make(map[uint32]struct{}) + for _, clobPair := range k.GetAllClobPairs(ctx) { + if clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT { + perpetualId := clobPair.MustGetPerpetualId() + finalSettlementPerpetualIds[perpetualId] = struct{}{} + } + } + + // Deleverage subaccounts with open positions in final settlement markets. + for perpetualId := range finalSettlementPerpetualIds { + for isBuy := range subaccountOpenPositionInfo[perpetualId] { + for subaccountId := range subaccountOpenPositionInfo[perpetualId][isBuy] { + if numDeleveragingAttempts >= int(k.Flags.MaxDeleveragingAttemptsPerBlock) { + return nil + } + + if _, err := k.MaybeDeleverageSubaccount(ctx, subaccountId, perpetualId); err != nil { + if err != nil { + k.Logger(ctx).Error( + "DeleverageSubaccountsInFinalSettlementMarkets: Failed to deleverage subaccount.", + "subaccount", subaccountId, + "perpetualId", perpetualId, + "error", err, + ) + return err + } + } + numDeleveragingAttempts++ + } + } + } + + return nil +} diff --git a/protocol/x/clob/keeper/liquidations.go b/protocol/x/clob/keeper/liquidations.go index 8017370cfa..45d3a91dfe 100644 --- a/protocol/x/clob/keeper/liquidations.go +++ b/protocol/x/clob/keeper/liquidations.go @@ -24,7 +24,7 @@ import ( func (k Keeper) LiquidateSubaccountsAgainstOrderbook( ctx sdk.Context, subaccountIds []satypes.SubaccountId, -) error { +) (numDeleveragingAttempts int, err error) { lib.AssertCheckTxMode(ctx) metrics.AddSample( @@ -35,7 +35,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( // Early return if there are 0 subaccounts to liquidate. numSubaccounts := len(subaccountIds) if numSubaccounts == 0 { - return nil + return 0, nil } defer telemetry.MeasureSince( @@ -53,6 +53,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( numLiqOrders := lib.Min(numSubaccounts, int(k.Flags.MaxLiquidationAttemptsPerBlock)) indexOffset := pseudoRand.Intn(numSubaccounts) + clobPairStatuses := make(map[types.ClobPairId]types.ClobPair_Status) startGetLiquidationOrders := time.Now() for i := 0; i < numLiqOrders; i++ { index := (i + indexOffset) % numSubaccounts @@ -68,7 +69,17 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( } // Return unexpected errors. - return err + return 0, err + } + + // Skip liquidation if the ClobPair for the liquidation order is in final settlement. There is a separate flow + // for triggering deleveraging/final settlement for positions in closed markets. + if _, found := clobPairStatuses[liquidationOrder.GetClobPairId()]; !found { + clobPair := k.mustGetClobPair(ctx, liquidationOrder.GetClobPairId()) + clobPairStatuses[liquidationOrder.GetClobPairId()] = clobPair.Status + } + if clobPairStatuses[liquidationOrder.GetClobPairId()] == types.ClobPair_STATUS_FINAL_SETTLEMENT { + continue } liquidationOrders = append(liquidationOrders, *liquidationOrder) @@ -105,7 +116,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( } // Return unexpected errors. - return err + return 0, err } optimisticallyFilledQuantums, _, err := k.PlacePerpetualLiquidation(ctx, *liquidationOrder) @@ -115,7 +126,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( "liquidationOrder", *liquidationOrder, "error", err, ) - return err + return 0, err } if optimisticallyFilledQuantums == 0 { @@ -131,7 +142,8 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( // For each unfilled liquidation, attempt to deleverage the subaccount. startDeleverageSubaccounts := time.Now() - for i := 0; i < int(k.Flags.MaxDeleveragingAttemptsPerBlock) && i < len(unfilledLiquidations); i++ { + var i int + for i = 0; i < int(k.Flags.MaxDeleveragingAttemptsPerBlock) && i < len(unfilledLiquidations); i++ { liquidationOrder := unfilledLiquidations[i] subaccountId := liquidationOrder.GetSubaccountId() @@ -145,9 +157,10 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( "perpetualId", perpetualId, "error", err, ) - return err + return i, err } } + telemetry.MeasureSince( startDeleverageSubaccounts, types.ModuleName, @@ -155,7 +168,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( metrics.Latency, ) - return nil + return i, nil } // MaybeGetLiquidationOrder takes a subaccount ID and returns a liquidation order that can be used to From 073f8f61c2789287521f0a613d0dd3f33d9f1e89 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Wed, 6 Dec 2023 15:04:28 -0800 Subject: [PATCH 08/31] add delivertx logic for deleveraging match --- protocol/x/clob/keeper/process_operations.go | 17 ++++++++++++++--- protocol/x/clob/types/errors.go | 5 +++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index 56f2d16649..2f833b308d 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -619,6 +619,18 @@ func (k Keeper) PersistMatchDeleveragingToState( matchDeleveraging *types.MatchPerpetualDeleveraging, ) error { liquidatedSubaccountId := matchDeleveraging.GetLiquidated() + perpetualId := matchDeleveraging.GetPerpetualId() + clobPair := k.mustGetClobPairForPerpetualId(ctx, perpetualId) + + // If `IsFinalSettlement` flag on deleveraging match is set to true, verify that the market is in final settlement. + if matchDeleveraging.GetIsFinalSettlement() && clobPair.Status != types.ClobPair_STATUS_FINAL_SETTLEMENT { + return errorsmod.Wrapf( + types.ErrFinalSettlementDeleveragingMatchForActiveMarket, + "MatchPerpetualDeleveraging %+v is a final settlement match, but the clob pair %+v is not in final settlement", + matchDeleveraging, + clobPair, + ) + } // Validate that the provided subaccount can be deleveraged. if canDeleverageSubaccount, err := k.CanDeleverageSubaccount(ctx, liquidatedSubaccountId); err != nil { @@ -630,7 +642,8 @@ func (k Keeper) PersistMatchDeleveragingToState( err, ), ) - } else if !canDeleverageSubaccount { + } else if clobPair.Status != types.ClobPair_STATUS_FINAL_SETTLEMENT && !canDeleverageSubaccount { + // If the clob pair is in final settlement, we automatically consider the subaccount deleveragable. // TODO(CLOB-853): Add more verbose error logging about why deleveraging failed validation. return errorsmod.Wrapf( types.ErrInvalidDeleveragedSubaccount, @@ -639,8 +652,6 @@ func (k Keeper) PersistMatchDeleveragingToState( ) } - perpetualId := matchDeleveraging.GetPerpetualId() - liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) position, exists := liquidatedSubaccount.GetPerpetualPositionForId(perpetualId) if !exists { diff --git a/protocol/x/clob/types/errors.go b/protocol/x/clob/types/errors.go index abb80ab27b..8b4b575cbb 100644 --- a/protocol/x/clob/types/errors.go +++ b/protocol/x/clob/types/errors.go @@ -308,6 +308,11 @@ var ( 1020, "Position cannot be fully offset", ) + ErrFinalSettlementDeleveragingMatchForActiveMarket = errorsmod.Register( + ModuleName, + 1021, + "Deleveraging match has final settlement flag, but market is active", + ) // Advanced order type errors. ErrFokOrderCouldNotBeFullyFilled = errorsmod.Register( From e37c37f8a16b99fe4bbf0bef1e316cde1bb1368d Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Wed, 6 Dec 2023 16:00:29 -0800 Subject: [PATCH 09/31] 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 --- protocol/indexer/events/deleveraging.go | 14 ++-- protocol/indexer/events/deleveraging_test.go | 4 +- protocol/x/clob/keeper/deleveraging.go | 74 +++++++++++--------- protocol/x/clob/keeper/deleveraging_test.go | 4 ++ protocol/x/clob/keeper/process_operations.go | 37 ++++++++-- 5 files changed, 85 insertions(+), 48 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/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index 3f021361fe..d2c6dd3dd5 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -6,12 +6,11 @@ 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" 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" @@ -237,20 +236,20 @@ 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 // deleveraging and at oracle price for final settlement deleveraging. - deltaQuoteQuantums, err := k.getDeleveragingQuoteQuantumsDelta( + deltaQuoteQuantums, isFinalSettlement, err := k.getDeleveragingQuoteQuantumsDelta( ctx, perpetualId, liquidatedSubaccountId, - deltaQuantums, + deltaBaseQuantums, ) if err != nil { liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) @@ -259,9 +258,10 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( "error", err, "blockHeight", ctx.BlockHeight(), "perpetualId", perpetualId, - "deltaQuantums", deltaQuantums, + "deltaBaseQuantums", deltaBaseQuantums, "liquidatedSubaccount", liquidatedSubaccount, "offsettingSubaccount", offsettingSubaccount, + "isFinalSettlement", isFinalSettlement, ) return false } @@ -272,18 +272,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( @@ -302,12 +321,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), @@ -346,7 +366,7 @@ func (k Keeper) getDeleveragingQuoteQuantumsDelta( perpetualId uint32, subaccountId satypes.SubaccountId, deltaQuantums *big.Int, -) (*big.Int, error) { +) (deltaQuoteQuantums *big.Int, isFinalSettlementDeleverage bool, err error) { clobPair := k.mustGetClobPairForPerpetualId(ctx, perpetualId) isFinalSettlement := clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT @@ -354,21 +374,23 @@ func (k Keeper) getDeleveragingQuoteQuantumsDelta( if isFinalSettlement { hasNegativeTnc, err := k.CanDeleverageSubaccount(ctx, subaccountId) if err != nil { - return new(big.Int), err + return new(big.Int), false, err } if !hasNegativeTnc { - return k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaQuantums) + quantums, err := k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaQuantums) + return quantums, true, err } } // For standard deleveraging, use the bankruptcy price. - return k.GetBankruptcyPriceInQuoteQuantums( + quantums, err := k.GetBankruptcyPriceInQuoteQuantums( ctx, subaccountId, perpetualId, deltaQuantums, ) + return quantums, false, err } // ProcessDeleveraging processes a deleveraging operation by closing both the liquidated subaccount's @@ -507,24 +529,6 @@ 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 } diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index 788b352d9f..3965e0625e 100644 --- a/protocol/x/clob/keeper/deleveraging_test.go +++ b/protocol/x/clob/keeper/deleveraging_test.go @@ -711,6 +711,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { satypes.BaseQuantums(fill.FillAmount), satypes.BaseQuantums(bankruptcyPriceQuoteQuantums.Uint64()), tc.deltaQuantums.Sign() > 0, + false, ), ), ).Return() @@ -1126,6 +1127,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 +1336,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 +1496,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/process_operations.go b/protocol/x/clob/keeper/process_operations.go index 2f833b308d..ad8558e397 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -670,12 +670,18 @@ func (k Keeper) PersistMatchDeleveragingToState( deltaBaseQuantums.Neg(deltaBaseQuantums) } - deltaQuoteQuantums, err := k.getDeleveragingQuoteQuantumsDelta( - ctx, - perpetualId, - liquidatedSubaccountId, - deltaBaseQuantums, - ) + deltaQuoteQuantums := new(big.Int) + var err error + if matchDeleveraging.IsFinalSettlement { + deltaQuoteQuantums, err = k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaBaseQuantums) + } else { + deltaQuoteQuantums, err = k.GetBankruptcyPriceInQuoteQuantums( + ctx, + liquidatedSubaccountId, + perpetualId, + deltaBaseQuantums, + ) + } if err != nil { return err } @@ -700,6 +706,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 From 1b9d06bfb0738d7c5baa8efbae2a78aab74245d5 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Wed, 6 Dec 2023 16:03:13 -0800 Subject: [PATCH 10/31] update delivertx validation --- protocol/x/clob/keeper/process_operations.go | 41 +++++++++++--------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index ad8558e397..3e3c7bbe07 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -623,7 +623,7 @@ func (k Keeper) PersistMatchDeleveragingToState( clobPair := k.mustGetClobPairForPerpetualId(ctx, perpetualId) // If `IsFinalSettlement` flag on deleveraging match is set to true, verify that the market is in final settlement. - if matchDeleveraging.GetIsFinalSettlement() && clobPair.Status != types.ClobPair_STATUS_FINAL_SETTLEMENT { + if matchDeleveraging.IsFinalSettlement && clobPair.Status != types.ClobPair_STATUS_FINAL_SETTLEMENT { return errorsmod.Wrapf( types.ErrFinalSettlementDeleveragingMatchForActiveMarket, "MatchPerpetualDeleveraging %+v is a final settlement match, but the clob pair %+v is not in final settlement", @@ -632,24 +632,29 @@ func (k Keeper) PersistMatchDeleveragingToState( ) } - // Validate that the provided subaccount can be deleveraged. - if canDeleverageSubaccount, err := k.CanDeleverageSubaccount(ctx, liquidatedSubaccountId); err != nil { - panic( - fmt.Sprintf( - "PersistMatchDeleveragingToState: Failed to determine if subaccount can be deleveraged. "+ - "SubaccountId %+v, error %+v", + // Skip the below validation for final settlement deleveraging matches. Final settlement matches do + // not require the subaccount to have negative TNC to be considered deleveragable. + if !matchDeleveraging.IsFinalSettlement { + // Validate that the provided subaccount can be deleveraged. + if canDeleverageSubaccount, err := k.CanDeleverageSubaccount(ctx, liquidatedSubaccountId); err != nil { + panic( + fmt.Sprintf( + "PersistMatchDeleveragingToState: Failed to determine if subaccount can be deleveraged. "+ + "SubaccountId %+v, error %+v", + liquidatedSubaccountId, + err, + ), + ) + } else if !canDeleverageSubaccount { + // If the deleverage is a final settlement match, skip this validation as the subaccount does + // not need to have negative TNC to be considered deleveragable. + // TODO(CLOB-853): Add more verbose error logging about why deleveraging failed validation. + return errorsmod.Wrapf( + types.ErrInvalidDeleveragedSubaccount, + "Subaccount %+v failed deleveraging validation", liquidatedSubaccountId, - err, - ), - ) - } else if clobPair.Status != types.ClobPair_STATUS_FINAL_SETTLEMENT && !canDeleverageSubaccount { - // If the clob pair is in final settlement, we automatically consider the subaccount deleveragable. - // TODO(CLOB-853): Add more verbose error logging about why deleveraging failed validation. - return errorsmod.Wrapf( - types.ErrInvalidDeleveragedSubaccount, - "Subaccount %+v failed deleveraging validation", - liquidatedSubaccountId, - ) + ) + } } liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) From 22328c7ce49ff87726ee1470b1cb82eba55b4979 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Thu, 7 Dec 2023 10:27:29 -0800 Subject: [PATCH 11/31] allow final settlement subaccounts to be deleveraged by regular deleveraging flow --- protocol/x/clob/keeper/liquidations.go | 23 ++++++++++---------- protocol/x/clob/keeper/process_operations.go | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/protocol/x/clob/keeper/liquidations.go b/protocol/x/clob/keeper/liquidations.go index 45d3a91dfe..48f1d7b2ee 100644 --- a/protocol/x/clob/keeper/liquidations.go +++ b/protocol/x/clob/keeper/liquidations.go @@ -53,7 +53,6 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( numLiqOrders := lib.Min(numSubaccounts, int(k.Flags.MaxLiquidationAttemptsPerBlock)) indexOffset := pseudoRand.Intn(numSubaccounts) - clobPairStatuses := make(map[types.ClobPairId]types.ClobPair_Status) startGetLiquidationOrders := time.Now() for i := 0; i < numLiqOrders; i++ { index := (i + indexOffset) % numSubaccounts @@ -72,16 +71,6 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( return 0, err } - // Skip liquidation if the ClobPair for the liquidation order is in final settlement. There is a separate flow - // for triggering deleveraging/final settlement for positions in closed markets. - if _, found := clobPairStatuses[liquidationOrder.GetClobPairId()]; !found { - clobPair := k.mustGetClobPair(ctx, liquidationOrder.GetClobPairId()) - clobPairStatuses[liquidationOrder.GetClobPairId()] = clobPair.Status - } - if clobPairStatuses[liquidationOrder.GetClobPairId()] == types.ClobPair_STATUS_FINAL_SETTLEMENT { - continue - } - liquidationOrders = append(liquidationOrders, *liquidationOrder) } telemetry.MeasureSince( @@ -105,6 +94,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( // Attempt to place each liquidation order and perform deleveraging if necessary. startPlaceLiquidationOrders := time.Now() unfilledLiquidations := make([]types.LiquidationOrder, 0) + clobPairStatuses := make(map[types.ClobPairId]types.ClobPair_Status) 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) @@ -119,6 +109,17 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( return 0, err } + // Skip liquidation if the ClobPair for the liquidation order is in final settlement. There is a separate flow + // for triggering deleveraging/final settlement for positions in closed markets. + if _, found := clobPairStatuses[liquidationOrder.GetClobPairId()]; !found { + clobPair := k.mustGetClobPair(ctx, liquidationOrder.GetClobPairId()) + clobPairStatuses[liquidationOrder.GetClobPairId()] = clobPair.Status + } + if clobPairStatuses[liquidationOrder.GetClobPairId()] == types.ClobPair_STATUS_FINAL_SETTLEMENT { + unfilledLiquidations = append(unfilledLiquidations, *liquidationOrder) + continue + } + optimisticallyFilledQuantums, _, err := k.PlacePerpetualLiquidation(ctx, *liquidationOrder) if err != nil { k.Logger(ctx).Error( diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index 3e3c7bbe07..ad0a675cca 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -646,7 +646,7 @@ func (k Keeper) PersistMatchDeleveragingToState( ), ) } else if !canDeleverageSubaccount { - // If the deleverage is a final settlement match, skip this validation as the subaccount does + // If the deleverage is a final settlement match, skip this validation as the subaccount does // not need to have negative TNC to be considered deleveragable. // TODO(CLOB-853): Add more verbose error logging about why deleveraging failed validation. return errorsmod.Wrapf( From 0418a255c0c6d42d52497f67c2c93234ca9f55c1 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Thu, 7 Dec 2023 10:30:32 -0800 Subject: [PATCH 12/31] remove superfluous comment --- protocol/x/clob/keeper/process_operations.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index ad0a675cca..e103264885 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -646,8 +646,6 @@ func (k Keeper) PersistMatchDeleveragingToState( ), ) } else if !canDeleverageSubaccount { - // If the deleverage is a final settlement match, skip this validation as the subaccount does - // not need to have negative TNC to be considered deleveragable. // TODO(CLOB-853): Add more verbose error logging about why deleveraging failed validation. return errorsmod.Wrapf( types.ErrInvalidDeleveragedSubaccount, From 7a52242e21e6da955441aac15239518a5a7a50ad Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Thu, 7 Dec 2023 10:47:10 -0800 Subject: [PATCH 13/31] update err name --- protocol/x/clob/keeper/process_operations.go | 2 +- protocol/x/clob/types/errors.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index e103264885..8bce30139c 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -625,7 +625,7 @@ func (k Keeper) PersistMatchDeleveragingToState( // If `IsFinalSettlement` flag on deleveraging match is set to true, verify that the market is in final settlement. if matchDeleveraging.IsFinalSettlement && clobPair.Status != types.ClobPair_STATUS_FINAL_SETTLEMENT { return errorsmod.Wrapf( - types.ErrFinalSettlementDeleveragingMatchForActiveMarket, + types.ErrInvalidFinalSettlementDeleveragingMatch, "MatchPerpetualDeleveraging %+v is a final settlement match, but the clob pair %+v is not in final settlement", matchDeleveraging, clobPair, diff --git a/protocol/x/clob/types/errors.go b/protocol/x/clob/types/errors.go index 8b4b575cbb..6f17d22044 100644 --- a/protocol/x/clob/types/errors.go +++ b/protocol/x/clob/types/errors.go @@ -308,10 +308,10 @@ var ( 1020, "Position cannot be fully offset", ) - ErrFinalSettlementDeleveragingMatchForActiveMarket = errorsmod.Register( + ErrInvalidFinalSettlementDeleveragingMatch = errorsmod.Register( ModuleName, 1021, - "Deleveraging match has final settlement flag, but market is active", + "Deleveraging match has final settlement flag, but market is not in final settlement", ) // Advanced order type errors. From 9bde41c5e95207b99fd8e63a8141557564326b8b Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Thu, 7 Dec 2023 10:55:09 -0800 Subject: [PATCH 14/31] fix import formatting --- protocol/x/clob/abci.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol/x/clob/abci.go b/protocol/x/clob/abci.go index 7e5e81e375..d9fcb87dc8 100644 --- a/protocol/x/clob/abci.go +++ b/protocol/x/clob/abci.go @@ -14,7 +14,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" "github.com/dydxprotocol/v4-chain/protocol/x/clob/keeper" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" - subaccounttypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" + satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) // BeginBlocker executes all ABCI BeginBlock logic respective to the clob module. @@ -205,7 +205,7 @@ func PrepareCheckState( } // 7. Deleverage subaccounts with open positions in final settlement markets. - subaccountOpenPositionInfo := make(map[uint32]map[bool]map[subaccounttypes.SubaccountId]struct{}) + subaccountOpenPositionInfo := make(map[uint32]map[bool]map[satypes.SubaccountId]struct{}) if err := keeper.DeleverageSubaccountsInFinalSettlementMarkets(ctx, subaccountOpenPositionInfo, numDeleveragingAttempts); err != nil { panic(err) } From d0b741c2e51952afbb3890129b41bad2d3c42e2b Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Thu, 7 Dec 2023 14:59:12 -0800 Subject: [PATCH 15/31] lint --- protocol/x/clob/abci.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/protocol/x/clob/abci.go b/protocol/x/clob/abci.go index d9fcb87dc8..08d1ccd5db 100644 --- a/protocol/x/clob/abci.go +++ b/protocol/x/clob/abci.go @@ -206,7 +206,11 @@ func PrepareCheckState( // 7. Deleverage subaccounts with open positions in final settlement markets. subaccountOpenPositionInfo := make(map[uint32]map[bool]map[satypes.SubaccountId]struct{}) - if err := keeper.DeleverageSubaccountsInFinalSettlementMarkets(ctx, subaccountOpenPositionInfo, numDeleveragingAttempts); err != nil { + if err := keeper.DeleverageSubaccountsInFinalSettlementMarkets( + ctx, + subaccountOpenPositionInfo, + numDeleveragingAttempts, + ); err != nil { panic(err) } From 81b1f4491bcaac75aed8404060f7b8080643791d Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Thu, 7 Dec 2023 15:28:32 -0800 Subject: [PATCH 16/31] begin adding tests --- protocol/testutil/constants/clob_pair.go | 12 +++ protocol/x/clob/abci.go | 14 ++- .../clob/e2e/liquidation_deleveraging_test.go | 90 +++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/protocol/testutil/constants/clob_pair.go b/protocol/testutil/constants/clob_pair.go index f1fc88945a..10b42e79bf 100644 --- a/protocol/testutil/constants/clob_pair.go +++ b/protocol/testutil/constants/clob_pair.go @@ -103,6 +103,18 @@ var ( QuantumConversionExponent: -8, Status: clobtypes.ClobPair_STATUS_INITIALIZING, } + ClobPair_Btc_Final_Settlement = clobtypes.ClobPair{ + Id: 0, + Metadata: &clobtypes.ClobPair_PerpetualClobMetadata{ + PerpetualClobMetadata: &clobtypes.PerpetualClobMetadata{ + PerpetualId: 0, + }, + }, + StepBaseQuantums: 5, + SubticksPerTick: 5, + QuantumConversionExponent: -8, + Status: clobtypes.ClobPair_STATUS_FINAL_SETTLEMENT, + } ClobPair_Btc_Paused = clobtypes.ClobPair{ Id: 0, Metadata: &clobtypes.ClobPair_PerpetualClobMetadata{ diff --git a/protocol/x/clob/abci.go b/protocol/x/clob/abci.go index 08d1ccd5db..519c3d1be3 100644 --- a/protocol/x/clob/abci.go +++ b/protocol/x/clob/abci.go @@ -12,6 +12,7 @@ import ( indexershared "github.com/dydxprotocol/v4-chain/protocol/indexer/shared" "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" + "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" "github.com/dydxprotocol/v4-chain/protocol/x/clob/keeper" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" @@ -205,10 +206,19 @@ func PrepareCheckState( } // 7. Deleverage subaccounts with open positions in final settlement markets. - subaccountOpenPositionInfo := make(map[uint32]map[bool]map[satypes.SubaccountId]struct{}) + subaccountPositionInfo := map[uint32]map[bool]map[satypes.SubaccountId]struct{}{ + 0: { + false: { + constants.Carl_Num0: {}, + }, + true: { + constants.Dave_Num0: {}, + }, + }, + } if err := keeper.DeleverageSubaccountsInFinalSettlementMarkets( ctx, - subaccountOpenPositionInfo, + subaccountPositionInfo, numDeleveragingAttempts, ); 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 6d67a3510f..e0ea941717 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 map[uint32]map[bool]map[satypes.SubaccountId]struct{} // Configuration. liquidationConfig clobtypes.LiquidationsConfig @@ -1168,6 +1169,95 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { }, }, }, + `Deleveraging occurs 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: map[uint32]map[bool]map[satypes.SubaccountId]struct{}{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance.GetId(): { + false: { + constants.Carl_Num0: {}, + }, + true: { + constants.Dave_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 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: map[uint32]map[bool]map[satypes.SubaccountId]struct{}{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance.GetId(): { + false: { + constants.Carl_Num0: {}, + }, + true: { + constants.Dave_Num0: {}, + }, + }, + }, + // Account should be deleveraged regardless of whether or not the liquidations engine returns this subaccount + 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 { From e0efd39e13234097256d63b8a1aee6321c23137c Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Thu, 7 Dec 2023 15:59:58 -0800 Subject: [PATCH 17/31] update logic so that isFinalSettlement flag on operation is used --- protocol/mocks/MemClob.go | 83 +++++++++++++------ protocol/mocks/MemClobKeeper.go | 77 +++++++++++------ protocol/testutil/memclob/keeper.go | 1 + protocol/x/clob/keeper/deleveraging.go | 40 ++++----- protocol/x/clob/keeper/deleveraging_test.go | 1 + protocol/x/clob/memclob/memclob.go | 3 + protocol/x/clob/types/internal_operation.go | 8 +- protocol/x/clob/types/mem_clob_keeper.go | 1 + protocol/x/clob/types/memclob.go | 1 + .../x/clob/types/operations_to_propose.go | 2 + .../clob/types/operations_to_propose_test.go | 4 + 11 files changed, 144 insertions(+), 77 deletions(-) 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..4343a370db 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 { @@ -89,13 +95,16 @@ func (_m *MemClobKeeper) CanDeleverageSubaccount(ctx types.Context, subaccountId 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 { @@ -140,13 +149,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 +173,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 +220,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 +260,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 +293,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 +326,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 +340,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 +366,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..73e85e3bb4 100644 --- a/protocol/testutil/memclob/keeper.go +++ b/protocol/testutil/memclob/keeper.go @@ -463,6 +463,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/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index d2c6dd3dd5..a8e9762f6f 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -43,10 +43,13 @@ func (k Keeper) MaybeDeleverageSubaccount( return new(big.Int), err } - // Early return to skip deleveraging if the subaccount can't be deleveraged. Make an exception - // for markets in final settlement, since final settlement deleveraging is allowed even for - // subaccounts with non-negative TNC. - if clobPair.Status != types.ClobPair_STATUS_FINAL_SETTLEMENT && !canPerformDeleveraging { + // Generate final settlement deleveraging matches (at oracle price) if subaccount has non-negative TNC and + // the market is in final settlement. This means we will regularly deleverage subaccounts with negative TNC + // in markets that are in final settlement. + isFinalSettlement := clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT && !canPerformDeleveraging + + // Early return to skip deleveraging if the subaccount can't be deleveraged. Final settlement deleveraging. + if !isFinalSettlement && !canPerformDeleveraging { metrics.IncrCounter( metrics.ClobPrepareCheckStateCannotDeleverageSubaccount, 1, @@ -69,7 +72,7 @@ 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, isFinalSettlement) labels := []metrics.Label{ metrics.GetLabelForIntValue(metrics.PerpetualId, int(perpetualId)), @@ -201,6 +204,7 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( liquidatedSubaccountId satypes.SubaccountId, perpetualId uint32, deltaQuantumsTotal *big.Int, + isFinalSettlement bool, ) ( fills []types.MatchPerpetualDeleveraging_Fill, deltaQuantumsRemaining *big.Int, @@ -245,11 +249,12 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( // Fetch delta quote quantums. Calculated at bankruptcy price for standard // deleveraging and at oracle price for final settlement deleveraging. - deltaQuoteQuantums, isFinalSettlement, err := k.getDeleveragingQuoteQuantumsDelta( + deltaQuoteQuantums, err := k.getDeleveragingQuoteQuantumsDelta( ctx, perpetualId, liquidatedSubaccountId, deltaBaseQuantums, + isFinalSettlement, ) if err != nil { liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) @@ -358,39 +363,26 @@ 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, -) (deltaQuoteQuantums *big.Int, isFinalSettlementDeleverage bool, err 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), false, err - } - - if !hasNegativeTnc { - quantums, err := k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaQuantums) - return quantums, true, err - } + return k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaQuantums) } // For standard deleveraging, use the bankruptcy price. - quantums, err := k.GetBankruptcyPriceInQuoteQuantums( + return k.GetBankruptcyPriceInQuoteQuantums( ctx, subaccountId, perpetualId, deltaQuantums, ) - return quantums, false, err } // ProcessDeleveraging processes a deleveraging operation by closing both the liquidated subaccount's diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index 3965e0625e..6b4e696b85 100644 --- a/protocol/x/clob/keeper/deleveraging_test.go +++ b/protocol/x/clob/keeper/deleveraging_test.go @@ -722,6 +722,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) 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/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..c1d5ebfad2 100644 --- a/protocol/x/clob/types/mem_clob_keeper.go +++ b/protocol/x/clob/types/mem_clob_keeper.go @@ -90,6 +90,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{ From 67b63bb52299dc6f3931ac972eaac230115d0058 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Thu, 7 Dec 2023 16:10:00 -0800 Subject: [PATCH 18/31] re-use helper function --- protocol/x/clob/keeper/deleveraging.go | 2 +- protocol/x/clob/keeper/process_operations.go | 19 +++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index a8e9762f6f..c2e04b9f6d 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -373,7 +373,7 @@ func (k Keeper) getDeleveragingQuoteQuantumsDelta( ) (deltaQuoteQuantums *big.Int, err error) { // If market is in final settlement and the subaccount has non-negative TNC, use the oracle price. if isFinalSettlement { - 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. diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index 8bce30139c..d897e789dd 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -673,18 +673,13 @@ func (k Keeper) PersistMatchDeleveragingToState( deltaBaseQuantums.Neg(deltaBaseQuantums) } - deltaQuoteQuantums := new(big.Int) - var err error - if matchDeleveraging.IsFinalSettlement { - deltaQuoteQuantums, err = k.perpetualsKeeper.GetNetNotional(ctx, perpetualId, deltaBaseQuantums) - } else { - deltaQuoteQuantums, err = k.GetBankruptcyPriceInQuoteQuantums( - ctx, - liquidatedSubaccountId, - perpetualId, - deltaBaseQuantums, - ) - } + deltaQuoteQuantums, err := k.getDeleveragingQuoteQuantumsDelta( + ctx, + perpetualId, + liquidatedSubaccountId, + deltaBaseQuantums, + matchDeleveraging.IsFinalSettlement, + ) if err != nil { return err } From 136248eb30bd436214f688c8f9eb608b24c85a40 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Mon, 11 Dec 2023 16:39:04 -0800 Subject: [PATCH 19/31] pr nits, redefine CanDeleverageSubaccount --- protocol/x/clob/keeper/deleveraging.go | 48 +++++++++-------- protocol/x/clob/keeper/liquidations.go | 3 +- protocol/x/clob/keeper/process_operations.go | 54 +++++++++----------- protocol/x/clob/types/errors.go | 4 +- 4 files changed, 51 insertions(+), 58 deletions(-) diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index c2e04b9f6d..4a15b9e2da 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -32,24 +32,13 @@ func (k Keeper) MaybeDeleverageSubaccount( ) { lib.AssertCheckTxMode(ctx) - clobPairId, err := k.GetClobPairIdForPerpetual(ctx, perpetualId) + canPerformDeleveraging, shouldFinalSettlePosition, err := k.CanDeleverageSubaccount(ctx, subaccountId, perpetualId) if err != nil { return new(big.Int), err } - clobPair := k.mustGetClobPair(ctx, clobPairId) - - canPerformDeleveraging, err := k.CanDeleverageSubaccount(ctx, subaccountId) - if err != nil { - return new(big.Int), err - } - - // Generate final settlement deleveraging matches (at oracle price) if subaccount has non-negative TNC and - // the market is in final settlement. This means we will regularly deleverage subaccounts with negative TNC - // in markets that are in final settlement. - isFinalSettlement := clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT && !canPerformDeleveraging - // Early return to skip deleveraging if the subaccount can't be deleveraged. Final settlement deleveraging. - if !isFinalSettlement && !canPerformDeleveraging { + // Early return to skip deleveraging if the subaccount can't be deleveraged. + if !canPerformDeleveraging { metrics.IncrCounter( metrics.ClobPrepareCheckStateCannotDeleverageSubaccount, 1, @@ -72,7 +61,7 @@ func (k Keeper) MaybeDeleverageSubaccount( } deltaQuantums := new(big.Int).Neg(position.GetBigQuantums()) - quantumsDeleveraged, err = k.MemClob.DeleverageSubaccount(ctx, subaccountId, perpetualId, deltaQuantums, isFinalSettlement) + quantumsDeleveraged, err = k.MemClob.DeleverageSubaccount(ctx, subaccountId, perpetualId, deltaQuantums, shouldFinalSettlePosition) labels := []metrics.Label{ metrics.GetLabelForIntValue(metrics.PerpetualId, int(perpetualId)), @@ -147,13 +136,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, canDeleverageSubaccount and shouldFinalSettlePosition. +// - canDeleverageSubaccount is true if the subaccount can be deleveraged. +// - shouldFinalSettlePosition is true if the position should be final settled (this occurs at oracle price) +// 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, +) (canDeleverageSubaccount bool, shouldFinalSettlePosition bool, err error) { bigNetCollateral, _, _, @@ -162,16 +154,22 @@ 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) + + // Can deleverage subaccount if net collateral is negative or if the market is in final settlement. + canDeleverageSubaccount = bigNetCollateral.Sign() == -1 || clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT + + // Should final settle position if net collateral is non-negative and the market is in final settlement. + shouldFinalSettlePosition = bigNetCollateral.Sign() >= 0 && clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT - // The subaccount's total net collateral is negative, so deleveraging can be performed. - return true, nil + return canDeleverageSubaccount, shouldFinalSettlePosition, nil } // IsValidInsuranceFundDelta returns true if the insurance fund has enough funds to cover the insurance diff --git a/protocol/x/clob/keeper/liquidations.go b/protocol/x/clob/keeper/liquidations.go index 48f1d7b2ee..50b4b2f941 100644 --- a/protocol/x/clob/keeper/liquidations.go +++ b/protocol/x/clob/keeper/liquidations.go @@ -109,8 +109,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( return 0, err } - // Skip liquidation if the ClobPair for the liquidation order is in final settlement. There is a separate flow - // for triggering deleveraging/final settlement for positions in closed markets. + // Skip liquidation if the ClobPair for the liquidation order is in final settlement. if _, found := clobPairStatuses[liquidationOrder.GetClobPairId()]; !found { clobPair := k.mustGetClobPair(ctx, liquidationOrder.GetClobPairId()) clobPairStatuses[liquidationOrder.GetClobPairId()] = clobPair.Status diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index d897e789dd..7a7ede357d 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -620,41 +620,37 @@ func (k Keeper) PersistMatchDeleveragingToState( ) error { liquidatedSubaccountId := matchDeleveraging.GetLiquidated() perpetualId := matchDeleveraging.GetPerpetualId() - clobPair := k.mustGetClobPairForPerpetualId(ctx, perpetualId) - // If `IsFinalSettlement` flag on deleveraging match is set to true, verify that the market is in final settlement. - if matchDeleveraging.IsFinalSettlement && clobPair.Status != types.ClobPair_STATUS_FINAL_SETTLEMENT { + // Validate that the provided subaccount can be deleveraged. + if canDeleverageSubaccount, shouldFinalSettlePosition, err := k.CanDeleverageSubaccount(ctx, liquidatedSubaccountId, perpetualId); err != nil { + panic( + fmt.Sprintf( + "PersistMatchDeleveragingToState: Failed to determine if subaccount can be deleveraged. "+ + "SubaccountId %+v, error %+v", + liquidatedSubaccountId, + err, + ), + ) + } else if !canDeleverageSubaccount { + // TODO(CLOB-853): Add more verbose error logging about why deleveraging failed validation. return errorsmod.Wrapf( - types.ErrInvalidFinalSettlementDeleveragingMatch, - "MatchPerpetualDeleveraging %+v is a final settlement match, but the clob pair %+v is not in final settlement", + types.ErrInvalidDeleveragedSubaccount, + "Subaccount %+v failed deleveraging validation", + liquidatedSubaccountId, + ) + } else if matchDeleveraging.IsFinalSettlement != shouldFinalSettlePosition { + // 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, - clobPair, + matchDeleveraging.IsFinalSettlement, + shouldFinalSettlePosition, ) } - // Skip the below validation for final settlement deleveraging matches. Final settlement matches do - // not require the subaccount to have negative TNC to be considered deleveragable. - if !matchDeleveraging.IsFinalSettlement { - // Validate that the provided subaccount can be deleveraged. - if canDeleverageSubaccount, err := k.CanDeleverageSubaccount(ctx, liquidatedSubaccountId); err != nil { - panic( - fmt.Sprintf( - "PersistMatchDeleveragingToState: Failed to determine if subaccount can be deleveraged. "+ - "SubaccountId %+v, error %+v", - liquidatedSubaccountId, - err, - ), - ) - } else if !canDeleverageSubaccount { - // TODO(CLOB-853): Add more verbose error logging about why deleveraging failed validation. - return errorsmod.Wrapf( - types.ErrInvalidDeleveragedSubaccount, - "Subaccount %+v failed deleveraging validation", - liquidatedSubaccountId, - ) - } - } - liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) position, exists := liquidatedSubaccount.GetPerpetualPositionForId(perpetualId) if !exists { diff --git a/protocol/x/clob/types/errors.go b/protocol/x/clob/types/errors.go index 6f17d22044..681c0ecd10 100644 --- a/protocol/x/clob/types/errors.go +++ b/protocol/x/clob/types/errors.go @@ -308,10 +308,10 @@ var ( 1020, "Position cannot be fully offset", ) - ErrInvalidFinalSettlementDeleveragingMatch = errorsmod.Register( + ErrDeleveragingIsFinalSettlementFlagMismatch = errorsmod.Register( ModuleName, 1021, - "Deleveraging match has final settlement flag, but market is not in final settlement", + "Deleveraging match has incorrect value for isFinalSettlement flag", ) // Advanced order type errors. From 795123bd9c1c06779a264d1ba387ea00c5c76dc2 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Mon, 11 Dec 2023 16:50:07 -0800 Subject: [PATCH 20/31] update tests for CanDeleverageSubaccount --- protocol/mocks/MemClobKeeper.go | 31 +++++--- protocol/testutil/memclob/keeper.go | 4 +- protocol/x/clob/keeper/deleveraging_test.go | 85 ++++++++++++++++++++- protocol/x/clob/types/mem_clob_keeper.go | 3 +- 4 files changed, 106 insertions(+), 17 deletions(-) diff --git a/protocol/mocks/MemClobKeeper.go b/protocol/mocks/MemClobKeeper.go index 4343a370db..5139e4b877 100644 --- a/protocol/mocks/MemClobKeeper.go +++ b/protocol/mocks/MemClobKeeper.go @@ -90,28 +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 - var r1 error - if rf, ok := ret.Get(0).(func(types.Context, subaccountstypes.SubaccountId) (bool, error)); ok { - return 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) bool); ok { - r0 = rf(ctx, subaccountId) + 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) } - 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 diff --git a/protocol/testutil/memclob/keeper.go b/protocol/testutil/memclob/keeper.go index 73e85e3bb4..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()`. diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index 6b4e696b85..c726788f1d 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 + expectedCanDeleverageSubaccount bool + expectedShouldFinalSettlePosition bool }{ `Cannot deleverage when subaccount has positive TNC`: { liquidationConfig: constants.LiquidationsConfig_No_Limit, @@ -229,6 +231,9 @@ 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, }, @@ -239,6 +244,9 @@ 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, }, @@ -249,9 +257,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, + }, expectedCanDeleverageSubaccount: true, }, + `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, + expectedShouldFinalSettlePosition: 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, + }, + + expectedCanDeleverageSubaccount: true, + expectedShouldFinalSettlePosition: true, + }, } for name, tc := range tests { @@ -259,7 +298,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,11 +353,45 @@ 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( + canDeleverageSubaccount, shouldFinalSettlePosition, err := ks.ClobKeeper.CanDeleverageSubaccount( ks.Ctx, *tc.subaccount.Id, + 0, ) require.NoError(t, err) require.Equal( @@ -325,6 +399,11 @@ func TestCanDeleverageSubaccount(t *testing.T) { tc.expectedCanDeleverageSubaccount, canDeleverageSubaccount, ) + require.Equal( + t, + tc.expectedShouldFinalSettlePosition, + shouldFinalSettlePosition, + ) }) } } diff --git a/protocol/x/clob/types/mem_clob_keeper.go b/protocol/x/clob/types/mem_clob_keeper.go index c1d5ebfad2..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, From 17c8e3ec15ba32f9af36eea5f3f17bb2a680d402 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Mon, 11 Dec 2023 17:37:26 -0800 Subject: [PATCH 21/31] split up PCS steps 6 and 7 to be for liquidations and then for deleveraging --- protocol/x/clob/abci.go | 21 +++++---- protocol/x/clob/keeper/deleveraging.go | 61 +++++++++++++++++--------- protocol/x/clob/keeper/liquidations.go | 59 ++++++++++--------------- 3 files changed, 76 insertions(+), 65 deletions(-) diff --git a/protocol/x/clob/abci.go b/protocol/x/clob/abci.go index 519c3d1be3..b053ea8d0a 100644 --- a/protocol/x/clob/abci.go +++ b/protocol/x/clob/abci.go @@ -200,12 +200,10 @@ func PrepareCheckState( // 6. Get all potentially liquidatable subaccount IDs and attempt to liquidate them. subaccountIds := liquidatableSubaccountIds.GetSubaccountIds() - numDeleveragingAttempts, err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, subaccountIds) + subaccountsToDeleverage, err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, subaccountIds) if err != nil { panic(err) } - - // 7. Deleverage subaccounts with open positions in final settlement markets. subaccountPositionInfo := map[uint32]map[bool]map[satypes.SubaccountId]struct{}{ 0: { false: { @@ -216,11 +214,18 @@ func PrepareCheckState( }, }, } - if err := keeper.DeleverageSubaccountsInFinalSettlementMarkets( - ctx, - subaccountPositionInfo, - numDeleveragingAttempts, - ); err != nil { + // Add subaccounts with open positions in final settlement markets to the slice of subaccounts/perps + // to be deleveraged. + subaccountsToDeleverage = append( + subaccountsToDeleverage, + keeper.GetSubaccountsWithOpenPositionsInFinalSettlementMarkets( + ctx, + subaccountPositionInfo, + )..., + ) + + // 7. Deleverage subaccounts. + if err := keeper.DeleverageSubaccounts(ctx, subaccountsToDeleverage); err != nil { panic(err) } diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index 4a15b9e2da..91d269ba6d 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -7,6 +7,7 @@ import ( "time" 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" @@ -526,11 +527,10 @@ func (k Keeper) ProcessDeleveraging( // liquidations daemon to deleverage subaccounts with open positions in final settlement markets. Note // this function will deleverage both negative TNC and non-negative TNC subaccounts. The deleveraging code // uses the bankruptcy price for the former and the oracle price for the latter. -func (k Keeper) DeleverageSubaccountsInFinalSettlementMarkets( +func (k Keeper) GetSubaccountsWithOpenPositionsInFinalSettlementMarkets( ctx sdk.Context, subaccountOpenPositionInfo map[uint32]map[bool]map[satypes.SubaccountId]struct{}, - numDeleveragingAttempts int, -) error { +) (subaccountsToDeleverage []subaccountToDeleverage) { // Gather perpetualIds for perpetuals whose clob pairs are in final settlement. finalSettlementPerpetualIds := make(map[uint32]struct{}) for _, clobPair := range k.GetAllClobPairs(ctx) { @@ -540,29 +540,50 @@ func (k Keeper) DeleverageSubaccountsInFinalSettlementMarkets( } } - // Deleverage subaccounts with open positions in final settlement markets. + // Fetch subaccounts with open positions in final settlement markets. for perpetualId := range finalSettlementPerpetualIds { for isBuy := range subaccountOpenPositionInfo[perpetualId] { for subaccountId := range subaccountOpenPositionInfo[perpetualId][isBuy] { - if numDeleveragingAttempts >= int(k.Flags.MaxDeleveragingAttemptsPerBlock) { - return nil - } - - if _, err := k.MaybeDeleverageSubaccount(ctx, subaccountId, perpetualId); err != nil { - if err != nil { - k.Logger(ctx).Error( - "DeleverageSubaccountsInFinalSettlementMarkets: Failed to deleverage subaccount.", - "subaccount", subaccountId, - "perpetualId", perpetualId, - "error", err, - ) - return err - } - } - numDeleveragingAttempts++ + subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ + SubaccountId: subaccountId, + PerpetualId: perpetualId, + }) } } } + return subaccountsToDeleverage +} + +// DeleverageSubaccounts deleverages a slice of subaccounts paired with a perpetual position to deleverage with. +func (k Keeper) DeleverageSubaccounts( + ctx sdk.Context, + subaccountsToDeleverage []subaccountToDeleverage, +) error { + // For each unfilled liquidation, attempt to deleverage the subaccount. + startDeleverageSubaccounts := time.Now() + var i int + 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 + } + } + + telemetry.MeasureSince( + startDeleverageSubaccounts, + types.ModuleName, + metrics.LiquidateSubaccounts_Deleverage, + metrics.Latency, + ) + return nil } diff --git a/protocol/x/clob/keeper/liquidations.go b/protocol/x/clob/keeper/liquidations.go index 50b4b2f941..9bfd64dd14 100644 --- a/protocol/x/clob/keeper/liquidations.go +++ b/protocol/x/clob/keeper/liquidations.go @@ -18,13 +18,21 @@ import ( satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) +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. func (k Keeper) LiquidateSubaccountsAgainstOrderbook( ctx sdk.Context, subaccountIds []satypes.SubaccountId, -) (numDeleveragingAttempts int, err error) { +) ( + subaccountsToDeleverage []subaccountToDeleverage, + err error, +) { lib.AssertCheckTxMode(ctx) metrics.AddSample( @@ -35,7 +43,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( // Early return if there are 0 subaccounts to liquidate. numSubaccounts := len(subaccountIds) if numSubaccounts == 0 { - return 0, nil + return nil, nil } defer telemetry.MeasureSince( @@ -68,7 +76,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( } // Return unexpected errors. - return 0, err + return nil, err } liquidationOrders = append(liquidationOrders, *liquidationOrder) @@ -93,7 +101,6 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( // Attempt to place each liquidation order and perform deleveraging if necessary. startPlaceLiquidationOrders := time.Now() - unfilledLiquidations := make([]types.LiquidationOrder, 0) clobPairStatuses := make(map[types.ClobPairId]types.ClobPair_Status) for _, subaccountId := range subaccountIdsToLiquidate { // Generate a new liquidation order with the appropriate order size from the sorted subaccount ids. @@ -106,7 +113,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( } // Return unexpected errors. - return 0, err + return nil, err } // Skip liquidation if the ClobPair for the liquidation order is in final settlement. @@ -115,7 +122,10 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( clobPairStatuses[liquidationOrder.GetClobPairId()] = clobPair.Status } if clobPairStatuses[liquidationOrder.GetClobPairId()] == types.ClobPair_STATUS_FINAL_SETTLEMENT { - unfilledLiquidations = append(unfilledLiquidations, *liquidationOrder) + subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ + SubaccountId: liquidationOrder.GetSubaccountId(), + PerpetualId: liquidationOrder.MustGetLiquidatedPerpetualId(), + }) continue } @@ -126,11 +136,14 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( "liquidationOrder", *liquidationOrder, "error", err, ) - return 0, err + return nil, err } if optimisticallyFilledQuantums == 0 { - unfilledLiquidations = append(unfilledLiquidations, *liquidationOrder) + subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ + SubaccountId: liquidationOrder.GetSubaccountId(), + PerpetualId: liquidationOrder.MustGetLiquidatedPerpetualId(), + }) } } telemetry.MeasureSince( @@ -140,35 +153,7 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( metrics.Latency, ) - // For each unfilled liquidation, attempt to deleverage the subaccount. - startDeleverageSubaccounts := time.Now() - var i int - 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 i, err - } - } - - telemetry.MeasureSince( - startDeleverageSubaccounts, - types.ModuleName, - metrics.LiquidateSubaccounts_Deleverage, - metrics.Latency, - ) - - return i, nil + return subaccountsToDeleverage, nil } // MaybeGetLiquidationOrder takes a subaccount ID and returns a liquidation order that can be used to From ed2192f7f95e4624fc00dc46dba0decf34bdb6cc Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Tue, 12 Dec 2023 10:29:05 -0800 Subject: [PATCH 22/31] update to use new subaccountOpenPositionInfo type --- protocol/x/clob/abci.go | 17 ++++++----- .../clob/e2e/liquidation_deleveraging_test.go | 28 ++++++++++--------- protocol/x/clob/keeper/deleveraging.go | 28 +++++++++++-------- 3 files changed, 41 insertions(+), 32 deletions(-) diff --git a/protocol/x/clob/abci.go b/protocol/x/clob/abci.go index 2a49b1fbde..1f90eed1ea 100644 --- a/protocol/x/clob/abci.go +++ b/protocol/x/clob/abci.go @@ -199,21 +199,24 @@ func PrepareCheckState( } // 6. Get all potentially liquidatable subaccount IDs and attempt to liquidate them. - subaccountIds := daemonLiquidationInfo.GetLiquidatableSubaccountIds() - subaccountsToDeleverage, err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, subaccountIds) + liquidatableSubaccountIds := daemonLiquidationInfo.GetLiquidatableSubaccountIds() + subaccountsToDeleverage, err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, liquidatableSubaccountIds) if err != nil { panic(err) } - subaccountPositionInfo := map[uint32]map[bool]map[satypes.SubaccountId]struct{}{ + // subaccountPositionInfo := daemonLiquidationInfo.GetSubaccountsWithPositions() + subaccountPositionInfo := map[uint32]*types.SubaccountOpenPositionInfo{ 0: { - false: { - constants.Carl_Num0: {}, + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Dave_Num0, }, - true: { - constants.Dave_Num0: {}, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Carl_Num0, }, }, } + // Add subaccounts with open positions in final settlement markets to the slice of subaccounts/perps // to be deleveraged. subaccountsToDeleverage = append( diff --git a/protocol/x/clob/e2e/liquidation_deleveraging_test.go b/protocol/x/clob/e2e/liquidation_deleveraging_test.go index 18d8fac0f1..e43107314a 100644 --- a/protocol/x/clob/e2e/liquidation_deleveraging_test.go +++ b/protocol/x/clob/e2e/liquidation_deleveraging_test.go @@ -645,7 +645,7 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { // Parameters. placedMatchableOrders []clobtypes.MatchableOrder liquidatableSubaccountIds []satypes.SubaccountId - subaccountPositionInfo map[uint32]map[bool]map[satypes.SubaccountId]struct{} + subaccountPositionInfo map[uint32]*clobtypes.SubaccountOpenPositionInfo // Configuration. liquidationConfig clobtypes.LiquidationsConfig @@ -1041,18 +1041,19 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { }, }, }, - `Deleveraging occurs for negative TNC subaccount with open position in final settlement market`: { + `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: map[uint32]map[bool]map[satypes.SubaccountId]struct{}{ + subaccountPositionInfo: map[uint32]*clobtypes.SubaccountOpenPositionInfo{ constants.BtcUsd_20PercentInitial_10PercentMaintenance.GetId(): { - false: { - constants.Carl_Num0: {}, + PerpetualId: constants.BtcUsd_20PercentInitial_10PercentMaintenance.GetId(), + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Dave_Num0, }, - true: { - constants.Dave_Num0: {}, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Carl_Num0, }, }, }, @@ -1085,18 +1086,19 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { }, }, }, - `Deleveraging occurs for non-negative TNC subaccounts with open positions in final settlement market`: { + `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: map[uint32]map[bool]map[satypes.SubaccountId]struct{}{ + subaccountPositionInfo: map[uint32]*clobtypes.SubaccountOpenPositionInfo{ constants.BtcUsd_20PercentInitial_10PercentMaintenance.GetId(): { - false: { - constants.Carl_Num0: {}, + PerpetualId: constants.BtcUsd_20PercentInitial_10PercentMaintenance.GetId(), + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Dave_Num0, }, - true: { - constants.Dave_Num0: {}, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Carl_Num0, }, }, }, diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index 91d269ba6d..c6c0fc5753 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -523,13 +523,13 @@ func (k Keeper) ProcessDeleveraging( return nil } -// DeleverageSubaccountsInFinalSettlementMarkets uses the subaccountOpenPositionInfo returned from the -// liquidations daemon to deleverage subaccounts with open positions in final settlement markets. Note -// this function will deleverage both negative TNC and non-negative TNC subaccounts. The deleveraging code -// uses the bankruptcy price for the former and the oracle price for the latter. +// GetSubaccountsWithOpenPositionsInFinalSettlementMarkets uses the subaccountOpenPositionInfo returned from the +// liquidations daemon to fetch subaccounts with open positions in final settlement markets. These subaccounts +// will be deleveraged either at the oracle price if non-negative TNC or at bankruptcy price if negative TNC during +// PrepareCheckState. func (k Keeper) GetSubaccountsWithOpenPositionsInFinalSettlementMarkets( ctx sdk.Context, - subaccountOpenPositionInfo map[uint32]map[bool]map[satypes.SubaccountId]struct{}, + subaccountOpenPositionInfo map[uint32]*types.SubaccountOpenPositionInfo, ) (subaccountsToDeleverage []subaccountToDeleverage) { // Gather perpetualIds for perpetuals whose clob pairs are in final settlement. finalSettlementPerpetualIds := make(map[uint32]struct{}) @@ -542,13 +542,17 @@ func (k Keeper) GetSubaccountsWithOpenPositionsInFinalSettlementMarkets( // Fetch subaccounts with open positions in final settlement markets. for perpetualId := range finalSettlementPerpetualIds { - for isBuy := range subaccountOpenPositionInfo[perpetualId] { - for subaccountId := range subaccountOpenPositionInfo[perpetualId][isBuy] { - subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ - SubaccountId: subaccountId, - PerpetualId: perpetualId, - }) - } + for _, subaccountId := range subaccountOpenPositionInfo[perpetualId].SubaccountsWithLongPosition { + subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ + SubaccountId: subaccountId, + PerpetualId: perpetualId, + }) + } + for _, subaccountId := range subaccountOpenPositionInfo[perpetualId].SubaccountsWithShortPosition { + subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ + SubaccountId: subaccountId, + PerpetualId: perpetualId, + }) } } From 0b46719ddff810b33a9756d89eadbe513131eba1 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Tue, 12 Dec 2023 11:23:26 -0800 Subject: [PATCH 23/31] update tests --- protocol/daemons/server/liquidation.go | 10 ++++++++++ protocol/x/clob/abci.go | 16 +--------------- .../x/clob/e2e/liquidation_deleveraging_test.go | 1 + protocol/x/clob/keeper/deleveraging.go | 10 ++++++++-- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/protocol/daemons/server/liquidation.go b/protocol/daemons/server/liquidation.go index 86f1231dcf..c5727ef1b0 100644 --- a/protocol/daemons/server/liquidation.go +++ b/protocol/daemons/server/liquidation.go @@ -8,6 +8,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types" liquidationtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/liquidations" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" + clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" ) // LiquidationServer defines the fields required for liquidation updates. @@ -25,6 +26,15 @@ func (server *Server) WithDaemonLiquidationInfo( return server } +// SetSubaccountOpenPositions stores the list of subaccount open positions in a go-routine safe map. +// Placeholder to allow for testing. +func (s *Server) SetSubaccountOpenPositions( + ctx context.Context, + subaccountsWithPositions map[uint32]*clobtypes.SubaccountOpenPositionInfo, +) { + s.daemonLiquidationInfo.UpdateSubaccountsWithPositions(subaccountsWithPositions) +} + // LiquidateSubaccounts stores the list of potentially liquidatable subaccount ids // in a go-routine safe slice. func (s *Server) LiquidateSubaccounts( diff --git a/protocol/x/clob/abci.go b/protocol/x/clob/abci.go index 1f90eed1ea..70c727aba6 100644 --- a/protocol/x/clob/abci.go +++ b/protocol/x/clob/abci.go @@ -12,10 +12,8 @@ import ( indexershared "github.com/dydxprotocol/v4-chain/protocol/indexer/shared" "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" - "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" "github.com/dydxprotocol/v4-chain/protocol/x/clob/keeper" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" - satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) // BeginBlocker executes all ABCI BeginBlock logic respective to the clob module. @@ -204,19 +202,7 @@ func PrepareCheckState( if err != nil { panic(err) } - // subaccountPositionInfo := daemonLiquidationInfo.GetSubaccountsWithPositions() - subaccountPositionInfo := map[uint32]*types.SubaccountOpenPositionInfo{ - 0: { - PerpetualId: 0, - SubaccountsWithLongPosition: []satypes.SubaccountId{ - constants.Dave_Num0, - }, - SubaccountsWithShortPosition: []satypes.SubaccountId{ - constants.Carl_Num0, - }, - }, - } - + subaccountPositionInfo := daemonLiquidationInfo.GetSubaccountsWithPositions() // Add subaccounts with open positions in final settlement markets to the slice of subaccounts/perps // to be deleveraged. subaccountsToDeleverage = append( diff --git a/protocol/x/clob/e2e/liquidation_deleveraging_test.go b/protocol/x/clob/e2e/liquidation_deleveraging_test.go index e43107314a..efef8aa569 100644 --- a/protocol/x/clob/e2e/liquidation_deleveraging_test.go +++ b/protocol/x/clob/e2e/liquidation_deleveraging_test.go @@ -1214,6 +1214,7 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) } + tApp.App.Server.SetSubaccountOpenPositions(ctx, tc.subaccountPositionInfo) _, err := tApp.App.Server.LiquidateSubaccounts(ctx, &api.LiquidateSubaccountsRequest{ SubaccountIds: tc.liquidatableSubaccountIds, }) diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index c6c0fc5753..9a5de17355 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -542,13 +542,19 @@ func (k Keeper) GetSubaccountsWithOpenPositionsInFinalSettlementMarkets( // Fetch subaccounts with open positions in final settlement markets. for perpetualId := range finalSettlementPerpetualIds { - for _, subaccountId := range subaccountOpenPositionInfo[perpetualId].SubaccountsWithLongPosition { + positionInfo, found := subaccountOpenPositionInfo[perpetualId] + if !found { + // No open positions in the market. + continue + } + + for _, subaccountId := range positionInfo.SubaccountsWithLongPosition { subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ SubaccountId: subaccountId, PerpetualId: perpetualId, }) } - for _, subaccountId := range subaccountOpenPositionInfo[perpetualId].SubaccountsWithShortPosition { + for _, subaccountId := range positionInfo.SubaccountsWithShortPosition { subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ SubaccountId: subaccountId, PerpetualId: perpetualId, From 07d0b9a2135c5377a7ac142894ed61a63f96d7e2 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Thu, 14 Dec 2023 12:25:55 -0800 Subject: [PATCH 24/31] pr nits --- protocol/x/clob/abci.go | 2 + protocol/x/clob/keeper/clob_pair.go | 36 +++++++++++++++++ protocol/x/clob/keeper/deleveraging.go | 41 +++++++++----------- protocol/x/clob/keeper/deleveraging_test.go | 31 ++++++++------- protocol/x/clob/keeper/liquidations.go | 22 ++++------- protocol/x/clob/keeper/process_operations.go | 12 ++++-- protocol/x/clob/types/errors.go | 5 +++ 7 files changed, 93 insertions(+), 56 deletions(-) diff --git a/protocol/x/clob/abci.go b/protocol/x/clob/abci.go index 70c727aba6..c438555e4d 100644 --- a/protocol/x/clob/abci.go +++ b/protocol/x/clob/abci.go @@ -214,6 +214,8 @@ func PrepareCheckState( ) // 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/keeper/clob_pair.go b/protocol/x/clob/keeper/clob_pair.go index d6f0fa5a49..db65673e00 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_FINAL_SETTLEMENT { + 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( diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index 9a5de17355..a7b8975c12 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -33,13 +33,13 @@ func (k Keeper) MaybeDeleverageSubaccount( ) { lib.AssertCheckTxMode(ctx) - canPerformDeleveraging, shouldFinalSettlePosition, err := k.CanDeleverageSubaccount(ctx, subaccountId, perpetualId) + 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 +62,7 @@ func (k Keeper) MaybeDeleverageSubaccount( } deltaQuantums := new(big.Int).Neg(position.GetBigQuantums()) - quantumsDeleveraged, err = k.MemClob.DeleverageSubaccount(ctx, subaccountId, perpetualId, deltaQuantums, shouldFinalSettlePosition) + quantumsDeleveraged, err = k.MemClob.DeleverageSubaccount(ctx, subaccountId, perpetualId, deltaQuantums, shouldDeleverageAtOraclePrice) labels := []metrics.Label{ metrics.GetLabelForIntValue(metrics.PerpetualId, int(perpetualId)), @@ -137,16 +137,16 @@ func (k Keeper) GetInsuranceFundBalance( } // CanDeleverageSubaccount returns true if a subaccount can be deleveraged. -// This function returns two booleans, canDeleverageSubaccount and shouldFinalSettlePosition. -// - canDeleverageSubaccount is true if the subaccount can be deleveraged. -// - shouldFinalSettlePosition is true if the position should be final settled (this occurs at oracle price) +// 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, perpetualId uint32, -) (canDeleverageSubaccount bool, shouldFinalSettlePosition bool, err error) { +) (shouldDeleverageAtBankruptcyPrice bool, shouldDeleverageAtOraclePrice bool, err error) { bigNetCollateral, _, _, @@ -164,13 +164,14 @@ func (k Keeper) CanDeleverageSubaccount( } clobPair := k.mustGetClobPair(ctx, clobPairId) - // Can deleverage subaccount if net collateral is negative or if the market is in final settlement. - canDeleverageSubaccount = bigNetCollateral.Sign() == -1 || clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT - - // Should final settle position if net collateral is non-negative and the market is in final settlement. - shouldFinalSettlePosition = bigNetCollateral.Sign() >= 0 && clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT + // Negative TNC, deleverage at bankruptcy price. + if bigNetCollateral.Sign() == -1 { + return true, false, nil + } - return canDeleverageSubaccount, shouldFinalSettlePosition, nil + // Should deleverage at oracle price if TNC is non-negative and the market is in final settlement. + shouldDeleverageAtOraclePrice = bigNetCollateral.Sign() >= 0 && clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT + return false, shouldDeleverageAtOraclePrice, nil } // IsValidInsuranceFundDelta returns true if the insurance fund has enough funds to cover the insurance @@ -525,23 +526,17 @@ func (k Keeper) ProcessDeleveraging( // GetSubaccountsWithOpenPositionsInFinalSettlementMarkets uses the subaccountOpenPositionInfo returned from the // liquidations daemon to fetch subaccounts with open positions in final settlement markets. These subaccounts -// will be deleveraged either at the oracle price if non-negative TNC or at bankruptcy price if negative TNC during -// PrepareCheckState. +// will be deleveraged either at the oracle price if non-negative TNC or at bankruptcy price if negative TNC. func (k Keeper) GetSubaccountsWithOpenPositionsInFinalSettlementMarkets( ctx sdk.Context, subaccountOpenPositionInfo map[uint32]*types.SubaccountOpenPositionInfo, ) (subaccountsToDeleverage []subaccountToDeleverage) { - // Gather perpetualIds for perpetuals whose clob pairs are in final settlement. - finalSettlementPerpetualIds := make(map[uint32]struct{}) for _, clobPair := range k.GetAllClobPairs(ctx) { - if clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT { - perpetualId := clobPair.MustGetPerpetualId() - finalSettlementPerpetualIds[perpetualId] = struct{}{} + if clobPair.Status != types.ClobPair_STATUS_FINAL_SETTLEMENT { + continue } - } - // Fetch subaccounts with open positions in final settlement markets. - for perpetualId := range finalSettlementPerpetualIds { + perpetualId := clobPair.MustGetPerpetualId() positionInfo, found := subaccountOpenPositionInfo[perpetualId] if !found { // No open positions in the market. diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index c726788f1d..6d5eb7ba76 100644 --- a/protocol/x/clob/keeper/deleveraging_test.go +++ b/protocol/x/clob/keeper/deleveraging_test.go @@ -221,8 +221,8 @@ func TestCanDeleverageSubaccount(t *testing.T) { clobPairs []types.ClobPair // Expectations. - expectedCanDeleverageSubaccount bool - expectedShouldFinalSettlePosition bool + expectedShouldDeleverageAtBankruptcyPrice bool + expectedShouldDeleverageAtOraclePrice bool }{ `Cannot deleverage when subaccount has positive TNC`: { liquidationConfig: constants.LiquidationsConfig_No_Limit, @@ -235,7 +235,8 @@ func TestCanDeleverageSubaccount(t *testing.T) { constants.ClobPair_Btc, }, - expectedCanDeleverageSubaccount: false, + expectedShouldDeleverageAtBankruptcyPrice: false, + expectedShouldDeleverageAtOraclePrice: false, }, `Cannot deleverage when subaccount has zero TNC`: { liquidationConfig: constants.LiquidationsConfig_No_Limit, @@ -248,7 +249,8 @@ func TestCanDeleverageSubaccount(t *testing.T) { constants.ClobPair_Btc, }, - expectedCanDeleverageSubaccount: false, + expectedShouldDeleverageAtBankruptcyPrice: false, + expectedShouldDeleverageAtOraclePrice: false, }, `Can deleverage when subaccount has negative TNC`: { liquidationConfig: constants.LiquidationsConfig_No_Limit, @@ -261,7 +263,8 @@ func TestCanDeleverageSubaccount(t *testing.T) { constants.ClobPair_Btc, }, - expectedCanDeleverageSubaccount: true, + expectedShouldDeleverageAtBankruptcyPrice: true, + expectedShouldDeleverageAtOraclePrice: false, }, `Can deleverage when subaccount has negative TNC and clob pair has status FINAL_SETTLEMENT`: { liquidationConfig: constants.LiquidationsConfig_No_Limit, @@ -274,8 +277,8 @@ func TestCanDeleverageSubaccount(t *testing.T) { constants.ClobPair_Btc_Final_Settlement, }, - expectedCanDeleverageSubaccount: true, - expectedShouldFinalSettlePosition: false, + 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, @@ -288,8 +291,8 @@ func TestCanDeleverageSubaccount(t *testing.T) { constants.ClobPair_Btc_Final_Settlement, }, - expectedCanDeleverageSubaccount: true, - expectedShouldFinalSettlePosition: true, + expectedShouldDeleverageAtBankruptcyPrice: false, + expectedShouldDeleverageAtOraclePrice: true, }, } @@ -388,7 +391,7 @@ func TestCanDeleverageSubaccount(t *testing.T) { ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.subaccount) - canDeleverageSubaccount, shouldFinalSettlePosition, err := ks.ClobKeeper.CanDeleverageSubaccount( + shouldDeleverageAtBankruptcyPrice, shouldDeleverageAtOraclePrice, err := ks.ClobKeeper.CanDeleverageSubaccount( ks.Ctx, *tc.subaccount.Id, 0, @@ -396,13 +399,13 @@ func TestCanDeleverageSubaccount(t *testing.T) { require.NoError(t, err) require.Equal( t, - tc.expectedCanDeleverageSubaccount, - canDeleverageSubaccount, + tc.expectedShouldDeleverageAtBankruptcyPrice, + shouldDeleverageAtBankruptcyPrice, ) require.Equal( t, - tc.expectedShouldFinalSettlePosition, - shouldFinalSettlePosition, + tc.expectedShouldDeleverageAtOraclePrice, + shouldDeleverageAtOraclePrice, ) }) } diff --git a/protocol/x/clob/keeper/liquidations.go b/protocol/x/clob/keeper/liquidations.go index 9bfd64dd14..d9d0f49866 100644 --- a/protocol/x/clob/keeper/liquidations.go +++ b/protocol/x/clob/keeper/liquidations.go @@ -101,7 +101,6 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( // Attempt to place each liquidation order and perform deleveraging if necessary. startPlaceLiquidationOrders := time.Now() - clobPairStatuses := make(map[types.ClobPairId]types.ClobPair_Status) 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) @@ -116,21 +115,10 @@ func (k Keeper) LiquidateSubaccountsAgainstOrderbook( return nil, err } - // Skip liquidation if the ClobPair for the liquidation order is in final settlement. - if _, found := clobPairStatuses[liquidationOrder.GetClobPairId()]; !found { - clobPair := k.mustGetClobPair(ctx, liquidationOrder.GetClobPairId()) - clobPairStatuses[liquidationOrder.GetClobPairId()] = clobPair.Status - } - if clobPairStatuses[liquidationOrder.GetClobPairId()] == types.ClobPair_STATUS_FINAL_SETTLEMENT { - subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ - SubaccountId: liquidationOrder.GetSubaccountId(), - PerpetualId: liquidationOrder.MustGetLiquidatedPerpetualId(), - }) - continue - } - 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, @@ -256,6 +244,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/process_operations.go b/protocol/x/clob/keeper/process_operations.go index 7a7ede357d..f88002e6cb 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -622,7 +622,11 @@ func (k Keeper) PersistMatchDeleveragingToState( perpetualId := matchDeleveraging.GetPerpetualId() // Validate that the provided subaccount can be deleveraged. - if canDeleverageSubaccount, shouldFinalSettlePosition, err := k.CanDeleverageSubaccount(ctx, liquidatedSubaccountId, perpetualId); err != nil { + if shouldDeleverageAtBankruptcyPrice, shouldDeleverageAtOraclePrice, err := k.CanDeleverageSubaccount( + ctx, + liquidatedSubaccountId, + perpetualId, + ); err != nil { panic( fmt.Sprintf( "PersistMatchDeleveragingToState: Failed to determine if subaccount can be deleveraged. "+ @@ -631,14 +635,14 @@ func (k Keeper) PersistMatchDeleveragingToState( err, ), ) - } else if !canDeleverageSubaccount { + } else if !shouldDeleverageAtBankruptcyPrice && !shouldDeleverageAtOraclePrice { // TODO(CLOB-853): Add more verbose error logging about why deleveraging failed validation. return errorsmod.Wrapf( types.ErrInvalidDeleveragedSubaccount, "Subaccount %+v failed deleveraging validation", liquidatedSubaccountId, ) - } else if matchDeleveraging.IsFinalSettlement != shouldFinalSettlePosition { + } else 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. @@ -647,7 +651,7 @@ func (k Keeper) PersistMatchDeleveragingToState( "MatchPerpetualDeleveraging %+v has isFinalSettlement flag (%v), expected (%v)", matchDeleveraging, matchDeleveraging.IsFinalSettlement, - shouldFinalSettlePosition, + shouldDeleverageAtOraclePrice, ) } diff --git a/protocol/x/clob/types/errors.go b/protocol/x/clob/types/errors.go index 681c0ecd10..b61ef69223 100644 --- a/protocol/x/clob/types/errors.go +++ b/protocol/x/clob/types/errors.go @@ -201,6 +201,11 @@ var ( 43, "Order has remaining size", ) + ErrLiquidationConflictsWithClobPairStatus = errorsmod.Register( + ModuleName, + 44, + "Liquidation conflicts with ClobPair status", + ) // Liquidations errors. ErrInvalidLiquidationsConfig = errorsmod.Register( From 7b1585371d0e8db1fff94870182632ff2290b8d9 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Thu, 14 Dec 2023 12:33:57 -0800 Subject: [PATCH 25/31] lint --- protocol/x/clob/keeper/deleveraging_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index 6d5eb7ba76..c19ebe1eb2 100644 --- a/protocol/x/clob/keeper/deleveraging_test.go +++ b/protocol/x/clob/keeper/deleveraging_test.go @@ -221,8 +221,8 @@ func TestCanDeleverageSubaccount(t *testing.T) { clobPairs []types.ClobPair // Expectations. - expectedShouldDeleverageAtBankruptcyPrice bool - expectedShouldDeleverageAtOraclePrice bool + expectedShouldDeleverageAtBankruptcyPrice bool + expectedShouldDeleverageAtOraclePrice bool }{ `Cannot deleverage when subaccount has positive TNC`: { liquidationConfig: constants.LiquidationsConfig_No_Limit, @@ -236,7 +236,7 @@ func TestCanDeleverageSubaccount(t *testing.T) { }, expectedShouldDeleverageAtBankruptcyPrice: false, - expectedShouldDeleverageAtOraclePrice: false, + expectedShouldDeleverageAtOraclePrice: false, }, `Cannot deleverage when subaccount has zero TNC`: { liquidationConfig: constants.LiquidationsConfig_No_Limit, @@ -250,7 +250,7 @@ func TestCanDeleverageSubaccount(t *testing.T) { }, expectedShouldDeleverageAtBankruptcyPrice: false, - expectedShouldDeleverageAtOraclePrice: false, + expectedShouldDeleverageAtOraclePrice: false, }, `Can deleverage when subaccount has negative TNC`: { liquidationConfig: constants.LiquidationsConfig_No_Limit, @@ -264,7 +264,7 @@ func TestCanDeleverageSubaccount(t *testing.T) { }, expectedShouldDeleverageAtBankruptcyPrice: true, - expectedShouldDeleverageAtOraclePrice: false, + expectedShouldDeleverageAtOraclePrice: false, }, `Can deleverage when subaccount has negative TNC and clob pair has status FINAL_SETTLEMENT`: { liquidationConfig: constants.LiquidationsConfig_No_Limit, @@ -277,8 +277,8 @@ func TestCanDeleverageSubaccount(t *testing.T) { constants.ClobPair_Btc_Final_Settlement, }, - expectedShouldDeleverageAtBankruptcyPrice: true, - expectedShouldDeleverageAtOraclePrice: false, + 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, @@ -291,8 +291,8 @@ func TestCanDeleverageSubaccount(t *testing.T) { constants.ClobPair_Btc_Final_Settlement, }, - expectedShouldDeleverageAtBankruptcyPrice: false, - expectedShouldDeleverageAtOraclePrice: true, + expectedShouldDeleverageAtBankruptcyPrice: false, + expectedShouldDeleverageAtOraclePrice: true, }, } From c62914fb5d3512833c1293233acd9833837a02b0 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Thu, 14 Dec 2023 13:52:13 -0800 Subject: [PATCH 26/31] formatting --- .../clob/e2e/liquidation_deleveraging_test.go | 3 ++- protocol/x/clob/keeper/deleveraging.go | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/protocol/x/clob/e2e/liquidation_deleveraging_test.go b/protocol/x/clob/e2e/liquidation_deleveraging_test.go index efef8aa569..b2ec406c65 100644 --- a/protocol/x/clob/e2e/liquidation_deleveraging_test.go +++ b/protocol/x/clob/e2e/liquidation_deleveraging_test.go @@ -1086,7 +1086,8 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { }, }, }, - `Deleveraging occurs at oracle price for non-negative TNC subaccounts with open positions in final settlement market`: { + `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, diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index a7b8975c12..76f740668e 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -33,7 +33,11 @@ func (k Keeper) MaybeDeleverageSubaccount( ) { lib.AssertCheckTxMode(ctx) - shouldDeleverageAtBankruptcyPrice, shouldDeleverageAtOraclePrice, err := k.CanDeleverageSubaccount(ctx, subaccountId, perpetualId) + shouldDeleverageAtBankruptcyPrice, shouldDeleverageAtOraclePrice, err := k.CanDeleverageSubaccount( + ctx, + subaccountId, + perpetualId, + ) if err != nil { return new(big.Int), err } @@ -62,7 +66,13 @@ func (k Keeper) MaybeDeleverageSubaccount( } deltaQuantums := new(big.Int).Neg(position.GetBigQuantums()) - quantumsDeleveraged, err = k.MemClob.DeleverageSubaccount(ctx, subaccountId, perpetualId, deltaQuantums, shouldDeleverageAtOraclePrice) + quantumsDeleveraged, err = k.MemClob.DeleverageSubaccount( + ctx, + subaccountId, + perpetualId, + deltaQuantums, + shouldDeleverageAtOraclePrice, + ) labels := []metrics.Label{ metrics.GetLabelForIntValue(metrics.PerpetualId, int(perpetualId)), @@ -169,9 +179,8 @@ func (k Keeper) CanDeleverageSubaccount( return true, false, nil } - // Should deleverage at oracle price if TNC is non-negative and the market is in final settlement. - shouldDeleverageAtOraclePrice = bigNetCollateral.Sign() >= 0 && clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT - return false, shouldDeleverageAtOraclePrice, nil + // Non-negative TNC, deleverage at oracle price if market is in final settlement. + return false, clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT, nil } // IsValidInsuranceFundDelta returns true if the insurance fund has enough funds to cover the insurance From ec22320425c50f9e6e731e48c8868eeacf3ea092 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Mon, 18 Dec 2023 16:48:23 -0800 Subject: [PATCH 27/31] nits and tests --- protocol/lib/metrics/constants.go | 213 ++++++++++---------- protocol/x/clob/keeper/clob_pair.go | 2 +- protocol/x/clob/keeper/deleveraging.go | 38 ++-- protocol/x/clob/keeper/liquidations.go | 7 +- protocol/x/clob/keeper/liquidations_test.go | 73 +++++++ 5 files changed, 210 insertions(+), 123 deletions(-) diff --git a/protocol/lib/metrics/constants.go b/protocol/lib/metrics/constants.go index 2b7c511198..5660e45999 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" + ClobGetSubaccountsWithOpenPositionsInFinalSettlementMarkets = "get_subaccounts_with_open_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/x/clob/keeper/clob_pair.go b/protocol/x/clob/keeper/clob_pair.go index db65673e00..70b27ba99b 100644 --- a/protocol/x/clob/keeper/clob_pair.go +++ b/protocol/x/clob/keeper/clob_pair.go @@ -328,7 +328,7 @@ func (k Keeper) validateLiquidationAgainstClobPairStatus( ) } - if clobPair.Status == types.ClobPair_STATUS_FINAL_SETTLEMENT { + if clobPair.Status != types.ClobPair_STATUS_ACTIVE { return errorsmod.Wrapf( types.ErrLiquidationConflictsWithClobPairStatus, "Liquidation order %+v cannot be placed for clob pair with status %+v", diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index 76f740668e..c4c97fdf7a 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -535,18 +535,26 @@ func (k Keeper) ProcessDeleveraging( // GetSubaccountsWithOpenPositionsInFinalSettlementMarkets uses the subaccountOpenPositionInfo returned from the // liquidations daemon to fetch subaccounts with open positions in final settlement markets. These subaccounts -// will be deleveraged either at the oracle price if non-negative TNC or at bankruptcy price if negative TNC. +// 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) GetSubaccountsWithOpenPositionsInFinalSettlementMarkets( ctx sdk.Context, subaccountOpenPositionInfo map[uint32]*types.SubaccountOpenPositionInfo, ) (subaccountsToDeleverage []subaccountToDeleverage) { + defer telemetry.MeasureSince( + time.Now(), + types.ModuleName, + metrics.ClobGetSubaccountsWithOpenPositionsInFinalSettlementMarkets, + metrics.Latency, + ) + for _, clobPair := range k.GetAllClobPairs(ctx) { if clobPair.Status != types.ClobPair_STATUS_FINAL_SETTLEMENT { continue } - perpetualId := clobPair.MustGetPerpetualId() - positionInfo, found := subaccountOpenPositionInfo[perpetualId] + finalSettlementPerpetualId := clobPair.MustGetPerpetualId() + positionInfo, found := subaccountOpenPositionInfo[finalSettlementPerpetualId] if !found { // No open positions in the market. continue @@ -555,13 +563,13 @@ func (k Keeper) GetSubaccountsWithOpenPositionsInFinalSettlementMarkets( for _, subaccountId := range positionInfo.SubaccountsWithLongPosition { subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ SubaccountId: subaccountId, - PerpetualId: perpetualId, + PerpetualId: finalSettlementPerpetualId, }) } for _, subaccountId := range positionInfo.SubaccountsWithShortPosition { subaccountsToDeleverage = append(subaccountsToDeleverage, subaccountToDeleverage{ SubaccountId: subaccountId, - PerpetualId: perpetualId, + PerpetualId: finalSettlementPerpetualId, }) } } @@ -574,10 +582,17 @@ 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. - startDeleverageSubaccounts := time.Now() - var i int - for i = 0; i < int(k.Flags.MaxDeleveragingAttemptsPerBlock) && i < len(subaccountsToDeleverage); i++ { + 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) @@ -592,12 +607,5 @@ func (k Keeper) DeleverageSubaccounts( } } - telemetry.MeasureSince( - startDeleverageSubaccounts, - types.ModuleName, - metrics.LiquidateSubaccounts_Deleverage, - metrics.Latency, - ) - return nil } diff --git a/protocol/x/clob/keeper/liquidations.go b/protocol/x/clob/keeper/liquidations.go index d9d0f49866..80a9642d81 100644 --- a/protocol/x/clob/keeper/liquidations.go +++ b/protocol/x/clob/keeper/liquidations.go @@ -18,6 +18,9 @@ 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 @@ -25,7 +28,9 @@ type subaccountToDeleverage struct { // 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, diff --git a/protocol/x/clob/keeper/liquidations_test.go b/protocol/x/clob/keeper/liquidations_test.go index bcaed32fd3..137808ea5c 100644 --- a/protocol/x/clob/keeper/liquidations_test.go +++ b/protocol/x/clob/keeper/liquidations_test.go @@ -343,6 +343,79 @@ 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. From 0171a7fb1ea439b8b48a890db53dbd6cc95d2b31 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Mon, 18 Dec 2023 17:07:18 -0800 Subject: [PATCH 28/31] more nits and comment updates --- protocol/lib/metrics/metric_keys.go | 1 + protocol/x/clob/keeper/deleveraging.go | 10 +++++++++- protocol/x/clob/keeper/liquidations.go | 3 ++- protocol/x/clob/keeper/process_operations.go | 18 +++++++++++++----- protocol/x/clob/types/errors.go | 10 +++++----- 5 files changed, 30 insertions(+), 12 deletions(-) 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/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index c4c97fdf7a..583061c32e 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -179,7 +179,9 @@ func (k Keeper) CanDeleverageSubaccount( return true, false, nil } - // Non-negative TNC, deleverage at oracle price if market is in final settlement. + // 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 } @@ -574,10 +576,16 @@ func (k Keeper) GetSubaccountsWithOpenPositionsInFinalSettlementMarkets( } } + 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, diff --git a/protocol/x/clob/keeper/liquidations.go b/protocol/x/clob/keeper/liquidations.go index 80a9642d81..6806eae843 100644 --- a/protocol/x/clob/keeper/liquidations.go +++ b/protocol/x/clob/keeper/liquidations.go @@ -232,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, diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index f88002e6cb..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. @@ -622,11 +625,12 @@ func (k Keeper) PersistMatchDeleveragingToState( perpetualId := matchDeleveraging.GetPerpetualId() // Validate that the provided subaccount can be deleveraged. - if shouldDeleverageAtBankruptcyPrice, shouldDeleverageAtOraclePrice, err := k.CanDeleverageSubaccount( + shouldDeleverageAtBankruptcyPrice, shouldDeleverageAtOraclePrice, err := k.CanDeleverageSubaccount( ctx, liquidatedSubaccountId, perpetualId, - ); err != nil { + ) + if err != nil { panic( fmt.Sprintf( "PersistMatchDeleveragingToState: Failed to determine if subaccount can be deleveraged. "+ @@ -635,14 +639,18 @@ func (k Keeper) PersistMatchDeleveragingToState( err, ), ) - } else if !shouldDeleverageAtBankruptcyPrice && !shouldDeleverageAtOraclePrice { + } + + if !shouldDeleverageAtBankruptcyPrice && !shouldDeleverageAtOraclePrice { // TODO(CLOB-853): Add more verbose error logging about why deleveraging failed validation. return errorsmod.Wrapf( types.ErrInvalidDeleveragedSubaccount, "Subaccount %+v failed deleveraging validation", liquidatedSubaccountId, ) - } else if matchDeleveraging.IsFinalSettlement != shouldDeleverageAtOraclePrice { + } + + 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. diff --git a/protocol/x/clob/types/errors.go b/protocol/x/clob/types/errors.go index b61ef69223..a9396f1026 100644 --- a/protocol/x/clob/types/errors.go +++ b/protocol/x/clob/types/errors.go @@ -201,11 +201,6 @@ var ( 43, "Order has remaining size", ) - ErrLiquidationConflictsWithClobPairStatus = errorsmod.Register( - ModuleName, - 44, - "Liquidation conflicts with ClobPair status", - ) // Liquidations errors. ErrInvalidLiquidationsConfig = errorsmod.Register( @@ -318,6 +313,11 @@ var ( 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( From 7ce548b1190514ed398d577970ca99e4140c0a5b Mon Sep 17 00:00:00 2001 From: Jakob Herlitz <125316911+jakob-dydx@users.noreply.github.com> Date: Tue, 19 Dec 2023 12:55:17 -0800 Subject: [PATCH 29/31] [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 --- protocol/x/clob/keeper/clob_pair.go | 13 + .../x/clob/keeper/process_operations_test.go | 312 ++++++++++++++++++ 2 files changed, 325 insertions(+) diff --git a/protocol/x/clob/keeper/clob_pair.go b/protocol/x/clob/keeper/clob_pair.go index 70b27ba99b..979d994f57 100644 --- a/protocol/x/clob/keeper/clob_pair.go +++ b/protocol/x/clob/keeper/clob_pair.go @@ -631,6 +631,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/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 { From 82c56c02e6b11f1fc9aa82ba31ac46d8af199bd6 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Wed, 20 Dec 2023 10:44:07 -0800 Subject: [PATCH 30/31] merge from upstream --- protocol/daemons/server/liquidation.go | 10 ---------- .../x/clob/e2e/liquidation_deleveraging_test.go | 15 +++++++-------- protocol/x/clob/types/clob_pair.go | 4 ++++ protocol/x/clob/types/clob_pair_test.go | 4 ++++ 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/protocol/daemons/server/liquidation.go b/protocol/daemons/server/liquidation.go index f6e78a215d..573ede7eb7 100644 --- a/protocol/daemons/server/liquidation.go +++ b/protocol/daemons/server/liquidation.go @@ -8,7 +8,6 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types" liquidationtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/liquidations" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" - clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" ) // LiquidationServer defines the fields required for liquidation updates. @@ -26,15 +25,6 @@ func (server *Server) WithDaemonLiquidationInfo( return server } -// SetSubaccountOpenPositions stores the list of subaccount open positions in a go-routine safe map. -// Placeholder to allow for testing. -func (s *Server) SetSubaccountOpenPositions( - ctx context.Context, - subaccountsWithPositions map[uint32]*clobtypes.SubaccountOpenPositionInfo, -) { - s.daemonLiquidationInfo.UpdateSubaccountsWithPositions(subaccountsWithPositions) -} - // LiquidateSubaccounts stores the list of potentially liquidatable subaccount ids // in a go-routine safe slice. func (s *Server) LiquidateSubaccounts( diff --git a/protocol/x/clob/e2e/liquidation_deleveraging_test.go b/protocol/x/clob/e2e/liquidation_deleveraging_test.go index c9b885241e..ac9de31a7a 100644 --- a/protocol/x/clob/e2e/liquidation_deleveraging_test.go +++ b/protocol/x/clob/e2e/liquidation_deleveraging_test.go @@ -645,7 +645,7 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { // Parameters. placedMatchableOrders []clobtypes.MatchableOrder liquidatableSubaccountIds []satypes.SubaccountId - subaccountPositionInfo map[uint32]*clobtypes.SubaccountOpenPositionInfo + subaccountPositionInfo []clobtypes.SubaccountOpenPositionInfo // Configuration. liquidationConfig clobtypes.LiquidationsConfig @@ -1046,8 +1046,8 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { constants.Carl_Num0_1BTC_Short_50499USD, constants.Dave_Num0_1BTC_Long_50000USD, }, - subaccountPositionInfo: map[uint32]*clobtypes.SubaccountOpenPositionInfo{ - constants.BtcUsd_20PercentInitial_10PercentMaintenance.GetId(): { + subaccountPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { PerpetualId: constants.BtcUsd_20PercentInitial_10PercentMaintenance.GetId(), SubaccountsWithLongPosition: []satypes.SubaccountId{ constants.Dave_Num0, @@ -1092,8 +1092,8 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { constants.Carl_Num0_1BTC_Short_100000USD, constants.Dave_Num0_1BTC_Long_50000USD, }, - subaccountPositionInfo: map[uint32]*clobtypes.SubaccountOpenPositionInfo{ - constants.BtcUsd_20PercentInitial_10PercentMaintenance.GetId(): { + subaccountPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { PerpetualId: constants.BtcUsd_20PercentInitial_10PercentMaintenance.GetId(), SubaccountsWithLongPosition: []satypes.SubaccountId{ constants.Dave_Num0, @@ -1103,7 +1103,6 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { }, }, }, - // Account should be deleveraged regardless of whether or not the liquidations engine returns this subaccount liquidatableSubaccountIds: []satypes.SubaccountId{}, liquidationConfig: constants.LiquidationsConfig_FillablePrice_Max_Smmr, liquidityTiers: constants.LiquidityTiers, @@ -1215,9 +1214,9 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) } - tApp.App.Server.SetSubaccountOpenPositions(ctx, tc.subaccountPositionInfo) _, 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/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): { From 6dae7517a2c64691755f2c2ade3e1cc4ae8b0d44 Mon Sep 17 00:00:00 2001 From: Jakob Herlitz Date: Wed, 20 Dec 2023 12:05:36 -0800 Subject: [PATCH 31/31] format --- protocol/lib/metrics/constants.go | 214 ++++++++++---------- protocol/x/clob/abci.go | 2 +- protocol/x/clob/keeper/deleveraging.go | 6 +- protocol/x/clob/keeper/liquidations_test.go | 5 +- 4 files changed, 115 insertions(+), 112 deletions(-) diff --git a/protocol/lib/metrics/constants.go b/protocol/lib/metrics/constants.go index e11fbbd2e8..5acaea443c 100644 --- a/protocol/lib/metrics/constants.go +++ b/protocol/lib/metrics/constants.go @@ -88,113 +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" - ClobGetSubaccountsWithOpenPositionsInFinalSettlementMarkets = "get_subaccounts_with_open_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" + 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/x/clob/abci.go b/protocol/x/clob/abci.go index c438555e4d..9739e24160 100644 --- a/protocol/x/clob/abci.go +++ b/protocol/x/clob/abci.go @@ -207,7 +207,7 @@ func PrepareCheckState( // to be deleveraged. subaccountsToDeleverage = append( subaccountsToDeleverage, - keeper.GetSubaccountsWithOpenPositionsInFinalSettlementMarkets( + keeper.GetSubaccountsWithPositionsInFinalSettlementMarkets( ctx, subaccountPositionInfo, )..., diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index 583061c32e..c8c89c85cd 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -535,18 +535,18 @@ func (k Keeper) ProcessDeleveraging( return nil } -// GetSubaccountsWithOpenPositionsInFinalSettlementMarkets uses the subaccountOpenPositionInfo returned from the +// 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) GetSubaccountsWithOpenPositionsInFinalSettlementMarkets( +func (k Keeper) GetSubaccountsWithPositionsInFinalSettlementMarkets( ctx sdk.Context, subaccountOpenPositionInfo map[uint32]*types.SubaccountOpenPositionInfo, ) (subaccountsToDeleverage []subaccountToDeleverage) { defer telemetry.MeasureSince( time.Now(), types.ModuleName, - metrics.ClobGetSubaccountsWithOpenPositionsInFinalSettlementMarkets, + metrics.ClobGetSubaccountsWithPositionsInFinalSettlementMarkets, metrics.Latency, ) diff --git a/protocol/x/clob/keeper/liquidations_test.go b/protocol/x/clob/keeper/liquidations_test.go index 137808ea5c..a6e6efb63d 100644 --- a/protocol/x/clob/keeper/liquidations_test.go +++ b/protocol/x/clob/keeper/liquidations_test.go @@ -408,7 +408,10 @@ func TestPlacePerpetualLiquidation_validateLiquidationAgainstClobPairStatus(t *t ) require.NoError(t, err) - _, _, err = ks.ClobKeeper.PlacePerpetualLiquidation(ctx, constants.LiquidationOrder_Dave_Num0_Clob0_Sell1BTC_Price50000) + _, _, err = ks.ClobKeeper.PlacePerpetualLiquidation( + ctx, + constants.LiquidationOrder_Dave_Num0_Clob0_Sell1BTC_Price50000, + ) if tc.expectedError != nil { require.ErrorIs(t, err, tc.expectedError) }