From 8ac3efb2d1c9a2e3d880c2dc1bd61816f2a94614 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 8 Sep 2023 11:43:12 -0400 Subject: [PATCH] refactor: state-compatible big decimal tick to sqrt price conversions (#6317) * refactor: state-compatible big decimal tick to sqrt price conversions * updates and clean ups * lint --- CHANGELOG.md | 1 + tests/e2e/e2e_cl_test.go | 8 +- x/concentrated-liquidity/math/math_test.go | 10 +- x/concentrated-liquidity/math/tick.go | 97 +++++-- x/concentrated-liquidity/math/tick_test.go | 266 +++++++++++++----- x/concentrated-liquidity/model/pool_test.go | 6 +- x/concentrated-liquidity/query.go | 4 +- x/concentrated-liquidity/query_test.go | 2 +- x/concentrated-liquidity/swaps.go | 2 +- x/concentrated-liquidity/swaps_test.go | 4 +- .../swaps_tick_cross_test.go | 2 +- x/concentrated-liquidity/types/constants.go | 12 +- 12 files changed, 300 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c40898e540f..a1b1d80cab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### API Breaks * [#6256](https://github.com/osmosis-labs/osmosis/pull/6256) Refactor CalcPriceToTick to operate on BigDec price to support new price range. +* [#6317](https://github.com/osmosis-labs/osmosis/pull/6317) Remove price return from CL `math.TickToSqrtPrice` ## v19.0.0 diff --git a/tests/e2e/e2e_cl_test.go b/tests/e2e/e2e_cl_test.go index ddd1e3142c9..58b53d03e3f 100644 --- a/tests/e2e/e2e_cl_test.go +++ b/tests/e2e/e2e_cl_test.go @@ -353,9 +353,9 @@ func (s *IntegrationTestSuite) ConcentratedLiquidity() { nextInitTick := int64(40000) // address1 position1's upper tick // Calculate sqrtPrice after and at the next initialized tick (upperTick of address1 position1 - 40000) - _, sqrtPriceAfterNextInitializedTick, err := clmath.TickToSqrtPrice(nextInitTick + tickOffset) + sqrtPriceAfterNextInitializedTick, err := clmath.TickToSqrtPrice(nextInitTick + tickOffset) s.Require().NoError(err) - _, sqrtPriceAtNextInitializedTick, err := clmath.TickToSqrtPrice(nextInitTick) + sqrtPriceAtNextInitializedTick, err := clmath.TickToSqrtPrice(nextInitTick) s.Require().NoError(err) sqrtPriceAfterNextInitializedTickBigDec := sqrtPriceAfterNextInitializedTick sqrtPriceAtNextInitializedTickBigDec := sqrtPriceAtNextInitializedTick @@ -500,9 +500,9 @@ func (s *IntegrationTestSuite) ConcentratedLiquidity() { // Using: CalcAmount0Delta = liquidity * ((sqrtPriceB - sqrtPriceA) / (sqrtPriceB * sqrtPriceA)) // Calculate sqrtPrice after and at the next initialized tick (which is upperTick of address1 position1 - 40000) - _, sqrtPricebBelowNextInitializedTick, err := clmath.TickToSqrtPrice(nextInitTick - tickOffset) + sqrtPricebBelowNextInitializedTick, err := clmath.TickToSqrtPrice(nextInitTick - tickOffset) s.Require().NoError(err) - _, sqrtPriceAtNextInitializedTick, err = clmath.TickToSqrtPrice(nextInitTick) + sqrtPriceAtNextInitializedTick, err = clmath.TickToSqrtPrice(nextInitTick) s.Require().NoError(err) sqrtPriceAtNextInitializedTickBigDec = sqrtPriceAtNextInitializedTick diff --git a/x/concentrated-liquidity/math/math_test.go b/x/concentrated-liquidity/math/math_test.go index f0711750106..930327ddbeb 100644 --- a/x/concentrated-liquidity/math/math_test.go +++ b/x/concentrated-liquidity/math/math_test.go @@ -7,7 +7,7 @@ import ( "github.com/osmosis-labs/osmosis/osmomath" "github.com/osmosis-labs/osmosis/v19/x/concentrated-liquidity/math" - cltypes "github.com/osmosis-labs/osmosis/v19/x/concentrated-liquidity/types" + "github.com/osmosis-labs/osmosis/v19/x/concentrated-liquidity/types" ) var ( @@ -314,8 +314,8 @@ func TestGetLiquidityFromAmounts(t *testing.T) { }, "full range, price proportional to amounts, equal liquidities (some rounding error) price of 4": { currentSqrtP: sqrt(osmomath.NewDec(4)), - sqrtPHigh: osmomath.BigDecFromDec(cltypes.MaxSqrtPrice), - sqrtPLow: osmomath.BigDecFromDec(cltypes.MinSqrtPrice), + sqrtPHigh: osmomath.BigDecFromDec(types.MaxSqrtPrice), + sqrtPLow: osmomath.BigDecFromDec(types.MinSqrtPrice), amount0Desired: osmomath.NewInt(4), amount1Desired: osmomath.NewInt(16), @@ -325,8 +325,8 @@ func TestGetLiquidityFromAmounts(t *testing.T) { }, "full range, price proportional to amounts, equal liquidities (some rounding error) price of 2": { currentSqrtP: sqrt(osmomath.NewDec(2)), - sqrtPHigh: osmomath.BigDecFromDec(cltypes.MaxSqrtPrice), - sqrtPLow: osmomath.BigDecFromDec(cltypes.MinSqrtPrice), + sqrtPHigh: osmomath.BigDecFromDec(types.MaxSqrtPrice), + sqrtPLow: osmomath.BigDecFromDec(types.MinSqrtPrice), amount0Desired: osmomath.NewInt(1), amount1Desired: osmomath.NewInt(2), diff --git a/x/concentrated-liquidity/math/tick.go b/x/concentrated-liquidity/math/tick.go index 39e7a8b9b7b..38bc2efaf66 100644 --- a/x/concentrated-liquidity/math/tick.go +++ b/x/concentrated-liquidity/math/tick.go @@ -15,11 +15,11 @@ func TicksToSqrtPrice(lowerTick, upperTick int64) (osmomath.BigDec, osmomath.Big if lowerTick >= upperTick { return osmomath.BigDec{}, osmomath.BigDec{}, types.InvalidLowerUpperTickError{LowerTick: lowerTick, UpperTick: upperTick} } - _, sqrtPriceUpperTick, err := TickToSqrtPrice(upperTick) + sqrtPriceUpperTick, err := TickToSqrtPrice(upperTick) if err != nil { return osmomath.BigDec{}, osmomath.BigDec{}, err } - _, sqrtPriceLowerTick, err := TickToSqrtPrice(lowerTick) + sqrtPriceLowerTick, err := TickToSqrtPrice(lowerTick) if err != nil { return osmomath.BigDec{}, osmomath.BigDec{}, err } @@ -29,22 +29,36 @@ func TicksToSqrtPrice(lowerTick, upperTick int64) (osmomath.BigDec, osmomath.Big // TickToSqrtPrice returns the sqrtPrice given a tickIndex // If tickIndex is zero, the function returns osmomath.OneDec(). // It is the combination of calling TickToPrice followed by Sqrt. -func TickToSqrtPrice(tickIndex int64) (osmomath.BigDec, osmomath.BigDec, error) { +func TickToSqrtPrice(tickIndex int64) (osmomath.BigDec, error) { priceBigDec, err := TickToPrice(tickIndex) if err != nil { - return osmomath.BigDec{}, osmomath.BigDec{}, err + return osmomath.BigDec{}, err } - // It is acceptable to truncate here as TickToPrice() function converts - // from osmomath.Dec to osmomath.BigDec before returning. - price := priceBigDec.Dec() + // N.B. at launch, we only supported price range + // of [tick(10^-12), tick(MaxSpotPrice)]. + // To maintain backwards state-compatibility, we use the original + // math based on 18 precision decimal on the at the launch tick range. + if tickIndex >= types.MinInitializedTick { + // It is acceptable to truncate here as TickToPrice() function converts + // from osmomath.Dec to osmomath.BigDec before returning specifically for this range. + // As a result, there is no data loss. + price := priceBigDec.Dec() + + sqrtPrice, err := osmomath.MonotonicSqrt(price) + if err != nil { + return osmomath.BigDec{}, err + } + return osmomath.BigDecFromDec(sqrtPrice), nil + } - // Determine the sqrtPrice from the price - sqrtPrice, err := osmomath.MonotonicSqrt(price) + // For the newly extended range of [tick(MinSpotPriceV2), MinInitializedTick), we use the new math + // based on 36 precision decimal. + sqrtPrice, err := osmomath.MonotonicSqrtBigDec(priceBigDec) if err != nil { - return osmomath.BigDec{}, osmomath.BigDec{}, err + return osmomath.BigDec{}, err } - return osmomath.BigDecFromDec(price), osmomath.BigDecFromDec(sqrtPrice), nil + return sqrtPrice, nil } // TickToPrice returns the price given a tickIndex @@ -54,9 +68,18 @@ func TickToPrice(tickIndex int64) (osmomath.BigDec, error) { return osmomath.OneBigDec(), nil } + // N.B. We special case MinInitializedTickV2 and MinCurrentTickV2 since MinInitializedTickV2 + // is the first one that requires taking 10 to the exponent of (-31 + -6) = -37 + // Given BigDec's precision of 36, that cannot be supported. + // The fact that MinInitializedTickV2 and MinCurrentTickV2 translate to the same + // price is acceptable since MinCurrentTickV2 cannot be initialized. + if tickIndex == types.MinInitializedTickV2 || tickIndex == types.MinCurrentTickV2 { + return types.MinSpotPriceV2, nil + } + // Check that the tick index is between min and max value - if tickIndex < types.MinCurrentTick { - return osmomath.BigDec{}, types.TickIndexMinimumError{MinTick: types.MinCurrentTick} + if tickIndex < types.MinCurrentTickV2 { + return osmomath.BigDec{}, types.TickIndexMinimumError{MinTick: types.MinCurrentTickV2} } if tickIndex > types.MaxTick { return osmomath.BigDec{}, types.TickIndexMaximumError{MaxTick: types.MaxTick} @@ -78,17 +101,27 @@ func TickToPrice(tickIndex int64) (osmomath.BigDec, error) { currentAdditiveIncrementInTicks := powTenBigDec(exponentAtCurrentTick) // Now, starting at the minimum tick of the current increment, we calculate how many ticks in the current geometricExponent we have passed - numAdditiveTicks := tickIndex - (geometricExponentDelta * geometricExponentIncrementDistanceInTicks) + numAdditiveTicks := osmomath.NewBigDec(tickIndex - (geometricExponentDelta * geometricExponentIncrementDistanceInTicks)) + + var price osmomath.BigDec // Finally, we can calculate the price - price := PowTenInternal(geometricExponentDelta).Add(osmomath.NewBigDec(numAdditiveTicks).Mul(currentAdditiveIncrementInTicks).Dec()) + // Note that to maintain backwards state-compatibility, we utilize the + // original math based on 18 precision decimal on the range of [MinInitializedTick, tick(MaxSpotPrice)] + // For the newly extended range of [MinInitializedTickV2, MinInitializedTick), we use the new math + // based on 36 precision decimal. + if tickIndex < types.MinInitializedTick { + price = powTenBigDec(geometricExponentDelta).Add(numAdditiveTicks.Mul(currentAdditiveIncrementInTicks)) + } else { + price = osmomath.BigDecFromDec(PowTenInternal(geometricExponentDelta).Add(numAdditiveTicks.Mul(currentAdditiveIncrementInTicks).Dec())) + } // 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 osmomath.BigDec{}, types.PriceBoundError{ProvidedPrice: osmomath.BigDecFromDec(price), MinSpotPrice: types.MinSpotPriceBigDec, MaxSpotPrice: types.MaxSpotPrice} + if price.GT(types.MaxSpotPriceBigDec) || price.LT(types.MinSpotPriceV2) { + return osmomath.BigDec{}, types.PriceBoundError{ProvidedPrice: price, MinSpotPrice: types.MinSpotPriceV2, MaxSpotPrice: types.MaxSpotPrice} } - return osmomath.BigDecFromDec(price), nil + return price, nil } // RoundDownTickToSpacing rounds the tick index down to the nearest tick spacing if the tickIndex is in between authorized tick values @@ -111,8 +144,8 @@ func RoundDownTickToSpacing(tickIndex int64, tickSpacing int64) (int64, error) { // Defense-in-depth check to ensure that the tick index is within the authorized range // Should never get here. - if tickIndex > types.MaxTick || tickIndex < types.MinInitializedTick { - return 0, types.TickIndexNotWithinBoundariesError{ActualTick: tickIndex, MinTick: types.MinInitializedTick, MaxTick: types.MaxTick} + if tickIndex > types.MaxTick || tickIndex < types.MinInitializedTickV2 { + return 0, types.TickIndexNotWithinBoundariesError{ActualTick: tickIndex, MinTick: types.MinInitializedTickV2, MaxTick: types.MaxTick} } return tickIndex, nil @@ -218,6 +251,18 @@ func CalculateSqrtPriceToTick(sqrtPrice osmomath.BigDec) (tickIndex int64, err e return 0, err } + // TODO: remove this check. It is present to maintain backwards state-compatibility with + // v19.x and earlier major releases of Osmosis. + // Once https://github.com/osmosis-labs/osmosis/issues/5726 is fully complete, + // this should be removed. + // + // Backwards state-compatibility is maintained by having the swap and LP logic error + // here in case the price/tick falls below the origina minimum tick bounds that are + // consistent with v19.x and earlier release lines. + if tick < types.MinCurrentTick { + return 0, types.TickIndexMinimumError{MinTick: types.MinCurrentTick} + } + // We have a candidate bucket index `t`. We discern here if: // * sqrtPrice in [TickToSqrtPrice(t - 1), TickToSqrtPrice(t)) // * sqrtPrice in [TickToSqrtPrice(t), TickToSqrtPrice(t + 1)) @@ -231,18 +276,18 @@ func CalculateSqrtPriceToTick(sqrtPrice osmomath.BigDec) (tickIndex int64, err e // We check this at max tick - 1 instead of max tick, since we expect the output to // have some error that can push us over the tick boundary. outOfBounds := false - if tick <= types.MinInitializedTick { - tick = types.MinInitializedTick + 1 + if tick <= types.MinInitializedTickV2 { + tick = types.MinInitializedTickV2 + 1 outOfBounds = true } else if tick >= types.MaxTick-1 { tick = types.MaxTick - 2 outOfBounds = true } - _, sqrtPriceTmin1, errM1 := TickToSqrtPrice(tick - 1) - _, sqrtPriceT, errT := TickToSqrtPrice(tick) - _, sqrtPriceTplus1, errP1 := TickToSqrtPrice(tick + 1) - _, sqrtPriceTplus2, errP2 := TickToSqrtPrice(tick + 2) + sqrtPriceTmin1, errM1 := TickToSqrtPrice(tick - 1) + sqrtPriceT, errT := TickToSqrtPrice(tick) + sqrtPriceTplus1, errP1 := TickToSqrtPrice(tick + 1) + sqrtPriceTplus2, errP2 := TickToSqrtPrice(tick + 2) if errM1 != nil || errT != nil || errP1 != nil || errP2 != nil { return 0, errors.New("internal error in computing square roots within CalculateSqrtPriceToTick") } diff --git a/x/concentrated-liquidity/math/tick_test.go b/x/concentrated-liquidity/math/tick_test.go index ca5cf4d2c58..5ee91c96e06 100644 --- a/x/concentrated-liquidity/math/tick_test.go +++ b/x/concentrated-liquidity/math/tick_test.go @@ -54,84 +54,108 @@ func PriceToTickRoundDownSpacing(price osmomath.BigDec, tickSpacing uint64) (int func TestTickToSqrtPrice(t *testing.T) { testCases := map[string]struct { tickIndex int64 - expectedPrice osmomath.Dec + expectedPrice osmomath.BigDec expectedError error }{ "Ten billionths cent increments at the millionths place: 1": { tickIndex: -51630100, - expectedPrice: osmomath.MustNewDecFromStr("0.0000033699"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.0000033699"), }, "Ten billionths cent increments at the millionths place: 2": { tickIndex: -51630000, - expectedPrice: osmomath.MustNewDecFromStr("0.0000033700"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.0000033700"), }, "One millionths cent increments at the hundredths place: 1": { tickIndex: -11999800, - expectedPrice: osmomath.MustNewDecFromStr("0.070002"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.070002"), }, "One millionths cent increments at the hundredths place: 2": { tickIndex: -11999700, - expectedPrice: osmomath.MustNewDecFromStr("0.070003"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.070003"), }, "One hundred thousandth cent increments at the tenths place: 1": { tickIndex: -999800, - expectedPrice: osmomath.MustNewDecFromStr("0.90002"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.90002"), }, "One hundred thousandth cent increments at the tenths place: 2": { tickIndex: -999700, - expectedPrice: osmomath.MustNewDecFromStr("0.90003"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.90003"), }, "One ten thousandth cent increments at the ones place: 1": { tickIndex: 1000000, - expectedPrice: osmomath.MustNewDecFromStr("2"), + expectedPrice: osmomath.MustNewBigDecFromStr("2"), }, "One dollar increments at the ten thousands place: 2": { tickIndex: 1000100, - expectedPrice: osmomath.MustNewDecFromStr("2.0001"), + expectedPrice: osmomath.MustNewBigDecFromStr("2.0001"), }, "One thousandth cent increments at the tens place: 1": { tickIndex: 9200100, - expectedPrice: osmomath.MustNewDecFromStr("12.001"), + expectedPrice: osmomath.MustNewBigDecFromStr("12.001"), }, "One thousandth cent increments at the tens place: 2": { tickIndex: 9200200, - expectedPrice: osmomath.MustNewDecFromStr("12.002"), + expectedPrice: osmomath.MustNewBigDecFromStr("12.002"), }, "One cent increments at the hundreds place: 1": { tickIndex: 18320100, - expectedPrice: osmomath.MustNewDecFromStr("132.01"), + expectedPrice: osmomath.MustNewBigDecFromStr("132.01"), }, "One cent increments at the hundreds place: 2": { tickIndex: 18320200, - expectedPrice: osmomath.MustNewDecFromStr("132.02"), + expectedPrice: osmomath.MustNewBigDecFromStr("132.02"), }, "Ten cent increments at the thousands place: 1": { tickIndex: 27732100, - expectedPrice: osmomath.MustNewDecFromStr("1732.10"), + expectedPrice: osmomath.MustNewBigDecFromStr("1732.10"), }, "Ten cent increments at the thousands place: 2": { tickIndex: 27732200, - expectedPrice: osmomath.MustNewDecFromStr("1732.20"), + expectedPrice: osmomath.MustNewBigDecFromStr("1732.20"), }, "Dollar increments at the ten thousands place: 1": { tickIndex: 36073200, - expectedPrice: osmomath.MustNewDecFromStr("10732"), + expectedPrice: osmomath.MustNewBigDecFromStr("10732"), }, "Dollar increments at the ten thousands place: 2": { tickIndex: 36073300, - expectedPrice: osmomath.MustNewDecFromStr("10733"), + expectedPrice: osmomath.MustNewBigDecFromStr("10733"), }, "Max tick and min k": { tickIndex: 342000000, - expectedPrice: types.MaxSpotPrice, + expectedPrice: types.MaxSpotPriceBigDec, }, - "Min tick and max k": { - tickIndex: types.MinInitializedTick, - expectedPrice: types.MinSpotPrice, + "tickIndex is MinInitializedTickV1": { + tickIndex: types.MinInitializedTick, + // 1 order of magnitude below min spot price of 10^-12 + 6 orders of magnitude smaller + // to account for exponent at price one of -6. + expectedPrice: types.MinSpotPriceBigDec, + }, + "tickIndex is MinCurrentTickV1": { + tickIndex: types.MinCurrentTick, + // 1 order of magnitude below min spot price of 10^-12 + 6 orders of magnitude smaller + // to account for exponent at price one of -6. + expectedPrice: types.MinSpotPriceBigDec.Sub(osmomath.BigDecFromDec(osmomath.SmallestDec()).Quo(bigTenDec)), + }, + "tickIndex is MinInitializedTickV2": { + tickIndex: types.MinInitializedTickV2, + expectedPrice: types.MinSpotPriceV2, + }, + "tickIndex is MinCurrentTickV2": { + tickIndex: types.MinCurrentTickV2, + expectedPrice: types.MinSpotPriceV2, + }, + "tickIndex is MinInitializedTick + 1 ULP": { + tickIndex: types.MinInitializedTickV2 + 1, + expectedPrice: types.MinSpotPriceV2.Add(smallestBigDec), + }, + "tickIndex is MinInitializedTick + 2 ULP": { + tickIndex: types.MinInitializedTickV2 + 2, + expectedPrice: types.MinSpotPriceV2.Add(smallestBigDec.MulInt64(2)), }, "error: tickIndex less than minimum": { - tickIndex: types.MinCurrentTick - 1, - expectedError: types.TickIndexMinimumError{MinTick: types.MinCurrentTick}, + tickIndex: types.MinCurrentTickV2 - 1, + expectedError: types.TickIndexMinimumError{MinTick: types.MinCurrentTickV2}, }, "error: tickIndex greater than maximum": { tickIndex: types.MaxTick + 1, @@ -139,84 +163,89 @@ func TestTickToSqrtPrice(t *testing.T) { }, "Gyen <> USD, tick -20594000 -> price 0.0074060": { tickIndex: -20594000, - expectedPrice: osmomath.MustNewDecFromStr("0.007406000000000000"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.007406000000000000"), }, "Gyen <> USD, tick -20594000 + 100 -> price 0.0074061": { tickIndex: -20593900, - expectedPrice: osmomath.MustNewDecFromStr("0.007406100000000000"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.007406100000000000"), }, "Spell <> USD, tick -29204000 -> price 0.00077960": { tickIndex: -29204000, - expectedPrice: osmomath.MustNewDecFromStr("0.000779600000000000"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.000779600000000000"), }, "Spell <> USD, tick -29204000 + 100 -> price 0.00077961": { tickIndex: -29203900, - expectedPrice: osmomath.MustNewDecFromStr("0.000779610000000000"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.000779610000000000"), }, "Atom <> Osmo, tick -12150000 -> price 0.068500": { tickIndex: -12150000, - expectedPrice: osmomath.MustNewDecFromStr("0.068500000000000000"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.068500000000000000"), }, "Atom <> Osmo, tick -12150000 + 100 -> price 0.068501": { tickIndex: -12149900, - expectedPrice: osmomath.MustNewDecFromStr("0.068501000000000000"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.068501000000000000"), }, "Boot <> Osmo, tick 64576000 -> price 25760000": { tickIndex: 64576000, - expectedPrice: osmomath.MustNewDecFromStr("25760000"), + expectedPrice: osmomath.MustNewBigDecFromStr("25760000"), }, "Boot <> Osmo, tick 64576000 + 100 -> price 25760000": { tickIndex: 64576100, - expectedPrice: osmomath.MustNewDecFromStr("25761000"), + expectedPrice: osmomath.MustNewBigDecFromStr("25761000"), }, "BTC <> USD, tick 38035200 -> price 30352": { tickIndex: 38035200, - expectedPrice: osmomath.MustNewDecFromStr("30352"), + expectedPrice: osmomath.MustNewBigDecFromStr("30352"), }, "BTC <> USD, tick 38035200 + 100 -> price 30353": { tickIndex: 38035300, - expectedPrice: osmomath.MustNewDecFromStr("30353"), + expectedPrice: osmomath.MustNewBigDecFromStr("30353"), }, "SHIB <> USD, tick -44821000 -> price 0.000011790": { tickIndex: -44821000, - expectedPrice: osmomath.MustNewDecFromStr("0.00001179"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.00001179"), }, "SHIB <> USD, tick -44821100 + 100 -> price 0.000011791": { tickIndex: -44820900, - expectedPrice: osmomath.MustNewDecFromStr("0.000011791"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.000011791"), }, "ETH <> BTC, tick -12104000 -> price 0.068960": { tickIndex: -12104000, - expectedPrice: osmomath.MustNewDecFromStr("0.068960"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.068960"), }, "ETH <> BTC, tick -121044000 + 1 -> price 0.068961": { tickIndex: -12103900, - expectedPrice: osmomath.MustNewDecFromStr("0.068961"), + expectedPrice: osmomath.MustNewBigDecFromStr("0.068961"), }, "one tick spacing interval smaller than max sqrt price, max tick neg six - 100 -> one tick spacing interval smaller than max sqrt price": { tickIndex: types.MaxTick - 100, - expectedPrice: osmomath.MustNewDecFromStr("99999000000000000000000000000000000000"), + expectedPrice: osmomath.MustNewBigDecFromStr("99999000000000000000000000000000000000"), }, "max sqrt price, max tick neg six -> max spot price": { tickIndex: types.MaxTick, - expectedPrice: types.MaxSpotPrice, + expectedPrice: types.MaxSpotPriceBigDec, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { - price, sqrtPrice, err := math.TickToSqrtPrice(tc.tickIndex) + sqrtPrice, err := math.TickToSqrtPrice(tc.tickIndex) if tc.expectedError != nil { require.Error(t, err) require.Equal(t, tc.expectedError.Error(), err.Error()) return } require.NoError(t, err) - expectedSqrtPrice, err := osmomath.MonotonicSqrt(tc.expectedPrice) - require.NoError(t, err) - require.Equal(t, osmomath.BigDecFromDec(tc.expectedPrice).String(), price.String()) - require.Equal(t, osmomath.BigDecFromDec(expectedSqrtPrice).String(), sqrtPrice.String()) + var expectedSqrtPrice osmomath.BigDec + if tc.expectedPrice.LT(types.MinSpotPriceBigDec) { + expectedSqrtPrice = osmomath.MustMonotonicSqrtBigDec(tc.expectedPrice) + } else { + expectedSqrtPrice = osmomath.BigDecFromDec(osmomath.MustMonotonicSqrt(tc.expectedPrice.Dec())) + require.NoError(t, err) + } + + require.Equal(t, expectedSqrtPrice.String(), sqrtPrice.String()) }) } } @@ -260,9 +289,9 @@ func TestTicksToSqrtPrice(t *testing.T) { expectedLowerPrice: types.MinSpotPrice, }, "error: lowerTickIndex less than minimum": { - lowerTickIndex: osmomath.NewInt(types.MinCurrentTick - 1), + lowerTickIndex: osmomath.NewInt(types.MinCurrentTickV2 - 1), upperTickIndex: osmomath.NewInt(36073300), - expectedError: types.TickIndexMinimumError{MinTick: types.MinCurrentTick}, + expectedError: types.TickIndexMinimumError{MinTick: types.MinCurrentTickV2}, }, "error: upperTickIndex greater than maximum": { lowerTickIndex: osmomath.NewInt(types.MinInitializedTick), @@ -386,8 +415,8 @@ func TestPriceToTick(t *testing.T) { expectedError: types.PriceBoundError{ProvidedPrice: osmomath.BigDecFromDec(types.MaxSpotPrice.Add(osmomath.OneDec())), MinSpotPrice: types.MinSpotPriceV2, MaxSpotPrice: types.MaxSpotPrice}, }, "price is smaller than min spot price": { - price: types.MinSpotPriceV2.Quo(bigTenDec), - expectedError: types.PriceBoundError{ProvidedPrice: types.MinSpotPriceV2.Quo(bigTenDec), MinSpotPrice: types.MinSpotPriceV2, MaxSpotPrice: types.MaxSpotPrice}, + price: osmomath.ZeroBigDec(), + expectedError: types.PriceBoundError{ProvidedPrice: osmomath.ZeroBigDec(), MinSpotPrice: types.MinSpotPriceV2, MaxSpotPrice: types.MaxSpotPrice}, }, } for name, tc := range testCases { @@ -438,7 +467,7 @@ func TestPriceToTickRoundDown(t *testing.T) { tickExpected: -300, }, "tick spacing 100, MinSpotPrice, MinTick": { - price: types.MinSpotPriceBigDec, + price: osmomath.BigDecFromDec(types.MinSpotPrice), tickSpacing: defaultTickSpacing, tickExpected: types.MinInitializedTick, }, @@ -531,7 +560,7 @@ func TestTickToSqrtPricePriceToTick_InverseRelationship(t *testing.T) { tickExpected: types.MaxTick - 1, // still max }, "min spot price": { - price: types.MinSpotPriceBigDec, + price: osmomath.BigDecFromDec(types.MinSpotPrice), tickExpected: types.MinInitializedTick, }, "smallest + min price + tick": { @@ -606,7 +635,7 @@ func TestTickToSqrtPricePriceToTick_InverseRelationship(t *testing.T) { require.Equal(t, tickFromPrice, inverseTickFromPrice) // 4. Validate PriceToTick and TickToSqrtPrice functions - _, sqrtPrice, err := math.TickToSqrtPrice(tickFromPrice) + sqrtPrice, err := math.TickToSqrtPrice(tickFromPrice) require.NoError(t, err) // TODO: investigate this separately @@ -646,6 +675,77 @@ func TestPriceToTick_ErrorCases(t *testing.T) { }) } } + +func TestTickToPrice_SuccessCases(t *testing.T) { + testCases := map[string]struct { + tickIndex int64 + expectedPrice osmomath.BigDec + expectedErr error + }{ + "tick index is Max tick": { + tickIndex: types.MaxTick, + expectedPrice: osmomath.BigDecFromDec(types.MaxSpotPrice), + }, + "tick index is between Min tick V1 and Max tick": { + tickIndex: 123456, + expectedPrice: osmomath.OneBigDec().Add(osmomath.NewBigDec(123456).Mul(osmomath.NewBigDecWithPrec(1, 6))), + }, + "tick index is V1 MinInitializedTick": { + tickIndex: types.MinInitializedTick, + expectedPrice: osmomath.BigDecFromDec(types.MinSpotPrice), + }, + "tick index is V1 MinCurrentTick": { + tickIndex: types.MinCurrentTick, + expectedPrice: osmomath.BigDecFromDec(types.MinSpotPrice).Sub(osmomath.NewBigDecWithPrec(1, 13+(-types.ExponentAtPriceOne))), + }, + "tick index is V2 MinInitializedTick": { + tickIndex: types.MinInitializedTickV2, + expectedPrice: types.MinSpotPriceV2, + }, + "tick index is V2 MinCurrentTickV2": { + tickIndex: types.MinCurrentTickV2, + expectedPrice: types.MinSpotPriceV2, + }, + "tick index is V2 MinInitializedTick + 1": { + tickIndex: types.MinInitializedTickV2 + 1, + expectedPrice: types.MinSpotPriceV2.Add(smallestBigDec), + }, + "tick index is V2 MinInitializedTick + 2": { + tickIndex: types.MinInitializedTickV2 + 2, + expectedPrice: types.MinSpotPriceV2.Add(smallestBigDec).Add(smallestBigDec), + }, + // Computed in Python: + // geometricExponentIncrementDistanceInTicks = 9000000 + // tickIndex = -9000000 * 18 - 123456 + // geometricExponentDelta = tickIndex // geometricExponentIncrementDistanceInTicks + 1 # add one because Python is a floor division when Go is truncation towards zero. + // exponentAtCurrentTick = -6 + geometricExponentDelta - 1 + // currentAdditiveIncrementInTicks = 10**exponentAtCurrentTick + // numAdditiveTicks = tickIndex - (geometricExponentDelta * geometricExponentIncrementDistanceInTicks) + // 10**geometricExponentDelta + numAdditiveTicks * currentAdditiveIncrementInTicks + // 9.876544e-19 + "tick index is between V2 MinInitializedTick and V1 MinInitializedTick": { + tickIndex: -9000000*18 - 123456, + expectedPrice: osmomath.NewBigDecWithPrec(9876544, 6+19), // 6 for number of digits after, 18 for geometric multiplier and 1 for negative ticks + }, + } + for name, tc := range testCases { + tc := tc + + t.Run(name, func(t *testing.T) { + price, err := math.TickToPrice(tc.tickIndex) + + if tc.expectedErr != nil { + require.Error(t, err) + return + } + + require.NoError(t, err) + + require.Equal(t, tc.expectedPrice, price) + }) + } +} + func TestTickToPrice_ErrorCases(t *testing.T) { testCases := map[string]struct { tickIndex int64 @@ -654,7 +754,7 @@ func TestTickToPrice_ErrorCases(t *testing.T) { tickIndex: types.MaxTick + 1, }, "tick index is less than min tick": { - tickIndex: types.MinCurrentTick - 1, + tickIndex: types.MinCurrentTickV2 - 1, }, } for name, tc := range testCases { @@ -697,11 +797,11 @@ func TestCalculatePriceToTick(t *testing.T) { expectedTickIndex: 72000001, }, "MinSpotPrice V1 -> MinInitializedTick": { - price: types.MinSpotPriceBigDec, + price: osmomath.BigDecFromDec(types.MinSpotPrice), expectedTickIndex: types.MinInitializedTick, }, "MinSpotPrice V1 - 10^-19 -> MinCurrentTick": { - price: types.MinSpotPriceBigDec.Sub(osmomath.NewBigDecWithPrec(1, 19)), + price: osmomath.BigDecFromDec(types.MinSpotPrice).Sub(osmomath.NewBigDecWithPrec(1, 19)), expectedTickIndex: types.MinCurrentTick, }, "MinSpotPrice V2 -> MinInitializedTick V2": { @@ -720,7 +820,7 @@ func TestCalculatePriceToTick(t *testing.T) { require.Equal(t, tc.expectedTickIndex, tickIndex) // Only run tests on the BigDec version on range [MinCurrentTickV2, MinCurrentTick] - if tc.price.LT(types.MinSpotPriceBigDec) { + if tc.price.LT(osmomath.BigDecFromDec(types.MinSpotPrice)) { return } @@ -731,6 +831,42 @@ func TestCalculatePriceToTick(t *testing.T) { } } +// This test validates that conversions at the new initialized boundary are sound. +func TestSqrtPriceToTick_MinInitializedTickV2(t *testing.T) { + + // TODO: remove this Skip(). It is present to maintain backwards state-compatibility with + // v19.x and earlier major releases of Osmosis. + // Once https://github.com/osmosis-labs/osmosis/issues/5726 is fully complete, + // this should be removed. + t.Skip() + + minSqrtPrice, err := osmomath.MonotonicSqrtBigDec(types.MinSpotPriceV2) + require.NoError(t, err) + + tickIndex, err := math.CalculateSqrtPriceToTick(minSqrtPrice) + require.NoError(t, err) + require.Equal(t, types.MinInitializedTickV2, tickIndex) +} + +// This test validates that tick conversions at the old initialized boundary are sound. +func TestSqrtPriceToTick_MinInitializedTickV1(t *testing.T) { + minSqrtPrice, err := osmomath.MonotonicSqrt(types.MinSpotPrice) + require.NoError(t, err) + + minSpotPrice := osmomath.BigDecFromDec(minSqrtPrice) + tickIndex, err := math.CalculateSqrtPriceToTick(minSpotPrice) + require.NoError(t, err) + require.Equal(t, types.MinInitializedTick, tickIndex) + + // Subtract one ULP given exponent at price one of -6. + minSpotPriceMinusULP := minSpotPrice.Sub(osmomath.NewBigDecWithPrec(1, 19+6)) + tickIndex, err = math.CalculateSqrtPriceToTick(minSpotPriceMinusULP) + require.NoError(t, err) + + // The tick index should be one less than the min initialized tick. + require.Equal(t, types.MinInitializedTick-1, tickIndex) +} + func TestPowTenInternal(t *testing.T) { testCases := map[string]struct { exponent int64 @@ -761,21 +897,21 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { sdkULP := osmomath.BigDecFromDec(osmomath.SmallestDec()) // Compute reference values that need to be satisfied - _, sqp1, err := math.TickToSqrtPrice(1) + sqp1, err := math.TickToSqrtPrice(1) require.NoError(t, err) - _, sqp99, err := math.TickToSqrtPrice(99) + sqp99, err := math.TickToSqrtPrice(99) require.NoError(t, err) - _, sqp100, err := math.TickToSqrtPrice(100) + sqp100, err := math.TickToSqrtPrice(100) require.NoError(t, err) - _, sqpn100, err := math.TickToSqrtPrice(-100) + sqpn100, err := math.TickToSqrtPrice(-100) require.NoError(t, err) - _, sqpn101, err := math.TickToSqrtPrice(-101) + sqpn101, err := math.TickToSqrtPrice(-101) require.NoError(t, err) - _, sqpMaxTickSubOne, err := math.TickToSqrtPrice(types.MaxTick - 1) + sqpMaxTickSubOne, err := math.TickToSqrtPrice(types.MaxTick - 1) require.NoError(t, err) - _, sqpMinTickPlusOne, err := math.TickToSqrtPrice(types.MinInitializedTick + 1) + sqpMinTickPlusOne, err := math.TickToSqrtPrice(types.MinInitializedTick + 1) require.NoError(t, err) - _, sqpMinTickPlusTwo, err := math.TickToSqrtPrice(types.MinInitializedTick + 2) + sqpMinTickPlusTwo, err := math.TickToSqrtPrice(types.MinInitializedTick + 2) require.NoError(t, err) testCases := map[string]struct { @@ -901,12 +1037,12 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) { // Ensure returned bucket properly encapsulates given sqrt price, skipping the upper bound // check if we're on the max tick - _, inverseSqrtPrice, err := math.TickToSqrtPrice(tickIndex) + inverseSqrtPrice, err := math.TickToSqrtPrice(tickIndex) require.NoError(t, err) require.True(t, inverseSqrtPrice.LTE(tc.sqrtPrice)) if tc.tickExpected != types.MaxTick { - _, inverseSqrtPriceTickAbove, err := math.TickToSqrtPrice(tickIndex + int64(tc.tickSpacing)) + inverseSqrtPriceTickAbove, err := math.TickToSqrtPrice(tickIndex + int64(tc.tickSpacing)) require.NoError(t, err) require.True(t, inverseSqrtPriceTickAbove.GT(tc.sqrtPrice)) } diff --git a/x/concentrated-liquidity/model/pool_test.go b/x/concentrated-liquidity/model/pool_test.go index 1ccda747c70..08f73e12c60 100644 --- a/x/concentrated-liquidity/model/pool_test.go +++ b/x/concentrated-liquidity/model/pool_test.go @@ -567,7 +567,7 @@ func (s *ConcentratedPoolTestSuite) TestNewConcentratedLiquidityPool() { func (suite *ConcentratedPoolTestSuite) TestCalcActualAmounts() { var ( tickToSqrtPrice = func(tick int64) osmomath.BigDec { - _, sqrtPrice, err := clmath.TickToSqrtPrice(tick) + sqrtPrice, err := clmath.TickToSqrtPrice(tick) suite.Require().NoError(err) return sqrtPrice } @@ -695,7 +695,7 @@ func (suite *ConcentratedPoolTestSuite) TestCalcActualAmounts() { pool := model.Pool{ CurrentTick: tc.currentTick, } - _, currenTicktSqrtPrice, _ := clmath.TickToSqrtPrice(pool.CurrentTick) + currenTicktSqrtPrice, _ := clmath.TickToSqrtPrice(pool.CurrentTick) pool.CurrentSqrtPrice = currenTicktSqrtPrice actualAmount0, actualAmount1, err := pool.CalcActualAmounts(suite.Ctx, tc.lowerTick, tc.upperTick, tc.liquidityDelta) @@ -790,7 +790,7 @@ func (suite *ConcentratedPoolTestSuite) TestUpdateLiquidityIfActivePosition() { CurrentTick: tc.currentTick, CurrentTickLiquidity: defaultLiquidityAmt, } - _, currenTicktSqrtPrice, _ := clmath.TickToSqrtPrice(pool.CurrentTick) + currenTicktSqrtPrice, _ := clmath.TickToSqrtPrice(pool.CurrentTick) pool.CurrentSqrtPrice = currenTicktSqrtPrice wasUpdated := pool.UpdateLiquidityIfActivePosition(suite.Ctx, tc.lowerTick, tc.upperTick, tc.liquidityDelta) diff --git a/x/concentrated-liquidity/query.go b/x/concentrated-liquidity/query.go index 370967824c2..611a686e6b4 100644 --- a/x/concentrated-liquidity/query.go +++ b/x/concentrated-liquidity/query.go @@ -152,7 +152,7 @@ func (k Keeper) GetTickLiquidityNetInDirection(ctx sdk.Context, poolId uint64, t swapStrategy := swapstrategy.New(zeroForOne, osmomath.ZeroBigDec(), k.storeKey, osmomath.ZeroDec()) currentTick := p.GetCurrentTick() - _, currentTickSqrtPrice, err := math.TickToSqrtPrice(currentTick) + currentTickSqrtPrice, err := math.TickToSqrtPrice(currentTick) if err != nil { return []queryproto.TickLiquidityNet{}, err } @@ -162,7 +162,7 @@ func (k Keeper) GetTickLiquidityNetInDirection(ctx sdk.Context, poolId uint64, t // function to validate that start tick and bound tick are // between current tick and the min/max tick depending on the swap direction. validateTickIsInValidRange := func(validateTick osmomath.Int) error { - _, validateSqrtPrice, err := math.TickToSqrtPrice(validateTick.Int64()) + validateSqrtPrice, err := math.TickToSqrtPrice(validateTick.Int64()) if err != nil { return err } diff --git a/x/concentrated-liquidity/query_test.go b/x/concentrated-liquidity/query_test.go index 5d48b32d0dd..1ad980e9a4c 100644 --- a/x/concentrated-liquidity/query_test.go +++ b/x/concentrated-liquidity/query_test.go @@ -566,7 +566,7 @@ func (s *KeeperTestSuite) TestGetTickLiquidityNetInDirection() { s.Require().NoError(err) var curSqrtPrice osmomath.BigDec = osmomath.OneBigDec() if test.currentPoolTick > 0 { - _, sqrtPrice, err := math.TickToSqrtPrice(test.currentPoolTick) + sqrtPrice, err := math.TickToSqrtPrice(test.currentPoolTick) s.Require().NoError(err) curTick = test.currentPoolTick diff --git a/x/concentrated-liquidity/swaps.go b/x/concentrated-liquidity/swaps.go index a6662b14abb..77edbb16651 100644 --- a/x/concentrated-liquidity/swaps.go +++ b/x/concentrated-liquidity/swaps.go @@ -333,7 +333,7 @@ func iteratorToNextInitializedTickSqrtPriceTarget(nextInitTickIter db.Iterator, } // Utilizing the next initialized tick, we find the corresponding nextInitializedTickSqrtPrice (the target sqrt price). - _, nextInitializedTickSqrtPrice, err := math.TickToSqrtPrice(nextInitializedTick) + nextInitializedTickSqrtPrice, err := math.TickToSqrtPrice(nextInitializedTick) if err != nil { return 0, osmomath.BigDec{}, osmomath.BigDec{}, fmt.Errorf("could not convert next tick (%v) to nextSqrtPrice", nextInitializedTick) } diff --git a/x/concentrated-liquidity/swaps_test.go b/x/concentrated-liquidity/swaps_test.go index 62cece5445a..b27d047ec9d 100644 --- a/x/concentrated-liquidity/swaps_test.go +++ b/x/concentrated-liquidity/swaps_test.go @@ -2027,9 +2027,9 @@ func (s *KeeperTestSuite) getExpectedLiquidity(test SwapTest, pool types.Concent newLowerTick, newUpperTick := s.lowerUpperPricesToTick(test.newLowerPrice, test.newUpperPrice, pool.GetTickSpacing()) - _, lowerSqrtPrice, err := math.TickToSqrtPrice(newLowerTick) + lowerSqrtPrice, err := math.TickToSqrtPrice(newLowerTick) s.Require().NoError(err) - _, upperSqrtPrice, err := math.TickToSqrtPrice(newUpperTick) + upperSqrtPrice, err := math.TickToSqrtPrice(newUpperTick) s.Require().NoError(err) if test.poolLiqAmount0.IsNil() && test.poolLiqAmount1.IsNil() { diff --git a/x/concentrated-liquidity/swaps_tick_cross_test.go b/x/concentrated-liquidity/swaps_tick_cross_test.go index d7ae9b269fd..c6e2b71291f 100644 --- a/x/concentrated-liquidity/swaps_tick_cross_test.go +++ b/x/concentrated-liquidity/swaps_tick_cross_test.go @@ -63,7 +63,7 @@ func (s *KeeperTestSuite) CreatePositionTickSpacingsFromCurrentTick(poolId uint6 // tickToSqrtPrice a helper to convert a tick to a sqrt price. func (s *KeeperTestSuite) tickToSqrtPrice(tick int64) osmomath.BigDec { - _, sqrtPrice, err := math.TickToSqrtPrice(tick) + sqrtPrice, err := math.TickToSqrtPrice(tick) s.Require().NoError(err) return sqrtPrice } diff --git a/x/concentrated-liquidity/types/constants.go b/x/concentrated-liquidity/types/constants.go index ffce46ffd8d..2b0d4aa0bd9 100644 --- a/x/concentrated-liquidity/types/constants.go +++ b/x/concentrated-liquidity/types/constants.go @@ -8,9 +8,9 @@ import ( const ( // Precomputed values for min and max tick + + // Tick corresponding to the at launch min spot price of 10^-12. MinInitializedTick, MaxTick int64 = -108000000, 342000000 - MinInitializedTickV2 int64 = -270000000 - MinCurrentTickV2 int64 = MinInitializedTickV2 - 1 // 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 @@ -19,7 +19,10 @@ const ( // Note, that this behavior is different from MaxTick since our "active range" // invariant is [lower tick, uppper tick). As a result, when we consume all lower // tick liquiditty, we must cross it and get kicked out of it. - MinCurrentTick int64 = MinInitializedTick - 1 + MinCurrentTick int64 = MinInitializedTick - 1 + // Tick corresponding to the extended min spot price of 10^-30. + MinInitializedTickV2 int64 = -270000000 + MinCurrentTickV2 int64 = MinInitializedTickV2 - 1 ExponentAtPriceOne int64 = -6 ConcentratedGasFeeForSwap = 10_000 BaseGasFeeForNewIncentive = 10_000 @@ -27,7 +30,8 @@ const ( ) var ( - MaxSpotPrice = osmomath.MustNewDecFromStr("100000000000000000000000000000000000000") + MaxSpotPrice = osmomath.MustNewDecFromStr("100000000000000000000000000000000000000") + MaxSpotPriceBigDec = osmomath.BigDecFromDec(MaxSpotPrice) // TODO: remove when https://github.com/osmosis-labs/osmosis/issues/5726 is complete. MinSpotPrice = osmomath.MustNewDecFromStr("0.000000000001") // 10^-12 // Note: this is the at launch min spot price that is getting lowered to 10^-30