Contract Source Code:
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.26;
import {FullMath} from './FullMath.sol';
import {FixedPoint128} from './FixedPoint128.sol';
import {FixedPoint32} from './FixedPoint32.sol';
import {FixedPoint96} from './FixedPoint96.sol';
import {Oracle} from './Oracle.sol';
import {SafeCast} from './SafeCast.sol';
import {Tick} from './Tick.sol';
import {TickBitmap} from './TickBitmap.sol';
import {PoolStorage, PositionInfo, PositionCheckpoint, RewardInfo} from './PoolStorage.sol';
/// @title Position
/// @notice Positions represent an owner address' liquidity between a lower and upper tick boundary
/// @dev Positions store additional state for tracking fees owed to the position
library Position {
error NP();
error FTR();
/// @notice Returns the hash used to store positions in a mapping
/// @param owner The address of the position owner
/// @param index The index of the position
/// @param tickLower The lower tick boundary of the position
/// @param tickUpper The upper tick boundary of the position
/// @return _hash The hash used to store positions in a mapping
function positionHash(
address owner,
uint256 index,
int24 tickLower,
int24 tickUpper
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(owner, index, tickLower, tickUpper));
}
/// @notice Returns the Info struct of a position, given an owner and position boundaries
/// @param self The mapping containing all user positions
/// @param owner The address of the position owner
/// @param tickLower The lower tick boundary of the position
/// @param tickUpper The upper tick boundary of the position
/// @return position The position info struct of the given owners' position
function get(
mapping(bytes32 => PositionInfo) storage self,
address owner,
uint256 index,
int24 tickLower,
int24 tickUpper
) internal view returns (PositionInfo storage position) {
position = self[positionHash(owner, index, tickLower, tickUpper)];
}
/// @notice Credits accumulated fees to a user's position
/// @param self The individual position to update
/// @param liquidityDelta The change in pool liquidity as a result of the position update
/// @param feeGrowthInside0X128 The all-time fee growth in token0, per unit of liquidity, inside the position's tick boundaries
/// @param feeGrowthInside1X128 The all-time fee growth in token1, per unit of liquidity, inside the position's tick boundaries
function update(
PositionInfo storage self,
int128 liquidityDelta,
uint256 feeGrowthInside0X128,
uint256 feeGrowthInside1X128,
bytes32 _positionHash,
uint256 period,
uint160 secondsPerLiquidityPeriodX128
) internal {
PoolStorage.PoolState storage $ = PoolStorage.getStorage();
uint128 liquidity = self.liquidity;
uint128 liquidityNext;
if (liquidityDelta == 0) {
/// @dev disallow pokes for 0 liquidity positions
if (liquidity <= 0) revert NP();
liquidityNext = liquidity;
} else {
liquidityNext = liquidityDelta < 0
? liquidity - uint128(-liquidityDelta)
: liquidity + uint128(liquidityDelta);
}
/// @dev calculate accumulated fees. overflow in the subtraction of fee growth is expected
uint128 tokensOwed0;
uint128 tokensOwed1;
unchecked {
tokensOwed0 = uint128(
FullMath.mulDiv(feeGrowthInside0X128 - self.feeGrowthInside0LastX128, liquidity, FixedPoint128.Q128)
);
tokensOwed1 = uint128(
FullMath.mulDiv(feeGrowthInside1X128 - self.feeGrowthInside1LastX128, liquidity, FixedPoint128.Q128)
);
/// @dev update the position
if (liquidityDelta != 0) self.liquidity = liquidityNext;
self.feeGrowthInside0LastX128 = feeGrowthInside0X128;
self.feeGrowthInside1LastX128 = feeGrowthInside1X128;
if (tokensOwed0 > 0 || tokensOwed1 > 0) {
/// @dev overflow is acceptable, user must withdraw before they hit type(uint128).max fees
self.tokensOwed0 += tokensOwed0;
self.tokensOwed1 += tokensOwed1;
}
}
/// @dev write checkpoint, push a checkpoint if the last period is different, overwrite if not
uint256 checkpointLength = $.positionCheckpoints[_positionHash].length;
if (checkpointLength == 0 || $.positionCheckpoints[_positionHash][checkpointLength - 1].period != period) {
$.positionCheckpoints[_positionHash].push(PositionCheckpoint({period: period, liquidity: liquidityNext}));
} else {
$.positionCheckpoints[_positionHash][checkpointLength - 1].liquidity = liquidityNext;
}
int160 secondsPerLiquidityPeriodIntX128 = int160(secondsPerLiquidityPeriodX128);
int160 secondsPerLiquidityPeriodStartX128 = self.periodRewardInfo[period].secondsPerLiquidityPeriodStartX128;
/// @dev take the difference to make the delta positive or zero
secondsPerLiquidityPeriodIntX128 -= secondsPerLiquidityPeriodStartX128;
/// @dev these int should never be negative
if (secondsPerLiquidityPeriodIntX128 < 0) {
secondsPerLiquidityPeriodIntX128 = 0;
}
/// @dev secondsDebtDeltaX96 is declared differently based on the liquidityDelta
int256 secondsDebtDeltaX96 = liquidityDelta > 0
/// @dev case: delta > 0
? SafeCast.toInt256(
/// @dev round upwards
FullMath.mulDivRoundingUp(
uint256(uint128(liquidityDelta)),
uint256(uint160(secondsPerLiquidityPeriodIntX128)),
FixedPoint32.Q32
)
)
/// @dev case: delta <= 0
: SafeCast.toInt256(
/// @dev round downwards
FullMath.mulDiv(
/// @dev flip liquidityDelta sign
uint256(uint128(-liquidityDelta)),
uint256(uint160(secondsPerLiquidityPeriodIntX128)),
FixedPoint32.Q32
)
);
self.periodRewardInfo[period].secondsDebtX96 = liquidityDelta > 0
? self.periodRewardInfo[period].secondsDebtX96 + secondsDebtDeltaX96 /// @dev can't overflow since each period is way less than uint31
: self.periodRewardInfo[period].secondsDebtX96 - secondsDebtDeltaX96;
}
/// @notice gets the checkpoint directly before the period
/// @dev returns the 0th index if there's no checkpoints
/// @param checkpoints the position's checkpoints in storage
/// @param period the period of interest
function getCheckpoint(
PositionCheckpoint[] storage checkpoints,
uint256 period
) internal view returns (uint256 checkpointIndex, uint256 checkpointPeriod) {
{
uint256 checkpointLength = checkpoints.length;
/// @dev return 0 if length is 0
if (checkpointLength == 0) {
return (0, 0);
}
checkpointPeriod = checkpoints[0].period;
/// @dev return 0 if first checkpoint happened after period
if (checkpointPeriod > period) {
return (0, 0);
}
checkpointIndex = checkpointLength - 1;
}
checkpointPeriod = checkpoints[checkpointIndex].period;
/// @dev Find relevant checkpoint if latest checkpoint isn't before period of interest
if (checkpointPeriod > period) {
uint256 lower = 0;
uint256 upper = checkpointIndex;
while (upper > lower) {
/// @dev ceil, avoiding overflow
uint256 center = upper - (upper - lower) / 2;
checkpointPeriod = checkpoints[center].period;
if (checkpointPeriod == period) {
checkpointIndex = center;
return (checkpointIndex, checkpointPeriod);
} else if (checkpointPeriod < period) {
lower = center;
} else {
upper = center - 1;
}
}
checkpointIndex = lower;
checkpointPeriod = checkpoints[checkpointIndex].period;
}
return (checkpointIndex, checkpointPeriod);
}
struct PositionPeriodSecondsInRangeParams {
uint256 period;
address owner;
uint256 index;
int24 tickLower;
int24 tickUpper;
uint32 _blockTimestamp;
}
/// @notice Get the period seconds in range of a specific position
/// @return periodSecondsInsideX96 seconds the position was not in range for the period
function positionPeriodSecondsInRange(
PositionPeriodSecondsInRangeParams memory params
) public view returns (uint256 periodSecondsInsideX96) {
PoolStorage.PoolState storage $ = PoolStorage.getStorage();
uint256 currentPeriod = $.lastPeriod;
if (params.period > currentPeriod) revert FTR();
bytes32 _positionHash = positionHash(params.owner, params.index, params.tickLower, params.tickUpper);
uint256 liquidity;
int160 secondsPerLiquidityPeriodStartX128;
PositionCheckpoint[] storage checkpoints = $.positionCheckpoints[_positionHash];
/// @dev get checkpoint at period, or last checkpoint before the period
(uint256 checkpointIndex, uint256 checkpointPeriod) = getCheckpoint(checkpoints, params.period);
/// @dev Return 0s if checkpointPeriod is 0
if (checkpointPeriod == 0) {
return 0;
}
liquidity = checkpoints[checkpointIndex].liquidity;
secondsPerLiquidityPeriodStartX128 = $
.positions[_positionHash]
.periodRewardInfo[params.period]
.secondsPerLiquidityPeriodStartX128;
uint160 secondsPerLiquidityInsideX128 = Oracle.periodCumulativesInside(
uint32(params.period),
params.tickLower,
params.tickUpper,
params._blockTimestamp
);
/// @dev underflow will be protected by sanity check
secondsPerLiquidityInsideX128 = uint160(
int160(secondsPerLiquidityInsideX128) - secondsPerLiquidityPeriodStartX128
);
RewardInfo storage rewardInfo = $.positions[_positionHash].periodRewardInfo[params.period];
int256 secondsDebtX96 = rewardInfo.secondsDebtX96;
/// @dev addDelta checks for under and overflows
periodSecondsInsideX96 = FullMath.mulDiv(liquidity, secondsPerLiquidityInsideX128, FixedPoint32.Q32);
/// @dev Need to check if secondsDebtX96>periodSecondsInsideX96, since rounding can cause underflows
if (secondsDebtX96 < 0 || periodSecondsInsideX96 > uint256(secondsDebtX96)) {
periodSecondsInsideX96 = secondsDebtX96 < 0
? periodSecondsInsideX96 + uint256(-secondsDebtX96)
: periodSecondsInsideX96 - uint256(secondsDebtX96);
} else {
periodSecondsInsideX96 = 0;
}
/// @dev sanity
if (periodSecondsInsideX96 > 1 weeks * FixedPoint96.Q96) {
periodSecondsInsideX96 = 0;
}
}
struct UpdatePositionParams {
/// @dev the owner of the position
address owner;
/// @dev the index of the position
uint256 index;
/// @dev the lower tick of the position's tick range
int24 tickLower;
/// @dev the upper tick of the position's tick range
int24 tickUpper;
/// @dev the amount liquidity changes by
int128 liquidityDelta;
/// @dev the current tick, passed to avoid sloads
int24 tick;
uint32 _blockTimestamp;
int24 tickSpacing;
uint128 maxLiquidityPerTick;
}
/// @dev Gets and updates a position with the given liquidity delta
/// @param params the position details and the change to the position's liquidity to effect
function _updatePosition(UpdatePositionParams memory params) external returns (PositionInfo storage position) {
PoolStorage.PoolState storage $ = PoolStorage.getStorage();
/// @dev calculate the period once, and reuse it
uint256 period = params._blockTimestamp / 1 weeks;
/// @dev precompute the position hash
bytes32 _positionHash = positionHash(params.owner, params.index, params.tickLower, params.tickUpper);
/// @dev fetch the position using the precomputed _positionHash
position = $.positions[_positionHash];
/// @dev SLOAD for gas optimization
uint256 _feeGrowthGlobal0X128 = $.feeGrowthGlobal0X128;
uint256 _feeGrowthGlobal1X128 = $.feeGrowthGlobal1X128;
/// @dev use the tick from `$.slot0` instead of `params.tick` for consistency
int24 currentTick = $.slot0.tick;
/// @dev check and update ticks if needed
bool flippedLower;
bool flippedUpper;
if (params.liquidityDelta != 0) {
/// @dev directly use params._blockTimestamp instead of creating a new `time` variable
(int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = Oracle.observeSingle(
$.observations,
params._blockTimestamp,
0,
currentTick, /// @dev use `currentTick` consistently
$.slot0.observationIndex,
$.liquidity,
$.slot0.observationCardinality
);
flippedLower = Tick.update(
$._ticks,
params.tickLower,
currentTick, /// @dev use `currentTick` consistently
params.liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
params._blockTimestamp,
false,
params.maxLiquidityPerTick
);
flippedUpper = Tick.update(
$._ticks,
params.tickUpper,
currentTick, /// @dev use `currentTick` consistently
params.liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
params._blockTimestamp,
true,
params.maxLiquidityPerTick
);
/// @dev flip ticks if needed
if (flippedLower) {
TickBitmap.flipTick($.tickBitmap, params.tickLower, params.tickSpacing);
}
if (flippedUpper) {
TickBitmap.flipTick($.tickBitmap, params.tickUpper, params.tickSpacing);
}
}
/// @dev calculate the fee growth inside
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = Tick.getFeeGrowthInside(
$._ticks,
params.tickLower,
params.tickUpper,
currentTick, /// @dev use `currentTick` consistently
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128
);
/// @dev get the seconds per liquidity period cumulatives
uint160 secondsPerLiquidityPeriodX128 = Oracle.periodCumulativesInside(
uint32(period),
params.tickLower,
params.tickUpper,
params._blockTimestamp
);
/// @dev initialize position reward info if needed
if (!position.periodRewardInfo[period].initialized || position.liquidity == 0) {
initializeSecondsStart(
position,
PositionPeriodSecondsInRangeParams({
period: period,
owner: params.owner,
index: params.index,
tickLower: params.tickLower,
tickUpper: params.tickUpper,
_blockTimestamp: params._blockTimestamp
}),
secondsPerLiquidityPeriodX128
);
}
/// @dev update the position
update(
position,
params.liquidityDelta,
feeGrowthInside0X128,
feeGrowthInside1X128,
_positionHash,
period,
secondsPerLiquidityPeriodX128
);
/// @dev clear tick data if liquidity delta is negative and the ticks no longer hold liquidity
if (params.liquidityDelta < 0) {
if (flippedLower) {
Tick.clear($._ticks, params.tickLower, period);
}
if (flippedUpper) {
Tick.clear($._ticks, params.tickUpper, period);
}
}
}
/// @notice Initializes secondsPerLiquidityPeriodStartX128 for a position
/// @param position The individual position
/// @param secondsInRangeParams Parameters used to find the seconds in range
/// @param secondsPerLiquidityPeriodX128 The seconds in range gained per unit of liquidity, inside the position's tick boundaries for this period
function initializeSecondsStart(
PositionInfo storage position,
PositionPeriodSecondsInRangeParams memory secondsInRangeParams,
uint160 secondsPerLiquidityPeriodX128
) internal {
/// @dev record initialized
position.periodRewardInfo[secondsInRangeParams.period].initialized = true;
/// @dev record owed tokens if liquidity > 0 (means position existed before period change)
if (position.liquidity > 0) {
uint256 periodSecondsInsideX96 = positionPeriodSecondsInRange(secondsInRangeParams);
position.periodRewardInfo[secondsInRangeParams.period].secondsDebtX96 = -int256(periodSecondsInsideX96);
}
/// @dev convert uint to int
/// @dev negative expected sometimes, which is allowed
int160 secondsPerLiquidityPeriodIntX128 = int160(secondsPerLiquidityPeriodX128);
position
.periodRewardInfo[secondsInRangeParams.period]
.secondsPerLiquidityPeriodStartX128 = secondsPerLiquidityPeriodIntX128;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
/// @title Contains 512-bit math functions
/// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision
/// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits
library FullMath {
/// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
/// @param a The multiplicand
/// @param b The multiplier
/// @param denominator The divisor
/// @return result The 256-bit result
/// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv
function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
unchecked {
// 512-bit multiply [prod1 prod0] = a * b
// Compute the product mod 2**256 and mod 2**256 - 1
// then use the Chinese Remainder Theorem to reconstruct
// the 512 bit result. The result is stored in two 256
// variables such that product = prod1 * 2**256 + prod0
uint256 prod0; // Least significant 256 bits of the product
uint256 prod1; // Most significant 256 bits of the product
assembly {
let mm := mulmod(a, b, not(0))
prod0 := mul(a, b)
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}
// Handle non-overflow cases, 256 by 256 division
if (prod1 == 0) {
require(denominator > 0);
assembly {
result := div(prod0, denominator)
}
return result;
}
// Make sure the result is less than 2**256.
// Also prevents denominator == 0
require(denominator > prod1);
///////////////////////////////////////////////
// 512 by 256 division.
///////////////////////////////////////////////
// Make division exact by subtracting the remainder from [prod1 prod0]
// Compute remainder using mulmod
uint256 remainder;
assembly {
remainder := mulmod(a, b, denominator)
}
// Subtract 256 bit number from 512 bit number
assembly {
prod1 := sub(prod1, gt(remainder, prod0))
prod0 := sub(prod0, remainder)
}
// Factor powers of two out of denominator
// Compute largest power of two divisor of denominator.
// Always >= 1.
uint256 twos = (0 - denominator) & denominator;
// Divide denominator by power of two
assembly {
denominator := div(denominator, twos)
}
// Divide [prod1 prod0] by the factors of two
assembly {
prod0 := div(prod0, twos)
}
// Shift in bits from prod1 into prod0. For this we need
// to flip `twos` such that it is 2**256 / twos.
// If twos is zero, then it becomes one
assembly {
twos := add(div(sub(0, twos), twos), 1)
}
prod0 |= prod1 * twos;
// Invert denominator mod 2**256
// Now that denominator is an odd number, it has an inverse
// modulo 2**256 such that denominator * inv = 1 mod 2**256.
// Compute the inverse by starting with a seed that is correct
// correct for four bits. That is, denominator * inv = 1 mod 2**4
uint256 inv = (3 * denominator) ^ 2;
// Now use Newton-Raphson iteration to improve the precision.
// Thanks to Hensel's lifting lemma, this also works in modular
// arithmetic, doubling the correct bits in each step.
inv *= 2 - denominator * inv; // inverse mod 2**8
inv *= 2 - denominator * inv; // inverse mod 2**16
inv *= 2 - denominator * inv; // inverse mod 2**32
inv *= 2 - denominator * inv; // inverse mod 2**64
inv *= 2 - denominator * inv; // inverse mod 2**128
inv *= 2 - denominator * inv; // inverse mod 2**256
// Because the division is now exact we can divide by multiplying
// with the modular inverse of denominator. This will give us the
// correct result modulo 2**256. Since the precoditions guarantee
// that the outcome is less than 2**256, this is the final result.
// We don't need to compute the high bits of the result and prod1
// is no longer required.
result = prod0 * inv;
return result;
}
}
/// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
/// @param a The multiplicand
/// @param b The multiplier
/// @param denominator The divisor
/// @return result The 256-bit result
function mulDivRoundingUp(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
unchecked {
result = mulDiv(a, b, denominator);
if (mulmod(a, b, denominator) > 0) {
require(result < type(uint256).max);
result++;
}
}
}
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.4.0;
/// @title FixedPoint128
/// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format)
library FixedPoint128 {
uint256 internal constant Q128 = 0x100000000000000000000000000000000;
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.4.0;
/// @title FixedPoint32
/// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format)
library FixedPoint32 {
uint8 internal constant RESOLUTION = 32;
uint256 internal constant Q32 = 0x100000000;
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.4.0;
/// @title FixedPoint96
/// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format)
/// @dev Used in SqrtPriceMath.sol
library FixedPoint96 {
uint8 internal constant RESOLUTION = 96;
uint256 internal constant Q96 = 0x1000000000000000000000000;
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.26;
import {PoolStorage, Observation, TickInfo, Slot0} from './PoolStorage.sol';
/// @title Oracle
/// @notice Provides price and liquidity data useful for a wide variety of system designs
/// @dev Instances of stored oracle data, "observations", are collected in the oracle array
/// Every pool is initialized with an oracle array length of 1. Anyone can pay the SSTOREs to increase the
/// maximum length of the oracle array. New slots will be added when the array is fully populated.
/// Observations are overwritten when the full length of the oracle array is populated.
/// The most recent observation is available, independent of the length of the oracle array, by passing 0 to observe()
library Oracle {
error I();
error OLD();
/// @notice Transforms a previous observation into a new observation, given the passage of time and the current tick and liquidity values
/// @dev blockTimestamp _must_ be chronologically equal to or greater than last.blockTimestamp, safe for 0 or 1 overflows
/// @param last The specified observation to be transformed
/// @param blockTimestamp The timestamp of the new observation
/// @param tick The active tick at the time of the new observation
/// @param liquidity The total in-range liquidity at the time of the new observation
/// @return Observation The newly populated observation
function transform(
Observation memory last,
uint32 blockTimestamp,
int24 tick,
uint128 liquidity
) private pure returns (Observation memory) {
unchecked {
uint32 delta = blockTimestamp - last.blockTimestamp;
return
Observation({
blockTimestamp: blockTimestamp,
tickCumulative: last.tickCumulative + int56(tick) * int56(uint56(delta)),
secondsPerLiquidityCumulativeX128: last.secondsPerLiquidityCumulativeX128 +
((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1)),
initialized: true
});
}
}
/// @notice Initialize the oracle array by writing the first slot. Called once for the lifecycle of the observations array
/// @param self The stored oracle array
/// @param time The time of the oracle initialization, via block.timestamp truncated to uint32
/// @return cardinality The number of populated elements in the oracle array
/// @return cardinalityNext The new length of the oracle array, independent of population
function initialize(
Observation[65535] storage self,
uint32 time
) internal returns (uint16 cardinality, uint16 cardinalityNext) {
self[0] = Observation({
blockTimestamp: time,
tickCumulative: 0,
secondsPerLiquidityCumulativeX128: 0,
initialized: true
});
return (1, 1);
}
/// @notice Writes an oracle observation to the array
/// @dev Writable at most once per block. Index represents the most recently written element. cardinality and index must be tracked externally.
/// If the index is at the end of the allowable array length (according to cardinality), and the next cardinality
/// is greater than the current one, cardinality may be increased. This restriction is created to preserve ordering.
/// @param self The stored oracle array
/// @param index The index of the observation that was most recently written to the observations array
/// @param blockTimestamp The timestamp of the new observation
/// @param tick The active tick at the time of the new observation
/// @param liquidity The total in-range liquidity at the time of the new observation
/// @param cardinality The number of populated elements in the oracle array
/// @param cardinalityNext The new length of the oracle array, independent of population
/// @return indexUpdated The new index of the most recently written element in the oracle array
/// @return cardinalityUpdated The new cardinality of the oracle array
function write(
Observation[65535] storage self,
uint16 index,
uint32 blockTimestamp,
int24 tick,
uint128 liquidity,
uint16 cardinality,
uint16 cardinalityNext
) internal returns (uint16 indexUpdated, uint16 cardinalityUpdated) {
unchecked {
Observation memory last = self[index];
/// @dev early return if we've already written an observation this block
if (last.blockTimestamp == blockTimestamp) return (index, cardinality);
/// @dev if the conditions are right, we can bump the cardinality
if (cardinalityNext > cardinality && index == (cardinality - 1)) {
cardinalityUpdated = cardinalityNext;
} else {
cardinalityUpdated = cardinality;
}
indexUpdated = (index + 1) % cardinalityUpdated;
self[indexUpdated] = transform(last, blockTimestamp, tick, liquidity);
}
}
/// @notice Prepares the oracle array to store up to `next` observations
/// @param self The stored oracle array
/// @param current The current next cardinality of the oracle array
/// @param next The proposed next cardinality which will be populated in the oracle array
/// @return next The next cardinality which will be populated in the oracle array
function grow(Observation[65535] storage self, uint16 current, uint16 next) internal returns (uint16) {
unchecked {
if (current <= 0) revert I();
/// @dev no-op if the passed next value isn't greater than the current next value
if (next <= current) return current;
/// @dev store in each slot to prevent fresh SSTOREs in swaps
/// @dev this data will not be used because the initialized boolean is still false
for (uint16 i = current; i < next; i++) self[i].blockTimestamp = 1;
return next;
}
}
/// @notice comparator for 32-bit timestamps
/// @dev safe for 0 or 1 overflows, a and b _must_ be chronologically before or equal to time
/// @param time A timestamp truncated to 32 bits
/// @param a A comparison timestamp from which to determine the relative position of `time`
/// @param b From which to determine the relative position of `time`
/// @return Whether `a` is chronologically <= `b`
function lte(uint32 time, uint32 a, uint32 b) private pure returns (bool) {
unchecked {
/// @dev if there hasn't been overflow, no need to adjust
if (a <= time && b <= time) return a <= b;
uint256 aAdjusted = a > time ? a : a + 2 ** 32;
uint256 bAdjusted = b > time ? b : b + 2 ** 32;
return aAdjusted <= bAdjusted;
}
}
/// @notice Fetches the observations beforeOrAt and atOrAfter a target, i.e. where [beforeOrAt, atOrAfter] is satisfied.
/// The result may be the same observation, or adjacent observations.
/// @dev The answer must be contained in the array, used when the target is located within the stored observation
/// boundaries: older than the most recent observation and younger, or the same age as, the oldest observation
/// @param self The stored oracle array
/// @param time The current block.timestamp
/// @param target The timestamp at which the reserved observation should be for
/// @param index The index of the observation that was most recently written to the observations array
/// @param cardinality The number of populated elements in the oracle array
/// @return beforeOrAt The observation recorded before, or at, the target
/// @return atOrAfter The observation recorded at, or after, the target
function binarySearch(
Observation[65535] storage self,
uint32 time,
uint32 target,
uint16 index,
uint16 cardinality
) private view returns (Observation memory beforeOrAt, Observation memory atOrAfter) {
unchecked {
/// @dev oldest observation
uint256 l = (index + 1) % cardinality;
/// @dev newest observation
uint256 r = l + cardinality - 1;
uint256 i;
while (true) {
i = (l + r) / 2;
beforeOrAt = self[i % cardinality];
/// @dev we've landed on an uninitialized tick, keep searching higher (more recently)
if (!beforeOrAt.initialized) {
l = i + 1;
continue;
}
atOrAfter = self[(i + 1) % cardinality];
bool targetAtOrAfter = lte(time, beforeOrAt.blockTimestamp, target);
/// @dev check if we've found the answer!
if (targetAtOrAfter && lte(time, target, atOrAfter.blockTimestamp)) break;
if (!targetAtOrAfter) r = i - 1;
else l = i + 1;
}
}
}
/// @notice Fetches the observations beforeOrAt and atOrAfter a given target, i.e. where [beforeOrAt, atOrAfter] is satisfied
/// @dev Assumes there is at least 1 initialized observation.
/// Used by observeSingle() to compute the counterfactual accumulator values as of a given block timestamp.
/// @param self The stored oracle array
/// @param time The current block.timestamp
/// @param target The timestamp at which the reserved observation should be for
/// @param tick The active tick at the time of the returned or simulated observation
/// @param index The index of the observation that was most recently written to the observations array
/// @param liquidity The total pool liquidity at the time of the call
/// @param cardinality The number of populated elements in the oracle array
/// @return beforeOrAt The observation which occurred at, or before, the given timestamp
/// @return atOrAfter The observation which occurred at, or after, the given timestamp
function getSurroundingObservations(
Observation[65535] storage self,
uint32 time,
uint32 target,
int24 tick,
uint16 index,
uint128 liquidity,
uint16 cardinality
) private view returns (Observation memory beforeOrAt, Observation memory atOrAfter) {
unchecked {
/// @dev optimistically set before to the newest observation
beforeOrAt = self[index];
/// @dev if the target is chronologically at or after the newest observation, we can early return
if (lte(time, beforeOrAt.blockTimestamp, target)) {
if (beforeOrAt.blockTimestamp == target) {
/// @dev if newest observation equals target, we're in the same block, so we can ignore atOrAfter
return (beforeOrAt, atOrAfter);
} else {
/// @dev otherwise, we need to transform
return (beforeOrAt, transform(beforeOrAt, target, tick, liquidity));
}
}
/// @dev now, set before to the oldest observation
beforeOrAt = self[(index + 1) % cardinality];
if (!beforeOrAt.initialized) beforeOrAt = self[0];
/// @dev ensure that the target is chronologically at or after the oldest observation
if (!lte(time, beforeOrAt.blockTimestamp, target)) revert OLD();
/// @dev if we've reached this point, we have to binary search
return binarySearch(self, time, target, index, cardinality);
}
}
/// @dev Reverts if an observation at or before the desired observation timestamp does not exist.
/// 0 may be passed as `secondsAgo' to return the current cumulative values.
/// If called with a timestamp falling between two observations, returns the counterfactual accumulator values
/// at exactly the timestamp between the two observations.
/// @param self The stored oracle array
/// @param time The current block timestamp
/// @param secondsAgo The amount of time to look back, in seconds, at which point to return an observation
/// @param tick The current tick
/// @param index The index of the observation that was most recently written to the observations array
/// @param liquidity The current in-range pool liquidity
/// @param cardinality The number of populated elements in the oracle array
/// @return tickCumulative The tick * time elapsed since the pool was first initialized, as of `secondsAgo`
/// @return secondsPerLiquidityCumulativeX128 The time elapsed / max(1, liquidity) since the pool was first initialized, as of `secondsAgo`
function observeSingle(
Observation[65535] storage self,
uint32 time,
uint32 secondsAgo,
int24 tick,
uint16 index,
uint128 liquidity,
uint16 cardinality
) internal view returns (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) {
unchecked {
if (secondsAgo == 0) {
Observation memory last = self[index];
if (last.blockTimestamp != time) last = transform(last, time, tick, liquidity);
return (last.tickCumulative, last.secondsPerLiquidityCumulativeX128);
}
uint32 target = time - secondsAgo;
(Observation memory beforeOrAt, Observation memory atOrAfter) = getSurroundingObservations(
self,
time,
target,
tick,
index,
liquidity,
cardinality
);
if (target == beforeOrAt.blockTimestamp) {
/// @dev we're at the left boundary
return (beforeOrAt.tickCumulative, beforeOrAt.secondsPerLiquidityCumulativeX128);
} else if (target == atOrAfter.blockTimestamp) {
/// @dev we're at the right boundary
return (atOrAfter.tickCumulative, atOrAfter.secondsPerLiquidityCumulativeX128);
} else {
/// @dev we're in the middle
uint32 observationTimeDelta = atOrAfter.blockTimestamp - beforeOrAt.blockTimestamp;
uint32 targetDelta = target - beforeOrAt.blockTimestamp;
return (
beforeOrAt.tickCumulative +
((atOrAfter.tickCumulative - beforeOrAt.tickCumulative) / int56(uint56(observationTimeDelta))) *
int56(uint56(targetDelta)),
beforeOrAt.secondsPerLiquidityCumulativeX128 +
uint160(
(uint256(
atOrAfter.secondsPerLiquidityCumulativeX128 -
beforeOrAt.secondsPerLiquidityCumulativeX128
) * targetDelta) / observationTimeDelta
)
);
}
}
}
/// @notice Returns the accumulator values as of each time seconds ago from the given time in the array of `secondsAgos`
/// @dev Reverts if `secondsAgos` > oldest observation
/// @param self The stored oracle array
/// @param time The current block.timestamp
/// @param secondsAgos Each amount of time to look back, in seconds, at which point to return an observation
/// @param tick The current tick
/// @param index The index of the observation that was most recently written to the observations array
/// @param liquidity The current in-range pool liquidity
/// @param cardinality The number of populated elements in the oracle array
/// @return tickCumulatives The tick * time elapsed since the pool was first initialized, as of each `secondsAgo`
/// @return secondsPerLiquidityCumulativeX128s The cumulative seconds / max(1, liquidity) since the pool was first initialized, as of each `secondsAgo`
function observe(
Observation[65535] storage self,
uint32 time,
uint32[] memory secondsAgos,
int24 tick,
uint16 index,
uint128 liquidity,
uint16 cardinality
) internal view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) {
unchecked {
if (cardinality <= 0) revert I();
tickCumulatives = new int56[](secondsAgos.length);
secondsPerLiquidityCumulativeX128s = new uint160[](secondsAgos.length);
for (uint256 i = 0; i < secondsAgos.length; i++) {
(tickCumulatives[i], secondsPerLiquidityCumulativeX128s[i]) = observeSingle(
self,
time,
secondsAgos[i],
tick,
index,
liquidity,
cardinality
);
}
}
}
function newPeriod(
Observation[65535] storage self,
uint16 index,
uint256 period
) external returns (uint160 secondsPerLiquidityCumulativeX128) {
Observation memory last = self[index];
PoolStorage.PoolState storage $ = PoolStorage.getStorage();
unchecked {
uint32 delta = uint32(period) * 1 weeks - 1 - last.blockTimestamp;
secondsPerLiquidityCumulativeX128 =
last.secondsPerLiquidityCumulativeX128 +
((uint160(delta) << 128) / ($.liquidity > 0 ? $.liquidity : 1));
self[index] = Observation({
blockTimestamp: uint32(period) * 1 weeks - 1,
tickCumulative: last.tickCumulative + int56($.slot0.tick) * int56(uint56(delta)),
secondsPerLiquidityCumulativeX128: secondsPerLiquidityCumulativeX128,
initialized: last.initialized
});
}
}
struct SnapShot {
int56 tickCumulativeLower;
int56 tickCumulativeUpper;
uint160 secondsPerLiquidityOutsideLowerX128;
uint160 secondsPerLiquidityOutsideUpperX128;
uint32 secondsOutsideLower;
uint32 secondsOutsideUpper;
}
struct SnapshotCumulativesInsideCache {
uint32 time;
int56 tickCumulative;
uint160 secondsPerLiquidityCumulativeX128;
}
/// @notice Returns a snapshot of the tick cumulative, seconds per liquidity and seconds inside a tick range
/// @dev Snapshots must only be compared to other snapshots, taken over a period for which a position existed.
/// I.e., snapshots cannot be compared if a position is not held for the entire period between when the first
/// snapshot is taken and the second snapshot is taken. Boosted data is only valid if it's within the same period
/// @param tickLower The lower tick of the range
/// @param tickUpper The upper tick of the range
/// @return tickCumulativeInside The snapshot of the tick accumulator for the range
/// @return secondsPerLiquidityInsideX128 The snapshot of seconds per liquidity for the range
/// @return secondsInside The snapshot of seconds per liquidity for the range
function snapshotCumulativesInside(
int24 tickLower,
int24 tickUpper,
uint32 _blockTimestamp
) external view returns (int56 tickCumulativeInside, uint160 secondsPerLiquidityInsideX128, uint32 secondsInside) {
PoolStorage.PoolState storage $ = PoolStorage.getStorage();
TickInfo storage lower = $._ticks[tickLower];
TickInfo storage upper = $._ticks[tickUpper];
SnapShot memory snapshot;
bool initializedLower;
(
snapshot.tickCumulativeLower,
snapshot.secondsPerLiquidityOutsideLowerX128,
snapshot.secondsOutsideLower,
initializedLower
) = (
lower.tickCumulativeOutside,
lower.secondsPerLiquidityOutsideX128,
lower.secondsOutside,
lower.initialized
);
require(initializedLower);
bool initializedUpper;
(
snapshot.tickCumulativeUpper,
snapshot.secondsPerLiquidityOutsideUpperX128,
snapshot.secondsOutsideUpper,
initializedUpper
) = (
upper.tickCumulativeOutside,
upper.secondsPerLiquidityOutsideX128,
upper.secondsOutside,
upper.initialized
);
require(initializedUpper);
Slot0 memory _slot0 = $.slot0;
unchecked {
if (_slot0.tick < tickLower) {
return (
snapshot.tickCumulativeLower - snapshot.tickCumulativeUpper,
snapshot.secondsPerLiquidityOutsideLowerX128 - snapshot.secondsPerLiquidityOutsideUpperX128,
snapshot.secondsOutsideLower - snapshot.secondsOutsideUpper
);
} else if (_slot0.tick < tickUpper) {
SnapshotCumulativesInsideCache memory cache;
cache.time = _blockTimestamp;
(cache.tickCumulative, cache.secondsPerLiquidityCumulativeX128) = observeSingle(
$.observations,
cache.time,
0,
_slot0.tick,
_slot0.observationIndex,
$.liquidity,
_slot0.observationCardinality
);
return (
cache.tickCumulative - snapshot.tickCumulativeLower - snapshot.tickCumulativeUpper,
cache.secondsPerLiquidityCumulativeX128 -
snapshot.secondsPerLiquidityOutsideLowerX128 -
snapshot.secondsPerLiquidityOutsideUpperX128,
cache.time - snapshot.secondsOutsideLower - snapshot.secondsOutsideUpper
);
} else {
return (
snapshot.tickCumulativeUpper - snapshot.tickCumulativeLower,
snapshot.secondsPerLiquidityOutsideUpperX128 - snapshot.secondsPerLiquidityOutsideLowerX128,
snapshot.secondsOutsideUpper - snapshot.secondsOutsideLower
);
}
}
}
/// @notice Returns the seconds per liquidity and seconds inside a tick range for a period
/// @dev This does not ensure the range is a valid range
/// @param period The timestamp of the period
/// @param tickLower The lower tick of the range
/// @param tickUpper The upper tick of the range
/// @return secondsPerLiquidityInsideX128 The snapshot of seconds per liquidity for the range
function periodCumulativesInside(
uint32 period,
int24 tickLower,
int24 tickUpper,
uint32 _blockTimestamp
) external view returns (uint160 secondsPerLiquidityInsideX128) {
PoolStorage.PoolState storage $ = PoolStorage.getStorage();
TickInfo storage lower = $._ticks[tickLower];
TickInfo storage upper = $._ticks[tickUpper];
SnapShot memory snapshot;
{
int24 startTick = $.periods[period].startTick;
uint256 previousPeriod = $.periods[period].previousPeriod;
snapshot.secondsPerLiquidityOutsideLowerX128 = uint160(lower.periodSecondsPerLiquidityOutsideX128[period]);
if (tickLower <= startTick && snapshot.secondsPerLiquidityOutsideLowerX128 == 0) {
snapshot.secondsPerLiquidityOutsideLowerX128 = $
.periods[previousPeriod]
.endSecondsPerLiquidityPeriodX128;
}
snapshot.secondsPerLiquidityOutsideUpperX128 = uint160(upper.periodSecondsPerLiquidityOutsideX128[period]);
if (tickUpper <= startTick && snapshot.secondsPerLiquidityOutsideUpperX128 == 0) {
snapshot.secondsPerLiquidityOutsideUpperX128 = $
.periods[previousPeriod]
.endSecondsPerLiquidityPeriodX128;
}
}
int24 lastTick;
uint256 currentPeriod = $.lastPeriod;
{
/// @dev if period is already finalized, use period's last tick, if not, use current tick
if (currentPeriod > period) {
lastTick = $.periods[period].lastTick;
} else {
lastTick = $.slot0.tick;
}
}
unchecked {
if (lastTick < tickLower) {
return snapshot.secondsPerLiquidityOutsideLowerX128 - snapshot.secondsPerLiquidityOutsideUpperX128;
} else if (lastTick < tickUpper) {
SnapshotCumulativesInsideCache memory cache;
/// @dev if period's on-going, observeSingle, if finalized, use endSecondsPerLiquidityPeriodX128
if (currentPeriod <= period) {
cache.time = _blockTimestamp;
/// @dev limit to the end of period
if (cache.time >= currentPeriod * 1 weeks + 1 weeks) {
cache.time = uint32(currentPeriod * 1 weeks + 1 weeks - 1);
}
Slot0 memory _slot0 = $.slot0;
(, cache.secondsPerLiquidityCumulativeX128) = observeSingle(
$.observations,
cache.time,
0,
_slot0.tick,
_slot0.observationIndex,
$.liquidity,
_slot0.observationCardinality
);
} else {
cache.secondsPerLiquidityCumulativeX128 = $.periods[period].endSecondsPerLiquidityPeriodX128;
}
return
cache.secondsPerLiquidityCumulativeX128 -
snapshot.secondsPerLiquidityOutsideLowerX128 -
snapshot.secondsPerLiquidityOutsideUpperX128;
} else {
return snapshot.secondsPerLiquidityOutsideUpperX128 - snapshot.secondsPerLiquidityOutsideLowerX128;
}
}
}
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0;
/// @title Safe casting methods
/// @notice Contains methods for safely casting between types
library SafeCast {
/// @notice Cast a uint256 to a uint160, revert on overflow
/// @param y The uint256 to be downcasted
/// @return z The downcasted integer, now type uint160
function toUint160(uint256 y) internal pure returns (uint160 z) {
require((z = uint160(y)) == y);
}
/// @notice Cast a int256 to a int128, revert on overflow or underflow
/// @param y The int256 to be downcasted
/// @return z The downcasted integer, now type int128
function toInt128(int256 y) internal pure returns (int128 z) {
require((z = int128(y)) == y);
}
/// @notice Cast a uint256 to a int256, revert on overflow
/// @param y The uint256 to be casted
/// @return z The casted integer, now type int256
function toInt256(uint256 y) internal pure returns (int256 z) {
require(y < 2 ** 255);
z = int256(y);
}
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.26;
import {SafeCast} from './SafeCast.sol';
import {TickMath} from './TickMath.sol';
import {TickInfo} from './PoolStorage.sol';
/// @title Tick
/// @notice Contains functions for managing tick processes and relevant calculations
library Tick {
error LO();
using SafeCast for int256;
/// @notice Derives max liquidity per tick from given tick spacing
/// @dev Executed within the pool constructor
/// @param tickSpacing The amount of required tick separation, realized in multiples of `tickSpacing`
/// e.g., a tickSpacing of 3 requires ticks to be initialized every 3rd tick i.e., ..., -6, -3, 0, 3, 6, ...
/// @return The max liquidity per tick
function tickSpacingToMaxLiquidityPerTick(int24 tickSpacing) internal pure returns (uint128) {
unchecked {
int24 minTick = (TickMath.MIN_TICK / tickSpacing) * tickSpacing;
int24 maxTick = (TickMath.MAX_TICK / tickSpacing) * tickSpacing;
uint24 numTicks = uint24((maxTick - minTick) / tickSpacing) + 1;
return type(uint128).max / numTicks;
}
}
/// @notice Retrieves fee growth data
/// @param self The mapping containing all tick information for initialized ticks
/// @param tickLower The lower tick boundary of the position
/// @param tickUpper The upper tick boundary of the position
/// @param tickCurrent The current tick
/// @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0
/// @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1
/// @return feeGrowthInside0X128 The all-time fee growth in token0, per unit of liquidity, inside the position's tick boundaries
/// @return feeGrowthInside1X128 The all-time fee growth in token1, per unit of liquidity, inside the position's tick boundaries
function getFeeGrowthInside(
mapping(int24 => TickInfo) storage self,
int24 tickLower,
int24 tickUpper,
int24 tickCurrent,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128
) internal view returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) {
unchecked {
TickInfo storage lower = self[tickLower];
TickInfo storage upper = self[tickUpper];
/// @dev calculate fee growth below
uint256 feeGrowthBelow0X128;
uint256 feeGrowthBelow1X128;
if (tickCurrent >= tickLower) {
feeGrowthBelow0X128 = lower.feeGrowthOutside0X128;
feeGrowthBelow1X128 = lower.feeGrowthOutside1X128;
} else {
feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128;
feeGrowthBelow1X128 = feeGrowthGlobal1X128 - lower.feeGrowthOutside1X128;
}
/// @dev calculate fee growth above
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;
}
}
/// @notice Updates a tick and returns true if the tick was flipped from initialized to uninitialized, or vice versa
/// @param self The mapping containing all tick information for initialized ticks
/// @param tick The tick that will be updated
/// @param tickCurrent The current tick
/// @param liquidityDelta A new amount of liquidity to be added (subtracted) when tick is crossed from left to right (right to left)
/// @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0
/// @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1
/// @param secondsPerLiquidityCumulativeX128 The all-time seconds per max(1, liquidity) of the pool
/// @param tickCumulative The tick * time elapsed since the pool was first initialized
/// @param time The current block timestamp cast to a uint32
/// @param upper true for updating a position's upper tick, or false for updating a position's lower tick
/// @param maxLiquidity The maximum liquidity allocation for a single tick
/// @return flipped Whether the tick was flipped from initialized to uninitialized, or vice versa
function update(
mapping(int24 => TickInfo) storage self,
int24 tick,
int24 tickCurrent,
int128 liquidityDelta,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128,
uint160 secondsPerLiquidityCumulativeX128,
int56 tickCumulative,
uint32 time,
bool upper,
uint128 maxLiquidity
) internal returns (bool flipped) {
TickInfo storage info = self[tick];
uint128 liquidityGrossBefore = info.liquidityGross;
uint128 liquidityGrossAfter = liquidityDelta < 0
? liquidityGrossBefore - uint128(-liquidityDelta)
: liquidityGrossBefore + uint128(liquidityDelta);
if (liquidityGrossAfter > maxLiquidity) revert LO();
flipped = (liquidityGrossAfter == 0) != (liquidityGrossBefore == 0);
if (liquidityGrossBefore == 0) {
/// @dev by convention, we assume that all growth before a tick was initialized happened _below_ the tick
if (tick <= tickCurrent) {
info.feeGrowthOutside0X128 = feeGrowthGlobal0X128;
info.feeGrowthOutside1X128 = feeGrowthGlobal1X128;
info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128;
info.tickCumulativeOutside = tickCumulative;
info.secondsOutside = time;
}
info.initialized = true;
}
info.liquidityGross = liquidityGrossAfter;
/// @dev when the lower (upper) tick is crossed left to right (right to left), liquidity must be added (removed)
info.liquidityNet = upper ? info.liquidityNet - liquidityDelta : info.liquidityNet + liquidityDelta;
}
/// @notice Clears tick data
/// @param self The mapping containing all initialized tick information for initialized ticks
/// @param tick The tick that will be cleared
/// @param period The period the tick was cleared on
function clear(mapping(int24 => TickInfo) storage self, int24 tick, uint256 period) internal {
delete self[tick].periodSecondsPerLiquidityOutsideX128[period];
delete self[tick];
}
/// @notice Transitions to next tick as needed by price movement
/// @param self The mapping containing all tick information for initialized ticks
/// @param tick The destination tick of the transition
/// @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0
/// @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1
/// @param secondsPerLiquidityCumulativeX128 The current seconds per liquidity
/// @param tickCumulative The tick * time elapsed since the pool was first initialized
/// @param time The current block.timestamp
/// @return liquidityNet The amount of liquidity added (subtracted) when tick is crossed from left to right (right to left)
function cross(
mapping(int24 => TickInfo) storage self,
int24 tick,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128,
uint160 secondsPerLiquidityCumulativeX128,
int56 tickCumulative,
uint32 time,
uint256 endSecondsPerLiquidityPeriodX128,
int24 periodStartTick
) internal returns (int128 liquidityNet) {
unchecked {
TickInfo storage info = self[tick];
uint256 period = time / 1 weeks;
info.feeGrowthOutside0X128 = feeGrowthGlobal0X128 - info.feeGrowthOutside0X128;
info.feeGrowthOutside1X128 = feeGrowthGlobal1X128 - info.feeGrowthOutside1X128;
info.secondsPerLiquidityOutsideX128 =
secondsPerLiquidityCumulativeX128 -
info.secondsPerLiquidityOutsideX128;
info.tickCumulativeOutside = tickCumulative - info.tickCumulativeOutside;
info.secondsOutside = time - info.secondsOutside;
liquidityNet = info.liquidityNet;
uint256 periodSecondsPerLiquidityOutsideX128;
uint256 periodSecondsPerLiquidityOutsideBeforeX128 = info.periodSecondsPerLiquidityOutsideX128[period];
if (tick <= periodStartTick && periodSecondsPerLiquidityOutsideBeforeX128 == 0) {
periodSecondsPerLiquidityOutsideX128 =
secondsPerLiquidityCumulativeX128 -
/* periodSecondsPerLiquidityOutsideBeforeX128 - */
endSecondsPerLiquidityPeriodX128;
} else {
periodSecondsPerLiquidityOutsideX128 =
secondsPerLiquidityCumulativeX128 -
periodSecondsPerLiquidityOutsideBeforeX128;
}
info.periodSecondsPerLiquidityOutsideX128[period] = periodSecondsPerLiquidityOutsideX128;
}
}
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.26;
import {BitMath} from './BitMath.sol';
/// @title Packed tick initialized state library
/// @notice Stores a packed mapping of tick index to its initialized state
/// @dev The mapping uses int16 for keys since ticks are represented as int24 and there are 256 (2^8) values per word.
library TickBitmap {
/// @notice Computes the position in the mapping where the initialized bit for a tick lives
/// @param tick The tick for which to compute the position
/// @return wordPos The key in the mapping containing the word in which the bit is stored
/// @return bitPos The bit position in the word where the flag is stored
function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) {
unchecked {
wordPos = int16(tick >> 8);
bitPos = uint8(int8(tick % 256));
}
}
/// @notice Flips the initialized state for a given tick from false to true, or vice versa
/// @param self The mapping in which to flip the tick
/// @param tick The tick to flip
/// @param tickSpacing The spacing between usable ticks
function flipTick(mapping(int16 => uint256) storage self, int24 tick, int24 tickSpacing) internal {
unchecked {
/// @dev ensure that the tick is spaced
require(tick % tickSpacing == 0);
(int16 wordPos, uint8 bitPos) = position(tick / tickSpacing);
uint256 mask = 1 << bitPos;
self[wordPos] ^= mask;
}
}
/// @notice Returns the next initialized tick contained in the same word (or adjacent word) as the tick that is either
/// to the left (less than or equal to) or right (greater than) of the given tick
/// @param self The mapping in which to compute the next initialized tick
/// @param tick The starting tick
/// @param tickSpacing The spacing between usable ticks
/// @param lte Whether to search for the next initialized tick to the left (less than or equal to the starting tick)
/// @return next The next initialized or uninitialized tick up to 256 ticks away from the current tick
/// @return initialized Whether the next tick is initialized, as the function only searches within up to 256 ticks
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--; /// @dev round towards negative infinity
if (lte) {
(int16 wordPos, uint8 bitPos) = position(compressed);
/// @dev all the 1s at or to the right of the current bitPos
uint256 mask = (1 << bitPos) - 1 + (1 << bitPos);
uint256 masked = self[wordPos] & mask;
/// @dev if there are no initialized ticks to the right of or at the current tick, return rightmost in the word
initialized = masked != 0;
/// @dev overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick
next = initialized
? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing
: (compressed - int24(uint24(bitPos))) * tickSpacing;
} else {
/// @dev start from the word of the next tick, since the current tick state doesn't matter
(int16 wordPos, uint8 bitPos) = position(compressed + 1);
/// @dev all the 1s at or to the left of the bitPos
uint256 mask = ~((1 << bitPos) - 1);
uint256 masked = self[wordPos] & mask;
/// @dev if there are no initialized ticks to the left of the current tick, return leftmost in the word
initialized = masked != 0;
/// @dev overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick
next = initialized
? (compressed + 1 + int24(uint24(BitMath.leastSignificantBit(masked) - bitPos))) * tickSpacing
: (compressed + 1 + int24(uint24(type(uint8).max - bitPos))) * tickSpacing;
}
}
}
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.26;
struct Slot0 {
/// @dev the current price
uint160 sqrtPriceX96;
/// @dev the current tick
int24 tick;
/// @dev the most-recently updated index of the observations array
uint16 observationIndex;
/// @dev the current maximum number of observations that are being stored
uint16 observationCardinality;
/// @dev the next maximum number of observations to store, triggered in observations.write
uint16 observationCardinalityNext;
/// @dev the current protocol fee as a percentage of the swap fee taken on withdrawal
/// @dev represented as an integer denominator (1/x)%
uint8 feeProtocol;
/// @dev whether the pool is locked
bool unlocked;
}
struct Observation {
/// @dev the block timestamp of the observation
uint32 blockTimestamp;
/// @dev the tick accumulator, i.e. tick * time elapsed since the pool was first initialized
int56 tickCumulative;
/// @dev the seconds per liquidity, i.e. seconds elapsed / max(1, liquidity) since the pool was first initialized
uint160 secondsPerLiquidityCumulativeX128;
/// @dev whether or not the observation is initialized
bool initialized;
}
struct RewardInfo {
/// @dev used to account for changes in the deposit amount
int256 secondsDebtX96;
/// @dev used to check if starting seconds have already been written
bool initialized;
/// @dev used to account for changes in secondsPerLiquidity
int160 secondsPerLiquidityPeriodStartX128;
}
/// @dev info stored for each user's position
struct PositionInfo {
/// @dev the amount of liquidity owned by this position
uint128 liquidity;
/// @dev fee growth per unit of liquidity as of the last update to liquidity or fees owed
uint256 feeGrowthInside0LastX128;
uint256 feeGrowthInside1LastX128;
/// @dev the fees owed to the position owner in token0/token1
uint128 tokensOwed0;
uint128 tokensOwed1;
mapping(uint256 => RewardInfo) periodRewardInfo;
}
/// @dev info stored for each initialized individual tick
struct TickInfo {
/// @dev the total position liquidity that references this tick
uint128 liquidityGross;
/// @dev amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left),
int128 liquidityNet;
/// @dev fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick)
/// @dev only has relative meaning, not absolute — the value depends on when the tick is initialized
uint256 feeGrowthOutside0X128;
uint256 feeGrowthOutside1X128;
/// @dev the cumulative tick value on the other side of the tick
int56 tickCumulativeOutside;
/// @dev the seconds per unit of liquidity on the _other_ side of this tick (relative to the current tick)
/// @dev only has relative meaning, not absolute — the value depends on when the tick is initialized
uint160 secondsPerLiquidityOutsideX128;
/// @dev the seconds spent on the other side of the tick (relative to the current tick)
/// @dev only has relative meaning, not absolute — the value depends on when the tick is initialized
uint32 secondsOutside;
/// @dev true iff the tick is initialized, i.e. the value is exactly equivalent to the expression liquidityGross != 0
/// @dev these 8 bits are set to prevent fresh sstores when crossing newly initialized ticks
bool initialized;
/// @dev secondsPerLiquidityOutsideX128 separated into periods, placed here to preserve struct slots
mapping(uint256 => uint256) periodSecondsPerLiquidityOutsideX128;
}
/// @dev info stored for each period
struct PeriodInfo {
uint32 previousPeriod;
int24 startTick;
int24 lastTick;
uint160 endSecondsPerLiquidityPeriodX128;
}
/// @dev accumulated protocol fees in token0/token1 units
struct ProtocolFees {
uint128 token0;
uint128 token1;
}
/// @dev Position period and liquidity
struct PositionCheckpoint {
uint256 period;
uint256 liquidity;
}
library PoolStorage {
/// @dev keccak256(abi.encode(uint256(keccak256("pool.storage")) - 1)) & ~bytes32(uint256(0xff));
bytes32 public constant POOL_STORAGE_LOCATION = 0xf047b0c59244a0faf8e48cb6b6fde518e6717176152b6dd953628cd9dccb2800;
/// @custom꞉storage‑location erc7201꞉pool.storage
struct PoolState {
Slot0 slot0;
uint24 fee;
uint256 feeGrowthGlobal0X128;
uint256 feeGrowthGlobal1X128;
ProtocolFees protocolFees;
uint128 liquidity;
mapping(int24 => TickInfo) _ticks;
mapping(int16 => uint256) tickBitmap;
mapping(bytes32 => PositionInfo) positions;
Observation[65535] observations;
mapping(uint256 => PeriodInfo) periods;
uint256 lastPeriod;
mapping(bytes32 => PositionCheckpoint[]) positionCheckpoints;
bool initialized;
address nfpManager;
}
/// @dev Return state storage struct for reading and writing
function getStorage() internal pure returns (PoolState storage $) {
assembly {
$.slot := POOL_STORAGE_LOCATION
}
}
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.26;
/// @title Math library for computing sqrt prices from ticks and vice versa
/// @notice Computes sqrt price for ticks of size 1.0001, i.e. sqrt(1.0001^tick) as fixed point Q64.96 numbers. Supports
/// prices between 2**-128 and 2**128
library TickMath {
error T();
error R();
/// @dev The minimum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**-128
int24 internal constant MIN_TICK = -887272;
/// @dev The maximum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**128
int24 internal constant MAX_TICK = -MIN_TICK;
/// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK)
uint160 internal constant MIN_SQRT_RATIO = 4295128739;
/// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK)
uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;
/// @notice Calculates sqrt(1.0001^tick) * 2^96
/// @dev Throws if |tick| > max tick
/// @param tick The input tick for the above formula
/// @return sqrtPriceX96 A Fixed point Q64.96 number representing the sqrt of the ratio of the two assets (token1/token0)
/// at the given tick
function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) {
unchecked {
uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick));
if (absTick > uint256(int256(MAX_TICK))) revert T();
uint256 ratio = absTick & 0x1 != 0
? 0xfffcb933bd6fad37aa2d162d1a594001
: 0x100000000000000000000000000000000;
if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128;
if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128;
if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128;
if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128;
if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128;
if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128;
if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128;
if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128;
if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128;
if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128;
if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128;
if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128;
if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128;
if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128;
if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128;
if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128;
if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128;
if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128;
if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128;
if (tick > 0) ratio = type(uint256).max / ratio;
/// @dev this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96.
/// @dev we then downcast because we know the result always fits within 160 bits due to our tick input constraint
/// @dev we round up in the division so getTickAtSqrtRatio of the output price is always consistent
sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1));
}
}
/// @notice Calculates the greatest tick value such that getRatioAtTick(tick) <= ratio
/// @dev Throws in case sqrtPriceX96 < MIN_SQRT_RATIO, as MIN_SQRT_RATIO is the lowest value getRatioAtTick may
/// ever return.
/// @param sqrtPriceX96 The sqrt ratio for which to compute the tick as a Q64.96
/// @return tick The greatest tick for which the ratio is less than or equal to the input ratio
function getTickAtSqrtRatio(uint160 sqrtPriceX96) internal pure returns (int24 tick) {
unchecked {
/// @dev second inequality must be < because the price can never reach the price at the max tick
if (!(sqrtPriceX96 >= MIN_SQRT_RATIO && sqrtPriceX96 < MAX_SQRT_RATIO)) revert R();
uint256 ratio = uint256(sqrtPriceX96) << 32;
uint256 r = ratio;
uint256 msb = 0;
assembly {
let f := shl(7, gt(r, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := shl(6, gt(r, 0xFFFFFFFFFFFFFFFF))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := shl(5, gt(r, 0xFFFFFFFF))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := shl(4, gt(r, 0xFFFF))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := shl(3, gt(r, 0xFF))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := shl(2, gt(r, 0xF))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := shl(1, gt(r, 0x3))
msb := or(msb, f)
r := shr(f, r)
}
assembly {
let f := gt(r, 0x1)
msb := or(msb, f)
}
if (msb >= 128) r = ratio >> (msb - 127);
else r = ratio << (127 - msb);
int256 log_2 = (int256(msb) - 128) << 64;
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(63, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(62, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(61, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(60, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(59, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(58, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(57, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(56, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(55, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(54, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(53, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(52, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(51, f))
r := shr(f, r)
}
assembly {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(50, f))
}
int256 log_sqrt10001 = log_2 * 255738958999603826347141; /// @dev 128.128 number
int24 tickLow = int24((log_sqrt10001 - 3402992956809132418596140100660247210) >> 128);
int24 tickHi = int24((log_sqrt10001 + 291339464771989622907027621153398088495) >> 128);
tick = tickLow == tickHi
? tickLow
: getSqrtRatioAtTick(tickHi) <= sqrtPriceX96
? tickHi
: tickLow;
}
}
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.26;
/// @title BitMath
/// @dev This library provides functionality for computing bit properties of an unsigned integer
library BitMath {
/// @notice Returns the index of the most significant bit of the number,
/// where the least significant bit is at index 0 and the most significant bit is at index 255
/// @dev The function satisfies the property:
/// x >= 2**mostSignificantBit(x) and x < 2**(mostSignificantBit(x)+1)
/// @param x the value for which to compute the most significant bit, must be greater than 0
/// @return r the index of the most significant bit
function mostSignificantBit(uint256 x) internal pure returns (uint8 r) {
require(x > 0);
unchecked {
if (x >= 0x100000000000000000000000000000000) {
x >>= 128;
r += 128;
}
if (x >= 0x10000000000000000) {
x >>= 64;
r += 64;
}
if (x >= 0x100000000) {
x >>= 32;
r += 32;
}
if (x >= 0x10000) {
x >>= 16;
r += 16;
}
if (x >= 0x100) {
x >>= 8;
r += 8;
}
if (x >= 0x10) {
x >>= 4;
r += 4;
}
if (x >= 0x4) {
x >>= 2;
r += 2;
}
if (x >= 0x2) r += 1;
}
}
/// @notice Returns the index of the least significant bit of the number,
/// where the least significant bit is at index 0 and the most significant bit is at index 255
/// @dev The function satisfies the property:
/// (x & 2**leastSignificantBit(x)) != 0 and (x & (2**(leastSignificantBit(x)) - 1)) == 0)
/// @param x the value for which to compute the least significant bit, must be greater than 0
/// @return r the index of the least significant bit
function leastSignificantBit(uint256 x) internal pure returns (uint8 r) {
require(x > 0);
unchecked {
r = 255;
if (x & type(uint128).max > 0) {
r -= 128;
} else {
x >>= 128;
}
if (x & type(uint64).max > 0) {
r -= 64;
} else {
x >>= 64;
}
if (x & type(uint32).max > 0) {
r -= 32;
} else {
x >>= 32;
}
if (x & type(uint16).max > 0) {
r -= 16;
} else {
x >>= 16;
}
if (x & type(uint8).max > 0) {
r -= 8;
} else {
x >>= 8;
}
if (x & 0xf > 0) {
r -= 4;
} else {
x >>= 4;
}
if (x & 0x3 > 0) {
r -= 2;
} else {
x >>= 2;
}
if (x & 0x1 > 0) r -= 1;
}
}
}