Tick Rounding
Let’s review some other changes we need to make to support different tick spacings.
Tick spacing greater than 1 won’t allow users to select arbitrary price ranges: tick indexes must be multiples of a tick spacing. For example, for tick spacing 60 we can have ticks: 0, 60, 120, 180, etc. Thus, when the user picks a range, we need to “round” it so its boundaries are multiples of the pool’s tick spacing.
nearestUsableTick
in JavaScript
In the Uniswap V3 SDK, the function that does that is called nearestUsableTick:
/**
* Returns the closest tick that is nearest a given tick and usable for the given tick spacing
* @param tick the target tick
* @param tickSpacing the spacing of the pool
*/
export function nearestUsableTick(tick: number, tickSpacing: number) {
invariant(Number.isInteger(tick) && Number.isInteger(tickSpacing), 'INTEGERS')
invariant(tickSpacing > 0, 'TICK_SPACING')
invariant(tick >= TickMath.MIN_TICK && tick <= TickMath.MAX_TICK, 'TICK_BOUND')
const rounded = Math.round(tick / tickSpacing) * tickSpacing
if (rounded < TickMath.MIN_TICK) return rounded + tickSpacing
else if (rounded > TickMath.MAX_TICK) return rounded - tickSpacing
else return rounded
}
At its core, it’s just:
Math.round(tick / tickSpacing) * tickSpacing
Where Math.round
is rounding to the nearest integer: when the fractional part is less than 0.5, it rounds to the lower integer; when it’s greater than 0.5 it rounds to the greater integer; and when it’s 0.5, it rounds to the greater integer as well.
So, in the web app, we’ll use nearestUsableTick
when building mint
parameters:
const mintParams = {
tokenA: pair.token0.address,
tokenB: pair.token1.address,
tickSpacing: pair.tickSpacing,
lowerTick: nearestUsableTick(lowerTick, pair.tickSpacing),
upperTick: nearestUsableTick(upperTick, pair.tickSpacing),
amount0Desired, amount1Desired, amount0Min, amount1Min
}
In reality, it should be called whenever the user adjusts a price range because we want the user to see the actual price that will be created. In our simplified app, we make it less user-friendly.
However, we also want to have a similar function in Solidity tests, but neither of the math libraries we’re using implements it.
nearestUsableTick
in Solidity
In our smart contract tests, we need a way to round ticks and convert rounded prices to . In a previous chapter, we chose to use ABDKMath64x64 to handle fixed-point numbers math in tests. The library, however, doesn’t implement the rounding function we need to port nearestUsableTick
, so we’ll need to implement it ourselves:
function divRound(int128 x, int128 y)
internal
pure
returns (int128 result)
{
int128 quot = ABDKMath64x64.div(x, y);
result = quot >> 64;
// Check if remainder is greater than 0.5
if (quot % 2**64 >= 0x8000000000000000) {
result += 1;
}
}
The function does multiple things:
- it divides two Q64.64 numbers;
- it then rounds the result to the decimal one (
result = quot >> 64
), the fractional part is lost at this point (i.e. the result is rounded down); - it then divides the quotient by , takes the remainder, and compares it with
0x8000000000000000
(which is 0.5 in Q64.64); - if the remainder is greater or equal to 0.5, it rounds the result to the greater integer.
What we get is an integer rounded according to the rules of Math.round
from JavaScript. We can then re-implement nearestUsableTick
:
function nearestUsableTick(int24 tick_, uint24 tickSpacing)
internal
pure
returns (int24 result)
{
result =
int24(divRound(int128(tick_), int128(int24(tickSpacing)))) *
int24(tickSpacing);
if (result < TickMath.MIN_TICK) {
result += int24(tickSpacing);
} else if (result > TickMath.MAX_TICK) {
result -= int24(tickSpacing);
}
}
That’s it!