From f4907734bae951309db1cf9686023a9698dc1714 Mon Sep 17 00:00:00 2001 From: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Date: Sun, 22 May 2022 18:53:51 -0400 Subject: [PATCH] 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 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 --- proto/perp/v1/tx.proto | 36 +- x/perp/keeper/clearing_house.go | 5 + x/perp/keeper/clearing_house_test.go | 23 +- x/perp/keeper/liquidate.go | 186 ++++++++++- x/perp/keeper/liquidate_test.go | 368 ++++++++++++++++++++ x/perp/keeper/liquidate_unit_test.go | 372 ++++++++++++++++++++- x/perp/keeper/withdraw.go | 36 ++ x/perp/keeper/withdraw_test.go | 68 ++++ x/perp/types/keys.go | 2 + x/perp/types/tx.pb.go | 480 ++++++++++++++++++++++++--- x/perp/types/tx.pb.gw.go | 80 +++++ 11 files changed, 1596 insertions(+), 60 deletions(-) create mode 100644 x/perp/keeper/liquidate_test.go diff --git a/proto/perp/v1/tx.proto b/proto/perp/v1/tx.proto index f187d8fae..772fa3554 100644 --- a/proto/perp/v1/tx.proto +++ b/proto/perp/v1/tx.proto @@ -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; @@ -39,6 +49,8 @@ message MsgRemoveMarginResponse { (gogoproto.nullable) = false]; } +// -------------------------- AddMargin -------------------------- + /* MsgAddMargin: Msg to remove margin. */ message MsgAddMargin { string sender = 1; @@ -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; @@ -67,4 +96,7 @@ message MsgOpenPosition { message MsgOpenPositionResponse { -} \ No newline at end of file +} + +// -------------------------- ClosePosition -------------------------- +// TODO \ No newline at end of file diff --git a/x/perp/keeper/clearing_house.go b/x/perp/keeper/clearing_house.go index 4a67ed2d0..5c3bcc50c 100644 --- a/x/perp/keeper/clearing_house.go +++ b/x/perp/keeper/clearing_house.go @@ -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) diff --git a/x/perp/keeper/clearing_house_test.go b/x/perp/keeper/clearing_house_test.go index ae3307b8b..6117bfc73 100644 --- a/x/perp/keeper/clearing_house_test.go +++ b/x/perp/keeper/clearing_house_test.go @@ -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( @@ -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, @@ -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) diff --git a/x/perp/keeper/liquidate.go b/x/perp/keeper/liquidate.go index 941ca05a4..12a0d665e 100644 --- a/x/perp/keeper/liquidate.go +++ b/x/perp/keeper/liquidate.go @@ -1,6 +1,9 @@ package keeper import ( + "context" + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/NibiruChain/nibiru/x/common" @@ -8,6 +11,179 @@ import ( "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) { // -------------------------------------------------------------- @@ -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), ) @@ -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 +} diff --git a/x/perp/keeper/liquidate_test.go b/x/perp/keeper/liquidate_test.go new file mode 100644 index 000000000..c1eef9020 --- /dev/null +++ b/x/perp/keeper/liquidate_test.go @@ -0,0 +1,368 @@ +package keeper_test + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/NibiruChain/nibiru/x/common" + "github.com/NibiruChain/nibiru/x/perp/events" + "github.com/NibiruChain/nibiru/x/perp/types" + "github.com/NibiruChain/nibiru/x/testutil" + + "github.com/NibiruChain/nibiru/x/testutil/sample" +) + +func TestExecuteFullLiquidation_EmptyPosition(t *testing.T) { + testCases := []struct { + name string + side types.Side + quote sdk.Int + leverage sdk.Dec + baseLimit sdk.Dec + liquidationFee sdk.Dec + traderFunds sdk.Coin + }{ + { + name: "liquidateEmptyPositionBUY", + side: types.Side_BUY, + quote: sdk.NewInt(0), + leverage: sdk.OneDec(), + baseLimit: sdk.ZeroDec(), + liquidationFee: sdk.MustNewDecFromStr("0.1"), + traderFunds: sdk.NewInt64Coin("NUSD", 60), + }, + { + name: "liquidateEmptyPositionSELL", + side: types.Side_SELL, + quote: sdk.NewInt(0), + leverage: sdk.OneDec(), + baseLimit: sdk.ZeroDec(), + liquidationFee: sdk.MustNewDecFromStr("0.1"), + traderFunds: sdk.NewInt64Coin("NUSD", 60), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + nibiruApp, ctx := testutil.NewNibiruApp(true) + pair := common.TokenPair("BTC:NUSD") + + t.Log("Set vpool defined by pair on VpoolKeeper") + vpoolKeeper := &nibiruApp.VpoolKeeper + vpoolKeeper.CreatePool( + ctx, + pair.String(), + /* tradeLimitRatio */ sdk.MustNewDecFromStr("0.9"), + /* quoteAssetReserves */ sdk.NewDec(10_000_000), + /* baseAssetReserves */ sdk.NewDec(5_000_000), + /* fluctuationLimitRatio */ sdk.MustNewDecFromStr("1"), + /* maxOracleSpreadRatio */ sdk.MustNewDecFromStr("0.1"), + ) + require.True(t, vpoolKeeper.ExistsPool(ctx, pair)) + + t.Log("Set vpool defined by pair on PerpKeeper") + perpKeeper := &nibiruApp.PerpKeeper + params := types.DefaultParams() + + perpKeeper.SetParams(ctx, types.NewParams( + params.Stopped, + params.MaintenanceMarginRatio, + params.GetTollRatioAsDec(), + params.GetSpreadRatioAsDec(), + tc.liquidationFee, + params.GetPartialLiquidationRatioAsDec(), + )) + + perpKeeper.PairMetadata().Set(ctx, &types.PairMetadata{ + Pair: pair.String(), + CumulativePremiumFractions: []sdk.Dec{sdk.OneDec()}, + }) + + t.Log("Fund trader (Alice) account with sufficient quote") + var err error + alice := sample.AccAddress() + err = simapp.FundAccount(nibiruApp.BankKeeper, ctx, alice, + sdk.NewCoins(tc.traderFunds)) + require.NoError(t, err) + + t.Log("Open position") + err = nibiruApp.PerpKeeper.OpenPosition( + ctx, pair, tc.side, alice, tc.quote, tc.leverage, tc.baseLimit) + + require.NoError(t, err) + + t.Log("Get the position") + position, err := nibiruApp.PerpKeeper.GetPosition(ctx, pair, alice.String()) + require.NoError(t, err) + + t.Log("Artificially populate Vault and PerpEF to prevent BankKeeper errors") + startingModuleFunds := sdk.NewCoins(sdk.NewInt64Coin( + pair.GetQuoteTokenDenom(), 1_000_000)) + assert.NoError(t, simapp.FundModuleAccount( + nibiruApp.BankKeeper, ctx, types.VaultModuleAccount, startingModuleFunds)) + assert.NoError(t, simapp.FundModuleAccount( + nibiruApp.BankKeeper, ctx, types.PerpEFModuleAccount, startingModuleFunds)) + + t.Log("Liquidate the position") + liquidator := sample.AccAddress() + _, err = nibiruApp.PerpKeeper.ExecuteFullLiquidation(ctx, liquidator, position) + + require.Error(t, err) + + // No change in the position + newPosition, _ := nibiruApp.PerpKeeper.GetPosition(ctx, pair, alice.String()) + require.Equal(t, position.Size_, newPosition.Size_) + require.Equal(t, position.Margin, newPosition.Margin) + require.Equal(t, position.OpenNotional, newPosition.OpenNotional) + }) + } +} + +func TestExecuteFullLiquidation(t *testing.T) { + // constants for this suite + pair := common.TokenPair("BTC:NUSD") + alice := sample.AccAddress() + + testCases := []struct { + name string + positionSide types.Side + quoteAmount sdk.Int + leverage sdk.Dec + baseAssetLimit sdk.Dec + liquidationFee sdk.Dec + traderFunds sdk.Coin + expectedLiquidatorBalance sdk.Coin + expectedPerpEFBalance sdk.Coin + expectedBadDebt sdk.Dec + expectedEvent sdk.Event + }{ + { + name: "happy path - Buy", + positionSide: types.Side_BUY, + quoteAmount: sdk.NewInt(50_000), + leverage: sdk.OneDec(), + baseAssetLimit: sdk.ZeroDec(), + liquidationFee: sdk.MustNewDecFromStr("0.1"), + traderFunds: sdk.NewInt64Coin("NUSD", 50_100), + // feeToLiquidator + // = positionResp.ExchangedQuoteAssetAmount * liquidationFee / 2 + // = 50_000 * 0.1 / 2 = 2500 + expectedLiquidatorBalance: sdk.NewInt64Coin("NUSD", 2_500), + // perpEFBalance = startingBalance + openPositionDelta + liquidateDelta + expectedPerpEFBalance: sdk.NewInt64Coin("NUSD", 1_047_550), + expectedBadDebt: sdk.MustNewDecFromStr("0"), + expectedEvent: events.NewInternalPositionResponseEvent( + &types.PositionResp{ + Position: &types.Position{ + Address: alice.String(), Pair: pair.String(), + Margin: sdk.ZeroDec(), OpenNotional: sdk.ZeroDec(), + }, + ExchangedQuoteAssetAmount: sdk.NewDec(50_000), + BadDebt: sdk.ZeroDec(), + ExchangedPositionSize: sdk.MustNewDecFromStr("-24875.621890547263681592"), + FundingPayment: sdk.ZeroDec(), + RealizedPnl: sdk.ZeroDec(), + MarginToVault: sdk.NewDec(-50_000), + UnrealizedPnlAfter: sdk.ZeroDec(), + }, + /* function */ "close_position_entirely", + ), + }, + { + name: "happy path - Sell", + positionSide: types.Side_SELL, + quoteAmount: sdk.NewInt(50_000), + traderFunds: sdk.NewInt64Coin("NUSD", 50_100), + leverage: sdk.OneDec(), + baseAssetLimit: sdk.ZeroDec(), + liquidationFee: sdk.MustNewDecFromStr("0.123123"), + // feeToLiquidator + // = positionResp.ExchangedQuoteAssetAmount * liquidationFee / 2 + // = 50_000 * 0.123123 / 2 = 3078.025 → 3078 + expectedLiquidatorBalance: sdk.NewInt64Coin("NUSD", 3078), + // perpEFBalance = startingBalance + openPositionDelta + liquidateDelta + expectedPerpEFBalance: sdk.NewInt64Coin("NUSD", 1_046_972), + expectedBadDebt: sdk.MustNewDecFromStr("0"), + expectedEvent: events.NewInternalPositionResponseEvent( + &types.PositionResp{ + Position: &types.Position{ + Address: alice.String(), Pair: pair.String(), + Margin: sdk.ZeroDec(), OpenNotional: sdk.ZeroDec(), + }, + ExchangedQuoteAssetAmount: sdk.NewDec(50_000), + BadDebt: sdk.ZeroDec(), + ExchangedPositionSize: sdk.MustNewDecFromStr("25125.628140703517587940"), + FundingPayment: sdk.ZeroDec(), + RealizedPnl: sdk.MustNewDecFromStr("-0.000000000000000001"), + MarginToVault: sdk.MustNewDecFromStr("-49999.999999999999999999"), + UnrealizedPnlAfter: sdk.ZeroDec(), + }, + /* function */ "close_position_entirely", + ), + }, + { + /* We open a position for 500k, with a liquidation fee of 50k. + This means 25k for the liquidator, and 25k for the perp fund. + Because the user only have margin for 50, we create 24950 of bad + debt (25000 due to liquidator minus 50). + */ + name: "happy path - BadDebt, long", + positionSide: types.Side_BUY, + quoteAmount: sdk.NewInt(50), + leverage: sdk.MustNewDecFromStr("10000"), + baseAssetLimit: sdk.ZeroDec(), + liquidationFee: sdk.MustNewDecFromStr("0.1"), + traderFunds: sdk.NewInt64Coin("NUSD", 1150), + // feeToLiquidator + // = positionResp.ExchangedQuoteAssetAmount * liquidationFee / 2 + // = 500_000 * 0.1 / 2 = 25_000 + expectedLiquidatorBalance: sdk.NewInt64Coin("NUSD", 25_000), + // perpEFBalance = startingBalance + openPositionDelta + liquidateDelta + expectedPerpEFBalance: sdk.NewInt64Coin("NUSD", 975_550), + expectedBadDebt: sdk.MustNewDecFromStr("24950"), + expectedEvent: events.NewInternalPositionResponseEvent( + &types.PositionResp{ + Position: &types.Position{ + Address: alice.String(), + Pair: pair.String(), + Margin: sdk.ZeroDec(), + OpenNotional: sdk.ZeroDec(), + }, + ExchangedQuoteAssetAmount: sdk.NewDec(500_000), + BadDebt: sdk.ZeroDec(), + ExchangedPositionSize: sdk.MustNewDecFromStr("-238095.238095238095238095"), + FundingPayment: sdk.ZeroDec(), + RealizedPnl: sdk.ZeroDec(), + MarginToVault: sdk.NewDec(-50), + UnrealizedPnlAfter: sdk.ZeroDec(), + }, + /* function */ "close_position_entirely", + ), + }, + { + // Same as above case but for shorts + name: "happy path - BadDebt, short", + positionSide: types.Side_SELL, + quoteAmount: sdk.NewInt(50), + leverage: sdk.MustNewDecFromStr("10000"), + baseAssetLimit: sdk.ZeroDec(), + liquidationFee: sdk.MustNewDecFromStr("0.1"), + traderFunds: sdk.NewInt64Coin("NUSD", 1150), + // feeToLiquidator + // = positionResp.ExchangedQuoteAssetAmount * liquidationFee / 2 + // = 500_000 * 0.1 / 2 = 25_000 + expectedLiquidatorBalance: sdk.NewInt64Coin("NUSD", 25_000), + // perpEFBalance = startingBalance + openPositionDelta + liquidateDelta + expectedPerpEFBalance: sdk.NewInt64Coin("NUSD", 975_550), + expectedBadDebt: sdk.MustNewDecFromStr("24950"), + expectedEvent: events.NewInternalPositionResponseEvent( + &types.PositionResp{ + Position: &types.Position{ + Address: alice.String(), Pair: pair.String(), + Margin: sdk.ZeroDec(), OpenNotional: sdk.ZeroDec(), + }, + ExchangedQuoteAssetAmount: sdk.NewDec(500_000), + BadDebt: sdk.ZeroDec(), + ExchangedPositionSize: sdk.MustNewDecFromStr("263157.894736842105263158"), + FundingPayment: sdk.ZeroDec(), + RealizedPnl: sdk.ZeroDec(), + MarginToVault: sdk.NewDec(-50), + UnrealizedPnlAfter: sdk.ZeroDec(), + }, + /* function */ "close_position_entirely", + ), + }, + } + + for _, testCase := range testCases { + tc := testCase + t.Run(tc.name, func(t *testing.T) { + t.Log("Initialize variables") + nibiruApp, ctx := testutil.NewNibiruApp(true) + vpoolKeeper := &nibiruApp.VpoolKeeper + perpKeeper := &nibiruApp.PerpKeeper + liquidator := sample.AccAddress() + var err error + + t.Log("Create vpool") + vpoolKeeper.CreatePool( + ctx, + pair.String(), + /* tradeLimitRatio */ sdk.MustNewDecFromStr("0.9"), + /* quoteAssetReserves */ sdk.NewDec(10_000_000), + /* baseAssetReserves */ sdk.NewDec(5_000_000), + /* fluctuationLimitRatio */ sdk.MustNewDecFromStr("1"), + /* maxOracleSpreadRatio */ sdk.MustNewDecFromStr("0.1"), + ) + require.True(t, vpoolKeeper.ExistsPool(ctx, pair)) + + t.Log("Set perp params") + params := types.DefaultParams() + params.LiquidationFee = tc.liquidationFee.MulInt64(1_000_000).RoundInt64() + perpKeeper.SetParams(ctx, params) + perpKeeper.PairMetadata().Set(ctx, &types.PairMetadata{ + Pair: pair.String(), + CumulativePremiumFractions: []sdk.Dec{sdk.ZeroDec()}, + }) + + t.Log("Fund trader (Alice) account with sufficient quote") + require.NoError(t, simapp.FundAccount(nibiruApp.BankKeeper, ctx, alice, + sdk.NewCoins(tc.traderFunds))) + + t.Log("Open position") + require.NoError(t, nibiruApp.PerpKeeper.OpenPosition( + ctx, pair, tc.positionSide, alice, tc.quoteAmount, tc.leverage, tc.baseAssetLimit)) + + t.Log("Get the position") + position, err := nibiruApp.PerpKeeper.GetPosition(ctx, pair, alice.String()) + require.NoError(t, err) + + t.Log("Fund vault and PerpEF") + startingModuleFunds := sdk.NewCoins( + sdk.NewInt64Coin(pair.GetQuoteTokenDenom(), 1_000_000), + ) + require.NoError(t, simapp.FundModuleAccount( + nibiruApp.BankKeeper, ctx, types.VaultModuleAccount, startingModuleFunds)) + require.NoError(t, simapp.FundModuleAccount( + nibiruApp.BankKeeper, ctx, types.PerpEFModuleAccount, startingModuleFunds)) + + t.Log("Liquidate the (entire) position") + _, err = nibiruApp.PerpKeeper.ExecuteFullLiquidation(ctx, liquidator, position) + require.NoError(t, err) + + t.Log("Check events") + assert.Contains(t, ctx.EventManager().Events(), tc.expectedEvent) + + t.Log("Check new position") + newPosition, _ := nibiruApp.PerpKeeper.GetPosition(ctx, pair, alice.String()) + assert.True(t, newPosition.Size_.IsZero()) + assert.True(t, newPosition.Margin.IsZero()) + assert.True(t, newPosition.OpenNotional.IsZero()) + + t.Log("Check liquidator balance") + assert.EqualValues(t, + tc.expectedLiquidatorBalance, + nibiruApp.BankKeeper.GetBalance( + ctx, + liquidator, + pair.GetQuoteTokenDenom(), + ), + ) + + t.Log("Check PerpEF balance") + require.EqualValues(t, + tc.expectedPerpEFBalance.String(), + nibiruApp.BankKeeper.GetBalance( + ctx, + nibiruApp.AccountKeeper.GetModuleAddress(types.PerpEFModuleAccount), + pair.GetQuoteTokenDenom(), + ).String(), + ) + }) + } +} diff --git a/x/perp/keeper/liquidate_unit_test.go b/x/perp/keeper/liquidate_unit_test.go index d0f8bfc2c..a1f5243b1 100644 --- a/x/perp/keeper/liquidate_unit_test.go +++ b/x/perp/keeper/liquidate_unit_test.go @@ -11,6 +11,7 @@ import ( "github.com/NibiruChain/nibiru/x/common" "github.com/NibiruChain/nibiru/x/perp/events" "github.com/NibiruChain/nibiru/x/perp/types" + vpooltypes "github.com/NibiruChain/nibiru/x/vpool/types" "github.com/NibiruChain/nibiru/x/testutil/sample" ) @@ -70,7 +71,7 @@ func Test_distributeLiquidateRewards_Error(t *testing.T) { test: func() { perpKeeper, mocks, ctx := getKeeper(t) liquidator := sample.AccAddress() - pair := common.TokenPair("xxx:yyy") + pair := common.TokenPair("BTC:NUSD") mocks.mockVpoolKeeper.EXPECT().ExistsPool(ctx, pair).Return(false) err := perpKeeper.distributeLiquidateRewards(ctx, types.LiquidateResp{BadDebt: sdk.OneDec(), FeeToLiquidator: sdk.OneDec(), @@ -106,7 +107,7 @@ func Test_distributeLiquidateRewards_Happy(t *testing.T) { test: func() { perpKeeper, mocks, ctx := getKeeper(t) liquidator := sample.AccAddress() - pair := common.TokenPair("xxx:yyy") + pair := common.TokenPair("BTC:NUSD") mocks.mockVpoolKeeper.EXPECT().ExistsPool(ctx, pair).Return(true) @@ -121,11 +122,11 @@ func Test_distributeLiquidateRewards_Happy(t *testing.T) { mocks.mockBankKeeper.EXPECT().SendCoinsFromModuleToModule( ctx, types.VaultModuleAccount, types.PerpEFModuleAccount, - sdk.NewCoins(sdk.NewCoin("yyy", sdk.OneInt())), + sdk.NewCoins(sdk.NewCoin("NUSD", sdk.OneInt())), ).Return(nil) mocks.mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount( - ctx, types.PerpEFModuleAccount, liquidator, - sdk.NewCoins(sdk.NewCoin("yyy", sdk.OneInt())), + ctx, types.VaultModuleAccount, liquidator, + sdk.NewCoins(sdk.NewCoin("NUSD", sdk.OneInt())), ).Return(nil) err := perpKeeper.distributeLiquidateRewards(ctx, @@ -142,13 +143,13 @@ func Test_distributeLiquidateRewards_Happy(t *testing.T) { expectedEvents := []sdk.Event{ events.NewTransferEvent( - /* coin */ sdk.NewCoin("yyy", sdk.OneInt()), + /* coin */ sdk.NewCoin("NUSD", sdk.OneInt()), /* from */ vaultAddr.String(), /* to */ perpEFAddr.String(), ), events.NewTransferEvent( - /* coin */ sdk.NewCoin("yyy", sdk.OneInt()), - /* from */ perpEFAddr.String(), + /* coin */ sdk.NewCoin("NUSD", sdk.OneInt()), + /* from */ vaultAddr.String(), /* to */ liquidator.String(), ), } @@ -166,3 +167,358 @@ func Test_distributeLiquidateRewards_Happy(t *testing.T) { }) } } + +func TestExecuteFullLiquidation_UnitWithMocks(t *testing.T) { + tests := []struct { + name string + + liquidationFee int64 + initialPositionSize sdk.Dec + initialMargin sdk.Dec + initialOpenNotional sdk.Dec + + // amount of quote obtained by trading base + baseAssetPriceInQuote sdk.Dec + + expectedLiquidationBadDebt sdk.Dec + expectedFundsToPerpEF sdk.Dec + expectedFundsToLiquidator sdk.Dec + expectedExchangedQuoteAssetAmount sdk.Dec + expectedMarginToVault sdk.Dec + expectedPositionRealizedPnl sdk.Dec + expectedPositionBadDebt sdk.Dec + }{ + { + /* + - long position + - open margin 10 NUSD, 10x leverage + - open notional and position notional of 100 NUSD + - position size 100 BTC (1 BTC = 1 NUSD) + + - remaining margin more than liquidation fee + - position has zero bad debt + - no funding payment + + - liquidation fee ratio is 0.1 + - notional exchanged is 100 NUSD + - liquidator gets 100 NUSD * 0.1 / 2 = 5 NUSD + - ecosystem fund gets remaining = 5 NUSD + */ + name: "remaining margin more than liquidation fee", + + liquidationFee: 100_000, // 0.1 liquidation fee + initialPositionSize: sdk.NewDec(100), + initialMargin: sdk.NewDec(10), + initialOpenNotional: sdk.NewDec(100), + + baseAssetPriceInQuote: sdk.NewDec(100), // no change in price + + expectedLiquidationBadDebt: sdk.ZeroDec(), + expectedFundsToPerpEF: sdk.NewDec(5), + expectedFundsToLiquidator: sdk.NewDec(5), + expectedExchangedQuoteAssetAmount: sdk.NewDec(100), + expectedMarginToVault: sdk.NewDec(-10), + expectedPositionRealizedPnl: sdk.ZeroDec(), + expectedPositionBadDebt: sdk.ZeroDec(), + }, + { + /* + - long position + - open margin 10 NUSD, 10x leverage + - open notional and position notional of 100 NUSD + - position size 100 BTC (1 BTC = 1 NUSD) + + - remaining margin less than liquidation fee but greater than zero + - position has zero bad debt + - no funding payment + + - liquidation fee ratio is 0.3 + - notional exchanged is 100 NUSD + - liquidator gets 100 NUSD * 0.3 / 2 = 15 NUSD + - position only has 10 NUSD margin, so bad debt accrues + - ecosystem fund gets nothing (0 NUSD) + */ + name: "remaining margin less than liquidation fee but greater than zero", + + liquidationFee: 300_000, // 0.3 liquidation fee + initialPositionSize: sdk.NewDec(100), + initialMargin: sdk.NewDec(10), + initialOpenNotional: sdk.NewDec(100), + + baseAssetPriceInQuote: sdk.NewDec(100), // no change in price + + expectedLiquidationBadDebt: sdk.NewDec(5), + expectedFundsToPerpEF: sdk.ZeroDec(), + expectedFundsToLiquidator: sdk.NewDec(15), + expectedExchangedQuoteAssetAmount: sdk.NewDec(100), + expectedMarginToVault: sdk.NewDec(-10), + expectedPositionRealizedPnl: sdk.ZeroDec(), + expectedPositionBadDebt: sdk.ZeroDec(), + }, + { + /* + - long position + - open margin 10 NUSD, 10x leverage + - open notional and position notional of 100 NUSD + - position size 100 BTC (1 BTC = 1 NUSD) + - BTC drops in price (1 BTC = 0.8 NUSD) + - position notional of 80 NUSD + - unrealized PnL of -20 NUSD, wipes out margin + + - position has zero margin remaining + - position has bad debt + - no funding payment + + - liquidation fee ratio is 0.3 + - notional exchanged is 80 NUSD + - liquidator gets 80 NUSD * 0.3 / 2 = 12 NUSD + - position has zero margin, so all of liquidation fee is bad debt + - ecosystem fund gets nothing (0 NUSD) + */ + name: "position has + margin and bad debt - 1", + + liquidationFee: 300_000, // 0.3 liquidation fee + initialPositionSize: sdk.NewDec(100), + initialMargin: sdk.NewDec(10), + initialOpenNotional: sdk.NewDec(100), + + baseAssetPriceInQuote: sdk.NewDec(80), // price dropped + + expectedLiquidationBadDebt: sdk.NewDec(22), + expectedFundsToPerpEF: sdk.ZeroDec(), + expectedFundsToLiquidator: sdk.NewDec(12), + expectedExchangedQuoteAssetAmount: sdk.NewDec(80), + expectedMarginToVault: sdk.ZeroDec(), + expectedPositionRealizedPnl: sdk.NewDec(-20), + expectedPositionBadDebt: sdk.NewDec(10), + }, + { + /* + - short position + - open margin 10 NUSD, 10x leverage + - open notional and position notional of 100 NUSD + - position size 100 BTC (1 BTC = 1 NUSD) + + - remaining margin more than liquidation fee + - position has zero bad debt + - no funding payment + + - liquidation fee ratio is 0.1 + - notional exchanged is 100 NUSD + - liquidator gets 100 NUSD * 0.1 / 2 = 5 NUSD + - ecosystem fund gets remaining = 5 NUSD + */ + name: "remaining margin more than liquidation fee", + + liquidationFee: 100_000, // 0.1 liquidation fee + initialPositionSize: sdk.NewDec(-100), + initialMargin: sdk.NewDec(10), + initialOpenNotional: sdk.NewDec(100), + + baseAssetPriceInQuote: sdk.NewDec(100), // no change in price + + expectedLiquidationBadDebt: sdk.ZeroDec(), + expectedFundsToPerpEF: sdk.NewDec(5), + expectedFundsToLiquidator: sdk.NewDec(5), + expectedExchangedQuoteAssetAmount: sdk.NewDec(100), + expectedMarginToVault: sdk.NewDec(-10), + expectedPositionRealizedPnl: sdk.ZeroDec(), + expectedPositionBadDebt: sdk.ZeroDec(), + }, + { + /* + - short position + - open margin 10 NUSD, 10x leverage + - open notional and position notional of 100 NUSD + - position size 100 BTC (1 BTC = 1 NUSD) + + - remaining margin less than liquidation fee but greater than zero + - position has zero bad debt + - no funding payment + + - liquidation fee ratio is 0.3 + - notional exchanged is 100 NUSD + - liquidator gets 100 NUSD * 0.3 / 2 = 15 NUSD + - position only has 10 NUSD margin, so bad debt accrues + - ecosystem fund gets nothing (0 NUSD) + */ + name: "remaining margin less than liquidation fee but greater than zero", + + liquidationFee: 300_000, // 0.3 liquidation fee + initialPositionSize: sdk.NewDec(-100), + initialMargin: sdk.NewDec(10), + initialOpenNotional: sdk.NewDec(100), + + baseAssetPriceInQuote: sdk.NewDec(100), // no change in price + + expectedLiquidationBadDebt: sdk.NewDec(5), + expectedFundsToPerpEF: sdk.ZeroDec(), + expectedFundsToLiquidator: sdk.NewDec(15), + expectedExchangedQuoteAssetAmount: sdk.NewDec(100), + expectedMarginToVault: sdk.NewDec(-10), + expectedPositionRealizedPnl: sdk.ZeroDec(), + expectedPositionBadDebt: sdk.ZeroDec(), + }, + { + /* + - short position + - open margin 10 NUSD, 10x leverage + - open notional and position notional of 100 NUSD + - position size 100 BTC (1 BTC = 1 NUSD) + - BTC increases in price (1 BTC = 1.2 NUSD) + - position notional of 120 NUSD + - unrealized PnL of -20 NUSD, wipes out margin + + - position has zero margin remaining + - position has bad debt + - no funding payment + + - liquidation fee ratio is 0.3 + - notional exchanged is 120 NUSD + - liquidator gets 120 NUSD * 0.3 / 2 = 18 NUSD + - position has zero margin, so all of liquidation fee is bad debt + - ecosystem fund gets nothing (0 NUSD) + */ + name: "position has + margin and bad debt - 2", + + liquidationFee: 300_000, // 0.3 liquidation fee + initialPositionSize: sdk.NewDec(-100), + initialMargin: sdk.NewDec(10), + initialOpenNotional: sdk.NewDec(100), + + baseAssetPriceInQuote: sdk.NewDec(120), // price increased + + expectedLiquidationBadDebt: sdk.NewDec(28), + expectedFundsToPerpEF: sdk.ZeroDec(), + expectedFundsToLiquidator: sdk.NewDec(18), + expectedExchangedQuoteAssetAmount: sdk.NewDec(120), + expectedMarginToVault: sdk.ZeroDec(), + expectedPositionRealizedPnl: sdk.NewDec(-20), + expectedPositionBadDebt: sdk.NewDec(10), + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Log("setup variables") + perpKeeper, mocks, ctx := getKeeper(t) + liquidatorAddr := sample.AccAddress() + traderAddr := sample.AccAddress() + pair := common.TokenPair("BTC:NUSD") + baseAssetDirection := vpooltypes.Direction_ADD_TO_POOL + if tc.initialPositionSize.IsNegative() { + baseAssetDirection = vpooltypes.Direction_REMOVE_FROM_POOL + } + + t.Log("mock account keeper") + vaultAddr := authtypes.NewModuleAddress(types.VaultModuleAccount) + perpEFAddr := authtypes.NewModuleAddress(types.PerpEFModuleAccount) + mocks.mockAccountKeeper.EXPECT().GetModuleAddress( + types.VaultModuleAccount).Return(vaultAddr) + mocks.mockAccountKeeper.EXPECT().GetModuleAddress( + types.PerpEFModuleAccount).Return(perpEFAddr) + + t.Log("mock bank keeper") + if tc.expectedFundsToPerpEF.IsPositive() { + mocks.mockBankKeeper.EXPECT().SendCoinsFromModuleToModule( + ctx, types.VaultModuleAccount, types.PerpEFModuleAccount, + sdk.NewCoins(sdk.NewCoin("NUSD", tc.expectedFundsToPerpEF.RoundInt())), + ).Return(nil) + } + if tc.expectedFundsToLiquidator.IsPositive() { + mocks.mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount( + ctx, types.VaultModuleAccount, liquidatorAddr, + sdk.NewCoins(sdk.NewCoin("NUSD", tc.expectedFundsToLiquidator.RoundInt())), + ).Return(nil) + } + expectedTotalBadDebtInt := tc.expectedLiquidationBadDebt.RoundInt() + if expectedTotalBadDebtInt.IsPositive() { + mocks.mockBankKeeper.EXPECT().SendCoinsFromModuleToModule( + ctx, types.PerpEFModuleAccount, types.VaultModuleAccount, + sdk.NewCoins(sdk.NewCoin("NUSD", expectedTotalBadDebtInt)), + ) + } + + t.Log("setup perp keeper params") + newParams := types.DefaultParams() + newParams.LiquidationFee = tc.liquidationFee + perpKeeper.SetParams(ctx, newParams) + perpKeeper.PairMetadata().Set(ctx, &types.PairMetadata{ + Pair: pair.String(), + CumulativePremiumFractions: []sdk.Dec{ + sdk.ZeroDec(), // zero funding payment for this test case + }, + }) + + t.Log("mock vpool") + mocks.mockVpoolKeeper.EXPECT().ExistsPool(ctx, pair).AnyTimes().Return(true) + mocks.mockVpoolKeeper.EXPECT(). + GetBaseAssetPrice( + ctx, + pair, + baseAssetDirection, + /*baseAssetAmount=*/ tc.initialPositionSize.Abs(), + ). + Return( /*quoteAssetAmount=*/ tc.baseAssetPriceInQuote, nil) + mocks.mockVpoolKeeper.EXPECT(). + SwapBaseForQuote( + ctx, + pair, + baseAssetDirection, + /*baseAssetAmount=*/ tc.initialPositionSize.Abs(), + /*quoteAssetAssetLimit=*/ sdk.ZeroDec(), + ).Return( /*quoteAssetAmount=*/ tc.baseAssetPriceInQuote, nil) + + t.Log("create and set the initial position") + position := types.Position{ + Address: traderAddr.String(), + Pair: pair.String(), + Size_: tc.initialPositionSize, + Margin: tc.initialMargin, + OpenNotional: tc.initialOpenNotional, + LastUpdateCumulativePremiumFraction: sdk.ZeroDec(), + LiquidityHistoryIndex: 0, + BlockNumber: ctx.BlockHeight(), + } + perpKeeper.SetPosition(ctx, pair, traderAddr.String(), &position) + + t.Log("execute full liquidation") + liquidationResp, err := perpKeeper.ExecuteFullLiquidation( + ctx, liquidatorAddr, &position) + require.NoError(t, err) + + t.Log("assert liquidation response fields") + assert.EqualValues(t, tc.expectedLiquidationBadDebt, liquidationResp.BadDebt) + assert.EqualValues(t, tc.expectedFundsToLiquidator, liquidationResp.FeeToLiquidator) + assert.EqualValues(t, tc.expectedFundsToPerpEF, liquidationResp.FeeToPerpEcosystemFund) + assert.EqualValues(t, liquidatorAddr, liquidationResp.Liquidator) + + t.Log("assert position response fields") + positionResp := liquidationResp.PositionResp + assert.EqualValues(t, + tc.expectedExchangedQuoteAssetAmount, + positionResp.ExchangedQuoteAssetAmount) // amount of quote exchanged + // Initial position size is sold back to to vpool + assert.EqualValues(t, tc.initialPositionSize.Neg(), positionResp.ExchangedPositionSize) + // ( oldMargin + unrealzedPnL - fundingPayment ) * -1 + assert.EqualValues(t, tc.expectedMarginToVault, positionResp.MarginToVault) + assert.EqualValues(t, tc.expectedPositionBadDebt, positionResp.BadDebt) + assert.EqualValues(t, tc.expectedPositionRealizedPnl, positionResp.RealizedPnl) + assert.True(t, positionResp.FundingPayment.IsZero()) + // Unrealized PnL should always be zero after a full close + assert.True(t, positionResp.UnrealizedPnlAfter.IsZero()) + + t.Log("assert new position fields") + newPosition := positionResp.Position + assert.EqualValues(t, traderAddr.String(), newPosition.Address) + assert.EqualValues(t, pair.String(), newPosition.Pair) + assert.True(t, newPosition.Size_.IsZero()) // always zero + assert.True(t, newPosition.Margin.IsZero()) // always zero + assert.True(t, newPosition.OpenNotional.IsZero()) // always zero + assert.True(t, newPosition.LastUpdateCumulativePremiumFraction.IsZero()) + assert.EqualValues(t, 0, newPosition.LiquidityHistoryIndex) + assert.EqualValues(t, ctx.BlockHeight(), newPosition.BlockNumber) + }) + } +} diff --git a/x/perp/keeper/withdraw.go b/x/perp/keeper/withdraw.go index b7f3b04ea..b32baecb1 100644 --- a/x/perp/keeper/withdraw.go +++ b/x/perp/keeper/withdraw.go @@ -66,3 +66,39 @@ func (k Keeper) Withdraw( ), ) } + +/* +Realizes the bad debt by first decrementing it from the prepaid bad debt. +Prepaid bad debt accrues when more coins are withdrawn from the vault than the +vault contains, so we "credit" ourselves with prepaid bad debt. + +Then, when bad debt is actually realized (by closing underwater positions), we +can consume the credit we have built before withdrawing more from the ecosystem fund. +*/ +func (k Keeper) realizeBadDebt(ctx sdk.Context, denom string, badDebtToRealize sdk.Int) ( + err error, +) { + prepaidBadDebtBalance := k.PrepaidBadDebtState().Get(ctx, denom) + + if prepaidBadDebtBalance.GTE(badDebtToRealize) { + // prepaidBadDebtBalance > totalBadDebt + k.PrepaidBadDebtState().Decrement(ctx, denom, badDebtToRealize) + } else { + // totalBadDebt > prepaidBadDebtBalance + + k.PrepaidBadDebtState().Set(ctx, denom, sdk.ZeroInt()) + + return k.BankKeeper.SendCoinsFromModuleToModule(ctx, + /*from=*/ types.PerpEFModuleAccount, + /*to=*/ types.VaultModuleAccount, + sdk.NewCoins( + sdk.NewCoin( + denom, + badDebtToRealize.Sub(prepaidBadDebtBalance), + ), + ), + ) + } + + return nil +} diff --git a/x/perp/keeper/withdraw_test.go b/x/perp/keeper/withdraw_test.go index 33e082f51..959ad3ffc 100644 --- a/x/perp/keeper/withdraw_test.go +++ b/x/perp/keeper/withdraw_test.go @@ -95,3 +95,71 @@ func TestWithdraw(t *testing.T) { }) } } + +func TestRealizeBadDebt(t *testing.T) { + tests := []struct { + name string + initialPrepaidBadDebt int64 + + badDebtToRealize int64 + + expectedPerpEFWithdrawal int64 + expectedFinalPrepaidBadDebt int64 + }{ + { + name: "prepaid bad debt completely covers bad debt to realize", + initialPrepaidBadDebt: 10, + + badDebtToRealize: 5, + + expectedPerpEFWithdrawal: 0, + expectedFinalPrepaidBadDebt: 5, + }, + { + name: "prepaid bad debt exactly covers bad debt to realize", + initialPrepaidBadDebt: 10, + + badDebtToRealize: 10, + + expectedPerpEFWithdrawal: 0, + expectedFinalPrepaidBadDebt: 0, + }, + { + name: "requires perpEF withdrawal", + initialPrepaidBadDebt: 5, + + badDebtToRealize: 10, + + expectedPerpEFWithdrawal: 5, + expectedFinalPrepaidBadDebt: 0, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Log("initialize variables") + perpKeeper, mocks, ctx := getKeeper(t) + denom := "NUSD" + + if tc.expectedPerpEFWithdrawal > 0 { + t.Log("mock bank keeper") + mocks.mockBankKeeper.EXPECT().SendCoinsFromModuleToModule( + ctx, types.PerpEFModuleAccount, types.VaultModuleAccount, + sdk.NewCoins(sdk.NewInt64Coin(denom, tc.expectedPerpEFWithdrawal)), + ).Return(nil) + } + + t.Log("initial prepaid bad debt") + perpKeeper.PrepaidBadDebtState().Set(ctx, denom, sdk.NewInt(tc.initialPrepaidBadDebt)) + + t.Log("execute withdrawal") + err := perpKeeper.realizeBadDebt(ctx, denom, sdk.NewInt(tc.badDebtToRealize)) + require.NoError(t, err) + + t.Log("assert new prepaid bad debt") + prepaidBadDebt := perpKeeper.PrepaidBadDebtState().Get(ctx, denom) + assert.EqualValues(t, tc.expectedFinalPrepaidBadDebt, prepaidBadDebt.Int64()) + }) + } +} diff --git a/x/perp/types/keys.go b/x/perp/types/keys.go index 6974cd652..9d22f53ab 100644 --- a/x/perp/types/keys.go +++ b/x/perp/types/keys.go @@ -4,6 +4,8 @@ var ( // StoreKey defines the primary module store key. StoreKey = ModuleName + MemStoreKey = "mem_perp" + // RouterKey is the message route for slashing. RouterKey = ModuleName diff --git a/x/perp/types/tx.pb.go b/x/perp/types/tx.pb.go index 5e632d60a..584e6b674 100644 --- a/x/perp/types/tx.pb.go +++ b/x/perp/types/tx.pb.go @@ -235,6 +235,105 @@ func (m *MsgAddMarginResponse) XXX_DiscardUnknown() { var xxx_messageInfo_MsgAddMarginResponse proto.InternalMessageInfo +type MsgLiquidate struct { + // Sender is the liquidator address + Sender string `protobuf:"bytes,1,opt,name=sender,proto3" json:"sender,omitempty"` + // TokenPair is the identifier for the position's virtual pool + TokenPair string `protobuf:"bytes,2,opt,name=token_pair,json=tokenPair,proto3" json:"token_pair,omitempty"` + // Trader is the address of the owner of the position + Trader string `protobuf:"bytes,3,opt,name=trader,proto3" json:"trader,omitempty"` +} + +func (m *MsgLiquidate) Reset() { *m = MsgLiquidate{} } +func (m *MsgLiquidate) String() string { return proto.CompactTextString(m) } +func (*MsgLiquidate) ProtoMessage() {} +func (*MsgLiquidate) Descriptor() ([]byte, []int) { + return fileDescriptor_28f06b306d51dcfb, []int{4} +} +func (m *MsgLiquidate) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgLiquidate) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgLiquidate.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgLiquidate) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgLiquidate.Merge(m, src) +} +func (m *MsgLiquidate) XXX_Size() int { + return m.Size() +} +func (m *MsgLiquidate) XXX_DiscardUnknown() { + xxx_messageInfo_MsgLiquidate.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgLiquidate proto.InternalMessageInfo + +func (m *MsgLiquidate) GetSender() string { + if m != nil { + return m.Sender + } + return "" +} + +func (m *MsgLiquidate) GetTokenPair() string { + if m != nil { + return m.TokenPair + } + return "" +} + +func (m *MsgLiquidate) GetTrader() string { + if m != nil { + return m.Trader + } + return "" +} + +type MsgLiquidateResponse struct { +} + +func (m *MsgLiquidateResponse) Reset() { *m = MsgLiquidateResponse{} } +func (m *MsgLiquidateResponse) String() string { return proto.CompactTextString(m) } +func (*MsgLiquidateResponse) ProtoMessage() {} +func (*MsgLiquidateResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_28f06b306d51dcfb, []int{5} +} +func (m *MsgLiquidateResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgLiquidateResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgLiquidateResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgLiquidateResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgLiquidateResponse.Merge(m, src) +} +func (m *MsgLiquidateResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgLiquidateResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgLiquidateResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgLiquidateResponse proto.InternalMessageInfo + type MsgOpenPosition struct { Sender string `protobuf:"bytes,1,opt,name=sender,proto3" json:"sender,omitempty"` TokenPair string `protobuf:"bytes,2,opt,name=token_pair,json=tokenPair,proto3" json:"token_pair,omitempty"` @@ -248,7 +347,7 @@ func (m *MsgOpenPosition) Reset() { *m = MsgOpenPosition{} } func (m *MsgOpenPosition) String() string { return proto.CompactTextString(m) } func (*MsgOpenPosition) ProtoMessage() {} func (*MsgOpenPosition) Descriptor() ([]byte, []int) { - return fileDescriptor_28f06b306d51dcfb, []int{4} + return fileDescriptor_28f06b306d51dcfb, []int{6} } func (m *MsgOpenPosition) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -305,7 +404,7 @@ func (m *MsgOpenPositionResponse) Reset() { *m = MsgOpenPositionResponse func (m *MsgOpenPositionResponse) String() string { return proto.CompactTextString(m) } func (*MsgOpenPositionResponse) ProtoMessage() {} func (*MsgOpenPositionResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_28f06b306d51dcfb, []int{5} + return fileDescriptor_28f06b306d51dcfb, []int{7} } func (m *MsgOpenPositionResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -339,6 +438,8 @@ func init() { proto.RegisterType((*MsgRemoveMarginResponse)(nil), "nibiru.perp.v1.MsgRemoveMarginResponse") proto.RegisterType((*MsgAddMargin)(nil), "nibiru.perp.v1.MsgAddMargin") proto.RegisterType((*MsgAddMarginResponse)(nil), "nibiru.perp.v1.MsgAddMarginResponse") + proto.RegisterType((*MsgLiquidate)(nil), "nibiru.perp.v1.MsgLiquidate") + proto.RegisterType((*MsgLiquidateResponse)(nil), "nibiru.perp.v1.MsgLiquidateResponse") proto.RegisterType((*MsgOpenPosition)(nil), "nibiru.perp.v1.MsgOpenPosition") proto.RegisterType((*MsgOpenPositionResponse)(nil), "nibiru.perp.v1.MsgOpenPositionResponse") } @@ -346,46 +447,48 @@ func init() { func init() { proto.RegisterFile("perp/v1/tx.proto", fileDescriptor_28f06b306d51dcfb) } var fileDescriptor_28f06b306d51dcfb = []byte{ - // 612 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x54, 0x4f, 0x6b, 0x13, 0x4f, - 0x18, 0xce, 0xb6, 0xfd, 0x85, 0x5f, 0xc6, 0xd2, 0x96, 0x31, 0xb4, 0xdb, 0x50, 0x37, 0x65, 0x11, - 0x2d, 0x82, 0x33, 0xa4, 0x1e, 0xbc, 0x09, 0xfd, 0x73, 0x51, 0x8c, 0x0d, 0xeb, 0x41, 0x28, 0xc2, - 0x32, 0xc9, 0xbe, 0x6e, 0x87, 0x66, 0x67, 0xd6, 0x9d, 0xd9, 0xd0, 0x82, 0x08, 0xea, 0x17, 0x10, - 0xfc, 0x26, 0x7e, 0x8a, 0x1e, 0x2b, 0x5e, 0xc4, 0x43, 0x91, 0xc4, 0x0f, 0x22, 0x3b, 0xbb, 0x89, - 0x49, 0x08, 0xfe, 0xc9, 0xc5, 0x53, 0x26, 0xf3, 0x3e, 0xf3, 0xbc, 0xcf, 0x3c, 0xef, 0x33, 0x8b, - 0xd6, 0x62, 0x48, 0x62, 0xda, 0x6b, 0x50, 0x7d, 0x46, 0xe2, 0x44, 0x6a, 0x89, 0x57, 0x04, 0x6f, - 0xf3, 0x24, 0x25, 0x59, 0x81, 0xf4, 0x1a, 0xb5, 0xad, 0x50, 0xca, 0xb0, 0x0b, 0x94, 0xc5, 0x9c, - 0x32, 0x21, 0xa4, 0x66, 0x9a, 0x4b, 0xa1, 0x72, 0x74, 0xcd, 0xe9, 0x48, 0x15, 0x49, 0x45, 0xdb, - 0x4c, 0x01, 0xed, 0x35, 0xda, 0xa0, 0x59, 0x83, 0x76, 0x24, 0x17, 0x45, 0xbd, 0x1a, 0xca, 0x50, - 0x9a, 0x25, 0xcd, 0x56, 0xc5, 0xee, 0xf5, 0x61, 0x57, 0xa5, 0x99, 0x86, 0x7c, 0xd3, 0x7d, 0x63, - 0xa1, 0xd5, 0xa6, 0x0a, 0x3d, 0x88, 0x64, 0x0f, 0x9a, 0x2c, 0x09, 0xb9, 0xc0, 0xeb, 0xa8, 0xac, - 0x40, 0x04, 0x90, 0xd8, 0xd6, 0xb6, 0xb5, 0x53, 0xf1, 0x8a, 0x7f, 0xf8, 0x06, 0x42, 0x5a, 0x9e, - 0x82, 0xf0, 0x63, 0xc6, 0x13, 0x7b, 0xc1, 0xd4, 0x2a, 0x66, 0xa7, 0xc5, 0x78, 0x82, 0xef, 0xa3, - 0x72, 0x64, 0x08, 0xec, 0xc5, 0x6d, 0x6b, 0xe7, 0xda, 0xee, 0x26, 0xc9, 0x65, 0x92, 0x4c, 0x26, - 0x29, 0x64, 0x92, 0x03, 0xc9, 0xc5, 0xfe, 0xd2, 0xc5, 0x55, 0xbd, 0xe4, 0x15, 0x70, 0xf7, 0xa3, - 0x85, 0x36, 0xa6, 0x34, 0x78, 0xa0, 0x62, 0x29, 0x14, 0xe0, 0x07, 0x08, 0xe5, 0x28, 0x5f, 0xa6, - 0xda, 0xe8, 0xf9, 0x03, 0xe2, 0x4a, 0x7e, 0xe4, 0x28, 0xd5, 0xf8, 0x19, 0x5a, 0x7d, 0x91, 0x8a, - 0x80, 0x8b, 0xd0, 0x8f, 0xd9, 0x79, 0x04, 0x42, 0xe7, 0xc2, 0xf7, 0x49, 0x86, 0xfc, 0x7a, 0x55, - 0xbf, 0x15, 0x72, 0x7d, 0x92, 0xb6, 0x49, 0x47, 0x46, 0xb4, 0xb0, 0x35, 0xff, 0xb9, 0xab, 0x82, - 0x53, 0xaa, 0xcf, 0x63, 0x50, 0xe4, 0x10, 0x3a, 0xde, 0x4a, 0x41, 0xd3, 0xca, 0x59, 0xdc, 0xd7, - 0x68, 0xb9, 0xa9, 0xc2, 0xbd, 0x20, 0xf8, 0x47, 0xa6, 0xad, 0xa3, 0xea, 0x78, 0xff, 0xa1, 0x61, - 0xee, 0xbb, 0x45, 0x33, 0xd0, 0xa3, 0x18, 0x44, 0x4b, 0x2a, 0x9e, 0xc5, 0x66, 0x5e, 0x6d, 0x3b, - 0x68, 0x49, 0xf1, 0x00, 0x8c, 0xb2, 0x95, 0xdd, 0x2a, 0x99, 0xcc, 0x28, 0x79, 0xca, 0x03, 0xf0, - 0x0c, 0x02, 0x3f, 0x47, 0xf8, 0x65, 0x2a, 0x35, 0xf8, 0x4c, 0x29, 0xd0, 0x3e, 0x8b, 0x64, 0x2a, - 0xb4, 0xbd, 0xf4, 0xd7, 0x46, 0x3f, 0x14, 0xda, 0x5b, 0x33, 0x4c, 0x7b, 0x19, 0xd1, 0x9e, 0xe1, - 0xc1, 0x8f, 0xd0, 0xff, 0x5d, 0xe8, 0x41, 0xc2, 0x42, 0xb0, 0xff, 0x9b, 0x6b, 0x78, 0xa3, 0xf3, - 0x18, 0xd0, 0x46, 0xe6, 0xec, 0x84, 0x50, 0xbf, 0xcb, 0x23, 0xae, 0xed, 0xf2, 0x5c, 0x72, 0xab, - 0x19, 0xdd, 0x98, 0xda, 0xc7, 0x19, 0x97, 0xbb, 0x69, 0x12, 0x3d, 0x3e, 0x84, 0xe1, 0x80, 0x76, - 0x3f, 0x2d, 0xa0, 0xc5, 0xa6, 0x0a, 0xf1, 0x2b, 0xb4, 0x3c, 0xf1, 0xea, 0xea, 0xd3, 0xfe, 0x4e, - 0x3d, 0x89, 0xda, 0xed, 0xdf, 0x00, 0x46, 0x11, 0x70, 0xdf, 0x7e, 0xfe, 0xfe, 0x61, 0x61, 0xcb, - 0xad, 0xd1, 0xfc, 0x00, 0x35, 0x0f, 0x3f, 0x31, 0x50, 0x3f, 0x8f, 0x0f, 0x8e, 0x51, 0xe5, 0x67, - 0x76, 0xb7, 0x66, 0x30, 0x8f, 0xaa, 0xb5, 0x9b, 0xbf, 0xaa, 0x8e, 0x9a, 0xd6, 0x4d, 0xd3, 0x4d, - 0x77, 0x63, 0xa2, 0x29, 0x0b, 0x82, 0x61, 0xc7, 0x63, 0xb4, 0x3c, 0x11, 0xca, 0x59, 0xf7, 0x1d, - 0x07, 0xcc, 0xbc, 0xef, 0x2c, 0x47, 0xdd, 0xd2, 0xfe, 0xe1, 0x45, 0xdf, 0xb1, 0x2e, 0xfb, 0x8e, - 0xf5, 0xad, 0xef, 0x58, 0xef, 0x07, 0x4e, 0xe9, 0x72, 0xe0, 0x94, 0xbe, 0x0c, 0x9c, 0xd2, 0xf1, - 0x9d, 0xb1, 0x31, 0x3e, 0x31, 0x74, 0x07, 0x27, 0x8c, 0x8b, 0xa1, 0xc8, 0xb3, 0x5c, 0xa6, 0x19, - 0x67, 0xbb, 0x6c, 0x3e, 0x89, 0xf7, 0x7e, 0x04, 0x00, 0x00, 0xff, 0xff, 0xb1, 0x1c, 0x03, 0xb7, - 0x9f, 0x05, 0x00, 0x00, + // 648 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x54, 0xdd, 0x6a, 0x13, 0x4d, + 0x18, 0xce, 0x36, 0xfd, 0xc2, 0x97, 0xb1, 0xb4, 0x65, 0x0c, 0x6d, 0x1a, 0xea, 0xa6, 0x2c, 0xa2, + 0x45, 0x70, 0x86, 0xd4, 0x03, 0xcf, 0x84, 0xfe, 0x9c, 0x28, 0x8d, 0x2d, 0xeb, 0x81, 0x20, 0xca, + 0x32, 0xc9, 0xbe, 0x6e, 0x87, 0x76, 0x67, 0xb6, 0x3b, 0xb3, 0xa1, 0x85, 0x22, 0xa8, 0x37, 0x20, + 0x78, 0x27, 0x5e, 0x45, 0x0f, 0x0b, 0x9e, 0x88, 0x07, 0x45, 0x5a, 0xef, 0xc0, 0x1b, 0x90, 0x9d, + 0xd9, 0xc4, 0xa4, 0xc4, 0xbf, 0x9c, 0x78, 0xb4, 0xbb, 0xf3, 0x3e, 0xef, 0xf3, 0x3c, 0xf3, 0xfe, + 0x2c, 0x9a, 0x4f, 0x20, 0x4d, 0x68, 0xaf, 0x45, 0xf5, 0x11, 0x49, 0x52, 0xa9, 0x25, 0x9e, 0x15, + 0xbc, 0xc3, 0xd3, 0x8c, 0xe4, 0x01, 0xd2, 0x6b, 0x35, 0x96, 0x23, 0x29, 0xa3, 0x03, 0xa0, 0x2c, + 0xe1, 0x94, 0x09, 0x21, 0x35, 0xd3, 0x5c, 0x0a, 0x65, 0xd1, 0x0d, 0xb7, 0x2b, 0x55, 0x2c, 0x15, + 0xed, 0x30, 0x05, 0xb4, 0xd7, 0xea, 0x80, 0x66, 0x2d, 0xda, 0x95, 0x5c, 0x14, 0xf1, 0x5a, 0x24, + 0x23, 0x69, 0x5e, 0x69, 0xfe, 0x56, 0x9c, 0x5e, 0xef, 0xab, 0x2a, 0xcd, 0x34, 0xd8, 0x43, 0xef, + 0xb5, 0x83, 0xe6, 0xda, 0x2a, 0xf2, 0x21, 0x96, 0x3d, 0x68, 0xb3, 0x34, 0xe2, 0x02, 0x2f, 0xa0, + 0x8a, 0x02, 0x11, 0x42, 0x5a, 0x77, 0x56, 0x9c, 0xd5, 0xaa, 0x5f, 0x7c, 0xe1, 0x1b, 0x08, 0x69, + 0xb9, 0x0f, 0x22, 0x48, 0x18, 0x4f, 0xeb, 0x53, 0x26, 0x56, 0x35, 0x27, 0xbb, 0x8c, 0xa7, 0xf8, + 0x3e, 0xaa, 0xc4, 0x86, 0xa0, 0x5e, 0x5e, 0x71, 0x56, 0xaf, 0xad, 0x2d, 0x11, 0x6b, 0x93, 0xe4, + 0x36, 0x49, 0x61, 0x93, 0x6c, 0x4a, 0x2e, 0x36, 0xa6, 0x4f, 0xcf, 0x9b, 0x25, 0xbf, 0x80, 0x7b, + 0x1f, 0x1c, 0xb4, 0x78, 0xc5, 0x83, 0x0f, 0x2a, 0x91, 0x42, 0x01, 0x7e, 0x80, 0x90, 0x45, 0x05, + 0x32, 0xd3, 0xc6, 0xcf, 0x1f, 0x10, 0x57, 0x6d, 0xca, 0x4e, 0xa6, 0xf1, 0x53, 0x34, 0xf7, 0x32, + 0x13, 0x21, 0x17, 0x51, 0x90, 0xb0, 0xe3, 0x18, 0x84, 0xb6, 0xc6, 0x37, 0x48, 0x8e, 0xfc, 0x7c, + 0xde, 0xbc, 0x15, 0x71, 0xbd, 0x97, 0x75, 0x48, 0x57, 0xc6, 0xb4, 0x28, 0xab, 0x7d, 0xdc, 0x55, + 0xe1, 0x3e, 0xd5, 0xc7, 0x09, 0x28, 0xb2, 0x05, 0x5d, 0x7f, 0xb6, 0xa0, 0xd9, 0xb5, 0x2c, 0xde, + 0x2b, 0x34, 0xd3, 0x56, 0xd1, 0x7a, 0x18, 0xfe, 0xa3, 0xa2, 0x2d, 0xa0, 0xda, 0xb0, 0x7e, 0xbf, + 0x60, 0xde, 0x0b, 0xe3, 0x6b, 0x9b, 0x1f, 0x66, 0x3c, 0x64, 0x1a, 0x26, 0xf5, 0xb5, 0x80, 0x2a, + 0x3a, 0x65, 0x79, 0x5a, 0xd9, 0xa6, 0xd9, 0xaf, 0x42, 0x76, 0x40, 0x3f, 0x90, 0x7d, 0x5b, 0x36, + 0x73, 0xb4, 0x93, 0x80, 0xd8, 0x95, 0x8a, 0xe7, 0xd3, 0x3a, 0xa9, 0xf4, 0x2a, 0x9a, 0x56, 0x3c, + 0x04, 0x23, 0x3c, 0xbb, 0x56, 0x23, 0xa3, 0xab, 0x41, 0x9e, 0xf0, 0x10, 0x7c, 0x83, 0xc0, 0xcf, + 0x11, 0x3e, 0xcc, 0xa4, 0x86, 0x80, 0x29, 0x05, 0x3a, 0x60, 0xb1, 0xcc, 0x84, 0xae, 0x4f, 0xff, + 0x75, 0x7f, 0x1f, 0x0a, 0xed, 0xcf, 0x1b, 0xa6, 0xf5, 0x9c, 0x68, 0xdd, 0xf0, 0xe0, 0x47, 0xe8, + 0xff, 0x03, 0xe8, 0x41, 0xca, 0x22, 0xa8, 0xff, 0x37, 0xd1, 0xcc, 0x0c, 0xf2, 0x31, 0xa0, 0xc5, + 0xbc, 0xa1, 0x23, 0x46, 0x83, 0x03, 0x1e, 0x73, 0x5d, 0xaf, 0x4c, 0x64, 0xb7, 0x96, 0xd3, 0x0d, + 0xb9, 0xdd, 0xce, 0xb9, 0xbc, 0x25, 0xb3, 0x48, 0xc3, 0x4d, 0xe8, 0x37, 0x68, 0xed, 0xdb, 0x14, + 0x2a, 0xb7, 0x55, 0x84, 0x4f, 0xd0, 0xcc, 0xc8, 0xb2, 0x37, 0xaf, 0xd6, 0xf7, 0xca, 0x26, 0x36, + 0x6e, 0xff, 0x06, 0x30, 0x18, 0x01, 0xef, 0xcd, 0xc7, 0xaf, 0xef, 0xa7, 0x96, 0xbd, 0x06, 0xb5, + 0x09, 0xd4, 0xfc, 0x6f, 0x52, 0x03, 0x0d, 0xec, 0xd4, 0xe2, 0x04, 0x55, 0x7f, 0xac, 0xcc, 0xf2, + 0x18, 0xe6, 0x41, 0xb4, 0x71, 0xf3, 0x57, 0xd1, 0x81, 0x68, 0xd3, 0x88, 0x2e, 0x79, 0x8b, 0x23, + 0xa2, 0x2c, 0x0c, 0xfb, 0x8a, 0x27, 0x68, 0x66, 0x64, 0x28, 0xc7, 0xdd, 0x77, 0x18, 0x30, 0xf6, + 0xbe, 0xe3, 0x2a, 0xfa, 0x93, 0xfb, 0xca, 0x24, 0x1f, 0xec, 0x02, 0xbb, 0xb1, 0x75, 0x7a, 0xe1, + 0x3a, 0x67, 0x17, 0xae, 0xf3, 0xe5, 0xc2, 0x75, 0xde, 0x5d, 0xba, 0xa5, 0xb3, 0x4b, 0xb7, 0xf4, + 0xe9, 0xd2, 0x2d, 0x3d, 0xbb, 0x33, 0xd4, 0xe8, 0xc7, 0x26, 0x7f, 0x73, 0x8f, 0x71, 0xd1, 0xe7, + 0x3a, 0xb2, 0x6c, 0xa6, 0xe1, 0x9d, 0x8a, 0xf9, 0x57, 0xdf, 0xfb, 0x1e, 0x00, 0x00, 0xff, 0xff, + 0xf0, 0x7d, 0x2f, 0x27, 0x38, 0x06, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -700,6 +803,73 @@ func (m *MsgAddMarginResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *MsgLiquidate) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgLiquidate) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgLiquidate) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Trader) > 0 { + i -= len(m.Trader) + copy(dAtA[i:], m.Trader) + i = encodeVarintTx(dAtA, i, uint64(len(m.Trader))) + i-- + dAtA[i] = 0x1a + } + if len(m.TokenPair) > 0 { + i -= len(m.TokenPair) + copy(dAtA[i:], m.TokenPair) + i = encodeVarintTx(dAtA, i, uint64(len(m.TokenPair))) + i-- + dAtA[i] = 0x12 + } + if len(m.Sender) > 0 { + i -= len(m.Sender) + copy(dAtA[i:], m.Sender) + i = encodeVarintTx(dAtA, i, uint64(len(m.Sender))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *MsgLiquidateResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgLiquidateResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgLiquidateResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + func (m *MsgOpenPosition) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -866,6 +1036,36 @@ func (m *MsgAddMarginResponse) Size() (n int) { return n } +func (m *MsgLiquidate) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Sender) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = len(m.TokenPair) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = len(m.Trader) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + return n +} + +func (m *MsgLiquidateResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + func (m *MsgOpenPosition) Size() (n int) { if m == nil { return 0 @@ -1368,6 +1568,202 @@ func (m *MsgAddMarginResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *MsgLiquidate) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgLiquidate: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgLiquidate: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Sender", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Sender = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TokenPair", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.TokenPair = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Trader", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Trader = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgLiquidateResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgLiquidateResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgLiquidateResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *MsgOpenPosition) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/x/perp/types/tx.pb.gw.go b/x/perp/types/tx.pb.gw.go index 711873528..4d1cc90f5 100644 --- a/x/perp/types/tx.pb.gw.go +++ b/x/perp/types/tx.pb.gw.go @@ -103,6 +103,42 @@ func local_request_Msg_AddMargin_0(ctx context.Context, marshaler runtime.Marsha } +var ( + filter_Msg_OpenPosition_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_Msg_OpenPosition_0(ctx context.Context, marshaler runtime.Marshaler, client MsgClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq MsgOpenPosition + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Msg_OpenPosition_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.OpenPosition(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Msg_OpenPosition_0(ctx context.Context, marshaler runtime.Marshaler, server MsgServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq MsgOpenPosition + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Msg_OpenPosition_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.OpenPosition(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterMsgHandlerServer registers the http handlers for service Msg to "mux". // UnaryRPC :call MsgServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -149,6 +185,26 @@ func RegisterMsgHandlerServer(ctx context.Context, mux *runtime.ServeMux, server }) + mux.Handle("POST", pattern_Msg_OpenPosition_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Msg_OpenPosition_0(rctx, inboundMarshaler, server, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Msg_OpenPosition_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -230,6 +286,26 @@ func RegisterMsgHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) + mux.Handle("POST", pattern_Msg_OpenPosition_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Msg_OpenPosition_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Msg_OpenPosition_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -237,10 +313,14 @@ var ( pattern_Msg_RemoveMargin_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"nibiru", "perp", "remove_margin"}, "", runtime.AssumeColonVerbOpt(false))) pattern_Msg_AddMargin_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"nibiru", "perp", "add_margin"}, "", runtime.AssumeColonVerbOpt(false))) + + pattern_Msg_OpenPosition_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"nibiru", "perp", "open_position"}, "", runtime.AssumeColonVerbOpt(false))) ) var ( forward_Msg_RemoveMargin_0 = runtime.ForwardResponseMessage forward_Msg_AddMargin_0 = runtime.ForwardResponseMessage + + forward_Msg_OpenPosition_0 = runtime.ForwardResponseMessage )