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
  • Loading branch information
p0mvn authored Sep 8, 2023
1 parent b44477c commit 8ac3efb
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 114 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
8 changes: 4 additions & 4 deletions tests/e2e/e2e_cl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
10 changes: 5 additions & 5 deletions x/concentrated-liquidity/math/math_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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),

Expand All @@ -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),

Expand Down
97 changes: 71 additions & 26 deletions x/concentrated-liquidity/math/tick.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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}
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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")
}
Expand Down
Loading

0 comments on commit 8ac3efb

Please sign in to comment.