feat(pool): add tick, tick-bitmap and position libraries

This commit is contained in:
dualfroz committed 2026-08-24 14:28:00 +02:00
1 parent 8c16223be5
commit 040cec1c55
3 files changed
+236

No files matched your search

+67
View File
@@ -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;
}
}
}
}
+114
View File
@@ -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;
}
}
}
+55
View File
@@ -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;
}
}
}
}