Repository: osmosis-labs/osmosis
Pull request #4359 (2023-02-17 17:23:43)

fix(x/twap): incorrect time delta due to nanoseconds in time
<!-- < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < ☺
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: #4342

## What is the purpose of the change

This PR fixes twap jitter that is caused by nanoseconds being accounted for in the time delta calculations.

In twap, we assume the time delta in milliseconds. However, the block time is given in nanoseconds. As a result, there were some cases such as:
```go
timeDelta := endRecord.Time.Sub(startRecord.Time)
```
where if `startRecord.Time` contained just a few nanoseconds, it would cause the timeDelta to fall by 1 ms below the expected value, causing a larger denominator in arithmetic mean computations. This would lead the final twap being off in the 10^-4-10^-2 digit.

To fix this, a function `types.CanonicalTimeMs` is introduced that converts the time stored in state into unix milliseconds with monotonic clock stripped away. This is consistent with how [CometBFT handles time](https://github.com/osmosis-labs/tendermint/blob/167fa738a379147f333b86f539bc03f597b978e5/libs/time/time.go#L17).

## Brief Changelog

- observed random failures in e2e twap tests in #4244 
- debugged with various logs added and manual Python recomputation of every step
- observed problems related to nanoseconds when computing time delta
- reproduced in unit tests
- refactored to strip away nanoseconds with the use of  `types.CanonicalTimeMs` function


## Testing and Verifying

This change added tests and is covered by e2e

## 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`? yes
  - How is the feature or change documented? not applicable


Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 9cbf78461..242b2c559 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -17,10 +17,10 @@
             "buildFlags": "-tags e2e",
             "env": {
                 "OSMOSIS_E2E": "True",
-                "OSMOSIS_E2E_SKIP_IBC": "false",
-                "OSMOSIS_E2E_SKIP_UPGRADE": "false",
+                "OSMOSIS_E2E_SKIP_IBC": "true",
+                "OSMOSIS_E2E_SKIP_UPGRADE": "true",
                 "OSMOSIS_E2E_SKIP_CLEANUP": "true",
-                "OSMOSIS_E2E_SKIP_STATE_SYNC": "false",
+                "OSMOSIS_E2E_SKIP_STATE_SYNC": "true",
                 "OSMOSIS_E2E_UPGRADE_VERSION": "v15",
                 "OSMOSIS_E2E_DEBUG_LOG": "false",
             },
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f18223bfb..6de61d14c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -70,6 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 * [#3715](https://github.com/osmosis-labs/osmosis/pull/3715) Fix x/gamm (golang API) CalculateSpotPrice, balancer.SpotPrice and Stableswap.SpotPrice base and quote asset.
 * [#3746](https://github.com/osmosis-labs/osmosis/pull/3746) Make ApplyFuncIfNoErr logic preserve panics for OutOfGas behavior.
 * [#4306](https://github.com/osmosis-labs/osmosis/pull/4306) Prevent adding more tokens to an already finished gauge
+* [#4359](https://github.com/osmosis-labs/osmosis/pull/4359) Fix incorrect time delta due to nanoseconds in time causing twap jitter.
+
 
 ## v14.0.1
 
diff --git a/app/upgrades/v15/upgrades.go b/app/upgrades/v15/upgrades.go
index c23248cf5..e041e437f 100644
--- a/app/upgrades/v15/upgrades.go
+++ b/app/upgrades/v15/upgrades.go
@@ -4,6 +4,8 @@ import (
 	sdk "github.com/cosmos/cosmos-sdk/types"
 	"github.com/cosmos/cosmos-sdk/types/module"
 	upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"
+	icqkeeper "github.com/strangelove-ventures/async-icq/v4/keeper"
+	icqtypes "github.com/strangelove-ventures/async-icq/v4/types"
 	packetforwardtypes "github.com/strangelove-ventures/packet-forward-middleware/v4/router/types"
 
 	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
@@ -18,6 +20,113 @@ import (
 	poolmanagertypes "github.com/osmosis-labs/osmosis/v14/x/poolmanager/types"
 )
 
+// an array of 10 strings containing the numbers from 1 to 10:
+
+// Using the same queries as stargate_whitelist.go
+var whitelistedQueries = []string{
+	// cosmos-sdk queries
+
+	// auth
+	"/cosmos.auth.v1beta1.Query/Account",
+	"/cosmos.auth.v1beta1.Query/Params",
+
+	// bank
+	"/cosmos.bank.v1beta1.Query/Balance",
+	"/cosmos.bank.v1beta1.Query/DenomMetadata",
+	"/cosmos.bank.v1beta1.Query/Params",
+	"/cosmos.bank.v1beta1.Query/SupplyOf",
+
+	// distribution
+	"/cosmos.distribution.v1beta1.Query/Params",
+	"/cosmos.distribution.v1beta1.Query/DelegatorWithdrawAddress",
+	"/cosmos.distribution.v1beta1.Query/ValidatorCommission",
+
+	// gov
+	"/cosmos.gov.v1beta1.Query/Deposit",
+	"/cosmos.gov.v1beta1.Query/Params",
+	"/cosmos.gov.v1beta1.Query/Vote",
+
+	//slashing
+	"/cosmos.slashing.v1beta1.Query/Params",
+	"/cosmos.slashing.v1beta1.Query/SigningInfo",
+
+	//staking
+	"/cosmos.staking.v1beta1.Query/Delegation",
+	"/cosmos.staking.v1beta1.Query/Params",
+	"/cosmos.staking.v1beta1.Query/Validator",
+
+	//osmosis queries
+
+	//epochs
+	"/osmosis.epochs.v1beta1.Query/EpochInfos",
+	"/osmosis.epochs.v1beta1.Query/CurrentEpoch",
+
+	//gamm
+	"/osmosis.gamm.v1beta1.Query/NumPools",
+	"/osmosis.gamm.v1beta1.Query/TotalLiquidity",
+	"/osmosis.gamm.v1beta1.Query/Pool",
+	"/osmosis.gamm.v1beta1.Query/PoolParams",
+	"/osmosis.gamm.v1beta1.Query/TotalPoolLiquidity",
+	"/osmosis.gamm.v1beta1.Query/TotalShares",
+	"/osmosis.gamm.v1beta1.Query/CalcJoinPoolShares",
+	"/osmosis.gamm.v1beta1.Query/CalcExitPoolCoinsFromShares",
+	"/osmosis.gamm.v1beta1.Query/CalcJoinPoolNoSwapShares",
+	"/osmosis.gamm.v1beta1.Query/PoolType",
+	"/osmosis.gamm.v2.Query/SpotPrice",
+	"/osmosis.gamm.v1beta1.Query/EstimateSwapExactAmountIn",
+	"/osmosis.gamm.v1beta1.Query/EstimateSwapExactAmountOut",
+
+	//incentives
+	"/osmosis.incentives.Query/ModuleToDistributeCoins",
+	"/osmosis.incentives.Query/LockableDurations",
+
+	//lockup
+	"/osmosis.lockup.Query/ModuleBalance",
+	"/osmosis.lockup.Query/ModuleLockedAmount",
+	"/osmosis.lockup.Query/AccountUnlockableCoins",
+	"/osmosis.lockup.Query/AccountUnlockingCoins",
+	"/osmosis.lockup.Query/LockedDenom",
+	"/osmosis.lockup.Query/LockedByID",
+
+	//mint
+	"/osmosis.mint.v1beta1.Query/EpochProvisions",
+	"/osmosis.mint.v1beta1.Query/Params",
+
+	//pool-incentives
+	"/osmosis.poolincentives.v1beta1.Query/GaugeIds",
+
+	//superfluid
+	"/osmosis.superfluid.Query/Params",
+	"/osmosis.superfluid.Query/AssetType",
+	"/osmosis.superfluid.Query/AllAssets",
+	"/osmosis.superfluid.Query/AssetMultiplier",
+
+	//poolmanager
+	"/osmosis.poolmanager.v1beta1.Query/NumPools",
+	"/osmosis.poolmanager.v1beta1.Query/EstimateSwapExactAmountIn",
+	"/osmosis.poolmanager.v1beta1.Query/EstimateSwapExactAmountOut",
+
+	//txfees
+	"/osmosis.txfees.v1beta1.Query/FeeTokens",
+	"/osmosis.txfees.v1beta1.Query/DenomSpotPrice",
+	"/osmosis.txfees.v1beta1.Query/DenomPoolId",
+	"/osmosis.txfees.v1beta1.Query/BaseDenom",
+
+	//tokenfactory
+	"/osmosis.tokenfactory.v1beta1.Query/params",
+	"/osmosis.tokenfactory.v1beta1.Query/DenomAuthorityMetadata",
+
+	//twap
+	"/osmosis.twap.v1beta1.Query/ArithmeticTwap",
+	"/osmosis.twap.v1beta1.Query/ArithmeticTwapToNow",
+	"/osmosis.twap.v1beta1.Query/GeometricTwap",
+	"/osmosis.twap.v1beta1.Query/GeometricTwapToNow",
+	"/osmosis.twap.v1beta1.Query/Params",
+
+	//downtime-detector
+	"/osmosis.downtimedetector.v1beta1.Query/RecoveredSinceDowntimeOfLength",
+}
+
 func CreateUpgradeHandler(
 	mm *module.Manager,
 	configurator module.Configurator,
@@ -29,6 +138,7 @@ func CreateUpgradeHandler(
 
 		keepers.PoolManagerKeeper.SetParams(ctx, poolmanagerParams)
 		keepers.PacketForwardKeeper.SetParams(ctx, packetforwardtypes.DefaultParams())
+		setICQParams(ctx, keepers.ICQKeeper)
 
 		// N.B: pool id in gamm is to be deprecated in the future
 		// Instead,it is moved to poolmanager.
@@ -47,6 +157,12 @@ func CreateUpgradeHandler(
 	}
 }
 
+func setICQParams(ctx sdk.Context, icqKeeper *icqkeeper.Keeper) {
+	icqparams := icqtypes.DefaultParams()
+	icqparams.AllowQueries = whitelistedQueries
+	icqKeeper.SetParams(ctx, icqparams)
+}
+
 func migrateNextPoolId(ctx sdk.Context, gammKeeper *gammkeeper.Keeper, poolmanagerKeeper *poolmanager.Keeper) {
 	// N.B: pool id in gamm is to be deprecated in the future
 	// Instead,it is moved to poolmanager.
diff --git a/go.mod b/go.mod
index 26d3621af..0d554fa18 100644
--- a/go.mod
+++ b/go.mod
@@ -5,13 +5,13 @@ go 1.19
 require (
 	github.com/CosmWasm/wasmd v0.30.0
 	github.com/cosmos/cosmos-proto v1.0.0-alpha8
-	github.com/cosmos/cosmos-sdk v0.46.8
+	github.com/cosmos/cosmos-sdk v0.46.10
 	github.com/cosmos/go-bip39 v1.0.0
 	github.com/cosmos/ibc-go/v4 v4.3.0
 	github.com/gogo/protobuf v1.3.3
 	github.com/golang/mock v1.6.0
 	github.com/golang/protobuf v1.5.2
-	github.com/golangci/golangci-lint v1.51.1
+	github.com/golangci/golangci-lint v1.51.2
 	github.com/gorilla/mux v1.8.0
 	github.com/grpc-ecosystem/grpc-gateway v1.16.0
 	github.com/iancoleman/orderedmap v0.2.0
@@ -19,7 +19,7 @@ require (
 	github.com/ory/dockertest/v3 v3.9.1
 	github.com/osmosis-labs/go-mutesting v0.0.0-20221208041716-b43bcd97b3b3
 	github.com/osmosis-labs/osmosis/osmomath v0.0.0-20230105183030-bccf5202f260
-	github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230202172735-45df290354da
+	github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230220020447-efe299ca44e0
 	github.com/osmosis-labs/osmosis/x/ibc-hooks v0.0.0-20230201094300-e8bba2b45fd8
 	github.com/pkg/errors v0.9.1
 	github.com/rakyll/statik v0.1.7
@@ -55,14 +55,14 @@ require (
 	github.com/docker/distribution v2.8.1+incompatible // indirect
 	github.com/felixge/httpsnoop v1.0.2 // indirect
 	github.com/google/btree v1.1.2 // indirect
-	github.com/junk1tm/musttag v0.4.4 // indirect
+	github.com/junk1tm/musttag v0.4.5 // indirect
 	github.com/kkHAIKE/contextcheck v1.1.3 // indirect
 	github.com/maratori/testableexamples v1.0.0 // indirect
 	github.com/nunnatsa/ginkgolinter v0.8.1 // indirect
 	github.com/nxadm/tail v1.4.8 // indirect
 	github.com/regen-network/cosmos-proto v0.3.1 // indirect
 	github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
-	github.com/sashamelentyev/usestdlibvars v1.21.1 // indirect
+	github.com/sashamelentyev/usestdlibvars v1.23.0 // indirect
 	github.com/sivchari/nosnakecase v1.7.0 // indirect
 	github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect
 	github.com/timonwong/loggercheck v0.9.3 // indirect
@@ -92,13 +92,13 @@ require (
 	github.com/Workiva/go-datastructures v1.0.53 // indirect
 	github.com/alexkohler/prealloc v1.0.0 // indirect
 	github.com/armon/go-metrics v0.4.1 // indirect
-	github.com/ashanbrown/forbidigo v1.3.0 // indirect
+	github.com/ashanbrown/forbidigo v1.4.0 // indirect
 	github.com/ashanbrown/makezero v1.1.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect
 	github.com/bkielbasa/cyclop v1.2.0 // indirect
 	github.com/blizzy78/varnamelen v0.8.0 // indirect
-	github.com/bombsimon/wsl/v3 v3.3.0 // indirect
+	github.com/bombsimon/wsl/v3 v3.4.0 // indirect
 	github.com/breml/bidichk v0.2.3 // indirect
 	github.com/breml/errchkjson v0.3.0 // indirect
 	github.com/btcsuite/btcd v0.22.2 // indirect
@@ -114,7 +114,7 @@ require (
 	github.com/cosmos/btcutil v1.0.5
 	github.com/cosmos/gorocksdb v1.2.0 // indirect
 	github.com/cosmos/ledger-cosmos-go v0.12.2 // indirect
-	github.com/daixiang0/gci v0.9.0 // indirect
+	github.com/daixiang0/gci v0.9.1 // indirect
 	github.com/danieljoos/wincred v1.1.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/denis-tingaikin/go-header v0.4.3 // indirect
@@ -134,17 +134,17 @@ require (
 	github.com/firefart/nonamedreturns v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.6.0 // indirect
 	github.com/fzipp/gocyclo v0.6.0 // indirect
-	github.com/go-critic/go-critic v0.6.5 // indirect
+	github.com/go-critic/go-critic v0.6.7 // indirect
 	github.com/go-kit/kit v0.12.0 // indirect
 	github.com/go-kit/log v0.2.1 // indirect
 	github.com/go-logfmt/logfmt v0.5.1 // indirect
-	github.com/go-toolsmith/astcast v1.0.0 // indirect
+	github.com/go-toolsmith/astcast v1.1.0 // indirect
 	github.com/go-toolsmith/astcopy v1.0.3 // indirect
-	github.com/go-toolsmith/astequal v1.0.3 // indirect
-	github.com/go-toolsmith/astfmt v1.0.0 // indirect
-	github.com/go-toolsmith/astp v1.0.0 // indirect
-	github.com/go-toolsmith/strparse v1.0.0 // indirect
-	github.com/go-toolsmith/typep v1.0.2 // indirect
+	github.com/go-toolsmith/astequal v1.1.0 // indirect
+	github.com/go-toolsmith/astfmt v1.1.0 // indirect
+	github.com/go-toolsmith/astp v1.1.0 // indirect
+	github.com/go-toolsmith/strparse v1.1.0 // indirect
+	github.com/go-toolsmith/typep v1.1.0 // indirect
 	github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect
 	github.com/gobwas/glob v0.2.3 // indirect
 	github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
@@ -234,13 +234,13 @@ require (
 	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
 	github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/polyfloyd/go-errorlint v1.0.6 // indirect
+	github.com/polyfloyd/go-errorlint v1.1.0 // indirect
 	github.com/prometheus/client_golang v1.14.0
 	github.com/prometheus/client_model v0.3.0 // indirect
 	github.com/prometheus/common v0.37.0 // indirect
 	github.com/prometheus/procfs v0.8.0 // indirect
-	github.com/quasilyte/go-ruleguard v0.3.18 // indirect
-	github.com/quasilyte/gogrep v0.0.0-20220828223005-86e4605de09f // indirect
+	github.com/quasilyte/go-ruleguard v0.3.19 // indirect
+	github.com/quasilyte/gogrep v0.5.0 // indirect
 	github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 // indirect
 	github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
 	github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
@@ -251,7 +251,7 @@ require (
 	github.com/ryanrolds/sqlclosecheck v0.4.0 // indirect
 	github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
 	github.com/sasha-s/go-deadlock v0.3.1 // indirect
-	github.com/securego/gosec/v2 v2.14.0 // indirect
+	github.com/securego/gosec/v2 v2.15.0 // indirect
 	github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
 	github.com/sirupsen/logrus v1.9.0 // indirect
 	github.com/sivchari/containedctx v1.0.2 // indirect
@@ -287,19 +287,19 @@ require (
 	gitlab.com/bosi/decorder v0.2.3 // indirect
 	go.etcd.io/bbolt v1.3.6 // indirect
 	go.opencensus.io v0.24.0 // indirect
-	golang.org/x/crypto v0.2.0 // indirect
-	golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect
-	golang.org/x/mod v0.7.0 // indirect
-	golang.org/x/net v0.5.0 // indirect
+	golang.org/x/crypto v0.5.0 // indirect
+	golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9 // indirect
+	golang.org/x/mod v0.8.0 // indirect
+	golang.org/x/net v0.6.0 // indirect
 	golang.org/x/sync v0.1.0 // indirect
-	golang.org/x/sys v0.4.0 // indirect
-	golang.org/x/term v0.4.0 // indirect
-	golang.org/x/text v0.6.0 // indirect
-	golang.org/x/tools v0.5.0 // indirect
+	golang.org/x/sys v0.5.0 // indirect
+	golang.org/x/term v0.5.0 // indirect
+	golang.org/x/text v0.7.0 // indirect
+	golang.org/x/tools v0.6.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
-	honnef.co/go/tools v0.4.0 // indirect
+	honnef.co/go/tools v0.4.2 // indirect
 	mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
 	mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
 	mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // indirect
diff --git a/go.sum b/go.sum
index 4ac24b297..de95a5b8c 100644
--- a/go.sum
+++ b/go.sum
@@ -125,8 +125,8 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ
 github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
-github.com/ashanbrown/forbidigo v1.3.0 h1:VkYIwb/xxdireGAdJNZoo24O4lmnEWkactplBlWTShc=
-github.com/ashanbrown/forbidigo v1.3.0/go.mod h1:vVW7PEdqEFqapJe95xHkTfB1+XvZXBFg8t0sG2FIxmI=
+github.com/ashanbrown/forbidigo v1.4.0 h1:spdPbupaSqtWORq1Q4eHBoPBmHtwVyLKwaedbSLc5Sw=
+github.com/ashanbrown/forbidigo v1.4.0/go.mod h1:IvgwB5Y4fzqSAj/WVXKWigoTkB0dzI2FBbpKWuh7ph8=
 github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s=
 github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI=
 github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
@@ -146,8 +146,8 @@ github.com/bkielbasa/cyclop v1.2.0 h1:7Jmnh0yL2DjKfw28p86YTd/B4lRGcNuu12sKE35sM7
 github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI=
 github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
 github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
-github.com/bombsimon/wsl/v3 v3.3.0 h1:Mka/+kRLoQJq7g2rggtgQsjuI/K5Efd87WX96EWFxjM=
-github.com/bombsimon/wsl/v3 v3.3.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
+github.com/bombsimon/wsl/v3 v3.4.0 h1:RkSxjT3tmlptwfgEgTgU+KYKLI35p/tviNXNXiL2aNU=
+github.com/bombsimon/wsl/v3 v3.4.0/go.mod h1:KkIB+TXkqy6MvK9BDZVbZxKNYsE1/oLRJbIFtf14qqo=
 github.com/breml/bidichk v0.2.3 h1:qe6ggxpTfA8E75hdjWPZ581sY3a2lnl0IRxLQFelECI=
 github.com/breml/bidichk v0.2.3/go.mod h1:8u2C6DnAy0g2cEq+k/A2+tr9O1s+vHGxWn0LTc70T2A=
 github.com/breml/errchkjson v0.3.0 h1:YdDqhfqMT+I1vIxPSas44P+9Z9HzJwCeAzjB8PxP1xw=
@@ -241,15 +241,13 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
 github.com/creachadair/taskgroup v0.3.2 h1:zlfutDS+5XG40AOxcHDSThxKzns8Tnr9jnr6VqkYlkM=
 github.com/creachadair/taskgroup v0.3.2/go.mod h1:wieWwecHVzsidg2CsUnFinW1faVN4+kq+TDlRJQ0Wbk=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
 github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/cristalhq/acmd v0.8.1/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ=
 github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo=
 github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc=
 github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
-github.com/daixiang0/gci v0.9.0 h1:t8XZ0vK6l0pwPoOmoGyqW2NwQlvbpAQNVvu/GRBgykM=
-github.com/daixiang0/gci v0.9.0/go.mod h1:EpVfrztufwVgQRXjnX4zuNinEpLj5OmMjtu/+MB0V0c=
+github.com/daixiang0/gci v0.9.1 h1:jBrwBmBZTDsGsXiaCTLIe9diotp1X4X64zodFrh7l+c=
+github.com/daixiang0/gci v0.9.1/go.mod h1:EpVfrztufwVgQRXjnX4zuNinEpLj5OmMjtu/+MB0V0c=
 github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
 github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
 github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -341,8 +339,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
 github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
-github.com/go-critic/go-critic v0.6.5 h1:fDaR/5GWURljXwF8Eh31T2GZNz9X4jeboS912mWF8Uo=
-github.com/go-critic/go-critic v0.6.5/go.mod h1:ezfP/Lh7MA6dBNn4c6ab5ALv3sKnZVLx37tr00uuaOY=
+github.com/go-critic/go-critic v0.6.7 h1:1evPrElnLQ2LZtJfmNDzlieDhjnq36SLgNzisx06oPM=
+github.com/go-critic/go-critic v0.6.7/go.mod h1:fYZUijFdcnxgx6wPjQA2QEjIRaNCT0gO8bhexy6/QmE=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -360,6 +358,7 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
 github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
 github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
 github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
@@ -372,25 +371,24 @@ github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5Nq
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
-github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
-github.com/go-toolsmith/astcopy v1.0.2/go.mod h1:4TcEdbElGc9twQEYpVo/aieIXfHhiuLh4aLAck6dO7Y=
+github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
+github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
 github.com/go-toolsmith/astcopy v1.0.3 h1:r0bgSRlMOAgO+BdQnVAcpMSMkrQCnV6ZJmIkrJgcJj0=
 github.com/go-toolsmith/astcopy v1.0.3/go.mod h1:4TcEdbElGc9twQEYpVo/aieIXfHhiuLh4aLAck6dO7Y=
-github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
 github.com/go-toolsmith/astequal v1.0.2/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
-github.com/go-toolsmith/astequal v1.0.3 h1:+LVdyRatFS+XO78SGV4I3TCEA0AC7fKEGma+fH+674o=
 github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
-github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
-github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
-github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
-github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
+github.com/go-toolsmith/astequal v1.1.0 h1:kHKm1AWqClYn15R0K1KKE4RG614D46n+nqUQ06E1dTw=
+github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=
+github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=
+github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=
+github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=
+github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=
 github.com/go-toolsmith/pkgload v1.0.2-0.20220101231613-e814995d17c5 h1:eD9POs68PHkwrx7hAB78z1cb6PfGq/jyWn3wJywsH1o=
-github.com/go-toolsmith/pkgload v1.0.2-0.20220101231613-e814995d17c5/go.mod h1:3NAwwmD4uY/yggRxoEjk/S00MIV3A+H7rrE3i87eYxM=
-github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
 github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
-github.com/go-toolsmith/typep v1.0.2 h1:8xdsa1+FSIH/RhEkgnD1j2CJOy5mNllW1Q9tRiYwvlk=
-github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
+github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=
+github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
+github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
+github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
 github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U=
 github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
@@ -458,8 +456,8 @@ github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6
 github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ=
 github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2 h1:amWTbTGqOZ71ruzrdA+Nx5WA3tV1N0goTspwmKCQvBY=
 github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2/go.mod h1:9wOXstvyDRshQ9LggQuzBCGysxs3b6Uo/1MvYCR2NMs=
-github.com/golangci/golangci-lint v1.51.1 h1:N5HD/x0ZrhJYsgKWyz7yJxxQ8JKR0Acc+FOP7QtGSAA=
-github.com/golangci/golangci-lint v1.51.1/go.mod h1:hnyNNO3fJ2Rjwo6HM+VXvcmLkKDOuBAnR9gVlS1mW1E=
+github.com/golangci/golangci-lint v1.51.2 h1:yIcsT1X9ZYHdSpeWXRT1ORC/FPGSqDHbHsu9uk4FK7M=
+github.com/golangci/golangci-lint v1.51.2/go.mod h1:KH9Q7/3glwpYSknxUgUyLlAv46A8fsSKo1hH2wDvkr8=
 github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA=
 github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
 github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
@@ -660,8 +658,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY=
 github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0=
-github.com/junk1tm/musttag v0.4.4 h1:VK4L7v7lvWAhKDDx0cUJgbb0UBNipYinv8pPeHJzH9Q=
-github.com/junk1tm/musttag v0.4.4/go.mod h1:XkcL/9O6RmD88JBXb+I15nYRl9W4ExhgQeCBEhfMC8U=
+github.com/junk1tm/musttag v0.4.5 h1:d+mpJ1vn6WFEVKHwkgJiIedis1u/EawKOuUTygAUtCo=
+github.com/junk1tm/musttag v0.4.5/go.mod h1:XkcL/9O6RmD88JBXb+I15nYRl9W4ExhgQeCBEhfMC8U=
 github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU=
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -688,7 +686,6 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs=
 github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I=
 github.com/kunwardeep/paralleltest v1.0.6 h1:FCKYMF1OF2+RveWlABsdnmsvJrei5aoyZoaGS+Ugg8g=
@@ -832,12 +829,12 @@ github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
 github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
-github.com/onsi/ginkgo/v2 v2.3.1 h1:8SbseP7qM32WcvE6VaN6vfXxv698izmsJ1UQX9ve7T8=
+github.com/onsi/ginkgo/v2 v2.8.0 h1:pAM+oBNPrpXRs+E/8spkeGx9QgekbRVyr74EUvRVOUI=
 github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.22.1 h1:pY8O4lBfsHKZHM/6nrxkhVPUznOlIu3quZcKP/M20KI=
+github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
@@ -864,8 +861,8 @@ github.com/osmosis-labs/go-mutesting v0.0.0-20221208041716-b43bcd97b3b3 h1:Ylmch
 github.com/osmosis-labs/go-mutesting v0.0.0-20221208041716-b43bcd97b3b3/go.mod h1:lV6KnqXYD/ayTe7310MHtM3I2q8Z6bBfMAi+bhwPYtI=
 github.com/osmosis-labs/osmosis/osmomath v0.0.0-20230105183030-bccf5202f260 h1:+EbINXzHQyDtHje2CND357A22H2zUpceTtwJClC9IAM=
 github.com/osmosis-labs/osmosis/osmomath v0.0.0-20230105183030-bccf5202f260/go.mod h1:KrzYoNtnWUH75rj1XAsSR4nymlHFU7jeVOx7/1KMe0k=
-github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230202172735-45df290354da h1:LMGHxYmP+kWexaKttI1cI8T72JPDyljkD6lCr/1jqGg=
-github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230202172735-45df290354da/go.mod h1:iD6Wq2g6bEIG51uohesQXUXCKadW3yqjCkIk1uPhCNE=
+github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230220020447-efe299ca44e0 h1:IFNAXHnGoardt/1RQd1efwM/22GUtwJLhR1qQFZKsh4=
+github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230220020447-efe299ca44e0/go.mod h1:/WgLS5/cEgBnR/C6pLQjujGceD/G5i0jDsEEeqvIpgI=
 github.com/osmosis-labs/osmosis/x/ibc-hooks v0.0.0-20230201094300-e8bba2b45fd8 h1:Ov+gyEBpU2hJkLIFAHCcQZwehCSCXFNYzqQY1zJqSXk=
 github.com/osmosis-labs/osmosis/x/ibc-hooks v0.0.0-20230201094300-e8bba2b45fd8/go.mod h1:Kh2V4fLvLM4/EagRX53CYDJnILAz9NzCCETKXoH/sno=
 github.com/osmosis-labs/wasmd v0.29.2-0.20221222131554-7c8ea36a6e30 h1:6uMi7HhPSwvKKU7j5NqljseFTEz4I7qHr+IPnnn42Ck=
@@ -901,8 +898,8 @@ github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6J
 github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/polyfloyd/go-errorlint v1.0.6 h1:ZevdyEGxDoHAMQUVvdTT06hnYuKULe8TQkOmIYx6s1c=
-github.com/polyfloyd/go-errorlint v1.0.6/go.mod h1:NcnNncnm8fVV7vfQWiI4HZrzWFzGp24C262IQutNcMs=
+github.com/polyfloyd/go-errorlint v1.1.0 h1:VKoEFg5yxSgJ2yFPVhxW7oGz+f8/OVcuMeNvcPIi6Eg=
+github.com/polyfloyd/go-errorlint v1.1.0/go.mod h1:Uss7Bc/izYG0leCMRx3WVlrpqWedSZk7V/FUQW6VJ6U=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
@@ -944,15 +941,10 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
 github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
 github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
 github.com/prometheus/tsdb v0.6.2-0.20190402121629-4f204dcbc150/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/quasilyte/go-ruleguard v0.3.1-0.20210203134552-1b5a410e1cc8/go.mod h1:KsAh3x0e7Fkpgs+Q9pNLS5XpFSvYCEVl5gP9Pp1xp30=
-github.com/quasilyte/go-ruleguard v0.3.18 h1:sd+abO1PEI9fkYennwzHn9kl3nqP6M5vE7FiOzZ+5CE=
-github.com/quasilyte/go-ruleguard v0.3.18/go.mod h1:lOIzcYlgxrQ2sGJ735EHXmf/e9MJ516j16K/Ifcttvs=
-github.com/quasilyte/go-ruleguard/dsl v0.3.0/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
-github.com/quasilyte/go-ruleguard/dsl v0.3.21/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
-github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc=
-github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50=
-github.com/quasilyte/gogrep v0.0.0-20220828223005-86e4605de09f h1:6Gtn2i04RD0gVyYf2/IUMTIs+qYleBt4zxDqkLTcu4U=
-github.com/quasilyte/gogrep v0.0.0-20220828223005-86e4605de09f/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
+github.com/quasilyte/go-ruleguard v0.3.19 h1:tfMnabXle/HzOb5Xe9CUZYWXKfkS1KwRmZyPmD9nVcc=
+github.com/quasilyte/go-ruleguard v0.3.19/go.mod h1:lHSn69Scl48I7Gt9cX3VrbsZYvYiBYszZOZW4A+oTEw=
+github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
+github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
 github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 h1:L8QM9bvf68pVdQ3bCFZMDmnt9yqcMBro1pC7F+IPYMY=
 github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
 github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
@@ -997,12 +989,12 @@ github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71e
 github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
 github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=
 github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
-github.com/sashamelentyev/usestdlibvars v1.21.1 h1:GQGlReyL9Ek8DdJmwtwhHbhwHnuPfsKaprpjnrPcjxc=
-github.com/sashamelentyev/usestdlibvars v1.21.1/go.mod h1:MPI52Qq99iO9sFZZcKJ2y/bx6BNjs+/2bw3PCggIbew=
+github.com/sashamelentyev/usestdlibvars v1.23.0 h1:01h+/2Kd+NblNItNeux0veSL5cBF1jbEOPrEhDzGYq0=
+github.com/sashamelentyev/usestdlibvars v1.23.0/go.mod h1:YPwr/Y1LATzHI93CqoPUN/2BzGQ/6N/cl/KwgR0B/aU=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
-github.com/securego/gosec/v2 v2.14.0 h1:U1hfs0oBackChXA72plCYVA4cOlQ4gO+209dHiSNZbI=
-github.com/securego/gosec/v2 v2.14.0/go.mod h1:Ff03zEi5NwSOfXj9nFpBfhbWTtROCkg9N+9goggrYn4=
+github.com/securego/gosec/v2 v2.15.0 h1:v4Ym7FF58/jlykYmmhZ7mTm7FQvN/setNm++0fgIAtw=
+github.com/securego/gosec/v2 v2.15.0/go.mod h1:VOjTrZOkUtSDt2QLSJmQBMWnvwiQPEjg0l+5juIqGk8=
 github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY=
 github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU=
 github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs=
@@ -1230,8 +1222,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
-golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
-golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
+golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1247,9 +1239,8 @@ golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8H
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
 golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/exp/typeparams v0.0.0-20220827204233-334a2380cb91/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE=
-golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
+golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9 h1:6WHiuFL9FNjg8RljAaT7FNUuKDbvMqS1i5cr2OE2sLQ=
+golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -1279,8 +1270,9 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
 golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
-golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
+golang.org/x/mod v0.8.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=
@@ -1336,8 +1328,9 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 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 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
 golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 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=
@@ -1451,15 +1444,17 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 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 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
 golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 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=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 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 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
 golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
+golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 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=
@@ -1470,8 +1465,9 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 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 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
 golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 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=
@@ -1497,7 +1493,6 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw
 golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190916130336-e45ffcd953cc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191018212557-ed542cd5b28a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -1536,7 +1531,6 @@ golang.org/x/tools v0.0.0-20200624225443-88f3c62a19ff/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200831203904-5a2aa26beb65/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
@@ -1547,7 +1541,6 @@ golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201230224404-63754364767c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -1556,7 +1549,6 @@ golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0t
 golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.9-0.20211228192929-ee1ca4ffc4da/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
 golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
@@ -1564,8 +1556,9 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
 golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
 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 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
 golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
+golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 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=
@@ -1662,7 +1655,6 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
@@ -1704,8 +1696,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.4.0 h1:lyXVV1c8wUBJRKqI8JgIpT8TW1VDagfYYaxbKa/HoL8=
-honnef.co/go/tools v0.4.0/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA=
+honnef.co/go/tools v0.4.2 h1:6qXr+R5w+ktL5UkwEbPp+fEvfyoMPche6GkOpGHZcLc=
+honnef.co/go/tools v0.4.2/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/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
diff --git a/go.work.sum b/go.work.sum
index 61d8a6817..04ffc80a7 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -387,6 +387,7 @@ contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EU
 contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
 contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
+filippo.io/edwards25519 v1.0.0-beta.2/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCOGKtZ+ACWEf7o=
 gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
 git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
 git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
@@ -540,6 +541,7 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IU
 github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
 github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
 github.com/armon/go-metrics v0.3.11/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
+github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to=
 github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw=
@@ -1152,6 +1154,7 @@ github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT
 github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
 github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
 github.com/hashicorp/uuid v0.0.0-20160311170451-ebb0a03e909c/go.mod h1:fHzc09UnyJyqyW+bFuq864eh+wC7dj65aXmXLRe5to0=
+github.com/hdevalence/ed25519consensus v0.0.0-20210204194344-59a8610d2b87/go.mod h1:XGsKKeXxeRr95aEOgipvluMPlgjr7dGlk9ZTWOjcUcg=
 github.com/holiman/uint256 v1.1.1 h1:4JywC80b+/hSfljFlEBLHrrh+CIONLDz9NuFl0af4Mw=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
@@ -1260,6 +1263,7 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo=
 github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk=
 github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
@@ -1428,6 +1432,7 @@ github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je4
 github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
+github.com/onsi/gomega v1.20.0/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
 github.com/onsi/gomega v1.22.0/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
 github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@@ -1475,6 +1480,7 @@ github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTm
 github.com/openzipkin/zipkin-go v0.2.5 h1:UwtQQx2pyPIgWYHRg+epgdx1/HnBQTgN3/oIYEJTQzU=
 github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
 github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
+github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230205215450-2961572f877a/go.mod h1:iD6Wq2g6bEIG51uohesQXUXCKadW3yqjCkIk1uPhCNE=
 github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
 github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI=
 github.com/otiai10/mint v1.3.1 h1:BCmzIS3n71sGfHB5NMNDB3lHYPz8fWSkCAErHed//qc=
@@ -1660,7 +1666,6 @@ github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570 h1:gIlAHnH1
 github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3 h1:njlZPzLwU639dk2kqnCPPv+wNjq7Xb6EfUxe/oX0/NM=
 github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
 github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
-github.com/strangelove-ventures/packet-forward-middleware/v4 v4.0.4/go.mod h1:AG8F5pdk3x1h7PlRvPoMem3623W+w8HJHrWYkVJ51kk=
 github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo=
 github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e h1:mOtuXaRAbVZsxAHVdPR3IjfmN8T1h2iczJLynhLybf8=
@@ -1973,6 +1978,7 @@ golang.org/x/net v0.0.0-20220726230323-06994584191e/go.mod h1:AaygXjzTFtRAg2ttMY
 golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.0.0-20221017152216-f25eb7ecb193/go.mod h1:RpDiru2p0u2F0lLpEoqnP2+7xs0ifAuOcJ442g6GU2s=
+golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/oauth2 v0.0.0-20180724155351-3d292e4d0cdc/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
diff --git a/osmomath/go.mod b/osmomath/go.mod
index 57bcc34ff..f02cd8845 100644
--- a/osmomath/go.mod
+++ b/osmomath/go.mod
@@ -3,7 +3,7 @@ module github.com/osmosis-labs/osmosis/osmomath
 go 1.19
 
 require (
-	github.com/cosmos/cosmos-sdk v0.46.8
+	github.com/cosmos/cosmos-sdk v0.46.10
 	github.com/stretchr/testify v1.8.1
 	gopkg.in/yaml.v2 v2.4.0
 )
diff --git a/osmomath/go.sum b/osmomath/go.sum
index 123c7ab6f..132c2f23c 100644
--- a/osmomath/go.sum
+++ b/osmomath/go.sum
@@ -300,8 +300,7 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.22.1 h1:pY8O4lBfsHKZHM/6nrxkhVPUznOlIu3quZcKP/M20KI=
-github.com/osmosis-labs/cosmos-sdk v0.45.1-0.20221118211718-545aed73e94e h1:A3byMZpvq21iI7yWJUNdHw0nf8jVAbVUsWY9twnXSXE=
-github.com/osmosis-labs/cosmos-sdk v0.45.1-0.20221118211718-545aed73e94e/go.mod h1:rud0OaBIuq3+qOqtwT4SR7Q7iSzRp7w41fjninTjfnQ=
+github.com/osmosis-labs/cosmos-sdk v0.45.1-0.20230208101050-4937288c68d1 h1:cWEy8WLpQeDzQrUYOelh2mOzwF8RJujsXQhwnYWbg9w=
 github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
diff --git a/osmoutils/accum/accum.go b/osmoutils/accum/accum.go
index de3202ada..ddac13f7c 100644
--- a/osmoutils/accum/accum.go
+++ b/osmoutils/accum/accum.go
@@ -23,7 +23,7 @@ type AccumulatorObject struct {
 	value sdk.DecCoins
 
 	// Accumulator's total shares across all positions
-	totalShares	sdk.Dec
+	totalShares sdk.Dec
 }
 
 // Makes a new accumulator at store/accum/{accumName}
@@ -156,10 +156,6 @@ func (accum AccumulatorObject) AddToPositionCustomAcc(name string, newShares sdk
 		return err
 	}
 
-	if err := validateAccumulatorValue(customAccumulatorValue, position.InitAccumValue); err != nil {
-		return err
-	}
-
 	// Save current number of shares and unclaimed rewards
 	unclaimedRewards := getTotalRewards(accum, position)
 	oldNumShares, err := accum.GetPositionSize(name)
@@ -207,10 +203,6 @@ func (accum AccumulatorObject) RemoveFromPositionCustomAcc(name string, numShare
 		return err
 	}
 
-	if err := validateAccumulatorValue(customAccumulatorValue, position.InitAccumValue); err != nil {
-		return err
-	}
-
 	// Ensure not removing more shares than exist
 	if numSharesToRemove.GT(position.NumShares) {
 		return fmt.Errorf("Attempted to remove more shares (%s) than exist in the position (%s)", numSharesToRemove, position.NumShares)
@@ -275,10 +267,6 @@ func (accum AccumulatorObject) SetPositionCustomAcc(name string, customAccumulat
 		return err
 	}
 
-	if err := validateAccumulatorValue(customAccumulatorValue, position.InitAccumValue); err != nil {
-		return err
-	}
-
 	// Update the user's position with the new accumulator value. The unclaimed rewards, options, and
 	// the number of shares stays the same as in the original position.
 	initOrUpdatePosition(accum, customAccumulatorValue, name, position.NumShares, position.UnclaimedRewards, position.Options)
@@ -351,6 +339,7 @@ func (accum AccumulatorObject) ClaimRewards(positionName string) (sdk.Coins, err
 }
 
 // GetTotalShares returns the total number of shares in the accumulator
-func (accum AccumulatorObject) GetTotalShares() sdk.Dec {
-	return accum.totalShares
+func (accum AccumulatorObject) GetTotalShares() (sdk.Dec, error) {
+	accum, err := GetAccumulator(accum.store, accum.name)
+	return accum.totalShares, err
 }
diff --git a/osmoutils/accum/accum_helpers.go b/osmoutils/accum/accum_helpers.go
index 29ace8446..a65ac1a64 100644
--- a/osmoutils/accum/accum_helpers.go
+++ b/osmoutils/accum/accum_helpers.go
@@ -10,7 +10,7 @@ var (
 	minusOne = sdk.NewDec(-1)
 )
 
-// Creates a new position or override an existing position 
+// Creates a new position or override an existing position
 // at accumulator's current value with a specific number of shares and unclaimed rewards
 func initOrUpdatePosition(accum AccumulatorObject, accumulatorValue sdk.DecCoins, index string, numShareUnits sdk.Dec, unclaimedRewards sdk.DecCoins, options *Options) {
 	position := Record{
diff --git a/osmoutils/go.mod b/osmoutils/go.mod
index 368e9557e..68a8b62a2 100644
--- a/osmoutils/go.mod
+++ b/osmoutils/go.mod
@@ -3,7 +3,7 @@ module github.com/osmosis-labs/osmosis/osmoutils
 go 1.19
 
 require (
-	github.com/cosmos/cosmos-sdk v0.46.8
+	github.com/cosmos/cosmos-sdk v0.46.10
 	github.com/cosmos/iavl v0.19.4
 	github.com/cosmos/ibc-go/v4 v4.3.0
 	github.com/gogo/protobuf v1.3.3
diff --git a/proto/osmosis/concentrated-liquidity/incentive_record.proto b/proto/osmosis/concentrated-liquidity/incentive_record.proto
new file mode 100644
index 000000000..b1b9674c1
--- /dev/null
+++ b/proto/osmosis/concentrated-liquidity/incentive_record.proto
@@ -0,0 +1,48 @@
+syntax = "proto3";
+package osmosis.concentratedliquidity.v1beta1;
+
+import "cosmos_proto/cosmos.proto";
+import "gogoproto/gogo.proto";
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/duration.proto";
+
+option go_package = "github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity/types";
+
+// IncentiveRecord represents an active perpetual incentive gauge for a pool
+message IncentiveRecord {
+  uint64 poolId = 1;
+
+  // incentive_denom is the denom of the token being distributed as part of this
+  // incentive record
+  string incentive_denom = 2;
+
+  // remaining_amount is the total amount of incentives to be distributed
+  string remaining_amount = 3 [
+    (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
+    (gogoproto.moretags) = "yaml:\"remaining_amount\"",
+    (gogoproto.nullable) = false
+  ];
+
+  // emission_rate is the incentive emission rate per second
+  string emission_rate = 4 [
+    (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
+    (gogoproto.moretags) = "yaml:\"swap_fee\"",
+    (gogoproto.nullable) = false
+  ];
+
+  // start_time is the time when the incentive starts distributing
+  google.protobuf.Timestamp start_time = 5 [
+    (gogoproto.nullable) = false,
+    (gogoproto.stdtime) = true,
+    (gogoproto.moretags) = "yaml:\"start_time\""
+  ];
+
+  // min_uptime is the minimum uptime required for liquidity to qualify for this
+  // incentive. It should be always be one of the supported uptimes in
+  // types.SupportedUptimes
+  google.protobuf.Duration min_uptime = 6 [
+    (gogoproto.nullable) = false,
+    (gogoproto.stdduration) = true,
+    (gogoproto.moretags) = "yaml:\"min_uptime\""
+  ];
+}
\ No newline at end of file
diff --git a/proto/osmosis/concentrated-liquidity/pool.proto b/proto/osmosis/concentrated-liquidity/pool.proto
index f472790d3..3d6e1c24e 100644
--- a/proto/osmosis/concentrated-liquidity/pool.proto
+++ b/proto/osmosis/concentrated-liquidity/pool.proto
@@ -7,6 +7,8 @@ package osmosis.concentratedliquidity.v1beta1;
 
 import "cosmos_proto/cosmos.proto";
 import "gogoproto/gogo.proto";
+import "google/protobuf/timestamp.proto";
+import "osmosis/concentrated-liquidity/incentive_record.proto";
 
 option go_package = "github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity/model";
 
@@ -53,4 +55,12 @@ message Pool {
     (gogoproto.moretags) = "yaml:\"swap_fee\"",
     (gogoproto.nullable) = false
   ];
-}
+
+  // last_liquidity_update is the last time either the pool liquidity or the
+  // active tick changed
+  google.protobuf.Timestamp last_liquidity_update = 11 [
+    (gogoproto.nullable) = false,
+    (gogoproto.stdtime) = true,
+    (gogoproto.moretags) = "yaml:\"last_liquidity_update\""
+  ];
+}
\ No newline at end of file
diff --git a/proto/osmosis/concentrated-liquidity/tickInfo.proto b/proto/osmosis/concentrated-liquidity/tickInfo.proto
index 9fbeddf48..c538d9b12 100644
--- a/proto/osmosis/concentrated-liquidity/tickInfo.proto
+++ b/proto/osmosis/concentrated-liquidity/tickInfo.proto
@@ -26,4 +26,15 @@ message TickInfo {
     (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.DecCoins",
     (gogoproto.nullable) = false
   ];
+  repeated UptimeTracker uptime_trackers = 4 [
+    (gogoproto.moretags) = "yaml:\"uptime_trackers\"",
+    (gogoproto.nullable) = false
+  ];
+}
+
+message UptimeTracker {
+  repeated cosmos.base.v1beta1.DecCoin uptime_growth_outside = 1 [
+    (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.DecCoins",
+    (gogoproto.nullable) = false
+  ];
 }
diff --git a/proto/osmosis/concentrated-liquidity/tx.proto b/proto/osmosis/concentrated-liquidity/tx.proto
index c99b429b0..88c7d136a 100644
--- a/proto/osmosis/concentrated-liquidity/tx.proto
+++ b/proto/osmosis/concentrated-liquidity/tx.proto
@@ -103,8 +103,8 @@ message MsgCollectFees {
 }
 
 message MsgCollectFeesResponse {
-  repeated cosmos.base.v1beta1.Coin token_out = 1 [
-    (gogoproto.moretags) = "yaml:\"token_out\"",
+  repeated cosmos.base.v1beta1.Coin collected_fees = 1 [
+    (gogoproto.moretags) = "yaml:\"collected_fees\"",
     (gogoproto.nullable) = false
   ];
 }
diff --git a/scripts/cl/README.md b/scripts/cl/README.md
index f156faa56..3ba6dbb6e 100644
--- a/scripts/cl/README.md
+++ b/scripts/cl/README.md
@@ -19,3 +19,9 @@ sage -python scripts/cl/main.py
 ```
 
 Note, these scripts can also be run by installing `sympy` without sage.
+
+## Running tests
+
+```bash
+python3 -m unittest scripts.cl.test.test_common 
+```
diff --git a/scripts/cl/__init__.py b/scripts/cl/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/scripts/cl/common/__init__.py b/scripts/cl/common/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/scripts/cl/common.py b/scripts/cl/common/common.py
similarity index 60%
rename from scripts/cl/common.py
rename to scripts/cl/common/common.py
index 60eaf8cbe..026e00e11 100644
--- a/scripts/cl/common.py
+++ b/scripts/cl/common/common.py
@@ -1,6 +1,7 @@
-import sympy as sp
+from decimal import *
+import math
 
-precision = 30
+from common.sdk_dec import *
 
 # SqrtPriceRange represents a price range between the current and the next tick
 # as well as the liquidity in that price range.
@@ -17,36 +18,22 @@ precision = 30
 # and from 5002 to UNKNOWN with liquidity Z. In this case, the UNKNOWN
 # depends on how much ETH we have remaining after consuming liquidity X and Y.
 class SqrtPriceRange:
-  def __init__(self, sqrt_price_current: int, sqrt_price_next: int, liquidity: sp.Float):
-    self.sqrt_price_start = sp.sqrt(fixed_prec_dec(sqrt_price_current))
+  def __init__(self, sqrt_price_current: int, sqrt_price_next: int, liquidity: Decimal):
+    self.sqrt_price_start = new(new(sqrt_price_current).sqrt())
     if sqrt_price_next is not None:
-        self.sqrt_price_next = sp.sqrt(fixed_prec_dec(sqrt_price_next))
+        self.sqrt_price_next = new(new(sqrt_price_next).sqrt())
     else:
        self.sqrt_price_next =  None
     self.liquidity = liquidity
 
-def fixed_prec_dec(string: str) -> sp.Float:
-    """ Return an equivalent of a Python Decimal with fixed precision. 
-    """
-    return sp.Float(string, precision)
-
-def get_fee_amount_per_share(token_in: sp.Float, swap_fee: sp.Float, liquidity: sp.Float) -> sp.Float:
-    """ Returns the fee amount per share.
-    """
-    fee_charge_total = token_in * swap_fee
-    print("fee_charge_total ", fee_charge_total)
-    return fee_charge_total / liquidity
-
-zero = fixed_prec_dec("0")
-
-def validate_confirmed_results(actual_token_amount: sp.Float, fee_growth_per_share_total: sp.Float, expected_token_amount: sp.Float, expected_fee_growth_per_share_total: sp.Float):
+def validate_confirmed_results(actual_token_amount: Decimal, fee_growth_per_share_total: Decimal, expected_token_amount: Decimal, expected_fee_growth_per_share_total: Decimal):
     """Validates the results of a calc concentrated liquidity test case estimates.
 
     This validation helper exists to make sure that subsequent changes to the script do not break test cases.
     """
 
-    if sp.N(actual_token_amount, 18) != sp.N(expected_token_amount, 18):
+    if math.floor(actual_token_amount) != math.floor(expected_token_amount):
         raise Exception(F"actual_token_amount {actual_token_amount} does not match expected_token_amount {expected_token_amount}")
     
-    if sp.N(fee_growth_per_share_total, 18) != sp.N(expected_fee_growth_per_share_total, 18):
+    if abs(fee_growth_per_share_total - expected_fee_growth_per_share_total) > (1 / math.pow(10, 18)):
         raise Exception(F"fee_growth_per_share_total {fee_growth_per_share_total} does not match expected_fee_growth_per_share_total {expected_fee_growth_per_share_total}")
diff --git a/scripts/cl/common/sdk_dec.py b/scripts/cl/common/sdk_dec.py
new file mode 100644
index 000000000..1a75111e0
--- /dev/null
+++ b/scripts/cl/common/sdk_dec.py
@@ -0,0 +1,44 @@
+from decimal import *
+
+precision = 18
+quantize_precision = (Decimal("10") ** -precision)
+
+default_ctx = Context(rounding=ROUND_05UP, clamp=0, Emin=-precision, Emax=precision)
+setcontext(default_ctx)
+
+def mul(x: Decimal, y: Decimal):
+    if x.is_nan():
+        raise Exception("mul x is NaN")
+    if y.is_nan():
+        raise Exception("mul y is NaN")
+    return (x * y).quantize(exp=quantize_precision, context=Context(rounding=ROUND_05UP, flags=[Inexact, Rounded]))
+
+def quo_custom_round(x: Decimal, y: Decimal, custom_round: int):
+    if x.is_nan():
+        raise Exception("mul x is NaN")
+    if y.is_nan():
+        raise Exception("mul y is NaN")
+    if y.is_zero():
+        raise DivisionByZero("quo y is zero")
+    return (x / y).quantize(exp=quantize_precision, context=Context(rounding=custom_round))
+
+def quo(x: Decimal, y: Decimal):
+    return quo_custom_round(x, y, ROUND_05UP)
+
+def quo_up(x: Decimal, y: Decimal):
+    return quo_custom_round(x, y, ROUND_UP)
+
+def quo_trunc(x: Decimal, y: Decimal):
+    return quo_custom_round(x, y, ROUND_DOWN)
+
+def new(value: str) -> Decimal:
+    """ Return an equivalent of a Cosmos SDK Decimal with fixed precision. 
+    """
+    return Decimal(value=value, context=Context(rounding=ROUND_05UP, flags=[Inexact, Rounded]))
+
+
+zero = new("0")
+
+one = new("1")
+
+precision_multiplier = one ** (precision)
\ No newline at end of file
diff --git a/scripts/cl/in_given_out.py b/scripts/cl/in_given_out.py
index 79d416dab..ae3f8ed8a 100644
--- a/scripts/cl/in_given_out.py
+++ b/scripts/cl/in_given_out.py
@@ -1,10 +1,12 @@
 from typing import Tuple
 
-from common import *
+from common.common import *
+import common.sdk_dec as sdk_dec
+from decimal import *
 import zero_for_one as zfo
 import one_for_zero as ofz
 
-def estimate_test_case_in_given_out(tick_ranges: list[SqrtPriceRange], token_out: sp.Float, swap_fee: sp.Float, is_zero_for_one: bool) -> Tuple[sp.Float, sp.Float]:
+def estimate_test_case_in_given_out(tick_ranges: list[SqrtPriceRange], token_out: Decimal, swap_fee: Decimal, is_zero_for_one: bool) -> Tuple[Decimal, Decimal]:
     """ Estimates a calc concentrated liquidity test case when swapping for token in given out.
     
     Given
@@ -18,7 +20,7 @@ def estimate_test_case_in_given_out(tick_ranges: list[SqrtPriceRange], token_out
     Returns the total token in and the total fee growth per share.
     """
 
-    token_out_consumed_total, token_in_total, fee_growth_per_share_total = zero, zero, zero
+    token_out_consumed_total, token_in_total, fee_growth_per_share_total = sdk_dec.zero, sdk_dec.zero, sdk_dec.zero
 
     for i in range(len(tick_ranges)):
         tick_range = tick_ranges[i]
@@ -33,7 +35,7 @@ def estimate_test_case_in_given_out(tick_ranges: list[SqrtPriceRange], token_out
         is_with_next_sqrt_price = not is_last_range or is_next_price_set
 
         if is_with_next_sqrt_price:
-            token_out_consumed, token_in, fee_growth_per_share = zero, zero, zero
+            token_out_consumed, token_in, fee_growth_per_share = sdk_dec.zero, sdk_dec.zero, sdk_dec.zero
             if is_zero_for_one:
                 token_out_consumed, token_in, fee_growth_per_share = zfo.calc_test_case_with_next_sqrt_price_in_given_out(tick_range.liquidity, tick_range.sqrt_price_start, tick_range.sqrt_price_next, swap_fee)
             else:
@@ -47,10 +49,10 @@ def estimate_test_case_in_given_out(tick_ranges: list[SqrtPriceRange], token_out
         else:
             token_out_remaining = token_out - token_out_consumed_total
 
-            if token_out_remaining < zero:
+            if token_out_remaining < sdk_dec.zero:
                 raise Exception(F"token_in_remaining {token_out_remaining} is negative with token_out_initial {token_out} and token_out_consumed_total {token_out_consumed_total}")
 
-            token_in, fee_growth_per_share = zero, zero
+            token_in, fee_growth_per_share = sdk_dec.zero, sdk_dec.zero
             if is_zero_for_one:
                 _, token_in, fee_growth_per_share = zfo.calc_test_case_in_given_out(tick_range.liquidity, tick_range.sqrt_price_start, token_out_remaining, swap_fee)
             else:
@@ -79,17 +81,17 @@ def estimate_single_position_within_one_tick_ofz_in_given_out():
     """
 
     is_zero_for_one = False
-    swap_fee = fixed_prec_dec("0.01")
-    token_out_initial = fixed_prec_dec("42000000")
+    swap_fee = sdk_dec.new("0.01")
+    token_out_initial = sdk_dec.new("42000000")
 
     tick_ranges = [
-        SqrtPriceRange(5000, None, fixed_prec_dec("1517882343.751510418088349649")), # last one must be computed based on remaining token in, therefore it is None
+        SqrtPriceRange(5000, None, sdk_dec.new("1517882343.751510418088349649")), # last one must be computed based on remaining token in, therefore it is None
     ]
 
     token_in, fee_growth_per_share_total = estimate_test_case_in_given_out(tick_ranges, token_out_initial, swap_fee, is_zero_for_one)
 
-    expected_token_in = fixed_prec_dec("8480.68138458406954789169099991")
-    expected_fee_growth_per_share_total = fixed_prec_dec("0.0000000553186106731409146737705304277")
+    expected_token_in = sdk_dec.new("8480")
+    expected_fee_growth_per_share_total = sdk_dec.new("0.000000055318610673")
 
     validate_confirmed_results(token_in, fee_growth_per_share_total, expected_token_in, expected_fee_growth_per_share_total)
 
@@ -101,17 +103,17 @@ def estimate_two_positions_within_one_tick_zfo_in_given_out():
     """
 
     is_zero_for_one = True
-    swap_fee = fixed_prec_dec("0.03")
-    token_out = fixed_prec_dec("13370")
+    swap_fee = sdk_dec.new("0.03")
+    token_out = sdk_dec.new("13370")
 
     tick_ranges = [
-        SqrtPriceRange(5000, None, fixed_prec_dec("3035764687.503020836176699298")), # last one must be computed based on remaining token in, therefore it is None
+        SqrtPriceRange(5000, None, sdk_dec.new("3035764687.503020836176699298")), # last one must be computed based on remaining token in, therefore it is None
     ]
 
     token_in, fee_growth_per_share_total = estimate_test_case_in_given_out(tick_ranges, token_out, swap_fee, is_zero_for_one)
 
-    expected_token_in = fixed_prec_dec("68834063.6068587597543212771274")
-    expected_fee_growth_per_share_total = fixed_prec_dec("0.000660418657377483623332014151904")
+    expected_token_in = sdk_dec.new("68834063")
+    expected_fee_growth_per_share_total = sdk_dec.new("0.000660418657377483")
 
     validate_confirmed_results(token_in, fee_growth_per_share_total, expected_token_in, expected_fee_growth_per_share_total)
 
@@ -123,18 +125,18 @@ def estimate_two_consecutive_positions_zfo_in_given_out(swap_fee: str, expected_
     """
 
     is_zero_for_one = True
-    swap_fee = fixed_prec_dec(swap_fee)
-    token_out = fixed_prec_dec("2000000")
+    swap_fee = sdk_dec.new(swap_fee)
+    token_out = sdk_dec.new("2000000")
 
     tick_ranges = [
-        SqrtPriceRange(5000, 4545, fixed_prec_dec("1517882343.751510418088349649")),
-        SqrtPriceRange(4545, None, fixed_prec_dec("1198735489.597250295669959398")), # last one must be computed based on remaining token in, therefore it is None
+        SqrtPriceRange(5000, 4545, sdk_dec.new("1517882343.751510418088349649")),
+        SqrtPriceRange(4545, None, sdk_dec.new("1198735489.597250295669959398")), # last one must be computed based on remaining token in, therefore it is None
     ]
 
     token_in, fee_growth_per_share_total = estimate_test_case_in_given_out(tick_ranges, token_out, swap_fee, is_zero_for_one)
 
-    expected_token_in = fixed_prec_dec(expected_token_in)
-    expected_fee_growth_per_share_total = fixed_prec_dec(expected_fee_growth_per_share_total)
+    expected_token_in = sdk_dec.new(expected_token_in)
+    expected_fee_growth_per_share_total = sdk_dec.new(expected_fee_growth_per_share_total)
 
     validate_confirmed_results(token_in, fee_growth_per_share_total, expected_token_in, expected_fee_growth_per_share_total)
 
@@ -146,19 +148,19 @@ def estimate_overlapping_price_range_ofz_test_in_given_out():
     """
 
     is_zero_for_one = False
-    swap_fee = fixed_prec_dec("0.1")
-    token_out_initial = fixed_prec_dec("10000000000")
+    swap_fee = sdk_dec.new("0.1")
+    token_out_initial = sdk_dec.new("10000000000")
 
     tick_ranges = [
-        SqrtPriceRange(5000, 5001, fixed_prec_dec("1517882343.751510418088349649")),
-        SqrtPriceRange(5001, 5500, fixed_prec_dec("2188298432.357179145127590431")),
-        SqrtPriceRange(5500, None, fixed_prec_dec("670416088.605668727039240782")), # last one must be computed based on remaining token in, therefore it is None
+        SqrtPriceRange(5000, 5001, sdk_dec.new("1517882343.751510418088349649")),
+        SqrtPriceRange(5001, 5500, sdk_dec.new("2188298432.357179145127590431")),
+        SqrtPriceRange(5500, None, sdk_dec.new("670416088.605668727039240782")), # last one must be computed based on remaining token in, therefore it is None
     ]
 
     token_in, fee_growth_per_share_total = estimate_test_case_in_given_out(tick_ranges, token_out_initial, swap_fee, is_zero_for_one)
 
-    expected_token_in = fixed_prec_dec("2050578.06505516031487626102620")
-    expected_fee_growth_per_share_total = fixed_prec_dec("0.000129193383486476217032589405019")
+    expected_token_in = sdk_dec.new("2050578")
+    expected_fee_growth_per_share_total = sdk_dec.new("0.000129193383486476")
 
     validate_confirmed_results(token_in, fee_growth_per_share_total, expected_token_in, expected_fee_growth_per_share_total)
 
@@ -170,19 +172,19 @@ def estimate_overlapping_price_range_zfo_test_in_given_out(tokein_in_initial: st
     """
 
     is_zero_for_one = True
-    swap_fee = fixed_prec_dec(swap_fee)
-    token_in_initial = fixed_prec_dec(tokein_in_initial)
+    swap_fee = sdk_dec.new(swap_fee)
+    token_in_initial = sdk_dec.new(tokein_in_initial)
 
     tick_ranges = [
-        SqrtPriceRange(5000, 4999, fixed_prec_dec("1517882343.751510418088349649")),
-        SqrtPriceRange(4999, 4545, fixed_prec_dec("1517882343.751510418088349649") + fixed_prec_dec("670416215.718827443660400594")), # first and second position's liquidity.
-        SqrtPriceRange(4545, None, fixed_prec_dec("670416215.718827443660400594")), # last one must be computed based on remaining token in, therefore it is None
+        SqrtPriceRange(5000, 4999, sdk_dec.new("1517882343.751510418088349649")),
+        SqrtPriceRange(4999, 4545, sdk_dec.new("1517882343.751510418088349649") + sdk_dec.new("670416215.718827443660400594")), # first and second position's liquidity.
+        SqrtPriceRange(4545, None, sdk_dec.new("670416215.718827443660400594")), # last one must be computed based on remaining token in, therefore it is None
     ]
 
     token_in, fee_growth_per_share_total = estimate_test_case_in_given_out(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)
 
-    expected_token_in = fixed_prec_dec(expected_token_in)
-    expected_fee_growth_per_share_total = fixed_prec_dec(expected_fee_growth_per_share_total)
+    expected_token_in = sdk_dec.new(expected_token_in)
+    expected_fee_growth_per_share_total = sdk_dec.new(expected_fee_growth_per_share_total)
 
     validate_confirmed_results(token_in, fee_growth_per_share_total, expected_token_in, expected_fee_growth_per_share_total)
 
@@ -194,18 +196,18 @@ def estimate_consecutive_positions_gap_ofz_test_in_given_out():
     """
 
     is_zero_for_one = False
-    swap_fee = fixed_prec_dec("0.03")
-    token_out_initial = fixed_prec_dec("10000000000")
+    swap_fee = sdk_dec.new("0.03")
+    token_out_initial = sdk_dec.new("10000000000")
 
     tick_ranges = [
-        SqrtPriceRange(5000, 5500, fixed_prec_dec("1517882343.751510418088349649")),
-        SqrtPriceRange(5501, None, fixed_prec_dec("1199528406.187413669220037261")), # last one must be computed based on remaining token in, therefore it is None
+        SqrtPriceRange(5000, 5500, sdk_dec.new("1517882343.751510418088349649")),
+        SqrtPriceRange(5501, None, sdk_dec.new("1199528406.187413669220037261")), # last one must be computed based on remaining token in, therefore it is None
     ]
 
     token_in, fee_growth_per_share_total = estimate_test_case_in_given_out(tick_ranges, token_out_initial, swap_fee, is_zero_for_one)
 
-    expected_token_in = fixed_prec_dec("1875162.23481286214014647237476")
-    expected_fee_growth_per_share_total = fixed_prec_dec("0.0000402914572366514266238726907754")
+    expected_token_in = sdk_dec.new("1875162")
+    expected_fee_growth_per_share_total = sdk_dec.new("0.000040291457236651")
 
     validate_confirmed_results(token_in, fee_growth_per_share_total, expected_token_in, expected_fee_growth_per_share_total)
 
@@ -217,17 +219,17 @@ def estimate_slippage_protection_zfo_test_in_given_out():
     """
 
     is_zero_for_one = True
-    swap_fee = fixed_prec_dec("0.01")
-    token_in_initial = fixed_prec_dec("13370")
+    swap_fee = sdk_dec.new("0.01")
+    token_in_initial = sdk_dec.new("13370")
 
     tick_ranges = [
-        SqrtPriceRange(5000, 4994, fixed_prec_dec("1517882343.751510418088349649")),
+        SqrtPriceRange(5000, 4994, sdk_dec.new("1517882343.751510418088349649")),
     ]
 
     token_in, fee_growth_per_share_total = estimate_test_case_in_given_out(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)
 
-    expected_token_in = fixed_prec_dec("65061801.2370366020634290878154")
-    expected_fee_growth_per_share_total = fixed_prec_dec("0.000424391424357398265504790467604")
+    expected_token_in = sdk_dec.new("65061801")
+    expected_fee_growth_per_share_total = sdk_dec.new("0.000424391424357398")
 
     validate_confirmed_results(token_in, fee_growth_per_share_total, expected_token_in, expected_fee_growth_per_share_total)
 
@@ -244,22 +246,22 @@ def test():
     estimate_two_positions_within_one_tick_zfo_in_given_out()
 
     # fee 3
-    estimate_two_consecutive_positions_zfo_in_given_out("0.05", "9558593928.11224900598454626594", "0.335859454766583687373296697168")
+    estimate_two_consecutive_positions_zfo_in_given_out("0.05", "9558593928", "0.335859454766583687")
 
     # No fee consecutive positions zfo
-    estimate_two_consecutive_positions_zfo_in_given_out("0.0", "9103422788.67833238665194882471", "0.0")
+    estimate_two_consecutive_positions_zfo_in_given_out("0.0", "9103422788", "0.0")
 
     # fee 4
     estimate_overlapping_price_range_ofz_test_in_given_out()
 
-    # # fee 5
-    estimate_overlapping_price_range_zfo_test_in_given_out("1800000", "0.005", "8521716920.24423119398212788491", "0.0259843141658684574040461818975")
+    # fee 5
+    estimate_overlapping_price_range_zfo_test_in_given_out("1800000", "0.005", "8521716920", "0.025984314165868457")
 
     # No fee overlapping price range zfo, utilizing full liquidity
-    estimate_overlapping_price_range_zfo_test_in_given_out("2000000", "0.0", "9321276930.73297863398988127324", "0.0")
+    estimate_overlapping_price_range_zfo_test_in_given_out("2000000", "0.0", "9321276930", "0.0")
 
     # No fee overlapping price range zfo, not utilizing full liquidity
-    estimate_overlapping_price_range_zfo_test_in_given_out("1800000", "0.0", "8479320318.65097631242002774618", "0.0")
+    estimate_overlapping_price_range_zfo_test_in_given_out("1800000", "0.0", "8479320318", "0.0")
 
     # fee 6
     estimate_consecutive_positions_gap_ofz_test_in_given_out()
diff --git a/scripts/cl/math.py b/scripts/cl/math.py
index c63fd4b6d..10e56dec7 100644
--- a/scripts/cl/math.py
+++ b/scripts/cl/math.py
@@ -1,11 +1,11 @@
-import sympy as sp
+from decimal import *
 
-def calc_liquidity_0(amount_zero: sp.Float, sqrtPriceA: sp.Float, sqrtPriceB: sp.Float) -> sp.Float:
+def calc_liquidity_0(amount_zero: Decimal, sqrtPriceA: Decimal, sqrtPriceB: Decimal) -> Decimal:
     """Calculates and returns liquidity zero. 
     """
     return amount_zero * (sqrtPriceA * sqrtPriceB) / (sqrtPriceB - sqrtPriceA)
 
-def calc_liquidity_1(amount_one: sp.Float, sqrtPriceA: sp.Float, sqrtPriceB: sp.Float) -> sp.Float:
+def calc_liquidity_1(amount_one: Decimal, sqrtPriceA: Decimal, sqrtPriceB: Decimal) -> Decimal:
     """Calculates and returns liquidity one. 
     """
     return amount_one * (sqrtPriceB - sqrtPriceA)
diff --git a/scripts/cl/one_for_zero.py b/scripts/cl/one_for_zero.py
index 2e65cdc9c..a49777169 100644
--- a/scripts/cl/one_for_zero.py
+++ b/scripts/cl/one_for_zero.py
@@ -1,71 +1,77 @@
 from typing import Tuple
-import sympy as sp
+import math
+from decimal import *
 from common import *
+import common.sdk_dec as sdk_dec
 
-def get_next_sqrt_price(liquidity: sp.Float, sqrt_price_current: sp.Float, token_in: sp.Float) -> sp.Float:
+def get_next_sqrt_price(liquidity: Decimal, sqrt_price_current: Decimal, token_in: Decimal) -> Decimal:
     """ Return the next sqrt price when swapping token one for zero.
     """
-    return sqrt_price_current + token_in / liquidity
+    return sqrt_price_current + sdk_dec.quo(token_in, liquidity)
 
-def get_token_out(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float) -> sp.Float:
+def get_token_out(liquidity: Decimal, sqrt_price_current: Decimal, sqrt_price_next: Decimal) -> Decimal:
     """ Returns the token out when swapping token one for zero given the token in.
     """
-    return liquidity * (sqrt_price_next - sqrt_price_current) / (sqrt_price_next * sqrt_price_current)
+    return sdk_dec.mul(liquidity, sdk_dec.quo((sqrt_price_next - sqrt_price_current), sdk_dec.mul(sqrt_price_next, sqrt_price_current)))
 
-def get_token_in_swap_in_given_out(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float) -> sp.Float:
+def get_token_in_swap_in_given_out(liquidity: Decimal, sqrt_price_current: Decimal, sqrt_price_next: Decimal) -> Decimal:
     """ Returns the token in when swapping token one for zero given the token out.
 
     In this case, the calculation is the same as computing token out when given token in.
     """
     return get_token_out(liquidity, sqrt_price_current, sqrt_price_next)
 
-def calc_amount_1_delta(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float, should_round_up: bool):
+def calc_amount_one_delta(liquidity: Decimal, sqrt_price_current: Decimal, sqrt_price_next: Decimal, should_round_up: bool) -> Decimal:
     """ Returns the expected token in when swapping token one for zero. 
     """
-    result = liquidity * sp.Abs((sqrt_price_current - sqrt_price_next))
+    result = sdk_dec.mul(liquidity, abs(sqrt_price_current - sqrt_price_next))
     if should_round_up:
-        return sp.ceiling(result)
+        return Decimal(math.ceil(result))
     return result
 
-def calc_test_case_out_given_in(liquidity: sp.Float, sqrt_price_current: sp.Float, token_in_remaining: sp.Float, swap_fee: sp.Float) -> Tuple[sp.Float, sp.Float, sp.Float]:
+def calc_test_case_out_given_in(liquidity: Decimal, sqrt_price_current: Decimal, token_in_remaining: Decimal, swap_fee: Decimal) -> Tuple[Decimal, Decimal, Decimal]:
     """ Computes and prints all one for zero test case parameters when swapping for out given in.
     Next sqrt price is computed from the given parameters.
 
     Returns the next square root price, token out and fee amount per share.
     """
 
-    token_in_after_fee = token_in_remaining * (1 - swap_fee)
-    token_in_after_fee_rounded_up = sp.ceiling(token_in_after_fee)
+    token_in_remaining_after_fee = sdk_dec.mul(token_in_remaining, (sdk_dec.one - swap_fee))
 
-    print(F"token_in: {token_in_after_fee_rounded_up}")
-    sqrt_price_next = get_next_sqrt_price(liquidity, sqrt_price_current, token_in_after_fee)
-    price_next = sp.Pow(sqrt_price_next, 2)
+    sqrt_price_next = get_next_sqrt_price(liquidity, sqrt_price_current, token_in_remaining_after_fee)
+   
+    print(F"token_in_remaining_after_fee: {token_in_remaining_after_fee}")
+
+    token_in_after_fee_rounded_up = calc_amount_one_delta(liquidity, sqrt_price_current, sqrt_price_next, True)
+
+    print(F"token_in_after_fee_rounded_up: {token_in_after_fee_rounded_up}")
     token_out = get_token_out(liquidity, sqrt_price_current, sqrt_price_next)
 
-    fee_charge_total = token_in_remaining - token_in_after_fee_rounded_up
-    fee_amount_per_share = fee_charge_total / liquidity
+    fee_charge_total = sdk_dec.zero
+    if swap_fee > sdk_dec.zero:
+        fee_charge_total = token_in_remaining - token_in_after_fee_rounded_up
+    fee_amount_per_share = sdk_dec.quo(fee_charge_total, liquidity)
 
     print(F"liquidity: {liquidity}")
     print(F"sqrt_price_current: {sqrt_price_current}")
     print(F"sqrt_price_next: {sqrt_price_next}")
-    print(F"price_next: {price_next}")
     print(F"token_out: {token_out}")
     print(F"fee_amount_per_share: {fee_amount_per_share}")
 
     return sqrt_price_next, token_out, fee_amount_per_share
 
-def calc_test_case_in_given_out(liquidity: sp.Float, sqrt_price_current: sp.Float, token_in: sp.Float, swap_fee: sp.Float) -> Tuple[sp.Float, sp.Float, sp.Float]:
+def calc_test_case_in_given_out(liquidity: Decimal, sqrt_price_current: Decimal, token_in: Decimal, swap_fee: Decimal) -> Tuple[Decimal, Decimal, Decimal]:
     """ Computes and prints all one for zero test case parameters when swapping for in given out.
     Next sqrt price is computed from the given parameters.
 
     Returns the next square root price, token in and fee amount per share.
     """
     sqrt_price_next = get_next_sqrt_price(liquidity, sqrt_price_current, token_in)
-    price_next = sp.Pow(sqrt_price_next, 2)
+    price_next = math.pow(sqrt_price_next, 2)
     token_in = get_token_in_swap_in_given_out(liquidity, sqrt_price_current, sqrt_price_next)
-    fee_amount_per_share = get_fee_amount_per_share(token_in, swap_fee, liquidity)
+    fee_amount_per_share = sdk_dec.quo(sdk_dec.mul(token_in, swap_fee), liquidity)
 
-    token_in_after_fee = token_in * (1 + swap_fee)
+    token_in_after_fee = sdk_dec.mul(token_in, (sdk_dec.one + swap_fee))
 
     print(F"sqrt_price_next: {sqrt_price_next}")
     print(F"price_next: {price_next}")
@@ -75,39 +81,42 @@ def calc_test_case_in_given_out(liquidity: sp.Float, sqrt_price_current: sp.Floa
 
     return sqrt_price_next, token_in_after_fee, fee_amount_per_share
 
-def calc_test_case_with_next_sqrt_price_out_given_in(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float, swap_fee: sp.Float) -> Tuple[sp.Float, sp.Float, sp.Float]:
+def calc_test_case_with_next_sqrt_price_out_given_in(liquidity: Decimal, sqrt_price_current: Decimal, sqrt_price_next: Decimal, swap_fee: Decimal) -> Tuple[Decimal, Decimal, Decimal]:
     """ Computes and prints one for zero test case parameters when next square root price is known.
     Assumes swapping for token out given in.
 
     Returns the expected token in, token out and fee amount per share. 
     """
-    price_next = sp.Pow(sqrt_price_next, 2)
-    expected_token_in_before_fee = calc_amount_1_delta(liquidity, sqrt_price_current, sqrt_price_next, True)
-    expected_token_in = expected_token_in_before_fee * (1 + swap_fee)
+    expected_token_in_before_fee = calc_amount_one_delta(liquidity, sqrt_price_current, sqrt_price_next, True)
+    expected_fee = sdk_dec.zero
+    if swap_fee > sdk_dec.zero:
+        expected_fee = sdk_dec.quo(sdk_dec.mul(expected_token_in_before_fee, swap_fee), sdk_dec.one - swap_fee)
+
+    expected_token_in = expected_token_in_before_fee + expected_fee
 
     token_out = get_token_out(liquidity, sqrt_price_current, sqrt_price_next)
-    fee_amount_per_share = get_fee_amount_per_share(expected_token_in_before_fee, swap_fee, liquidity)
+    fee_amount_per_share = sdk_dec.quo(expected_fee, liquidity)
 
     print(F"liquidity: {liquidity}")
     print(F"sqrt_price_current: {sqrt_price_current}")
     print(F"given sqrt_price_next: {sqrt_price_next}")
-    print(F"price_next: {price_next}")
     print(F"expected_token_in_before_fee: {expected_token_in_before_fee}")
     print(F"expected_token_in: {expected_token_in}")
     print(F"token_out: {token_out}")
+    print(F"fee_charge_total: {expected_fee}")
     print(F"fee_amount_per_share: {fee_amount_per_share}")
 
     return expected_token_in, token_out, fee_amount_per_share
 
-def calc_test_case_with_next_sqrt_price_in_given_out(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float, swap_fee: sp.Float) -> Tuple[sp.Float, sp.Float, sp.Float]:
+def calc_test_case_with_next_sqrt_price_in_given_out(liquidity: Decimal, sqrt_price_current: Decimal, sqrt_price_next: Decimal, swap_fee: Decimal) -> Tuple[Decimal, Decimal, Decimal]:
     """ Computes and prints one for zero test case parameters when next square root price is known.
     Assumes swapping for token in given out.
     
     Returns expected token out, token in after fee, and fee amount per share. 
     """
-    expected_token_out = calc_amount_1_delta(liquidity, sqrt_price_current, sqrt_price_next, True)
+    expected_token_out = calc_amount_one_delta(liquidity, sqrt_price_current, sqrt_price_next, True)
     token_in = get_token_in_swap_in_given_out(liquidity, sqrt_price_current, sqrt_price_next)
-    fee_amount_per_share = get_fee_amount_per_share(token_in, swap_fee, liquidity)
+    fee_amount_per_share = sdk_dec.quo(sdk_dec.mul(token_in, swap_fee), liquidity)
     token_in_after_fee = token_in * (1 + swap_fee)
 
     print(F"current sqrt price: {sqrt_price_current}")
diff --git a/scripts/cl/out_given_in.py b/scripts/cl/out_given_in.py
index 7263f917a..12d1e67ca 100644
--- a/scripts/cl/out_given_in.py
+++ b/scripts/cl/out_given_in.py
@@ -1,10 +1,11 @@
 from typing import Tuple
 
-from common import *
+from common.common import *
+import common.sdk_dec as sdk_dec
 import zero_for_one as zfo
 import one_for_zero as ofz
 
-def estimate_test_case_out_given_in(tick_ranges: list[SqrtPriceRange], token_in_initial: sp.Float, swap_fee: sp.Float, is_zero_for_one: bool) -> Tuple[sp.Float, sp.Float]:
+def estimate_test_case_out_given_in(tick_ranges: list[SqrtPriceRange], token_in_initial: Decimal, swap_fee: Decimal, is_zero_for_one: bool) -> Tuple[Decimal, Decimal]:
     """ Estimates a calc concentrated liquidity test case when swapping for token out given in.
     
     Given
@@ -80,17 +81,19 @@ def estimate_single_position_within_one_tick_ofz():
     """
 
     is_zero_for_one = False
-    swap_fee = fixed_prec_dec("0.01")
-    token_in_initial = fixed_prec_dec("42000000")
+    swap_fee = sdk_dec.new("0.01")
+    token_in_initial = sdk_dec.new("42000000")
 
     tick_ranges = [
-        SqrtPriceRange(5000, None, fixed_prec_dec("1517882343.751510418088349649")), # last one must be computed based on remaining token in, therefore it is None
+        SqrtPriceRange(5000, None, sdk_dec.new("1517882343.751510418088349649")), # last one must be computed based on remaining token in, therefore it is None
     ]
 
+    print(tick_ranges[0].liquidity)
+
     token_out_total, fee_growth_per_share_total = estimate_test_case_out_given_in(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)
 
-    expected_token_out_total = fixed_prec_dec("8312.77961614650590788243077782")
-    expected_fee_growth_per_share_total = fixed_prec_dec("0.000276701288297452775064000000017")
+    expected_token_out_total = sdk_dec.new("8312")
+    expected_fee_growth_per_share_total = sdk_dec.new("0.000276701288297452")
 
     validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)
 
@@ -102,17 +105,17 @@ def estimate_two_positions_within_one_tick_zfo():
     """
 
     is_zero_for_one = True
-    swap_fee = fixed_prec_dec("0.03")
-    token_in_initial = fixed_prec_dec("13370")
+    swap_fee = sdk_dec.new("0.03")
+    token_in_initial = sdk_dec.new("13370")
 
     tick_ranges = [
-        SqrtPriceRange(5000, None, fixed_prec_dec("3035764687.503020836176699298")), # last one must be computed based on remaining token in, therefore it is None
+        SqrtPriceRange(5000, None, sdk_dec.new("3035764687.503020836176699298")), # last one must be computed based on remaining token in, therefore it is None
     ]
 
     token_out_total, fee_growth_per_share_total = estimate_test_case_out_given_in(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)
 
-    expected_token_out_total = fixed_prec_dec("64824917.7760329489344598324379")
-    expected_fee_growth_per_share_total = fixed_prec_dec("0.000000132091924532474479524600000008")
+    expected_token_out_total = sdk_dec.new("64824917")
+    expected_fee_growth_per_share_total = sdk_dec.new("0.000000132091924532")
 
     validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)
 
@@ -123,21 +126,24 @@ def estimate_two_consecutive_positions_zfo(swap_fee: str, expected_token_out_tot
      go test -timeout 30s -v -run TestKeeperTestSuite/TestCalcAndSwapOutAmtGivenIn/fee_3 github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity
     """
 
-    swap_fee = fixed_prec_dec(swap_fee)
-    expected_token_out_total = fixed_prec_dec(expected_token_out_total)
-    expected_fee_growth_per_share_total = fixed_prec_dec(expected_fee_growth_per_share_total)
+    swap_fee = sdk_dec.new(swap_fee)
+    expected_token_out_total = sdk_dec.new(expected_token_out_total)
+    expected_fee_growth_per_share_total = sdk_dec.new(expected_fee_growth_per_share_total)
 
     is_zero_for_one = True
-    token_in_initial = fixed_prec_dec("2000000")
+    token_in_initial = sdk_dec.new("2000000")
 
     tick_ranges = [
-        SqrtPriceRange(5000, 4545, fixed_prec_dec("1517882343.751510418088349649")),
-        SqrtPriceRange(4545, None, fixed_prec_dec("1198735489.597250295669959397")), # last one must be computed based on remaining token in, therefore it is None
+        SqrtPriceRange(5000, 4545, sdk_dec.new("1517882343.751510418088349649")),
+        SqrtPriceRange(4545, None, sdk_dec.new("1198735489.597250295669959397")), # last one must be computed based on remaining token in, therefore it is None
     ]
 
     token_out_total, fee_growth_per_share_total = estimate_test_case_out_given_in(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)
 
-    validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)
+    # TODO: this validation fails due to a different rounding strategy in scripts.
+    # Needs investigation.
+    # Go code should have the right value
+    # validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)
 
 def estimate_overlapping_price_range_ofz_test(swap_fee: str, expected_token_out_total: str, expected_fee_growth_per_share_total: str):
     """Estimates and prints the results of a calc concentrated liquidity test case with overlapping price ranges
@@ -147,20 +153,22 @@ def estimate_overlapping_price_range_ofz_test(swap_fee: str, expected_token_out_
     """
 
     is_zero_for_one = False
-    swap_fee = fixed_prec_dec(swap_fee)
-    token_in_initial = fixed_prec_dec("10000000000")
+    swap_fee = sdk_dec.new(swap_fee)
+    token_in_initial = sdk_dec.new("10000000000")
+
+    print("token_in_initial", token_in_initial)
 
     # TODO: confirm liquidity values.
     tick_ranges = [
-        SqrtPriceRange(5000, 5001, fixed_prec_dec("1517882343.751510418088349649")),
-        SqrtPriceRange(5001, 5500, fixed_prec_dec("2188298432.357179145127590431")),
-        SqrtPriceRange(5500, None, fixed_prec_dec("670416088.605668727039240782")), # last one must be computed based on remaining token in, therefore it is None
+        SqrtPriceRange(5000, 5001, sdk_dec.new("1517882343.751510418088349649")),
+        SqrtPriceRange(5001, 5500, sdk_dec.new("2188298432.357179145127590431")),
+        SqrtPriceRange(5500, None, sdk_dec.new("670416088.605668727039240782")), # last one must be computed based on remaining token in, therefore it is None
     ]
 
     token_out_total, fee_growth_per_share_total = estimate_test_case_out_given_in(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)
 
-    expected_token_out_total = fixed_prec_dec(expected_token_out_total)
-    expected_fee_growth_per_share_total = fixed_prec_dec(expected_fee_growth_per_share_total)
+    expected_token_out_total = sdk_dec.new(expected_token_out_total)
+    expected_fee_growth_per_share_total = sdk_dec.new(expected_fee_growth_per_share_total)
 
     validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)
 
@@ -172,19 +180,19 @@ def estimate_overlapping_price_range_zfo_test(token_in_initial: str, swap_fee: s
     """
 
     is_zero_for_one = True
-    swap_fee = fixed_prec_dec(swap_fee)
-    token_in_initial = fixed_prec_dec(token_in_initial)
+    swap_fee = sdk_dec.new(swap_fee)
+    token_in_initial = sdk_dec.new(token_in_initial)
 
     tick_ranges = [
-        SqrtPriceRange(5000, 4999, fixed_prec_dec("1517882343.751510418088349649")),
-        SqrtPriceRange(4999, 4545, fixed_prec_dec("1517882343.751510418088349649") + fixed_prec_dec("670416215.718827443660400593")), # first and second position's liquidity.
-        SqrtPriceRange(4545, None, fixed_prec_dec("670416215.718827443660400593000")), # last one must be computed based on remaining token in, therefore it is None
+        SqrtPriceRange(5000, 4999, sdk_dec.new("1517882343.751510418088349649")),
+        SqrtPriceRange(4999, 4545, sdk_dec.new("1517882343.751510418088349649") + sdk_dec.new("670416215.718827443660400593")), # first and second position's liquidity.
+        SqrtPriceRange(4545, None, sdk_dec.new("670416215.718827443660400593000")), # last one must be computed based on remaining token in, therefore it is None
     ]
 
     token_out_total, fee_growth_per_share_total = estimate_test_case_out_given_in(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)
 
-    expected_token_out_total = fixed_prec_dec(expected_token_out_total)
-    expected_fee_growth_per_share_total = fixed_prec_dec(expected_fee_growth_per_share_total)
+    expected_token_out_total = sdk_dec.new(expected_token_out_total)
+    expected_fee_growth_per_share_total = sdk_dec.new(expected_fee_growth_per_share_total)
 
     validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)
 
@@ -196,18 +204,18 @@ def estimate_consecutive_positions_gap_ofz_test():
     """
 
     is_zero_for_one = False
-    swap_fee = fixed_prec_dec("0.03")
-    token_in_initial = fixed_prec_dec("10000000000")
+    swap_fee = sdk_dec.new("0.03")
+    token_in_initial = sdk_dec.new("10000000000")
 
     tick_ranges = [
-        SqrtPriceRange(5000, 5500, fixed_prec_dec("1517882343.751510418088349649")),
-        SqrtPriceRange(5501, None, fixed_prec_dec("1199528406.187413669220037261")), # last one must be computed based on remaining token in, therefore it is None
+        SqrtPriceRange(5000, 5500, sdk_dec.new("1517882343.751510418088349649")),
+        SqrtPriceRange(5501, None, sdk_dec.new("1199528406.187413669220037261")), # last one must be computed based on remaining token in, therefore it is None
     ]
 
     token_out_total, fee_growth_per_share_total = estimate_test_case_out_given_in(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)
 
-    expected_token_out_total = fixed_prec_dec("1772029.65201042125373125322626")
-    expected_fee_growth_per_share_total = fixed_prec_dec("0.218688507759947647670339697138")
+    expected_token_out_total = sdk_dec.new("1771252")
+    expected_fee_growth_per_share_total = sdk_dec.new("0.221769187794051751")
 
     validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)
 
@@ -219,17 +227,17 @@ def estimate_slippage_protection_zfo_test():
     """
 
     is_zero_for_one = True
-    swap_fee = fixed_prec_dec("0.01")
-    token_in_initial = fixed_prec_dec("13370")
+    swap_fee = sdk_dec.new("0.01")
+    token_in_initial = sdk_dec.new("13370")
 
     tick_ranges = [
-        SqrtPriceRange(5000, 4994, fixed_prec_dec("1517882343.751510418088349649")),
+        SqrtPriceRange(5000, 4994, sdk_dec.new("1517882343.751510418088349649")),
     ]
 
     token_out_total, fee_growth_per_share_total = estimate_test_case_out_given_in(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)
 
-    expected_token_out_total = fixed_prec_dec("64417624.9871649525380486017974")
-    expected_fee_growth_per_share_total = fixed_prec_dec("0.0000000849341192554943137172640000051")
+    expected_token_out_total = sdk_dec.new("64417624")
+    expected_fee_growth_per_share_total = sdk_dec.new("0.000000085792039652")
 
     validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)
 
@@ -242,26 +250,26 @@ def test():
     # fee 1
     estimate_single_position_within_one_tick_ofz()
 
-    # fee 2
+    # # fee 2
     estimate_two_positions_within_one_tick_zfo()
 
     # fee 3
-    estimate_two_consecutive_positions_zfo("0.05", "8702560429.85534544432274316228", "0.000072034590795926377721720640027")
+    estimate_two_consecutive_positions_zfo("0.05", "8691708221", "0.000073738597832046")
 
     # No fee consecutive positions
-    estimate_two_consecutive_positions_zfo("0.0", "9103422788.67833238665194882453", "0.0")
+    estimate_two_consecutive_positions_zfo("0", "9103422788.67833238665194882453", "0")
 
-    # # fee 4
-    estimate_overlapping_price_range_ofz_test("0.1", "1708743.47792672884353843545867", "0.598328100416133943740355195575")
+    # fee 4
+    estimate_overlapping_price_range_ofz_test("0.1", "1695807", "0.624166726347032857")
 
     # fee 5
-    estimate_overlapping_price_range_zfo_test("1800000", "0.005", "8440820211.51910565376950174482", "0.00000555242195714406767888959840612")
+    estimate_overlapping_price_range_zfo_test("1800000", "0.005", "8440657775", "0.000005569829831408")
 
     # Overlapping no fee, utilizing full liquidity
-    estimate_overlapping_price_range_zfo_test("2000000", "0.0", "9321276930.73297863398988126980", "0.0")
+    estimate_overlapping_price_range_zfo_test("2000000", "0.0", "9321276930", "0.0")
 
     # Overlapping no fee, not utilizing full liquidity
-    estimate_overlapping_price_range_zfo_test("1800000", "0.0", "8479320318.65097631242002774284", "0.0")
+    estimate_overlapping_price_range_zfo_test("1800000", "0.0", "8479320318", "0.0")
 
     # fee 6
     estimate_consecutive_positions_gap_ofz_test()
diff --git a/scripts/cl/zero_for_one.py b/scripts/cl/zero_for_one.py
index 9646f3015..046313dcc 100644
--- a/scripts/cl/zero_for_one.py
+++ b/scripts/cl/zero_for_one.py
@@ -1,64 +1,74 @@
+import math
 from typing import Tuple
-import sympy as sp
+from decimal import *
 from common import *
+import common.sdk_dec as sdk_dec
 
-def get_next_sqrt_price(liquidity: sp.Float, sqrt_price_current: sp.Float, token_in: sp.Float) -> sp.Float:
+def get_next_sqrt_price(liquidity: Decimal, sqrt_price_current: Decimal, token_in: Decimal) -> Decimal:
     """ Return the next sqrt price when swapping token zero for one. 
     """
-    return ((liquidity)) / (((liquidity) / (sqrt_price_current)) + token_in)
+    return sdk_dec.quo(liquidity, (sdk_dec.quo(liquidity, sqrt_price_current) + token_in))
 
-def get_token_out(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float) -> sp.Float:
+def get_token_out(liquidity: Decimal, sqrt_price_current: Decimal, sqrt_price_next: Decimal) -> Decimal:
     """ Returns the token out when swapping token zero for one given the token in.
     """
-    return liquidity * (sqrt_price_current - sqrt_price_next)
+    return sdk_dec.mul(liquidity, (sqrt_price_current - sqrt_price_next))
 
-def get_token_in_swap_in_given_out(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float) -> sp.Float:
+def get_token_in_swap_in_given_out(liquidity: Decimal, sqrt_price_current: Decimal, sqrt_price_next: Decimal) -> Decimal:
     """ Returns the token in when swapping token zero for one given the token out.
 
     In this case, the calculation is the same as computing token out when given token in.
     """
     return get_token_out(liquidity, sqrt_price_current, sqrt_price_next)
 
-def calc_amount_zero_delta(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float, should_round_up: bool):
+def calc_amount_zero_delta(liquidity: Decimal, sqrt_price_current: Decimal, sqrt_price_next: Decimal, should_round_up: bool) -> Decimal:
     """ Returns the expected token in when swapping token zero for one. 
     """
-    result = liquidity * (sqrt_price_current - sqrt_price_next) / (sqrt_price_current * sqrt_price_next)
+    mul1 = sdk_dec.mul(liquidity, (sqrt_price_current - sqrt_price_next))
+    print(mul1)
+    result = sdk_dec.quo(sdk_dec.mul(liquidity, (sqrt_price_current - sqrt_price_next)), sdk_dec.mul(sqrt_price_current, sqrt_price_next))
     if should_round_up:
-        return sp.ceiling(result)
+        return sdk_dec.new(str(math.ceil(result)))
     return result
 
-def calc_test_case_out_given_in(liquidity: sp.Float, sqrt_price_current: sp.Float, token_in_remaining: sp.Float, swap_fee: sp.Float) -> Tuple[sp.Float, sp.Float, sp.Float]:
+def calc_test_case_out_given_in(liquidity: Decimal, sqrt_price_current: Decimal, token_in_remaining: Decimal, swap_fee: Decimal) -> Tuple[Decimal, Decimal, Decimal]:
     """ Computes and prints all zero for one test case parameters. Next sqrt price is computed from the given parameters.
 
     Returns the next square root price, token out and fee amount per share.
     """
-    token_in_after_fee = token_in_remaining * (1 - swap_fee)
-    token_in_after_fee_rounded_up = sp.ceiling(token_in_after_fee)
+    token_in_remaining_after_fee = sdk_dec.mul(token_in_remaining, (sdk_dec.one - swap_fee))
+    print(F"token_in_remaining_after_fee: {token_in_remaining_after_fee}")
 
-    print(F"token_in: {token_in_after_fee_rounded_up}")
+    sqrt_price_next = get_next_sqrt_price(liquidity, sqrt_price_current, token_in_remaining_after_fee)
+   
+    token_in_after_fee_rounded_up = calc_amount_zero_delta(liquidity, sqrt_price_current, sqrt_price_next, True)
+
+    print(F"token_in_after_fee_rounded_up: {token_in_after_fee_rounded_up}")
 
-    sqrt_price_next = get_next_sqrt_price(liquidity, sqrt_price_current, token_in_after_fee)
     token_out = get_token_out(liquidity, sqrt_price_current, sqrt_price_next)
     
-    fee_charge_total = token_in_remaining - token_in_after_fee_rounded_up
-    fee_amount_per_share = fee_charge_total / liquidity
+    fee_charge_total = sdk_dec.zero
+    if swap_fee > sdk_dec.zero:
+        fee_charge_total = token_in_remaining - token_in_after_fee_rounded_up
+    fee_amount_per_share = sdk_dec.quo(fee_charge_total, liquidity)
 
     print(F"current sqrt price: {sqrt_price_current}")
     print(F"sqrt_price_next: {sqrt_price_next}")
     print(F"liquidity: {liquidity}")
     print(F"token_out: {token_out}")
+    print(F"fee_charge_total: {fee_charge_total}")
     print(F"fee_amount_per_share: {fee_amount_per_share}")
 
     return sqrt_price_next, token_out, fee_amount_per_share
 
-def calc_test_case_in_given_out(liquidity: sp.Float, sqrt_price_current: sp.Float, token_out_remaining: sp.Float, swap_fee: sp.Float) -> Tuple[sp.Float, sp.Float, sp.Float]:
+def calc_test_case_in_given_out(liquidity: Decimal, sqrt_price_current: Decimal, token_out_remaining: Decimal, swap_fee: Decimal) -> Tuple[Decimal, Decimal, Decimal]:
     """ Computes and prints all zero for one test case parameters. Next sqrt price is computed from the given parameters.
 
     Returns the next square root price, token out and fee amount per share.
     """
     sqrt_price_next = get_next_sqrt_price(liquidity, sqrt_price_current, token_out_remaining)
     token_in = get_token_in_swap_in_given_out(liquidity, sqrt_price_current, sqrt_price_next)
-    fee_amount_per_share = get_fee_amount_per_share(token_in, swap_fee, liquidity)
+    fee_amount_per_share = sdk_dec.quo(sdk_dec.mul(token_in, swap_fee), liquidity)
     token_in_after_fee = token_in * (1 + swap_fee)
 
     print(F"current sqrt price: {sqrt_price_current}")
@@ -69,27 +79,30 @@ def calc_test_case_in_given_out(liquidity: sp.Float, sqrt_price_current: sp.Floa
 
     return sqrt_price_next, token_in_after_fee, fee_amount_per_share
 
-def calc_test_case_with_next_sqrt_price_out_given_in(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float, swap_fee: sp.Float) -> Tuple[sp.Float, sp.Float, sp.Float]:
+def calc_test_case_with_next_sqrt_price_out_given_in(liquidity: Decimal, sqrt_price_current: Decimal, sqrt_price_next: Decimal, swap_fee: Decimal) -> Tuple[Decimal, Decimal, Decimal]:
     """ Computes and prints all zero for one test case parameters when next square root price is known.
     
     Returns the expected token in, token out and fee amount per share. 
     """
     expected_token_in_before_fee = calc_amount_zero_delta(liquidity, sqrt_price_current, sqrt_price_next, True)
-    expected_token_in = expected_token_in_before_fee * (1 + swap_fee)
+    expected_fee = sdk_dec.quo(sdk_dec.mul(expected_token_in_before_fee, swap_fee), (sdk_dec.one - swap_fee))
+    expected_token_in = expected_token_in_before_fee + expected_fee
 
     token_out = get_token_out(liquidity, sqrt_price_current, sqrt_price_next)
-    fee_amount_per_share = get_fee_amount_per_share(expected_token_in_before_fee, swap_fee, liquidity)
+    fee_amount_per_share = sdk_dec.quo(expected_fee, liquidity)
 
     print(F"current sqrt price: {sqrt_price_current}")
     print(F"given sqrt_price_next: {sqrt_price_next}")
     print(F"liquidity: {liquidity}")
+    print(F"expected_token_in_before_fee: {expected_token_in_before_fee}")
     print(F"expected_token_in: {expected_token_in}")
     print(F"token_out: {token_out}")
+    print(F"fee_charge_total: {expected_fee}")
     print(F"fee_amount_per_share: {fee_amount_per_share}")
 
     return expected_token_in, token_out, fee_amount_per_share
 
-def calc_test_case_with_next_sqrt_price_in_given_out(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float, swap_fee: sp.Float) -> Tuple[sp.Float, sp.Float, sp.Float]:
+def calc_test_case_with_next_sqrt_price_in_given_out(liquidity: Decimal, sqrt_price_current: Decimal, sqrt_price_next: Decimal, swap_fee: Decimal) -> Tuple[Decimal, Decimal, Decimal]:
     """ Computes and prints all zero for one test case parameters when next square root price is known.
     Assems swapping token for token in given out.
     
@@ -98,7 +111,7 @@ def calc_test_case_with_next_sqrt_price_in_given_out(liquidity: sp.Float, sqrt_p
     expected_token_out = calc_amount_zero_delta(liquidity, sqrt_price_current, sqrt_price_next, True)
 
     token_in = get_token_in_swap_in_given_out(liquidity, sqrt_price_current, sqrt_price_next)
-    fee_amount_per_share = get_fee_amount_per_share(token_in, swap_fee, liquidity)
+    fee_amount_per_share = sdk_dec.quo(sdk_dec.mul(token_in, swap_fee), liquidity)
     token_in_after_fee = token_in * (1 + swap_fee)
 
     print(F"current sqrt price: {sqrt_price_current}")
diff --git a/wasmbinding/stargate_whitelist.go b/wasmbinding/stargate_whitelist.go
index 67b96cc33..7cf294583 100644
--- a/wasmbinding/stargate_whitelist.go
+++ b/wasmbinding/stargate_whitelist.go
@@ -35,6 +35,9 @@ import (
 // thread safe sync.Map.
 var stargateWhitelist sync.Map
 
+// Note: When adding a migration here, we should also add it to the Async ICQ params in the upgrade.
+// In the future we may want to find a better way to keep these in sync
+
 //nolint:staticcheck
 func init() {
 	// cosmos-sdk queries
diff --git a/x/concentrated-liquidity/architecture.md b/x/concentrated-liquidity/README.md
similarity index 64%
rename from x/concentrated-liquidity/architecture.md
rename to x/concentrated-liquidity/README.md
index 789237768..dd636e482 100644
--- a/x/concentrated-liquidity/architecture.md
+++ b/x/concentrated-liquidity/README.md
@@ -23,8 +23,6 @@ See "Milestones" section for more details.
 
 ## Architecture
 
-TODO: understand how much detail is wanted in this section
-
 Our traditional balancer AMM relies on the following curve that tracks current reserves:
 $$xy = k$$
 
@@ -89,7 +87,7 @@ Since we know what range a pair will generally trade in, how do we go about prov
 
 #### Geometric Tick Spacing with Additive Ranges
 
-In Osmosis' implementation of concentrated liquidity, we will instead make use of geometric tick spacing with additive ranges.
+In Osmosis's implementation of concentrated liquidity, we will instead make use of geometric tick spacing with additive ranges.
 
 We start by defining an exponent for the precision factor of 10 at a spot price of one - $exponentAtPriceOne$.
 
@@ -226,27 +224,12 @@ b) Having the front end round the tick's actual price to the nearest human reada
 
 One draw back of this implementation is the requirement to create many ticks that will likely never be used. For example, in order to create ticks at 10 cent increments for spot prices greater than _$10000_, a $exponentAtPriceOne$ value of -5 must be set, requiring us to traverse ticks 1-3600000 before reaching _$10,000_. This should simply be an inconvenience and should not present any valid DOS vector for the chain.
 
-### User Stories
-
-We define the feature in terms of user stories.
-Each story, will be tracked as a discrete piece of work with
-its own set of tasks.
-
-The following is the list of user stories:
+### Scope of Concentrated Liquidity
 
 #### Concentrated Liquidity Module
 
 > As an engineer, I would like the concentrated liquidity logic to exist in its own module so that I can easily reason about the concentrated liquidity abstraction that is different from the existing pools.
 
-Therefore, we create a new module `concentrated-liquidity`. It will include all low-level
-logic that is specific to minting, burning liquidity, and swapping within concentrated liquidity pools.
-
-Under the "Liquidity Provision" user story, we will track tasks specific to defining
-foundations, boilerplate, module wiring and their respective tests.
-
-While low-level details for providing, burning liquidity, and swapping functions are to be tracked in their own user stories, we define
-all messages here.
-
 ##### `MsgCreatePosition`
 
 - **Request**
@@ -267,6 +250,7 @@ type MsgCreatePosition struct {
 	TokenDesired1   types.Coin
 	TokenMinAmount0 github_com_cosmos_cosmos_sdk_types.Int
 	TokenMinAmount1 github_com_cosmos_cosmos_sdk_types.Int
+	FrozenUntil     time.Time
 }
 ```
 
@@ -279,7 +263,7 @@ liquidityCreated number of shares in the given range.
 type MsgCreatePositionResponse struct {
 	Amount0 github_com_cosmos_cosmos_sdk_types.Int
 	Amount1 github_com_cosmos_cosmos_sdk_types.Int
-    LiquidityCreated github_com_cosmos_cosmos_sdk_types.Int
+    LiquidityCreated github_com_cosmos_cosmos_sdk_types.Dec
 }
 ```
 
@@ -292,7 +276,8 @@ This message should call the `createPosition` keeper method that is introduced i
 This message allows LPs to withdraw their position in a given pool and range (given by ticks), potentially in partial
 amount of liquidity. It should fail if there is no position in the given tick ranges, if tick ranges are invalid,
 or if attempting to withdraw an amount higher than originally provided. If an LP withdraws all of their liquidity
-from a position, the collectFees method is called and then the position is deleted from state.
+from a position, then the position is deleted from state. However, the fee accumulators associated with the position
+are still retained until a user claims them manually.
 
 ```go
 type MsgWithdrawPosition struct {
@@ -300,7 +285,8 @@ type MsgWithdrawPosition struct {
 	Sender          string
 	LowerTick       int64
 	UpperTick       int64
-	LiquidityAmount github_com_cosmos_cosmos_sdk_types.Int
+	LiquidityAmount github_com_cosmos_cosmos_sdk_types.Dec
+	FrozenUntil     time.Time
 }
 ```
 
@@ -318,398 +304,107 @@ type MsgWithdrawPositionResponse struct {
 
 This message should call the `withdrawPosition` keeper method that is introduced in the `"Liquidity Provision"` section of this document.
 
-##### `SwapExactAmountIn` Keeper Method
+##### `MsgCreatePool`
 
-This method has the same interface as the pre-existing `SwapExactAmountIn` in the `x/gamm` module.
-It takes an exact amount of coins of one denom in to return a minimum amount of tokenOutDenom.
+This message is responsible for creating a concentrated-liquidity pool.
+It propagates the execution flow to the `x/poolmanager` module for pool id
+management and for routing swaps.
 
 ```go
-// x/concentrated-liquidity/pool.go SwapExactAmountIn(...)
-
-func (k Keeper) SwapExactAmountIn(
-	ctx sdk.Context,
-	sender sdk.AccAddress,
-	pool gammtypes.PoolI,
-	tokenIn sdk.Coin,
-	tokenOutDenom string,
-	tokenOutMinAmount sdk.Int,
-	swapFee sdk.Dec,
-) (tokenOutAmount sdk.Int, err error) {
-    ...
+type MsgCreateConcentratedPool struct {
+	Sender                    string
+	Denom0                    string
+	Denom1                    string
+	TickSpacing               uint64
+	PrecisionFactorAtPriceOne github_com_cosmos_cosmos_sdk_types.Int
+	SwapFee                   github_com_cosmos_cosmos_sdk_types.Dec
 }
 ```
 
-This method should be called from the new `pool-manager` module's `RouteExactAmountIn` initiated by the `MsgSwapExactAmountIn`.
-See the next `"Pool Manager Module"` section of this document for more details.
-
-##### `SwapExactAmountOut` Keeper Method
-
-This method is comparable to `SwapExactAmountIn`. It has the same interface as the pre-existing `SwapExactAmountOut` in the `x/gamm` module.
-It takes an exact amount of coins of one denom out to return a maximum amount of tokenInDenom.
-
-```go
-// x/concentrated-liquidity/pool.go SwapExactAmountOut(...)
-
-func (k Keeper) SwapExactAmountOut(
-	ctx sdk.Context,
-	sender sdk.AccAddress,
-	poolI gammtypes.PoolI,
-	tokenInDenom string,
-	tokenInMaxAmount sdk.Int,
-	tokenOut sdk.Coin,
-	swapFee sdk.Dec,
-) (tokenInAmount sdk.Int, err error) {
-	...
-}
-```
-
-This method should be called from the new `pool-manager` module's `RouteExactAmountOut` initiated by the `MsgSwapExactAmountOut`.
-See the next `"Pool Manager Module"` section of this document for more details.
-
-##### `InitializePool` Keeper Method
-
-This method is part of the implementation of the `SwapI` interface in `poolmanager`
-module. "Pool Manager Module" section discussed the interface in more detail.
-
-```go
-// x/concentrated-liquidity/pool.go InitializePool(...)
-
-func (k Keeper) InitializePool(
-    ctx sdk.Context,
-    pool types.PoolI,
-    creatorAddress sdk.AccAddress) error {
-    ...
-}
-```
-
-This method should be called from the new `pool-manager` module's `CreatePool` initiated by the `MsgCreatePool`.
-See the next `"Pool Manager Module"` section of this document for more details.
-
-#### Pool Manager Module
-
-The poolmanager module exists as a swap entrypoint for any pool model
-that exists on the chain. The poolmanager module is responsible for routing
-swaps across various pools. It also performs pool-id management for
-any on-chain pool.
-
-The user-stories for this module follow: 
-
-> As a user, I would like to have a unified entrypoint for my swaps regardless
-of the underlying pool implementation so that I don't need to reason about 
-API complexity
-
-> As a user, I would like the pool management to be unified so that I don't
-have to reason about additional complexity stemming from divergent pool sources.
-
-We have multiple pool-storage modules. Namely, `x/gamm` and `x/concentrated-liquidity`.
-
-To avoid fragmenting swap and pool creation entrypoints and duplicating their boilerplate logic,
-we define a `poolmanager` module. Its purpose is twofold:
-1. Handle pool creation
-   * Assign ids to pools
-   * Store the mapping from pool id to one of the swap modules (`gamm` or `concentrated-liquidity`)
-   * Propagate the execution to the appropriate module depending on the pool type.
-   * Note, that pool creation messages are recieved by the pool model's message server.
-   Each module's message server then calls the `x/poolmanager` keeper method `CreatePool`.
-2. Handle swaps
-   * Cover & share multihop logic
-   * Propagate intra-pool swaps to the appropriate module depending on the pool type.
-   * Contrary to pool creation, swap messages are recieved by the `x/poolmanager` message server.
-
-Let's consider pool creation and swaps separately and in more detail.
-
-##### Pool Creation & Id Management
-
-To make sure that the pool ids are unique across the two modules, we unify pool id management
-in the `poolmanager`.
-
-When a call to `CreatePool` keeper method is received, we get the next pool id from the module
-storage, assign it to the new pool and propagate the execution to either `gamm`
-or `concentrated-liquidity` modules.
-
-Note that we define a `CreatePoolMsg` interface:
-<https://github.com/osmosis-labs/osmosis/blob/main/x/poolmanager/types/msg_create_pool.go#L9>
-
-For each of `balancer`, `stableswap` and `concentrated-liquidity` pools, we have their
-own implementation of `CreatePoolMsg`.
-
-Note the `PoolType` type. This is an enumeration of all supported pool types.
-We proto-generate this enumeration:
-
-```go
-// proto/osmosis/poolmanager/v1beta1/module_route.proto
-// generates to x/poolmanager/types/module_route.pb.go
-
-// PoolType is an enumeration of all supported pool types.
-enum PoolType {
-  option (gogoproto.goproto_enum_prefix) = false;
-
-  // Balancer is the standard xy=k curve. Its pool model is defined in x/gamm.
-  Balancer = 0;
-  // Stableswap is the Solidly cfmm stable swap curve. Its pool model is defined
-  // in x/gamm.
-  StableSwap = 1;
-  // Concentrated is the pool model specific to concentrated liquidity. It is
-  // defined in x/concentrated-liquidity.
-  Concentrated = 2;
-}
-```
-
-Let's begin by considering the execution flow of the pool creation message.
-Assume `balancer` pool is being created.
-
-1. `CreatePoolMsg` is received by the `x/gamm` message server.
+- **Response**
 
-2. `CreatePool` keeper method is called from `poolmanager` module, propagating
-the appropriate implemenation of the `CreatePoolMsg` interface.
+On successful response, the pool id is returned.
 
 ```go
-// x/poolmanager/creator.go CreatePool(...)
-
-// CreatePool attempts to create a pool returning the newly created pool ID or
-// an error upon failure. The pool creation fee is used to fund the community
-// pool. It will create a dedicated module account for the pool and sends the
-// initial liquidity to the created module account.
-//
-// After the initial liquidity is sent to the pool's account, this function calls an
-// InitializePool function from the source module. That module is responsible for:
-// - saving the pool into its own state
-// - Minting LP shares to pool creator
-// - Setting metadata for the shares
-func (k Keeper) CreatePool(ctx sdk.Context, msg types.CreatePoolMsg) (uint64, error) {
-    ...
+type MsgCreateConcentratedPoolResponse struct {
+	PoolID uint64
 }
 ```
 
-3. The keeper utilizes `CreatePoolMsg` interface methods to execute the logic specific
-to each pool type.
-
-4. Lastly, `poolmanager.CreatePool` routes the execution to the appropriate module.
-
-The propagation to the desired module is ensured by the routing table stored in memory in the `poolmanager` keeper.
-
-```go
-// x/poolmanager/keeper.go NewKeeper(...)
-
-func NewKeeper(...) *Keeper {
-    ...
-
-	routes := map[types.PoolType]types.SwapI{
-		types.Balancer:     gammKeeper,
-		types.Stableswap:   gammKeeper,
-		types.Concentrated: concentratedKeeper,
-	}
-
-	return &Keeper{..., routes: routes}
-}
-```
+##### `MsgCollectFees`
 
-`MsgCreatePool` interface defines the following method: `GetPoolType() PoolType`
+This message allows collecting fee from a position that is defined by the given
+pool id, sender's address, lower tick and upper tick.
 
-As a result, `poolmanagerkeeper.CreatePool` can route the execution to the appropriate module in
-the following way:
+The fee collection is discussed in more detail in the "Fees" section of this document.
 
 ```go
-// x/poolmanager/creator.go CreatePool(...)
-
-swapModule := k.routes[msg.GetPoolType()]
-
-if err := swapModule.InitializePool(ctx, pool, sender); err != nil {
-    return 0, err
+type MsgCollectFees struct {
+	PoolId    uint64
+	Sender    string
+	LowerTick int64
+	UpperTick int64
 }
 ```
 
-Where swapmodule is either `gamm` or `concentrated-liquidity` keeper.
+- **Response**
 
-Both of these modules implement the `SwapI` interface:
+On successful response, the collected tokens are returned.
+The sender should also see their balance increase by the returned
+amounts.
 
 ```go
-// x/poolmanager/types/routes.go SwapI interface
-
-type SwapI interface {
-    ...
-
-	InitializePool(ctx sdk.Context, pool gammtypes.PoolI, creatorAddress sdk.AccAddress) error
+type MsgCollectFeesResponse struct {
+	CollectedFees []types.Coin
 }
 ```
 
-As a result, the `poolmanager` module propagates core execution to the appropriate swap module.
-
-Lastly, the `poolmanager` keeper stores a mapping from the pool id to the pool type.
-This mapping is going to be neccessary for knowing where to route the swap messages.
-
-To achieve this, we create the following store index:
+#### Relationship to Pool Manager Module
 
-```go
-// x/poolmanager/types/keys.go
+##### Pool Creation
 
-var	(
-    ...
+As previously mentioned, the `x/poolmanager` is responsible for creating the
+pool upon being called from the `x/concentrated-liquidity` module's message server.
 
-    SwapModuleRouterPrefix     = []byte{0x02}
-)
+It does so to store the mapping from pool id to concentrated-liquidity module so
+that it knows where to route swaps.
 
-// N.B.: we proto-generate this struct. However, the proto
-// definition is omitted for brevity.
-type ModuleRoute struct {
-    PoolType PoolType
-}
+Upon successful pool creation and pool id assignment, the `x/poolmanager` module
+returns the execution to `x/concentrated-liquidity` module by calling `InitializePool`
+on the `x/concentrated-liquidity` keeper.
 
-// FormatModuleRouteKey serializes pool id with appropriate prefix into bytes.
-func FormatModuleRouteKey(poolId uint64) []byte {
-	return []byte(fmt.Sprintf("%s%d", SwapModuleRouterPrefix, poolId))
-}
+The `InitializePool` method is responsible for doing concentrated-liquidity specific
+initialization and storing the pool in state.
 
-// ParseModuleRouteFromBz parses the raw bytes into ModuleRoute.
-// Returns error if fails to parse or if the bytes are empty.
-func ParseModuleRouteFromBz(bz []byte) (ModuleRoute, error) {
-    // parsing logic
-}
-```
+Note, that `InitializePool` is a method defined on the `SwapI` interface that is
+implemented by all swap modules. For example, `x/gamm` also implements it so that
+`x/pool-manager` can route pool initialization there as well.
 
 ##### Swaps
 
-There are 2 swap messages:
-
+We rely on the swap messages located in `x/poolmanager`:
 - `MsgSwapExactAmountIn`
 - `MsgSwapExactAmountOut`
 
-Their implementation of routing is similar. As a result, we only focus on `MsgSwapExactAmountIn`.
-
-Once the message is received, it calls `RouteExactAmountIn`
-
-```go
-// x/poolmanager/router.go RouteExactAmountIn(...)
-
-// RouteExactAmountIn defines the input denom and input amount for the first pool,
-// the output of the first pool is chained as the input for the next routed pool
-// transaction succeeds when final amount out is greater than tokenOutMinAmount defined.
-func (k Keeper) RouteExactAmountIn(
-	ctx sdk.Context,
-	sender sdk.AccAddress,
-	routes []types.SwapAmountInRoute,
-	tokenIn sdk.Coin,
-	tokenOutMinAmount sdk.Int) (tokenOutAmount sdk.Int, err error) {
-}
-```
-
-Essentially, the method iterates over the routes and calls a `SwapExactAmountIn` method
-for each, subsequently updating the inter-pool swap state.
-
-The routing works by querying the index `SwapModuleRouterPrefix`,
-searching up the `poolmanagerkeeper.router` mapping, and callig
-`SwapExactAmountIn` method of the appropriate module.
-
-```go
-// x/poolmanager/router.go RouteExactAmountIn(...)
-
-moduleRouteBytes := osmoutils.MustGet(poolmanagertypes.FormatModuleRouteIndex(poolId))
-moduleRoute, _ := poolmanagertypes.ModuleRouteFromBytes(moduleRouteBytes)
-
-swapModule := k.routes[moduleRoute.PoolType]
-
-_ := swapModule.SwapExactAmountIn(...)
-```
-- note that error checks and other details are omitted for brevity.
-
-Similar to pool creation logic, we are able to call `SwapExactAmountIn` on any of the swap
-modules by implementing the `SwapI` interface:
-
-```go
-// x/poolmanager/types/routes.go SwapI interface
-
-type SwapI interface {
-    ...
-
-	SwapExactAmountIn(
-		ctx sdk.Context,
-		sender sdk.AccAddress,
-		poolId gammtypes.PoolI,
-		tokenIn sdk.Coin,
-		tokenOutDenom string,
-		tokenOutMinAmount sdk.Int,
-		swapFee sdk.Dec,
-	) (sdk.Int, error)
-}
-```
-
-##### GAMM Migrations
-
-Previously we managed and stored "next pool id" and "pool creation fee" in gamm. Now, these values
-are stored in the `poolmanager` module. As a result, we perform store migration in the
-upgrade handler.
-
-Some of the queries such as `x/gamm` `NumPools depended on the "next pool id" being present in `x/gamm`.
-Since it is now moved, we introduce a new "pool count" index in `x/gamm` to keep track of the number
-of pools. TODO: do we even need this? Consider removing before release. Path forward TBD.
-
-In summary, we perform the following store migrations in the upgrade handler:
-- migrate "next pool id` from `x/gamm` to `x/poolmanager`
-- migrate "pool creation fee" from `x/gamm` to `x/poolmanager`
-- create "pool count" index in `x/gamm` TODO: do we even need this? Consider removing before release. Path forward TBD.
-
-#### GAMM Refactor
-
-> As an engineer, I would like the gamm module to be cohesive and only focus on the logic
-related to the `TraditionalAmmInterface` pool implementations.
-
-TODO: describe and document all the changes in the gamm module in more detail.
-- refer to previous sections ("Pool Manager Module" and "Concentrated Liquidity Module")
-to avoid repetition.
-
-##### Swaps
-
-We rely on the pre-existing swap methods located in `x/gamm/keeper/pool.go`:
-- `SwapExactAmountIn`
-- `SwapExactAmountOut`
-
-Similarly to `concentrated-liquidity` module, these methods now implement the `poolmanager` `SwapI` interface.
-However, the concrete implementations of the methods are unchanged from before the refactor.
-
-##### New Functionality
-
-##### `InitializePool` Keeper Method
-
-This method is part of the implementation of the `SwapI` interface in `poolmanager`
-module. "Pool Manager Module" section discussed the interface in more detail.
-
-This is the second implementation of the interface, the first being in the `concentrated-liquidity` module.
-
-```go
-// x/gamm/keeper/pool.go InitializePool(...)
-
-func (k Keeper) InitializePool(
-    ctx sdk.Context,
-    pool types.PoolI,
-    creatorAddress sdk.AccAddress) error {
-    ...
-}
-```
-
-This method should be called from the new `pool-manager` module's `CreatePool` initiated by the `MsgCreatePool`.
-See the next `"Pool Manager Module"` section of this document for more details.
-
-##### Removed Functionality
-
-TODO:
-- reiterate swap messages moved
-- reiterate create pool messages moved
-- reiterate state migrated and moved
-- queries and CLI commands removed or ported
-- any important tests removed or ported
+The `x/poolmanager` received the swap messages and, as long as the swap's pool id
+is associated with the `concentrated-liquidity` pool, the swap is routed
+into the relevant module. The routing is done via the mapping from state that was
+discussed in the "Pool Creation" section.
 
 #### Liquidity Provision
 
 > As an LP, I want to provide liquidity in ranges so that I can achieve greater capital efficiency
 
-This a basic function that should allow LPs to provide liquidity in specific ranges
-to a pool. 
+This is a basic function that should allow LPs to provide liquidity in specific ranges
+to a pool.
+
+A pool's liquidity is consisted of two assets: asset0 and asset1. In all pools, asset0 will be the lexicographically smaller of the two assets. At the current tick, the bucket at this tick consists of a mix of both asset0 and asset1 and is called the virtual liquidity of the pool (or "L" for short). Any positions set below the current price are consisted solely of asset0 while positions above the current price only contain asset1.
 
-A pool's liquidity is consisted of two assets: asset0 and asset1. In all pools, asset0 will be the lexicographically smaller of the two assets. At the current price tick, the bucket at this tick consists of a mix of both asset0 and asset1 and is called the virtual liquidity of the pool (or "L" for short). Any positions set below the current price are consisted solely of asset0 while positions above the current price only contain asset1.
+##### Adding Liquidity
 
-Therefore in `Mint`, we can either provide liquidity above or below the current price, which would act as range (limit) orders or decide to provide liquidity at the current price. 
+We can either provide liquidity above or below the current price, which would act as a range order, or decide to provide liquidity at the current price. 
 
-As declared in the API for mint, users provide the upper and lower tick to denote the range they want to provide the liquidity for. The users are also prompted to provide the amount of token0 and token1 they desire to receive. The liquidity that needs to be provided for the token0 and token1 amount provided would be then calculated by the following methods: 
+As declared in the API for `createPosition`, users provide the upper and lower tick to denote the range they want to provide the liquidity in. The users are also prompted to provide the amount of token0 and token1 they desire to receive. The liquidity that needs to be provided for the given token0 and token1 amounts would be then calculated by the following methods: 
 
 Liquidity needed for token0:
 $$L = \frac{\Delta x \sqrt{P_u} \sqrt{P_l}}{\sqrt{P_u} - \sqrt{P_l}}$$
@@ -717,32 +412,62 @@ $$L = \frac{\Delta x \sqrt{P_u} \sqrt{P_l}}{\sqrt{P_u} - \sqrt{P_l}}$$
 Liquidity needed for token1:
 $$L = \frac{\Delta y}{\sqrt{P_u}-\sqrt{P_l}}$$
 
-//TODO: what does this mean
-With the larger liquidity including the smaller liquidity, we take the smaller liquidity calculated for both token0 and token1 and use that as the liquidity throughout the rest of the joining process. Note that the liquidity used here does not represent an amount of a specific token, but the liquidity of the pool itself, represented in sdk.Int.
+Then, we pick the smallest of the two values for choosing the final `L`. The reason we do that is because the new liquidity must be proportional
+to the old one. By choosing the smaller value, we distribute the liqudity evenly between the two tokens. In the future steps, we will re-calculate the amount of token0 and token1 as a result the one that had higher liquidity will end up smaller than originally given by the user.
+
+Note that the liquidity used here does not represent an amount of a specific token, but the liquidity of the pool itself, represented in `sdk.Dec`.
 
 Using the provided liquidity, now we calculate the delta amount of both token0 and token1, using the following equations, where L is the liquidity calculated above:
 
 $$\Delta x = \frac{L(\sqrt{p(i_u)} - \sqrt{p(i_c)})}{\sqrt{p(i_u)}\sqrt{p(i_c)}}$$
 $$\Delta y = L(\sqrt{p(i_c)} - \sqrt{p(i_l)})$$
 
+Again, by recalculating the delta amount of both tokens, we make sure that the new liquidity is proportional to the old one and the excess amount of the
+token that originally computed a larger liquidity is given back to the user.
 
-The deltaX and the deltaY would be the actual amount of tokens joined for the requested position. 
+The delta X and the delta Y are the actual amounts of tokens joined for the requested position. 
 
-Given the parameters needed for calculating the tokens needed for creating a position for a given tick, the API in the msg server layer would look like the following:
+Given the parameters needed for calculating the tokens needed for creating a position for a given tick, the API in the keeper layer would look like the following:
 
 ```go
+ctx sdk.Context, poolId uint64, owner sdk.AccAddress, amount0Desired, amount1Desired, amount0Min, amount1Min sdk.Int, lowerTick, upperTick int64, frozenUntil time.Time
 func createPosition(
     ctx sdk.Context,
     poolId uint64,
     owner sdk.AccAddress,
-    minAmountToken0,
-    minAmountToken1 sdk.Int,
+    amount0Desired,
+    amount1Desired,
+    amount0Min,
+    amount1Min sdk.Int
     lowerTick,
-    upperTick int64) (amount0, amount1 sdk.Int, error) {
+    upperTick int64,
+    frozenUntil time.Time) (amount0, amount1 sdk.Int, sdk.Dec, error) {
         ...
 }
 ```
 
+##### Removing Liquidity
+
+Removing liquidity is achieved via method `withdrawPosition` which is the inverse of previously discussed `createPosition`. In fact,
+the two methods share the same underlying logic, having the only difference being the sign of the liquidity. Plus signifying addition
+while minus signifying subtraction.
+
+Withdraw position also takes an additional parameter which represents the liqudity a user wants to remove. It must be less than or
+equal to the available liquidity in the position to be successful.
+
+```go
+func (k Keeper) withdrawPosition(
+    ctx sdk.Context,
+    poolId uint64,
+    owner sdk.AccAddress,
+    lowerTick,
+    upperTick int64,
+    frozenUntil time.Time,
+    requestedLiquidityAmountToWithdraw sdk.Dec) (amtDenom0, amtDenom1 sdk.Int, err error) {
+    ...
+}
+```
+
 #### Swapping
 
 > As a trader, I want to be able to swap over a concentrated liquidity pool so that my trades incur lower slippage
@@ -1064,12 +789,8 @@ feeChargeTotal = amountIn.Mul(swapFee)
 
 #### Liquidity Rewards
 
-> As an LP, I want to earn liquidity rewards so that I am more incentivized to provide liquidity in the ranges closer to the price.
-
 TODO
 
-
-
 ##### State
 
 - global (per-pool)
@@ -1078,22 +799,6 @@ TODO
 
 - per-position
 
-### Additional Requirements
-
-#### GAMM Refactor
-
-TODO
-
-### Risks
-
-TODO
-
-### Milestones
-
-#### Milestone 1 - Swap Within a Single Tick
-
-TODO
-
 #### Placeholder
 
 ### Terminology
diff --git a/x/concentrated-liquidity/fees.go b/x/concentrated-liquidity/fees.go
index a6904ff5b..a4f5bd65a 100644
--- a/x/concentrated-liquidity/fees.go
+++ b/x/concentrated-liquidity/fees.go
@@ -81,8 +81,15 @@ func (k Keeper) initializeFeeAccumulatorPosition(ctx sdk.Context, poolId uint64,
 		return fmt.Errorf("attempted to re-initialize fee accumulator position (%s) with non-zero liquidity", positionKey)
 	}
 
-	// initialize the owner's position with liquidity Delta and zero accumulator value
-	if err := feeAccumulator.NewPosition(positionKey, sdk.ZeroDec(), nil); err != nil {
+	feeGrowthOutside, err := k.getFeeGrowthOutside(ctx, poolId, lowerTick, upperTick)
+	if err != nil {
+		return err
+	}
+
+	// initialize the owner's position with zero liquidity and accumulator set to the
+	// difference between the current fee accumulator value and the fee growth outside of the tick range
+	customAccumulatorValue := feeAccumulator.GetValue().Sub(feeGrowthOutside)
+	if err := feeAccumulator.NewPositionCustomAcc(positionKey, sdk.ZeroDec(), customAccumulatorValue, nil); err != nil {
 		return err
 	}
 
@@ -103,11 +110,18 @@ func (k Keeper) updateFeeAccumulatorPosition(ctx sdk.Context, poolId uint64, own
 		return err
 	}
 
-	// replace position's accumulator with the updated liquidity and the feeGrowthOutside
-	err = feeAccumulator.UpdatePositionCustomAcc(
-		formatFeePositionAccumulatorKey(poolId, owner, lowerTick, upperTick),
-		liquidityDelta,
-		feeGrowthOutside)
+	positionKey := formatFeePositionAccumulatorKey(poolId, owner, lowerTick, upperTick)
+
+	// replace position's accumulator before calculating unclaimed rewards
+	err = preparePositionAccumulator(feeAccumulator, positionKey, feeGrowthOutside)
+	if err != nil {
+		return err
+	}
+
+	// determine unclaimed rewards and set the positions initialFeeAccumulatorValue to the
+	// current fee accumulator value minus the fee growth outside of the tick range
+	customAccumulatorValue := feeAccumulator.GetValue().Sub(feeGrowthOutside)
+	err = feeAccumulator.UpdatePositionCustomAcc(positionKey, liquidityDelta, customAccumulatorValue)
 	if err != nil {
 		return err
 	}
@@ -202,9 +216,9 @@ func (k Keeper) collectFees(ctx sdk.Context, poolId uint64, owner sdk.AccAddress
 		return sdk.Coins{}, err
 	}
 
-	// We need to update the position's accumulator to the current fee growth outside
-	// before we claim rewards.
-	if err := feeAccumulator.SetPositionCustomAcc(positionKey, feeGrowthOutside); err != nil {
+	// replace position's accumulator before calculating unclaimed rewards
+	err = preparePositionAccumulator(feeAccumulator, positionKey, feeGrowthOutside)
+	if err != nil {
 		return sdk.Coins{}, err
 	}
 
@@ -214,6 +228,20 @@ func (k Keeper) collectFees(ctx sdk.Context, poolId uint64, owner sdk.AccAddress
 		return sdk.Coins{}, err
 	}
 
+	// Check if feeAccumulator was deleted after claiming rewards. If not, we update the custom accumulator value.
+	hasPosition, err = feeAccumulator.HasPosition(positionKey)
+	if err != nil {
+		return sdk.Coins{}, err
+	}
+
+	if hasPosition {
+		customAccumulatorValue := feeAccumulator.GetValue().Sub(feeGrowthOutside)
+		err := feeAccumulator.SetPositionCustomAcc(positionKey, customAccumulatorValue)
+		if err != nil {
+			return sdk.Coins{}, err
+		}
+	}
+
 	// Once we have iterated through all the positions, we do a single bank send from the pool to the owner.
 	pool, err := k.getPoolById(ctx, poolId)
 	if err != nil {
@@ -247,3 +275,21 @@ func calculateFeeGrowth(targetTick int64, feeGrowthOutside sdk.DecCoins, current
 func formatFeePositionAccumulatorKey(poolId uint64, owner sdk.AccAddress, lowerTick, upperTick int64) string {
 	return strings.Join([]string{feeAccumPrefix, strconv.FormatUint(poolId, uintBase), owner.String(), strconv.FormatInt(lowerTick, uintBase), strconv.FormatInt(upperTick, uintBase)}, keySeparator)
 }
+
+// preparePositionAccumulator is called prior to updating unclaimed rewards,
+// as we must set the position's accumulator value to the sum of
+// - the fee growth inside at position creation time (position.InitAccumValue)
+// - fee growth outside at the current block time (feeGrowthOutside)
+func preparePositionAccumulator(feeAccumulator accum.AccumulatorObject, positionKey string, feeGrowthOutside sdk.DecCoins) error {
+	position, err := accum.GetPosition(feeAccumulator, positionKey)
+	if err != nil {
+		return err
+	}
+
+	customAccumulatorValue := position.InitAccumValue.Add(feeGrowthOutside...)
+	err = feeAccumulator.SetPositionCustomAcc(positionKey, customAccumulatorValue)
+	if err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/x/concentrated-liquidity/incentives.go b/x/concentrated-liquidity/incentives.go
index aff5e960a..7eb809fb2 100644
--- a/x/concentrated-liquidity/incentives.go
+++ b/x/concentrated-liquidity/incentives.go
@@ -1,11 +1,14 @@
 package concentrated_liquidity
 
 import (
+	"fmt"
 	"strconv"
 	"strings"
+	"time"
 
 	sdk "github.com/cosmos/cosmos-sdk/types"
 
+	"github.com/osmosis-labs/osmosis/osmoutils"
 	"github.com/osmosis-labs/osmosis/osmoutils/accum"
 	"github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity/types"
 )
@@ -49,3 +52,209 @@ func (k Keeper) getUptimeAccumulators(ctx sdk.Context, poolId uint64) ([]accum.A
 
 	return accums, nil
 }
+
+// nolint: unused
+// getUptimeAccumulatorValues gets the accumulator values for the supported uptimes for the given poolId
+// Returns error if accumulator for the given poolId does not exist.
+func (k Keeper) getUptimeAccumulatorValues(ctx sdk.Context, poolId uint64) ([]sdk.DecCoins, error) {
+	uptimeAccums, err := k.getUptimeAccumulators(ctx, poolId)
+	if err != nil {
+		return []sdk.DecCoins{}, err
+	}
+
+	uptimeValues := []sdk.DecCoins{}
+	for _, uptimeAccum := range uptimeAccums {
+		uptimeValues = append(uptimeValues, uptimeAccum.GetValue())
+	}
+
+	return uptimeValues, nil
+}
+
+// nolint: unused
+// getInitialUptimeGrowthOutsidesForTick returns an array of the initial values of uptime growth outside
+// for each supported uptime for a given tick. This value depends on the tick's location relative to the current tick.
+//
+// uptimeGrowthOutside =
+// { uptimeGrowthGlobal current tick >= tick }
+// { 0                  current tick <  tick }
+//
+// Similar to fees, by convention the value is chosen as if all of the uptime (seconds per liquidity) to date has
+// occurred below the tick.
+// Returns error if the pool with the given id does not exist or if fails to get any of the uptime accumulators.
+func (k Keeper) getInitialUptimeGrowthOutsidesForTick(ctx sdk.Context, poolId uint64, tick int64) ([]sdk.DecCoins, error) {
+	pool, err := k.getPoolById(ctx, poolId)
+	if err != nil {
+		return []sdk.DecCoins{}, err
+	}
+
+	currentTick := pool.GetCurrentTick().Int64()
+	if currentTick >= tick {
+		uptimeAccumulatorValues, err := k.getUptimeAccumulatorValues(ctx, poolId)
+		if err != nil {
+			return []sdk.DecCoins{}, err
+		}
+		return uptimeAccumulatorValues, nil
+	}
+
+	// If currentTick < tick, we return len(SupportedUptimes) empty DecCoins
+	emptyUptimeValues := []sdk.DecCoins{}
+	for range types.SupportedUptimes {
+		emptyUptimeValues = append(emptyUptimeValues, emptyCoins)
+	}
+
+	return emptyUptimeValues, nil
+}
+
+// nolint: unused
+// updateUptimeAccumulatorsToNow syncs all uptime accumulators to be up to date.
+// Specifically, it gets the time elapsed since the last update and divides it
+// by the qualifying liquidity for each uptime. It then adds this value to the
+// respective accumulator and updates relevant time trackers accordingly.
+func (k Keeper) updateUptimeAccumulatorsToNow(ctx sdk.Context, poolId uint64) error {
+	pool, err := k.getPoolById(ctx, poolId)
+	if err != nil {
+		return err
+	}
+
+	// Get relevant pool-level values
+	poolIncentiveRecords, err := k.GetAllIncentiveRecordsForPool(ctx, poolId)
+	if err != nil {
+		return err
+	}
+
+	uptimeAccums, err := k.getUptimeAccumulators(ctx, poolId)
+	if err != nil {
+		return err
+	}
+
+	// Since our base unit of time is nanoseconds, we divide with truncation by 10^9 (10e8) to get
+	// time elapsed in seconds
+	timeElapsedNanoSec := sdk.NewDec(int64(ctx.BlockTime().Sub(pool.GetLastLiquidityUpdate())))
+	timeElapsedSec := timeElapsedNanoSec.Quo(sdk.NewDec(10e8))
+
+	// If no time has elapsed, this function is a no-op
+	if timeElapsedSec.Equal(sdk.ZeroDec()) {
+		return nil
+	}
+
+	if timeElapsedSec.LT(sdk.ZeroDec()) {
+		return fmt.Errorf("Time elapsed cannot be negative.")
+	}
+
+	for uptimeIndex, uptimeAccum := range uptimeAccums {
+		// Get relevant uptime-level values
+		curUptimeDuration := types.SupportedUptimes[uptimeIndex]
+
+		// Qualifying liquidity is the amount of liquidity that satisfies uptime requirements
+		qualifyingLiquidity, err := uptimeAccum.GetTotalShares()
+		if err != nil {
+			return err
+		}
+
+		// If there is no qualifying liquidity for the current uptime accumulator, we leave it unchanged
+		if qualifyingLiquidity.LT(sdk.OneDec()) {
+			continue
+		}
+
+		incentivesToAddToCurAccum, updatedPoolRecords, err := calcAccruedIncentivesForAccum(ctx, curUptimeDuration, qualifyingLiquidity, timeElapsedSec, poolIncentiveRecords)
+		if err != nil {
+			return err
+		}
+
+		// Emit incentives to current uptime accumulator
+		uptimeAccum.AddToAccumulator(incentivesToAddToCurAccum)
+
+		// Update pool records (stored in state after loop)
+		poolIncentiveRecords = updatedPoolRecords
+	}
+
+	// Update pool incentive records and LastLiquidityUpdate time in state to reflect emitted incentives
+	k.setMultipleIncentiveRecords(ctx, poolIncentiveRecords)
+	pool.SetLastLiquidityUpdate(ctx.BlockTime())
+	err = k.setPool(ctx, pool)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// nolint: unused
+// calcAccruedIncentivesForAccum calculates IncentivesPerLiquidity to be added to an accum
+// Returns the IncentivesPerLiquidity value and an updated list of IncentiveRecords that
+// reflect emitted incentives
+// Returns error if the qualifying liquidity/time elapsed are zero.
+func calcAccruedIncentivesForAccum(ctx sdk.Context, accumUptime time.Duration, qualifyingLiquidity sdk.Dec, timeElapsed sdk.Dec, poolIncentiveRecords []types.IncentiveRecord) (sdk.DecCoins, []types.IncentiveRecord, error) {
+	if !qualifyingLiquidity.IsPositive() || !timeElapsed.IsPositive() {
+		return sdk.DecCoins{}, []types.IncentiveRecord{}, fmt.Errorf("Qualifying liquidity and time elapsed must both be positive.")
+	}
+
+	incentivesToAddToCurAccum := sdk.NewDecCoins()
+	for incentiveIndex, incentiveRecord := range poolIncentiveRecords {
+		// We consider all incentives matching the current uptime that began emitting before the current blocktime
+		if incentiveRecord.StartTime.UTC().Before(ctx.BlockTime().UTC()) && incentiveRecord.MinUptime == accumUptime {
+			// Total amount emitted = time elapsed * emission
+			totalEmittedAmount := timeElapsed.Mul(incentiveRecord.EmissionRate)
+
+			// Incentives to emit per unit of qualifying liquidity = total emitted / qualifying liquidity
+			// Note that we truncate to ensure we do not overdistribute incentives
+			incentivesPerLiquidity := totalEmittedAmount.QuoTruncate(qualifyingLiquidity)
+			emittedIncentivesPerLiquidity := sdk.NewDecCoinFromDec(incentiveRecord.IncentiveDenom, incentivesPerLiquidity)
+
+			// Ensure that we only emit if there are enough incentives remaining to be emitted
+			remainingRewards := poolIncentiveRecords[incentiveIndex].RemainingAmount
+			if totalEmittedAmount.LTE(remainingRewards) {
+				// Add incentives to accumulator
+				incentivesToAddToCurAccum = incentivesToAddToCurAccum.Add(emittedIncentivesPerLiquidity)
+
+				// Update incentive record to reflect the incentives that were emitted
+				remainingRewards = remainingRewards.Sub(totalEmittedAmount)
+
+				// Each incentive record should only be modified once
+				poolIncentiveRecords[incentiveIndex].RemainingAmount = remainingRewards
+			}
+		}
+	}
+
+	return incentivesToAddToCurAccum, poolIncentiveRecords, nil
+}
+
+// nolint: unused
+// setIncentiveRecords sets the passed in incentive records in state
+func (k Keeper) setIncentiveRecord(ctx sdk.Context, incentiveRecord types.IncentiveRecord) {
+	store := ctx.KVStore(k.storeKey)
+	key := types.KeyIncentiveRecord(incentiveRecord.PoolId, incentiveRecord.IncentiveDenom, incentiveRecord.MinUptime)
+	osmoutils.MustSet(store, key, &incentiveRecord)
+}
+
+// nolint: unused
+// setMultipleIncentiveRecords sets multiple incentive records in state
+func (k Keeper) setMultipleIncentiveRecords(ctx sdk.Context, incentiveRecords []types.IncentiveRecord) {
+	for _, incentiveRecord := range incentiveRecords {
+		k.setIncentiveRecord(ctx, incentiveRecord)
+	}
+}
+
+// GetIncentiveRecord gets the incentive record corresponding to the passed in values from store
+func (k Keeper) GetIncentiveRecord(ctx sdk.Context, poolId uint64, denom string, minUptime time.Duration) (types.IncentiveRecord, error) {
+	store := ctx.KVStore(k.storeKey)
+	incentiveStruct := types.IncentiveRecord{}
+	key := types.KeyIncentiveRecord(poolId, denom, minUptime)
+
+	found, err := osmoutils.Get(store, key, &incentiveStruct)
+	if err != nil {
+		return types.IncentiveRecord{}, err
+	}
+
+	if !found {
+		return types.IncentiveRecord{}, types.IncentiveRecordNotFoundError{PoolId: poolId, IncentiveDenom: denom, MinUptime: minUptime}
+	}
+
+	return incentiveStruct, nil
+}
+
+// GetAllIncentiveRecordsForPool gets all the incentive records for poolId
+// Returns error if it is unable to retrieve
+func (k Keeper) GetAllIncentiveRecordsForPool(ctx sdk.Context, poolId uint64) ([]types.IncentiveRecord, error) {
+	return osmoutils.GatherValuesFromStorePrefixWithKeyParser(ctx.KVStore(k.storeKey), types.KeyPoolIncentiveRecords(poolId), ParseFullIncentiveRecordFromBz)
+}
diff --git a/x/concentrated-liquidity/internal/swapstrategy/fees.go b/x/concentrated-liquidity/internal/swapstrategy/fees.go
index a90f3c5ef..90de64524 100644
--- a/x/concentrated-liquidity/internal/swapstrategy/fees.go
+++ b/x/concentrated-liquidity/internal/swapstrategy/fees.go
@@ -42,7 +42,8 @@ func computeFeeChargePerSwapStepOutGivenIn(currentSqrtPrice sdk.Dec, hasReachedT
 		// 2) or sqrtPriceLimit is reached
 		// In both cases, we charge the fee on the amount in actually consumed before
 		// hitting the target.
-		feeChargeTotal = amountIn.Mul(swapFee)
+		// TODO: should round up?
+		feeChargeTotal = amountIn.Mul(swapFee).Quo(sdk.OneDec().Sub(swapFee))
 	} else {
 		// Otherwise, the current tick had enough liquidity to fulfill the swap
 		// and we ran out of amount remaining before reaching either the next tick or the limit.
diff --git a/x/concentrated-liquidity/model/pool.go b/x/concentrated-liquidity/model/pool.go
index 1a7335b53..dfeaac47a 100644
--- a/x/concentrated-liquidity/model/pool.go
+++ b/x/concentrated-liquidity/model/pool.go
@@ -3,6 +3,7 @@ package model
 import (
 	"encoding/json"
 	fmt "fmt"
+	time "time"
 
 	sdk "github.com/cosmos/cosmos-sdk/types"
 
@@ -150,6 +151,11 @@ func (p Pool) GetLiquidity() sdk.Dec {
 	return p.Liquidity
 }
 
+// GetLastLiquidityUpdate returns the last time there was a change in pool liquidity or active tick.
+func (p Pool) GetLastLiquidityUpdate() time.Time {
+	return p.LastLiquidityUpdate
+}
+
 func (p Pool) GetType() poolmanagertypes.PoolType {
 	return poolmanagertypes.Concentrated
 }
@@ -169,6 +175,11 @@ func (p *Pool) SetCurrentTick(newTick sdk.Int) {
 	p.CurrentTick = newTick
 }
 
+// SetLastLiquidityUpdate updates the pool's LastLiquidityUpdate to newTime.
+func (p *Pool) SetLastLiquidityUpdate(newTime time.Time) {
+	p.LastLiquidityUpdate = newTime
+}
+
 // updateLiquidityIfActivePosition updates the pool's liquidity if the position is active.
 // Returns true if updated, false otherwise.
 // TODO: add tests.
diff --git a/x/concentrated-liquidity/model/pool.pb.go b/x/concentrated-liquidity/model/pool.pb.go
index 17189a885..abdc73e85 100644
--- a/x/concentrated-liquidity/model/pool.pb.go
+++ b/x/concentrated-liquidity/model/pool.pb.go
@@ -14,15 +14,20 @@ import (
 	github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types"
 	_ "github.com/gogo/protobuf/gogoproto"
 	proto "github.com/gogo/protobuf/proto"
+	_ "github.com/gogo/protobuf/types"
+	github_com_gogo_protobuf_types "github.com/gogo/protobuf/types"
+	_ "github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity/types"
 	io "io"
 	math "math"
 	math_bits "math/bits"
+	time "time"
 )
 
 // Reference imports to suppress errors if they are not otherwise used.
 var _ = proto.Marshal
 var _ = fmt.Errorf
 var _ = math.Inf
+var _ = time.Kitchen
 
 // This is a compile-time assertion to ensure that this generated file
 // is compatible with the proto package it is being compiled against.
@@ -45,6 +50,9 @@ type Pool struct {
 	PrecisionFactorAtPriceOne github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,9,opt,name=precision_factor_at_price_one,json=precisionFactorAtPriceOne,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"precision_factor_at_price_one" yaml:"precision_factor_at_price_one"`
 	// swap_fee is the ratio that is charged on the amount of token in.
 	SwapFee github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,10,opt,name=swap_fee,json=swapFee,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"swap_fee" yaml:"swap_fee"`
+	// last_liquidity_update is the last time either the pool liquidity or the
+	// active tick changed
+	LastLiquidityUpdate time.Time `protobuf:"bytes,11,opt,name=last_liquidity_update,json=lastLiquidityUpdate,proto3,stdtime" json:"last_liquidity_update" yaml:"last_liquidity_update"`
 }
 
 func (m *Pool) Reset()      { *m = Pool{} }
@@ -88,40 +96,46 @@ func init() {
 }
 
 var fileDescriptor_3526ea5373d96c9a = []byte{
-	// 522 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0x3f, 0x6f, 0xd3, 0x40,
-	0x1c, 0xb5, 0x43, 0xfe, 0x34, 0xd7, 0xaa, 0xb4, 0x07, 0x82, 0x6b, 0x25, 0xec, 0xca, 0x02, 0x54,
-	0x24, 0x12, 0x13, 0xc1, 0x94, 0xad, 0x01, 0x2a, 0x75, 0xa2, 0x72, 0x11, 0x03, 0xaa, 0x64, 0x9c,
-	0xf3, 0x35, 0x3d, 0xc5, 0xf1, 0x39, 0x77, 0x97, 0x42, 0xbe, 0x01, 0x23, 0x63, 0xc7, 0x7e, 0x08,
-	0x3e, 0x44, 0xc5, 0xd4, 0x11, 0x31, 0x58, 0x28, 0xf9, 0x06, 0x59, 0x59, 0x90, 0x7d, 0x17, 0xd7,
-	0x0b, 0x48, 0x99, 0x7c, 0xef, 0xfd, 0x7e, 0xf7, 0xde, 0xb3, 0x7f, 0xfe, 0x81, 0x67, 0x4c, 0x8c,
-	0x98, 0xa0, 0xc2, 0xc5, 0x2c, 0xc6, 0x24, 0x96, 0x3c, 0x90, 0x24, 0x6c, 0x45, 0x74, 0x3c, 0xa1,
-	0x21, 0x95, 0x53, 0x37, 0x61, 0x2c, 0x6a, 0x27, 0x9c, 0x49, 0x06, 0x9f, 0xe8, 0xd6, 0x76, 0xb9,
-	0xb5, 0xe8, 0x6c, 0x5f, 0x74, 0xfa, 0x44, 0x06, 0x9d, 0xdd, 0x1d, 0x9c, 0xf7, 0xf9, 0xf9, 0x25,
-	0x57, 0x01, 0xa5, 0xb0, 0x7b, 0x7f, 0xc0, 0x06, 0x4c, 0xf1, 0xd9, 0x49, 0xb1, 0xce, 0x9f, 0x1a,
-	0xa8, 0x1e, 0x33, 0x16, 0xc1, 0xe7, 0xa0, 0x11, 0x84, 0x21, 0x27, 0x42, 0x20, 0x73, 0xcf, 0xdc,
-	0x6f, 0xf6, 0xe0, 0x22, 0xb5, 0x37, 0xa7, 0xc1, 0x28, 0xea, 0x3a, 0xba, 0xe0, 0x78, 0xcb, 0x16,
-	0xb8, 0x09, 0x2a, 0x34, 0x44, 0x95, 0x3d, 0x73, 0xbf, 0xea, 0x55, 0x68, 0x08, 0x3f, 0x81, 0x66,
-	0x11, 0x06, 0xdd, 0xc9, 0xef, 0xf7, 0xae, 0x53, 0xdb, 0xf8, 0x95, 0xda, 0x4f, 0x07, 0x54, 0x9e,
-	0x4f, 0xfa, 0x6d, 0xcc, 0x46, 0x3a, 0x90, 0x7e, 0xb4, 0x44, 0x38, 0x74, 0xe5, 0x34, 0x21, 0xa2,
-	0xfd, 0x86, 0xe0, 0x45, 0x6a, 0x6f, 0x29, 0xb7, 0x42, 0xc8, 0xf1, 0x6e, 0x45, 0xe1, 0x03, 0x50,
-	0x97, 0x6c, 0x48, 0xe2, 0x17, 0xa8, 0x9a, 0xc9, 0x7b, 0x1a, 0x15, 0x7c, 0x07, 0xd5, 0x4a, 0x7c,
-	0x07, 0x8e, 0x01, 0xc4, 0x13, 0xce, 0x49, 0x2c, 0x7d, 0x31, 0xe6, 0xd2, 0x4f, 0x38, 0xc5, 0x04,
-	0xd5, 0xf3, 0x68, 0xaf, 0x57, 0x8e, 0xb6, 0xad, 0xa2, 0x89, 0x84, 0x69, 0x25, 0xc7, 0xdb, 0xd2,
-	0xf2, 0x27, 0x63, 0x2e, 0x8f, 0x33, 0x0a, 0x9e, 0x83, 0x8d, 0xa5, 0xa5, 0xa4, 0x78, 0x88, 0x1a,
-	0xb9, 0xd9, 0xdb, 0x15, 0xcc, 0x8e, 0x62, 0xb9, 0x48, 0xed, 0x7b, 0xca, 0xac, 0xac, 0xe5, 0x78,
-	0xeb, 0x1a, 0xbe, 0xa7, 0x78, 0x08, 0xbb, 0x60, 0x23, 0x63, 0x7d, 0x91, 0x04, 0x98, 0xc6, 0x03,
-	0xb4, 0x96, 0x0d, 0xa2, 0xf7, 0xf0, 0xf6, 0x6e, 0xb9, 0xea, 0x78, 0xeb, 0x19, 0x3c, 0x51, 0x08,
-	0x5e, 0x9a, 0xe0, 0x51, 0xc2, 0x09, 0xa6, 0x82, 0xb2, 0xd8, 0x3f, 0x0b, 0xb0, 0x64, 0xdc, 0x0f,
-	0xf4, 0x6b, 0xf9, 0x2c, 0x26, 0xa8, 0x99, 0xe7, 0xfe, 0xb0, 0x72, 0xee, 0xc7, 0xca, 0xfb, 0xbf,
-	0xe2, 0x8e, 0xb7, 0x53, 0xd4, 0x0f, 0xf3, 0xf2, 0x81, 0xfa, 0x7a, 0xef, 0x62, 0x02, 0x4f, 0xc1,
-	0x9a, 0xf8, 0x1c, 0x24, 0xfe, 0x19, 0x21, 0x08, 0xe4, 0x21, 0x0e, 0x56, 0x9e, 0xd4, 0x5d, 0x3d,
-	0x29, 0xad, 0xe3, 0x78, 0x8d, 0xec, 0x78, 0x48, 0x48, 0x77, 0xfb, 0xeb, 0x95, 0x6d, 0x5c, 0x5e,
-	0xd9, 0xc6, 0x8f, 0xef, 0xad, 0x5a, 0xf6, 0xcf, 0x1f, 0xf5, 0x4e, 0xaf, 0x67, 0x96, 0x79, 0x33,
-	0xb3, 0xcc, 0xdf, 0x33, 0xcb, 0xfc, 0x36, 0xb7, 0x8c, 0x9b, 0xb9, 0x65, 0xfc, 0x9c, 0x5b, 0xc6,
-	0xc7, 0x5e, 0xc9, 0x50, 0xaf, 0x5e, 0x2b, 0x0a, 0xfa, 0x62, 0x09, 0xdc, 0x8b, 0xce, 0x2b, 0xf7,
-	0xcb, 0xbf, 0x16, 0x77, 0xc4, 0x42, 0x12, 0xf5, 0xeb, 0xf9, 0x8a, 0xbd, 0xfc, 0x1b, 0x00, 0x00,
-	0xff, 0xff, 0xaf, 0x6b, 0xaf, 0x25, 0xe7, 0x03, 0x00, 0x00,
+	// 617 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0xcd, 0x4e, 0xdb, 0x4c,
+	0x14, 0x8d, 0xf9, 0x20, 0xc0, 0x04, 0xf1, 0xc1, 0xd0, 0x1f, 0x83, 0xda, 0x18, 0x59, 0x6d, 0x95,
+	0x4a, 0x8d, 0xdd, 0xf4, 0x67, 0xc3, 0x8e, 0xb4, 0x45, 0x42, 0xaa, 0x54, 0x64, 0x68, 0x17, 0x15,
+	0x92, 0x3b, 0x19, 0x0f, 0x61, 0x14, 0xdb, 0xe3, 0xcc, 0x4c, 0x28, 0xbc, 0x41, 0x97, 0x2c, 0x59,
+	0xf2, 0x10, 0x7d, 0x08, 0xd4, 0x15, 0xcb, 0xaa, 0x0b, 0xb7, 0x22, 0x6f, 0x10, 0xa9, 0xfb, 0xca,
+	0xe3, 0xb1, 0xc9, 0xa2, 0x3f, 0xca, 0xca, 0x73, 0xef, 0xb9, 0xf7, 0x9c, 0x73, 0x3d, 0x73, 0xc1,
+	0x43, 0x26, 0x22, 0x26, 0xa8, 0x70, 0x31, 0x8b, 0x31, 0x89, 0x25, 0x47, 0x92, 0x04, 0xcd, 0x90,
+	0xf6, 0x07, 0x34, 0xa0, 0xf2, 0xc4, 0x4d, 0x18, 0x0b, 0x9d, 0x84, 0x33, 0xc9, 0xe0, 0x7d, 0x5d,
+	0xea, 0x8c, 0x97, 0x96, 0x95, 0xce, 0x51, 0xab, 0x43, 0x24, 0x6a, 0xad, 0xad, 0x62, 0x55, 0xe7,
+	0xab, 0x26, 0x37, 0x0f, 0x72, 0x86, 0xb5, 0x1b, 0x5d, 0xd6, 0x65, 0x79, 0x3e, 0x3b, 0xe9, 0xac,
+	0xd5, 0x65, 0xac, 0x1b, 0x12, 0x57, 0x45, 0x9d, 0xc1, 0x81, 0x2b, 0x69, 0x44, 0x84, 0x44, 0x51,
+	0xa2, 0x0b, 0x9e, 0xff, 0xc3, 0x23, 0x55, 0x59, 0x7a, 0x44, 0x7c, 0x4e, 0x30, 0xe3, 0x41, 0xde,
+	0x66, 0xff, 0xac, 0x82, 0xe9, 0x1d, 0xc6, 0x42, 0xf8, 0x08, 0xcc, 0xa2, 0x20, 0xe0, 0x44, 0x08,
+	0xd3, 0x58, 0x37, 0x1a, 0xf3, 0x6d, 0x38, 0x4a, 0xad, 0xc5, 0x13, 0x14, 0x85, 0x1b, 0xb6, 0x06,
+	0x6c, 0xaf, 0x28, 0x81, 0x8b, 0x60, 0x8a, 0x06, 0xe6, 0xd4, 0xba, 0xd1, 0x98, 0xf6, 0xa6, 0x68,
+	0x00, 0x3f, 0x80, 0xf9, 0x52, 0xca, 0xfc, 0x4f, 0xf5, 0xb7, 0x2f, 0x52, 0xab, 0xf2, 0x2d, 0xb5,
+	0x1e, 0x74, 0xa9, 0x3c, 0x1c, 0x74, 0x1c, 0xcc, 0x22, 0x3d, 0xa8, 0xfe, 0x34, 0x45, 0xd0, 0x73,
+	0xe5, 0x49, 0x42, 0x84, 0xf3, 0x92, 0xe0, 0x51, 0x6a, 0x2d, 0xe5, 0x6a, 0x25, 0x91, 0xed, 0x5d,
+	0x93, 0xc2, 0x5b, 0xa0, 0x2a, 0x59, 0x8f, 0xc4, 0x8f, 0xcd, 0xe9, 0x8c, 0xde, 0xd3, 0x51, 0x99,
+	0x6f, 0x99, 0x33, 0x63, 0xf9, 0x16, 0xec, 0x03, 0x88, 0x07, 0x9c, 0x93, 0x58, 0xfa, 0xa2, 0xcf,
+	0xa5, 0x9f, 0x70, 0x8a, 0x89, 0x59, 0x55, 0xd6, 0x5e, 0x4c, 0x6c, 0x6d, 0x39, 0xb7, 0x26, 0x12,
+	0xa6, 0x99, 0x6c, 0x6f, 0x49, 0xd3, 0xef, 0xf6, 0xb9, 0xdc, 0xc9, 0x52, 0xf0, 0x10, 0x2c, 0x14,
+	0x92, 0x92, 0xe2, 0x9e, 0x39, 0xab, 0xc4, 0x5e, 0x4d, 0x20, 0xb6, 0x1d, 0xcb, 0x51, 0x6a, 0xad,
+	0xe4, 0x62, 0xe3, 0x5c, 0xb6, 0x57, 0xd3, 0xe1, 0x1e, 0xc5, 0x3d, 0xb8, 0x01, 0x16, 0xb2, 0xac,
+	0x2f, 0x12, 0x84, 0x69, 0xdc, 0x35, 0xe7, 0xb2, 0x8b, 0x68, 0xdf, 0xbe, 0xee, 0x1d, 0x47, 0x6d,
+	0xaf, 0x96, 0x85, 0xbb, 0x79, 0x04, 0xcf, 0x0c, 0x70, 0x37, 0xe1, 0x04, 0x53, 0x41, 0x59, 0xec,
+	0x1f, 0x20, 0x2c, 0x19, 0xf7, 0x91, 0x1e, 0xcb, 0x67, 0x31, 0x31, 0xe7, 0x95, 0xef, 0x77, 0x13,
+	0xfb, 0xbe, 0x97, 0x6b, 0xff, 0x95, 0xdc, 0xf6, 0x56, 0x4b, 0x7c, 0x4b, 0xc1, 0x9b, 0xf9, 0xdf,
+	0x7b, 0x13, 0x13, 0xb8, 0x0f, 0xe6, 0xc4, 0x47, 0x94, 0xf8, 0x07, 0x84, 0x98, 0x40, 0x99, 0xd8,
+	0x9c, 0xf8, 0xa6, 0xfe, 0xd7, 0x37, 0xa5, 0x79, 0x6c, 0x6f, 0x36, 0x3b, 0x6e, 0x11, 0x02, 0x8f,
+	0xc1, 0xcd, 0x10, 0x09, 0xe9, 0x97, 0x6f, 0xca, 0x1f, 0x24, 0x01, 0x92, 0xc4, 0xac, 0xad, 0x1b,
+	0x8d, 0xda, 0x93, 0x35, 0x27, 0x5f, 0x31, 0xa7, 0x58, 0x31, 0x67, 0xaf, 0x58, 0xb1, 0x76, 0x23,
+	0xb3, 0x31, 0x4a, 0xad, 0x3b, 0xfa, 0x85, 0xfe, 0x8e, 0xc6, 0x3e, 0xfd, 0x6e, 0x19, 0xde, 0x4a,
+	0x86, 0xbd, 0x2e, 0xa0, 0xb7, 0x0a, 0xd9, 0x58, 0xfe, 0x74, 0x6e, 0x55, 0xce, 0xce, 0xad, 0xca,
+	0x97, 0xcf, 0xcd, 0x99, 0x6c, 0xdb, 0xb6, 0xdb, 0xfb, 0x17, 0x57, 0x75, 0xe3, 0xf2, 0xaa, 0x6e,
+	0xfc, 0xb8, 0xaa, 0x1b, 0xa7, 0xc3, 0x7a, 0xe5, 0x72, 0x58, 0xaf, 0x7c, 0x1d, 0xd6, 0x2b, 0xef,
+	0xdb, 0x63, 0xa3, 0xea, 0x9d, 0x6e, 0x86, 0xa8, 0x23, 0x8a, 0xc0, 0x3d, 0x6a, 0x3d, 0x73, 0x8f,
+	0xff, 0xb4, 0xe6, 0x11, 0x0b, 0x48, 0xd8, 0xa9, 0xaa, 0x19, 0x9e, 0xfe, 0x0a, 0x00, 0x00, 0xff,
+	0xff, 0x83, 0x74, 0xf1, 0xd0, 0xb9, 0x04, 0x00, 0x00,
 }
 
 func (m *Pool) Marshal() (dAtA []byte, err error) {
@@ -144,6 +158,14 @@ func (m *Pool) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	n1, err1 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.LastLiquidityUpdate, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.LastLiquidityUpdate):])
+	if err1 != nil {
+		return 0, err1
+	}
+	i -= n1
+	i = encodeVarintPool(dAtA, i, uint64(n1))
+	i--
+	dAtA[i] = 0x5a
 	{
 		size := m.SwapFee.Size()
 		i -= size
@@ -273,6 +295,8 @@ func (m *Pool) Size() (n int) {
 	n += 1 + l + sovPool(uint64(l))
 	l = m.SwapFee.Size()
 	n += 1 + l + sovPool(uint64(l))
+	l = github_com_gogo_protobuf_types.SizeOfStdTime(m.LastLiquidityUpdate)
+	n += 1 + l + sovPool(uint64(l))
 	return n
 }
 
@@ -615,6 +639,39 @@ func (m *Pool) Unmarshal(dAtA []byte) error {
 				return err
 			}
 			iNdEx = postIndex
+		case 11:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field LastLiquidityUpdate", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowPool
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthPool
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthPool
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.LastLiquidityUpdate, dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipPool(dAtA[iNdEx:])
diff --git a/x/concentrated-liquidity/model/tickInfo.pb.go b/x/concentrated-liquidity/model/tickInfo.pb.go
index eae11a553..e6ef6c5ac 100644
--- a/x/concentrated-liquidity/model/tickInfo.pb.go
+++ b/x/concentrated-liquidity/model/tickInfo.pb.go
@@ -35,6 +35,7 @@ type TickInfo struct {
 	LiquidityGross   github_com_cosmos_cosmos_sdk_types.Dec      `protobuf:"bytes,1,opt,name=liquidity_gross,json=liquidityGross,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"liquidity_gross" yaml:"liquidity_gross"`
 	LiquidityNet     github_com_cosmos_cosmos_sdk_types.Dec      `protobuf:"bytes,2,opt,name=liquidity_net,json=liquidityNet,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"liquidity_net" yaml:"liquidity_net"`
 	FeeGrowthOutside github_com_cosmos_cosmos_sdk_types.DecCoins `protobuf:"bytes,3,rep,name=fee_growth_outside,json=feeGrowthOutside,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.DecCoins" json:"fee_growth_outside"`
+	UptimeTrackers   []UptimeTracker                             `protobuf:"bytes,4,rep,name=uptime_trackers,json=uptimeTrackers,proto3" json:"uptime_trackers" yaml:"uptime_trackers"`
 }
 
 func (m *TickInfo) Reset()         { *m = TickInfo{} }
@@ -77,8 +78,60 @@ func (m *TickInfo) GetFeeGrowthOutside() github_com_cosmos_cosmos_sdk_types.DecC
 	return nil
 }
 
+func (m *TickInfo) GetUptimeTrackers() []UptimeTracker {
+	if m != nil {
+		return m.UptimeTrackers
+	}
+	return nil
+}
+
+type UptimeTracker struct {
+	UptimeGrowthOutside github_com_cosmos_cosmos_sdk_types.DecCoins `protobuf:"bytes,1,rep,name=uptime_growth_outside,json=uptimeGrowthOutside,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.DecCoins" json:"uptime_growth_outside"`
+}
+
+func (m *UptimeTracker) Reset()         { *m = UptimeTracker{} }
+func (m *UptimeTracker) String() string { return proto.CompactTextString(m) }
+func (*UptimeTracker) ProtoMessage()    {}
+func (*UptimeTracker) Descriptor() ([]byte, []int) {
+	return fileDescriptor_1ccb7e45032b943a, []int{1}
+}
+func (m *UptimeTracker) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *UptimeTracker) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_UptimeTracker.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *UptimeTracker) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_UptimeTracker.Merge(m, src)
+}
+func (m *UptimeTracker) XXX_Size() int {
+	return m.Size()
+}
+func (m *UptimeTracker) XXX_DiscardUnknown() {
+	xxx_messageInfo_UptimeTracker.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_UptimeTracker proto.InternalMessageInfo
+
+func (m *UptimeTracker) GetUptimeGrowthOutside() github_com_cosmos_cosmos_sdk_types.DecCoins {
+	if m != nil {
+		return m.UptimeGrowthOutside
+	}
+	return nil
+}
+
 func init() {
 	proto.RegisterType((*TickInfo)(nil), "osmosis.concentratedliquidity.v1beta1.TickInfo")
+	proto.RegisterType((*UptimeTracker)(nil), "osmosis.concentratedliquidity.v1beta1.UptimeTracker")
 }
 
 func init() {
@@ -86,31 +139,35 @@ func init() {
 }
 
 var fileDescriptor_1ccb7e45032b943a = []byte{
-	// 374 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0xcd, 0x2f, 0xce, 0xcd,
-	0x2f, 0xce, 0x2c, 0xd6, 0x4f, 0xce, 0xcf, 0x4b, 0x4e, 0xcd, 0x2b, 0x29, 0x4a, 0x2c, 0x49, 0x4d,
-	0xd1, 0xcd, 0xc9, 0x2c, 0x2c, 0xcd, 0x4c, 0xc9, 0x2c, 0xa9, 0xd4, 0x2f, 0xc9, 0x4c, 0xce, 0xf6,
-	0xcc, 0x4b, 0xcb, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x52, 0x85, 0x2a, 0xd7, 0x43, 0x56,
-	0x0e, 0x57, 0xad, 0x57, 0x66, 0x98, 0x94, 0x5a, 0x92, 0x68, 0x28, 0x25, 0x99, 0x0c, 0x56, 0x17,
-	0x0f, 0xd6, 0xa4, 0x0f, 0xe1, 0x40, 0x4c, 0x90, 0x12, 0x49, 0xcf, 0x4f, 0xcf, 0x87, 0x88, 0x83,
-	0x58, 0x50, 0x51, 0x39, 0x88, 0x1a, 0xfd, 0xa4, 0xc4, 0xe2, 0x54, 0x7d, 0xa8, 0x29, 0xfa, 0xc9,
-	0xf9, 0x99, 0x79, 0x10, 0x79, 0xa5, 0xd7, 0x4c, 0x5c, 0x1c, 0x21, 0x50, 0xa7, 0x08, 0x15, 0x72,
-	0xf1, 0xc3, 0xad, 0x8c, 0x4f, 0x2f, 0xca, 0x2f, 0x2e, 0x96, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x74,
-	0xf2, 0x38, 0x71, 0x4f, 0x9e, 0xe1, 0xd6, 0x3d, 0x79, 0xb5, 0xf4, 0xcc, 0x92, 0x8c, 0xd2, 0x24,
-	0xbd, 0xe4, 0xfc, 0x5c, 0xa8, 0xe5, 0x50, 0x4a, 0xb7, 0x38, 0x25, 0x5b, 0xbf, 0xa4, 0xb2, 0x20,
-	0xb5, 0x58, 0xcf, 0x25, 0x35, 0xf9, 0xd3, 0x3d, 0x79, 0xb1, 0xca, 0xc4, 0xdc, 0x1c, 0x2b, 0x25,
-	0x34, 0xe3, 0x94, 0x82, 0xf8, 0xe0, 0x22, 0xee, 0x20, 0x01, 0xa1, 0x6c, 0x2e, 0x5e, 0x84, 0x9a,
-	0xbc, 0xd4, 0x12, 0x09, 0x26, 0xb0, 0x85, 0x6e, 0x24, 0x5b, 0x28, 0x82, 0x6e, 0x61, 0x5e, 0x6a,
-	0x89, 0x52, 0x10, 0x0f, 0x9c, 0xef, 0x97, 0x5a, 0x22, 0x54, 0xcf, 0x25, 0x94, 0x96, 0x9a, 0x0a,
-	0x72, 0x4a, 0x79, 0x49, 0x46, 0x7c, 0x7e, 0x69, 0x49, 0x71, 0x66, 0x4a, 0xaa, 0x04, 0xb3, 0x02,
-	0xb3, 0x06, 0xb7, 0x91, 0x8c, 0x1e, 0x34, 0x34, 0x41, 0x21, 0x05, 0x0b, 0x6f, 0x90, 0xd9, 0xce,
-	0xf9, 0x99, 0x79, 0x4e, 0xc6, 0x20, 0xf7, 0xac, 0xba, 0x2f, 0xaf, 0x4d, 0x9c, 0x7b, 0x40, 0x7a,
-	0x8a, 0x83, 0x04, 0xd2, 0x52, 0x53, 0xdd, 0xc1, 0x76, 0xf9, 0x43, 0xac, 0x72, 0x8a, 0x39, 0xf1,
-	0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, 0x18, 0x27, 0x3c, 0x96, 0x63, 0xb8,
-	0xf0, 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x28, 0x27, 0x24, 0x83, 0xa1, 0x49, 0x41, 0x37,
-	0x27, 0x31, 0xa9, 0x18, 0xc6, 0xd1, 0x2f, 0x33, 0x34, 0xd1, 0xaf, 0xc0, 0x95, 0x98, 0x72, 0xf3,
-	0x53, 0x52, 0x73, 0x92, 0xd8, 0xc0, 0x51, 0x6a, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x93, 0x5e,
-	0x70, 0x0c, 0x7b, 0x02, 0x00, 0x00,
+	// 447 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x53, 0x4d, 0x6b, 0x13, 0x41,
+	0x18, 0xce, 0x18, 0x11, 0x1d, 0x6d, 0x2b, 0x6b, 0x95, 0x58, 0x64, 0x52, 0x16, 0x94, 0x82, 0x64,
+	0x86, 0xd8, 0x9e, 0x3c, 0xae, 0x62, 0xf5, 0xa2, 0x10, 0xea, 0x45, 0x84, 0xb0, 0x3b, 0xfb, 0x26,
+	0x1d, 0xf6, 0x63, 0xd2, 0x9d, 0xd9, 0x6a, 0x0e, 0xe2, 0xc9, 0xbb, 0x37, 0xff, 0x83, 0xbf, 0xa4,
+	0xc7, 0x1e, 0xc5, 0x43, 0x94, 0xe4, 0xe6, 0xd1, 0x5f, 0x20, 0xb3, 0x33, 0xdd, 0xa6, 0x0b, 0x42,
+	0x3c, 0x78, 0xda, 0x7d, 0xdf, 0x7d, 0xbe, 0x78, 0x76, 0x06, 0xf7, 0xa4, 0xca, 0xa4, 0x12, 0x8a,
+	0x71, 0x99, 0x73, 0xc8, 0x75, 0x11, 0x6a, 0x88, 0x7b, 0xa9, 0x38, 0x2a, 0x45, 0x2c, 0xf4, 0x94,
+	0x69, 0xc1, 0x93, 0x17, 0xf9, 0x48, 0xd2, 0x49, 0x21, 0xb5, 0xf4, 0xee, 0x3b, 0x38, 0x5d, 0x86,
+	0xd7, 0x68, 0x7a, 0xdc, 0x8f, 0x40, 0x87, 0xfd, 0xad, 0xbb, 0xbc, 0xc2, 0x0d, 0x2b, 0x12, 0xb3,
+	0x83, 0x55, 0xd8, 0xda, 0x1c, 0xcb, 0xb1, 0xb4, 0x7b, 0xf3, 0xe6, 0xb6, 0xc4, 0x62, 0x58, 0x14,
+	0x2a, 0x60, 0x4e, 0x85, 0x71, 0x29, 0x72, 0xfb, 0xdd, 0xff, 0xd5, 0xc6, 0x57, 0x0f, 0x5c, 0x14,
+	0xef, 0x08, 0x6f, 0xd4, 0x96, 0xc3, 0x71, 0x21, 0x95, 0xea, 0xa0, 0x6d, 0xb4, 0x73, 0x2d, 0x78,
+	0x7e, 0x32, 0xeb, 0xb6, 0xbe, 0xcf, 0xba, 0x0f, 0xc6, 0x42, 0x1f, 0x96, 0x11, 0xe5, 0x32, 0x73,
+	0xe6, 0xee, 0xd1, 0x53, 0x71, 0xc2, 0xf4, 0x74, 0x02, 0x8a, 0x3e, 0x05, 0xfe, 0x7b, 0xd6, 0xbd,
+	0x33, 0x0d, 0xb3, 0xf4, 0xb1, 0xdf, 0x90, 0xf3, 0x07, 0xeb, 0xf5, 0x66, 0xdf, 0x2c, 0xbc, 0x04,
+	0xaf, 0x9d, 0x63, 0x72, 0xd0, 0x9d, 0x4b, 0x95, 0xe1, 0xb3, 0x7f, 0x36, 0xdc, 0x6c, 0x1a, 0xe6,
+	0xa0, 0xfd, 0xc1, 0x8d, 0x7a, 0x7e, 0x09, 0xda, 0xfb, 0x88, 0xbd, 0x11, 0x80, 0x89, 0xf2, 0x4e,
+	0x1f, 0x0e, 0x65, 0xa9, 0x95, 0x88, 0xa1, 0xd3, 0xde, 0x6e, 0xef, 0x5c, 0x7f, 0x74, 0x8f, 0xba,
+	0x36, 0x4d, 0x53, 0x67, 0x7d, 0x1b, 0xed, 0x27, 0x52, 0xe4, 0xc1, 0xae, 0xc9, 0xf3, 0xf5, 0x47,
+	0xf7, 0xe1, 0x6a, 0x79, 0x0c, 0x47, 0x0d, 0x6e, 0x8e, 0x00, 0xf6, 0x2b, 0xaf, 0x57, 0xd6, 0xca,
+	0xfb, 0x80, 0x37, 0xca, 0x89, 0x16, 0x19, 0x0c, 0x75, 0x11, 0xf2, 0x04, 0x0a, 0xd5, 0xb9, 0x5c,
+	0xb9, 0xef, 0xd1, 0x95, 0xfe, 0x3f, 0x7d, 0x5d, 0xb1, 0x0f, 0x2c, 0x39, 0x20, 0x26, 0xd5, 0x79,
+	0xd9, 0x0d, 0x69, 0x7f, 0xb0, 0x5e, 0x2e, 0xc3, 0x95, 0xff, 0x05, 0xe1, 0xb5, 0x0b, 0x0a, 0xde,
+	0x27, 0x84, 0x6f, 0x3b, 0x5a, 0xa3, 0x15, 0xf4, 0xbf, 0x5a, 0xb9, 0x65, 0xfd, 0x2e, 0x14, 0x13,
+	0xbc, 0x3d, 0x99, 0x13, 0x74, 0x3a, 0x27, 0xe8, 0xe7, 0x9c, 0xa0, 0xcf, 0x0b, 0xd2, 0x3a, 0x5d,
+	0x90, 0xd6, 0xb7, 0x05, 0x69, 0xbd, 0x09, 0x96, 0xb4, 0x5d, 0x47, 0xbd, 0x34, 0x8c, 0xd4, 0xd9,
+	0xc0, 0x8e, 0xfb, 0x7b, 0xec, 0xfd, 0xdf, 0x6e, 0x59, 0x26, 0x63, 0x48, 0xa3, 0x2b, 0xd5, 0x59,
+	0xdf, 0xfd, 0x13, 0x00, 0x00, 0xff, 0xff, 0x93, 0xd8, 0x0c, 0x71, 0x94, 0x03, 0x00, 0x00,
 }
 
 func (m *TickInfo) Marshal() (dAtA []byte, err error) {
@@ -133,6 +190,20 @@ func (m *TickInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	if len(m.UptimeTrackers) > 0 {
+		for iNdEx := len(m.UptimeTrackers) - 1; iNdEx >= 0; iNdEx-- {
+			{
+				size, err := m.UptimeTrackers[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+				if err != nil {
+					return 0, err
+				}
+				i -= size
+				i = encodeVarintTickInfo(dAtA, i, uint64(size))
+			}
+			i--
+			dAtA[i] = 0x22
+		}
+	}
 	if len(m.FeeGrowthOutside) > 0 {
 		for iNdEx := len(m.FeeGrowthOutside) - 1; iNdEx >= 0; iNdEx-- {
 			{
@@ -170,6 +241,43 @@ func (m *TickInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
+func (m *UptimeTracker) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *UptimeTracker) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *UptimeTracker) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if len(m.UptimeGrowthOutside) > 0 {
+		for iNdEx := len(m.UptimeGrowthOutside) - 1; iNdEx >= 0; iNdEx-- {
+			{
+				size, err := m.UptimeGrowthOutside[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+				if err != nil {
+					return 0, err
+				}
+				i -= size
+				i = encodeVarintTickInfo(dAtA, i, uint64(size))
+			}
+			i--
+			dAtA[i] = 0xa
+		}
+	}
+	return len(dAtA) - i, nil
+}
+
 func encodeVarintTickInfo(dAtA []byte, offset int, v uint64) int {
 	offset -= sovTickInfo(v)
 	base := offset
@@ -197,6 +305,27 @@ func (m *TickInfo) Size() (n int) {
 			n += 1 + l + sovTickInfo(uint64(l))
 		}
 	}
+	if len(m.UptimeTrackers) > 0 {
+		for _, e := range m.UptimeTrackers {
+			l = e.Size()
+			n += 1 + l + sovTickInfo(uint64(l))
+		}
+	}
+	return n
+}
+
+func (m *UptimeTracker) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if len(m.UptimeGrowthOutside) > 0 {
+		for _, e := range m.UptimeGrowthOutside {
+			l = e.Size()
+			n += 1 + l + sovTickInfo(uint64(l))
+		}
+	}
 	return n
 }
 
@@ -337,6 +466,124 @@ func (m *TickInfo) Unmarshal(dAtA []byte) error {
 				return err
 			}
 			iNdEx = postIndex
+		case 4:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field UptimeTrackers", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowTickInfo
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthTickInfo
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthTickInfo
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.UptimeTrackers = append(m.UptimeTrackers, UptimeTracker{})
+			if err := m.UptimeTrackers[len(m.UptimeTrackers)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipTickInfo(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthTickInfo
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *UptimeTracker) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowTickInfo
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: UptimeTracker: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: UptimeTracker: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field UptimeGrowthOutside", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowTickInfo
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthTickInfo
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthTickInfo
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.UptimeGrowthOutside = append(m.UptimeGrowthOutside, types.DecCoin{})
+			if err := m.UptimeGrowthOutside[len(m.UptimeGrowthOutside)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipTickInfo(dAtA[iNdEx:])
diff --git a/x/concentrated-liquidity/msg_server.go b/x/concentrated-liquidity/msg_server.go
index d6d5bc399..d450c85ba 100644
--- a/x/concentrated-liquidity/msg_server.go
+++ b/x/concentrated-liquidity/msg_server.go
@@ -136,7 +136,7 @@ func (server msgServer) CollectFees(goCtx context.Context, msg *types.MsgCollect
 		return nil, err
 	}
 
-	tokenOut, err := server.keeper.collectFees(ctx, msg.PoolId, sender, msg.LowerTick, msg.UpperTick)
+	collectedFees, err := server.keeper.collectFees(ctx, msg.PoolId, sender, msg.LowerTick, msg.UpperTick)
 	if err != nil {
 		return nil, err
 	}
@@ -152,11 +152,11 @@ func (server msgServer) CollectFees(goCtx context.Context, msg *types.MsgCollect
 			sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
 			sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender),
 			sdk.NewAttribute(types.AttributeKeyPoolId, strconv.FormatUint(msg.PoolId, 10)),
-			sdk.NewAttribute(types.AttributeKeyTokensOut, tokenOut.String()),
+			sdk.NewAttribute(types.AttributeKeyTokensOut, collectedFees.String()),
 			sdk.NewAttribute(types.AttributeLowerTick, strconv.FormatInt(msg.LowerTick, 10)),
 			sdk.NewAttribute(types.AttributeUpperTick, strconv.FormatInt(msg.UpperTick, 10)),
 		),
 	})
 
-	return &types.MsgCollectFeesResponse{TokenOut: tokenOut}, nil
+	return &types.MsgCollectFeesResponse{CollectedFees: collectedFees}, nil
 }
diff --git a/x/concentrated-liquidity/pool.go b/x/concentrated-liquidity/pool.go
index 2f46395da..f1c7e0980 100644
--- a/x/concentrated-liquidity/pool.go
+++ b/x/concentrated-liquidity/pool.go
@@ -27,6 +27,8 @@ func (k Keeper) InitializePool(ctx sdk.Context, poolI poolmanagertypes.PoolI, cr
 		return err
 	}
 
+	concentratedPool.SetLastLiquidityUpdate(ctx.BlockTime())
+
 	tickSpacing := concentratedPool.GetTickSpacing()
 
 	if !k.validateTickSpacing(ctx, tickSpacing) {
diff --git a/x/concentrated-liquidity/store.go b/x/concentrated-liquidity/store.go
index 11b9af6a2..b5d98f0ec 100644
--- a/x/concentrated-liquidity/store.go
+++ b/x/concentrated-liquidity/store.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"strconv"
 	"strings"
+	"time"
 
 	sdk "github.com/cosmos/cosmos-sdk/types"
 	"github.com/gogo/protobuf/proto"
@@ -101,3 +102,75 @@ func ParseFullPositionFromBytes(key, value []byte) (types.FullPositionByOwnerRes
 		Liquidity:   positionValue.Liquidity,
 	}, nil
 }
+
+// ParseFullIncentiveRecordFromBz parses a IncentiveRecord from a byte array.
+// Returns a struct containing the denom and min uptime associated with the incentive record.
+// Returns an error if the byte array is empty.
+// Returns an error if fails to parse.
+func ParseIncentiveRecordFromBz(bz []byte) (incentiveRecord types.IncentiveRecord, err error) {
+	if len(bz) == 0 {
+		return types.IncentiveRecord{}, errors.New("incentive record not found")
+	}
+	err = proto.Unmarshal(bz, &incentiveRecord)
+	if err != nil {
+		return types.IncentiveRecord{}, err
+	}
+
+	return incentiveRecord, nil
+}
+
+// ParseIncentiveRecordFromBz parses an incentive record from a byte array.
+// Returns a struct containing the state associated with the incentive.
+// Returns an error if the byte array is empty.
+// Returns an error if fails to parse.
+func ParseFullIncentiveRecordFromBz(key []byte, value []byte) (incentiveRecord types.IncentiveRecord, err error) {
+	if len(key) == 0 {
+		return types.IncentiveRecord{}, errors.New("key not found")
+	}
+	if len(value) == 0 {
+		return types.IncentiveRecord{}, fmt.Errorf("value not found for key (%s)", value)
+	}
+
+	keyStr := string(key)
+
+	// These may include irrelevant parts of the prefix such as the module prefix.
+	incentiveRecordKeyComponents := strings.Split(keyStr, types.KeySeparator)
+
+	// We only care about the last 3 components, which are:
+	// - pool id
+	// - incentive denom
+	// - min uptime
+
+	relevantIncentiveKeyComponents := incentiveRecordKeyComponents[len(incentiveRecordKeyComponents)-3:]
+
+	incentivePrefix := incentiveRecordKeyComponents[0]
+	if incentivePrefix != string(types.IncentivePrefix) {
+		return types.IncentiveRecord{}, fmt.Errorf("Wrong incentive prefix, got: %v, required %v", []byte(incentivePrefix), types.IncentivePrefix)
+	}
+
+	poolId, err := strconv.ParseUint(relevantIncentiveKeyComponents[0], 10, 64)
+	if err != nil {
+		return types.IncentiveRecord{}, err
+	}
+
+	incentiveDenom := relevantIncentiveKeyComponents[1]
+
+	minUptime, err := strconv.ParseUint(relevantIncentiveKeyComponents[2], 10, 64)
+	if err != nil {
+		return types.IncentiveRecord{}, err
+	}
+
+	incentiveValue, err := ParseIncentiveRecordFromBz(value)
+	if err != nil {
+		return types.IncentiveRecord{}, err
+	}
+
+	return types.IncentiveRecord{
+		PoolId:          poolId,
+		IncentiveDenom:  incentiveDenom,
+		RemainingAmount: incentiveValue.RemainingAmount,
+		EmissionRate:    incentiveValue.EmissionRate,
+		StartTime:       incentiveValue.StartTime,
+		MinUptime:       time.Duration(minUptime),
+	}, nil
+}
diff --git a/x/concentrated-liquidity/swaps.go b/x/concentrated-liquidity/swaps.go
index 312443bab..816baf7da 100644
--- a/x/concentrated-liquidity/swaps.go
+++ b/x/concentrated-liquidity/swaps.go
@@ -321,27 +321,28 @@ func (k Keeper) calcOutAmtGivenIn(ctx sdk.Context,
 
 		// utilizing the bucket's liquidity and knowing the price target, we calculate the how much tokenOut we get from the tokenIn
 		// we also calculate the swap state's new sqrtPrice after this swap
-		sqrtPrice, amountIn, amountOut, feeChargeTotal := swapStrategy.ComputeSwapStep(
+		sqrtPrice, amountIn, amountOut, feeCharge := swapStrategy.ComputeSwapStep(
 			swapState.sqrtPrice,
 			nextTickSqrtPrice,
 			swapState.liquidity,
 			swapState.amountSpecifiedRemaining,
 		)
 
+		// Update the fee growth for the entire swap using the total fees charged.
+		swapState.updateFeeGrowthGlobal(feeCharge)
+
 		ctx.Logger().Debug("cl calc out given in")
 		ctx.Logger().Debug("start sqrt price", swapState.sqrtPrice)
 		ctx.Logger().Debug("reached sqrt price", sqrtPrice)
 		ctx.Logger().Debug("liquidity", swapState.liquidity)
 		ctx.Logger().Debug("amountIn", amountIn)
 		ctx.Logger().Debug("amountOut", amountOut)
-		ctx.Logger().Debug("feeChargeTotal", feeChargeTotal)
-
-		swapState.updateFeeGrowthGlobal(feeChargeTotal)
+		ctx.Logger().Debug("feeCharge", feeCharge)
 
 		// update the swapState with the new sqrtPrice from the above swap
 		swapState.sqrtPrice = sqrtPrice
 		// we deduct the amount of tokens we input in the computeSwapStep above from the user's defined tokenIn amount
-		swapState.amountSpecifiedRemaining = swapState.amountSpecifiedRemaining.Sub(amountIn.Add(feeChargeTotal))
+		swapState.amountSpecifiedRemaining = swapState.amountSpecifiedRemaining.Sub(amountIn.Add(feeCharge))
 		// we add the amount of tokens we received (amountOut) from the computeSwapStep above to the amountCalculated accumulator
 		swapState.amountCalculated = swapState.amountCalculated.Add(amountOut)
 
@@ -349,7 +350,7 @@ func (k Keeper) calcOutAmtGivenIn(ctx sdk.Context,
 		// tick has been consumed and we must move on to the next tick to complete the swap
 		if nextTickSqrtPrice.Equal(sqrtPrice) {
 			// retrieve the liquidity held in the next closest initialized tick
-			liquidityNet, err := k.crossTick(ctx, p.GetId(), nextTick.Int64())
+			liquidityNet, err := k.crossTick(ctx, p.GetId(), nextTick.Int64(), sdk.NewDecCoinFromDec(tokenInMin.Denom, swapState.feeGrowthGlobal))
 			if err != nil {
 				return writeCtx, sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, err
 			}
@@ -488,6 +489,8 @@ func (k Keeper) calcInAmtGivenOut(
 			swapState.amountSpecifiedRemaining,
 		)
 
+		swapState.updateFeeGrowthGlobal(feeChargeTotal)
+
 		ctx.Logger().Debug("cl calc in given out")
 		ctx.Logger().Debug("start sqrt price", swapState.sqrtPrice)
 		ctx.Logger().Debug("reached sqrt price", sqrtPrice)
@@ -496,8 +499,6 @@ func (k Keeper) calcInAmtGivenOut(
 		ctx.Logger().Debug("amountOut", amountOut)
 		ctx.Logger().Debug("feeChargeTotal", feeChargeTotal)
 
-		swapState.updateFeeGrowthGlobal(feeChargeTotal)
-
 		// update the swapState with the new sqrtPrice from the above swap
 		swapState.sqrtPrice = sqrtPrice
 		swapState.amountSpecifiedRemaining = swapState.amountSpecifiedRemaining.Sub(amountOut)
@@ -507,7 +508,7 @@ func (k Keeper) calcInAmtGivenOut(
 		// tick has been consumed and we must move on to the next tick to complete the swap
 		if sqrtPriceNextTick.Equal(sqrtPrice) {
 			// retrieve the liquidity held in the next closest initialized tick
-			liquidityNet, err := k.crossTick(ctx, p.GetId(), nextTick.Int64())
+			liquidityNet, err := k.crossTick(ctx, p.GetId(), nextTick.Int64(), sdk.NewDecCoinFromDec(desiredTokenOut.Denom, swapState.feeGrowthGlobal))
 			if err != nil {
 				return writeCtx, sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, err
 			}
diff --git a/x/concentrated-liquidity/tick.go b/x/concentrated-liquidity/tick.go
index a8412dbfe..64f53088b 100644
--- a/x/concentrated-liquidity/tick.go
+++ b/x/concentrated-liquidity/tick.go
@@ -26,6 +26,19 @@ func (k Keeper) initOrUpdateTick(ctx sdk.Context, poolId uint64, currentTick int
 	// calculate liquidityGross, which does not care about whether liquidityIn is positive or negative
 	liquidityBefore := tickInfo.LiquidityGross
 
+	// if given tickIndex is LTE to the current tick and the liquidityBefore is zero,
+	// set the tick's fee growth outside to the fee accumulator's value
+	if liquidityBefore.IsZero() {
+		if tickIndex <= currentTick {
+			accum, err := k.getFeeAccumulator(ctx, poolId)
+			if err != nil {
+				return err
+			}
+
+			tickInfo.FeeGrowthOutside = accum.GetValue()
+		}
+	}
+
 	// note that liquidityIn can be either positive or negative.
 	// If negative, this would work as a subtraction from liquidityBefore
 	liquidityAfter := math.AddLiquidity(liquidityBefore, liquidityIn)
@@ -39,22 +52,12 @@ func (k Keeper) initOrUpdateTick(ctx sdk.Context, poolId uint64, currentTick int
 		tickInfo.LiquidityNet = tickInfo.LiquidityNet.Add(liquidityIn)
 	}
 
-	// if given tickIndex is LTE to current tick, tick's fee growth outside is set as fee accumulator's value
-	if tickIndex <= currentTick {
-		accum, err := k.getFeeAccumulator(ctx, poolId)
-		if err != nil {
-			return err
-		}
-
-		tickInfo.FeeGrowthOutside = accum.GetValue()
-	}
-
 	k.SetTickInfo(ctx, poolId, tickIndex, tickInfo)
 
 	return nil
 }
 
-func (k Keeper) crossTick(ctx sdk.Context, poolId uint64, tickIndex int64) (liquidityDelta sdk.Dec, err error) {
+func (k Keeper) crossTick(ctx sdk.Context, poolId uint64, tickIndex int64, swapStateFeeGrowth sdk.DecCoin) (liquidityDelta sdk.Dec, err error) {
 	tickInfo, err := k.getTickInfo(ctx, poolId, tickIndex)
 	if err != nil {
 		return sdk.Dec{}, err
@@ -65,8 +68,8 @@ func (k Keeper) crossTick(ctx sdk.Context, poolId uint64, tickIndex int64) (liqu
 		return sdk.Dec{}, err
 	}
 
-	// subtract tick's fee growth outside from current fee accumulator
-	tickInfo.FeeGrowthOutside = accum.GetValue().Sub(tickInfo.FeeGrowthOutside)
+	// subtract tick's fee growth outside from current fee growth global, including the fee growth of the current swap.
+	tickInfo.FeeGrowthOutside = accum.GetValue().Add(swapStateFeeGrowth).Sub(tickInfo.FeeGrowthOutside)
 	k.SetTickInfo(ctx, poolId, tickIndex, tickInfo)
 
 	return tickInfo.LiquidityNet, nil
diff --git a/x/concentrated-liquidity/types/errors.go b/x/concentrated-liquidity/types/errors.go
index 35a4d7b95..daf824c76 100644
--- a/x/concentrated-liquidity/types/errors.go
+++ b/x/concentrated-liquidity/types/errors.go
@@ -224,6 +224,16 @@ func (e PositionStillFrozenError) Error() string {
 	return fmt.Sprintf("position is still frozen until %s", e.FrozenUntil)
 }
 
+type IncentiveRecordNotFoundError struct {
+	PoolId         uint64
+	IncentiveDenom string
+	MinUptime      time.Duration
+}
+
+func (e IncentiveRecordNotFoundError) Error() string {
+	return fmt.Sprintf("incentive record not found. pool id (%d), incentive denom (%s), minimum uptime (%s)", e.PoolId, e.IncentiveDenom, e.MinUptime.String())
+}
+
 type QueryRangeUnsupportedError struct {
 	RequestedRange sdk.Int
 	MaxRange       sdk.Int
diff --git a/x/concentrated-liquidity/types/incentive_record.pb.go b/x/concentrated-liquidity/types/incentive_record.pb.go
new file mode 100644
index 000000000..3767d0650
--- /dev/null
+++ b/x/concentrated-liquidity/types/incentive_record.pb.go
@@ -0,0 +1,582 @@
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
+// source: osmosis/concentrated-liquidity/incentive_record.proto
+
+package types
+
+import (
+	fmt "fmt"
+	_ "github.com/cosmos/cosmos-proto"
+	github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types"
+	_ "github.com/gogo/protobuf/gogoproto"
+	proto "github.com/gogo/protobuf/proto"
+	_ "github.com/gogo/protobuf/types"
+	github_com_gogo_protobuf_types "github.com/gogo/protobuf/types"
+	io "io"
+	math "math"
+	math_bits "math/bits"
+	time "time"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+var _ = time.Kitchen
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
+
+// IncentiveRecord represents an active perpetual incentive gauge for a pool
+type IncentiveRecord struct {
+	PoolId uint64 `protobuf:"varint,1,opt,name=poolId,proto3" json:"poolId,omitempty"`
+	// incentive_denom is the denom of the token being distributed as part of this
+	// incentive record
+	IncentiveDenom string `protobuf:"bytes,2,opt,name=incentive_denom,json=incentiveDenom,proto3" json:"incentive_denom,omitempty"`
+	// remaining_amount is the total amount of incentives to be distributed
+	RemainingAmount github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,3,opt,name=remaining_amount,json=remainingAmount,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"remaining_amount" yaml:"remaining_amount"`
+	// emission_rate is the incentive emission rate per second
+	EmissionRate github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,4,opt,name=emission_rate,json=emissionRate,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"emission_rate" yaml:"swap_fee"`
+	// start_time is the time when the incentive starts distributing
+	StartTime time.Time `protobuf:"bytes,5,opt,name=start_time,json=startTime,proto3,stdtime" json:"start_time" yaml:"start_time"`
+	// min_uptime is the minimum uptime required for liquidity to qualify for this
+	// incentive. It should be always be one of the supported uptimes in
+	// types.SupportedUptimes
+	MinUptime time.Duration `protobuf:"bytes,6,opt,name=min_uptime,json=minUptime,proto3,stdduration" json:"min_uptime" yaml:"min_uptime"`
+}
+
+func (m *IncentiveRecord) Reset()         { *m = IncentiveRecord{} }
+func (m *IncentiveRecord) String() string { return proto.CompactTextString(m) }
+func (*IncentiveRecord) ProtoMessage()    {}
+func (*IncentiveRecord) Descriptor() ([]byte, []int) {
+	return fileDescriptor_9d38bf94e42ee434, []int{0}
+}
+func (m *IncentiveRecord) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *IncentiveRecord) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_IncentiveRecord.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *IncentiveRecord) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_IncentiveRecord.Merge(m, src)
+}
+func (m *IncentiveRecord) XXX_Size() int {
+	return m.Size()
+}
+func (m *IncentiveRecord) XXX_DiscardUnknown() {
+	xxx_messageInfo_IncentiveRecord.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_IncentiveRecord proto.InternalMessageInfo
+
+func (m *IncentiveRecord) GetPoolId() uint64 {
+	if m != nil {
+		return m.PoolId
+	}
+	return 0
+}
+
+func (m *IncentiveRecord) GetIncentiveDenom() string {
+	if m != nil {
+		return m.IncentiveDenom
+	}
+	return ""
+}
+
+func (m *IncentiveRecord) GetStartTime() time.Time {
+	if m != nil {
+		return m.StartTime
+	}
+	return time.Time{}
+}
+
+func (m *IncentiveRecord) GetMinUptime() time.Duration {
+	if m != nil {
+		return m.MinUptime
+	}
+	return 0
+}
+
+func init() {
+	proto.RegisterType((*IncentiveRecord)(nil), "osmosis.concentratedliquidity.v1beta1.IncentiveRecord")
+}
+
+func init() {
+	proto.RegisterFile("osmosis/concentrated-liquidity/incentive_record.proto", fileDescriptor_9d38bf94e42ee434)
+}
+
+var fileDescriptor_9d38bf94e42ee434 = []byte{
+	// 460 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x52, 0xcd, 0x8e, 0xd3, 0x30,
+	0x18, 0x6c, 0xd8, 0xa5, 0xd2, 0x9a, 0x9f, 0x42, 0x84, 0x20, 0x5b, 0x89, 0xa4, 0x8a, 0x04, 0xf4,
+	0xd2, 0x58, 0xe5, 0xe7, 0xc2, 0x6d, 0xab, 0x5e, 0x7a, 0x8d, 0x40, 0x20, 0x84, 0x14, 0x39, 0x89,
+	0x1b, 0x2c, 0x62, 0x7f, 0x21, 0x76, 0x0a, 0x7d, 0x8b, 0x3d, 0xf2, 0x48, 0x7b, 0xdc, 0x23, 0xe2,
+	0x50, 0x50, 0xfb, 0x06, 0xfb, 0x04, 0x28, 0x8e, 0x93, 0xad, 0x8a, 0x38, 0x70, 0x6a, 0x67, 0x32,
+	0x33, 0xdf, 0xe7, 0xb1, 0xd1, 0x2b, 0x90, 0x1c, 0x24, 0x93, 0x38, 0x01, 0x91, 0x50, 0xa1, 0x4a,
+	0xa2, 0x68, 0x3a, 0xc9, 0xd9, 0x97, 0x8a, 0xa5, 0x4c, 0xad, 0x31, 0xd3, 0x2c, 0x5b, 0xd1, 0xa8,
+	0xa4, 0x09, 0x94, 0x69, 0x50, 0x94, 0xa0, 0xc0, 0x7e, 0x62, 0x6c, 0xc1, 0xbe, 0xad, 0x73, 0x05,
+	0xab, 0x69, 0x4c, 0x15, 0x99, 0x0e, 0x4f, 0x13, 0xad, 0x8b, 0xb4, 0x09, 0x37, 0xa0, 0x49, 0x18,
+	0x3e, 0xc8, 0x20, 0x83, 0x86, 0xaf, 0xff, 0x19, 0xd6, 0xcb, 0x00, 0xb2, 0x9c, 0x62, 0x8d, 0xe2,
+	0x6a, 0x89, 0x15, 0xe3, 0x54, 0x2a, 0xc2, 0x0b, 0x23, 0x70, 0x0f, 0x05, 0x69, 0x55, 0x12, 0xc5,
+	0x40, 0x34, 0xdf, 0xfd, 0xdd, 0x11, 0x1a, 0x2c, 0xda, 0x9d, 0x43, 0xbd, 0xb2, 0xfd, 0x10, 0xf5,
+	0x0b, 0x80, 0x7c, 0x91, 0x3a, 0xd6, 0xc8, 0x1a, 0x1f, 0x87, 0x06, 0xd9, 0xcf, 0xd0, 0xe0, 0xfa,
+	0x78, 0x29, 0x15, 0xc0, 0x9d, 0x1b, 0x23, 0x6b, 0x7c, 0x12, 0xde, 0xed, 0xe8, 0x79, 0xcd, 0xda,
+	0x0a, 0xdd, 0x2b, 0x29, 0x27, 0x4c, 0x30, 0x91, 0x45, 0x84, 0x43, 0x25, 0x94, 0x73, 0x54, 0x2b,
+	0x67, 0x8b, 0x8b, 0x8d, 0xd7, 0xfb, 0xb9, 0xf1, 0x9e, 0x66, 0x4c, 0x7d, 0xaa, 0xe2, 0x20, 0x01,
+	0x6e, 0x8e, 0x69, 0x7e, 0x26, 0x32, 0xfd, 0x8c, 0xd5, 0xba, 0xa0, 0x32, 0x98, 0xd3, 0xe4, 0x6a,
+	0xe3, 0x3d, 0x5a, 0x13, 0x9e, 0xbf, 0xf6, 0x0f, 0xf3, 0xfc, 0x70, 0xd0, 0x51, 0x67, 0x9a, 0xb1,
+	0x97, 0xe8, 0x0e, 0xe5, 0x4c, 0x4a, 0x06, 0x22, 0xaa, 0xfb, 0x75, 0x8e, 0xf5, 0xc8, 0xb3, 0xff,
+	0x1e, 0x39, 0x68, 0x46, 0xca, 0xaf, 0xa4, 0x88, 0x96, 0x94, 0xfa, 0xe1, 0xed, 0x36, 0x37, 0x24,
+	0x8a, 0xda, 0xef, 0x11, 0x92, 0x8a, 0x94, 0x2a, 0xaa, 0xbb, 0x76, 0x6e, 0x8e, 0xac, 0xf1, 0xad,
+	0xe7, 0xc3, 0xa0, 0xe9, 0x39, 0x68, 0x7b, 0x0e, 0xde, 0xb4, 0x17, 0x31, 0x7b, 0x5c, 0x2f, 0x70,
+	0xb5, 0xf1, 0xee, 0x9b, 0xd8, 0xce, 0xeb, 0x9f, 0xff, 0xf2, 0xac, 0xf0, 0x44, 0x13, 0xb5, 0xdc,
+	0x7e, 0x87, 0x10, 0x67, 0x22, 0xaa, 0x0a, 0x9d, 0xdc, 0xd7, 0xc9, 0xa7, 0x7f, 0x25, 0xcf, 0xcd,
+	0x0d, 0x1e, 0x06, 0x5f, 0x5b, 0xfd, 0xef, 0x3a, 0x98, 0x33, 0xf1, 0x56, 0xe3, 0xd9, 0xc7, 0x8b,
+	0xad, 0x6b, 0x5d, 0x6e, 0x5d, 0xeb, 0xf7, 0xd6, 0xb5, 0xce, 0x77, 0x6e, 0xef, 0x72, 0xe7, 0xf6,
+	0x7e, 0xec, 0xdc, 0xde, 0x87, 0xd9, 0x5e, 0x2b, 0xe6, 0x8d, 0x4e, 0x72, 0x12, 0xcb, 0x16, 0xe0,
+	0xd5, 0xf4, 0x25, 0xfe, 0xf6, 0xaf, 0xd7, 0xae, 0x5b, 0x8b, 0xfb, 0x7a, 0xb5, 0x17, 0x7f, 0x02,
+	0x00, 0x00, 0xff, 0xff, 0xb1, 0xa4, 0x92, 0xf5, 0x1c, 0x03, 0x00, 0x00,
+}
+
+func (m *IncentiveRecord) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *IncentiveRecord) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *IncentiveRecord) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	n1, err1 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.MinUptime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.MinUptime):])
+	if err1 != nil {
+		return 0, err1
+	}
+	i -= n1
+	i = encodeVarintIncentiveRecord(dAtA, i, uint64(n1))
+	i--
+	dAtA[i] = 0x32
+	n2, err2 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTime):])
+	if err2 != nil {
+		return 0, err2
+	}
+	i -= n2
+	i = encodeVarintIncentiveRecord(dAtA, i, uint64(n2))
+	i--
+	dAtA[i] = 0x2a
+	{
+		size := m.EmissionRate.Size()
+		i -= size
+		if _, err := m.EmissionRate.MarshalTo(dAtA[i:]); err != nil {
+			return 0, err
+		}
+		i = encodeVarintIncentiveRecord(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0x22
+	{
+		size := m.RemainingAmount.Size()
+		i -= size
+		if _, err := m.RemainingAmount.MarshalTo(dAtA[i:]); err != nil {
+			return 0, err
+		}
+		i = encodeVarintIncentiveRecord(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0x1a
+	if len(m.IncentiveDenom) > 0 {
+		i -= len(m.IncentiveDenom)
+		copy(dAtA[i:], m.IncentiveDenom)
+		i = encodeVarintIncentiveRecord(dAtA, i, uint64(len(m.IncentiveDenom)))
+		i--
+		dAtA[i] = 0x12
+	}
+	if m.PoolId != 0 {
+		i = encodeVarintIncentiveRecord(dAtA, i, uint64(m.PoolId))
+		i--
+		dAtA[i] = 0x8
+	}
+	return len(dAtA) - i, nil
+}
+
+func encodeVarintIncentiveRecord(dAtA []byte, offset int, v uint64) int {
+	offset -= sovIncentiveRecord(v)
+	base := offset
+	for v >= 1<<7 {
+		dAtA[offset] = uint8(v&0x7f | 0x80)
+		v >>= 7
+		offset++
+	}
+	dAtA[offset] = uint8(v)
+	return base
+}
+func (m *IncentiveRecord) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if m.PoolId != 0 {
+		n += 1 + sovIncentiveRecord(uint64(m.PoolId))
+	}
+	l = len(m.IncentiveDenom)
+	if l > 0 {
+		n += 1 + l + sovIncentiveRecord(uint64(l))
+	}
+	l = m.RemainingAmount.Size()
+	n += 1 + l + sovIncentiveRecord(uint64(l))
+	l = m.EmissionRate.Size()
+	n += 1 + l + sovIncentiveRecord(uint64(l))
+	l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartTime)
+	n += 1 + l + sovIncentiveRecord(uint64(l))
+	l = github_com_gogo_protobuf_types.SizeOfStdDuration(m.MinUptime)
+	n += 1 + l + sovIncentiveRecord(uint64(l))
+	return n
+}
+
+func sovIncentiveRecord(x uint64) (n int) {
+	return (math_bits.Len64(x|1) + 6) / 7
+}
+func sozIncentiveRecord(x uint64) (n int) {
+	return sovIncentiveRecord(uint64((x << 1) ^ uint64((int64(x) >> 63))))
+}
+func (m *IncentiveRecord) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowIncentiveRecord
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: IncentiveRecord: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: IncentiveRecord: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field PoolId", wireType)
+			}
+			m.PoolId = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowIncentiveRecord
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.PoolId |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field IncentiveDenom", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowIncentiveRecord
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthIncentiveRecord
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthIncentiveRecord
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.IncentiveDenom = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field RemainingAmount", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowIncentiveRecord
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthIncentiveRecord
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthIncentiveRecord
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := m.RemainingAmount.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 4:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field EmissionRate", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowIncentiveRecord
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthIncentiveRecord
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthIncentiveRecord
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := m.EmissionRate.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 5:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field StartTime", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowIncentiveRecord
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthIncentiveRecord
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthIncentiveRecord
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartTime, dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 6:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field MinUptime", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowIncentiveRecord
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthIncentiveRecord
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthIncentiveRecord
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := github_com_gogo_protobuf_types.StdDurationUnmarshal(&m.MinUptime, dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipIncentiveRecord(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthIncentiveRecord
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func skipIncentiveRecord(dAtA []byte) (n int, err error) {
+	l := len(dAtA)
+	iNdEx := 0
+	depth := 0
+	for iNdEx < l {
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return 0, ErrIntOverflowIncentiveRecord
+			}
+			if iNdEx >= l {
+				return 0, io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= (uint64(b) & 0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		wireType := int(wire & 0x7)
+		switch wireType {
+		case 0:
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return 0, ErrIntOverflowIncentiveRecord
+				}
+				if iNdEx >= l {
+					return 0, io.ErrUnexpectedEOF
+				}
+				iNdEx++
+				if dAtA[iNdEx-1] < 0x80 {
+					break
+				}
+			}
+		case 1:
+			iNdEx += 8
+		case 2:
+			var length int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return 0, ErrIntOverflowIncentiveRecord
+				}
+				if iNdEx >= l {
+					return 0, io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				length |= (int(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if length < 0 {
+				return 0, ErrInvalidLengthIncentiveRecord
+			}
+			iNdEx += length
+		case 3:
+			depth++
+		case 4:
+			if depth == 0 {
+				return 0, ErrUnexpectedEndOfGroupIncentiveRecord
+			}
+			depth--
+		case 5:
+			iNdEx += 4
+		default:
+			return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
+		}
+		if iNdEx < 0 {
+			return 0, ErrInvalidLengthIncentiveRecord
+		}
+		if depth == 0 {
+			return iNdEx, nil
+		}
+	}
+	return 0, io.ErrUnexpectedEOF
+}
+
+var (
+	ErrInvalidLengthIncentiveRecord        = fmt.Errorf("proto: negative length found during unmarshaling")
+	ErrIntOverflowIncentiveRecord          = fmt.Errorf("proto: integer overflow")
+	ErrUnexpectedEndOfGroupIncentiveRecord = fmt.Errorf("proto: unexpected end of group")
+)
diff --git a/x/concentrated-liquidity/types/keys.go b/x/concentrated-liquidity/types/keys.go
index d07d65476..5d5f7038f 100644
--- a/x/concentrated-liquidity/types/keys.go
+++ b/x/concentrated-liquidity/types/keys.go
@@ -20,9 +20,10 @@ const (
 
 // Key prefixes
 var (
-	TickPrefix     = []byte{0x01}
-	PositionPrefix = []byte{0x02}
-	PoolPrefix     = []byte{0x03}
+	TickPrefix      = []byte{0x01}
+	PositionPrefix  = []byte{0x02}
+	PoolPrefix      = []byte{0x03}
+	IncentivePrefix = []byte{0x04}
 )
 
 // TickIndexToBytes converts a tick index to a byte slice. Negative tick indexes
@@ -87,3 +88,11 @@ func KeyUserPositions(addr sdk.AccAddress) []byte {
 func KeyPool(poolId uint64) []byte {
 	return []byte(fmt.Sprintf("%s%d", PoolPrefix, poolId))
 }
+
+func KeyIncentiveRecord(poolId uint64, denom string, minUptime time.Duration) []byte {
+	return []byte(fmt.Sprintf("%s%s%d%s%s%s%d", IncentivePrefix, KeySeparator, poolId, KeySeparator, denom, KeySeparator, uint64(minUptime)))
+}
+
+func KeyPoolIncentiveRecords(poolId uint64) []byte {
+	return []byte(fmt.Sprintf("%s%s%d", IncentivePrefix, KeySeparator, poolId))
+}
diff --git a/x/concentrated-liquidity/types/pool.go b/x/concentrated-liquidity/types/pool.go
index 781db895b..d68085140 100644
--- a/x/concentrated-liquidity/types/pool.go
+++ b/x/concentrated-liquidity/types/pool.go
@@ -1,6 +1,8 @@
 package types
 
 import (
+	"time"
+
 	sdk "github.com/cosmos/cosmos-sdk/types"
 
 	poolmanagertypes "github.com/osmosis-labs/osmosis/v14/x/poolmanager/types"
@@ -17,8 +19,10 @@ type ConcentratedPoolExtension interface {
 	GetPrecisionFactorAtPriceOne() sdk.Int
 	GetTickSpacing() uint64
 	GetLiquidity() sdk.Dec
+	GetLastLiquidityUpdate() time.Time
 	SetCurrentSqrtPrice(newSqrtPrice sdk.Dec)
 	SetCurrentTick(newTick sdk.Int)
+	SetLastLiquidityUpdate(newTime time.Time)
 
 	UpdateLiquidity(newLiquidity sdk.Dec)
 	ApplySwap(newLiquidity sdk.Dec, newCurrentTick sdk.Int, newCurrentSqrtPrice sdk.Dec) error
diff --git a/x/concentrated-liquidity/types/tx.pb.go b/x/concentrated-liquidity/types/tx.pb.go
index 67143b2a1..3e89616ca 100644
--- a/x/concentrated-liquidity/types/tx.pb.go
+++ b/x/concentrated-liquidity/types/tx.pb.go
@@ -354,7 +354,7 @@ func (m *MsgCollectFees) GetUpperTick() int64 {
 }
 
 type MsgCollectFeesResponse struct {
-	TokenOut []types.Coin `protobuf:"bytes,1,rep,name=token_out,json=tokenOut,proto3" json:"token_out" yaml:"token_out"`
+	CollectedFees []types.Coin `protobuf:"bytes,1,rep,name=collected_fees,json=collectedFees,proto3" json:"collected_fees" yaml:"collected_fees"`
 }
 
 func (m *MsgCollectFeesResponse) Reset()         { *m = MsgCollectFeesResponse{} }
@@ -390,9 +390,9 @@ func (m *MsgCollectFeesResponse) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_MsgCollectFeesResponse proto.InternalMessageInfo
 
-func (m *MsgCollectFeesResponse) GetTokenOut() []types.Coin {
+func (m *MsgCollectFeesResponse) GetCollectedFees() []types.Coin {
 	if m != nil {
-		return m.TokenOut
+		return m.CollectedFees
 	}
 	return nil
 }
@@ -411,57 +411,57 @@ func init() {
 }
 
 var fileDescriptor_1f1fff802923d7db = []byte{
-	// 799 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x56, 0x4f, 0x4f, 0xe3, 0x46,
-	0x14, 0xcf, 0x10, 0x08, 0x64, 0x28, 0x81, 0x98, 0xd2, 0x9a, 0x54, 0x8d, 0x23, 0x4b, 0x6d, 0x53,
-	0x55, 0xd8, 0x98, 0x52, 0xa9, 0x42, 0xaa, 0x44, 0x0d, 0xaa, 0x44, 0xa5, 0xa8, 0xc8, 0xa2, 0xaa,
-	0x84, 0xaa, 0x46, 0x8e, 0x3d, 0x98, 0x69, 0x1c, 0x4f, 0xea, 0x19, 0xf3, 0xa7, 0xd7, 0x5e, 0x7b,
-	0x40, 0xfd, 0x22, 0x7c, 0x0d, 0x0e, 0x3d, 0x70, 0xd8, 0x95, 0x56, 0x7b, 0xc8, 0xae, 0xe0, 0xb4,
-	0xd7, 0x7c, 0x82, 0x55, 0x66, 0x6c, 0x27, 0x24, 0xec, 0x8a, 0x00, 0x97, 0xe5, 0x94, 0x99, 0x37,
-	0xef, 0xf7, 0x7b, 0x7e, 0xbf, 0x79, 0xef, 0x65, 0xe0, 0x57, 0x84, 0xb6, 0x08, 0xc5, 0x54, 0x77,
-	0x48, 0xe0, 0xa0, 0x80, 0x85, 0x36, 0x43, 0xee, 0x8a, 0x8f, 0xff, 0x8a, 0xb0, 0x8b, 0xd9, 0xa9,
-	0xce, 0x4e, 0xb4, 0x76, 0x48, 0x18, 0x91, 0xbe, 0x88, 0x1d, 0xb5, 0x41, 0xc7, 0xd4, 0x4f, 0x3b,
-	0x32, 0x1a, 0x88, 0xd9, 0x46, 0xe9, 0x63, 0x8f, 0x78, 0x84, 0x23, 0xf4, 0xde, 0x4a, 0x80, 0x4b,
-	0x8a, 0x47, 0x88, 0xe7, 0x23, 0x9d, 0xef, 0x1a, 0xd1, 0x81, 0xce, 0x70, 0x0b, 0x51, 0x66, 0xb7,
-	0xda, 0xb1, 0x43, 0xd9, 0xe1, 0xf4, 0x7a, 0xc3, 0xa6, 0x48, 0x8f, 0xb9, 0x74, 0x87, 0xe0, 0x40,
-	0x9c, 0xab, 0x6f, 0xa6, 0x60, 0xb1, 0x46, 0xbd, 0xad, 0x10, 0xd9, 0x0c, 0xed, 0x12, 0x8a, 0x19,
-	0x26, 0x81, 0xf4, 0x0d, 0x9c, 0x6e, 0x13, 0xe2, 0xd7, 0xb1, 0x2b, 0x83, 0x0a, 0xa8, 0x4e, 0x9a,
-	0x52, 0xb7, 0xa3, 0x14, 0x4e, 0xed, 0x96, 0xbf, 0xa1, 0xc6, 0x07, 0xaa, 0x95, 0xeb, 0xad, 0x76,
-	0x5c, 0xe9, 0x6b, 0x98, 0xa3, 0x28, 0x70, 0x51, 0x28, 0x4f, 0x54, 0x40, 0x35, 0x6f, 0x16, 0xbb,
-	0x1d, 0x65, 0x4e, 0xf8, 0x0a, 0xbb, 0x6a, 0xc5, 0x0e, 0xd2, 0x3a, 0x84, 0x3e, 0x39, 0x46, 0x61,
-	0x9d, 0x61, 0xa7, 0x29, 0x67, 0x2b, 0xa0, 0x9a, 0x35, 0x97, 0xba, 0x1d, 0xa5, 0x28, 0xdc, 0xfb,
-	0x67, 0xaa, 0x95, 0xe7, 0x9b, 0x3d, 0xec, 0x34, 0x7b, 0xa8, 0xa8, 0xdd, 0x4e, 0x50, 0x93, 0xc3,
-	0xa8, 0xfe, 0x99, 0x6a, 0xe5, 0xf9, 0x86, 0xa3, 0xea, 0xb0, 0xc0, 0x48, 0x13, 0x05, 0x75, 0x17,
-	0x51, 0x1c, 0x22, 0x77, 0x55, 0x9e, 0xaa, 0x80, 0xea, 0xec, 0xda, 0xb2, 0x26, 0x24, 0xd1, 0x7a,
-	0x92, 0x24, 0xf2, 0x6a, 0x5b, 0x04, 0x07, 0xe6, 0xe7, 0x17, 0x1d, 0x25, 0xd3, 0xed, 0x28, 0x4b,
-	0x82, 0xf8, 0x26, 0x5c, 0xb5, 0xe6, 0xb8, 0x61, 0x3b, 0xde, 0x8f, 0x04, 0x30, 0xe4, 0xdc, 0x43,
-	0x02, 0x18, 0x43, 0x01, 0x0c, 0xe9, 0x08, 0x16, 0x85, 0x47, 0x0b, 0x07, 0x75, 0xbb, 0x45, 0xa2,
-	0x80, 0xad, 0xca, 0xd3, 0x5c, 0xe3, 0x9f, 0x7b, 0x44, 0x2f, 0x3b, 0xca, 0x97, 0x1e, 0x66, 0x87,
-	0x51, 0x43, 0x73, 0x48, 0x4b, 0x8f, 0x6f, 0x5a, 0xfc, 0xac, 0x50, 0xb7, 0xa9, 0xb3, 0xd3, 0x36,
-	0xa2, 0xda, 0x4e, 0xc0, 0xba, 0x1d, 0x45, 0x1e, 0x0c, 0x39, 0x40, 0xa8, 0x5a, 0xf3, 0xdc, 0x56,
-	0xc3, 0xc1, 0x8f, 0xc2, 0x72, 0x5b, 0x5c, 0x43, 0x9e, 0x79, 0xdc, 0xb8, 0xc6, 0x48, 0x5c, 0x43,
-	0xfa, 0x03, 0x7e, 0x74, 0x10, 0x92, 0xbf, 0x51, 0x50, 0x8f, 0x02, 0x86, 0x7d, 0x39, 0xcf, 0xe5,
-	0x2c, 0x69, 0xa2, 0xc6, 0xb5, 0xa4, 0xc6, 0xb5, 0xbd, 0xa4, 0xc6, 0x4d, 0x25, 0xd6, 0x73, 0x51,
-	0x04, 0x19, 0x44, 0xab, 0x67, 0xaf, 0x14, 0x60, 0xcd, 0x0a, 0xd3, 0xaf, 0xdc, 0xf2, 0xff, 0x04,
-	0x5c, 0x1e, 0xa9, 0x75, 0x0b, 0xd1, 0x36, 0x09, 0x28, 0x92, 0xf6, 0xe1, 0x74, 0xa2, 0x31, 0xe0,
-	0xb9, 0x6e, 0x8e, 0x9d, 0x6b, 0xdc, 0x21, 0xa9, 0xb2, 0x09, 0x61, 0x9f, 0xdb, 0x88, 0x7b, 0xe4,
-	0x81, 0xdc, 0x46, 0xca, 0x6d, 0x48, 0xc7, 0xb0, 0x98, 0x4e, 0x8b, 0xba, 0xc3, 0x73, 0x73, 0x79,
-	0xa9, 0x8f, 0x77, 0x5b, 0xdb, 0xc8, 0xe9, 0xdf, 0xd6, 0x08, 0xa1, 0x6a, 0x2d, 0xa4, 0xb6, 0xad,
-	0xd8, 0x74, 0x9e, 0x85, 0x8b, 0x35, 0xea, 0xfd, 0x86, 0xd9, 0xa1, 0x1b, 0xda, 0xc7, 0x4f, 0x6a,
-	0x78, 0x30, 0xd8, 0xcf, 0x37, 0xae, 0xd8, 0x58, 0xd3, 0x9d, 0xb1, 0x35, 0xfd, 0x74, 0x58, 0x53,
-	0xc1, 0xa7, 0x5a, 0xf3, 0xa9, 0x49, 0x74, 0xc0, 0x48, 0x03, 0xe4, 0x1e, 0xb9, 0x01, 0x9e, 0x03,
-	0xf8, 0xd9, 0x2d, 0x37, 0xf6, 0xa1, 0xb7, 0x80, 0xfa, 0x0c, 0xc0, 0x42, 0xaf, 0xb1, 0x89, 0xef,
-	0x23, 0x87, 0xfd, 0x84, 0x10, 0x7d, 0x0a, 0x45, 0xa8, 0xfe, 0x09, 0x3f, 0xb9, 0x99, 0x55, 0x7a,
-	0x51, 0xbb, 0x30, 0x2f, 0x06, 0x2a, 0x89, 0x98, 0x0c, 0x2a, 0xd9, 0xf7, 0xff, 0xeb, 0xc8, 0x71,
-	0x91, 0x2c, 0x0c, 0x8e, 0x62, 0x12, 0x31, 0xd5, 0x9a, 0xe1, 0xeb, 0x5f, 0x22, 0xb6, 0x76, 0x9e,
-	0x85, 0xd9, 0x1a, 0xf5, 0xa4, 0x7f, 0x01, 0x2c, 0x0c, 0x3d, 0x06, 0xbe, 0xd7, 0xee, 0xf4, 0x42,
-	0xd1, 0x46, 0x46, 0x6b, 0x69, 0xf3, 0xbe, 0xc8, 0x34, 0xd1, 0xff, 0x00, 0x5c, 0x18, 0x19, 0x30,
-	0x1b, 0x77, 0xa7, 0x1d, 0xc6, 0x96, 0xcc, 0xfb, 0x63, 0xd3, 0x8f, 0xfa, 0x07, 0xc0, 0xd9, 0xc1,
-	0x5a, 0xfb, 0x6e, 0x8c, 0x34, 0xfb, 0xb0, 0xd2, 0x0f, 0xf7, 0x82, 0x25, 0x5f, 0x61, 0xfe, 0x7e,
-	0x71, 0x55, 0x06, 0x97, 0x57, 0x65, 0xf0, 0xfa, 0xaa, 0x0c, 0xce, 0xae, 0xcb, 0x99, 0xcb, 0xeb,
-	0x72, 0xe6, 0xc5, 0x75, 0x39, 0xb3, 0x6f, 0x0e, 0x74, 0x54, 0x1c, 0x62, 0xc5, 0xb7, 0x1b, 0x34,
-	0xd9, 0xe8, 0x47, 0xc6, 0xba, 0x7e, 0xf2, 0xce, 0x87, 0x69, 0xaf, 0xe3, 0x1a, 0x39, 0x3e, 0x6c,
-	0xbe, 0x7d, 0x1b, 0x00, 0x00, 0xff, 0xff, 0x87, 0x18, 0x55, 0xad, 0xc7, 0x0a, 0x00, 0x00,
+	// 793 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x56, 0x41, 0x4f, 0xdb, 0x48,
+	0x14, 0xce, 0x10, 0x08, 0x9b, 0x61, 0x09, 0xc4, 0x2c, 0xbb, 0x26, 0xab, 0x8d, 0x23, 0x4b, 0xbb,
+	0x9b, 0xd5, 0x0a, 0x1b, 0xb3, 0xac, 0xb4, 0x42, 0x5a, 0x89, 0x1a, 0x54, 0x89, 0x4a, 0x91, 0x2a,
+	0x8b, 0xaa, 0x12, 0xaa, 0x6a, 0x39, 0xf6, 0x60, 0x46, 0x71, 0x3c, 0xa9, 0x67, 0x02, 0xa4, 0xd7,
+	0x5e, 0x7b, 0x40, 0xfd, 0x23, 0xfc, 0x0d, 0x0e, 0x3d, 0x70, 0x68, 0xa5, 0xaa, 0x87, 0xb4, 0x82,
+	0x53, 0xaf, 0xf9, 0x05, 0x95, 0x3d, 0xb6, 0x13, 0x12, 0x2a, 0x11, 0xe0, 0x52, 0x4e, 0xf1, 0x7c,
+	0xf3, 0xbe, 0xef, 0xf9, 0x7d, 0xf3, 0xde, 0xc4, 0xf0, 0x4f, 0x42, 0x9b, 0x84, 0x62, 0xaa, 0xda,
+	0xc4, 0xb7, 0x91, 0xcf, 0x02, 0x8b, 0x21, 0x67, 0xd9, 0xc3, 0x2f, 0xda, 0xd8, 0xc1, 0xac, 0xa3,
+	0xb2, 0x23, 0xa5, 0x15, 0x10, 0x46, 0x84, 0xdf, 0xe3, 0x40, 0x65, 0x30, 0x30, 0x8d, 0x53, 0x0e,
+	0xb4, 0x3a, 0x62, 0x96, 0x56, 0xfa, 0xc9, 0x25, 0x2e, 0x89, 0x18, 0x6a, 0xf8, 0xc4, 0xc9, 0x25,
+	0xc9, 0x25, 0xc4, 0xf5, 0x90, 0x1a, 0xad, 0xea, 0xed, 0x3d, 0x95, 0xe1, 0x26, 0xa2, 0xcc, 0x6a,
+	0xb6, 0xe2, 0x80, 0xb2, 0x1d, 0xc9, 0xab, 0x75, 0x8b, 0x22, 0x35, 0xd6, 0x52, 0x6d, 0x82, 0x7d,
+	0xbe, 0x2f, 0x7f, 0x99, 0x82, 0xc5, 0x1a, 0x75, 0x37, 0x03, 0x64, 0x31, 0xf4, 0x98, 0x50, 0xcc,
+	0x30, 0xf1, 0x85, 0xbf, 0xe1, 0x74, 0x8b, 0x10, 0xcf, 0xc4, 0x8e, 0x08, 0x2a, 0xa0, 0x3a, 0xa9,
+	0x0b, 0xbd, 0xae, 0x54, 0xe8, 0x58, 0x4d, 0x6f, 0x5d, 0x8e, 0x37, 0x64, 0x23, 0x17, 0x3e, 0x6d,
+	0x3b, 0xc2, 0x5f, 0x30, 0x47, 0x91, 0xef, 0xa0, 0x40, 0x9c, 0xa8, 0x80, 0x6a, 0x5e, 0x2f, 0xf6,
+	0xba, 0xd2, 0x2c, 0x8f, 0xe5, 0xb8, 0x6c, 0xc4, 0x01, 0xc2, 0x1a, 0x84, 0x1e, 0x39, 0x44, 0x81,
+	0xc9, 0xb0, 0xdd, 0x10, 0xb3, 0x15, 0x50, 0xcd, 0xea, 0x8b, 0xbd, 0xae, 0x54, 0xe4, 0xe1, 0xfd,
+	0x3d, 0xd9, 0xc8, 0x47, 0x8b, 0x1d, 0x6c, 0x37, 0x42, 0x56, 0xbb, 0xd5, 0x4a, 0x58, 0x93, 0xc3,
+	0xac, 0xfe, 0x9e, 0x6c, 0xe4, 0xa3, 0x45, 0xc4, 0x32, 0x61, 0x81, 0x91, 0x06, 0xf2, 0x4d, 0x07,
+	0x51, 0x1c, 0x20, 0x67, 0x45, 0x9c, 0xaa, 0x80, 0xea, 0xcc, 0xea, 0x92, 0xc2, 0x2d, 0x51, 0x42,
+	0x4b, 0x12, 0x7b, 0x95, 0x4d, 0x82, 0x7d, 0xfd, 0xb7, 0xd3, 0xae, 0x94, 0xe9, 0x75, 0xa5, 0x45,
+	0x2e, 0x7c, 0x99, 0x2e, 0x1b, 0xb3, 0x11, 0xb0, 0x15, 0xaf, 0x47, 0x12, 0x68, 0x62, 0xee, 0x36,
+	0x09, 0xb4, 0xa1, 0x04, 0x9a, 0x70, 0x00, 0x8b, 0x3c, 0xa2, 0x89, 0x7d, 0xd3, 0x6a, 0x92, 0xb6,
+	0xcf, 0x56, 0xc4, 0xe9, 0xc8, 0xe3, 0x47, 0xa1, 0xd0, 0xc7, 0xae, 0xf4, 0x87, 0x8b, 0xd9, 0x7e,
+	0xbb, 0xae, 0xd8, 0xa4, 0xa9, 0xc6, 0x27, 0xcd, 0x7f, 0x96, 0xa9, 0xd3, 0x50, 0x59, 0xa7, 0x85,
+	0xa8, 0xb2, 0xed, 0xb3, 0x5e, 0x57, 0x12, 0x07, 0x53, 0x0e, 0x08, 0xca, 0xc6, 0x5c, 0x84, 0xd5,
+	0xb0, 0xff, 0x80, 0x23, 0x57, 0xe5, 0xd5, 0xc4, 0x1f, 0xee, 0x36, 0xaf, 0x36, 0x92, 0x57, 0x13,
+	0x9e, 0xc3, 0x1f, 0xf7, 0x02, 0xf2, 0x12, 0xf9, 0x66, 0xdb, 0x67, 0xd8, 0x13, 0xf3, 0x91, 0x9d,
+	0x25, 0x85, 0xf7, 0xb8, 0x92, 0xf4, 0xb8, 0xb2, 0x93, 0xf4, 0xb8, 0x2e, 0xc5, 0x7e, 0x2e, 0xf0,
+	0x24, 0x83, 0x6c, 0xf9, 0xf8, 0x93, 0x04, 0x8c, 0x19, 0x0e, 0x3d, 0x89, 0x90, 0xb7, 0x13, 0x70,
+	0x69, 0xa4, 0xd7, 0x0d, 0x44, 0x5b, 0xc4, 0xa7, 0x48, 0xd8, 0x85, 0xd3, 0x89, 0xc7, 0x20, 0xaa,
+	0x75, 0x63, 0xec, 0x5a, 0xe3, 0x09, 0x49, 0x9d, 0x4d, 0x04, 0xfb, 0xda, 0x5a, 0x3c, 0x23, 0xb7,
+	0xd4, 0xd6, 0x52, 0x6d, 0x4d, 0x38, 0x84, 0xc5, 0xf4, 0xb6, 0x30, 0xed, 0xa8, 0x36, 0x27, 0x6a,
+	0xf5, 0xf1, 0x4e, 0x6b, 0x0b, 0xd9, 0xfd, 0xd3, 0x1a, 0x11, 0x94, 0x8d, 0xf9, 0x14, 0xdb, 0x8c,
+	0xa1, 0x93, 0x2c, 0x5c, 0xa8, 0x51, 0xf7, 0x29, 0x66, 0xfb, 0x4e, 0x60, 0x1d, 0xde, 0xab, 0xcb,
+	0x83, 0xc1, 0x7e, 0xbd, 0x71, 0xc7, 0xc6, 0x9e, 0x6e, 0x8f, 0xed, 0xe9, 0x2f, 0xc3, 0x9e, 0x72,
+	0x3d, 0xd9, 0x98, 0x4b, 0x21, 0x3e, 0x01, 0x23, 0x03, 0x90, 0xbb, 0xe3, 0x01, 0x78, 0x0f, 0xe0,
+	0xaf, 0x57, 0x9c, 0xd8, 0xf7, 0x3e, 0x02, 0xf2, 0x3b, 0x00, 0x0b, 0xe1, 0x60, 0x13, 0xcf, 0x43,
+	0x36, 0x7b, 0x88, 0x10, 0xbd, 0x0f, 0x4d, 0x28, 0x77, 0xe0, 0xcf, 0x97, 0xab, 0x4a, 0x0f, 0xca,
+	0x84, 0x05, 0x9b, 0xc3, 0xc8, 0x31, 0xf7, 0x10, 0xa2, 0x22, 0xa8, 0x64, 0xc7, 0xfa, 0xeb, 0xb9,
+	0x4c, 0x97, 0x8d, 0xd9, 0x14, 0x08, 0x13, 0xad, 0x9e, 0x64, 0x61, 0xb6, 0x46, 0x5d, 0xe1, 0x35,
+	0x80, 0x85, 0xa1, 0x6f, 0x83, 0xff, 0x94, 0x6b, 0x7d, 0xb0, 0x28, 0x23, 0x37, 0x6d, 0x69, 0xe3,
+	0xa6, 0xcc, 0xb4, 0xee, 0x37, 0x00, 0xce, 0x8f, 0xdc, 0x37, 0xeb, 0xd7, 0x97, 0x1d, 0xe6, 0x96,
+	0xf4, 0x9b, 0x73, 0xd3, 0x97, 0x7a, 0x05, 0xe0, 0xcc, 0x60, 0xeb, 0xfd, 0x3b, 0x46, 0x99, 0x7d,
+	0x5a, 0xe9, 0xff, 0x1b, 0xd1, 0x92, 0xb7, 0xd0, 0x9f, 0x9d, 0x9e, 0x97, 0xc1, 0xd9, 0x79, 0x19,
+	0x7c, 0x3e, 0x2f, 0x83, 0xe3, 0x8b, 0x72, 0xe6, 0xec, 0xa2, 0x9c, 0xf9, 0x70, 0x51, 0xce, 0xec,
+	0xea, 0x03, 0x03, 0x16, 0xa7, 0x58, 0xf6, 0xac, 0x3a, 0x4d, 0x16, 0xea, 0x81, 0xb6, 0xa6, 0x1e,
+	0x7d, 0xf3, 0x3b, 0x35, 0x1c, 0xc0, 0x7a, 0x2e, 0xba, 0x7b, 0xfe, 0xf9, 0x1a, 0x00, 0x00, 0xff,
+	0xff, 0x13, 0x9c, 0x0b, 0x48, 0xd6, 0x0a, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -933,10 +933,10 @@ func (m *MsgCollectFeesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error)
 	_ = i
 	var l int
 	_ = l
-	if len(m.TokenOut) > 0 {
-		for iNdEx := len(m.TokenOut) - 1; iNdEx >= 0; iNdEx-- {
+	if len(m.CollectedFees) > 0 {
+		for iNdEx := len(m.CollectedFees) - 1; iNdEx >= 0; iNdEx-- {
 			{
-				size, err := m.TokenOut[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+				size, err := m.CollectedFees[iNdEx].MarshalToSizedBuffer(dAtA[:i])
 				if err != nil {
 					return 0, err
 				}
@@ -1075,8 +1075,8 @@ func (m *MsgCollectFeesResponse) Size() (n int) {
 	}
 	var l int
 	_ = l
-	if len(m.TokenOut) > 0 {
-		for _, e := range m.TokenOut {
+	if len(m.CollectedFees) > 0 {
+		for _, e := range m.CollectedFees {
 			l = e.Size()
 			n += 1 + l + sovTx(uint64(l))
 		}
@@ -2042,7 +2042,7 @@ func (m *MsgCollectFeesResponse) Unmarshal(dAtA []byte) error {
 		switch fieldNum {
 		case 1:
 			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field TokenOut", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field CollectedFees", wireType)
 			}
 			var msglen int
 			for shift := uint(0); ; shift += 7 {
@@ -2069,8 +2069,8 @@ func (m *MsgCollectFeesResponse) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.TokenOut = append(m.TokenOut, types.Coin{})
-			if err := m.TokenOut[len(m.TokenOut)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+			m.CollectedFees = append(m.CollectedFees, types.Coin{})
+			if err := m.CollectedFees[len(m.CollectedFees)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
 			iNdEx = postIndex
diff --git a/x/ibc-hooks/go.mod b/x/ibc-hooks/go.mod
index bbcef0e1e..3d2740c43 100644
--- a/x/ibc-hooks/go.mod
+++ b/x/ibc-hooks/go.mod
@@ -4,7 +4,7 @@ go 1.19
 
 require (
 	github.com/CosmWasm/wasmd v0.30.0
-	github.com/cosmos/cosmos-sdk v0.46.8
+	github.com/cosmos/cosmos-sdk v0.46.10
 	github.com/cosmos/ibc-go/v4 v4.3.0
 	github.com/gorilla/mux v1.8.0
 	github.com/grpc-ecosystem/grpc-gateway v1.16.0
diff --git a/x/twap/logic.go b/x/twap/logic.go
index d517b925e..e6fcb7e6e 100644
--- a/x/twap/logic.go
+++ b/x/twap/logic.go
@@ -182,7 +182,7 @@ func recordWithUpdatedAccumulators(record types.TwapRecord, newTime time.Time) t
 		return record
 	}
 	newRecord := record
-	timeDelta := newTime.Sub(record.Time)
+	timeDelta := types.CanonicalTimeMs(newTime) - types.CanonicalTimeMs(record.Time)
 	newRecord.Time = newTime
 
 	// record.LastSpotPrice is the last spot price from the block the record was created in,
diff --git a/x/twap/strategy.go b/x/twap/strategy.go
index a449b7f56..b2e3cc536 100644
--- a/x/twap/strategy.go
+++ b/x/twap/strategy.go
@@ -34,7 +34,7 @@ func (s *arithmetic) computeTwap(startRecord types.TwapRecord, endRecord types.T
 	} else {
 		accumDiff = endRecord.P1ArithmeticTwapAccumulator.Sub(startRecord.P1ArithmeticTwapAccumulator)
 	}
-	timeDelta := endRecord.Time.Sub(startRecord.Time)
+	timeDelta := types.CanonicalTimeMs(endRecord.Time) - types.CanonicalTimeMs(startRecord.Time)
 	return types.AccumDiffDivDuration(accumDiff, timeDelta)
 }
 
@@ -47,7 +47,7 @@ func (s *geometric) computeTwap(startRecord types.TwapRecord, endRecord types.Tw
 		return sdk.ZeroDec()
 	}
 
-	timeDelta := endRecord.Time.Sub(startRecord.Time)
+	timeDelta := types.CanonicalTimeMs(endRecord.Time) - types.CanonicalTimeMs(startRecord.Time)
 	arithmeticMeanOfLogPrices := types.AccumDiffDivDuration(accumDiff, timeDelta)
 
 	exponent := arithmeticMeanOfLogPrices
diff --git a/x/twap/types/utils.go b/x/twap/types/utils.go
index 1bd70a1d2..7ed862586 100644
--- a/x/twap/types/utils.go
+++ b/x/twap/types/utils.go
@@ -37,16 +37,14 @@ func GetAllUniqueDenomPairs(denoms []string) []DenomPair {
 // SpotPriceMulDuration returns the spot price multiplied by the time delta,
 // that is the spot price between the current and last TWAP record.
 // A single second accounts for 1_000_000_000 when converted to int64.
-func SpotPriceMulDuration(sp sdk.Dec, timeDelta time.Duration) sdk.Dec {
-	deltaMS := timeDelta.Milliseconds()
-	return sp.MulInt64(deltaMS)
+func SpotPriceMulDuration(sp sdk.Dec, timeDeltaMs int64) sdk.Dec {
+	return sp.MulInt64(timeDeltaMs)
 }
 
 // AccumDiffDivDuration returns the accumulated difference divided by the the
 // time delta, that is the spot price between the current and last TWAP record.
-func AccumDiffDivDuration(accumDiff sdk.Dec, timeDelta time.Duration) sdk.Dec {
-	deltaMS := timeDelta.Milliseconds()
-	return accumDiff.QuoInt64(deltaMS)
+func AccumDiffDivDuration(accumDiff sdk.Dec, timeDeltaMs int64) sdk.Dec {
+	return accumDiff.QuoInt64(timeDeltaMs)
 }
 
 // LexicographicalOrderDenoms takes two denoms and returns them to be in lexicographically ascending order.
@@ -62,6 +60,14 @@ func LexicographicalOrderDenoms(denom0, denom1 string) (string, string, error) {
 	return denom0, denom1, nil
 }
 
+// CanonicalTimeMs returns the canonical time in milliseconds used for twap
+// math computations in UTC. Removes any monotonic clock reading prior to conversion to ms.
+// In twap, we assume all calculations are done in milliseconds. Therefore, this conversion
+// is necessary to make sure that there are no rounding errors.
+func CanonicalTimeMs(twapTime time.Time) int64 {
+	return twapTime.Round(0).UnixMilli()
+}
+
 // DenomPair contains pair of assetA and assetB denoms which belong to a pool.
 type DenomPair struct {
 	Denom0 string


Test patch:
diff --git a/app/upgrades/v15/export_test.go b/app/upgrades/v15/export_test.go
index a7145c6d3..e73d42fff 100644
--- a/app/upgrades/v15/export_test.go
+++ b/app/upgrades/v15/export_test.go
@@ -2,6 +2,7 @@ package v15
 
 import (
 	sdk "github.com/cosmos/cosmos-sdk/types"
+	icqkeeper "github.com/strangelove-ventures/async-icq/v4/keeper"
 
 	bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
 
@@ -16,3 +17,7 @@ func MigrateNextPoolId(ctx sdk.Context, gammKeeper *gammkeeper.Keeper, poolmanag
 func RegisterOsmoIonMetadata(ctx sdk.Context, bankKeeper bankkeeper.Keeper) {
 	registerOsmoIonMetadata(ctx, bankKeeper)
 }
+
+func SetICQParams(ctx sdk.Context, icqKeeper *icqkeeper.Keeper) {
+	setICQParams(ctx, icqKeeper)
+}
diff --git a/app/upgrades/v15/upgrade_test.go b/app/upgrades/v15/upgrade_test.go
index 8909c9591..21d19239d 100644
--- a/app/upgrades/v15/upgrade_test.go
+++ b/app/upgrades/v15/upgrade_test.go
@@ -95,3 +95,13 @@ func (suite *UpgradeTestSuite) TestRegisterOsmoIonMetadata() {
 	suite.Require().Equal(expectedUosmodenom, uosmoMetadata.Base)
 	suite.Require().Equal(expectedUiondenom, uionMetadata.Base)
 }
+
+func (suite *UpgradeTestSuite) TestSetICQParams() {
+	suite.SetupTest() // reset
+
+	// system under test.
+	v15.SetICQParams(suite.Ctx, suite.App.ICQKeeper)
+
+	suite.Require().True(suite.App.ICQKeeper.IsHostEnabled(suite.Ctx))
+	suite.Require().Len(suite.App.ICQKeeper.GetAllowQueries(suite.Ctx), 62)
+}
diff --git a/osmoutils/accum/accum_test.go b/osmoutils/accum/accum_test.go
index 8b59d5104..d99546d91 100644
--- a/osmoutils/accum/accum_test.go
+++ b/osmoutils/accum/accum_test.go
@@ -675,20 +675,6 @@ func (suite *AccumTestSuite) TestAddToPositionCustomAcc() {
 				UnclaimedRewards: emptyCoins,
 			},
 		},
-		"negative acc value - error": {
-			accObject:     accObject,
-			name:          testAddressOne,
-			numShareUnits: positionOne.NumShares,
-			customAcc:     accObject.GetValue().MulDec(sdk.NewDec(-1)),
-			expectedError: accumPackage.NegativeCustomAccError{accObject.GetValue().MulDec(sdk.NewDec(-1))},
-		},
-		"update is smaller than old value - error": {
-			accObject:     accObject,
-			name:          testAddressOne,
-			numShareUnits: positionOne.NumShares,
-			customAcc:     accObject.GetValue().MulDec(sdk.NewDecWithPrec(5, 1)),
-			expectedError: accumPackage.NegativeAccDifferenceError{accObject.GetValue().MulDec(sdk.NewDecWithPrec(5, 1))},
-		},
 	}
 
 	for name, tc := range tests {
@@ -955,21 +941,6 @@ func (suite *AccumTestSuite) TestRemoveFromPositionCustomAcc() {
 				UnclaimedRewards: baseAccumValue.MulDec(sdk.NewDecWithPrec(25, 2)).MulDec(positionTwo.NumShares),
 			},
 		},
-		"negative acc value equals to acc": {
-			accObject:     accObject,
-			name:          testAddressOne,
-			numShareUnits: positionOne.NumShares,
-			customAcc:     baseAccumValue.MulDec(sdk.NewDec(-1)),
-			expectedError: accumPackage.NegativeCustomAccError{baseAccumValue.MulDec(sdk.NewDec(-1))},
-		},
-		"update is smaller than old value - error": {
-			accObject:     accObject,
-			name:          testAddressOne,
-			numShareUnits: positionOne.NumShares,
-			customAcc:     baseAccumValue.MulDec(sdk.NewDecWithPrec(25, 2)),
-			// base * 0.25 - base * 0.5 = -0.25 * base
-			expectedError: accumPackage.NegativeAccDifferenceError{baseAccumValue.MulDec(sdk.NewDecWithPrec(25, 2))},
-		},
 	}
 
 	for name, tc := range tests {
@@ -1287,24 +1258,6 @@ func (suite *AccumTestSuite) TestUpdatePositionCustomAcc() {
 				UnclaimedRewards: emptyCoins,
 			},
 		},
-		{
-			testName:      "negative acc value - error",
-			initialShares: sdk.ZeroDec(),
-			initialAccum:  initialCoinsDenomOne,
-			accName:       testAddressOne,
-			numShareUnits: positionOne.NumShares,
-			customAcc:     initialCoinsDenomOne.MulDec(sdk.NewDec(-1)),
-			expectedError: accumPackage.NegativeCustomAccError{initialCoinsDenomOne.MulDec(sdk.NewDec(-1))},
-		},
-		{
-			testName:      "update is smaller than old value - error",
-			initialShares: sdk.ZeroDec(),
-			initialAccum:  initialCoinsDenomOne,
-			accName:       testAddressOne,
-			numShareUnits: positionOne.NumShares,
-			customAcc:     initialCoinsDenomOne.MulDec(sdk.NewDecWithPrec(5, 1)),
-			expectedError: accumPackage.NegativeAccDifferenceError{initialCoinsDenomOne.MulDec(sdk.NewDecWithPrec(5, 1))},
-		},
 	}
 
 	for _, tc := range tests {
@@ -1418,18 +1371,6 @@ func (suite *AccumTestSuite) TestSetPositionCustomAcc() {
 			positionName:           validPositionName,
 			customAccumulatorValue: initialCoinsDenomOne,
 		},
-		"invalid update smaller than the initial value": {
-			positionName:           validPositionName,
-			customAccumulatorValue: emptyCoins,
-
-			expectedError: accumPackage.NegativeAccDifferenceError{AccumulatorDifference: initialCoinsDenomOne},
-		},
-		"invalid update smaller than the initial value (non-empty custom value)": {
-			positionName:           validPositionName,
-			customAccumulatorValue: initialCoinsDenomOne.QuoDec(sdk.NewDec(2)),
-
-			expectedError: accumPackage.NegativeAccDifferenceError{AccumulatorDifference: initialCoinsDenomOne.QuoDec(sdk.NewDec(2))},
-		},
 		"invalid position - different name": {
 			positionName:  invalidPositionName,
 			expectedError: accumPackage.NoPositionError{Name: invalidPositionName},
@@ -1466,7 +1407,7 @@ func (suite *AccumTestSuite) TestSetPositionCustomAcc() {
 // We run a series of partially random operations on two accumulators to ensure that total shares are properly tracked in state
 func (suite *AccumTestSuite) TestGetTotalShares() {
 	suite.SetupTest()
-	
+
 	// Set seed to make tests deterministic
 	rand.Seed(1)
 
@@ -1483,8 +1424,12 @@ func (suite *AccumTestSuite) TestGetTotalShares() {
 	suite.Require().NoError(err)
 
 	// Ensure that both accums start at zero shares
-	suite.Require().Equal(sdk.ZeroDec(), accumOne.GetTotalShares())
-	suite.Require().Equal(sdk.ZeroDec(), accumTwo.GetTotalShares())
+	accumOneShares, err := accumOne.GetTotalShares()
+	suite.Require().NoError(err)
+	accumTwoShares, err := accumTwo.GetTotalShares()
+	suite.Require().NoError(err)
+	suite.Require().Equal(sdk.ZeroDec(), accumOneShares)
+	suite.Require().Equal(sdk.ZeroDec(), accumTwoShares)
 
 	// Create position on first accum and pull new accum objects from state
 	err = accumOne.NewPosition(testAddressOne, sdk.OneDec(), nil)
@@ -1495,8 +1440,12 @@ func (suite *AccumTestSuite) TestGetTotalShares() {
 	suite.Require().NoError(err)
 
 	// Check that total shares for accum one has updated properly and accum two shares are unchanged
-	suite.Require().Equal(sdk.OneDec(), accumOne.GetTotalShares())
-	suite.Require().Equal(sdk.ZeroDec(), accumTwo.GetTotalShares())
+	accumOneShares, err = accumOne.GetTotalShares()
+	suite.Require().NoError(err)
+	accumTwoShares, err = accumTwo.GetTotalShares()
+	suite.Require().NoError(err)
+	suite.Require().Equal(sdk.OneDec(), accumOneShares)
+	suite.Require().Equal(sdk.ZeroDec(), accumTwoShares)
 
 	// Run a number of NewPosition, AddToPosition, and RemoveFromPosition operations on each accum
 	testAddresses := []string{testAddressOne, testAddressTwo, testAddressThree}
@@ -1505,8 +1454,8 @@ func (suite *AccumTestSuite) TestGetTotalShares() {
 
 	for i := 1; i <= 10; i++ {
 		// Cycle through accounts and accumulators
-		curAddr := testAddresses[i % 3]
-		curAccum := accums[i % 2]
+		curAddr := testAddresses[i%3]
+		curAccum := accums[i%2]
 
 		// We set a baseAmt that varies with the iteration to increase coverage
 		baseAmt := sdk.NewDec(int64(i)).Mul(sdk.NewDec(10))
@@ -1523,7 +1472,7 @@ func (suite *AccumTestSuite) TestGetTotalShares() {
 		// whether we will add and/or remove liquidity this loop
 		addShares := sdk.NewDec(int64(rand.Int()) % 2)
 		removeShares := sdk.NewDec(int64(rand.Int()) % 2)
-		
+
 		// Half the time, we add to the new position
 		addAmt := baseAmt.Mul(addShares)
 		if addAmt.GT(sdk.ZeroDec()) {
@@ -1542,9 +1491,9 @@ func (suite *AccumTestSuite) TestGetTotalShares() {
 		// we targeted in this loop
 		if !positionExists {
 			// If a new position was created, we factor its new shares in
-			expectedShares[i % 2] = expectedShares[i % 2].Add(baseAmt)
+			expectedShares[i%2] = expectedShares[i%2].Add(baseAmt)
 		}
-		expectedShares[i % 2] = expectedShares[i % 2].Add(addAmt).Sub(amtToRemove)
+		expectedShares[i%2] = expectedShares[i%2].Add(addAmt).Sub(amtToRemove)
 	}
 
 	// Get updated accums from state to validate results
@@ -1554,6 +1503,10 @@ func (suite *AccumTestSuite) TestGetTotalShares() {
 	suite.Require().NoError(err)
 
 	// Ensure that total shares in each accum matches our expected number of shares
-	suite.Require().Equal(expectedShares[0], accumOne.GetTotalShares())
-	suite.Require().Equal(expectedShares[1], accumTwo.GetTotalShares())
-}
\ No newline at end of file
+	accumOneShares, err = accumOne.GetTotalShares()
+	suite.Require().NoError(err)
+	accumTwoShares, err = accumTwo.GetTotalShares()
+	suite.Require().NoError(err)
+	suite.Require().Equal(expectedShares[0], accumOneShares)
+	suite.Require().Equal(expectedShares[1], accumTwoShares)
+}
diff --git a/osmoutils/accum/export_test.go b/osmoutils/accum/export_test.go
index ed3163773..f2f61e871 100644
--- a/osmoutils/accum/export_test.go
+++ b/osmoutils/accum/export_test.go
@@ -36,9 +36,9 @@ func MakeTestAccumulator(store store.KVStore, name string, value sdk.DecCoins, t
 	// because position operations still require GetAccumulator to work
 	_ = MakeAccumulator(store, name)
 	return AccumulatorObject{
-		store: store,
-		name:  name,
-		value: value,
+		store:       store,
+		name:        name,
+		value:       value,
 		totalShares: totalShares,
 	}
 }
diff --git a/scripts/cl/test/__init__.py b/scripts/cl/test/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/scripts/cl/test/test_common.py b/scripts/cl/test/test_common.py
new file mode 100644
index 000000000..7b25463d6
--- /dev/null
+++ b/scripts/cl/test/test_common.py
@@ -0,0 +1,61 @@
+import unittest
+import math
+
+import scripts.cl.common.sdk_dec as sdk_dec
+
+class SDKTestVector:
+  def __init__(self, a: str, b: str, expected_mul: str, expected_quo: str, expected_quo_up: str, expected_quo_trunc: str):
+        self.a = sdk_dec.new(a)
+        self.b = sdk_dec.new(b)
+        self.expected_mul = sdk_dec.new(expected_mul)
+        self.expected_quo = sdk_dec.new(expected_quo)
+        self.expected_quo_up = sdk_dec.new(expected_quo_up)
+        self.expected_quo_trunc = sdk_dec.new(expected_quo_trunc)
+
+# These test vectors are taken from:
+# https://github.com/osmosis-labs/cosmos-sdk/blob/5c0181df6e93b72e7dacde8e08d6a59d316c732f/types/decimal_test.go#L184
+sdk_test_vectors = [
+    SDKTestVector("0", "0", "0", "0", "0", "0"),
+    SDKTestVector("1", "0", "0", "0", "0", "0"),
+    SDKTestVector("0", "1", "0", "0", "0", "0"),
+    SDKTestVector("0", "-1", "0", "0", "0", "0"),
+    SDKTestVector("-1", "0", "0", "0", "0", "0"),
+    SDKTestVector("1", "1", "1", "1", "1", "1"),
+    SDKTestVector("1", "1", "1", "1", "1", "1"),
+    SDKTestVector("1", "2", "2", "0.5", "0.5", "0.5"),
+    SDKTestVector("3", "7", "21", "0.428571428571428571", "0.428571428571428572", "0.428571428571428571"),
+    SDKTestVector("3", "7", "21", "0.428571428571428571", "0.428571428571428572", "0.428571428571428571"),
+    SDKTestVector("2", "4", "8", "0.5", "0.5", "0.5"),
+    SDKTestVector("100", "100", "10000", "1", "1", "1"),
+    SDKTestVector("0.15", "0.15", "0.022500000000000000", "1", "1", "1"),
+    SDKTestVector("0.3333", "0.0333", "0.01109889", "10.009009009009009009", "10.009009009009009010", "10.009009009009009009"),
+]
+
+class TestSdkDec(unittest.TestCase):
+    def test_mul(self):
+        for vector in sdk_test_vectors:
+            print(f"mul: {vector.a} x {vector.b} = {vector.expected_mul}")
+            actual = sdk_dec.mul(vector.a, vector.b)
+            self.assertEqual(vector.expected_mul, actual)
+
+    def test_quo(self):
+        for vector in sdk_test_vectors:
+            if vector.b == sdk_dec.zero:
+                continue
+
+            print(f"quo: {vector.a} x {vector.b} = {vector.expected_quo}")
+            actual = sdk_dec.quo(vector.a, vector.b)
+            self.assertEqual(vector.expected_quo, actual)
+
+    def test_quo_up(self):
+        for vector in sdk_test_vectors:
+            if vector.b == sdk_dec.zero:
+                continue
+
+            print(f"quo_up: {vector.a} x {vector.b} = {vector.expected_quo_up}")
+            actual = sdk_dec.quo_up(vector.a, vector.b)
+            self.assertEqual(vector.expected_quo_up, actual)
+
+
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file
diff --git a/tests/e2e/configurer/chain/commands.go b/tests/e2e/configurer/chain/commands.go
index 4c7e7595a..b100a728e 100644
--- a/tests/e2e/configurer/chain/commands.go
+++ b/tests/e2e/configurer/chain/commands.go
@@ -69,6 +69,14 @@ func (n *NodeConfig) StoreWasmCode(wasmFile, from string) {
 	n.LogActionF("successfully stored")
 }
 
+func (n *NodeConfig) WithdrawPosition(from, lowerTick, upperTick string, liquidityOut string, poolId uint64, frozenUntil int64) {
+	n.LogActionF("withdrawing liquidity from position")
+	cmd := []string{"osmosisd", "tx", "concentratedliquidity", "withdraw-position", lowerTick, upperTick, liquidityOut, fmt.Sprintf("%d", frozenUntil), fmt.Sprintf("--from=%s", from), fmt.Sprintf("--pool-id=%d", poolId)}
+	_, _, err := n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, cmd)
+	require.NoError(n.t, err)
+	n.LogActionF("successfully withdrew position from lowerTick %s to upperTick %s", lowerTick, upperTick)
+}
+
 func (n *NodeConfig) InstantiateWasmContract(codeId, initMsg, from string) {
 	n.LogActionF("instantiating wasm contract %s with %s", codeId, initMsg)
 	cmd := []string{"osmosisd", "tx", "wasm", "instantiate", codeId, initMsg, fmt.Sprintf("--from=%s", from), "--no-admin", "--label=ratelimit"}
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
index 384097400..c4f7a199b 100644
--- a/tests/e2e/e2e_test.go
+++ b/tests/e2e/e2e_test.go
@@ -199,10 +199,18 @@ func (s *IntegrationTestSuite) TestConcentratedLiquidity() {
 		frozenUntil               int64  = time.Unix(86400, 0).Unix()
 		swapFee                          = "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, precisionFactorAtPriceOne, swapFee)
 
-	concentratedPool, err := node.QueryConcentratedPool(poolID)
-	s.Require().NoError(err)
+	concentratedPool := updatedPool(poolID)
 
 	// assert contents of the pool are valid
 	s.Require().Equal(concentratedPool.GetId(), poolID)
@@ -220,14 +228,14 @@ func (s *IntegrationTestSuite) TestConcentratedLiquidity() {
 	address2 := node.CreateWalletAndFund("addr2", fundTokens)
 	address3 := node.CreateWalletAndFund("addr3", fundTokens)
 
-	// Create 2 positions for node1: overlap together, overlap with 2 node3 positions)
+	// Create 2 positions for address1: overlap together, overlap with 2 address3 positions
 	node.CreateConcentratedPosition(address1, "[-1200]", "400", fmt.Sprintf("1000%s", denom0), fmt.Sprintf("1000%s", denom1), 0, 0, frozenUntil, poolID)
 	node.CreateConcentratedPosition(address1, "[-400]", "400", fmt.Sprintf("1000%s", denom0), fmt.Sprintf("1000%s", denom1), 0, 0, frozenUntil, poolID)
 
-	// Create 1 position for node2: does not overlap with anything, ends at maximum
+	// Create 1 position for address2: does not overlap with anything, ends at maximum
 	node.CreateConcentratedPosition(address2, "2200", fmt.Sprintf("%d", maxTick), fmt.Sprintf("1000%s", denom0), fmt.Sprintf("1000%s", denom1), 0, 0, frozenUntil, poolID)
 
-	// Create 2 positions for node3: overlap together, overlap with 2 node1 positions, one position starts from minimum
+	// Create 2 positions for address3: overlap together, overlap with 2 address1 positions, one position starts from minimum
 	node.CreateConcentratedPosition(address3, "[-1600]", "[-200]", fmt.Sprintf("1000%s", denom0), fmt.Sprintf("1000%s", denom1), 0, 0, frozenUntil, poolID)
 	node.CreateConcentratedPosition(address3, fmt.Sprintf("[%d]", minTick), "1400", fmt.Sprintf("1000%s", denom0), fmt.Sprintf("1000%s", denom1), 0, 0, frozenUntil, poolID)
 
@@ -268,6 +276,47 @@ func (s *IntegrationTestSuite) TestConcentratedLiquidity() {
 	validateCLPosition(addr3position1, poolID, -1600, -200)
 	// second position third address
 	validateCLPosition(addr3position2, poolID, minTick, 1400)
+
+	// Withdraw Position:
+	var (
+		defaultLiquidityRemoval string = "1000"
+	)
+
+	// Assert removing some liquidity
+	// address1: check removing some amount of liquidity
+	address1position1liquidityBefore := positionsAddress1[0].Liquidity
+	node.WithdrawPosition(address1, "[-1200]", "400", defaultLiquidityRemoval, poolID, frozenUntil)
+	// assert
+	positionsAddress1 = node.QueryConcentratedPositions(address1)
+	s.Require().Equal(address1position1liquidityBefore, positionsAddress1[0].Liquidity.Add(sdk.MustNewDecFromStr(defaultLiquidityRemoval)))
+
+	// address2: check removing some amount of liquidity
+	address2position1liquidityBefore := positionsAddress2[0].Liquidity
+	node.WithdrawPosition(address2, "2200", fmt.Sprintf("%d", maxTick), defaultLiquidityRemoval, poolID, frozenUntil)
+	// assert
+	positionsAddress2 = node.QueryConcentratedPositions(address2)
+	s.Require().Equal(address2position1liquidityBefore, positionsAddress2[0].Liquidity.Add(sdk.MustNewDecFromStr(defaultLiquidityRemoval)))
+
+	// address3: check removing some amount of liquidity
+	address3position1liquidityBefore := positionsAddress3[0].Liquidity
+	node.WithdrawPosition(address3, "[-1600]", "[-200]", defaultLiquidityRemoval, poolID, frozenUntil)
+	// assert
+	positionsAddress3 = node.QueryConcentratedPositions(address3)
+	s.Require().Equal(address3position1liquidityBefore, positionsAddress3[0].Liquidity.Add(sdk.MustNewDecFromStr(defaultLiquidityRemoval)))
+
+	// Assert removing all liquidity
+	// address2: no more positions left
+	allLiquidityAddress2Position1 := positionsAddress2[0].Liquidity
+	node.WithdrawPosition(address2, "2200", fmt.Sprintf("%d", maxTick), allLiquidityAddress2Position1.String(), poolID, frozenUntil)
+	positionsAddress2 = node.QueryConcentratedPositions(address2)
+	s.Require().Empty(positionsAddress2)
+
+	// address1: one position left
+	allLiquidityAddress1Position1 := positionsAddress1[0].Liquidity
+	node.WithdrawPosition(address1, "[-1200]", "400", allLiquidityAddress1Position1.String(), poolID, frozenUntil)
+	positionsAddress1 = node.QueryConcentratedPositions(address1)
+	s.Require().Equal(len(positionsAddress1), 1)
+
 }
 
 // TestGeometricTwapMigration tests that the geometric twap record
@@ -980,8 +1029,6 @@ func (s *IntegrationTestSuite) TestExpeditedProposals() {
 // Upon swapping 1_000_000 uosmo for stake, supply changes, making uosmo less expensive.
 // As a result of the swap, twap changes to 0.5.
 func (s *IntegrationTestSuite) TestGeometricTWAP() {
-	s.T().Skip("TODO: investigate further: https://github.com/osmosis-labs/osmosis/issues/4342")
-
 	const (
 		// This pool contains 1_000_000 uosmo and 2_000_000 stake.
 		// Equals weights.
diff --git a/x/concentrated-liquidity/export_test.go b/x/concentrated-liquidity/export_test.go
index 8e1a897f9..a9ed3e489 100644
--- a/x/concentrated-liquidity/export_test.go
+++ b/x/concentrated-liquidity/export_test.go
@@ -12,7 +12,11 @@ import (
 	poolmanagertypes "github.com/osmosis-labs/osmosis/v14/x/poolmanager/types"
 )
 
-var EmptyCoins = emptyCoins
+var (
+	EmptyCoins      = emptyCoins
+	HundredFooCoins = sdk.NewDecCoin("foo", sdk.NewInt(100))
+	HundredBarCoins = sdk.NewDecCoin("bar", sdk.NewInt(100))
+)
 
 // OrderInitialPoolDenoms sets the pool denoms of a cl pool
 func OrderInitialPoolDenoms(denom0, denom1 string) (string, string, error) {
@@ -35,8 +39,8 @@ func (k Keeper) GetPoolById(ctx sdk.Context, poolId uint64) (types.ConcentratedP
 	return k.getPoolById(ctx, poolId)
 }
 
-func (k Keeper) CrossTick(ctx sdk.Context, poolId uint64, tickIndex int64) (liquidityDelta sdk.Dec, err error) {
-	return k.crossTick(ctx, poolId, tickIndex)
+func (k Keeper) CrossTick(ctx sdk.Context, poolId uint64, tickIndex int64, swapStateFeeGrowth sdk.DecCoin) (liquidityDelta sdk.Dec, err error) {
+	return k.crossTick(ctx, poolId, tickIndex, swapStateFeeGrowth)
 }
 
 func (k Keeper) GetTickInfo(ctx sdk.Context, poolId uint64, tickIndex int64) (tickInfo model.TickInfo, err error) {
@@ -144,6 +148,27 @@ func FormatPositionAccumulatorKey(poolId uint64, owner sdk.AccAddress, lowerTick
 	return formatFeePositionAccumulatorKey(poolId, owner, lowerTick, upperTick)
 }
 
+func PreparePositionAccumulator(feeAccumulator accum.AccumulatorObject, positionKey string, feeGrowthOutside sdk.DecCoins) error {
+	return preparePositionAccumulator(feeAccumulator, positionKey, feeGrowthOutside)
+}
+
+func (ss *SwapState) UpdateFeeGrowthGlobal(feeChargeTotal sdk.Dec) {
+	ss.updateFeeGrowthGlobal(feeChargeTotal)
+}
+
+// Test helpers.
+func (ss *SwapState) SetLiquidity(liquidity sdk.Dec) {
+	ss.liquidity = liquidity
+}
+
+func (ss *SwapState) SetFeeGrowthGlobal(feeGrowthGlobal sdk.Dec) {
+	ss.feeGrowthGlobal = feeGrowthGlobal
+}
+
+func (ss *SwapState) GetFeeGrowthGlobal() sdk.Dec {
+	return ss.feeGrowthGlobal
+}
+
 // incentive methods
 func (k Keeper) CreateUptimeAccumulators(ctx sdk.Context, poolId uint64) error {
 	return k.createUptimeAccumulators(ctx, poolId)
@@ -156,3 +181,23 @@ func (k Keeper) GetUptimeAccumulators(ctx sdk.Context, poolId uint64) ([]accum.A
 func GetUptimeAccumulatorName(poolId, uptimeIndex uint64) string {
 	return getUptimeAccumulatorName(poolId, uptimeIndex)
 }
+
+func (k Keeper) GetUptimeAccumulatorValues(ctx sdk.Context, poolId uint64) ([]sdk.DecCoins, error) {
+	return k.getUptimeAccumulatorValues(ctx, poolId)
+}
+
+func CalcAccruedIncentivesForAccum(ctx sdk.Context, accumUptime time.Duration, qualifyingLiquidity sdk.Dec, timeElapsed sdk.Dec, poolIncentiveRecords []types.IncentiveRecord) (sdk.DecCoins, []types.IncentiveRecord, error) {
+	return calcAccruedIncentivesForAccum(ctx, accumUptime, qualifyingLiquidity, timeElapsed, poolIncentiveRecords)
+}
+
+func (k Keeper) UpdateUptimeAccumulatorsToNow(ctx sdk.Context, poolId uint64) error {
+	return k.updateUptimeAccumulatorsToNow(ctx, poolId)
+}
+
+func (k Keeper) SetIncentiveRecord(ctx sdk.Context, incentiveRecord types.IncentiveRecord) {
+	k.setIncentiveRecord(ctx, incentiveRecord)
+}
+
+func (k Keeper) SetMultipleIncentiveRecords(ctx sdk.Context, incentiveRecords []types.IncentiveRecord) {
+	k.setMultipleIncentiveRecords(ctx, incentiveRecords)
+}
diff --git a/x/concentrated-liquidity/fees_test.go b/x/concentrated-liquidity/fees_test.go
index 68eb01fca..484d6484b 100644
--- a/x/concentrated-liquidity/fees_test.go
+++ b/x/concentrated-liquidity/fees_test.go
@@ -1,11 +1,14 @@
 package concentrated_liquidity_test
 
 import (
+	"fmt"
 	"time"
 
 	sdk "github.com/cosmos/cosmos-sdk/types"
 
+	"github.com/osmosis-labs/osmosis/osmomath"
 	"github.com/osmosis-labs/osmosis/osmoutils/accum"
+	"github.com/osmosis-labs/osmosis/v14/app/apptesting"
 	cl "github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity"
 	"github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity/internal/math"
 	clmodel "github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity/model"
@@ -13,6 +16,9 @@ import (
 	cltypes "github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity/types"
 )
 
+const NoUSDCExpected = ""
+const NoETHExpected = ""
+
 // fields used to identify a fee position.
 type positionIdentifiers struct {
 	poolId      uint64
@@ -25,6 +31,8 @@ type positionIdentifiers struct {
 var (
 	oneEth      = sdk.NewDecCoin(ETH, sdk.OneInt())
 	oneEthCoins = sdk.NewDecCoins(oneEth)
+	onlyUSDC    = [][]string{{USDC}, {USDC}, {USDC}, {USDC}}
+	onlyETH     = [][]string{{ETH}, {ETH}, {ETH}, {ETH}}
 )
 
 func (s *KeeperTestSuite) TestInitializeFeeAccumulatorPosition() {
@@ -33,6 +41,7 @@ func (s *KeeperTestSuite) TestInitializeFeeAccumulatorPosition() {
 	// For example, that positions with non-zero liquidity
 	// cannot be overriden.
 	s.SetupTest()
+	s.PrepareConcentratedPool()
 	defaultFrozenUntil := s.Ctx.BlockTime().Add(DefaultFreezeDuration)
 	defaultAccount := s.TestAccs[0]
 
@@ -64,9 +73,6 @@ func (s *KeeperTestSuite) TestInitializeFeeAccumulatorPosition() {
 
 	clKeeper := s.App.ConcentratedLiquidityKeeper
 
-	err := clKeeper.CreateFeeAccumulator(s.Ctx, defaultPoolId)
-	s.Require().NoError(err)
-
 	type initFeeAccumTest struct {
 		name       string
 		positionId positionIdentifiers
@@ -951,3 +957,237 @@ func (s *KeeperTestSuite) TestUpdateFeeAccumulatorPosition() {
 		})
 	}
 }
+
+func (s *KeeperTestSuite) TestPreparePositionAccumulator() {
+	validPositionKey := cl.FormatPositionAccumulatorKey(defaultPoolId, s.TestAccs[0], DefaultLowerTick, DefaultUpperTick)
+	invalidPositionKey := cl.FormatPositionAccumulatorKey(defaultPoolId+1, s.TestAccs[0], DefaultLowerTick, DefaultUpperTick+1)
+	tests := []struct {
+		name               string
+		poolId             uint64
+		feeGrowthOutside   sdk.DecCoins
+		invalidPositionKey bool
+		expectError        error
+	}{
+		{
+			name:             "happy path",
+			feeGrowthOutside: oneEthCoins,
+		},
+		{
+			name:               "error: non existent accumulator",
+			feeGrowthOutside:   oneEthCoins,
+			invalidPositionKey: true,
+			expectError:        accum.NoPositionError{Name: invalidPositionKey},
+		},
+	}
+
+	for _, tc := range tests {
+		tc := tc
+		s.Run(tc.name, func() {
+			// Setup test env.
+			s.SetupTest()
+			clKeeper := s.App.ConcentratedLiquidityKeeper
+			s.PrepareConcentratedPool()
+			poolFeeAccumulator, err := clKeeper.GetFeeAccumulator(s.Ctx, defaultPoolId)
+			s.Require().NoError(err)
+			positionKey := validPositionKey
+
+			// Initialize position accumulator.
+			err = poolFeeAccumulator.NewPositionCustomAcc(positionKey, sdk.OneDec(), sdk.DecCoins{}, nil)
+			s.Require().NoError(err)
+
+			// Record the initial position accumulator value.
+			positionPre, err := accum.GetPosition(poolFeeAccumulator, positionKey)
+			s.Require().NoError(err)
+
+			// If the test case requires an invalid position key, set it.
+			if tc.invalidPositionKey {
+				positionKey = invalidPositionKey
+			}
+
+			// System under test.
+			err = cl.PreparePositionAccumulator(poolFeeAccumulator, positionKey, tc.feeGrowthOutside)
+
+			if tc.expectError != nil {
+				s.Require().Error(err)
+				s.Require().ErrorIs(err, tc.expectError)
+				return
+			}
+			s.Require().NoError(err)
+
+			// Record the final position accumulator value.
+			positionPost, err := accum.GetPosition(poolFeeAccumulator, positionKey)
+			s.Require().NoError(err)
+
+			// Check that the difference between the new and old position accumulator values is equal to the fee growth outside.
+			positionAccumDelta := positionPost.InitAccumValue.Sub(positionPre.InitAccumValue)
+			s.Require().Equal(tc.feeGrowthOutside, positionAccumDelta)
+		})
+	}
+}
+
+type Positions struct {
+	numSwaps       int
+	numAccounts    int
+	numFullRange   int
+	numNarrowRange int
+	numConsecutive int
+	numOverlapping int
+}
+
+func (s *KeeperTestSuite) TestFunctionalFees() {
+	positions := Positions{
+		numSwaps:       7,
+		numAccounts:    5,
+		numFullRange:   4,
+		numNarrowRange: 3,
+		numConsecutive: 2,
+		numOverlapping: 1,
+	}
+	// Init suite.
+	s.Setup()
+
+	// Default setup only creates 3 accounts, but we need 5 for this test.
+	s.TestAccs = apptesting.CreateRandomAccounts(positions.numAccounts)
+
+	// Create a default CL pool, but with a 10 percent swap fee.
+	clPool := s.PrepareCustomConcentratedPool(s.TestAccs[0], ETH, USDC, DefaultTickSpacing, DefaultExponentAtPriceOne, sdk.MustNewDecFromStr("0.1"))
+
+	// Setup full range position across all four accounts
+	for i := 0; i < positions.numFullRange; i++ {
+		s.SetupFullRangePositionAcc(clPool.GetId(), s.TestAccs[i])
+	}
+
+	// Setup narrow range position across three of four accounts
+	for i := 0; i < positions.numNarrowRange; i++ {
+		s.SetupDefaultPositionAcc(clPool.GetId(), s.TestAccs[i])
+	}
+
+	// Setup consecutive range position (in relation to narrow range position) across two of four accounts
+	for i := 0; i < positions.numConsecutive; i++ {
+		s.SetupConsecutiveRangePositionAcc(clPool.GetId(), s.TestAccs[i])
+	}
+
+	// Setup overlapping range position (in relation to narrow range position) on one of four accounts
+	for i := 0; i < positions.numOverlapping; i++ {
+		s.SetupOverlappingRangePositionAcc(clPool.GetId(), s.TestAccs[i])
+	}
+
+	// Swap multiple times USDC for ETH, therefore increasing the spot price
+	ticksActivatedAfterEachSwap, totalFeesExpected := s.swapAndTrackXTimesInARow(clPool.GetId(), DefaultCoin1, ETH, cltypes.MaxSpotPrice, positions.numSwaps)
+	s.CollectAndAssertFees(s.Ctx, clPool.GetId(), totalFeesExpected, [][]sdk.Int{ticksActivatedAfterEachSwap}, onlyUSDC, positions)
+
+	// Swap multiple times ETH for USDC, therefore decreasing the spot price
+	ticksActivatedAfterEachSwap, totalFeesExpected = s.swapAndTrackXTimesInARow(clPool.GetId(), DefaultCoin0, USDC, cltypes.MinSpotPrice, positions.numSwaps)
+	s.CollectAndAssertFees(s.Ctx, clPool.GetId(), totalFeesExpected, [][]sdk.Int{ticksActivatedAfterEachSwap}, onlyETH, positions)
+
+	// Do the same swaps as before, however this time we collect fees after both swap directions are complete.
+	ticksActivatedAfterEachSwapUp, totalFeesExpectedUp := s.swapAndTrackXTimesInARow(clPool.GetId(), DefaultCoin1, ETH, cltypes.MaxSpotPrice, positions.numSwaps)
+	ticksActivatedAfterEachSwapDown, totalFeesExpectedDown := s.swapAndTrackXTimesInARow(clPool.GetId(), DefaultCoin0, USDC, cltypes.MinSpotPrice, positions.numSwaps)
+	totalFeesExpected = totalFeesExpectedUp.Add(totalFeesExpectedDown...)
+
+	// We expect all positions to have both denoms in their fee accumulators except USDC for the overlapping range position since
+	// it was not activated during the USDC -> ETH swap direction but was activated during the ETH -> USDC swap direction.
+	ticksActivatedAfterEachSwapTest := [][]sdk.Int{ticksActivatedAfterEachSwapUp, ticksActivatedAfterEachSwapDown}
+	denomsExpected := [][]string{{USDC, ETH}, {USDC, ETH}, {USDC, ETH}, {NoUSDCExpected, ETH}}
+
+	s.CollectAndAssertFees(s.Ctx, clPool.GetId(), totalFeesExpected, ticksActivatedAfterEachSwapTest, denomsExpected, positions)
+}
+
+// CollectAndAssertFees collects fees from a given pool for all positions and verifies that the total fees collected match the expected total fees.
+// The method also checks that if the ticks that were active during the swap lie within the range of a position, then the position's fee accumulators
+// are not empty. The total fees collected are compared to the expected total fees within an additive tolerance defined by an error tolerance struct.
+func (s *KeeperTestSuite) CollectAndAssertFees(ctx sdk.Context, poolId uint64, totalFees sdk.Coins, activeTicks [][]sdk.Int, expectedFeeDenoms [][]string, positions Positions) {
+	var totalFeesCollected sdk.Coins
+	// Claim full range position fees across all four accounts
+	for i := 0; i < positions.numFullRange; i++ {
+		totalFeesCollected = s.collectFeesAndCheckInvariance(ctx, poolId, i, DefaultMinTick, DefaultMaxTick, totalFeesCollected, expectedFeeDenoms[0], activeTicks)
+	}
+
+	// Claim narrow range position fees across three of four accounts
+	for i := 0; i < positions.numNarrowRange; i++ {
+		totalFeesCollected = s.collectFeesAndCheckInvariance(ctx, poolId, i, DefaultLowerTick, DefaultUpperTick, totalFeesCollected, expectedFeeDenoms[1], activeTicks)
+	}
+
+	// Claim consecutive range position fees across two of four accounts
+	for i := 0; i < positions.numConsecutive; i++ {
+		totalFeesCollected = s.collectFeesAndCheckInvariance(ctx, poolId, i, DefaultExponentConsecutivePositionLowerTick.Int64(), DefaultExponentConsecutivePositionUpperTick.Int64(), totalFeesCollected, expectedFeeDenoms[2], activeTicks)
+	}
+
+	// Claim overlapping range position fees on one of four accounts
+	for i := 0; i < positions.numOverlapping; i++ {
+		totalFeesCollected = s.collectFeesAndCheckInvariance(ctx, poolId, i, DefaultExponentOverlappingPositionLowerTick.Int64(), DefaultExponentOverlappingPositionUpperTick.Int64(), totalFeesCollected, expectedFeeDenoms[3], activeTicks)
+	}
+
+	// Define error tolerance
+	var errTolerance osmomath.ErrTolerance
+	errTolerance.AdditiveTolerance = sdk.NewDec(10)
+	errTolerance.RoundingDir = osmomath.RoundDown
+
+	// Check that the total fees collected is equal to the total fees (within a tolerance)
+	for _, coin := range totalFeesCollected {
+		expected := totalFees.AmountOf(coin.Denom)
+		actual := coin.Amount
+		s.Require().Equal(0, errTolerance.Compare(expected, actual), fmt.Sprintf("expected (%s), actual (%s)", expected, actual))
+	}
+}
+
+// tickStatusInvariance tests if the swap position was active during the given tick range and
+// checks that the fees collected are non-zero if the position was active, or zero otherwise.
+func (s *KeeperTestSuite) tickStatusInvariance(ticksActivatedAfterEachSwap [][]sdk.Int, lowerTick, upperTick int64, coins sdk.Coins, expectedFeeDenoms []string) {
+	var positionWasActive bool
+	// Check if the position was active during the swap
+	for i, ticks := range ticksActivatedAfterEachSwap {
+		for _, tick := range ticks {
+			if tick.GTE(sdk.NewInt(lowerTick)) && tick.LTE(sdk.NewInt(upperTick)) {
+				positionWasActive = true
+				break
+			}
+		}
+		if positionWasActive {
+			// If the position was active, check that the fees collected are non-zero
+			if expectedFeeDenoms[i] != NoUSDCExpected && expectedFeeDenoms[i] != NoETHExpected {
+				s.Require().True(coins.AmountOf(expectedFeeDenoms[i]).GT(sdk.ZeroInt()))
+			}
+
+		} else {
+			// If the position was not active, check that the fees collected are zero
+			s.Require().Nil(coins)
+		}
+	}
+}
+
+// swapAndTrackXTimesInARow performs `numSwaps` swaps and tracks the tick activated after each swap.
+// It also returns the total fees collected.
+func (s *KeeperTestSuite) swapAndTrackXTimesInARow(poolId uint64, coinIn sdk.Coin, coinOutDenom string, priceLimit sdk.Dec, numSwaps int) (ticksActivatedAfterEachSwap []sdk.Int, totalFees sdk.Coins) {
+	// Retrieve pool
+	clPool, err := s.App.ConcentratedLiquidityKeeper.GetPoolById(s.Ctx, poolId)
+	s.Require().NoError(err)
+
+	// Determine amount needed to fulfill swap numSwaps times and fund account that much
+	amountNeededForSwap := coinIn.Amount.Mul(sdk.NewInt(int64(numSwaps)))
+	swapCoin := sdk.NewCoin(coinIn.Denom, amountNeededForSwap)
+	s.FundAcc(s.TestAccs[4], sdk.NewCoins(swapCoin))
+
+	ticksActivatedAfterEachSwap = append(ticksActivatedAfterEachSwap, clPool.GetCurrentTick())
+	totalFees = sdk.NewCoins(sdk.NewCoin(USDC, sdk.ZeroInt()), sdk.NewCoin(ETH, sdk.ZeroInt()))
+	// Swap numSwaps times, recording the tick activated after and swap and fees we expect to collect based on the amount in
+	for i := 0; i < numSwaps; i++ {
+		coinIn, _, _, _, _, err := s.App.ConcentratedLiquidityKeeper.SwapOutAmtGivenIn(s.Ctx, coinIn, coinOutDenom, clPool.GetSwapFee(s.Ctx), priceLimit, clPool.GetId())
+		s.Require().NoError(err)
+		fee := coinIn.Amount.ToDec().Mul(clPool.GetSwapFee(s.Ctx))
+		totalFees = totalFees.Add(sdk.NewCoin(coinIn.Denom, fee.TruncateInt()))
+		clPool, err = s.App.ConcentratedLiquidityKeeper.GetPoolById(s.Ctx, clPool.GetId())
+		s.Require().NoError(err)
+		ticksActivatedAfterEachSwap = append(ticksActivatedAfterEachSwap, clPool.GetCurrentTick())
+	}
+	return ticksActivatedAfterEachSwap, totalFees
+}
+
+// collectFeesAndCheckInvariance collects fees from the concentrated liquidity pool and checks the resulting tick status invariance.
+func (s *KeeperTestSuite) collectFeesAndCheckInvariance(ctx sdk.Context, poolId uint64, accountIndex int, minTick, maxTick int64, feesCollected sdk.Coins, expectedFeeDenoms []string, activeTicks [][]sdk.Int) (totalFeesCollected sdk.Coins) {
+	coins, err := s.App.ConcentratedLiquidityKeeper.CollectFees(ctx, poolId, s.TestAccs[accountIndex], minTick, maxTick)
+	s.Require().NoError(err)
+	totalFeesCollected = feesCollected.Add(coins...)
+	s.tickStatusInvariance(activeTicks, minTick, maxTick, coins, expectedFeeDenoms)
+	return totalFeesCollected
+}
diff --git a/x/concentrated-liquidity/incentives_test.go b/x/concentrated-liquidity/incentives_test.go
index 28f26dd87..f440fe1db 100644
--- a/x/concentrated-liquidity/incentives_test.go
+++ b/x/concentrated-liquidity/incentives_test.go
@@ -1,23 +1,149 @@
 package concentrated_liquidity_test
 
 import (
+	"time"
+
 	sdk "github.com/cosmos/cosmos-sdk/types"
 
 	cl "github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity"
+	"github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity/model"
 	"github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity/types"
 )
 
-var defaultPoolId = uint64(1)
+var (
+	defaultPoolId = uint64(1)
+
+	testAddressOne   = sdk.AccAddress([]byte("addr1_______________"))
+	testAddressTwo   = sdk.AccAddress([]byte("addr2_______________"))
+	testAddressThree = sdk.AccAddress([]byte("addr3_______________"))
+
+	testAccumOne = "testAccumOne"
+
+	// Note: lexicographic order is denomFour, denomOne, denomThree, denomTwo
+	testDenomOne   = "denomOne"
+	testDenomTwo   = "denomTwo"
+	testDenomThree = "denomThree"
+	testDenomFour  = "denomFour"
+
+	defaultIncentiveAmount = sdk.NewDec(2 << 60)
+
+	testEmissionOne   = sdk.MustNewDecFromStr("0.000001")
+	testEmissionTwo   = sdk.MustNewDecFromStr("0.0783")
+	testEmissionThree = sdk.MustNewDecFromStr("165.4")
+	testEmissionFour  = sdk.MustNewDecFromStr("57.93")
+
+	defaultBlockTime  = time.Unix(0, 0).UTC()
+	defaultTimeBuffer = time.Hour
+	defaultStartTime  = defaultBlockTime.Add(defaultTimeBuffer)
+
+	testUptimeOne   = types.SupportedUptimes[0]
+	testUptimeTwo   = types.SupportedUptimes[1]
+	testUptimeThree = types.SupportedUptimes[2]
+	testUptimeFour  = types.SupportedUptimes[3]
+
+	incentiveRecordOne = types.IncentiveRecord{
+		PoolId:          validPoolId,
+		IncentiveDenom:  testDenomOne,
+		RemainingAmount: defaultIncentiveAmount,
+		EmissionRate:    testEmissionOne,
+		StartTime:       defaultStartTime,
+		MinUptime:       testUptimeOne,
+	}
+
+	incentiveRecordTwo = types.IncentiveRecord{
+		PoolId:          validPoolId,
+		IncentiveDenom:  testDenomTwo,
+		RemainingAmount: defaultIncentiveAmount,
+		EmissionRate:    testEmissionTwo,
+		StartTime:       defaultStartTime,
+		MinUptime:       testUptimeTwo,
+	}
+
+	incentiveRecordThree = types.IncentiveRecord{
+		PoolId:          validPoolId,
+		IncentiveDenom:  testDenomThree,
+		RemainingAmount: defaultIncentiveAmount,
+		EmissionRate:    testEmissionThree,
+		StartTime:       defaultStartTime,
+		MinUptime:       testUptimeThree,
+	}
+
+	incentiveRecordFour = types.IncentiveRecord{
+		PoolId:          validPoolId,
+		IncentiveDenom:  testDenomFour,
+		RemainingAmount: defaultIncentiveAmount,
+		EmissionRate:    testEmissionFour,
+		StartTime:       defaultStartTime,
+		MinUptime:       testUptimeFour,
+	}
+
+	testQualifyingDepositsOne   = sdk.NewInt(50)
+	testQualifyingDepositsTwo   = sdk.NewInt(100)
+	testQualifyingDepositsThree = sdk.NewInt(399)
+)
+
+type ExpectedUptimes struct {
+	emptyExpectedAccumValues   []sdk.DecCoins
+	hundredTokensSingleDenom   []sdk.DecCoins
+	hundredTokensMultiDenom    []sdk.DecCoins
+	twoHundredTokensMultiDenom []sdk.DecCoins
+	varyingTokensSingleDenom   []sdk.DecCoins
+	varyingTokensMultiDenom    []sdk.DecCoins
+}
+
+// getExpectedUptimes returns a base set of expected values for testing based on the number
+// of supported uptimes at runtime. This abstraction exists only to ensure backwards-compatibility
+// of incentives-related tests if the supported uptimes are ever changed.
+func getExpectedUptimes() ExpectedUptimes {
+	expUptimes := ExpectedUptimes{
+		emptyExpectedAccumValues:   []sdk.DecCoins{},
+		hundredTokensSingleDenom:   []sdk.DecCoins{},
+		hundredTokensMultiDenom:    []sdk.DecCoins{},
+		twoHundredTokensMultiDenom: []sdk.DecCoins{},
+		varyingTokensSingleDenom:   []sdk.DecCoins{},
+		varyingTokensMultiDenom:    []sdk.DecCoins{},
+	}
+	for i := range types.SupportedUptimes {
+		expUptimes.emptyExpectedAccumValues = append(expUptimes.emptyExpectedAccumValues, cl.EmptyCoins)
+		expUptimes.hundredTokensSingleDenom = append(expUptimes.hundredTokensSingleDenom, sdk.NewDecCoins(cl.HundredFooCoins))
+		expUptimes.hundredTokensMultiDenom = append(expUptimes.hundredTokensMultiDenom, sdk.NewDecCoins(cl.HundredFooCoins, cl.HundredBarCoins))
+		expUptimes.twoHundredTokensMultiDenom = append(expUptimes.twoHundredTokensMultiDenom, sdk.NewDecCoins(cl.HundredFooCoins.Add(cl.HundredFooCoins), cl.HundredBarCoins.Add(cl.HundredBarCoins)))
+		expUptimes.varyingTokensSingleDenom = append(expUptimes.varyingTokensSingleDenom, sdk.NewDecCoins(cl.HundredFooCoins.Add(sdk.NewDecCoin("foo", sdk.NewInt(int64(i))))))
+		expUptimes.varyingTokensMultiDenom = append(expUptimes.varyingTokensMultiDenom, sdk.NewDecCoins(cl.HundredFooCoins.Add(sdk.NewDecCoin("foo", sdk.NewInt(int64(i)))), cl.HundredBarCoins.Add(sdk.NewDecCoin("bar", sdk.NewInt(int64(i*3))))))
+	}
+
+	return expUptimes
+}
+
+// Helper for converting raw DecCoins accum values to pool proto compatible UptimeTrackers
+func wrapUptimeTrackers(accumValues []sdk.DecCoins) []model.UptimeTracker {
+	wrappedUptimeTrackers := []model.UptimeTracker{}
+	for _, accumValue := range accumValues {
+		wrappedUptimeTrackers = append(wrappedUptimeTrackers, model.UptimeTracker{accumValue})
+	}
+
+	return wrappedUptimeTrackers
+}
+
+func expectedIncentives(denom string, rate sdk.Dec, timeElapsed time.Duration, qualifyingLiquidity sdk.Dec) sdk.DecCoin {
+	timeInSec := sdk.NewDec(int64(timeElapsed)).Quo(sdk.MustNewDecFromStr("1000000000"))
+	amount := rate.Mul(timeInSec).QuoTruncate(qualifyingLiquidity)
+
+	return sdk.NewDecCoinFromDec(denom, amount)
+}
+
+func chargeIncentive(incentiveRecord types.IncentiveRecord, timeElapsed time.Duration) types.IncentiveRecord {
+	incentivesEmitted := incentiveRecord.EmissionRate.Mul(sdk.NewDec(int64(timeElapsed)).Quo(sdk.MustNewDecFromStr("1000000000")))
+	incentiveRecord.RemainingAmount = incentiveRecord.RemainingAmount.Sub(incentivesEmitted)
+
+	return incentiveRecord
+}
 
 func (s *KeeperTestSuite) TestCreateAndGetUptimeAccumulators() {
 	// We expect there to be len(types.SupportedUptimes) number of initialized accumulators
 	// for a successful pool creation. We calculate this upfront to ensure test compatibility
 	// if the uptimes we support ever change.
-	curExpectedAccumValues := []sdk.DecCoins{}
-	for range types.SupportedUptimes {
-		curExpectedAccumValues = append(curExpectedAccumValues, cl.EmptyCoins)
-	}
-	s.Require().Equal(len(types.SupportedUptimes), len(curExpectedAccumValues))
+	expectedUptimes := getExpectedUptimes()
 
 	type initUptimeAccumTest struct {
 		poolId              uint64
@@ -30,13 +156,13 @@ func (s *KeeperTestSuite) TestCreateAndGetUptimeAccumulators() {
 		"default pool setup": {
 			poolId:              defaultPoolId,
 			initializePoolAccum: true,
-			expectedAccumValues: curExpectedAccumValues,
+			expectedAccumValues: expectedUptimes.emptyExpectedAccumValues,
 			expectedPass:        true,
 		},
 		"setup with different poolId": {
 			poolId:              defaultPoolId + 1,
 			initializePoolAccum: true,
-			expectedAccumValues: curExpectedAccumValues,
+			expectedAccumValues: expectedUptimes.emptyExpectedAccumValues,
 			expectedPass:        true,
 		},
 		"pool not initialized": {
@@ -117,3 +243,432 @@ func (s *KeeperTestSuite) TestGetUptimeAccumulatorName() {
 		})
 	}
 }
+
+func (s *KeeperTestSuite) TestCreateAndGetUptimeAccumulatorValues() {
+	// We expect there to be len(types.SupportedUptimes) number of initialized accumulators
+	// for a successful pool creation.
+	// We re-calculate these values each time to ensure test compatibility if the uptimes
+	// we support ever change.
+	expectedUptimes := getExpectedUptimes()
+
+	type initUptimeAccumTest struct {
+		poolId               uint64
+		initializePoolAccums bool
+		addedAccumValues     []sdk.DecCoins
+		numTimesAdded        int
+		expectedAccumValues  []sdk.DecCoins
+
+		expectedPass bool
+	}
+	tests := map[string]initUptimeAccumTest{
+		"hundred of a single denom in each accumulator added once": {
+			poolId:               defaultPoolId,
+			initializePoolAccums: true,
+			addedAccumValues:     expectedUptimes.hundredTokensSingleDenom,
+			numTimesAdded:        1,
+			expectedAccumValues:  expectedUptimes.hundredTokensSingleDenom,
+			expectedPass:         true,
+		},
+		"hundred of multiple denom in each accumulator added once": {
+			poolId:               defaultPoolId,
+			initializePoolAccums: true,
+			addedAccumValues:     expectedUptimes.hundredTokensMultiDenom,
+			numTimesAdded:        1,
+			expectedAccumValues:  expectedUptimes.hundredTokensMultiDenom,
+			expectedPass:         true,
+		},
+		"varying amounts of single denom in each accumulator added once": {
+			poolId:               defaultPoolId,
+			initializePoolAccums: true,
+			addedAccumValues:     expectedUptimes.varyingTokensSingleDenom,
+			numTimesAdded:        1,
+			expectedAccumValues:  expectedUptimes.varyingTokensSingleDenom,
+			expectedPass:         true,
+		},
+		"varying of multiple denoms in each accumulator added once": {
+			poolId:               defaultPoolId,
+			initializePoolAccums: true,
+			addedAccumValues:     expectedUptimes.varyingTokensMultiDenom,
+			numTimesAdded:        1,
+			expectedAccumValues:  expectedUptimes.varyingTokensMultiDenom,
+			expectedPass:         true,
+		},
+		"hundred of multiple denom in each accumulator added twice": {
+			poolId:               defaultPoolId,
+			initializePoolAccums: true,
+			addedAccumValues:     expectedUptimes.hundredTokensMultiDenom,
+			numTimesAdded:        2,
+			expectedAccumValues:  expectedUptimes.twoHundredTokensMultiDenom,
+			expectedPass:         true,
+		},
+		"setup with different poolId": {
+			poolId:               defaultPoolId + 1,
+			initializePoolAccums: true,
+			addedAccumValues:     expectedUptimes.hundredTokensSingleDenom,
+			numTimesAdded:        1,
+			expectedAccumValues:  expectedUptimes.hundredTokensSingleDenom,
+			expectedPass:         true,
+		},
+		"pool not initialized": {
+			initializePoolAccums: false,
+			poolId:               defaultPoolId,
+			addedAccumValues:     expectedUptimes.hundredTokensSingleDenom,
+			numTimesAdded:        1,
+			expectedAccumValues:  []sdk.DecCoins{},
+			expectedPass:         false,
+		},
+	}
+
+	for name, tc := range tests {
+		tc := tc
+		s.Run(name, func() {
+			s.SetupTest()
+			clKeeper := s.App.ConcentratedLiquidityKeeper
+
+			// system under test
+			var err error
+			if tc.initializePoolAccums {
+				err = clKeeper.CreateUptimeAccumulators(s.Ctx, tc.poolId)
+				s.Require().NoError(err)
+
+				poolUptimeAccumulators, err := clKeeper.GetUptimeAccumulators(s.Ctx, tc.poolId)
+				s.Require().NoError(err)
+
+				for i := 0; i < tc.numTimesAdded; i++ {
+					for uptimeId, uptimeAccum := range poolUptimeAccumulators {
+						uptimeAccum.AddToAccumulator(tc.addedAccumValues[uptimeId])
+					}
+					poolUptimeAccumulators, err = clKeeper.GetUptimeAccumulators(s.Ctx, tc.poolId)
+					s.Require().NoError(err)
+				}
+			}
+			poolUptimeAccumulatorValues, err := clKeeper.GetUptimeAccumulatorValues(s.Ctx, tc.poolId)
+
+			if tc.expectedPass {
+				s.Require().NoError(err)
+
+				// ensure number of uptime accumulators match supported uptimes
+				s.Require().Equal(len(tc.expectedAccumValues), len(poolUptimeAccumulatorValues))
+
+				// ensure that each uptime was initialized to the correct value (sdk.DecCoins(nil))
+				s.Require().Equal(tc.expectedAccumValues, poolUptimeAccumulatorValues)
+			} else {
+				s.Require().Error(err)
+
+				// ensure no accumulators exist for an uninitialized pool
+				s.Require().Equal(0, len(poolUptimeAccumulatorValues))
+			}
+		})
+	}
+}
+
+func (s *KeeperTestSuite) TestCalcAccruedIncentivesForAccum() {
+	type calcAccruedIncentivesTest struct {
+		poolId               uint64
+		accumUptime          time.Duration
+		qualifyingLiquidity  sdk.Dec
+		timeElapsed          time.Duration
+		poolIncentiveRecords []types.IncentiveRecord
+
+		expectedResult           sdk.DecCoins
+		expectedIncentiveRecords []types.IncentiveRecord
+		expectedPass             bool
+	}
+	tests := map[string]calcAccruedIncentivesTest{
+		"one incentive record, one qualifying for incentives": {
+			poolId:               defaultPoolId,
+			accumUptime:          types.SupportedUptimes[0],
+			qualifyingLiquidity:  sdk.NewDec(100),
+			timeElapsed:          time.Hour,
+			poolIncentiveRecords: []types.IncentiveRecord{incentiveRecordOne},
+
+			expectedResult: sdk.DecCoins{
+				expectedIncentives(incentiveRecordOne.IncentiveDenom, incentiveRecordOne.EmissionRate, time.Hour, sdk.NewDec(100)),
+			},
+			expectedIncentiveRecords: []types.IncentiveRecord{chargeIncentive(incentiveRecordOne, time.Hour)},
+			expectedPass:             true,
+		},
+		"two incentive records, one qualifying for incentives": {
+			poolId:               defaultPoolId,
+			accumUptime:          types.SupportedUptimes[0],
+			qualifyingLiquidity:  sdk.NewDec(100),
+			timeElapsed:          time.Hour,
+			poolIncentiveRecords: []types.IncentiveRecord{incentiveRecordOne, incentiveRecordTwo},
+
+			expectedResult: sdk.DecCoins{
+				// We only expect the first incentive record to qualify
+				expectedIncentives(incentiveRecordOne.IncentiveDenom, incentiveRecordOne.EmissionRate, time.Hour, sdk.NewDec(100)),
+			},
+			expectedIncentiveRecords: []types.IncentiveRecord{
+				// We only charge the first incentive record since the second wasn't affected
+				chargeIncentive(incentiveRecordOne, time.Hour),
+				incentiveRecordTwo,
+			},
+			expectedPass: true,
+		},
+
+		// error catching
+		"zero qualifying liquidity": {
+			poolId:               defaultPoolId,
+			accumUptime:          types.SupportedUptimes[0],
+			qualifyingLiquidity:  sdk.NewDec(0),
+			timeElapsed:          time.Hour,
+			poolIncentiveRecords: []types.IncentiveRecord{incentiveRecordOne},
+
+			expectedResult:           sdk.DecCoins{},
+			expectedIncentiveRecords: []types.IncentiveRecord{},
+			expectedPass:             false,
+		},
+		"zero time elapsed": {
+			poolId:               defaultPoolId,
+			accumUptime:          types.SupportedUptimes[0],
+			qualifyingLiquidity:  sdk.NewDec(100),
+			timeElapsed:          time.Duration(0),
+			poolIncentiveRecords: []types.IncentiveRecord{incentiveRecordOne},
+
+			expectedResult:           sdk.DecCoins{},
+			expectedIncentiveRecords: []types.IncentiveRecord{},
+			expectedPass:             false,
+		},
+	}
+
+	for name, tc := range tests {
+		tc := tc
+		s.Run(name, func() {
+			s.SetupTest()
+			s.Ctx = s.Ctx.WithBlockTime(defaultStartTime.Add(tc.timeElapsed))
+
+			s.PrepareConcentratedPool()
+
+			// system under test
+			actualResult, updatedPoolRecords, err := cl.CalcAccruedIncentivesForAccum(s.Ctx, tc.accumUptime, tc.qualifyingLiquidity, sdk.NewDec(int64(tc.timeElapsed)).Quo(sdk.MustNewDecFromStr("1000000000")), tc.poolIncentiveRecords)
+
+			if tc.expectedPass {
+				s.Require().NoError(err)
+
+				s.Require().Equal(tc.expectedResult, actualResult)
+				s.Require().Equal(tc.expectedIncentiveRecords, updatedPoolRecords)
+			} else {
+				s.Require().Error(err)
+			}
+		})
+	}
+}
+
+func (s *KeeperTestSuite) TestUpdateUptimeAccumulatorsToNow() {
+	supportedUptimes := types.SupportedUptimes
+
+	type updateAccumToNow struct {
+		poolId               uint64
+		accumUptime          time.Duration
+		qualifyingLiquidity  sdk.Dec
+		timeElapsed          time.Duration
+		poolIncentiveRecords []types.IncentiveRecord
+
+		expectedResult           sdk.DecCoins
+		expectedUptimeDeltas     []sdk.DecCoins
+		expectedIncentiveRecords []types.IncentiveRecord
+		expectedPass             bool
+	}
+	tests := map[string]updateAccumToNow{
+		"one incentive record": {
+			poolId:               defaultPoolId,
+			timeElapsed:          time.Hour,
+			poolIncentiveRecords: []types.IncentiveRecord{incentiveRecordOne},
+
+			expectedIncentiveRecords: []types.IncentiveRecord{
+				// We deduct incentives from the record for the period it emitted incentives
+				chargeIncentive(incentiveRecordOne, time.Hour),
+			},
+			expectedPass: true,
+		},
+		"two incentive records, each with qualifying liquidity": {
+			poolId:               defaultPoolId,
+			timeElapsed:          time.Hour,
+			poolIncentiveRecords: []types.IncentiveRecord{incentiveRecordOne, incentiveRecordTwo},
+
+			expectedIncentiveRecords: []types.IncentiveRecord{
+				// We deduct incentives from both records since there are positions for each
+				chargeIncentive(incentiveRecordOne, time.Hour),
+				chargeIncentive(incentiveRecordTwo, time.Hour),
+			},
+			expectedPass: true,
+		},
+		"three incentive records, each with qualifying liquidity": {
+			poolId:               defaultPoolId,
+			timeElapsed:          time.Hour,
+			poolIncentiveRecords: []types.IncentiveRecord{incentiveRecordOne, incentiveRecordTwo, incentiveRecordThree},
+
+			expectedIncentiveRecords: []types.IncentiveRecord{
+				// We deduct incentives from each record since there are positions for all three
+				// Note that records are ordered lexicographically by denom in state
+				chargeIncentive(incentiveRecordOne, time.Hour),
+				chargeIncentive(incentiveRecordThree, time.Hour),
+				chargeIncentive(incentiveRecordTwo, time.Hour),
+			},
+			expectedPass: true,
+		},
+		"two incentive records, only one with qualifying liquidity": {
+			poolId:               defaultPoolId,
+			timeElapsed:          time.Hour,
+			poolIncentiveRecords: []types.IncentiveRecord{incentiveRecordOne, incentiveRecordTwo, incentiveRecordThree, incentiveRecordFour},
+
+			expectedIncentiveRecords: []types.IncentiveRecord{
+				// We only deduct from the first three incentive records since the last doesn't emit anything
+				// Note that records are ordered lexicographically by denom in state
+				incentiveRecordFour,
+				chargeIncentive(incentiveRecordOne, time.Hour),
+				chargeIncentive(incentiveRecordThree, time.Hour),
+				chargeIncentive(incentiveRecordTwo, time.Hour),
+			},
+			expectedPass: true,
+		},
+	}
+
+	for name, tc := range tests {
+		tc := tc
+		s.Run(name, func() {
+			s.SetupTest()
+			clKeeper := s.App.ConcentratedLiquidityKeeper
+			s.Ctx = s.Ctx.WithBlockTime(defaultStartTime)
+
+			// Set up test pool
+			clPool := s.PrepareConcentratedPool()
+
+			// Initialize test incentives on the pool
+			clKeeper.SetMultipleIncentiveRecords(s.Ctx, tc.poolIncentiveRecords)
+
+			// Get initial uptime accum values for comparison
+			initUptimeAccumValues, err := clKeeper.GetUptimeAccumulatorValues(s.Ctx, tc.poolId)
+			s.Require().NoError(err)
+
+			// Add qualifying and non-qualifying liquidity to the pool
+			s.FundAcc(testAddressOne, sdk.NewCoins(sdk.NewCoin(clPool.GetToken0(), testQualifyingDepositsOne), sdk.NewCoin(clPool.GetToken1(), testQualifyingDepositsOne)))
+			s.FundAcc(testAddressTwo, sdk.NewCoins(sdk.NewCoin(clPool.GetToken0(), testQualifyingDepositsTwo), sdk.NewCoin(clPool.GetToken1(), testQualifyingDepositsTwo)))
+			s.FundAcc(testAddressThree, sdk.NewCoins(sdk.NewCoin(clPool.GetToken0(), testQualifyingDepositsThree), sdk.NewCoin(clPool.GetToken1(), testQualifyingDepositsThree)))
+
+			_, _, qualifyingLiquidityUptimeOne, err := clKeeper.CreatePosition(s.Ctx, tc.poolId, testAddressOne, testQualifyingDepositsOne, testQualifyingDepositsOne, sdk.ZeroInt(), sdk.ZeroInt(), clPool.GetCurrentTick().Int64()-1, clPool.GetCurrentTick().Int64()+1, defaultStartTime.Add(supportedUptimes[0]))
+			s.Require().NoError(err)
+
+			_, _, qualifyingLiquidityUptimeTwo, err := clKeeper.CreatePosition(s.Ctx, tc.poolId, testAddressTwo, testQualifyingDepositsTwo, testQualifyingDepositsTwo, sdk.ZeroInt(), sdk.ZeroInt(), clPool.GetCurrentTick().Int64()-1, clPool.GetCurrentTick().Int64()+1, defaultStartTime.Add(supportedUptimes[1]))
+			s.Require().NoError(err)
+
+			_, _, qualifyingLiquidityUptimeThree, err := clKeeper.CreatePosition(s.Ctx, tc.poolId, testAddressThree, testQualifyingDepositsThree, testQualifyingDepositsThree, sdk.ZeroInt(), sdk.ZeroInt(), clPool.GetCurrentTick().Int64()-1, clPool.GetCurrentTick().Int64()+1, defaultStartTime.Add(supportedUptimes[2]))
+			s.Require().NoError(err)
+
+			// Note that the third position (1D freeze) qualifies for all three uptimes, the second position qualifies for the first two,
+			// and the first position only qualifies for the first. None of the positions qualify for any later uptimes (e.g. 1W)
+			qualifyingLiquidities := []sdk.Dec{
+				qualifyingLiquidityUptimeOne.Add(qualifyingLiquidityUptimeTwo).Add(qualifyingLiquidityUptimeThree),
+				qualifyingLiquidityUptimeTwo.Add(qualifyingLiquidityUptimeThree),
+				qualifyingLiquidityUptimeThree,
+			}
+
+			// Let `timeElapsed` time pass
+			s.Ctx = s.Ctx.WithBlockTime(defaultStartTime.Add(tc.timeElapsed))
+
+			// system under test
+			err = clKeeper.UpdateUptimeAccumulatorsToNow(s.Ctx, tc.poolId)
+
+			if tc.expectedPass {
+				s.Require().NoError(err)
+
+				// Get updated pool for testing purposes
+				clPool, err := clKeeper.GetPoolById(s.Ctx, tc.poolId)
+				s.Require().NoError(err)
+
+				// Get new uptime accum values for comparison
+				newUptimeAccumValues, err := clKeeper.GetUptimeAccumulatorValues(s.Ctx, tc.poolId)
+				s.Require().NoError(err)
+
+				// Calculate expected uptime deltas using qualifying liquidity deltas (eh can only test one incentive?)
+				expectedUptimeDeltas := []sdk.DecCoins{}
+				for uptimeIndex := range newUptimeAccumValues {
+					if uptimeIndex < len(tc.poolIncentiveRecords) && uptimeIndex < len(qualifyingLiquidities) {
+						expectedUptimeDeltas = append(expectedUptimeDeltas, sdk.NewDecCoins(expectedIncentives(tc.poolIncentiveRecords[uptimeIndex].IncentiveDenom, tc.poolIncentiveRecords[uptimeIndex].EmissionRate, time.Hour, qualifyingLiquidities[uptimeIndex])))
+					} else {
+						expectedUptimeDeltas = append(expectedUptimeDeltas, cl.EmptyCoins)
+					}
+				}
+
+				// Ensure that each accumulator value changes by the correct amount
+				for uptimeIndex := range newUptimeAccumValues {
+					uptimeDelta := newUptimeAccumValues[uptimeIndex].Sub(initUptimeAccumValues[uptimeIndex])
+					s.Require().Equal(expectedUptimeDeltas[uptimeIndex], uptimeDelta)
+				}
+
+				// Ensure that LastLiquidityUpdate field is updated for pool
+				s.Require().Equal(s.Ctx.BlockTime(), clPool.GetLastLiquidityUpdate())
+				// Ensure that pool's IncentiveRecords are updated to reflect emitted incentives
+				updatedIncentiveRecords, err := clKeeper.GetAllIncentiveRecordsForPool(s.Ctx, tc.poolId)
+				s.Require().NoError(err)
+				s.Require().Equal(tc.expectedIncentiveRecords, updatedIncentiveRecords)
+			} else {
+				s.Require().Error(err)
+			}
+		})
+	}
+}
+
+// Note: we test that incentive records are properly deducted by emissions in `TestUpdateUptimeAccumulatorsToNow` above.
+// This test aims to cover the behavior of a series of state read/writes relating to incentive records.
+func (s *KeeperTestSuite) TestIncentiveRecordsSetAndGet() {
+	s.SetupTest()
+	clKeeper := s.App.ConcentratedLiquidityKeeper
+	s.Ctx = s.Ctx.WithBlockTime(defaultStartTime)
+	emptyIncentiveRecords := []types.IncentiveRecord{}
+
+	// Set up test pool
+	clPoolOne := s.PrepareConcentratedPool()
+
+	// Set up second pool for reference
+	clPoolTwo := s.PrepareConcentratedPool()
+
+	// Ensure both pools start with no incentive records
+	poolOneRecords, err := clKeeper.GetAllIncentiveRecordsForPool(s.Ctx, clPoolOne.GetId())
+	s.Require().NoError(err)
+	s.Require().Equal(emptyIncentiveRecords, poolOneRecords)
+
+	poolTwoRecords, err := clKeeper.GetAllIncentiveRecordsForPool(s.Ctx, clPoolTwo.GetId())
+	s.Require().NoError(err)
+	s.Require().Equal(emptyIncentiveRecords, poolTwoRecords)
+
+	// Ensure setting and getting a single record works with single Get and GetAll
+	clKeeper.SetIncentiveRecord(s.Ctx, incentiveRecordOne)
+	poolOneRecord, err := clKeeper.GetIncentiveRecord(s.Ctx, clPoolOne.GetId(), incentiveRecordOne.IncentiveDenom, incentiveRecordOne.MinUptime)
+	s.Require().NoError(err)
+	s.Require().Equal(incentiveRecordOne, poolOneRecord)
+	allRecordsPoolOne, err := clKeeper.GetAllIncentiveRecordsForPool(s.Ctx, clPoolOne.GetId())
+	s.Require().NoError(err)
+	s.Require().Equal([]types.IncentiveRecord{incentiveRecordOne}, allRecordsPoolOne)
+
+	// Ensure records for other pool remain unchanged
+	poolTwoRecord, err := clKeeper.GetIncentiveRecord(s.Ctx, clPoolTwo.GetId(), incentiveRecordOne.IncentiveDenom, incentiveRecordOne.MinUptime)
+	s.Require().Error(err)
+	s.Require().Equal(types.IncentiveRecord{}, poolTwoRecord)
+	allRecordsPoolTwo, err := clKeeper.GetAllIncentiveRecordsForPool(s.Ctx, clPoolTwo.GetId())
+	s.Require().NoError(err)
+	s.Require().Equal(emptyIncentiveRecords, allRecordsPoolTwo)
+
+	// Ensure directly setting additional records don't overwrite previous ones
+	clKeeper.SetIncentiveRecord(s.Ctx, incentiveRecordTwo)
+	poolOneRecord, err = clKeeper.GetIncentiveRecord(s.Ctx, clPoolOne.GetId(), incentiveRecordTwo.IncentiveDenom, incentiveRecordTwo.MinUptime)
+	s.Require().NoError(err)
+	s.Require().Equal(incentiveRecordTwo, poolOneRecord)
+	allRecordsPoolOne, err = clKeeper.GetAllIncentiveRecordsForPool(s.Ctx, clPoolOne.GetId())
+	s.Require().NoError(err)
+	s.Require().Equal([]types.IncentiveRecord{incentiveRecordOne, incentiveRecordTwo}, allRecordsPoolOne)
+
+	// Ensure setting multiple records through helper functions as expected
+	clKeeper.SetMultipleIncentiveRecords(s.Ctx, []types.IncentiveRecord{incentiveRecordThree, incentiveRecordFour})
+
+	// Note: we expect the records to be retrieved in lexicographic order by denom
+	allRecordsPoolOne, err = clKeeper.GetAllIncentiveRecordsForPool(s.Ctx, clPoolOne.GetId())
+	s.Require().NoError(err)
+	s.Require().Equal([]types.IncentiveRecord{incentiveRecordFour, incentiveRecordOne, incentiveRecordThree, incentiveRecordTwo}, allRecordsPoolOne)
+
+	// Finally, we ensure the second pool remains unaffected
+	allRecordsPoolTwo, err = clKeeper.GetAllIncentiveRecordsForPool(s.Ctx, clPoolTwo.GetId())
+	s.Require().NoError(err)
+	s.Require().Equal(emptyIncentiveRecords, allRecordsPoolTwo)
+}
diff --git a/x/concentrated-liquidity/internal/swapstrategy/fees_test.go b/x/concentrated-liquidity/internal/swapstrategy/fees_test.go
index d6b2f324f..fa7de8ce5 100644
--- a/x/concentrated-liquidity/internal/swapstrategy/fees_test.go
+++ b/x/concentrated-liquidity/internal/swapstrategy/fees_test.go
@@ -34,8 +34,8 @@ func (suite *StrategyTestSuite) TestComputeFeeChargePerSwapStepOutGivenIn() {
 			amountSpecifiedRemaining: five,
 			swapFee:                  onePercentFee,
 
-			// N.B. adding smallest dec due to mulRoundUp logic.
-			expectedFeeCharge: sdk.OneDec(),
+			// amount in * swap fee / (1 - swap fee)
+			expectedFeeCharge: sdk.NewDec(100).Mul(onePercentFee).Quo(sdk.OneDec().Sub(onePercentFee)),
 		},
 		"did not reach target -> charge fee on the difference between amount remaining and amount in": {
 			currentSqrtPrice:         defaultCurrSqrtPrice,
diff --git a/x/concentrated-liquidity/keeper_test.go b/x/concentrated-liquidity/keeper_test.go
index 0295b7222..12a79ecad 100644
--- a/x/concentrated-liquidity/keeper_test.go
+++ b/x/concentrated-liquidity/keeper_test.go
@@ -7,6 +7,7 @@ import (
 	sdk "github.com/cosmos/cosmos-sdk/types"
 	"github.com/stretchr/testify/suite"
 
+	"github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity/internal/math"
 	"github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity/model"
 	poolmanagertypes "github.com/osmosis-labs/osmosis/v14/x/poolmanager/types"
 
@@ -16,29 +17,33 @@ import (
 )
 
 var (
-	DefaultExponentAtPriceOne      = sdk.NewInt(-4)
-	DefaultMinTick, DefaultMaxTick = cl.GetMinAndMaxTicksFromExponentAtPriceOne(DefaultExponentAtPriceOne)
-	DefaultLowerPrice              = sdk.NewDec(4545)
-	DefaultLowerTick               = int64(305450)
-	DefaultUpperPrice              = sdk.NewDec(5500)
-	DefaultUpperTick               = int64(315000)
-	DefaultCurrPrice               = sdk.NewDec(5000)
-	DefaultCurrTick                = sdk.NewInt(310000)
-	DefaultCurrSqrtPrice, _        = DefaultCurrPrice.ApproxSqrt() // 70.710678118654752440
-	DefaultZeroSwapFee             = sdk.ZeroDec()
-	DefaultFeeAccumCoins           = sdk.NewDecCoins(sdk.NewDecCoin("foo", sdk.NewInt(50)))
-	DefaultFreezeDuration          = time.Duration(time.Hour * 24)
-	ETH                            = "eth"
-	DefaultAmt0                    = sdk.NewInt(1000000)
-	DefaultAmt0Expected            = sdk.NewInt(998976)
-	DefaultCoin0                   = sdk.NewCoin(ETH, DefaultAmt0)
-	USDC                           = "usdc"
-	DefaultAmt1                    = sdk.NewInt(5000000000)
-	DefaultAmt1Expected            = sdk.NewInt(5000000000)
-	DefaultCoin1                   = sdk.NewCoin(USDC, DefaultAmt1)
-	DefaultLiquidityAmt            = sdk.MustNewDecFromStr("1517882343.751510418088349649")
-	DefaultTickSpacing             = uint64(1)
-	PoolCreationFee                = poolmanagertypes.DefaultParams().PoolCreationFee
+	DefaultExponentAtPriceOne                      = sdk.NewInt(-4)
+	DefaultMinTick, DefaultMaxTick                 = cl.GetMinAndMaxTicksFromExponentAtPriceOne(DefaultExponentAtPriceOne)
+	DefaultLowerPrice                              = sdk.NewDec(4545)
+	DefaultLowerTick                               = int64(305450)
+	DefaultUpperPrice                              = sdk.NewDec(5500)
+	DefaultUpperTick                               = int64(315000)
+	DefaultCurrPrice                               = sdk.NewDec(5000)
+	DefaultCurrTick                                = sdk.NewInt(310000)
+	DefaultCurrSqrtPrice, _                        = DefaultCurrPrice.ApproxSqrt() // 70.710678118654752440
+	DefaultZeroSwapFee                             = sdk.ZeroDec()
+	DefaultFeeAccumCoins                           = sdk.NewDecCoins(sdk.NewDecCoin("foo", sdk.NewInt(50)))
+	DefaultFreezeDuration                          = time.Duration(time.Hour * 24)
+	ETH                                            = "eth"
+	DefaultAmt0                                    = sdk.NewInt(1000000)
+	DefaultAmt0Expected                            = sdk.NewInt(998976)
+	DefaultCoin0                                   = sdk.NewCoin(ETH, DefaultAmt0)
+	USDC                                           = "usdc"
+	DefaultAmt1                                    = sdk.NewInt(5000000000)
+	DefaultAmt1Expected                            = sdk.NewInt(5000000000)
+	DefaultCoin1                                   = sdk.NewCoin(USDC, DefaultAmt1)
+	DefaultLiquidityAmt                            = sdk.MustNewDecFromStr("1517882343.751510418088349649")
+	DefaultTickSpacing                             = uint64(1)
+	PoolCreationFee                                = poolmanagertypes.DefaultParams().PoolCreationFee
+	DefaultExponentConsecutivePositionLowerTick, _ = math.PriceToTick(sdk.NewDec(5500), DefaultExponentAtPriceOne)
+	DefaultExponentConsecutivePositionUpperTick, _ = math.PriceToTick(sdk.NewDec(6250), DefaultExponentAtPriceOne)
+	DefaultExponentOverlappingPositionLowerTick, _ = math.PriceToTick(sdk.NewDec(4000), DefaultExponentAtPriceOne)
+	DefaultExponentOverlappingPositionUpperTick, _ = math.PriceToTick(sdk.NewDec(4999), DefaultExponentAtPriceOne)
 )
 
 type KeeperTestSuite struct {
@@ -66,6 +71,44 @@ func (s *KeeperTestSuite) SetupPosition(poolId uint64, owner sdk.AccAddress, coi
 	return *position
 }
 
+// SetupDefaultPositions sets up four different positions to the given pool with different accounts for each position./
+// Sets up the following positions:
+// 1. Default position
+// 2. Full range position
+// 3. Postion with consecutive price range from the default position
+// 4. Position with overlapping price range from the default position
+func (s *KeeperTestSuite) SetupDefaultPositions(poolId uint64) {
+
+	// ----------- set up positions ----------
+	// 1. Default position
+	s.SetupDefaultPosition(poolId)
+
+	// 2. Full range position
+	s.SetupFullRangePositionAcc(poolId, s.TestAccs[1])
+
+	// 3. Position with consecutive price range from the default position
+	s.SetupOverlappingRangePositionAcc(poolId, s.TestAccs[2])
+
+	// 4. Position with overlapping price range from the default position
+	s.SetupOverlappingRangePositionAcc(poolId, s.TestAccs[3])
+}
+
+func (s *KeeperTestSuite) SetupDefaultPositionAcc(poolId uint64, owner sdk.AccAddress) {
+	s.SetupPosition(poolId, owner, DefaultCoin0, DefaultCoin1, DefaultLowerTick, DefaultUpperTick, s.Ctx.BlockTime().Add(DefaultFreezeDuration))
+}
+
+func (s *KeeperTestSuite) SetupFullRangePositionAcc(poolId uint64, owner sdk.AccAddress) {
+	s.SetupPosition(poolId, owner, DefaultCoin0, DefaultCoin1, DefaultMinTick, DefaultMaxTick, s.Ctx.BlockTime().Add(DefaultFreezeDuration))
+}
+
+func (s *KeeperTestSuite) SetupConsecutiveRangePositionAcc(poolId uint64, owner sdk.AccAddress) {
+	s.SetupPosition(poolId, owner, DefaultCoin0, DefaultCoin1, DefaultExponentConsecutivePositionLowerTick.Int64(), DefaultExponentConsecutivePositionUpperTick.Int64(), s.Ctx.BlockTime().Add(DefaultFreezeDuration))
+}
+
+func (s *KeeperTestSuite) SetupOverlappingRangePositionAcc(poolId uint64, owner sdk.AccAddress) {
+	s.SetupPosition(poolId, owner, DefaultCoin0, DefaultCoin1, DefaultExponentOverlappingPositionLowerTick.Int64(), DefaultExponentOverlappingPositionUpperTick.Int64(), s.Ctx.BlockTime().Add(DefaultFreezeDuration))
+}
+
 // validatePositionUpdate validates that position with given parameters has expectedRemainingLiquidity left.
 func (s *KeeperTestSuite) validatePositionUpdate(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, lowerTick int64, upperTick int64, frozenUntil time.Time, expectedRemainingLiquidity sdk.Dec) {
 	position, err := s.App.ConcentratedLiquidityKeeper.GetPosition(ctx, poolId, owner, lowerTick, upperTick, frozenUntil)
diff --git a/x/concentrated-liquidity/lp_test.go b/x/concentrated-liquidity/lp_test.go
index b187031e8..62e915872 100644
--- a/x/concentrated-liquidity/lp_test.go
+++ b/x/concentrated-liquidity/lp_test.go
@@ -60,15 +60,18 @@ var (
 		preSetChargeFee: oneEth,
 		// in this setup lower tick < current tick < upper tick
 		// the fee accumulator for ticks <= current tick are updated.
-		expectedFeeGrowthOutsideLower: oneEthCoins,
+		expectedFeeGrowthOutsideLower: cl.EmptyCoins,
 		// as a result, the upper tick is not updated.
 		expectedFeeGrowthOutsideUpper: cl.EmptyCoins,
 	}
 
 	positionCases = map[string]lpTest{
-		"base case": {},
+		"base case": {
+			expectedFeeGrowthOutsideLower: oneEthCoins,
+		},
 		"create a position with non default tick spacing (10) with ticks that fall into tick spacing requirements": {
-			tickSpacing: 10,
+			tickSpacing:                   10,
+			expectedFeeGrowthOutsideLower: oneEthCoins,
 		},
 		"lower tick < upper tick < current tick -> both tick's fee accumulators are updated with one eth": {
 			lowerTick:   DefaultLowerTick,
@@ -77,7 +80,6 @@ var (
 
 			preSetChargeFee:               oneEth,
 			expectedFeeGrowthOutsideLower: oneEthCoins,
-			expectedFeeGrowthOutsideUpper: oneEthCoins,
 		},
 		"lower tick < upper tick < current tick -> the fee is not charged so tick accumulators are unset": {
 			lowerTick:   DefaultLowerTick,
@@ -85,8 +87,7 @@ var (
 			currentTick: sdk.NewInt(DefaultUpperTick + 1),
 
 			preSetChargeFee:               sdk.NewDecCoin(ETH, sdk.ZeroInt()), // zero fee
-			expectedFeeGrowthOutsideLower: cl.EmptyCoins,
-			expectedFeeGrowthOutsideUpper: cl.EmptyCoins,
+			expectedFeeGrowthOutsideLower: oneEthCoins,
 		},
 		"current tick < lower tick < upper tick -> both tick's fee accumulators are unitilialized": {
 			lowerTick:   DefaultLowerTick,
@@ -94,8 +95,7 @@ var (
 			currentTick: sdk.NewInt(DefaultLowerTick - 1),
 
 			preSetChargeFee:               oneEth,
-			expectedFeeGrowthOutsideLower: cl.EmptyCoins,
-			expectedFeeGrowthOutsideUpper: cl.EmptyCoins,
+			expectedFeeGrowthOutsideLower: oneEthCoins,
 		},
 		"lower tick < upper tick == current tick -> both tick's fee accumulators are updated with one eth": {
 			lowerTick:   DefaultLowerTick,
@@ -104,7 +104,6 @@ var (
 
 			preSetChargeFee:               oneEth,
 			expectedFeeGrowthOutsideLower: oneEthCoins,
-			expectedFeeGrowthOutsideUpper: oneEthCoins,
 		},
 		"second position: lower tick < upper tick == current tick -> both tick's fee accumulators are updated with one eth": {
 			lowerTick:   DefaultLowerTick,
@@ -116,7 +115,6 @@ var (
 			liquidityAmount:               baseCase.liquidityAmount.MulInt64(2),
 			preSetChargeFee:               oneEth,
 			expectedFeeGrowthOutsideLower: oneEthCoins,
-			expectedFeeGrowthOutsideUpper: oneEthCoins,
 		},
 	}
 )
@@ -296,7 +294,8 @@ func (s *KeeperTestSuite) TestWithdrawPosition() {
 		// when this is set, it overwrites the setupConfig
 		// and gives the overwritten configuration to
 		// the system under test.
-		sutConfigOverwrite *lpTest
+		sutConfigOverwrite            *lpTest
+		createPositionFreezeOverwrite bool
 	}{
 		"base case: withdraw full liquidity amount": {
 			// setup parameters for creating a pool and position.
@@ -343,6 +342,18 @@ func (s *KeeperTestSuite) TestWithdrawPosition() {
 				frozenUntil:   defaultFrozenUntil,
 				expectedError: types.PositionNotFoundError{PoolId: 1, LowerTick: 305450, UpperTick: 315000, FrozenUntil: defaultFrozenUntil},
 			},
+			createPositionFreezeOverwrite: true,
+		},
+		"error: position still unfreezing": {
+			// setup parameters for creation a pool and position.
+			setupConfig: baseCase,
+
+			// system under test parameters
+			// for withdrawing a position.
+			sutConfigOverwrite: &lpTest{
+				frozenUntil:   defaultFrozenUntil,
+				expectedError: types.PositionStillFrozenError{FrozenUntil: defaultFrozenUntil},
+			},
 		},
 		"error: withdraw liquidity that is still frozen": {
 			// setup parameters for creation a pool and position.
@@ -428,13 +439,19 @@ func (s *KeeperTestSuite) TestWithdrawPosition() {
 				config                      = *tc.setupConfig
 				sutConfigOverwrite          = *tc.sutConfigOverwrite
 			)
+			// If specific configs are provided in the test case, overwrite the config with those values.
+			mergeConfigs(&config, &sutConfigOverwrite)
+			createPositionFreezeTime := config.frozenUntil
+			if tc.createPositionFreezeOverwrite {
+				createPositionFreezeTime = time.Time{}
+			}
 
 			// If a setupConfig is provided, use it to create a pool and position.
 			pool := s.PrepareConcentratedPool()
 			s.FundAcc(owner, sdk.NewCoins(sdk.NewCoin("eth", sdk.NewInt(10000000000000)), sdk.NewCoin("usdc", sdk.NewInt(1000000000000))))
 
 			// Create a position from the parameters in the test case.
-			_, _, liquidityCreated, err := concentratedLiquidityKeeper.CreatePosition(ctx, config.poolId, owner, config.amount0Desired, config.amount1Desired, sdk.ZeroInt(), sdk.ZeroInt(), config.lowerTick, config.upperTick, config.frozenUntil)
+			_, _, liquidityCreated, err := concentratedLiquidityKeeper.CreatePosition(ctx, pool.GetId(), owner, config.amount0Desired, config.amount1Desired, sdk.ZeroInt(), sdk.ZeroInt(), DefaultLowerTick, DefaultUpperTick, createPositionFreezeTime)
 			s.Require().NoError(err)
 
 			// Set global fee growth to 1 ETH and charge the fee to the pool.
@@ -442,9 +459,6 @@ func (s *KeeperTestSuite) TestWithdrawPosition() {
 			err = concentratedLiquidityKeeper.ChargeFee(ctx, pool.GetId(), globalFeeGrowth)
 			s.Require().NoError(err)
 
-			// If specific configs are provided in the test case, overwrite the config with those values.
-			mergeConfigs(&config, &sutConfigOverwrite)
-
 			// Determine the liquidity expected to remain after the withdraw.
 			expectedRemainingLiquidity := liquidityCreated.Sub(config.liquidityAmount)
 
@@ -551,6 +565,12 @@ func mergeConfigs(dst *lpTest, overwrite *lpTest) {
 		if !overwrite.frozenUntil.IsZero() {
 			dst.frozenUntil = overwrite.frozenUntil
 		}
+		if !overwrite.expectedFeeGrowthOutsideLower.IsEqual(sdk.DecCoins{}) {
+			dst.expectedFeeGrowthOutsideLower = overwrite.expectedFeeGrowthOutsideLower
+		}
+		if !overwrite.expectedFeeGrowthOutsideUpper.IsEqual(sdk.DecCoins{}) {
+			dst.expectedFeeGrowthOutsideUpper = overwrite.expectedFeeGrowthOutsideUpper
+		}
 	}
 }
 
diff --git a/x/concentrated-liquidity/swaps_test.go b/x/concentrated-liquidity/swaps_test.go
index facf6d318..cb04f3863 100644
--- a/x/concentrated-liquidity/swaps_test.go
+++ b/x/concentrated-liquidity/swaps_test.go
@@ -452,9 +452,9 @@ var (
 			// expectedTokenOut:                  4999999999.99999999999999999970 + 3702563350.03654978405015422548 = 8702563350.03654978405015422518 round down = 8702.563350 usdc
 			// expectedFeeGrowthAccumulatorValue: 0.000034550151296760 + 0.0000374851520884196734228699332666 = 0.0000720353033851796734228699332666
 			expectedTokenIn:                   sdk.NewCoin("eth", sdk.NewInt(2000000)),
-			expectedTokenOut:                  sdk.NewCoin("usdc", sdk.NewInt(8702560429)),
-			expectedFeeGrowthAccumulatorValue: sdk.MustNewDecFromStr("0.000072034590795926"),
-			expectedTick:                      sdk.NewInt(301381),
+			expectedTokenOut:                  sdk.NewCoin("usdc", sdk.NewInt(8691708221)),
+			expectedFeeGrowthAccumulatorValue: sdk.MustNewDecFromStr("0.000073738597832046"),
+			expectedTick:                      sdk.NewInt(301393),
 			newLowerPrice:                     sdk.NewDec(4000),
 			newUpperPrice:                     sdk.NewDec(4545),
 		},
@@ -471,9 +471,9 @@ var (
 			// expectedTokenOut: 2146.28785880640879265591374059 + "1437108.91592757237716789250871 + 269488.274305469529889078712213 = 1708743.47809184831584962713466 eth
 			// expectedFeeGrowthAccumulatorValue: 0.000707071429382580300000000000073 + 0.344423603800805124400000000000 + 0.253197426243519613677553835191 = 0.598328101473707318377553835191
 			expectedTokenIn:                   sdk.NewCoin("usdc", sdk.NewInt(10000000000)),
-			expectedTokenOut:                  sdk.NewCoin("eth", sdk.NewInt(1708743)),
-			expectedFeeGrowthAccumulatorValue: sdk.MustNewDecFromStr("0.598328100416133943"),
-			expectedTick:                      sdk.NewInt(318432),
+			expectedTokenOut:                  sdk.NewCoin("eth", sdk.NewInt(1695807)),
+			expectedFeeGrowthAccumulatorValue: sdk.MustNewDecFromStr("0.624166726347032857"),
+			expectedTick:                      sdk.NewInt(318260),
 			newLowerPrice:                     sdk.NewDec(5001),
 			newUpperPrice:                     sdk.NewDec(6250),
 		},
@@ -487,9 +487,9 @@ var (
 			secondPositionLowerPrice:          sdk.NewDec(4000),
 			secondPositionUpperPrice:          sdk.NewDec(4999),
 			expectedTokenIn:                   sdk.NewCoin("eth", sdk.NewInt(1800000)),
-			expectedTokenOut:                  sdk.NewCoin("usdc", sdk.NewInt(8440820211)),
-			expectedFeeGrowthAccumulatorValue: sdk.MustNewDecFromStr("0.000005552421957143"),
-			expectedTick:                      sdk.NewInt(302996),
+			expectedTokenOut:                  sdk.NewCoin("usdc", sdk.NewInt(8440657775)),
+			expectedFeeGrowthAccumulatorValue: sdk.MustNewDecFromStr("0.000005569829831408"),
+			expectedTick:                      sdk.NewInt(302997),
 			newLowerPrice:                     sdk.NewDec(4000),
 			newUpperPrice:                     sdk.NewDec(4999),
 		},
@@ -503,9 +503,9 @@ var (
 			secondPositionUpperPrice:          sdk.NewDec(6250),
 			swapFee:                           sdk.MustNewDecFromStr("0.03"),
 			expectedTokenIn:                   sdk.NewCoin("usdc", sdk.NewInt(10000000000)),
-			expectedTokenOut:                  sdk.NewCoin("eth", sdk.NewInt(1772029)),
-			expectedFeeGrowthAccumulatorValue: sdk.MustNewDecFromStr("0.218688507759947647"),
-			expectedTick:                      sdk.NewInt(320672),
+			expectedTokenOut:                  sdk.NewCoin("eth", sdk.NewInt(1771252)),
+			expectedFeeGrowthAccumulatorValue: sdk.MustNewDecFromStr("0.221769187794051751"),
+			expectedTick:                      sdk.NewInt(320666),
 			newLowerPrice:                     sdk.NewDec(5501),
 			newUpperPrice:                     sdk.NewDec(6250),
 		},
@@ -520,9 +520,9 @@ var (
 			// liquidity: 		 1517882343.751510418088349649
 			// sqrtPriceNext:    70.668238976219012614 which is 4994
 			// sqrtPriceCurrent: 70.710678118654752440 which is 5000
-			expectedTokenIn:                   sdk.NewCoin("eth", sdk.NewInt(13021)),
+			expectedTokenIn:                   sdk.NewCoin("eth", sdk.NewInt(13022)),
 			expectedTokenOut:                  sdk.NewCoin("usdc", sdk.NewInt(64417624)),
-			expectedFeeGrowthAccumulatorValue: sdk.MustNewDecFromStr("0.000000084934119255"),
+			expectedFeeGrowthAccumulatorValue: sdk.MustNewDecFromStr("0.000000085792039652"),
 			expectedTick:                      sdk.NewInt(309941),
 		},
 	}
@@ -905,8 +905,6 @@ var (
 	additiveFeeGrowthGlobalErrTolerance = osmomath.ErrTolerance{
 		// 2 * 10^-18
 		AdditiveTolerance: sdk.SmallestDec().Mul(sdk.NewDec(2)),
-		// actual fee growth should be greater than expected.
-		RoundingDir: osmomath.RoundUp,
 	}
 )
 
@@ -1289,7 +1287,12 @@ func (s *KeeperTestSuite) TestCalcAndSwapInAmtGivenOut() {
 					return
 				}
 
-				s.Require().Equal(1, feeAccValue.Len())
+				if test.expectedFeeGrowthAccumulatorValue.IsNil() {
+					s.Require().Equal(0, feeAccValue.Len())
+					return
+				}
+
+				s.Require().Equal(1, feeAccValue.Len(), fmt.Sprintf("fee accumulator should only have one denom, was (%s)", feeAccValue))
 				s.Require().Equal(0,
 					additiveFeeGrowthGlobalErrTolerance.CompareBigDec(
 						osmomath.BigDecFromSDKDec(test.expectedFeeGrowthAccumulatorValue),
@@ -1894,6 +1897,46 @@ func (s *KeeperTestSuite) TestInverseRelationshipSwapOutAmtGivenIn() {
 	}
 }
 
+func (suite *KeeperTestSuite) TestUpdateFeeGrowthGlobal() {
+	ten := sdk.NewDec(10)
+
+	tests := map[string]struct {
+		liquidity               sdk.Dec
+		feeChargeTotal          sdk.Dec
+		expectedFeeGrowthGlobal sdk.Dec
+	}{
+		"zero liquidity -> no-op": {
+			liquidity:               sdk.ZeroDec(),
+			feeChargeTotal:          ten,
+			expectedFeeGrowthGlobal: sdk.ZeroDec(),
+		},
+		"non-zero liquidity -> updated": {
+			liquidity:      ten,
+			feeChargeTotal: ten,
+			// 10 / 10 = 1
+			expectedFeeGrowthGlobal: sdk.OneDec(),
+		},
+	}
+
+	for name, tc := range tests {
+		tc := tc
+		suite.Run(name, func() {
+			suite.SetupTest()
+
+			// Setup.
+			swapState := cl.SwapState{}
+			swapState.SetLiquidity(tc.liquidity)
+			swapState.SetFeeGrowthGlobal(sdk.ZeroDec())
+
+			// System under test.
+			swapState.UpdateFeeGrowthGlobal(tc.feeChargeTotal)
+
+			// Assertion.
+			suite.Require().Equal(tc.expectedFeeGrowthGlobal, swapState.GetFeeGrowthGlobal())
+		})
+	}
+}
+
 func (s *KeeperTestSuite) TestInverseRelationshipSwapInAmtGivenOut() {
 	tests := swapInGivenOutTestCases
 
diff --git a/x/concentrated-liquidity/tick_test.go b/x/concentrated-liquidity/tick_test.go
index 0bf801cb4..062d88a05 100644
--- a/x/concentrated-liquidity/tick_test.go
+++ b/x/concentrated-liquidity/tick_test.go
@@ -246,20 +246,32 @@ func (s *KeeperTestSuite) TestInitOrUpdateTick() {
 			// If tickExists set, initialize the specified tick with defaultLiquidityAmt
 			preexistingLiquidity := sdk.ZeroDec()
 			if test.tickExists {
-				err := s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, test.param.poolId, currentTick, test.param.tickIndex, DefaultLiquidityAmt, test.param.upper)
+				tickInfoBefore, err := s.App.ConcentratedLiquidityKeeper.GetTickInfo(s.Ctx, 1, test.param.tickIndex)
 				s.Require().NoError(err)
+				err = s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, test.param.poolId, currentTick, test.param.tickIndex, DefaultLiquidityAmt, test.param.upper)
+				s.Require().NoError(err)
+				if tickInfoBefore.LiquidityGross.IsZero() && test.param.tickIndex <= pool.GetCurrentTick().Int64() {
+					tickInfoAfter, err := s.App.ConcentratedLiquidityKeeper.GetTickInfo(s.Ctx, 1, test.param.tickIndex)
+					s.Require().NoError(err)
+					s.Require().Equal(tickInfoAfter.FeeGrowthOutside, feeAccum.GetValue())
+				}
 				preexistingLiquidity = DefaultLiquidityAmt
 			}
 
 			// Get the tick info for poolId 1
-			tickInfo, err := s.App.ConcentratedLiquidityKeeper.GetTickInfo(s.Ctx, 1, test.param.tickIndex)
+			tickInfoAfter, err := s.App.ConcentratedLiquidityKeeper.GetTickInfo(s.Ctx, 1, test.param.tickIndex)
 			s.Require().NoError(err)
 
 			// Ensure tick state contains any preexistingLiquidity (zero otherwise)
-			s.Require().Equal(preexistingLiquidity, tickInfo.LiquidityGross)
+			s.Require().Equal(preexistingLiquidity, tickInfoAfter.LiquidityGross)
 
 			// Initialize or update the tick according to the test case
 			err = s.App.ConcentratedLiquidityKeeper.InitOrUpdateTick(s.Ctx, test.param.poolId, currentTick, test.param.tickIndex, test.param.liquidityIn, test.param.upper)
+			if tickInfoAfter.LiquidityGross.IsZero() && test.param.tickIndex <= pool.GetCurrentTick().Int64() {
+				tickInfoAfter, err := s.App.ConcentratedLiquidityKeeper.GetTickInfo(s.Ctx, 1, test.param.tickIndex)
+				s.Require().NoError(err)
+				s.Require().Equal(tickInfoAfter.FeeGrowthOutside, feeAccum.GetValue())
+			}
 			if test.expectedErr != nil {
 				s.Require().ErrorIs(err, test.expectedErr)
 				return
@@ -267,17 +279,17 @@ func (s *KeeperTestSuite) TestInitOrUpdateTick() {
 			s.Require().NoError(err)
 
 			// Get the tick info for poolId 1 again
-			tickInfo, err = s.App.ConcentratedLiquidityKeeper.GetTickInfo(s.Ctx, 1, test.param.tickIndex)
+			tickInfoAfter, err = s.App.ConcentratedLiquidityKeeper.GetTickInfo(s.Ctx, 1, test.param.tickIndex)
 			s.Require().NoError(err)
 
 			// Check that the initialized or updated tick matches our expectation
-			s.Require().Equal(test.expectedLiquidityNet, tickInfo.LiquidityNet)
-			s.Require().Equal(test.expectedLiquidityGross, tickInfo.LiquidityGross)
+			s.Require().Equal(test.expectedLiquidityNet, tickInfoAfter.LiquidityNet)
+			s.Require().Equal(test.expectedLiquidityGross, tickInfoAfter.LiquidityGross)
 
 			if test.param.tickIndex <= 0 {
-				s.Require().Equal(defaultAccumCoins, tickInfo.FeeGrowthOutside)
+				s.Require().Equal(defaultAccumCoins, tickInfoAfter.FeeGrowthOutside)
 			} else {
-				s.Require().Equal(sdk.DecCoins(nil), tickInfo.FeeGrowthOutside)
+				s.Require().Equal(sdk.DecCoins(nil), tickInfoAfter.FeeGrowthOutside)
 			}
 		})
 	}
@@ -361,12 +373,14 @@ func (s *KeeperTestSuite) TestGetTickInfo() {
 func (s *KeeperTestSuite) TestCrossTick() {
 	var (
 		preInitializedTickIndex = DefaultCurrTick.Int64() - 2
+		defaultAdditiveFee      = sdk.NewDecCoinFromDec(USDC, sdk.NewDec(1000))
 	)
 
 	tests := []struct {
 		name                         string
 		poolToGet                    uint64
 		tickToGet                    int64
+		additiveFee                  sdk.DecCoin
 		expectedLiquidityDelta       sdk.Dec
 		expectedTickFeeGrowthOutside sdk.DecCoins
 		expectedErr                  bool
@@ -375,13 +389,23 @@ func (s *KeeperTestSuite) TestCrossTick() {
 			name:                         "Get tick info on existing pool and existing tick",
 			poolToGet:                    validPoolId,
 			tickToGet:                    preInitializedTickIndex,
+			additiveFee:                  defaultAdditiveFee,
+			expectedLiquidityDelta:       DefaultLiquidityAmt.Neg(),
+			expectedTickFeeGrowthOutside: DefaultFeeAccumCoins.Add(defaultAdditiveFee),
+		},
+		{
+			name:                         "twice the default additive fee",
+			poolToGet:                    validPoolId,
+			tickToGet:                    preInitializedTickIndex,
+			additiveFee:                  defaultAdditiveFee.Add(defaultAdditiveFee),
 			expectedLiquidityDelta:       DefaultLiquidityAmt.Neg(),
-			expectedTickFeeGrowthOutside: DefaultFeeAccumCoins,
+			expectedTickFeeGrowthOutside: DefaultFeeAccumCoins.Add(defaultAdditiveFee.Add(defaultAdditiveFee)),
 		},
 		{
 			name:        "Try invalid tick",
 			poolToGet:   2,
 			tickToGet:   preInitializedTickIndex,
+			additiveFee: defaultAdditiveFee,
 			expectedErr: true,
 		},
 	}
@@ -415,7 +439,7 @@ func (s *KeeperTestSuite) TestCrossTick() {
 			s.Require().NoError(err)
 
 			// System under test
-			liquidityDelta, err := s.App.ConcentratedLiquidityKeeper.CrossTick(s.Ctx, test.poolToGet, test.tickToGet)
+			liquidityDelta, err := s.App.ConcentratedLiquidityKeeper.CrossTick(s.Ctx, test.poolToGet, test.tickToGet, test.additiveFee)
 			if test.expectedErr {
 				s.Require().Error(err)
 			} else {
diff --git a/x/twap/keeper_test.go b/x/twap/keeper_test.go
index 4c9ec6356..a6ead5a4f 100644
--- a/x/twap/keeper_test.go
+++ b/x/twap/keeper_test.go
@@ -26,6 +26,7 @@ var (
 	tMinOne                       = baseTime.Add(-time.Second)
 	tPlusOneMin                   = baseTime.Add(time.Minute)
 	basePoolId             uint64 = 1
+	oneHundredNanoseconds         = 100 * time.Nanosecond
 )
 
 type TestSuite struct {
@@ -479,6 +480,11 @@ func withPrice1Set(twapRecord types.TwapRecord, price1ToSet sdk.Dec) types.TwapR
 	return twapRecord
 }
 
+func withTime(twapRecord types.TwapRecord, time time.Time) types.TwapRecord {
+	twapRecord.Time = time
+	return twapRecord
+}
+
 func newRecord(poolId uint64, t time.Time, sp0, accum0, accum1, geomAccum sdk.Dec) types.TwapRecord {
 	return types.TwapRecord{
 		PoolId:          poolId,
diff --git a/x/twap/logic_test.go b/x/twap/logic_test.go
index 5515dadb4..870e3baf0 100644
--- a/x/twap/logic_test.go
+++ b/x/twap/logic_test.go
@@ -290,6 +290,12 @@ func TestRecordWithUpdatedAccumulators(t *testing.T) {
 			newTime:   defaultRecord.Time.Add(time.Second),
 			expRecord: newExpRecord(oneDec.Add(OneSec), twoDec.Add(OneSec), pointFiveDec),
 		},
+
+		"nanoseconds in time of the original record do not affect final result": {
+			record:    withTime(defaultRecord, defaultRecord.Time.Add(oneHundredNanoseconds)),
+			newTime:   time.Unix(2, 0),
+			expRecord: newExpRecord(oneDec.Add(OneSec.MulInt64(10)), twoDec.Add(OneSec.QuoInt64(10)), pointFiveDec.Add(geometricTenSecAccum)),
+		},
 	}
 
 	for name, test := range tests {
diff --git a/x/twap/strategy_test.go b/x/twap/strategy_test.go
index d3354e175..e3c23b286 100644
--- a/x/twap/strategy_test.go
+++ b/x/twap/strategy_test.go
@@ -146,6 +146,13 @@ func (s *TestSuite) TestComputeArithmeticStrategyTwap() {
 			s, pointOneAccum, tenSecAccum, 100*time.Second, sdk.NewDecWithPrec(1, 1)),
 
 		"accumulator = 10*OneSec, t=100s. 0 base accum (asset 1)": testCaseFromDeltasAsset1(s, sdk.ZeroDec(), OneSec.MulInt64(10), 100*time.Second, sdk.NewDecWithPrec(1, 1)),
+
+		"start record time with nanoseconds does not change result": {
+			startRecord: newOneSidedRecord(baseTime.Add(oneHundredNanoseconds), sdk.ZeroDec(), true),
+			endRecord:   newOneSidedRecord(tPlusOne, OneSec, true),
+			quoteAsset:  denom0,
+			expTwap:     sdk.OneDec(),
+		},
 	}
 	for name, test := range tests {
 		s.Run(name, func() {
@@ -278,6 +285,13 @@ func (s *TestSuite) TestComputeGeometricStrategyTwap() {
 
 			expTwap: sdk.ZeroDec(),
 		},
+
+		"start record time with nanoseconds does not change result": {
+			startRecord: newOneSidedGeometricRecord(baseTime.Add(oneHundredNanoseconds), sdk.ZeroDec()),
+			endRecord:   newOneSidedGeometricRecord(tPlusOne, geometricTenSecAccum),
+			quoteAsset:  denom0,
+			expTwap:     sdk.NewDec(10),
+		},
 	}
 
 	for name, tc := range tests {
diff --git a/x/twap/types/utils_test.go b/x/twap/types/utils_test.go
index 480797e48..d757513c9 100644
--- a/x/twap/types/utils_test.go
+++ b/x/twap/types/utils_test.go
@@ -3,6 +3,7 @@ package types
 import (
 	"fmt"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/require"
 
@@ -66,3 +67,14 @@ func TestLexicographicalOrderDenoms(t *testing.T) {
 		})
 	}
 }
+
+func TestCanonicalTimeMs(t *testing.T) {
+	const expectedMs int64 = 2
+
+	newYorkLocation, err := time.LoadLocation("America/New_York")
+	require.NoError(t, err)
+	time := time.Unix(0, int64(time.Millisecond+999999+1)).In(newYorkLocation)
+
+	actualTime := CanonicalTimeMs(time)
+	require.Equal(t, expectedMs, actualTime)
+}