From e0876fde4244f77f56e97608a05b722753be544d Mon Sep 17 00:00:00 2001 From: Abhinav Kumar <57705190+avkr003@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:27:28 +0530 Subject: [PATCH] Fixing close position for non-zero exit fee pool (#685) * Fixing close position for non-zero exit fee pool * Fixing query exit pool estimation, position health calc and adding unit test cases * Fixing test cases * Fixing CheckAmmPoolUsdcBalance incorrect comparison (amount to value) * Fixing test cases --- x/amm/keeper/apply_exit_pool_state_change.go | 30 +-- .../apply_exit_pool_state_change_test.go | 2 +- x/amm/keeper/keeper_exit_pool.go | 18 +- x/amm/keeper/msg_server_exit_pool.go | 2 +- x/amm/keeper/query_exit_pool_estimation.go | 4 +- x/leveragelp/keeper/begin_blocker_test.go | 6 +- x/leveragelp/keeper/hooks_amm.go | 8 +- x/leveragelp/keeper/keeper_test.go | 189 +++++++++++++----- x/leveragelp/keeper/msg_server_close_test.go | 188 +++++++++++++++++ x/leveragelp/keeper/position.go | 4 +- x/leveragelp/keeper/position_close.go | 4 +- x/leveragelp/keeper/position_close_test.go | 38 ++-- x/leveragelp/keeper/position_open_test.go | 2 +- x/leveragelp/keeper/utils_test.go | 2 +- x/leveragelp/types/expected_keepers.go | 2 +- x/masterchef/keeper/hooks_masterchef_test.go | 4 +- 16 files changed, 397 insertions(+), 106 deletions(-) create mode 100644 x/leveragelp/keeper/msg_server_close_test.go diff --git a/x/amm/keeper/apply_exit_pool_state_change.go b/x/amm/keeper/apply_exit_pool_state_change.go index bda236eef..949067bf4 100644 --- a/x/amm/keeper/apply_exit_pool_state_change.go +++ b/x/amm/keeper/apply_exit_pool_state_change.go @@ -6,45 +6,45 @@ import ( "github.com/elys-network/elys/x/amm/types" ) -func (k Keeper) ApplyExitPoolStateChange(ctx sdk.Context, pool types.Pool, exiter sdk.AccAddress, numShares math.Int, exitCoins sdk.Coins) error { +func (k Keeper) ApplyExitPoolStateChange(ctx sdk.Context, pool types.Pool, exiter sdk.AccAddress, numShares math.Int, exitCoins sdk.Coins) (sdk.Coins, error) { // Withdraw exit amount of token from commitment module to exiter's wallet. poolShareDenom := types.GetPoolShareDenom(pool.GetPoolId()) // Withdraw committed LP tokens err := k.commitmentKeeper.UncommitTokens(ctx, exiter, poolShareDenom, numShares) if err != nil { - return err + return sdk.Coins{}, err } - if err := k.bankKeeper.SendCoins(ctx, sdk.MustAccAddressFromBech32(pool.GetAddress()), exiter, exitCoins); err != nil { - return err + if err = k.bankKeeper.SendCoins(ctx, sdk.MustAccAddressFromBech32(pool.GetAddress()), exiter, exitCoins); err != nil { + return sdk.Coins{}, err } exitFeeCoins := PortionCoins(exitCoins, pool.PoolParams.ExitFee) rebalanceTreasury := sdk.MustAccAddressFromBech32(pool.GetRebalanceTreasury()) - if err := k.bankKeeper.SendCoins(ctx, exiter, rebalanceTreasury, exitFeeCoins); err != nil { - return err + if err = k.bankKeeper.SendCoins(ctx, exiter, rebalanceTreasury, exitFeeCoins); err != nil { + return sdk.Coins{}, err } - if err := k.OnCollectFee(ctx, pool, exitFeeCoins); err != nil { - return err + if err = k.OnCollectFee(ctx, pool, exitFeeCoins); err != nil { + return sdk.Coins{}, err } - if err := k.BurnPoolShareFromAccount(ctx, pool, exiter, numShares); err != nil { - return err + if err = k.BurnPoolShareFromAccount(ctx, pool, exiter, numShares); err != nil { + return sdk.Coins{}, err } - if err := k.SetPool(ctx, pool); err != nil { - return err + if err = k.SetPool(ctx, pool); err != nil { + return sdk.Coins{}, err } types.EmitRemoveLiquidityEvent(ctx, exiter, pool.GetPoolId(), exitCoins) if k.hooks != nil { - err := k.hooks.AfterExitPool(ctx, exiter, pool, numShares, exitCoins) + err = k.hooks.AfterExitPool(ctx, exiter, pool, numShares, exitCoins) if err != nil { - return err + return sdk.Coins{}, err } } k.RecordTotalLiquidityDecrease(ctx, exitCoins) - return nil + return exitCoins.Sub(exitFeeCoins...), nil } diff --git a/x/amm/keeper/apply_exit_pool_state_change_test.go b/x/amm/keeper/apply_exit_pool_state_change_test.go index 9fa6adf65..65c8de89d 100644 --- a/x/amm/keeper/apply_exit_pool_state_change_test.go +++ b/x/amm/keeper/apply_exit_pool_state_change_test.go @@ -75,6 +75,6 @@ func (suite *KeeperTestSuite) TestApplyExitPoolStateChange_WithdrawFromCommitmen suite.Require().True(lpTokenBalance.Amount.Equal(sdk.ZeroInt())) ctx = ctx.WithBlockTime(ctx.BlockTime().Add(time.Hour)) - err = app.AmmKeeper.ApplyExitPoolStateChange(ctx, pool, addrs[0], pool.TotalShares.Amount, coins) + _, err = app.AmmKeeper.ApplyExitPoolStateChange(ctx, pool, addrs[0], pool.TotalShares.Amount, coins) suite.Require().NoError(err) } diff --git a/x/amm/keeper/keeper_exit_pool.go b/x/amm/keeper/keeper_exit_pool.go index 9dd9879ce..de3dd1cac 100644 --- a/x/amm/keeper/keeper_exit_pool.go +++ b/x/amm/keeper/keeper_exit_pool.go @@ -14,35 +14,35 @@ func (k Keeper) ExitPool( shareInAmount math.Int, tokenOutMins sdk.Coins, tokenOutDenom string, -) (exitCoins sdk.Coins, err error) { +) (exitCoins, exitCoinsAfterExitFee sdk.Coins, err error) { pool, poolExists := k.GetPool(ctx, poolId) if !poolExists { - return sdk.Coins{}, types.ErrInvalidPoolId + return sdk.Coins{}, sdk.Coins{}, types.ErrInvalidPoolId } totalSharesAmount := pool.GetTotalShares() if shareInAmount.GTE(totalSharesAmount.Amount) { - return sdk.Coins{}, errorsmod.Wrapf(types.ErrInvalidMathApprox, "Trying to exit >= the number of shares contained in the pool.") + return sdk.Coins{}, sdk.Coins{}, errorsmod.Wrapf(types.ErrInvalidMathApprox, "Trying to exit >= the number of shares contained in the pool.") } else if shareInAmount.LTE(sdk.ZeroInt()) { - return sdk.Coins{}, errorsmod.Wrapf(types.ErrInvalidMathApprox, "Trying to exit a negative amount of shares") + return sdk.Coins{}, sdk.Coins{}, errorsmod.Wrapf(types.ErrInvalidMathApprox, "Trying to exit a negative amount of shares") } exitCoins, err = pool.ExitPool(ctx, k.oracleKeeper, k.accountedPoolKeeper, shareInAmount, tokenOutDenom) if err != nil { - return sdk.Coins{}, err + return sdk.Coins{}, sdk.Coins{}, err } if !tokenOutMins.DenomsSubsetOf(exitCoins) || tokenOutMins.IsAnyGT(exitCoins) { - return sdk.Coins{}, errorsmod.Wrapf(types.ErrLimitMinAmount, + return sdk.Coins{}, sdk.Coins{}, errorsmod.Wrapf(types.ErrLimitMinAmount, "Exit pool returned %s , minimum tokens out specified as %s", exitCoins, tokenOutMins) } - err = k.ApplyExitPoolStateChange(ctx, pool, sender, shareInAmount, exitCoins) + exitCoinsAfterExitFee, err = k.ApplyExitPoolStateChange(ctx, pool, sender, shareInAmount, exitCoins) if err != nil { - return sdk.Coins{}, err + return sdk.Coins{}, sdk.Coins{}, err } // Decrease liquidty amount k.RecordTotalLiquidityDecrease(ctx, exitCoins) - return exitCoins, nil + return exitCoins, exitCoinsAfterExitFee, nil } diff --git a/x/amm/keeper/msg_server_exit_pool.go b/x/amm/keeper/msg_server_exit_pool.go index 4fec9dc42..829b9f0f3 100644 --- a/x/amm/keeper/msg_server_exit_pool.go +++ b/x/amm/keeper/msg_server_exit_pool.go @@ -15,7 +15,7 @@ func (k msgServer) ExitPool(goCtx context.Context, msg *types.MsgExitPool) (*typ return nil, err } - exitCoins, err := k.Keeper.ExitPool(ctx, sender, msg.PoolId, msg.ShareAmountIn, msg.MinAmountsOut, msg.TokenOutDenom) + exitCoins, _, err := k.Keeper.ExitPool(ctx, sender, msg.PoolId, msg.ShareAmountIn, msg.MinAmountsOut, msg.TokenOutDenom) if err != nil { return nil, err } diff --git a/x/amm/keeper/query_exit_pool_estimation.go b/x/amm/keeper/query_exit_pool_estimation.go index 65b828f3d..9175abb05 100644 --- a/x/amm/keeper/query_exit_pool_estimation.go +++ b/x/amm/keeper/query_exit_pool_estimation.go @@ -51,5 +51,7 @@ func (k Keeper) ExitPoolEst( return sdk.Coins{}, math.LegacyZeroDec(), err } - return exitCoins, weightBalanceBonus, nil + exitFeeCoins := PortionCoins(exitCoins, pool.PoolParams.ExitFee) + + return exitCoins.Sub(exitFeeCoins...), weightBalanceBonus, nil } diff --git a/x/leveragelp/keeper/begin_blocker_test.go b/x/leveragelp/keeper/begin_blocker_test.go index 190c28f87..1f0527b52 100644 --- a/x/leveragelp/keeper/begin_blocker_test.go +++ b/x/leveragelp/keeper/begin_blocker_test.go @@ -22,7 +22,7 @@ func (suite KeeperTestSuite) TestBeginBlocker() { health, err := k.GetPositionHealth(suite.ctx, *position) suite.Require().NoError(err) // suite.Require().Equal(health.String(), "1.221000000000000000") // slippage enabled on amm - suite.Require().Equal(health.String(), "1.250000000000000000") // slippage disabled on amm + suite.Require().Equal("1.250000000000000000", health.String()) // slippage disabled on amm suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 24 * 500)) suite.app.StablestakeKeeper.BeginBlocker(suite.ctx) @@ -30,7 +30,7 @@ func (suite KeeperTestSuite) TestBeginBlocker() { health, err = k.GetPositionHealth(suite.ctx, *position) suite.Require().NoError(err) // suite.Require().Equal(health.String(), "1.024543738200125865") // slippage enabled on amm - suite.Require().Equal(health.String(), "1.025220422390814025") // slippage disabled on amm + suite.Require().Equal("1.048855698433009587", health.String()) // slippage disabled on amm params := k.GetParams(suite.ctx) params.SafetyFactor = sdk.NewDecWithPrec(11, 1) @@ -57,7 +57,7 @@ func (suite KeeperTestSuite) TestLiquidatePositionIfUnhealthy() { health, err = k.GetPositionHealth(suite.ctx, *position) suite.Require().NoError(err) // suite.Require().Equal(health.String(), "1.024543738200125865") // slippage enabled on amm - suite.Require().Equal(health.String(), "1.025220422390814025") // slippage disabled on amm + suite.Require().Equal("1.048855698433009587", health.String()) // slippage disabled on amm cacheCtx, _ := suite.ctx.CacheContext() params := k.GetParams(cacheCtx) diff --git a/x/leveragelp/keeper/hooks_amm.go b/x/leveragelp/keeper/hooks_amm.go index 545501e2c..1fe09b9de 100644 --- a/x/leveragelp/keeper/hooks_amm.go +++ b/x/leveragelp/keeper/hooks_amm.go @@ -22,11 +22,11 @@ func (k Keeper) CheckAmmPoolUsdcBalance(ctx sdk.Context, ammPool ammtypes.Pool) Quo(sdk.NewDecFromInt(ammPool.TotalShares.Amount)) depositDenom := k.stableKeeper.GetDepositDenom(ctx) + price := k.oracleKeeper.GetAssetPriceFromDenom(ctx, depositDenom) + for _, asset := range ammPool.PoolAssets { - if asset.Token.Denom == depositDenom { - if asset.Token.Amount.LT(leverageLpTvl.RoundInt()) { - return types.ErrInsufficientUsdcAfterOp - } + if asset.Token.Denom == depositDenom && price.MulInt(asset.Token.Amount).LT(leverageLpTvl) { + return types.ErrInsufficientUsdcAfterOp } } return nil diff --git a/x/leveragelp/keeper/keeper_test.go b/x/leveragelp/keeper/keeper_test.go index 402e30f0b..2878d4e63 100644 --- a/x/leveragelp/keeper/keeper_test.go +++ b/x/leveragelp/keeper/keeper_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "testing" + "time" "github.com/cometbft/cometbft/crypto/ed25519" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" @@ -15,10 +16,41 @@ import ( "github.com/stretchr/testify/suite" ) +type assetPriceInfo struct { + denom string + display string + price sdk.Dec +} + const ( initChain = true ) +var ( + priceMap = map[string]assetPriceInfo{ + "uusdc": { + denom: ptypes.BaseCurrency, + display: "USDC", + price: sdk.OneDec(), + }, + "uusdt": { + denom: "uusdt", + display: "USDT", + price: sdk.OneDec(), + }, + "uelys": { + denom: ptypes.Elys, + display: "ELYS", + price: sdk.MustNewDecFromStr("3.0"), + }, + "uatom": { + denom: ptypes.ATOM, + display: "ATOM", + price: sdk.MustNewDecFromStr("6.0"), + }, + } +) + type KeeperTestSuite struct { suite.Suite @@ -35,62 +67,119 @@ func (suite *KeeperTestSuite) SetupTest() { suite.app = app } +func (suite *KeeperTestSuite) ResetSuite() { + suite.SetupTest() +} + +func (suite *KeeperTestSuite) AddBlockTime(d time.Duration) { + suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(d)) +} + +func (suite *KeeperTestSuite) EnableWhiteListing() { + params := suite.app.LeveragelpKeeper.GetParams(suite.ctx) + params.WhitelistingEnabled = true + err := suite.app.LeveragelpKeeper.SetParams(suite.ctx, ¶ms) + if err != nil { + panic(err) + } +} + +func (suite *KeeperTestSuite) DisableWhiteListing() { + params := suite.app.LeveragelpKeeper.GetParams(suite.ctx) + params.WhitelistingEnabled = false + err := suite.app.LeveragelpKeeper.SetParams(suite.ctx, ¶ms) + if err != nil { + panic(err) + } +} + +func (suite *KeeperTestSuite) SetMaxOpenPositions(value int64) { + params := suite.app.LeveragelpKeeper.GetParams(suite.ctx) + params.MaxOpenPositions = value + err := suite.app.LeveragelpKeeper.SetParams(suite.ctx, ¶ms) + if err != nil { + panic(err) + } +} + +func (suite *KeeperTestSuite) SetPoolThreshold(value sdk.Dec) { + params := suite.app.LeveragelpKeeper.GetParams(suite.ctx) + params.PoolOpenThreshold = value + err := suite.app.LeveragelpKeeper.SetParams(suite.ctx, ¶ms) + if err != nil { + panic(err) + } +} + +func (suite *KeeperTestSuite) SetSafetyFactor(value sdk.Dec) { + params := suite.app.LeveragelpKeeper.GetParams(suite.ctx) + params.SafetyFactor = value + err := suite.app.LeveragelpKeeper.SetParams(suite.ctx, ¶ms) + if err != nil { + panic(err) + } +} + +func (suite *KeeperTestSuite) EnablePool(poolId uint64) { + pool, found := suite.app.LeveragelpKeeper.GetPool(suite.ctx, poolId) + if !found { + panic("pool not found") + } + pool.Enabled = true + suite.app.LeveragelpKeeper.SetPool(suite.ctx, pool) +} + +func (suite *KeeperTestSuite) DisablePool(poolId uint64) { + pool, found := suite.app.LeveragelpKeeper.GetPool(suite.ctx, poolId) + if !found { + panic("pool not found") + } + pool.Enabled = false + suite.app.LeveragelpKeeper.SetPool(suite.ctx, pool) +} + func TestKeeperSuite(t *testing.T) { suite.Run(t, new(KeeperTestSuite)) } -func SetupStableCoinPrices(ctx sdk.Context, oracle oraclekeeper.Keeper) { +func SetupCoinPrices(ctx sdk.Context, oracle oraclekeeper.Keeper) { // prices set for USDT and USDC provider := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) - oracle.SetAssetInfo(ctx, oracletypes.AssetInfo{ - Denom: ptypes.BaseCurrency, - Display: "USDC", - Decimal: 6, - }) - oracle.SetAssetInfo(ctx, oracletypes.AssetInfo{ - Denom: "uusdt", - Display: "USDT", - Decimal: 6, - }) - oracle.SetAssetInfo(ctx, oracletypes.AssetInfo{ - Denom: ptypes.Elys, - Display: "ELYS", - Decimal: 6, - }) - oracle.SetAssetInfo(ctx, oracletypes.AssetInfo{ - Denom: ptypes.ATOM, - Display: "ATOM", - Decimal: 6, - }) - - oracle.SetPrice(ctx, oracletypes.Price{ - Asset: "USDC", - Price: sdk.NewDec(1000000), - Source: "elys", - Provider: provider.String(), - Timestamp: uint64(ctx.BlockTime().Unix()), - }) - oracle.SetPrice(ctx, oracletypes.Price{ - Asset: "USDT", - Price: sdk.NewDec(1000000), - Source: "elys", - Provider: provider.String(), - Timestamp: uint64(ctx.BlockTime().Unix()), - }) - oracle.SetPrice(ctx, oracletypes.Price{ - Asset: "ELYS", - Price: sdk.NewDec(100), - Source: "elys", - Provider: provider.String(), - Timestamp: uint64(ctx.BlockTime().Unix()), - }) - oracle.SetPrice(ctx, oracletypes.Price{ - Asset: "ATOM", - Price: sdk.NewDec(100), - Source: "atom", - Provider: provider.String(), - Timestamp: uint64(ctx.BlockTime().Unix()), - }) + + for _, v := range priceMap { + oracle.SetAssetInfo(ctx, oracletypes.AssetInfo{ + Denom: v.denom, + Display: v.display, + Decimal: 6, + }) + oracle.SetPrice(ctx, oracletypes.Price{ + Asset: v.display, + Price: v.price, + Source: "elys", + Provider: provider.String(), + Timestamp: uint64(ctx.BlockTime().Unix()), + }) + } +} + +func AddCoinPrices(ctx sdk.Context, oracle oraclekeeper.Keeper, denoms []string) { + // prices set for USDT and USDC + provider := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + + for _, v := range denoms { + oracle.SetAssetInfo(ctx, oracletypes.AssetInfo{ + Denom: priceMap[v].denom, + Display: priceMap[v].display, + Decimal: 6, + }) + oracle.SetPrice(ctx, oracletypes.Price{ + Asset: priceMap[v].display, + Price: priceMap[v].price, + Source: "elys", + Provider: provider.String(), + Timestamp: uint64(ctx.BlockTime().Unix()), + }) + } } func TestGetAllWhitelistedAddress(t *testing.T) { diff --git a/x/leveragelp/keeper/msg_server_close_test.go b/x/leveragelp/keeper/msg_server_close_test.go new file mode 100644 index 000000000..f480e9604 --- /dev/null +++ b/x/leveragelp/keeper/msg_server_close_test.go @@ -0,0 +1,188 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + simapp "github.com/elys-network/elys/app" + ammtypes "github.com/elys-network/elys/x/amm/types" + "github.com/elys-network/elys/x/leveragelp/types" + ptypes "github.com/elys-network/elys/x/parameter/types" + stablekeeper "github.com/elys-network/elys/x/stablestake/keeper" + stabletypes "github.com/elys-network/elys/x/stablestake/types" + "time" +) + +func initializeForClose(suite *KeeperTestSuite, addresses []sdk.AccAddress, asset1, asset2 string) { + fee := sdk.MustNewDecFromStr("0.0002") + issueAmount := sdk.NewInt(10_000_000_000_000) + for _, address := range addresses { + coins := sdk.NewCoins( + sdk.NewCoin(ptypes.ATOM, issueAmount), + sdk.NewCoin(ptypes.Elys, issueAmount), + sdk.NewCoin(ptypes.BaseCurrency, issueAmount), + ) + err := suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, coins) + if err != nil { + panic(err) + } + err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, address, coins) + if err != nil { + panic(err) + } + } + msgCreatePool := ammtypes.MsgCreatePool{ + Sender: addresses[0].String(), + PoolParams: &ammtypes.PoolParams{ + SwapFee: fee, + ExitFee: fee, + UseOracle: true, + WeightBreakingFeeMultiplier: fee, + WeightBreakingFeeExponent: fee, + ExternalLiquidityRatio: fee, + WeightRecoveryFeePortion: fee, + ThresholdWeightDifference: fee, + FeeDenom: ptypes.Elys, + }, + PoolAssets: []ammtypes.PoolAsset{ + { + Token: sdk.NewInt64Coin(asset1, 100_000_000), + Weight: sdk.NewInt(50), + }, + { + Token: sdk.NewInt64Coin(asset2, 1000_000_000), + Weight: sdk.NewInt(50), + }, + }, + } + poolId, err := suite.app.AmmKeeper.CreatePool(suite.ctx, &msgCreatePool) + if err != nil { + panic(err) + } + suite.app.LeveragelpKeeper.SetPool(suite.ctx, types.NewPool(poolId)) + msgBond := stabletypes.MsgBond{ + Creator: addresses[1].String(), + Amount: issueAmount.QuoRaw(20), + } + stableStakeMsgServer := stablekeeper.NewMsgServerImpl(suite.app.StablestakeKeeper) + _, err = stableStakeMsgServer.Bond(suite.ctx, &msgBond) + if err != nil { + panic(err) + } + msgBond.Creator = addresses[2].String() + _, err = stableStakeMsgServer.Bond(suite.ctx, &msgBond) + if err != nil { + panic(err) + } +} + +func (suite *KeeperTestSuite) TestClose() { + suite.ResetSuite() + SetupCoinPrices(suite.ctx, suite.app.OracleKeeper) + addresses := simapp.AddTestAddrs(suite.app, suite.ctx, 10, sdk.NewInt(1000000)) + asset1 := ptypes.ATOM + asset2 := ptypes.BaseCurrency + initializeForClose(suite, addresses, asset1, asset2) + testCases := []struct { + name string + input *types.MsgClose + expectErr bool + expectErrMsg string + prerequisiteFunction func() + }{ + {"No position to close", + &types.MsgClose{ + Creator: addresses[0].String(), + Id: 1, + LpAmount: sdk.NewInt(0), + }, + true, + types.ErrPositionDoesNotExist.Error(), + func() { + }, + }, + {"Unlock time not reached", + &types.MsgClose{ + Creator: addresses[0].String(), + Id: 1, + LpAmount: sdk.NewInt(0), + }, + true, + "your funds will be locked for 1 hour", + func() { + msg := types.MsgOpen{ + Creator: addresses[0].String(), + CollateralAsset: ptypes.BaseCurrency, + CollateralAmount: sdk.NewInt(10000000), + AmmPoolId: 1, + Leverage: sdk.MustNewDecFromStr("2.0"), + StopLossPrice: sdk.MustNewDecFromStr("50.0"), + } + _, err := suite.app.LeveragelpKeeper.Open(suite.ctx, &msg) + if err != nil { + panic(err) + } + }, + }, + {"Closing whole position", + &types.MsgClose{ + Creator: addresses[0].String(), + Id: 1, + LpAmount: sdk.NewInt(0), + }, + false, + "", + func() { + msg := types.MsgOpen{ + Creator: addresses[0].String(), + CollateralAsset: ptypes.BaseCurrency, + CollateralAmount: sdk.NewInt(10000000), + AmmPoolId: 1, + Leverage: sdk.MustNewDecFromStr("2.0"), + StopLossPrice: sdk.MustNewDecFromStr("50.0"), + } + _, err := suite.app.LeveragelpKeeper.Open(suite.ctx, &msg) + if err != nil { + panic(err) + } + suite.AddBlockTime(time.Hour) + }, + }, + {"Closing partial position", + &types.MsgClose{ + Creator: addresses[0].String(), + Id: 2, + LpAmount: sdk.MustNewDecFromStr("9997999787743811730").TruncateInt(), + }, + false, + "", + func() { + msg := types.MsgOpen{ + Creator: addresses[0].String(), + CollateralAsset: ptypes.BaseCurrency, + CollateralAmount: sdk.NewInt(10000000), + AmmPoolId: 1, + Leverage: sdk.MustNewDecFromStr("2.0"), + StopLossPrice: sdk.MustNewDecFromStr("50.0"), + } + _, err := suite.app.LeveragelpKeeper.Open(suite.ctx, &msg) + if err != nil { + panic(err) + } + suite.AddBlockTime(time.Hour) + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + tc.prerequisiteFunction() + _, err := suite.app.LeveragelpKeeper.Close(suite.ctx, tc.input) + if tc.expectErr { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expectErrMsg) + } else { + suite.Require().NoError(err) + } + }) + } +} diff --git a/x/leveragelp/keeper/position.go b/x/leveragelp/keeper/position.go index d2dcb8dd4..8ad90cd33 100644 --- a/x/leveragelp/keeper/position.go +++ b/x/leveragelp/keeper/position.go @@ -399,11 +399,11 @@ func (k Keeper) GetPositionHealth(ctx sdk.Context, position types.Position) (sdk for _, commitment := range commitments.CommittedTokens { cacheCtx, _ := ctx.CacheContext() cacheCtx = cacheCtx.WithBlockTime(cacheCtx.BlockTime().Add(time.Hour)) - exitCoins, err := k.amm.ExitPool(cacheCtx, position.GetPositionAddress(), position.AmmPoolId, commitment.Amount, sdk.Coins{}, depositDenom) + _, exitCoinsAfterExitFee, err := k.amm.ExitPool(cacheCtx, position.GetPositionAddress(), position.AmmPoolId, commitment.Amount, sdk.Coins{}, depositDenom) if err != nil { return sdk.ZeroDec(), err } - positionVal = positionVal.Add(sdk.NewDecFromInt(exitCoins.AmountOf(depositDenom))) + positionVal = positionVal.Add(sdk.NewDecFromInt(exitCoinsAfterExitFee.AmountOf(depositDenom))) } lr := positionVal.Quo(sdk.NewDecFromBigInt(xl.BigInt())) diff --git a/x/leveragelp/keeper/position_close.go b/x/leveragelp/keeper/position_close.go index 23d5cf6fb..528062c92 100644 --- a/x/leveragelp/keeper/position_close.go +++ b/x/leveragelp/keeper/position_close.go @@ -27,7 +27,7 @@ func (k Keeper) ForceCloseLong(ctx sdk.Context, position types.Position, pool ty } // Exit liquidity with collateral token - exitCoins, err := k.amm.ExitPool(ctx, position.GetPositionAddress(), position.AmmPoolId, lpAmount, sdk.Coins{}, position.Collateral.Denom) + _, exitCoinsAfterExitFee, err := k.amm.ExitPool(ctx, position.GetPositionAddress(), position.AmmPoolId, lpAmount, sdk.Coins{}, position.Collateral.Denom) if err != nil { return sdk.ZeroInt(), err } @@ -47,7 +47,7 @@ func (k Keeper) ForceCloseLong(ctx sdk.Context, position types.Position, pool ty return sdk.ZeroInt(), err } - userAmount := exitCoins[0].Amount.Sub(repayAmount) + userAmount := exitCoinsAfterExitFee[0].Amount.Sub(repayAmount) if userAmount.IsNegative() { return sdk.ZeroInt(), types.ErrNegUserAmountAfterRepay } diff --git a/x/leveragelp/keeper/position_close_test.go b/x/leveragelp/keeper/position_close_test.go index 717f61eab..705bfe782 100644 --- a/x/leveragelp/keeper/position_close_test.go +++ b/x/leveragelp/keeper/position_close_test.go @@ -15,9 +15,10 @@ import ( func (suite KeeperTestSuite) OpenPosition(addr sdk.AccAddress) (*types.Position, types.Pool) { k := suite.app.LeveragelpKeeper - SetupStableCoinPrices(suite.ctx, suite.app.OracleKeeper) + SetupCoinPrices(suite.ctx, suite.app.OracleKeeper) poolAddr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) treasuryAddr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + amount := int64(10_000_000) pool := types.Pool{ AmmPoolId: 1, Enabled: true, @@ -26,7 +27,7 @@ func (suite KeeperTestSuite) OpenPosition(addr sdk.AccAddress) (*types.Position, LeveragedLpAmount: sdk.ZeroInt(), LeverageMax: sdk.ZeroDec(), } - poolInit := sdk.Coins{sdk.NewInt64Coin("uusdc", 100000), sdk.NewInt64Coin("uusdt", 100000)} + poolInit := sdk.Coins{sdk.NewInt64Coin("uusdc", amount), sdk.NewInt64Coin("uusdt", amount)} err := suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, poolInit) suite.Require().NoError(err) @@ -64,14 +65,14 @@ func (suite KeeperTestSuite) OpenPosition(addr sdk.AccAddress) (*types.Position, k.SetPool(suite.ctx, pool) suite.app.AmmKeeper.SetDenomLiquidity(suite.ctx, ammtypes.DenomLiquidity{ Denom: "uusdc", - Liquidity: sdk.NewInt(100000), + Liquidity: sdk.NewInt(amount), }) suite.app.AmmKeeper.SetDenomLiquidity(suite.ctx, ammtypes.DenomLiquidity{ Denom: "uusdt", - Liquidity: sdk.NewInt(100000), + Liquidity: sdk.NewInt(amount), }) - usdcToken := sdk.NewInt64Coin("uusdc", 100000) + usdcToken := sdk.NewInt64Coin("uusdc", amount*20) err = suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, sdk.Coins{usdcToken}) suite.Require().NoError(err) err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr, sdk.Coins{usdcToken}) @@ -80,7 +81,7 @@ func (suite KeeperTestSuite) OpenPosition(addr sdk.AccAddress) (*types.Position, stableMsgServer := stablestakekeeper.NewMsgServerImpl(suite.app.StablestakeKeeper) _, err = stableMsgServer.Bond(sdk.WrapSDKContext(suite.ctx), &stablestaketypes.MsgBond{ Creator: addr.String(), - Amount: sdk.NewInt(10000), + Amount: sdk.NewInt(amount * 10), }) suite.Require().NoError(err) @@ -88,7 +89,7 @@ func (suite KeeperTestSuite) OpenPosition(addr sdk.AccAddress) (*types.Position, position, err := k.OpenLong(suite.ctx, &types.MsgOpen{ Creator: addr.String(), CollateralAsset: "uusdc", - CollateralAmount: sdk.NewInt(1000), + CollateralAmount: sdk.NewInt(amount).QuoRaw(1000), AmmPoolId: 1, Leverage: sdk.NewDec(5), }) @@ -118,7 +119,13 @@ func (suite KeeperTestSuite) TestForceCloseLong() { k := suite.app.LeveragelpKeeper addr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) position, pool := suite.OpenPosition(addr) - repayAmount := math.NewInt(4000) + timeDifference := suite.ctx.BlockTime().Add(time.Hour).Unix() - suite.ctx.BlockTime().Unix() + interestRate := suite.app.StablestakeKeeper.GetParams(suite.ctx).InterestRate + borrowed := position.Leverage.Sub(sdk.OneDec()).MulInt(position.Collateral.Amount) + repayAmount := borrowed.Add(borrowed. + Mul(interestRate). + Mul(sdk.NewDec(timeDifference)). + Quo(sdk.NewDec(86400 * 365))).RoundInt() suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour)) repayAmountOut, err := k.ForceCloseLong(suite.ctx, *position, pool, position.LeveragedLpAmount) @@ -130,8 +137,13 @@ func (suite KeeperTestSuite) TestForceCloseLongPartial() { k := suite.app.LeveragelpKeeper addr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) position, pool := suite.OpenPosition(addr) - repayAmount := math.NewInt(4000) - + timeDifference := suite.ctx.BlockTime().Add(time.Hour).Unix() - suite.ctx.BlockTime().Unix() + interestRate := suite.app.StablestakeKeeper.GetParams(suite.ctx).InterestRate + borrowed := position.Leverage.Sub(sdk.OneDec()).MulInt(position.Collateral.Amount) + repayAmount := borrowed.Add(borrowed. + Mul(interestRate). + Mul(sdk.NewDec(timeDifference)). + Quo(sdk.NewDec(86400 * 365))).RoundInt() suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour)) // close 50% repayAmountOut, err := k.ForceCloseLong(suite.ctx, *position, pool, position.LeveragedLpAmount.Quo(sdk.NewInt(2))) @@ -148,13 +160,13 @@ func (suite KeeperTestSuite) TestHealthDecreaseForInterest() { health, err := k.GetPositionHealth(suite.ctx, *position) suite.Require().NoError(err) // suite.Require().Equal(health.String(), "1.221000000000000000") // slippage enabled on amm - suite.Require().Equal(health.String(), "1.250000000000000000") // slippage disabled on amm + suite.Require().Equal("1.250000000000000000", health.String()) // slippage disabled on amm suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 24 * 365)) suite.app.StablestakeKeeper.BeginBlocker(suite.ctx) - suite.app.StablestakeKeeper.UpdateInterestStackedByAddress(suite.ctx, sdk.AccAddress(position.GetPositionAddress())) + suite.app.StablestakeKeeper.UpdateInterestStackedByAddress(suite.ctx, position.GetPositionAddress()) health, err = k.GetPositionHealth(suite.ctx, *position) suite.Require().NoError(err) // suite.Require().Equal(health.String(), "0.610500000000000000") // slippage enabled on amm - suite.Require().Equal(health.String(), "1.077586206896551724") // slippage disabled on amm + suite.Require().Equal("1.096491228070175439", health.String()) // slippage disabled on amm } diff --git a/x/leveragelp/keeper/position_open_test.go b/x/leveragelp/keeper/position_open_test.go index de2150bd6..d50ce17e3 100644 --- a/x/leveragelp/keeper/position_open_test.go +++ b/x/leveragelp/keeper/position_open_test.go @@ -12,7 +12,7 @@ import ( func (suite KeeperTestSuite) TestOpenLong() { k := suite.app.LeveragelpKeeper - SetupStableCoinPrices(suite.ctx, suite.app.OracleKeeper) + SetupCoinPrices(suite.ctx, suite.app.OracleKeeper) addr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) poolAddr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) treasuryAddr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) diff --git a/x/leveragelp/keeper/utils_test.go b/x/leveragelp/keeper/utils_test.go index 1740f2a92..953080205 100644 --- a/x/leveragelp/keeper/utils_test.go +++ b/x/leveragelp/keeper/utils_test.go @@ -35,7 +35,7 @@ func (suite KeeperTestSuite) TestCheckSameAssets() { app := suite.app k := app.LeveragelpKeeper addr := simapp.AddTestAddrs(app, suite.ctx, 1, sdk.NewInt(1000000)) - SetupStableCoinPrices(suite.ctx, suite.app.OracleKeeper) + SetupCoinPrices(suite.ctx, suite.app.OracleKeeper) position := types.NewPosition(addr[0].String(), sdk.NewInt64Coin("USDC", 0), sdk.NewDec(5), 1) k.SetPosition(suite.ctx, position, sdk.NewInt(0)) diff --git a/x/leveragelp/types/expected_keepers.go b/x/leveragelp/types/expected_keepers.go index 939126043..bf4cd6d00 100644 --- a/x/leveragelp/types/expected_keepers.go +++ b/x/leveragelp/types/expected_keepers.go @@ -31,7 +31,7 @@ type AmmKeeper interface { CalcOutAmtGivenIn(ctx sdk.Context, poolId uint64, oracle ammtypes.OracleKeeper, snapshot *ammtypes.Pool, tokensIn sdk.Coins, tokenOutDenom string, swapFee sdk.Dec) (sdk.Coin, sdk.Dec, error) CalcInAmtGivenOut(ctx sdk.Context, poolId uint64, oracle ammtypes.OracleKeeper, snapshot *ammtypes.Pool, tokensOut sdk.Coins, tokenInDenom string, swapFee sdk.Dec) (tokenIn sdk.Coin, slippage sdk.Dec, err error) JoinPoolNoSwap(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, shareOutAmount math.Int, tokenInMaxs sdk.Coins) (tokenIn sdk.Coins, sharesOut math.Int, err error) - ExitPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, shareInAmount math.Int, tokenOutMins sdk.Coins, tokenOutDenom string) (exitCoins sdk.Coins, err error) + ExitPool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, shareInAmount math.Int, tokenOutMins sdk.Coins, tokenOutDenom string) (exitCoins, exitCoinsAfterExitFee sdk.Coins, err error) } // BankKeeper defines the expected interface needed to retrieve account balances. diff --git a/x/masterchef/keeper/hooks_masterchef_test.go b/x/masterchef/keeper/hooks_masterchef_test.go index 618e2987d..29098965d 100644 --- a/x/masterchef/keeper/hooks_masterchef_test.go +++ b/x/masterchef/keeper/hooks_masterchef_test.go @@ -169,7 +169,7 @@ func TestHookMasterchef(t *testing.T) { // check length of pools require.Equal(t, len(pools), 1) - _, err = amm.ExitPool(ctx, addr[0], pools[0].PoolId, math.NewIntWithDecimal(1, 21), sdk.NewCoins(), "") + _, _, err = amm.ExitPool(ctx, addr[0], pools[0].PoolId, math.NewIntWithDecimal(1, 21), sdk.NewCoins(), "") require.NoError(t, err) // new user join pool with same shares @@ -255,7 +255,7 @@ func TestHookMasterchef(t *testing.T) { require.Len(t, res.TotalRewards, 0) // first user exit pool - _, err = amm.ExitPool(ctx, addr[1], pools[0].PoolId, share.Quo(math.NewInt(2)), sdk.NewCoins(), "") + _, _, err = amm.ExitPool(ctx, addr[1], pools[0].PoolId, share.Quo(math.NewInt(2)), sdk.NewCoins(), "") require.NoError(t, err) // check rewards after 100 block