Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CLOB-1017] final settlement deleveraging step in PrepareCheckState #848

Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
64598c8
add functionality in ProcessDeleveraging to allow settlement at oracl…
jakob-dydx Dec 4, 2023
81bd4b7
Merge branch 'main' into jakob-dydx/oracle-price-deleveraging
jakob-dydx Dec 5, 2023
b2e6b2e
pull deltaQuoteQuantums out of ProcessDeleveraging
jakob-dydx Dec 5, 2023
f81ad94
fix lint
jakob-dydx Dec 5, 2023
25bcb31
fix issue with test
jakob-dydx Dec 6, 2023
64503ff
pr nits
jakob-dydx Dec 6, 2023
b469e2e
update getDeleveragingQuoteQuantumsDelta helper
jakob-dydx Dec 6, 2023
01e35c2
initial implementation of final settlement deleveraging
jakob-dydx Dec 6, 2023
073f8f6
add delivertx logic for deleveraging match
jakob-dydx Dec 6, 2023
16e0fd3
Merge branch 'main' into jakob-dydx/final-settlement-pcs
jakob-dydx Dec 6, 2023
e37c37f
update to have DeliverTx calculate price based off of IsFinalSettleme…
jakob-dydx Dec 7, 2023
1b9d06b
update delivertx validation
jakob-dydx Dec 7, 2023
22328c7
allow final settlement subaccounts to be deleveraged by regular delev…
jakob-dydx Dec 7, 2023
0418a25
remove superfluous comment
jakob-dydx Dec 7, 2023
7a52242
update err name
jakob-dydx Dec 7, 2023
9bde41c
fix import formatting
jakob-dydx Dec 7, 2023
d0b741c
lint
jakob-dydx Dec 7, 2023
81b1f44
begin adding tests
jakob-dydx Dec 7, 2023
d88cdbc
Merge branch 'jakob-dydx/final-settlement-endblocker' into jakob-dydx…
jakob-dydx Dec 7, 2023
e0efd39
update logic so that isFinalSettlement flag on operation is used
jakob-dydx Dec 7, 2023
67b63bb
re-use helper function
jakob-dydx Dec 8, 2023
136248e
pr nits, redefine CanDeleverageSubaccount
jakob-dydx Dec 12, 2023
795123b
update tests for CanDeleverageSubaccount
jakob-dydx Dec 12, 2023
17c8e3e
split up PCS steps 6 and 7 to be for liquidations and then for deleve…
jakob-dydx Dec 12, 2023
a42b20c
merge from main
jakob-dydx Dec 12, 2023
ed2192f
update to use new subaccountOpenPositionInfo type
jakob-dydx Dec 12, 2023
0b46719
update tests
jakob-dydx Dec 12, 2023
1a52062
Merge branch 'jakob-dydx/final-settlement-endblocker' into jakob-dydx…
jakob-dydx Dec 12, 2023
07d0b9a
pr nits
jakob-dydx Dec 14, 2023
7b15853
lint
jakob-dydx Dec 14, 2023
c62914f
formatting
jakob-dydx Dec 14, 2023
ec22320
nits and tests
jakob-dydx Dec 19, 2023
0171a7f
more nits and comment updates
jakob-dydx Dec 19, 2023
7ce548b
[CLOB-1021] final settlement DeliverTx validation to block trading (#…
jakob-dydx Dec 19, 2023
2bddbbe
Merge branch 'jakob-dydx/final-settlement-endblocker' into jakob-dydx…
jakob-dydx Dec 20, 2023
82c56c0
merge from upstream
jakob-dydx Dec 20, 2023
6dae751
format
jakob-dydx Dec 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions protocol/indexer/events/deleveraging.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
4 changes: 3 additions & 1 deletion protocol/indexer/events/deleveraging_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -26,6 +27,7 @@ func TestNewDeleveragingEvent_Success(t *testing.T) {
fillAmount,
price,
isBuy,
false,
)
indexerLiquidatedSubaccountId := v1.SubaccountIdToIndexerSubaccountId(liquidatedSubaccountId)
indexerOffsettingSubaccountId := v1.SubaccountIdToIndexerSubaccountId(offsettingSubaccountId)
Expand Down
10 changes: 9 additions & 1 deletion protocol/x/clob/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
)

// BeginBlocker executes all ABCI BeginBlock logic respective to the clob module.
Expand Down Expand Up @@ -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)
jakob-dydx marked this conversation as resolved.
Show resolved Hide resolved
}

// 7. Deleverage subaccounts with open positions in final settlement markets.
jakob-dydx marked this conversation as resolved.
Show resolved Hide resolved
subaccountOpenPositionInfo := make(map[uint32]map[bool]map[satypes.SubaccountId]struct{})
if err := keeper.DeleverageSubaccountsInFinalSettlementMarkets(ctx, subaccountOpenPositionInfo, numDeleveragingAttempts); err != nil {
panic(err)
}

Expand Down
129 changes: 93 additions & 36 deletions protocol/x/clob/keeper/deleveraging.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
jakob-dydx marked this conversation as resolved.
Show resolved Hide resolved

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"
Expand All @@ -33,13 +32,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,
Expand Down Expand Up @@ -229,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)
Expand All @@ -251,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
}
Expand All @@ -264,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(
Expand All @@ -294,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),
Expand Down Expand Up @@ -338,29 +366,31 @@ 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

// If market is in final settlement and the subaccount has non-negative TNC, use the oracle price.
if isFinalSettlement {
hasNegativeTnc, err := k.CanDeleverageSubaccount(ctx, subaccountId)
if err != nil {
return new(big.Int), err
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
Expand Down Expand Up @@ -499,23 +529,50 @@ 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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pulled this onchain event out of ProcessDeleveraging so that we can easily set the isFinalSettlement flag on the event. This information is not known to the ProcessDeleveraging function.

This is similar to how ProcessSingleMatch works, the caller of the function is responsible for emitting the event instead of the event being in ProcessSingleMatch. This allows us to treat these "Process" functions solely as state-updating functions. Their callers are responsible for generating events.

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
}

// 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(
jakob-dydx marked this conversation as resolved.
Show resolved Hide resolved
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{}{}
}
}
jakob-dydx marked this conversation as resolved.
Show resolved Hide resolved

// Deleverage subaccounts with open positions in final settlement markets.
for perpetualId := range finalSettlementPerpetualIds {
for isBuy := range subaccountOpenPositionInfo[perpetualId] {
for subaccountId := range subaccountOpenPositionInfo[perpetualId][isBuy] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is going to introduce a ton of MEV (although deleveraging is currently not included in MEV calculation IIRC), but still, we should probably iterate over these deterministically.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that makes sense, can update this. Wasn't exactly sure of the types I'd be working with so just implemented the most naive approach at first

if numDeleveragingAttempts >= int(k.Flags.MaxDeleveragingAttemptsPerBlock) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use a separate flag to limit the number of final settlement deleveraging ops? This looks through all subaccounts with open positions in the market so it's bounded right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this, but opted for re-using the flag in the interest of respecting the current performance implications of proposing deleveraging operations.

When you say this operation is bounded, what do you mean by that? In theory there is no limit to the number of subaccounts that can havae open positions in a market so it isn't bounded

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
}
4 changes: 4 additions & 0 deletions protocol/x/clob/keeper/deleveraging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) {
satypes.BaseQuantums(fill.FillAmount),
satypes.BaseQuantums(bankruptcyPriceQuoteQuantums.Uint64()),
tc.deltaQuantums.Sign() > 0,
false,
),
),
).Return()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading