Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stableswap]: Convert all core stableswap arithmetic to use BigDec #2777

Merged
merged 6 commits into from
Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions osmomath/decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"strconv"
"strings"
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
)

// NOTE: never use new(BigDec) or else we will panic unmarshalling into the
Expand Down Expand Up @@ -494,6 +496,27 @@ func (d BigDec) MustFloat64() float64 {
}
}

// SdkDec returns the Sdk.Dec representation of a BigDec.
// Values in any additional decimal places are truncated.
func (d BigDec) SdkDec() sdk.Dec {
AlpinYukseloglu marked this conversation as resolved.
Show resolved Hide resolved
precisionDiff := Precision - sdk.Precision
precisionFactor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(precisionDiff)), nil)

if precisionDiff < 0 {
panic("invalid decimal precision")
}

truncatedDec := sdk.NewDecFromBigIntWithPrec(new(big.Int).Quo(d.BigInt(), precisionFactor), sdk.Precision)
AlpinYukseloglu marked this conversation as resolved.
Show resolved Hide resolved

return truncatedDec
}

// BigDecFromSdkDec returns the BigDec representation of an SdkDec.
// Values in any additional decimal places are truncated.
func BigDecFromSdkDec(d sdk.Dec) BigDec {
return NewDecFromBigIntWithPrec(d.BigInt(), sdk.Precision)
}

// ____
// __| |__ "chop 'em
// ` \ round!"
Expand Down
51 changes: 51 additions & 0 deletions osmomath/decimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"math/big"
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"gopkg.in/yaml.v2"
Expand Down Expand Up @@ -148,6 +149,56 @@ func (s *decimalTestSuite) TestDecFloat64() {
}
}

func (s *decimalTestSuite) TestSdkDec() {
tests := []struct {
d BigDec
want sdk.Dec
expPanic bool
}{
{NewBigDec(0), sdk.MustNewDecFromStr("0.000000000000000000"), false},
{NewBigDec(1), sdk.MustNewDecFromStr("1.000000000000000000"), false},
{NewBigDec(10), sdk.MustNewDecFromStr("10.000000000000000000"), false},
{NewBigDec(12340), sdk.MustNewDecFromStr("12340.000000000000000000"), false},
{NewDecWithPrec(12340, 4), sdk.MustNewDecFromStr("1.234000000000000000"), false},
{NewDecWithPrec(12340, 5), sdk.MustNewDecFromStr("0.123400000000000000"), false},
{NewDecWithPrec(12340, 8), sdk.MustNewDecFromStr("0.000123400000000000"), false},
{NewDecWithPrec(1009009009009009009, 17), sdk.MustNewDecFromStr("10.090090090090090090"), false},
AlpinYukseloglu marked this conversation as resolved.
Show resolved Hide resolved
}
for tcIndex, tc := range tests {
if tc.expPanic {
s.Require().Panics(func() { tc.d.SdkDec() })
} else {
value := tc.d.SdkDec()
s.Require().Equal(tc.want, value, "bad SdkDec(), index: %v", tcIndex)
}
}
}

func (s *decimalTestSuite) TestBigDecFromSdkDec() {
tests := []struct {
d sdk.Dec
want BigDec
expPanic bool
}{
{sdk.MustNewDecFromStr("0.000000000000000000"), NewBigDec(0), false},
{sdk.MustNewDecFromStr("1.000000000000000000"), NewBigDec(1), false},
{sdk.MustNewDecFromStr("10.000000000000000000"), NewBigDec(10), false},
{sdk.MustNewDecFromStr("12340.000000000000000000"), NewBigDec(12340), false},
{sdk.MustNewDecFromStr("1.234000000000000000"), NewDecWithPrec(12340, 4), false},
{sdk.MustNewDecFromStr("0.123400000000000000"), NewDecWithPrec(12340, 5), false},
{sdk.MustNewDecFromStr("0.000123400000000000"), NewDecWithPrec(12340, 8), false},
{sdk.MustNewDecFromStr("10.090090090090090090"), NewDecWithPrec(1009009009009009009, 17), false},
}
for tcIndex, tc := range tests {
if tc.expPanic {
s.Require().Panics(func() { BigDecFromSdkDec(tc.d) })
} else {
value := BigDecFromSdkDec(tc.d)
s.Require().Equal(tc.want, value, "bad BigDecFromSdkDec(), index: %v", tcIndex)
}
}
}

