Skip to content

Commit

Permalink
feat, test (perp): Liquidate (1/4) - 'ExecuteFullLiquidation' (#432)
Browse files Browse the repository at this point in the history
* feat: liquidate proto changes, new params, and new methods on perp interfaces

* fix: restore passing state

* refactor: coalesce errors to one location

* feat (liquidate.go): ExecuteFullLiquidation, distributeLiquidateRewards

* test: Check expected fee to liquidator

* test (liquidate_test.go): turn tests green with expectedPerpEFBalance

* test: Test_distributeLiquidateRewards

* typo correction

* typo correction

* refactor: replace panic(err) with require.NoError

* fix: ExecuteFullLiquidation

* feat (perp): Emit internal events when positionResp objects are returned

* linter

* fix, test: perp.go and margin.go tests pass again

* fix: settleposition test restored

* fix: calc_test.go, calc_unit_test.go

* test: liquidate_unit_test passing

* fix, refactor: passing margin_test, liquidate_test

* fix (clearing_house_test.go): Margin and MarginToVault should be sdk.Int, not sdk.Dec

* test, docs (liquidate_test.go): Check correctness of emitted events. Add docs for calculations

* refactor: require.EqualValues -> assert.EqualValues + more docs

* docs: small decription

* refactor: universal sdk.Decs

* verify event calls

* refactor: consistency b/w assert and require

* refactor: rename CalcFee -> CalcPerpTxFee

* refactor: rename CalcFee -> CalcPerpTxFee

* refactor:  Liquidate (0/4) - asserts, String() calls, and new params

* refactor: clean up old TODOs in clearing_house.go

* feat: add liquidateresp as a proto type

* feat: Remove duplicate sdk.AccAddress transform

* Update x/perp/keeper/liquidate_test.go

Co-authored-by: Walter White <[email protected]>

* fix: added check to please linter

* fix: all remsining margin goes to ef fund; might break tests

* Add memStoreKey to perp keeper

* Fix fee to perpEF and return liquidation resp

* Add unit tests with mock for ExecuteFullLiquidation

* Change xxx:yyy to BTC:NUSD

* Rename typo

* Refactor ExecuteFullLiquidation test scenario

* Refactor test more

* Refactor tests

* Add early return in getPositionNotionalAndUnrealizedPnl

* Refactor liquidation unit tests

* Add short position test cases

* Fix sender of funds to vault instead of PerpEF

* fix: Resolve missing store issue on NibiruApp.PerpKeeper

* Run executeliquidate from Liquidate

* fix (liquidate_unit_test.go): Add mock for realized bad debt

* refactor: cleanup

* refactor: cleanup

* Add realizeBadDebt method

* Add method comment

* Add realize bad debt calculation

Co-authored-by: AgentSmithMatrix <[email protected]>
Co-authored-by: MD <[email protected]>
Co-authored-by: Mat-Cosmos <[email protected]>
Co-authored-by: Walter White <[email protected]>
Co-authored-by: Walter White <[email protected]>
  • Loading branch information
6 people authored May 22, 2022
1 parent 883dceb commit f490773
Show file tree
Hide file tree
Showing 11 changed files with 1,596 additions and 60 deletions.
36 changes: 34 additions & 2 deletions proto/perp/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,20 @@ service Msg {
option (google.api.http).post = "/nibiru/perp/add_margin";
}

rpc OpenPosition(MsgOpenPosition) returns (MsgOpenPositionResponse) {}
/* Liquidate is a transaction that allows the caller to fully or partially
liquidate an existing position. */
// rpc Liquidate(MsgLiquidate) returns (MsgLiquidateResponse) {
// option (google.api.http).post = "/nibiru/perp/liquidate";
// }

rpc OpenPosition(MsgOpenPosition) returns (MsgOpenPositionResponse) {
option (google.api.http).post = "/nibiru/perp/open_position";
}

}

// -------------------------- RemoveMargin --------------------------

/* MsgRemoveMargin: Msg to remove margin. */
message MsgRemoveMargin {
string sender = 1;
Expand All @@ -39,6 +49,8 @@ message MsgRemoveMarginResponse {
(gogoproto.nullable) = false];
}

// -------------------------- AddMargin --------------------------

