Repository: osmosis-labs/osmosis
Pull request #4855 (2023-04-07 04:44:41)

feat: canonical pool in upgrade handler; gauge redirection; balancer link
<!-- < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < ☺
v                               ✰  Thanks for creating a PR! ✰    
v    Before smashing the submit button please review the checkboxes.
v    If a checkbox is n/a - please still include it but + a little note why
v    If your PR doesn't close an issue, that's OK!  Just remove the Closes: #XXX line!
☺ > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > >  -->

Closes: #4839

## What is the purpose of the change

Creates a concentrated liquidity pool in the upgrade handler from balancer pool. The desired denom0 for the concentrated liquidity pool is hard coded to"uosmo" in the upgrade handler. The final choice is to be decided later. The choice of denom0 matters for tick spacing purposes.

The balancer pool used is the DAI / OSMO pool: https://app.osmosis.zone/pool/674

Upon concentrated pool creation, the upgrade handler redirects the distribution record associated with the balancer pool into the new concentrated liquidity pool.

The balancer gauges remain active and full as they are expected to get emptied at the next epoch

Finally, stores the migration link between balancer pool and CL pool.


## Testing and Verifying

- added unit tests for this logic
- added full-coverage upgrade handler tests
- covered by e2e upgrade test

## Documentation and Release Note

  - Does this pull request introduce a new feature or user-facing behavior changes? yes
  - Is a relevant changelog entry added to the `Unreleased` section in `CHANGELOG.md`? no
  - How is the feature or change documented? not applicable


Patch:
diff --git a/.dockerignore b/.dockerignore
index 14ef7bb17..c47b4ae51 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -5,7 +5,8 @@ networks/
 proto/
 scripts/
 tools/
-tests/cl-go-client
+tests/localosmosis/
+tests/localrelayer/
 .github/
 .git/
 .vscode/
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..c166f17f3
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,23 @@
+app/wasm/**/* @nicolaslara @czarcas7ic
+x/concentrated-liquidity/ @czarcas7ic @p0mvn
+x/downtime-detector/ @nicolaslara
+x/epochs/ @mattverse @nicolaslara
+x/gamm/ @mattverse @czarcas7ic @p0mvn
+x/incentives/ @mattverse @p0mvn
+x/lockup/ @mattverse @stackman27 @czarcas7ic
+x/mint/ @p0mvn
+x/pool-incentives/ @czarcas7ic @p0mvn
+x/poolmanager/ @czarcas7ic @p0mvn @mattverse
+x/protorev/ @p0mvn
+x/superfluid/ @mattverse @stackman27
+x/tokenfactory/ @mattverse @nicolaslara @p0mvn
+x/twap/ @nicolaslara @p0mvn
+x/txfees/ @nicolaslara @p0mvn
+x/valset-pref/ @mattverse @stackman27
+
+/.github/ @osmosis-labs/devops
+Dockerfile @osmosis-labs/devops
+.dockerignore @osmosis-labs/devops
+Makefile @osmosis-labs/devops
+/tests/localosmosis/ @osmosis-labs/devops
+/tests/localrelayer/ @osmosis-labs/devops
\ No newline at end of file
diff --git a/.github/workflows/push-dev-docker-images.yml b/.github/workflows/push-dev-docker-images.yml
index b7a262458..4211f62c2 100644
--- a/.github/workflows/push-dev-docker-images.yml
+++ b/.github/workflows/push-dev-docker-images.yml
@@ -23,7 +23,7 @@ on:
     - v[0-9]+.x
   
 env:
-  RUNNER_BASE_IMAGE_ALPINE: alpine:3.16
+  RUNNER_BASE_IMAGE_ALPINE: alpine:3.17
   OSMOSIS_DEV_IMAGE_REPOSITORY: osmolabs/osmosis-dev
   OSMOSIS_INIT_CHAIN_IMAGE_REPOSITORY: osmolabs/osmosis-e2e-init-chain
 
diff --git a/.github/workflows/push-docker-images.yml b/.github/workflows/push-docker-images.yml
index f36515418..acd6705c9 100644
--- a/.github/workflows/push-docker-images.yml
+++ b/.github/workflows/push-docker-images.yml
@@ -17,7 +17,7 @@
 # - `osmolabs/osmosis:X.Y.Z`             uses `gcr.io/distroless/static-debian11`
 # - `osmolabs/osmosis:X.Y.Z-distroless`  uses `gcr.io/distroless/static-debian11`
 # - `osmolabs/osmosis:X.Y.Z-nonroot`     uses `gcr.io/distroless/static-debian11:nonroot`
-# - `osmolabs/osmosis:X.Y.Z-alpine`      uses `alpine:3.16` 
+# - `osmolabs/osmosis:X.Y.Z-alpine`      uses `alpine:3.17` 
 #
 # All the images above have support for linux/amd64 and linux/arm64.
 #
@@ -35,7 +35,7 @@ env:
   DOCKER_REPOSITORY: osmolabs/osmosis
   RUNNER_BASE_IMAGE_DISTROLESS: gcr.io/distroless/static-debian11
   RUNNER_BASE_IMAGE_NONROOT: gcr.io/distroless/static-debian11:nonroot
