diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ba147f7e..224f46da9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug Fixes * [#766](https://github.com/NibiruChain/nibiru/pull/766) Fixed margin ratio calculation for trader position. +* [#776](https://github.com/NibiruChain/nibiru/pull/776) - Fix a bug where the user could open infinite leverage positions ## [v0.11.0](https://github.com/NibiruChain/nibiru/releases/tag/v0.11.0) - 2022-07-29 diff --git a/x/perp/keeper/clearing_house.go b/x/perp/keeper/clearing_house.go index 6fb5c2cf6..85e9e4edb 100644 --- a/x/perp/keeper/clearing_house.go +++ b/x/perp/keeper/clearing_house.go @@ -129,7 +129,7 @@ func (k Keeper) afterPositionUpdate( return fmt.Errorf("bad debt must be zero to prevent attacker from leveraging it") } - if !isNewPosition && !positionResp.Position.Size_.IsZero() { + if !positionResp.Position.Size_.IsZero() { marginRatio, err := k.GetMarginRatio( ctx, *positionResp.Position, @@ -141,7 +141,7 @@ func (k Keeper) afterPositionUpdate( maintenanceMarginRatio := k.VpoolKeeper.GetMaintenanceMarginRatio(ctx, pair) if err = requireMoreMarginRatio(marginRatio, maintenanceMarginRatio, true); err != nil { - return err + return types.ErrMarginRatioTooLow } } diff --git a/x/perp/keeper/clearing_house_integration_test.go b/x/perp/keeper/clearing_house_integration_test.go index 5ef243cdf..cc4c50055 100644 --- a/x/perp/keeper/clearing_house_integration_test.go +++ b/x/perp/keeper/clearing_house_integration_test.go @@ -1,7 +1,6 @@ package keeper_test import ( - "fmt" "testing" "time" @@ -357,7 +356,7 @@ func TestOpenPositionError(t *testing.T) { margin: sdk.NewInt(1), leverage: sdk.OneDec(), baseLimit: sdk.ZeroDec(), - expectedErr: fmt.Errorf("margin ratio did not meet criteria"), + expectedErr: types.ErrMarginRatioTooLow, }, { name: "new long position not over base limit", @@ -399,6 +398,26 @@ func TestOpenPositionError(t *testing.T) { baseLimit: sdk.NewDec(10_000), expectedErr: types.ErrLeverageIsZero, }, + { + name: "leverage amount is too high - SELL", + traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 1020)), + initialPosition: nil, + side: types.Side_SELL, + margin: sdk.NewInt(100), + leverage: sdk.NewDec(100), + baseLimit: sdk.NewDec(11_000), + expectedErr: types.ErrMarginRatioTooLow, + }, + { + name: "leverage amount is too high - BUY", + traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 1020)), + initialPosition: nil, + side: types.Side_BUY, + margin: sdk.NewInt(100), + leverage: sdk.NewDec(100), + baseLimit: sdk.NewDec(0), + expectedErr: types.ErrMarginRatioTooLow, + }, { name: "new long position over fluctuation limit", traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 1_000_000_000_000)), diff --git a/x/perp/keeper/liquidate_test.go b/x/perp/keeper/liquidate_test.go index 286557143..b46cbf07e 100644 --- a/x/perp/keeper/liquidate_test.go +++ b/x/perp/keeper/liquidate_test.go @@ -31,7 +31,6 @@ func TestExecuteFullLiquidation(t *testing.T) { traderFunds sdk.Coin expectedLiquidatorBalance sdk.Coin expectedPerpEFBalance sdk.Coin - expectedBadDebt sdk.Dec } testCases := map[string]test{ @@ -52,7 +51,6 @@ func TestExecuteFullLiquidation(t *testing.T) { // startingBalance = 1_000_000 // perpEFBalance = startingBalance + openPositionDelta + liquidateDelta expectedPerpEFBalance: sdk.NewInt64Coin("NUSD", 1_047_550), - expectedBadDebt: sdk.MustNewDecFromStr("0"), }, "happy path - Sell": { positionSide: types.Side_SELL, @@ -71,45 +69,6 @@ func TestExecuteFullLiquidation(t *testing.T) { // startingBalance = 1_000_000 // perpEFBalance = startingBalance + openPositionDelta + liquidateDelta expectedPerpEFBalance: sdk.NewInt64Coin("NUSD", 1_046_972), - expectedBadDebt: sdk.MustNewDecFromStr("0"), - }, - "happy path - bad debt, long": { - /* 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). - */ - 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.ExchangedNotionalValue * liquidationFee / 2 - // = 500_000 * 0.1 / 2 = 25_000 - expectedLiquidatorBalance: sdk.NewInt64Coin("NUSD", 25_000), - // startingBalance = 1_000_000 - // perpEFBalance = startingBalance + openPositionDelta + liquidateDelta - expectedPerpEFBalance: sdk.NewInt64Coin("NUSD", 975_550), - expectedBadDebt: sdk.MustNewDecFromStr("24950"), - }, - "happy path - bad debt, short": { - // Same as above case but for shorts - 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.ExchangedNotionalValue * liquidationFee / 2 - // = 500_000 * 0.1 / 2 = 25_000 - expectedLiquidatorBalance: sdk.NewInt64Coin("NUSD", 25_000), - // startingBalance = 1_000_000 - // perpEFBalance = startingBalance + openPositionDelta + liquidateDelta - expectedPerpEFBalance: sdk.NewInt64Coin("NUSD", 975_550), - expectedBadDebt: sdk.MustNewDecFromStr("24950"), }, } @@ -156,6 +115,10 @@ func TestExecuteFullLiquidation(t *testing.T) { sdk.NewCoins(tc.traderFunds)) require.NoError(t, err) + t.Log("increment block height and time for TWAP calculation") + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1). + WithBlockTime(time.Now().Add(time.Minute)) + t.Log("Open position") positionResp, err := nibiruApp.PerpKeeper.OpenPosition( ctx, tokenPair, tc.positionSide, traderAddr, tc.quoteAmount, tc.leverage, tc.baseAssetLimit) @@ -232,7 +195,6 @@ func TestExecutePartialLiquidation(t *testing.T) { expectedLiquidatorBalance sdk.Coin expectedPerpEFBalance sdk.Coin - expectedBadDebt sdk.Dec expectedPositionSize sdk.Dec expectedMarginRemaining sdk.Dec }{ @@ -257,7 +219,6 @@ func TestExecutePartialLiquidation(t *testing.T) { // startingBalance = 1_000_000 // perpEFBalance = startingBalance + openPositionDelta + liquidateDelta expectedPerpEFBalance: sdk.NewInt64Coin("yyy", 1_001_050), - expectedBadDebt: sdk.MustNewDecFromStr("0"), }, { name: "happy path - Sell", @@ -282,7 +243,6 @@ func TestExecutePartialLiquidation(t *testing.T) { // startingBalance = 1_000_000 // perpEFBalance = startingBalance + openPositionDelta + liquidateDelta expectedPerpEFBalance: sdk.NewInt64Coin("yyy", 1_001_050), - expectedBadDebt: sdk.MustNewDecFromStr("0"), }, } diff --git a/x/perp/keeper/msg_server_test.go b/x/perp/keeper/msg_server_test.go index f034bcef3..04e9276ea 100644 --- a/x/perp/keeper/msg_server_test.go +++ b/x/perp/keeper/msg_server_test.go @@ -288,6 +288,10 @@ func TestMsgServerOpenPosition(t *testing.T) { require.NoError(t, simapp.FundAccount(app.BankKeeper, ctx, traderAddr, tc.traderFunds)) } + t.Log("increment block height and time for TWAP calculation") + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1). + WithBlockTime(time.Now().Add(time.Minute)) + resp, err := msgServer.OpenPosition(sdk.WrapSDKContext(ctx), &types.MsgOpenPosition{ Sender: tc.sender, TokenPair: tc.pair, diff --git a/x/perp/keeper/perp_test.go b/x/perp/keeper/perp_test.go index e6087f918..f45cc2028 100644 --- a/x/perp/keeper/perp_test.go +++ b/x/perp/keeper/perp_test.go @@ -176,6 +176,7 @@ func TestKeeperClosePosition(t *testing.T) { ) t.Log("open position for alice - long") + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1).WithBlockTime(time.Now().Add(time.Minute)) alice := sample.AccAddress() err := simapp.FundAccount(nibiruApp.BankKeeper, ctx, alice, @@ -206,6 +207,7 @@ func TestKeeperClosePosition(t *testing.T) { bobQuote := sdk.NewInt(60) bobLeverage := sdk.NewDec(10) bobBaseLimit := sdk.NewDec(150) + _, err = nibiruApp.PerpKeeper.OpenPosition( ctx, pair, bobSide, bob, bobQuote, bobLeverage, bobBaseLimit) require.NoError(t, err) diff --git a/x/perp/types/types.go b/x/perp/types/types.go index 779ed632e..69558991d 100644 --- a/x/perp/types/types.go +++ b/x/perp/types/types.go @@ -26,6 +26,7 @@ var ( ErrFailedRemoveMarginCanCauseBadDebt = sdkerrors.Register(ModuleName, 7, "failed to remove margin; position would have bad debt if removed") ErrQuoteAmountIsZero = sdkerrors.Register(ModuleName, 8, "quote amount cannot be zero") ErrLeverageIsZero = sdkerrors.Register(ModuleName, 9, "leverage cannot be zero") + ErrMarginRatioTooLow = sdkerrors.Register(ModuleName, 10, "margin ratio did not meet maintenance margin ratio") ) func ZeroPosition(ctx sdk.Context, tokenPair common.AssetPair, traderAddr sdk.AccAddress) *Position {