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-1028] allow settlement at oracle price in ProcessDeleveraging #835

Merged
merged 7 commits into from
Dec 6, 2023
Merged
Changes from 1 commit
Commits
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
Next Next commit
add functionality in ProcessDeleveraging to allow settlement at oracl…
…e price instead of bankruptcy price
jakob-dydx committed Dec 4, 2023
commit 64598c868be7464644a86e924d60211a9a1318fb
42 changes: 27 additions & 15 deletions protocol/x/clob/keeper/deleveraging.go
Original file line number Diff line number Diff line change
@@ -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,
),
),
211 changes: 209 additions & 2 deletions protocol/x/clob/keeper/deleveraging_test.go
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions protocol/x/clob/keeper/process_operations.go
Original file line number Diff line number Diff line change
@@ -665,6 +665,7 @@ func (k Keeper) PersistMatchDeleveragingToState(
fill.OffsettingSubaccountId,
perpetualId,
deltaQuantums,
false,
); err != nil {
return errorsmod.Wrapf(
types.ErrInvalidDeleveragingFill,