Skip to content

Commit

Permalink
refactor: state-compatible big decimal tick to sqrt price conversions (
Browse files Browse the repository at this point in the history
…#6317)

* refactor: state-compatible big decimal tick to sqrt price conversions

* updates and clean ups

* lint

(cherry picked from commit 8ac3efb)

# Conflicts:
#	tests/e2e/e2e_cl_test.go
#	x/concentrated-liquidity/math/math_test.go
#	x/concentrated-liquidity/math/tick.go
#	x/concentrated-liquidity/math/tick_test.go
#	x/concentrated-liquidity/query.go
#	x/concentrated-liquidity/types/constants.go
  • Loading branch information
p0mvn authored and mergify[bot] committed Sep 8, 2023
1 parent 6efdff2 commit c7323e2
Show file tree
Hide file tree
Showing 12 changed files with 1,239 additions and 59 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
803 changes: 803 additions & 0 deletions tests/e2e/e2e_cl_test.go

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion x/concentrated-liquidity/math/math_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,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 (
Expand Down Expand Up @@ -314,22 +314,38 @@ func TestGetLiquidityFromAmounts(t *testing.T) {
expectedLiquidity: "741249214.836069764856625637",
},
"full range, price proportional to amounts, equal liquidities (some rounding error) price of 4": {
<<<<<<< HEAD
currentSqrtP: sqrt(sdk.NewDec(4)),
sqrtPHigh: osmomath.BigDecFromSDKDec(cltypes.MaxSqrtPrice),
sqrtPLow: osmomath.BigDecFromSDKDec(cltypes.MinSqrtPrice),
amount0Desired: sdk.NewInt(4),
amount1Desired: sdk.NewInt(16),
=======
currentSqrtP: sqrt(osmomath.NewDec(4)),
sqrtPHigh: osmomath.BigDecFromDec(types.MaxSqrtPrice),
sqrtPLow: osmomath.BigDecFromDec(types.MinSqrtPrice),
amount0Desired: osmomath.NewInt(4),
amount1Desired: osmomath.NewInt(16),
>>>>>>> 8ac3efb2 (refactor: state-compatible big decimal tick to sqrt price conversions (#6317))

expectedLiquidity: sdk.MustNewDecFromStr("8.000000000000000001").String(),
expectedLiquidity0: sdk.MustNewDecFromStr("8.000000000000000001"),
expectedLiquidity1: sdk.MustNewDecFromStr("8.000000004000000002"),
},
"full range, price proportional to amounts, equal liquidities (some rounding error) price of 2": {
<<<<<<< HEAD
currentSqrtP: sqrt(sdk.NewDec(2)),
sqrtPHigh: osmomath.BigDecFromSDKDec(cltypes.MaxSqrtPrice),
sqrtPLow: osmomath.BigDecFromSDKDec(cltypes.MinSqrtPrice),
amount0Desired: sdk.NewInt(1),
amount1Desired: sdk.NewInt(2),
=======
currentSqrtP: sqrt(osmomath.NewDec(2)),
sqrtPHigh: osmomath.BigDecFromDec(types.MaxSqrtPrice),
sqrtPLow: osmomath.BigDecFromDec(types.MinSqrtPrice),
amount0Desired: osmomath.NewInt(1),
amount1Desired: osmomath.NewInt(2),
>>>>>>> 8ac3efb2 (refactor: state-compatible big decimal tick to sqrt price conversions (#6317))

expectedLiquidity: sdk.MustNewDecFromStr("1.414213562373095049").String(),
expectedLiquidity0: sdk.MustNewDecFromStr("1.414213562373095049"),
Expand Down
104 changes: 86 additions & 18 deletions x/concentrated-liquidity/math/tick.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,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
}
Expand All @@ -31,22 +31,46 @@ func TicksToSqrtPrice(lowerTick, upperTick int64) (osmomath.BigDec, osmomath.Big
// 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) (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
}

<<<<<<< HEAD
// It is acceptable to truncate here as TickToPrice() function converts
// from sdk.Dec to osmomath.BigDec before returning.
price := priceBigDec.SDKDec()
=======
// 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()
>>>>>>> 8ac3efb2 (refactor: state-compatible big decimal tick to sqrt price conversions (#6317))

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
}
<<<<<<< HEAD
return osmomath.BigDecFromSDKDec(price), osmomath.BigDecFromSDKDec(sqrtPrice), nil
=======
return sqrtPrice, nil
>>>>>>> 8ac3efb2 (refactor: state-compatible big decimal tick to sqrt price conversions (#6317))
}

// TickToPrice returns the price given a tickIndex
Expand All @@ -56,14 +80,25 @@ func TickToPrice(tickIndex int64) (osmomath.BigDec, error) {
return osmomath.OneDec(), nil
}

<<<<<<< HEAD
// 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
geometricExponentIncrementDistanceInTicks := sdkNineDec.Mul(PowTenInternal(-exponentAtPriceOne)).TruncateInt64()
=======
// 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
}
>>>>>>> 8ac3efb2 (refactor: state-compatible big decimal tick to sqrt price conversions (#6317))

// 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}
Expand All @@ -85,9 +120,12 @@ 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
<<<<<<< HEAD
price := PowTenInternal(geometricExponentDelta).Add(osmomath.NewBigDec(numAdditiveTicks).Mul(currentAdditiveIncrementInTicks).SDKDec())

// defense in depth, this logic would not be reached due to use having checked if given tick is in between
Expand All @@ -96,6 +134,24 @@ func TickToPrice(tickIndex int64) (osmomath.BigDec, error) {
return osmomath.BigDec{}, types.PriceBoundError{ProvidedPrice: osmomath.BigDecFromSDKDec(price), MinSpotPrice: types.MinSpotPriceBigDec, MaxSpotPrice: types.MaxSpotPrice}
}
return osmomath.BigDecFromSDKDec(price), nil
=======
// 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.MaxSpotPriceBigDec) || price.LT(types.MinSpotPriceV2) {
return osmomath.BigDec{}, types.PriceBoundError{ProvidedPrice: price, MinSpotPrice: types.MinSpotPriceV2, MaxSpotPrice: types.MaxSpotPrice}
}
return price, nil
>>>>>>> 8ac3efb2 (refactor: state-compatible big decimal tick to sqrt price conversions (#6317))
}

// RoundDownTickToSpacing rounds the tick index down to the nearest tick spacing if the tickIndex is in between authorized tick values
Expand All @@ -118,8 +174,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
Expand Down Expand Up @@ -227,6 +283,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))
Expand All @@ -240,18 +308,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")
}
Expand Down
Loading

0 comments on commit c7323e2

Please sign in to comment.