Skip to content

Commit

Permalink
next tick deleveraging
Browse files Browse the repository at this point in the history
  • Loading branch information
jayy04 committed May 25, 2024
1 parent e2b8cb8 commit a7b0d6f
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 37 deletions.
1 change: 1 addition & 0 deletions .github/workflows/protocol-build-and-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on: # yamllint disable-line rule:truthy
- main
- 'release/protocol/v[0-9]+.[0-9]+.x' # e.g. release/protocol/v0.1.x
- 'release/protocol/v[0-9]+.x' # e.g. release/protocol/v1.x
- 'jy/heap'

jobs:
build-and-push-dev:
Expand Down
34 changes: 20 additions & 14 deletions protocol/x/clob/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ func EndBlocker(
// Prune any rate limiting information that is no longer relevant.
keeper.PruneRateLimits(ctx)

keeper.NextTickDeleverage(ctx)

// Emit relevant metrics at the end of every block.
metrics.SetGauge(
metrics.InsuranceFundBalance,
Expand Down Expand Up @@ -219,30 +221,34 @@ func PrepareCheckState(

// 6. Get all potentially liquidatable subaccount IDs and attempt to liquidate them.
liquidatableSubaccountIds := keeper.DaemonLiquidationInfo.GetLiquidatableSubaccountIds()
subaccountsToDeleverage, err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, liquidatableSubaccountIds)
_, err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, liquidatableSubaccountIds)
if err != nil {
panic(err)
}

// Add subaccounts with open positions in final settlement markets to the slice of subaccounts/perps
// to be deleveraged.
subaccountsToDeleverage = append(
subaccountsToDeleverage,
keeper.GetSubaccountsWithPositionsInFinalSettlementMarkets(ctx)...,
)

// 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)
}
// subaccountsToDeleverage = append(
// subaccountsToDeleverage,
// keeper.GetSubaccountsWithPositionsInFinalSettlementMarkets(ctx)...,
// )

// // 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)
// }

