Skip to content

Commit

Permalink
test(perp): MsgServer tests (#692)
Browse files Browse the repository at this point in the history
* Add MsgServerOpenPosition test

* Add TestMsgServerClosePosition tests

* Update MsgServerOpenPosition tests

* Add TestMsgServerLiquidate

* Update CHANGELOG.md

* Fix ling errors

* Remove unnecessary address and pair checks in ClosePosition
  • Loading branch information
NibiruHeisenberg authored Jul 11, 2022
1 parent 09fe399 commit 13e1857
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 29 deletions.
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")
)

0 comments on commit 13e1857

Please sign in to comment.