diff --git a/x/amm/keeper/calc_swap_estimation_by_denom.go b/x/amm/keeper/calc_swap_estimation_by_denom.go index 21d42d49c..b4cefd4f4 100644 --- a/x/amm/keeper/calc_swap_estimation_by_denom.go +++ b/x/amm/keeper/calc_swap_estimation_by_denom.go @@ -14,7 +14,7 @@ func (k Keeper) CalcSwapEstimationByDenom( amount sdk.Coin, denomIn string, denomOut string, - baseCurrency string, + baseAssetsDenoms []string, address string, overrideSwapFee osmomath.BigDec, decimals uint64, @@ -48,9 +48,19 @@ func (k Keeper) CalcSwapEstimationByDenom( // Determine the correct route based on the amount's denom if amount.Denom == denomIn { - inRoute, err = k.CalcInRouteByDenom(ctx, denomIn, denomOut, baseCurrency) + for _, baseCurrency := range baseAssetsDenoms { + inRoute, err = k.CalcInRouteByDenom(ctx, denomIn, denomOut, baseCurrency) + if err == nil { + break + } + } } else if amount.Denom == denomOut { - outRoute, err = k.CalcOutRouteByDenom(ctx, denomOut, denomIn, baseCurrency) + for _, baseCurrency := range baseAssetsDenoms { + outRoute, err = k.CalcOutRouteByDenom(ctx, denomOut, denomIn, baseCurrency) + if err == nil { + break + } + } } else { err = types.ErrInvalidDenom return diff --git a/x/amm/keeper/calc_swap_estimation_by_denom_test.go b/x/amm/keeper/calc_swap_estimation_by_denom_test.go index b2945f7dd..4e74da408 100644 --- a/x/amm/keeper/calc_swap_estimation_by_denom_test.go +++ b/x/amm/keeper/calc_swap_estimation_by_denom_test.go @@ -106,7 +106,7 @@ func (suite *AmmKeeperTestSuite) TestCalcSwapEstimationByDenom() { inRoute, outRoute, tokenOut, spotPrice, _, _, _, _, _, _, err := suite.app.AmmKeeper.CalcSwapEstimationByDenom( suite.ctx, amount, - ptypes.Elys, "uusda", ptypes.BaseCurrency, + ptypes.Elys, "uusda", []string{ptypes.BaseCurrency}, "", osmomath.ZeroBigDec(), 1, @@ -121,7 +121,7 @@ func (suite *AmmKeeperTestSuite) TestCalcSwapEstimationByDenom() { inRoute, outRoute, tokenOut, spotPrice, _, _, _, _, _, _, err = suite.app.AmmKeeper.CalcSwapEstimationByDenom( suite.ctx, amount, - ptypes.Elys, "uusda", ptypes.BaseCurrency, + ptypes.Elys, "uusda", []string{ptypes.BaseCurrency}, "", osmomath.ZeroBigDec(), 1, @@ -136,7 +136,7 @@ func (suite *AmmKeeperTestSuite) TestCalcSwapEstimationByDenom() { amount = sdk.NewCoin("invalid", sdkmath.NewInt(1000)) _, _, _, _, _, _, _, _, _, _, err = suite.app.AmmKeeper.CalcSwapEstimationByDenom( suite.ctx, amount, - ptypes.Elys, "uusda", ptypes.BaseCurrency, + ptypes.Elys, "uusda", []string{ptypes.BaseCurrency}, "", osmomath.ZeroBigDec(), 1, diff --git a/x/amm/keeper/msg_server_create_pool.go b/x/amm/keeper/msg_server_create_pool.go index 3a02b6be2..f00e06469 100644 --- a/x/amm/keeper/msg_server_create_pool.go +++ b/x/amm/keeper/msg_server_create_pool.go @@ -2,7 +2,6 @@ package keeper import ( "context" - "errors" "fmt" "strconv" "strings" @@ -22,10 +21,6 @@ func (k msgServer) CreatePool(goCtx context.Context, msg *types.MsgCreatePool) ( // Pay pool creation fee params := k.GetParams(ctx) - if !params.IsCreatorAllowed(msg.Sender) { - return nil, errors.New("sender is not allowed to create pool") - } - sender := sdk.MustAccAddressFromBech32(msg.Sender) baseAssetExists := false @@ -39,6 +34,18 @@ func (k msgServer) CreatePool(goCtx context.Context, msg *types.MsgCreatePool) ( return nil, errorsmod.Wrapf(types.ErrOnlyBaseAssetsPoolAllowed, "one of the asset must be from %s", strings.Join(params.BaseAssets, ", ")) } + // gov module is allowed to create pools + if !params.IsCreatorAllowed(msg.Sender) { + if msg.PoolParams.UseOracle { + return nil, errorsmod.Wrapf(types.ErrPoolCreationNotAllowed, "oracle pool is not allowed to be created by %s", msg.Sender) + } + + poolExistForSameAssets := k.Keeper.CheckExistingPoolWithSameAssets(ctx, msg.PoolAssets) + if poolExistForSameAssets { + return nil, types.ErrPoolExistsWithSameAssets + } + } + feeAssetExists := k.CheckBaseAssetExist(ctx, msg.PoolParams.FeeDenom) if !feeAssetExists { return nil, fmt.Errorf("fee denom must be from %s", strings.Join(params.BaseAssets, ", ")) diff --git a/x/amm/keeper/msg_server_swap_by_denom.go b/x/amm/keeper/msg_server_swap_by_denom.go index be0477b80..c4f031c38 100644 --- a/x/amm/keeper/msg_server_swap_by_denom.go +++ b/x/amm/keeper/msg_server_swap_by_denom.go @@ -23,13 +23,16 @@ func (k Keeper) SwapByDenom(ctx sdk.Context, msg *types.MsgSwapByDenom) (*types. return nil, err } - // retrieve base currency denom - baseCurrency, found := k.assetProfileKeeper.GetUsdcDenom(ctx) - if !found { - return nil, errorsmod.Wrapf(assetprofiletypes.ErrAssetProfileNotFound, "asset %s not found", ptypes.BaseCurrency) + baseAssetsDenoms := k.GetParams(ctx).BaseAssets + if len(baseAssetsDenoms) == 0 { + baseCurrency, found := k.assetProfileKeeper.GetUsdcDenom(ctx) + if !found { + return nil, errorsmod.Wrapf(assetprofiletypes.ErrAssetProfileNotFound, "asset %s not found", ptypes.BaseCurrency) + } + baseAssetsDenoms = []string{baseCurrency} } - inRoute, outRoute, _, spotPrice, _, _, _, slippage, weightBonus, _, err := k.CalcSwapEstimationByDenom(ctx, msg.Amount, msg.DenomIn, msg.DenomOut, baseCurrency, msg.Sender, osmomath.ZeroBigDec(), 0) + inRoute, outRoute, _, spotPrice, _, _, _, slippage, weightBonus, _, err := k.CalcSwapEstimationByDenom(ctx, msg.Amount, msg.DenomIn, msg.DenomOut, baseAssetsDenoms, msg.Sender, osmomath.ZeroBigDec(), 0) if err != nil { return nil, err } diff --git a/x/amm/keeper/msg_server_update_pool_params.go b/x/amm/keeper/msg_server_update_pool_params.go index d058f0ab8..1d83df805 100644 --- a/x/amm/keeper/msg_server_update_pool_params.go +++ b/x/amm/keeper/msg_server_update_pool_params.go @@ -19,45 +19,28 @@ func (k Keeper) UpdatePoolParams(ctx sdk.Context, poolId uint64, newPoolParams t return 0, types.PoolParams{}, types.ErrPoolNotFound } - baseCurrency, found := k.assetProfileKeeper.GetUsdcDenom(ctx) + usdcDenom, found := k.assetProfileKeeper.GetUsdcDenom(ctx) if !found { return 0, types.PoolParams{}, errorsmod.Wrapf(assetprofiletypes.ErrAssetProfileNotFound, "asset %s not found", ptypes.BaseCurrency) } // If the fee denom is empty, set it to the base currency if newPoolParams.FeeDenom == "" { - newPoolParams.FeeDenom = baseCurrency + newPoolParams.FeeDenom = usdcDenom } // changing from non-oracle pool to oracle pool if !pool.PoolParams.UseOracle && newPoolParams.UseOracle { - - nonBaseCurrencyDenom := "" - usdcDenomFound := false - for _, asset := range pool.PoolAssets { - if asset.Token.Denom != baseCurrency { - nonBaseCurrencyDenom = asset.Token.Denom + entry, found := k.assetProfileKeeper.GetEntryByDenom(ctx, asset.Token.Denom) + if !found { + return 0, types.PoolParams{}, fmt.Errorf("asset profile for %s not found", asset.Token.Denom) } - if asset.Token.Denom == baseCurrency { - usdcDenomFound = true + _, found = k.oracleKeeper.GetAssetPrice(ctx, entry.DisplayName) + if !found { + return 0, types.PoolParams{}, fmt.Errorf("oracle price for %s not found", entry.DisplayName) } } - if !usdcDenomFound { - return 0, types.PoolParams{}, fmt.Errorf("no usdc denom in the amm pool %d", poolId) - } - if nonBaseCurrencyDenom == "" { - return 0, types.PoolParams{}, fmt.Errorf("no non-usdc denom in the amm pool %d", poolId) - } - - entry, found := k.assetProfileKeeper.GetEntryByDenom(ctx, nonBaseCurrencyDenom) - if !found { - return 0, types.PoolParams{}, fmt.Errorf("asset profile for %s not found", nonBaseCurrencyDenom) - } - _, found = k.oracleKeeper.GetAssetPrice(ctx, entry.DisplayName) - if !found { - return 0, types.PoolParams{}, fmt.Errorf("oracle price for %s not found", entry.DisplayName) - } } pool.PoolParams = newPoolParams err := pool.Validate() diff --git a/x/amm/keeper/params.go b/x/amm/keeper/params.go index 9fc6d85aa..5cd68ce91 100644 --- a/x/amm/keeper/params.go +++ b/x/amm/keeper/params.go @@ -48,6 +48,35 @@ func (k Keeper) CheckBaseAssetExist(ctx sdk.Context, denom string) bool { return found } +// CheckExistingPoolWithSameAssets returns true if a pool with the same set of assets already exists. +func (k Keeper) CheckExistingPoolWithSameAssets(ctx sdk.Context, newAssets []types.PoolAsset) bool { + newAssetsMap := make(map[string]bool, len(newAssets)) + for _, asset := range newAssets { + newAssetsMap[asset.Token.Denom] = true + } + + existingPools := k.GetAllPool(ctx) + for _, pool := range existingPools { + if len(pool.PoolAssets) != len(newAssets) { + continue + } + + matches := true + for _, poolAsset := range pool.PoolAssets { + if !newAssetsMap[poolAsset.Token.Denom] { + matches = false + break + } + } + + if matches { + return true + } + } + + return false +} + func (k Keeper) V8Migrate(ctx sdk.Context) error { baseCurrencyDenom, found := k.assetProfileKeeper.GetUsdcDenom(ctx) if !found { diff --git a/x/amm/keeper/params_test.go b/x/amm/keeper/params_test.go index 67cd39e95..0009583c1 100644 --- a/x/amm/keeper/params_test.go +++ b/x/amm/keeper/params_test.go @@ -3,8 +3,15 @@ package keeper_test import ( "testing" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" testkeeper "github.com/elys-network/elys/v6/testutil/keeper" + "github.com/elys-network/elys/v6/x/amm/keeper" "github.com/elys-network/elys/v6/x/amm/types" + ptypes "github.com/elys-network/elys/v6/x/parameter/types" "github.com/stretchr/testify/require" ) @@ -17,3 +24,79 @@ func TestGetParams(t *testing.T) { require.EqualValues(t, params, k.GetParams(ctx)) } + +func (suite *AmmKeeperTestSuite) TestCheckExistingPoolWithSameAssets() { + suite.SetupTest() + suite.SetupCoinPrices() + suite.SetAmmParams() + suite.SetupAssetProfile() + + // Bootstrap accounts + sender := authtypes.NewModuleAddress(govtypes.ModuleName) + params := suite.app.AmmKeeper.GetParams(suite.ctx) + + // Bootstrap balances + poolCreationFee := sdk.NewCoin(ptypes.Elys, params.PoolCreationFee) + coins := sdk.NewCoins( + sdk.NewCoin("uatom", sdkmath.NewInt(1000)), + sdk.NewCoin("uusdc", sdkmath.NewInt(1000)), + poolCreationFee, + ) + err := suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, coins) + suite.Require().NoError(err) + err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, sender, coins) + suite.Require().NoError(err) + + // Create an initial pool + msgServer := keeper.NewMsgServerImpl(*suite.app.AmmKeeper) + poolAssets := []types.PoolAsset{ + { + Token: sdk.NewCoin("uatom", sdkmath.NewInt(500)), + Weight: sdkmath.NewInt(10), + ExternalLiquidityRatio: sdkmath.LegacyOneDec(), + }, + { + Token: sdk.NewCoin("uusdc", sdkmath.NewInt(500)), + Weight: sdkmath.NewInt(10), + ExternalLiquidityRatio: sdkmath.LegacyOneDec(), + }, + } + poolParams := types.PoolParams{ + SwapFee: sdkmath.LegacyZeroDec(), + UseOracle: false, + FeeDenom: ptypes.BaseCurrency, + } + _, err = msgServer.CreatePool( + suite.ctx, + &types.MsgCreatePool{ + Sender: sender.String(), + PoolParams: poolParams, + PoolAssets: poolAssets, + }, + ) + suite.Require().NoError(err) + + // Test case 1: No matching denoms + newAssets := []types.PoolAsset{ + { + Token: sdk.NewCoin("uosmo", sdkmath.NewInt(500)), + }, + { + Token: sdk.NewCoin("uusdc", sdkmath.NewInt(500)), + }, + } + exists := suite.app.AmmKeeper.CheckExistingPoolWithSameAssets(suite.ctx, newAssets) + suite.Require().False(exists, "Expected no matching pool for new assets") + + // Test case 2: Matching denoms + matchingAssets := []types.PoolAsset{ + { + Token: sdk.NewCoin("uusdc", sdkmath.NewInt(500)), + }, + { + Token: sdk.NewCoin("uatom", sdkmath.NewInt(500)), + }, + } + exists = suite.app.AmmKeeper.CheckExistingPoolWithSameAssets(suite.ctx, matchingAssets) + suite.Require().True(exists, "Expected a matching pool for the same assets") +} diff --git a/x/amm/keeper/query_swap_estimation_by_denom.go b/x/amm/keeper/query_swap_estimation_by_denom.go index 1a5f5d5f5..3b4ec162b 100644 --- a/x/amm/keeper/query_swap_estimation_by_denom.go +++ b/x/amm/keeper/query_swap_estimation_by_denom.go @@ -20,10 +20,13 @@ func (k Keeper) SwapEstimationByDenom(goCtx context.Context, req *types.QuerySwa ctx := sdk.UnwrapSDKContext(goCtx) - // retrieve base currency denom - baseCurrency, found := k.assetProfileKeeper.GetUsdcDenom(ctx) - if !found { - return nil, errorsmod.Wrapf(assetprofiletypes.ErrAssetProfileNotFound, "asset %s not found", ptypes.BaseCurrency) + baseAssetsDenoms := k.GetParams(ctx).BaseAssets + if len(baseAssetsDenoms) == 0 { + baseCurrency, found := k.assetProfileKeeper.GetUsdcDenom(ctx) + if !found { + return nil, errorsmod.Wrapf(assetprofiletypes.ErrAssetProfileNotFound, "asset %s not found", ptypes.BaseCurrency) + } + baseAssetsDenoms = []string{baseCurrency} } // retrieve denom in decimals @@ -32,7 +35,7 @@ func (k Keeper) SwapEstimationByDenom(goCtx context.Context, req *types.QuerySwa return nil, errorsmod.Wrapf(assetprofiletypes.ErrAssetProfileNotFound, "asset %s not found", req.DenomIn) } - inRoute, outRoute, amount, spotPrice, swapFee, discount, availableLiquidity, slippage, weightBonus, priceImpact, err := k.CalcSwapEstimationByDenom(ctx, req.Amount, req.DenomIn, req.DenomOut, baseCurrency, req.Address, osmomath.ZeroBigDec(), entry.Decimals) + inRoute, outRoute, amount, spotPrice, swapFee, discount, availableLiquidity, slippage, weightBonus, priceImpact, err := k.CalcSwapEstimationByDenom(ctx, req.Amount, req.DenomIn, req.DenomOut, baseAssetsDenoms, req.Address, osmomath.ZeroBigDec(), entry.Decimals) if err != nil { return nil, err } diff --git a/x/amm/types/errors.go b/x/amm/types/errors.go index 065fb70ef..860bf2d5a 100644 --- a/x/amm/types/errors.go +++ b/x/amm/types/errors.go @@ -41,6 +41,9 @@ var ( ErrTokenOutAmountZero = errors.Register(ModuleName, 115, "token out amount is zero") ErrUnauthorizedUpFrontSwap = errors.Register(ModuleName, 116, "sender is not allowed to make upfront swaps") + + ErrPoolExistsWithSameAssets = errors.Register(ModuleName, 117, "pool already exists with the same assets") + ErrPoolCreationNotAllowed = errors.Register(ModuleName, 118, "sender is not allowed to create pool") ) const (