From 040cec1c5539ebbd14ad8b190fe0e68ecb05e2f0 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 24 Aug 2026 14:28:00 +0200 Subject: [PATCH] feat(pool): add tick, tick-bitmap and position libraries --- src/libraries/Position.sol | 67 ++++++++++++++++++++ src/libraries/Tick.sol | 114 +++++++++++++++++++++++++++++++++++ src/libraries/TickBitmap.sol | 55 +++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 src/libraries/Position.sol create mode 100644 src/libraries/Tick.sol create mode 100644 src/libraries/TickBitmap.sol diff --git a/src/libraries/Position.sol b/src/libraries/Position.sol new file mode 100644 index 0000000..9d813a9 --- /dev/null +++ b/src/libraries/Position.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.36; + +import {FullMath} from "./FullMath.sol"; +import {FixedPoint128} from "./FixedPoint128.sol"; +import {LiquidityMath} from "./LiquidityMath.sol"; + +/// @title Per-owner liquidity range positions and their accrued fees. +library Position { + struct Info { + uint128 liquidity; + uint256 feeGrowthInside0LastX128; + uint256 feeGrowthInside1LastX128; + uint128 tokensOwed0; + uint128 tokensOwed1; + } + + function get(mapping(bytes32 => Info) storage self, address owner, int24 tickLower, int24 tickUpper) + internal + view + returns (Position.Info storage position) + { + position = self[keccak256(abi.encodePacked(owner, tickLower, tickUpper))]; + } + + function update( + Info storage self, + int128 liquidityDelta, + uint256 feeGrowthInside0X128, + uint256 feeGrowthInside1X128 + ) internal { + Info memory current = self; + + uint128 liquidityNext; + if (liquidityDelta == 0) { + require(current.liquidity > 0, "Position: EMPTY"); + liquidityNext = current.liquidity; + } else { + liquidityNext = LiquidityMath.addDelta(current.liquidity, liquidityDelta); + } + + uint128 tokensOwed0; + uint128 tokensOwed1; + unchecked { + tokensOwed0 = uint128( + FullMath.mulDiv( + feeGrowthInside0X128 - current.feeGrowthInside0LastX128, current.liquidity, FixedPoint128.q128 + ) + ); + tokensOwed1 = uint128( + FullMath.mulDiv( + feeGrowthInside1X128 - current.feeGrowthInside1LastX128, current.liquidity, FixedPoint128.q128 + ) + ); + } + + if (liquidityDelta != 0) self.liquidity = liquidityNext; + self.feeGrowthInside0LastX128 = feeGrowthInside0X128; + self.feeGrowthInside1LastX128 = feeGrowthInside1X128; + if (tokensOwed0 > 0 || tokensOwed1 > 0) { + unchecked { + self.tokensOwed0 += tokensOwed0; + self.tokensOwed1 += tokensOwed1; + } + } + } +} diff --git a/src/libraries/Tick.sol b/src/libraries/Tick.sol new file mode 100644 index 0000000..d40b472 --- /dev/null +++ b/src/libraries/Tick.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.36; + +import {TickMath} from "./TickMath.sol"; +import {LiquidityMath} from "./LiquidityMath.sol"; + +/// @title Per-tick liquidity and fee-growth bookkeeping. +library Tick { + struct Info { + uint128 liquidityGross; + int128 liquidityNet; + uint256 feeGrowthOutside0X128; + uint256 feeGrowthOutside1X128; + bool initialized; + } + + function tickSpacingToMaxLiquidityPerTick(int24 tickSpacing) internal pure returns (uint128) { + unchecked { + int24 minUsableTick = (TickMath.minTick / tickSpacing) * tickSpacing; + int24 maxUsableTick = (TickMath.maxTick / tickSpacing) * tickSpacing; + uint24 numTicks = uint24((maxUsableTick - minUsableTick) / tickSpacing) + 1; + return type(uint128).max / numTicks; + } + } + + function getFeeGrowthInside( + mapping(int24 => Info) storage self, + int24 tickLower, + int24 tickUpper, + int24 tickCurrent, + uint256 feeGrowthGlobal0X128, + uint256 feeGrowthGlobal1X128 + ) internal view returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) { + unchecked { + Info storage lower = self[tickLower]; + Info storage upper = self[tickUpper]; + + uint256 feeGrowthBelow0X128; + uint256 feeGrowthBelow1X128; + if (tickCurrent >= tickLower) { + feeGrowthBelow0X128 = lower.feeGrowthOutside0X128; + feeGrowthBelow1X128 = lower.feeGrowthOutside1X128; + } else { + feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128; + feeGrowthBelow1X128 = feeGrowthGlobal1X128 - lower.feeGrowthOutside1X128; + } + + uint256 feeGrowthAbove0X128; + uint256 feeGrowthAbove1X128; + if (tickCurrent < tickUpper) { + feeGrowthAbove0X128 = upper.feeGrowthOutside0X128; + feeGrowthAbove1X128 = upper.feeGrowthOutside1X128; + } else { + feeGrowthAbove0X128 = feeGrowthGlobal0X128 - upper.feeGrowthOutside0X128; + feeGrowthAbove1X128 = feeGrowthGlobal1X128 - upper.feeGrowthOutside1X128; + } + + feeGrowthInside0X128 = feeGrowthGlobal0X128 - feeGrowthBelow0X128 - feeGrowthAbove0X128; + feeGrowthInside1X128 = feeGrowthGlobal1X128 - feeGrowthBelow1X128 - feeGrowthAbove1X128; + } + } + + function update( + mapping(int24 => Info) storage self, + int24 tick, + int24 tickCurrent, + int128 liquidityDelta, + uint256 feeGrowthGlobal0X128, + uint256 feeGrowthGlobal1X128, + bool upper, + uint128 maxLiquidity + ) internal returns (bool flipped) { + Info storage info = self[tick]; + + uint128 liquidityGrossBefore = info.liquidityGross; + uint128 liquidityGrossAfter = LiquidityMath.addDelta(liquidityGrossBefore, liquidityDelta); + + require(liquidityGrossAfter <= maxLiquidity, "Tick: LIQ_MAX"); + + flipped = (liquidityGrossAfter == 0) != (liquidityGrossBefore == 0); + + if (liquidityGrossBefore == 0) { + if (tick <= tickCurrent) { + info.feeGrowthOutside0X128 = feeGrowthGlobal0X128; + info.feeGrowthOutside1X128 = feeGrowthGlobal1X128; + } + info.initialized = true; + } + + info.liquidityGross = liquidityGrossAfter; + + info.liquidityNet = upper + ? int128(int256(info.liquidityNet) - liquidityDelta) + : int128(int256(info.liquidityNet) + liquidityDelta); + } + + function clear(mapping(int24 => Info) storage self, int24 tick) internal { + delete self[tick]; + } + + function cross( + mapping(int24 => Info) storage self, + int24 tick, + uint256 feeGrowthGlobal0X128, + uint256 feeGrowthGlobal1X128 + ) internal returns (int128 liquidityNet) { + unchecked { + Info storage info = self[tick]; + info.feeGrowthOutside0X128 = feeGrowthGlobal0X128 - info.feeGrowthOutside0X128; + info.feeGrowthOutside1X128 = feeGrowthGlobal1X128 - info.feeGrowthOutside1X128; + liquidityNet = info.liquidityNet; + } + } +} diff --git a/src/libraries/TickBitmap.sol b/src/libraries/TickBitmap.sol new file mode 100644 index 0000000..93521cd --- /dev/null +++ b/src/libraries/TickBitmap.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.36; + +import {BitMath} from "./BitMath.sol"; + +/// @title Packed bitmap tracking which ticks hold liquidity. +library TickBitmap { + function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) { + unchecked { + wordPos = int16(tick >> 8); + bitPos = uint8(int8(tick % 256)); + } + } + + function flipTick(mapping(int16 => uint256) storage self, int24 tick, int24 tickSpacing) internal { + unchecked { + require(tick % tickSpacing == 0, "TickBitmap: SPACING"); + (int16 wordPos, uint8 bitPos) = position(tick / tickSpacing); + uint256 mask = 1 << bitPos; + self[wordPos] ^= mask; + } + } + + function nextInitializedTickWithinOneWord( + mapping(int16 => uint256) storage self, + int24 tick, + int24 tickSpacing, + bool lte + ) internal view returns (int24 next, bool initialized) { + unchecked { + int24 compressed = tick / tickSpacing; + if (tick < 0 && tick % tickSpacing != 0) compressed--; + + if (lte) { + (int16 wordPos, uint8 bitPos) = position(compressed); + uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); + uint256 masked = self[wordPos] & mask; + + initialized = masked != 0; + next = initialized + ? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing + : (compressed - int24(uint24(bitPos))) * tickSpacing; + } else { + (int16 wordPos, uint8 bitPos) = position(compressed + 1); + uint256 mask = ~((1 << bitPos) - 1); + uint256 masked = self[wordPos] & mask; + + initialized = masked != 0; + next = initialized + ? (compressed + 1 + int24(uint24(BitMath.leastSignificantBit(masked) - bitPos))) * tickSpacing + : (compressed + 1 + int24(uint24(type(uint8).max - bitPos))) * tickSpacing; + } + } + } +}