Skip to content

Commit f490773

Browse files
Unique-DivineAgentSmithMatrixMDmatthiasmattNibiruHeisenberg
authored
feat, test (perp): Liquidate (1/4) - 'ExecuteFullLiquidation' (#432)
* 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 <101130700+MatrixHeisenberg@users.noreply.github.com> * 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 <98403347+AgentSmithMatrix@users.noreply.github.com> Co-authored-by: MD <matthias@matrixsystems.co> Co-authored-by: Mat-Cosmos <97468149+matthiasmatt@users.noreply.github.com> Co-authored-by: Walter White <101130700+MatrixHeisenberg@users.noreply.github.com> Co-authored-by: Walter White <heisenberg@matrixsystems.co>
1 parent 883dceb commit f490773

File tree

11 files changed

+1596
-60
lines changed

11 files changed

+1596
-60
lines changed

proto/perp/v1/tx.proto

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,20 @@ service Msg {
2020
option (google.api.http).post = "/nibiru/perp/add_margin";
2121
}
2222

23-
rpc OpenPosition(MsgOpenPosition) returns (MsgOpenPositionResponse) {}
23+
/* Liquidate is a transaction that allows the caller to fully or partially
24+
liquidate an existing position. */
25+
// rpc Liquidate(MsgLiquidate) returns (MsgLiquidateResponse) {
26+
// option (google.api.http).post = "/nibiru/perp/liquidate";
27+
// }
28+
29+
rpc OpenPosition(MsgOpenPosition) returns (MsgOpenPositionResponse) {
30+
option (google.api.http).post = "/nibiru/perp/open_position";
31+
}
2432

2533
}
2634

35+
// -------------------------- RemoveMargin --------------------------
36+
2737
/* MsgRemoveMargin: Msg to remove margin. */
2838
message MsgRemoveMargin {
2939
string sender = 1;
@@ -39,6 +49,8 @@ message MsgRemoveMarginResponse {
3949
(gogoproto.nullable) = false];
4050
}
4151

