Skip to content

Commit

Permalink
[feat][protocol] Write block number to state when encountering zero-f…
Browse files Browse the repository at this point in the history
…ill deleveraging op [CLOB-1030] (#904)
  • Loading branch information
lucas-dydx authored Jan 4, 2024
1 parent d2ec340 commit 925b3b3
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 12 deletions.
1 change: 1 addition & 0 deletions protocol/lib/metrics/metric_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
LiquidationsPlacePerpetualLiquidationQuoteQuantums = "liquidations_place_perpetual_liquidation_quote_quantums"
LiquidationsLiquidationMatchNegativeTNC = "liquidations_liquidation_match_negative_tnc"
ClobMevErrorCount = "clob_mev_error_count"
SubaccountsNegativeTncSubaccountSeen = "negative_tnc_subaccount_seen"

// Gauges
InsuranceFundBalance = "insurance_fund_balance"
Expand Down
26 changes: 26 additions & 0 deletions protocol/x/clob/keeper/process_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,32 @@ func (k Keeper) PersistMatchDeleveragingToState(
}
deltaBaseQuantumsIsNegative := position.GetIsLong()

// If there are zero-fill deleveraging operations, this is a sentinel value to indicate a subaccount could not be
// liquidated or deleveraged and still has negative equity. Mark the current block number in state to indicate a
// negative TNC subaccount was seen.
if len(matchDeleveraging.GetFills()) == 0 {
if !shouldDeleverageAtBankruptcyPrice {
return errorsmod.Wrapf(
types.ErrZeroFillDeleveragingForNonNegativeTncSubaccount,
fmt.Sprintf(
"PersistMatchDeleveragingToState: zero-fill deleveraging operation included for subaccount %+v"+
" and perpetual %d but subaccount isn't negative TNC",
liquidatedSubaccountId,
perpetualId,
),
)
}

metrics.IncrCountMetricWithLabels(
types.ModuleName,
metrics.SubaccountsNegativeTncSubaccountSeen,
metrics.GetLabelForIntValue(metrics.PerpetualId, int(perpetualId)),
metrics.GetLabelForBoolValue(metrics.IsLong, position.GetIsLong()),
)
k.subaccountsKeeper.SetNegativeTncSubaccountSeenAtBlock(ctx, lib.MustConvertIntegerToUint32(ctx.BlockHeight()))
return nil
}

for _, fill := range matchDeleveraging.GetFills() {
deltaBaseQuantums := new(big.Int).SetUint64(fill.FillAmount)
if deltaBaseQuantumsIsNegative {
Expand Down
232 changes: 231 additions & 1 deletion protocol/x/clob/keeper/process_operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type processProposerOperationsTestCase struct {
expectedQuoteBalances map[satypes.SubaccountId]int64
expectedPerpetualPositions map[satypes.SubaccountId][]*satypes.PerpetualPosition
expectedSubaccountLiquidationInfo map[satypes.SubaccountId]types.SubaccountLiquidationInfo
expectedNegativeTncSubaccountSeen bool
expectedError error
expectedPanics string
}
Expand Down Expand Up @@ -920,6 +921,189 @@ func TestProcessProposerOperations(t *testing.T) {
constants.Dave_Num0: {},
},
},
"Zero-fill deleveraging succeeds when the account is negative TNC and updates the last negative TNC subaccount " +
"seen block number in state": {
perpetuals: []*perptypes.Perpetual{
&constants.BtcUsd_20PercentInitial_10PercentMaintenance,
},
perpetualFeeParams: &constants.PerpetualFeeParams,
clobPairs: []types.ClobPair{
constants.ClobPair_Btc,
},
subaccounts: []satypes.Subaccount{
// deleverageable since TNC = -$1
constants.Carl_Num0_1BTC_Short_50499USD,
},
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{},
},
),
},

expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{
BlockHeight: blockHeight,
},
expectedQuoteBalances: map[satypes.SubaccountId]int64{
constants.Carl_Num0: constants.Carl_Num0_1BTC_Short_50499USD.GetUsdcPosition().Int64(),
},
expectedPerpetualPositions: map[satypes.SubaccountId][]*satypes.PerpetualPosition{
constants.Carl_Num0: constants.Carl_Num0_1BTC_Short_50499USD.GetPerpetualPositions(),
},
expectedNegativeTncSubaccountSeen: true,
},
"Zero-fill deleveraging succeeds when the account is negative TNC and has a position in final settlement" +
" market. It updates the last negative TNC subaccount seen block number in state": {
perpetuals: []*perptypes.Perpetual{
&constants.BtcUsd_100PercentMarginRequirement,
},
perpetualFeeParams: &constants.PerpetualFeeParams,
clobPairs: []types.ClobPair{
constants.ClobPair_Btc_Final_Settlement,
},
subaccounts: []satypes.Subaccount{
// liquidatable: MMR = $5000, TNC = -$1.
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{},
},
),
},
expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{
BlockHeight: blockHeight,
},
expectedQuoteBalances: map[satypes.SubaccountId]int64{
constants.Carl_Num0: constants.Carl_Num0_1BTC_Short_50499USD.GetUsdcPosition().Int64(),
constants.Dave_Num0: constants.Dave_Num0_1BTC_Long_50000USD.GetUsdcPosition().Int64(),
},
expectedPerpetualPositions: map[satypes.SubaccountId][]*satypes.PerpetualPosition{
constants.Carl_Num0: constants.Carl_Num0_1BTC_Short_50499USD.GetPerpetualPositions(),
constants.Dave_Num0: constants.Dave_Num0_1BTC_Long_50000USD.GetPerpetualPositions(),
},

