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(CL): convert CalculatePriceToTickDec to operate on BigDec conversions in a state-compatible way #6256

Merged
merged 6 commits into from
Sep 1, 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### API Breaks

* [#6256](https://github.com/osmosis-labs/osmosis/pull/6256) Refactor CalcPriceToTick to operate on BigDec price to support new price range.

## v19.0.0

### Features
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"title":"Param Change","description":"Changing the DefaultTakerFee param","is_expedited":false,"changes":[{"subspace":"poolmanager","key":"DefaultTakerFee","value":"0.001500000000000000"}],"deposit":"625000000uosmo"}
2 changes: 1 addition & 1 deletion x/concentrated-liquidity/lp.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type CreatePositionData struct {
// - the liquidity delta is zero
// - the amount0 or amount1 returned from the position update is less than the given minimums
// - the pool or user does not have enough tokens to satisfy the requested amount
func (k Keeper) CreatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, tokensProvided sdk.Coins, amount0Min, amount1Min osmomath.Int, lowerTick, upperTick int64) (positionDate CreatePositionData, err error) {
func (k Keeper) CreatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, tokensProvided sdk.Coins, amount0Min, amount1Min osmomath.Int, lowerTick, upperTick int64) (CreatePositionData, error) {
// Use the current blockTime as the position's join time.
joinTime := ctx.BlockTime()

Expand Down
20 changes: 10 additions & 10 deletions x/concentrated-liquidity/math/precompute.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,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 osmomath.Dec
initialPrice osmomath.BigDec
// if price >= maxPrice, we are not in this exponent range.
maxPrice osmomath.Dec
maxPrice osmomath.BigDec
// TODO: Change to normal Dec, if min spot price increases.
// additive increment per tick here.
additiveIncrementPerTick osmomath.BigDec
Expand All @@ -47,27 +47,27 @@ 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.BigDecFromDec(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,
}
maxPrice = tickExpCache[curExpIndex].maxPrice
curExpIndex += 1
}

