Repository: osmosis-labs/osmosis
Pull request #2908 (2022-09-30 08:44:57)

stableswap: Improve the scaling factor API
<!-- < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < ☺
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: #2903 

## What is the purpose of the change

Context in 2903, makes the API scale to/from BigDec.

Adds rounding mode for Int / U64 -> BigDec conversion

## Testing and Verifying

This updates tests for existing behavior. We need to add tests for other rounding modes.


Patch:
diff --git a/osmomath/rounding_direction.go b/osmomath/rounding_direction.go
new file mode 100644
index 000000000..adef0e4e5
--- /dev/null
+++ b/osmomath/rounding_direction.go
@@ -0,0 +1,43 @@
+package osmomath
+
+import (
+	"errors"
+	"fmt"
+
+	sdk "github.com/cosmos/cosmos-sdk/types"
+)
+
+type RoundingDirection int
+
+const (
+	RoundUp      RoundingDirection = 1
+	RoundDown    RoundingDirection = 2
+	RoundBankers RoundingDirection = 3
+)
+
+func DivIntByU64ToBigDec(i sdk.Int, u uint64, round RoundingDirection) (BigDec, error) {
+	if u == 0 {
+		return BigDec{}, errors.New("div by zero")
+	}
+	d := BigDecFromSDKDec(i.ToDec())
+	if round == RoundUp {
+		return d.QuoRoundUp(NewBigDec(int64(u))), nil
+	} else if round == RoundDown {
+		return d.QuoInt64(int64(u)), nil
+	} else if round == RoundBankers {
+		return d.Quo(NewBigDec(int64(u))), nil
+	}
+	return BigDec{}, fmt.Errorf("invalid rounding mode %d", int(round))
+}
+
+func DivCoinAmtsByU64ToBigDec(coins []sdk.Coin, scales []uint64, round RoundingDirection) ([]BigDec, error) {
+	result := make([]BigDec, len(coins))
+	for i, coin := range coins {
+		res, err := DivIntByU64ToBigDec(coin.Amount, scales[i], round)
+		if err != nil {
+			return nil, err
+		}
+		result[i] = res
+	}
+	return result, nil
+}
diff --git a/x/gamm/pool-models/stableswap/README.md b/x/gamm/pool-models/stableswap/README.md
index 597ad828a..4187a2df1 100644
--- a/x/gamm/pool-models/stableswap/README.md
+++ b/x/gamm/pool-models/stableswap/README.md
@@ -22,17 +22,26 @@ One key concept, is that the pool has a native concept of
 An important concept thats up to now, not been mentioned is how do we set the expected price ratio.
 In the choice of curve section, we see that its the case that when `x_reserves ~= y_reserves`, that spot price is very close to `1`. However, there are a couple issues with just this in practice:
 
-* Precision of pegged coins may differ. e.g. 1 Foo = 10^12 base units, whereas 1 WrappedFoo = 10^6 base units, but  1 Foo should trade at around 1 Wrapped Foo.
-* Related, I could have a token called TwoFoo which should trade around 1 TwoFoo = 2 Foo
-* For staking derivatives, where value accrues within the token, the expected price to concentrate around dynamically changes (very slowly).
+1) Precision of pegged coins may differ. Suppose `1 Foo = 10^12 base units`, whereas `1 WrappedFoo = 10^6 base units`, but `1 Foo` is expected to trade near the price of `1 Wrapped Foo`.
+2) Relatedly, suppose theres a token called `TwoFoo` which should trade around `1 TwoFoo = 2 Foo`
+3) For staking derivatives, where value accrues within the token, the expected price to concentrate around dynamically changes (very slowly).
 
-To handle these cases, we introduce scaling factors. What we do is we have a scaling factor for every asset in the pool (defaulted to 1).
-Then we map from 'true coin reserves' to 'amm math reasoned about reserves', by doing:
-`amm_eq_asset_reserve = true_asset_reserve / asset_scaling_factor`.
-<!-- TODO, cc @alpin we need to think about rounding behavior -->
+To handle these cases, we introduce scaling factors. A scaling factor maps from "base coin units" to "amm math reserve units", by dividing.
+To handle the first case, we would make `Foo` have a scaling factor of `10^6`, and `WrappedFoo` have a scaling factor of `1`.
+Then the reserves we pass into all AMM equations for this pool, would be computed based off the following reserves:
 
