diff --git a/src/FluxClPool.sol b/src/FluxClPool.sol new file mode 100644 index 0000000..e6e1a15 --- /dev/null +++ b/src/FluxClPool.sol @@ -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 + ); + } +}