minPrice := sdkOneDec
minPrice := osmomathBigOneDec
curExpIndex = -1
for minPrice.GT(types.MinSpotPrice) {
for minPrice.GT(osmomath.NewBigDecWithPrec(1, 30)) {
tickExpCache[curExpIndex] = &tickExpIndexData{
// price range 10^curExpIndex to 10^(curExpIndex + 1). (0.001, 0.01)
initialPrice: powTenBigDec(curExpIndex).Dec(),
maxPrice: powTenBigDec(curExpIndex + 1).Dec(),
initialPrice: powTenBigDec(curExpIndex),
maxPrice: powTenBigDec(curExpIndex + 1),
additiveIncrementPerTick: powTenBigDec(types.ExponentAtPriceOne + curExpIndex),
initialTick: geometricExponentIncrementDistanceInTicks * curExpIndex,
}
Expand Down
67 changes: 34 additions & 33 deletions x/concentrated-liquidity/math/tick.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TickToPrice(tickIndex int64) (osmomath.BigDec, 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(types.MaxSpotPrice) || price.LT(types.MinSpotPrice) {
return osmomath.BigDec{}, types.PriceBoundError{ProvidedPrice: price, MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice}
return osmomath.BigDec{}, types.PriceBoundError{ProvidedPrice: osmomath.BigDecFromDec(price), MinSpotPrice: types.MinSpotPriceBigDec, MaxSpotPrice: types.MaxSpotPrice}
}
return osmomath.BigDecFromDec(price), nil
}
Expand Down Expand Up @@ -150,19 +150,26 @@ func powTenBigDec(exponent int64) osmomath.BigDec {
return bigNegPowersOfTen[-exponent]
}

func CalculatePriceToTickDec(priceBigDec osmomath.BigDec) (tickIndex osmomath.Dec, err error) {
// It is acceptable to truncate price as the minimum we support is
// 10**-12 which is above the smallest value of osmomath.Dec.
price := priceBigDec.Dec()

// CalculatePriceToTick calculates tickIndex from price. Contrary to CalculatePriceToTickV1,
// it uses BigDec in internal calculations
func CalculatePriceToTick(price osmomath.BigDec) (tickIndex int64, err error) {
if price.IsNegative() {
return osmomath.ZeroDec(), fmt.Errorf("price must be greater than zero")
return 0, fmt.Errorf("price must be greater than zero")
}
if price.GT(types.MaxSpotPrice) || price.LT(types.MinSpotPrice) {
return osmomath.ZeroDec(), types.PriceBoundError{ProvidedPrice: price, MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice}
if price.GT(osmomath.BigDecFromDec(types.MaxSpotPrice)) || price.LT(types.MinSpotPriceV2) {
return 0, types.PriceBoundError{ProvidedPrice: price, MinSpotPrice: types.MinSpotPriceV2, MaxSpotPrice: types.MaxSpotPrice}
}
if price.Equal(sdkOneDec) {
return osmomath.ZeroDec(), nil
if price.Equal(osmomathBigOneDec) {
return 0, nil
}

// N.B. this exists to maintain backwards compatibility with
// the old version of the function that operated on decimal with precision of 18.
if price.GTE(types.MinSpotPriceBigDec) {
// TODO: implement efficient big decimal truncation.
// It is acceptable to truncate price as the minimum we support is
// 10**-12 which is above the smallest value of sdk.Dec.
price = osmomath.BigDecFromDec(price.Dec())
}

// The approach here is to try determine which "geometric spacing" are we in.
Expand All @@ -171,7 +178,7 @@ func CalculatePriceToTickDec(priceBigDec osmomath.BigDec) (tickIndex osmomath.De
// 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
Copy link
Member Author

Choose a reason for hiding this comment

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

Note: this is copied from the original function

var geoSpacing *tickExpIndexData
if price.GT(sdkOneDec) {
if price.GT(osmomathBigOneDec) {
index := 0
geoSpacing = tickExpCache[int64(index)]
for geoSpacing.maxPrice.LT(price) {
Expand All @@ -190,16 +197,12 @@ func CalculatePriceToTickDec(priceBigDec osmomath.BigDec) (tickIndex osmomath.De
// 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.BigDecFromDec(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
// * adding to it the number of ticks filled by the current spacing
// We leave rounding of this to the caller
// (NOTE: You'd expect it to be number of ticks "completely" filled by the current spacing,
// which would be truncation. However price may have errors, hence it being callers job)
tickIndex = ticksFilledByCurrentSpacing.Dec()
tickIndex = tickIndex.Add(osmomath.NewDec(geoSpacing.initialTick))
tickIndex = ticksFilledByCurrentSpacing.TruncateInt64() + geoSpacing.initialTick
return tickIndex, nil
}

Expand All @@ -210,13 +213,11 @@ 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)

tick, err := CalculatePriceToTickDec(price)
tick, err := CalculatePriceToTick(price)
if err != nil {
return 0, err
}

truncatedTick := tick.TruncateInt64()

// 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 @@ -230,18 +231,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 truncatedTick <= types.MinInitializedTick {
truncatedTick = types.MinInitializedTick + 1
if tick <= types.MinInitializedTick {
tick = types.MinInitializedTick + 1
outOfBounds = true
} else if truncatedTick >= types.MaxTick-1 {
truncatedTick = types.MaxTick - 2
} else if tick >= types.MaxTick-1 {
tick = types.MaxTick - 2
outOfBounds = true
}

_, sqrtPriceTmin1, errM1 := TickToSqrtPrice(truncatedTick - 1)
_, sqrtPriceT, errT := TickToSqrtPrice(truncatedTick)
_, sqrtPriceTplus1, errP1 := TickToSqrtPrice(truncatedTick + 1)
_, sqrtPriceTplus2, errP2 := TickToSqrtPrice(truncatedTick + 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 All @@ -257,15 +258,15 @@ func CalculateSqrtPriceToTick(sqrtPrice osmomath.BigDec) (tickIndex int64, err e

// We expect this case to only be hit when the original provided sqrt price is exactly equal to the max sqrt price.
if sqrtPrice.Equal(sqrtPriceTplus2) {
return truncatedTick + 2, nil
return tick + 2, nil
}

// The remaining cases handle shifting tick index by +/- 1.
if sqrtPrice.GTE(sqrtPriceTplus1) {
return truncatedTick + 1, nil
return tick + 1, nil
}
if sqrtPrice.GTE(sqrtPriceT) {
return truncatedTick, nil
return tick, nil
}
return truncatedTick - 1, nil
return tick - 1, nil
}
74 changes: 56 additions & 18 deletions x/concentrated-liquidity/math/tick_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,16 @@ var (
closestPriceBelowMaxPriceDefaultTickSpacing = types.MaxSpotPrice.Sub(osmomath.NewDec(10).PowerMut(uint64(len(types.MaxSpotPrice.TruncateInt().String()) - 1 - int(-types.ExponentAtPriceOne) - 1)))
// min tick + 10 ^ -expoentAtPriceOne
closestTickAboveMinPriceDefaultTickSpacing = osmomath.NewInt(types.MinInitializedTick).Add(osmomath.NewInt(10).ToLegacyDec().Power(uint64(types.ExponentAtPriceOne * -1)).TruncateInt())
)

// testing helper for price to tick, state machine only implements sqrt price to tick.
func PriceToTick(price osmomath.BigDec) (int64, error) {
tickDec, err := math.CalculatePriceToTickDec(price)
tickIndex := tickDec.TruncateInt64()
return tickIndex, err
}
smallestBigDec = osmomath.SmallestBigDec()
bigOneDec = osmomath.OneDec()
bigTenDec = osmomath.NewBigDec(10)
)

// testing helper for price to tick round down spacing,
// state machine only implements sqrt price to tick round dow spacing.
func PriceToTickRoundDownSpacing(price osmomath.BigDec, tickSpacing uint64) (int64, error) {
tickIndex, err := PriceToTick(price)
tickIndex, err := math.CalculatePriceToTick(price)
if err != nil {
return 0, err
}
Expand Down Expand Up @@ -386,19 +383,19 @@ func TestPriceToTick(t *testing.T) {
},
"price is greater than max spot price": {
price: osmomath.BigDecFromDec(types.MaxSpotPrice.Add(osmomath.OneDec())),
expectedError: types.PriceBoundError{ProvidedPrice: types.MaxSpotPrice.Add(osmomath.OneDec()), MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice},
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: osmomath.BigDecFromDec(types.MinSpotPrice.Quo(osmomath.NewDec(10))),
expectedError: types.PriceBoundError{ProvidedPrice: types.MinSpotPrice.Quo(osmomath.NewDec(10)), MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice},
price: types.MinSpotPriceV2.Quo(bigTenDec),
expectedError: types.PriceBoundError{ProvidedPrice: types.MinSpotPriceV2.Quo(bigTenDec), MinSpotPrice: types.MinSpotPriceV2, MaxSpotPrice: types.MaxSpotPrice},
},
}
for name, tc := range testCases {
tc := tc

t.Run(name, func(t *testing.T) {
// surpress error here, we only listen to errors from system under test.
tick, _ := PriceToTick(tc.price)
tick, _ := math.CalculatePriceToTick(tc.price)

// With tick spacing of one, no rounding should occur.
tickRoundDown, err := PriceToTickRoundDownSpacing(tc.price, one)
Expand Down Expand Up @@ -441,7 +438,7 @@ func TestPriceToTickRoundDown(t *testing.T) {
tickExpected: -300,
},
"tick spacing 100, MinSpotPrice, MinTick": {
price: osmomath.BigDecFromDec(types.MinSpotPrice),
price: types.MinSpotPriceBigDec,
tickSpacing: defaultTickSpacing,
tickExpected: types.MinInitializedTick,
},
Expand Down Expand Up @@ -534,7 +531,7 @@ func TestTickToSqrtPricePriceToTick_InverseRelationship(t *testing.T) {
tickExpected: types.MaxTick - 1, // still max
},
"min spot price": {
price: osmomath.BigDecFromDec(types.MinSpotPrice),
price: types.MinSpotPriceBigDec,
tickExpected: types.MinInitializedTick,
},
"smallest + min price + tick": {
Expand Down Expand Up @@ -586,7 +583,7 @@ func TestTickToSqrtPricePriceToTick_InverseRelationship(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
// 1. Compute tick from price.
tickFromPrice, err := PriceToTick(tc.price)
tickFromPrice, err := math.CalculatePriceToTick(tc.price)
require.NoError(t, err)
require.Equal(t, tc.tickExpected, tickFromPrice)

Expand All @@ -602,7 +599,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 := math.CalculatePriceToTick(price)
require.NoError(t, err)

// Make sure original tick and inverse tick match.
Expand Down Expand Up @@ -643,7 +640,7 @@ func TestPriceToTick_ErrorCases(t *testing.T) {
tc := tc

t.Run(name, func(t *testing.T) {
tickFromPrice, err := PriceToTick(tc.price)
tickFromPrice, err := math.CalculatePriceToTick(tc.price)
require.Error(t, err)
require.Equal(t, tickFromPrice, int64(0))
})
Expand Down Expand Up @@ -699,15 +696,41 @@ func TestCalculatePriceToTick(t *testing.T) {
price: osmomath.NewBigDec(100_000_100),
expectedTickIndex: 72000001,
},
"MinSpotPrice V1 -> MinInitializedTick": {
price: types.MinSpotPriceBigDec,
expectedTickIndex: types.MinInitializedTick,
},
"MinSpotPrice V1 - 10^-19 -> MinCurrentTick": {
price: types.MinSpotPriceBigDec.Sub(osmomath.NewBigDecWithPrec(1, 19)),
expectedTickIndex: types.MinCurrentTick,
},
"MinSpotPrice V2 -> MinInitializedTick V2": {
price: types.MinSpotPriceV2,
expectedTickIndex: types.MinInitializedTickV2,
},
"between MinSpotPrice V2 + 1 ULP -> MinInitializedTick V2 + 1": {
price: types.MinSpotPriceV2.Add(smallestBigDec),
expectedTickIndex: types.MinInitializedTickV2 + 1,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
tickIndex, err := PriceToTick(tc.price)
tickIndex, err := math.CalculatePriceToTick(tc.price)
require.NoError(t, err)
require.Equal(t, tc.expectedTickIndex, tickIndex)

// Only run tests on the BigDec version on range [MinCurrentTickV2, MinCurrentTick]
if tc.price.LT(types.MinSpotPriceBigDec) {
return
}

tickIndex, err = math.CalculatePriceToTick(tc.price)
require.NoError(t, err)
require.Equal(t, tc.expectedTickIndex, tickIndex)
})
}
}

func TestPowTenInternal(t *testing.T) {
testCases := map[string]struct {
exponent int64
Expand Down Expand Up @@ -890,3 +913,18 @@ func TestSqrtPriceToTickRoundDownSpacing(t *testing.T) {
})
}
}

// Computes sqrt price to tick near the min spot price V1 bound (10^-12)
// This case is important because it helped catch non-monotonicity when
// BigDec price with Dec sqrt function was used.
// To work around this issue, the price is truncated to 18 decimals
// in the original price range of [10^-12, 10^38] and 18 decimal TickToSqrt is used,
// helping maintain backwards compatibility.
//
// In the future, for price range [10^-30, 10^-12), we will use non-truncated BigDec
// with 36 decimal TickToSqrt function.
func TestCalculateSqrtPriceToTick_NearMinSpotPriceV1Bound(t *testing.T) {
sqrtPrice := osmomath.MustNewBigDecFromStr("0.000001000049998750999999999999999999")
_, err := math.CalculateSqrtPriceToTick(sqrtPrice)
require.NoError(t, err)
}
5 changes: 2 additions & 3 deletions x/concentrated-liquidity/model/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,8 @@ func (p Pool) CalcActualAmounts(ctx sdk.Context, lowerTick, upperTick int64, liq

var (
liquidityDeltaBigDec = osmomath.BigDecFromDec(liquidityDelta)

actualAmountDenom0 osmomath.BigDec
actualAmountDenom1 osmomath.BigDec
actualAmountDenom0 osmomath.BigDec
actualAmountDenom1 osmomath.BigDec
)

if p.IsCurrentTickInRange(lowerTick, upperTick) {
Expand Down
4 changes: 2 additions & 2 deletions x/concentrated-liquidity/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,10 @@ func (k Keeper) CalculateSpotPrice(
}

if price.IsZero() {
return osmomath.Dec{}, types.PriceBoundError{ProvidedPrice: price, MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice}
return osmomath.Dec{}, types.PriceBoundError{ProvidedPrice: osmomath.BigDecFromDec(price), MinSpotPrice: types.MinSpotPriceV2, MaxSpotPrice: types.MaxSpotPrice}
}
if price.GT(types.MaxSpotPrice) || price.LT(types.MinSpotPrice) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is spot price still on 18 precision? How do we expect assets with spot price below the previous MinSpotPrice to be represented here?

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, it is.

This query will not work correctly for the pools with spot price below the old MinSpotPrice.

I might have erroneously set MinSpotPriceV2 in the error. Will fix.

The new query that will work universally for all pools is TBD: #6064

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. I ask because we do lean on SpotPrice in volume splitting incentives so there might be an implication here that it wouldn't be possible to retrieve spot price for pools with too small of a spot price

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah - I thought the conclusion was that this is acceptable for the majority of the pools that we care about for volume splitting. Do you still have any concerns?

return osmomath.Dec{}, types.PriceBoundError{ProvidedPrice: price, MinSpotPrice: types.MinSpotPrice, MaxSpotPrice: types.MaxSpotPrice}
return osmomath.Dec{}, types.PriceBoundError{ProvidedPrice: osmomath.BigDecFromDec(price), MinSpotPrice: types.MinSpotPriceBigDec, MaxSpotPrice: types.MaxSpotPrice}
}

return price, nil
Expand Down
Loading