func (s *decimalTestSuite) TestEqualities() {
tests := []struct {
d1, d2 BigDec
Expand Down
53 changes: 27 additions & 26 deletions x/gamm/pool-models/stableswap/amm.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ import (

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/osmosis-labs/osmosis/v12/osmomath"
"github.com/osmosis-labs/osmosis/v12/x/gamm/pool-models/internal/cfmm_common"
types "github.com/osmosis-labs/osmosis/v12/x/gamm/types"
)

var (
cubeRootTwo, _ = sdk.NewDec(2).ApproxRoot(3)
cubeRootTwo, _ = osmomath.NewBigDec(2).ApproxRoot(3)
threeCubeRootTwo = cubeRootTwo.MulInt64(3)
)

// solidly CFMM is xy(x^2 + y^2) = k
func cfmmConstant(xReserve, yReserve sdk.Dec) sdk.Dec {
func cfmmConstant(xReserve, yReserve osmomath.BigDec) osmomath.BigDec {
if !xReserve.IsPositive() || !yReserve.IsPositive() {
panic("invalid input: reserves must be positive")
}
Expand All @@ -30,7 +31,7 @@ func cfmmConstant(xReserve, yReserve sdk.Dec) sdk.Dec {
// outside of x and y (e.g. u = wz), and v is the sum
// of their squares (e.g. v = w^2 + z^2).
// When u = 1 and v = 0, this is equivalent to solidly's CFMM
func cfmmConstantMulti(xReserve, yReserve, uReserve, vSumSquares sdk.Dec) sdk.Dec {
func cfmmConstantMulti(xReserve, yReserve, uReserve, vSumSquares osmomath.BigDec) osmomath.BigDec {
if !xReserve.IsPositive() || !yReserve.IsPositive() || !uReserve.IsPositive() || vSumSquares.IsNegative() {
panic("invalid input: reserves must be positive")
}
Expand All @@ -46,7 +47,7 @@ func cfmmConstantMulti(xReserve, yReserve, uReserve, vSumSquares sdk.Dec) sdk.De
// how many units `a` of x do we get out.
// So we solve the following expression for `a`
// xy(x^2 + y^2) = (x - a)(y + b)((x - a)^2 + (y + b)^2)
func solveCfmm(xReserve, yReserve, yIn sdk.Dec) sdk.Dec {
func solveCfmm(xReserve, yReserve, yIn osmomath.BigDec) osmomath.BigDec {
if !xReserve.IsPositive() || !yReserve.IsPositive() || !yIn.IsPositive() {
panic("invalid input: reserves and input must be positive")
}
Expand Down Expand Up @@ -94,7 +95,7 @@ func solveCfmm(xReserve, yReserve, yIn sdk.Dec) sdk.Dec {
// and maybe in state.
x := xReserve
y := yReserve
x2py2 := x.Mul(x).AddMut(y.Mul(y))
x2py2 := x.Mul(x).Add(y.Mul(y))

xy := x.Mul(y)

Expand Down Expand Up @@ -128,19 +129,19 @@ func solveCfmm(xReserve, yReserve, yIn sdk.Dec) sdk.Dec {
e := xy.Mul(x2py2) // xy(x^2 + y^2)

// t1 = -27 (b + y)^2 e
t1 := e.Mul(bpy2).MulInt64Mut(-27)
t1 := e.Mul(bpy2).MulInt64(-27)

// compute d = (b + y)^2 sqrt(729 e^2 + 108 (b+y)^8)
bpy8 := bpy4.Mul(bpy4)
sqrt_inner := e.MulMut(e).MulInt64Mut(729).AddMut(bpy8.MulInt64Mut(108)) // 729 e^2 + 108 (b+y)^8
sqrt_inner := e.Mul(e).MulInt64(729).Add(bpy8.MulInt64(108)) // 729 e^2 + 108 (b+y)^8
sqrt, err := sqrt_inner.ApproxSqrt()
if err != nil {
panic(err)
}
d := sqrt.MulMut(bpy2)
d := sqrt.Mul(bpy2)

// foo = (t1 + d)^(1/3)
foo3 := t1.AddMut(d)
foo3 := t1.Add(d)
foo, _ := foo3.ApproxRoot(3)

// a = {foo / (3 2^(1/3) (b + y))}
Expand Down Expand Up @@ -171,7 +172,7 @@ func solveCfmm(xReserve, yReserve, yIn sdk.Dec) sdk.Dec {
// how many units `a` of x do we get out.
// So we solve the following expression for `a`
// xyz(x^2 + y^2 + w) = (x - a)(y + b)z((x - a)^2 + (y + b)^2 + w)
func solveCfmmMulti(xReserve, yReserve, wSumSquares, yIn sdk.Dec) sdk.Dec {
func solveCfmmMulti(xReserve, yReserve, wSumSquares, yIn osmomath.BigDec) osmomath.BigDec {
if !xReserve.IsPositive() || !yReserve.IsPositive() || !yIn.IsPositive() {
panic("invalid input: reserves and input must be positive")
}
Expand Down Expand Up @@ -275,23 +276,23 @@ func solveCfmmMulti(xReserve, yReserve, wSumSquares, yIn sdk.Dec) sdk.Dec {
return a
}

func approxDecEqual(a, b, tol sdk.Dec) bool {
func approxDecEqual(a, b, tol osmomath.BigDec) bool {
diff := a.Sub(b).Abs()
return diff.Quo(a).LTE(tol) && diff.Quo(b).LTE(tol)
}

var (
twodec = sdk.MustNewDecFromStr("2.0")
threshold = sdk.NewDecWithPrec(1, 10) // Correct within a factor of 1 * 10^{-10}
twodec = osmomath.MustNewDecFromStr("2.0")
threshold = osmomath.NewDecWithPrec(1, 10) // Correct within a factor of 1 * 10^{-10}
)

// solveCFMMBinarySearch searches the correct dx using binary search over constant K.
// added for future extension
func solveCFMMBinarySearch(constantFunction func(sdk.Dec, sdk.Dec) sdk.Dec) func(sdk.Dec, sdk.Dec, sdk.Dec) sdk.Dec {
return func(xReserve, yReserve, yIn sdk.Dec) sdk.Dec {
func solveCFMMBinarySearch(constantFunction func(osmomath.BigDec, osmomath.BigDec) osmomath.BigDec) func(osmomath.BigDec, osmomath.BigDec, osmomath.BigDec) osmomath.BigDec {
return func(xReserve, yReserve, yIn osmomath.BigDec) osmomath.BigDec {
k := constantFunction(xReserve, yReserve)
yf := yReserve.Add(yIn)
x_low_est := sdk.ZeroDec()
x_low_est := osmomath.ZeroDec()
x_high_est := xReserve
x_est := (x_high_est.Add(x_low_est)).Quo(twodec)
cur_k := constantFunction(x_est, yf)
Expand All @@ -310,11 +311,11 @@ func solveCFMMBinarySearch(constantFunction func(sdk.Dec, sdk.Dec) sdk.Dec) func

// solveCFMMBinarySearch searches the correct dx using binary search over constant K.
// added for future extension
func solveCFMMBinarySearchMulti(constantFunction func(sdk.Dec, sdk.Dec, sdk.Dec, sdk.Dec) sdk.Dec) func(sdk.Dec, sdk.Dec, sdk.Dec, sdk.Dec, sdk.Dec) sdk.Dec {
return func(xReserve, yReserve, uReserve, wSumSquares, yIn sdk.Dec) sdk.Dec {
func solveCFMMBinarySearchMulti(constantFunction func(osmomath.BigDec, osmomath.BigDec, osmomath.BigDec, osmomath.BigDec) osmomath.BigDec) func(osmomath.BigDec, osmomath.BigDec, osmomath.BigDec, osmomath.BigDec, osmomath.BigDec) osmomath.BigDec {
return func(xReserve, yReserve, uReserve, wSumSquares, yIn osmomath.BigDec) osmomath.BigDec {
k := constantFunction(xReserve, yReserve, uReserve, wSumSquares)
yf := yReserve.Add(yIn)
x_low_est := sdk.ZeroDec()
x_low_est := osmomath.ZeroDec()
x_high_est := xReserve
x_est := (x_high_est.Add(x_low_est)).Quo(twodec)
cur_k := constantFunction(x_est, yf, uReserve, wSumSquares)
Expand All @@ -331,7 +332,7 @@ func solveCFMMBinarySearchMulti(constantFunction func(sdk.Dec, sdk.Dec, sdk.Dec,
}
}

func spotPrice(baseReserve, quoteReserve sdk.Dec) sdk.Dec {
func spotPrice(baseReserve, quoteReserve 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 @@ -346,7 +347,7 @@ func spotPrice(baseReserve, quoteReserve sdk.Dec) sdk.Dec {

// We arbitrarily choose a = 1, and anticipate that this is a small value at the scale of
// xReserve & yReserve.
a := sdk.OneDec()
a := osmomath.OneDec()
// no need to divide by a, since a = 1.
return solveCfmm(baseReserve, quoteReserve, a)
}
Expand All @@ -359,9 +360,9 @@ func (p *Pool) calcOutAmtGivenIn(tokenIn sdk.Coin, tokenOutDenom string, swapFee
}
tokenInSupply, tokenOutSupply := reserves[0], reserves[1]
// We are solving for the amount of token out, hence x = tokenOutSupply, y = tokenInSupply
cfmmOut := solveCfmm(tokenOutSupply, tokenInSupply, tokenIn.Amount.ToDec())
cfmmOut := solveCfmm(osmomath.BigDecFromSdkDec(tokenOutSupply), osmomath.BigDecFromSdkDec(tokenInSupply), osmomath.BigDecFromSdkDec(tokenIn.Amount.ToDec()))
outAmt := p.getDescaledPoolAmt(tokenOutDenom, cfmmOut)
return outAmt, nil
return outAmt.SdkDec(), nil
}

// returns inAmt as a decimal
Expand All @@ -373,9 +374,9 @@ func (p *Pool) calcInAmtGivenOut(tokenOut sdk.Coin, tokenInDenom string, swapFee
tokenInSupply, tokenOutSupply := reserves[0], reserves[1]
// 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, tokenOut.Amount.ToDec().Neg())
inAmt := p.getDescaledPoolAmt(tokenInDenom, cfmmIn.NegMut())
return inAmt, nil
cfmmIn := solveCfmm(osmomath.BigDecFromSdkDec(tokenInSupply), osmomath.BigDecFromSdkDec(tokenOutSupply), osmomath.BigDecFromSdkDec(tokenOut.Amount.ToDec().Neg()))
inAmt := p.getDescaledPoolAmt(tokenInDenom, cfmmIn.Neg())
return inAmt.SdkDec(), nil
}

func (p *Pool) calcSingleAssetJoinShares(tokenIn sdk.Coin, swapFee sdk.Dec) (sdk.Int, error) {
Expand Down
10 changes: 5 additions & 5 deletions x/gamm/pool-models/stableswap/amm_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"math/rand"
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/osmosis-labs/osmosis/v12/osmomath"
)

func BenchmarkCFMM(b *testing.B) {
Expand All @@ -20,9 +20,9 @@ func BenchmarkBinarySearch(b *testing.B) {
}
}

func runCalc(solve func(sdk.Dec, sdk.Dec, sdk.Dec) sdk.Dec) {
xReserve := sdk.NewDec(rand.Int63n(100000) + 50000)
yReserve := sdk.NewDec(rand.Int63n(100000) + 50000)
yIn := sdk.NewDec(rand.Int63n(100000))
func runCalc(solve func(osmomath.BigDec, osmomath.BigDec, osmomath.BigDec) osmomath.BigDec) {
xReserve := osmomath.NewBigDec(rand.Int63n(100000) + 50000)
yReserve := osmomath.NewBigDec(rand.Int63n(100000) + 50000)
yIn := osmomath.NewBigDec(rand.Int63n(100000))
solve(xReserve, yReserve, yIn)
}
Loading