52+
// -------------------------- AddMargin --------------------------
53+
4254
/* MsgAddMargin: Msg to remove margin. */
4355
message MsgAddMargin {
4456
string sender = 1;
@@ -50,6 +62,23 @@ message MsgAddMarginResponse {
5062
// MarginOut: tokens transferred back to the trader
5163
}
5264

65+
// -------------------------- Liquidate --------------------------
66+
67+
message MsgLiquidate {
68+
// Sender is the liquidator address
69+
string sender = 1;
70+
// TokenPair is the identifier for the position's virtual pool
71+
string token_pair = 2;
72+
// Trader is the address of the owner of the position
73+
string trader = 3;
74+
}
75+
76+
message MsgLiquidateResponse {
77+
// TODO: blank for now
78+
}
79+
80+
// -------------------------- OpenPosition --------------------------
81+
5382
message MsgOpenPosition {
5483
string sender = 1;
5584
string token_pair = 2;
@@ -67,4 +96,7 @@ message MsgOpenPosition {
6796

6897
message MsgOpenPositionResponse {
6998

70-
}
99+
}
100+
101+
// -------------------------- ClosePosition --------------------------
102+
// TODO

x/perp/keeper/clearing_house.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@ func (k Keeper) getPositionNotionalAndUnrealizedPnL(
321321
panic("unrecognized pnl calc option: " + pnlCalcOption.String())
322322
}
323323

324+
if positionNotional.Equal(position.OpenNotional) {
325+
// if position notional and open notional are the same, then early return
326+
return positionNotional, sdk.ZeroDec(), nil
327+
}
328+
324329
if position.Size_.IsPositive() {
325330
// LONG
326331
unrealizedPnL = positionNotional.Sub(position.OpenNotional)

x/perp/keeper/clearing_house_test.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,19 @@ type mockedDependencies struct {
8282
}
8383

8484
func getKeeper(t *testing.T) (Keeper, mockedDependencies, sdk.Context) {
85+
db := tmdb.NewMemDB()
86+
commitMultiStore := store.NewCommitMultiStore(db)
87+
// Mount the KV store with the x/perp store key
8588
storeKey := sdk.NewKVStoreKey(types.StoreKey)
86-
memStoreKey := storetypes.NewMemoryStoreKey(types.StoreKey)
89+
commitMultiStore.MountStoreWithDB(storeKey, sdk.StoreTypeIAVL, db)
90+
// Mount Transient store
91+
transientStoreKey := sdk.NewTransientStoreKey("transient" + types.StoreKey)
92+
commitMultiStore.MountStoreWithDB(transientStoreKey, sdk.StoreTypeTransient, nil)
93+
// Mount Memory store
94+
memStoreKey := storetypes.NewMemoryStoreKey("mem" + types.StoreKey)
95+
commitMultiStore.MountStoreWithDB(memStoreKey, sdk.StoreTypeMemory, nil)
8796

88-
db := tmdb.NewMemDB()
89-
stateStore := store.NewCommitMultiStore(db)
90-
stateStore.MountStoreWithDB(storeKey, sdk.StoreTypeIAVL, db)
91-
require.NoError(t, stateStore.LoadLatestVersion())
97+
require.NoError(t, commitMultiStore.LoadLatestVersion())
9298

9399
protoCodec := codec.NewProtoCodec(codectypes.NewInterfaceRegistry())
94100
params := initParamsKeeper(
@@ -117,7 +123,7 @@ func getKeeper(t *testing.T) (Keeper, mockedDependencies, sdk.Context) {
117123
mockedVpoolKeeper,
118124
)
119125

120-
ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, nil)
126+
ctx := sdk.NewContext(commitMultiStore, tmproto.Header{}, false, nil)
121127

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

130-
func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino, key, tkey sdk.StoreKey) paramskeeper.Keeper {
136+
func initParamsKeeper(
137+
appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino,
138+
key sdk.StoreKey, tkey sdk.StoreKey,
139+
) paramskeeper.Keeper {
131140
paramsKeeper := paramskeeper.NewKeeper(appCodec, legacyAmino, key, tkey)
132141
paramsKeeper.Subspace(types.ModuleName)
133142

x/perp/keeper/liquidate.go

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,189 @@
11
package keeper
22

33
import (
4+
"context"
5+
"fmt"
6+
47
sdk "github.com/cosmos/cosmos-sdk/types"
58

69
"github.com/NibiruChain/nibiru/x/common"
710
"github.com/NibiruChain/nibiru/x/perp/events"
811
"github.com/NibiruChain/nibiru/x/perp/types"
912
)
1013

14+
/* Liquidate allows to liquidate the trader position if the margin is below the
15+
required margin maintenance ratio.
16+
*/
17+
func (k Keeper) Liquidate(
18+
goCtx context.Context, msg *types.MsgLiquidate,
19+
) (res *types.MsgLiquidateResponse, err error) {
20+
// ------------- Liquidation Message Setup -------------
21+
22+
ctx := sdk.UnwrapSDKContext(goCtx)
23+
24+
// validate liquidator (msg.Sender)
25+
liquidator, err := sdk.AccAddressFromBech32(msg.Sender)
26+
if err != nil {
27+
return res, err
28+
}
29+
30+
// validate trader (msg.PositionOwner)
31+
trader, err := sdk.AccAddressFromBech32(msg.Trader)
32+
if err != nil {
33+
return res, err
34+
}
35+
36+
// validate pair
37+
pair, err := common.NewTokenPairFromStr(msg.TokenPair)
38+
if err != nil {
39+
return res, err
40+
}
41+
err = k.requireVpool(ctx, pair)
42+
if err != nil {
43+
return res, err
44+
}
45+
46+
position, err := k.GetPosition(ctx, pair, trader.String())
47+
if err != nil {
48+
return res, err
49+
}
50+
51+
marginRatio, err := k.GetMarginRatio(ctx, *position, types.MarginCalculationPriceOption_MAX_PNL)
52+
if err != nil {
53+
return res, err
54+
}
55+
56+
if k.VpoolKeeper.IsOverSpreadLimit(ctx, pair) {
57+
marginRatioBasedOnOracle, err := k.GetMarginRatio(
58+
ctx, *position, types.MarginCalculationPriceOption_INDEX)
59+
if err != nil {
60+
return res, err
61+
}
62+
63+
marginRatio = sdk.MaxDec(marginRatio, marginRatioBasedOnOracle)
64+
}
65+
66+
params := k.GetParams(ctx)
67+
err = requireMoreMarginRatio(marginRatio, params.MaintenanceMarginRatio, false)
68+
if err != nil {
69+
return res, types.ErrMarginHighEnough
70+
}
71+
72+
marginRatioBasedOnSpot, err := k.GetMarginRatio(
73+
ctx, *position, types.MarginCalculationPriceOption_SPOT)
74+
if err != nil {
75+
return res, err
76+
}
77+
78+
fmt.Println("marginRatioBasedOnSpot", marginRatioBasedOnSpot)
79+
80+
var (
81+
liquidateResp types.LiquidateResp
82+
)
83+
84+
if marginRatioBasedOnSpot.GTE(params.GetPartialLiquidationRatioAsDec()) {
85+
_, err = k.ExecuteFullLiquidation(ctx, liquidator, position)
86+
if err != nil {
87+
return res, err
88+
}
89+
} else {
90+
err = k.ExecutePartialLiquidation(ctx, liquidator, position)
91+
if err != nil {
92+
return res, err
93+
}
94+
}
95+
96+
events.EmitPositionLiquidate(
97+
/* ctx */ ctx,
98+
/* vpool */ pair.String(),
99+
/* owner */ trader,
100+
/* notional */ liquidateResp.PositionResp.ExchangedQuoteAssetAmount,
101+
/* vsize */ liquidateResp.PositionResp.ExchangedPositionSize,
102+
/* liquidator */ liquidator,
103+
/* liquidationFee */ liquidateResp.FeeToLiquidator.TruncateInt(),
104+
/* badDebt */ liquidateResp.BadDebt,
105+
)
106+
107+
return res, nil
108+
}
109+
110+
/*
111+
Fully liquidates a position. It is assumed that the margin ratio has already been
112+
checked prior to calling this method.
113+
114+
args:
115+
- ctx: cosmos-sdk context
116+
- liquidator: the liquidator's address
117+
- position: the position to liquidate
118+
119+
ret:
120+
- liquidationResp: a response object containing the results of the liquidation
121+
- err: error
122+
*/
123+
func (k Keeper) ExecuteFullLiquidation(
124+
ctx sdk.Context, liquidator sdk.AccAddress, position *types.Position,
125+
) (liquidationResp types.LiquidateResp, err error) {
126+
params := k.GetParams(ctx)
127+
tokenPair, err := common.NewTokenPairFromStr(position.Pair)
128+
if err != nil {
129+
return types.LiquidateResp{}, err
130+
}
131+
132+
positionResp, err := k.closePositionEntirely(
133+
ctx,
134+
/* currentPosition */ *position,
135+
/* quoteAssetAmountLimit */ sdk.ZeroDec())
136+
if err != nil {
137+
return types.LiquidateResp{}, err
138+
}
139+
140+
remainMargin := positionResp.MarginToVault.Abs()
141+
142+
feeToLiquidator := params.GetLiquidationFeeAsDec().
143+
Mul(positionResp.ExchangedQuoteAssetAmount).
144+
QuoInt64(2)
145+
totalBadDebt := positionResp.BadDebt
146+
147+
if feeToLiquidator.GT(remainMargin) {
148+
// if the remainMargin is not enough for liquidationFee, count it as bad debt
149+
totalBadDebt = totalBadDebt.Add(feeToLiquidator.Sub(remainMargin))
150+
remainMargin = sdk.ZeroDec()
151+
} else {
152+
// Otherwise, the remaining margin rest will be transferred to ecosystemFund
153+
remainMargin = remainMargin.Sub(feeToLiquidator)
154+
}
155+
156+
// Realize bad debt
157+
if totalBadDebt.IsPositive() {
158+
if err = k.realizeBadDebt(
159+
ctx,
160+
tokenPair.GetQuoteTokenDenom(),
161+
totalBadDebt.RoundInt(),
162+
); err != nil {
163+
return types.LiquidateResp{}, err
164+
}
165+
}
166+
167+
feeToPerpEcosystemFund := sdk.ZeroDec()
168+
if remainMargin.IsPositive() {
169+
feeToPerpEcosystemFund = remainMargin
170+
}
171+
172+
liquidationResp = types.LiquidateResp{
173+
BadDebt: totalBadDebt,
174+
FeeToLiquidator: feeToLiquidator,
175+
FeeToPerpEcosystemFund: feeToPerpEcosystemFund,
176+
Liquidator: liquidator,
177+
PositionResp: positionResp,
178+
}
179+
err = k.distributeLiquidateRewards(ctx, liquidationResp)
180+
if err != nil {
181+
return types.LiquidateResp{}, err
182+
}
183+
184+
return liquidationResp, nil
185+
}
186+
11187
func (k Keeper) distributeLiquidateRewards(
12188
ctx sdk.Context, liquidateResp types.LiquidateResp) (err error) {
13189
// --------------------------------------------------------------
@@ -65,7 +241,7 @@ func (k Keeper) distributeLiquidateRewards(
65241
pair.GetQuoteTokenDenom(), feeToLiquidator)
66242
err = k.BankKeeper.SendCoinsFromModuleToAccount(
67243
ctx,
68-
/* from */ types.PerpEFModuleAccount,
244+
/* from */ types.VaultModuleAccount,
69245
/* to */ liquidateResp.Liquidator,
70246
sdk.NewCoins(coinToLiquidator),
71247
)
@@ -81,3 +257,11 @@ func (k Keeper) distributeLiquidateRewards(
81257

82258
return nil
83259
}
260+
261+
// ExecutePartialLiquidation fully liquidates a position.
262+
func (k Keeper) ExecutePartialLiquidation(
263+
ctx sdk.Context, liquidator sdk.AccAddress, position *types.Position,
264+
) (err error) {
265+
// TODO: https://github.com/NibiruChain/nibiru/pull/437
266+
return nil
267+
}

0 commit comments

Comments
 (0)