/* MsgAddMargin: Msg to remove margin. */
message MsgAddMargin {
string sender = 1;
Expand All @@ -50,6 +62,23 @@ message MsgAddMarginResponse {
// MarginOut: tokens transferred back to the trader
}

// -------------------------- Liquidate --------------------------

message MsgLiquidate {
// Sender is the liquidator address
string sender = 1;
// TokenPair is the identifier for the position's virtual pool
string token_pair = 2;
// Trader is the address of the owner of the position
string trader = 3;
}

message MsgLiquidateResponse {
// TODO: blank for now
}

// -------------------------- OpenPosition --------------------------

message MsgOpenPosition {
string sender = 1;
string token_pair = 2;
Expand All @@ -67,4 +96,7 @@ message MsgOpenPosition {

message MsgOpenPositionResponse {

}
}

// -------------------------- ClosePosition --------------------------
// TODO
5 changes: 5 additions & 0 deletions x/perp/keeper/clearing_house.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ func (k Keeper) getPositionNotionalAndUnrealizedPnL(
panic("unrecognized pnl calc option: " + pnlCalcOption.String())
}

if positionNotional.Equal(position.OpenNotional) {
// if position notional and open notional are the same, then early return
return positionNotional, sdk.ZeroDec(), nil
}

if position.Size_.IsPositive() {
// LONG
unrealizedPnL = positionNotional.Sub(position.OpenNotional)
Expand Down
23 changes: 16 additions & 7 deletions x/perp/keeper/clearing_house_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,19 @@ type mockedDependencies struct {
}

func getKeeper(t *testing.T) (Keeper, mockedDependencies, sdk.Context) {
db := tmdb.NewMemDB()
commitMultiStore := store.NewCommitMultiStore(db)
// Mount the KV store with the x/perp store key
storeKey := sdk.NewKVStoreKey(types.StoreKey)
memStoreKey := storetypes.NewMemoryStoreKey(types.StoreKey)
commitMultiStore.MountStoreWithDB(storeKey, sdk.StoreTypeIAVL, db)
// Mount Transient store
transientStoreKey := sdk.NewTransientStoreKey("transient" + types.StoreKey)
commitMultiStore.MountStoreWithDB(transientStoreKey, sdk.StoreTypeTransient, nil)
// Mount Memory store
memStoreKey := storetypes.NewMemoryStoreKey("mem" + types.StoreKey)
commitMultiStore.MountStoreWithDB(memStoreKey, sdk.StoreTypeMemory, nil)

db := tmdb.NewMemDB()
stateStore := store.NewCommitMultiStore(db)
stateStore.MountStoreWithDB(storeKey, sdk.StoreTypeIAVL, db)
require.NoError(t, stateStore.LoadLatestVersion())
require.NoError(t, commitMultiStore.LoadLatestVersion())

protoCodec := codec.NewProtoCodec(codectypes.NewInterfaceRegistry())
params := initParamsKeeper(
Expand Down Expand Up @@ -117,7 +123,7 @@ func getKeeper(t *testing.T) (Keeper, mockedDependencies, sdk.Context) {
mockedVpoolKeeper,
)

ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, nil)
ctx := sdk.NewContext(commitMultiStore, tmproto.Header{}, false, nil)

return k, mockedDependencies{
mockAccountKeeper: mockedAccountKeeper,
Expand All @@ -127,7 +133,10 @@ func getKeeper(t *testing.T) (Keeper, mockedDependencies, sdk.Context) {
}, ctx
}

func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino, key, tkey sdk.StoreKey) paramskeeper.Keeper {
func initParamsKeeper(
appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino,
key sdk.StoreKey, tkey sdk.StoreKey,
) paramskeeper.Keeper {
paramsKeeper := paramskeeper.NewKeeper(appCodec, legacyAmino, key, tkey)
paramsKeeper.Subspace(types.ModuleName)

Expand Down
186 changes: 185 additions & 1 deletion x/perp/keeper/liquidate.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,189 @@
package keeper

import (
"context"
"fmt"

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

"github.com/NibiruChain/nibiru/x/common"
"github.com/NibiruChain/nibiru/x/perp/events"
"github.com/NibiruChain/nibiru/x/perp/types"
)

/* 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)
liquidator, err := sdk.AccAddressFromBech32(msg.Sender)
if err != nil {
return res, err
}

// validate trader (msg.PositionOwner)
trader, err := sdk.AccAddressFromBech32(msg.Trader)
if err != nil {
return res, err
}

// validate pair
pair, err := common.NewTokenPairFromStr(msg.TokenPair)
if err != nil {
return res, err
}
err = k.requireVpool(ctx, pair)
if err != nil {
return res, err
}

position, err := k.GetPosition(ctx, pair, trader.String())
if err != nil {
return res, err
}

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

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

marginRatio = sdk.MaxDec(marginRatio, marginRatioBasedOnOracle)
}

params := k.GetParams(ctx)
err = requireMoreMarginRatio(marginRatio, params.MaintenanceMarginRatio, false)
if err != nil {
return res, types.ErrMarginHighEnough
}

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

fmt.Println("marginRatioBasedOnSpot", marginRatioBasedOnSpot)

var (
liquidateResp types.LiquidateResp
)

if marginRatioBasedOnSpot.GTE(params.GetPartialLiquidationRatioAsDec()) {
_, err = k.ExecuteFullLiquidation(ctx, liquidator, position)
if err != nil {
return res, err
}
} else {
err = k.ExecutePartialLiquidation(ctx, liquidator, position)
if err != nil {
return res, err
}
}

events.EmitPositionLiquidate(
/* ctx */ ctx,
/* vpool */ pair.String(),
/* owner */ trader,
/* notional */ liquidateResp.PositionResp.ExchangedQuoteAssetAmount,
/* vsize */ liquidateResp.PositionResp.ExchangedPositionSize,
/* liquidator */ liquidator,
/* liquidationFee */ liquidateResp.FeeToLiquidator.TruncateInt(),
/* badDebt */ liquidateResp.BadDebt,
)

return res, nil
}

/*
Fully liquidates a position. It is assumed that the margin ratio has already been
checked prior to calling this method.
args:
- ctx: cosmos-sdk context
- liquidator: the liquidator's address
- position: the position to liquidate
ret:
- liquidationResp: a response object containing the results of the liquidation
- err: error
*/
func (k Keeper) ExecuteFullLiquidation(
ctx sdk.Context, liquidator sdk.AccAddress, position *types.Position,
) (liquidationResp types.LiquidateResp, err error) {
params := k.GetParams(ctx)
tokenPair, err := common.NewTokenPairFromStr(position.Pair)
if err != nil {
return types.LiquidateResp{}, err
}

positionResp, err := k.closePositionEntirely(
ctx,
/* currentPosition */ *position,
/* quoteAssetAmountLimit */ sdk.ZeroDec())
if err != nil {
return types.LiquidateResp{}, err
}

remainMargin := positionResp.MarginToVault.Abs()

feeToLiquidator := params.GetLiquidationFeeAsDec().
Mul(positionResp.ExchangedQuoteAssetAmount).
QuoInt64(2)
totalBadDebt := positionResp.BadDebt

if feeToLiquidator.GT(remainMargin) {
// if the remainMargin is not enough for liquidationFee, count it as bad debt
totalBadDebt = totalBadDebt.Add(feeToLiquidator.Sub(remainMargin))
remainMargin = sdk.ZeroDec()
} else {
// Otherwise, the remaining margin rest will be transferred to ecosystemFund
remainMargin = remainMargin.Sub(feeToLiquidator)
}

// Realize bad debt
if totalBadDebt.IsPositive() {
if err = k.realizeBadDebt(
ctx,
tokenPair.GetQuoteTokenDenom(),
totalBadDebt.RoundInt(),
); err != nil {
return types.LiquidateResp{}, err
}
}

feeToPerpEcosystemFund := sdk.ZeroDec()
if remainMargin.IsPositive() {
feeToPerpEcosystemFund = remainMargin
}

liquidationResp = types.LiquidateResp{
BadDebt: totalBadDebt,
FeeToLiquidator: feeToLiquidator,
FeeToPerpEcosystemFund: feeToPerpEcosystemFund,
Liquidator: liquidator,
PositionResp: positionResp,
}
err = k.distributeLiquidateRewards(ctx, liquidationResp)
if err != nil {
return types.LiquidateResp{}, err
}

return liquidationResp, nil
}

func (k Keeper) distributeLiquidateRewards(
ctx sdk.Context, liquidateResp types.LiquidateResp) (err error) {
// --------------------------------------------------------------
Expand Down Expand Up @@ -65,7 +241,7 @@ func (k Keeper) distributeLiquidateRewards(
pair.GetQuoteTokenDenom(), feeToLiquidator)
err = k.BankKeeper.SendCoinsFromModuleToAccount(
ctx,
/* from */ types.PerpEFModuleAccount,
/* from */ types.VaultModuleAccount,
/* to */ liquidateResp.Liquidator,
sdk.NewCoins(coinToLiquidator),
)
Expand All @@ -81,3 +257,11 @@ func (k Keeper) distributeLiquidateRewards(

return nil
}

// ExecutePartialLiquidation fully liquidates a position.
func (k Keeper) ExecutePartialLiquidation(
ctx sdk.Context, liquidator sdk.AccAddress, position *types.Position,
) (err error) {
// TODO: https://github.com/NibiruChain/nibiru/pull/437
return nil
}
Loading

0 comments on commit f490773

Please sign in to comment.