Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,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()
p0mvn marked this conversation as resolved.
Show resolved Hide resolved

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this feels like a generally risky thing to do, but just to confirm that it's safe as is, the claim is that one would never be able to create a position on MinCurrentTickV2 and so it's okay if this returns the same price as MinInitializedTickV2?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, MinCurrentTickV2 can only be reached if all liquidity is drained when swapping zero for one.

However, it does not contain any liquidity itself. Before the swap can begin, we must first cross MinInitializedTickV2 and kick in liquidity into the bucket.

Therefore, the price translation of MinCurrentTickV2 does not play any significant role here

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