From d7ff6227acc90cb9c90556b153392bfcaa15e1e1 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 22 Nov 2022 16:16:22 -0500 Subject: [PATCH] feat(CL): swaprouter types and barebones keeper (merge to `main` #2) --- app/apptesting/test_suite.go | 2 +- app/keepers/keepers.go | 16 ++ app/keepers/modules.go | 2 + x/gamm/keeper/multihop_test.go | 4 +- x/gamm/keeper/pool.go | 10 + x/gamm/keeper/pool_service.go | 2 +- x/gamm/keeper/swap.go | 40 ++- x/gamm/keeper/swap_test.go | 20 +- x/swaprouter/export_test.go | 9 + x/swaprouter/keeper.go | 110 +++++++++ x/swaprouter/keeper_test.go | 55 +++++ x/swaprouter/module/module.go | 135 +++++++++++ x/swaprouter/types/codec.go | 40 +++ x/swaprouter/types/constants.go | 6 + x/swaprouter/types/errors.go | 38 +++ x/swaprouter/types/events.go | 5 + x/swaprouter/types/expected_keepers.go | 14 ++ x/swaprouter/types/genesis.go | 18 ++ x/swaprouter/types/keys.go | 43 ++++ x/swaprouter/types/listeners.go | 21 ++ x/swaprouter/types/msg_create_pool.go | 23 ++ x/swaprouter/types/msgs.go | 89 +++++++ x/swaprouter/types/msgs_test.go | 322 +++++++++++++++++++++++++ x/swaprouter/types/params.go | 62 +++++ x/swaprouter/types/routes.go | 149 ++++++++++++ x/txfees/keeper/hooks.go | 2 +- x/txfees/types/expected_keepers.go | 4 +- 27 files changed, 1222 insertions(+), 19 deletions(-) create mode 100644 x/swaprouter/export_test.go create mode 100644 x/swaprouter/keeper.go create mode 100644 x/swaprouter/keeper_test.go create mode 100644 x/swaprouter/module/module.go create mode 100644 x/swaprouter/types/codec.go create mode 100644 x/swaprouter/types/constants.go create mode 100644 x/swaprouter/types/errors.go create mode 100644 x/swaprouter/types/events.go create mode 100644 x/swaprouter/types/expected_keepers.go create mode 100644 x/swaprouter/types/genesis.go create mode 100644 x/swaprouter/types/keys.go create mode 100644 x/swaprouter/types/listeners.go create mode 100644 x/swaprouter/types/msg_create_pool.go create mode 100644 x/swaprouter/types/msgs.go create mode 100644 x/swaprouter/types/msgs_test.go create mode 100644 x/swaprouter/types/params.go create mode 100644 x/swaprouter/types/routes.go diff --git a/app/apptesting/test_suite.go b/app/apptesting/test_suite.go index c52ebae6f13..90ed03c9844 100644 --- a/app/apptesting/test_suite.go +++ b/app/apptesting/test_suite.go @@ -313,7 +313,7 @@ func (s *KeeperTestHelper) SwapAndSetSpotPrice(poolId uint64, fromAsset sdk.Coin coins := sdk.Coins{sdk.NewInt64Coin(fromAsset.Denom, 100000000000000)} s.FundAcc(acc1, coins) - _, err := s.App.GAMMKeeper.SwapExactAmountOut( + _, err := s.App.GAMMKeeper.SwapExactAmountOutLegacy( s.Ctx, acc1, poolId, diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 973d4d84208..3ec4e7d4bd0 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -52,6 +52,8 @@ import ( // IBC Transfer: Defines the "transfer" IBC port transfer "github.com/cosmos/ibc-go/v3/modules/apps/transfer" + "github.com/osmosis-labs/osmosis/v13/x/swaprouter" + _ "github.com/osmosis-labs/osmosis/v13/client/docs/statik" owasm "github.com/osmosis-labs/osmosis/v13/wasmbinding" epochskeeper "github.com/osmosis-labs/osmosis/v13/x/epochs/keeper" @@ -70,6 +72,7 @@ import ( "github.com/osmosis-labs/osmosis/v13/x/superfluid" superfluidkeeper "github.com/osmosis-labs/osmosis/v13/x/superfluid/keeper" superfluidtypes "github.com/osmosis-labs/osmosis/v13/x/superfluid/types" + swaproutertypes "github.com/osmosis-labs/osmosis/v13/x/swaprouter/types" tokenfactorykeeper "github.com/osmosis-labs/osmosis/v13/x/tokenfactory/keeper" tokenfactorytypes "github.com/osmosis-labs/osmosis/v13/x/tokenfactory/types" "github.com/osmosis-labs/osmosis/v13/x/twap" @@ -119,6 +122,7 @@ type AppKeepers struct { WasmKeeper *wasm.Keeper ContractKeeper *wasmkeeper.PermissionedKeeper TokenFactoryKeeper *tokenfactorykeeper.Keeper + SwapRouterKeeper *swaprouter.Keeper ValidatorSetPreferenceKeeper *valsetpref.Keeper // IBC modules @@ -251,6 +255,16 @@ func (appKeepers *AppKeepers) InitNormalKeepers( appKeepers.GetSubspace(twaptypes.ModuleName), appKeepers.GAMMKeeper) + appKeepers.SwapRouterKeeper = swaprouter.NewKeeper( + appKeepers.keys[swaproutertypes.StoreKey], + appKeepers.GetSubspace(swaproutertypes.ModuleName), + appKeepers.GAMMKeeper, + nil, // TODO: set CL keeper once it is merged. + appKeepers.BankKeeper, + appKeepers.AccountKeeper, + appKeepers.DistrKeeper, + ) + appKeepers.LockupKeeper = lockupkeeper.NewKeeper( appKeepers.keys[lockuptypes.StoreKey], // TODO: Visit why this needs to be deref'd @@ -509,6 +523,7 @@ func (appKeepers *AppKeepers) initParamsKeeper(appCodec codec.BinaryCodec, legac paramsKeeper.Subspace(wasm.ModuleName) paramsKeeper.Subspace(tokenfactorytypes.ModuleName) paramsKeeper.Subspace(twaptypes.ModuleName) + paramsKeeper.Subspace(swaproutertypes.ModuleName) paramsKeeper.Subspace(ibcratelimittypes.ModuleName) return paramsKeeper @@ -594,6 +609,7 @@ func KVStoreKeys() []string { capabilitytypes.StoreKey, gammtypes.StoreKey, twaptypes.StoreKey, + swaproutertypes.StoreKey, lockuptypes.StoreKey, incentivestypes.StoreKey, epochstypes.StoreKey, diff --git a/app/keepers/modules.go b/app/keepers/modules.go index 88a16beabfe..83f8fe23132 100644 --- a/app/keepers/modules.go +++ b/app/keepers/modules.go @@ -39,6 +39,7 @@ import ( poolincentivesclient "github.com/osmosis-labs/osmosis/v13/x/pool-incentives/client" superfluid "github.com/osmosis-labs/osmosis/v13/x/superfluid" superfluidclient "github.com/osmosis-labs/osmosis/v13/x/superfluid/client" + swaproutermodule "github.com/osmosis-labs/osmosis/v13/x/swaprouter/module" "github.com/osmosis-labs/osmosis/v13/x/tokenfactory" "github.com/osmosis-labs/osmosis/v13/x/twap/twapmodule" "github.com/osmosis-labs/osmosis/v13/x/txfees" @@ -80,6 +81,7 @@ var AppModuleBasics = []module.AppModuleBasic{ vesting.AppModuleBasic{}, gamm.AppModuleBasic{}, twapmodule.AppModuleBasic{}, + swaproutermodule.AppModuleBasic{}, txfees.AppModuleBasic{}, incentives.AppModuleBasic{}, lockup.AppModuleBasic{}, diff --git a/x/gamm/keeper/multihop_test.go b/x/gamm/keeper/multihop_test.go index 8922ed848b0..f55276dd4c8 100644 --- a/x/gamm/keeper/multihop_test.go +++ b/x/gamm/keeper/multihop_test.go @@ -263,7 +263,7 @@ func (suite *KeeperTestSuite) TestBalancerPoolMultihopSwapExactAmountIn() { nextTokenIn := test.param.tokenIn // we then do individual swaps until we reach the end of the swap route for _, hop := range test.param.routes { - tokenOut, err := keeper.SwapExactAmountIn(cacheCtx, suite.TestAccs[0], hop.PoolId, nextTokenIn, hop.TokenOutDenom, sdk.OneInt()) + tokenOut, err := keeper.SwapExactAmountInLegacy(cacheCtx, suite.TestAccs[0], hop.PoolId, nextTokenIn, hop.TokenOutDenom, sdk.OneInt()) suite.Require().NoError(err) nextTokenIn = sdk.NewCoin(hop.TokenOutDenom, tokenOut) } @@ -533,7 +533,7 @@ func (suite *KeeperTestSuite) TestBalancerPoolMultihopSwapExactAmountOut() { // we then do individual swaps until we reach the end of the swap route for i := len(test.param.routes) - 1; i >= 0; i-- { hop := test.param.routes[i] - tokenOut, err := keeper.SwapExactAmountOut(cacheCtx, suite.TestAccs[0], hop.PoolId, hop.TokenInDenom, sdk.NewInt(100000000), nextTokenOut) + tokenOut, err := keeper.SwapExactAmountOutLegacy(cacheCtx, suite.TestAccs[0], hop.PoolId, hop.TokenInDenom, sdk.NewInt(100000000), nextTokenOut) suite.Require().NoError(err) nextTokenOut = sdk.NewCoin(hop.TokenInDenom, tokenOut) } diff --git a/x/gamm/keeper/pool.go b/x/gamm/keeper/pool.go index 940ef3e6bfc..5ed51397e65 100644 --- a/x/gamm/keeper/pool.go +++ b/x/gamm/keeper/pool.go @@ -14,6 +14,11 @@ import ( "github.com/osmosis-labs/osmosis/v13/x/gamm/types" ) +// TODO spec and tests +func (k Keeper) InitializePool(ctx sdk.Context, pool types.PoolI, creatorAddress sdk.AccAddress) error { + panic("not implemented") +} + func (k Keeper) MarshalPool(pool types.PoolI) ([]byte, error) { return k.cdc.MarshalInterface(pool) } @@ -23,6 +28,11 @@ func (k Keeper) UnmarshalPool(bz []byte) (types.PoolI, error) { return acc, k.cdc.UnmarshalInterface(bz, &acc) } +// GetPool returns a pool with a given id. +func (k Keeper) GetPool(ctx sdk.Context, poolId uint64) (types.PoolI, error) { + return k.getPoolForSwap(ctx, poolId) +} + // GetPoolAndPoke returns a PoolI based on it's identifier if one exists. If poolId corresponds // to a pool with weights (e.g. balancer), the weights of the pool are updated via PokePool prior to returning. // TODO: Consider rename to GetPool due to downstream API confusion. diff --git a/x/gamm/keeper/pool_service.go b/x/gamm/keeper/pool_service.go index 9be704b9ac8..5e573439d7e 100644 --- a/x/gamm/keeper/pool_service.go +++ b/x/gamm/keeper/pool_service.go @@ -430,7 +430,7 @@ func (k Keeper) ExitSwapShareAmountIn( if coin.Denom == tokenOutDenom { continue } - swapOut, err := k.SwapExactAmountIn(ctx, sender, poolId, coin, tokenOutDenom, sdk.ZeroInt()) + swapOut, err := k.SwapExactAmountInLegacy(ctx, sender, poolId, coin, tokenOutDenom, sdk.ZeroInt()) if err != nil { return sdk.Int{}, err } diff --git a/x/gamm/keeper/swap.go b/x/gamm/keeper/swap.go index 040cfe16723..5a966b897c3 100644 --- a/x/gamm/keeper/swap.go +++ b/x/gamm/keeper/swap.go @@ -11,12 +11,31 @@ import ( "github.com/osmosis-labs/osmosis/v13/x/gamm/types" ) -// SwapExactAmountIn attempts to swap one asset, tokenIn, for another asset +// swapExactAmountIn is an internal method for swapping an exact amount of tokens +// as input to a pool, using the provided swapFee. This is intended to allow +// different swap fees as determined by multi-hops, or when recovering from +// chain liveness failures. +// TODO: investigate if swapFee can be unexported +// https://github.com/osmosis-labs/osmosis/issues/3130 +func (k Keeper) SwapExactAmountIn( + ctx sdk.Context, + sender sdk.AccAddress, + pool types.PoolI, + tokenIn sdk.Coin, + tokenOutDenom string, + tokenOutMinAmount sdk.Int, + swapFee sdk.Dec, +) (tokenOutAmount sdk.Int, err error) { + panic("not implemented") +} + +// SwapExactAmountInLegacy attempts to swap one asset, tokenIn, for another asset // denominated via tokenOutDenom through a pool denoted by poolId specifying that // tokenOutMinAmount must be returned in the resulting asset returning an error // upon failure. Upon success, the resulting tokens swapped for are returned. A // swap fee is applied determined by the pool's parameters. -func (k Keeper) SwapExactAmountIn( +// TODO: to be removed in future merges. +func (k Keeper) SwapExactAmountInLegacy( ctx sdk.Context, sender sdk.AccAddress, poolId uint64, @@ -84,7 +103,24 @@ func (k Keeper) swapExactAmountIn( return tokenOutAmount, nil } +// SwapExactAmountOut is a method for swapping by providing an exact amount of tokens out. +// Returns the amount of tokenIn consumed to performed the swap. Otherwise, returns error. +// Consumes swapFee. This is intended to allow different swap fees as determined by multi-hops, +// or when recovering from chain liveness failures. func (k Keeper) SwapExactAmountOut( + ctx sdk.Context, + sender sdk.AccAddress, + pool types.PoolI, + tokenInDenom string, + tokenInMaxAmount sdk.Int, + tokenOut sdk.Coin, + swapFee sdk.Dec, +) (tokenInAmount sdk.Int, err error) { + panic("not implemented") +} + +// +func (k Keeper) SwapExactAmountOutLegacy( ctx sdk.Context, sender sdk.AccAddress, poolId uint64, diff --git a/x/gamm/keeper/swap_test.go b/x/gamm/keeper/swap_test.go index c6466dd6520..8ff4872a580 100644 --- a/x/gamm/keeper/swap_test.go +++ b/x/gamm/keeper/swap_test.go @@ -98,7 +98,7 @@ func (suite *KeeperTestSuite) TestBalancerPoolSimpleSwapExactAmountIn() { suite.NoError(err, "test: %v", test.name) prevGasConsumed := suite.Ctx.GasMeter().GasConsumed() - tokenOutAmount, err := keeper.SwapExactAmountIn(ctx, suite.TestAccs[0], poolId, test.param.tokenIn, test.param.tokenOutDenom, test.param.tokenOutMinAmount) + tokenOutAmount, err := keeper.SwapExactAmountInLegacy(ctx, suite.TestAccs[0], poolId, test.param.tokenIn, test.param.tokenOutDenom, test.param.tokenOutMinAmount) suite.NoError(err, "test: %v", test.name) suite.True(tokenOutAmount.Equal(test.param.expectedTokenOut), "test: %v", test.name) gasConsumedForSwap := suite.Ctx.GasMeter().GasConsumed() - prevGasConsumed @@ -114,7 +114,7 @@ func (suite *KeeperTestSuite) TestBalancerPoolSimpleSwapExactAmountIn() { tradeAvgPrice := test.param.tokenIn.Amount.ToDec().Quo(tokenOutAmount.ToDec()) suite.True(tradeAvgPrice.GT(spotPriceBefore) && tradeAvgPrice.LT(spotPriceAfter), "test: %v", test.name) } else { - _, err := keeper.SwapExactAmountIn(ctx, suite.TestAccs[0], poolId, test.param.tokenIn, test.param.tokenOutDenom, test.param.tokenOutMinAmount) + _, err := keeper.SwapExactAmountInLegacy(ctx, suite.TestAccs[0], poolId, test.param.tokenIn, test.param.tokenOutDenom, test.param.tokenOutMinAmount) suite.Error(err, "test: %v", test.name) } }) @@ -206,7 +206,7 @@ func (suite *KeeperTestSuite) TestBalancerPoolSimpleSwapExactAmountOut() { suite.NoError(err, "test: %v", test.name) prevGasConsumed := suite.Ctx.GasMeter().GasConsumed() - tokenInAmount, err := keeper.SwapExactAmountOut(ctx, suite.TestAccs[0], poolId, test.param.tokenInDenom, test.param.tokenInMaxAmount, test.param.tokenOut) + tokenInAmount, err := keeper.SwapExactAmountOutLegacy(ctx, suite.TestAccs[0], poolId, test.param.tokenInDenom, test.param.tokenInMaxAmount, test.param.tokenOut) suite.NoError(err, "test: %v", test.name) suite.True(tokenInAmount.Equal(test.param.expectedTokenInAmount), "test: %v\n expect_eq actual: %s, expected: %s", @@ -224,7 +224,7 @@ func (suite *KeeperTestSuite) TestBalancerPoolSimpleSwapExactAmountOut() { tradeAvgPrice := tokenInAmount.ToDec().Quo(test.param.tokenOut.Amount.ToDec()) suite.True(tradeAvgPrice.GT(spotPriceBefore) && tradeAvgPrice.LT(spotPriceAfter), "test: %v", test.name) } else { - _, err := keeper.SwapExactAmountOut(suite.Ctx, suite.TestAccs[0], poolId, test.param.tokenInDenom, test.param.tokenInMaxAmount, test.param.tokenOut) + _, err := keeper.SwapExactAmountOutLegacy(suite.Ctx, suite.TestAccs[0], poolId, test.param.tokenInDenom, test.param.tokenInMaxAmount, test.param.tokenOut) suite.Error(err, "test: %v", test.name) } }) @@ -259,14 +259,14 @@ func (suite *KeeperTestSuite) TestActiveBalancerPoolSwap() { foocoin := sdk.NewCoin("foo", sdk.NewInt(10)) if tc.expectPass { - _, err := suite.App.GAMMKeeper.SwapExactAmountIn(suite.Ctx, suite.TestAccs[0], poolId, foocoin, "bar", sdk.ZeroInt()) + _, err := suite.App.GAMMKeeper.SwapExactAmountInLegacy(suite.Ctx, suite.TestAccs[0], poolId, foocoin, "bar", sdk.ZeroInt()) suite.Require().NoError(err) - _, err = suite.App.GAMMKeeper.SwapExactAmountOut(suite.Ctx, suite.TestAccs[0], poolId, "bar", sdk.NewInt(1000000000000000000), foocoin) + _, err = suite.App.GAMMKeeper.SwapExactAmountOutLegacy(suite.Ctx, suite.TestAccs[0], poolId, "bar", sdk.NewInt(1000000000000000000), foocoin) suite.Require().NoError(err) } else { - _, err := suite.App.GAMMKeeper.SwapExactAmountIn(suite.Ctx, suite.TestAccs[0], poolId, foocoin, "bar", sdk.ZeroInt()) + _, err := suite.App.GAMMKeeper.SwapExactAmountInLegacy(suite.Ctx, suite.TestAccs[0], poolId, foocoin, "bar", sdk.ZeroInt()) suite.Require().Error(err) - _, err = suite.App.GAMMKeeper.SwapExactAmountOut(suite.Ctx, suite.TestAccs[0], poolId, "bar", sdk.NewInt(1000000000000000000), foocoin) + _, err = suite.App.GAMMKeeper.SwapExactAmountOutLegacy(suite.Ctx, suite.TestAccs[0], poolId, "bar", sdk.NewInt(1000000000000000000), foocoin) suite.Require().Error(err) } } @@ -313,8 +313,8 @@ func (suite *KeeperTestSuite) TestInactivePoolFreezeSwaps() { for _, test := range testCases { suite.Run(test.name, func() { // Check swaps - _, swapInErr := gammKeeper.SwapExactAmountIn(suite.Ctx, suite.TestAccs[0], test.poolId, testCoin, "bar", sdk.ZeroInt()) - _, swapOutErr := gammKeeper.SwapExactAmountOut(suite.Ctx, suite.TestAccs[0], test.poolId, "bar", sdk.NewInt(1000000000000000000), testCoin) + _, swapInErr := gammKeeper.SwapExactAmountInLegacy(suite.Ctx, suite.TestAccs[0], test.poolId, testCoin, "bar", sdk.ZeroInt()) + _, swapOutErr := gammKeeper.SwapExactAmountOutLegacy(suite.Ctx, suite.TestAccs[0], test.poolId, "bar", sdk.NewInt(1000000000000000000), testCoin) if test.expectPass { suite.Require().NoError(swapInErr) suite.Require().NoError(swapOutErr) diff --git a/x/swaprouter/export_test.go b/x/swaprouter/export_test.go new file mode 100644 index 00000000000..9bb7fcf062b --- /dev/null +++ b/x/swaprouter/export_test.go @@ -0,0 +1,9 @@ +package swaprouter + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (k Keeper) GetNextPoolIdAndIncrement(ctx sdk.Context) uint64 { + return k.getNextPoolIdAndIncrement(ctx) +} diff --git a/x/swaprouter/keeper.go b/x/swaprouter/keeper.go new file mode 100644 index 00000000000..0b0d88ba858 --- /dev/null +++ b/x/swaprouter/keeper.go @@ -0,0 +1,110 @@ +package swaprouter + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + gogotypes "github.com/gogo/protobuf/types" + + "github.com/osmosis-labs/osmosis/v13/osmoutils" + "github.com/osmosis-labs/osmosis/v13/x/swaprouter/types" + + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" +) + +type Keeper struct { + storeKey sdk.StoreKey + + gammKeeper types.SwapI + concentratedKeeper types.SwapI + poolIncentivesKeeper types.PoolIncentivesKeeperI + bankKeeper types.BankI + accountKeeper types.AccountI + communityPoolKeeper types.CommunityPoolI + + poolCreationListeners types.PoolCreationListeners + + routes map[types.PoolType]types.SwapI + + paramSpace paramtypes.Subspace +} + +func NewKeeper(storeKey sdk.StoreKey, paramSpace paramtypes.Subspace, gammKeeper types.SwapI, concentratedKeeper types.SwapI, bankKeeper types.BankI, accountKeeper types.AccountI, communityPoolKeeper types.CommunityPoolI) *Keeper { + // set KeyTable if it has not already been set + if !paramSpace.HasKeyTable() { + paramSpace = paramSpace.WithKeyTable(types.ParamKeyTable()) + } + + routes := map[types.PoolType]types.SwapI{ + types.Balancer: gammKeeper, + types.StableSwap: gammKeeper, + types.Concentrated: concentratedKeeper, + } + + return &Keeper{storeKey: storeKey, paramSpace: paramSpace, gammKeeper: gammKeeper, concentratedKeeper: concentratedKeeper, bankKeeper: bankKeeper, accountKeeper: accountKeeper, communityPoolKeeper: communityPoolKeeper, routes: routes} +} + +// GetParams returns the total set of swaprouter parameters. +func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) { + k.paramSpace.GetParamSet(ctx, ¶ms) + return params +} + +// SetParams sets the total set of swaprouter parameters. +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { + k.paramSpace.SetParamSet(ctx, ¶ms) +} + +// InitGenesis initializes the swaprouter module's state from a provided genesis +// state. +func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) { + k.SetNextPoolId(ctx, genState.NextPoolId) + if err := genState.Validate(); err != nil { + panic(err) + } + + k.SetParams(ctx, genState.Params) +} + +// ExportGenesis returns the swaprouter module's exported genesis. +func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { + return &types.GenesisState{ + Params: k.GetParams(ctx), + NextPoolId: k.GetNextPoolId(ctx), + } +} + +// GetNextPoolId returns the next pool id. +func (k Keeper) GetNextPoolId(ctx sdk.Context) uint64 { + store := ctx.KVStore(k.storeKey) + nextPoolId := gogotypes.UInt64Value{} + osmoutils.MustGet(store, types.KeyNextGlobalPoolId, &nextPoolId) + return nextPoolId.Value +} + +// SetPoolCreationListeners sets the pool creation listeners. +func (k *Keeper) SetPoolCreationListeners(listeners types.PoolCreationListeners) *Keeper { + if k.poolCreationListeners != nil { + panic("cannot set pool creation listeners twice") + } + + k.poolCreationListeners = listeners + + return k +} + +// SetNextPoolId sets next pool Id. +func (k Keeper) SetNextPoolId(ctx sdk.Context, poolId uint64) { + store := ctx.KVStore(k.storeKey) + osmoutils.MustSet(store, types.KeyNextGlobalPoolId, &gogotypes.UInt64Value{Value: poolId}) +} + +// SetPoolIncentivesKeeper sets pool incentives keeper +func (k *Keeper) SetPoolIncentivesKeeper(poolIncentivesKeeper types.PoolIncentivesKeeperI) { + k.poolIncentivesKeeper = poolIncentivesKeeper +} + +// getNextPoolIdAndIncrement returns the next pool Id, and increments the corresponding state entry. +func (k Keeper) getNextPoolIdAndIncrement(ctx sdk.Context) uint64 { + nextPoolId := k.GetNextPoolId(ctx) + k.SetNextPoolId(ctx, nextPoolId+1) + return nextPoolId +} diff --git a/x/swaprouter/keeper_test.go b/x/swaprouter/keeper_test.go new file mode 100644 index 00000000000..9b8e63f6185 --- /dev/null +++ b/x/swaprouter/keeper_test.go @@ -0,0 +1,55 @@ +package swaprouter_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/osmosis-labs/osmosis/v13/app/apptesting" + "github.com/osmosis-labs/osmosis/v13/x/swaprouter/types" + "github.com/stretchr/testify/suite" +) + +type KeeperTestSuite struct { + apptesting.KeeperTestHelper +} + +const testExpectedPoolId = 3 + +var testPoolCreationFee = sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000_000_000)} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} + +func (suite *KeeperTestSuite) SetupTest() { + suite.Setup() +} + +func (suite *KeeperTestSuite) TestInitGenesis() { + suite.Setup() + + suite.App.SwapRouterKeeper.InitGenesis(suite.Ctx, &types.GenesisState{ + Params: types.Params{ + PoolCreationFee: testPoolCreationFee, + }, + NextPoolId: testExpectedPoolId, + }) + + suite.Require().Equal(uint64(testExpectedPoolId), suite.App.SwapRouterKeeper.GetNextPoolIdAndIncrement(suite.Ctx)) + suite.Require().Equal(testPoolCreationFee, suite.App.SwapRouterKeeper.GetParams(suite.Ctx).PoolCreationFee) +} + +func (suite *KeeperTestSuite) TestExportGenesis() { + suite.Setup() + + suite.App.SwapRouterKeeper.InitGenesis(suite.Ctx, &types.GenesisState{ + Params: types.Params{ + PoolCreationFee: testPoolCreationFee, + }, + NextPoolId: testExpectedPoolId, + }) + + genesis := suite.App.SwapRouterKeeper.ExportGenesis(suite.Ctx) + suite.Require().Equal(uint64(testExpectedPoolId), genesis.NextPoolId) + suite.Require().Equal(testPoolCreationFee, genesis.Params.PoolCreationFee) +} diff --git a/x/swaprouter/module/module.go b/x/swaprouter/module/module.go new file mode 100644 index 00000000000..18379394d06 --- /dev/null +++ b/x/swaprouter/module/module.go @@ -0,0 +1,135 @@ +package module + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/osmosis-labs/osmosis/v13/x/swaprouter" + "github.com/osmosis-labs/osmosis/v13/x/swaprouter/client/queryproto" + "github.com/osmosis-labs/osmosis/v13/x/swaprouter/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +type AppModuleBasic struct{} + +func (AppModuleBasic) Name() string { return types.ModuleName } + +func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { + types.RegisterLegacyAminoCodec(cdc) +} + +func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + return cdc.MustMarshalJSON(types.DefaultGenesis()) +} + +// ValidateGenesis performs genesis state validation for the swaprouter module. +func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error { + var genState types.GenesisState + if err := cdc.UnmarshalJSON(bz, &genState); err != nil { + return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err) + } + return genState.Validate() +} + +// --------------------------------------- +// Interfaces. +func (b AppModuleBasic) RegisterRESTRoutes(ctx client.Context, r *mux.Router) { +} + +func (b AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { + if err := queryproto.RegisterQueryHandlerClient(context.Background(), mux, queryproto.NewQueryClient(clientCtx)); err != nil { + panic(err) + } +} + +func (b AppModuleBasic) GetTxCmd() *cobra.Command { + return nil +} + +func (b AppModuleBasic) GetQueryCmd() *cobra.Command { + return nil +} + +// RegisterInterfaces registers interfaces and implementations of the gamm module. +func (AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) { + types.RegisterInterfaces(registry) +} + +type AppModule struct { + AppModuleBasic + + k swaprouter.Keeper + gammKeeper types.GammKeeper +} + +func (am AppModule) RegisterServices(cfg module.Configurator) { +} + +func NewAppModule(swaprouterKeeper swaprouter.Keeper, gammKeeper types.GammKeeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + k: swaprouterKeeper, + gammKeeper: gammKeeper, + } +} + +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { +} + +func (am AppModule) Route() sdk.Route { + return sdk.Route{} +} + +// QuerierRoute returns the gamm module's querier route name. +func (AppModule) QuerierRoute() string { return types.RouterKey } + +// LegacyQuerierHandler returns the x/gamm module's sdk.Querier. +func (am AppModule) LegacyQuerierHandler(legacyQuerierCdc *codec.LegacyAmino) sdk.Querier { + return func(sdk.Context, []string, abci.RequestQuery) ([]byte, error) { + return nil, fmt.Errorf("legacy querier not supported for the x/%s module", types.ModuleName) + } +} + +// InitGenesis performs genesis initialization for the swaprouter module. +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, gs json.RawMessage) []abci.ValidatorUpdate { + var genesisState types.GenesisState + + cdc.MustUnmarshalJSON(gs, &genesisState) + + am.k.InitGenesis(ctx, &genesisState) + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the exported genesis state as raw bytes for the swaprouter. +// module. +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + genState := am.k.ExportGenesis(ctx) + return cdc.MustMarshalJSON(genState) +} + +// BeginBlock performs a no-op. +func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +// EndBlock performs a no-op. +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +// ConsensusVersion implements AppModule/ConsensusVersion. +func (AppModule) ConsensusVersion() uint64 { return 1 } diff --git a/x/swaprouter/types/codec.go b/x/swaprouter/types/codec.go new file mode 100644 index 00000000000..27004edff83 --- /dev/null +++ b/x/swaprouter/types/codec.go @@ -0,0 +1,40 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/msgservice" + authzcodec "github.com/cosmos/cosmos-sdk/x/authz/codec" +) + +// RegisterLegacyAminoCodec registers the necessary x/swaprouter interfaces and concrete types +// on the provided LegacyAmino codec. These types are used for Amino JSON serialization. +func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { + cdc.RegisterConcrete(&MsgSwapExactAmountIn{}, "osmosis/swaprouter/swap-exact-amount-in", nil) + cdc.RegisterConcrete(&MsgSwapExactAmountOut{}, "osmosis/swaprouter/swap-exact-amount-out", nil) +} + +func RegisterInterfaces(registry types.InterfaceRegistry) { + registry.RegisterImplementations( + (*sdk.Msg)(nil), + &MsgSwapExactAmountIn{}, + &MsgSwapExactAmountOut{}, + ) + msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) +} + +var ( + amino = codec.NewLegacyAmino() + ModuleCdc = codec.NewAminoCodec(amino) +) + +func init() { + RegisterLegacyAminoCodec(amino) + // Register all Amino interfaces and concrete types on the authz Amino codec so that this can later be + // used to properly serialize MsgGrant and MsgExec instances + sdk.RegisterLegacyAminoCodec(amino) + RegisterLegacyAminoCodec(authzcodec.Amino) + + amino.Seal() +} diff --git a/x/swaprouter/types/constants.go b/x/swaprouter/types/constants.go new file mode 100644 index 00000000000..7a99299341b --- /dev/null +++ b/x/swaprouter/types/constants.go @@ -0,0 +1,6 @@ +package types + +const ( + MinPoolAssets = 2 + MaxPoolAssets = 8 +) diff --git a/x/swaprouter/types/errors.go b/x/swaprouter/types/errors.go new file mode 100644 index 00000000000..888b40334a6 --- /dev/null +++ b/x/swaprouter/types/errors.go @@ -0,0 +1,38 @@ +package types + +import ( + "errors" + "fmt" +) + +var ( + ErrEmptyRoutes = errors.New("provided empty routes") + ErrInvalidPool = errors.New("attempting to create an invalid pool") + ErrTooFewPoolAssets = errors.New("pool should have at least 2 assets, as they must be swapping between at least two assets") + ErrTooManyPoolAssets = errors.New("pool has too many assets (currently capped at 8 assets per pool)") +) + +type nonPositiveAmountError struct { + Amount string +} + +func (e nonPositiveAmountError) Error() string { + return fmt.Sprintf("min out amount or max in amount should be positive, was (%s)", e.Amount) +} + +type FailedToFindRouteError struct { + PoolId uint64 +} + +func (e FailedToFindRouteError) Error() string { + return fmt.Sprintf("failed to find route for pool id (%d)", e.PoolId) +} + +type UndefinedRouteError struct { + PoolType PoolType + PoolId uint64 +} + +func (e UndefinedRouteError) Error() string { + return fmt.Sprintf("route is not defined for the given pool type (%s) and pool id (%d)", e.PoolType, e.PoolId) +} diff --git a/x/swaprouter/types/events.go b/x/swaprouter/types/events.go new file mode 100644 index 00000000000..885a885a754 --- /dev/null +++ b/x/swaprouter/types/events.go @@ -0,0 +1,5 @@ +package types + +const ( + AttributeValueCategory = ModuleName +) diff --git a/x/swaprouter/types/expected_keepers.go b/x/swaprouter/types/expected_keepers.go new file mode 100644 index 00000000000..c8211f6970a --- /dev/null +++ b/x/swaprouter/types/expected_keepers.go @@ -0,0 +1,14 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + gammtypes "github.com/osmosis-labs/osmosis/v13/x/gamm/types" +) + +// GammKeeper defines the expected interface needed for swaprouter module +type GammKeeper interface { + GetPoolAndPoke(ctx sdk.Context, poolId uint64) (gammtypes.PoolI, error) + + GetNextPoolId(ctx sdk.Context) uint64 +} diff --git a/x/swaprouter/types/genesis.go b/x/swaprouter/types/genesis.go new file mode 100644 index 00000000000..cce472a4cdb --- /dev/null +++ b/x/swaprouter/types/genesis.go @@ -0,0 +1,18 @@ +package types + +// DefaultGenesis returns the default swaprouter genesis state. +func DefaultGenesis() *GenesisState { + return &GenesisState{ + Params: DefaultParams(), + NextPoolId: 1, + } +} + +// Validate performs basic genesis state validation returning an error upon any +// failure. +func (gs GenesisState) Validate() error { + if err := gs.Params.Validate(); err != nil { + return err + } + return nil +} diff --git a/x/swaprouter/types/keys.go b/x/swaprouter/types/keys.go new file mode 100644 index 00000000000..b7aea814801 --- /dev/null +++ b/x/swaprouter/types/keys.go @@ -0,0 +1,43 @@ +package types + +import ( + "errors" + "fmt" + + "github.com/gogo/protobuf/proto" +) + +const ( + ModuleName = "swaprouter" + + StoreKey = ModuleName + + RouterKey = ModuleName +) + +var ( + // KeyNextGlobalPoolId defines key to store the next Pool ID to be used. + KeyNextGlobalPoolId = []byte{0x01} + + // SwapModuleRouterPrefix defines prefix to store pool id to swap module mappings. + SwapModuleRouterPrefix = []byte{0x02} +) + +// ModuleRouteToBytes serializes moduleRoute to bytes. +func FormatModuleRouteKey(poolId uint64) []byte { + return []byte(fmt.Sprintf("%s%d", SwapModuleRouterPrefix, poolId)) +} + +// ParseModuleRouteFromBz parses the raw bytes into ModuleRoute. +// Returns error if fails to parse or if the bytes are empty. +func ParseModuleRouteFromBz(bz []byte) (ModuleRoute, error) { + if len(bz) == 0 { + return ModuleRoute{}, errors.New("module route not found") + } + moduleRoute := ModuleRoute{} + err := proto.Unmarshal(bz, &moduleRoute) + if err != nil { + return ModuleRoute{}, err + } + return moduleRoute, err +} diff --git a/x/swaprouter/types/listeners.go b/x/swaprouter/types/listeners.go new file mode 100644 index 00000000000..8e3df6e040b --- /dev/null +++ b/x/swaprouter/types/listeners.go @@ -0,0 +1,21 @@ +package types + +import sdk "github.com/cosmos/cosmos-sdk/types" + +type PoolCreationListener interface { + // AfterPoolCreated is called after CreatePool + AfterPoolCreated(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) +} + +type PoolCreationListeners []PoolCreationListener + +func (l PoolCreationListeners) AfterPoolCreated(ctx sdk.Context, sender sdk.AccAddress, poolId uint64) { + for i := range l { + l[i].AfterPoolCreated(ctx, sender, poolId) + } +} + +// Creates hooks for the Gamm Module. +func NewPoolCreationListeners(listeners ...PoolCreationListener) PoolCreationListeners { + return listeners +} diff --git a/x/swaprouter/types/msg_create_pool.go b/x/swaprouter/types/msg_create_pool.go new file mode 100644 index 00000000000..4e320c18b9c --- /dev/null +++ b/x/swaprouter/types/msg_create_pool.go @@ -0,0 +1,23 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + gammtypes "github.com/osmosis-labs/osmosis/v13/x/gamm/types" +) + +// CreatePoolMsg defines an interface that every CreatePool transaction should implement. +// The gamm logic will use this to create a pool. +type CreatePoolMsg interface { + // GetPoolType returns the type of the pool to create. + GetPoolType() PoolType + // The creator of the pool, who pays the PoolCreationFee, provides initial liquidity, + // and gets the initial LP shares. + PoolCreator() sdk.AccAddress + // A stateful validation function. + Validate(ctx sdk.Context) error + // Initial Liquidity for the pool that the sender is required to send to the pool account + InitialLiquidity() sdk.Coins + // CreatePool creates a pool implementing PoolI, using data from the message. + CreatePool(ctx sdk.Context, poolID uint64) (gammtypes.PoolI, error) +} diff --git a/x/swaprouter/types/msgs.go b/x/swaprouter/types/msgs.go new file mode 100644 index 00000000000..c41a69c90e4 --- /dev/null +++ b/x/swaprouter/types/msgs.go @@ -0,0 +1,89 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// constants. +const ( + TypeMsgSwapExactAmountIn = "swap_exact_amount_in" + TypeMsgSwapExactAmountOut = "swap_exact_amount_out" +) + +var _ sdk.Msg = &MsgSwapExactAmountIn{} + +func (msg MsgSwapExactAmountIn) Route() string { return RouterKey } +func (msg MsgSwapExactAmountIn) Type() string { return TypeMsgSwapExactAmountIn } +func (msg MsgSwapExactAmountIn) ValidateBasic() error { + _, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid sender address (%s)", err) + } + + err = SwapAmountInRoutes(msg.Routes).Validate() + if err != nil { + return err + } + + if !msg.TokenIn.IsValid() || !msg.TokenIn.IsPositive() { + // TODO: remove sdk errors + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, msg.TokenIn.String()) + } + + if !msg.TokenOutMinAmount.IsPositive() { + return nonPositiveAmountError{msg.TokenOutMinAmount.String()} + } + + return nil +} + +func (msg MsgSwapExactAmountIn) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgSwapExactAmountIn) GetSigners() []sdk.AccAddress { + sender, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + panic(err) + } + return []sdk.AccAddress{sender} +} + +var _ sdk.Msg = &MsgSwapExactAmountOut{} + +func (msg MsgSwapExactAmountOut) Route() string { return RouterKey } +func (msg MsgSwapExactAmountOut) Type() string { return TypeMsgSwapExactAmountOut } +func (msg MsgSwapExactAmountOut) ValidateBasic() error { + _, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid sender address (%s)", err) + } + + err = SwapAmountOutRoutes(msg.Routes).Validate() + if err != nil { + return err + } + + if !msg.TokenOut.IsValid() || !msg.TokenOut.IsPositive() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, msg.TokenOut.String()) + } + + if !msg.TokenInMaxAmount.IsPositive() { + return nonPositiveAmountError{msg.TokenInMaxAmount.String()} + } + + return nil +} + +func (msg MsgSwapExactAmountOut) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgSwapExactAmountOut) GetSigners() []sdk.AccAddress { + sender, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + panic(err) + } + return []sdk.AccAddress{sender} +} diff --git a/x/swaprouter/types/msgs_test.go b/x/swaprouter/types/msgs_test.go new file mode 100644 index 00000000000..57871241b9c --- /dev/null +++ b/x/swaprouter/types/msgs_test.go @@ -0,0 +1,322 @@ +package types_test + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/osmosis-labs/osmosis/v13/x/swaprouter/types" + + "github.com/osmosis-labs/osmosis/v13/app/apptesting" + appParams "github.com/osmosis-labs/osmosis/v13/app/params" +) + +func TestMsgSwapExactAmountIn(t *testing.T) { + appParams.SetAddressPrefixes() + pk1 := ed25519.GenPrivKey().PubKey() + addr1 := sdk.AccAddress(pk1.Address()).String() + invalidAddr := sdk.AccAddress("invalid") + + createMsg := func(after func(msg types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn { + properMsg := types.MsgSwapExactAmountIn{ + Sender: addr1, + Routes: []types.SwapAmountInRoute{{ + PoolId: 0, + TokenOutDenom: "test", + }, { + PoolId: 1, + TokenOutDenom: "test2", + }}, + TokenIn: sdk.NewCoin("test", sdk.NewInt(100)), + TokenOutMinAmount: sdk.NewInt(200), + } + + return after(properMsg) + } + + msg := createMsg(func(msg types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn { + // Do nothing + return msg + }) + + require.Equal(t, msg.Route(), types.RouterKey) + require.Equal(t, msg.Type(), "swap_exact_amount_in") + signers := msg.GetSigners() + require.Equal(t, len(signers), 1) + require.Equal(t, signers[0].String(), addr1) + + tests := []struct { + name string + msg types.MsgSwapExactAmountIn + expectPass bool + }{ + { + name: "proper msg", + msg: createMsg(func(msg types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn { + // Do nothing + return msg + }), + expectPass: true, + }, + { + name: "invalid sender", + msg: createMsg(func(msg types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn { + msg.Sender = invalidAddr.String() + return msg + }), + expectPass: false, + }, + { + name: "empty routes", + msg: createMsg(func(msg types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn { + msg.Routes = nil + return msg + }), + expectPass: false, + }, + { + name: "empty routes2", + msg: createMsg(func(msg types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn { + msg.Routes = []types.SwapAmountInRoute{} + return msg + }), + expectPass: false, + }, + { + name: "invalid denom", + msg: createMsg(func(msg types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn { + msg.Routes[1].TokenOutDenom = "1" + return msg + }), + expectPass: false, + }, + { + name: "invalid denom2", + msg: createMsg(func(msg types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn { + msg.TokenIn.Denom = "1" + return msg + }), + expectPass: false, + }, + { + name: "zero amount token", + msg: createMsg(func(msg types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn { + msg.TokenIn.Amount = sdk.NewInt(0) + return msg + }), + expectPass: false, + }, + { + name: "negative amount token", + msg: createMsg(func(msg types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn { + msg.TokenIn.Amount = sdk.NewInt(-10) + return msg + }), + expectPass: false, + }, + { + name: "zero amount criteria", + msg: createMsg(func(msg types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn { + msg.TokenOutMinAmount = sdk.NewInt(0) + return msg + }), + expectPass: false, + }, + { + name: "negative amount criteria", + msg: createMsg(func(msg types.MsgSwapExactAmountIn) types.MsgSwapExactAmountIn { + msg.TokenOutMinAmount = sdk.NewInt(-10) + return msg + }), + expectPass: false, + }, + } + + for _, test := range tests { + if test.expectPass { + require.NoError(t, test.msg.ValidateBasic(), "test: %v", test.name) + } else { + require.Error(t, test.msg.ValidateBasic(), "test: %v", test.name) + } + } +} + +func TestMsgSwapExactAmountOut(t *testing.T) { + appParams.SetAddressPrefixes() + pk1 := ed25519.GenPrivKey().PubKey() + addr1 := sdk.AccAddress(pk1.Address()).String() + invalidAddr := sdk.AccAddress("invalid") + + createMsg := func(after func(msg types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut { + properMsg := types.MsgSwapExactAmountOut{ + Sender: addr1, + Routes: []types.SwapAmountOutRoute{{ + PoolId: 0, + TokenInDenom: "test", + }, { + PoolId: 1, + TokenInDenom: "test2", + }}, + TokenOut: sdk.NewCoin("test", sdk.NewInt(100)), + TokenInMaxAmount: sdk.NewInt(200), + } + + return after(properMsg) + } + + msg := createMsg(func(msg types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut { + // Do nothing + return msg + }) + + require.Equal(t, msg.Route(), types.RouterKey) + require.Equal(t, msg.Type(), "swap_exact_amount_out") + signers := msg.GetSigners() + require.Equal(t, len(signers), 1) + require.Equal(t, signers[0].String(), addr1) + + tests := []struct { + name string + msg types.MsgSwapExactAmountOut + expectPass bool + }{ + { + name: "proper msg", + msg: createMsg(func(msg types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut { + // Do nothing + return msg + }), + expectPass: true, + }, + { + name: "invalid sender", + msg: createMsg(func(msg types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut { + msg.Sender = invalidAddr.String() + return msg + }), + expectPass: false, + }, + { + name: "empty routes", + msg: createMsg(func(msg types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut { + msg.Routes = nil + return msg + }), + expectPass: false, + }, + { + name: "empty routes2", + msg: createMsg(func(msg types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut { + msg.Routes = []types.SwapAmountOutRoute{} + return msg + }), + expectPass: false, + }, + { + name: "invalid denom", + msg: createMsg(func(msg types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut { + msg.Routes[1].TokenInDenom = "1" + return msg + }), + expectPass: false, + }, + { + name: "invalid denom", + msg: createMsg(func(msg types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut { + msg.TokenOut.Denom = "1" + return msg + }), + expectPass: false, + }, + { + name: "zero amount token", + msg: createMsg(func(msg types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut { + msg.TokenOut.Amount = sdk.NewInt(0) + return msg + }), + expectPass: false, + }, + { + name: "negative amount token", + msg: createMsg(func(msg types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut { + msg.TokenOut.Amount = sdk.NewInt(-10) + return msg + }), + expectPass: false, + }, + { + name: "zero amount criteria", + msg: createMsg(func(msg types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut { + msg.TokenInMaxAmount = sdk.NewInt(0) + return msg + }), + expectPass: false, + }, + { + name: "negative amount criteria", + msg: createMsg(func(msg types.MsgSwapExactAmountOut) types.MsgSwapExactAmountOut { + msg.TokenInMaxAmount = sdk.NewInt(-10) + return msg + }), + expectPass: false, + }, + } + + for _, test := range tests { + if test.expectPass { + require.NoError(t, test.msg.ValidateBasic(), "test: %v", test.name) + } else { + require.Error(t, test.msg.ValidateBasic(), "test: %v", test.name) + } + } +} + +// Test authz serialize and de-serializes for swaprouter msg. +func TestAuthzMsg(t *testing.T) { + pk1 := ed25519.GenPrivKey().PubKey() + addr1 := sdk.AccAddress(pk1.Address()).String() + coin := sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1)) + + testCases := []struct { + name string + gammMsg sdk.Msg + }{ + { + name: "MsgJoinSwapShareAmountOut", + gammMsg: &types.MsgSwapExactAmountIn{ + Sender: addr1, + Routes: []types.SwapAmountInRoute{{ + PoolId: 0, + TokenOutDenom: "test", + }, { + PoolId: 1, + TokenOutDenom: "test2", + }}, + TokenIn: coin, + TokenOutMinAmount: sdk.NewInt(1), + }, + }, + { + name: "MsgSwapExactAmountOut", + gammMsg: &types.MsgSwapExactAmountOut{ + Sender: addr1, + Routes: []types.SwapAmountOutRoute{{ + PoolId: 0, + TokenInDenom: "test", + }, { + PoolId: 1, + TokenInDenom: "test2", + }}, + TokenOut: coin, + TokenInMaxAmount: sdk.NewInt(1), + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + apptesting.TestMessageAuthzSerialization(t, tc.gammMsg) + }) + } +} diff --git a/x/swaprouter/types/params.go b/x/swaprouter/types/params.go new file mode 100644 index 00000000000..f648ef0d738 --- /dev/null +++ b/x/swaprouter/types/params.go @@ -0,0 +1,62 @@ +package types + +import ( + "fmt" + + appparams "github.com/osmosis-labs/osmosis/v13/app/params" + + sdk "github.com/cosmos/cosmos-sdk/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" +) + +// Parameter store keys. +var ( + KeyPoolCreationFee = []byte("PoolCreationFee") +) + +// ParamTable for gamm module. +func ParamKeyTable() paramtypes.KeyTable { + return paramtypes.NewKeyTable().RegisterParamSet(&Params{}) +} + +func NewParams(poolCreationFee sdk.Coins) Params { + return Params{ + PoolCreationFee: poolCreationFee, + } +} + +// DefaultParams are the default swaprouter module parameters. +func DefaultParams() Params { + return Params{ + PoolCreationFee: sdk.Coins{sdk.NewInt64Coin(appparams.BaseCoinUnit, 1000_000_000)}, // 1000 OSMO + } +} + +// validate params. +func (p Params) Validate() error { + if err := validatePoolCreationFee(p.PoolCreationFee); err != nil { + return err + } + + return nil +} + +// Implements params.ParamSet. +func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { + return paramtypes.ParamSetPairs{ + paramtypes.NewParamSetPair(KeyPoolCreationFee, &p.PoolCreationFee, validatePoolCreationFee), + } +} + +func validatePoolCreationFee(i interface{}) error { + v, ok := i.(sdk.Coins) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if v.Validate() != nil { + return fmt.Errorf("invalid pool creation fee: %+v", i) + } + + return nil +} diff --git a/x/swaprouter/types/routes.go b/x/swaprouter/types/routes.go new file mode 100644 index 00000000000..97c872f0789 --- /dev/null +++ b/x/swaprouter/types/routes.go @@ -0,0 +1,149 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + gammtypes "github.com/osmosis-labs/osmosis/v13/x/gamm/types" +) + +// AccountKeeper defines the account contract that must be fulfilled when +// creating a x/gamm keeper. +type AccountI interface { + NewAccount(sdk.Context, authtypes.AccountI) authtypes.AccountI + GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI + SetAccount(ctx sdk.Context, acc authtypes.AccountI) +} + +// BankKeeper defines the banking contract that must be fulfilled when +// creating a x/gamm keeper. +type BankI interface { + SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error + SetDenomMetaData(ctx sdk.Context, denomMetaData banktypes.Metadata) +} + +// CommunityPoolKeeper defines the contract needed to be fulfilled for distribution keeper. +type CommunityPoolI interface { + FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error +} + +// TODO: godoc +type SwapI interface { + InitializePool(ctx sdk.Context, pool gammtypes.PoolI, creatorAddress sdk.AccAddress) error + + GetPool(ctx sdk.Context, poolId uint64) (gammtypes.PoolI, error) + + SwapExactAmountIn( + ctx sdk.Context, + sender sdk.AccAddress, + pool gammtypes.PoolI, + tokenIn sdk.Coin, + tokenOutDenom string, + tokenOutMinAmount sdk.Int, + swapFee sdk.Dec, + ) (sdk.Int, error) + + SwapExactAmountOut( + ctx sdk.Context, + sender sdk.AccAddress, + pool gammtypes.PoolI, + tokenInDenom string, + tokenInMaxAmount sdk.Int, + tokenOut sdk.Coin, + swapFee sdk.Dec, + ) (tokenInAmount sdk.Int, err error) +} + +type PoolIncentivesKeeperI interface { + IsPoolIncentivized(ctx sdk.Context, poolId uint64) bool +} + +type MultihopRoute interface { + Length() int + PoolIds() []uint64 + IntermediateDenoms() []string +} + +type SwapAmountInRoutes []SwapAmountInRoute + +func (routes SwapAmountInRoutes) Validate() error { + if len(routes) == 0 { + return ErrEmptyRoutes + } + + for _, route := range routes { + err := sdk.ValidateDenom(route.TokenOutDenom) + if err != nil { + return err + } + } + + return nil +} + +func (routes SwapAmountInRoutes) IntermediateDenoms() []string { + if len(routes) < 2 { + return nil + } + intermediateDenoms := make([]string, 0, len(routes)-1) + for _, route := range routes[:len(routes)-1] { + intermediateDenoms = append(intermediateDenoms, route.TokenOutDenom) + } + + return intermediateDenoms +} + +func (routes SwapAmountInRoutes) PoolIds() []uint64 { + poolIds := make([]uint64, 0, len(routes)) + for _, route := range routes { + poolIds = append(poolIds, route.PoolId) + } + return poolIds +} + +func (routes SwapAmountInRoutes) Length() int { + return len(routes) +} + +type SwapAmountOutRoutes []SwapAmountOutRoute + +func (routes SwapAmountOutRoutes) Validate() error { + if len(routes) == 0 { + return ErrEmptyRoutes + } + + for _, route := range routes { + err := sdk.ValidateDenom(route.TokenInDenom) + if err != nil { + return err + } + } + + return nil +} + +func (routes SwapAmountOutRoutes) IntermediateDenoms() []string { + if len(routes) < 2 { + return nil + } + intermediateDenoms := make([]string, 0, len(routes)-1) + for _, route := range routes[1:] { + intermediateDenoms = append(intermediateDenoms, route.TokenInDenom) + } + + return intermediateDenoms +} + +func (routes SwapAmountOutRoutes) PoolIds() []uint64 { + poolIds := make([]uint64, 0, len(routes)) + for _, route := range routes { + poolIds = append(poolIds, route.PoolId) + } + return poolIds +} + +func (routes SwapAmountOutRoutes) Length() int { + return len(routes) +} diff --git a/x/txfees/keeper/hooks.go b/x/txfees/keeper/hooks.go index 3ef8f8671d5..f3cd4718322 100644 --- a/x/txfees/keeper/hooks.go +++ b/x/txfees/keeper/hooks.go @@ -34,7 +34,7 @@ func (k Keeper) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumb // The only thing that could be done is a costly griefing attack to reduce the amount of osmo given as tx fees. // However the idea of the txfees FeeToken gating is that the pool is sufficiently liquid for that base token. minAmountOut := sdk.ZeroInt() - _, err := k.gammKeeper.SwapExactAmountIn(cacheCtx, nonNativeFeeAddr, feetoken.PoolID, coinBalance, baseDenom, minAmountOut) + _, err := k.gammKeeper.SwapExactAmountInLegacy(cacheCtx, nonNativeFeeAddr, feetoken.PoolID, coinBalance, baseDenom, minAmountOut) return err }) } diff --git a/x/txfees/types/expected_keepers.go b/x/txfees/types/expected_keepers.go index b4f050e75cb..ca72a75eb3d 100644 --- a/x/txfees/types/expected_keepers.go +++ b/x/txfees/types/expected_keepers.go @@ -13,10 +13,10 @@ type SpotPriceCalculator interface { // GammKeeper defines the contract needed for AccountKeeper related APIs. type GammKeeper interface { - SwapExactAmountIn( + SwapExactAmountInLegacy( ctx sdk.Context, sender sdk.AccAddress, - poolId uint64, + pool uint64, tokenIn sdk.Coin, tokenOutDenom string, tokenOutMinAmount sdk.Int,