-Then we run the AMM equation around `amm_eq_asset_reserve`, to get an `amm_eq_asset_out`.
-Then we get the `true_asset_out = amm_eq_asset_out * asset_out_scaling_factor`
+```python
+Foo_reserves = round(pool.Foo_liquidity / 10^6, RoundingMode)
+WrappedFoo_reserves = round(pool.WrappedFoo_liquidity / 1, RoundingMode)
+```
+
+Similarly all token inputs would be scaled as such.
+The AMM equations need to each ensure that rounding happens correctly,
+for cases where the scaling factor doesn't perfectly divide into the liquidity.
+We detail rounding modes and scaling details as pseudocode in the relevant sections of the spec.
+(And rounding modes for 'descaling' from AMM eq output to real liquidity amounts, via multiplying by the respective scaling factor)
+
+<!-- TODO come back and revise the scaling factor section for clarity -->
 
 ## Algorithm details
 
@@ -121,7 +130,8 @@ The maximal upperbound is obviously unworkable, and in general binary searching
 This would suggest that we should do something smarter to iteratively approach the right value for the upperbound at least. 
 Notice that $h$ is super-linearly related in $y$, and at most cubically related to $y$. 
 This means that $\forall c \in \mathbb{R}^+, c * h(x,y,w) < h(x,c*y,w) < c^3 * h(x,y,w)$. 
