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

test(perp): MsgServer tests #692

Merged
merged 9 commits into from
Jul 11, 2022
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* [#687](https://github.com/NibiruChain/nibiru/pull/687) Emit `PositionChangedEvent` upon changing margin.
* [#685](https://github.com/NibiruChain/nibiru/pull/685) Represent `PositionChangedEvent` bad debt as Coin.
* [#689](https://github.com/NibiruChain/nibiru/pull/689) Change liquidation params to 2.5% liquidation fee ratio and 25% partial liquidation ratio.

### Testing

* [#695](https://github.com/NibiruChain/nibiru/pull/695) Add `OpenPosition` integration tests.
* [#692](https://github.com/NibiruChain/nibiru/pull/692) Add test coverage for Perp MsgServer methods.
* [#689](https://github.com/NibiruChain/nibiru/pull/689) Change liquidation params to 2.5% liquidation fee ratio and 25% partial liquidation ratio.
20 changes: 6 additions & 14 deletions x/perp/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import (
"context"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

"github.com/NibiruChain/nibiru/x/common"
"github.com/NibiruChain/nibiru/x/perp/types"
vpooltypes "github.com/NibiruChain/nibiru/x/vpool/types"
)

type msgServer struct {
Expand Down Expand Up @@ -37,14 +35,15 @@ func (m msgServer) OpenPosition(goCtx context.Context, req *types.MsgOpenPositio
) (response *types.MsgOpenPositionResponse, err error) {
pair, err := common.NewAssetPair(req.TokenPair)
if err != nil {
panic(err) // must not happen
return nil, err
}
sender, err := sdk.AccAddressFromBech32(req.Sender)
if err != nil {
return nil, err
}

ctx := sdk.UnwrapSDKContext(goCtx)

err = m.k.OpenPosition(
ctx,
pair,
Expand All @@ -55,25 +54,18 @@ func (m msgServer) OpenPosition(goCtx context.Context, req *types.MsgOpenPositio
req.BaseAssetAmountLimit.ToDec(),
)
if err != nil {
return nil, sdkerrors.Wrap(vpooltypes.ErrOpeningPosition, err.Error())
return nil, err
}

return &types.MsgOpenPositionResponse{}, nil
}

func (m msgServer) ClosePosition(goCtx context.Context, position *types.MsgClosePosition) (*types.MsgClosePositionResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
addr, err := sdk.AccAddressFromBech32(position.Sender)
if err != nil {
panic(err)
}

tokenPair, err := common.NewAssetPair(position.TokenPair)
if err != nil {
panic(err)
}
traderAddr := sdk.MustAccAddressFromBech32(position.Sender)
tokenPair := common.MustNewAssetPair(position.TokenPair)

_, err = m.k.ClosePosition(ctx, tokenPair, addr)
_, err := m.k.ClosePosition(ctx, tokenPair, traderAddr)

return &types.MsgClosePositionResponse{}, err
}
Expand Down
254 changes: 254 additions & 0 deletions x/perp/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package keeper_test

import (
"fmt"
"testing"
"time"

"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/stretchr/testify/require"

"github.com/NibiruChain/nibiru/x/common"
"github.com/NibiruChain/nibiru/x/perp/keeper"
"github.com/NibiruChain/nibiru/x/perp/types"
"github.com/NibiruChain/nibiru/x/testutil/sample"
"github.com/NibiruChain/nibiru/x/testutil/testapp"
)

func TestMsgServerOpenPosition(t *testing.T) {
tests := []struct {
name string

traderFunds sdk.Coins
pair string
sender string

expectedErr error
}{
{
name: "trader not enough funds",
traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 999)),
pair: common.PairBTCStable.String(),
sender: sample.AccAddress().String(),
expectedErr: sdkerrors.ErrInsufficientFunds,
},
{
name: "invalid pair",
traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 1020)),
pair: "foo",
sender: sample.AccAddress().String(),
expectedErr: common.ErrInvalidTokenPair,
},
{
name: "invalid address",
traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 1020)),
pair: common.PairBTCStable.String(),
sender: "bar",
expectedErr: fmt.Errorf("decoding bech32 failed"),
},
{
name: "success",
traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 1020)),
pair: common.PairBTCStable.String(),
sender: sample.AccAddress().String(),
expectedErr: nil,
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
app, ctx := testapp.NewNibiruAppAndContext(true)
msgServer := keeper.NewMsgServerImpl(app.PerpKeeper)

t.Log("create vpool")
app.VpoolKeeper.CreatePool(ctx, common.PairBTCStable, sdk.OneDec(), sdk.NewDec(1_000_000), sdk.NewDec(1_000_000), sdk.OneDec(), sdk.OneDec())
app.PerpKeeper.PairMetadataState(ctx).Set(&types.PairMetadata{
Pair: common.PairBTCStable,
CumulativePremiumFractions: []sdk.Dec{sdk.ZeroDec()},
})

traderAddr, err := sdk.AccAddressFromBech32(tc.sender)
if err == nil {
t.Log("fund trader")
require.NoError(t, simapp.FundAccount(app.BankKeeper, ctx, traderAddr, tc.traderFunds))
}

resp, err := msgServer.OpenPosition(sdk.WrapSDKContext(ctx), &types.MsgOpenPosition{
Sender: tc.sender,
TokenPair: tc.pair,
Side: types.Side_BUY,
QuoteAssetAmount: sdk.NewInt(1000),
Leverage: sdk.NewDec(10),
BaseAssetAmountLimit: sdk.ZeroInt(),
})

if tc.expectedErr != nil {
require.ErrorContains(t, err, tc.expectedErr.Error())
require.Nil(t, resp)
} else {
require.NoError(t, err)
require.NotNil(t, resp)
}
})
}
}

