From ee5826b08eec011cc767fd2806589cbd5d5d74be Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 10 Aug 2023 22:18:13 +0200 Subject: [PATCH 1/3] test: monotonicity maintained with high precision --- osmomath/sqrt.go | 31 +++++++++ x/concentrated-liquidity/math/tick.go | 36 +++++----- x/concentrated-liquidity/math/tick_test.go | 75 +++++++++++++++------ x/concentrated-liquidity/types/constants.go | 4 +- x/concentrated-liquidity/types/errors.go | 2 +- 5 files changed, 108 insertions(+), 40 deletions(-) diff --git a/osmomath/sqrt.go b/osmomath/sqrt.go index b49779d5039..53b895d2bcc 100644 --- a/osmomath/sqrt.go +++ b/osmomath/sqrt.go @@ -49,6 +49,37 @@ func MonotonicSqrt(d sdk.Dec) (sdk.Dec, error) { return root, nil } +func MonotonicSqrtBigDec(d BigDec) (BigDec, error) { + if d.IsNegative() { + return d, errors.New("cannot take square root of negative number") + } + + // A decimal value of d, is represented as an integer of value v = 10^18 * d. + // We have an integer square root function, and we'd like to get the square root of d. + // recall integer square root is floor(sqrt(x)), hence its accurate up to 1 integer. + // we want sqrt d accurate to 18 decimal places. + // So first we multiply our current value by 10^18, then we take the integer square root. + // since sqrt(10^18 * v) = 10^9 * sqrt(v) = 10^18 * sqrt(d), we get the answer we want. + // + // We can than interpret sqrt(10^18 * v) as our resulting decimal and return it. + // monotonicity is guaranteed by correctness of integer square root. + dBi := d.BigInt() + r := big.NewInt(0).Mul(dBi, tenTo18) + r.Sqrt(r) + // However this square root r is s.t. r^2 <= d. We want to flip this to be r^2 >= d. + // To do so, we check that if r^2 < d, do r += 1. Then by correctness we will be in the case we want. + // To compare r^2 and d, we can just compare r^2 and 10^18 * v. (recall r = 10^18 * sqrt(d), v = 10^18 * d) + check := big.NewInt(0).Mul(r, r) + // dBi is a copy of d, so we can modify it. + shiftedD := dBi.Mul(dBi, tenTo18) + if check.Cmp(shiftedD) == -1 { + r.Add(r, oneBigInt) + } + root := NewDecFromBigIntWithPrec(r, 18) + + return root, nil +} + // MustMonotonicSqrt returns the output of MonotonicSqrt, panicking on error. func MustMonotonicSqrt(d sdk.Dec) sdk.Dec { sqrt, err := MonotonicSqrt(d) diff --git a/x/concentrated-liquidity/math/tick.go b/x/concentrated-liquidity/math/tick.go index 5888ce1febf..5e5d9d36490 100644 --- a/x/concentrated-liquidity/math/tick.go +++ b/x/concentrated-liquidity/math/tick.go @@ -13,17 +13,17 @@ import ( // TicksToSqrtPrice returns the sqrtPrice for the lower and upper ticks by // individually calling `TickToSqrtPrice` method. // Returns error if fails to calculate price. -func TicksToSqrtPrice(lowerTick, upperTick int64) (sdk.Dec, sdk.Dec, sdk.Dec, sdk.Dec, error) { +func TicksToSqrtPrice(lowerTick, upperTick int64) (osmomath.BigDec, osmomath.BigDec, osmomath.BigDec, osmomath.BigDec, error) { if lowerTick >= upperTick { - return sdk.Dec{}, sdk.Dec{}, sdk.Dec{}, sdk.Dec{}, types.InvalidLowerUpperTickError{LowerTick: lowerTick, UpperTick: upperTick} + return osmomath.BigDec{}, osmomath.BigDec{}, osmomath.BigDec{}, osmomath.BigDec{}, types.InvalidLowerUpperTickError{LowerTick: lowerTick, UpperTick: upperTick} } priceUpperTick, sqrtPriceUpperTick, err := TickToSqrtPrice(upperTick) if err != nil { - return sdk.Dec{}, sdk.Dec{}, sdk.Dec{}, sdk.Dec{}, err + return osmomath.BigDec{}, osmomath.BigDec{}, osmomath.BigDec{}, osmomath.BigDec{}, err } priceLowerTick, sqrtPriceLowerTick, err := TickToSqrtPrice(lowerTick) if err != nil { - return sdk.Dec{}, sdk.Dec{}, sdk.Dec{}, sdk.Dec{}, err + return osmomath.BigDec{}, osmomath.BigDec{}, osmomath.BigDec{}, osmomath.BigDec{}, err } return priceLowerTick, priceUpperTick, sqrtPriceLowerTick, sqrtPriceUpperTick, nil } @@ -31,16 +31,16 @@ func TicksToSqrtPrice(lowerTick, upperTick int64) (sdk.Dec, sdk.Dec, sdk.Dec, sd // TickToSqrtPrice returns the sqrtPrice given a tickIndex // If tickIndex is zero, the function returns sdk.OneDec(). // It is the combination of calling TickToPrice followed by Sqrt. -func TickToSqrtPrice(tickIndex int64) (sdk.Dec, sdk.Dec, error) { +func TickToSqrtPrice(tickIndex int64) (osmomath.BigDec, osmomath.BigDec, error) { price, err := TickToPrice(tickIndex) if err != nil { - return sdk.Dec{}, sdk.Dec{}, err + return osmomath.BigDec{}, osmomath.BigDec{}, err } // Determine the sqrtPrice from the price - sqrtPrice, err := osmomath.MonotonicSqrt(price) + sqrtPrice, err := osmomath.MonotonicSqrtBigDec(price) if err != nil { - return sdk.Dec{}, sdk.Dec{}, err + return osmomath.BigDec{}, osmomath.BigDec{}, err } return price, sqrtPrice, nil } @@ -54,18 +54,18 @@ func TickToSqrtPriceBigDec(tickIndex int64) (osmomath.BigDec, error) { } // Determine the sqrtPrice from the price - sqrtPrice, err := osmomath.MonotonicSqrt(price) + sqrtPrice, err := osmomath.MonotonicSqrtBigDec(price) if err != nil { return osmomath.BigDec{}, err } - return osmomath.BigDecFromSDKDec(sqrtPrice), nil + return sqrtPrice, nil } // TickToPrice returns the price given a tickIndex // If tickIndex is zero, the function returns sdk.OneDec(). -func TickToPrice(tickIndex int64) (price sdk.Dec, err error) { +func TickToPrice(tickIndex int64) (price osmomath.BigDec, err error) { if tickIndex == 0 { - return sdk.OneDec(), nil + return osmomath.OneDec(), nil } // The formula is as follows: geometricExponentIncrementDistanceInTicks = 9 * 10**(-exponentAtPriceOne) @@ -75,10 +75,10 @@ func TickToPrice(tickIndex int64) (price sdk.Dec, err error) { // Check that the tick index is between min and max value if tickIndex < types.MinCurrentTick { - return sdk.Dec{}, types.TickIndexMinimumError{MinTick: types.MinCurrentTick} + return osmomath.BigDec{}, types.TickIndexMinimumError{MinTick: types.MinCurrentTick} } if tickIndex > types.MaxTick { - return sdk.Dec{}, types.TickIndexMaximumError{MaxTick: types.MaxTick} + return osmomath.BigDec{}, types.TickIndexMaximumError{MaxTick: types.MaxTick} } // Use floor division to determine what the geometricExponent is now (the delta) @@ -100,12 +100,12 @@ func TickToPrice(tickIndex int64) (price sdk.Dec, err error) { numAdditiveTicks := tickIndex - (geometricExponentDelta * geometricExponentIncrementDistanceInTicks) // Finally, we can calculate the price - price = PowTenInternal(geometricExponentDelta).Add(osmomath.NewBigDec(numAdditiveTicks).Mul(currentAdditiveIncrementInTicks).SDKDec()) + price = powTenBigDec(geometricExponentDelta).Add(osmomath.NewBigDec(numAdditiveTicks).Mul(currentAdditiveIncrementInTicks)) // defense in depth, this logic would not be reached due to use having checked if given tick is in between // min tick and max tick. - if price.GT(types.MaxSpotPrice) || price.LT(types.MinSpotPrice) { - return sdk.Dec{}, types.PriceBoundError{ProvidedPrice: price, MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice} + if price.GT(osmomath.BigDecFromSDKDec(types.MaxSpotPrice)) || price.LT(osmomath.BigDecFromSDKDec(types.MinSpotPrice)) { + return osmomath.BigDec{}, types.PriceBoundError{ProvidedPrice: price, MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice} } return price, nil } @@ -174,7 +174,7 @@ func CalculatePriceToTickDec(price sdk.Dec) (tickIndex sdk.Dec, err error) { return sdk.ZeroDec(), fmt.Errorf("price must be greater than zero") } if price.GT(types.MaxSpotPrice) || price.LT(types.MinSpotPrice) { - return sdk.ZeroDec(), types.PriceBoundError{ProvidedPrice: price, MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice} + return sdk.ZeroDec(), types.PriceBoundError{ProvidedPrice: osmomath.BigDecFromSDKDec(price), MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice} } if price.Equal(sdkOneDec) { return sdk.ZeroDec(), nil diff --git a/x/concentrated-liquidity/math/tick_test.go b/x/concentrated-liquidity/math/tick_test.go index 1372a5f9d30..5bc0b984534 100644 --- a/x/concentrated-liquidity/math/tick_test.go +++ b/x/concentrated-liquidity/math/tick_test.go @@ -389,11 +389,11 @@ func TestPriceToTick(t *testing.T) { }, "price is greater than max spot price": { price: types.MaxSpotPrice.Add(sdk.OneDec()), - expectedError: types.PriceBoundError{ProvidedPrice: types.MaxSpotPrice.Add(sdk.OneDec()), MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice}, + expectedError: types.PriceBoundError{ProvidedPrice: osmomath.BigDecFromSDKDec(types.MaxSpotPrice).Add(osmomath.OneDec()), MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice}, }, "price is smaller than min spot price": { price: types.MinSpotPrice.Quo(sdk.NewDec(10)), - expectedError: types.PriceBoundError{ProvidedPrice: types.MinSpotPrice.Quo(sdk.NewDec(10)), MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice}, + expectedError: types.PriceBoundError{ProvidedPrice: osmomath.BigDecFromSDKDec(types.MinSpotPrice.Quo(sdk.NewDec(10))), MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice}, }, } for name, tc := range testCases { @@ -605,7 +605,7 @@ func TestTickToSqrtPricePriceToTick_InverseRelationship(t *testing.T) { require.Equal(t, expectedPrice, price) // 3. Compute tick from inverse price (inverse tick) - inverseTickFromPrice, err := PriceToTick(price) + inverseTickFromPrice, err := PriceToTick(price.SDKDec()) require.NoError(t, err) // Make sure original tick and inverse tick match. @@ -620,7 +620,7 @@ func TestTickToSqrtPricePriceToTick_InverseRelationship(t *testing.T) { // require.Equal(t, expectedPrice.String(), priceFromSqrtPrice.String()) // 5. Compute tick from sqrt price from the original tick. - inverseTickFromSqrtPrice, err := math.CalculateSqrtPriceToTick(osmomath.BigDecFromSDKDec(sqrtPrice)) + inverseTickFromSqrtPrice, err := math.CalculateSqrtPriceToTick(sqrtPrice) require.NoError(t, err) require.Equal(t, tickFromPrice, inverseTickFromSqrtPrice, "expected: %s, actual: %s", tickFromPrice, inverseTickFromSqrtPrice) @@ -757,12 +757,12 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { require.NoError(t, err) testCases := map[string]struct { - sqrtPrice sdk.Dec + sqrtPrice osmomath.BigDec tickSpacing uint64 tickExpected int64 }{ "sqrt price of 1 (tick spacing 1)": { - sqrtPrice: sdk.OneDec(), + sqrtPrice: osmomath.OneDec(), tickSpacing: 1, tickExpected: 0, }, @@ -772,7 +772,7 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { tickExpected: 1, }, "sqrt price one ULP below boundary of next tick (tick spacing 1)": { - sqrtPrice: sqp1.Sub(sdk.SmallestDec()), + sqrtPrice: sqp1.Sub(osmomath.SmallestDec()), tickSpacing: 1, tickExpected: 0, }, @@ -787,7 +787,7 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { tickExpected: 100, }, "sqrt price one ULP below bucket 100 (tick spacing 100)": { - sqrtPrice: sqp100.Sub(sdk.SmallestDec()), + sqrtPrice: sqp100.Sub(osmomath.SmallestDec()), tickSpacing: defaultTickSpacing, tickExpected: 0, }, @@ -797,7 +797,7 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { tickExpected: -100, }, "sqrt price one ULP below bucket -100 (tick spacing 100)": { - sqrtPrice: sqpn100.Sub(sdk.SmallestDec()), + sqrtPrice: sqpn100.Sub(osmomath.SmallestDec()), tickSpacing: defaultTickSpacing, tickExpected: -200, }, @@ -807,17 +807,17 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { tickExpected: -200, }, "sqrt price exactly equal to max sqrt price": { - sqrtPrice: types.MaxSqrtPrice, + sqrtPrice: osmomath.BigDecFromSDKDec(types.MaxSqrtPrice), tickSpacing: defaultTickSpacing, tickExpected: types.MaxTick, }, "sqrt price exactly equal to min sqrt price": { - sqrtPrice: types.MinSqrtPrice, + sqrtPrice: osmomath.BigDecFromSDKDec(types.MinSqrtPrice), tickSpacing: defaultTickSpacing, tickExpected: types.MinInitializedTick, }, "sqrt price equal to max sqrt price minus one ULP": { - sqrtPrice: types.MaxSqrtPrice.Sub(sdk.SmallestDec()), + sqrtPrice: osmomath.BigDecFromSDKDec(types.MaxSqrtPrice).Sub(osmomath.SmallestDec()), tickSpacing: defaultTickSpacing, tickExpected: types.MaxTick - defaultTickSpacing, }, @@ -827,12 +827,12 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { tickExpected: types.MaxTick - 1, }, "sqrt price one ULP below max tick - 1 (tick spacing 1)": { - sqrtPrice: sqpMaxTickSubOne.Sub(sdk.SmallestDec()), + sqrtPrice: sqpMaxTickSubOne.Sub(osmomath.SmallestDec()), tickSpacing: 1, tickExpected: types.MaxTick - 2, }, "sqrt price one ULP below max tick - 1 (tick spacing 100)": { - sqrtPrice: sqpMaxTickSubOne.Sub(sdk.SmallestDec()), + sqrtPrice: sqpMaxTickSubOne.Sub(osmomath.SmallestDec()), tickSpacing: defaultTickSpacing, tickExpected: types.MaxTick - defaultTickSpacing, }, @@ -843,13 +843,13 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { }, "sqrt price corresponds exactly to min tick + 1 minus 1 ULP (tick spacing 1)": { // Calculated using TickToSqrtPrice(types.MinInitializedTick + 1) - 1 ULP - sqrtPrice: sqpMinTickPlusOne.Sub(sdk.SmallestDec()), + sqrtPrice: sqpMinTickPlusOne.Sub(osmomath.SmallestDec()), tickSpacing: 1, tickExpected: types.MinInitializedTick, }, "sqrt price corresponds exactly to min tick + 1 plus 1 ULP (tick spacing 1)": { // Calculated using TickToSqrtPrice(types.MinInitializedTick + 1) + 1 ULP - sqrtPrice: sqpMinTickPlusOne.Add(sdk.SmallestDec()), + sqrtPrice: sqpMinTickPlusOne.Add(osmomath.SmallestDec()), tickSpacing: 1, tickExpected: types.MinInitializedTick + 1, }, @@ -860,20 +860,20 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { }, "sqrt price corresponds exactly to min tick + 2 plus 1 ULP (tick spacing 1)": { // Calculated using TickToSqrtPrice(types.MinInitializedTick + 2) + 1 ULP - sqrtPrice: sqpMinTickPlusTwo.Add(sdk.SmallestDec()), + sqrtPrice: sqpMinTickPlusTwo.Add(osmomath.SmallestDec()), tickSpacing: 1, tickExpected: types.MinInitializedTick + 2, }, "sqrt price corresponds exactly to min tick + 2 minus 1 ULP (tick spacing 1)": { // Calculated using TickToSqrtPrice(types.MinInitializedTick + 2) - 1 ULP - sqrtPrice: sqpMinTickPlusTwo.Sub(sdk.SmallestDec()), + sqrtPrice: sqpMinTickPlusTwo.Sub(osmomath.SmallestDec()), tickSpacing: 1, tickExpected: types.MinInitializedTick + 1, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { - tickIndex, err := math.SqrtPriceToTickRoundDownSpacing(tc.sqrtPrice, tc.tickSpacing) + tickIndex, err := math.SqrtPriceToTickRoundDownSpacing(tc.sqrtPrice.SDKDec(), tc.tickSpacing) require.NoError(t, err) require.Equal(t, tc.tickExpected, tickIndex) @@ -891,3 +891,40 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { }) } } + +func TestMonotnicityAtPriceBounds(t *testing.T) { + // Note: this starting value was manually adjusted until it was on the boundary of where the + // ticks started becoming monotonic + x := int64(-108000002) + lastValueMonotonic := true + highestMonotonicTick := types.MinInitializedTick + + // Find the highest tick where the sqrt price is monotonic. If nothing is found in 50,000 ticks, + // lastValueMonotonic is false and starting value should be adjusted. + for i := 0; i < 50000; i++ { + _, xSqrtPrice, err := math.TickToSqrtPrice(x) + require.NoError(t, err) + _, xSqrtPriceNext, err := math.TickToSqrtPrice(x + 1) + require.NoError(t, err) + if xSqrtPrice.GTE(xSqrtPriceNext) { + fmt.Printf("Non-monotonic behavior detected at x = %d\n", x) + lastValueMonotonic = false + } else if !lastValueMonotonic { + highestMonotonicTick = x + lastValueMonotonic = true + } else { + lastValueMonotonic = true + } + x++ + } + fmt.Println("Last value was monotonic: ", lastValueMonotonic) + + // highestMonotonicTick lands on tick -108000000 + fmt.Println("Highest monotonic tick: ", highestMonotonicTick) + + // smallestSupportedPrice = 10^-12 + smallestSupportedPrice, smallestSqrtPrice, err := math.TickToSqrtPrice(highestMonotonicTick) + require.NoError(t, err) + fmt.Println("smallestSupportedPrice: ", smallestSupportedPrice) + fmt.Println("smallestSqrtPrice: ", smallestSqrtPrice) +} diff --git a/x/concentrated-liquidity/types/constants.go b/x/concentrated-liquidity/types/constants.go index b2ddc0a3b96..636d443b432 100644 --- a/x/concentrated-liquidity/types/constants.go +++ b/x/concentrated-liquidity/types/constants.go @@ -10,7 +10,7 @@ import ( const ( // Precomputed values for min and max tick - MinInitializedTick, MaxTick int64 = -108000000, 342000000 + MinInitializedTick, MaxTick int64 = -162000000, 342000000 // If we consume all liquidity and cross the min initialized tick, // our current tick will equal to MinInitializedTick - 1 with zero liquidity. // However, note that this tick cannot be crossed. If current tick @@ -28,7 +28,7 @@ const ( var ( MaxSpotPrice = sdk.MustNewDecFromStr("100000000000000000000000000000000000000") - MinSpotPrice = sdk.MustNewDecFromStr("0.000000000001") // 10^-12 + MinSpotPrice = sdk.MustNewDecFromStr("0.000000000000000001") // 10^-12 MaxSqrtPrice = osmomath.MustMonotonicSqrt(MaxSpotPrice) MinSqrtPrice = osmomath.MustMonotonicSqrt(MinSpotPrice) MaxSqrtPriceBigDec = osmomath.BigDecFromSDKDec(MaxSqrtPrice) diff --git a/x/concentrated-liquidity/types/errors.go b/x/concentrated-liquidity/types/errors.go index 30cd0ee4c01..2101c852a44 100644 --- a/x/concentrated-liquidity/types/errors.go +++ b/x/concentrated-liquidity/types/errors.go @@ -264,7 +264,7 @@ func (e TickNotFoundError) Error() string { } type PriceBoundError struct { - ProvidedPrice sdk.Dec + ProvidedPrice osmomath.BigDec MinSpotPrice sdk.Dec MaxSpotPrice sdk.Dec } From 6026111b81ed7478823b16ae065fc59e4814e25c Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 10 Aug 2023 23:06:43 +0200 Subject: [PATCH 2/3] scan all legative ticks --- x/concentrated-liquidity/math/tick_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/concentrated-liquidity/math/tick_test.go b/x/concentrated-liquidity/math/tick_test.go index 5bc0b984534..bfbcf722687 100644 --- a/x/concentrated-liquidity/math/tick_test.go +++ b/x/concentrated-liquidity/math/tick_test.go @@ -895,13 +895,13 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { func TestMonotnicityAtPriceBounds(t *testing.T) { // Note: this starting value was manually adjusted until it was on the boundary of where the // ticks started becoming monotonic - x := int64(-108000002) + x := int64(-162000000) lastValueMonotonic := true highestMonotonicTick := types.MinInitializedTick // Find the highest tick where the sqrt price is monotonic. If nothing is found in 50,000 ticks, // lastValueMonotonic is false and starting value should be adjusted. - for i := 0; i < 50000; i++ { + for i := 0; i < 162000000; i++ { _, xSqrtPrice, err := math.TickToSqrtPrice(x) require.NoError(t, err) _, xSqrtPriceNext, err := math.TickToSqrtPrice(x + 1) From ba0965dffddc7b2d2425d0cd9ac68d60f6f240b0 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 15 Aug 2023 19:25:22 +0200 Subject: [PATCH 3/3] updates --- osmomath/sqrt.go | 17 +- osmomath/sqrt_big_test.go | 160 +++++++++++++++++++ x/concentrated-liquidity/math/precompute.go | 20 +-- x/concentrated-liquidity/math/tick.go | 28 ++-- x/concentrated-liquidity/math/tick_test.go | 168 +++++++++++--------- x/concentrated-liquidity/types/constants.go | 11 +- 6 files changed, 302 insertions(+), 102 deletions(-) create mode 100644 osmomath/sqrt_big_test.go diff --git a/osmomath/sqrt.go b/osmomath/sqrt.go index 53b895d2bcc..2f57fba47ae 100644 --- a/osmomath/sqrt.go +++ b/osmomath/sqrt.go @@ -8,7 +8,9 @@ import ( ) var smallestDec = sdk.SmallestDec() +var smallestBigDec = SmallestDec() var tenTo18 = big.NewInt(1e18) +var tenTo36 = big.NewInt(0).Mul(tenTo18, tenTo18) var oneBigInt = big.NewInt(1) // Returns square root of d @@ -64,18 +66,18 @@ func MonotonicSqrtBigDec(d BigDec) (BigDec, error) { // We can than interpret sqrt(10^18 * v) as our resulting decimal and return it. // monotonicity is guaranteed by correctness of integer square root. dBi := d.BigInt() - r := big.NewInt(0).Mul(dBi, tenTo18) + r := big.NewInt(0).Mul(dBi, tenTo36) r.Sqrt(r) // However this square root r is s.t. r^2 <= d. We want to flip this to be r^2 >= d. // To do so, we check that if r^2 < d, do r += 1. Then by correctness we will be in the case we want. // To compare r^2 and d, we can just compare r^2 and 10^18 * v. (recall r = 10^18 * sqrt(d), v = 10^18 * d) check := big.NewInt(0).Mul(r, r) // dBi is a copy of d, so we can modify it. - shiftedD := dBi.Mul(dBi, tenTo18) + shiftedD := dBi.Mul(dBi, tenTo36) if check.Cmp(shiftedD) == -1 { r.Add(r, oneBigInt) } - root := NewDecFromBigIntWithPrec(r, 18) + root := NewDecFromBigIntWithPrec(r, 36) return root, nil } @@ -88,3 +90,12 @@ func MustMonotonicSqrt(d sdk.Dec) sdk.Dec { } return sqrt } + +// MustMonotonicSqrt returns the output of MonotonicSqrt, panicking on error. +func MustMonotonicSqrtBigDec(d BigDec) BigDec { + sqrt, err := MonotonicSqrtBigDec(d) + if err != nil { + panic(err) + } + return sqrt +} diff --git a/osmomath/sqrt_big_test.go b/osmomath/sqrt_big_test.go new file mode 100644 index 00000000000..54c65ecdd65 --- /dev/null +++ b/osmomath/sqrt_big_test.go @@ -0,0 +1,160 @@ +package osmomath + +import ( + "math/big" + "math/rand" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func generateRandomDecForEachBitlenBigDec(r *rand.Rand, numPerBitlen int) []BigDec { + res := make([]BigDec, (255+sdk.DecimalPrecisionBits)*numPerBitlen) + for i := 0; i < 255+sdk.DecimalPrecisionBits; i++ { + upperbound := big.NewInt(1) + upperbound.Lsh(upperbound, uint(i)) + for j := 0; j < numPerBitlen; j++ { + v := big.NewInt(0).Rand(r, upperbound) + res[i*numPerBitlen+j] = NewDecFromBigIntWithPrec(v, 36) + } + } + return res +} + +func TestSdkApproxSqrtVectors_BigDec(t *testing.T) { + testCases := []struct { + input BigDec + expected BigDec + }{ + {OneDec(), OneDec()}, // 1.0 => 1.0 + {NewDecWithPrec(25, 2), NewDecWithPrec(5, 1)}, // 0.25 => 0.5 + {NewDecWithPrec(4, 2), NewDecWithPrec(2, 1)}, // 0.09 => 0.3 + {NewDecFromInt(NewInt(9)), NewDecFromInt(NewInt(3))}, // 9 => 3 + {NewDecFromInt(NewInt(2)), MustNewDecFromStr("1.414213562373095048801688724209698079")}, // 2 => 1.414213562373095048801688724209698079 + {smallestBigDec, NewDecWithPrec(1, 18)}, // 10^-36 => 10^-18 + {smallestBigDec.MulInt64(3), NewDecWithPrec(1732050807568877294, 36)}, // 3*10^-36 => sqrt(3)*10^-18 + } + + for i, tc := range testCases { + res, err := MonotonicSqrtBigDec(tc.input) + require.NoError(t, err) + require.Equal(t, tc.expected, res, "unexpected result for test case %d, input: %v", i, tc.input) + } +} + +func testMonotonicityAroundBigDec(t *testing.T, x BigDec) { + // test that sqrt(x) is monotonic around x + // i.e. sqrt(x-1) <= sqrt(x) <= sqrt(x+1) + sqrtX, err := MonotonicSqrtBigDec(x) + require.NoError(t, err) + sqrtXMinusOne, err := MonotonicSqrtBigDec(x.Sub(smallestBigDec)) + require.NoError(t, err) + sqrtXPlusOne, err := MonotonicSqrtBigDec(x.Add(smallestBigDec)) + require.NoError(t, err) + assert.True(t, sqrtXMinusOne.LTE(sqrtX), "sqrtXMinusOne: %s, sqrtX: %s", sqrtXMinusOne, sqrtX) + assert.True(t, sqrtX.LTE(sqrtXPlusOne), "sqrtX: %s, sqrtXPlusOne: %s", sqrtX, sqrtXPlusOne) +} + +func TestSqrtMonotinicity_BigDec(t *testing.T) { + type testcase struct { + smaller BigDec + bigger BigDec + } + testCases := []testcase{ + {MustNewDecFromStr("120.120060020005000000"), MustNewDecFromStr("120.120060020005000001")}, + {smallestBigDec, smallestBigDec.MulInt64(2)}, + } + // create random test vectors for every bit-length + r := rand.New(rand.NewSource(rand.Int63())) + for i := 0; i < 255+sdk.DecimalPrecisionBits; i++ { + upperbound := big.NewInt(1) + upperbound.Lsh(upperbound, uint(i)) + for j := 0; j < 100; j++ { + v := big.NewInt(0).Rand(r, upperbound) + d := NewDecFromBigIntWithPrec(v, 36) + testCases = append(testCases, testcase{d, d.Add(smallestBigDec)}) + } + } + for i := 0; i < 1024; i++ { + d := NewDecWithPrec(int64(i), 18) + testCases = append(testCases, testcase{d, d.Add(smallestBigDec)}) + } + + for _, i := range testCases { + sqrtSmaller, err := MonotonicSqrtBigDec(i.smaller) + require.NoError(t, err, "smaller: %s", i.smaller) + sqrtBigger, err := MonotonicSqrtBigDec(i.bigger) + require.NoError(t, err, "bigger: %s", i.bigger) + assert.True(t, sqrtSmaller.LTE(sqrtBigger), "sqrtSmaller: %s, sqrtBigger: %s", sqrtSmaller, sqrtBigger) + + // separately sanity check that sqrt * sqrt >= input + sqrtSmallerSquared := sqrtSmaller.Mul(sqrtSmaller) + assert.True(t, sqrtSmallerSquared.GTE(i.smaller), "sqrt %s, sqrtSmallerSquared: %s, smaller: %s", sqrtSmaller, sqrtSmallerSquared, i.smaller) + } +} + +// Test that square(sqrt(x)) = x when x is a perfect square. +// We do this by sampling sqrt(v) from the set of numbers `a.b`, where a in [0, 2^128], b in [0, 10^9]. +// and then setting x = sqrt(v) +// this is because this is the set of values whose squares are perfectly representable. +func TestPerfectSquares_BigDec(t *testing.T) { + cases := []sdk.Dec{ + sdk.NewDec(100), + } + r := rand.New(rand.NewSource(rand.Int63())) + tenToMin9 := big.NewInt(1_000_000_000) + for i := 0; i < 128; i++ { + upperbound := big.NewInt(1) + upperbound.Lsh(upperbound, uint(i)) + for j := 0; j < 100; j++ { + v := big.NewInt(0).Rand(r, upperbound) + dec := big.NewInt(0).Rand(r, tenToMin9) + d := sdk.NewDecFromBigInt(v).Add(sdk.NewDecFromBigIntWithPrec(dec, 9)) + cases = append(cases, d.MulMut(d)) + } + } + + for _, i := range cases { + sqrt, err := MonotonicSqrt(i) + require.NoError(t, err, "smaller: %s", i) + assert.Equal(t, i, sqrt.MulMut(sqrt)) + if !i.IsZero() { + testMonotonicityAround(t, i) + } + } +} + +func TestSqrtRounding_BigDec(t *testing.T) { + testCases := []sdk.Dec{ + // TODO: uncomment when SDK supports dec from str with bigger bitlenghths. + // it works if you override the sdk panic locally. + // sdk.MustNewDecFromStr("11662930532952632574132537947829685675668532938920838254939577167671385459971.396347723368091000"), + } + r := rand.New(rand.NewSource(rand.Int63())) + testCases = append(testCases, generateRandomDecForEachBitlen(r, 10)...) + for _, i := range testCases { + sqrt, err := MonotonicSqrt(i) + require.NoError(t, err, "smaller: %s", i) + // Sanity check that sqrt * sqrt >= input + sqrtSquared := sqrt.Mul(sqrt) + assert.True(t, sqrtSquared.GTE(i), "sqrt %s, sqrtSquared: %s, original: %s", sqrt, sqrtSquared, i) + // (aside) check that (sqrt - 1ulp)^2 <= input + sqrtMin1 := sqrt.Sub(smallestDec) + sqrtSquared = sqrtMin1.Mul(sqrtMin1) + assert.True(t, sqrtSquared.LTE(i), "sqrtMin1ULP %s, sqrtSquared: %s, original: %s", sqrt, sqrtSquared, i) + } +} + +// benchmarks the new square root across bit-lengths, for comparison with the SDK square root. +func BenchmarkMonotonicSqrt_BigDec(b *testing.B) { + r := rand.New(rand.NewSource(1)) + vectors := generateRandomDecForEachBitlen(r, 1) + for i := 0; i < b.N; i++ { + for j := 0; j < len(vectors); j++ { + a, _ := MonotonicSqrt(vectors[j]) + _ = a + } + } +} diff --git a/x/concentrated-liquidity/math/precompute.go b/x/concentrated-liquidity/math/precompute.go index 7de436bd48e..bfb4b058f38 100644 --- a/x/concentrated-liquidity/math/precompute.go +++ b/x/concentrated-liquidity/math/precompute.go @@ -36,9 +36,9 @@ var ( // -1 => (0.1, 10^(types.ExponentAtPriceOne - 1), 9 * (types.ExponentAtPriceOne - 1)) type tickExpIndexData struct { // if price < initialPrice, we are not in this exponent range. - initialPrice sdk.Dec + initialPrice osmomath.BigDec // if price >= maxPrice, we are not in this exponent range. - maxPrice sdk.Dec + maxPrice osmomath.BigDec // TODO: Change to normal Dec, if min spot price increases. // additive increment per tick here. additiveIncrementPerTick osmomath.BigDec @@ -50,13 +50,13 @@ var tickExpCache map[int64]*tickExpIndexData = make(map[int64]*tickExpIndexData) func buildTickExpCache() { // build positive indices first - maxPrice := sdkOneDec + maxPrice := osmomathBigOneDec curExpIndex := int64(0) - for maxPrice.LT(types.MaxSpotPrice) { + for maxPrice.LT(osmomath.BigDecFromSDKDec(types.MaxSpotPrice)) { tickExpCache[curExpIndex] = &tickExpIndexData{ // price range 10^curExpIndex to 10^(curExpIndex + 1). (10, 100) - initialPrice: sdkTenDec.Power(uint64(curExpIndex)), - maxPrice: sdkTenDec.Power(uint64(curExpIndex + 1)), + initialPrice: osmomathBigTenDec.PowerInteger(uint64(curExpIndex)), + maxPrice: osmomathBigTenDec.PowerInteger(uint64(curExpIndex + 1)), additiveIncrementPerTick: powTenBigDec(types.ExponentAtPriceOne + curExpIndex), initialTick: geometricExponentIncrementDistanceInTicks * curExpIndex, } @@ -64,13 +64,13 @@ func buildTickExpCache() { curExpIndex += 1 } - minPrice := sdkOneDec + minPrice := osmomathBigOneDec curExpIndex = -1 - for minPrice.GT(types.MinSpotPrice) { + for minPrice.GT(osmomath.NewDecWithPrec(1, 30)) { tickExpCache[curExpIndex] = &tickExpIndexData{ // price range 10^curExpIndex to 10^(curExpIndex + 1). (0.001, 0.01) - initialPrice: powTenBigDec(curExpIndex).SDKDec(), - maxPrice: powTenBigDec(curExpIndex + 1).SDKDec(), + initialPrice: powTenBigDec(curExpIndex), + maxPrice: powTenBigDec(curExpIndex + 1), additiveIncrementPerTick: powTenBigDec(types.ExponentAtPriceOne + curExpIndex), initialTick: geometricExponentIncrementDistanceInTicks * curExpIndex, } diff --git a/x/concentrated-liquidity/math/tick.go b/x/concentrated-liquidity/math/tick.go index 5e5d9d36490..fd38988c0f9 100644 --- a/x/concentrated-liquidity/math/tick.go +++ b/x/concentrated-liquidity/math/tick.go @@ -68,6 +68,10 @@ func TickToPrice(tickIndex int64) (price osmomath.BigDec, err error) { return osmomath.OneDec(), nil } + if tickIndex == -270000000 { + return osmomath.SmallestDec(), nil + } + // The formula is as follows: geometricExponentIncrementDistanceInTicks = 9 * 10**(-exponentAtPriceOne) // Due to sdk.Power restrictions, if the resulting power is negative, we take 9 * (1/10**exponentAtPriceOne) exponentAtPriceOne := types.ExponentAtPriceOne @@ -104,7 +108,7 @@ func TickToPrice(tickIndex int64) (price osmomath.BigDec, err error) { // defense in depth, this logic would not be reached due to use having checked if given tick is in between // min tick and max tick. - if price.GT(osmomath.BigDecFromSDKDec(types.MaxSpotPrice)) || price.LT(osmomath.BigDecFromSDKDec(types.MinSpotPrice)) { + if price.GT(types.MaxSpotPriceBigDec) || price.LT(types.MinSpotPriceBigDec) { return osmomath.BigDec{}, types.PriceBoundError{ProvidedPrice: price, MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice} } return price, nil @@ -139,8 +143,8 @@ func RoundDownTickToSpacing(tickIndex int64, tickSpacing int64) (int64, error) { // SqrtPriceToTickRoundDown converts the given sqrt price to its corresponding tick rounded down // to the nearest tick spacing. -func SqrtPriceToTickRoundDownSpacing(sqrtPrice sdk.Dec, tickSpacing uint64) (int64, error) { - tickIndex, err := CalculateSqrtPriceToTick(osmomath.BigDecFromSDKDec(sqrtPrice)) +func SqrtPriceToTickRoundDownSpacing(sqrtPrice osmomath.BigDec, tickSpacing uint64) (int64, error) { + tickIndex, err := CalculateSqrtPriceToTick(sqrtPrice) if err != nil { return 0, err } @@ -169,14 +173,14 @@ func powTenBigDec(exponent int64) osmomath.BigDec { return bigNegPowersOfTen[-exponent] } -func CalculatePriceToTickDec(price sdk.Dec) (tickIndex sdk.Dec, err error) { +func CalculatePriceToTickDec(price osmomath.BigDec) (tickIndex sdk.Dec, err error) { if price.IsNegative() { return sdk.ZeroDec(), fmt.Errorf("price must be greater than zero") } - if price.GT(types.MaxSpotPrice) || price.LT(types.MinSpotPrice) { - return sdk.ZeroDec(), types.PriceBoundError{ProvidedPrice: osmomath.BigDecFromSDKDec(price), MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice} + if price.GT(types.MaxSpotPriceBigDec) || price.LT(types.MinSpotPriceBigDec) { + return sdk.ZeroDec(), types.PriceBoundError{ProvidedPrice: price, MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice} } - if price.Equal(sdkOneDec) { + if price.Equal(osmomathBigOneDec) { return sdk.ZeroDec(), nil } @@ -186,7 +190,7 @@ func CalculatePriceToTickDec(price sdk.Dec) (tickIndex sdk.Dec, err error) { // If price < 1, we search for the first geometric spacing w/ a min price smaller than our price. // TODO: We can optimize by using smarter search algorithms var geoSpacing *tickExpIndexData - if price.GT(sdkOneDec) { + if price.GT(osmomathBigOneDec) { index := 0 geoSpacing = tickExpCache[int64(index)] for geoSpacing.maxPrice.LT(price) { @@ -205,7 +209,7 @@ func CalculatePriceToTickDec(price sdk.Dec) (tickIndex sdk.Dec, err error) { // We know were between (geoSpacing.initialPrice, geoSpacing.endPrice) // The number of ticks that need to be filled by our current spacing is // (price - geoSpacing.initialPrice) / geoSpacing.additiveIncrementPerTick - priceInThisExponent := osmomath.BigDecFromSDKDec(price.Sub(geoSpacing.initialPrice)) + priceInThisExponent := price.Sub(geoSpacing.initialPrice) ticksFilledByCurrentSpacing := priceInThisExponent.Quo(geoSpacing.additiveIncrementPerTick) // we get the bucket index by: // * taking the bucket index of the smallest price in this tick @@ -225,11 +229,7 @@ func CalculateSqrtPriceToTick(sqrtPrice osmomath.BigDec) (tickIndex int64, err e // and move it in a +/- 1 tick range based on the sqrt price those ticks would imply. price := sqrtPrice.Mul(sqrtPrice) - // It is acceptable to truncate price as the minimum we support is - // 10**-12 which is above the smallest value of sdk.Dec. - priceDec := price.SDKDec() - - tick, err := CalculatePriceToTickDec(priceDec) + tick, err := CalculatePriceToTickDec(price) if err != nil { return 0, err } diff --git a/x/concentrated-liquidity/math/tick_test.go b/x/concentrated-liquidity/math/tick_test.go index bfbcf722687..70ac00ae282 100644 --- a/x/concentrated-liquidity/math/tick_test.go +++ b/x/concentrated-liquidity/math/tick_test.go @@ -25,7 +25,7 @@ var ( ) // testing helper for price to tick, state machine only implements sqrt price to tick. -func PriceToTick(price sdk.Dec) (int64, error) { +func PriceToTick(price osmomath.BigDec) (int64, error) { tickDec, err := math.CalculatePriceToTickDec(price) tickIndex := tickDec.TruncateInt64() return tickIndex, err @@ -33,7 +33,7 @@ func PriceToTick(price sdk.Dec) (int64, error) { // testing helper for price to tick round down spacing, // state machine only implements sqrt price to tick round dow spacing. -func PriceToTickRoundDownSpacing(price sdk.Dec, tickSpacing uint64) (int64, error) { +func PriceToTickRoundDownSpacing(price osmomath.BigDec, tickSpacing uint64) (int64, error) { tickIndex, err := PriceToTick(price) if err != nil { return 0, err @@ -311,88 +311,88 @@ func TestPriceToTick(t *testing.T) { ) testCases := map[string]struct { - price sdk.Dec + price osmomath.BigDec tickExpected int64 expectedError error }{ "BTC <> USD, tick 38035200 -> price 30352": { - price: sdk.MustNewDecFromStr("30352"), + price: osmomath.MustNewDecFromStr("30352"), tickExpected: 38035200, }, "BTC <> USD, tick 38035300 + 100 -> price 30353": { - price: sdk.MustNewDecFromStr("30353"), + price: osmomath.MustNewDecFromStr("30353"), tickExpected: 38035300, }, "SHIB <> USD, tick -44821000 -> price 0.000011790": { - price: sdk.MustNewDecFromStr("0.000011790"), + price: osmomath.MustNewDecFromStr("0.000011790"), tickExpected: -44821000, }, "SHIB <> USD, tick -44820900 -> price 0.000011791": { - price: sdk.MustNewDecFromStr("0.000011791"), + price: osmomath.MustNewDecFromStr("0.000011791"), tickExpected: -44820900, }, "ETH <> BTC, tick -12104000 -> price 0.068960": { - price: sdk.MustNewDecFromStr("0.068960"), + price: osmomath.MustNewDecFromStr("0.068960"), tickExpected: -12104000, }, "ETH <> BTC, tick -12104000 + 100 -> price 0.068961": { - price: sdk.MustNewDecFromStr("0.068961"), + price: osmomath.MustNewDecFromStr("0.068961"), tickExpected: -12103900, }, "max sqrt price -1, max neg tick six - 100 -> max tick neg six - 100": { - price: sdk.MustNewDecFromStr("99999000000000000000000000000000000000"), + price: osmomath.MustNewDecFromStr("99999000000000000000000000000000000000"), tickExpected: types.MaxTick - 100, }, "max sqrt price, max tick neg six -> max spot price": { - price: types.MaxSqrtPrice.Power(2), + price: osmomath.BigDecFromSDKDec(types.MaxSqrtPrice.Power(2)), tickExpected: types.MaxTick, }, "Gyen <> USD, tick -20594000 -> price 0.0074060": { - price: sdk.MustNewDecFromStr("0.007406"), + price: osmomath.MustNewDecFromStr("0.007406"), tickExpected: -20594000, }, "Gyen <> USD, tick -20594000 + 100 -> price 0.0074061": { - price: sdk.MustNewDecFromStr("0.0074061"), + price: osmomath.MustNewDecFromStr("0.0074061"), tickExpected: -20593900, }, "Spell <> USD, tick -29204000 -> price 0.00077960": { - price: sdk.MustNewDecFromStr("0.0007796"), + price: osmomath.MustNewDecFromStr("0.0007796"), tickExpected: -29204000, }, "Spell <> USD, tick -29204000 + 100 -> price 0.00077961": { - price: sdk.MustNewDecFromStr("0.00077961"), + price: osmomath.MustNewDecFromStr("0.00077961"), tickExpected: -29203900, }, "Atom <> Osmo, tick -12150000 -> price 0.068500": { - price: sdk.MustNewDecFromStr("0.0685"), + price: osmomath.MustNewDecFromStr("0.0685"), tickExpected: -12150000, }, "Atom <> Osmo, tick -12150000 + 100 -> price 0.068501": { - price: sdk.MustNewDecFromStr("0.068501"), + price: osmomath.MustNewDecFromStr("0.068501"), tickExpected: -12149900, }, "Boot <> Osmo, tick 64576000 -> price 25760000": { - price: sdk.MustNewDecFromStr("25760000"), + price: osmomath.MustNewDecFromStr("25760000"), tickExpected: 64576000, }, "Boot <> Osmo, tick 64576000 + 100 -> price 25761000": { - price: sdk.MustNewDecFromStr("25761000"), + price: osmomath.MustNewDecFromStr("25761000"), tickExpected: 64576100, }, "price is one Dec": { - price: sdk.OneDec(), + price: osmomath.OneDec(), tickExpected: 0, }, "price is negative decimal": { - price: sdk.OneDec().Neg(), + price: osmomath.OneDec().Neg(), expectedError: fmt.Errorf("price must be greater than zero"), }, "price is greater than max spot price": { - price: types.MaxSpotPrice.Add(sdk.OneDec()), + price: osmomath.BigDecFromSDKDec(types.MaxSpotPrice.Add(sdk.OneDec())), expectedError: types.PriceBoundError{ProvidedPrice: osmomath.BigDecFromSDKDec(types.MaxSpotPrice).Add(osmomath.OneDec()), MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice}, }, "price is smaller than min spot price": { - price: types.MinSpotPrice.Quo(sdk.NewDec(10)), + price: osmomath.BigDecFromSDKDec(types.MinSpotPrice.Quo(sdk.NewDec(10))), expectedError: types.PriceBoundError{ProvidedPrice: osmomath.BigDecFromSDKDec(types.MinSpotPrice.Quo(sdk.NewDec(10))), MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice}, }, } @@ -419,64 +419,64 @@ func TestPriceToTick(t *testing.T) { func TestPriceToTickRoundDown(t *testing.T) { testCases := map[string]struct { - price sdk.Dec + price osmomath.BigDec tickSpacing uint64 tickExpected int64 }{ "tick spacing 100, price of 1": { - price: sdk.OneDec(), + price: osmomath.OneDec(), tickSpacing: defaultTickSpacing, tickExpected: 0, }, "tick spacing 100, price of 1.000030, tick 30 -> 0": { - price: sdk.MustNewDecFromStr("1.000030"), + price: osmomath.MustNewDecFromStr("1.000030"), tickSpacing: defaultTickSpacing, tickExpected: 0, }, "tick spacing 100, price of 0.9999970, tick -30 -> -100": { - price: sdk.MustNewDecFromStr("0.9999970"), + price: osmomath.MustNewDecFromStr("0.9999970"), tickSpacing: defaultTickSpacing, tickExpected: -100, }, "tick spacing 50, price of 0.9999730, tick -270 -> -300": { - price: sdk.MustNewDecFromStr("0.9999730"), + price: osmomath.MustNewDecFromStr("0.9999730"), tickSpacing: 50, tickExpected: -300, }, "tick spacing 100, MinSpotPrice, MinTick": { - price: types.MinSpotPrice, + price: osmomath.BigDecFromSDKDec(types.MinSpotPrice), tickSpacing: defaultTickSpacing, tickExpected: types.MinInitializedTick, }, "tick spacing 100, Spot price one tick above min, one tick above min -> MinTick": { - price: types.MinSpotPrice.Add(sdk.SmallestDec()), + price: osmomath.BigDecFromSDKDec(types.MinSpotPrice.Add(sdk.SmallestDec())), tickSpacing: defaultTickSpacing, // Since the tick should always be the closest tick below (and `smallestDec` isn't sufficient // to push us into the next tick), we expect MinTick to be returned here. tickExpected: types.MinInitializedTick, }, "tick spacing 100, Spot price one tick below max, one tick below max -> MaxTick - 1": { - price: closestPriceBelowMaxPriceDefaultTickSpacing, + price: osmomath.BigDecFromSDKDec(closestPriceBelowMaxPriceDefaultTickSpacing), tickSpacing: defaultTickSpacing, tickExpected: types.MaxTick - 100, }, "tick spacing 100, Spot price 100_000_050 -> 72000000": { - price: sdk.NewDec(100_000_050), + price: osmomath.NewBigDec(100_000_050), tickSpacing: defaultTickSpacing, tickExpected: 72000000, }, "tick spacing 100, Spot price 100_000_051 -> 72000100 (rounded up to tick spacing)": { - price: sdk.NewDec(100_000_051), + price: osmomath.NewBigDec(100_000_051), tickSpacing: defaultTickSpacing, tickExpected: 72000000, }, "tick spacing 1, Spot price 100_000_051 -> 72000000 no tick spacing rounding": { - price: sdk.NewDec(100_000_051), + price: osmomath.NewBigDec(100_000_051), tickSpacing: 1, tickExpected: 72000000, }, "tick spacing 1, Spot price 100_000_101 -> 72000001 no tick spacing rounding": { - price: sdk.NewDec(100_000_101), + price: osmomath.NewBigDec(100_000_101), tickSpacing: 1, tickExpected: 72000001, }, @@ -498,90 +498,90 @@ func TestPriceToTickRoundDown(t *testing.T) { // TODO: Revisit this test, under the lens of bucket index. func TestTickToSqrtPricePriceToTick_InverseRelationship(t *testing.T) { type testcase struct { - price sdk.Dec - truncatedPrice sdk.Dec + price osmomath.BigDec + truncatedPrice osmomath.BigDec tickExpected int64 } testCases := map[string]testcase{ "50000 to tick": { - price: sdk.MustNewDecFromStr("50000"), + price: osmomath.MustNewDecFromStr("50000"), tickExpected: 40000000, }, "5.01 to tick": { - price: sdk.MustNewDecFromStr("5.01"), + price: osmomath.MustNewDecFromStr("5.01"), tickExpected: 4010000, }, "50000.01 to tick": { - price: sdk.MustNewDecFromStr("50000.01"), + price: osmomath.MustNewDecFromStr("50000.01"), tickExpected: 40000001, }, "0.090001 to tick": { - price: sdk.MustNewDecFromStr("0.090001"), + price: osmomath.MustNewDecFromStr("0.090001"), tickExpected: -9999900, }, "0.9998 to tick": { - price: sdk.MustNewDecFromStr("0.9998"), + price: osmomath.MustNewDecFromStr("0.9998"), tickExpected: -2000, }, "53030 to tick": { - price: sdk.MustNewDecFromStr("53030"), + price: osmomath.MustNewDecFromStr("53030"), tickExpected: 40303000, }, "max spot price": { - price: types.MaxSpotPrice, + price: osmomath.BigDecFromSDKDec(types.MaxSpotPrice), tickExpected: types.MaxTick, }, "max spot price - smallest price delta given exponent at price one of -6": { // 37 - 6 is calculated by counting the exponent of max spot price and subtracting exponent at price one - price: types.MaxSpotPrice.Sub(sdk.NewDec(10).PowerMut(37 - 6)), + price: osmomath.BigDecFromSDKDec(types.MaxSpotPrice.Sub(sdk.NewDec(10).PowerMut(37 - 6))), tickExpected: types.MaxTick - 1, // still max }, "min spot price": { - price: types.MinSpotPrice, + price: osmomath.BigDecFromSDKDec(types.MinSpotPrice), tickExpected: types.MinInitializedTick, }, "smallest + min price + tick": { - price: sdk.MustNewDecFromStr("0.000000000001000001"), + price: osmomath.MustNewDecFromStr("0.000000000001000001"), tickExpected: types.MinInitializedTick + 1, }, "at price level of 0.01 - odd": { - price: sdk.MustNewDecFromStr("0.012345670000000000"), + price: osmomath.MustNewDecFromStr("0.012345670000000000"), tickExpected: -17765433, }, "at price level of 0.01 - even": { - price: sdk.MustNewDecFromStr("0.01234568000000000"), + price: osmomath.MustNewDecFromStr("0.01234568000000000"), tickExpected: -17765432, }, "at min price level of 0.01 - odd": { - price: sdk.MustNewDecFromStr("0.000000000001234567"), + price: osmomath.MustNewDecFromStr("0.000000000001234567"), tickExpected: -107765433, }, "at min price level of 0.01 - even": { - price: sdk.MustNewDecFromStr("0.000000000001234568"), + price: osmomath.MustNewDecFromStr("0.000000000001234568"), tickExpected: -107765432, }, "at price level of 1_000_000_000 - odd end": { - price: sdk.MustNewDecFromStr("1234567000"), + price: osmomath.MustNewDecFromStr("1234567000"), tickExpected: 81234567, }, "at price level of 1_000_000_000 - in-between supported": { - price: sdk.MustNewDecFromStr("1234567500"), + price: osmomath.MustNewDecFromStr("1234567500"), tickExpected: 81234567, - truncatedPrice: sdk.MustNewDecFromStr("1234567000"), + truncatedPrice: osmomath.MustNewDecFromStr("1234567000"), }, "at price level of 1_000_000_000 - even end": { - price: sdk.MustNewDecFromStr("1234568000"), + price: osmomath.MustNewDecFromStr("1234568000"), tickExpected: 81234568, }, "inverse testing with 1": { - price: sdk.OneDec(), + price: osmomath.OneDec(), tickExpected: 0, }, } var powTen int64 = 10 for i := 1; i < 13; i++ { testCases[fmt.Sprintf("min spot price * 10^%d", i)] = testcase{ - price: types.MinSpotPrice.MulInt64(powTen), + price: osmomath.BigDecFromSDKDec(types.MinSpotPrice.MulInt64(powTen)), tickExpected: types.MinInitializedTick + (int64(i) * 9e6), } powTen *= 10 @@ -605,7 +605,7 @@ func TestTickToSqrtPricePriceToTick_InverseRelationship(t *testing.T) { require.Equal(t, expectedPrice, price) // 3. Compute tick from inverse price (inverse tick) - inverseTickFromPrice, err := PriceToTick(price.SDKDec()) + inverseTickFromPrice, err := PriceToTick(price) require.NoError(t, err) // Make sure original tick and inverse tick match. @@ -630,16 +630,16 @@ func TestTickToSqrtPricePriceToTick_InverseRelationship(t *testing.T) { func TestPriceToTick_ErrorCases(t *testing.T) { testCases := map[string]struct { - price sdk.Dec + price osmomath.BigDec }{ "use negative price": { - price: sdk.OneDec().Neg(), + price: osmomath.OneDec().Neg(), }, "price is greater than max spot price": { - price: types.MaxSpotPrice.Add(sdk.OneDec()), + price: osmomath.BigDecFromSDKDec(types.MaxSpotPrice.Add(sdk.OneDec())), }, "price is less than min spot price": { - price: types.MinSpotPrice.Sub(sdk.OneDec()), + price: osmomath.BigDecFromSDKDec(types.MinSpotPrice.Sub(sdk.OneDec())), }, } for name, tc := range testCases { @@ -675,33 +675,57 @@ func TestTickToPrice_ErrorCases(t *testing.T) { func TestCalculatePriceToTick(t *testing.T) { testCases := map[string]struct { - price sdk.Dec + price osmomath.BigDec expectedTickIndex int64 }{ "Price greater than 1": { - price: sdk.MustNewDecFromStr("9.78"), + price: osmomath.MustNewDecFromStr("9.78"), expectedTickIndex: 8780000, }, "Price less than 1": { - price: sdk.MustNewDecFromStr("0.71"), + price: osmomath.MustNewDecFromStr("0.71"), expectedTickIndex: -2900000, }, "100_000_000 -> 72000000": { - price: sdk.NewDec(100_000_000), + price: osmomath.NewBigDec(100_000_000), expectedTickIndex: 72000000, }, "100_000_050 -> 72000000": { - price: sdk.NewDec(100_000_050), + price: osmomath.NewBigDec(100_000_050), expectedTickIndex: 72000000, }, "100_000_051 -> 72000000": { - price: sdk.NewDec(100_000_051), + price: osmomath.NewBigDec(100_000_051), expectedTickIndex: 72000000, }, "100_000_100 -> 72000001": { - price: sdk.NewDec(100_000_100), + price: osmomath.NewBigDec(100_000_100), expectedTickIndex: 72000001, }, + "1 -> 0": { + price: osmomath.OneDec(), + expectedTickIndex: 0, + }, + "10^-1 -> -9000000": { + price: osmomath.MustNewDecFromStr("0.1"), + expectedTickIndex: -9000000, + }, + "10^-12 -> 12 * -9000000": { + price: osmomath.NewDecFromIntWithPrec(osmomath.OneInt(), 12), + expectedTickIndex: -9000000 * 12, + }, + "10^-13 -> 13 * -9000000": { + price: osmomath.NewDecFromIntWithPrec(osmomath.OneInt(), 13), + expectedTickIndex: -9000000 * 13, + }, + "10^-18 -> 18 * -9000000": { + price: osmomath.NewDecFromIntWithPrec(osmomath.OneInt(), 18), + expectedTickIndex: -9000000 * 18, + }, + "10^-30 -> 30 * -9000000": { + price: osmomath.NewDecFromIntWithPrec(osmomath.OneInt(), 30), + expectedTickIndex: -9000000 * 30, + }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { @@ -873,7 +897,7 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { } for name, tc := range testCases { t.Run(name, func(t *testing.T) { - tickIndex, err := math.SqrtPriceToTickRoundDownSpacing(tc.sqrtPrice.SDKDec(), tc.tickSpacing) + tickIndex, err := math.SqrtPriceToTickRoundDownSpacing(tc.sqrtPrice, tc.tickSpacing) require.NoError(t, err) require.Equal(t, tc.tickExpected, tickIndex) @@ -895,13 +919,13 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { func TestMonotnicityAtPriceBounds(t *testing.T) { // Note: this starting value was manually adjusted until it was on the boundary of where the // ticks started becoming monotonic - x := int64(-162000000) + x := int64(-9000000 * 30) lastValueMonotonic := true highestMonotonicTick := types.MinInitializedTick // Find the highest tick where the sqrt price is monotonic. If nothing is found in 50,000 ticks, // lastValueMonotonic is false and starting value should be adjusted. - for i := 0; i < 162000000; i++ { + for i := -9000000 * 30; i < 0; i++ { _, xSqrtPrice, err := math.TickToSqrtPrice(x) require.NoError(t, err) _, xSqrtPriceNext, err := math.TickToSqrtPrice(x + 1) diff --git a/x/concentrated-liquidity/types/constants.go b/x/concentrated-liquidity/types/constants.go index 636d443b432..d302a84255f 100644 --- a/x/concentrated-liquidity/types/constants.go +++ b/x/concentrated-liquidity/types/constants.go @@ -10,7 +10,8 @@ import ( const ( // Precomputed values for min and max tick - MinInitializedTick, MaxTick int64 = -162000000, 342000000 + // With 9000000 ticks per order of magnitude, 10^-30 and 10^38 + MinInitializedTick, MaxTick int64 = -270000000, 342000000 // If we consume all liquidity and cross the min initialized tick, // our current tick will equal to MinInitializedTick - 1 with zero liquidity. // However, note that this tick cannot be crossed. If current tick @@ -27,8 +28,12 @@ const ( ) var ( - MaxSpotPrice = sdk.MustNewDecFromStr("100000000000000000000000000000000000000") - MinSpotPrice = sdk.MustNewDecFromStr("0.000000000000000001") // 10^-12 + // TODO: change these + MaxSpotPrice = sdk.MustNewDecFromStr("100000000000000000000000000000000000000") + MinSpotPrice = sdk.MustNewDecFromStr("0.000000000000000001") // 10^-12 + + MaxSpotPriceBigDec = osmomath.BigDecFromSDKDec(MaxSpotPrice) + MinSpotPriceBigDec = osmomath.SmallestDec() MaxSqrtPrice = osmomath.MustMonotonicSqrt(MaxSpotPrice) MinSqrtPrice = osmomath.MustMonotonicSqrt(MinSpotPrice) MaxSqrtPriceBigDec = osmomath.BigDecFromSDKDec(MaxSqrtPrice)