-We can use this fact to get a pretty-good initial upperbound guess for $y$ using the linear estimate. In the lowerbound case, we leave it as lower-bounded by $0$, otherwise we would need to take a cubed root to get a better estimate.
+We can use this fact to get a pretty-good initial upperbound guess for $y$ using the linear estimate.
+In the lowerbound case, we leave it as lower-bounded by $0$, otherwise we would need to take a cubed root to get a better estimate.
 
 ```python
 def iterative_search(x_f, y_0, w, k, err_tolerance):
@@ -165,6 +175,19 @@ def binary_search(lowerbound, upperbound, approximation_fn, target, max_iteratio
 
 Detail how we take the previously discussed solver, and build SwapExactAmountIn and SwapExactAmountOut.
 
+##### SwapExactAmountIn
+
+<!-- TODO: Maybe we just use normal pseudocode syntax -->
+```python
+def SwapExactAmountIn(pool, in_coin, out_denom):
+  # Round down as lower reserves -> higher slippage
+  scaledReserves = pool.ScaledLiquidity(RoundingMode.RoundDown)
+  in_amt_scaled = pool.ScaleToken(in_coin)
+  in_reserve, out_reserve = scaledReserves[in_coin.Denom], scaledReserves[out_denom]
+  rem_reserves = { x for x in scaledReserves if (x != in_coin.Denom and x != out_denom) }
+  solveCfmm(out_reserve, in_reserve, remReserves, in_amt_scaled)
+```
+
 ### Spot Price
 
 Spot price for an AMM pool is the derivative of its `CalculateOutAmountGivenIn` equation.
@@ -180,6 +203,9 @@ Then $\text{spot price} = \frac{\text{CalculateOutAmountGivenIn}(\epsilon)}{\eps
 
 We divide this section into two parts, `JoinPoolNoSwap & ExitPool`, and `JoinPool`.
 
+First we recap what are the properties that we'd expect from `JoinPoolNoSwap`, `ExitPool`, and LP shares.
+From this, we then derive what we'd expect for `JoinPool`.
+
 #### JoinPoolNoSwap and ExitPool
 
 Both of these methods can be implemented via generic AMM techniques.
@@ -189,6 +215,10 @@ Both of these methods can be implemented via generic AMM techniques.
 
 The JoinPool API only supports JoinPoolNoSwap if 
 
+#### Join pool single asset in
+
+Couple ways to define JoinPool Exit Pool relation
+
 ## Code structure
 
 ## Testing strategy
diff --git a/x/gamm/pool-models/stableswap/amm.go b/x/gamm/pool-models/stableswap/amm.go
index b23742ecb..66ca0dbb4 100644
--- a/x/gamm/pool-models/stableswap/amm.go
+++ b/x/gamm/pool-models/stableswap/amm.go
@@ -223,7 +223,13 @@ func solveCFMMBinarySearchMulti(constantFunction func(osmomath.BigDec, osmomath.
 	}
 }
 
-func spotPrice(baseReserve, quoteReserve osmomath.BigDec, remReserves []osmomath.BigDec) osmomath.BigDec {
+func (p Pool) spotPrice(baseDenom, quoteDenom string) (sdk.Dec, error) {
+	roundMode := osmomath.RoundBankers // TODO:
+	reserves, err := p.scaledSortedPoolReserves(baseDenom, quoteDenom, roundMode)
+	if err != nil {
+		return sdk.Dec{}, err
+	}
+	baseReserve, quoteReserve, remReserves := reserves[0], reserves[1], reserves[2:]
 	// y = baseAsset, x = quoteAsset
 	// Define f_{y -> x}(a) as the function that outputs the amount of tokens X you'd get by
 	// trading "a" units of Y against the pool, assuming 0 swap fee, at the current liquidity.
@@ -240,19 +246,19 @@ func spotPrice(baseReserve, quoteReserve osmomath.BigDec, remReserves []osmomath
 	// xReserve & yReserve.
 	a := osmomath.OneDec()
 	// no need to divide by a, since a = 1.
-	return solveCfmm(baseReserve, quoteReserve, remReserves, a)
+	bigDec := solveCfmm(baseReserve, quoteReserve, remReserves, a)
+	return bigDec.SDKDec(), nil
 }
 
 // returns outAmt as a decimal
-func (p *Pool) calcOutAmtGivenIn(tokenIn sdk.Coin, tokenOutDenom string, swapFee sdk.Dec) (sdk.Dec, error) {
-	reserves, err := p.scaledSortedPoolReserves(tokenIn.Denom, tokenOutDenom)
+func (p Pool) calcOutAmtGivenIn(tokenIn sdk.Coin, tokenOutDenom string, swapFee sdk.Dec) (sdk.Dec, error) {
+	roundMode := osmomath.RoundDown // TODO:
+	reserves, err := p.scaledSortedPoolReserves(tokenIn.Denom, tokenOutDenom, roundMode)
 	if err != nil {
 		return sdk.Dec{}, err
 	}
-	tokenInSupply := osmomath.BigDecFromSDKDec(reserves[0].Amount)
-	tokenOutSupply := osmomath.BigDecFromSDKDec(reserves[1].Amount)
-	remReserves := osmomath.BigDecFromSDKDecCoinSlice(reserves[2:])
-	tokenInDec := osmomath.BigDecFromSDKDec(tokenIn.Amount.ToDec())
+	tokenInSupply, tokenOutSupply, remReserves := reserves[0], reserves[1], reserves[2:]
+	tokenInDec := osmomath.BigDecFromSDKDec(tokenIn.Amount.ToDec()) // TODO: Round mode
 	// We are solving for the amount of token out, hence x = tokenOutSupply, y = tokenInSupply
 	cfmmOut := solveCfmm(tokenOutSupply, tokenInSupply, remReserves, tokenInDec)
 	outAmt := p.getDescaledPoolAmt(tokenOutDenom, cfmmOut)
@@ -261,19 +267,18 @@ func (p *Pool) calcOutAmtGivenIn(tokenIn sdk.Coin, tokenOutDenom string, swapFee
 
 // returns inAmt as a decimal
 func (p *Pool) calcInAmtGivenOut(tokenOut sdk.Coin, tokenInDenom string, swapFee sdk.Dec) (sdk.Dec, error) {
-	reserves, err := p.scaledSortedPoolReserves(tokenInDenom, tokenOut.Denom)
+	roundMode := osmomath.RoundDown // TODO:
+	reserves, err := p.scaledSortedPoolReserves(tokenInDenom, tokenOut.Denom, roundMode)
 	if err != nil {
 		return sdk.Dec{}, err
 	}
-	tokenInSupply := osmomath.BigDecFromSDKDec(reserves[0].Amount)
-	tokenOutSupply := osmomath.BigDecFromSDKDec(reserves[1].Amount)
-	remReserves := osmomath.BigDecFromSDKDecCoinSlice(reserves[2:])
-	tokenOutAmount := osmomath.BigDecFromSDKDec(tokenOut.Amount.ToDec())
+	tokenInSupply, tokenOutSupply, remReserves := reserves[0], reserves[1], reserves[2:]
+	tokenOutAmount := osmomath.BigDecFromSDKDec(tokenOut.Amount.ToDec()) // TODO: round mode
 
 	// We are solving for the amount of token in, cfmm(x,y) = cfmm(x + x_in, y - y_out)
 	// x = tokenInSupply, y = tokenOutSupply, yIn = -tokenOutAmount
 	cfmmIn := solveCfmm(tokenInSupply, tokenOutSupply, remReserves, tokenOutAmount.Neg())
-	inAmt := p.getDescaledPoolAmt(tokenInDenom, cfmmIn.Neg())
+	inAmt := p.getDescaledPoolAmt(tokenInDenom, cfmmIn.Neg()) // TODO: round mode
 	return inAmt.SDKDec(), nil
 }
 
diff --git a/x/gamm/pool-models/stableswap/pool.go b/x/gamm/pool-models/stableswap/pool.go
index 26aeca514..65dc8e6f4 100644
--- a/x/gamm/pool-models/stableswap/pool.go
+++ b/x/gamm/pool-models/stableswap/pool.go
@@ -104,34 +104,6 @@ func (p Pool) NumAssets() int {
 	return len(p.PoolLiquidity)
 }
 
-// returns pool liquidity of the provided denoms, in the same order the denoms were provided in
-func (p Pool) getPoolAmts(denoms ...string) ([]sdk.Int, error) {
-	result := make([]sdk.Int, len(denoms))
-	poolLiquidity := p.PoolLiquidity
-	for i, d := range denoms {
-		amt := poolLiquidity.AmountOf(d)
-		if amt.IsZero() {
-			return []sdk.Int{}, fmt.Errorf("denom %s does not exist in pool", d)
-		}
-		result[i] = amt
-	}
-	return result, nil
-}
-
-// scaledPoolReserves returns scaled amount of pool liquidity for usage in AMM equations
-func (p Pool) scaledPoolReserves() ([]sdk.DecCoin, error) {
-	scaledReserves := make([]sdk.DecCoin, len(p.PoolLiquidity))
-
-	for i, poolReserve := range p.PoolLiquidity {
-		scalingFactor := p.GetScalingFactorByLiquidityIndex(i)
-		scaledReserves[i] = sdk.NewDecCoinFromDec(
-			poolReserve.Denom,
-			poolReserve.Amount.ToDec().QuoInt64Mut(int64(scalingFactor)))
-	}
-
-	return scaledReserves, nil
-}
-
 // getDescaledPoolAmts gets descaled amount of given denom and amount
 // TODO: Review rounding of this in all contexts
 func (p Pool) getDescaledPoolAmt(denom string, amount osmomath.BigDec) osmomath.BigDec {
@@ -154,37 +126,47 @@ func (p Pool) getLiquidityIndexMap() map[string]int {
 	return liquidityIndexMap
 }
 
-func reorderDecCoinSliceByDenom(coins []sdk.DecCoin, first string, second string) ([]sdk.DecCoin, error) {
-	newCoins := make([]sdk.DecCoin, len(coins))
+func (p Pool) scaledSortedPoolReserves(first string, second string, round osmomath.RoundingDirection) ([]osmomath.BigDec, error) {
+	reorderedLiquidity, reorderedScalingFactors, err := p.reorderReservesAndScalingFactors(first, second)
+	if err != nil {
+		return nil, err
+	}
+	return osmomath.DivCoinAmtsByU64ToBigDec(reorderedLiquidity, reorderedScalingFactors, round)
+}
+
+// reorderReservesAndScalingFactors takes the pool liquidity and scaling factors, and reorders them s.t.
+// reorderedReserves[0] = p.GetLiquidity().AmountOf(first)
+// reorderedScalingFactors[0] = p.ScalingFactors[p.getLiquidityIndexMap()[first]]
+// and the same for index 1, and second.
+//
+// The remainder of the lists includes every remaining (reserve asset, scaling factor) pair,
+// in a deterministic order.
+//
+// Returns an error if the pool does not contain either of first or second.
+func (p Pool) reorderReservesAndScalingFactors(first string, second string) ([]sdk.Coin, []uint64, error) {
+	coins := p.PoolLiquidity
+	scalingFactors := p.ScalingFactor
+	reorderedReserves := make([]sdk.Coin, len(coins))
+	reorderedScalingFactors := make([]uint64, len(coins))
 	curIndex := 2
-	for _, coin := range coins {
+	for i, coin := range coins {
 		if coin.Denom == first {
-			newCoins[0] = coin
+			reorderedReserves[0] = coin
+			reorderedScalingFactors[0] = scalingFactors[i]
 		} else if coin.Denom == second {
-			newCoins[1] = coin
+			reorderedReserves[1] = coin
+			reorderedScalingFactors[1] = scalingFactors[i]
 		} else {
-			newCoins[curIndex] = coin
+			// if we hit this case, then oneof first or second is not in pool liquidity
+			if curIndex == len(coins) {
+				return nil, nil, fmt.Errorf("one of denom (%s, %s) not found in pool liquidity", first, second)
+			}
+			reorderedReserves[curIndex] = coin
+			reorderedScalingFactors[curIndex] = scalingFactors[i]
 			curIndex += 1
 		}
 	}
-	if (newCoins[0] == sdk.DecCoin{}) {
-		return nil, fmt.Errorf("denom %s not found in pool liquidity", first)
-	} else if (newCoins[1] == sdk.DecCoin{}) {
-		return nil, fmt.Errorf("denom %s not found in pool liquidity", second)
-	}
-	return newCoins, nil
-}
-
-func (p Pool) scaledSortedPoolReserves(first string, second string) ([]sdk.DecCoin, error) {
-	scaledReserves, err := p.scaledPoolReserves()
-	if err != nil {
-		return nil, err
-	}
-	scaledReserves, err = reorderDecCoinSliceByDenom(scaledReserves, first, second)
-	if err != nil {
-		return nil, err
-	}
-	return scaledReserves, nil
+	return reorderedReserves, reorderedScalingFactors, nil
 }
 
 // updatePoolLiquidityForSwap updates the pool liquidity.
@@ -278,20 +260,7 @@ func (p *Pool) SwapInAmtGivenOut(ctx sdk.Context, tokenOut sdk.Coins, tokenInDen
 }
 
 func (p Pool) SpotPrice(ctx sdk.Context, baseAssetDenom string, quoteAssetDenom string) (sdk.Dec, error) {
-	scaledSortedReserves, err := p.scaledSortedPoolReserves(baseAssetDenom, quoteAssetDenom)
-	if err != nil {
-		return sdk.Dec{}, err
-	}
-
-	baseScaledAmount := osmomath.BigDecFromSDKDec(scaledSortedReserves[0].Amount)
-	quoteScaledAmount := osmomath.BigDecFromSDKDec(scaledSortedReserves[0].Amount)
-	remScaledAmounts := osmomath.BigDecFromSDKDecCoinSlice(scaledSortedReserves[2:])
-	scaledSpotPrice := spotPrice(baseScaledAmount, quoteScaledAmount, remScaledAmounts)
-	// TODO: I don't think this is right for descaling spot price
-	spotPrice := p.getDescaledPoolAmt(baseAssetDenom, scaledSpotPrice)
-	spotPriceSdkDec := spotPrice.SDKDec()
-
-	return spotPriceSdkDec, nil
+	return p.spotPrice(baseAssetDenom, quoteAssetDenom)
 }
 
 func (p Pool) Copy() Pool {


Test patch:
diff --git a/osmomath/rounding_direction_test.go b/osmomath/rounding_direction_test.go
new file mode 100644
index 000000000..8668c84c6
--- /dev/null
+++ b/osmomath/rounding_direction_test.go
@@ -0,0 +1,52 @@
+package osmomath
+
+import (
+	"fmt"
+	"testing"
+
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	"github.com/stretchr/testify/require"
+
+	"github.com/osmosis-labs/osmosis/v12/app/apptesting/osmoassert"
+)
+
+func TestDivIntByU64ToBigDec(t *testing.T) {
+	type testcase struct {
+		i      sdk.Int
+		u      uint64
+		round  RoundingDirection
+		want   BigDec
+		expErr bool
+	}
+	tests := map[string]testcase{
+		"div by zero": {sdk.NewInt(5), 0, RoundUp, BigDec{}, true},
+		"5/3 round up": {sdk.NewInt(5), 3, RoundUp,
+			MustNewDecFromStr("1.666666666666666666666666666666666667"), false},
+		"5/3 round down": {sdk.NewInt(5), 3, RoundDown,
+			MustNewDecFromStr("1.666666666666666666666666666666666666"), false},
+		"5/3 round banker": {sdk.NewInt(5), 3, RoundBankers,
+			MustNewDecFromStr("1.666666666666666666666666666666666667"), false},
+		"7/3 round up": {sdk.NewInt(7), 3, RoundUp,
+			MustNewDecFromStr("2.333333333333333333333333333333333334"), false},
+		"7/3 round down": {sdk.NewInt(7), 3, RoundDown,
+			MustNewDecFromStr("2.333333333333333333333333333333333333"), false},
+		"7/3 round banker": {sdk.NewInt(7), 3, RoundBankers,
+			MustNewDecFromStr("2.333333333333333333333333333333333333"), false},
+	}
+	addTCForAllRoundingModes := func(prefix string, i sdk.Int, u uint64, want BigDec) {
+		for round := 1; round < 4; round++ {
+			tests[fmt.Sprintf("%s rounding=%d", prefix, round)] =
+				testcase{i, u, RoundingDirection(round), want, false}
+		}
+	}
+	addTCForAllRoundingModes("odd divided by 2", sdk.NewInt(5), 2, NewDecWithPrec(25, 1))
+
+	for name, tt := range tests {
+		fmt.Println("start")
+		t.Run(name, func(t *testing.T) {
+			got, err := DivIntByU64ToBigDec(tt.i, tt.u, tt.round)
+			require.Equal(t, tt.want, got)
+			osmoassert.ConditionalError(t, tt.expErr, err)
+		})
+	}
+}
diff --git a/x/gamm/pool-models/stableswap/pool_test.go b/x/gamm/pool-models/stableswap/pool_test.go
index 6ccd810fe..94d6fcbe4 100644
--- a/x/gamm/pool-models/stableswap/pool_test.go
+++ b/x/gamm/pool-models/stableswap/pool_test.go
@@ -41,116 +41,174 @@ var (
 	)
 )
 
+// we create a pool struct directly to bypass checks in NewStableswapPool()
+func poolStructFromAssets(assets sdk.Coins, scalingFactors []uint64) Pool {
+	p := Pool{
+		Address:            types.NewPoolAddress(defaultPoolId).String(),
+		Id:                 defaultPoolId,
+		PoolParams:         defaultStableswapPoolParams,
+		TotalShares:        sdk.NewCoin(types.GetPoolShareDenom(defaultPoolId), types.InitPoolSharesSupply),
+		PoolLiquidity:      assets,
+		ScalingFactor:      scalingFactors,
+		FuturePoolGovernor: defaultFutureGovernor,
+	}
+	return p
+}
+
+func TestReorderReservesAndScalingFactors(t *testing.T) {
+	tests := map[string]struct {
+		denoms                [2]string
+		poolAssets            sdk.Coins
+		scalingFactors        []uint64
+		reordedReserves       []sdk.Coin
+		reordedScalingFactors []uint64
+		expError              bool
+	}{
+		"two of 5 assets in pool": {
+			denoms:         [2]string{"asset/c", "asset/b"},
+			poolAssets:     fiveUnevenStablePoolAssets,
+			scalingFactors: []uint64{1, 2, 3, 4, 5},
+			reordedReserves: []sdk.Coin{
+				sdk.NewInt64Coin("asset/c", 3000000000),
+				sdk.NewInt64Coin("asset/b", 2000000000),
+				sdk.NewInt64Coin("asset/a", 1000000000),
+				sdk.NewInt64Coin("asset/d", 4000000000),
+				sdk.NewInt64Coin("asset/e", 5000000000)},
+			reordedScalingFactors: []uint64{3, 2, 1, 4, 5},
+		},
+		"two of 5 assets in pool v2": {
+			denoms:         [2]string{"asset/e", "asset/b"},
+			poolAssets:     fiveUnevenStablePoolAssets,
+			scalingFactors: []uint64{1, 2, 3, 4, 5},
+			reordedReserves: []sdk.Coin{
+				sdk.NewInt64Coin("asset/e", 5000000000),
+				sdk.NewInt64Coin("asset/b", 2000000000),
+				sdk.NewInt64Coin("asset/a", 1000000000),
+				sdk.NewInt64Coin("asset/c", 3000000000),
+				sdk.NewInt64Coin("asset/d", 4000000000)},
+			reordedScalingFactors: []uint64{5, 2, 1, 3, 4},
+		},
+		"asset 1 doesn't exist": {
+			denoms:         [2]string{"foo", "asset/b"},
+			poolAssets:     fiveUnevenStablePoolAssets,
+			scalingFactors: []uint64{1, 2, 3, 4, 5},
+			expError:       true,
+		},
+		"asset 2 doesn't exist": {
+			denoms:         [2]string{"asset/a", "foo"},
+			poolAssets:     fiveUnevenStablePoolAssets,
+			scalingFactors: []uint64{1, 2, 3, 4, 5},
+			expError:       true,
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			p := poolStructFromAssets(tc.poolAssets, tc.scalingFactors)
+
+			reserves, factors, err := p.reorderReservesAndScalingFactors(tc.denoms[0], tc.denoms[1])
+			if !tc.expError {
+				require.Equal(t, tc.reordedReserves, reserves)
+				require.Equal(t, tc.reordedScalingFactors, factors)
+			}
+			osmoassert.ConditionalError(t, tc.expError, err)
+		})
+	}
+}
+
 func TestScaledSortedPoolReserves(t *testing.T) {
-	baseEvenAmt := sdk.NewDec(1000000000)
+	baseEvenAmt := osmomath.NewBigDec(1000000000)
 	tests := map[string]struct {
 		denoms         [2]string
+		roundMode      osmomath.RoundingDirection
 		poolAssets     sdk.Coins
 		scalingFactors []uint64
-		expReserves    []sdk.DecCoin
-		expPanic       bool
+		expReserves    []osmomath.BigDec
+		expError       bool
 	}{
 		// sanity checks, default scaling factors
 		"even two-asset pool with default scaling factors": {
 			denoms:         [2]string{"foo", "bar"},
 			poolAssets:     twoEvenStablePoolAssets,
 			scalingFactors: defaultTwoAssetScalingFactors,
-			expReserves:    []sdk.DecCoin{{"foo", baseEvenAmt}, {"bar", sdk.NewDec(1000000000)}},
-			expPanic:       false,
+			expReserves:    []osmomath.BigDec{baseEvenAmt, baseEvenAmt},
 		},
 		"uneven two-asset pool with default scaling factors": {
 			denoms:         [2]string{"foo", "bar"},
 			poolAssets:     twoUnevenStablePoolAssets,
 			scalingFactors: defaultTwoAssetScalingFactors,
-			expReserves:    []sdk.DecCoin{{"foo", sdk.NewDec(2000000000)}, {"bar", sdk.NewDec(1000000000)}},
-			expPanic:       false,
+			expReserves:    []osmomath.BigDec{baseEvenAmt.MulInt64(2), baseEvenAmt},
 		},
 		"even two-asset pool with even scaling factors greater than 1": {
 			denoms:         [2]string{"foo", "bar"},
 			poolAssets:     twoEvenStablePoolAssets,
 			scalingFactors: []uint64{10, 10},
-			expReserves:    []sdk.DecCoin{{"foo", sdk.NewDec(100000000)}, {"bar", sdk.NewDec(100000000)}},
-			expPanic:       false,
+			expReserves:    []osmomath.BigDec{baseEvenAmt.QuoInt64(10), baseEvenAmt.QuoInt64(10)},
 		},
 		"even two-asset pool with uneven scaling factors greater than 1": {
 			denoms:         [2]string{"foo", "bar"},
 			poolAssets:     twoUnevenStablePoolAssets,
 			scalingFactors: []uint64{10, 5},
-			expReserves: []sdk.DecCoin{sdk.NewInt64DecCoin("foo", 2000000000/5),
-				sdk.NewInt64DecCoin("bar", 1000000000/10)},
-			expPanic: false,
+			expReserves: []osmomath.BigDec{
+				osmomath.NewBigDec(2000000000 / 5), osmomath.NewBigDec(1000000000 / 10)},
 		},
 		"even two-asset pool with even, massive scaling factors greater than 1": {
 			denoms:         [2]string{"foo", "bar"},
 			poolAssets:     twoEvenStablePoolAssets,
 			scalingFactors: []uint64{10000000000, 10000000000},
-			expReserves:    []sdk.DecCoin{{"foo", sdk.NewDecWithPrec(1, 1)}, {"bar", sdk.NewDecWithPrec(1, 1)}},
-			expPanic:       false,
+			expReserves:    []osmomath.BigDec{osmomath.NewDecWithPrec(1, 1), osmomath.NewDecWithPrec(1, 1)},
 		},
 		"five asset pool, scaling factors = 1": {
 			denoms:         [2]string{"asset/c", "asset/d"},
 			poolAssets:     fiveUnevenStablePoolAssets,
 			scalingFactors: []uint64{1, 1, 1, 1, 1},
-			expReserves: []sdk.DecCoin{
-				{"asset/c", baseEvenAmt.MulInt64(3)},
-				{"asset/d", baseEvenAmt.MulInt64(4)},
-				{"asset/a", baseEvenAmt},
-				{"asset/b", baseEvenAmt.MulInt64(2)},
-				{"asset/e", baseEvenAmt.MulInt64(5)}},
-			expPanic: false,
+			expReserves: []osmomath.BigDec{
+				baseEvenAmt.MulInt64(3),  // {"asset/c", baseEvenAmt.MulInt64(3)},
+				baseEvenAmt.MulInt64(4),  // {"asset/d", baseEvenAmt.MulInt64(4)},
+				baseEvenAmt,              // {"asset/a", baseEvenAmt},
+				baseEvenAmt.MulInt64(2),  // {"asset/b", baseEvenAmt.MulInt64(2)},
+				baseEvenAmt.MulInt64(5)}, // {"asset/e", baseEvenAmt.MulInt64(5)}},
 		},
 		"five asset pool, scaling factors = 1,2,3,4,5": {
 			denoms:         [2]string{"asset/a", "asset/e"},
 			poolAssets:     fiveUnevenStablePoolAssets,
 			scalingFactors: []uint64{1, 2, 3, 4, 5},
-			expReserves: []sdk.DecCoin{
-				{"asset/a", baseEvenAmt},
-				{"asset/e", baseEvenAmt},
-				{"asset/b", baseEvenAmt},
-				{"asset/c", baseEvenAmt},
-				{"asset/d", baseEvenAmt}},
-			expPanic: false,
+			expReserves: []osmomath.BigDec{
+				baseEvenAmt,  // {"asset/a", baseEvenAmt},
+				baseEvenAmt,  // {"asset/e", baseEvenAmt},
+				baseEvenAmt,  // {"asset/b", baseEvenAmt},
+				baseEvenAmt,  // {"asset/c", baseEvenAmt},
+				baseEvenAmt}, // {"asset/d", baseEvenAmt}},
 		},
 		"max scaling factors": {
 			denoms:         [2]string{"foo", "bar"},
 			poolAssets:     twoEvenStablePoolAssets,
 			scalingFactors: []uint64{(1 << 62), (1 << 62)},
-			expReserves: []sdk.DecCoin{
-				{"foo", sdk.NewDec(1000000000).QuoInt64(int64(1 << 62))},
-				{"bar", sdk.NewDec(1000000000).QuoInt64(int64(1 << 62))}},
-			expPanic: false,
+			expReserves: []osmomath.BigDec{
+				osmomath.NewBigDec(1000000000).QuoInt64(int64(1 << 62)),
+				osmomath.NewBigDec(1000000000).QuoInt64(int64(1 << 62))},
 		},
 		"zero scaling factor": {
 			denoms:         [2]string{"foo", "bar"},
 			poolAssets:     twoEvenStablePoolAssets,
 			scalingFactors: []uint64{0, 1},
-			expPanic:       true,
+			expError:       true,
 		},
 	}
 	// TODO: Add for loop for trying to re-order test cases
 
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
-			// system under test
-			sut := func() {
-				// we create the pool directly to bypass checks in NewStableswapPool()
-				p := Pool{
-					Address:            types.NewPoolAddress(defaultPoolId).String(),
-					Id:                 defaultPoolId,
-					PoolParams:         defaultStableswapPoolParams,
-					TotalShares:        sdk.NewCoin(types.GetPoolShareDenom(defaultPoolId), types.InitPoolSharesSupply),
-					PoolLiquidity:      tc.poolAssets,
-					ScalingFactor:      tc.scalingFactors,
-					FuturePoolGovernor: defaultFutureGovernor,
-				}
-
-				reserves, err := p.scaledSortedPoolReserves(tc.denoms[0], tc.denoms[1])
+			if tc.roundMode == 0 {
+				tc.roundMode = osmomath.RoundBankers
+			}
+			p := poolStructFromAssets(tc.poolAssets, tc.scalingFactors)
 
-				require.NoError(t, err, "test: %s", name)
+			reserves, err := p.scaledSortedPoolReserves(tc.denoms[0], tc.denoms[1], tc.roundMode)
+			if !tc.expError {
 				require.Equal(t, tc.expReserves, reserves)
 			}
-
-			osmoassert.ConditionalPanic(t, tc.expPanic, sut)
+			osmoassert.ConditionalError(t, tc.expError, err)
 		})
 	}
 }