Skip to content

Commit

Permalink
fix(perp): Liquidate calls proper Liquidation method (#676)
Browse files Browse the repository at this point in the history
* Fix liquidate to select partial liquidations correctly

Also changed interface to not accept msg, and have msgServer break down the msg proto

* Add Liquidate unit test

* Refactor liquidate unit test

* Add full liquidation test

* Formatting

* Refactor tests

* Bump cosmos-sdk to v0.45.6

* Change badDebt to sdk.Int

* Add full liquidation tests with bad debt

* Bump go version to 1.18

* Bump workflows to 1.18

Co-authored-by: Mat-Cosmos <[email protected]>
  • Loading branch information
NibiruHeisenberg and matthiasmatt authored Jul 7, 2022
1 parent 060ab8f commit cca45e4
Show file tree
Hide file tree
Showing 7 changed files with 720 additions and 203 deletions.
2 changes: 1 addition & 1 deletion proto/perp/v1/state.proto
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ message PositionResp {
message LiquidateResp {
// Amount of bad debt created by the liquidation event
string bad_debt = 1 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false];

// Fee paid to the liquidator
Expand Down
85 changes: 38 additions & 47 deletions x/perp/keeper/liquidate.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package keeper

import (
"context"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/NibiruChain/nibiru/x/common"
Expand All @@ -12,51 +10,47 @@ import (

/* Liquidate allows to liquidate the trader position if the margin is below the
required margin maintenance ratio.
*/
func (k Keeper) Liquidate(
goCtx context.Context, msg *types.MsgLiquidate,
) (res *types.MsgLiquidateResponse, err error) {
// ------------- Liquidation Message Setup -------------

ctx := sdk.UnwrapSDKContext(goCtx)

// validate liquidator (msg.Sender)
liquidatorAddr, err := sdk.AccAddressFromBech32(msg.Sender)
if err != nil {
return res, err
}
// validate trader (msg.PositionOwner)
traderAddr, err := sdk.AccAddressFromBech32(msg.Trader)
if err != nil {
return res, err
}
args:
- liquidatorAddr: the liquidator who is executing the liquidation
- pair: the asset pair
- traderAddr: the trader who owns the position being liquidated
// validate pair
pair, err := common.NewAssetPair(msg.TokenPair)
if err != nil {
return res, err
}
ret:
- feeToLiquidator: the amount of coins given to the liquidator
- feeToFund: the amount of coins given to the ecosystem fund
- err: error
*/
func (k Keeper) Liquidate(
ctx sdk.Context,
liquidatorAddr sdk.AccAddress,
pair common.AssetPair,
traderAddr sdk.AccAddress,
) (feeToLiquidator sdk.Coin, feeToFund sdk.Coin, err error) {
err = k.requireVpool(ctx, pair)
if err != nil {
return res, err
return sdk.Coin{}, sdk.Coin{}, err
}

position, err := k.GetPosition(ctx, pair, traderAddr)
if err != nil {
return res, err
return sdk.Coin{}, sdk.Coin{}, err
}

marginRatio, err := k.GetMarginRatio(ctx, *position, types.MarginCalculationPriceOption_MAX_PNL)
marginRatio, err := k.GetMarginRatio(
ctx,
*position,
types.MarginCalculationPriceOption_MAX_PNL,
)
if err != nil {
return res, err
return sdk.Coin{}, sdk.Coin{}, err
}

if k.VpoolKeeper.IsOverSpreadLimit(ctx, pair) {
marginRatioBasedOnOracle, err := k.GetMarginRatio(
ctx, *position, types.MarginCalculationPriceOption_INDEX)
if err != nil {
return res, err
return sdk.Coin{}, sdk.Coin{}, err
}

marginRatio = sdk.MaxDec(marginRatio, marginRatioBasedOnOracle)
Expand All @@ -65,39 +59,36 @@ func (k Keeper) Liquidate(
params := k.GetParams(ctx)
err = requireMoreMarginRatio(marginRatio, params.MaintenanceMarginRatio, false)
if err != nil {
return res, types.ErrMarginHighEnough
return sdk.Coin{}, sdk.Coin{}, types.ErrMarginHighEnough
}

marginRatioBasedOnSpot, err := k.GetMarginRatio(
ctx, *position, types.MarginCalculationPriceOption_SPOT)
if err != nil {
return res, err
return sdk.Coin{}, sdk.Coin{}, err
}

var liquidationResponse types.LiquidateResp
if marginRatioBasedOnSpot.GTE(params.PartialLiquidationRatio) {
liquidationResponse, err = k.ExecuteFullLiquidation(ctx, liquidatorAddr, position)
} else {
if marginRatioBasedOnSpot.GTE(params.LiquidationFeeRatio) {
liquidationResponse, err = k.ExecutePartialLiquidation(ctx, liquidatorAddr, position)
} else {
liquidationResponse, err = k.ExecuteFullLiquidation(ctx, liquidatorAddr, position)
}
if err != nil {
return res, err
return sdk.Coin{}, sdk.Coin{}, err
}

feeToLiquidator := sdk.NewCoin(
feeToLiquidator = sdk.NewCoin(
pair.GetQuoteTokenDenom(),
liquidationResponse.FeeToLiquidator,
)

feeToEcosystemFund := sdk.NewCoin(
feeToFund = sdk.NewCoin(
pair.GetQuoteTokenDenom(),
liquidationResponse.FeeToPerpEcosystemFund,
)

return &types.MsgLiquidateResponse{
FeeToLiquidator: feeToLiquidator,
FeeToPerpEcosystemFund: feeToEcosystemFund,
}, nil
return feeToLiquidator, feeToFund, nil
}

/*
Expand Down Expand Up @@ -143,7 +134,7 @@ func (k Keeper) ExecuteFullLiquidation(
totalBadDebt = totalBadDebt.Add(feeToLiquidator.Sub(remainMargin))
remainMargin = sdk.ZeroDec()
} else {
// Otherwise, the remaining margin rest will be transferred to ecosystemFund
// Otherwise, the remaining margin will be transferred to ecosystemFund
remainMargin = remainMargin.Sub(feeToLiquidator)
}

Expand All @@ -164,7 +155,7 @@ func (k Keeper) ExecuteFullLiquidation(
}

liquidationResp = types.LiquidateResp{
BadDebt: totalBadDebt,
BadDebt: totalBadDebt.RoundInt(),
FeeToLiquidator: feeToLiquidator.RoundInt(),
FeeToPerpEcosystemFund: feeToPerpEcosystemFund.RoundInt(),
Liquidator: liquidator.String(),
Expand Down Expand Up @@ -244,7 +235,7 @@ func (k Keeper) distributeLiquidateRewards(
}
}

// Transfer fee from PerpEF to liquidator
// Transfer fee from vault to liquidator
feeToLiquidator := liquidateResp.FeeToLiquidator
if feeToLiquidator.IsPositive() {
err = k.Withdraw(ctx, pair.GetQuoteTokenDenom(), liquidator, feeToLiquidator)
Expand Down Expand Up @@ -309,7 +300,7 @@ func (k Keeper) ExecutePartialLiquidation(
feeToPerpEcosystemFund := liquidationFeeAmount.Sub(feeToLiquidator)

liquidationResponse := types.LiquidateResp{
BadDebt: sdk.ZeroDec(),
BadDebt: sdk.ZeroInt(),
FeeToLiquidator: feeToLiquidator.RoundInt(),
FeeToPerpEcosystemFund: feeToPerpEcosystemFund.RoundInt(),
Liquidator: liquidator.String(),
Expand All @@ -333,7 +324,7 @@ func (k Keeper) ExecutePartialLiquidation(
LiquidatorAddress: liquidator.String(),
FeeToLiquidator: sdk.NewCoin(currentPosition.Pair.GetQuoteTokenDenom(), feeToLiquidator.RoundInt()),
FeeToEcosystemFund: sdk.NewCoin(currentPosition.Pair.GetQuoteTokenDenom(), feeToPerpEcosystemFund.RoundInt()),
BadDebt: liquidationResponse.BadDebt,
BadDebt: liquidationResponse.BadDebt.ToDec(),
Margin: sdk.NewCoin(currentPosition.Pair.GetQuoteTokenDenom(), liquidationResponse.PositionResp.Position.Margin.RoundInt()),
PositionNotional: liquidationResponse.PositionResp.PositionNotional,
PositionSize: liquidationResponse.PositionResp.Position.Size_,
Expand Down
32 changes: 16 additions & 16 deletions x/perp/keeper/liquidate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func TestExecuteFullLiquidation(t *testing.T) {

t.Log("Liquidate the (entire) position")
liquidatorAddr := sample.AccAddress()
resp, err := nibiruApp.PerpKeeper.ExecuteFullLiquidation(ctx, liquidatorAddr, position)
liquidationResp, err := nibiruApp.PerpKeeper.ExecuteFullLiquidation(ctx, liquidatorAddr, position)
require.NoError(t, err)

t.Log("Check correctness of new position")
Expand All @@ -311,16 +311,16 @@ func TestExecuteFullLiquidation(t *testing.T) {
testutilevents.RequireHasTypedEvent(t, ctx, &types.PositionLiquidatedEvent{
Pair: tokenPair.String(),
TraderAddress: traderAddr.String(),
ExchangedQuoteAmount: resp.PositionResp.ExchangedNotionalValue,
ExchangedPositionSize: resp.PositionResp.ExchangedPositionSize,
ExchangedQuoteAmount: liquidationResp.PositionResp.ExchangedNotionalValue,
ExchangedPositionSize: liquidationResp.PositionResp.ExchangedPositionSize,
LiquidatorAddress: liquidatorAddr.String(),
FeeToLiquidator: sdk.NewCoin(tokenPair.GetQuoteTokenDenom(), resp.FeeToLiquidator),
FeeToEcosystemFund: sdk.NewCoin(tokenPair.GetQuoteTokenDenom(), resp.FeeToPerpEcosystemFund),
BadDebt: resp.BadDebt,
FeeToLiquidator: sdk.NewCoin(tokenPair.GetQuoteTokenDenom(), liquidationResp.FeeToLiquidator),
FeeToEcosystemFund: sdk.NewCoin(tokenPair.GetQuoteTokenDenom(), liquidationResp.FeeToPerpEcosystemFund),
BadDebt: liquidationResp.BadDebt.ToDec(),
Margin: sdk.NewCoin(tokenPair.GetQuoteTokenDenom(), sdk.ZeroInt()),
PositionNotional: resp.PositionResp.PositionNotional,
PositionNotional: liquidationResp.PositionResp.PositionNotional,
PositionSize: sdk.ZeroDec(),
UnrealizedPnl: resp.PositionResp.UnrealizedPnlAfter,
UnrealizedPnl: liquidationResp.PositionResp.UnrealizedPnlAfter,
MarkPrice: newMarkPrice,
BlockHeight: ctx.BlockHeight(),
BlockTimeMs: ctx.BlockTime().UnixMilli(),
Expand Down Expand Up @@ -564,7 +564,7 @@ func TestExecutePartialLiquidation(t *testing.T) {

t.Log("Liquidate the (partial) position")
liquidator := sample.AccAddress()
resp, err := nibiruApp.PerpKeeper.ExecutePartialLiquidation(ctx, liquidator, position)
liquidationResp, err := nibiruApp.PerpKeeper.ExecutePartialLiquidation(ctx, liquidator, position)
require.NoError(t, err)

t.Log("Check correctness of new position")
Expand Down Expand Up @@ -601,16 +601,16 @@ func TestExecutePartialLiquidation(t *testing.T) {
testutilevents.RequireHasTypedEvent(t, ctx, &types.PositionLiquidatedEvent{
Pair: tokenPair.String(),
TraderAddress: traderAddr.String(),
ExchangedQuoteAmount: resp.PositionResp.ExchangedNotionalValue,
ExchangedPositionSize: resp.PositionResp.ExchangedPositionSize,
ExchangedQuoteAmount: liquidationResp.PositionResp.ExchangedNotionalValue,
ExchangedPositionSize: liquidationResp.PositionResp.ExchangedPositionSize,
LiquidatorAddress: liquidator.String(),
FeeToLiquidator: sdk.NewCoin(tokenPair.GetQuoteTokenDenom(), resp.FeeToLiquidator),
FeeToEcosystemFund: sdk.NewCoin(tokenPair.GetQuoteTokenDenom(), resp.FeeToPerpEcosystemFund),
BadDebt: resp.BadDebt,
FeeToLiquidator: sdk.NewCoin(tokenPair.GetQuoteTokenDenom(), liquidationResp.FeeToLiquidator),
FeeToEcosystemFund: sdk.NewCoin(tokenPair.GetQuoteTokenDenom(), liquidationResp.FeeToPerpEcosystemFund),
BadDebt: liquidationResp.BadDebt.ToDec(),
Margin: sdk.NewCoin(tokenPair.GetQuoteTokenDenom(), newPosition.Margin.RoundInt()),
PositionNotional: resp.PositionResp.PositionNotional,
PositionNotional: liquidationResp.PositionResp.PositionNotional,
PositionSize: newPosition.Size_,
UnrealizedPnl: resp.PositionResp.UnrealizedPnlAfter,
UnrealizedPnl: liquidationResp.PositionResp.UnrealizedPnlAfter,
MarkPrice: newMarkPrice,
BlockHeight: ctx.BlockHeight(),
BlockTimeMs: ctx.BlockTime().UnixMilli(),
Expand Down
Loading

0 comments on commit cca45e4

Please sign in to comment.