diff --git a/x/perp/v2/integration/action/settlement.go b/x/perp/v2/integration/action/settlement.go index b55b94c50..742e9c618 100644 --- a/x/perp/v2/integration/action/settlement.go +++ b/x/perp/v2/integration/action/settlement.go @@ -1,13 +1,17 @@ package action import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/NibiruChain/nibiru/app" "github.com/NibiruChain/nibiru/x/common/asset" "github.com/NibiruChain/nibiru/x/common/testutil/action" + "github.com/NibiruChain/nibiru/x/perp/v2/types" ) +// closeMarket type closeMarket struct { pair asset.Pair } @@ -25,25 +29,62 @@ func CloseMarket(pair asset.Pair) action.Action { return closeMarket{pair: pair} } +// settlePosition type settlePosition struct { - pair asset.Pair - version uint64 - trader sdk.AccAddress + pair asset.Pair + version uint64 + trader sdk.AccAddress + responseCheckers []SettlePositionChecker } func (c settlePosition) Do(app *app.NibiruApp, ctx sdk.Context) (sdk.Context, error, bool) { - _, err := app.PerpKeeperV2.SettlePosition(ctx, c.pair, c.version, c.trader) + resp, err := app.PerpKeeperV2.SettlePosition(ctx, c.pair, c.version, c.trader) if err != nil { return ctx, err, false } + for _, checker := range c.responseCheckers { + if err := checker(*resp); err != nil { + return ctx, err, false + } + } + return ctx, nil, true } -func SettlePosition(pair asset.Pair, version uint64, trader sdk.AccAddress) action.Action { - return settlePosition{pair: pair, version: version, trader: trader} +func SettlePosition(pair asset.Pair, version uint64, trader sdk.AccAddress, responseCheckers ...SettlePositionChecker) action.Action { + return settlePosition{pair: pair, version: version, trader: trader, responseCheckers: responseCheckers} +} + +type SettlePositionChecker func(resp types.PositionResp) error + +func SettlePositionChecker_PositionEquals(expected types.Position) SettlePositionChecker { + return func(resp types.PositionResp) error { + return types.PositionsAreEqual(&expected, &resp.Position) + } +} + +func SettlePositionChecker_MarginToVault(expectedMarginToVault sdk.Dec) SettlePositionChecker { + return func(resp types.PositionResp) error { + if expectedMarginToVault.Equal(resp.MarginToVault) { + return nil + } else { + return fmt.Errorf("expected margin to vault %s, got %s", expectedMarginToVault, resp.MarginToVault) + } + } +} + +func SettlePositionChecker_BadDebt(expectedBadDebt sdk.Dec) SettlePositionChecker { + return func(resp types.PositionResp) error { + if expectedBadDebt.Equal(resp.BadDebt) { + return nil + } else { + return fmt.Errorf("expected bad debt %s, got %s", expectedBadDebt, resp.BadDebt) + } + } } +// settlePositionShouldFail type settlePositionShouldFail struct { pair asset.Pair version uint64 diff --git a/x/perp/v2/keeper/settlement.go b/x/perp/v2/keeper/settlement.go index 0e632d167..62df34791 100644 --- a/x/perp/v2/keeper/settlement.go +++ b/x/perp/v2/keeper/settlement.go @@ -90,7 +90,7 @@ func (k Keeper) SettlePosition(ctx sdk.Context, pair asset.Pair, version uint64, // Settles a position and realizes PnL and funding payments. // Returns the updated AMM and the realized PnL and funding payments. func (k Keeper) settlePosition(ctx sdk.Context, market types.Market, amm types.AMM, position types.Position) (updatedAMM *types.AMM, resp *types.PositionResp, err error) { - positionNotional := position.Size_.Mul(amm.SettlementPrice) + positionNotional := position.Size_.Abs().Mul(amm.SettlementPrice) resp = &types.PositionResp{ ExchangedPositionSize: position.Size_.Neg(), diff --git a/x/perp/v2/keeper/settlement_test.go b/x/perp/v2/keeper/settlement_test.go index 8ad89bb73..c2e9cb15b 100644 --- a/x/perp/v2/keeper/settlement_test.go +++ b/x/perp/v2/keeper/settlement_test.go @@ -10,6 +10,7 @@ import ( "github.com/NibiruChain/nibiru/x/common/denoms" "github.com/NibiruChain/nibiru/x/common/testutil" . "github.com/NibiruChain/nibiru/x/common/testutil/action" + . "github.com/NibiruChain/nibiru/x/common/testutil/assertion" . "github.com/NibiruChain/nibiru/x/perp/v2/integration/action" . "github.com/NibiruChain/nibiru/x/perp/v2/integration/assertion" "github.com/NibiruChain/nibiru/x/perp/v2/types" @@ -120,6 +121,7 @@ func TestSettlePosition(t *testing.T) { startTime := time.Now() alice := testutil.AccAddress() + bob := testutil.AccAddress() tc := TestCases{ TC("Happy path").When( @@ -146,6 +148,74 @@ func TestSettlePosition(t *testing.T) { PositionShouldNotExist(alice, pairBtcUsdc, 1), ), + TC("Happy path, but with bad debt").When( + CreateCustomMarket( + pairBtcUsdc, + WithPricePeg(sdk.OneDec()), + WithSqrtDepth(sdk.NewDec(100_000)), + ), + SetBlockNumber(1), + SetBlockTime(startTime), + FundAccount(alice, sdk.NewCoins(sdk.NewCoin(denoms.NUSD, sdk.NewInt(104)))), // need 4 because we need to pay for the close position fee + FundAccount(bob, sdk.NewCoins(sdk.NewCoin(denoms.NUSD, sdk.NewInt(1_020)))), + MarketOrder( + alice, + pairBtcUsdc, + types.Direction_SHORT, + sdk.NewInt(100), + sdk.NewDec(10), + sdk.ZeroDec(), + ), + MarketOrder( + bob, + pairBtcUsdc, + types.Direction_LONG, + sdk.NewInt(1_000), + sdk.NewDec(10), + sdk.ZeroDec(), + ), + QueryPosition(pairBtcUsdc, alice, QueryPosition_MarginRatioEquals(sdk.MustNewDecFromStr("-0.093502230451982156"))), + ).When( + // Alice opened a short position (leverage x10) while bob a bigger long position + // Price jumped by 10%, with a settlement price of 1.09 + // That creates a bad debt for alice + // Her Realized Pnl is -101.01010101 and her margin is 100, so -1.01010101 is bad debt + // Bob's Realized Pnl is 1010, so he has 1010 more than his margin + + CloseMarket(pairBtcUsdc), + SettlePosition( + pairBtcUsdc, + 1, + alice, + SettlePositionChecker_PositionEquals( + types.Position{ + TraderAddress: alice.String(), + Pair: "ubtc:unusd", + Size_: sdk.MustNewDecFromStr("0"), + Margin: sdk.MustNewDecFromStr("0"), + OpenNotional: sdk.MustNewDecFromStr("0"), + LatestCumulativePremiumFraction: sdk.MustNewDecFromStr("0"), + LastUpdatedBlockNumber: 1, + }, + ), + SettlePositionChecker_MarginToVault(sdk.ZeroDec()), + SettlePositionChecker_BadDebt(sdk.MustNewDecFromStr("1.010101010101010101")), + ), + SettlePosition( + pairBtcUsdc, + 1, + bob, + SettlePositionChecker_MarginToVault(sdk.MustNewDecFromStr("-1101.010101010101010100")), + SettlePositionChecker_BadDebt(sdk.ZeroDec()), + ), + ).Then( + PositionShouldNotExist(alice, pairBtcUsdc, 1), + PositionShouldNotExist(bob, pairBtcUsdc, 1), + SetBlockNumber(2), + BalanceEqual(alice, "unusd", sdk.NewInt(0)), + BalanceEqual(bob, "unusd", sdk.NewInt(1101-20)), + ), + TC("Error: can't settle on enabled market").When( CreateCustomMarket( pairBtcUsdc,