Skip to content

Commit a7b0d6f

Browse files
committed
next tick deleveraging
1 parent e2b8cb8 commit a7b0d6f

File tree

9 files changed

+273
-37
lines changed

9 files changed

+273
-37
lines changed

.github/workflows/protocol-build-and-push.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on: # yamllint disable-line rule:truthy
66
- main
77
- 'release/protocol/v[0-9]+.[0-9]+.x' # e.g. release/protocol/v0.1.x
88
- 'release/protocol/v[0-9]+.x' # e.g. release/protocol/v1.x
9+
- 'jy/heap'
910

1011
jobs:
1112
build-and-push-dev:

protocol/x/clob/abci.go

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ func EndBlocker(
125125
// Prune any rate limiting information that is no longer relevant.
126126
keeper.PruneRateLimits(ctx)
127127

128+
keeper.NextTickDeleverage(ctx)
129+
128130
// Emit relevant metrics at the end of every block.
129131
metrics.SetGauge(
130132
metrics.InsuranceFundBalance,
@@ -219,30 +221,34 @@ func PrepareCheckState(
219221

220222
// 6. Get all potentially liquidatable subaccount IDs and attempt to liquidate them.
221223
liquidatableSubaccountIds := keeper.DaemonLiquidationInfo.GetLiquidatableSubaccountIds()
222-
subaccountsToDeleverage, err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, liquidatableSubaccountIds)
224+
_, err := keeper.LiquidateSubaccountsAgainstOrderbook(ctx, liquidatableSubaccountIds)
223225
if err != nil {
224226
panic(err)
225227
}
228+
226229
// Add subaccounts with open positions in final settlement markets to the slice of subaccounts/perps
227230
// to be deleveraged.
228-
subaccountsToDeleverage = append(
229-
subaccountsToDeleverage,
230-
keeper.GetSubaccountsWithPositionsInFinalSettlementMarkets(ctx)...,
231-
)
232-
233-
// 7. Deleverage subaccounts.
234-
// TODO(CLOB-1052) - decouple steps 6 and 7 by using DaemonLiquidationInfo.NegativeTncSubaccounts
235-
// as the input for this function.
236-
if err := keeper.DeleverageSubaccounts(ctx, subaccountsToDeleverage); err != nil {
237-
panic(err)
238-
}
231+
// subaccountsToDeleverage = append(
232+
// subaccountsToDeleverage,
233+
// keeper.GetSubaccountsWithPositionsInFinalSettlementMarkets(ctx)...,
234+
// )
235+
236+
// // 7. Deleverage subaccounts.
237+
// // TODO(CLOB-1052) - decouple steps 6 and 7 by using DaemonLiquidationInfo.NegativeTncSubaccounts
238+
// // as the input for this function.
239+
// if err := keeper.DeleverageSubaccounts(ctx, subaccountsToDeleverage); err != nil {
240+
// panic(err)
241+
// }
239242

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

247253
// Send all off-chain Indexer events
248254
keeper.SendOffchainMessages(offchainUpdates, nil, metrics.SendPrepareCheckStateOffchainUpdates)

protocol/x/clob/keeper/deleveraging.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@ import (
2222
satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
2323
)
2424

25+
func (k Keeper) NextTickDeleverage(
26+
ctx sdk.Context,
27+
) {
28+
negativeTncSubaccounts := k.subaccountsKeeper.GetAllNegativeTncSubaccounts(ctx)
29+
30+
for _, subaccountId := range negativeTncSubaccounts {
31+
k.DeleverageEntireSubaccount(ctx, subaccountId)
32+
}
33+
}
34+
35+
func (k Keeper) DeleverageEntireSubaccount(
36+
ctx sdk.Context,
37+
subaccountId satypes.SubaccountId,
38+
) {
39+
subaccount := k.subaccountsKeeper.GetSubaccount(ctx, subaccountId)
40+
for _, position := range subaccount.PerpetualPositions {
41+
k.OffsetSubaccountPerpetualPositionV2(ctx, subaccountId, position.PerpetualId)
42+
}
43+
}
44+
2545
// MaybeDeleverageSubaccount is the main entry point to deleverage a subaccount. It attempts to find positions
2646
// on the opposite side of deltaQuantums and use them to offset the liquidated subaccount's position at
2747
// the bankruptcy price of the liquidated position.
@@ -289,6 +309,106 @@ func (k Keeper) IsValidInsuranceFundDelta(ctx sdk.Context, insuranceFundDelta *b
289309
return new(big.Int).Add(currentInsuranceFundBalance, insuranceFundDelta).Sign() >= 0
290310
}
291311

312+
func (k Keeper) OffsetSubaccountPerpetualPositionV2(
313+
ctx sdk.Context,
314+
liquidatedSubaccountId satypes.SubaccountId,
315+
perpetualId uint32,
316+
) {
317+
liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId)
318+
position, exists := liquidatedSubaccount.GetPerpetualPositionForId(perpetualId)
319+
if !exists {
320+
log.ErrorLog(
321+
ctx,
322+
"Failed to find position for perpetual in liquidated subaccount",
323+
"perpetualId", perpetualId,
324+
"liquidatedSubaccount", liquidatedSubaccount,
325+
)
326+
return
327+
}
328+
deltaQuantumsRemaining := new(big.Int).Neg(position.GetBigQuantums())
329+
330+
// Find subaccounts with open positions on the opposite side of the liquidated subaccount.
331+
var offsettingSide satypes.PositionSide
332+
if deltaQuantumsRemaining.Sign() == -1 {
333+
offsettingSide = satypes.Short
334+
} else {
335+
offsettingSide = satypes.Long
336+
}
337+
subaccountsWithOpenPositions := k.subaccountsKeeper.GetSubaccountsWithOpenPositionsOnSide(
338+
ctx,
339+
perpetualId,
340+
offsettingSide,
341+
)
342+
343+
for index := 0; index < len(subaccountsWithOpenPositions) && deltaQuantumsRemaining.Sign() != 0; index++ {
344+
subaccountId := subaccountsWithOpenPositions[index]
345+
346+
offsettingSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, subaccountId)
347+
offsettingPosition, _ := offsettingSubaccount.GetPerpetualPositionForId(perpetualId)
348+
bigOffsettingPositionQuantums := offsettingPosition.GetBigQuantums()
349+
350+
// Skip subaccounts that do not have a position in the opposite direction as the liquidated subaccount.
351+
if deltaQuantumsRemaining.Sign() != bigOffsettingPositionQuantums.Sign() {
352+
continue
353+
}
354+
355+
// TODO(DEC-1495): Determine max amount to offset per offsetting subaccount.
356+
var deltaBaseQuantums *big.Int
357+
if deltaQuantumsRemaining.CmpAbs(bigOffsettingPositionQuantums) > 0 {
358+
deltaBaseQuantums = new(big.Int).Set(bigOffsettingPositionQuantums)
359+
} else {
360+
deltaBaseQuantums = new(big.Int).Set(deltaQuantumsRemaining)
361+
}
362+
363+
// Fetch delta quote quantums. Calculated at bankruptcy price for standard
364+
// deleveraging and at oracle price for final settlement deleveraging.
365+
deltaQuoteQuantums, err := k.getDeleveragingQuoteQuantumsDelta(
366+
ctx,
367+
perpetualId,
368+
liquidatedSubaccountId,
369+
deltaBaseQuantums,
370+
false,
371+
)
372+
if err != nil {
373+
panic("failed to get deleveraging quote quantums delta")
374+
}
375+
376+
// Try to process the deleveraging operation for both subaccounts.
377+
if err := k.ProcessDeleveraging(
378+
ctx,
379+
liquidatedSubaccountId,
380+
*offsettingSubaccount.Id,
381+
perpetualId,
382+
deltaBaseQuantums,
383+
deltaQuoteQuantums,
384+
); err == nil {
385+
// Update the remaining liquidatable quantums.
386+
deltaQuantumsRemaining.Sub(deltaQuantumsRemaining, deltaBaseQuantums)
387+
} else if errors.Is(err, types.ErrInvalidPerpetualPositionSizeDelta) {
388+
panic(
389+
fmt.Sprintf(
390+
"Invalid perpetual position size delta when processing deleveraging. error: %v",
391+
err,
392+
),
393+
)
394+
} else {
395+
// If an error is returned, it's likely because the subaccounts' bankruptcy prices do not overlap.
396+
// TODO(CLOB-75): Support deleveraging subaccounts with non overlapping bankruptcy prices.
397+
liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId)
398+
offsettingSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, *offsettingSubaccount.Id)
399+
log.ErrorLog(ctx, "Encountered error when processing deleveraging",
400+
err,
401+
"blockHeight", ctx.BlockHeight(),
402+
"checkTx", ctx.IsCheckTx(),
403+
"perpetualId", perpetualId,
404+
"deltaBaseQuantums", deltaBaseQuantums,
405+
"liquidatedSubaccount", liquidatedSubaccount,
406+
"offsettingSubaccount", offsettingSubaccount,
407+
)
408+
}
409+
}
410+
}
411+
292412
// OffsetSubaccountPerpetualPosition iterates over all subaccounts and use those with positions
293413
// on the opposite side to offset the liquidated subaccount's position by `deltaQuantumsTotal`.
294414
//