// 8. Gate withdrawals by inserting a zero-fill deleveraging operation into the operations queue if any
// of the negative TNC subaccounts still have negative TNC after liquidations and deleveraging steps.
negativeTncSubaccountIds := keeper.DaemonLiquidationInfo.GetNegativeTncSubaccountIds()
if err := keeper.GateWithdrawalsIfNegativeTncSubaccountSeen(ctx, negativeTncSubaccountIds); err != nil {
panic(err)
if len(negativeTncSubaccountIds) > 0 {
log.ErrorLog(ctx, "Found negative TNC subaccounts with next tick deleveraging")
}
// if err := keeper.GateWithdrawalsIfNegativeTncSubaccountSeen(ctx, negativeTncSubaccountIds); err != nil {
// panic(err)
// }

// Send all off-chain Indexer events
keeper.SendOffchainMessages(offchainUpdates, nil, metrics.SendPrepareCheckStateOffchainUpdates)
Expand Down
120 changes: 120 additions & 0 deletions protocol/x/clob/keeper/deleveraging.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ import (
satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
)

func (k Keeper) NextTickDeleverage(
ctx sdk.Context,
) {
negativeTncSubaccounts := k.subaccountsKeeper.GetAllNegativeTncSubaccounts(ctx)

for _, subaccountId := range negativeTncSubaccounts {
k.DeleverageEntireSubaccount(ctx, subaccountId)
}
}

func (k Keeper) DeleverageEntireSubaccount(
ctx sdk.Context,
subaccountId satypes.SubaccountId,
) {
subaccount := k.subaccountsKeeper.GetSubaccount(ctx, subaccountId)
for _, position := range subaccount.PerpetualPositions {
k.OffsetSubaccountPerpetualPositionV2(ctx, subaccountId, position.PerpetualId)
}
}

// MaybeDeleverageSubaccount is the main entry point to deleverage a subaccount. It attempts to find positions
// on the opposite side of deltaQuantums and use them to offset the liquidated subaccount's position at
// the bankruptcy price of the liquidated position.
Expand Down Expand Up @@ -289,6 +309,106 @@ func (k Keeper) IsValidInsuranceFundDelta(ctx sdk.Context, insuranceFundDelta *b
return new(big.Int).Add(currentInsuranceFundBalance, insuranceFundDelta).Sign() >= 0
}

func (k Keeper) OffsetSubaccountPerpetualPositionV2(
ctx sdk.Context,
liquidatedSubaccountId satypes.SubaccountId,
perpetualId uint32,
) {
liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId)
position, exists := liquidatedSubaccount.GetPerpetualPositionForId(perpetualId)
if !exists {
log.ErrorLog(
ctx,
"Failed to find position for perpetual in liquidated subaccount",
"perpetualId", perpetualId,
"liquidatedSubaccount", liquidatedSubaccount,
)
return
}
deltaQuantumsRemaining := new(big.Int).Neg(position.GetBigQuantums())

// Find subaccounts with open positions on the opposite side of the liquidated subaccount.
var offsettingSide satypes.PositionSide
if deltaQuantumsRemaining.Sign() == -1 {
offsettingSide = satypes.Short
} else {
offsettingSide = satypes.Long
}
subaccountsWithOpenPositions := k.subaccountsKeeper.GetSubaccountsWithOpenPositionsOnSide(
ctx,
perpetualId,
offsettingSide,
)

for index := 0; index < len(subaccountsWithOpenPositions) && deltaQuantumsRemaining.Sign() != 0; index++ {
subaccountId := subaccountsWithOpenPositions[index]

offsettingSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, subaccountId)
offsettingPosition, _ := offsettingSubaccount.GetPerpetualPositionForId(perpetualId)
bigOffsettingPositionQuantums := offsettingPosition.GetBigQuantums()

// Skip subaccounts that do not have a position in the opposite direction as the liquidated subaccount.
if deltaQuantumsRemaining.Sign() != bigOffsettingPositionQuantums.Sign() {
continue
}

// TODO(DEC-1495): Determine max amount to offset per offsetting subaccount.
var deltaBaseQuantums *big.Int
if deltaQuantumsRemaining.CmpAbs(bigOffsettingPositionQuantums) > 0 {
deltaBaseQuantums = new(big.Int).Set(bigOffsettingPositionQuantums)
} else {
deltaBaseQuantums = new(big.Int).Set(deltaQuantumsRemaining)
}

// Fetch delta quote quantums. Calculated at bankruptcy price for standard
// deleveraging and at oracle price for final settlement deleveraging.
deltaQuoteQuantums, err := k.getDeleveragingQuoteQuantumsDelta(
ctx,
perpetualId,
liquidatedSubaccountId,
deltaBaseQuantums,
false,
)
if err != nil {
panic("failed to get deleveraging quote quantums delta")
}

// Try to process the deleveraging operation for both subaccounts.
if err := k.ProcessDeleveraging(
ctx,
liquidatedSubaccountId,
*offsettingSubaccount.Id,
perpetualId,
deltaBaseQuantums,
deltaQuoteQuantums,
); err == nil {
// Update the remaining liquidatable quantums.
deltaQuantumsRemaining.Sub(deltaQuantumsRemaining, deltaBaseQuantums)
} else if errors.Is(err, types.ErrInvalidPerpetualPositionSizeDelta) {
panic(
fmt.Sprintf(
"Invalid perpetual position size delta when processing deleveraging. error: %v",
err,
),
)
} else {
// If an error is returned, it's likely because the subaccounts' bankruptcy prices do not overlap.
// TODO(CLOB-75): Support deleveraging subaccounts with non overlapping bankruptcy prices.
liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId)
offsettingSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, *offsettingSubaccount.Id)
log.ErrorLog(ctx, "Encountered error when processing deleveraging",
err,
"blockHeight", ctx.BlockHeight(),
"checkTx", ctx.IsCheckTx(),
"perpetualId", perpetualId,
"deltaBaseQuantums", deltaBaseQuantums,
"liquidatedSubaccount", liquidatedSubaccount,
"offsettingSubaccount", offsettingSubaccount,
)
}
}
}

// OffsetSubaccountPerpetualPosition iterates over all subaccounts and use those with positions
// on the opposite side to offset the liquidated subaccount's position by `deltaQuantumsTotal`.
//
Expand Down
8 changes: 8 additions & 0 deletions protocol/x/clob/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ type SubaccountsKeeper interface {
ctx sdk.Context,
perpetualId uint32,
) (sdk.AccAddress, error)
GetAllNegativeTncSubaccounts(
ctx sdk.Context,
) []satypes.SubaccountId
GetSubaccountsWithOpenPositionsOnSide(
ctx sdk.Context,
perpetualId uint32,
side satypes.PositionSide,
) []satypes.SubaccountId
}