-  RUNNER_BASE_IMAGE_ALPINE: alpine:3.16
+  RUNNER_BASE_IMAGE_ALPINE: alpine:3.17
 
 jobs:
   osmosisd-images:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa6001fa3..45e93e836 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -52,6 +52,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   * [#4582](https://github.com/osmosis-labs/osmosis/pull/4582) Consistently generate build tags metadata, to return a comma-separated list without stray quotes. This affects the output from `version` CLI subcommand and server info API calls.
   * [#4549](https://github.com/osmosis-labs/osmosis/pull/4549) Add single pool price estimate queries
   * [#4767](https://github.com/osmosis-labs/osmosis/pull/4767) Disable create pool with non-zero exit fee
+  * [#4847](https://github.com/osmosis-labs/osmosis/pull/4847) Update `make build` command to build only `osmosisd` binary
+  * [#4891](https://github.com/osmosis-labs/osmosis/pull/4891) Enable CORS by default on localosmosis
+  * [#4893](https://github.com/osmosis-labs/osmosis/pull/4893) Update alpine docker base image to `alpine:3.17`
 
 ### API Breaks
 
@@ -61,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 * [#4658](https://github.com/osmosis-labs/osmosis/pull/4658) Deprecate x/gamm Pool query. The new one is located in x/poolmanager.
 * [#4682](https://github.com/osmosis-labs/osmosis/pull/4682) Deprecate x/gamm SpotPrice v2 query. The new one is located in x/poolmanager.
 * [#4801](https://github.com/osmosis-labs/osmosis/pull/4801) remove GetTotalShares, GetTotalLiquidity and GetExitFee from PoolI. Define all on CFMMPoolI, define GetTotalLiquidity on PoolModuleI only.
+* [#4868](https://github.com/osmosis-labs/osmosis/pull/4868) Remove wasmEnabledProposals []wasm.ProposalType from NewOsmosisApp
 
 
 ## v15.0.0
diff --git a/Makefile b/Makefile
index c6eb55cc4..ef22fef48 100644
--- a/Makefile
+++ b/Makefile
@@ -5,8 +5,9 @@ COMMIT := $(shell git log -1 --format='%H')
 LEDGER_ENABLED ?= true
 SDK_PACK := $(shell go list -m github.com/cosmos/cosmos-sdk | sed  's/ /\@/g')
 GO_VERSION := $(shell cat go.mod | grep -E 'go [0-9].[0-9]+' | cut -d ' ' -f 2)
-DOCKER := $(shell which docker)
+GO_MODULE := $(shell cat go.mod | grep module | cut -d ' ' -f 2)
 BUILDDIR ?= $(CURDIR)/build
+DOCKER := $(shell which docker)
 E2E_UPGRADE_VERSION := "v16"
 
 
@@ -94,14 +95,16 @@ endif
 
 all: install lint test
 
-BUILD_TARGETS := build install
-
-build: BUILD_ARGS=-o $(BUILDDIR)/
+build: check_version go.sum
+	mkdir -p $(BUILDDIR)/
+	GOWORK=off go build -mod=readonly  $(BUILD_FLAGS) -o $(BUILDDIR)/ $(GO_MODULE)/cmd/osmosisd
 
-$(BUILD_TARGETS): check_version go.sum $(BUILDDIR)/
-	GOWORK=off go $@ -mod=readonly $(BUILD_FLAGS) $(BUILD_ARGS) ./...
-$(BUILDDIR)/:
+build-all: check_version go.sum
 	mkdir -p $(BUILDDIR)/
+	GOWORK=off go build -mod=readonly $(BUILD_FLAGS) -o $(BUILDDIR)/ ./...
+
+install: check_version go.sum
+	GOWORK=off go install -mod=readonly $(BUILD_FLAGS) $(GO_MODULE)/cmd/osmosisd
 
 # Cross-building for arm64 from amd64 (or viceversa) takes
 # a lot of time due to QEMU virtualization but it's the only way (afaik)
@@ -116,7 +119,7 @@ build-reproducible-amd64: go.sum $(BUILDDIR)/
 		--build-arg GO_VERSION=$(GO_VERSION) \
 		--build-arg GIT_VERSION=$(VERSION) \
 		--build-arg GIT_COMMIT=$(COMMIT) \
-		--build-arg RUNNER_IMAGE=alpine:3.16 \
+		--build-arg RUNNER_IMAGE=alpine:3.17 \
 		--platform linux/amd64 \
 		-t osmosis:local-amd64 \
 		--load \
@@ -133,7 +136,7 @@ build-reproducible-arm64: go.sum $(BUILDDIR)/
 		--build-arg GO_VERSION=$(GO_VERSION) \
 		--build-arg GIT_VERSION=$(VERSION) \
 		--build-arg GIT_COMMIT=$(COMMIT) \
-		--build-arg RUNNER_IMAGE=alpine:3.16 \
+		--build-arg RUNNER_IMAGE=alpine:3.17 \
 		--platform linux/arm64 \
 		-t osmosis:local-arm64 \
 		--load \
@@ -307,7 +310,7 @@ docker-build-debug:
 	@DOCKER_BUILDKIT=1 docker tag osmosis:${COMMIT} osmosis:debug
 
 docker-build-e2e-init-chain:
-	@DOCKER_BUILDKIT=1 docker build -t osmosis-e2e-init-chain:debug --build-arg E2E_SCRIPT_NAME=chain --platform=linux/x86_64 -f tests/e2e/initialization/init.Dockerfile .
+	@DOCKER_BUILDKIT=1 docker build -t osmolabs/osmosis-e2e-init-chain:debug --build-arg E2E_SCRIPT_NAME=chain --platform=linux/x86_64 -f tests/e2e/initialization/init.Dockerfile .
 
 docker-build-e2e-init-node:
 	@DOCKER_BUILDKIT=1 docker build -t osmosis-e2e-init-node:debug --build-arg E2E_SCRIPT_NAME=node --platform=linux/x86_64 -f tests/e2e/initialization/init.Dockerfile .
@@ -328,7 +331,7 @@ e2e-remove-resources:
 ###############################################################################
 
 RUNNER_BASE_IMAGE_DISTROLESS := gcr.io/distroless/static-debian11
-RUNNER_BASE_IMAGE_ALPINE := alpine:3.16
+RUNNER_BASE_IMAGE_ALPINE := alpine:3.17
 RUNNER_BASE_IMAGE_NONROOT := gcr.io/distroless/static-debian11:nonroot
 
 docker-build:
diff --git a/app/app.go b/app/app.go
index 8fdffc39a..ad6b94aa4 100644
--- a/app/app.go
+++ b/app/app.go
@@ -179,7 +179,6 @@ func NewOsmosisApp(
 	homePath string,
 	invCheckPeriod uint,
 	appOpts servertypes.AppOptions,
-	wasmEnabledProposals []wasm.ProposalType,
 	wasmOpts []wasm.Option,
 	baseAppOptions ...func(*baseapp.BaseApp),
 ) *OsmosisApp {
@@ -189,6 +188,7 @@ func NewOsmosisApp(
 	appCodec := encodingConfig.Marshaler
 	cdc := encodingConfig.Amino
 	interfaceRegistry := encodingConfig.InterfaceRegistry
+	wasmEnabledProposals := GetWasmEnabledProposals()
 
 	bApp := baseapp.NewBaseApp(appName, logger, db, encodingConfig.TxConfig.TxDecoder(), baseAppOptions...)
 	bApp.SetCommitMultiStoreTracer(traceStore)
diff --git a/app/config.go b/app/config.go
index 6d271c9b8..052f2e762 100644
--- a/app/config.go
+++ b/app/config.go
@@ -52,7 +52,6 @@ func NewAppConstructor() network.AppConstructor {
 		return NewOsmosisApp(
 			val.Ctx.Logger, dbm.NewMemDB(), nil, true, make(map[int64]bool), val.Ctx.Config.RootDir, 0,
 			simapp.EmptyAppOptions{},
-			GetWasmEnabledProposals(),
 			EmptyWasmOpts,
 			baseapp.SetMinGasPrices(val.AppConfig.MinGasPrices),
 		)
diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go
index dcb6c8142..fc6e6e1d5 100644
--- a/app/keepers/keepers.go
+++ b/app/keepers/keepers.go
@@ -296,10 +296,17 @@ func (appKeepers *AppKeepers) InitNormalKeepers(
 		appKeepers.SlashingKeeper,
 	)
 
+	appKeepers.LockupKeeper = lockupkeeper.NewKeeper(
+		appKeepers.keys[lockuptypes.StoreKey],
+		appKeepers.AccountKeeper,
+		appKeepers.BankKeeper,
+		appKeepers.DistrKeeper, appKeepers.GetSubspace(lockuptypes.ModuleName))
+
 	appKeepers.ConcentratedLiquidityKeeper = concentratedliquidity.NewKeeper(
 		appCodec,
 		appKeepers.keys[concentratedliquiditytypes.StoreKey],
 		appKeepers.BankKeeper,
+		appKeepers.LockupKeeper,
 		appKeepers.GetSubspace(concentratedliquiditytypes.ModuleName),
 	)
 
@@ -329,13 +336,6 @@ func (appKeepers *AppKeepers) InitNormalKeepers(
 		appKeepers.GetSubspace(twaptypes.ModuleName),
 		appKeepers.PoolManagerKeeper)
 
-	appKeepers.LockupKeeper = lockupkeeper.NewKeeper(
-		appKeepers.keys[lockuptypes.StoreKey],
-		// TODO: Visit why this needs to be deref'd
-		*appKeepers.AccountKeeper,
-		appKeepers.BankKeeper,
-		appKeepers.DistrKeeper, appKeepers.GetSubspace(lockuptypes.ModuleName))
-
 	appKeepers.EpochsKeeper = epochskeeper.NewKeeper(appKeepers.keys[epochstypes.StoreKey])
 
 	protorevKeeper := protorevkeeper.NewKeeper(
diff --git a/app/modules.go b/app/modules.go
index 0dbce5fb8..7518300c7 100644
--- a/app/modules.go
+++ b/app/modules.go
@@ -118,6 +118,7 @@ var moduleAccountPermissions = map[string][]string{
 	wasm.ModuleName:                          {authtypes.Burner},
 	tokenfactorytypes.ModuleName:             {authtypes.Minter, authtypes.Burner},
 	valsetpreftypes.ModuleName:               {authtypes.Staking},
+	poolmanagertypes.ModuleName:              nil,
 }
 
 // appModules return modules to initialize module manager.
diff --git a/app/upgrades/v16/concentrated_pool.go b/app/upgrades/v16/concentrated_pool.go
new file mode 100644
index 000000000..26d6c9639
--- /dev/null
+++ b/app/upgrades/v16/concentrated_pool.go
@@ -0,0 +1,122 @@
+package v16
+
+import (
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
+
+	"github.com/osmosis-labs/osmosis/v15/app/keepers"
+	clmodel "github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/model"
+	gammkeeper "github.com/osmosis-labs/osmosis/v15/x/gamm/keeper"
+	gammtypes "github.com/osmosis-labs/osmosis/v15/x/gamm/types"
+	"github.com/osmosis-labs/osmosis/v15/x/poolmanager"
+	poolmanagertypes "github.com/osmosis-labs/osmosis/v15/x/poolmanager/types"
+)
+
+// createConcentratedPoolFromCFMM creates a new concentrated liquidity pool with the desiredDenom0 token as the
+// token 0, links it with an existing CFMM pool, and returns the created pool.
+// It uses pool manager module account as the creator of the pool.
+// Returns error if desired denom 0 is not in associated with the CFMM pool.
+// Returns error if CFMM pool does not have exactly 2 denoms.
+// Returns error if pool creation fails.
+func createConcentratedPoolFromCFMM(ctx sdk.Context, cfmmPoolIdToLinkWith uint64, desiredDenom0 string, accountKeeper authkeeper.AccountKeeper, gammKeeper gammkeeper.Keeper, poolmanagerKeeper poolmanager.Keeper) (poolmanagertypes.PoolI, error) {
+	cfmmPool, err := gammKeeper.GetCFMMPool(ctx, cfmmPoolIdToLinkWith)
+	if err != nil {
+		return nil, err
+	}
+
+	poolmanagerModuleAcc := accountKeeper.GetModuleAccount(ctx, poolmanagertypes.ModuleName)
+	poolCreatorAddress := poolmanagerModuleAcc.GetAddress()
+
+	poolLiquidity := cfmmPool.GetTotalPoolLiquidity(ctx)
+	if len(poolLiquidity) != 2 {
+		return nil, ErrMustHaveTwoDenoms
+	}
+
+	foundDenom0 := false
+	denom1 := ""
+	for _, coin := range poolLiquidity {
+		if coin.Denom == desiredDenom0 {
+			foundDenom0 = true
+		} else {
+			denom1 = coin.Denom
+		}
+	}
+
+	if !foundDenom0 {
+		return nil, NoDesiredDenomInPoolError{desiredDenom0}
+	}
+
+	// TODO: confirm pre-launch that it is the same for CL as in balancer.
+	swapFee := cfmmPool.GetSwapFee(ctx)
+
+	createPoolMsg := clmodel.NewMsgCreateConcentratedPool(poolCreatorAddress, desiredDenom0, denom1, tickSpacing, exponentAtPriceOne, swapFee)
+	concentratedPool, err := poolmanagerKeeper.CreateConcentratedPoolAsPoolManager(ctx, createPoolMsg)
+	if err != nil {
+		return nil, err
+	}
+
+	return concentratedPool, nil
+}
+
+// createCanonicalConcentratedLiquidityPoolAndMigrationLink creates a new concentrated liquidity pool from an existing
+// CFMM pool, and migrates the gauges and distribution records from the CFMM pool to the new CL pool.
+// Additionally, it creates a migration link between the CFMM pool and the CL pool and stores it in x/gamm.
+// Returns error if fails to create concentrated liquidity pool from CFMM pool.
+// Returns error if fails to get gauges for CFMM pool.
+// Returns error if fails to get gauge for the concentrated liquidity pool.
+func createCanonicalConcentratedLiquidityPoolAndMigrationLink(ctx sdk.Context, cfmmPoolId uint64, desiredDenom0 string, keepers *keepers.AppKeepers) error {
+	concentratedPool, err := createConcentratedPoolFromCFMM(ctx, cfmmPoolId, desiredDenom0, *keepers.AccountKeeper, *keepers.GAMMKeeper, *keepers.PoolManagerKeeper)
+	if err != nil {
+		return err
+	}
+
+	// Get CFMM gauges
+	cfmmGauges, err := keepers.PoolIncentivesKeeper.GetGaugesForCFMMPool(ctx, cfmmPoolId)
+	if err != nil {
+		return err
+	}
+
+	if len(cfmmGauges) == 0 {
+		return ErrNoGaugeToRedirect
+	}
+
+	// Get longest gauge duration from balancer.
+	longestDurationGauge := cfmmGauges[0]
+	for i := 1; i < len(cfmmGauges); i++ {
+		if cfmmGauges[i].DistributeTo.Duration > longestDurationGauge.DistributeTo.Duration {
+			longestDurationGauge = cfmmGauges[i]
+		}
+	}
+
+	// Get concentrated liquidity gauge duration.
+	distributionEpochDuration := keepers.IncentivesKeeper.GetEpochInfo(ctx).Duration
+
+	// Get concentrated gauge correspondng to the distribution epoch duration.
+	concentratedGaugeId, err := keepers.PoolIncentivesKeeper.GetPoolGaugeId(ctx, concentratedPool.GetId(), distributionEpochDuration)
+	if err != nil {
+		return err
+	}
+
+	// Iterate through all the distr records, and redirect the old balancer gauge to the new concentrated gauge.
+	distrInfo := keepers.PoolIncentivesKeeper.GetDistrInfo(ctx)
+	for i, distrRecord := range distrInfo.Records {
+		if distrRecord.GaugeId == longestDurationGauge.Id {
+			distrInfo.Records[i].GaugeId = concentratedGaugeId
+		}
+	}
+
+	// Set the new distr info.
+	keepers.PoolIncentivesKeeper.SetDistrInfo(ctx, distrInfo)
+
+	// Set the migration link in x/gamm.
+	keepers.GAMMKeeper.SetMigrationInfo(ctx, gammtypes.MigrationRecords{
+		BalancerToConcentratedPoolLinks: []gammtypes.BalancerToConcentratedPoolLink{
+			{
+				BalancerPoolId: cfmmPoolId,
+				ClPoolId:       concentratedPool.GetId(),
+			},
+		},
+	})
+
+	return nil
+}
diff --git a/app/upgrades/v16/errors.go b/app/upgrades/v16/errors.go
new file mode 100644
index 000000000..00b9f4d61
--- /dev/null
+++ b/app/upgrades/v16/errors.go
@@ -0,0 +1,19 @@
+package v16
+
+import (
+	"errors"
+	"fmt"
+)
+
+var (
+	ErrMustHaveTwoDenoms = errors.New("can only have 2 denoms in CL pool")
+	ErrNoGaugeToRedirect = errors.New("could not find gauge to redirect")
+)
+
+type NoDesiredDenomInPoolError struct {
+	DesiredDenom string
+}
+
+func (e NoDesiredDenomInPoolError) Error() string {
+	return fmt.Sprintf("desired denom (%s) was not found in the pool", e.DesiredDenom)
+}
diff --git a/app/upgrades/v16/upgrades.go b/app/upgrades/v16/upgrades.go
index 67af66ce9..507b3c208 100644
--- a/app/upgrades/v16/upgrades.go
+++ b/app/upgrades/v16/upgrades.go
@@ -9,6 +9,23 @@ import (
 	"github.com/osmosis-labs/osmosis/v15/app/upgrades"
 )
 
+const (
+	// DAI/OSMO pool ID
+	// https://app.osmosis.zone/pool/674
+	daiOsmoPoolId = uint64(674)
+	// TODO: make sure this is what we desire.
+	desiredDenom0 = "uosmo"
+	// TODO: confirm pre-launch.
+	tickSpacing = 1
+
+	// TODO: confirm that concentrated pool swap fee should equal balancer swap fee.
+)
+
+var (
+	// TODO: confirm pre-launch.
+	exponentAtPriceOne = sdk.OneInt().Neg()
+)
+
 func CreateUpgradeHandler(
 	mm *module.Manager,
 	configurator module.Configurator,
@@ -23,6 +40,10 @@ func CreateUpgradeHandler(
 			return nil, err
 		}
 
+		if err := createCanonicalConcentratedLiquidityPoolAndMigrationLink(ctx, daiOsmoPoolId, desiredDenom0, keepers); err != nil {
+			return nil, err
+		}
+
 		return migrations, nil
 	}
 }
diff --git a/chain.schema.json b/chain.schema.json
index 029cd2715..dfa5a9739 100644
--- a/chain.schema.json
+++ b/chain.schema.json
@@ -83,7 +83,26 @@
       {
         "name": "v15",
         "tag" : "v15.0.0",
-        "height": 8732500
+        "height": 8732500,
+        "recommended_version": "v15.0.0",
+        "compatible_versions": [
+          "v15.0.0"
+        ],
+        "cosmos_sdk_version": "0.45",
+        "consensus": {
+          "type": "tendermint",
+          "version": "0.34"
+        },
+        "cosmwasm_version": "0.30",
+        "cosmwasm_enabled": true,
+        "ibc_go_version": "4.3.0",
+        "ics_enabled": [
+          "ics20-1"
+        ],
+        "binaries": {
+          "linux/amd64": "https://github.com/osmosis-labs/osmosis/releases/download/v15.0.0/osmosisd-15.0.0-linux-amd64?checksum=sha256:6f5cead57c16c1e708df2a0f336e6e4494a026ba97b8d8afef95e5fc5b80b465",
+          "linux/arm64": "https://github.com/osmosis-labs/osmosis/releases/download/v15.0.0/osmosisd-15.0.0-linux-arm64?checksum=sha256:94aee34e288148b155a2b0fdfe268a0bdc0d4a90de6db8f8a9cee74c2e829294"
+        }
       }
     ]
   }
diff --git a/cmd/osmosisd/cmd/init.go b/cmd/osmosisd/cmd/init.go
index f61ce528e..b7a6b2c19 100644
--- a/cmd/osmosisd/cmd/init.go
+++ b/cmd/osmosisd/cmd/init.go
@@ -36,6 +36,9 @@ const (
 
 	// FlagSeed defines a flag to initialize the private validator key from a specific seed.
 	FlagRecover = "recover"
+
+	// FlagSetEnv defines a flag to create environment file & save current home directory into it.
+	FlagSetEnv = "set-env"
 )
 
 type printInfo struct {
@@ -169,9 +172,12 @@ func InitCmd(mbm module.BasicManager, defaultNodeHome string) *cobra.Command {
 
 			tmcfg.WriteConfigFile(filepath.Join(config.RootDir, "config", "config.toml"), config)
 
-			err = CreateEnvFile(cmd)
-			if err != nil {
-				return errors.Wrapf(err, "Failed to create environment file")
+			createEnv, _ := cmd.Flags().GetBool(FlagSetEnv)
+			if createEnv {
+				err = CreateEnvFile(cmd)
+				if err != nil {
+					return errors.Wrapf(err, "Failed to create environment file")
+				}
 			}
 			return displayInfo(toPrint)
 		},
@@ -181,6 +187,7 @@ func InitCmd(mbm module.BasicManager, defaultNodeHome string) *cobra.Command {
 	cmd.Flags().BoolP(FlagOverwrite, "o", false, "overwrite the genesis.json file")
 	cmd.Flags().Bool(FlagRecover, false, "provide seed phrase to recover existing key instead of creating")
 	cmd.Flags().String(flags.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created")
+	cmd.Flags().Bool(FlagSetEnv, false, "set and save current directory as home directory")
 
 	return cmd
 }
diff --git a/cmd/osmosisd/cmd/root.go b/cmd/osmosisd/cmd/root.go
index 8c8415a29..dbfcd8b1d 100644
--- a/cmd/osmosisd/cmd/root.go
+++ b/cmd/osmosisd/cmd/root.go
@@ -295,7 +295,6 @@ func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts serverty
 		cast.ToString(appOpts.Get(flags.FlagHome)),
 		cast.ToUint(appOpts.Get(server.FlagInvCheckPeriod)),
 		appOpts,
-		osmosis.GetWasmEnabledProposals(),
 		wasmOpts,
 		baseapp.SetPruning(pruningOpts),
 		baseapp.SetMinGasPrices(cast.ToString(appOpts.Get(server.FlagMinGasPrices))),
@@ -319,7 +318,7 @@ func createOsmosisAppAndExport(
 	encCfg.Marshaler = codec.NewProtoCodec(encCfg.InterfaceRegistry)
 	loadLatest := height == -1
 	homeDir := cast.ToString(appOpts.Get(flags.FlagHome))
-	app := osmosis.NewOsmosisApp(logger, db, traceStore, loadLatest, map[int64]bool{}, homeDir, 0, appOpts, osmosis.GetWasmEnabledProposals(), osmosis.EmptyWasmOpts)
+	app := osmosis.NewOsmosisApp(logger, db, traceStore, loadLatest, map[int64]bool{}, homeDir, 0, appOpts, osmosis.EmptyWasmOpts)
 
 	if !loadLatest {
 		if err := app.LoadHeight(height); err != nil {
diff --git a/go.mod b/go.mod
index 7a41681d4..f95bce4b5 100644
--- a/go.mod
+++ b/go.mod
@@ -25,7 +25,7 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/rakyll/statik v0.1.7
 	github.com/spf13/cast v1.5.0
-	github.com/spf13/cobra v1.6.1
+	github.com/spf13/cobra v1.7.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.15.0
 	github.com/strangelove-ventures/async-icq/v4 v4.0.0-rc0
@@ -35,12 +35,12 @@ require (
 	github.com/tendermint/tm-db v0.6.8-0.20220506192307-f628bb5dc95b
 	github.com/tidwall/btree v1.6.0
 	github.com/tidwall/gjson v1.14.4
-	go.uber.org/multierr v1.10.0
+	go.uber.org/multierr v1.11.0
 	golang.org/x/exp v0.0.0-20230131160201-f062dba9d201
 	google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa
 	google.golang.org/grpc v1.53.0
 	gopkg.in/yaml.v2 v2.4.0
-	mvdan.cc/gofumpt v0.4.0
+	mvdan.cc/gofumpt v0.5.0
 )
 
 require (
@@ -190,7 +190,7 @@ require (
 	github.com/hexops/gotextdiff v1.0.3 // indirect
 	github.com/imdario/mergo v0.3.13 // indirect
 	github.com/improbable-eng/grpc-web v0.15.0 // indirect
-	github.com/inconshreveable/mousetrap v1.0.1 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jessevdk/go-flags v1.4.0 // indirect
 	github.com/jgautheron/goconst v1.5.1 // indirect
 	github.com/jingyugao/rowserrcheck v1.1.1 // indirect
@@ -292,13 +292,13 @@ require (
 	go.opencensus.io v0.24.0 // indirect
 	golang.org/x/crypto v0.5.0 // indirect
 	golang.org/x/exp/typeparams v0.0.0-20230224173230-c95f2b4c22f2 // indirect
-	golang.org/x/mod v0.9.0 // indirect
-	golang.org/x/net v0.8.0 // indirect
+	golang.org/x/mod v0.10.0 // indirect
+	golang.org/x/net v0.9.0 // indirect
 	golang.org/x/sync v0.1.0 // indirect
-	golang.org/x/sys v0.6.0 // indirect
-	golang.org/x/term v0.6.0 // indirect
-	golang.org/x/text v0.8.0 // indirect
-	golang.org/x/tools v0.7.0 // indirect
+	golang.org/x/sys v0.7.0 // indirect
+	golang.org/x/term v0.7.0 // indirect
+	golang.org/x/text v0.9.0 // indirect
+	golang.org/x/tools v0.8.0 // indirect
 	google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1
diff --git a/go.sum b/go.sum
index 28bfc23de..09d0dec90 100644
--- a/go.sum
+++ b/go.sum
@@ -356,7 +356,7 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8
 github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
 github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
 github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
-github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
@@ -661,8 +661,8 @@ github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK
 github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ=
 github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
-github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY=
 github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI=
 github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8=
@@ -966,7 +966,6 @@ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG
 github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
 github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -1045,7 +1044,7 @@ github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRr
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
 github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U=
 github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
@@ -1115,8 +1114,8 @@ github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
-github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
-github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
@@ -1287,8 +1286,8 @@ go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
 go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
-go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
-go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
 go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
@@ -1371,8 +1370,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
-golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
+golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1432,8 +1431,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
-golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1555,8 +1554,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1565,8 +1564,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
 golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
-golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1580,8 +1579,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
-golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1669,8 +1668,8 @@ golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
 golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
 golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
-golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
+golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
+golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1820,8 +1819,8 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
 honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
 honnef.co/go/tools v0.4.3 h1:o/n5/K5gXqk8Gozvs2cnL0F2S1/g1vcGCAx2vETjITw=
 honnef.co/go/tools v0.4.3/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA=
-mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM=
-mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ=
+mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E=
+mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js=
 mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
 mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
 mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
diff --git a/networks/osmosis-1/upgrades/v13/mainnet/guide.md b/networks/osmosis-1/upgrades/v13/mainnet/guide.md
index d6857123e..b5f727001 100644
--- a/networks/osmosis-1/upgrades/v13/mainnet/guide.md
+++ b/networks/osmosis-1/upgrades/v13/mainnet/guide.md
@@ -4,7 +4,7 @@ Osmosis v13 Gov Prop: <https://www.mintscan.io/osmosis/proposals/370>
 
 Countdown: <https://www.mintscan.io/osmosis/blocks/7241500>
 
-Height: 6246000 TODO: Update upgrade height
+Height: 7241500
 
 ## Memory Requirements
 
diff --git a/x/concentrated-liquidity/export_internal.go b/x/concentrated-liquidity/export_internal.go
new file mode 100644
index 000000000..1ff4bf136
--- /dev/null
+++ b/x/concentrated-liquidity/export_internal.go
@@ -0,0 +1,11 @@
+package concentrated_liquidity
+
+import (
+	sdk "github.com/cosmos/cosmos-sdk/types"
+
+	"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/internal/math"
+)
+
+func TickToSqrtPrice(tickIndex, exponentAtPriceOne sdk.Int) (price sdk.Dec, err error) {
+	return math.TickToSqrtPrice(tickIndex, exponentAtPriceOne)
+}
diff --git a/x/concentrated-liquidity/grpc_query.go b/x/concentrated-liquidity/grpc_query.go
index 1da32c7f2..91978ed98 100644
--- a/x/concentrated-liquidity/grpc_query.go
+++ b/x/concentrated-liquidity/grpc_query.go
@@ -212,7 +212,7 @@ func (q Querier) LiquidityNetInDirection(goCtx context.Context, req *clquery.Que
 		return nil, err
 	}
 
-	return &clquery.QueryLiquidityNetInDirectionResponse{LiquidityDepths: liquidityDepths, CurrentLiquidity: pool.GetLiquidity()}, nil
+	return &clquery.QueryLiquidityNetInDirectionResponse{LiquidityDepths: liquidityDepths, CurrentLiquidity: pool.GetLiquidity(), CurrentTick: pool.GetCurrentTick().Int64()}, nil
 }
 
 func (q Querier) ClaimableFees(ctx context.Context, req *clquery.QueryClaimableFeesRequest) (*clquery.QueryClaimableFeesResponse, error) {
diff --git a/x/concentrated-liquidity/keeper.go b/x/concentrated-liquidity/keeper.go
index 7798976de..40ae0f747 100644
--- a/x/concentrated-liquidity/keeper.go
+++ b/x/concentrated-liquidity/keeper.go
@@ -21,18 +21,20 @@ type Keeper struct {
 	// keepers
 	poolmanagerKeeper types.PoolManagerKeeper
 	bankKeeper        types.BankKeeper
+	lockupKeeper      types.LockupKeeper
 }
 
-func NewKeeper(cdc codec.BinaryCodec, storeKey sdk.StoreKey, bankKeeper types.BankKeeper, paramSpace paramtypes.Subspace) *Keeper {
+func NewKeeper(cdc codec.BinaryCodec, storeKey sdk.StoreKey, bankKeeper types.BankKeeper, lockupKeeper types.LockupKeeper, paramSpace paramtypes.Subspace) *Keeper {
 	// ParamSubspace must be initialized within app/keepers/keepers.go
 	if !paramSpace.HasKeyTable() {
 		paramSpace = paramSpace.WithKeyTable(types.ParamKeyTable())
 	}
 	return &Keeper{
-		storeKey:   storeKey,
-		paramSpace: paramSpace,
-		cdc:        cdc,
-		bankKeeper: bankKeeper,
+		storeKey:     storeKey,
+		paramSpace:   paramSpace,
+		cdc:          cdc,
+		bankKeeper:   bankKeeper,
+		lockupKeeper: lockupKeeper,
 	}
 }
 
diff --git a/x/concentrated-liquidity/lp.go b/x/concentrated-liquidity/lp.go
index b1fab3dc2..b84133b35 100644
--- a/x/concentrated-liquidity/lp.go
+++ b/x/concentrated-liquidity/lp.go
@@ -8,6 +8,7 @@ import (
 
 	sdk "github.com/cosmos/cosmos-sdk/types"
 
+	"github.com/osmosis-labs/osmosis/osmomath"
 	"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/internal/math"
 	types "github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/types"
 )
@@ -305,15 +306,17 @@ func (k Keeper) initializeInitialPositionForPool(ctx sdk.Context, pool types.Con
 		return types.InitialLiquidityZeroError{Amount0: amount0Desired, Amount1: amount1Desired}
 	}
 
-	// Calculate the spot price and sqrt price from the amount provided
-	initialSpotPrice := amount1Desired.ToDec().Quo(amount0Desired.ToDec())
-	initialSqrtPrice, err := initialSpotPrice.ApproxSqrt()
+	initialSpotPrice := osmomath.BigDecFromSDKDec(amount1Desired.ToDec()).Quo(osmomath.BigDecFromSDKDec((amount0Desired.ToDec())))
+
+	// Calculate the initial tick from the initial spot price
+	initialTick, err := math.PriceToTick(initialSpotPrice.SDKDec(), pool.GetExponentAtPriceOne())
 	if err != nil {
 		return err
 	}
 
-	// Calculate the initial tick from the initial spot price
-	initialTick, err := math.PriceToTick(initialSpotPrice, pool.GetExponentAtPriceOne())
+	// Re-Calculate the spot price from initial tick so that it is aligned with our internal
+	// tick to price conversion.
+	initialSqrtPrice, err := math.TickToSqrtPrice(initialTick, pool.GetExponentAtPriceOne())
 	if err != nil {
 		return err
 	}
diff --git a/x/concentrated-liquidity/model/pool.go b/x/concentrated-liquidity/model/pool.go
index fd1dc9a98..48206da29 100644
--- a/x/concentrated-liquidity/model/pool.go
+++ b/x/concentrated-liquidity/model/pool.go
@@ -25,10 +25,9 @@ var (
 // NewConcentratedLiquidityPool creates a new ConcentratedLiquidity pool with the specified parameters.
 // The two provided denoms are ordered so that denom0 is lexicographically smaller than denom1.
 func NewConcentratedLiquidityPool(poolId uint64, denom0, denom1 string, tickSpacing uint64, exponentAtPriceOne sdk.Int, swapFee sdk.Dec) (Pool, error) {
-	// Order the initial pool denoms so that denom0 is lexicographically smaller than denom1.
-	denom0, denom1, err := types.OrderInitialPoolDenoms(denom0, denom1)
-	if err != nil {
-		return Pool{}, err
+	// Ensure that the two denoms are different
+	if denom0 == denom1 {
+		return Pool{}, types.MatchingDenomError{Denom: denom0}
 	}
 
 	// Only allow precision values in specified range
diff --git a/x/concentrated-liquidity/tick.go b/x/concentrated-liquidity/tick.go
index 66e71122a..3f8717f7e 100644
--- a/x/concentrated-liquidity/tick.go
+++ b/x/concentrated-liquidity/tick.go
@@ -1,6 +1,8 @@
 package concentrated_liquidity
 
 import (
+	"fmt"
+
 	"github.com/cosmos/cosmos-sdk/store/prefix"
 	storetypes "github.com/cosmos/cosmos-sdk/store/types"
 	sdk "github.com/cosmos/cosmos-sdk/types"
@@ -304,10 +306,13 @@ func (k Keeper) GetTickLiquidityNetInDirection(ctx sdk.Context, poolId uint64, t
 		return []query.TickLiquidityNet{}, err
 	}
 
+	ctx.Logger().Debug(fmt.Sprintf("userGivenStartTick %s, boundTick %s, currentTick %s\n", userGivenStartTick, boundTick, p.GetCurrentTick()))
+
 	startTick := p.GetCurrentTick()
 	// If start tick is set, use it as the current tick for grabbing liquidities from.
 	if !userGivenStartTick.IsNil() {
 		startTick = userGivenStartTick
+		ctx.Logger().Debug(fmt.Sprintf("startTick %s set to user given\n", startTick))
 	}
 
 	// sanity check that given tokenIn is an asset in pool.
@@ -318,9 +323,15 @@ func (k Keeper) GetTickLiquidityNetInDirection(ctx sdk.Context, poolId uint64, t
 	// figure out zero for one depending on the token in.
 	zeroForOne := p.GetToken0() == tokenIn
 
+	ctx.Logger().Debug(fmt.Sprintf("is_zero_for_one %t\n", zeroForOne))
+
 	// use max or min tick if provided bound is nil
 	exponentAtPriceOne := p.GetExponentAtPriceOne()
 	minTick, maxTick := math.GetMinAndMaxTicksFromExponentAtPriceOneInternal(exponentAtPriceOne)
+
+	ctx.Logger().Debug(fmt.Sprintf("min_tick %d\n", minTick))
+	ctx.Logger().Debug(fmt.Sprintf("max_tick %d\n", maxTick))
+
 	if boundTick.IsNil() {
 		if zeroForOne {
 			boundTick = sdk.NewInt(minTick)
@@ -336,6 +347,7 @@ func (k Keeper) GetTickLiquidityNetInDirection(ctx sdk.Context, poolId uint64, t
 	// between current tick and the min/max tick depending on the swap direction.
 	validateTickIsInValidRange := func(validateTick sdk.Int) error {
 		validateSqrtPrice, err := math.TickToSqrtPrice(validateTick, exponentAtPriceOne)
+		ctx.Logger().Debug(fmt.Sprintf("validateTick %s; validate sqrtPrice %s\n", validateTick.String(), validateSqrtPrice.String()))
 		if err != nil {
 			return err
 		}
@@ -347,10 +359,12 @@ func (k Keeper) GetTickLiquidityNetInDirection(ctx sdk.Context, poolId uint64, t
 		return nil
 	}
 
+	ctx.Logger().Debug("validating bound tick")
 	if err := validateTickIsInValidRange(boundTick); err != nil {
 		return []query.TickLiquidityNet{}, err
 	}
 
+	ctx.Logger().Debug("validating start tick")
 	if err := validateTickIsInValidRange(startTick); err != nil {
 		return []query.TickLiquidityNet{}, err
 	}
diff --git a/x/concentrated-liquidity/types/cl.go b/x/concentrated-liquidity/types/cl.go
deleted file mode 100644
index a27a69ef1..000000000
--- a/x/concentrated-liquidity/types/cl.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package types
-
-import (
-	fmt "fmt"
-)
-
-func OrderInitialPoolDenoms(denom0, denom1 string) (string, string, error) {
-	if denom0 == denom1 {
-		return "", "", fmt.Errorf("cannot have the same asset in a single pool")
-	}
-	if denom0 > denom1 {
-		denom1, denom0 = denom0, denom1
-	}
-
-	return denom0, denom1, nil
-}
diff --git a/x/concentrated-liquidity/types/errors.go b/x/concentrated-liquidity/types/errors.go
index 69823481e..f4d22df79 100644
--- a/x/concentrated-liquidity/types/errors.go
+++ b/x/concentrated-liquidity/types/errors.go
@@ -546,3 +546,11 @@ type PositionsNotInSameTickRangeError struct {
 func (e PositionsNotInSameTickRangeError) Error() string {
 	return fmt.Sprintf("positions not in same tick range, position 1 tick lower (%d), position 1 tick upper (%d), position 2 tick lower (%d), position 2 tick upper (%d)", e.Position1TickLower, e.Position1TickUpper, e.Position2TickLower, e.Position2TickUpper)
 }
+
+type MatchingDenomError struct {
+	Denom string
+}
+
+func (e MatchingDenomError) Error() string {
+	return fmt.Sprintf("received matching denoms (%s), must be different", e.Denom)
+}
diff --git a/x/concentrated-liquidity/types/expected_keepers.go b/x/concentrated-liquidity/types/expected_keepers.go
index 737f47567..c5b00a79a 100644
--- a/x/concentrated-liquidity/types/expected_keepers.go
+++ b/x/concentrated-liquidity/types/expected_keepers.go
@@ -1,9 +1,12 @@
 package types
 
 import (
+	"time"
+
 	sdk "github.com/cosmos/cosmos-sdk/types"
 	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
 
+	lockuptypes "github.com/osmosis-labs/osmosis/v15/x/lockup/types"
 	poolmanagertypes "github.com/osmosis-labs/osmosis/v15/x/poolmanager/types"
 )
 
@@ -22,3 +25,14 @@ type PoolManagerKeeper interface {
 	CreatePool(ctx sdk.Context, msg poolmanagertypes.CreatePoolMsg) (uint64, error)
 	GetNextPoolId(ctx sdk.Context) uint64
 }
+
+// LockupKeeper defines the expected interface needed to retrieve locks.
+type LockupKeeper interface {
+	GetLockByID(ctx sdk.Context, lockID uint64) (*lockuptypes.PeriodLock, error)
+	// Despite the name, BeginForceUnlock is really BeginUnlock
+	// TODO: Fix this in future code update
+	BeginForceUnlock(ctx sdk.Context, lockID uint64, coins sdk.Coins) (uint64, error)
+	ForceUnlock(ctx sdk.Context, lock lockuptypes.PeriodLock) error
+	CreateLock(ctx sdk.Context, owner sdk.AccAddress, coins sdk.Coins, duration time.Duration) (lockuptypes.PeriodLock, error)
+	SlashTokensFromLockByID(ctx sdk.Context, lockID uint64, coins sdk.Coins) (*lockuptypes.PeriodLock, error)
+}
diff --git a/x/ibc-hooks/client/cli/query.go b/x/ibc-hooks/client/cli/query.go
index cd9b8aaa0..c17c60294 100644
--- a/x/ibc-hooks/client/cli/query.go
+++ b/x/ibc-hooks/client/cli/query.go
@@ -35,7 +35,6 @@ func GetQueryCmd() *cobra.Command {
 		RunE:                       indexRunCmd,
 	}
 
-	cmd.Short = fmt.Sprintf("Querying commands for the %s module", types.ModuleName)
 	cmd.AddCommand(
 		GetCmdWasmSender(),
 	)
diff --git a/x/pool-incentives/keeper/keeper.go b/x/pool-incentives/keeper/keeper.go
index bfc2ce1f2..b1d8a1e24 100644
--- a/x/pool-incentives/keeper/keeper.go
+++ b/x/pool-incentives/keeper/keeper.go
@@ -13,7 +13,6 @@ import (
 	"github.com/osmosis-labs/osmosis/v15/x/pool-incentives/types"
 
 	sdk "github.com/cosmos/cosmos-sdk/types"
-	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 	paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
 
 	appparams "github.com/osmosis-labs/osmosis/v15/app/params"
@@ -144,7 +143,7 @@ func (k Keeper) GetPoolGaugeId(ctx sdk.Context, poolId uint64, lockableDuration
 	bz := store.Get(key)
 
 	if len(bz) == 0 {
-		return 0, sdkerrors.Wrapf(types.ErrNoGaugeIdExist, "gauge id for pool (%d) with duration (%s) not exist", poolId, lockableDuration.String())
+		return 0, types.NoGaugeAssociatedWithPoolError{PoolId: poolId, Duration: lockableDuration}
 	}
 
 	return sdk.BigEndianToUint64(bz), nil
@@ -162,6 +161,30 @@ func (k Keeper) GetPoolIdFromGaugeId(ctx sdk.Context, gaugeId uint64, lockableDu
 	return sdk.BigEndianToUint64(bz), nil
 }
 
+// GetGaugesForCFMMPool returns the gauges associated with the given CFMM pool ID, by first retrieving
+// the lockable durations for the pool, then using them to query the pool incentives keeper for the
+// gauge IDs associated with each duration, and finally using the incentives keeper to retrieve the
+// actual gauges from the retrieved gauge IDs.
+// CONTRACT: pool id must be assocated with a CFMM pool.
+func (k Keeper) GetGaugesForCFMMPool(ctx sdk.Context, poolId uint64) ([]incentivestypes.Gauge, error) {
+	lockableDurations := k.GetLockableDurations(ctx)
+	cfmmGauges := make([]incentivestypes.Gauge, 0, len(lockableDurations))
+	for _, duration := range lockableDurations {
+		gaugeId, err := k.GetPoolGaugeId(ctx, poolId, duration)
+		if err != nil {
+			return nil, err
+		}
+		gauge, err := k.incentivesKeeper.GetGaugeByID(ctx, gaugeId)
+		if err != nil {
+			return nil, err
+		}
+
+		cfmmGauges = append(cfmmGauges, *gauge)
+	}
+
+	return cfmmGauges, nil
+}
+
 func (k Keeper) SetLockableDurations(ctx sdk.Context, lockableDurations []time.Duration) {
 	store := ctx.KVStore(k.storeKey)
 	info := types.LockableDurationsInfo{LockableDurations: lockableDurations}
diff --git a/x/pool-incentives/types/errors.go b/x/pool-incentives/types/errors.go
index be843e938..3a95a47fa 100644
--- a/x/pool-incentives/types/errors.go
+++ b/x/pool-incentives/types/errors.go
@@ -19,6 +19,15 @@ var (
 	ErrEmptyProposalGaugeIds = sdkerrors.Register(ModuleName, 11, "gauge ids are empty")
 )
 
+type NoGaugeAssociatedWithPoolError struct {
+	PoolId   uint64
+	Duration time.Duration
+}
+
+func (e NoGaugeAssociatedWithPoolError) Error() string {
+	return fmt.Sprintf("no gauge associated with pool id (%d) and duration (%d)", e.PoolId, e.Duration)
+}
+
 type NoPoolAssociatedWithGaugeError struct {
 	GaugeId  uint64
 	Duration time.Duration
diff --git a/x/poolmanager/create_pool.go b/x/poolmanager/create_pool.go
index 87a812db6..525b46ae0 100644
--- a/x/poolmanager/create_pool.go
+++ b/x/poolmanager/create_pool.go
@@ -40,8 +40,7 @@ func (k Keeper) validateCreatedPool(
 // - Minting LP shares to pool creator
 // - Setting metadata for the shares
 func (k Keeper) CreatePool(ctx sdk.Context, msg types.CreatePoolMsg) (uint64, error) {
-	// Run validate basic on the message.
-	err := msg.Validate(ctx)
+	pool, err := k.createPoolZeroLiquidityNoCreationFee(ctx, msg)
 	if err != nil {
 		return 0, err
 	}
@@ -53,40 +52,78 @@ func (k Keeper) CreatePool(ctx sdk.Context, msg types.CreatePoolMsg) (uint64, er
 		return 0, err
 	}
 
+	// Send initial liquidity to the pool's address.
+	initialPoolLiquidity := msg.InitialLiquidity()
+	err = k.bankKeeper.SendCoins(ctx, sender, pool.GetAddress(), initialPoolLiquidity)
+	if err != nil {
+		return 0, err
+	}
+
+	return pool.GetId(), nil
+}
+
+// CreateConcentratedPoolAsPoolManager creates a concentrated liquidity pool from given message without sending any initial liquidity to the pool
+// and paying a creation fee. This is meant to be used for creating the pools internally. For example, in the upgrade handler.
+// The creator of the pool must be a poolmanager module account. Returns error if not. Otherwise, functions the same as
+// the regular CreatePool.
+func (k Keeper) CreateConcentratedPoolAsPoolManager(ctx sdk.Context, msg types.CreatePoolMsg) (types.PoolI, error) {
+	// Validate that creator is a polmanager module account as a sanity check.
+	creator := msg.PoolCreator()
+	poolmanagerModuleAcc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleName)
+	if !poolmanagerModuleAcc.GetAddress().Equals(creator) {
+		return nil, types.InvalidPoolCreatorError{CreatorAddresss: creator.String()}
+	}
+
+	// Disallow this for any pool type other than concentrated liquidity pool.
+	// This can be further relaxed in the future.
+	// The reason for this constraint is having balancer and stableswap pools mint gamm shares during InitializePool()
+	// Module accounts cannot receive shares, so we cannot use this function for the above pool types without refactor.
+	if msg.GetPoolType() != types.Concentrated {
+		return nil, types.InvalidPoolTypeError{PoolType: msg.GetPoolType()}
+	}
+
+	return k.createPoolZeroLiquidityNoCreationFee(ctx, msg)
+}
+
+// createPoolZeroLiquidityNoCreationFee is an internal helper to create a pool from message with zero initial liquidity
+// and no creation fee charged. It validates the message gets the next pool ID and creates the pool with the given pool ID and the desired type.
+// It persists the module routing in state for future use. Initializes the pool in its respective module. Emits a create pool event.
+// Returns error if failes to validate the pool creation message, fails to create a module account for the pool or if faile to initialize the pool.
+// It is used by CreatePoolZeroLiquidityNoCreationFee and CreatePool.
+func (k Keeper) createPoolZeroLiquidityNoCreationFee(ctx sdk.Context, msg types.CreatePoolMsg) (types.PoolI, error) {
+	// Run validate basic on the message.
+	err := msg.Validate(ctx)
+	if err != nil {
+		return nil, err
+	}
+
 	// Get the next pool ID and increment the pool ID counter
 	// Create the pool with the given pool ID
 	poolId := k.getNextPoolIdAndIncrement(ctx)
 	pool, err := msg.CreatePool(ctx, poolId)
 	if err != nil {
-		return 0, err
+		return nil, err
 	}
 
 	k.SetPoolRoute(ctx, poolId, msg.GetPoolType())
 
 	if err := k.validateCreatedPool(ctx, poolId, pool); err != nil {
-		return 0, err
+		return nil, err
 	}
 
 	// create and save the pool's module account to the account keeper
 	if err := osmoutils.CreateModuleAccount(ctx, k.accountKeeper, pool.GetAddress()); err != nil {
-		return 0, fmt.Errorf("creating pool module account for id %d: %w", poolId, err)
+		return nil, fmt.Errorf("creating pool module account for id %d: %w", poolId, err)
 	}
 
 	// Run the respective pool type's initialization logic.
 	swapModule := k.routes[msg.GetPoolType()]
-	if err := swapModule.InitializePool(ctx, pool, sender); err != nil {
-		return 0, err
-	}
-
-	// Send initial liquidity to the pool's address.
-	initialPoolLiquidity := msg.InitialLiquidity()
-	err = k.bankKeeper.SendCoins(ctx, sender, pool.GetAddress(), initialPoolLiquidity)
-	if err != nil {
-		return 0, err
+	if err := swapModule.InitializePool(ctx, pool, msg.PoolCreator()); err != nil {
+		return nil, err
 	}
 
 	emitCreatePoolEvents(ctx, poolId, msg)
-	return pool.GetId(), nil
+	return pool, nil
 }
 
 func emitCreatePoolEvents(ctx sdk.Context, poolId uint64, msg types.CreatePoolMsg) {
diff --git a/x/poolmanager/router.go b/x/poolmanager/router.go
index dae91f64f..d454b921f 100644
--- a/x/poolmanager/router.go
+++ b/x/poolmanager/router.go
@@ -84,7 +84,6 @@ func (k Keeper) RouteExactAmountIn(
 
 		tokenOutAmount, err = swapModule.SwapExactAmountIn(ctx, sender, pool, tokenIn, route.TokenOutDenom, _outMinAmount, swapFee)
 		if err != nil {
-			ctx.Logger().Error(err.Error())
 			return sdk.Int{}, err
 		}
 
diff --git a/x/poolmanager/types/errors.go b/x/poolmanager/types/errors.go
index 888b40334..5b58d56df 100644
--- a/x/poolmanager/types/errors.go
+++ b/x/poolmanager/types/errors.go
@@ -36,3 +36,19 @@ type UndefinedRouteError struct {
 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)
 }
+
+type InvalidPoolCreatorError struct {
+	CreatorAddresss string
+}
+
+func (e InvalidPoolCreatorError) Error() string {
+	return fmt.Sprintf("invalid pool creator (%s), only poolmanager can create pools with no fee", e.CreatorAddresss)
+}
+
+type InvalidPoolTypeError struct {
+	PoolType PoolType
+}
+
+func (e InvalidPoolTypeError) Error() string {
+	return fmt.Sprintf("invalid pool type (%s)", PoolType_name[int32(e.PoolType)])
+}
diff --git a/x/poolmanager/types/routes.go b/x/poolmanager/types/routes.go
index 1664552cc..1c57f8d82 100644
--- a/x/poolmanager/types/routes.go
+++ b/x/poolmanager/types/routes.go
@@ -10,6 +10,7 @@ import (
 // AccountI defines the account contract that must be fulfilled when
 // creating a x/gamm keeper.
 type AccountI interface {
+	GetModuleAccount(ctx sdk.Context, moduleName string) authtypes.ModuleAccountI
 	NewAccount(sdk.Context, authtypes.AccountI) authtypes.AccountI
 	GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI
 	SetAccount(ctx sdk.Context, acc authtypes.AccountI)


Test patch:
diff --git a/app/modules_test.go b/app/modules_test.go
index 3790e3aae..e2e7cda98 100644
--- a/app/modules_test.go
+++ b/app/modules_test.go
@@ -12,7 +12,7 @@ import (
 
 func TestOrderEndBlockers_Determinism(t *testing.T) {
 	db := dbm.NewMemDB()
-	app := NewOsmosisApp(log.NewNopLogger(), db, nil, true, map[int64]bool{}, DefaultNodeHome, 5, simapp.EmptyAppOptions{}, GetWasmEnabledProposals(), EmptyWasmOpts)
+	app := NewOsmosisApp(log.NewNopLogger(), db, nil, true, map[int64]bool{}, DefaultNodeHome, 5, simapp.EmptyAppOptions{}, EmptyWasmOpts)
 
 	for i := 0; i < 1000; i++ {
 		a := OrderEndBlockers(app.mm.ModuleNames())
diff --git a/app/test_helpers.go b/app/test_helpers.go
index 331e7c158..83a12f8cb 100644
--- a/app/test_helpers.go
+++ b/app/test_helpers.go
@@ -29,7 +29,7 @@ func getDefaultGenesisStateBytes() []byte {
 // Setup initializes a new OsmosisApp.
 func Setup(isCheckTx bool) *OsmosisApp {
 	db := dbm.NewMemDB()
-	app := NewOsmosisApp(log.NewNopLogger(), db, nil, true, map[int64]bool{}, DefaultNodeHome, 0, simapp.EmptyAppOptions{}, GetWasmEnabledProposals(), EmptyWasmOpts)
+	app := NewOsmosisApp(log.NewNopLogger(), db, nil, true, map[int64]bool{}, DefaultNodeHome, 0, simapp.EmptyAppOptions{}, EmptyWasmOpts)
 	if !isCheckTx {
 		stateBytes := getDefaultGenesisStateBytes()
 
@@ -56,7 +56,7 @@ func SetupTestingAppWithLevelDb(isCheckTx bool) (app *OsmosisApp, cleanupFn func
 	if err != nil {
 		panic(err)
 	}
-	app = NewOsmosisApp(log.NewNopLogger(), db, nil, true, map[int64]bool{}, DefaultNodeHome, 5, simapp.EmptyAppOptions{}, GetWasmEnabledProposals(), EmptyWasmOpts)
+	app = NewOsmosisApp(log.NewNopLogger(), db, nil, true, map[int64]bool{}, DefaultNodeHome, 5, simapp.EmptyAppOptions{}, EmptyWasmOpts)
 	if !isCheckTx {
 		genesisState := NewDefaultGenesisState()
 		stateBytes, err := json.MarshalIndent(genesisState, "", " ")
diff --git a/app/upgrades/v16/concentrated_pool_test.go b/app/upgrades/v16/concentrated_pool_test.go
new file mode 100644
index 000000000..3f085cfd2
--- /dev/null
+++ b/app/upgrades/v16/concentrated_pool_test.go
@@ -0,0 +1,260 @@
+package v16_test
+
+import (
+	"testing"
+	"time"
+
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	"github.com/stretchr/testify/suite"
+
+	"github.com/osmosis-labs/osmosis/v15/app/apptesting"
+	v16 "github.com/osmosis-labs/osmosis/v15/app/upgrades/v16"
+	gammtypes "github.com/osmosis-labs/osmosis/v15/x/gamm/types"
+	poolincentivestypes "github.com/osmosis-labs/osmosis/v15/x/pool-incentives/types"
+)
+
+type ConcentratedUpgradeTestSuite struct {
+	apptesting.KeeperTestHelper
+}
+
+const (
+	validPoolId = uint64(1)
+)
+
+var (
+	defaultAmount     = sdk.NewInt(100)
+	desiredDenom0     = v16.DesiredDenom0
+	uusdDenom         = "uusd"
+	udaiDenom         = "udai"
+	desiredDenom0Coin = sdk.NewCoin(desiredDenom0, defaultAmount)
+	coinB             = sdk.NewCoin("uatom", defaultAmount)
+	coinC             = sdk.NewCoin(uusdDenom, defaultAmount)
+)
+
+func (suite *ConcentratedUpgradeTestSuite) SetupTest() {
+	suite.Setup()
+}
+
+func TestConcentratedUpgradeTestSuite(t *testing.T) {
+	suite.Run(t, new(ConcentratedUpgradeTestSuite))
+}
+
+func (suite *ConcentratedUpgradeTestSuite) TestCreateConcentratedPoolFromCFMM() {
+	tests := map[string]struct {
+		poolLiquidity sdk.Coins
+
+		cfmmPoolIdToLinkWith uint64
+		desiredDenom0        string
+		expectedDenoms       []string
+		expectError          error
+	}{
+		"success": {
+			poolLiquidity:        sdk.NewCoins(desiredDenom0Coin, coinB),
+			cfmmPoolIdToLinkWith: validPoolId,
+			desiredDenom0:        desiredDenom0,
+			expectedDenoms:       []string{desiredDenom0, coinB.Denom},
+		},
+		"error: invalid denom 0": {
+			poolLiquidity:        sdk.NewCoins(desiredDenom0Coin, coinB),
+			cfmmPoolIdToLinkWith: validPoolId,
+			desiredDenom0:        uusdDenom,
+			expectError:          v16.NoDesiredDenomInPoolError{uusdDenom},
+		},
+		"error: pool with 3 assets, must have two": {
+			poolLiquidity:        sdk.NewCoins(desiredDenom0Coin, coinB, coinC),
+			cfmmPoolIdToLinkWith: validPoolId,
+			desiredDenom0:        uusdDenom,
+			expectError:          v16.ErrMustHaveTwoDenoms,
+		},
+	}
+
+	for name, tc := range tests {
+		tc := tc
+		suite.Run(name, func() {
+			suite.SetupTest()
+
+			balancerId := suite.PrepareBalancerPoolWithCoins(tc.poolLiquidity...)
+
+			balancerPool, err := suite.App.PoolManagerKeeper.GetPool(suite.Ctx, balancerId)
+			suite.Require().NoError(err)
+
+			clPoolReturned, err := v16.CreateConcentratedPoolFromCFMM(suite.Ctx, tc.cfmmPoolIdToLinkWith, tc.desiredDenom0, *suite.App.AccountKeeper, *suite.App.GAMMKeeper, *suite.App.PoolManagerKeeper)
+
+			if tc.expectError != nil {
+				suite.Require().Error(err)
+				suite.Require().Nil(clPoolReturned)
+				return
+			}
+			suite.Require().NoError(err)
+
+			// Validate that pool saved in state is the same as the one returned
+			clPoolInState, err := suite.App.PoolManagerKeeper.GetPool(suite.Ctx, clPoolReturned.GetId())
+			suite.Require().NoError(err)
+			suite.Require().Equal(clPoolReturned, clPoolInState)
+
+			// Validate CL and balancer pools have the same swap fee.
+			suite.Require().Equal(balancerPool.GetSwapFee(suite.Ctx), clPoolReturned.GetSwapFee(suite.Ctx))
+
+			// Validate that CL and balancer pools have the same denoms
+			balancerDenoms, err := suite.App.PoolManagerKeeper.RouteGetPoolDenoms(suite.Ctx, balancerPool.GetId())
+			suite.Require().NoError(err)
+
+			concentratedDenoms, err := suite.App.PoolManagerKeeper.RouteGetPoolDenoms(suite.Ctx, clPoolReturned.GetId())
+			suite.Require().NoError(err)
+
+			// Order between balancer and concentrated might differ
+			// because balancer lexicographically orders denoms but CL does not.
+			suite.Require().ElementsMatch(balancerDenoms, concentratedDenoms)
+			suite.Require().Equal(tc.expectedDenoms, concentratedDenoms)
+		})
+	}
+}
+
+func (suite *ConcentratedUpgradeTestSuite) TestCreateCanonicalConcentratedLiuqidityPoolAndMigrationLink() {
+	suite.Setup()
+
+	lockableDurations := suite.App.PoolIncentivesKeeper.GetLockableDurations(suite.Ctx)
+	longestLockableDuration := lockableDurations[len(lockableDurations)-1]
+
+	tests := map[string]struct {
+		poolLiquidity              sdk.Coins
+		cfmmPoolIdToLinkWith       uint64
+		desiredDenom0              string
+		expectedBalancerDenoms     []string
+		expectedConcentratedDenoms []string
+		setupInvalidDuraitons      bool
+		expectError                error
+	}{
+		"success - denoms reordered relative to balancer": {
+			poolLiquidity:        sdk.NewCoins(desiredDenom0Coin, coinB),
+			cfmmPoolIdToLinkWith: validPoolId,
+			// lexicographically ordered
+			expectedBalancerDenoms: []string{coinB.Denom, desiredDenom0Coin.Denom},
+			// determined by desired denom 0
+			expectedConcentratedDenoms: []string{desiredDenom0Coin.Denom, coinB.Denom},
+			desiredDenom0:              desiredDenom0,
+		},
+		"success - denoms are not reordered relative to balancer": {
+			poolLiquidity:        sdk.NewCoins(desiredDenom0Coin, coinB),
+			cfmmPoolIdToLinkWith: validPoolId,
+			// lexicographically ordered
+			expectedBalancerDenoms: []string{coinB.Denom, desiredDenom0Coin.Denom},
+			// determined by desired denom 0
+			expectedConcentratedDenoms: []string{coinB.Denom, desiredDenom0Coin.Denom},
+			desiredDenom0:              coinB.Denom,
+		},
+		"error: invalid denom 0": {
+			poolLiquidity:        sdk.NewCoins(desiredDenom0Coin, coinB),
+			cfmmPoolIdToLinkWith: validPoolId,
+			desiredDenom0:        uusdDenom,
+			expectError:          v16.NoDesiredDenomInPoolError{uusdDenom},
+		},
+		"error: pool with 3 assets, must have two": {
+			poolLiquidity:        sdk.NewCoins(desiredDenom0Coin, coinB, coinC),
+			cfmmPoolIdToLinkWith: validPoolId,
+			desiredDenom0:        uusdDenom,
+			expectError:          v16.ErrMustHaveTwoDenoms,
+		},
+		"error: invalid denom durations": {
+			poolLiquidity:         sdk.NewCoins(desiredDenom0Coin, coinB),
+			cfmmPoolIdToLinkWith:  validPoolId,
+			desiredDenom0:         desiredDenom0,
+			setupInvalidDuraitons: true,
+			expectError:           v16.ErrNoGaugeToRedirect,
+		},
+	}
+
+	for name, tc := range tests {
+		tc := tc
+		suite.Run(name, func() {
+			suite.SetupTest()
+
+			if tc.setupInvalidDuraitons {
+				// Overwrite default lockable durations.
+				suite.App.PoolIncentivesKeeper.SetLockableDurations(suite.Ctx, []time.Duration{})
+			}
+
+			balancerId := suite.PrepareBalancerPoolWithCoins(tc.poolLiquidity...)
+
+			// Another pool for testing that its gauge linkes are unchanged
+			balancerId2 := suite.PrepareBalancerPoolWithCoins(tc.poolLiquidity...)
+
+			balancerPool, err := suite.App.PoolManagerKeeper.GetPool(suite.Ctx, balancerId)
+			suite.Require().NoError(err)
+
+			// Get balancer gauges.
+			gaugeToRedirect, err := suite.App.PoolIncentivesKeeper.GetPoolGaugeId(suite.Ctx, balancerPool.GetId(), longestLockableDuration)
+
+			gaugeToNotRedeirect, err := suite.App.PoolIncentivesKeeper.GetPoolGaugeId(suite.Ctx, balancerId2, longestLockableDuration)
+
+			originalDistrInfo := poolincentivestypes.DistrInfo{
+				TotalWeight: sdk.NewInt(100),
+				Records: []poolincentivestypes.DistrRecord{
+					{
+						GaugeId: gaugeToRedirect,
+						Weight:  sdk.NewInt(50),
+					},
+					{
+						GaugeId: gaugeToNotRedeirect,
+						Weight:  sdk.NewInt(50),
+					},
+				},
+			}
+			suite.App.PoolIncentivesKeeper.SetDistrInfo(suite.Ctx, originalDistrInfo)
+
+			err = v16.CreateCanonicalConcentratedLiuqidityPoolAndMigrationLink(suite.Ctx, tc.cfmmPoolIdToLinkWith, tc.desiredDenom0, &suite.App.AppKeepers)
+
+			if tc.expectError != nil {
+				suite.Require().Error(err)
+				return
+			}
+			suite.Require().NoError(err)
+
+			// Get the new concentrated pool.
+			// Note, + 2 becuse we create 2 balancer pools during test setup, and 1 concentrated pool during migration.
+			clPoolInState, err := suite.App.PoolManagerKeeper.GetPool(suite.Ctx, validPoolId+2)
+			suite.Require().NoError(err)
+
+			// Validate that CL and balancer pools have the same denoms
+			balancerDenoms, err := suite.App.PoolManagerKeeper.RouteGetPoolDenoms(suite.Ctx, balancerPool.GetId())
+			suite.Require().NoError(err)
+
+			concentratedDenoms, err := suite.App.PoolManagerKeeper.RouteGetPoolDenoms(suite.Ctx, clPoolInState.GetId())
+			suite.Require().NoError(err)
+
+			// This check does not guarantee order.
+			suite.Require().ElementsMatch(balancerDenoms, concentratedDenoms)
+
+			// Validate order of balancer denoms is lexicographically sorted.
+			suite.Require().Equal(tc.expectedBalancerDenoms, balancerDenoms)
+
+			// Validate order of concentrated pool denoms which might be different from balancer.
+			suite.Require().Equal(tc.expectedConcentratedDenoms, concentratedDenoms)
+
+			// Validate that CFMM gauge is linked to the new concentrated pool.
+			concentratedPoolGaugeId, err := suite.App.PoolIncentivesKeeper.GetPoolGaugeId(suite.Ctx, clPoolInState.GetId(), suite.App.IncentivesKeeper.GetEpochInfo(suite.Ctx).Duration)
+			suite.Require().NoError(err)
+
+			distrInfo := suite.App.PoolIncentivesKeeper.GetDistrInfo(suite.Ctx)
+			suite.Require().Equal(distrInfo.Records[0].GaugeId, concentratedPoolGaugeId)
+
+			// Validate that distribution record from another pool is not redirected.
+			suite.Require().Equal(distrInfo.Records[1].GaugeId, gaugeToNotRedeirect)
+
+			// Validate migration record.
+			migrationInfo := suite.App.GAMMKeeper.GetMigrationInfo(suite.Ctx)
+			suite.Require().Equal(migrationInfo, gammtypes.MigrationRecords{
+				BalancerToConcentratedPoolLinks: []gammtypes.BalancerToConcentratedPoolLink{
+					{
+						BalancerPoolId: balancerId,
+						ClPoolId:       clPoolInState.GetId(),
+					},
+				},
+			})
+
+			// Validate that old gauge still exist
+			_, err = suite.App.IncentivesKeeper.GetGaugeByID(suite.Ctx, gaugeToRedirect)
+			suite.Require().NoError(err)
+		})
+	}
+}
diff --git a/app/upgrades/v16/export_test.go b/app/upgrades/v16/export_test.go
new file mode 100644
index 000000000..d9de878a7
--- /dev/null
+++ b/app/upgrades/v16/export_test.go
@@ -0,0 +1,24 @@
+package v16
+
+import (
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
+
+	"github.com/osmosis-labs/osmosis/v15/app/keepers"
+	gammkeeper "github.com/osmosis-labs/osmosis/v15/x/gamm/keeper"
+	"github.com/osmosis-labs/osmosis/v15/x/poolmanager"
+	poolmanagertypes "github.com/osmosis-labs/osmosis/v15/x/poolmanager/types"
+)
+
+const (
+	DaiOsmoPoolId = daiOsmoPoolId
+	DesiredDenom0 = desiredDenom0
+)
+
+func CreateConcentratedPoolFromCFMM(ctx sdk.Context, cfmmPoolIdToLinkWith uint64, desiredDenom0 string, accountKeeper authkeeper.AccountKeeper, gammKeeper gammkeeper.Keeper, poolmanagerKeeper poolmanager.Keeper) (poolmanagertypes.PoolI, error) {
+	return createConcentratedPoolFromCFMM(ctx, cfmmPoolIdToLinkWith, desiredDenom0, accountKeeper, gammKeeper, poolmanagerKeeper)
+}
+
+func CreateCanonicalConcentratedLiuqidityPoolAndMigrationLink(ctx sdk.Context, cfmmPoolId uint64, desiredDenom0 string, keepers *keepers.AppKeepers) error {
+	return createCanonicalConcentratedLiquidityPoolAndMigrationLink(ctx, cfmmPoolId, desiredDenom0, keepers)
+}
diff --git a/app/upgrades/v16/upgrades_test.go b/app/upgrades/v16/upgrades_test.go
new file mode 100644
index 000000000..576dfa278
--- /dev/null
+++ b/app/upgrades/v16/upgrades_test.go
@@ -0,0 +1,126 @@
+package v16_test
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/cosmos/cosmos-sdk/store/prefix"
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"
+	"github.com/stretchr/testify/suite"
+	abci "github.com/tendermint/tendermint/abci/types"
+
+	"github.com/osmosis-labs/osmosis/v15/app/apptesting"
+	v16 "github.com/osmosis-labs/osmosis/v15/app/upgrades/v16"
+	cltypes "github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/types"
+	poolmanagertypes "github.com/osmosis-labs/osmosis/v15/x/poolmanager/types"
+)
+
+type UpgradeTestSuite struct {
+	apptesting.KeeperTestHelper
+}
+
+func (suite *UpgradeTestSuite) SetupTest() {
+	suite.Setup()
+}
+
+func TestUpgradeTestSuite(t *testing.T) {
+	suite.Run(t, new(UpgradeTestSuite))
+}
+
+const dummyUpgradeHeight = 5
+
+func dummyUpgrade(suite *UpgradeTestSuite) {
+	suite.Ctx = suite.Ctx.WithBlockHeight(dummyUpgradeHeight - 1)
+	plan := upgradetypes.Plan{Name: "v16", Height: dummyUpgradeHeight}
+	err := suite.App.UpgradeKeeper.ScheduleUpgrade(suite.Ctx, plan)
+	suite.Require().NoError(err)
+	plan, exists := suite.App.UpgradeKeeper.GetUpgradePlan(suite.Ctx)
+	suite.Require().True(exists)
+
+	suite.Ctx = suite.Ctx.WithBlockHeight(dummyUpgradeHeight)
+}
+
+func (suite *UpgradeTestSuite) TestUpgrade() {
+	upgradeSetup := func() {
+		// This is done to ensure that we run the InitGenesis() logic for the new modules
+		upgradeStoreKey := suite.App.AppKeepers.GetKey(upgradetypes.StoreKey)
+		store := suite.Ctx.KVStore(upgradeStoreKey)
+		versionStore := prefix.NewStore(store, []byte{upgradetypes.VersionMapByte})
+		versionStore.Delete([]byte(cltypes.ModuleName))
+	}
+
+	testCases := []struct {
+		name         string
+		pre_upgrade  func()
+		upgrade      func()
+		post_upgrade func()
+	}{
+		{
+			"Test that the upgrade succeeds",
+			func() {
+				upgradeSetup()
+
+				// Create earlier pools
+				for i := uint64(1); i < v16.DaiOsmoPoolId; i++ {
+					suite.PrepareBalancerPoolWithCoins(desiredDenom0Coin, coinB)
+				}
+
+				// Create DAI / OSMO pool
+				suite.PrepareBalancerPoolWithCoins(sdk.NewCoin(udaiDenom, desiredDenom0Coin.Amount), desiredDenom0Coin)
+			},
+			func() {
+				dummyUpgrade(suite)
+				suite.Require().NotPanics(func() {
+					suite.App.BeginBlocker(suite.Ctx, abci.RequestBeginBlock{})
+				})
+
+				// Validate CL pool was created.
+				concentratedPool, err := suite.App.PoolManagerKeeper.GetPool(suite.Ctx, v16.DaiOsmoPoolId+1)
+				suite.Require().NoError(err)
+				suite.Require().Equal(poolmanagertypes.Concentrated, concentratedPool.GetType())
+
+				// Validate that denom0 and denom1 were set correctly
+				concentratedTypePool, ok := concentratedPool.(cltypes.ConcentratedPoolExtension)
+				suite.Require().True(ok)
+				suite.Require().Equal(v16.DesiredDenom0, concentratedTypePool.GetToken0())
+				suite.Require().Equal(udaiDenom, concentratedTypePool.GetToken1())
+
+				// Validate that link was created.
+				migrationInfo := suite.App.GAMMKeeper.GetMigrationInfo(suite.Ctx)
+				suite.Require().Equal(1, len(migrationInfo.BalancerToConcentratedPoolLinks))
+
+				// Validate that the link is correct.
+				link := migrationInfo.BalancerToConcentratedPoolLinks[0]
+				suite.Require().Equal(v16.DaiOsmoPoolId, link.BalancerPoolId)
+				suite.Require().Equal(concentratedPool.GetId(), link.ClPoolId)
+			},
+			func() {
+			},
+		},
+		{
+			"Fails because CFMM pool is not found",
+			func() {
+				upgradeSetup()
+			},
+			func() {
+				dummyUpgrade(suite)
+				suite.Require().Panics(func() {
+					suite.App.BeginBlocker(suite.Ctx, abci.RequestBeginBlock{})
+				})
+			},
+			func() {
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		suite.Run(fmt.Sprintf("Case %s", tc.name), func() {
+			suite.SetupTest() // reset
+
+			tc.pre_upgrade()
+			tc.upgrade()
+			tc.post_upgrade()
+		})
+	}
+}
diff --git a/tests/cl-go-client/README.md b/tests/cl-go-client/README.md
index 1860f8675..75d7c6089 100644
--- a/tests/cl-go-client/README.md
+++ b/tests/cl-go-client/README.md
@@ -13,7 +13,12 @@ setting up a concentrated liquidity pool with positions.
 
 Make sure that you run `localosmosis` in the background and have keys
 added to your keyring with:
+
 ```bash
+make set-env localosmosis # sets environment to $HOME/.osmosisd-local
+
+make localnet-start
+
 make localnet-keys
 ```
 
@@ -22,14 +27,10 @@ See `tests/localosmosis` for more info.
 ## Running
 
 ```bash
-go run tests/cl-go-client/main.go
+make localnet-cl-create-positions
 ```
 
 In the current state, it does the following:
 - Queries status of the chain to make sure it's running.
 - Queries pool with id 1. If does not exist, creates it
 - Sets up one CL position
-
-## TODOs
-
-- Create many positions across multiple accounts with randomized parameters.
diff --git a/tests/cl-go-client/main.go b/tests/cl-go-client/main.go
index c7385e5e2..399250782 100644
--- a/tests/cl-go-client/main.go
+++ b/tests/cl-go-client/main.go
@@ -6,6 +6,7 @@ import (
 	"log"
 	"math"
 	"math/rand"
+	"os/user"
 	"strings"
 	"sync"
 	"time"
@@ -23,34 +24,37 @@ import (
 )
 
 const (
-	expectedPoolId     uint64 = 1
-	addressPrefix             = "osmo"
-	clientHomePath            = "/root/.osmosisd-local"
-	consensusFee              = "1500uosmo"
-	denom0                    = "uosmo"
-	denom1                    = "uion"
-	accountNamePrefix         = "lo-test"
-	numPositions              = 1_000
-	minAmountDeposited        = int64(1_000_000)
-	randSeed                  = 1
-	maxAmountDeposited        = 1_00_000_000
+	expectedPoolId           uint64 = 1
+	addressPrefix                   = "osmo"
+	localosmosisFromHomePath        = "/.osmosisd-local"
+	consensusFee                    = "1500uosmo"
+	denom0                          = "uosmo"
+	denom1                          = "uion"
+	accountNamePrefix               = "lo-test"
+	numPositions                    = 1_000
+	minAmountDeposited              = int64(1_000_000)
+	randSeed                        = 1
+	maxAmountDeposited              = 1_00_000_000
 )
 
 var (
 	defaultAccountName = fmt.Sprintf("%s%d", accountNamePrefix, 1)
 	exponentAtPriceOne = sdk.OneInt().Neg()
 	defaultMinAmount   = sdk.ZeroInt()
+	accountMutex       sync.Mutex
 )
 
 func main() {
 	ctx := context.Background()
 
+	clientHome := getClientHomePath()
+
 	// Create a Cosmos igniteClient instance
 	igniteClient, err := cosmosclient.New(
 		ctx,
 		cosmosclient.WithAddressPrefix(addressPrefix),
 		cosmosclient.WithKeyringBackend(cosmosaccount.KeyringTest),
-		cosmosclient.WithHome(clientHomePath),
+		cosmosclient.WithHome(clientHome),
 	)
 	if err != nil {
 		log.Fatal(err)
@@ -67,6 +71,13 @@ func main() {
 	// Instantiate a query client
 	clQueryClient := poolmanagerqueryproto.NewQueryClient(igniteClient.Context())
 
+	// Print warnings with common problems
+	log.Println(fmt.Sprintf("\n\n\nWARNING 1: your localosmosis and client home are assummed to be %s. Run 'osmosisd get-env' and confirm it matches the path you see printed here\n\n\n", clientHome))
+
+	log.Println(fmt.Sprintf("\n\n\nWARNING 2: you are attempting to interact with pool id %d.\nConfirm that the pool exists. if this is not the pool you want to interact with, please change the expectedPoolId variable in the code\n\n\n", expectedPoolId))
+
+	log.Println("\n\n\nWARNING 3: sometimes the script hangs when just started. In that case, kill it and restart\n\n\n")
+
 	// Query pool with id 1 and create new if does not exist.
 	_, err = clQueryClient.Pool(ctx, &poolmanagerqueryproto.PoolRequest{PoolId: expectedPoolId})
 	if err != nil {
@@ -84,41 +95,36 @@ func main() {
 
 	rand.Seed(randSeed)
 
-	var wg sync.WaitGroup
 	for i := 0; i < numPositions; i++ {
-		wg.Add(1)
-		go func(i int) {
-			defer wg.Done()
-			var (
-				// 1 to 9. These are localosmosis keyring test accounts with names such as:
-				// lo-test1
-				// lo-test2
-				// ...
-				randAccountNum = rand.Intn(8) + 1
-				accountName    = fmt.Sprintf("%s%d", accountNamePrefix, randAccountNum)
-				// minTick <= lowerTick <= upperTick
-				lowerTick = rand.Int63n(maxTick-minTick+1) + minTick
-				// lowerTick <= upperTick <= maxTick
-				upperTick = maxTick - rand.Int63n(int64(math.Abs(float64(maxTick-lowerTick))))
-
-				tokenDesired0 = sdk.NewCoin(denom0, sdk.NewInt(rand.Int63n(maxAmountDeposited)))
-				tokenDesired1 = sdk.NewCoin(denom1, sdk.NewInt(rand.Int63n(maxAmountDeposited)))
-			)
-
-			log.Println("creating position: pool id", expectedPoolId, "accountName", accountName, "lowerTick", lowerTick, "upperTick", upperTick, "token0Desired", tokenDesired0, "tokenDesired1", tokenDesired1, "defaultMinAmount", defaultMinAmount)
-
-			maxRetries := 100
-			for j := 0; j < maxRetries; j++ {
-				amt0, amt1, liquidity := createPosition(igniteClient, expectedPoolId, accountName, lowerTick, upperTick, tokenDesired0, tokenDesired1, defaultMinAmount, defaultMinAmount)
-				if err == nil {
-					log.Println("created position: amt0", amt0, "amt1", amt1, "liquidity", liquidity)
-					break
-				}
-				time.Sleep(8 * time.Second)
+		var (
+			// 1 to 9. These are localosmosis keyring test accounts with names such as:
+			// lo-test1
+			// lo-test2
+			// ...
+			randAccountNum = rand.Intn(8) + 1
+			accountName    = fmt.Sprintf("%s%d", accountNamePrefix, randAccountNum)
+			// minTick <= lowerTick <= upperTick
+			lowerTick = rand.Int63n(maxTick-minTick+1) + minTick
+			// lowerTick <= upperTick <= maxTick
+			upperTick = maxTick - rand.Int63n(int64(math.Abs(float64(maxTick-lowerTick))))
+
+			tokenDesired0 = sdk.NewCoin(denom0, sdk.NewInt(rand.Int63n(maxAmountDeposited)))
+			tokenDesired1 = sdk.NewCoin(denom1, sdk.NewInt(rand.Int63n(maxAmountDeposited)))
+		)
+
+		log.Println("creating position: pool id", expectedPoolId, "accountName", accountName, "lowerTick", lowerTick, "upperTick", upperTick, "token0Desired", tokenDesired0, "tokenDesired1", tokenDesired1, "defaultMinAmount", defaultMinAmount)
+
+		maxRetries := 100
+		for j := 0; j < maxRetries; j++ {
+			amt0, amt1, liquidity := createPosition(igniteClient, expectedPoolId, accountName, lowerTick, upperTick, tokenDesired0, tokenDesired1, defaultMinAmount, defaultMinAmount)
+			if err == nil {
+				log.Println("created position: amt0", amt0, "amt1", amt1, "liquidity", liquidity)
+				break
 			}
-		}(i)
+			time.Sleep(8 * time.Second)
+		}
 	}
-	wg.Wait()
+
 }
 
 func createPool(igniteClient cosmosclient.Client, accountName string) uint64 {
@@ -142,9 +148,13 @@ func createPool(igniteClient cosmosclient.Client, accountName string) uint64 {
 }
 
 func createPosition(client cosmosclient.Client, poolId uint64, senderKeyringAccountName string, lowerTick int64, upperTick int64, tokenDesired0, tokenDesired1 sdk.Coin, tokenMinAmount0, tokenMinAmount1 sdk.Int) (amountCreated0, amountCreated1 sdk.Int, liquidityCreated sdk.Dec) {
+	accountMutex.Lock() // Lock access to getAccountAddressFromKeyring
+	senderAddress := getAccountAddressFromKeyring(client, senderKeyringAccountName)
+	accountMutex.Unlock() // Unlock access to getAccountAddressFromKeyring
+
 	msg := &cltypes.MsgCreatePosition{
 		PoolId:          poolId,
-		Sender:          getAccountAddressFromKeyring(client, senderKeyringAccountName),
+		Sender:          senderAddress,
 		LowerTick:       lowerTick,
 		UpperTick:       upperTick,
 		TokenDesired0:   tokenDesired0,
@@ -153,6 +163,7 @@ func createPosition(client cosmosclient.Client, poolId uint64, senderKeyringAcco
 		TokenMinAmount1: tokenMinAmount1,
 	}
 	txResp, err := client.BroadcastTx(senderKeyringAccountName, msg)
+
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -175,3 +186,13 @@ func getAccountAddressFromKeyring(igniteClient cosmosclient.Client, accountName
 	}
 	return address
 }
+
+func getClientHomePath() string {
+	currentUser, err := user.Current()
+	if err != nil {
+		log.Fatal(err)
+		return ""
+	}
+
+	return currentUser.HomeDir + localosmosisFromHomePath
+}
diff --git a/tests/e2e/configurer/chain/commands.go b/tests/e2e/configurer/chain/commands.go
index 4f46cd927..c8dfcc78f 100644
--- a/tests/e2e/configurer/chain/commands.go
+++ b/tests/e2e/configurer/chain/commands.go
@@ -57,6 +57,18 @@ func (n *NodeConfig) CreateStableswapPool(poolFile, from string) uint64 {
 	return poolID
 }
 
+// CollectFees collects fees earned by concentrated position in range of [lowerTick; upperTick] in pool with id of poolId
+func (n *NodeConfig) CollectFees(from, positionIds string) {
+	n.LogActionF("collecting fees from concentrated position")
+	cmd := []string{"osmosisd", "tx", "concentratedliquidity", "collect-fees", positionIds, fmt.Sprintf("--from=%s", from)}
+	_, _, err := n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, cmd)
+	require.NoError(n.t, err)
+
+	n.LogActionF("successfully collected fees for account %s", from)
+}
+
+// CreateConcentratedPool creates a concentrated pool.
+// Returns pool id of newly created pool on success
 func (n *NodeConfig) CreateConcentratedPool(from, denom1, denom2 string, tickSpacing uint64, exponentAtPriceOne int64, swapFee string) uint64 {
 	n.LogActionF("creating concentrated pool")
 
@@ -69,6 +81,8 @@ func (n *NodeConfig) CreateConcentratedPool(from, denom1, denom2 string, tickSpa
 	return poolID
 }
 
+// CreateConcentratedPosition creates a concentrated position from [lowerTick; upperTick] in pool with id of poolId
+// token{0,1} - liquidity to create position with
 func (n *NodeConfig) CreateConcentratedPosition(from, lowerTick, upperTick string, token0, token1 string, token0MinAmt, token1MinAmt int64, poolId uint64) uint64 {
 	n.LogActionF("creating concentrated position")
 
diff --git a/tests/e2e/configurer/config/constants.go b/tests/e2e/configurer/config/constants.go
index a0d458534..7830b0613 100644
--- a/tests/e2e/configurer/config/constants.go
+++ b/tests/e2e/configurer/config/constants.go
@@ -26,7 +26,12 @@ var (
 	InitialMinDeposit = MinDepositValue / 4
 	// Minimum expedited deposit value for proposal to be submitted.
 	InitialMinExpeditedDeposit = MinExpeditedDepositValue / 4
-	// The first id of a pool create via CLI before starting an
+	// v16 upgrade specific canonical OSMO/DAI pool id.
+	// It is expected to create a concentrated liquidity pool
+	// associated with this balancer pool in the upgrade handler.
+	// This is meant to be removed post-v16.
+	DaiOsmoPoolIdv16 uint64
+	// A pool created via CLI before starting an
 	// upgrade.
 	PreUpgradePoolId uint64
 
diff --git a/tests/e2e/configurer/upgrade.go b/tests/e2e/configurer/upgrade.go
index 8f1020457..aeeb2f7fa 100644
--- a/tests/e2e/configurer/upgrade.go
+++ b/tests/e2e/configurer/upgrade.go
@@ -122,6 +122,11 @@ func (uc *UpgradeConfigurer) CreatePreUpgradeState() error {
 	chainA.SendIBC(chainB, chainB.NodeConfigs[0].PublicAddress, initialization.StakeToken)
 	chainB.SendIBC(chainA, chainA.NodeConfigs[0].PublicAddress, initialization.StakeToken)
 
+	// Create DAI/OSMO pool from v16 migration testing with superfluid enabled.
+	config.DaiOsmoPoolIdv16 = chainANode.CreateBalancerPool("daiosmov16.json", initialization.ValidatorWalletName)
+	daiOsmoShareDenom := fmt.Sprintf("gamm/pool/%d", config.DaiOsmoPoolIdv16)
+	chainA.EnableSuperfluidAsset(daiOsmoShareDenom)
+
 	config.PreUpgradePoolId = chainANode.CreateBalancerPool("pool1A.json", initialization.ValidatorWalletName)
 	poolShareDenom := fmt.Sprintf("gamm/pool/%d", config.PreUpgradePoolId)
 	chainBNode.CreateBalancerPool("pool1B.json", initialization.ValidatorWalletName)
diff --git a/tests/e2e/containers/config.go b/tests/e2e/containers/config.go
index d0084a777..d302e71e5 100644
--- a/tests/e2e/containers/config.go
+++ b/tests/e2e/containers/config.go
@@ -27,7 +27,7 @@ const (
 	previousVersionOsmoTag        = "v15.x-833705e4-1679340262"
 	// Pre-upgrade repo/tag for osmosis initialization (this should be one version below upgradeVersion)
 	previousVersionInitRepository = "osmolabs/osmosis-e2e-init-chain"
-	previousVersionInitTag        = "v15.x-833705e4-1679340262"
+	previousVersionInitTag        = "v15.x-5aa42ed-manual"
 	// Hermes repo/version for relayer
 	relayerRepository = "informalsystems/hermes"
 	relayerTag        = "1.3.0"
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
index 5869f8c7e..ac7b89813 100644
--- a/tests/e2e/e2e_test.go
+++ b/tests/e2e/e2e_test.go
@@ -19,8 +19,6 @@ import (
 
 	ibchookskeeper "github.com/osmosis-labs/osmosis/x/ibc-hooks/keeper"
 
-	"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/model"
-	"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/types"
 	ibcratelimittypes "github.com/osmosis-labs/osmosis/v15/x/ibc-rate-limit/types"
 
 	sdk "github.com/cosmos/cosmos-sdk/types"
@@ -191,26 +189,24 @@ func (s *IntegrationTestSuite) TestConcentratedLiquidity() {
 	s.Require().NoError(err)
 
 	var (
-		denom0                    string = "uion"
-		denom1                    string = "uosmo"
-		tickSpacing               uint64 = 1
-		exponentAtPriceOne        int64  = -1
-		swapFee                          = "0.01"
+		denom0             string  = "uion"
+		denom1             string  = "uosmo"
+		tickSpacing        uint64  = 1
+		exponentAtPriceOne int64   = -1
+		swapFee                    = "0.01"
+		swapFeeDec         sdk.Dec = sdk.MustNewDecFromStr("0.01")
 	)
 
-	// helpers
-	var (
-		updatedPool = func(poolId uint64) types.ConcentratedPoolExtension {
-			concentratedPool, err := node.QueryConcentratedPool(poolId)
-			s.Require().NoError(err)
-			return concentratedPool
-		}
-	)
 	poolID := node.CreateConcentratedPool(initialization.ValidatorWalletName, denom0, denom1, tickSpacing, exponentAtPriceOne, swapFee)
 
-	concentratedPool := updatedPool(poolID)
+	concentratedPool := s.updatedPool(node, poolID)
 
-	// assert contents of the pool are valid
+	// Sanity check that pool initialized with valid parameters (the ones that we haven't explicitly specified)
+	s.Require().Equal(concentratedPool.GetCurrentTick(), sdk.ZeroInt())
+	s.Require().Equal(concentratedPool.GetCurrentSqrtPrice(), sdk.ZeroDec())
+	s.Require().Equal(concentratedPool.GetLiquidity(), sdk.ZeroDec())
+
+	// Assert contents of the pool are valid (that we explicitly specified)
 	s.Require().Equal(concentratedPool.GetId(), poolID)
 	s.Require().Equal(concentratedPool.GetToken0(), denom0)
 	s.Require().Equal(concentratedPool.GetToken1(), denom1)
@@ -218,65 +214,438 @@ func (s *IntegrationTestSuite) TestConcentratedLiquidity() {
 	s.Require().Equal(concentratedPool.GetExponentAtPriceOne(), sdk.NewInt(exponentAtPriceOne))
 	s.Require().Equal(concentratedPool.GetSwapFee(sdk.Context{}), sdk.MustNewDecFromStr(swapFee))
 
+	// Concentrated Positions
 	minTick, maxTick := cl.GetMinAndMaxTicksFromExponentAtPriceOne(sdk.NewInt(exponentAtPriceOne))
 
-	fundTokens := []string{"1000000uosmo", "1000000uion", "1000000stake"}
-	// get 3 addresses to create positions
+	fundTokens := []string{"100000000uosmo", "100000000uion", "100000000stake"}
+
+	// Get 3 addresses to create positions
 	address1 := node.CreateWalletAndFund("addr1", fundTokens)
 	address2 := node.CreateWalletAndFund("addr2", fundTokens)
 	address3 := node.CreateWalletAndFund("addr3", fundTokens)
 
 	// Create 2 positions for address1: overlap together, overlap with 2 address3 positions
-	addr1PosId := node.CreateConcentratedPosition(address1, "[-1200]", "400", fmt.Sprintf("1000%s", denom0), fmt.Sprintf("1000%s", denom1), 0, 0, poolID)
-	node.CreateConcentratedPosition(address1, "[-400]", "400", fmt.Sprintf("1000%s", denom0), fmt.Sprintf("1000%s", denom1), 0, 0, poolID)
+	node.CreateConcentratedPosition(address1, "[-1200]", "400", fmt.Sprintf("10000000%s", denom0), fmt.Sprintf("10000000%s", denom1), 0, 0, poolID)
+	node.CreateConcentratedPosition(address1, "[-400]", "1200", fmt.Sprintf("10000000%s", denom0), fmt.Sprintf("10000000%s", denom1), 0, 0, poolID)
 
 	// Create 1 position for address2: does not overlap with anything, ends at maximum
-	addr2PosId := node.CreateConcentratedPosition(address2, "2200", fmt.Sprintf("%d", maxTick), fmt.Sprintf("1000%s", denom0), fmt.Sprintf("1000%s", denom1), 0, 0, poolID)
+	node.CreateConcentratedPosition(address2, "2200", fmt.Sprintf("%d", maxTick), fmt.Sprintf("10000000%s", denom0), fmt.Sprintf("10000000%s", denom1), 0, 0, poolID)
 
 	// Create 2 positions for address3: overlap together, overlap with 2 address1 positions, one position starts from minimum
-	addr3PosId := node.CreateConcentratedPosition(address3, "[-1600]", "[-200]", fmt.Sprintf("1000%s", denom0), fmt.Sprintf("1000%s", denom1), 0, 0, poolID)
-	node.CreateConcentratedPosition(address3, fmt.Sprintf("[%d]", minTick), "1400", fmt.Sprintf("1000%s", denom0), fmt.Sprintf("1000%s", denom1), 0, 0, poolID)
+	node.CreateConcentratedPosition(address3, "[-1600]", "[-200]", fmt.Sprintf("10000000%s", denom0), fmt.Sprintf("10000000%s", denom1), 0, 0, poolID)
+	node.CreateConcentratedPosition(address3, fmt.Sprintf("[%d]", minTick), "1400", fmt.Sprintf("10000000%s", denom0), fmt.Sprintf("10000000%s", denom1), 0, 0, poolID)
 
-	// get newly created positions
+	// Get newly created positions
 	positionsAddress1 := node.QueryConcentratedPositions(address1)
 	positionsAddress2 := node.QueryConcentratedPositions(address2)
 	positionsAddress3 := node.QueryConcentratedPositions(address3)
 
-	// assert number of positions per address
+	concentratedPool = s.updatedPool(node, poolID)
+
+	// Assert number of positions per address
 	s.Require().Equal(len(positionsAddress1), 2)
 	s.Require().Equal(len(positionsAddress2), 1)
 	s.Require().Equal(len(positionsAddress3), 2)
 
-	// Assert returned positions:
-	validateCLPosition := func(position model.Position, poolId uint64, lowerTick, upperTick int64) {
-		s.Require().Equal(position.PoolId, poolId)
-		s.Require().Equal(position.LowerTick, int64(lowerTick))
-		s.Require().Equal(position.UpperTick, int64(upperTick))
-	}
-
-	// assert positions for address1
+	// Assert positions for address1
 	addr1position1 := positionsAddress1[0].Position
 	addr1position2 := positionsAddress1[1].Position
-	// first position first address
-	validateCLPosition(addr1position1, poolID, -1200, 400)
-	// second position second address
-	validateCLPosition(addr1position2, poolID, -400, 400)
+	// First position first address
+	s.validateCLPosition(addr1position1, poolID, -1200, 400)
+	// Second position second address
+	s.validateCLPosition(addr1position2, poolID, -400, 1200)
 
-	// assert positions for address2
+	// Assert positions for address2
 	addr2position1 := positionsAddress2[0].Position
-	// first position second address
-	validateCLPosition(addr2position1, poolID, 2200, maxTick)
+	// First position second address
+	s.validateCLPosition(addr2position1, poolID, 2200, maxTick)
 
-	// assert positions for address3
+	// Assert positions for address3
 	addr3position1 := positionsAddress3[0].Position
 	addr3position2 := positionsAddress3[1].Position
-	// first position third address
-	validateCLPosition(addr3position1, poolID, -1600, -200)
-	// second position third address
-	validateCLPosition(addr3position2, poolID, minTick, 1400)
+	// First position third address
+	s.validateCLPosition(addr3position1, poolID, -1600, -200)
+	// Second position third address
+	s.validateCLPosition(addr3position2, poolID, minTick, 1400)
+
+	// Collect Fees
+
+	var (
+		// feeGrowthGlobal is a variable for tracking global fee growth
+		feeGrowthGlobal = sdk.ZeroDec()
+		outMinAmt       = "1"
+	)
+
+	// Swap 1
+	// Not crossing initialized ticks => performed in one swap step
+	// Swap affects 3 positions: both that address1 has and one of address3's positions
+	// Asserts that fees are correctly collected for non cross-tick swaps
+	var (
+		// Swap parameters
+		uosmoInDec_Swap1 = sdk.NewDec(3465198)
+		uosmoIn_Swap1    = fmt.Sprintf("%suosmo", uosmoInDec_Swap1.String())
+	)
+	// Perform swap (not crossing initialized ticks)
+	node.SwapExactAmountIn(uosmoIn_Swap1, outMinAmt, fmt.Sprintf("%d", poolID), denom0, initialization.ValidatorWalletName)
+	// Calculate and track global fee growth for swap 1
+	feeGrowthGlobal.AddMut(calculateFeeGrowthGlobal(uosmoInDec_Swap1, swapFeeDec, concentratedPool.GetLiquidity()))
+
+	// Update pool and track liquidity and sqrt price
+	liquidityBeforeSwap := concentratedPool.GetLiquidity()
+	sqrtPriceBeforeSwap := concentratedPool.GetCurrentSqrtPrice()
+
+	concentratedPool = s.updatedPool(node, poolID)
+
+	liquidityAfterSwap := concentratedPool.GetLiquidity()
+	sqrtPriceAfterSwap := concentratedPool.GetCurrentSqrtPrice()
+
+	// Assert swaps don't change pool's liquidity amount
+	s.Require().Equal(liquidityAfterSwap, liquidityBeforeSwap)
+
+	// Assert current sqrt price
+	inAmountSubFee := uosmoInDec_Swap1.Mul(sdk.OneDec().Sub(swapFeeDec))
+	expectedSqrtPriceDelta := inAmountSubFee.QuoTruncate(concentratedPool.GetLiquidity()) // Δ(sqrtPrice) = Δy / L
+	expectedSqrtPrice := sqrtPriceBeforeSwap.Add(expectedSqrtPriceDelta)
+
+	s.Require().Equal(expectedSqrtPrice, sqrtPriceAfterSwap)
+
+	// Collect Fees: Swap 1
+
+	// Track balances for address1 position1
+	addr1BalancesBefore := s.addrBalance(node, address1)
+	node.CollectFees(address1, fmt.Sprint(positionsAddress1[0].Position.PositionId))
+	addr1BalancesAfter := s.addrBalance(node, address1)
+
+	// Assert that the balance changed only for tokenIn (uosmo)
+	s.assertBalancesInvariants(addr1BalancesBefore, addr1BalancesAfter, false, true)
+
+	// Assert Balances: Swap 1
+
+	// Calculate uncollected fees for address1 position1
+	feesUncollectedAddress1Position1_Swap1 := calculateUncollectedFees(
+		positionsAddress1[0].Position.Liquidity,
+		sdk.ZeroDec(), // no growth below
+		sdk.ZeroDec(), // no growth above
+		sdk.ZeroDec(), // no feeGrowthInsideLast - it is the first swap
+		feeGrowthGlobal,
+	)
+
+	// Assert
+	s.Require().Equal(
+		addr1BalancesBefore.AmountOf("uosmo").Add(feesUncollectedAddress1Position1_Swap1.TruncateInt()),
+		addr1BalancesAfter.AmountOf("uosmo"),
+	)
+
+	// Swap 2
+	//
+	// Cross-tick swap:
+	// * Part of swap happens in range of liquidity of 3 positions: both of address1 and one for address3 (until tick 400 - upper tick of address1 position1)
+	// * Another part happens in range of liquidity of 2 positions: one from address1 and address3
+	//
+	// Asserts:
+	// * Net liquidity is kicked out when crossing initialized tick
+	// * Liquidity of position that was kicked out after first swap step does not earn rewards from second swap step
+	// * Uncollected fees from multiple swaps are correctly summed up and collected
+
+	// tickOffset is a tick index after the next initialized tick to which this swap needs to move the current price
+	tickOffset := sdk.NewInt(3)
+	sqrtPriceBeforeSwap = concentratedPool.GetCurrentSqrtPrice()
+	liquidityBeforeSwap = concentratedPool.GetLiquidity()
+	nextInitTick := sdk.NewInt(400) // address1 position1's upper tick
+
+	// Calculate sqrtPrice after and at the next initialized tick (upperTick of address1 position1 - 400)
+	sqrtPriceAfterNextInitializedTick, err := cl.TickToSqrtPrice(nextInitTick.Add(tickOffset), sdk.NewInt(exponentAtPriceOne))
+	s.Require().NoError(err)
+	sqrtPriceAtNextInitializedTick, err := cl.TickToSqrtPrice(nextInitTick, sdk.NewInt(exponentAtPriceOne))
+	s.Require().NoError(err)
+
+	// Calculate Δ(sqrtPrice):
+	// deltaSqrtPriceAfterNextInitializedTick = ΔsqrtP(403) - ΔsqrtP(400)
+	// deltaSqrtPriceAtNextInitializedTick = ΔsqrtP(400) - ΔsqrtP(currentTick)
+	deltaSqrtPriceAfterNextInitializedTick := sqrtPriceAfterNextInitializedTick.Sub(sqrtPriceAtNextInitializedTick)
+	deltaSqrtPriceAtNextInitializedTick := sqrtPriceAtNextInitializedTick.Sub(sqrtPriceBeforeSwap)
+
+	// Calculate the amount of osmo required to:
+	// * amountInToGetToTickAfterInitialized - move price from next initialized tick (400) to destination tick (400 + tickOffset)
+	// * amountInToGetToNextInitTick - move price from current tick to next initialized tick
+	// Formula is as follows:
+	// Δy = L * Δ(sqrtPrice)
+	amountInToGetToTickAfterInitialized := deltaSqrtPriceAfterNextInitializedTick.Mul(liquidityBeforeSwap.Sub(positionsAddress1[0].Position.Liquidity))
+	amountInToGetToNextInitTick := deltaSqrtPriceAtNextInitializedTick.Mul(liquidityBeforeSwap)
+
+	var (
+		// Swap parameters
+
+		// uosmoInDec_Swap2_NoFee is calculated such that swapping this amount (not considering fee) moves the price over the next initialized tick
+		uosmoInDec_Swap2_NoFee = amountInToGetToNextInitTick.Add(amountInToGetToTickAfterInitialized)
+		uosmoInDec_Swap2       = uosmoInDec_Swap2_NoFee.Quo(sdk.OneDec().Sub(swapFeeDec)).TruncateDec() // account for swap fee of 1%
+		uosmoIn_Swap2          = fmt.Sprintf("%suosmo", uosmoInDec_Swap2.String())
+
+		feeGrowthGlobal_Swap1 = feeGrowthGlobal.Clone()
+	)
+	// Perform a swap
+	node.SwapExactAmountIn(uosmoIn_Swap2, outMinAmt, fmt.Sprintf("%d", poolID), denom0, initialization.ValidatorWalletName)
+
+	// Calculate the amount of liquidity of the position that was kicked out during swap (address1 position1)
+	liquidityOfKickedOutPosition := positionsAddress1[0].Position.Liquidity
+
+	// Update pool and track pool's liquidity
+	concentratedPool = s.updatedPool(node, poolID)
+
+	liquidityAfterSwap = concentratedPool.GetLiquidity()
+
+	// Assert that net liquidity of kicked out position was successfully removed from current pool's liquidity
+	s.Require().Equal(liquidityBeforeSwap.Sub(liquidityOfKickedOutPosition), liquidityAfterSwap)
+
+	// Collect fees: Swap 2
+
+	// Calculate fee charges per each step
+
+	// Step1: amountIn is uosmo tokens that are swapped + uosmo tokens that are paid for fee
+	// hasReachedTarget in SwapStep is true, hence, to find fees, calculate:
+	// feeCharge = amountIn * swapFee / (1 - swapFee)
+	feeCharge_Swap2_Step1 := amountInToGetToNextInitTick.Mul(swapFeeDec).Quo(sdk.OneDec().Sub(swapFeeDec))
+
+	// Step2: hasReachedTarget in SwapStep is false (nextTick is 1200), hence, to find fees, calculate:
+	// feeCharge = amountRemaining - amountOne
+	amountRemainingAfterStep1 := uosmoInDec_Swap2.Sub(amountInToGetToNextInitTick).Sub(feeCharge_Swap2_Step1)
+	feeCharge_Swap2_Step2 := amountRemainingAfterStep1.Sub(amountInToGetToTickAfterInitialized)
+
+	// per unit of virtual liquidity
+	feeCharge_Swap2_Step1.QuoMut(liquidityBeforeSwap)
+	feeCharge_Swap2_Step2.QuoMut(liquidityAfterSwap)
+
+	// Update feeGrowthGlobal
+	feeGrowthGlobal.AddMut(feeCharge_Swap2_Step1.Add(feeCharge_Swap2_Step2))
+
+	// Assert Balances: Swap 2
+
+	// Assert that address1 position1 earned fees only from first swap step
+
+	// Track balances for address1 position1
+	addr1BalancesBefore = s.addrBalance(node, address1)
+	node.CollectFees(address1, fmt.Sprint(positionsAddress1[0].Position.PositionId))
+	addr1BalancesAfter = s.addrBalance(node, address1)
+
+	// Assert that the balance changed only for tokenIn (uosmo)
+	s.assertBalancesInvariants(addr1BalancesBefore, addr1BalancesAfter, false, true)
+
+	// Calculate uncollected fees for position, which liquidity will only be live part of the swap
+	feesUncollectedAddress1Position1_Swap2 := calculateUncollectedFees(
+		positionsAddress1[0].Position.Liquidity,
+		sdk.ZeroDec(),
+		sdk.ZeroDec(),
+		calculateFeeGrowthInside(feeGrowthGlobal_Swap1, sdk.ZeroDec(), sdk.ZeroDec()),
+		feeGrowthGlobal_Swap1.Add(feeCharge_Swap2_Step1), // cannot use feeGrowthGlobal, it was already increased by second swap's step
+	)
+
+	// Assert
+	s.Require().Equal(
+		addr1BalancesBefore.AmountOf("uosmo").Add(feesUncollectedAddress1Position1_Swap2.TruncateInt()),
+		addr1BalancesAfter.AmountOf("uosmo"),
+	)
+
+	// Assert that address3 position2 earned rewards from first and second swaps
+
+	// Track balance off address3 position2: check that position that has not been kicked out earned full rewards
+	addr3BalancesBefore := s.addrBalance(node, address3)
+	node.CollectFees(address3, fmt.Sprint(positionsAddress3[1].Position.PositionId))
+	addr3BalancesAfter := s.addrBalance(node, address3)
+
+	// Calculate uncollected fees for address3 position2 earned from Swap 1
+	feesUncollectedAddress3Position2_Swap1 := calculateUncollectedFees(
+		positionsAddress3[1].Position.Liquidity,
+		sdk.ZeroDec(),
+		sdk.ZeroDec(),
+		sdk.ZeroDec(),
+		feeGrowthGlobal_Swap1,
+	)
+
+	// Calculate uncollected fees for address3 position2 (was active throughout both swap steps): Swap2
+	feesUncollectedAddress3Position2_Swap2 := calculateUncollectedFees(
+		positionsAddress3[1].Position.Liquidity,
+		sdk.ZeroDec(),
+		sdk.ZeroDec(),
+		calculateFeeGrowthInside(feeGrowthGlobal_Swap1, sdk.ZeroDec(), sdk.ZeroDec()),
+		feeGrowthGlobal,
+	)
+
+	// Total fees earned by address3 position2 from 2 swaps
+	totalUncollectedFeesAddress3Position2 := feesUncollectedAddress3Position2_Swap1.Add(feesUncollectedAddress3Position2_Swap2)
+
+	// Assert
+	s.Require().Equal(
+		addr3BalancesBefore.AmountOf("uosmo").Add(totalUncollectedFeesAddress3Position2.TruncateInt()),
+		addr3BalancesAfter.AmountOf("uosmo"),
+	)
+
+	// Swap 3
+	// Asserts:
+	// * swapping amountZero for amountOne works correctly
+	// * liquidity of positions that come in range are correctly kicked in
+
+	// tickOffset is a tick index after the next initialized tick to which this swap needs to move the current price
+	tickOffset = sdk.NewInt(3)
+	sqrtPriceBeforeSwap = concentratedPool.GetCurrentSqrtPrice()
+	liquidityBeforeSwap = concentratedPool.GetLiquidity()
+	nextInitTick = sdk.NewInt(400)
+
+	// Calculate amount required to get to
+	// 1) next initialized tick
+	// 2) tick below next initialized (-3)
+	// Using: CalcAmount0Delta = liquidity * ((sqrtPriceB - sqrtPriceA) / (sqrtPriceB * sqrtPriceA))
+
+	// Calculate sqrtPrice after and at the next initialized tick (which is upperTick of address1 position1 - 400)
+	sqrtPricebBelowNextInitializedTick, err := cl.TickToSqrtPrice(nextInitTick.Sub(tickOffset), sdk.NewInt(exponentAtPriceOne))
+	s.Require().NoError(err)
+	sqrtPriceAtNextInitializedTick, err = cl.TickToSqrtPrice(nextInitTick, sdk.NewInt(exponentAtPriceOne))
+	s.Require().NoError(err)
+
+	// Calculate numerators
+	numeratorBelowNextInitializedTick := sqrtPriceAtNextInitializedTick.Sub(sqrtPricebBelowNextInitializedTick)
+	numeratorNextInitializedTick := sqrtPriceBeforeSwap.Sub(sqrtPriceAtNextInitializedTick)
+
+	// Calculate denominators
+	denominatorBelowNextInitializedTick := sqrtPriceAtNextInitializedTick.Mul(sqrtPricebBelowNextInitializedTick)
+	denominatorNextInitializedTick := sqrtPriceBeforeSwap.Mul(sqrtPriceAtNextInitializedTick)
+
+	// Calculate fractions
+	fractionBelowNextInitializedTick := numeratorBelowNextInitializedTick.Quo(denominatorBelowNextInitializedTick)
+	fractionAtNextInitializedTick := numeratorNextInitializedTick.Quo(denominatorNextInitializedTick)
+
+	// Calculate amounts of uionIn needed
+	amountInToGetToTickBelowInitialized := liquidityBeforeSwap.Add(positionsAddress1[0].Position.Liquidity).Mul(fractionBelowNextInitializedTick)
+	amountInToGetToNextInitTick = liquidityBeforeSwap.Mul(fractionAtNextInitializedTick)
 
-	// Withdraw Position:
 	var (
+		// Swap parameters
+		uionInDec_Swap3_NoFee = amountInToGetToNextInitTick.Add(amountInToGetToTickBelowInitialized)  // amount of uion to move price from current to desired (not considering swapFee)
+		uionInDec_Swap3       = uionInDec_Swap3_NoFee.Quo(sdk.OneDec().Sub(swapFeeDec)).TruncateDec() // consider swapFee
+		uionIn_Swap3          = fmt.Sprintf("%suion", uionInDec_Swap3.String())
+
+		// Save variables from previous swaps
+		feeGrowthGlobal_Swap2                = feeGrowthGlobal.Clone()
+		feeGrowthInsideAddress1Position1Last = feeGrowthGlobal_Swap1.Add(feeCharge_Swap2_Step1)
+	)
+	// Collect fees for address1 position1 to avoid overhead computations (swap2 already asserted fees are aggregated correctly from multiple swaps)
+	node.CollectFees(address1, fmt.Sprint(positionsAddress1[0].Position.PositionId))
+
+	// Perform a swap
+	node.SwapExactAmountIn(uionIn_Swap3, outMinAmt, fmt.Sprintf("%d", poolID), denom1, initialization.ValidatorWalletName)
+
+	// Assert liquidity of kicked in position was successfully added to the pool
+	concentratedPool = s.updatedPool(node, poolID)
+
+	liquidityAfterSwap = concentratedPool.GetLiquidity()
+	s.Require().Equal(liquidityBeforeSwap.Add(positionsAddress1[0].Position.Liquidity), liquidityAfterSwap)
+
+	// Track balance of address1
+	addr1BalancesBefore = s.addrBalance(node, address1)
+	node.CollectFees(address1, fmt.Sprint(positionsAddress1[0].Position.PositionId))
+	addr1BalancesAfter = s.addrBalance(node, address1)
+
+	// Assert that the balance changed only for tokenIn (uion)
+	s.assertBalancesInvariants(addr1BalancesBefore, addr1BalancesAfter, true, false)
+
+	// Assert the amount of collected fees:
+
+	// Step1: amountIn is uion tokens that are swapped + uion tokens that are paid for fee
+	// hasReachedTarget in SwapStep is true, hence, to find fees, calculate:
+	// feeCharge = amountIn * swapFee / (1 - swapFee)
+	feeCharge_Swap3_Step1 := amountInToGetToNextInitTick.Mul(swapFeeDec).Quo(sdk.OneDec().Sub(swapFeeDec))
+
+	// Step2: hasReachedTarget in SwapStep is false (next initialized tick is -200), hence, to find fees, calculate:
+	// feeCharge = amountRemaining - amountZero
+	amountRemainingAfterStep1 = uionInDec_Swap3.Sub(amountInToGetToNextInitTick).Sub(feeCharge_Swap3_Step1)
+	feeCharge_Swap3_Step2 := amountRemainingAfterStep1.Sub(amountInToGetToTickBelowInitialized)
+
+	// Per unit of virtual liquidity
+	feeCharge_Swap3_Step1.QuoMut(liquidityBeforeSwap)
+	feeCharge_Swap3_Step2.QuoMut(liquidityAfterSwap)
+
+	// Update feeGrowthGlobal
+	feeGrowthGlobal.AddMut(feeCharge_Swap3_Step1.Add(feeCharge_Swap3_Step2))
+
+	// Assert position that was active throughout second swap step (address1 position1) only earned fees for this step:
+
+	// Only collects fees for second swap step
+	feesUncollectedAddress1Position1_Swap3 := calculateUncollectedFees(
+		positionsAddress1[0].Position.Liquidity,
+		sdk.ZeroDec(),
+		feeCharge_Swap2_Step2.Add(feeCharge_Swap3_Step1), // fees acquired by swap2 step2 and swap3 step1 (steps happened above upper tick of this position)
+		feeGrowthInsideAddress1Position1Last,             // feeGrowthInside from first and second swaps
+		feeGrowthGlobal,
+	)
+
+	// Assert
+	s.Require().Equal(
+		addr1BalancesBefore.AmountOf("uion").Add(feesUncollectedAddress1Position1_Swap3.TruncateInt()),
+		addr1BalancesAfter.AmountOf("uion"),
+	)
+
+	// Assert position that was active thoughout the whole swap:
+
+	// Track balance of address3
+	addr3BalancesBefore = s.addrBalance(node, address3)
+	node.CollectFees(address3, fmt.Sprint(positionsAddress3[1].Position.PositionId))
+	addr3BalancesAfter = s.addrBalance(node, address3)
+
+	// Assert that the balance changed only for tokenIn (uion)
+	s.assertBalancesInvariants(addr3BalancesBefore, addr3BalancesAfter, true, false)
+
+	// Was active throughout the whole swap, collects fees from 2 steps
+
+	// Step 1
+	feesUncollectedAddress3Position2_Swap3_Step1 := calculateUncollectedFees(
+		positionsAddress3[1].Position.Liquidity,
+		sdk.ZeroDec(), // no growth below
+		sdk.ZeroDec(), // no growth above
+		calculateFeeGrowthInside(feeGrowthGlobal_Swap2, sdk.ZeroDec(), sdk.ZeroDec()), // snapshot of fee growth at swap 2
+		feeGrowthGlobal.Sub(feeCharge_Swap3_Step2),                                    // step 1 hasn't earned fees from step 2
+	)
+
+	// Step 2
+	feesUncollectedAddress3Position2_Swap3_Step2 := calculateUncollectedFees(
+		positionsAddress3[1].Position.Liquidity,
+		sdk.ZeroDec(), // no growth below
+		sdk.ZeroDec(), // no growth above
+		calculateFeeGrowthInside(feeGrowthGlobal_Swap2, sdk.ZeroDec(), sdk.ZeroDec()), // snapshot of fee growth at swap 2
+		feeGrowthGlobal.Sub(feeCharge_Swap3_Step1),                                    // step 2 hasn't earned fees from step 1
+	)
+
+	// Calculate total fees acquired by address3 position2 from all swap steps
+	totalUncollectedFeesAddress3Position2 = feesUncollectedAddress3Position2_Swap3_Step1.Add(feesUncollectedAddress3Position2_Swap3_Step2)
+
+	// Assert
+	s.Require().Equal(
+		addr3BalancesBefore.AmountOf("uion").Add(totalUncollectedFeesAddress3Position2.TruncateInt()),
+		addr3BalancesAfter.AmountOf("uion"),
+	)
+
+	// Collect Fees: Sanity Checks
+
+	// Assert that positions, which were not included in swaps, were not affected
+
+	// Address3 Position1: [-1600; -200]
+	addr3BalancesBefore = s.addrBalance(node, address3)
+	node.CollectFees(address3, fmt.Sprint(positionsAddress3[0].Position.PositionId))
+	addr3BalancesAfter = s.addrBalance(node, address3)
+
+	// Assert that balances did not change for any token
+	s.assertBalancesInvariants(addr3BalancesBefore, addr3BalancesAfter, true, true)
+
+	// Address2's only position: [2200; 3420]
+	addr2BalancesBefore := s.addrBalance(node, address2)
+	node.CollectFees(address2, fmt.Sprint(positionsAddress2[0].Position.PositionId))
+	addr2BalancesAfter := s.addrBalance(node, address2)
+
+	// Assert the balances did not change for every token
+	s.assertBalancesInvariants(addr2BalancesBefore, addr2BalancesAfter, true, true)
+
+	// Withdraw Position
+
+	var (
+		// Withdraw Position parameters
 		defaultLiquidityRemoval string = "1000"
 	)
 
@@ -285,21 +654,21 @@ func (s *IntegrationTestSuite) TestConcentratedLiquidity() {
 	// Assert removing some liquidity
 	// address1: check removing some amount of liquidity
 	address1position1liquidityBefore := positionsAddress1[0].Position.Liquidity
-	node.WithdrawPosition(address1, defaultLiquidityRemoval, addr1PosId)
+	node.WithdrawPosition(address1, defaultLiquidityRemoval, positionsAddress1[0].Position.PositionId)
 	// assert
 	positionsAddress1 = node.QueryConcentratedPositions(address1)
 	s.Require().Equal(address1position1liquidityBefore, positionsAddress1[0].Position.Liquidity.Add(sdk.MustNewDecFromStr(defaultLiquidityRemoval)))
 
 	// address2: check removing some amount of liquidity
 	address2position1liquidityBefore := positionsAddress2[0].Position.Liquidity
-	node.WithdrawPosition(address2, defaultLiquidityRemoval, addr2PosId)
+	node.WithdrawPosition(address2, defaultLiquidityRemoval, positionsAddress2[0].Position.PositionId)
 	// assert
 	positionsAddress2 = node.QueryConcentratedPositions(address2)
 	s.Require().Equal(address2position1liquidityBefore, positionsAddress2[0].Position.Liquidity.Add(sdk.MustNewDecFromStr(defaultLiquidityRemoval)))
 
 	// address3: check removing some amount of liquidity
 	address3position1liquidityBefore := positionsAddress3[0].Position.Liquidity
-	node.WithdrawPosition(address3, defaultLiquidityRemoval, addr3PosId)
+	node.WithdrawPosition(address3, defaultLiquidityRemoval, positionsAddress3[0].Position.PositionId)
 	// assert
 	positionsAddress3 = node.QueryConcentratedPositions(address3)
 	s.Require().Equal(address3position1liquidityBefore, positionsAddress3[0].Position.Liquidity.Add(sdk.MustNewDecFromStr(defaultLiquidityRemoval)))
@@ -307,16 +676,15 @@ func (s *IntegrationTestSuite) TestConcentratedLiquidity() {
 	// Assert removing all liquidity
 	// address2: no more positions left
 	allLiquidityAddress2Position1 := positionsAddress2[0].Position.Liquidity
-	node.WithdrawPosition(address2, allLiquidityAddress2Position1.String(), addr2PosId)
+	node.WithdrawPosition(address2, allLiquidityAddress2Position1.String(), positionsAddress2[0].Position.PositionId)
 	positionsAddress2 = node.QueryConcentratedPositions(address2)
 	s.Require().Empty(positionsAddress2)
 
 	// address1: one position left
 	allLiquidityAddress1Position1 := positionsAddress1[0].Position.Liquidity
-	node.WithdrawPosition(address1, allLiquidityAddress1Position1.String(), addr1PosId)
+	node.WithdrawPosition(address1, allLiquidityAddress1Position1.String(), positionsAddress1[0].Position.PositionId)
 	positionsAddress1 = node.QueryConcentratedPositions(address1)
 	s.Require().Equal(len(positionsAddress1), 1)
-
 }
 
 func (s *IntegrationTestSuite) TestStableSwapPostUpgrade() {
diff --git a/tests/e2e/helpers_e2e_test.go b/tests/e2e/helpers_e2e_test.go
new file mode 100644
index 000000000..caaadd081
--- /dev/null
+++ b/tests/e2e/helpers_e2e_test.go
@@ -0,0 +1,89 @@
+package e2e
+
+import (
+	sdk "github.com/cosmos/cosmos-sdk/types"
+
+	"github.com/osmosis-labs/osmosis/v15/tests/e2e/configurer/chain"
+	"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/model"
+	"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/types"
+)
+
+var (
+	defaultFeePerTx = sdk.NewInt(1000)
+)
+
+// calculateFeeGrowthGlobal calculates fee growth global per unit of virtual liquidity based on swap parameters:
+// amountIn - amount being swapped
+// swapFee - pool's swap fee
+// poolLiquidity - current pool liquidity
+func calculateFeeGrowthGlobal(amountIn, swapFee, poolLiquidity sdk.Dec) sdk.Dec {
+	// First we get total fee charge for the swap (ΔY * swapFee)
+	feeChargeTotal := amountIn.Mul(swapFee)
+
+	// Calculating fee growth global (dividing by pool liquidity to find fee growth per unit of virtual liquidity)
+	feeGrowthGlobal := feeChargeTotal.Quo(poolLiquidity)
+	return feeGrowthGlobal
+}
+
+// calculateFeeGrowthInside calculates fee growth inside range per unit of virtual liquidity
+// feeGrowthGlobal - global fee growth per unit of virtual liquidity
+// feeGrowthBelow - fee growth below lower tick
+// feeGrowthAbove - fee growth above upper tick
+// Formula: feeGrowthGlobal - feeGrowthBelowLowerTick - feeGrowthAboveUpperTick
+func calculateFeeGrowthInside(feeGrowthGlobal, feeGrowthBelow, feeGrowthAbove sdk.Dec) sdk.Dec {
+	return feeGrowthGlobal.Sub(feeGrowthBelow).Sub(feeGrowthAbove)
+}
+
+// Assert balances that are not affected by swap:
+// * same amount of `stake` in balancesBefore and balancesAfter
+// * amount of `e2e-default-feetoken` dropped by 1000 (default amount for fee per tx)
+// * depending on `assertUosmoBalanceIsConstant` and `assertUionBalanceIsConstant` parameters, check that those balances have also not been changed
+func (s *IntegrationTestSuite) assertBalancesInvariants(balancesBefore, balancesAfter sdk.Coins, assertUosmoBalanceIsConstant, assertUionBalanceIsConstant bool) {
+	s.Require().True(balancesAfter.AmountOf("stake").Equal(balancesBefore.AmountOf("stake")))
+	s.Require().True(balancesAfter.AmountOf("e2e-default-feetoken").Equal(balancesBefore.AmountOf("e2e-default-feetoken").Sub(defaultFeePerTx)))
+	if assertUionBalanceIsConstant {
+		s.Require().True(balancesAfter.AmountOf("uion").Equal(balancesBefore.AmountOf("uion")))
+	}
+	if assertUosmoBalanceIsConstant {
+		s.Require().True(balancesAfter.AmountOf("uosmo").Equal(balancesBefore.AmountOf("uosmo")))
+	}
+}
+
+// Get balances for address
+func (s *IntegrationTestSuite) addrBalance(node *chain.NodeConfig, address string) sdk.Coins {
+	addrBalances, err := node.QueryBalances(address)
+	s.Require().NoError(err)
+	return addrBalances
+}
+
+// Helper function for calculating uncollected fees since the time that feeGrowthInsideLast corresponds to
+// positionLiquidity - current position liquidity
+// feeGrowthBelow - fee growth below lower tick
+// feeGrowthAbove - fee growth above upper tick
+// feeGrowthInsideLast - amount of fee growth inside range at the time from which we want to calculate the amount of uncollected fees
+// feeGrowthGlobal - variable for tracking global fee growth
+func calculateUncollectedFees(positionLiquidity, feeGrowthBelow, feeGrowthAbove, feeGrowthInsideLast sdk.Dec, feeGrowthGlobal sdk.Dec) sdk.Dec {
+	// Calculating fee growth inside range [-1200; 400]
+	feeGrowthInside := calculateFeeGrowthInside(feeGrowthGlobal, feeGrowthBelow, feeGrowthAbove)
+
+	// Calculating uncollected fees
+	// Formula for finding uncollected fees in time range [t1; t2]:
+	// F_u = position_liquidity * (fee_growth_inside_t2 - fee_growth_inside_t1).
+	feesUncollected := positionLiquidity.Mul(feeGrowthInside.Sub(feeGrowthInsideLast))
+
+	return feesUncollected.TruncateDec()
+}
+
+// Get current (updated) pool
+func (s *IntegrationTestSuite) updatedPool(node *chain.NodeConfig, poolId uint64) types.ConcentratedPoolExtension {
+	concentratedPool, err := node.QueryConcentratedPool(poolId)
+	s.Require().NoError(err)
+	return concentratedPool
+}
+
+// Assert returned positions:
+func (s *IntegrationTestSuite) validateCLPosition(position model.Position, poolId uint64, lowerTick, upperTick int64) {
+	s.Require().Equal(position.PoolId, poolId)
+	s.Require().Equal(position.LowerTick, int64(lowerTick))
+	s.Require().Equal(position.UpperTick, int64(upperTick))
+}
diff --git a/tests/e2e/initialization/config.go b/tests/e2e/initialization/config.go
index 7bc957ae5..b1db9d693 100644
--- a/tests/e2e/initialization/config.go
+++ b/tests/e2e/initialization/config.go
@@ -53,6 +53,7 @@ const (
 	IonDenom            = "uion"
 	StakeDenom          = "stake"
 	AtomDenom           = "uatom"
+	DaiDenom            = "udai"
 	OsmoIBCDenom        = "ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518"
 	StakeIBCDenom       = "ibc/C053D637CCA2A2BA030E2C5EE1B28A16F71CCB0E45E8BE52766DC1B241B7787"
 	E2EFeeToken         = "e2e-default-feetoken"
@@ -81,6 +82,8 @@ const (
 	EpochDayDuration      = time.Second * 60
 	EpochWeekDuration     = time.Second * 120
 	TWAPPruningKeepPeriod = EpochDayDuration / 4
+
+	DaiOsmoPoolId = 674
 )
 
 var (
@@ -89,6 +92,8 @@ var (
 	StakeAmountIntB  = sdk.NewInt(StakeAmountB)
 	StakeAmountCoinB = sdk.NewCoin(OsmoDenom, StakeAmountIntB)
 
+	DaiOsmoPoolBalances = fmt.Sprintf("%d%s", LuncBalanceA, DaiDenom)
+
 	InitBalanceStrA = fmt.Sprintf("%d%s,%d%s,%d%s,%d%s,%d%s", OsmoBalanceA, OsmoDenom, StakeBalanceA, StakeDenom, IonBalanceA, IonDenom, UstBalanceA, UstIBCDenom, LuncBalanceA, LuncIBCDenom)
 	InitBalanceStrB = fmt.Sprintf("%d%s,%d%s,%d%s", OsmoBalanceB, OsmoDenom, StakeBalanceB, StakeDenom, IonBalanceB, IonDenom)
 	OsmoToken       = sdk.NewInt64Coin(OsmoDenom, IbcSendAmount)  // 3,300uosmo
@@ -193,11 +198,11 @@ func initGenesis(chain *internalChain, votingPeriod, expeditedVotingPeriod time.
 	configDir := chain.nodes[0].configDir()
 	for _, val := range chain.nodes {
 		if chain.chainMeta.Id == ChainAID {
-			if err := addAccount(configDir, "", InitBalanceStrA, val.keyInfo.GetAddress(), forkHeight); err != nil {
+			if err := addAccount(configDir, "", InitBalanceStrA+","+DaiOsmoPoolBalances, val.keyInfo.GetAddress(), forkHeight); err != nil {
 				return err
 			}
 		} else if chain.chainMeta.Id == ChainBID {
-			if err := addAccount(configDir, "", InitBalanceStrB, val.keyInfo.GetAddress(), forkHeight); err != nil {
+			if err := addAccount(configDir, "", InitBalanceStrB+","+DaiOsmoPoolBalances, val.keyInfo.GetAddress(), forkHeight); err != nil {
 				return err
 			}
 		}
@@ -314,7 +319,7 @@ func initGenesis(chain *internalChain, votingPeriod, expeditedVotingPeriod time.
 
 func updateBankGenesis(appGenState map[string]json.RawMessage) func(s *banktypes.GenesisState) {
 	return func(bankGenState *banktypes.GenesisState) {
-		denomsToRegister := []string{StakeDenom, IonDenom, OsmoDenom, AtomDenom, LuncIBCDenom, UstIBCDenom}
+		denomsToRegister := []string{StakeDenom, IonDenom, OsmoDenom, AtomDenom, LuncIBCDenom, UstIBCDenom, DaiDenom}
 		for _, denom := range denomsToRegister {
 			setDenomMetadata(bankGenState, denom)
 		}
@@ -357,8 +362,8 @@ func updateStakeGenesis(stakeGenState *staketypes.GenesisState) {
 
 func updatePoolIncentiveGenesis(pooliGenState *poolitypes.GenesisState) {
 	pooliGenState.LockableDurations = []time.Duration{
+		time.Second * 60,
 		time.Second * 120,
-		time.Second * 180,
 		time.Second * 240,
 	}
 	pooliGenState.Params = poolitypes.Params{
@@ -368,9 +373,8 @@ func updatePoolIncentiveGenesis(pooliGenState *poolitypes.GenesisState) {
 
 func updateIncentivesGenesis(incentivesGenState *incentivestypes.GenesisState) {
 	incentivesGenState.LockableDurations = []time.Duration{
-		time.Second,
+		time.Second * 60,
 		time.Second * 120,
-		time.Second * 180,
 		time.Second * 240,
 	}
 	incentivesGenState.Params = incentivestypes.Params{
@@ -397,9 +401,15 @@ func updateGammGenesis(gammGenState *gammtypes.GenesisState) {
 
 	gammGenState.Pools = []*types1.Any{uosmoFeeTokenPool}
 
+	// Notice that this is non-inclusive. The DAI/OSMO pool should be created in the
+	// pre-upgrade logic of the upgrade configurer.
+	for poolId := uint64(2); poolId < DaiOsmoPoolId; poolId++ {
+		gammGenState.Pools = append(gammGenState.Pools, setupPool(poolId, OsmoDenom, AtomDenom))
+	}
+
 	// Note that we set the next pool number as 1 greater than the latest created pool.
 	// This is to ensure that migrations are performed correctly.
-	gammGenState.NextPoolNumber = 2
+	gammGenState.NextPoolNumber = DaiOsmoPoolId
 }
 
 func updatePoolManagerGenesis(appGenState map[string]json.RawMessage) func(*poolmanagertypes.GenesisState) {
diff --git a/tests/e2e/scripts/daiosmov16.json b/tests/e2e/scripts/daiosmov16.json
new file mode 100644
index 000000000..e9b70055d
--- /dev/null
+++ b/tests/e2e/scripts/daiosmov16.json
@@ -0,0 +1,7 @@
+{
+    "weights": "5udai,5uosmo",
+    "initial-deposit": "5000000000udai,5000000000uosmo",
+    "swap-fee": "0.01",
+    "exit-fee": "0.00",
+    "future-governor": ""
+}
diff --git a/tests/localosmosis/README.md b/tests/localosmosis/README.md
index 3ac706759..31461acd8 100644
--- a/tests/localosmosis/README.md
+++ b/tests/localosmosis/README.md
@@ -265,7 +265,7 @@ services:
     #     context: ../../
     #     dockerfile: Dockerfile
     #     args:
-    #     RUNNER_IMAGE: alpine:3.16
+    #     RUNNER_IMAGE: alpine:3.17
     #     GO_VERSION: 1.19
 ```
 
diff --git a/tests/localosmosis/docker-compose.yml b/tests/localosmosis/docker-compose.yml
index 6c10554a2..c4fb2de3d 100644
--- a/tests/localosmosis/docker-compose.yml
+++ b/tests/localosmosis/docker-compose.yml
@@ -8,7 +8,7 @@ services:
       context: ../../
       dockerfile: Dockerfile
       args:
-        RUNNER_IMAGE: alpine:3.16
+        RUNNER_IMAGE: alpine:3.17
         GO_VERSION: 1.19
     volumes:
       - ./scripts/nativeDenomPoolA.json:/osmosis/nativeDenomPoolA.json
diff --git a/tests/localosmosis/scripts/setup.sh b/tests/localosmosis/scripts/setup.sh
index a367fda70..579df3a14 100755
--- a/tests/localosmosis/scripts/setup.sh
+++ b/tests/localosmosis/scripts/setup.sh
@@ -105,6 +105,7 @@ add_genesis_accounts () {
 }
 
 edit_config () {
+
     # Remove seeds
     dasel put string -f $CONFIG_FOLDER/config.toml '.p2p.seeds' ''
 
@@ -112,6 +113,24 @@ edit_config () {
     dasel put string -f $CONFIG_FOLDER/config.toml '.rpc.laddr' "tcp://0.0.0.0:26657"
 }
 
+enable_cors () {
+
+    # Enable cors on RPC
+    dasel put string -f $CONFIG_FOLDER/config.toml -v "*" '.rpc.cors_allowed_origins.[]'
+    dasel put string -f $CONFIG_FOLDER/config.toml -v "Accept-Encoding" '.rpc.cors_allowed_headers.[]'
+    dasel put string -f $CONFIG_FOLDER/config.toml -v "DELETE" '.rpc.cors_allowed_methods.[]'
+    dasel put string -f $CONFIG_FOLDER/config.toml -v "OPTIONS" '.rpc.cors_allowed_methods.[]'
+    dasel put string -f $CONFIG_FOLDER/config.toml -v "PATCH" '.rpc.cors_allowed_methods.[]'
+    dasel put string -f $CONFIG_FOLDER/config.toml -v "PUT" '.rpc.cors_allowed_methods.[]'
+
+    # Enable unsafe cors and swagger on the api
+    dasel put bool -f $CONFIG_FOLDER/app.toml -v "true" '.api.swagger'
+    dasel put bool -f $CONFIG_FOLDER/app.toml -v "true" '.api.enabled-unsafe-cors'
+
+    # Enable cors on gRPC Web
+    dasel put bool -f $CONFIG_FOLDER/app.toml -v "true" '.grpc-web.enable-unsafe-cors'
+}
+
 run_with_retries() {
   cmd=$1
   success_msg=$2
@@ -154,7 +173,7 @@ create_concentrated_pool_positions () {
     COUNTER=0
     # Loop through each set of parameters in the array
     for param in "$@"; do
-        run_with_retries "osmosisd tx concentratedliquidity create-position $param 5000000000uosmo 1000000uion 0 0 0 --pool-id=4 --from pools --chain-id=$CHAIN_ID --home $OSMOSIS_HOME --keyring-backend=test -b block --fees 5000uosmo --gas 900000 --yes"
+        run_with_retries "osmosisd tx concentratedliquidity create-position $param 5000000000uosmo 1000000uion 0 0 --pool-id=4 --from pools --chain-id=$CHAIN_ID --home $OSMOSIS_HOME --keyring-backend=test -b block --fees 5000uosmo --gas 900000 --yes"
     done
 }
 
@@ -165,6 +184,7 @@ then
     edit_genesis
     add_genesis_accounts
     edit_config
+    enable_cors
 fi
 
 osmosisd start --home $OSMOSIS_HOME &
diff --git a/tests/localosmosis/state_export/docker-compose.yml b/tests/localosmosis/state_export/docker-compose.yml
index 220b8e755..6606e3675 100644
--- a/tests/localosmosis/state_export/docker-compose.yml
+++ b/tests/localosmosis/state_export/docker-compose.yml
@@ -8,7 +8,7 @@ services:
       context: ../../../
       dockerfile: Dockerfile
       args:
-        RUNNER_IMAGE: alpine:3.16
+        RUNNER_IMAGE: alpine:3.17
         GO_VERSION: 1.19
     volumes:
       - ./scripts/start.sh:/osmosis/start.sh
diff --git a/tests/localosmosis/state_export/scripts/start.sh b/tests/localosmosis/state_export/scripts/start.sh
index ffcade0f6..bbf826788 100755
--- a/tests/localosmosis/state_export/scripts/start.sh
+++ b/tests/localosmosis/state_export/scripts/start.sh
@@ -33,6 +33,24 @@ edit_config () {
     dasel put string -f $CONFIG_FOLDER/config.toml '.rpc.laddr' "tcp://0.0.0.0:26657"
 }
 
+enable_cors () {
+
+    # Enable cors on RPC
+    dasel put string -f $CONFIG_FOLDER/config.toml -v "*" '.rpc.cors_allowed_origins.[]'
+    dasel put string -f $CONFIG_FOLDER/config.toml -v "Accept-Encoding" '.rpc.cors_allowed_headers.[]'
+    dasel put string -f $CONFIG_FOLDER/config.toml -v "DELETE" '.rpc.cors_allowed_methods.[]'
+    dasel put string -f $CONFIG_FOLDER/config.toml -v "OPTIONS" '.rpc.cors_allowed_methods.[]'
+    dasel put string -f $CONFIG_FOLDER/config.toml -v "PATCH" '.rpc.cors_allowed_methods.[]'
+    dasel put string -f $CONFIG_FOLDER/config.toml -v "PUT" '.rpc.cors_allowed_methods.[]'
+
+    # Enable unsafe cors and swagger on the api
+    dasel put bool -f $CONFIG_FOLDER/app.toml -v "true" '.api.swagger'
+    dasel put bool -f $CONFIG_FOLDER/app.toml -v "true" '.api.enabled-unsafe-cors'
+
+    # Enable cors on gRPC Web
+    dasel put bool -f $CONFIG_FOLDER/app.toml -v "true" '.grpc-web.enable-unsafe-cors'
+}
+
 if [[ ! -d $CONFIG_FOLDER ]]
 then
 
@@ -67,6 +85,7 @@ then
     --prune-ibc
 
     edit_config
+    enable_cors
 fi
 
 osmosisd start --home $OSMOSIS_HOME --x-crisis-skip-assert-invariants
diff --git a/tests/localosmosis/state_export/scripts/testnetify.py b/tests/localosmosis/state_export/scripts/testnetify.py
index 3d273047a..0b11e4a89 100755
--- a/tests/localosmosis/state_export/scripts/testnetify.py
+++ b/tests/localosmosis/state_export/scripts/testnetify.py
@@ -27,24 +27,23 @@ config = {
     "epoch_duration": '21600s',
 }
 
-def replace(d, old_value, new_value):
+def replace(obj, old_value, new_value):
     """
-    Replace all the occurences of `old_value` with `new_value`
-    in `d` dictionary
+    Replace all the occurrences of `old_value` with `new_value`
+    in `obj`, which can be a dictionary or a list
     """
-    for k in d.keys():
-        if isinstance(d[k], dict):
-            replace(d[k], old_value, new_value)
-        elif isinstance(d[k], list):
-            for i in range(len(d[k])):
-                if isinstance(d[k][i], dict) or isinstance(d[k][i], list):
-                    replace(d[k][i], old_value, new_value)
-                else:
-                    if d[k][i] == old_value:
-                        d[k][i] = new_value
-        else:
-            if d[k] == old_value:
-                d[k] = new_value
+    if isinstance(obj, dict):
+        for key, value in obj.items():
+            if isinstance(value, (dict, list)):
+                replace(value, old_value, new_value)
+            elif value == old_value:
+                obj[key] = new_value
+    elif isinstance(obj, list):
+        for index, value in enumerate(obj):
+            if isinstance(value, (dict, list)):
+                replace(value, old_value, new_value)
+            elif value == old_value:
+                obj[index] = new_value
 
 def replace_validator(genesis, old_validator, new_validator):
     
diff --git a/tests/localrelayer/docker-compose.yml b/tests/localrelayer/docker-compose.yml
index f21c2c9b8..1aa7d1484 100644
--- a/tests/localrelayer/docker-compose.yml
+++ b/tests/localrelayer/docker-compose.yml
@@ -8,7 +8,7 @@ services:
       context: ../../
       dockerfile: Dockerfile
       args:
-        RUNNER_IMAGE: alpine:3.16
+        RUNNER_IMAGE: alpine:3.17
         GO_VERSION: 1.19
     volumes:
       - ./scripts/setup_chain.sh:/osmosis/setup.sh
@@ -59,7 +59,7 @@ services:
       context: ../../
       dockerfile: Dockerfile
       args:
-        RUNNER_IMAGE: alpine:3.16
+        RUNNER_IMAGE: alpine:3.17
         GO_VERSION: 1.19
     volumes:
       - ./scripts/setup_chain.sh:/osmosis/setup.sh
diff --git a/tests/simulator/osmosis_helper.go b/tests/simulator/osmosis_helper.go
index 652984544..c37df23f1 100644
--- a/tests/simulator/osmosis_helper.go
+++ b/tests/simulator/osmosis_helper.go
@@ -24,7 +24,6 @@ func OsmosisAppCreator(logger log.Logger, db db.DB) simtypes.AppCreator {
 			homepath,
 			legacyInvariantPeriod,
 			emptyAppOptions{},
-			app.GetWasmEnabledProposals(),
 			app.EmptyWasmOpts,
 			baseappOptions...)
 	}
diff --git a/x/concentrated-liquidity/export_test.go b/x/concentrated-liquidity/export_test.go
index c8034f92d..c37c108b7 100644
--- a/x/concentrated-liquidity/export_test.go
+++ b/x/concentrated-liquidity/export_test.go
@@ -8,7 +8,6 @@ import (
 	"github.com/osmosis-labs/osmosis/osmoutils/accum"
 	"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/model"
 	"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/types"
-	cltypes "github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/types"
 	poolmanagertypes "github.com/osmosis-labs/osmosis/v15/x/poolmanager/types"
 )
 
@@ -25,11 +24,6 @@ var (
 	FullyChargedDuration = types.SupportedUptimes[len(types.SupportedUptimes)-1]
 )
 
-// OrderInitialPoolDenoms sets the pool denoms of a cl pool
-func OrderInitialPoolDenoms(denom0, denom1 string) (string, string, error) {
-	return cltypes.OrderInitialPoolDenoms(denom0, denom1)
-}
-
 func (k Keeper) SetPool(ctx sdk.Context, pool types.ConcentratedPoolExtension) error {
 	return k.setPool(ctx, pool)
 }
diff --git a/x/concentrated-liquidity/fees_test.go b/x/concentrated-liquidity/fees_test.go
index 6f3201d31..88f79bb81 100644
--- a/x/concentrated-liquidity/fees_test.go
+++ b/x/concentrated-liquidity/fees_test.go
@@ -468,7 +468,7 @@ func (suite *KeeperTestSuite) TestGetInitialFeeGrowthOutsideForTick() {
 			ctx := suite.Ctx
 			clKeeper := suite.App.ConcentratedLiquidityKeeper
 
-			pool, err := clmodel.NewConcentratedLiquidityPool(validPoolId, USDC, ETH, DefaultTickSpacing, DefaultExponentAtPriceOne, DefaultZeroSwapFee)
+			pool, err := clmodel.NewConcentratedLiquidityPool(validPoolId, ETH, USDC, DefaultTickSpacing, DefaultExponentAtPriceOne, DefaultZeroSwapFee)
 			suite.Require().NoError(err)
 
 			// N.B.: we set the listener mock because we would like to avoid
diff --git a/x/concentrated-liquidity/keeper_test.go b/x/concentrated-liquidity/keeper_test.go
index f684087da..9dd4ef5b0 100644
--- a/x/concentrated-liquidity/keeper_test.go
+++ b/x/concentrated-liquidity/keeper_test.go
@@ -33,6 +33,7 @@ var (
 	DefaultZeroSwapFee                             = sdk.ZeroDec()
 	DefaultFeeAccumCoins                           = sdk.NewDecCoins(sdk.NewDecCoin("foo", sdk.NewInt(50)))
 	DefaultPositionId                              = uint64(1)
+	DefaultUnderlyingLockId                        = uint64(0)
 	DefaultJoinTime                                = time.Unix(0, 0)
 	ETH                                            = "eth"
 	DefaultAmt0                                    = sdk.NewInt(1000000)
diff --git a/x/concentrated-liquidity/model/pool_test.go b/x/concentrated-liquidity/model/pool_test.go
index 42356cc37..275e82eee 100644
--- a/x/concentrated-liquidity/model/pool_test.go
+++ b/x/concentrated-liquidity/model/pool_test.go
@@ -275,7 +275,7 @@ func (s *ConcentratedPoolTestSuite) TestNewConcentratedLiquidityPool() {
 			expectedTickSpacing: DefaultTickSpacing,
 		},
 		{
-			name: "Non lexicographical order of denoms should get reordered",
+			name: "Non lexicographical order of denoms should not get reordered",
 			param: param{
 				poolId:         DefaultValidPoolID,
 				denom0:         USDC,
@@ -285,8 +285,8 @@ func (s *ConcentratedPoolTestSuite) TestNewConcentratedLiquidityPool() {
 				swapFee:        sdk.ZeroDec(),
 			},
 			expectedPoolId:      DefaultValidPoolID,
-			expectedDenom0:      ETH,
-			expectedDenom1:      USDC,
+			expectedDenom0:      USDC,
+			expectedDenom1:      ETH,
 			expectedTickSpacing: DefaultTickSpacing,
 		},
 		{
@@ -323,7 +323,7 @@ func (s *ConcentratedPoolTestSuite) TestNewConcentratedLiquidityPool() {
 				precisionValue: DefaultExponentAtPriceOne,
 				swapFee:        DefaultSwapFee,
 			},
-			expectedErr: fmt.Errorf("cannot have the same asset in a single pool"),
+			expectedErr: types.MatchingDenomError{Denom: USDC},
 		},
 		{
 			name: "Error: negative swap fee",
diff --git a/x/concentrated-liquidity/pool_test.go b/x/concentrated-liquidity/pool_test.go
index cc6e2692a..91f25c3a1 100644
--- a/x/concentrated-liquidity/pool_test.go
+++ b/x/concentrated-liquidity/pool_test.go
@@ -11,21 +11,6 @@ import (
 	poolmanagertypes "github.com/osmosis-labs/osmosis/v15/x/poolmanager/types"
 )
 
-func (s *KeeperTestSuite) TestOrderInitialPoolDenoms() {
-	denom0, denom1, err := types.OrderInitialPoolDenoms("axel", "osmo")
-	s.Require().NoError(err)
-	s.Require().Equal(denom0, "axel")
-	s.Require().Equal(denom1, "osmo")
-
-	denom0, denom1, err = types.OrderInitialPoolDenoms("usdc", "eth")
-	s.Require().NoError(err)
-	s.Require().Equal(denom0, "eth")
-	s.Require().Equal(denom1, "usdc")
-
-	denom0, denom1, err = types.OrderInitialPoolDenoms("usdc", "usdc")
-	s.Require().Error(err)
-}
-
 func (s *KeeperTestSuite) TestInitializePool() {
 	// Create a valid PoolI from a valid ConcentratedPoolExtension
 	validConcentratedPool := s.PrepareConcentratedPool()
diff --git a/x/concentrated-liquidity/tick_test.go b/x/concentrated-liquidity/tick_test.go
index 824b52e47..38fab5901 100644
--- a/x/concentrated-liquidity/tick_test.go
+++ b/x/concentrated-liquidity/tick_test.go
@@ -38,7 +38,7 @@ func (s *KeeperTestSuite) TestTickOrdering() {
 	storeKey := sdk.NewKVStoreKey("concentrated_liquidity")
 	tKey := sdk.NewTransientStoreKey("transient_test")
 	s.Ctx = testutil.DefaultContext(storeKey, tKey)
-	s.App.ConcentratedLiquidityKeeper = cl.NewKeeper(s.App.AppCodec(), storeKey, s.App.BankKeeper, s.App.GetSubspace(types.ModuleName))
+	s.App.ConcentratedLiquidityKeeper = cl.NewKeeper(s.App.AppCodec(), storeKey, s.App.BankKeeper, s.App.LockupKeeper, s.App.GetSubspace(types.ModuleName))
 
 	liquidityTicks := []int64{-200, -55, -4, 70, 78, 84, 139, 240, 535}
 	for _, t := range liquidityTicks {
diff --git a/x/pool-incentives/keeper/keeper_test.go b/x/pool-incentives/keeper/keeper_test.go
index fcd50adf5..6b7fc23aa 100644
--- a/x/pool-incentives/keeper/keeper_test.go
+++ b/x/pool-incentives/keeper/keeper_test.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/osmosis-labs/osmosis/v15/app/apptesting"
 	gammtypes "github.com/osmosis-labs/osmosis/v15/x/gamm/types"
+	incentivestypes "github.com/osmosis-labs/osmosis/v15/x/incentives/types"
 	"github.com/osmosis-labs/osmosis/v15/x/pool-incentives/types"
 	poolmanagertypes "github.com/osmosis-labs/osmosis/v15/x/poolmanager/types"
 )
@@ -156,3 +157,48 @@ func (suite *KeeperTestSuite) TestCreatePoolGauges() {
 		})
 	}
 }
+
+func (suite *KeeperTestSuite) TestGetGaugesForCFMMPool() {
+	const validPoolId = 1
+
+	tests := map[string]struct {
+		poolId         uint64
+		expectedGauges incentivestypes.Gauge
+		expectError    error
+	}{
+		"valid pool id - gauges created": {
+			poolId: validPoolId,
+		},
+		"invalid pool id - error": {
+			poolId:      validPoolId + 1,
+			expectError: types.NoGaugeAssociatedWithPoolError{PoolId: 2, Duration: time.Hour},
+		},
+	}
+
+	for name, tc := range tests {
+		tc := tc
+		suite.Run(name, func() {
+			suite.SetupTest()
+
+			suite.PrepareBalancerPool()
+
+			gauges, err := suite.App.PoolIncentivesKeeper.GetGaugesForCFMMPool(suite.Ctx, tc.poolId)
+
+			if tc.expectError != nil {
+				suite.Require().Error(err)
+				suite.Require().ErrorIs(err, tc.expectError)
+				return
+			}
+
+			suite.Require().NoError(err)
+
+			// Validate that  3 gauges for each lockable duration were created.
+			suite.Require().Equal(3, len(gauges))
+			for i, lockableDuration := range suite.App.PoolIncentivesKeeper.GetLockableDurations(suite.Ctx) {
+				suite.Require().Equal(uint64(i+1), gauges[i].Id)
+				suite.Require().Equal(lockableDuration, gauges[i].DistributeTo.Duration)
+				suite.Require().True(gauges[i].IsActiveGauge(suite.Ctx.BlockTime()))
+			}
+		})
+	}
+}
diff --git a/x/poolmanager/create_pool_test.go b/x/poolmanager/create_pool_test.go
index 1c8499017..6593fe94f 100644
--- a/x/poolmanager/create_pool_test.go
+++ b/x/poolmanager/create_pool_test.go
@@ -171,7 +171,7 @@ func (suite *KeeperTestSuite) TestCreatePool() {
 			creatorFundAmount:  sdk.NewCoins(sdk.NewCoin(foo, defaultInitPoolAmount.Mul(sdk.NewInt(2))), sdk.NewCoin(bar, defaultInitPoolAmount.Mul(sdk.NewInt(2)))),
 			msg:                invalidBalancerPoolMsg,
 			expectedModuleType: gammKeeperType,
-			expectError: 		true,
+			expectError:        true,
 		},
 		// TODO: add stableswap test
 		// TODO: add concentrated-liquidity test
@@ -207,6 +207,84 @@ func (suite *KeeperTestSuite) TestCreatePool() {
 	}
 }
 
+// Tests that only poolmanager as a pool creator can create a pool via CreatePoolZeroLiquidityNoCreationFee
+func (suite *KeeperTestSuite) TestCreatePoolZeroLiquidityNoCreationFee() {
+	suite.SetupTest()
+
+	poolManagerModuleAcc := suite.App.AccountKeeper.GetModuleAccount(suite.Ctx, types.ModuleName)
+
+	withCreator := func(msg clmodel.MsgCreateConcentratedPool, address sdk.AccAddress) clmodel.MsgCreateConcentratedPool {
+		msg.Sender = address.String()
+		return msg
+	}
+
+	balancerPoolMsg := balancer.NewMsgCreateBalancerPool(poolManagerModuleAcc.GetAddress(), balancer.NewPoolParams(sdk.ZeroDec(), sdk.ZeroDec(), nil), []balancer.PoolAsset{
+		{
+			Token:  sdk.NewCoin(foo, defaultInitPoolAmount),
+			Weight: sdk.NewInt(1),
+		},
+		{
+			Token:  sdk.NewCoin(bar, defaultInitPoolAmount),
+			Weight: sdk.NewInt(1),
+		},
+	}, "")
+
+	concentratedPoolMsg := clmodel.NewMsgCreateConcentratedPool(poolManagerModuleAcc.GetAddress(), foo, bar, 1, DefaultExponentAtPriceOne, defaultPoolSwapFee)
+
+	tests := []struct {
+		name               string
+		msg                types.CreatePoolMsg
+		expectedModuleType reflect.Type
+		expectError        error
+	}{
+		{
+			name:               "pool manager creator for concentrated pool - success",
+			msg:                concentratedPoolMsg,
+			expectedModuleType: concentratedKeeperType,
+		},
+		{
+			name:        "creator is not pool manager - failure",
+			msg:         withCreator(concentratedPoolMsg, suite.TestAccs[0]),
+			expectError: types.InvalidPoolCreatorError{CreatorAddresss: suite.TestAccs[0].String()},
+		},
+		{
+			name:        "balancer pool with pool manager creator - error, wrong pool",
+			msg:         balancerPoolMsg,
+			expectError: types.InvalidPoolTypeError{PoolType: types.Balancer},
+		},
+	}
+
+	for i, tc := range tests {
+		suite.Run(tc.name, func() {
+			tc := tc
+
+			poolmanagerKeeper := suite.App.PoolManagerKeeper
+			ctx := suite.Ctx
+
+			// Note: this is necessary for gauge creation in the after pool created hook.
+			// There is a check requiring positive supply existing on-chain.
+			suite.MintCoins(sdk.NewCoins(sdk.NewCoin("uosmo", sdk.OneInt())))
+
+			pool, err := poolmanagerKeeper.CreateConcentratedPoolAsPoolManager(ctx, tc.msg)
+
+			if tc.expectError != nil {
+				suite.Require().Error(err)
+				suite.Require().ErrorIs(err, tc.expectError)
+				return
+			}
+
+			// Validate pool.
+			suite.Require().NoError(err)
+			suite.Require().Equal(uint64(i+1), pool.GetId())
+
+			// Validate that mapping pool id -> module type has been persisted.
+			swapModule, err := poolmanagerKeeper.GetPoolModule(ctx, pool.GetId())
+			suite.Require().NoError(err)
+			suite.Require().Equal(tc.expectedModuleType, reflect.TypeOf(swapModule))
+		})
+	}
+}
+
 func (suite *KeeperTestSuite) TestGetAllModuleRoutes() {
 	tests := []struct {
 		name         string