Repository: osmosis-labs/osmosis
Pull request #5164 (2023-05-12 06:20:58)

osmomath: disable thelper for some helper functions
<!-- < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < ☺
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: #5163 

<del>Marks helper functions in `osmomath` with `t.Helper()`. </del>

Disables thelper warnings for some functions. Reason for not marking those functions with `t.Helper()`: https://github.com/osmosis-labs/osmosis/issues/5163#issuecomment-1545983797


Patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 0364254b3..930306eac 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -11,17 +11,7 @@ Closes: #XXX
 
 > Add a description of the overall background and high level changes that this PR introduces
 
-*(E.g.: This pull request improves documation of area A by adding ....*
-
-
-## Brief Changelog
-
-*(for example:)*
- 
-  - *The metadata is stored in the blob store on job creation time as a persistent artifact*
-  - *Deployments RPC transmits only the blob storage reference*
-  - *Daemons retrieve the RPC data from the blob cache*
-
+*(E.g.: This pull request improves documentation of area A by adding ....*
 
 ## Testing and Verifying
 
@@ -45,6 +35,11 @@ This change added tests and can be verified as follows:
 
 ## Documentation and Release Note
 
-  - Does this pull request introduce a new feature or user-facing behavior changes? (yes / no)
-  - Is a relevant changelog entry added to the `Unreleased` section in `CHANGELOG.md`? (yes / no)
-  - How is the feature or change documented? (not applicable   /   specification (`x/<module>/spec/`)  /  [Osmosis docs repo](https://github.com/osmosis-labs/docs)   /   not documented)
\ No newline at end of file
+  - [ ] Does this pull request introduce a new feature or user-facing behavior changes?
+  - [ ] Changelog entry added to `Unreleased` section of `CHANGELOG.md`?
+
+Where is the change documented? 
+  - [ ] Specification (`x/{module}/README.md`)
+  - [ ] Osmosis documentation site
+  - [ ] Code comments?
+  - [ ] N/A
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14d2c0377..4482471e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -66,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   * [#4974](https://github.com/osmosis-labs/osmosis/pull/4974) Add lock id to `MsgSuperfluidUndelegateAndUnbondLockResponse`
   * [#2741](https://github.com/osmosis-labs/osmosis/pull/2741) Prevent updating the twap record if `ctx.BlockTime <= record.Time` or `ctx.BlockHeight <= record.Height`. Exception, can update the record created when creating the pool in the same block.
   * [#5129](https://github.com/osmosis-labs/osmosis/pull/5129) Relax twap record validation in init genesis to allow one of the spot prices to be non-zero when twap error is observed.
+  * [#5165](https://github.com/osmosis-labs/osmosis/pull/5165) Improve error message when fully exiting from a pool.
   
 ### API breaks
 
diff --git a/osmomath/decimal.go b/osmomath/decimal.go
index 03b6fea77..1b8e21ce0 100644
--- a/osmomath/decimal.go
+++ b/osmomath/decimal.go
@@ -887,12 +887,16 @@ func MaxDec(d1, d2 BigDec) BigDec {
 
 // DecEq returns true if two given decimals are equal.
 // Intended to be used with require/assert:  require.True(t, DecEq(...))
+//
+//nolint:thelper
 func DecEq(t *testing.T, exp, got BigDec) (*testing.T, bool, string, string, string) {
 	return t, exp.Equal(got), "expected:\t%v\ngot:\t\t%v", exp.String(), got.String()
 }
 
 // DecApproxEq returns true if the differences between two given decimals are smaller than the tolerance range.
 // Intended to be used with require/assert:  require.True(t, DecEq(...))
+//
+//nolint:thelper
 func DecApproxEq(t *testing.T, d1 BigDec, d2 BigDec, tol BigDec) (*testing.T, bool, string, string, string) {
 	diff := d1.Sub(d2).Abs()
 	return t, diff.LTE(tol), "expected |d1 - d2| <:\t%v\ngot |d1 - d2| = \t\t%v", tol.String(), diff.String()
diff --git a/osmomath/int.go b/osmomath/int.go
index c3ae4ef28..6b2fe8002 100644
--- a/osmomath/int.go
+++ b/osmomath/int.go
@@ -435,6 +435,8 @@ func (i BigInt) MarshalAmino() ([]byte, error)   { return i.Marshal() }
 func (i *BigInt) UnmarshalAmino(bz []byte) error { return i.Unmarshal(bz) }
 
 // intended to be used with require/assert:  require.True(IntEq(...))
+//
+//nolint:thelper
 func IntEq(t *testing.T, exp, got BigInt) (*testing.T, bool, string, string, string) {
 	return t, exp.Equal(got), "expected:\t%v\ngot:\t\t%v", exp.String(), got.String()
 }
diff --git a/osmoutils/accum/prefix.go b/osmoutils/accum/prefix.go
index 4dea6af88..c207d215b 100644
--- a/osmoutils/accum/prefix.go
+++ b/osmoutils/accum/prefix.go
@@ -8,23 +8,16 @@ const (
 	positionPrefix    = "pos"
 )
 
-// formatAccumPrefix returns the key prefix used for any
-// accum module values to be stored in the KVStore.
-// Returns "accum/{key}" as bytes.
-func formatModulePrefixKey(key string) []byte {
-	return []byte(fmt.Sprintf("%s/%s", modulePrefix, key))
-}
-
 // formatAccumPrefix returns the key prefix used
 // specifically for accumulator values in the KVStore.
 // Returns "accum/acc/{name}" as bytes.
 func formatAccumPrefixKey(name string) []byte {
-	return formatModulePrefixKey(fmt.Sprintf("%s/%s", accumulatorPrefix, name))
+	return []byte(fmt.Sprintf(modulePrefix+"/"+accumulatorPrefix+"/%s", name))
 }
 
 // FormatPositionPrefixKey returns the key prefix used
 // specifically for position values in the KVStore.
-// Returns "accum/pos/{accumName}/{name}" as bytes.
+// Returns "accum/acc/pos/{accumName}/{name}" as bytes.
 func FormatPositionPrefixKey(accumName, name string) []byte {
-	return formatAccumPrefixKey(fmt.Sprintf("%s/%s/%s", positionPrefix, accumName, name))
+	return []byte(fmt.Sprintf(modulePrefix+"/"+accumulatorPrefix+"/"+positionPrefix+"/%s/%s", accumName, name))
 }
diff --git a/x/concentrated-liquidity/math/precompute.go b/x/concentrated-liquidity/math/precompute.go
new file mode 100644
index 000000000..7996ba305
--- /dev/null
+++ b/x/concentrated-liquidity/math/precompute.go
@@ -0,0 +1,44 @@
+package math
+
+import (
+	sdk "github.com/cosmos/cosmos-sdk/types"
+
+	"github.com/osmosis-labs/osmosis/osmomath"
+)
+
+var (
+	sdkOneInt      = sdk.OneInt()
+	sdkOneDec      = sdk.NewDec(1)
+	sdkNineDec     = sdk.NewDec(9)
+	sdkTenDec      = sdk.NewDec(10)
+	powersOfTen    []sdk.Dec
+	negPowersOfTen []sdk.Dec
+
+	osmomathBigOneDec = osmomath.NewBigDec(1)
+	osmomathBigTenDec = osmomath.NewBigDec(10)
+	bigPowersOfTen    []osmomath.BigDec
+	bigNegPowersOfTen []osmomath.BigDec
+)
+
+// Set precision multipliers
+func init() {
+	negPowersOfTen = make([]sdk.Dec, sdk.Precision+1)
+	for i := 0; i <= sdk.Precision; i++ {
+		negPowersOfTen[i] = sdkOneDec.Quo(sdkTenDec.Power(uint64(i)))
+	}
+	// 10^77 < sdk.MaxInt < 10^78
+	powersOfTen = make([]sdk.Dec, 78)
+	for i := 0; i <= 77; i++ {
+		powersOfTen[i] = sdkTenDec.Power(uint64(i))
+	}
+
+	bigNegPowersOfTen = make([]osmomath.BigDec, osmomath.Precision+1)
+	for i := 0; i <= osmomath.Precision; i++ {
+		bigNegPowersOfTen[i] = osmomathBigOneDec.Quo(osmomathBigTenDec.PowerInteger(uint64(i)))
+	}
+	// 10^308 < osmomath.MaxInt < 10^309
+	bigPowersOfTen = make([]osmomath.BigDec, 309)
+	for i := 0; i <= 308; i++ {
+		bigPowersOfTen[i] = osmomathBigTenDec.PowerInteger(uint64(i))
+	}
+}
diff --git a/x/concentrated-liquidity/math/tick.go b/x/concentrated-liquidity/math/tick.go
index af492f7c0..5a1138a72 100644
--- a/x/concentrated-liquidity/math/tick.go
+++ b/x/concentrated-liquidity/math/tick.go
@@ -9,11 +9,6 @@ import (
 	"github.com/osmosis-labs/osmosis/v15/x/concentrated-liquidity/types"
 )
 
-var (
-	sdkNineDec = sdk.NewDec(9)
-	sdkTenDec  = sdk.NewDec(10)
-)
-
 // TicksToSqrtPrice returns the sqrtPrice for the lower and upper ticks by
 // individually calling `TickToSqrtPrice` method.
 // Returns error if fails to calculate price.
@@ -49,7 +44,7 @@ func TickToSqrtPrice(tickIndex sdk.Int) (sdk.Dec, error) {
 	return sqrtPrice, nil
 }
 
-// TickToSqrtPrice returns the sqrtPrice given a tickIndex
+// TickToPrice returns the price given a tickIndex
 // If tickIndex is zero, the function returns sdk.OneDec().
 func TickToPrice(tickIndex sdk.Int) (price sdk.Dec, err error) {
 	if tickIndex.IsZero() {
@@ -77,7 +72,7 @@ func TickToPrice(tickIndex sdk.Int) (price sdk.Dec, err error) {
 	if tickIndex.IsNegative() {
 		// We must decrement the exponentAtCurrentTick when entering the negative tick range in order to constantly step up in precision when going further down in ticks
 		// Otherwise, from tick 0 to tick -(geometricExponentIncrementDistanceInTicks), we would use the same exponent as the exponentAtPriceOne
-		exponentAtCurrentTick = exponentAtCurrentTick.Sub(sdk.OneInt())
+		exponentAtCurrentTick = exponentAtCurrentTick.Sub(sdkOneInt)
 	}
 
 	// Knowing what our exponentAtCurrentTick is, we can then figure out what power of 10 this exponent corresponds to
@@ -146,16 +141,16 @@ func PriceToTickRoundDown(price sdk.Dec, tickSpacing uint64) (sdk.Int, error) {
 // This is because the sdk.Dec.Power function does not support negative exponents
 func PowTenInternal(exponent sdk.Int) sdk.Dec {
 	if exponent.GTE(sdk.ZeroInt()) {
-		return sdkTenDec.Power(exponent.Uint64())
+		return powersOfTen[exponent.Int64()]
 	}
-	return sdk.OneDec().Quo(sdkTenDec.Power(exponent.Abs().Uint64()))
+	return negPowersOfTen[-exponent.Int64()]
 }
 
 func powTenBigDec(exponent sdk.Int) osmomath.BigDec {
 	if exponent.GTE(sdk.ZeroInt()) {
-		return osmomath.NewBigDec(10).PowerInteger(exponent.Uint64())
+		return bigPowersOfTen[exponent.Int64()]
 	}
-	return osmomath.OneDec().Quo(osmomath.NewBigDec(10).PowerInteger(exponent.Abs().Uint64()))
+	return bigNegPowersOfTen[-exponent.Int64()]
 }
 
 // CalculatePriceToTick takes in a price and returns the corresponding tick index.
@@ -180,23 +175,23 @@ func CalculatePriceToTick(price sdk.Dec) (tickIndex sdk.Int) {
 	// as well as how many ticks that corresponds to
 	// In the opposite direction (price < 1), we do the same thing (just decrement the geometric exponent instead of incrementing).
 	// The only difference is we must reduce the increment distance by a factor of 10.
-	if price.GT(sdk.OneDec()) {
+	if price.GT(sdkOneDec) {
 		for currentPrice.LT(price) {
 			currentAdditiveIncrementInTicks = powTenBigDec(exponentAtCurrentTick)
 			maxPriceForCurrentAdditiveIncrementInTicks := osmomath.BigDecFromSDKDec(geometricExponentIncrementDistanceInTicks).Mul(currentAdditiveIncrementInTicks)
-			currentPrice = currentPrice.Add(maxPriceForCurrentAdditiveIncrementInTicks.SDKDec())
-			exponentAtCurrentTick = exponentAtCurrentTick.Add(sdk.OneInt())
+			currentPrice.AddMut(maxPriceForCurrentAdditiveIncrementInTicks.SDKDec())
+			exponentAtCurrentTick = exponentAtCurrentTick.Add(sdkOneInt)
 			ticksPassed = ticksPassed.Add(geometricExponentIncrementDistanceInTicks.TruncateInt())
 		}
 	} else {
 		// We must decrement the exponentAtCurrentTick by one when traversing negative ticks in order to constantly step up in precision when going further down in ticks
 		// Otherwise, from tick 0 to tick -(geometricExponentIncrementDistanceInTicks), we would use the same exponent as the exponentAtPriceOne
-		exponentAtCurrentTick := exponentAtPriceOne.Sub(sdk.OneInt())
+		exponentAtCurrentTick := exponentAtPriceOne.Sub(sdkOneInt)
 		for currentPrice.GT(price) {
 			currentAdditiveIncrementInTicks = powTenBigDec(exponentAtCurrentTick)
 			maxPriceForCurrentAdditiveIncrementInTicks := osmomath.BigDecFromSDKDec(geometricExponentIncrementDistanceInTicks).Mul(currentAdditiveIncrementInTicks)
-			currentPrice = currentPrice.Sub(maxPriceForCurrentAdditiveIncrementInTicks.SDKDec())
-			exponentAtCurrentTick = exponentAtCurrentTick.Sub(sdk.OneInt())
+			currentPrice.SubMut(maxPriceForCurrentAdditiveIncrementInTicks.SDKDec())
+			exponentAtCurrentTick = exponentAtCurrentTick.Sub(sdkOneInt)
 			ticksPassed = ticksPassed.Sub(geometricExponentIncrementDistanceInTicks.TruncateInt())
 		}
 	}
diff --git a/x/concentrated-liquidity/swaps.go b/x/concentrated-liquidity/swaps.go
index f370c5fb8..17250d66c 100644
--- a/x/concentrated-liquidity/swaps.go
+++ b/x/concentrated-liquidity/swaps.go
@@ -58,14 +58,15 @@ type SwapState struct {
 // when liquidity is positive.
 //
 // If the liquidity is zero, this is a no-op. This case may occur when there is no liquidity
-// between the ticks.This is possible when there are only 2 positions with no overlapping ranges.
+// between the ticks. This is possible when there are only 2 positions with no overlapping ranges.
 // As a result, the range from the end of position one to the beginning of position
 // two has no liquidity and can be skipped.
-// TODO: test
 func (ss *SwapState) updateFeeGrowthGlobal(feeChargeTotal sdk.Dec) {
 	if !ss.liquidity.IsZero() {
-		feeChargePerUnitOfLiquidity := feeChargeTotal.Quo(ss.liquidity)
-		ss.feeGrowthGlobal = ss.feeGrowthGlobal.Add(feeChargePerUnitOfLiquidity)
+		// We round down here since we want to avoid overdistributing (the "fee charge" refers to
+		// the total fees that will be accrued to the fee accumulator)
+		feesAccruedPerUnitOfLiquidity := feeChargeTotal.QuoTruncate(ss.liquidity)
+		ss.feeGrowthGlobal.AddMut(feesAccruedPerUnitOfLiquidity)
 		return
 	}
 }
@@ -83,16 +84,17 @@ func (k Keeper) SwapExactAmountIn(
 		return sdk.Int{}, types.DenomDuplicatedError{TokenInDenom: tokenIn.Denom, TokenOutDenom: tokenOutDenom}
 	}
 
+	// Convert pool interface to CL pool type
 	pool, err := convertPoolInterfaceToConcentrated(poolI)
 	if err != nil {
 		return sdk.Int{}, err
 	}
 
-	// determine if we are swapping asset0 for asset1 or vice versa
+	// Determine if we are swapping asset0 for asset1 or vice versa
 	asset0 := pool.GetToken0()
 	zeroForOne := tokenIn.Denom == asset0
 
-	// change priceLimit based on which direction we are swapping
+	// Change priceLimit based on which direction we are swapping
 	priceLimit := swapstrategy.GetPriceLimit(zeroForOne)
 	tokenIn, tokenOut, _, _, _, err := k.swapOutAmtGivenIn(ctx, sender, pool, tokenIn, tokenOutDenom, swapFee, priceLimit)
 	if err != nil {
@@ -246,6 +248,7 @@ func (k Keeper) CalcInAmtGivenOut(
 // what the updated tick, liquidity, and currentSqrtPrice for the pool would be after this swap.
 // Note this method is non-mutative, so the values returned by CalcOutAmtGivenIn do not get stored
 // Instead, we return writeCtx function so that the caller of this method can decide to write the cached ctx to store or not.
+// Note that passing in 0 for `priceLimit` will result in the price limit being set to the max/min value based on swap direction
 func (k Keeper) calcOutAmtGivenIn(ctx sdk.Context,
 	tokenInMin sdk.Coin,
 	tokenOutDenom string,
@@ -254,6 +257,8 @@ func (k Keeper) calcOutAmtGivenIn(ctx sdk.Context,
 	poolId uint64,
 ) (writeCtx func(), tokenIn, tokenOut sdk.Coin, updatedTick sdk.Int, updatedLiquidity, updatedSqrtPrice sdk.Dec, err error) {
 	ctx, writeCtx = ctx.CacheContext()
+
+	// Get pool and asset info
 	p, err := k.getPoolById(ctx, poolId)
 	if err != nil {
 		return writeCtx, sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, err
@@ -262,40 +267,41 @@ func (k Keeper) calcOutAmtGivenIn(ctx sdk.Context,
 	asset1 := p.GetToken1()
 	tokenAmountInSpecified := tokenInMin.Amount.ToDec()
 
-	// if swapping asset0 for asset1, zeroForOne is true
+	// If swapping asset0 for asset1, zeroForOne is true
 	zeroForOne := tokenInMin.Denom == asset0
 
-	// if priceLimit not set, set to max/min value based on swap direction
+	// If priceLimit not set (i.e. set to zero), set to max/min value based on swap direction
 	if zeroForOne && priceLimit.Equal(sdk.ZeroDec()) {
 		priceLimit = types.MinSpotPrice
 	} else if !zeroForOne && priceLimit.Equal(sdk.ZeroDec()) {
 		priceLimit = types.MaxSpotPrice
 	}
 
-	// take provided price limit and turn this into a sqrt price limit since formulas use sqrtPrice
+	// Take provided price limit and turn this into a sqrt price limit since formulas use sqrtPrice
 	sqrtPriceLimit, err := priceLimit.ApproxSqrt()
 	if err != nil {
 		return writeCtx, sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, fmt.Errorf("issue calculating square root of price limit")
 	}
 
-	// set the swap strategy
+	// Set the swap strategy
 	swapStrategy := swapstrategy.New(zeroForOne, sqrtPriceLimit, k.storeKey, swapFee, p.GetTickSpacing())
 
-	// get current sqrt price from pool
+	// Get current sqrt price from pool and run sanity check that current sqrt price is
+	// on the correct side of the price limit given swap direction.
 	curSqrtPrice := p.GetCurrentSqrtPrice()
 	if err := swapStrategy.ValidateSqrtPrice(sqrtPriceLimit, curSqrtPrice); err != nil {
 		return writeCtx, sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, err
 	}
 
-	// check that the specified tokenIn matches one of the assets in the specified pool
+	// Check that the specified tokenIn matches one of the assets in the specified pool
 	if tokenInMin.Denom != asset0 && tokenInMin.Denom != asset1 {
 		return writeCtx, sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, types.TokenInDenomNotInPoolError{TokenInDenom: tokenInMin.Denom}
 	}
-	// check that the specified tokenOut matches one of the assets in the specified pool
+	// Check that the specified tokenOut matches one of the assets in the specified pool
 	if tokenOutDenom != asset0 && tokenOutDenom != asset1 {
 		return writeCtx, sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, types.TokenOutDenomNotInPoolError{TokenOutDenom: tokenOutDenom}
 	}
-	// check that token in and token out are different denominations
+	// Check that token in and token out are different denominations
 	if tokenInMin.Denom == tokenOutDenom {
 		return writeCtx, sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, types.DenomDuplicatedError{TokenInDenom: tokenInMin.Denom, TokenOutDenom: tokenOutDenom}
 	}
@@ -306,20 +312,21 @@ func (k Keeper) calcOutAmtGivenIn(ctx sdk.Context,
 		amountSpecifiedRemaining: tokenAmountInSpecified, // tokenIn
 		amountCalculated:         sdk.ZeroDec(),          // tokenOut
 		sqrtPrice:                curSqrtPrice,
-		tick:                     swapStrategy.InitializeTickValue(p.GetCurrentTick()),
-		liquidity:                p.GetLiquidity(),
-		feeGrowthGlobal:          sdk.ZeroDec(),
+		// Pad (or don't pad) current tick based on swap direction to avoid off-by-one errors
+		tick:            swapStrategy.InitializeTickValue(p.GetCurrentTick()),
+		liquidity:       p.GetLiquidity(),
+		feeGrowthGlobal: sdk.ZeroDec(),
 	}
 
-	// iterate and update swapState until we swap all tokenIn or we reach the specific sqrtPriceLimit
+	// Iterate and update swapState until we swap all tokenIn or we reach the specific sqrtPriceLimit
 	// TODO: for now, we check if amountSpecifiedRemaining is GT 0.0000001. This is because there are times when the remaining
 	// amount may be extremely small, and that small amount cannot generate and amountIn/amountOut and we are therefore left
 	// in an infinite loop.
 	for swapState.amountSpecifiedRemaining.GT(sdk.SmallestDec()) && !swapState.sqrtPrice.Equal(sqrtPriceLimit) {
-		// log the sqrtPrice we start the iteration with
+		// Log the sqrtPrice we start the iteration with
 		sqrtPriceStart := swapState.sqrtPrice
 
-		// we first check to see what the position of the nearest initialized tick is
+		// We first check to see what the position of the nearest initialized tick is
 		// if zeroForOneStrategy, we look to the left of the tick the current sqrt price is at
 		// if oneForZeroStrategy, we look to the right of the tick the current sqrt price is at
 		// if no ticks are initialized (no users have created liquidity positions) then we return an error
@@ -328,15 +335,16 @@ func (k Keeper) calcOutAmtGivenIn(ctx sdk.Context,
 			return writeCtx, sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, fmt.Errorf("there are no more ticks initialized to fill the swap")
 		}
 
-		// utilizing the next initialized tick, we find the corresponding nextPrice (the target price)
+		// Utilizing the next initialized tick, we find the corresponding nextPrice (the target price).
 		nextTickSqrtPrice, err := math.TickToSqrtPrice(nextTick)
 		if err != nil {
 			return writeCtx, sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, fmt.Errorf("could not convert next tick (%v) to nextSqrtPrice", nextTick)
 		}
 
+		// If nextSqrtPrice exceeds the price limit, we set the nextSqrtPrice to the price limit.
 		sqrtPriceTarget := swapStrategy.GetSqrtTargetPrice(nextTickSqrtPrice)
 
-		// utilizing the bucket's liquidity and knowing the price target, we calculate the how much tokenOut we get from the tokenIn
+		// 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, feeCharge := swapStrategy.ComputeSwapStepOutGivenIn(
 			swapState.sqrtPrice,
@@ -356,30 +364,30 @@ func (k Keeper) calcOutAmtGivenIn(ctx sdk.Context,
 		ctx.Logger().Debug("amountOut", amountOut)
 		ctx.Logger().Debug("feeCharge", feeCharge)
 
-		// update the swapState with the new sqrtPrice from the above swap
+		// 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
+		// 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(feeCharge))
-		// we add the amount of tokens we received (amountOut) from the computeSwapStep above to the amountCalculated accumulator
+		// We add the amount of tokens we received (amountOut) from the computeSwapStep above to the amountCalculated accumulator
 		swapState.amountCalculated = swapState.amountCalculated.Add(amountOut)
 
-		// if the computeSwapStep calculated a sqrtPrice that is equal to the nextSqrtPrice, this means all liquidity in the current
+		// If the computeSwapStep calculated a sqrtPrice that is equal to the nextSqrtPrice, this means all liquidity in the current
 		// 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
+			// Retrieve the liquidity held in the next closest initialized tick
 			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
 			}
 			liquidityNet = swapStrategy.SetLiquidityDeltaSign(liquidityNet)
-			// update the swapState's liquidity with the new tick's liquidity
+			// Update the swapState's liquidity with the new tick's liquidity
 			newLiquidity := math.AddLiquidity(swapState.liquidity, liquidityNet)
 			swapState.liquidity = newLiquidity
 
-			// update the swapState's tick with the tick we retrieved liquidity from
+			// Update the swapState's tick with the tick we retrieved liquidity from
 			swapState.tick = nextTick
 		} else if !sqrtPriceStart.Equal(sqrtPrice) {
-			// otherwise if the sqrtPrice calculated from computeSwapStep does not equal the sqrtPrice we started with at the
+			// Otherwise if the sqrtPrice calculated from computeSwapStep does not equal the sqrtPrice we started with at the
 			// beginning of this iteration, we set the swapState tick to the corresponding tick of the sqrtPrice calculated from computeSwapStep
 			price := sqrtPrice.Mul(sqrtPrice)
 			swapState.tick, err = math.PriceToTickRoundDown(price, p.GetTickSpacing())
@@ -393,10 +401,10 @@ func (k Keeper) calcOutAmtGivenIn(ctx sdk.Context,
 		return writeCtx, sdk.Coin{}, sdk.Coin{}, sdk.Int{}, sdk.Dec{}, sdk.Dec{}, err
 	}
 
-	// coin amounts require int values
-	// round amountIn up to avoid under charging
-	amt0 := tokenAmountInSpecified.Sub(swapState.amountSpecifiedRemaining).Ceil().TruncateInt()
-	// round amountOut down to avoid over refunding.
+	// Coin amounts require int values
+	// Round amountIn up to avoid under charging
+	amt0 := (tokenAmountInSpecified.Sub(swapState.amountSpecifiedRemaining)).Ceil().TruncateInt()
+	// Round amountOut down to avoid over distributing.
 	amt1 := swapState.amountCalculated.TruncateInt()
 
 	ctx.Logger().Debug("final amount in", amt0)
diff --git a/x/concentrated-liquidity/swapstrategy/swap_strategy.go b/x/concentrated-liquidity/swapstrategy/swap_strategy.go
index b9c06dff7..367e88319 100644
--- a/x/concentrated-liquidity/swapstrategy/swap_strategy.go
+++ b/x/concentrated-liquidity/swapstrategy/swap_strategy.go
@@ -64,7 +64,7 @@ type swapStrategy interface {
 	// and subtract from the upper tick to reflect that this new
 	// liquidity would be added when the price crosses the lower tick
 	// going up, and subtracted when the price crosses the upper tick
-	// going up. As a result, the sign depend on the direction we are moving.
+	// going up. As a result, the sign depends on the direction we are moving.
 	// See oneForZeroStrategy or zeroForOneStrategy for implementation details.
 	SetLiquidityDeltaSign(liquidityDelta sdk.Dec) sdk.Dec
 	// ValidateSqrtPrice validates the given square root price
diff --git a/x/gamm/pool-models/internal/cfmm_common/lp.go b/x/gamm/pool-models/internal/cfmm_common/lp.go
index 6ab857391..860b3f6d1 100644
--- a/x/gamm/pool-models/internal/cfmm_common/lp.go
+++ b/x/gamm/pool-models/internal/cfmm_common/lp.go
@@ -11,13 +11,13 @@ import (
 	"github.com/osmosis-labs/osmosis/v15/x/gamm/types"
 )
 
-const errMsgFormatSharesLargerThanMax = "%s resulted shares is larger than the max amount of %s"
+const errMsgFormatSharesLargerThanMax = "cannot exit all shares in a pool. Attempted to exit %s shares, max allowed is %s"
 
 // CalcExitPool returns how many tokens should come out, when exiting k LP shares against a "standard" CFMM
 func CalcExitPool(ctx sdk.Context, pool types.CFMMPoolI, exitingShares sdk.Int, exitFee sdk.Dec) (sdk.Coins, error) {
 	totalShares := pool.GetTotalShares()
 	if exitingShares.GTE(totalShares) {
-		return sdk.Coins{}, errorsmod.Wrapf(types.ErrLimitMaxAmount, errMsgFormatSharesLargerThanMax, exitingShares, totalShares)
+		return sdk.Coins{}, errorsmod.Wrapf(types.ErrLimitMaxAmount, errMsgFormatSharesLargerThanMax, exitingShares, totalShares.Sub(sdk.OneInt()))
 	}
 
 	// refundedShares = exitingShares * (1 - exit fee)
diff --git a/x/pool-incentives/keeper/keeper.go b/x/pool-incentives/keeper/keeper.go
index 7035fe270..48ece964e 100644
--- a/x/pool-incentives/keeper/keeper.go
+++ b/x/pool-incentives/keeper/keeper.go
@@ -217,13 +217,30 @@ func (k Keeper) GetAllGauges(ctx sdk.Context) []incentivestypes.Gauge {
 	return gauges
 }
 
+// IsPoolIncentivized returns a boolean representing whether the given pool ID
+// corresponds to an incentivized pool. It fails quietly by returning false if
+// the pool does not exist or does not have any records, as this is technically
+// equivalent to the pool not being incentivized.
 func (k Keeper) IsPoolIncentivized(ctx sdk.Context, poolId uint64) bool {
-	lockableDurations := k.GetLockableDurations(ctx)
+	pool, err := k.poolmanagerKeeper.GetPool(ctx, poolId)
+	if err != nil {
+		return false
+	}
+	isCLPool := pool.GetType() == poolmanagertypes.Concentrated
+
+	var lockableDurations []time.Duration
+	if isCLPool {
+		incParams := k.incentivesKeeper.GetEpochInfo(ctx)
+		lockableDurations = []time.Duration{incParams.Duration}
+	} else {
+		lockableDurations = k.GetLockableDurations(ctx)
+	}
+
 	distrInfo := k.GetDistrInfo(ctx)
 
 	candidateGaugeIds := []uint64{}
-	for _, lockableDuration := range lockableDurations {
-		gaugeId, err := k.GetPoolGaugeId(ctx, poolId, lockableDuration)
+	for _, gaugeDuration := range lockableDurations {
+		gaugeId, err := k.GetPoolGaugeId(ctx, poolId, gaugeDuration)
 		if err == nil {
 			candidateGaugeIds = append(candidateGaugeIds, gaugeId)
 		}
diff --git a/x/poolmanager/router.go b/x/poolmanager/router.go
index dd2e1f8a2..50f285ab8 100644
--- a/x/poolmanager/router.go
+++ b/x/poolmanager/router.go
@@ -15,13 +15,16 @@ import (
 // 1 << 256 - 1 where 256 is the max bit length defined for sdk.Int
 var intMaxValue = sdk.NewIntFromBigInt(new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)))
 
-// 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.
+// RouteExactAmountIn processes a swap along the given route using the swap function
+// corresponding to poolID's pool type. It takes in the input denom and amount for
+// the initial swap against the first pool and chains the output as the input for the
+// next routed pool until the last pool is reached.
+// Transaction succeeds if final amount out is greater than tokenOutMinAmount defined
+// and no errors are encountered along the way.
 func (k Keeper) RouteExactAmountIn(
 	ctx sdk.Context,
 	sender sdk.AccAddress,
-	routes []types.SwapAmountInRoute,
+	route []types.SwapAmountInRoute,
 	tokenIn sdk.Coin,
 	tokenOutMinAmount sdk.Int,
 ) (tokenOutAmount sdk.Int, err error) {
@@ -31,95 +34,98 @@ func (k Keeper) RouteExactAmountIn(
 		sumOfSwapFees    sdk.Dec
 	)
 
-	route := types.SwapAmountInRoutes(routes)
-	if err := route.Validate(); err != nil {
+	// Ensure that provided route is not empty and has valid denom format.
+	routeStep := types.SwapAmountInRoutes(route)
+	if err := routeStep.Validate(); err != nil {
 		return sdk.Int{}, err
 	}
 
-	// In this loop, we check if:
-	// - the route is of length 2
-	// - route 1 and route 2 don't trade via the same pool
-	// - route 1 contains uosmo
-	// - both route 1 and route 2 are incentivized pools
+	// In this loop (isOsmoRoutedMultihop), we check if:
+	// - the routeStep is of length 2
+	// - routeStep 1 and routeStep 2 don't trade via the same pool
+	// - routeStep 1 contains uosmo
+	// - both routeStep 1 and routeStep 2 are incentivized pools
 	//
 	// If all of the above is true, then we collect the additive and max fee between the
 	// two pools to later calculate the following:
-	// total_swap_fee = total_swap_fee = max(swapfee1, swapfee2)
+	// total_swap_fee = max(swapfee1, swapfee2)
 	// fee_per_pool = total_swap_fee * ((pool_fee) / (swapfee1 + swapfee2))
-	if k.isOsmoRoutedMultihop(ctx, route, routes[0].TokenOutDenom, tokenIn.Denom) {
+	if k.isOsmoRoutedMultihop(ctx, routeStep, route[0].TokenOutDenom, tokenIn.Denom) {
 		isMultiHopRouted = true
-		routeSwapFee, sumOfSwapFees, err = k.getOsmoRoutedMultihopTotalSwapFee(ctx, route)
+		routeSwapFee, sumOfSwapFees, err = k.getOsmoRoutedMultihopTotalSwapFee(ctx, routeStep)
 		if err != nil {
 			return sdk.Int{}, err
 		}
 	}
 
-	for i, route := range routes {
+	// Iterate through the route and execute a series of swaps through each pool.
+	for i, routeStep := range route {
 		// To prevent the multihop swap from being interrupted prematurely, we keep
 		// the minimum expected output at a very low number until the last pool
 		_outMinAmount := sdk.NewInt(1)
-		if len(routes)-1 == i {
+		if len(route)-1 == i {
 			_outMinAmount = tokenOutMinAmount
 		}
 
-		swapModule, err := k.GetPoolModule(ctx, route.PoolId)
+		// Get underlying pool type corresponding to the pool ID at the current routeStep.
+		swapModule, err := k.GetPoolModule(ctx, routeStep.PoolId)
 		if err != nil {
 			return sdk.Int{}, err
 		}
 
 		// Execute the expected swap on the current routed pool
-		pool, poolErr := swapModule.GetPool(ctx, route.PoolId)
+		pool, poolErr := swapModule.GetPool(ctx, routeStep.PoolId)
 		if poolErr != nil {
 			return sdk.Int{}, poolErr
 		}
 
-		// check if pool is active, if not error
+		// Check if pool has swaps enabled.
 		if !pool.IsActive(ctx) {
-			return sdk.Int{}, fmt.Errorf("pool %d is not active", pool.GetId())
+			return sdk.Int{}, types.InactivePoolError{PoolId: pool.GetId()}
 		}
 
 		swapFee := pool.GetSwapFee(ctx)
 
-		// If we determined the route is an osmo multi-hop and both routes are incentivized,
+		// If we determined the routeStep is an osmo multi-hop and both route are incentivized,
 		// we modify the swap fee accordingly.
 		if isMultiHopRouted {
-			swapFee = routeSwapFee.Mul((swapFee.Quo(sumOfSwapFees)))
+			swapFee = routeSwapFee.MulRoundUp((swapFee.QuoRoundUp(sumOfSwapFees)))
 		}
 
-		tokenOutAmount, err = swapModule.SwapExactAmountIn(ctx, sender, pool, tokenIn, route.TokenOutDenom, _outMinAmount, swapFee)
+		tokenOutAmount, err = swapModule.SwapExactAmountIn(ctx, sender, pool, tokenIn, routeStep.TokenOutDenom, _outMinAmount, swapFee)
 		if err != nil {
 			return sdk.Int{}, err
 		}
 
 		// Chain output of current pool as the input for the next routed pool
-		tokenIn = sdk.NewCoin(route.TokenOutDenom, tokenOutAmount)
+		tokenIn = sdk.NewCoin(routeStep.TokenOutDenom, tokenOutAmount)
 	}
 	return tokenOutAmount, nil
 }
 
-// SplitRouteExactAmountIn routes the swap across multiple multihop paths
+// SplitRouteExactAmountIn route the swap across multiple multihop paths
 // to get the desired token out. This is useful for achieving the most optimal execution. However, note that the responsibility
-// of determining the optimal split is left to the client. This method simply routes the swap across the given routes.
-// The routes must end with the same token out and begin with the same token in.
+// of determining the optimal split is left to the client. This method simply route the swap across the given route.
+// The route must end with the same token out and begin with the same token in.
 //
 // It performs the price impact protection check on the combination of tokens out from all multihop paths. The given tokenOutMinAmount
 // is used for comparison.
 //
 // Returns error if:
-//   - routes are empty
-//   - routes contain duplicate multihop paths
-//   - last token out denom is not the same for all multihop paths in route
+//   - route are empty
+//   - route contain duplicate multihop paths
+//   - last token out denom is not the same for all multihop paths in routeStep
 //   - one of the multihop swaps fails for internal reasons
 //   - final token out computed is not positive
 //   - final token out computed is smaller than tokenOutMinAmount
 func (k Keeper) SplitRouteExactAmountIn(
 	ctx sdk.Context,
 	sender sdk.AccAddress,
-	routes []types.SwapAmountInSplitRoute,
+	route []types.SwapAmountInSplitRoute,
 	tokenInDenom string,
 	tokenOutMinAmount sdk.Int,
 ) (sdk.Int, error) {
-	if err := types.ValidateSwapAmountInSplitRoute(routes); err != nil {
+	if err := types.ValidateSwapAmountInSplitRoute(route); err != nil {
 		return sdk.Int{}, err
 	}
 
@@ -131,7 +137,7 @@ func (k Keeper) SplitRouteExactAmountIn(
 		totalOutAmount                 = sdk.ZeroInt()
 	)
 
-	for _, multihopRoute := range routes {
+	for _, multihopRoute := range route {
 		tokenOutAmount, err := k.RouteExactAmountIn(
 			ctx,
 			sender,
@@ -169,23 +175,28 @@ func (k Keeper) SwapExactAmountIn(
 	tokenOutDenom string,
 	tokenOutMinAmount sdk.Int,
 ) (tokenOutAmount sdk.Int, err error) {
+	// Get the pool-specific module implementation to ensure that
+	// swaps are routed to the pool type corresponding to pool ID's pool.
 	swapModule, err := k.GetPoolModule(ctx, poolId)
 	if err != nil {
 		return sdk.Int{}, err
 	}
 
+	// Get pool as a general pool type. Note that the underlying function used
+	// still varies with the pool type.
 	pool, poolErr := swapModule.GetPool(ctx, poolId)
 	if poolErr != nil {
 		return sdk.Int{}, poolErr
 	}
 
-	// check if pool is active, if not error
+	// Check if pool has swaps enabled.
 	if !pool.IsActive(ctx) {
 		return sdk.Int{}, fmt.Errorf("pool %d is not active", pool.GetId())
 	}
 
 	swapFee := pool.GetSwapFee(ctx)
 
+	// routeStep to the pool-specific SwapExactAmountIn implementation.
 	tokenOutAmount, err = swapModule.SwapExactAmountIn(ctx, sender, pool, tokenIn, tokenOutDenom, tokenOutMinAmount, swapFee)
 	if err != nil {
 		return sdk.Int{}, err
@@ -196,7 +207,7 @@ func (k Keeper) SwapExactAmountIn(
 
 func (k Keeper) MultihopEstimateOutGivenExactAmountIn(
 	ctx sdk.Context,
-	routes []types.SwapAmountInRoute,
+	route []types.SwapAmountInRoute,
 	tokenIn sdk.Coin,
 ) (tokenOutAmount sdk.Int, err error) {
 	var (
@@ -213,40 +224,40 @@ func (k Keeper) MultihopEstimateOutGivenExactAmountIn(
 		}
 	}()
 
-	route := types.SwapAmountInRoutes(routes)
-	if err := route.Validate(); err != nil {
+	routeStep := types.SwapAmountInRoutes(route)
+	if err := routeStep.Validate(); err != nil {
 		return sdk.Int{}, err
 	}
 
-	if k.isOsmoRoutedMultihop(ctx, route, routes[0].TokenOutDenom, tokenIn.Denom) {
+	if k.isOsmoRoutedMultihop(ctx, routeStep, route[0].TokenOutDenom, tokenIn.Denom) {
 		isMultiHopRouted = true
-		routeSwapFee, sumOfSwapFees, err = k.getOsmoRoutedMultihopTotalSwapFee(ctx, route)
+		routeSwapFee, sumOfSwapFees, err = k.getOsmoRoutedMultihopTotalSwapFee(ctx, routeStep)
 		if err != nil {
 			return sdk.Int{}, err
 		}
 	}
 
-	for _, route := range routes {
-		swapModule, err := k.GetPoolModule(ctx, route.PoolId)
+	for _, routeStep := range route {
+		swapModule, err := k.GetPoolModule(ctx, routeStep.PoolId)
 		if err != nil {
 			return sdk.Int{}, err
 		}
 
 		// Execute the expected swap on the current routed pool
-		poolI, poolErr := swapModule.GetPool(ctx, route.PoolId)
+		poolI, poolErr := swapModule.GetPool(ctx, routeStep.PoolId)
 		if poolErr != nil {
 			return sdk.Int{}, poolErr
 		}
 
 		swapFee := poolI.GetSwapFee(ctx)
 
-		// If we determined the route is an osmo multi-hop and both routes are incentivized,
+		// If we determined the routeStep is an osmo multi-hop and both route are incentivized,
 		// we modify the swap fee accordingly.
 		if isMultiHopRouted {
 			swapFee = routeSwapFee.Mul((swapFee.Quo(sumOfSwapFees)))
 		}
 
-		tokenOut, err := swapModule.CalcOutAmtGivenIn(ctx, poolI, tokenIn, route.TokenOutDenom, swapFee)
+		tokenOut, err := swapModule.CalcOutAmtGivenIn(ctx, poolI, tokenIn, routeStep.TokenOutDenom, swapFee)
 		if err != nil {
 			return sdk.Int{}, err
 		}
@@ -257,7 +268,7 @@ func (k Keeper) MultihopEstimateOutGivenExactAmountIn(
 		}
 
 		// Chain output of current pool as the input for the next routed pool
-		tokenIn = sdk.NewCoin(route.TokenOutDenom, tokenOutAmount)
+		tokenIn = sdk.NewCoin(routeStep.TokenOutDenom, tokenOutAmount)
 	}
 	return tokenOutAmount, err
 }
@@ -268,13 +279,13 @@ func (k Keeper) MultihopEstimateOutGivenExactAmountIn(
 // Transaction succeeds if the calculated tokenInAmount of the first pool is less than the defined tokenInMaxAmount defined.
 func (k Keeper) RouteExactAmountOut(ctx sdk.Context,
 	sender sdk.AccAddress,
-	routes []types.SwapAmountOutRoute,
+	route []types.SwapAmountOutRoute,
 	tokenInMaxAmount sdk.Int,
 	tokenOut sdk.Coin,
 ) (tokenInAmount sdk.Int, err error) {
 	isMultiHopRouted, routeSwapFee, sumOfSwapFees := false, sdk.Dec{}, sdk.Dec{}
-	route := types.SwapAmountOutRoutes(routes)
-	if err := route.Validate(); err != nil {
+	routeStep := types.SwapAmountOutRoutes(route)
+	if err := routeStep.Validate(); err != nil {
 		return sdk.Int{}, err
 	}
 
@@ -286,29 +297,29 @@ func (k Keeper) RouteExactAmountOut(ctx sdk.Context,
 	}()
 
 	// in this loop, we check if:
-	// - the route is of length 2
-	// - route 1 and route 2 don't trade via the same pool
-	// - route 1 contains uosmo
-	// - both route 1 and route 2 are incentivized pools
+	// - the routeStep is of length 2
+	// - routeStep 1 and routeStep 2 don't trade via the same pool
+	// - routeStep 1 contains uosmo
+	// - both routeStep 1 and routeStep 2 are incentivized pools
 	// if all of the above is true, then we collect the additive and max fee between the two pools to later calculate the following:
 	// total_swap_fee = total_swap_fee = max(swapfee1, swapfee2)
 	// fee_per_pool = total_swap_fee * ((pool_fee) / (swapfee1 + swapfee2))
-	if k.isOsmoRoutedMultihop(ctx, route, routes[0].TokenInDenom, tokenOut.Denom) {
+	if k.isOsmoRoutedMultihop(ctx, routeStep, route[0].TokenInDenom, tokenOut.Denom) {
 		isMultiHopRouted = true
-		routeSwapFee, sumOfSwapFees, err = k.getOsmoRoutedMultihopTotalSwapFee(ctx, route)
+		routeSwapFee, sumOfSwapFees, err = k.getOsmoRoutedMultihopTotalSwapFee(ctx, routeStep)
 		if err != nil {
 			return sdk.Int{}, err
 		}
 	}
 
-	// Determine what the estimated input would be for each pool along the multi-hop route
-	// if we determined the route is an osmo multi-hop and both routes are incentivized,
+	// Determine what the estimated input would be for each pool along the multi-hop routeStep
+	// if we determined the routeStep is an osmo multi-hop and both route are incentivized,
 	// we utilize a separate function that calculates the discounted swap fees
 	var insExpected []sdk.Int
 	if isMultiHopRouted {
-		insExpected, err = k.createOsmoMultihopExpectedSwapOuts(ctx, routes, tokenOut, routeSwapFee, sumOfSwapFees)
+		insExpected, err = k.createOsmoMultihopExpectedSwapOuts(ctx, route, tokenOut, routeSwapFee, sumOfSwapFees)
 	} else {
-		insExpected, err = k.createMultihopExpectedSwapOuts(ctx, routes, tokenOut)
+		insExpected, err = k.createMultihopExpectedSwapOuts(ctx, route, tokenOut)
 	}
 	if err != nil {
 		return sdk.Int{}, err
@@ -322,22 +333,22 @@ func (k Keeper) RouteExactAmountOut(ctx sdk.Context,
 	// Iterates through each routed pool and executes their respective swaps. Note that all of the work to get the return
 	// value of this method is done when we calculate insExpected – this for loop primarily serves to execute the actual
 	// swaps on each pool.
-	for i, route := range routes {
-		swapModule, err := k.GetPoolModule(ctx, route.PoolId)
+	for i, routeStep := range route {
+		swapModule, err := k.GetPoolModule(ctx, routeStep.PoolId)
 		if err != nil {
 			return sdk.Int{}, err
 		}
 
 		_tokenOut := tokenOut
 
-		// If there is one pool left in the route, set the expected output of the current swap
+		// If there is one pool left in the routeStep, set the expected output of the current swap
 		// to the estimated input of the final pool.
-		if i != len(routes)-1 {
-			_tokenOut = sdk.NewCoin(routes[i+1].TokenInDenom, insExpected[i+1])
+		if i != len(route)-1 {
+			_tokenOut = sdk.NewCoin(route[i+1].TokenInDenom, insExpected[i+1])
 		}
 
 		// Execute the expected swap on the current routed pool
-		pool, poolErr := swapModule.GetPool(ctx, route.PoolId)
+		pool, poolErr := swapModule.GetPool(ctx, routeStep.PoolId)
 		if poolErr != nil {
 			return sdk.Int{}, poolErr
 		}
@@ -352,7 +363,7 @@ func (k Keeper) RouteExactAmountOut(ctx sdk.Context,
 			swapFee = routeSwapFee.Mul((swapFee.Quo(sumOfSwapFees)))
 		}
 
-		_tokenInAmount, swapErr := swapModule.SwapExactAmountOut(ctx, sender, pool, route.TokenInDenom, insExpected[i], _tokenOut, swapFee)
+		_tokenInAmount, swapErr := swapModule.SwapExactAmountOut(ctx, sender, pool, routeStep.TokenInDenom, insExpected[i], _tokenOut, swapFee)
 		if swapErr != nil {
 			return sdk.Int{}, swapErr
 		}
@@ -368,29 +379,29 @@ func (k Keeper) RouteExactAmountOut(ctx sdk.Context,
 	return tokenInAmount, nil
 }
 
-// SplitRouteExactAmountOut routes the swap across multiple multihop paths
+// SplitRouteExactAmountOut route the swap across multiple multihop paths
 // to get the desired token in. This is useful for achieving the most optimal execution. However, note that the responsibility
-// of determining the optimal split is left to the client. This method simply routes the swap across the given routes.
-// The routes must end with the same token out and begin with the same token in.
+// of determining the optimal split is left to the client. This method simply route the swap across the given route.
+// The route must end with the same token out and begin with the same token in.
 //
 // It performs the price impact protection check on the combination of tokens in from all multihop paths. The given tokenInMaxAmount
 // is used for comparison.
 //
 // Returns error if:
-//   - routes are empty
-//   - routes contain duplicate multihop paths
-//   - last token out denom is not the same for all multihop paths in route
+//   - route are empty
+//   - route contain duplicate multihop paths
+//   - last token out denom is not the same for all multihop paths in routeStep
 //   - one of the multihop swaps fails for internal reasons
 //   - final token out computed is not positive
 //   - final token out computed is smaller than tokenInMaxAmount
 func (k Keeper) SplitRouteExactAmountOut(
 	ctx sdk.Context,
 	sender sdk.AccAddress,
-	routes []types.SwapAmountOutSplitRoute,
+	route []types.SwapAmountOutSplitRoute,
 	tokenOutDenom string,
 	tokenInMaxAmount sdk.Int,
 ) (sdk.Int, error) {
-	if err := types.ValidateSwapAmountOutSplitRoute(routes); err != nil {
+	if err := types.ValidateSwapAmountOutSplitRoute(route); err != nil {
 		return sdk.Int{}, err
 	}
 
@@ -403,7 +414,7 @@ func (k Keeper) SplitRouteExactAmountOut(
 		totalInAmount                 = sdk.ZeroInt()
 	)
 
-	for _, multihopRoute := range routes {
+	for _, multihopRoute := range route {
 		tokenOutAmount, err := k.RouteExactAmountOut(
 			ctx,
 			sender,
@@ -466,7 +477,7 @@ func (k Keeper) RouteCalculateSpotPrice(
 
 func (k Keeper) MultihopEstimateInGivenExactAmountOut(
 	ctx sdk.Context,
-	routes []types.SwapAmountOutRoute,
+	route []types.SwapAmountOutRoute,
 	tokenOut sdk.Coin,
 ) (tokenInAmount sdk.Int, err error) {
 	isMultiHopRouted, routeSwapFee, sumOfSwapFees := false, sdk.Dec{}, sdk.Dec{}
@@ -480,26 +491,26 @@ func (k Keeper) MultihopEstimateInGivenExactAmountOut(
 		}
 	}()
 
-	route := types.SwapAmountOutRoutes(routes)
-	if err := route.Validate(); err != nil {
+	routeStep := types.SwapAmountOutRoutes(route)
+	if err := routeStep.Validate(); err != nil {
 		return sdk.Int{}, err
 	}
 
-	if k.isOsmoRoutedMultihop(ctx, route, routes[0].TokenInDenom, tokenOut.Denom) {
+	if k.isOsmoRoutedMultihop(ctx, routeStep, route[0].TokenInDenom, tokenOut.Denom) {
 		isMultiHopRouted = true
-		routeSwapFee, sumOfSwapFees, err = k.getOsmoRoutedMultihopTotalSwapFee(ctx, route)
+		routeSwapFee, sumOfSwapFees, err = k.getOsmoRoutedMultihopTotalSwapFee(ctx, routeStep)
 		if err != nil {
 			return sdk.Int{}, err
 		}
 	}
 
-	// Determine what the estimated input would be for each pool along the multi-hop route
-	// if we determined the route is an osmo multi-hop and both routes are incentivized,
+	// Determine what the estimated input would be for each pool along the multi-hop routeStep
+	// if we determined the routeStep is an osmo multi-hop and both route are incentivized,
 	// we utilize a separate function that calculates the discounted swap fees
 	if isMultiHopRouted {
-		insExpected, err = k.createOsmoMultihopExpectedSwapOuts(ctx, routes, tokenOut, routeSwapFee, sumOfSwapFees)
+		insExpected, err = k.createOsmoMultihopExpectedSwapOuts(ctx, route, tokenOut, routeSwapFee, sumOfSwapFees)
 	} else {
-		insExpected, err = k.createMultihopExpectedSwapOuts(ctx, routes, tokenOut)
+		insExpected, err = k.createMultihopExpectedSwapOuts(ctx, route, tokenOut)
 	}
 	if err != nil {
 		return sdk.Int{}, err
@@ -570,11 +581,14 @@ func (k Keeper) isOsmoRoutedMultihop(ctx sdk.Context, route types.MultihopRoute,
 	return route0Incentivized && route1Incentivized
 }
 
+// getOsmoRoutedMultihopTotalSwapFee calculates and returns the average swap fee and the sum of swap fees for
+// a given route. For the former, it sets a lower bound of the highest swap fee pool in the route to ensure total
+// swap fees for a route are never more than halved.
 func (k Keeper) getOsmoRoutedMultihopTotalSwapFee(ctx sdk.Context, route types.MultihopRoute) (
 	totalPathSwapFee sdk.Dec, sumOfSwapFees sdk.Dec, err error,
 ) {
 	additiveSwapFee := sdk.ZeroDec()
-	maxSwapFee := sdk.ZeroDec()
+	highestSwapFee := sdk.ZeroDec()
 
 	for _, poolId := range route.PoolIds() {
 		swapModule, err := k.GetPoolModule(ctx, poolId)
@@ -588,39 +602,45 @@ func (k Keeper) getOsmoRoutedMultihopTotalSwapFee(ctx sdk.Context, route types.M
 		}
 		swapFee := pool.GetSwapFee(ctx)
 		additiveSwapFee = additiveSwapFee.Add(swapFee)
-		maxSwapFee = sdk.MaxDec(maxSwapFee, swapFee)
+		highestSwapFee = sdk.MaxDec(highestSwapFee, swapFee)
 	}
+
+	// We divide by 2 to get the average since OSMO-routed multihops always have exactly 2 pools.
 	averageSwapFee := additiveSwapFee.QuoInt64(2)
-	maxSwapFee = sdk.MaxDec(maxSwapFee, averageSwapFee)
-	return maxSwapFee, additiveSwapFee, nil
+
+	// We take the max here as a guardrail to ensure that there is a lowerbound on the swap fee for the
+	// whole route equivalent to the highest fee pool
+	routeSwapFee := sdk.MaxDec(highestSwapFee, averageSwapFee)
+
+	return routeSwapFee, additiveSwapFee, nil
 }
 
 // createMultihopExpectedSwapOuts defines the output denom and output amount for the last pool in
-// the route of pools the caller is intending to hop through in a fixed-output multihop tx. It estimates the input
-// amount for this last pool and then chains that input as the output of the previous pool in the route, repeating
+// the routeStep of pools the caller is intending to hop through in a fixed-output multihop tx. It estimates the input
+// amount for this last pool and then chains that input as the output of the previous pool in the routeStep, repeating
 // until the first pool is reached. It returns an array of inputs, each of which correspond to a pool ID in the
-// route of pools for the original multihop transaction.
+// routeStep of pools for the original multihop transaction.
 // TODO: test this.
 func (k Keeper) createMultihopExpectedSwapOuts(
 	ctx sdk.Context,
-	routes []types.SwapAmountOutRoute,
+	route []types.SwapAmountOutRoute,
 	tokenOut sdk.Coin,
 ) ([]sdk.Int, error) {
-	insExpected := make([]sdk.Int, len(routes))
-	for i := len(routes) - 1; i >= 0; i-- {
-		route := routes[i]
+	insExpected := make([]sdk.Int, len(route))
+	for i := len(route) - 1; i >= 0; i-- {
+		routeStep := route[i]
 
-		swapModule, err := k.GetPoolModule(ctx, route.PoolId)
+		swapModule, err := k.GetPoolModule(ctx, routeStep.PoolId)
 		if err != nil {
 			return nil, err
 		}
 
-		poolI, err := swapModule.GetPool(ctx, route.PoolId)
+		poolI, err := swapModule.GetPool(ctx, routeStep.PoolId)
 		if err != nil {
 			return nil, err
 		}
 
-		tokenIn, err := swapModule.CalcInAmtGivenOut(ctx, poolI, tokenOut, route.TokenInDenom, poolI.GetSwapFee(ctx))
+		tokenIn, err := swapModule.CalcInAmtGivenOut(ctx, poolI, tokenOut, routeStep.TokenInDenom, poolI.GetSwapFee(ctx))
 		if err != nil {
 			return nil, err
 		}
@@ -635,26 +655,26 @@ func (k Keeper) createMultihopExpectedSwapOuts(
 // createOsmoMultihopExpectedSwapOuts does the same as createMultihopExpectedSwapOuts, however discounts the swap fee
 func (k Keeper) createOsmoMultihopExpectedSwapOuts(
 	ctx sdk.Context,
-	routes []types.SwapAmountOutRoute,
+	route []types.SwapAmountOutRoute,
 	tokenOut sdk.Coin,
 	cumulativeRouteSwapFee, sumOfSwapFees sdk.Dec,
 ) ([]sdk.Int, error) {
-	insExpected := make([]sdk.Int, len(routes))
-	for i := len(routes) - 1; i >= 0; i-- {
-		route := routes[i]
+	insExpected := make([]sdk.Int, len(route))
+	for i := len(route) - 1; i >= 0; i-- {
+		routeStep := route[i]
 
-		swapModule, err := k.GetPoolModule(ctx, route.PoolId)
+		swapModule, err := k.GetPoolModule(ctx, routeStep.PoolId)
 		if err != nil {
 			return nil, err
 		}
 
-		poolI, err := swapModule.GetPool(ctx, route.PoolId)
+		poolI, err := swapModule.GetPool(ctx, routeStep.PoolId)
 		if err != nil {
 			return nil, err
 		}
 
 		swapFee := poolI.GetSwapFee(ctx)
-		tokenIn, err := swapModule.CalcInAmtGivenOut(ctx, poolI, tokenOut, route.TokenInDenom, cumulativeRouteSwapFee.Mul((swapFee.Quo(sumOfSwapFees))))
+		tokenIn, err := swapModule.CalcInAmtGivenOut(ctx, poolI, tokenOut, routeStep.TokenInDenom, cumulativeRouteSwapFee.Mul((swapFee.Quo(sumOfSwapFees))))
 		if err != nil {
 			return nil, err
 		}
diff --git a/x/poolmanager/types/errors.go b/x/poolmanager/types/errors.go
index 784d6eb56..350592395 100644
--- a/x/poolmanager/types/errors.go
+++ b/x/poolmanager/types/errors.go
@@ -125,3 +125,11 @@ type IncorrectPoolAddressError struct {
 func (e IncorrectPoolAddressError) Error() string {
 	return fmt.Sprintf("Pool was attempted to be created with incorrect pool address. Expected (%s), actual (%s)", e.ExpectedPoolAddress, e.ActualPoolAddress)
 }
+
+type InactivePoolError struct {
+	PoolId uint64
+}
+
+func (e InactivePoolError) Error() string {
+	return fmt.Sprintf("Pool %d is not active.", e.PoolId)
+}


Test patch:
diff --git a/app/apptesting/concentrated_liquidity.go b/app/apptesting/concentrated_liquidity.go
index 41502974c..1b192a067 100644
--- a/app/apptesting/concentrated_liquidity.go
+++ b/app/apptesting/concentrated_liquidity.go
@@ -41,6 +41,31 @@ func (s *KeeperTestHelper) PrepareConcentratedPoolWithCoinsAndFullRangePosition(
 	return clPool
 }
 
+// createConcentratedPoolsFromCoinsWithSwapFee creates CL pools from given sets of coins and respective swap fees.
+// Where element 1 of the input corresponds to the first pool created, element 2 to the second pool created etc.
+func (s *KeeperTestHelper) CreateConcentratedPoolsAndFullRangePositionWithSwapFee(poolDenoms [][]string, swapFee []sdk.Dec) {
+	for i, curPoolDenoms := range poolDenoms {
+		s.Require().Equal(2, len(curPoolDenoms))
+		var curSwapFee sdk.Dec
+		if len(swapFee) > i {
+			curSwapFee = swapFee[i]
+		} else {
+			curSwapFee = sdk.ZeroDec()
+		}
+
+		clPool := s.PrepareCustomConcentratedPool(s.TestAccs[0], curPoolDenoms[0], curPoolDenoms[1], DefaultTickSpacing, curSwapFee)
+		fundCoins := sdk.NewCoins(sdk.NewCoin(curPoolDenoms[0], DefaultCoinAmount), sdk.NewCoin(curPoolDenoms[1], DefaultCoinAmount))
+		s.FundAcc(s.TestAccs[0], fundCoins)
+		s.CreateFullRangePosition(clPool, fundCoins)
+	}
+}
+
+// createConcentratedPoolsFromCoins creates CL pools from given sets of coins (with zero swap fees).
+// Where element 1 of the input corresponds to the first pool created, element 2 to the second pool created etc.
+func (s *KeeperTestHelper) CreateConcentratedPoolsAndFullRangePosition(poolDenoms [][]string) {
+	s.CreateConcentratedPoolsAndFullRangePositionWithSwapFee(poolDenoms, []sdk.Dec{sdk.ZeroDec()})
+}
+
 // PrepareConcentratedPoolWithCoinsAndLockedFullRangePosition sets up a concentrated liquidity pool with custom denoms.
 // It also creates a full range position and locks it for 14 days.
 func (s *KeeperTestHelper) PrepareConcentratedPoolWithCoinsAndLockedFullRangePosition(denom1, denom2 string) (types.ConcentratedPoolExtension, uint64, uint64) {
diff --git a/x/concentrated-liquidity/fees_test.go b/x/concentrated-liquidity/fees_test.go
index 42a2c61ff..0ee5d92b9 100644
--- a/x/concentrated-liquidity/fees_test.go
+++ b/x/concentrated-liquidity/fees_test.go
@@ -1516,7 +1516,12 @@ func (s *KeeperTestSuite) TestFunctional_Fees_LP() {
 
 	// Collect fees.
 	feesCollected := s.collectFeesAndCheckInvariance(ctx, 0, DefaultMinTick, DefaultMaxTick, positionIdOne, sdk.NewCoins(), []string{USDC}, [][]sdk.Int{ticksActivatedAfterEachSwap})
-	s.Require().Equal(totalFeesExpected, feesCollected)
+	expectedFeesTruncated := totalFeesExpected
+	for i, feeToken := range totalFeesExpected {
+		// We run expected fees through a cycle of divison and multiplication by liquidity to capture appropriate rounding behavior
+		expectedFeesTruncated[i] = sdk.NewCoin(feeToken.Denom, feeToken.Amount.ToDec().QuoTruncate(liquidity).MulTruncate(liquidity).TruncateInt())
+	}
+	s.Require().Equal(expectedFeesTruncated, feesCollected)
 
 	// Unclaimed rewards should be emptied since fees were collected.
 	s.validatePositionFeeGrowth(pool.GetId(), positionIdOne, cl.EmptyCoins)
diff --git a/x/concentrated-liquidity/math/math_test.go b/x/concentrated-liquidity/math/math_test.go
index 6b21d9f82..f3fcb89bb 100644
--- a/x/concentrated-liquidity/math/math_test.go
+++ b/x/concentrated-liquidity/math/math_test.go
@@ -80,6 +80,49 @@ func (suite *ConcentratedMathTestSuite) TestLiquidity0() {
 	}
 }
 
+func (suite *ConcentratedMathTestSuite) TestAddLiquidity() {
+	testCases := map[string]struct {
+		inputLiqA sdk.Dec
+		inputLiqB sdk.Dec
+
+		expectedOutout sdk.Dec
+	}{
+		"happy path": {
+			inputLiqA: sdk.MustNewDecFromStr("1000000000"),
+			inputLiqB: sdk.MustNewDecFromStr("300000999"),
+
+			expectedOutout: sdk.MustNewDecFromStr("1300000999"),
+		},
+		"second value negative": {
+			inputLiqA: sdk.MustNewDecFromStr("1000000000"),
+			inputLiqB: sdk.MustNewDecFromStr("-300000999"),
+
+			expectedOutout: sdk.MustNewDecFromStr("699999001"),
+		},
+		"first value negative": {
+			inputLiqA: sdk.MustNewDecFromStr("-1000000000"),
+			inputLiqB: sdk.MustNewDecFromStr("300000999"),
+
+			expectedOutout: sdk.MustNewDecFromStr("-699999001"),
+		},
+		"both values negative": {
+			inputLiqA: sdk.MustNewDecFromStr("-1000000000"),
+			inputLiqB: sdk.MustNewDecFromStr("-300000999"),
+
+			expectedOutout: sdk.MustNewDecFromStr("-1300000999"),
+		},
+	}
+
+	for name, tc := range testCases {
+		tc := tc
+
+		suite.Run(name, func() {
+			actualOutput := math.AddLiquidity(tc.inputLiqA, tc.inputLiqB)
+			suite.Require().Equal(tc.expectedOutout, actualOutput)
+		})
+	}
+}
+
 // TestGetNextSqrtPriceFromAmount0RoundingUp tests that getNextSqrtPriceFromAmount0RoundingUp utilizes
 // the current squareRootPrice, liquidity of denom0, and amount of denom0 that still needs
 // to be swapped in order to determine the next squareRootPrice
diff --git a/x/concentrated-liquidity/position_test.go b/x/concentrated-liquidity/position_test.go
index 06d3e683b..70d5e0d3c 100644
--- a/x/concentrated-liquidity/position_test.go
+++ b/x/concentrated-liquidity/position_test.go
@@ -1186,6 +1186,10 @@ func (s *KeeperTestSuite) TestFungifyChargedPositions_SwapAndClaimFees() {
 	// Perform a swap to earn fees
 	swapAmountIn := sdk.NewCoin(ETH, sdk.NewInt(swapAmount))
 	expectedFee := swapAmountIn.Amount.ToDec().Mul(swapFee)
+	// We run expected fees through a cycle of divison and multiplication by liquidity to capture appropriate rounding behavior.
+	// Note that we truncate the int at the end since it is not possible to have a decimal fee amount collected (the QuoTruncate
+	// and MulTruncates are much smaller operations that round down for values past the 18th decimal place).
+	expectedFeeTruncated := expectedFee.QuoTruncate(totalLiquidity).MulTruncate(totalLiquidity).TruncateInt()
 	s.FundAcc(s.TestAccs[0], sdk.NewCoins(swapAmountIn))
 	s.swapAndTrackXTimesInARow(defaultPoolId, swapAmountIn, USDC, types.MinSpotPrice, 1)
 
@@ -1201,7 +1205,7 @@ func (s *KeeperTestSuite) TestFungifyChargedPositions_SwapAndClaimFees() {
 	s.Require().NoError(err)
 
 	// Validate that the correct fee amount was collected.
-	s.Require().Equal(expectedFee, collected.AmountOf(swapAmountIn.Denom).ToDec())
+	s.Require().Equal(expectedFeeTruncated, collected.AmountOf(swapAmountIn.Denom))
 
 	// Check that cannot claim again.
 	collected, err = s.App.ConcentratedLiquidityKeeper.CollectFees(s.Ctx, defaultAddress, newPositionId)
diff --git a/x/concentrated-liquidity/swaps_test.go b/x/concentrated-liquidity/swaps_test.go
index adab84b38..a1285e502 100644
--- a/x/concentrated-liquidity/swaps_test.go
+++ b/x/concentrated-liquidity/swaps_test.go
@@ -2517,6 +2517,12 @@ func (suite *KeeperTestSuite) TestUpdateFeeGrowthGlobal() {
 			// 10 / 10 = 1
 			expectedFeeGrowthGlobal: sdk.OneDec(),
 		},
+		"rounding test: boundary fee growth": {
+			liquidity:      ten.Add(ten).Mul(sdk.NewDec(1e18)),
+			feeChargeTotal: ten,
+			// 10 / (20 * 10^18) = 5 * 10^-19, which we expect to truncate and leave 0.
+			expectedFeeGrowthGlobal: sdk.ZeroDec(),
+		},
 	}
 
 	for name, tc := range tests {
@@ -2616,6 +2622,15 @@ func (suite *KeeperTestSuite) TestUpdatePoolForSwap() {
 			newLiquidity:         sdk.NewDec(2),
 			newSqrtPrice:         sdk.NewDec(2),
 		},
+		"success case with different/uneven numbers": {
+			senderInitialBalance: defaultInitialBalance.Add(defaultInitialBalance...),
+			poolInitialBalance:   defaultInitialBalance,
+			tokenIn:              oneHundredETH.Add(oneHundredETH),
+			tokenOut:             oneHundredUSDC,
+			newCurrentTick:       sdk.NewInt(8),
+			newLiquidity:         sdk.NewDec(37),
+			newSqrtPrice:         sdk.NewDec(91),
+		},
 		"sender does not have enough balance": {
 			senderInitialBalance: defaultInitialBalance,
 			poolInitialBalance:   defaultInitialBalance,
diff --git a/x/pool-incentives/keeper/keeper_test.go b/x/pool-incentives/keeper/keeper_test.go
index cedbd9feb..bfeaadb2f 100644
--- a/x/pool-incentives/keeper/keeper_test.go
+++ b/x/pool-incentives/keeper/keeper_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 	"time"
 
+	sdk "github.com/cosmos/cosmos-sdk/types"
 	"github.com/stretchr/testify/suite"
 
 	"github.com/osmosis-labs/osmosis/v15/app/apptesting"
@@ -11,6 +12,7 @@ import (
 	gammtypes "github.com/osmosis-labs/osmosis/v15/x/gamm/types"
 	incentivestypes "github.com/osmosis-labs/osmosis/v15/x/incentives/types"
 	"github.com/osmosis-labs/osmosis/v15/x/pool-incentives/types"
+	poolincentivestypes "github.com/osmosis-labs/osmosis/v15/x/pool-incentives/types"
 	poolmanagertypes "github.com/osmosis-labs/osmosis/v15/x/poolmanager/types"
 )
 
@@ -316,3 +318,43 @@ func (suite *KeeperTestSuite) TestGetLongestLockableDuration() {
 		})
 	}
 }
+
+func (suite *KeeperTestSuite) TestIsPoolIncentivized() {
+	testCases := []struct {
+		name                   string
+		poolId                 uint64
+		expectedIsIncentivized bool
+	}{
+		{
+			name:                   "Incentivized Pool",
+			poolId:                 1,
+			expectedIsIncentivized: true,
+		},
+		{
+			name:                   "Unincentivized Pool",
+			poolId:                 2,
+			expectedIsIncentivized: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		suite.Run(tc.name, func() {
+			suite.SetupTest()
+			suite.PrepareConcentratedPool()
+
+			suite.App.PoolIncentivesKeeper.SetDistrInfo(suite.Ctx, poolincentivestypes.DistrInfo{
+				TotalWeight: sdk.NewInt(100),
+				Records: []poolincentivestypes.DistrRecord{
+					{
+						GaugeId: tc.poolId,
+						Weight:  sdk.NewInt(50),
+					},
+				},
+			})
+
+			actualIsIncentivized := suite.App.PoolIncentivesKeeper.IsPoolIncentivized(suite.Ctx, tc.poolId)
+			suite.Require().Equal(tc.expectedIsIncentivized, actualIsIncentivized)
+		})
+	}
+
+}
diff --git a/x/poolmanager/export_test.go b/x/poolmanager/export_test.go
index d659850f6..a88f76cfb 100644
--- a/x/poolmanager/export_test.go
+++ b/x/poolmanager/export_test.go
@@ -41,3 +41,7 @@ func (k Keeper) GetAllPoolRoutes(ctx sdk.Context) []types.ModuleRoute {
 func (k Keeper) ValidateCreatedPool(ctx sdk.Context, poolId uint64, pool types.PoolI) error {
 	return k.validateCreatedPool(ctx, poolId, pool)
 }
+
+func (k Keeper) IsOsmoRoutedMultihop(ctx sdk.Context, route types.MultihopRoute, inDenom, outDenom string) (isRouted bool) {
+	return k.isOsmoRoutedMultihop(ctx, route, inDenom, outDenom)
+}
diff --git a/x/poolmanager/keeper_test.go b/x/poolmanager/keeper_test.go
index ce5958b9e..0b9eb7a7f 100644
--- a/x/poolmanager/keeper_test.go
+++ b/x/poolmanager/keeper_test.go
@@ -52,6 +52,19 @@ func (suite *KeeperTestSuite) createBalancerPoolsFromCoinsWithSwapFee(poolCoins
 	}
 }
 
+// createBalancerPoolsFromCoins creates balancer pools from given sets of coins and zero swap fees.
+// Where element 1 of the input corresponds to the first pool created,
+// element 2 to the second pool created, up until the last element.
+func (suite *KeeperTestSuite) createBalancerPoolsFromCoins(poolCoins []sdk.Coins) {
+	for _, curPoolCoins := range poolCoins {
+		suite.FundAcc(suite.TestAccs[0], curPoolCoins)
+		suite.PrepareCustomBalancerPoolFromCoins(curPoolCoins, balancer.PoolParams{
+			SwapFee: sdk.ZeroDec(),
+			ExitFee: sdk.ZeroDec(),
+		})
+	}
+}
+
 func (suite *KeeperTestSuite) TestInitGenesis() {
 	suite.Setup()
 
diff --git a/x/poolmanager/router_test.go b/x/poolmanager/router_test.go
index 7c10e0a05..600b7d579 100644
--- a/x/poolmanager/router_test.go
+++ b/x/poolmanager/router_test.go
@@ -111,6 +111,11 @@ func (suite *KeeperTestSuite) TestGetPoolModule() {
 			poolId:            1,
 			expectedModule:    gammKeeperType,
 		},
+		"valid concentrated liquidity pool": {
+			preCreatePoolType: types.Concentrated,
+			poolId:            1,
+			expectedModule:    concentratedKeeperType,
+		},
 		"non-existent pool": {
 			preCreatePoolType: types.Balancer,
 			poolId:            2,
@@ -127,7 +132,6 @@ func (suite *KeeperTestSuite) TestGetPoolModule() {
 
 			expectError: types.UndefinedRouteError{PoolId: 1, PoolType: types.Balancer},
 		},
-		// TODO: valid concentrated liquidity test case.
 	}
 
 	for name, tc := range tests {
@@ -338,12 +342,14 @@ func (suite *KeeperTestSuite) TestMultihopSwapExactAmountIn() {
 	tests := []struct {
 		name                    string
 		poolCoins               []sdk.Coins
+		poolDenoms              [][]string
 		poolFee                 []sdk.Dec
 		routes                  []types.SwapAmountInRoute
 		incentivizedGauges      []uint64
 		tokenIn                 sdk.Coin
 		tokenOutMinAmount       sdk.Int
 		swapFee                 sdk.Dec
+		isConcentrated          bool
 		expectError             bool
 		expectReducedFeeApplied bool
 	}{
@@ -530,8 +536,50 @@ func (suite *KeeperTestSuite) TestMultihopSwapExactAmountIn() {
 			tokenOutMinAmount:       sdk.NewInt(1),
 			expectReducedFeeApplied: false,
 		},
+		{
+			name:           "[Concentrated] One route: Swap - [foo -> bar], 1 percent fee",
+			isConcentrated: true,
+			poolDenoms: [][]string{
+				{foo, bar},
+			},
+			poolFee: []sdk.Dec{defaultPoolSwapFee},
+			routes: []types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: bar,
+				},
+			},
+			tokenIn:           sdk.NewCoin(foo, sdk.NewInt(100000)),
+			tokenOutMinAmount: sdk.NewInt(1),
+		},
+		{
+			name:           "[Concentrated[ Three routes: Swap - [foo -> uosmo](pool 1) - [uosmo -> baz](pool 2) - [baz -> bar](pool 3), all pools 1 percent fee",
+			isConcentrated: true,
+			poolDenoms: [][]string{
+				{foo, uosmo}, // pool 1.
+				{baz, uosmo}, // pool 2.
+				{bar, baz},   // pool 3.
+			},
+			poolFee: []sdk.Dec{defaultPoolSwapFee, defaultPoolSwapFee, defaultPoolSwapFee},
+			routes: []types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: uosmo,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: baz,
+				},
+				{
+					PoolId:        3,
+					TokenOutDenom: bar,
+				},
+			},
+			incentivizedGauges: []uint64{1, 2, 3, 4, 5, 6},
+			tokenIn:            sdk.NewCoin(foo, sdk.NewInt(100000)),
+			tokenOutMinAmount:  sdk.NewInt(1),
+		},
 		// TODO:
-		// tests for concentrated liquidity
 		// change values in and out to be different with each swap module type
 		// tests for stable-swap pools
 		// edge cases:
@@ -545,7 +593,12 @@ func (suite *KeeperTestSuite) TestMultihopSwapExactAmountIn() {
 			suite.SetupTest()
 			poolmanagerKeeper := suite.App.PoolManagerKeeper
 
-			suite.createBalancerPoolsFromCoinsWithSwapFee(tc.poolCoins, tc.poolFee)
+			if tc.isConcentrated {
+				// create a concentrated pool with a full range position
+				suite.CreateConcentratedPoolsAndFullRangePositionWithSwapFee(tc.poolDenoms, tc.poolFee)
+			} else {
+				suite.createBalancerPoolsFromCoinsWithSwapFee(tc.poolCoins, tc.poolFee)
+			}
 
 			// if test specifies incentivized gauges, set them here
 			if len(tc.incentivizedGauges) > 0 {
@@ -558,7 +611,8 @@ func (suite *KeeperTestSuite) TestMultihopSwapExactAmountIn() {
 				suite.Require().Error(err)
 			} else {
 				// calculate the swap as separate swaps with either the reduced swap fee or normal fee
-				expectedMultihopTokenOutAmount := suite.calcInAmountAsSeparateSwaps(tc.expectReducedFeeApplied, tc.routes, tc.tokenIn)
+				expectedMultihopTokenOutAmount := suite.calcInAmountAsSeparatePoolSwaps(tc.expectReducedFeeApplied, tc.routes, tc.tokenIn)
+
 				// execute the swap
 				multihopTokenOutAmount, err := poolmanagerKeeper.RouteExactAmountIn(suite.Ctx, suite.TestAccs[0], tc.routes, tc.tokenIn, tc.tokenOutMinAmount)
 				// compare the expected tokenOut to the actual tokenOut
@@ -1226,7 +1280,10 @@ func (suite *KeeperTestSuite) calcOutAmountAsSeparateSwaps(osmoFeeReduced bool,
 	}
 }
 
-func (suite *KeeperTestSuite) calcInAmountAsSeparateSwaps(osmoFeeReduced bool, routes []types.SwapAmountInRoute, tokenIn sdk.Coin) sdk.Coin {
+// calcInAmountAsSeparatePoolSwaps calculates the output amount of a series of swaps on PoolManager pools while factoring in reduces swap fee changes.
+// If its GAMM pool functions directly to ensure the poolmanager functions route to the correct modules. It it's CL pool functions directly to ensure the
+// poolmanager functions route to the correct modules.
+func (suite *KeeperTestSuite) calcInAmountAsSeparatePoolSwaps(osmoFeeReduced bool, routes []types.SwapAmountInRoute, tokenIn sdk.Coin) sdk.Coin {
 	cacheCtx, _ := suite.Ctx.CacheContext()
 	if osmoFeeReduced {
 		// extract route from swap
@@ -1237,33 +1294,49 @@ func (suite *KeeperTestSuite) calcInAmountAsSeparateSwaps(osmoFeeReduced bool, r
 		routeSwapFee, sumOfSwapFees, err := suite.App.PoolManagerKeeper.GetOsmoRoutedMultihopTotalSwapFee(suite.Ctx, route)
 		suite.Require().NoError(err)
 		nextTokenIn := tokenIn
+
 		for _, hop := range routes {
-			// extract the current pool's swap fee
-			hopPool, err := suite.App.GAMMKeeper.GetPoolAndPoke(cacheCtx, hop.PoolId)
+			swapModule, err := suite.App.PoolManagerKeeper.GetPoolModule(cacheCtx, hop.PoolId)
 			suite.Require().NoError(err)
-			currentPoolSwapFee := hopPool.GetSwapFee(cacheCtx)
+
+			pool, err := swapModule.GetPool(suite.Ctx, hop.PoolId)
+			suite.Require().NoError(err)
+
 			// utilize the routeSwapFee, sumOfSwapFees, and current pool swap fee to calculate the new reduced swap fee
-			swapFee := routeSwapFee.Mul((currentPoolSwapFee.Quo(sumOfSwapFees)))
+			swapFee := routeSwapFee.Mul(pool.GetSwapFee(cacheCtx).Quo(sumOfSwapFees))
+
 			// we then do individual swaps until we reach the end of the swap route
-			tokenOut, err := suite.App.GAMMKeeper.SwapExactAmountIn(cacheCtx, suite.TestAccs[0], hopPool, nextTokenIn, hop.TokenOutDenom, sdk.OneInt(), swapFee)
+			tokenOut, err := swapModule.SwapExactAmountIn(cacheCtx, suite.TestAccs[0], pool, nextTokenIn, hop.TokenOutDenom, sdk.OneInt(), swapFee)
 			suite.Require().NoError(err)
+
 			nextTokenIn = sdk.NewCoin(hop.TokenOutDenom, tokenOut)
 		}
 		return nextTokenIn
 	} else {
 		nextTokenIn := tokenIn
 		for _, hop := range routes {
-			hopPool, err := suite.App.GAMMKeeper.GetPoolAndPoke(cacheCtx, hop.PoolId)
+			swapModule, err := suite.App.PoolManagerKeeper.GetPoolModule(cacheCtx, hop.PoolId)
 			suite.Require().NoError(err)
-			updatedPoolSwapFee := hopPool.GetSwapFee(cacheCtx)
-			tokenOut, err := suite.App.GAMMKeeper.SwapExactAmountIn(cacheCtx, suite.TestAccs[0], hopPool, nextTokenIn, hop.TokenOutDenom, sdk.OneInt(), updatedPoolSwapFee)
+
+			pool, err := swapModule.GetPool(suite.Ctx, hop.PoolId)
+			suite.Require().NoError(err)
+
+			// utilize the routeSwapFee, sumOfSwapFees, and current pool swap fee to calculate the new reduced swap fee
+			swapFee := pool.GetSwapFee(cacheCtx)
+
+			// we then do individual swaps until we reach the end of the swap route
+			tokenOut, err := swapModule.SwapExactAmountIn(cacheCtx, suite.TestAccs[0], pool, nextTokenIn, hop.TokenOutDenom, sdk.OneInt(), swapFee)
 			suite.Require().NoError(err)
+
 			nextTokenIn = sdk.NewCoin(hop.TokenOutDenom, tokenOut)
+
 		}
 		return nextTokenIn
 	}
 }
 
+// TODO: abstract SwapAgainstBalancerPool and SwapAgainstConcentratedPool
+
 func (suite *KeeperTestSuite) TestSingleSwapExactAmountIn() {
 	tests := []struct {
 		name                   string
@@ -2067,3 +2140,304 @@ func (s *KeeperTestSuite) TestGetTotalPoolLiquidity() {
 		})
 	}
 }
+
+func (suite *KeeperTestSuite) TestIsOsmoRoutedMultihop() {
+	tests := map[string]struct {
+		route                  types.MultihopRoute
+		balancerPoolCoins      []sdk.Coins
+		concentratedPoolDenoms [][]string
+		incentivizedGauges     []uint64
+		inDenom                string
+		outDenom               string
+		expectIsRouted         bool
+	}{
+		"happy path: osmo routed (balancer)": {
+			route: types.SwapAmountInRoutes([]types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: uosmo,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: bar,
+				},
+			}),
+			balancerPoolCoins: []sdk.Coins{
+				sdk.NewCoins(sdk.NewCoin(foo, defaultInitPoolAmount), sdk.NewCoin(uosmo, defaultInitPoolAmount)), // pool 1.
+				sdk.NewCoins(sdk.NewCoin(uosmo, defaultInitPoolAmount), sdk.NewCoin(bar, defaultInitPoolAmount)), // pool 2.
+			},
+			// Note that we incentivize all candidate gauges for the sake of test readability.
+			incentivizedGauges: []uint64{1, 2, 3, 4, 5, 6},
+			inDenom:            foo,
+			outDenom:           bar,
+
+			expectIsRouted: true,
+		},
+		"happy path: osmo routed (balancer, only one active gauge for each pool)": {
+			route: types.SwapAmountInRoutes([]types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: uosmo,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: bar,
+				},
+			}),
+			balancerPoolCoins: []sdk.Coins{
+				sdk.NewCoins(sdk.NewCoin(foo, defaultInitPoolAmount), sdk.NewCoin(uosmo, defaultInitPoolAmount)), // pool 1.
+				sdk.NewCoins(sdk.NewCoin(uosmo, defaultInitPoolAmount), sdk.NewCoin(bar, defaultInitPoolAmount)), // pool 2.
+			},
+			incentivizedGauges: []uint64{1, 4},
+			inDenom:            foo,
+			outDenom:           bar,
+
+			expectIsRouted: true,
+		},
+		"osmo routed (concentrated)": {
+			route: types.SwapAmountInRoutes([]types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: uosmo,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: bar,
+				},
+			}),
+			concentratedPoolDenoms: [][]string{
+				{foo, uosmo}, // pool 1.
+				{uosmo, baz}, // pool 2.
+			},
+			incentivizedGauges: []uint64{1, 2},
+			inDenom:            foo,
+			outDenom:           bar,
+
+			expectIsRouted: true,
+		},
+		"osmo routed (mixed concentrated and balancer)": {
+			route: types.SwapAmountInRoutes([]types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: uosmo,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: bar,
+				},
+			}),
+			concentratedPoolDenoms: [][]string{
+				{foo, uosmo}, // pool 1.
+			},
+			balancerPoolCoins: []sdk.Coins{
+				sdk.NewCoins(sdk.NewCoin(uosmo, defaultInitPoolAmount), sdk.NewCoin(bar, defaultInitPoolAmount)), // pool 2.
+			},
+
+			incentivizedGauges: []uint64{1, 2},
+			inDenom:            foo,
+			outDenom:           bar,
+
+			expectIsRouted: true,
+		},
+		"not osmo routed (single pool)": {
+			route: types.SwapAmountInRoutes([]types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: bar,
+				},
+			}),
+			inDenom:  foo,
+			outDenom: bar,
+
+			expectIsRouted: false,
+		},
+		"not osmo routed (two pools)": {
+			route: types.SwapAmountInRoutes([]types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: bar,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: baz,
+				},
+			}),
+			inDenom:  foo,
+			outDenom: baz,
+
+			expectIsRouted: false,
+		},
+	}
+
+	for name, tc := range tests {
+		suite.Run(name, func() {
+			suite.SetupTest()
+			poolManagerKeeper := suite.App.PoolManagerKeeper
+
+			// Create pools to route through
+			if tc.concentratedPoolDenoms != nil {
+				suite.CreateConcentratedPoolsAndFullRangePosition(tc.concentratedPoolDenoms)
+			}
+
+			if tc.balancerPoolCoins != nil {
+				suite.createBalancerPoolsFromCoins(tc.balancerPoolCoins)
+			}
+
+			// If test specifies incentivized gauges, set them here
+			if len(tc.incentivizedGauges) > 0 {
+				suite.makeGaugesIncentivized(tc.incentivizedGauges)
+			}
+
+			// System under test
+			isRouted := poolManagerKeeper.IsOsmoRoutedMultihop(suite.Ctx, tc.route, tc.inDenom, tc.outDenom)
+
+			// Check output
+			suite.Require().Equal(tc.expectIsRouted, isRouted)
+		})
+	}
+}
+
+func (suite *KeeperTestSuite) TestGetOsmoRoutedMultihopTotalSwapFee() {
+
+	tests := map[string]struct {
+		route                  types.MultihopRoute
+		balancerPoolCoins      []sdk.Coins
+		concentratedPoolDenoms [][]string
+		poolFees               []sdk.Dec
+
+		expectedRouteFee sdk.Dec
+		expectedTotalFee sdk.Dec
+		expectedError    error
+	}{
+		"happy path: balancer route": {
+			route: types.SwapAmountInRoutes([]types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: uosmo,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: bar,
+				},
+			}),
+			poolFees: []sdk.Dec{defaultPoolSwapFee, defaultPoolSwapFee},
+			balancerPoolCoins: []sdk.Coins{
+				sdk.NewCoins(sdk.NewCoin(foo, defaultInitPoolAmount), sdk.NewCoin(uosmo, defaultInitPoolAmount)), // pool 1.
+				sdk.NewCoins(sdk.NewCoin(uosmo, defaultInitPoolAmount), sdk.NewCoin(bar, defaultInitPoolAmount)), // pool 2.
+			},
+
+			expectedRouteFee: defaultPoolSwapFee,
+			expectedTotalFee: defaultPoolSwapFee.Add(defaultPoolSwapFee),
+		},
+		"concentrated route": {
+			route: types.SwapAmountInRoutes([]types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: uosmo,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: bar,
+				},
+			}),
+			poolFees: []sdk.Dec{defaultPoolSwapFee, defaultPoolSwapFee},
+			concentratedPoolDenoms: [][]string{
+				{foo, uosmo}, // pool 1.
+				{uosmo, baz}, // pool 2.
+			},
+
+			expectedRouteFee: defaultPoolSwapFee,
+			expectedTotalFee: defaultPoolSwapFee.Add(defaultPoolSwapFee),
+		},
+		"mixed concentrated and balancer route": {
+			route: types.SwapAmountInRoutes([]types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: uosmo,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: bar,
+				},
+			}),
+			poolFees: []sdk.Dec{defaultPoolSwapFee, defaultPoolSwapFee},
+			concentratedPoolDenoms: [][]string{
+				{foo, uosmo}, // pool 1.
+			},
+			balancerPoolCoins: []sdk.Coins{
+				sdk.NewCoins(sdk.NewCoin(uosmo, defaultInitPoolAmount), sdk.NewCoin(bar, defaultInitPoolAmount)), // pool 2.
+			},
+
+			expectedRouteFee: defaultPoolSwapFee,
+			expectedTotalFee: defaultPoolSwapFee.Add(defaultPoolSwapFee),
+		},
+		"edge case: average fee is lower than highest pool fee": {
+			route: types.SwapAmountInRoutes([]types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: uosmo,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: bar,
+				},
+			}),
+			// Note that pool 2 has 5x the swap fee of pool 1
+			poolFees: []sdk.Dec{defaultPoolSwapFee, defaultPoolSwapFee.Mul(sdk.NewDec(5))},
+			concentratedPoolDenoms: [][]string{
+				{foo, uosmo}, // pool 1.
+				{uosmo, baz}, // pool 2.
+			},
+
+			expectedRouteFee: defaultPoolSwapFee.Mul(sdk.NewDec(5)),
+			expectedTotalFee: defaultPoolSwapFee.Mul(sdk.NewDec(6)),
+		},
+		"error: pool does not exist": {
+			route: types.SwapAmountInRoutes([]types.SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: uosmo,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: bar,
+				},
+			}),
+			poolFees: []sdk.Dec{defaultPoolSwapFee, defaultPoolSwapFee},
+
+			expectedError: types.FailedToFindRouteError{PoolId: 1},
+		},
+	}
+
+	for name, tc := range tests {
+		suite.Run(name, func() {
+			suite.SetupTest()
+			poolManagerKeeper := suite.App.PoolManagerKeeper
+
+			// Create pools for test route
+			if tc.concentratedPoolDenoms != nil {
+				suite.CreateConcentratedPoolsAndFullRangePositionWithSwapFee(tc.concentratedPoolDenoms, tc.poolFees)
+			}
+
+			if tc.balancerPoolCoins != nil {
+				suite.createBalancerPoolsFromCoinsWithSwapFee(tc.balancerPoolCoins, tc.poolFees)
+			}
+
+			// System under test
+			routeFee, totalFee, err := poolManagerKeeper.GetOsmoRoutedMultihopTotalSwapFee(suite.Ctx, tc.route)
+
+			// Assertions
+			if tc.expectedError != nil {
+				suite.Require().Error(err)
+				suite.Require().Equal(tc.expectedError.Error(), err.Error())
+				suite.Require().Equal(sdk.Dec{}, routeFee)
+				suite.Require().Equal(sdk.Dec{}, totalFee)
+				return
+			}
+
+			suite.Require().NoError(err)
+			suite.Require().Equal(tc.expectedRouteFee, routeFee)
+			suite.Require().Equal(tc.expectedTotalFee, totalFee)
+		})
+	}
+}
diff --git a/x/poolmanager/types/routes_test.go b/x/poolmanager/types/routes_test.go
index 35fcb2093..c1dece9f2 100644
--- a/x/poolmanager/types/routes_test.go
+++ b/x/poolmanager/types/routes_test.go
@@ -286,3 +286,66 @@ func TestValidateSwapAmountOutSplitRoute(t *testing.T) {
 		})
 	}
 }
+
+func TestIntermediateDenoms(t *testing.T) {
+
+	tests := map[string]struct {
+		route          SwapAmountInRoutes
+		expectedDenoms []string
+	}{
+		"happy path: one intermediate denom": {
+			route: SwapAmountInRoutes([]SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: bar,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: baz,
+				},
+			}),
+
+			expectedDenoms: []string{bar},
+		},
+		"multiple intermediate denoms": {
+			route: SwapAmountInRoutes([]SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: bar,
+				},
+				{
+					PoolId:        2,
+					TokenOutDenom: baz,
+				},
+				{
+					PoolId:        5,
+					TokenOutDenom: uosmo,
+				},
+				{
+					PoolId:        3,
+					TokenOutDenom: foo,
+				},
+			}),
+
+			expectedDenoms: []string{bar, baz, uosmo},
+		},
+		"no intermediate denoms (single pool)": {
+			route: SwapAmountInRoutes([]SwapAmountInRoute{
+				{
+					PoolId:        1,
+					TokenOutDenom: bar,
+				},
+			}),
+
+			// Note that we expect the function to fail quietly
+			expectedDenoms: nil,
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			actualIntermediateDenoms := tc.route.IntermediateDenoms()
+			require.Equal(t, tc.expectedDenoms, actualIntermediateDenoms)
+		})
+	}
+}