protocol/x/clob/types/expected_keepers.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ type SubaccountsKeeper interface {
7474
ctx sdk.Context,
7575
perpetualId uint32,
7676
) (sdk.AccAddress, error)
77+
GetAllNegativeTncSubaccounts(
78+
ctx sdk.Context,
79+
) []satypes.SubaccountId
80+
GetSubaccountsWithOpenPositionsOnSide(
81+
ctx sdk.Context,
82+
perpetualId uint32,
83+
side satypes.PositionSide,
84+
) []satypes.SubaccountId
7785
}
7886

7987
type AssetsKeeper interface {

protocol/x/subaccounts/keeper/safety_heap.go

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,97 @@ package keeper
22

33
import (
44
"cosmossdk.io/store/prefix"
5+
storetypes "cosmossdk.io/store/types"
56
sdk "github.com/cosmos/cosmos-sdk/types"
7+
"github.com/dydxprotocol/v4-chain/protocol/lib"
68
"github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
79
)
810

11+
// TODO: optimize this
12+
func (k Keeper) GetSubaccountsWithOpenPositionsOnSide(
13+
ctx sdk.Context,
14+
perpetualId uint32,
15+
side types.PositionSide,
16+
) []types.SubaccountId {
17+
store := k.GetSafetyHeapStore(ctx, perpetualId, side)
18+
19+
iterator := storetypes.KVStoreReversePrefixIterator(store, []byte{})
20+
defer iterator.Close()
21+
22+
result := make([]types.SubaccountId, 0)
23+
for ; iterator.Valid(); iterator.Next() {
24+
var val types.SubaccountId
25+
k.cdc.MustUnmarshal(iterator.Value(), &val)
26+
result = append(result, val)
27+
}
28+
return result
29+
}
30+
31+
func (k Keeper) GetAllNegativeTncSubaccounts(
32+
ctx sdk.Context,
33+
) []types.SubaccountId {
34+
perpetuals := k.perpetualsKeeper.GetAllPerpetuals(ctx)
35+
negativeTncSubaccounts := make(map[types.SubaccountId]bool)
36+
37+
for _, perpetual := range perpetuals {
38+
for _, side := range []types.PositionSide{types.Long, types.Short} {
39+
store := k.GetSafetyHeapStore(ctx, perpetual.GetId(), side)
40+
subaccounts := k.GetNegativeTncSubaccounts(ctx, store, 0)
41+
42+
for _, subaccountId := range subaccounts {
43+
negativeTncSubaccounts[subaccountId] = true
44+
}
45+
}
46+
}
47+
48+
sortedSubaccountIds := lib.GetSortedKeys[types.SortedSubaccountIds](negativeTncSubaccounts)
49+
return sortedSubaccountIds
50+
}
51+
52+
func (k Keeper) GetNegativeTncSubaccounts(
53+
ctx sdk.Context,
54+
store prefix.Store,
55+
index uint32,
56+
) []types.SubaccountId {
57+
result := []types.SubaccountId{}
58+
59+
subaccountId, found := k.GetSubaccountAtIndex(store, index)
60+
if !found {
61+
return []types.SubaccountId{}
62+
}
63+
64+
settledUpdates, _, err := k.getSettledUpdates(
65+
ctx,
66+
[]types.Update{
67+
{
68+
SubaccountId: subaccountId,
69+
},
70+
},
71+
true,
72+
)
73+
if err != nil {
74+
panic(types.ErrFailedToGetNegativeTncSubaccounts)
75+
}
76+
77+
tnc, _, _, err :=
78+
k.internalGetNetCollateralAndMarginRequirements(
79+
ctx,
80+
settledUpdates[0],
81+
)
82+
if err != nil {
83+
panic(types.ErrFailedToGetNegativeTncSubaccounts)
84+
}
85+
86+
if tnc.Sign() == -1 {
87+
result = append(result, subaccountId)
88+
89+
// Recursively get the negative TNC subaccounts for left and right children.
90+
result = append(result, k.GetNegativeTncSubaccounts(ctx, store, 2*index+1)...)
91+
result = append(result, k.GetNegativeTncSubaccounts(ctx, store, 2*index+2)...)
92+
}
93+
return result
94+
}
95+
996
func (k Keeper) UpdateSafetyHeap(
1097
ctx sdk.Context,
1198
subaccount types.Subaccount,
@@ -15,24 +102,25 @@ func (k Keeper) UpdateSafetyHeap(
15102

16103
// TODO: optimize this
17104
for _, position := range oldSubaccount.PerpetualPositions {
18-
var side PositionSide
105+
var side types.PositionSide
19106
if position.Quantums.BigInt().Sign() == 1 {
20-
side = Long
107+
side = types.Long
21108
} else {
22-
side = Short
109+
side = types.Short
23110
}
24111

25112
store := k.GetSafetyHeapStore(ctx, position.PerpetualId, side)
26-
index := k.MustGetSubaccountHeapIndex(store, *subaccountId)
27-
_, _ = k.RemoveElementAtIndex(ctx, store, index)
113+
if index, found := k.GetSubaccountHeapIndex(store, *subaccountId); found {
114+
_, _ = k.RemoveElementAtIndex(ctx, store, index)
115+
}
28116
}
29117

30118
for _, position := range subaccount.PerpetualPositions {
31-
var side PositionSide
119+
var side types.PositionSide
32120
if position.Quantums.BigInt().Sign() == 1 {
33-
side = Long
121+
side = types.Long
34122
} else {
35-
side = Short
123+
side = types.Short
36124
}
37125

38126
store := k.GetSafetyHeapStore(ctx, position.PerpetualId, side)

protocol/x/subaccounts/keeper/safety_heap_state.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,6 @@ import (
77
"github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
88
)
99

10-
type PositionSide uint
11-
12-
const (
13-
Long PositionSide = iota
14-
Short
15-
)
16-
1710
// AppendToLast inserts a subaccount into the safety heap.
1811
func (k Keeper) AppendToLast(
1912
store prefix.Store,
@@ -102,19 +95,31 @@ func (k Keeper) MustGetSubaccountHeapIndex(
10295
subaccountId types.SubaccountId,
10396
) (
10497
heapIndex uint32,
98+
) {
99+
heapIndex, found := k.GetSubaccountHeapIndex(store, subaccountId)
100+
if !found {
101+
panic(types.ErrSafetyHeapSubaccountIndexNotFound)
102+
}
103+
return heapIndex
104+
}
105+
106+
// GetSubaccountHeapIndex returns the heap index of the subaccount.
107+
func (k Keeper) GetSubaccountHeapIndex(
108+
store prefix.Store,
109+
subaccountId types.SubaccountId,
110+
) (
111+
heapIndex uint32,
112+
found bool,
105113
) {
106114
key := subaccountId.ToStateKey()
107115

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

111-
if b == nil {
112-
panic(types.ErrSafetyHeapSubaccountNotFoundAtIndex)
119+
if b != nil {
120+
k.cdc.MustUnmarshal(b, &index)
113121
}
114-
115-
k.cdc.MustUnmarshal(b, &index)
116-
117-
return index.Value
122+
return index.Value, b != nil
118123
}
119124

120125
// SetSubaccountHeapIndex sets the heap index of the subaccount.

0 commit comments

Comments
 (0)