From 1aa837f3cd1f4d030085f95855c0251496fb2f3f Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 9 Dec 2022 12:50:26 -0500 Subject: [PATCH] feat(osmomath): BigDec power function with integer exponent, overflow tests at max spot price (#3676) * feat(osmomath): BigDec power function with integer exponent, overflow tests at max spot price * euler's number * nolint * more tests * changelog --- CHANGELOG.md | 1 + osmomath/decimal.go | 20 ++++++ osmomath/decimal_test.go | 133 +++++++++++++++++++++++++++++++++++++++ osmomath/math.go | 4 ++ 4 files changed, 158 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c550a45be9b..09e8e133d6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The v1beta1 queries actually have base asset and quote asset reversed, so you were always getting 1/correct spot price. People fixed this by reordering the arguments. - This PR adds v2 queries for doing the correct thing, and giving people time to migrate from v1beta1 queries to v2. - It also changes cosmwasm to only allow the v2 queries, as no contracts on Osmosis mainnet uses the v1beta1 queries. +* [#3676](https://github.com/osmosis-labs/osmosis/pull/3676) implement `PowerInteger` function on `osmomath.BigDec` ### Bug fixes diff --git a/osmomath/decimal.go b/osmomath/decimal.go index 821b0f5b4e1..71a9b9517e1 100644 --- a/osmomath/decimal.go +++ b/osmomath/decimal.go @@ -972,3 +972,23 @@ func (x BigDec) CustomBaseLog(base BigDec) BigDec { return y } + +// PowerInteger takes a given decimal to an integer power +// and returns the result. Non-mutative. Uses square and multiply +// algorithm for performing the calculation. +func (d BigDec) PowerInteger(power uint64) BigDec { + if power == 0 { + return OneDec() + } + tmp := OneDec() + + for i := power; i > 1; { + if i%2 != 0 { + tmp = tmp.Mul(d) + } + i /= 2 + d = d.Mul(d) + } + + return d.Mul(tmp) +} diff --git a/osmomath/decimal_test.go b/osmomath/decimal_test.go index b50e9150b6d..74146463065 100644 --- a/osmomath/decimal_test.go +++ b/osmomath/decimal_test.go @@ -13,6 +13,7 @@ import ( "gopkg.in/yaml.v2" "github.com/osmosis-labs/osmosis/v13/app/apptesting/osmoassert" + gammtypes "github.com/osmosis-labs/osmosis/v13/x/gamm/types" ) type decimalTestSuite struct { @@ -1018,3 +1019,135 @@ func (s *decimalTestSuite) TestCustomBaseLog() { }) } } + +func (s *decimalTestSuite) TestPowerInteger() { + var expectedErrTolerance = MustNewDecFromStr("0.000000000000000000000000000000100000") + + tests := map[string]struct { + base BigDec + exponent uint64 + expectedResult BigDec + + expectedToleranceOverwrite BigDec + }{ + "0^2": { + base: ZeroDec(), + exponent: 2, + + expectedResult: ZeroDec(), + }, + "1^2": { + base: OneDec(), + exponent: 2, + + expectedResult: OneDec(), + }, + "4^4": { + base: MustNewDecFromStr("4"), + exponent: 4, + + expectedResult: MustNewDecFromStr("256"), + }, + "5^3": { + base: MustNewDecFromStr("5"), + exponent: 4, + + expectedResult: MustNewDecFromStr("625"), + }, + "e^10": { + base: eulersNumber, + exponent: 10, + + // https://www.wolframalpha.com/input?i=e%5E10+41+digits + expectedResult: MustNewDecFromStr("22026.465794806716516957900645284244366354"), + }, + "geom twap overflow: 2^log_2{max spot price + 1}": { + base: twoBigDec, + // add 1 for simplicity of calculation to isolate overflow. + exponent: uint64(BigDecFromSDKDec(gammtypes.MaxSpotPrice).Add(OneDec()).LogBase2().TruncateInt().Uint64()), + + // https://www.wolframalpha.com/input?i=2%5E%28floor%28+log+base+2+%282%5E128%29%29%29+++39+digits + expectedResult: MustNewDecFromStr("340282366920938463463374607431768211456"), + }, + "geom twap overflow: 2^log_2{max spot price}": { + base: twoBigDec, + exponent: uint64(BigDecFromSDKDec(gammtypes.MaxSpotPrice).LogBase2().TruncateInt().Uint64()), + + // https://www.wolframalpha.com/input?i=2%5E%28floor%28+log+base+2+%282%5E128+-+1%29%29%29+++39+digits + expectedResult: MustNewDecFromStr("170141183460469231731687303715884105728"), + }, + "geom twap overflow: 2^log_2{max spot price / 2 - 2017}": { // 2017 is prime. + base: twoBigDec, + exponent: uint64(BigDecFromSDKDec(gammtypes.MaxSpotPrice.Quo(sdk.NewDec(2)).Sub(sdk.NewDec(2017))).LogBase2().TruncateInt().Uint64()), + + // https://www.wolframalpha.com/input?i=e%5E10+41+digits + expectedResult: MustNewDecFromStr("85070591730234615865843651857942052864"), + }, + + // sdk.Dec test vectors copied from osmosis-labs/cosmos-sdk: + + "1.0 ^ (10) => 1.0": { + base: OneDec(), + exponent: 10, + + expectedResult: OneDec(), + }, + "0.5 ^ 2 => 0.25": { + base: NewDecWithPrec(5, 1), + exponent: 2, + + expectedResult: NewDecWithPrec(25, 2), + }, + "0.2 ^ 2 => 0.04": { + base: NewDecWithPrec(2, 1), + exponent: 2, + + expectedResult: NewDecWithPrec(4, 2), + }, + "3 ^ 3 => 27": { + base: NewBigDec(3), + exponent: 3, + + expectedResult: NewBigDec(27), + }, + "-3 ^ 4 = 81": { + base: NewBigDec(-3), + exponent: 4, + + expectedResult: NewBigDec(81), + }, + "-3 ^ 50 = 717897987691852588770249": { + base: NewBigDec(-3), + exponent: 50, + + expectedResult: MustNewDecFromStr("717897987691852588770249"), + }, + "-3 ^ 51 = -2153693963075557766310747": { + base: NewBigDec(-3), + exponent: 51, + + expectedResult: MustNewDecFromStr("-2153693963075557766310747"), + }, + "1.414213562373095049 ^ 2 = 2": { + base: NewDecWithPrec(1414213562373095049, 18), + exponent: 2, + + expectedResult: NewBigDec(2), + expectedToleranceOverwrite: MustNewDecFromStr("0.0000000000000000006"), + }, + } + + for name, tc := range tests { + tc := tc + s.Run(name, func() { + + tolerance := expectedErrTolerance + if !tc.expectedToleranceOverwrite.IsNil() { + tolerance = tc.expectedToleranceOverwrite + } + + actualResult := tc.base.PowerInteger(tc.exponent) + require.True(DecApproxEq(s.T(), tc.expectedResult, actualResult, tolerance)) + }) + } +} diff --git a/osmomath/math.go b/osmomath/math.go index d8f341bb605..0e07cac8216 100644 --- a/osmomath/math.go +++ b/osmomath/math.go @@ -18,6 +18,10 @@ var ( one_half sdk.Dec = sdk.MustNewDecFromStr("0.5") one sdk.Dec = sdk.OneDec() two sdk.Dec = sdk.MustNewDecFromStr("2") + + // https://www.wolframalpha.com/input?i=2.718281828459045235360287471352662498&assumption=%22ClashPrefs%22+-%3E+%7B%22Math%22%7D + // nolint: unused + eulersNumber = MustNewDecFromStr("2.718281828459045235360287471352662498") ) // Returns the internal "power precision".