Skip to content

Commit

Permalink
stableswap: Add multi-asset support threading, via scaled reserve API…
Browse files Browse the repository at this point in the history
… change (#2896)

* Add multi-asset support for spot price, change scaled reserves API

* Add test for multi-asset pool
  • Loading branch information
ValarDragon authored Sep 30, 2022
1 parent 3e6c814 commit bd9c40b
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 95 deletions.
10 changes: 10 additions & 0 deletions osmomath/decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,16 @@ func BigDecFromSDKDecSlice(ds []sdk.Dec) []BigDec {
return result
}

// BigDecFromSdkDecSlice returns the []BigDec representation of an []SDKDec.
// Values in any additional decimal places are truncated.
func BigDecFromSDKDecCoinSlice(ds []sdk.DecCoin) []BigDec {
result := make([]BigDec, len(ds))
for i, d := range ds {
result[i] = NewDecFromBigIntWithPrec(d.Amount.BigInt(), sdk.Precision)
}
return result
}

// ____
// __| |__ "chop 'em
// ` \ round!"
Expand Down
25 changes: 15 additions & 10 deletions x/gamm/pool-models/stableswap/amm.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func solveCFMMBinarySearchMulti(constantFunction func(osmomath.BigDec, osmomath.
}
}

func spotPrice(baseReserve, quoteReserve osmomath.BigDec) osmomath.BigDec {
func spotPrice(baseReserve, quoteReserve osmomath.BigDec, remReserves []osmomath.BigDec) osmomath.BigDec {
// 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.
Expand All @@ -240,34 +240,39 @@ func spotPrice(baseReserve, quoteReserve osmomath.BigDec) osmomath.BigDec {
// xReserve & yReserve.
a := osmomath.OneDec()
// no need to divide by a, since a = 1.
return solveCfmm(baseReserve, quoteReserve, []osmomath.BigDec{}, a)
return solveCfmm(baseReserve, quoteReserve, remReserves, a)
}

// returns outAmt as a decimal
func (p *Pool) calcOutAmtGivenIn(tokenIn sdk.Coin, tokenOutDenom string, swapFee sdk.Dec) (sdk.Dec, error) {
reserves, err := p.getScaledPoolAmts(tokenIn.Denom, tokenOutDenom)
reserves, err := p.scaledSortedPoolReserves(tokenIn.Denom, tokenOutDenom)
if err != nil {
return sdk.Dec{}, err
}
tokenInSupply, tokenOutSupply := reserves[0], reserves[1]
remReserves := osmomath.BigDecFromSDKDecSlice(reserves[2:])
tokenInSupply := osmomath.BigDecFromSDKDec(reserves[0].Amount)
tokenOutSupply := osmomath.BigDecFromSDKDec(reserves[1].Amount)
remReserves := osmomath.BigDecFromSDKDecCoinSlice(reserves[2:])
tokenInDec := osmomath.BigDecFromSDKDec(tokenIn.Amount.ToDec())
// We are solving for the amount of token out, hence x = tokenOutSupply, y = tokenInSupply
cfmmOut := solveCfmm(osmomath.BigDecFromSDKDec(tokenOutSupply), osmomath.BigDecFromSDKDec(tokenInSupply), remReserves, osmomath.BigDecFromSDKDec(tokenIn.Amount.ToDec()))
cfmmOut := solveCfmm(tokenOutSupply, tokenInSupply, remReserves, tokenInDec)
outAmt := p.getDescaledPoolAmt(tokenOutDenom, cfmmOut)
return outAmt.SDKDec(), nil
}

// returns inAmt as a decimal
func (p *Pool) calcInAmtGivenOut(tokenOut sdk.Coin, tokenInDenom string, swapFee sdk.Dec) (sdk.Dec, error) {
reserves, err := p.getScaledPoolAmts(tokenInDenom, tokenOut.Denom)
reserves, err := p.scaledSortedPoolReserves(tokenInDenom, tokenOut.Denom)
if err != nil {
return sdk.Dec{}, err
}
tokenInSupply, tokenOutSupply := reserves[0], reserves[1]
remReserves := osmomath.BigDecFromSDKDecSlice(reserves[2:])
tokenInSupply := osmomath.BigDecFromSDKDec(reserves[0].Amount)
tokenOutSupply := osmomath.BigDecFromSDKDec(reserves[1].Amount)
remReserves := osmomath.BigDecFromSDKDecCoinSlice(reserves[2:])
tokenOutAmount := osmomath.BigDecFromSDKDec(tokenOut.Amount.ToDec())

// 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(osmomath.BigDecFromSDKDec(tokenInSupply), osmomath.BigDecFromSDKDec(tokenOutSupply), remReserves, osmomath.BigDecFromSDKDec(tokenOut.Amount.ToDec().Neg()))
cfmmIn := solveCfmm(tokenInSupply, tokenOutSupply, remReserves, tokenOutAmount.Neg())
inAmt := p.getDescaledPoolAmt(tokenInDenom, cfmmIn.Neg())
return inAmt.SDKDec(), nil
}
Expand Down
70 changes: 51 additions & 19 deletions x/gamm/pool-models/stableswap/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,28 +118,22 @@ func (p Pool) getPoolAmts(denoms ...string) ([]sdk.Int, error) {
return result, nil
}

// getScaledPoolAmts returns scaled amount of pool liquidity based on each asset's precisions
func (p Pool) getScaledPoolAmts(denoms ...string) ([]sdk.Dec, error) {
result := make([]sdk.Dec, len(denoms))
poolLiquidity := p.PoolLiquidity
liquidityIndexes := p.getLiquidityIndexMap()

for i, denom := range denoms {
liquidityIndex := liquidityIndexes[denom]

amt := poolLiquidity.AmountOf(denom)
if amt.IsZero() {
return []sdk.Dec{}, fmt.Errorf("denom %s does not exist in pool", denom)
}
scalingFactor := p.GetScalingFactorByLiquidityIndex(liquidityIndex)

result[i] = amt.ToDec().QuoInt64Mut(int64(scalingFactor))
// 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 result, nil
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 {
liquidityIndexes := p.getLiquidityIndexMap()
liquidityIndex := liquidityIndexes[denom]
Expand All @@ -150,6 +144,7 @@ func (p Pool) getDescaledPoolAmt(denom string, amount osmomath.BigDec) osmomath.
}

// getLiquidityIndexMap creates a map of denoms to its index in pool liquidity
// TODO: Review all uses of this
func (p Pool) getLiquidityIndexMap() map[string]int {
poolLiquidity := p.PoolLiquidity
liquidityIndexMap := make(map[string]int, poolLiquidity.Len())
Expand All @@ -159,6 +154,39 @@ 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))
curIndex := 2
for _, coin := range coins {
if coin.Denom == first {
newCoins[0] = coin
} else if coin.Denom == second {
newCoins[1] = coin
} else {
newCoins[curIndex] = coin
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
}

// updatePoolLiquidityForSwap updates the pool liquidity.
// It requires caller to validate that tokensIn and tokensOut only consist of
// denominations in the pool.
Expand Down Expand Up @@ -250,12 +278,16 @@ 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) {
reserves, err := p.getScaledPoolAmts(baseAssetDenom, quoteAssetDenom)
scaledSortedReserves, err := p.scaledSortedPoolReserves(baseAssetDenom, quoteAssetDenom)
if err != nil {
return sdk.Dec{}, err
}

scaledSpotPrice := spotPrice(osmomath.BigDecFromSDKDec(reserves[0]), osmomath.BigDecFromSDKDec(reserves[1]))
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()

Expand Down
127 changes: 61 additions & 66 deletions x/gamm/pool-models/stableswap/pool_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//nolint:composites
package stableswap

import (
Expand Down Expand Up @@ -31,108 +32,102 @@ var (
sdk.NewInt64Coin("foo", 2000000000),
sdk.NewInt64Coin("bar", 1000000000),
)
fiveUnevenStablePoolAssets = sdk.NewCoins(
sdk.NewInt64Coin("asset/a", 1000000000),
sdk.NewInt64Coin("asset/b", 2000000000),
sdk.NewInt64Coin("asset/c", 3000000000),
sdk.NewInt64Coin("asset/d", 4000000000),
sdk.NewInt64Coin("asset/e", 5000000000),
)
)

func TestGetScaledPoolAmts(t *testing.T) {
func TestScaledSortedPoolReserves(t *testing.T) {
baseEvenAmt := sdk.NewDec(1000000000)
tests := map[string]struct {
denoms []string
denoms [2]string
poolAssets sdk.Coins
scalingFactors []uint64
expReserves []sdk.Dec
expReserves []sdk.DecCoin
expPanic bool
}{
// sanity checks, default scaling factors
"get both pool assets, even two-asset pool with default scaling factors": {
denoms: []string{"foo", "bar"},
poolAssets: twoEvenStablePoolAssets,
scalingFactors: defaultTwoAssetScalingFactors,
expReserves: []sdk.Dec{sdk.NewDec(1000000000), sdk.NewDec(1000000000)},
expPanic: false,
},
"get one pool asset, even two-asset pool with default scaling factors": {
denoms: []string{"foo"},
"even two-asset pool with default scaling factors": {
denoms: [2]string{"foo", "bar"},
poolAssets: twoEvenStablePoolAssets,
scalingFactors: defaultTwoAssetScalingFactors,
expReserves: []sdk.Dec{sdk.NewDec(1000000000)},
expReserves: []sdk.DecCoin{{"foo", baseEvenAmt}, {"bar", sdk.NewDec(1000000000)}},
expPanic: false,
},
"get both pool assets, uneven two-asset pool with default scaling factors": {
denoms: []string{"foo", "bar"},
"uneven two-asset pool with default scaling factors": {
denoms: [2]string{"foo", "bar"},
poolAssets: twoUnevenStablePoolAssets,
scalingFactors: defaultTwoAssetScalingFactors,
expReserves: []sdk.Dec{sdk.NewDec(2000000000), sdk.NewDec(1000000000)},
expReserves: []sdk.DecCoin{{"foo", sdk.NewDec(2000000000)}, {"bar", sdk.NewDec(1000000000)}},
expPanic: false,
},
"get first pool asset, uneven two-asset pool with default scaling factors": {
denoms: []string{"foo"},
poolAssets: twoUnevenStablePoolAssets,
scalingFactors: defaultTwoAssetScalingFactors,
expReserves: []sdk.Dec{sdk.NewDec(2000000000)},
expPanic: false,
},
"get second pool asset, uneven two-asset pool with default scaling factors": {
denoms: []string{"bar"},
poolAssets: twoUnevenStablePoolAssets,
scalingFactors: defaultTwoAssetScalingFactors,
expReserves: []sdk.Dec{sdk.NewDec(1000000000)},
expPanic: false,
},
"get both pool assets, even two-asset pool with even scaling factors greater than 1": {
denoms: []string{"foo", "bar"},
"even two-asset pool with even scaling factors greater than 1": {
denoms: [2]string{"foo", "bar"},
poolAssets: twoEvenStablePoolAssets,
scalingFactors: []uint64{10, 10},
expReserves: []sdk.Dec{sdk.NewDec(100000000), sdk.NewDec(100000000)},
expPanic: false,
},
"get both pool assets, even two-asset pool with uneven scaling factors greater than 1": {
denoms: []string{"foo", "bar"},
poolAssets: twoUnevenStablePoolAssets,
scalingFactors: []uint64{10, 5},
expReserves: []sdk.Dec{sdk.NewDec(2000000000 / 5), sdk.NewDec(1000000000 / 10)},
expPanic: false,
},
"get first pool asset, even two-asset pool with uneven scaling factors greater than 1": {
denoms: []string{"foo"},
poolAssets: twoUnevenStablePoolAssets,
scalingFactors: []uint64{10, 5},
expReserves: []sdk.Dec{sdk.NewDec(2000000000 / 5)},
expReserves: []sdk.DecCoin{{"foo", sdk.NewDec(100000000)}, {"bar", sdk.NewDec(100000000)}},
expPanic: false,
},
"get second pool asset, even two-asset pool with uneven scaling factors greater than 1": {
denoms: []string{"bar"},
"even two-asset pool with uneven scaling factors greater than 1": {
denoms: [2]string{"foo", "bar"},
poolAssets: twoUnevenStablePoolAssets,
scalingFactors: []uint64{10, 5},
expReserves: []sdk.Dec{sdk.NewDec(1000000000 / 10)},
expPanic: false,
expReserves: []sdk.DecCoin{sdk.NewInt64DecCoin("foo", 2000000000/5),
sdk.NewInt64DecCoin("bar", 1000000000/10)},
expPanic: false,
},
"get both pool assets, even two-asset pool with even, massive scaling factors greater than 1": {
denoms: []string{"foo", "bar"},
"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.Dec{sdk.NewDecWithPrec(1, 1), sdk.NewDecWithPrec(1, 1)},
expReserves: []sdk.DecCoin{{"foo", sdk.NewDecWithPrec(1, 1)}, {"bar", sdk.NewDecWithPrec(1, 1)}},
expPanic: false,
},
"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,
},
"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,
},
"max scaling factors": {
denoms: []string{"foo", "bar"},
denoms: [2]string{"foo", "bar"},
poolAssets: twoEvenStablePoolAssets,
scalingFactors: []uint64{(1 << 62), (1 << 62)},
expReserves: []sdk.Dec{sdk.NewDec(1000000000).QuoInt64(int64(1 << 62)), sdk.NewDec(1000000000).QuoInt64(int64(1 << 62))},
expPanic: false,
},
"pass in no denoms": {
denoms: []string{},
poolAssets: twoEvenStablePoolAssets,
scalingFactors: defaultTwoAssetScalingFactors,
expReserves: []sdk.Dec{},
expPanic: false,
expReserves: []sdk.DecCoin{
{"foo", sdk.NewDec(1000000000).QuoInt64(int64(1 << 62))},
{"bar", sdk.NewDec(1000000000).QuoInt64(int64(1 << 62))}},
expPanic: false,
},
"zero scaling factor": {
denoms: []string{"foo", "bar"},
denoms: [2]string{"foo", "bar"},
poolAssets: twoEvenStablePoolAssets,
scalingFactors: []uint64{0, 1},
expPanic: true,
},
}
// TODO: Add for loop for trying to re-order test cases

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
Expand All @@ -149,7 +144,7 @@ func TestGetScaledPoolAmts(t *testing.T) {
FuturePoolGovernor: defaultFutureGovernor,
}

reserves, err := p.getScaledPoolAmts(tc.denoms...)
reserves, err := p.scaledSortedPoolReserves(tc.denoms[0], tc.denoms[1])

require.NoError(t, err, "test: %s", name)
require.Equal(t, tc.expReserves, reserves)
Expand Down

0 comments on commit bd9c40b

Please sign in to comment.