Introduction
Different Price Ranges
Notice that this is the only scenario where we want to update liquidity
since the variable tracks liquidity that’s available immediately.
Cross-Tick Swaps
How Cross-Tick Swaps Work
Pools also track L (liquidity
variable in our code), which is the total liquidity provided by all price ranges that include the current price. It’s expected that, during big price moves, the current price moves outside of price ranges. When this happens, such price ranges become inactive and their liquidity gets subtracted from L. On the other hand, when the current price enters a price range, L is increased and the price range gets activated.
Updating the swap
Function
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
if (step.initialized) {
int128 liquidityDelta = ticks.cross(step.nextTick);
if (zeroForOne) liquidityDelta = -liquidityDelta;
state.liquidity = LiquidityMath.addLiquidity(
state.liquidity,
liquidityDelta
);
if (state.liquidity == 0) revert NotEnoughLiquidity();
}
state.tick = zeroForOne ? step.nextTick - 1 : step.nextTick;
} else {
state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}
When updating state.tick
, if the price moves down (zeroForOne
is true), we need to subtract 1 to step out of the price range. When moving up (zeroForOne
is false), the current tick is always excluded in TickBitmap.nextInitializedTickWithinOneWord
.
Liquidity Tracking and Ticks Crossing
function update(
mapping(int24 => Tick.Info) storage self,
int24 tick,
int128 liquidityDelta,
bool upper
) internal returns (bool flipped) {
...
tickInfo.liquidityNet = upper
? int128(int256(tickInfo.liquidityNet) - liquidityDelta)
: int128(int256(tickInfo.liquidityNet) + liquidityDelta);
}
function cross(mapping(int24 => Tick.Info) storage self, int24 tick)
internal
view
returns (int128 liquidityDelta)
{
Tick.Info storage info = self[tick];
liquidityDelta = info.liquidityNet;
}
function swap(...){
...
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
if (step.initialized) {
int128 liquidityDelta = ticks.cross(step.nextTick);
if (zeroForOne) liquidityDelta = -liquidityDelta;
state.liquidity = LiquidityMath.addLiquidity(
state.liquidity,
liquidityDelta
);
if (state.liquidity == 0) revert NotEnoughLiquidity();
}
state.tick = zeroForOne ? step.nextTick - 1 : step.nextTick;
} else {
state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}
Slippage Protection
Liquidity Calculation
There are fixed-point numbers and fixed-point numbers. The Q64.96 fixed-point number used by Uniswap V3 is a binary number–64 and 96 signify binary places. But PRBMathUD60x18
implements a decimal fixed-point number (UD in the contract name means “unsigned, decimal”), where 60 and 18 signify decimal places. This difference is quite significant.
A Little Bit More on Fixed-point Numbers
function tick(uint256 price) internal pure returns (int24 tick_) {
tick_ = TickMath.getTickAtSqrtRatio(
uint160(
int160(
ABDKMath64x64.sqrt(int128(int256(price << 64))) <<
(FixedPoint96.RESOLUTION - 64)
)
)
);
}
ABDKMath64x64.sqrt
takes Q64.64 numbers so we need to convert price
to such number. The price is expected to not have the fractional part, so we’re shifting it by 64 bits. The sqrt
function also returns a Q64.64 number but TickMath.getTickAtSqrtRatio
takes a Q64.96 number–this is why we need to shift the result of the square root operation by 96 - 64
bits to the left.
Flash Loans
Code
Check repo