From 0dfa83f70945a55856d7f63bb7c304acf23f95e7 Mon Sep 17 00:00:00 2001 From: Adarsh Kumar Date: Mon, 9 Dec 2024 22:36:50 +0530 Subject: [PATCH] Increase coverage for Amm keeper and types package (#1052) * increasse coverage for amm keepers * swap by denom test for out route * add more amm tests * increase coverage for amm types package --- x/amm/keeper/calc_in_route_spot_price_test.go | 3 +- x/amm/keeper/estimate_price_test.go | 50 ++++ x/amm/keeper/fee_test.go | 21 +- x/amm/keeper/keeper_create_pool_test.go | 57 +++++ x/amm/keeper/keeper_exit_pool_test.go | 57 +++++ x/amm/keeper/keeper_join_pool_no_swap_test.go | 91 ++++++++ .../keeper_swap_exact_amount_in_test.go | 74 +++++- .../keeper_swap_exact_amount_out_test.go | 59 +++++ x/amm/keeper/msg_server_swap_by_denom_test.go | 165 +++++++++++++ x/amm/types/amm_price_test.go | 144 ++++++++++++ x/amm/types/calc_exit_pool_test.go | 218 ++++++++++++++++++ x/amm/types/message_create_pool_test.go | 100 +++++++- x/amm/types/message_exit_pool_test.go | 34 ++- ...e_feed_multiple_external_liquidity_test.go | 132 +++++++++++ x/amm/types/message_join_pool_test.go | 31 ++- x/amm/types/message_swap_by_denom_test.go | 40 +++- .../message_swap_exact_amount_in_test.go | 53 ++++- .../message_swap_exact_amount_out_test.go | 55 ++++- ...pool_calc_join_pool_no_swap_shares_test.go | 87 +++++++ 19 files changed, 1452 insertions(+), 19 deletions(-) create mode 100644 x/amm/keeper/keeper_create_pool_test.go create mode 100644 x/amm/keeper/keeper_exit_pool_test.go create mode 100644 x/amm/keeper/keeper_join_pool_no_swap_test.go create mode 100644 x/amm/types/amm_price_test.go create mode 100644 x/amm/types/calc_exit_pool_test.go create mode 100644 x/amm/types/message_feed_multiple_external_liquidity_test.go create mode 100644 x/amm/types/pool_calc_join_pool_no_swap_shares_test.go diff --git a/x/amm/keeper/calc_in_route_spot_price_test.go b/x/amm/keeper/calc_in_route_spot_price_test.go index 7a255d49f..0966ac816 100644 --- a/x/amm/keeper/calc_in_route_spot_price_test.go +++ b/x/amm/keeper/calc_in_route_spot_price_test.go @@ -88,9 +88,10 @@ func (suite *AmmKeeperTestSuite) TestCalcInRouteSpotPrice() { tokenIn := sdk.NewCoin(ptypes.Elys, sdkmath.NewInt(100)) routes := []*types.SwapAmountInRoute{{PoolId: 1, TokenOutDenom: ptypes.BaseCurrency}} - spotPrice, _, _, _, _, _, _, _, err := suite.app.AmmKeeper.CalcInRouteSpotPrice(suite.ctx, tokenIn, routes, sdkmath.LegacyZeroDec(), sdkmath.LegacyZeroDec()) + spotPrice, _, _, totalDiscountedSwapFee, _, _, _, _, err := suite.app.AmmKeeper.CalcInRouteSpotPrice(suite.ctx, tokenIn, routes, sdkmath.LegacyZeroDec(), sdkmath.LegacyMustNewDecFromStr("0.1")) suite.Require().NoError(err) suite.Require().Equal(spotPrice.String(), sdkmath.LegacyOneDec().String()) + suite.Require().Equal(sdkmath.LegacyMustNewDecFromStr("0.1"), totalDiscountedSwapFee) routes = []*types.SwapAmountInRoute{ {PoolId: 1, TokenOutDenom: ptypes.BaseCurrency}, diff --git a/x/amm/keeper/estimate_price_test.go b/x/amm/keeper/estimate_price_test.go index 5bc1d60d2..93df0ed3f 100644 --- a/x/amm/keeper/estimate_price_test.go +++ b/x/amm/keeper/estimate_price_test.go @@ -32,6 +32,56 @@ func (suite *AmmKeeperTestSuite) TestEstimatePrice() { suite.Require().Equal(math.LegacyMustNewDecFromStr("0.000001000000000000"), price) }, }, + { + "Asset Info Not found for tokenInDenom", + func() { + suite.ResetSuite() + suite.SetupCoinPrices() + }, + func() { + suite.app.OracleKeeper.RemoveAssetInfo(suite.ctx, ptypes.BaseCurrency) + price := suite.app.AmmKeeper.GetTokenPrice(suite.ctx, ptypes.BaseCurrency, ptypes.BaseCurrency) + suite.Require().Equal(math.LegacyMustNewDecFromStr("0.000000000000000000"), price) + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + tc.prerequisiteFunction() + tc.postValidateFunction() + }) + } +} + +func (suite *AmmKeeperTestSuite) TestCalculateUSDValue() { + testCases := []struct { + name string + prerequisiteFunction func() + postValidateFunction func() + }{ + { + "Success: get token value at oracle price", + func() { + suite.ResetSuite() + suite.SetupCoinPrices() + }, + func() { + value := suite.app.AmmKeeper.CalculateUSDValue(suite.ctx, ptypes.ATOM, math.NewInt(1000)) + suite.Require().Equal(value, math.LegacyMustNewDecFromStr("0.001")) + }, + }, + { + "Calculate Usd value for asset not found in AssetProfile", + func() { + suite.ResetSuite() + suite.SetupCoinPrices() + }, + func() { + value := suite.app.AmmKeeper.CalculateUSDValue(suite.ctx, "dummy", math.NewInt(1000)) + suite.Require().Equal(value.String(), math.LegacyZeroDec().String()) + }, + }, } for _, tc := range testCases { diff --git a/x/amm/keeper/fee_test.go b/x/amm/keeper/fee_test.go index 7db8db0e9..b99ba9cdd 100644 --- a/x/amm/keeper/fee_test.go +++ b/x/amm/keeper/fee_test.go @@ -33,6 +33,7 @@ func (suite *AmmKeeperTestSuite) TestOnCollectFee() { poolInitBalance sdk.Coins expRevenueBalance sdk.Coins expPass bool + useOracle bool }{ { desc: "multiple fees collected", @@ -40,6 +41,7 @@ func (suite *AmmKeeperTestSuite) TestOnCollectFee() { poolInitBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.Elys, 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, expRevenueBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.BaseCurrency, 1999)}, expPass: true, + useOracle: false, }, { desc: "zero fees collected", @@ -47,6 +49,7 @@ func (suite *AmmKeeperTestSuite) TestOnCollectFee() { poolInitBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.Elys, 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, expRevenueBalance: sdk.Coins{}, expPass: true, + useOracle: false, }, { desc: "base currency fee collected", @@ -54,6 +57,15 @@ func (suite *AmmKeeperTestSuite) TestOnCollectFee() { poolInitBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.Elys, 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, expRevenueBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.BaseCurrency, 1000)}, expPass: true, + useOracle: false, + }, + { + desc: "fee collected after weight recovery fee deduction", + fee: sdk.Coins{sdk.NewInt64Coin(ptypes.BaseCurrency, 1000)}, + poolInitBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.Elys, 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + expRevenueBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.BaseCurrency, 900)}, + expPass: true, + useOracle: true, }, } { suite.Run(tc.desc, func() { @@ -87,7 +99,7 @@ func (suite *AmmKeeperTestSuite) TestOnCollectFee() { RebalanceTreasury: treasuryAddr.String(), PoolParams: types.PoolParams{ SwapFee: sdkmath.LegacyZeroDec(), - UseOracle: false, + UseOracle: tc.useOracle, FeeDenom: ptypes.BaseCurrency, }, TotalShares: sdk.NewCoin(types.GetPoolShareDenom(1), sdkmath.ZeroInt()), @@ -149,6 +161,13 @@ func (suite *AmmKeeperTestSuite) TestSwapFeesToRevenueToken() { expRevenueBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.BaseCurrency, 1000)}, expPass: true, }, + { + desc: "token not available in pools for swap", + fee: sdk.Coins{sdk.NewInt64Coin("dummy", 1000)}, + poolInitBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.Elys, 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + expRevenueBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.BaseCurrency, 1000)}, + expPass: false, + }, } { suite.Run(tc.desc, func() { suite.SetupTest() diff --git a/x/amm/keeper/keeper_create_pool_test.go b/x/amm/keeper/keeper_create_pool_test.go new file mode 100644 index 000000000..4921a7702 --- /dev/null +++ b/x/amm/keeper/keeper_create_pool_test.go @@ -0,0 +1,57 @@ +package keeper_test + +import ( + "github.com/elys-network/elys/x/amm/types" + ptypes "github.com/elys-network/elys/x/parameter/types" + "github.com/stretchr/testify/require" +) + +func (suite *AmmKeeperTestSuite) TestCreatePool() { + // Define test cases + testCases := []struct { + name string + setup func() *types.MsgCreatePool + expectedErrMsg string + }{ + { + "asset profile not found", + func() *types.MsgCreatePool { + addr := suite.AddAccounts(1, nil) + suite.app.AssetprofileKeeper.RemoveEntry(suite.ctx, ptypes.BaseCurrency) + return &types.MsgCreatePool{ + Sender: addr[0].String(), + PoolParams: types.PoolParams{}, + PoolAssets: []types.PoolAsset{}, + } + }, + "asset profile not found for denom", + }, + { + "Balance pool Create Error", + func() *types.MsgCreatePool { + suite.ResetSuite() + addr := suite.AddAccounts(1, nil) + return &types.MsgCreatePool{ + Sender: addr[0].String(), + PoolParams: types.PoolParams{}, + PoolAssets: []types.PoolAsset{}, + } + }, + "swap_fee is nil", + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + msg := tc.setup() + poolId, err := suite.app.AmmKeeper.CreatePool(suite.ctx, msg) + if tc.expectedErrMsg != "" { + require.Error(suite.T(), err) + require.Contains(suite.T(), err.Error(), tc.expectedErrMsg) + } else { + require.NoError(suite.T(), err) + require.NotZero(suite.T(), poolId) + } + }) + } +} diff --git a/x/amm/keeper/keeper_exit_pool_test.go b/x/amm/keeper/keeper_exit_pool_test.go new file mode 100644 index 000000000..71c1957a4 --- /dev/null +++ b/x/amm/keeper/keeper_exit_pool_test.go @@ -0,0 +1,57 @@ +package keeper_test + +import ( + "cosmossdk.io/math" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + ptypes "github.com/elys-network/elys/x/parameter/types" +) + +func (suite *AmmKeeperTestSuite) TestExitPool() { + testCases := []struct { + name string + setup func() (sdk.AccAddress, uint64, math.Int, sdk.Coins, string, bool) + expectedErrMsg string + }{ + { + "pool does not exist", + func() (sdk.AccAddress, uint64, math.Int, sdk.Coins, string, bool) { + addr := suite.AddAccounts(1, nil) + return addr[0], 1, math.NewInt(100), sdk.NewCoins(sdk.NewCoin("uatom", math.NewInt(100))), "uatom", false + }, + "invalid pool id", + }, + { + "exiting more shares than available", + func() (sdk.AccAddress, uint64, math.Int, sdk.Coins, string, bool) { + suite.SetupCoinPrices() + addr := suite.AddAccounts(1, nil) + amount := sdkmath.NewInt(100000000000) + pool := suite.CreateNewAmmPool(addr[0], true, sdkmath.LegacyZeroDec(), sdkmath.LegacyZeroDec(), ptypes.ATOM, amount.MulRaw(10), amount.MulRaw(10)) + return addr[0], 1, pool.TotalShares.Amount.Add(sdkmath.NewInt(10)), sdk.NewCoins(sdk.NewCoin("uatom", math.NewInt(100))), "uatom", false + }, + "Trying to exit >= the number of shares contained in the pool", + }, + { + "exiting negative shares", + func() (sdk.AccAddress, uint64, math.Int, sdk.Coins, string, bool) { + addr := suite.AddAccounts(1, nil) + return addr[0], 1, sdkmath.NewInt(1).Neg(), sdk.NewCoins(sdk.NewCoin("uatom", math.NewInt(100))), "uatom", false + }, + "Trying to exit a negative amount of shares", + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + exiter, poolId, inShares, minTokensOut, tokenOutDenom, isLiq := tc.setup() + _, err := suite.app.AmmKeeper.ExitPool(suite.ctx, exiter, poolId, inShares, minTokensOut, tokenOutDenom, isLiq) + if tc.expectedErrMsg != "" { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expectedErrMsg) + } else { + suite.Require().NoError(err) + } + }) + } +} diff --git a/x/amm/keeper/keeper_join_pool_no_swap_test.go b/x/amm/keeper/keeper_join_pool_no_swap_test.go new file mode 100644 index 000000000..b597a0da2 --- /dev/null +++ b/x/amm/keeper/keeper_join_pool_no_swap_test.go @@ -0,0 +1,91 @@ +package keeper_test + +import ( + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/elys-network/elys/x/parameter/types" +) + +func (suite *AmmKeeperTestSuite) TestJoinPoolNoSwap() { + // Define test cases + testCases := []struct { + name string + setup func() (sdk.AccAddress, uint64, sdkmath.Int, sdk.Coins) + expectedErrMsg string + }{ + { + "pool does not exist", + func() (sdk.AccAddress, uint64, sdkmath.Int, sdk.Coins) { + return sdk.AccAddress([]byte("sender")), 1, sdkmath.NewInt(100), sdk.Coins{} + }, + "invalid pool id", + }, + { + "successful join pool No oracle", + func() (sdk.AccAddress, uint64, sdkmath.Int, sdk.Coins) { + suite.SetupCoinPrices() + addr := suite.AddAccounts(1, nil) + amount := sdkmath.NewInt(100000000000) + pool := suite.CreateNewAmmPool(addr[0], false, sdkmath.LegacyZeroDec(), sdkmath.LegacyZeroDec(), types.ATOM, amount.MulRaw(10), amount.MulRaw(10)) + return addr[0], 1, pool.TotalShares.Amount, sdk.Coins{sdk.NewCoin(types.ATOM, amount.MulRaw(10)), sdk.NewCoin(types.BaseCurrency, amount.MulRaw(10))} + }, + "", + }, + { + "successful join pool with oracle", + func() (sdk.AccAddress, uint64, sdkmath.Int, sdk.Coins) { + suite.SetupCoinPrices() + addr := suite.AddAccounts(1, nil) + amount := sdkmath.NewInt(100000000000) + pool := suite.CreateNewAmmPool(addr[0], true, sdkmath.LegacyZeroDec(), sdkmath.LegacyZeroDec(), types.ATOM, amount.MulRaw(10), amount.MulRaw(10)) + return addr[0], 2, pool.TotalShares.Amount, sdk.Coins{sdk.NewCoin(types.ATOM, amount.MulRaw(10))} + }, + "", + }, + { + "Needed LpLiquidity is more than tokenInMaxs", + func() (sdk.AccAddress, uint64, sdkmath.Int, sdk.Coins) { + addr := suite.AddAccounts(1, nil) + amount := sdkmath.NewInt(100) + share, _ := sdkmath.NewIntFromString("20000000000000000000000000000000") + return addr[0], 1, share, sdk.Coins{sdk.NewCoin(types.ATOM, amount), sdk.NewCoin(types.BaseCurrency, amount)} + }, + "TokenInMaxs is less than the needed LP liquidity to this JoinPoolNoSwap", + }, + { + "tokenInMaxs does not contain Needed LpLiquidity coins", + func() (sdk.AccAddress, uint64, sdkmath.Int, sdk.Coins) { + addr := suite.AddAccounts(1, nil) + amount := sdkmath.NewInt(100) + share, _ := sdkmath.NewIntFromString("20000000000000000000000000000000") + return addr[0], 1, share, sdk.Coins{sdk.NewCoin("nocoin", amount), sdk.NewCoin(types.BaseCurrency, amount)} + }, + "TokenInMaxs does not include all the tokens that are part of the target pool", + }, + { + "tokenInMaxs does not contain Needed LpLiquidity coins", + func() (sdk.AccAddress, uint64, sdkmath.Int, sdk.Coins) { + addr := suite.AddAccounts(1, nil) + amount := sdkmath.NewInt(100) + share, _ := sdkmath.NewIntFromString("20000000000000000000000000000000") + return addr[0], 1, share, sdk.Coins{sdk.NewCoin("nocoin", amount), sdk.NewCoin(types.ATOM, amount), sdk.NewCoin(types.BaseCurrency, amount)} + }, + "TokenInMaxs includes tokens that are not part of the target pool", + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + sender, poolId, shareOutAmount, tokenInMaxs := tc.setup() + tokenIn, sharesOut, err := suite.app.AmmKeeper.JoinPoolNoSwap(suite.ctx, sender, poolId, shareOutAmount, tokenInMaxs) + if tc.expectedErrMsg != "" { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expectedErrMsg) + } else { + suite.Require().NoError(err) + suite.Require().True(tokenIn.IsAllLTE(tokenInMaxs)) + suite.Require().True(sharesOut.LTE(shareOutAmount)) + } + }) + } +} diff --git a/x/amm/keeper/keeper_swap_exact_amount_in_test.go b/x/amm/keeper/keeper_swap_exact_amount_in_test.go index 52cbf25a0..cadfad8cd 100644 --- a/x/amm/keeper/keeper_swap_exact_amount_in_test.go +++ b/x/amm/keeper/keeper_swap_exact_amount_in_test.go @@ -28,15 +28,16 @@ func (suite *AmmKeeperTestSuite) TestSwapExactAmountIn() { expPoolBalance sdk.Coins expTreasuryBalance sdk.Coins expPass bool + errMsg string }{ { - desc: "pool does not enough balance for out", + desc: "tokenIn is same as tokenOut", senderInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, poolInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 100)}, treasuryInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, swapFeeIn: sdkmath.LegacyZeroDec(), swapFeeOut: sdkmath.LegacyZeroDec(), - tokenIn: sdk.NewInt64Coin("uusda", 10000), + tokenIn: sdk.NewInt64Coin("uusdc", 10000), tokenOutMin: sdkmath.ZeroInt(), tokenOut: sdk.NewInt64Coin(ptypes.BaseCurrency, 10000), weightBalanceBonus: sdkmath.LegacyZeroDec(), @@ -47,6 +48,67 @@ func (suite *AmmKeeperTestSuite) TestSwapExactAmountIn() { expPoolBalance: sdk.Coins{}, expTreasuryBalance: sdk.Coins{}, expPass: false, + errMsg: "cannot trade the same denomination in and out", + }, + { + desc: "tokenOut is 0", + senderInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + poolInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 100)}, + treasuryInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + swapFeeIn: sdkmath.LegacyZeroDec(), + swapFeeOut: sdkmath.LegacyZeroDec(), + tokenIn: sdk.NewInt64Coin("uusda", 0), + tokenOutMin: sdkmath.ZeroInt(), + tokenOut: sdk.NewInt64Coin(ptypes.BaseCurrency, 10000), + weightBalanceBonus: sdkmath.LegacyZeroDec(), + isOraclePool: false, + useNewRecipient: false, + expSenderBalance: sdk.Coins{}, + expRecipientBalance: sdk.Coins{}, + expPoolBalance: sdk.Coins{}, + expTreasuryBalance: sdk.Coins{}, + expPass: false, + errMsg: "token out amount is zero", + }, + { + desc: "tokenOut is less than tokenOut minimum amount", + senderInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + poolInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + treasuryInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + swapFeeIn: sdkmath.LegacyNewDecWithPrec(1, 2), // 1% + swapFeeOut: sdkmath.LegacyZeroDec(), + tokenIn: sdk.NewInt64Coin("uusda", 10000), + tokenOutMin: sdkmath.NewInt(10000000), + tokenOut: sdk.NewInt64Coin(ptypes.BaseCurrency, 9802), + weightBalanceBonus: sdkmath.LegacyZeroDec(), + isOraclePool: false, + useNewRecipient: false, + expSenderBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 990000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1009802)}, + expRecipientBalance: sdk.Coins{}, + expPoolBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1010000), sdk.NewInt64Coin(ptypes.BaseCurrency, 990100)}, + expTreasuryBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + expPass: false, + errMsg: "token is less than the minimum amount", + }, + { + desc: "pool does not enough balance for out", + senderInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + poolInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 100)}, + treasuryInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + swapFeeIn: sdkmath.LegacyZeroDec(), + swapFeeOut: sdkmath.LegacyZeroDec(), + tokenIn: sdk.NewInt64Coin("uusda", 1000), + tokenOutMin: sdkmath.ZeroInt(), + tokenOut: sdk.NewInt64Coin(ptypes.BaseCurrency, 1000), + weightBalanceBonus: sdkmath.LegacyZeroDec(), + isOraclePool: false, + useNewRecipient: false, + expSenderBalance: sdk.Coins{}, + expRecipientBalance: sdk.Coins{}, + expPoolBalance: sdk.Coins{}, + expTreasuryBalance: sdk.Coins{}, + expPass: false, + errMsg: "token out amount is zero", }, { desc: "sender does not have enough balance for in", @@ -66,6 +128,7 @@ func (suite *AmmKeeperTestSuite) TestSwapExactAmountIn() { expPoolBalance: sdk.Coins{}, expTreasuryBalance: sdk.Coins{}, expPass: false, + errMsg: "insufficient funds", }, { desc: "successful execution with positive swap fee", @@ -85,6 +148,7 @@ func (suite *AmmKeeperTestSuite) TestSwapExactAmountIn() { expPoolBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1010000), sdk.NewInt64Coin(ptypes.BaseCurrency, 990100)}, expTreasuryBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, expPass: true, + errMsg: "", }, { desc: "successful execution with zero swap fee", @@ -104,6 +168,7 @@ func (suite *AmmKeeperTestSuite) TestSwapExactAmountIn() { expPoolBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1010000), sdk.NewInt64Coin(ptypes.BaseCurrency, 990100)}, expTreasuryBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, expPass: true, + errMsg: "", }, { desc: "successful execution with positive slippage on oracle pool", @@ -123,6 +188,7 @@ func (suite *AmmKeeperTestSuite) TestSwapExactAmountIn() { expPoolBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1009997), sdk.NewInt64Coin(ptypes.BaseCurrency, 990056)}, expTreasuryBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000003), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, expPass: true, + errMsg: "", }, { desc: "successful weight bonus & huge amount rebalance treasury", @@ -142,6 +208,7 @@ func (suite *AmmKeeperTestSuite) TestSwapExactAmountIn() { expPoolBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1010000), sdk.NewInt64Coin(ptypes.BaseCurrency, 990100)}, expTreasuryBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, expPass: true, + errMsg: "", }, { desc: "successful weight bonus & lack of rebalance treasury", @@ -161,6 +228,7 @@ func (suite *AmmKeeperTestSuite) TestSwapExactAmountIn() { expPoolBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1010000), sdk.NewInt64Coin(ptypes.BaseCurrency, 990100)}, expTreasuryBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 100)}, expPass: true, + errMsg: "", }, { desc: "new recipient address", @@ -180,6 +248,7 @@ func (suite *AmmKeeperTestSuite) TestSwapExactAmountIn() { expPoolBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1010000), sdk.NewInt64Coin(ptypes.BaseCurrency, 990100)}, expTreasuryBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 100)}, expPass: true, + errMsg: "", }, } { suite.Run(tc.desc, func() { @@ -246,6 +315,7 @@ func (suite *AmmKeeperTestSuite) TestSwapExactAmountIn() { tokenOut, err := suite.app.AmmKeeper.InternalSwapExactAmountIn(suite.ctx, sender, recipient, pool, tc.tokenIn, tc.tokenOut.Denom, tc.tokenOutMin, tc.swapFeeIn) if !tc.expPass { suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.errMsg) } else { suite.Require().NoError(err) suite.Require().Equal(tokenOut.String(), tc.tokenOut.Amount.String()) diff --git a/x/amm/keeper/keeper_swap_exact_amount_out_test.go b/x/amm/keeper/keeper_swap_exact_amount_out_test.go index a3fb49378..e71fcf186 100644 --- a/x/amm/keeper/keeper_swap_exact_amount_out_test.go +++ b/x/amm/keeper/keeper_swap_exact_amount_out_test.go @@ -27,7 +27,65 @@ func (suite *AmmKeeperTestSuite) TestSwapExactAmountOut() { expPoolBalance sdk.Coins expTreasuryBalance sdk.Coins expPass bool + errMsg string }{ + { + desc: "tokenIn is same as tokenOut", + senderInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + poolInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 100)}, + treasuryInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + swapFeeOut: sdkmath.LegacyZeroDec(), + tokenIn: sdk.NewInt64Coin("uusdc", 10000), + tokenInMax: sdkmath.NewInt(10000000), + tokenOut: sdk.NewInt64Coin(ptypes.BaseCurrency, 10000), + weightBalanceBonus: sdkmath.LegacyZeroDec(), + isOraclePool: false, + useNewRecipient: false, + expSenderBalance: sdk.Coins{}, + expRecipientBalance: sdk.Coins{}, + expPoolBalance: sdk.Coins{}, + expTreasuryBalance: sdk.Coins{}, + expPass: false, + errMsg: "cannot trade the same denomination in and out", + }, + { + desc: "tokenIn is 0 corrosponsing to tokenOut", + senderInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + poolInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + treasuryInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + swapFeeOut: sdkmath.LegacyZeroDec(), + tokenIn: sdk.NewInt64Coin("uusda", 1000), + tokenInMax: sdkmath.NewInt(10000000), + tokenOut: sdk.NewInt64Coin(ptypes.BaseCurrency, 0), + weightBalanceBonus: sdkmath.LegacyZeroDec(), + isOraclePool: true, + useNewRecipient: false, + expSenderBalance: sdk.Coins{}, + expRecipientBalance: sdk.Coins{}, + expPoolBalance: sdk.Coins{}, + expTreasuryBalance: sdk.Coins{}, + expPass: false, + errMsg: "amount too low", + }, + { + desc: "MaxTokenIn is less than required tokenIn amount", + senderInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + poolInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + treasuryInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + swapFeeOut: sdkmath.LegacyZeroDec(), + tokenIn: sdk.NewInt64Coin("uusda", 10000), + tokenInMax: sdkmath.NewInt(10), + tokenOut: sdk.NewInt64Coin(ptypes.BaseCurrency, 9802), + weightBalanceBonus: sdkmath.LegacyZeroDec(), + isOraclePool: false, + useNewRecipient: false, + expSenderBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 990000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1009802)}, + expRecipientBalance: sdk.Coins{}, + expPoolBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1010000), sdk.NewInt64Coin(ptypes.BaseCurrency, 990100)}, + expTreasuryBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + expPass: false, + errMsg: "calculated amount is larger than max amount", + }, { desc: "pool does not enough balance for out", senderInitBalance: sdk.Coins{sdk.NewInt64Coin("uusda", 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, @@ -237,6 +295,7 @@ func (suite *AmmKeeperTestSuite) TestSwapExactAmountOut() { tokenInAmount, err := suite.app.AmmKeeper.InternalSwapExactAmountOut(suite.ctx, sender, recipient, pool, tc.tokenIn.Denom, tc.tokenInMax, tc.tokenOut, tc.swapFeeOut) if !tc.expPass { suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.errMsg) } else { suite.Require().NoError(err) suite.Require().Equal(tokenInAmount.String(), tc.tokenIn.Amount.String()) diff --git a/x/amm/keeper/msg_server_swap_by_denom_test.go b/x/amm/keeper/msg_server_swap_by_denom_test.go index d8fe7b4cd..2c6c2812e 100644 --- a/x/amm/keeper/msg_server_swap_by_denom_test.go +++ b/x/amm/keeper/msg_server_swap_by_denom_test.go @@ -21,6 +21,7 @@ func (suite *AmmKeeperTestSuite) TestMsgServerSwapByDenom() { tokenOut sdk.Coin expSenderBalance sdk.Coins expPass bool + errMsg string }{ { desc: "successful execution with positive swap fee", @@ -180,3 +181,167 @@ func (suite *AmmKeeperTestSuite) TestMsgServerSwapByDenom() { }) } } + +func (suite *AmmKeeperTestSuite) TestMsgServerSwapByDenomWithOutRoute() { + for _, tc := range []struct { + desc string + senderInitBalance sdk.Coins + swapFee sdkmath.LegacyDec + tokenOut sdk.Coin + tokenOutMax sdkmath.Int + tokenDenomIn string + expTokenIn sdk.Coin + expSenderBalance sdk.Coins + expPass bool + errMsg string + }{ + { + desc: "successful execution with positive swap fee", + senderInitBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.Elys, 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + swapFee: sdkmath.LegacyNewDecWithPrec(1, 2), // 1% + tokenOut: sdk.NewInt64Coin(ptypes.Elys, 10000), + expTokenIn: sdk.NewInt64Coin(ptypes.BaseCurrency, 10204), + tokenOutMax: sdkmath.NewInt(1000000), + tokenDenomIn: ptypes.BaseCurrency, + expSenderBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.Elys, 1010000), sdk.NewInt64Coin(ptypes.BaseCurrency, 989796)}, + expPass: true, + }, + { + desc: "successful execution with zero swap fee", + senderInitBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.Elys, 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)}, + swapFee: sdkmath.LegacyZeroDec(), + tokenOut: sdk.NewInt64Coin(ptypes.Elys, 10000), + expTokenIn: sdk.NewInt64Coin(ptypes.BaseCurrency, 10102), + tokenOutMax: sdkmath.NewInt(1000000), + tokenDenomIn: ptypes.BaseCurrency, + expSenderBalance: sdk.Coins{sdk.NewInt64Coin(ptypes.Elys, 1010000), sdk.NewInt64Coin(ptypes.BaseCurrency, 989898)}, + expPass: true, + }, + } { + suite.Run(tc.desc, func() { + suite.SetupTest() + + // set asset profile + suite.app.AssetprofileKeeper.SetEntry(suite.ctx, assetprofiletypes.Entry{ + BaseDenom: ptypes.Elys, + Denom: ptypes.Elys, + Decimals: 6, + }) + + suite.app.AssetprofileKeeper.SetEntry(suite.ctx, assetprofiletypes.Entry{ + BaseDenom: ptypes.BaseCurrency, + Denom: ptypes.BaseCurrency, + Decimals: 6, + }) + + // bootstrap accounts + sender := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + poolAddr := types.NewPoolAddress(uint64(1)) + treasuryAddr := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + poolAddr2 := types.NewPoolAddress(uint64(2)) + treasuryAddr2 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + poolCoins := sdk.Coins{sdk.NewInt64Coin(ptypes.Elys, 1000000), sdk.NewInt64Coin(ptypes.BaseCurrency, 1000000)} + pool2Coins := sdk.Coins{sdk.NewInt64Coin(ptypes.Elys, 1000000), sdk.NewInt64Coin("uusdt", 1000000)} + + // bootstrap balances + err := suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, tc.senderInitBalance) + suite.Require().NoError(err) + err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, sender, tc.senderInitBalance) + suite.Require().NoError(err) + err = suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, poolCoins) + suite.Require().NoError(err) + err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, poolAddr, poolCoins) + suite.Require().NoError(err) + err = suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, pool2Coins) + suite.Require().NoError(err) + err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, poolAddr2, pool2Coins) + suite.Require().NoError(err) + + // execute function + suite.app.AmmKeeper.SetDenomLiquidity(suite.ctx, types.DenomLiquidity{ + Denom: ptypes.Elys, + Liquidity: sdkmath.NewInt(2000000), + }) + suite.app.AmmKeeper.SetDenomLiquidity(suite.ctx, types.DenomLiquidity{ + Denom: ptypes.BaseCurrency, + Liquidity: sdkmath.NewInt(1000000), + }) + suite.app.AmmKeeper.SetDenomLiquidity(suite.ctx, types.DenomLiquidity{ + Denom: "uusdt", + Liquidity: sdkmath.NewInt(1000000), + }) + + pool := types.Pool{ + PoolId: 1, + Address: poolAddr.String(), + RebalanceTreasury: treasuryAddr.String(), + PoolParams: types.PoolParams{ + SwapFee: tc.swapFee, + FeeDenom: ptypes.BaseCurrency, + }, + TotalShares: sdk.Coin{}, + PoolAssets: []types.PoolAsset{ + { + Token: poolCoins[0], + Weight: sdkmath.NewInt(10), + }, + { + Token: poolCoins[1], + Weight: sdkmath.NewInt(10), + }, + }, + TotalWeight: sdkmath.ZeroInt(), + } + pool2 := types.Pool{ + PoolId: 2, + Address: poolAddr2.String(), + RebalanceTreasury: treasuryAddr2.String(), + PoolParams: types.PoolParams{ + SwapFee: tc.swapFee, + FeeDenom: ptypes.BaseCurrency, + }, + TotalShares: sdk.Coin{}, + PoolAssets: []types.PoolAsset{ + { + Token: pool2Coins[0], + Weight: sdkmath.NewInt(10), + }, + { + Token: pool2Coins[1], + Weight: sdkmath.NewInt(10), + }, + }, + TotalWeight: sdkmath.ZeroInt(), + } + suite.app.AmmKeeper.SetPool(suite.ctx, pool) + suite.app.AmmKeeper.SetPool(suite.ctx, pool2) + suite.Require().True(suite.VerifyPoolAssetWithBalance(1)) + suite.Require().True(suite.VerifyPoolAssetWithBalance(2)) + + msgServer := keeper.NewMsgServerImpl(*suite.app.AmmKeeper) + resp, err := msgServer.SwapByDenom( + suite.ctx, + &types.MsgSwapByDenom{ + Sender: sender.String(), + Amount: tc.tokenOut, + MinAmount: sdk.Coin{}, + MaxAmount: sdk.NewCoin(tc.tokenOut.Denom, tc.tokenOutMax), + DenomIn: tc.tokenDenomIn, + DenomOut: tc.tokenOut.Denom, + }) + if !tc.expPass { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + suite.Require().Equal(tc.expTokenIn.String(), resp.Amount.String()) + suite.app.AmmKeeper.EndBlocker(suite.ctx) + suite.Require().True(suite.VerifyPoolAssetWithBalance(1)) + suite.Require().True(suite.VerifyPoolAssetWithBalance(2)) + + // check balance change on sender + balances := suite.app.BankKeeper.GetAllBalances(suite.ctx, sender) + suite.Require().Equal(tc.expSenderBalance.String(), balances.String()) + } + }) + } +} diff --git a/x/amm/types/amm_price_test.go b/x/amm/types/amm_price_test.go new file mode 100644 index 000000000..5dd07477c --- /dev/null +++ b/x/amm/types/amm_price_test.go @@ -0,0 +1,144 @@ +package types_test + +import ( + "testing" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/elys-network/elys/x/amm/types" + "github.com/elys-network/elys/x/amm/types/mocks" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestGetTokenARate(t *testing.T) { + ctx := sdk.Context{} + accKeeper := mocks.NewAccountedPoolKeeper(t) + + // Define test cases + testCases := []struct { + name string + setupMock func(oracleKeeper *mocks.OracleKeeper) + pool *types.Pool + tokenA string + tokenB string + expectedRate sdkmath.LegacyDec + expectedErrMsg string + }{ + { + "balancer pricing", + func(oracleKeeper *mocks.OracleKeeper) {}, + &types.Pool{ + PoolParams: types.PoolParams{UseOracle: false}, + PoolAssets: []types.PoolAsset{ + {Token: sdk.NewCoin("tokenA", sdkmath.NewInt(1500)), Weight: sdkmath.NewInt(1)}, + {Token: sdk.NewCoin("tokenB", sdkmath.NewInt(2000)), Weight: sdkmath.NewInt(1)}, + }, + }, + "tokenA", + "tokenB", + sdkmath.LegacyNewDec(4).Quo(sdkmath.LegacyNewDec(3)), + "", + }, + { + "oracle pricing", + func(oracleKeeper *mocks.OracleKeeper) { + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenA").Return(sdkmath.LegacyNewDec(10)) + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenB").Return(sdkmath.LegacyNewDec(5)) + }, + &types.Pool{ + PoolParams: types.PoolParams{UseOracle: true}, + }, + "tokenA", + "tokenB", + sdkmath.LegacyNewDec(2), + "", + }, + { + "token price not set for tokenA", + func(oracleKeeper *mocks.OracleKeeper) { + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "unknownToken").Return(sdkmath.LegacyZeroDec()) + }, + &types.Pool{ + PoolParams: types.PoolParams{UseOracle: true}, + }, + "unknownToken", + "tokenB", + sdkmath.LegacyZeroDec(), + "token price not set: unknownToken", + }, + { + "token price not set for tokenB", + func(oracleKeeper *mocks.OracleKeeper) { + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenA").Return(sdkmath.LegacyNewDec(5)) + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "unknownToken").Return(sdkmath.LegacyZeroDec()) + }, + &types.Pool{ + PoolParams: types.PoolParams{UseOracle: true}, + }, + "tokenA", + "unknownToken", + sdkmath.LegacyZeroDec(), + "token price not set: unknownToken", + }, + { + "Success with oracle pricing", + func(oracleKeeper *mocks.OracleKeeper) { + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenA").Return(sdkmath.LegacyNewDec(5)) + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenB").Return(sdkmath.LegacyNewDec(2)) + }, + &types.Pool{ + PoolParams: types.PoolParams{UseOracle: true}, + }, + "tokenA", + "tokenB", + sdkmath.LegacyNewDec(5).Quo(sdkmath.LegacyNewDec(2)), + "", + }, + { + "Success with oracle pricing", + func(oracleKeeper *mocks.OracleKeeper) { + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenA").Return(sdkmath.LegacyNewDec(5)) + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenB").Return(sdkmath.LegacyNewDec(2)) + }, + &types.Pool{ + PoolParams: types.PoolParams{UseOracle: true}, + }, + "tokenA", + "tokenB", + sdkmath.LegacyNewDec(5).Quo(sdkmath.LegacyNewDec(2)), + "", + }, + { + "Success with oracle pricing with price less than 1", + func(oracleKeeper *mocks.OracleKeeper) { + // for 6 decimal tokens + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenA").Return(sdkmath.LegacyMustNewDecFromStr("0.0000002")) + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenB").Return(sdkmath.LegacyNewDec(1)) + }, + &types.Pool{ + PoolParams: types.PoolParams{UseOracle: true}, + }, + "tokenA", + "tokenB", + sdkmath.LegacyMustNewDecFromStr("0.0000002"), + "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + oracleKeeper := mocks.NewOracleKeeper(t) + tc.setupMock(oracleKeeper) + + rate, err := tc.pool.GetTokenARate(ctx, oracleKeeper, tc.pool, tc.tokenA, tc.tokenB, accKeeper) + if tc.expectedErrMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrMsg) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedRate, rate) + } + }) + } +} diff --git a/x/amm/types/calc_exit_pool_test.go b/x/amm/types/calc_exit_pool_test.go new file mode 100644 index 000000000..67c2e67f6 --- /dev/null +++ b/x/amm/types/calc_exit_pool_test.go @@ -0,0 +1,218 @@ +package types_test + +import ( + "testing" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/elys-network/elys/x/amm/types" + "github.com/elys-network/elys/x/amm/types/mocks" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestCalcExitValueWithoutSlippage(t *testing.T) { + ctx := sdk.Context{} + + // Define test cases + testCases := []struct { + name string + setupMock func(oracleKeeper *mocks.OracleKeeper, accKeeper *mocks.AccountedPoolKeeper) + pool types.Pool + exitingShares sdkmath.Int + tokenOutDenom string + expectedValue sdkmath.LegacyDec + expectedErrMsg string + }{ + { + "successful exit value calculation", + func(oracleKeeper *mocks.OracleKeeper, accKeeper *mocks.AccountedPoolKeeper) { + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenA").Return(sdkmath.LegacyNewDec(10)) + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenB").Return(sdkmath.LegacyNewDec(5)) + accKeeper.On("GetAccountedBalance", mock.Anything, mock.Anything, "tokenA").Return(sdkmath.NewInt(1000)) + accKeeper.On("GetAccountedBalance", mock.Anything, mock.Anything, "tokenB").Return(sdkmath.NewInt(2000)) + }, + types.Pool{ + PoolParams: types.PoolParams{UseOracle: true}, + PoolAssets: []types.PoolAsset{ + {Token: sdk.NewCoin("tokenA", sdkmath.NewInt(1000)), Weight: sdkmath.NewInt(1)}, + {Token: sdk.NewCoin("tokenB", sdkmath.NewInt(2000)), Weight: sdkmath.NewInt(1)}, + }, + TotalShares: sdk.NewCoin("shares", sdkmath.NewInt(100)), + }, + sdkmath.NewInt(10), + "tokenA", + sdkmath.LegacyNewDec(2000), + "", + }, + { + "total shares is zero", + func(oracleKeeper *mocks.OracleKeeper, accKeeper *mocks.AccountedPoolKeeper) { + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenA").Return(sdkmath.LegacyNewDec(10)) + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenB").Return(sdkmath.LegacyNewDec(5)) + accKeeper.On("GetAccountedBalance", mock.Anything, mock.Anything, "tokenA").Return(sdkmath.NewInt(1000)) + accKeeper.On("GetAccountedBalance", mock.Anything, mock.Anything, "tokenB").Return(sdkmath.NewInt(2000)) + }, + types.Pool{ + PoolParams: types.PoolParams{UseOracle: true}, + PoolAssets: []types.PoolAsset{ + {Token: sdk.NewCoin("tokenA", sdkmath.NewInt(1000)), Weight: sdkmath.NewInt(1)}, + {Token: sdk.NewCoin("tokenB", sdkmath.NewInt(2000)), Weight: sdkmath.NewInt(1)}, + }, + TotalShares: sdk.NewCoin("shares", sdkmath.ZeroInt()), + }, + sdkmath.NewInt(10), + "tokenA", + sdkmath.LegacyZeroDec(), + "amount too low", + }, + { + "exiting shares greater than total shares", + func(oracleKeeper *mocks.OracleKeeper, accKeeper *mocks.AccountedPoolKeeper) { + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenA").Return(sdkmath.LegacyNewDec(10)) + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenB").Return(sdkmath.LegacyNewDec(5)) + accKeeper.On("GetAccountedBalance", mock.Anything, mock.Anything, "tokenA").Return(sdkmath.NewInt(1000)) + accKeeper.On("GetAccountedBalance", mock.Anything, mock.Anything, "tokenB").Return(sdkmath.NewInt(2000)) + }, + types.Pool{ + PoolParams: types.PoolParams{UseOracle: true}, + PoolAssets: []types.PoolAsset{ + {Token: sdk.NewCoin("tokenA", sdkmath.NewInt(1000)), Weight: sdkmath.NewInt(1)}, + {Token: sdk.NewCoin("tokenB", sdkmath.NewInt(2000)), Weight: sdkmath.NewInt(1)}, + }, + TotalShares: sdk.NewCoin("shares", sdkmath.NewInt(10)), + }, + sdkmath.NewInt(100), + "tokenA", + sdkmath.LegacyZeroDec(), + "shares is larger than the max amount", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + oracleKeeper := mocks.NewOracleKeeper(t) + accKeeper := mocks.NewAccountedPoolKeeper(t) + tc.setupMock(oracleKeeper, accKeeper) + + value, err := types.CalcExitValueWithoutSlippage(ctx, oracleKeeper, accKeeper, tc.pool, tc.exitingShares, tc.tokenOutDenom) + if tc.expectedErrMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrMsg) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedValue, value) + } + + oracleKeeper.AssertExpectations(t) + }) + } +} + +func TestCalcExitPool(t *testing.T) { + ctx := sdk.Context{} + + // Define test cases + testCases := []struct { + name string + setupMock func(oracleKeeper *mocks.OracleKeeper, accKeeper *mocks.AccountedPoolKeeper) + pool types.Pool + exitingShares sdkmath.Int + tokenOutDenom string + params types.Params + expectedCoins sdk.Coins + expectedBonus sdkmath.LegacyDec + expectedErrMsg string + }{ + { + "successful exit with oracle pricing", + func(oracleKeeper *mocks.OracleKeeper, accKeeper *mocks.AccountedPoolKeeper) { + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenA").Return(sdkmath.LegacyMustNewDecFromStr("0.00001")) + accKeeper.On("GetAccountedBalance", mock.Anything, mock.Anything, "tokenA").Return(sdkmath.NewInt(1000)) + }, + types.Pool{ + PoolParams: types.PoolParams{UseOracle: true}, + PoolAssets: []types.PoolAsset{ + {Token: sdk.NewCoin("tokenA", sdkmath.NewInt(1000)), Weight: sdkmath.NewInt(1)}, + }, + TotalShares: sdk.NewCoin("shares", sdkmath.NewInt(100)), + }, + sdkmath.NewInt(10), + "tokenA", + types.Params{ + WeightBreakingFeeMultiplier: sdkmath.LegacyMustNewDecFromStr("0.0005"), + }, + sdk.Coins{sdk.NewCoin("tokenA", sdkmath.NewInt(100))}, + sdkmath.LegacyZeroDec(), + "", + }, + { + "exiting shares greater than total shares", + func(oracleKeeper *mocks.OracleKeeper, accKeeper *mocks.AccountedPoolKeeper) {}, + types.Pool{ + PoolParams: types.PoolParams{UseOracle: true}, + TotalShares: sdk.NewCoin("shares", sdkmath.NewInt(10)), + }, + sdkmath.NewInt(20), + "tokenA", + types.Params{}, + sdk.Coins{}, + sdkmath.LegacyZeroDec(), + "shares is larger than the max amount", + }, + { + "exiting shares greater than total shares", + func(oracleKeeper *mocks.OracleKeeper, accKeeper *mocks.AccountedPoolKeeper) { + oracleKeeper.On("GetAssetPriceFromDenom", mock.Anything, "tokenA").Return(sdkmath.LegacyNewDec(0)) + }, + types.Pool{ + PoolParams: types.PoolParams{UseOracle: true}, + TotalShares: sdk.NewCoin("shares", sdkmath.NewInt(100)), + }, + sdkmath.NewInt(10), + "tokenA", + types.Params{}, + sdk.Coins{}, + sdkmath.LegacyZeroDec(), + "amount too low", + }, + { + "successful exit without oracle pricing", + func(oracleKeeper *mocks.OracleKeeper, accKeeper *mocks.AccountedPoolKeeper) {}, + types.Pool{ + PoolParams: types.PoolParams{UseOracle: false}, + PoolAssets: []types.PoolAsset{ + {Token: sdk.NewCoin("tokenA", sdkmath.NewInt(1000)), Weight: sdkmath.NewInt(1)}, + }, + TotalShares: sdk.NewCoin("shares", sdkmath.NewInt(100)), + }, + sdkmath.NewInt(10), + "", + types.Params{}, + sdk.Coins{sdk.NewCoin("tokenA", sdkmath.NewInt(100))}, + sdkmath.LegacyZeroDec(), + "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + oracleKeeper := mocks.NewOracleKeeper(t) + accKeeper := mocks.NewAccountedPoolKeeper(t) + tc.setupMock(oracleKeeper, accKeeper) + + exitCoins, weightBalanceBonus, err := types.CalcExitPool(ctx, oracleKeeper, tc.pool, accKeeper, tc.exitingShares, tc.tokenOutDenom, tc.params) + if tc.expectedErrMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrMsg) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedCoins, exitCoins) + require.Equal(t, tc.expectedBonus, weightBalanceBonus) + } + + oracleKeeper.AssertExpectations(t) + accKeeper.AssertExpectations(t) + }) + } +} diff --git a/x/amm/types/message_create_pool_test.go b/x/amm/types/message_create_pool_test.go index 7fe57a709..9695e8736 100644 --- a/x/amm/types/message_create_pool_test.go +++ b/x/amm/types/message_create_pool_test.go @@ -1,9 +1,11 @@ package types_test import ( + "fmt" + "testing" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - "testing" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/elys-network/elys/testutil/sample" @@ -30,6 +32,30 @@ func TestMsgCreatePool_ValidateBasic(t *testing.T) { }, err: sdkerrors.ErrInvalidAddress, }, + { + name: "swap fee is negative, invalid params", + msg: types.MsgCreatePool{ + Sender: sample.AccAddress(), + PoolParams: types.PoolParams{ + SwapFee: sdkmath.LegacyNewDec(-1), + UseOracle: false, + FeeDenom: ptypes.BaseCurrency, + }, + PoolAssets: []types.PoolAsset{ + { + Token: sdk.NewCoin("uusdc", sdkmath.NewInt(10000000)), + Weight: sdkmath.NewInt(10), + ExternalLiquidityRatio: sdkmath.LegacyOneDec(), + }, + { + Token: sdk.NewCoin("uatom", sdkmath.NewInt(10000000)), + Weight: sdkmath.NewInt(10), + ExternalLiquidityRatio: sdkmath.LegacyOneDec(), + }, + }, + }, + err: types.ErrNegativeSwapFee, + }, { name: "pool assets must be exactly two", msg: types.MsgCreatePool{ @@ -48,6 +74,30 @@ func TestMsgCreatePool_ValidateBasic(t *testing.T) { }, err: types.ErrPoolAssetsMustBeTwo, }, + { + name: "Invalid Pool Assets", + msg: types.MsgCreatePool{ + Sender: sample.AccAddress(), + PoolParams: types.PoolParams{ + SwapFee: sdkmath.LegacyZeroDec(), + UseOracle: false, + FeeDenom: ptypes.BaseCurrency, + }, + PoolAssets: []types.PoolAsset{ + { + Token: sdk.NewCoin("uusdc", sdkmath.NewInt(10000000)), + Weight: sdkmath.NewInt(10), + ExternalLiquidityRatio: sdkmath.LegacyOneDec(), + }, + { + Token: sdk.NewCoin("uatom", sdkmath.NewInt(10000000)), + Weight: sdkmath.NewInt(-1), + ExternalLiquidityRatio: sdkmath.LegacyOneDec(), + }, + }, + }, + err: fmt.Errorf("invalid pool asset"), + }, { name: "valid address", msg: types.MsgCreatePool{ @@ -76,10 +126,56 @@ func TestMsgCreatePool_ValidateBasic(t *testing.T) { t.Run(tt.name, func(t *testing.T) { err := tt.msg.ValidateBasic() if tt.err != nil { - require.ErrorIs(t, err, tt.err) + require.Contains(t, err.Error(), tt.err.Error()) return } require.NoError(t, err) }) } } + +func TestInitialLiquidity(t *testing.T) { + // Define test cases + testCases := []struct { + name string + msg types.MsgCreatePool + expectedCoins sdk.Coins + expectedPanic bool + }{ + { + "successful initial liquidity with sorted poolAssets", + types.MsgCreatePool{ + PoolAssets: []types.PoolAsset{ + {Token: sdk.NewCoin("tokenB", sdkmath.NewInt(2000))}, + {Token: sdk.NewCoin("tokenA", sdkmath.NewInt(1000))}, + }, + }, + sdk.Coins{ + sdk.NewCoin("tokenA", sdkmath.NewInt(1000)), + sdk.NewCoin("tokenB", sdkmath.NewInt(2000)), + }, + false, + }, + { + "empty pool assets", + types.MsgCreatePool{ + PoolAssets: []types.PoolAsset{}, + }, + sdk.Coins{}, + true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.expectedPanic { + require.Panics(t, func() { + _ = tc.msg.InitialLiquidity() + }) + } else { + coins := tc.msg.InitialLiquidity() + require.Equal(t, tc.expectedCoins, coins) + } + }) + } +} diff --git a/x/amm/types/message_exit_pool_test.go b/x/amm/types/message_exit_pool_test.go index 95134086b..081df4080 100644 --- a/x/amm/types/message_exit_pool_test.go +++ b/x/amm/types/message_exit_pool_test.go @@ -1,9 +1,11 @@ package types_test import ( + "fmt" "testing" "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/elys-network/elys/testutil/sample" "github.com/elys-network/elys/x/amm/types" @@ -23,19 +25,47 @@ func TestMsgExitPool_ValidateBasic(t *testing.T) { ShareAmountIn: math.NewInt(100), }, err: sdkerrors.ErrInvalidAddress, - }, { + }, + { name: "valid address", msg: types.MsgExitPool{ Sender: sample.AccAddress(), ShareAmountIn: math.NewInt(100), }, }, + { + name: "Invalid Minimum Amounts Out", + msg: types.MsgExitPool{ + Sender: sample.AccAddress(), + ShareAmountIn: math.NewInt(100), + MinAmountsOut: sdk.Coins{sdk.Coin{Denom: "uusdc", Amount: math.NewInt(-100)}}, + }, + err: fmt.Errorf("negative coin amount"), + }, + { + name: "ShareAmount is Nil", + msg: types.MsgExitPool{ + Sender: sample.AccAddress(), + ShareAmountIn: math.Int{}, + MinAmountsOut: sdk.Coins{sdk.Coin{Denom: "uusdc", Amount: math.NewInt(100)}}, + }, + err: types.ErrInvalidShareAmountOut, + }, + { + name: "ShareAmount is Negative", + msg: types.MsgExitPool{ + Sender: sample.AccAddress(), + ShareAmountIn: math.NewInt(-100), + MinAmountsOut: sdk.Coins{sdk.Coin{Denom: "uusdc", Amount: math.NewInt(100)}}, + }, + err: types.ErrInvalidShareAmountOut, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.msg.ValidateBasic() if tt.err != nil { - require.ErrorIs(t, err, tt.err) + require.Contains(t, err.Error(), tt.err.Error()) return } require.NoError(t, err) diff --git a/x/amm/types/message_feed_multiple_external_liquidity_test.go b/x/amm/types/message_feed_multiple_external_liquidity_test.go new file mode 100644 index 000000000..c23e73732 --- /dev/null +++ b/x/amm/types/message_feed_multiple_external_liquidity_test.go @@ -0,0 +1,132 @@ +package types_test + +import ( + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/elys-network/elys/testutil/sample" + "github.com/elys-network/elys/x/amm/types" + "github.com/stretchr/testify/require" +) + +func TestMsgFeedMultipleExternalLiquidity_ValidateBasic(t *testing.T) { + // Define test cases + testCases := []struct { + name string + msg types.MsgFeedMultipleExternalLiquidity + expectedErrMsg string + }{ + { + "Invalid address", + types.MsgFeedMultipleExternalLiquidity{ + Sender: "elys11", + Liquidity: []types.ExternalLiquidity{ + { + AmountDepthInfo: []types.AssetAmountDepth{ + {Asset: "tokenA", Depth: sdkmath.LegacyNewDec(1000), Amount: sdkmath.LegacyNewDec(500)}, + {Asset: "tokenB", Depth: sdkmath.LegacyNewDec(2000), Amount: sdkmath.LegacyNewDec(1000)}, + }, + }, + }, + }, + "invalid sender address", + }, + { + "valid message", + types.MsgFeedMultipleExternalLiquidity{ + Sender: sample.AccAddress(), + Liquidity: []types.ExternalLiquidity{ + { + AmountDepthInfo: []types.AssetAmountDepth{ + {Asset: "tokenA", Depth: sdkmath.LegacyNewDec(1000), Amount: sdkmath.LegacyNewDec(500)}, + {Asset: "tokenB", Depth: sdkmath.LegacyNewDec(2000), Amount: sdkmath.LegacyNewDec(1000)}, + }, + }, + }, + }, + "", + }, + { + "invalid asset denom", + types.MsgFeedMultipleExternalLiquidity{ + Sender: sample.AccAddress(), + Liquidity: []types.ExternalLiquidity{ + { + AmountDepthInfo: []types.AssetAmountDepth{ + {Asset: "invalid denom", Depth: sdkmath.LegacyNewDec(1000), Amount: sdkmath.LegacyNewDec(500)}, + }, + }, + }, + }, + "invalid denom: invalid denom", + }, + { + "negative depth", + types.MsgFeedMultipleExternalLiquidity{ + Sender: sample.AccAddress(), + Liquidity: []types.ExternalLiquidity{ + { + AmountDepthInfo: []types.AssetAmountDepth{ + {Asset: "tokenA", Depth: sdkmath.LegacyNewDec(-1000), Amount: sdkmath.LegacyNewDec(500)}, + }, + }, + }, + }, + "depth cannot be negative or nil", + }, + { + "negative amount", + types.MsgFeedMultipleExternalLiquidity{ + Sender: sample.AccAddress(), + Liquidity: []types.ExternalLiquidity{ + { + AmountDepthInfo: []types.AssetAmountDepth{ + {Asset: "tokenA", Depth: sdkmath.LegacyNewDec(1000), Amount: sdkmath.LegacyNewDec(-500)}, + }, + }, + }, + }, + "depth amount cannot be negative or nil", + }, + { + "nil depth", + types.MsgFeedMultipleExternalLiquidity{ + Sender: sample.AccAddress(), + Liquidity: []types.ExternalLiquidity{ + { + AmountDepthInfo: []types.AssetAmountDepth{ + {Asset: "tokenA", Depth: sdkmath.LegacyDec{}, Amount: sdkmath.LegacyNewDec(500)}, + }, + }, + }, + }, + "depth cannot be negative or nil", + }, + { + "nil amount", + types.MsgFeedMultipleExternalLiquidity{ + Sender: sample.AccAddress(), + Liquidity: []types.ExternalLiquidity{ + { + AmountDepthInfo: []types.AssetAmountDepth{ + {Asset: "tokenA", Depth: sdkmath.LegacyNewDec(1000), Amount: sdkmath.LegacyDec{}}, + }, + }, + }, + }, + "depth amount cannot be negative or nil", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.expectedErrMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrMsg) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/amm/types/message_join_pool_test.go b/x/amm/types/message_join_pool_test.go index 399b444f0..5cfced70f 100644 --- a/x/amm/types/message_join_pool_test.go +++ b/x/amm/types/message_join_pool_test.go @@ -1,9 +1,11 @@ package types_test import ( + "fmt" "testing" "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/elys-network/elys/testutil/sample" "github.com/elys-network/elys/x/amm/types" @@ -29,12 +31,39 @@ func TestMsgJoinPool_ValidateBasic(t *testing.T) { ShareAmountOut: math.NewInt(100), }, }, + { + name: "Invalid Maximum Amounts in", + msg: types.MsgJoinPool{ + Sender: sample.AccAddress(), + ShareAmountOut: math.NewInt(100), + MaxAmountsIn: sdk.Coins{sdk.Coin{Denom: "uusdc", Amount: math.NewInt(-100)}}, + }, + err: fmt.Errorf("negative coin amount"), + }, + { + name: "ShareAmount is Nil", + msg: types.MsgJoinPool{ + Sender: sample.AccAddress(), + ShareAmountOut: math.Int{}, + MaxAmountsIn: sdk.Coins{sdk.Coin{Denom: "uusdc", Amount: math.NewInt(100)}}, + }, + err: types.ErrInvalidShareAmountOut, + }, + { + name: "ShareAmount is Negative", + msg: types.MsgJoinPool{ + Sender: sample.AccAddress(), + ShareAmountOut: math.NewInt(-100), + MaxAmountsIn: sdk.Coins{sdk.Coin{Denom: "uusdc", Amount: math.NewInt(100)}}, + }, + err: types.ErrInvalidShareAmountOut, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.msg.ValidateBasic() if tt.err != nil { - require.ErrorIs(t, err, tt.err) + require.Contains(t, err.Error(), tt.err.Error()) return } require.NoError(t, err) diff --git a/x/amm/types/message_swap_by_denom_test.go b/x/amm/types/message_swap_by_denom_test.go index f6d01a842..692afbed3 100644 --- a/x/amm/types/message_swap_by_denom_test.go +++ b/x/amm/types/message_swap_by_denom_test.go @@ -1,9 +1,12 @@ package types import ( + "fmt" + "testing" + + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" ptypes "github.com/elys-network/elys/x/parameter/types" - "testing" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/elys-network/elys/testutil/sample" @@ -22,7 +25,8 @@ func TestMsgSwapByDenom_ValidateBasic(t *testing.T) { Sender: "invalid_address", }, err: sdkerrors.ErrInvalidAddress, - }, { + }, + { name: "valid address", msg: MsgSwapByDenom{ Sender: sample.AccAddress(), @@ -31,12 +35,42 @@ func TestMsgSwapByDenom_ValidateBasic(t *testing.T) { DenomOut: ptypes.BaseCurrency, }, }, + { + name: "Invalid Amount", + msg: MsgSwapByDenom{ + Sender: sample.AccAddress(), + Amount: sdk.Coin{Denom: ptypes.ATOM, Amount: sdkmath.NewInt(-10)}, + DenomIn: ptypes.ATOM, + DenomOut: ptypes.BaseCurrency, + }, + err: fmt.Errorf("negative coin amount"), + }, + { + name: "Invalid DenomIn", + msg: MsgSwapByDenom{ + Sender: sample.AccAddress(), + Amount: sdk.Coin{Denom: ptypes.ATOM, Amount: sdkmath.NewInt(10)}, + DenomIn: "invalid denom in", + DenomOut: ptypes.BaseCurrency, + }, + err: fmt.Errorf("invalid denom"), + }, + { + name: "Invalid Denomout", + msg: MsgSwapByDenom{ + Sender: sample.AccAddress(), + Amount: sdk.Coin{Denom: ptypes.ATOM, Amount: sdkmath.NewInt(10)}, + DenomIn: ptypes.ATOM, + DenomOut: "invalid denom out", + }, + err: fmt.Errorf("invalid denom"), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.msg.ValidateBasic() if tt.err != nil { - require.ErrorIs(t, err, tt.err) + require.Contains(t, err.Error(), tt.err.Error()) return } require.NoError(t, err) diff --git a/x/amm/types/message_swap_exact_amount_in_test.go b/x/amm/types/message_swap_exact_amount_in_test.go index 3de28ffe4..920b9186f 100644 --- a/x/amm/types/message_swap_exact_amount_in_test.go +++ b/x/amm/types/message_swap_exact_amount_in_test.go @@ -1,10 +1,12 @@ package types_test import ( + "fmt" + "testing" + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" ptypes "github.com/elys-network/elys/x/parameter/types" - "testing" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/elys-network/elys/testutil/sample" @@ -24,7 +26,8 @@ func TestMsgSwapExactAmountIn_ValidateBasic(t *testing.T) { Sender: "invalid_address", }, err: sdkerrors.ErrInvalidAddress, - }, { + }, + { name: "valid address", msg: types.MsgSwapExactAmountIn{ Sender: sample.AccAddress(), @@ -34,12 +37,56 @@ func TestMsgSwapExactAmountIn_ValidateBasic(t *testing.T) { Recipient: "", }, }, + { + name: "Invalid recipient address", + msg: types.MsgSwapExactAmountIn{ + Sender: sample.AccAddress(), + Routes: nil, + TokenIn: sdk.Coin{Denom: ptypes.ATOM, Amount: math.NewInt(10)}, + TokenOutMinAmount: math.NewInt(1), + Recipient: "cosmos1invalid", + }, + err: sdkerrors.ErrInvalidAddress, + }, + { + name: "Invalid TokenOutDenom in route", + msg: types.MsgSwapExactAmountIn{ + Sender: sample.AccAddress(), + Routes: []types.SwapAmountInRoute{{TokenOutDenom: "invalid denom"}}, + TokenIn: sdk.Coin{Denom: ptypes.ATOM, Amount: math.NewInt(10)}, + TokenOutMinAmount: math.NewInt(1), + Recipient: sample.AccAddress(), + }, + err: fmt.Errorf("invalid denom"), + }, + { + name: "Invalid TokenIn", + msg: types.MsgSwapExactAmountIn{ + Sender: sample.AccAddress(), + Routes: []types.SwapAmountInRoute{{TokenOutDenom: "uusdc"}}, + TokenIn: sdk.Coin{Denom: ptypes.ATOM, Amount: math.NewInt(-10)}, + TokenOutMinAmount: math.NewInt(1), + Recipient: sample.AccAddress(), + }, + err: fmt.Errorf("negative coin amount"), + }, + { + name: "Invalid TokenIn amount", + msg: types.MsgSwapExactAmountIn{ + Sender: sample.AccAddress(), + Routes: []types.SwapAmountInRoute{{TokenOutDenom: "uusdc"}}, + TokenIn: sdk.Coin{Denom: ptypes.ATOM, Amount: math.NewInt(0)}, + TokenOutMinAmount: math.NewInt(1), + Recipient: sample.AccAddress(), + }, + err: fmt.Errorf("token in is zero"), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.msg.ValidateBasic() if tt.err != nil { - require.ErrorIs(t, err, tt.err) + require.Contains(t, err.Error(), tt.err.Error()) return } require.NoError(t, err) diff --git a/x/amm/types/message_swap_exact_amount_out_test.go b/x/amm/types/message_swap_exact_amount_out_test.go index 8679173c7..d0b5ea327 100644 --- a/x/amm/types/message_swap_exact_amount_out_test.go +++ b/x/amm/types/message_swap_exact_amount_out_test.go @@ -1,10 +1,12 @@ package types_test import ( + "fmt" + "testing" + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" ptypes "github.com/elys-network/elys/x/parameter/types" - "testing" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/elys-network/elys/testutil/sample" @@ -24,22 +26,67 @@ func TestMsgSwapExactAmountOut_ValidateBasic(t *testing.T) { Sender: "invalid_address", }, err: sdkerrors.ErrInvalidAddress, - }, { + }, + { name: "valid address", msg: types.MsgSwapExactAmountOut{ Sender: sample.AccAddress(), Routes: nil, - TokenOut: sdk.Coin{ptypes.ATOM, math.NewInt(10)}, + TokenOut: sdk.Coin{Denom: ptypes.ATOM, Amount: math.NewInt(10)}, TokenInMaxAmount: math.NewInt(1), Recipient: "", }, }, + { + name: "Invalid recipient address", + msg: types.MsgSwapExactAmountOut{ + Sender: sample.AccAddress(), + Routes: nil, + TokenOut: sdk.Coin{Denom: ptypes.ATOM, Amount: math.NewInt(10)}, + TokenInMaxAmount: math.NewInt(1), + Recipient: "cosmos1invalid", + }, + err: sdkerrors.ErrInvalidAddress, + }, + { + name: "Invalid tokenInDenom in route", + msg: types.MsgSwapExactAmountOut{ + Sender: sample.AccAddress(), + Routes: []types.SwapAmountOutRoute{{TokenInDenom: "invalid denom"}}, + TokenOut: sdk.Coin{Denom: ptypes.ATOM, Amount: math.NewInt(10)}, + TokenInMaxAmount: math.NewInt(1), + Recipient: sample.AccAddress(), + }, + err: fmt.Errorf("invalid denom"), + }, + { + name: "Invalid tokenOut", + msg: types.MsgSwapExactAmountOut{ + Sender: sample.AccAddress(), + Routes: []types.SwapAmountOutRoute{{TokenInDenom: "uusdc"}}, + TokenOut: sdk.Coin{Denom: ptypes.ATOM, Amount: math.NewInt(-10)}, + TokenInMaxAmount: math.NewInt(1), + Recipient: sample.AccAddress(), + }, + err: fmt.Errorf("negative coin amount"), + }, + { + name: "Invalid tokenOut amount", + msg: types.MsgSwapExactAmountOut{ + Sender: sample.AccAddress(), + Routes: []types.SwapAmountOutRoute{{TokenInDenom: "uusdc"}}, + TokenOut: sdk.Coin{Denom: ptypes.ATOM, Amount: math.NewInt(0)}, + TokenInMaxAmount: math.NewInt(1), + Recipient: sample.AccAddress(), + }, + err: fmt.Errorf("token in is zero"), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.msg.ValidateBasic() if tt.err != nil { - require.ErrorIs(t, err, tt.err) + require.Contains(t, err.Error(), tt.err.Error()) return } require.NoError(t, err) diff --git a/x/amm/types/pool_calc_join_pool_no_swap_shares_test.go b/x/amm/types/pool_calc_join_pool_no_swap_shares_test.go new file mode 100644 index 000000000..e619d8495 --- /dev/null +++ b/x/amm/types/pool_calc_join_pool_no_swap_shares_test.go @@ -0,0 +1,87 @@ +// pool_calc_join_pool_no_swap_shares_test.go +package types_test + +import ( + "testing" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/elys-network/elys/x/amm/types" + "github.com/stretchr/testify/require" +) + +func TestMaximalExactRatioJoin(t *testing.T) { + // Define test cases + testCases := []struct { + name string + pool *types.Pool + tokensIn sdk.Coins + expectedShares sdkmath.Int + expectedRem sdk.Coins + expectedErrMsg string + }{ + { + "successful join with exact ratio", + &types.Pool{ + TotalShares: sdk.NewCoin("shares", sdkmath.NewInt(1000)), + PoolAssets: []types.PoolAsset{ + {Token: sdk.NewCoin("tokenA", sdkmath.NewInt(1000))}, + {Token: sdk.NewCoin("tokenB", sdkmath.NewInt(2000))}, + }, + }, + sdk.Coins{ + sdk.NewCoin("tokenA", sdkmath.NewInt(100)), + sdk.NewCoin("tokenB", sdkmath.NewInt(200)), + }, + sdkmath.NewInt(100), + sdk.Coins{}, + "", + }, + { + "successful join with remaining coins", + &types.Pool{ + TotalShares: sdk.NewCoin("shares", sdkmath.NewInt(1000)), + PoolAssets: []types.PoolAsset{ + {Token: sdk.NewCoin("tokenA", sdkmath.NewInt(1000))}, + {Token: sdk.NewCoin("tokenB", sdkmath.NewInt(2000))}, + }, + }, + sdk.Coins{ + sdk.NewCoin("tokenA", sdkmath.NewInt(150)), + sdk.NewCoin("tokenB", sdkmath.NewInt(200)), + }, + sdkmath.NewInt(100), + sdk.Coins{ + sdk.NewCoin("tokenA", sdkmath.NewInt(50)), + }, + "", + }, + { + "unexpected error due to pool liquidity is zero for denom", + &types.Pool{ + TotalShares: sdk.NewCoin("shares", sdkmath.NewInt(1000)), + PoolAssets: []types.PoolAsset{}, + }, + sdk.Coins{ + sdk.NewCoin("tokenA", sdkmath.NewInt(100)), + }, + sdkmath.Int{}, + sdk.Coins{}, + "pool liquidity is zero for denom", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + shares, rem, err := types.MaximalExactRatioJoin(tc.pool, tc.tokensIn) + if tc.expectedErrMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrMsg) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedShares, shares) + require.Equal(t, tc.expectedRem, rem) + } + }) + } +}