func TestMsgServerClosePosition(t *testing.T) {
tests := []struct {
name string

pair string
sender string

expectedErr error
}{
{
name: "success",
pair: common.PairBTCStable.String(),
sender: sample.AccAddress().String(),
expectedErr: nil,
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
app, ctx := testapp.NewNibiruAppAndContext(true)
msgServer := keeper.NewMsgServerImpl(app.PerpKeeper)

t.Log("create vpool")
app.VpoolKeeper.CreatePool(ctx, common.PairBTCStable, sdk.OneDec(), sdk.NewDec(1_000_000), sdk.NewDec(1_000_000), sdk.OneDec(), sdk.OneDec())
app.PerpKeeper.PairMetadataState(ctx).Set(&types.PairMetadata{
Pair: common.PairBTCStable,
CumulativePremiumFractions: []sdk.Dec{sdk.ZeroDec()},
})

pair, err := common.NewAssetPair(tc.pair)
traderAddr, err2 := sdk.AccAddressFromBech32(tc.sender)
if err == nil && err2 == nil {
t.Log("create position")
require.NoError(t, app.PerpKeeper.PositionsState(ctx).Create(&types.Position{
TraderAddress: traderAddr.String(),
Pair: pair,
Size_: sdk.OneDec(),
Margin: sdk.OneDec(),
OpenNotional: sdk.OneDec(),
LastUpdateCumulativePremiumFraction: sdk.ZeroDec(),
BlockNumber: 1,
}))
require.NoError(t, simapp.FundModuleAccount(app.BankKeeper, ctx, types.VaultModuleAccount, sdk.NewCoins(sdk.NewInt64Coin(pair.GetQuoteTokenDenom(), 1))))
}

resp, err := msgServer.ClosePosition(sdk.WrapSDKContext(ctx), &types.MsgClosePosition{
Sender: tc.sender,
TokenPair: tc.pair,
})

if tc.expectedErr != nil {
require.ErrorContains(t, err, tc.expectedErr.Error())
require.Nil(t, resp)
} else {
require.NoError(t, err)
require.NotNil(t, resp)
}
})
}
}

func TestMsgServerLiquidate(t *testing.T) {
tests := []struct {
name string

pair string
liquidator string
trader string

expectedErr error
}{
{
name: "invalid pair",
pair: "foo",
liquidator: sample.AccAddress().String(),
trader: sample.AccAddress().String(),

expectedErr: common.ErrInvalidTokenPair,
},
{
name: "invalid liquidator address",
pair: common.PairBTCStable.String(),
liquidator: "foo",
trader: sample.AccAddress().String(),
expectedErr: fmt.Errorf("decoding bech32 failed"),
},
{
name: "invalid trader address",
pair: common.PairBTCStable.String(),
liquidator: sample.AccAddress().String(),
trader: "foo",
expectedErr: fmt.Errorf("decoding bech32 failed"),
},
{
name: "success",
pair: common.PairBTCStable.String(),
liquidator: sample.AccAddress().String(),
trader: sample.AccAddress().String(),
expectedErr: nil,
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
app, ctx := testapp.NewNibiruAppAndContext(true)
msgServer := keeper.NewMsgServerImpl(app.PerpKeeper)

t.Log("create vpool")
app.VpoolKeeper.CreatePool(ctx, common.PairBTCStable, sdk.OneDec(), sdk.NewDec(1_000_000), sdk.NewDec(1_000_000), sdk.OneDec(), sdk.OneDec())
app.PerpKeeper.PairMetadataState(ctx).Set(&types.PairMetadata{
Pair: common.PairBTCStable,
CumulativePremiumFractions: []sdk.Dec{sdk.ZeroDec()},
})
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1).WithBlockTime(time.Now().Add(time.Minute))

pair, err := common.NewAssetPair(tc.pair)
traderAddr, err2 := sdk.AccAddressFromBech32(tc.trader)
if err == nil && err2 == nil {
t.Log("set pricefeed oracle price")
oracle := sample.AccAddress()
app.PricefeedKeeper.WhitelistOracles(ctx, []sdk.AccAddress{oracle})
_, err = app.PricefeedKeeper.SetPrice(ctx, oracle, pair.String(), sdk.OneDec(), time.Now().Add(time.Hour))
require.NoError(t, err)
require.NoError(t, app.PricefeedKeeper.SetCurrentPrices(ctx, pair.GetBaseTokenDenom(), pair.GetQuoteTokenDenom()))

t.Log("create position")
require.NoError(t, app.PerpKeeper.PositionsState(ctx).Create(&types.Position{
TraderAddress: traderAddr.String(),
Pair: pair,
Size_: sdk.OneDec(),
Margin: sdk.OneDec(),
OpenNotional: sdk.NewDec(2), // new spot price is 1, so position can be liquidated
LastUpdateCumulativePremiumFraction: sdk.ZeroDec(),
BlockNumber: 1,
}))
require.NoError(t, simapp.FundModuleAccount(app.BankKeeper, ctx, types.VaultModuleAccount, sdk.NewCoins(sdk.NewInt64Coin(pair.GetQuoteTokenDenom(), 1))))
}

resp, err := msgServer.Liquidate(sdk.WrapSDKContext(ctx), &types.MsgLiquidate{
Sender: tc.liquidator,
TokenPair: tc.pair,
Trader: tc.trader,
})

if tc.expectedErr != nil {
require.ErrorContains(t, err, tc.expectedErr.Error())
require.Nil(t, resp)
} else {
require.NoError(t, err)
require.NotNil(t, resp)
}
})
}
}
3 changes: 1 addition & 2 deletions x/perp/types/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,7 @@ func (m MsgClosePosition) Route() string { return RouterKey }
func (m MsgClosePosition) Type() string { return "liquidate_msg" }

