feat(pool): add concentrated-liquidity pool with swap across ticks

This commit is contained in:
dualfroz committed 2026-08-24 22:40:00 +02:00
1 parent ab61aa0ecf
commit 2eeb91e0f1
1 file changed
+450
+450
View File
@@ -0,0 +1,450 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.36;
import {IErc20} from "./interfaces/IErc20.sol";
import {IFluxClPool} from "./interfaces/IFluxClPool.sol";
import {IFluxMintCallback} from "./interfaces/IFluxMintCallback.sol";
import {IFluxSwapCallback} from "./interfaces/IFluxSwapCallback.sol";
import {TickMath} from "./libraries/TickMath.sol";
import {SqrtPriceMath} from "./libraries/SqrtPriceMath.sol";
import {SwapMath} from "./libraries/SwapMath.sol";
import {FullMath} from "./libraries/FullMath.sol";
import {FixedPoint128} from "./libraries/FixedPoint128.sol";
import {LiquidityMath} from "./libraries/LiquidityMath.sol";
import {SafeCast} from "./libraries/SafeCast.sol";
import {Tick} from "./libraries/Tick.sol";
import {TickBitmap} from "./libraries/TickBitmap.sol";
import {Position} from "./libraries/Position.sol";
import {Oracle} from "./libraries/Oracle.sol";
/// @title Concentrated-liquidity pool: liquidity indexed by ticks, priced in Q64.96 sqrt space.
contract FluxClPool is IFluxClPool {
using SafeCast for uint256;
using SafeCast for int256;
using Tick for mapping(int24 => Tick.Info);
using TickBitmap for mapping(int16 => uint256);
using Position for mapping(bytes32 => Position.Info);
using Position for Position.Info;
using Oracle for Oracle.Observation[65535];
address public immutable factory;
address public immutable token0;
address public immutable token1;
uint24 public immutable fee;
int24 public immutable tickSpacing;
uint128 public immutable maxLiquidityPerTick;
struct Slot0 {
uint160 sqrtPriceX96;
int24 tick;
uint16 observationIndex;
uint16 observationCardinality;
uint16 observationCardinalityNext;
bool unlocked;
}
Slot0 public slot0;
uint256 public feeGrowthGlobal0X128;
uint256 public feeGrowthGlobal1X128;
uint128 public liquidity;
mapping(int24 => Tick.Info) public ticks;
mapping(int16 => uint256) public tickBitmap;
mapping(bytes32 => Position.Info) public positions;
Oracle.Observation[65535] public observations;
modifier lock() {
require(slot0.unlocked, "Flux: LOCKED");
slot0.unlocked = false;
_;
slot0.unlocked = true;
}
constructor(address factoryAddress, address firstToken, address secondToken, uint24 feeAmount, int24 spacing) {
factory = factoryAddress;
token0 = firstToken;
token1 = secondToken;
fee = feeAmount;
tickSpacing = spacing;
maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick(spacing);
}
function _blockTimestamp() internal view returns (uint32) {
return uint32(block.timestamp);
}
function _balance0() private view returns (uint256) {
return IErc20(token0).balanceOf(address(this));
}
function _balance1() private view returns (uint256) {
return IErc20(token1).balanceOf(address(this));
}
function _safeTransfer(address token, address to, uint256 value) private {
(bool success, bytes memory returndata) =
token.call(abi.encodeWithSelector(IErc20.transfer.selector, to, value));
require(success && (returndata.length == 0 || abi.decode(returndata, (bool))), "Flux: TRANSFER_FAILED");
}
function checkTicks(int24 tickLower, int24 tickUpper) private view {
require(tickLower < tickUpper, "Flux: TLU");
require(tickLower >= TickMath.minTick, "Flux: TLM");
require(tickUpper <= TickMath.maxTick, "Flux: TUM");
require(tickLower % tickSpacing == 0, "Flux: TLS");
require(tickUpper % tickSpacing == 0, "Flux: TUS");
}
function initialize(uint160 sqrtPriceX96) external {
require(slot0.sqrtPriceX96 == 0, "Flux: INIT");
int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
(uint16 cardinality, uint16 cardinalityNext) = observations.initialize(_blockTimestamp());
slot0 = Slot0({
sqrtPriceX96: sqrtPriceX96,
tick: tick,
observationIndex: 0,
observationCardinality: cardinality,
observationCardinalityNext: cardinalityNext,
unlocked: true
});
emit Initialize(sqrtPriceX96, tick);
}
struct ModifyPositionParams {
address owner;
int24 tickLower;
int24 tickUpper;
int128 liquidityDelta;
}
function _updatePosition(address owner, int24 tickLower, int24 tickUpper, int128 liquidityDelta, int24 tick)
private
returns (Position.Info storage position)
{
position = positions.get(owner, tickLower, tickUpper);
uint256 feeGrowthGlobal0 = feeGrowthGlobal0X128;
uint256 feeGrowthGlobal1 = feeGrowthGlobal1X128;
bool flippedLower;
bool flippedUpper;
if (liquidityDelta != 0) {
flippedLower = ticks.update(
tickLower, tick, liquidityDelta, feeGrowthGlobal0, feeGrowthGlobal1, false, maxLiquidityPerTick
);
flippedUpper = ticks.update(
tickUpper, tick, liquidityDelta, feeGrowthGlobal0, feeGrowthGlobal1, true, maxLiquidityPerTick
);
if (flippedLower) tickBitmap.flipTick(tickLower, tickSpacing);
if (flippedUpper) tickBitmap.flipTick(tickUpper, tickSpacing);
}
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
ticks.getFeeGrowthInside(tickLower, tickUpper, tick, feeGrowthGlobal0, feeGrowthGlobal1);
position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);
if (liquidityDelta < 0) {
if (flippedLower) ticks.clear(tickLower);
if (flippedUpper) ticks.clear(tickUpper);
}
}
function _modifyPosition(ModifyPositionParams memory params)
private
returns (Position.Info storage position, int256 amount0, int256 amount1)
{
checkTicks(params.tickLower, params.tickUpper);
Slot0 memory slot0Cache = slot0;
position =
_updatePosition(params.owner, params.tickLower, params.tickUpper, params.liquidityDelta, slot0Cache.tick);
if (params.liquidityDelta != 0) {
if (slot0Cache.tick < params.tickLower) {
amount0 = SqrtPriceMath.getAmount0Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
} else if (slot0Cache.tick < params.tickUpper) {
uint128 liquidityBefore = liquidity;
(slot0.observationIndex, slot0.observationCardinality) = observations.write(
slot0Cache.observationIndex,
_blockTimestamp(),
slot0Cache.tick,
liquidityBefore,
slot0Cache.observationCardinality,
slot0Cache.observationCardinalityNext
);
amount0 = SqrtPriceMath.getAmount0Delta(
slot0Cache.sqrtPriceX96, TickMath.getSqrtRatioAtTick(params.tickUpper), params.liquidityDelta
);
amount1 = SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtRatioAtTick(params.tickLower), slot0Cache.sqrtPriceX96, params.liquidityDelta
);
liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta);
} else {
amount1 = SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
}
}
}
function mint(address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data)
external
lock
returns (uint256 amount0, uint256 amount1)
{
require(amount > 0, "Flux: AMOUNT");
(, int256 amount0Int, int256 amount1Int) = _modifyPosition(
ModifyPositionParams({
owner: recipient,
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: SafeCast.toInt128(int256(uint256(amount)))
})
);
amount0 = uint256(amount0Int);
amount1 = uint256(amount1Int);
uint256 balance0Before;
uint256 balance1Before;
if (amount0 > 0) balance0Before = _balance0();
if (amount1 > 0) balance1Before = _balance1();
IFluxMintCallback(msg.sender).fluxMintCallback(amount0, amount1, data);
if (amount0 > 0) require(balance0Before + amount0 <= _balance0(), "Flux: M0");
if (amount1 > 0) require(balance1Before + amount1 <= _balance1(), "Flux: M1");
emit Mint(msg.sender, recipient, tickLower, tickUpper, amount, amount0, amount1);
}
function collect(
address recipient,
int24 tickLower,
int24 tickUpper,
uint128 amount0Requested,
uint128 amount1Requested
) external lock returns (uint128 amount0, uint128 amount1) {
Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper);
amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested;
amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested;
if (amount0 > 0) {
position.tokensOwed0 -= amount0;
_safeTransfer(token0, recipient, amount0);
}
if (amount1 > 0) {
position.tokensOwed1 -= amount1;
_safeTransfer(token1, recipient, amount1);
}
emit Collect(msg.sender, recipient, tickLower, tickUpper, amount0, amount1);
}
function burn(int24 tickLower, int24 tickUpper, uint128 amount)
external
lock
returns (uint256 amount0, uint256 amount1)
{
(Position.Info storage position, int256 amount0Int, int256 amount1Int) = _modifyPosition(
ModifyPositionParams({
owner: msg.sender,
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: -SafeCast.toInt128(int256(uint256(amount)))
})
);
amount0 = uint256(-amount0Int);
amount1 = uint256(-amount1Int);
if (amount0 > 0 || amount1 > 0) {
(position.tokensOwed0, position.tokensOwed1) =
(position.tokensOwed0 + uint128(amount0), position.tokensOwed1 + uint128(amount1));
}
emit Burn(msg.sender, tickLower, tickUpper, amount, amount0, amount1);
}
struct SwapCache {
uint128 liquidityStart;
uint32 blockTimestamp;
}
struct SwapState {
int256 amountSpecifiedRemaining;
int256 amountCalculated;
uint160 sqrtPriceX96;
int24 tick;
uint256 feeGrowthGlobalX128;
uint128 liquidity;
}
struct StepComputations {
uint160 sqrtPriceStartX96;
int24 tickNext;
bool initialized;
uint160 sqrtPriceNextX96;
uint256 amountIn;
uint256 amountOut;
uint256 feeAmount;
}
function swap(
address recipient,
bool zeroForOne,
int256 amountSpecified,
uint160 sqrtPriceLimitX96,
bytes calldata data
) external returns (int256 amount0, int256 amount1) {
require(amountSpecified != 0, "Flux: AS");
Slot0 memory slot0Start = slot0;
require(slot0Start.unlocked, "Flux: LOCKED");
require(
zeroForOne
? sqrtPriceLimitX96 < slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 > TickMath.minSqrtRatio
: sqrtPriceLimitX96 > slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 < TickMath.maxSqrtRatio,
"Flux: SPL"
);
slot0.unlocked = false;
SwapCache memory cache = SwapCache({liquidityStart: liquidity, blockTimestamp: _blockTimestamp()});
bool exactInput = amountSpecified > 0;
SwapState memory state = SwapState({
amountSpecifiedRemaining: amountSpecified,
amountCalculated: 0,
sqrtPriceX96: slot0Start.sqrtPriceX96,
tick: slot0Start.tick,
feeGrowthGlobalX128: zeroForOne ? feeGrowthGlobal0X128 : feeGrowthGlobal1X128,
liquidity: cache.liquidityStart
});
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
StepComputations memory step;
step.sqrtPriceStartX96 = state.sqrtPriceX96;
(step.tickNext, step.initialized) =
tickBitmap.nextInitializedTickWithinOneWord(state.tick, tickSpacing, zeroForOne);
if (step.tickNext < TickMath.minTick) step.tickNext = TickMath.minTick;
else if (step.tickNext > TickMath.maxTick) step.tickNext = TickMath.maxTick;
step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);
(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(
state.sqrtPriceX96,
(zeroForOne ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 : step.sqrtPriceNextX96 > sqrtPriceLimitX96)
? sqrtPriceLimitX96
: step.sqrtPriceNextX96,
state.liquidity,
state.amountSpecifiedRemaining,
fee
);
if (exactInput) {
unchecked {
state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();
}
state.amountCalculated -= step.amountOut.toInt256();
} else {
unchecked {
state.amountSpecifiedRemaining += step.amountOut.toInt256();
}
state.amountCalculated += (step.amountIn + step.feeAmount).toInt256();
}
if (state.liquidity > 0) {
unchecked {
state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.q128, state.liquidity);
}
}
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
if (step.initialized) {
int128 liquidityNet = ticks.cross(
step.tickNext,
zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128,
zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128
);
if (zeroForOne) liquidityNet = -liquidityNet;
state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);
}
state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}
}
if (state.tick != slot0Start.tick) {
(uint16 observationIndex, uint16 observationCardinality) = observations.write(
slot0Start.observationIndex,
cache.blockTimestamp,
slot0Start.tick,
cache.liquidityStart,
slot0Start.observationCardinality,
slot0Start.observationCardinalityNext
);
(slot0.sqrtPriceX96, slot0.tick, slot0.observationIndex, slot0.observationCardinality) =
(state.sqrtPriceX96, state.tick, observationIndex, observationCardinality);
} else {
slot0.sqrtPriceX96 = state.sqrtPriceX96;
}
if (cache.liquidityStart != state.liquidity) liquidity = state.liquidity;
if (zeroForOne) {
feeGrowthGlobal0X128 = state.feeGrowthGlobalX128;
} else {
feeGrowthGlobal1X128 = state.feeGrowthGlobalX128;
}
(amount0, amount1) = zeroForOne == exactInput
? (amountSpecified - state.amountSpecifiedRemaining, state.amountCalculated)
: (state.amountCalculated, amountSpecified - state.amountSpecifiedRemaining);
if (zeroForOne) {
if (amount1 < 0) _safeTransfer(token1, recipient, uint256(-amount1));
uint256 balance0Before = _balance0();
IFluxSwapCallback(msg.sender).fluxSwapCallback(amount0, amount1, data);
require(balance0Before + uint256(amount0) <= _balance0(), "Flux: IIA");
} else {
if (amount0 < 0) _safeTransfer(token0, recipient, uint256(-amount0));
uint256 balance1Before = _balance1();
IFluxSwapCallback(msg.sender).fluxSwapCallback(amount0, amount1, data);
require(balance1Before + uint256(amount1) <= _balance1(), "Flux: IIA");
}
emit Swap(msg.sender, recipient, amount0, amount1, state.sqrtPriceX96, state.liquidity, state.tick);
slot0.unlocked = true;
}
function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external lock {
uint16 previous = slot0.observationCardinalityNext;
uint16 next = observations.grow(previous, observationCardinalityNext);
slot0.observationCardinalityNext = next;
}
function observe(uint32[] calldata secondsAgos)
external
view
returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s)
{
return observations.observe(
_blockTimestamp(), secondsAgos, slot0.tick, slot0.observationIndex, liquidity, slot0.observationCardinality
);
}
}