From 52612ca3147d34fa270332cc3268772d5bebe9e0 Mon Sep 17 00:00:00 2001 From: Adarsh Kumar Date: Thu, 21 Nov 2024 14:52:17 +0530 Subject: [PATCH] Refactor perpetual test and add more test cases (#977) * refactor and add perpetual tests * refactor close positions test and add more test cases * add more test in perpetual close position * update whitelist and dewhitelist tests * refactor open consolidate test * add test cases for open consolidate * fix open_test --- x/perpetual/keeper/begin_blocker_test.go | 91 +++ x/perpetual/keeper/close_test.go | 521 ++++++++++++------ x/perpetual/keeper/get_epoch_position_test.go | 5 + .../keeper/get_net_open_interest_test.go | 96 ++++ x/perpetual/keeper/keeper_test.go | 3 + .../keeper/msg_server_close_positions_test.go | 381 ++++++++----- .../keeper/msg_server_dewhitelist_test.go | 8 +- .../keeper/msg_server_whitelist_test.go | 8 +- x/perpetual/keeper/open_consolidate_test.go | 381 +++++++------ x/perpetual/keeper/open_test.go | 92 ++++ 10 files changed, 1117 insertions(+), 469 deletions(-) create mode 100644 x/perpetual/keeper/get_net_open_interest_test.go diff --git a/x/perpetual/keeper/begin_blocker_test.go b/x/perpetual/keeper/begin_blocker_test.go index 89f1b0621..34fe65153 100644 --- a/x/perpetual/keeper/begin_blocker_test.go +++ b/x/perpetual/keeper/begin_blocker_test.go @@ -2,11 +2,14 @@ package keeper_test import ( "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + ammtypes "github.com/elys-network/elys/x/amm/types" leveragelpmodulekeeper "github.com/elys-network/elys/x/leveragelp/keeper" leveragelpmoduletypes "github.com/elys-network/elys/x/leveragelp/types" ptypes "github.com/elys-network/elys/x/parameter/types" + "github.com/elys-network/elys/x/perpetual/types" ) func (suite *PerpetualKeeperTestSuite) TestBeginBlocker() { @@ -28,3 +31,91 @@ func (suite *PerpetualKeeperTestSuite) TestBeginBlocker() { suite.Require().NoError(err) suite.app.PerpetualKeeper.BeginBlocker(suite.ctx) } + +func (suite *PerpetualKeeperTestSuite) TestComputeFundingRate() { + ammPool := ammtypes.Pool{ + PoolId: 1, + PoolAssets: []ammtypes.PoolAsset{ + { + Token: sdk.Coin{ + Denom: "testAsset", + Amount: math.NewInt(100), + }, + }, + }, + } + pool := types.NewPool(ammPool) + pool.PoolAssetsLong = []types.PoolAsset{ + { + Liabilities: math.NewInt(0), + Custody: math.NewInt(0), + Collateral: math.NewInt(0), + AssetDenom: "testAsset", + }, + } + pool.PoolAssetsShort = []types.PoolAsset{ + { + Liabilities: math.NewInt(0), + Custody: math.NewInt(0), + Collateral: math.NewInt(0), + AssetDenom: "testAsset", + }, + } + + params := suite.app.PerpetualKeeper.GetParams(suite.ctx) + params.FixedFundingRate = math.LegacyMustNewDecFromStr("0.500000000000000000") + suite.app.PerpetualKeeper.SetParams(suite.ctx, ¶ms) + + testCases := []struct { + name string + expectLongRate math.LegacyDec + expectShortRate math.LegacyDec + Liabilities math.Int + Custody math.Int + Collateral math.Int + }{ + { + "totalLongOpenInterest is zero", + math.LegacyNewDec(0), + math.LegacyNewDec(0), + math.NewInt(0), + math.NewInt(0), + math.NewInt(0), + }, + { + "totalShortOpenInterest is zero", + math.LegacyNewDec(0), + math.LegacyNewDec(0), + math.NewInt(0), + math.NewInt(0), + math.NewInt(0), + }, + { + "totalShortOpenInterest is less than totalLongOpenInterest", + math.LegacyMustNewDecFromStr("0.166666666666666666"), + math.LegacyNewDec(0), + math.NewInt(500), + math.NewInt(2000), + math.NewInt(1000), + }, + { + "totalLongOpenInterest is less than totalShortOpenInterest", + math.LegacyNewDec(0), + math.LegacyMustNewDecFromStr("0.1"), + math.NewInt(1500), + math.NewInt(2000), + math.NewInt(1000), + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + pool.PoolAssetsLong[0].Custody = tc.Custody + pool.PoolAssetsLong[0].Collateral = tc.Collateral + pool.PoolAssetsShort[0].Liabilities = tc.Liabilities + longRate, shortRate := suite.app.PerpetualKeeper.ComputeFundingRate(suite.ctx, pool) + suite.Require().Equal(tc.expectLongRate, longRate) + suite.Require().Equal(tc.expectShortRate, shortRate) + }) + } +} diff --git a/x/perpetual/keeper/close_test.go b/x/perpetual/keeper/close_test.go index 9702e52a7..1129e26c7 100644 --- a/x/perpetual/keeper/close_test.go +++ b/x/perpetual/keeper/close_test.go @@ -3,212 +3,369 @@ package keeper_test import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/elys-network/elys/testutil/sample" ammtypes "github.com/elys-network/elys/x/amm/types" - assetprofiletypes "github.com/elys-network/elys/x/assetprofile/types" - leveragelpmodulekeeper "github.com/elys-network/elys/x/leveragelp/keeper" - leveragelpmoduletypes "github.com/elys-network/elys/x/leveragelp/types" + oracletypes "github.com/elys-network/elys/x/oracle/types" ptypes "github.com/elys-network/elys/x/parameter/types" "github.com/elys-network/elys/x/perpetual/types" ) -func (suite *PerpetualKeeperTestSuite) TestClose_ErrorGetMtp() { - _, err := suite.app.PerpetualKeeper.Close(suite.ctx, &types.MsgClose{ - Creator: sample.AccAddress(), - Id: uint64(10), - Amount: math.NewInt(12000), - }) +func (suite *PerpetualKeeperTestSuite) TestClose() { - suite.Require().ErrorIs(err, types.ErrMTPDoesNotExist) -} -func (suite *PerpetualKeeperTestSuite) TestClose_ErrorGetEntry() { - suite.SetupCoinPrices() - - addr := suite.AddAccounts(2, nil) - amount := math.NewInt(1000) - positionCreator := addr[1] - tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) - suite.Require().NoError(err) - - poolCreator := addr[0] - ammPool := suite.CreateNewAmmPool(poolCreator, true, math.LegacyZeroDec(), math.LegacyZeroDec(), ptypes.ATOM, amount.MulRaw(10), amount.MulRaw(10)) - enablePoolMsg := leveragelpmoduletypes.MsgAddPool{ - Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), - Pool: leveragelpmoduletypes.AddPool{ - AmmPoolId: ammPool.PoolId, - LeverageMax: math.LegacyMustNewDecFromStr("10"), + // Define test cases + testCases := []struct { + name string + setup func() *types.MsgClose + expectedErrMsg string + repayAmount math.Int + }{ + { + "mtp not found", + func() *types.MsgClose { + return &types.MsgClose{ + Creator: sample.AccAddress(), + Id: uint64(10), + Amount: math.NewInt(12000), + } + }, + "mtp not found", + math.NewInt(0), }, - } - _, err = leveragelpmodulekeeper.NewMsgServerImpl(*suite.app.LeveragelpKeeper).AddPool(suite.ctx, &enablePoolMsg) - suite.Require().NoError(err) - openPositionMsg := &types.MsgOpen{ - Creator: positionCreator.String(), - Leverage: math.LegacyNewDec(2), - Position: types.Position_LONG, - PoolId: ammPool.PoolId, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: tradingAssetPrice.MulInt64(4), - StopLossPrice: math.LegacyZeroDec(), - } + { + "asset profile not found", + func() *types.MsgClose { + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + _, _, ammPool := suite.SetPerpetualPool(1) + tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(2), + Position: types.Position_LONG, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)), + TakeProfitPrice: tradingAssetPrice.MulInt64(4), + StopLossPrice: math.LegacyZeroDec(), + } + + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) + suite.app.AssetprofileKeeper.RemoveEntry(suite.ctx, ptypes.BaseCurrency) + return &types.MsgClose{ + Creator: positionCreator.String(), + Id: position.Id, + Amount: math.NewInt(500), + } + }, + "asset uusdc not found", + math.NewInt(0), + }, + { + "Success: Close amount greater than position size, CloseRatio > 1", // CloseRatio gets changed to 1 + func() *types.MsgClose { + suite.ResetSuite() + + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + _, _, ammPool := suite.SetPerpetualPool(1) + tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(2), + Position: types.Position_LONG, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)), + TakeProfitPrice: tradingAssetPrice.MulInt64(4), + StopLossPrice: math.LegacyZeroDec(), + } - position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) - suite.Require().NoError(err) + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) - suite.app.AssetprofileKeeper.RemoveEntry(suite.ctx, ptypes.BaseCurrency) + return &types.MsgClose{ + Creator: positionCreator.String(), + Id: position.Id, + Amount: math.NewInt(50000000000000), // same as with amount 399 + } + }, + "", + math.NewInt(200), + }, + { + "Close with price greater than open price and less than take profit price", + func() *types.MsgClose { + suite.ResetSuite() - _, err = suite.app.PerpetualKeeper.Close(suite.ctx, &types.MsgClose{ - Creator: positionCreator.String(), - Id: position.Id, - Amount: math.NewInt(500), - }) + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + _, _, ammPool := suite.SetPerpetualPool(1) + tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(2), + Position: types.Position_LONG, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)), + TakeProfitPrice: tradingAssetPrice.MulInt64(4), + StopLossPrice: math.LegacyZeroDec(), + } - suite.Require().ErrorIs(err, assetprofiletypes.ErrAssetProfileNotFound) -} + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) -func (suite *PerpetualKeeperTestSuite) TestClose_Short() { - suite.SetupCoinPrices() - - addr := suite.AddAccounts(2, nil) - amount := math.NewInt(1000) - positionCreator := addr[1] - //tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) - //suite.Require().NoError(err) - - poolCreator := addr[0] - ammPool := suite.CreateNewAmmPool(poolCreator, true, math.LegacyZeroDec(), math.LegacyZeroDec(), ptypes.ATOM, amount.MulRaw(10), amount.MulRaw(10)) - enablePoolMsg := leveragelpmoduletypes.MsgAddPool{ - Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), - Pool: leveragelpmoduletypes.AddPool{ - AmmPoolId: ammPool.PoolId, - LeverageMax: math.LegacyMustNewDecFromStr("10"), + suite.app.OracleKeeper.SetPrice(suite.ctx, oracletypes.Price{ + Asset: "ATOM", + Price: math.LegacyMustNewDecFromStr("10.0"), + Source: "elys", + Provider: oracleProvider.String(), + Timestamp: uint64(suite.ctx.BlockTime().Unix()), + }) + + return &types.MsgClose{ + Creator: positionCreator.String(), + Id: position.Id, + Amount: math.NewInt(399), + } + }, + "", + math.NewInt(100), // less than at the same price }, - } - _, err := leveragelpmodulekeeper.NewMsgServerImpl(*suite.app.LeveragelpKeeper).AddPool(suite.ctx, &enablePoolMsg) - suite.Require().NoError(err) - openPositionMsg := &types.MsgOpen{ - Creator: positionCreator.String(), - Leverage: math.LegacyNewDec(2), - Position: types.Position_SHORT, - PoolId: ammPool.PoolId, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), - StopLossPrice: math.LegacyZeroDec(), - } + { + "Close at take profit price", + func() *types.MsgClose { + suite.ResetSuite() - position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) - suite.Require().NoError(err) + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + _, _, ammPool := suite.SetPerpetualPool(1) + tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(2), + Position: types.Position_LONG, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)), + TakeProfitPrice: tradingAssetPrice.MulInt64(4), + StopLossPrice: math.LegacyZeroDec(), + } - _, err = suite.app.PerpetualKeeper.Close(suite.ctx, &types.MsgClose{ - Creator: positionCreator.String(), - Id: position.Id, - Amount: math.NewInt(900), - }) + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) - suite.Require().Nil(err) -} -func (suite *PerpetualKeeperTestSuite) TestClose_Long() { - suite.SetupCoinPrices() - - suite.app.AmmKeeper.SetDenomLiquidity(suite.ctx, ammtypes.DenomLiquidity{ - Denom: ptypes.Elys, - Liquidity: math.NewInt(2000000), - }) - suite.app.AmmKeeper.SetDenomLiquidity(suite.ctx, ammtypes.DenomLiquidity{ - Denom: ptypes.BaseCurrency, - Liquidity: math.NewInt(1000000), - }) - suite.app.AmmKeeper.SetDenomLiquidity(suite.ctx, ammtypes.DenomLiquidity{ - Denom: ptypes.ATOM, - Liquidity: math.NewInt(1000000), - }) - - addr := suite.AddAccounts(3, nil) - amount := math.NewInt(1000) - positionCreator := addr[1] - tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) - suite.Require().NoError(err) - - poolCreator := addr[0] - ammPool := suite.CreateNewAmmPool(poolCreator, true, math.LegacyZeroDec(), math.LegacyZeroDec(), ptypes.ATOM, amount.MulRaw(10), amount.MulRaw(10)) - enablePoolMsg := leveragelpmoduletypes.MsgAddPool{ - Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), - Pool: leveragelpmoduletypes.AddPool{ - AmmPoolId: ammPool.PoolId, - LeverageMax: math.LegacyMustNewDecFromStr("10"), + suite.app.OracleKeeper.SetPrice(suite.ctx, oracletypes.Price{ + Asset: "ATOM", + Price: tradingAssetPrice.MulInt64(4), + Source: "elys", + Provider: oracleProvider.String(), + Timestamp: uint64(suite.ctx.BlockTime().Unix()), + }) + + return &types.MsgClose{ + Creator: positionCreator.String(), + Id: position.Id, + Amount: math.NewInt(399), + } + }, + "", + math.NewInt(50), }, - } - _, err = leveragelpmodulekeeper.NewMsgServerImpl(*suite.app.LeveragelpKeeper).AddPool(suite.ctx, &enablePoolMsg) - suite.Require().NoError(err) - openPositionMsg := &types.MsgOpen{ - Creator: positionCreator.String(), - Leverage: math.LegacyNewDec(5), - Position: types.Position_LONG, - PoolId: ammPool.PoolId, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: tradingAssetPrice.MulInt64(4), - StopLossPrice: math.LegacyZeroDec(), - } + { + "Close at stopLoss price", + func() *types.MsgClose { + suite.ResetSuite() - tokensIn := sdk.NewCoins(sdk.NewCoin(ptypes.ATOM, math.NewInt(1000_000_000)), sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000_000_000))) - suite.AddLiquidity(ammPool, addr[2], tokensIn) - position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) - suite.Require().NoError(err) + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + _, _, ammPool := suite.SetPerpetualPool(1) + tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(2), + Position: types.Position_LONG, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)), + TakeProfitPrice: tradingAssetPrice.MulInt64(4), + StopLossPrice: math.LegacyMustNewDecFromStr("2.0"), + } - _, err = suite.app.PerpetualKeeper.Close(suite.ctx, &types.MsgClose{ - Creator: positionCreator.String(), - Id: position.Id, - Amount: math.NewInt(500), - }) + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) - suite.Require().Nil(err) -} -func (suite *PerpetualKeeperTestSuite) TestClose_Short_NotEnoughLiquidity() { - suite.SetupCoinPrices() - - addr := suite.AddAccounts(2, nil) - amount := math.NewInt(1000) - positionCreator := addr[1] - - poolCreator := addr[0] - ammPool := suite.CreateNewAmmPool(poolCreator, true, math.LegacyZeroDec(), math.LegacyZeroDec(), ptypes.ATOM, amount.MulRaw(10), amount.MulRaw(10)) - enablePoolMsg := leveragelpmoduletypes.MsgAddPool{ - Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), - Pool: leveragelpmoduletypes.AddPool{ - AmmPoolId: ammPool.PoolId, - LeverageMax: math.LegacyMustNewDecFromStr("10"), + suite.app.OracleKeeper.SetPrice(suite.ctx, oracletypes.Price{ + Asset: "ATOM", + Price: math.LegacyMustNewDecFromStr("2.0"), + Source: "elys", + Provider: oracleProvider.String(), + Timestamp: uint64(suite.ctx.BlockTime().Unix()), + }) + + return &types.MsgClose{ + Creator: positionCreator.String(), + Id: position.Id, + Amount: math.NewInt(399), + } + }, + "", + math.NewInt(500), }, - } - _, err := leveragelpmodulekeeper.NewMsgServerImpl(*suite.app.LeveragelpKeeper).AddPool(suite.ctx, &enablePoolMsg) - suite.Require().NoError(err) - openPositionMsg := &types.MsgOpen{ - Creator: positionCreator.String(), - Leverage: math.LegacyNewDec(2), - Position: types.Position_SHORT, - PoolId: ammPool.PoolId, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), - StopLossPrice: math.LegacyZeroDec(), - } + { + "Sucess: close long position,at same price as open price", + func() *types.MsgClose { + suite.ResetSuite() + + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + _, _, ammPool := suite.SetPerpetualPool(1) + tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(2), + Position: types.Position_LONG, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)), + TakeProfitPrice: tradingAssetPrice.MulInt64(4), + StopLossPrice: math.LegacyZeroDec(), + } + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) + return &types.MsgClose{ + Creator: positionCreator.String(), + Id: position.Id, + Amount: math.NewInt(399), + } + }, + "", + math.NewInt(200), + }, + { + "Success: close short position at same price as open price", + func() *types.MsgClose { + suite.ResetSuite() + + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + _, _, ammPool := suite.SetPerpetualPool(1) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(5), + Position: types.Position_SHORT, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)), + TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), + StopLossPrice: math.LegacyZeroDec(), + } + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) + return &types.MsgClose{ + Creator: positionCreator.String(), + Id: position.Id, + Amount: math.NewInt(900), + } + }, + "", + math.NewInt(4499), + }, + { + "Close with too much unpaid Liability to make custody amount 0", + func() *types.MsgClose { + suite.ResetSuite() + + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + _, _, ammPool := suite.SetPerpetualPool(1) + tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(2), + Position: types.Position_LONG, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)), + TakeProfitPrice: tradingAssetPrice.MulInt64(4), + StopLossPrice: math.LegacyZeroDec(), + } + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) - position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) - suite.Require().NoError(err) + // Increase unpaid liability + mtp, _ := suite.app.PerpetualKeeper.GetMTP(suite.ctx, positionCreator, position.Id) + mtp.BorrowInterestUnpaidLiability = math.NewInt(1995) + suite.app.PerpetualKeeper.SetMTP(suite.ctx, &mtp) + suite.T().Log("MTP: ", mtp) - suite.app.AmmKeeper.SetDenomLiquidity(suite.ctx, ammtypes.DenomLiquidity{ - Denom: ptypes.BaseCurrency, - Liquidity: math.NewInt(0), - }) + return &types.MsgClose{ + Creator: positionCreator.String(), + Id: position.Id, + Amount: math.NewInt(399), + } + }, + "error handling funding fee", + math.NewInt(0), + }, + { + "Close short with Not Enough liquidity", + func() *types.MsgClose { + suite.ResetSuite() + + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + _, _, ammPool := suite.SetPerpetualPool(1) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(5), + Position: types.Position_SHORT, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)), + TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), + StopLossPrice: math.LegacyZeroDec(), + } + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) + + suite.app.AmmKeeper.SetDenomLiquidity(suite.ctx, ammtypes.DenomLiquidity{ + Denom: ptypes.BaseCurrency, + Liquidity: math.NewInt(0), + }) + + return &types.MsgClose{ + Creator: positionCreator.String(), + Id: position.Id, + Amount: math.NewInt(900), + } + }, + "not enough liquidity", + math.NewInt(0), + }, + } - _, err = suite.app.PerpetualKeeper.Close(suite.ctx, &types.MsgClose{ - Creator: positionCreator.String(), - Id: position.Id, - Amount: math.NewInt(900), - }) + for _, tc := range testCases { + suite.Run(tc.name, func() { + msg := tc.setup() - suite.Require().EqualError(err, "not enough liquidity") + res, err := suite.app.PerpetualKeeper.Close(suite.ctx, msg) + + if tc.expectedErrMsg != "" { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expectedErrMsg) + } else { + suite.Require().NoError(err) + suite.Require().Equal(tc.repayAmount, res.Amount) + } + }) + } } diff --git a/x/perpetual/keeper/get_epoch_position_test.go b/x/perpetual/keeper/get_epoch_position_test.go index 06cd32d9d..18eed22ee 100644 --- a/x/perpetual/keeper/get_epoch_position_test.go +++ b/x/perpetual/keeper/get_epoch_position_test.go @@ -8,4 +8,9 @@ func (suite *PerpetualKeeperTestSuite) TestGetEpochPosition() { currentBlock := k.GetEpochPosition(ctx, 100) blockWant := int64(23) suite.Require().Equal(currentBlock, blockWant) + + // epoch length is 0 + currentBlock = k.GetEpochPosition(ctx, 0) + blockWant = int64(0) + suite.Require().Equal(currentBlock, blockWant) } diff --git a/x/perpetual/keeper/get_net_open_interest_test.go b/x/perpetual/keeper/get_net_open_interest_test.go new file mode 100644 index 000000000..61baf4d4f --- /dev/null +++ b/x/perpetual/keeper/get_net_open_interest_test.go @@ -0,0 +1,96 @@ +package keeper_test + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + ammtypes "github.com/elys-network/elys/x/amm/types" + "github.com/elys-network/elys/x/perpetual/types" +) + +func (suite *PerpetualKeeperTestSuite) TestGetFundingPaymentRates() { + ammPool := ammtypes.Pool{ + PoolId: 1, + PoolAssets: []ammtypes.PoolAsset{ + { + Token: sdk.Coin{ + Denom: "testAsset", + Amount: math.NewInt(100), + }, + }, + }, + } + pool := types.NewPool(ammPool) + pool.PoolAssetsLong = []types.PoolAsset{ + { + Liabilities: math.NewInt(0), + Custody: math.NewInt(0), + Collateral: math.NewInt(0), + AssetDenom: "testAsset", + }, + } + pool.PoolAssetsShort = []types.PoolAsset{ + { + Liabilities: math.NewInt(0), + Custody: math.NewInt(0), + Collateral: math.NewInt(0), + AssetDenom: "testAsset", + }, + } + + params := suite.app.PerpetualKeeper.GetParams(suite.ctx) + params.FixedFundingRate = math.LegacyMustNewDecFromStr("0.500000000000000000") + suite.app.PerpetualKeeper.SetParams(suite.ctx, ¶ms) + + testCases := []struct { + name string + expectLongRate math.LegacyDec + expectShortRate math.LegacyDec + expectLongFundingRate math.LegacyDec + expectShortFundingRate math.LegacyDec + Liabilities math.Int + Custody math.Int + Collateral math.Int + }{ + { + "fundingRateLong is zero, totalLongOpenInterest is non zero", + math.LegacyNewDec(0), + math.LegacyMustNewDecFromStr("0.1"), + math.LegacyMustNewDecFromStr("-0.15"), + math.LegacyMustNewDecFromStr("0.1"), + math.NewInt(1500), + math.NewInt(2000), + math.NewInt(1000), + }, + { + "fundingRateLong is zero, totalLongOpenInterest is zero", + math.LegacyNewDec(0), + math.LegacyNewDec(0), + math.LegacyNewDec(0), + math.LegacyNewDec(0), + math.NewInt(0), + math.NewInt(0), + math.NewInt(0), + }, + { + "fundingRateLong is non zero, totalShortOpenInterest is non zero", + math.LegacyMustNewDecFromStr("0.166666666666666666"), + math.LegacyNewDec(0), + math.LegacyMustNewDecFromStr("0.166666666666666666"), + math.LegacyMustNewDecFromStr("-0.333333333333333332"), + math.NewInt(500), + math.NewInt(2000), + math.NewInt(1000), + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + pool.PoolAssetsLong[0].Custody = tc.Custody + pool.PoolAssetsLong[0].Collateral = tc.Collateral + pool.PoolAssetsShort[0].Liabilities = tc.Liabilities + longRate, shortRate := suite.app.PerpetualKeeper.GetFundingPaymentRates(suite.ctx, pool) + suite.Require().Equal(tc.expectLongFundingRate, longRate) + suite.Require().Equal(tc.expectShortFundingRate, shortRate) + }) + } +} diff --git a/x/perpetual/keeper/keeper_test.go b/x/perpetual/keeper/keeper_test.go index 24e751381..58b0bcb82 100644 --- a/x/perpetual/keeper/keeper_test.go +++ b/x/perpetual/keeper/keeper_test.go @@ -97,6 +97,9 @@ func (suite *PerpetualKeeperTestSuite) ResetAndSetSuite(addr []sdk.AccAddress, u suite.app.PerpetualKeeper.SetPool(suite.ctx, pool) params := suite.app.PerpetualKeeper.GetParams(suite.ctx) params.BorrowInterestRateMin = math.LegacyMustNewDecFromStr("0.12") + params.MaximumLongTakeProfitPriceRatio = math.LegacyMustNewDecFromStr("11.000000000000000000") + params.MinimumLongTakeProfitPriceRatio = math.LegacyMustNewDecFromStr("1.020000000000000000") + params.MaximumShortTakeProfitPriceRatio = math.LegacyMustNewDecFromStr("0.980000000000000000") err := suite.app.PerpetualKeeper.SetParams(suite.ctx, ¶ms) suite.Require().NoError(err) diff --git a/x/perpetual/keeper/msg_server_close_positions_test.go b/x/perpetual/keeper/msg_server_close_positions_test.go index e345cf69e..fc9a49b92 100644 --- a/x/perpetual/keeper/msg_server_close_positions_test.go +++ b/x/perpetual/keeper/msg_server_close_positions_test.go @@ -4,166 +4,289 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/elys-network/elys/testutil/sample" - assetprofiletypes "github.com/elys-network/elys/x/assetprofile/types" + oracletypes "github.com/elys-network/elys/x/oracle/types" ptypes "github.com/elys-network/elys/x/parameter/types" "github.com/elys-network/elys/x/perpetual/keeper" "github.com/elys-network/elys/x/perpetual/types" ) -func (suite *PerpetualKeeperTestSuite) TestClosePositions_GetEntryError() { - k := suite.app.PerpetualKeeper - ctx := suite.ctx - msg := keeper.NewMsgServerImpl(*k) +func (suite *PerpetualKeeperTestSuite) TestClosePositions() { - suite.app.AssetprofileKeeper.RemoveEntry(ctx, ptypes.BaseCurrency) - - _, err := msg.ClosePositions(ctx, &types.MsgClosePositions{Creator: sample.AccAddress()}) - suite.Require().Nil(err) -} - -func (suite *PerpetualKeeperTestSuite) TestMsgServerClose_HandleLiquidateErrors() { - k := suite.app.PerpetualKeeper - ctx := suite.ctx - msg := keeper.NewMsgServerImpl(*k) + // Define test cases + testCases := []struct { + name string + setup func() *types.MsgClosePositions + expectedErrMsg string + expectedTotalPositions int + }{ + { + "BaseCurrency not found in asset profile", + func() *types.MsgClosePositions { + suite.app.AssetprofileKeeper.RemoveEntry(suite.ctx, ptypes.BaseCurrency) + return &types.MsgClosePositions{ + Creator: sample.AccAddress(), + } + }, + "", + 0, + }, + { + "Pool Not found for the close positions requests, just continue", + func() *types.MsgClosePositions { + suite.ResetSuite() + firstPool := uint64(1) + secondPool := uint64(2) - firstPool := uint64(1) - secondPool := uint64(2) + suite.SetPerpetualPool(firstPool) + suite.SetPerpetualPool(secondPool) - suite.SetPerpetualPool(firstPool) - suite.SetPerpetualPool(secondPool) + amount := math.NewInt(400) - amount := math.NewInt(400) + addr := suite.AddAccounts(2, nil) + firstPositionCreator := addr[0] + secondPositionCreator := addr[1] - addr := suite.AddAccounts(2, nil) - firstPositionCreator := addr[0] - secondPositionCreator := addr[1] + firstOpenPositionMsg := &types.MsgOpen{ + Creator: firstPositionCreator.String(), + Leverage: math.LegacyNewDec(5), + Position: types.Position_SHORT, + PoolId: firstPool, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), + TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), + StopLossPrice: math.LegacyZeroDec(), + } - firstOpenPositionMsg := &types.MsgOpen{ - Creator: firstPositionCreator.String(), - Leverage: math.LegacyNewDec(5), - Position: types.Position_SHORT, - PoolId: firstPool, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), - StopLossPrice: math.LegacyZeroDec(), - } + firstPosition, err := suite.app.PerpetualKeeper.Open(suite.ctx, firstOpenPositionMsg) + suite.Require().NoError(err) - firstPosition, err := suite.app.PerpetualKeeper.Open(ctx, firstOpenPositionMsg) - suite.Require().NoError(err) - - secondOpenPositionMsg := &types.MsgOpen{ - Creator: secondPositionCreator.String(), - Leverage: math.LegacyNewDec(5), - Position: types.Position_SHORT, - PoolId: secondPool, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), - StopLossPrice: math.LegacyZeroDec(), - } + secondOpenPositionMsg := &types.MsgOpen{ + Creator: secondPositionCreator.String(), + Leverage: math.LegacyNewDec(5), + Position: types.Position_SHORT, + PoolId: secondPool, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), + TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), + StopLossPrice: math.LegacyZeroDec(), + } - secondPosition, err := suite.app.PerpetualKeeper.Open(ctx, secondOpenPositionMsg) - suite.Require().NoError(err) + secondPosition, err := suite.app.PerpetualKeeper.Open(suite.ctx, secondOpenPositionMsg) + suite.Require().NoError(err) - k.RemovePool(ctx, firstPool) - suite.app.AmmKeeper.RemovePool(ctx, secondPool) + suite.app.PerpetualKeeper.RemovePool(suite.ctx, firstPool) + suite.app.AmmKeeper.RemovePool(suite.ctx, secondPool) - _, err = msg.ClosePositions(ctx, &types.MsgClosePositions{ - Creator: sample.AccAddress(), - Liquidate: []types.PositionRequest{ - { - Address: firstPositionCreator.String(), - Id: firstPosition.Id, - }, - { - Address: secondPositionCreator.String(), - Id: secondPosition.Id, - }, - { - Address: sample.AccAddress(), - Id: 2000, - }, - }, - StopLoss: []types.PositionRequest{ - { - Address: firstPositionCreator.String(), - Id: firstPosition.Id, - }, - { - Address: sample.AccAddress(), - Id: 2000, + return &types.MsgClosePositions{ + Creator: sample.AccAddress(), + Liquidate: []types.PositionRequest{ + { + Address: firstPositionCreator.String(), + Id: firstPosition.Id, + }, + { + Address: secondPositionCreator.String(), + Id: secondPosition.Id, + }, + { + Address: sample.AccAddress(), + Id: 2000, + }, + }, + StopLoss: []types.PositionRequest{ + { + Address: firstPositionCreator.String(), + Id: firstPosition.Id, + }, + { + Address: sample.AccAddress(), + Id: 2000, + }, + }, + TakeProfit: []types.PositionRequest{ + { + Address: firstPositionCreator.String(), + Id: firstPosition.Id, + }, + { + Address: sample.AccAddress(), + Id: 2000, + }, + }, + } }, + "", + 2, }, - TakeProfit: []types.PositionRequest{ - { - Address: firstPositionCreator.String(), - Id: firstPosition.Id, - }, - { - Address: sample.AccAddress(), - Id: 2000, + { + "Liquidate unhealthy position", + func() *types.MsgClosePositions { + suite.ResetSuite() + firstPool := uint64(1) + + suite.SetPerpetualPool(firstPool) + + amount := math.NewInt(400) + + addr := suite.AddAccounts(1, nil) + firstPositionCreator := addr[0] + tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + + firstOpenPositionMsg := &types.MsgOpen{ + Creator: firstPositionCreator.String(), + Leverage: math.LegacyNewDec(5), + Position: types.Position_LONG, + PoolId: firstPool, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), + TakeProfitPrice: tradingAssetPrice.MulInt64(4), + StopLossPrice: math.LegacyZeroDec(), + } + + firstPosition, err := suite.app.PerpetualKeeper.Open(suite.ctx, firstOpenPositionMsg) + suite.Require().NoError(err) + + // Increase unpaid liability to reduce the MTP health + mtp, _ := suite.app.PerpetualKeeper.GetMTP(suite.ctx, firstPositionCreator, firstPosition.Id) + mtp.BorrowInterestUnpaidLiability = math.NewInt(389) + suite.app.PerpetualKeeper.SetMTP(suite.ctx, &mtp) + + return &types.MsgClosePositions{ + Creator: sample.AccAddress(), + Liquidate: []types.PositionRequest{ + { + Address: firstPositionCreator.String(), + Id: firstPosition.Id, + }, + }, + StopLoss: []types.PositionRequest{}, + TakeProfit: []types.PositionRequest{}, + } }, + "", + 0, }, - }) - suite.Require().Nil(err) + { + "Close at stop Loss", + func() *types.MsgClosePositions { + suite.ResetSuite() + firstPool := uint64(1) -} + suite.SetPerpetualPool(firstPool) -func (suite *PerpetualKeeperTestSuite) TestMsgServerClose_HandleLiquidateCheck() { - k := suite.app.PerpetualKeeper - ctx := suite.ctx - msg := keeper.NewMsgServerImpl(*k) + amount := math.NewInt(400) - firstPool := uint64(1) + addr := suite.AddAccounts(1, nil) + firstPositionCreator := addr[0] + tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) - suite.SetPerpetualPool(firstPool) + firstOpenPositionMsg := &types.MsgOpen{ + Creator: firstPositionCreator.String(), + Leverage: math.LegacyNewDec(5), + Position: types.Position_LONG, + PoolId: firstPool, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), + TakeProfitPrice: tradingAssetPrice.MulInt64(4), + StopLossPrice: math.LegacyMustNewDecFromStr("2.00"), + } - amount := math.NewInt(400) + firstPosition, err := suite.app.PerpetualKeeper.Open(suite.ctx, firstOpenPositionMsg) + suite.Require().NoError(err) - addr := suite.AddAccounts(1, nil) - firstPositionCreator := addr[0] + suite.app.OracleKeeper.SetPrice(suite.ctx, oracletypes.Price{ + Asset: "ATOM", + Price: math.LegacyMustNewDecFromStr("2.00"), + Source: "elys", + Provider: oracleProvider.String(), + Timestamp: uint64(suite.ctx.BlockTime().Unix()), + }) - suite.app.AssetprofileKeeper.SetEntry(ctx, assetprofiletypes.Entry{ - BaseDenom: ptypes.ATOM, - Denom: ptypes.ATOM, - Decimals: 6, - }) + return &types.MsgClosePositions{ + Creator: sample.AccAddress(), + Liquidate: []types.PositionRequest{}, + StopLoss: []types.PositionRequest{ + { + Address: firstPositionCreator.String(), + Id: firstPosition.Id, + }, + }, + TakeProfit: []types.PositionRequest{}, + } + }, + "", + 0, + }, + { + "Close at Take Profit Price", + func() *types.MsgClosePositions { + suite.ResetSuite() + firstPool := uint64(1) - tradingAssetPrice, err := k.GetAssetPrice(suite.ctx, ptypes.ATOM) - suite.Require().NoError(err) + suite.SetPerpetualPool(firstPool) - firstOpenPositionMsg := &types.MsgOpen{ - Creator: firstPositionCreator.String(), - Leverage: math.LegacyNewDec(5), - Position: types.Position_LONG, - PoolId: firstPool, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: tradingAssetPrice.MulInt64(4), - StopLossPrice: math.LegacyZeroDec(), - } + amount := math.NewInt(400) + + addr := suite.AddAccounts(1, nil) + firstPositionCreator := addr[0] + tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + + firstOpenPositionMsg := &types.MsgOpen{ + Creator: firstPositionCreator.String(), + Leverage: math.LegacyNewDec(5), + Position: types.Position_LONG, + PoolId: firstPool, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), + TakeProfitPrice: tradingAssetPrice.MulInt64(4), + StopLossPrice: math.LegacyMustNewDecFromStr("2.00"), + } + + firstPosition, err := suite.app.PerpetualKeeper.Open(suite.ctx, firstOpenPositionMsg) + suite.Require().NoError(err) - firstPosition, err := suite.app.PerpetualKeeper.Open(ctx, firstOpenPositionMsg) - suite.Require().NoError(err) + suite.app.OracleKeeper.SetPrice(suite.ctx, oracletypes.Price{ + Asset: "ATOM", + Price: tradingAssetPrice.MulInt64(4), + Source: "elys", + Provider: oracleProvider.String(), + Timestamp: uint64(suite.ctx.BlockTime().Unix()), + }) - _, err = msg.ClosePositions(ctx, &types.MsgClosePositions{ - Creator: sample.AccAddress(), - Liquidate: []types.PositionRequest{ - { - Address: firstPositionCreator.String(), - Id: firstPosition.Id, + return &types.MsgClosePositions{ + Creator: sample.AccAddress(), + Liquidate: []types.PositionRequest{}, + StopLoss: []types.PositionRequest{}, + TakeProfit: []types.PositionRequest{ + { + Address: firstPositionCreator.String(), + Id: firstPosition.Id, + }, + }, + } }, + "", + 0, }, - StopLoss: []types.PositionRequest{{ - Address: firstPositionCreator.String(), - Id: firstPosition.Id, - }}, - TakeProfit: []types.PositionRequest{{ - Address: firstPositionCreator.String(), - Id: firstPosition.Id, - }}, - }) - suite.Require().Nil(err) + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + msg := tc.setup() + msgSrvr := keeper.NewMsgServerImpl(*suite.app.PerpetualKeeper) + _, err := msgSrvr.ClosePositions(suite.ctx, msg) + if tc.expectedErrMsg != "" { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expectedErrMsg) + } else { + suite.Require().NoError(err) + totalMTPs := suite.app.PerpetualKeeper.GetAllMTPs(suite.ctx) + suite.Require().Equal(tc.expectedTotalPositions, len(totalMTPs)) + } + }) + } } diff --git a/x/perpetual/keeper/msg_server_dewhitelist_test.go b/x/perpetual/keeper/msg_server_dewhitelist_test.go index 2e32ea38a..6cc905425 100644 --- a/x/perpetual/keeper/msg_server_dewhitelist_test.go +++ b/x/perpetual/keeper/msg_server_dewhitelist_test.go @@ -36,11 +36,17 @@ func (suite *PerpetualKeeperTestSuite) TestMsgServerDeWhileList() { t.Run("Successful", func(t *testing.T) { msg := keeper.NewMsgServerImpl(*suite.app.PerpetualKeeper) + address := sample.AccAddress() _, err := msg.Dewhitelist(suite.ctx, &types.MsgDewhitelist{ Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), - WhitelistedAddress: sample.AccAddress(), + WhitelistedAddress: address, + }) + suite.Require().Nil(err) + isWhitelisted, err := suite.app.PerpetualKeeper.IsWhitelisted(suite.ctx, &types.IsWhitelistedRequest{ + Address: address, }) suite.Require().Nil(err) + suite.Require().Equal(false, isWhitelisted.IsWhitelisted) }) } diff --git a/x/perpetual/keeper/msg_server_whitelist_test.go b/x/perpetual/keeper/msg_server_whitelist_test.go index c78f9528f..f49961a8f 100644 --- a/x/perpetual/keeper/msg_server_whitelist_test.go +++ b/x/perpetual/keeper/msg_server_whitelist_test.go @@ -36,11 +36,17 @@ func (suite *PerpetualKeeperTestSuite) TestMsgServerWhileList() { t.Run("Successful", func(t *testing.T) { msg := keeper.NewMsgServerImpl(*suite.app.PerpetualKeeper) + address := sample.AccAddress() _, err := msg.Whitelist(suite.ctx, &types.MsgWhitelist{ Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), - WhitelistedAddress: sample.AccAddress(), + WhitelistedAddress: address, + }) + suite.Require().Nil(err) + isWhitelisted, err := suite.app.PerpetualKeeper.IsWhitelisted(suite.ctx, &types.IsWhitelistedRequest{ + Address: address, }) suite.Require().Nil(err) + suite.Require().Equal(true, isWhitelisted.IsWhitelisted) }) } diff --git a/x/perpetual/keeper/open_consolidate_test.go b/x/perpetual/keeper/open_consolidate_test.go index 1b28363ba..7b94ac906 100644 --- a/x/perpetual/keeper/open_consolidate_test.go +++ b/x/perpetual/keeper/open_consolidate_test.go @@ -7,165 +7,234 @@ import ( "github.com/elys-network/elys/x/perpetual/types" ) -func (suite *PerpetualKeeperTestSuite) TestOpenConsolidate_ErrPoolDoesNotExist() { - k := suite.app.PerpetualKeeper - ctx := suite.ctx - - firstPool := uint64(1) - suite.SetPerpetualPool(firstPool) - - amount := math.NewInt(400) - addr := suite.AddAccounts(2, nil) - firstPositionCreator := addr[0] - - firstOpenPositionMsg := &types.MsgOpen{ - Creator: firstPositionCreator.String(), - Leverage: math.LegacyNewDec(5), - Position: types.Position_SHORT, - PoolId: firstPool, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), - StopLossPrice: math.LegacyZeroDec(), +func (suite *PerpetualKeeperTestSuite) TestOpenConsolidate() { + testCases := []struct { + name string + setup func() (*types.MsgOpen, *types.MTP, *types.MTP) + expectedErrMsg string + }{ + { + "Pool does not exist", + func() (*types.MsgOpen, *types.MTP, *types.MTP) { + suite.ResetSuite() + + firstPool := uint64(1) + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + _, _, ammPool := suite.SetPerpetualPool(1) + tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(2), + Position: types.Position_LONG, + PoolId: ammPool.PoolId, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, math.NewInt(1000)), + TakeProfitPrice: tradingAssetPrice.MulInt64(4), + StopLossPrice: math.LegacyZeroDec(), + } + + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) + mtp, err := suite.app.PerpetualKeeper.GetMTP(suite.ctx, positionCreator, position.Id) + suite.Require().NoError(err) + + suite.app.AmmKeeper.RemovePool(suite.ctx, firstPool) + + return openPositionMsg, &mtp, &mtp + }, + "perpetual pool does not exist", + }, + { + "Mtp health will be low for the safety factor", + func() (*types.MsgOpen, *types.MTP, *types.MTP) { + suite.ResetSuite() + + firstPool := uint64(1) + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + suite.SetPerpetualPool(1) + _, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + + amount := math.NewInt(400) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(5), + Position: types.Position_SHORT, + PoolId: firstPool, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), + TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), + StopLossPrice: math.LegacyZeroDec(), + } + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) + mtp, err := suite.app.PerpetualKeeper.GetMTP(suite.ctx, positionCreator, position.Id) + suite.Require().NoError(err) + + params := suite.app.PerpetualKeeper.GetParams(suite.ctx) + params.SafetyFactor = math.LegacyMustNewDecFromStr("1.30") + suite.app.PerpetualKeeper.SetParams(suite.ctx, ¶ms) + + return openPositionMsg, &mtp, &mtp + }, + "mtp health would be too low for safety factor", + }, + { + "Sucess: MTP consolidation", + func() (*types.MsgOpen, *types.MTP, *types.MTP) { + suite.ResetSuite() + + firstPool := uint64(1) + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + suite.SetPerpetualPool(1) + _, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + + amount := math.NewInt(400) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(5), + Position: types.Position_SHORT, + PoolId: firstPool, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), + TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), + StopLossPrice: math.LegacyZeroDec(), + } + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) + mtp, err := suite.app.PerpetualKeeper.GetMTP(suite.ctx, positionCreator, position.Id) + suite.Require().NoError(err) + + return openPositionMsg, &mtp, &mtp + }, + "", + }, } - - firstPosition, err := suite.app.PerpetualKeeper.Open(ctx, firstOpenPositionMsg) - suite.Require().NoError(err) - - mtp, err := k.GetMTP(ctx, firstPositionCreator, firstPosition.Id) - suite.Require().NoError(err) - - suite.app.AmmKeeper.RemovePool(ctx, firstPool) - - positionCreator := addr[1] - tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) - - msg := &types.MsgOpen{ - Creator: positionCreator.String(), - Leverage: math.LegacyNewDec(5), - Position: types.Position_LONG, - PoolId: firstPool, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: tradingAssetPrice.MulInt64(4), - StopLossPrice: math.LegacyZeroDec()} - - _, err = k.OpenConsolidate( - ctx, - &mtp, - &mtp, - msg, - ptypes.BaseCurrency, - ) - - suite.Require().ErrorIs(err, types.ErrPoolDoesNotExist) -} - -func (suite *PerpetualKeeperTestSuite) TestOpenConsolidate_ErrMTPUnhealthy() { - k := suite.app.PerpetualKeeper - ctx := suite.ctx - - firstPool := uint64(1) - suite.SetPerpetualPool(firstPool) - - amount := math.NewInt(400) - addr := suite.AddAccounts(2, nil) - firstPositionCreator := addr[0] - - firstOpenPositionMsg := &types.MsgOpen{ - Creator: firstPositionCreator.String(), - Leverage: math.LegacyNewDec(5), - Position: types.Position_SHORT, - PoolId: firstPool, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), - StopLossPrice: math.LegacyZeroDec(), + for _, tc := range testCases { + suite.Run(tc.name, func() { + msg, existingMtp, newMtp := tc.setup() + _, err := suite.app.PerpetualKeeper.OpenConsolidate(suite.ctx, existingMtp, newMtp, msg, ptypes.BaseCurrency) + + if tc.expectedErrMsg != "" { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expectedErrMsg) + } else { + suite.Require().NoError(err) + } + }) } - - firstPosition, err := suite.app.PerpetualKeeper.Open(ctx, firstOpenPositionMsg) - suite.Require().NoError(err) - - mtp, err := k.GetMTP(ctx, firstPositionCreator, firstPosition.Id) - suite.Require().NoError(err) - - positionCreator := addr[1] - tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) - - msg := &types.MsgOpen{ - Creator: positionCreator.String(), - Leverage: math.LegacyNewDec(5), - Position: types.Position_LONG, - PoolId: firstPool, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: tradingAssetPrice.MulInt64(4), - StopLossPrice: math.LegacyZeroDec()} - - params := k.GetParams(ctx) - params.SafetyFactor = math.LegacyMustNewDecFromStr("1.30") - k.SetParams(ctx, ¶ms) - - _, err = k.OpenConsolidate( - ctx, - &mtp, - &mtp, - msg, - ptypes.BaseCurrency, - ) - - suite.Require().ErrorIs(err, types.ErrMTPUnhealthy) } -func (suite *PerpetualKeeperTestSuite) TestOpenConsolidate_Successful() { - k := suite.app.PerpetualKeeper - ctx := suite.ctx - - firstPool := uint64(1) - suite.SetPerpetualPool(firstPool) - - amount := math.NewInt(400) - addr := suite.AddAccounts(2, nil) - firstPositionCreator := addr[0] - - firstOpenPositionMsg := &types.MsgOpen{ - Creator: firstPositionCreator.String(), - Leverage: math.LegacyNewDec(5), - Position: types.Position_SHORT, - PoolId: firstPool, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), - StopLossPrice: math.LegacyZeroDec(), +func (suite *PerpetualKeeperTestSuite) TestOpenConsolidateUsingOpen() { + testCases := []struct { + name string + setup func() *types.MsgOpen + expectedErrMsg string + consolidatedMtp *types.MTP + }{ + { + "Sucess: Consolidate two position with different leverage and take profit price", + func() *types.MsgOpen { + suite.ResetSuite() + + firstPool := uint64(1) + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + suite.SetPerpetualPool(1) + _, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + + amount := math.NewInt(400) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(5), + Position: types.Position_SHORT, + PoolId: firstPool, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), + TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), + StopLossPrice: math.LegacyZeroDec(), + } + _, err = suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) + + openPositionMsg.Leverage = math.LegacyNewDec(3) + openPositionMsg.TakeProfitPrice = math.LegacyMustNewDecFromStr("0.5") + + return openPositionMsg + }, + "", + &types.MTP{ + Collateral: math.NewInt(800), + Liabilities: math.NewInt(641), + Custody: math.NewInt(4000), + TakeProfitPrice: math.LegacyMustNewDecFromStr("0.692857142857142857"), + }, + }, + { + "Sucess: add collateral for the existing position", + func() *types.MsgOpen { + suite.ResetSuite() + + firstPool := uint64(1) + addr := suite.AddAccounts(1, nil) + positionCreator := addr[0] + suite.SetPerpetualPool(1) + _, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + suite.Require().NoError(err) + + amount := math.NewInt(400) + openPositionMsg := &types.MsgOpen{ + Creator: positionCreator.String(), + Leverage: math.LegacyNewDec(5), + Position: types.Position_SHORT, + PoolId: firstPool, + TradingAsset: ptypes.ATOM, + Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), + TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), + StopLossPrice: math.LegacyZeroDec(), + } + _, err = suite.app.PerpetualKeeper.Open(suite.ctx, openPositionMsg) + suite.Require().NoError(err) + + // make new Positon leverage 0 to add collateral + openPositionMsg.Leverage = math.LegacyNewDec(0) + + return openPositionMsg + }, + "", + &types.MTP{ + Collateral: math.NewInt(800), + Liabilities: math.NewInt(400), + Custody: math.NewInt(2800), + TakeProfitPrice: math.LegacyMustNewDecFromStr("0.95"), + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + msg := tc.setup() + position, err := suite.app.PerpetualKeeper.Open(suite.ctx, msg) + suite.Require().NoError(err) + + if tc.expectedErrMsg != "" { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expectedErrMsg) + } else { + suite.Require().NoError(err) + consolidateMtp, mtpErr := suite.app.PerpetualKeeper.GetMTP(suite.ctx, sdk.MustAccAddressFromBech32(msg.Creator), position.Id) + suite.Require().NoError(mtpErr) + suite.Require().Equal(tc.consolidatedMtp.Collateral, consolidateMtp.Collateral) + suite.Require().Equal(tc.consolidatedMtp.Liabilities, consolidateMtp.Liabilities) + suite.Require().Equal(tc.consolidatedMtp.Custody, consolidateMtp.Custody) + suite.Require().Equal(tc.consolidatedMtp.TakeProfitPrice, consolidateMtp.TakeProfitPrice) + } + }) } - - firstPosition, err := suite.app.PerpetualKeeper.Open(ctx, firstOpenPositionMsg) - suite.Require().NoError(err) - - mtp, err := k.GetMTP(ctx, firstPositionCreator, firstPosition.Id) - suite.Require().NoError(err) - - positionCreator := addr[1] - tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) - - msg := &types.MsgOpen{ - Creator: positionCreator.String(), - Leverage: math.LegacyNewDec(5), - Position: types.Position_LONG, - PoolId: firstPool, - TradingAsset: ptypes.ATOM, - Collateral: sdk.NewCoin(ptypes.BaseCurrency, amount), - TakeProfitPrice: tradingAssetPrice.MulInt64(4), - StopLossPrice: math.LegacyZeroDec()} - - resp, err := k.OpenConsolidate( - ctx, - &mtp, - &mtp, - msg, - ptypes.BaseCurrency, - ) - - suite.Require().Nil(err) - suite.Require().Equal(resp.Id, firstPosition.Id) } diff --git a/x/perpetual/keeper/open_test.go b/x/perpetual/keeper/open_test.go index ad324a1bd..2fe967a7f 100644 --- a/x/perpetual/keeper/open_test.go +++ b/x/perpetual/keeper/open_test.go @@ -1,6 +1,8 @@ package keeper_test import ( + "fmt" + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" @@ -20,7 +22,9 @@ func (suite *PerpetualKeeperTestSuite) TestOpen() { positionCreator := addr[1] poolId := uint64(1) tradingAssetPrice, err := suite.app.PerpetualKeeper.GetAssetPrice(suite.ctx, ptypes.ATOM) + params := suite.app.PerpetualKeeper.GetParams(suite.ctx) suite.Require().NoError(err) + var ammPool ammtypes.Pool msg := &types.MsgOpen{ Creator: positionCreator.String(), @@ -48,6 +52,71 @@ func (suite *PerpetualKeeperTestSuite) TestOpen() { func(mtp *types.MTP) { }, }, + { + "borrow asset is usdc in long", + "invalid operation: the borrowed asset cannot be the base currency: invalid borrowing asset", + func() { + suite.ResetSuite() + suite.SetupCoinPrices() + + msg.TradingAsset = ptypes.BaseCurrency + }, + func(mtp *types.MTP) { + }, + }, + { + "invalid collateral", + "collateral must either match the borrowed asset or be the base currency: invalid borrowing asset", + func() { + suite.ResetSuite() + suite.SetupCoinPrices() + + msg.Collateral.Denom = ptypes.ATOM + msg.TradingAsset = ptypes.Elys + }, + func(mtp *types.MTP) { + }, + }, + { + "short base currency", + "cannot take a short position against the base currency: invalid borrowing asset", + func() { + suite.ResetSuite() + suite.SetupCoinPrices() + + msg.Position = types.Position_SHORT + msg.TradingAsset = ptypes.BaseCurrency + }, + func(mtp *types.MTP) { + }, + }, + { + "short same coin as collateral", + "invalid operation: collateral asset cannot be identical to the borrowed asset for a short position: invalid collateral asset", + func() { + suite.ResetSuite() + suite.SetupCoinPrices() + + msg.Position = types.Position_SHORT + msg.TradingAsset = ptypes.ATOM + }, + func(mtp *types.MTP) { + }, + }, + { + "short with nonUSDC coin", + "invalid collateral: the collateral asset for a short position must be the base currency: invalid collateral asset", + func() { + suite.ResetSuite() + suite.SetupCoinPrices() + + msg.Position = types.Position_SHORT + msg.Collateral.Denom = ptypes.Elys + msg.TradingAsset = ptypes.ATOM + }, + func(mtp *types.MTP) { + }, + }, { "user not whitelisted", "unauthorised: address not on whitelist", @@ -60,6 +129,9 @@ func (suite *PerpetualKeeperTestSuite) TestOpen() { params.WhitelistingEnabled = true err := suite.app.PerpetualKeeper.SetParams(suite.ctx, ¶ms) suite.Require().NoError(err) + msg.Position = types.Position_LONG + msg.Collateral.Denom = ptypes.BaseCurrency + msg.TradingAsset = ptypes.ATOM }, func(mtp *types.MTP) { }, @@ -216,6 +288,26 @@ func (suite *PerpetualKeeperTestSuite) TestOpen() { func(mtp *types.MTP) { }, }, + { + "take profit price below minimum ratio", + fmt.Sprintf("take profit price should be between %s and %s times of current market price for long", params.MinimumLongTakeProfitPriceRatio.String(), params.MaximumLongTakeProfitPriceRatio.String()), + func() { + suite.ResetSuite() + suite.SetupCoinPrices() + msg.TakeProfitPrice = tradingAssetPrice.Mul(params.MinimumLongTakeProfitPriceRatio).Quo(math.LegacyNewDec(2)) + }, + func(mtp *types.MTP) { + }, + }, + { + "take profit price above maximum ratio", + fmt.Sprintf("take profit price should be between %s and %s times of current market price for long", params.MinimumLongTakeProfitPriceRatio.String(), params.MaximumLongTakeProfitPriceRatio.String()), + func() { + msg.TakeProfitPrice = tradingAssetPrice.Mul(params.MaximumLongTakeProfitPriceRatio).Mul(math.LegacyNewDec(2)) + }, + func(mtp *types.MTP) { + }, + }, } for _, tc := range testCases {