expectedNegativeTncSubaccountSeen: true,
},
"Zero-fill deleveraging succeeds when there's multiple zero-fill deleveraging events for the same subaccount " +
"and perpetual ID": {
perpetuals: []*perptypes.Perpetual{
&constants.BtcUsd_20PercentInitial_10PercentMaintenance,
},
perpetualFeeParams: &constants.PerpetualFeeParams,
clobPairs: []types.ClobPair{
constants.ClobPair_Btc,
},
subaccounts: []satypes.Subaccount{
// deleverageable since TNC = -$1
constants.Carl_Num0_1BTC_Short_50499USD,
},
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{},
},
),
clobtest.NewMatchOperationRawFromPerpetualDeleveragingLiquidation(
types.MatchPerpetualDeleveraging{
Liquidated: constants.Carl_Num0,
PerpetualId: 0,
Fills: []types.MatchPerpetualDeleveraging_Fill{},
},
),
},

expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{
BlockHeight: blockHeight,
},
expectedQuoteBalances: map[satypes.SubaccountId]int64{
constants.Carl_Num0: constants.Carl_Num0_1BTC_Short_50499USD.GetUsdcPosition().Int64(),
},
expectedPerpetualPositions: map[satypes.SubaccountId][]*satypes.PerpetualPosition{
constants.Carl_Num0: constants.Carl_Num0_1BTC_Short_50499USD.GetPerpetualPositions(),
},
expectedNegativeTncSubaccountSeen: true,
},
"Zero-fill deleverage succeeds after the same subaccount is partially deleveraged": {
perpetuals: []*perptypes.Perpetual{
&constants.BtcUsd_20PercentInitial_10PercentMaintenance,
},
perpetualFeeParams: &constants.PerpetualFeeParams,
clobPairs: []types.ClobPair{
constants.ClobPair_Btc,
},
subaccounts: []satypes.Subaccount{
// deleveragable: TNC = -$1
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: 50_000_000,
},
},
},
),
clobtest.NewMatchOperationRawFromPerpetualDeleveragingLiquidation(
types.MatchPerpetualDeleveraging{
Liquidated: constants.Carl_Num0,
PerpetualId: 0,
Fills: []types.MatchPerpetualDeleveraging_Fill{},
},
),
},
expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{
BlockHeight: blockHeight,
},
expectedQuoteBalances: map[satypes.SubaccountId]int64{
constants.Carl_Num0: constants.Carl_Num0_1BTC_Short_50499USD.GetUsdcPosition().Int64() - 25_249_500_000,
constants.Dave_Num0: constants.Dave_Num0_1BTC_Long_50000USD.GetUsdcPosition().Int64() + 25_249_500_000,
},
expectedPerpetualPositions: map[satypes.SubaccountId][]*satypes.PerpetualPosition{
constants.Carl_Num0: {
{
PerpetualId: 0,
Quantums: dtypes.NewInt(-100_000_000 + 50_000_000),
FundingIndex: dtypes.ZeroInt(),
},
},
constants.Dave_Num0: {
{
PerpetualId: 0,
Quantums: dtypes.NewInt(100_000_000 - 50_000_000),
FundingIndex: dtypes.ZeroInt(),
},
},
},
expectedNegativeTncSubaccountSeen: true,
},
"Succeeds order removal operations with previous stateful orders": {
perpetuals: []*perptypes.Perpetual{
&constants.BtcUsd_100PercentMarginRequirement,
Expand Down Expand Up @@ -1351,6 +1535,42 @@ func TestProcessProposerOperations(t *testing.T) {
},
expectedPanics: "validateInternalOperationAgainstClobPairStatus: ClobPair's status is not supported",
},
"Returns error if zero-fill deleveraging operation proposed for non-negative TNC subaccount 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{},
IsFinalSettlement: true,
},
),
},
expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{
BlockHeight: blockHeight,
},
expectedQuoteBalances: map[satypes.SubaccountId]int64{
constants.Carl_Num0: constants.Carl_Num0_1BTC_Short_100000USD.GetUsdcPosition().Int64(),
constants.Dave_Num0: constants.Dave_Num0_1BTC_Long_50000USD.GetUsdcPosition().Int64(),
},
expectedPerpetualPositions: map[satypes.SubaccountId][]*satypes.PerpetualPosition{
constants.Carl_Num0: constants.Carl_Num0_1BTC_Short_100000USD.GetPerpetualPositions(),
constants.Dave_Num0: constants.Dave_Num0_1BTC_Long_50000USD.GetPerpetualPositions(),
},
expectedError: types.ErrZeroFillDeleveragingForNonNegativeTncSubaccount,
},
"Fails with clob match for market in initializing mode": {
perpetuals: []*perptypes.Perpetual{
&constants.BtcUsd_100PercentMarginRequirement,
Expand Down Expand Up @@ -1517,7 +1737,7 @@ func TestProcessProposerOperations(t *testing.T) {
constants.ClobPair_Btc_Final_Settlement,
},
subaccounts: []satypes.Subaccount{
// liquidatable: MMR = $5000, TNC = $499
// liquidatable: MMR = $5000, TNC = -$1.
constants.Carl_Num0_1BTC_Short_50499USD,
constants.Dave_Num0_1BTC_Long_50000USD,
},
Expand Down Expand Up @@ -2166,6 +2386,16 @@ func runProcessProposerOperationsTestCase(
require.Equal(t, fillAmount, actualFillAmount)
}

// Verify the negative TNC subaccount seen block.
seenNegativeTncSubaccountBlock, exists := ks.SubaccountsKeeper.GetNegativeTncSubaccountSeenAtBlock(ctx)
if tc.expectedNegativeTncSubaccountSeen {
require.True(t, exists)
require.Equal(t, uint32(ctx.BlockHeight()), seenNegativeTncSubaccountBlock)
} else {
require.False(t, exists)
require.Equal(t, uint32(0), seenNegativeTncSubaccountBlock)
}

mockIndexerEventManager.AssertExpectations(t)

// TODO(CLOB-230) Add more assertions.
Expand Down
11 changes: 6 additions & 5 deletions protocol/x/clob/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,7 @@ var (
1015,
"Invalid delta base and/or quote quantums for insurance fund delta calculation",
)
ErrEmptyDeleveragingFills = errorsmod.Register(
ModuleName,
1016,
"Deleveraging fills length must be greater than 0",
)
// TODO: Should the error code be skipped or re-assigned?
ErrDeleveragingAgainstSelf = errorsmod.Register(
ModuleName,
1017,
Expand Down Expand Up @@ -444,6 +440,11 @@ var (
4007,
"Order Removal reason is invalid",
)
ErrZeroFillDeleveragingForNonNegativeTncSubaccount = errorsmod.Register(
ModuleName,
4008,
"Zero-fill deleveraging operation included in block for non-negative TNC subaccount",
)

// Block rate limit errors.
ErrInvalidBlockRateLimitConfig = errorsmod.Register(
Expand Down
4 changes: 4 additions & 0 deletions protocol/x/clob/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ type SubaccountsKeeper interface {
successPerUpdate []satypes.UpdateResult,
err error,
)
SetNegativeTncSubaccountSeenAtBlock(
ctx sdk.Context,
blockHeight uint32,
)
TransferFeesToFeeCollectorModule(
ctx sdk.Context,
assetId uint32,
Expand Down
5 changes: 1 addition & 4 deletions protocol/x/clob/types/match_perpetual_deleveraging.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
// Validate performs stateless validation on a `MatchPerpetualDeleveraging` object.
// It checks the following conditions to be true:
// - Validation for all subaccount Ids
// - length of fills to be greater than zero
// - For each fill, fill amount must be greater than zero
// - Subaccount ids in fills are all unique
// - Subaccount ids in fills cannot be the same as the liquidated subaccount id
Expand All @@ -17,10 +16,8 @@ func (match *MatchPerpetualDeleveraging) Validate() error {
return err
}

// Note that zero-fill deleveraging operations are valid, iff the subaccount is negative equity.
fills := match.GetFills()
if len(fills) == 0 {
return ErrEmptyDeleveragingFills
}
seenOffsettingSubacountIds := map[satypes.SubaccountId]struct{}{}
for _, fill := range fills {
offsettingSubaccountId := fill.GetOffsettingSubaccountId()
Expand Down
2 changes: 1 addition & 1 deletion protocol/x/clob/types/match_perpetual_deleveraging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestPerformStatelessMatchPerpetualDeleveragingValidation(t *testing.T) {
PerpetualId: constants.ClobPair_Eth.MustGetPerpetualId(),
Fills: []types.MatchPerpetualDeleveraging_Fill{},
},
expectedError: types.ErrEmptyDeleveragingFills,
expectedError: nil,
},
"Deleveraging fill subaccount id the same as liquidation subaccount id": {
match: types.MatchPerpetualDeleveraging{
Expand Down
2 changes: 1 addition & 1 deletion protocol/x/clob/types/message_proposed_operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ func TestValidateAndTransformRawOperations(t *testing.T) {
},
),
},
expectedError: types.ErrEmptyDeleveragingFills,
expectedError: nil,
},

// Tests for byte functionality
Expand Down

0 comments on commit 925b3b3

Please sign in to comment.