func (msg MsgClosePosition) ValidateBasic() error {
_, err := sdk.AccAddressFromBech32(msg.Sender)
if err != nil {
if _, err := sdk.AccAddressFromBech32(msg.Sender); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid sender address (%s)", err)
}
if _, err := common.NewAssetPair(msg.TokenPair); err != nil {
Expand Down
13 changes: 6 additions & 7 deletions x/perp/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ const (

// x/perp module sentinel errors
var (
ErrMarginHighEnough = sdkerrors.Register(ModuleName, 1, "margin is higher than required maintenance margin ratio")
ErrPositionNotFound = sdkerrors.Register(ModuleName, 2, "no position found")
ErrPairNotFound = sdkerrors.Register(ModuleName, 3, "pair doesn't have live vpool")
ErrPairMetadataNotFound = sdkerrors.Register(ModuleName, 4, "pair doesn't have metadata")
ErrPositionZero = sdkerrors.Register(ModuleName, 5, "position is zero")
ErrExchangeStopped = sdkerrors.Register(ModuleName, 6, "exchange is stopped")
// failed to remove margin; position has bad debt
ErrMarginHighEnough = sdkerrors.Register(ModuleName, 1, "margin is higher than required maintenance margin ratio")
ErrPositionNotFound = sdkerrors.Register(ModuleName, 2, "no position found")
ErrPairNotFound = sdkerrors.Register(ModuleName, 3, "pair doesn't have live vpool")
ErrPairMetadataNotFound = sdkerrors.Register(ModuleName, 4, "pair doesn't have metadata")
ErrPositionZero = sdkerrors.Register(ModuleName, 5, "position is zero")
ErrExchangeStopped = sdkerrors.Register(ModuleName, 6, "exchange is stopped")
ErrFailedRemoveMarginCanCauseBadDebt = sdkerrors.Register(ModuleName, 7, "failed to remove margin; position would have bad debt if removed")
)

Expand Down
8 changes: 3 additions & 5 deletions x/vpool/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ var (
ErrBaseReserveAtZero = sdkerrors.Register(ModuleName, 4, "base reserve after at zero")
ErrNoLastSnapshotSaved = sdkerrors.Register(ModuleName, 5, "There was no last snapshot, could be that you did not do snapshot on pool creation")
ErrOverFluctuationLimit = sdkerrors.Register(ModuleName, 6, "price is over fluctuation limit")
ErrAssetOverUserLimit = sdkerrors.Register(ModuleName, 7, "amount of assets traded is over user-defined limit")
ErrOpeningPosition = sdkerrors.Register(ModuleName, 8, "error opening position")
ErrClosingPosition = sdkerrors.Register(ModuleName, 9, "error closing position")
ErrNoValidPrice = sdkerrors.Register(ModuleName, 10, "no valid prices available")
ErrNoValidTWAP = sdkerrors.Register(ModuleName, 11, "TWAP price not found")
ErrAssetOverUserLimit = sdkerrors.Register(ModuleName, 7, "amout of assets traded is over user-defined limit")
ErrNoValidPrice = sdkerrors.Register(ModuleName, 8, "no valid prices available")
ErrNoValidTWAP = sdkerrors.Register(ModuleName, 9, "TWAP price not found")
)