Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(perp): Block users from opening infinite leverage positions #776

Merged
merged 9 commits into from
Aug 3, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions x/perp/keeper/clearing_house.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}
}

Expand Down
23 changes: 21 additions & 2 deletions x/perp/keeper/clearing_house_integration_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package keeper_test

import (
"fmt"
"testing"
"time"

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)),
Expand Down
48 changes: 4 additions & 44 deletions x/perp/keeper/liquidate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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,
Expand All @@ -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"),
},
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -232,7 +195,6 @@ func TestExecutePartialLiquidation(t *testing.T) {

expectedLiquidatorBalance sdk.Coin
expectedPerpEFBalance sdk.Coin
expectedBadDebt sdk.Dec
expectedPositionSize sdk.Dec
expectedMarginRemaining sdk.Dec
}{
Expand All @@ -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",
Expand All @@ -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"),
},
}

Expand Down
4 changes: 4 additions & 0 deletions x/perp/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions x/perp/keeper/perp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions x/perp/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down