type AssetsKeeper interface {
Expand Down
104 changes: 96 additions & 8 deletions protocol/x/subaccounts/keeper/safety_heap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,97 @@ package keeper

import (
"cosmossdk.io/store/prefix"
storetypes "cosmossdk.io/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/dydxprotocol/v4-chain/protocol/lib"
"github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
)

// TODO: optimize this
func (k Keeper) GetSubaccountsWithOpenPositionsOnSide(
ctx sdk.Context,
perpetualId uint32,
side types.PositionSide,
) []types.SubaccountId {
store := k.GetSafetyHeapStore(ctx, perpetualId, side)

iterator := storetypes.KVStoreReversePrefixIterator(store, []byte{})
defer iterator.Close()

result := make([]types.SubaccountId, 0)
for ; iterator.Valid(); iterator.Next() {
var val types.SubaccountId
k.cdc.MustUnmarshal(iterator.Value(), &val)
result = append(result, val)
}
return result
}

func (k Keeper) GetAllNegativeTncSubaccounts(
ctx sdk.Context,
) []types.SubaccountId {
perpetuals := k.perpetualsKeeper.GetAllPerpetuals(ctx)
negativeTncSubaccounts := make(map[types.SubaccountId]bool)

for _, perpetual := range perpetuals {
for _, side := range []types.PositionSide{types.Long, types.Short} {
store := k.GetSafetyHeapStore(ctx, perpetual.GetId(), side)
subaccounts := k.GetNegativeTncSubaccounts(ctx, store, 0)

for _, subaccountId := range subaccounts {
negativeTncSubaccounts[subaccountId] = true
}
}
}

sortedSubaccountIds := lib.GetSortedKeys[types.SortedSubaccountIds](negativeTncSubaccounts)
return sortedSubaccountIds
}

func (k Keeper) GetNegativeTncSubaccounts(
ctx sdk.Context,
store prefix.Store,
index uint32,
) []types.SubaccountId {
result := []types.SubaccountId{}

subaccountId, found := k.GetSubaccountAtIndex(store, index)
if !found {
return []types.SubaccountId{}
}

settledUpdates, _, err := k.getSettledUpdates(
ctx,
[]types.Update{
{
SubaccountId: subaccountId,
},
},
true,
)
if err != nil {
panic(types.ErrFailedToGetNegativeTncSubaccounts)
}

tnc, _, _, err :=
k.internalGetNetCollateralAndMarginRequirements(
ctx,
settledUpdates[0],
)
if err != nil {
panic(types.ErrFailedToGetNegativeTncSubaccounts)
}

if tnc.Sign() == -1 {
result = append(result, subaccountId)

// Recursively get the negative TNC subaccounts for left and right children.
result = append(result, k.GetNegativeTncSubaccounts(ctx, store, 2*index+1)...)
result = append(result, k.GetNegativeTncSubaccounts(ctx, store, 2*index+2)...)
}
return result
}

func (k Keeper) UpdateSafetyHeap(
ctx sdk.Context,
subaccount types.Subaccount,
Expand All @@ -15,24 +102,25 @@ func (k Keeper) UpdateSafetyHeap(

// TODO: optimize this
for _, position := range oldSubaccount.PerpetualPositions {
var side PositionSide
var side types.PositionSide
if position.Quantums.BigInt().Sign() == 1 {
side = Long
side = types.Long
} else {
side = Short
side = types.Short
}

store := k.GetSafetyHeapStore(ctx, position.PerpetualId, side)
index := k.MustGetSubaccountHeapIndex(store, *subaccountId)
_, _ = k.RemoveElementAtIndex(ctx, store, index)
if index, found := k.GetSubaccountHeapIndex(store, *subaccountId); found {
_, _ = k.RemoveElementAtIndex(ctx, store, index)
}
}

for _, position := range subaccount.PerpetualPositions {
var side PositionSide
var side types.PositionSide
if position.Quantums.BigInt().Sign() == 1 {
side = Long
side = types.Long
} else {
side = Short
side = types.Short
}

store := k.GetSafetyHeapStore(ctx, position.PerpetualId, side)
Expand Down
31 changes: 18 additions & 13 deletions protocol/x/subaccounts/keeper/safety_heap_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,6 @@ import (
"github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
)

type PositionSide uint

const (
Long PositionSide = iota
Short
)

// AppendToLast inserts a subaccount into the safety heap.
func (k Keeper) AppendToLast(
store prefix.Store,
Expand Down Expand Up @@ -102,19 +95,31 @@ func (k Keeper) MustGetSubaccountHeapIndex(
subaccountId types.SubaccountId,
) (
heapIndex uint32,
) {
heapIndex, found := k.GetSubaccountHeapIndex(store, subaccountId)
if !found {
panic(types.ErrSafetyHeapSubaccountIndexNotFound)
}
return heapIndex
}

// GetSubaccountHeapIndex returns the heap index of the subaccount.
func (k Keeper) GetSubaccountHeapIndex(
store prefix.Store,
subaccountId types.SubaccountId,
) (
heapIndex uint32,
found bool,
) {
key := subaccountId.ToStateKey()

index := gogotypes.UInt32Value{Value: 0}
b := store.Get(key)

if b == nil {
panic(types.ErrSafetyHeapSubaccountNotFoundAtIndex)
if b != nil {
k.cdc.MustUnmarshal(b, &index)
}

k.cdc.MustUnmarshal(b, &index)

return index.Value
return index.Value, b != nil
}

// SetSubaccountHeapIndex sets the heap index of the subaccount.
Expand Down
Loading

0 comments on commit a7b0d6f

Please sign in to comment.