-
Notifications
You must be signed in to change notification settings - Fork 608
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
[CL]: Fix low level tick rounding to mitigate fund drain bug #5510
Changes from all commits
aec07c7
f4a0ff5
a217217
ee3f78c
0818d97
a5fc1e2
42a5e34
06b77df
0b299a2
f52a1e8
ea6d585
b58fe39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -108,17 +108,20 @@ func PriceToTick(price sdk.Dec) (int64, error) { | |
|
||
// Determine the tick that corresponds to the price | ||
// This does not take into account the tickSpacing | ||
tickIndex := CalculatePriceToTick(price) | ||
tickIndex, err := CalculatePriceToTick(price) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
return tickIndex, nil | ||
} | ||
|
||
// PriceToTickRoundDown takes a price and returns the corresponding tick index. | ||
// PriceToTickRoundDownSpacing takes a price and returns the corresponding tick index. | ||
// If tickSpacing is provided, the tick index will be rounded down to the nearest multiple of tickSpacing. | ||
// CONTRACT: tickSpacing must be smaller or equal to the max of 1 << 63 - 1. | ||
// This is not a concern because we have authorized tick spacings that are smaller than this max, | ||
// and we don't expect to ever require it to be this large. | ||
func PriceToTickRoundDown(price sdk.Dec, tickSpacing uint64) (int64, error) { | ||
func PriceToTickRoundDownSpacing(price sdk.Dec, tickSpacing uint64) (int64, error) { | ||
tickIndex, err := PriceToTick(price) | ||
if err != nil { | ||
return 0, err | ||
|
@@ -168,11 +171,12 @@ func powTenBigDec(exponent int64) osmomath.BigDec { | |
|
||
// CalculatePriceToTick takes in a price and returns the corresponding tick index. | ||
// This function does not take into consideration tick spacing. | ||
// CONTRACT: `price` is between MinSpotPrice and MaxSpotPrice, inclusive. | ||
// NOTE: This is really returning a "Bucket index". Bucket index `b` corresponds to | ||
// all prices in range [TickToPrice(b), TickToPrice(b+1)). | ||
func CalculatePriceToTick(price sdk.Dec) (tickIndex int64) { | ||
func CalculatePriceToTick(price sdk.Dec) (tickIndex int64, err error) { | ||
if price.Equal(sdkOneDec) { | ||
return 0 | ||
return 0, nil | ||
} | ||
|
||
// The approach here is to try determine which "geometric spacing" are we in. | ||
|
@@ -197,7 +201,7 @@ func CalculatePriceToTick(price sdk.Dec) (tickIndex int64) { | |
} | ||
} | ||
|
||
// We know were between (geoSpacing.initialPrice, geoSpacing.endPrice) | ||
// We know we're 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.BigDecFromSDKDec(price.Sub(geoSpacing.initialPrice)) | ||
|
@@ -206,7 +210,64 @@ func CalculatePriceToTick(price sdk.Dec) (tickIndex int64) { | |
// * taking the bucket index of the smallest price in this tick | ||
// * adding to it the number of ticks "completely" filled by the current spacing | ||
// the latter is the truncation of the division above | ||
// TODO: This should be rounding down? | ||
tickIndex = geoSpacing.initialTick + ticksFilledByCurrentSpacing.SDKDec().RoundInt64() | ||
return tickIndex | ||
tickIndex = geoSpacing.initialTick + ticksFilledByCurrentSpacing.SDKDec().TruncateInt64() | ||
|
||
// We get the price corresponding to our calculated bucket index to compare against the input price. | ||
curPrice, err := TickToPrice(tickIndex) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
// We first handle cases related to min and max tick independently here, as in either case checking both the tick above and below is not possible. | ||
if curPrice.Equal(types.MinSpotPrice) && curPrice.LTE(price) { | ||
// We assume that the error to the downside is caught by input price bound checks, as it | ||
// would be below the minimum allowed spot price. | ||
return types.MinTick, nil | ||
} else if curPrice.Equal(types.MaxSpotPrice) && curPrice.GTE(price) { | ||
// We assume that the error to the upside is caught by input price bound checks, so we only | ||
// check the price in the tick below. | ||
tickBelowPrice, err := TickToPrice(tickIndex - 1) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
// If price is lower than the price in the tick below, our error assumption is violated so we panic. | ||
if price.LT(tickBelowPrice) { | ||
panic(fmt.Sprintf("price %s is outside of error bounds for tick %d", price, tickIndex)) | ||
} | ||
|
||
// If price falls in the bucket below `tickIndex`, return that bucket index. | ||
// Otherwise, return the current tick index. | ||
if price.GTE(tickBelowPrice) && price.LT(curPrice) { | ||
return tickIndex - 1, nil | ||
} | ||
Comment on lines
+234
to
+243
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should make a new SqrtPriceToTick function, that does the squaring internally, then does this comparison on the derived sqrt prices. As the problem is our price definition doesn't match due to errors. (I don't think this on its own will save us from anything aside from the erroneous bucket definition due to not truncating) |
||
|
||
return types.MaxTick, nil | ||
} | ||
|
||
// We get the prices corresponding to the ticks above and below our calculated tick index to compare against the input price. | ||
tickBelowPrice, err := TickToPrice(tickIndex - 1) | ||
if err != nil { | ||
return 0, err | ||
} | ||
tickAbovePrice, err := TickToPrice(tickIndex + 1) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
// If rounded incorrectly, fix current tick. | ||
// Claim tick t is correct tick for current price p. | ||
// To test this, assuming error < 1, convert tick t - 1 (p0), t (p1) and t + 1 (p2) to prices | ||
// If p < p0 || p > p2, panic (violates error < 1 assumption) | ||
if price.LT(tickBelowPrice) || price.GT(tickAbovePrice) { | ||
panic(fmt.Sprintf("price %s is outside of error bounds for tick %d", price, tickIndex)) | ||
} | ||
|
||
// If tickBelowPrice <= price < curPrice, then set tick = t - 1 because by convention, bucket index b is for ticks t <= b < t + 1. | ||
// If not, this means that curPrice <= price < tickAbovePrice, which is already accurately represented by our original tickIndex guess. | ||
if price.GTE(tickBelowPrice) && price.LT(curPrice) { | ||
return tickIndex - 1, nil | ||
} | ||
|
||
return tickIndex, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to reviewer: returning 0 here isn't ideal, but it seems to be what we do in all other tick logic upon error. If there's a better value to return here that's more clearly invalid (e.g. an
sdk.Int{}
equivalent) please lmk