// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.20; import {BloodX} from 'src/BloodX.sol'; import '@uniswap/v3-core/contracts/libraries/TickMath.sol'; import '@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol'; import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol'; import '@uniswap/v3-periphery/contracts/libraries/PositionKey.sol'; import '@uniswap/v3-core/contracts/libraries/FixedPoint128.sol'; import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; /** * @title LiquidityManager - A contract that supports the $bloodX ecosystem. It * protects the communities liquidity while allowing a manager role to * take strategic liqudity positions. */ contract LiquidityManager { // default fee of 1% uint24 constant FEE = uint24(10_000); // the address of the Uniswap V3 factory address public immutable factory; // the address of WETH9 address public immutable WETH9; struct AddLiquidityParams { address token0; address token1; int24 tickLower; int24 tickUpper; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; uint256 deadline; } struct DecreaseLiquidityParams { address token0; address token1; int24 tickLower; int24 tickUpper; uint128 liquidity; uint256 amount0Min; uint256 amount1Min; uint256 deadline; } struct TokenPosition { // the liquidity of the position uint128 liquidity; // the fee growth of the aggregate position as of the last action on the individual position uint256 feeGrowthInside0LastX128; uint256 feeGrowthInside1LastX128; } // uint256 = uint128 tokensOwed + uint128 ethOwed mapping(address => uint256) private _feesOwed; /// @dev The token ID position data mapping(bytes26 => TokenPosition) private _positions; modifier checkDeadline(uint256 deadline) { require(block.timestamp <= deadline, 'Transaction too old'); _; } /// @notice Emitted when liquidity is increased for a position /// @param liquidity The amount by which liquidity for the NFT position was increased /// @param amount0 The amount of token0 that was paid for the increase in liquidity /// @param amount1 The amount of token1 that was paid for the increase in liquidity event IncreaseLiquidity(address indexed token, uint128 liquidity, uint256 amount0, uint256 amount1); /// @notice Emitted when liquidity is decreased for a position /// @param liquidity The amount by which liquidity for the NFT position was decreased /// @param amount0 The amount of token0 that was accounted for the decrease in liquidity /// @param amount1 The amount of token1 that was accounted for the decrease in liquidity event DecreaseLiquidity(address indexed token, uint128 liquidity, uint256 amount0, uint256 amount1); constructor( address _factory, address _WETH9 ) { factory = _factory; WETH9 = _WETH9; } function getToken(address token0, address token1) internal view returns (bool token0isWeth, address token) { token0isWeth = (token0 == WETH9); require(token0isWeth || token1 == WETH9, "token error"); token = (token0isWeth) ? token1 : token0; } function getFeesOwed(address token) public view returns (uint128 tokensOwed, uint128 ethOwed) { uint256 feesOwed = _feesOwed[token]; return (uint128(feesOwed >> 128), uint128(feesOwed)); } function updateFeesOwed(bool token0isEth, address token, uint128 tokensOwed0, uint128 tokensOwed1) internal { (uint128 tokensOwed, uint128 ethOwed) = getFeesOwed(token); tokensOwed += token0isEth ? tokensOwed1 : tokensOwed0; ethOwed += token0isEth ? tokensOwed0 : tokensOwed1; _feesOwed[token] = uint256(tokensOwed) << 128 + ethOwed; } function posKey(address token, int24 tickLower, int24 tickUpper) internal pure returns (bytes26 _posKey) { bytes memory _posKeyBytes = abi.encodePacked(token, tickLower, tickUpper); assembly { _posKey := mload(add(_posKeyBytes, 26)) } } function positions(address token, int24 tickLower, int24 tickUpper) external view returns ( uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128 ) { TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)]; return ( position.liquidity, position.feeGrowthInside0LastX128, position.feeGrowthInside1LastX128 ); } function uniswapV3MintCallback( uint256 amount0Owed, uint256 amount1Owed, bytes calldata data ) external { PoolAddress.PoolKey memory poolKey = abi.decode(data, (PoolAddress.PoolKey)); IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); require(msg.sender == address(pool)); if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed); if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed); } /// @notice Add liquidity to an initialized pool function addLiquidity(AddLiquidityParams memory params) external checkDeadline(params.deadline) returns ( uint128 liquidity, uint256 amount0, uint256 amount1 ) { PoolAddress.PoolKey memory poolKey = PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: FEE}); IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); // compute the liquidity amount { (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper); liquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, params.amount0Desired, params.amount1Desired ); } (amount0, amount1) = pool.mint( address(this), params.tickLower, params.tickUpper, liquidity, abi.encode(poolKey) ); require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check'); bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper); // this is now updated to the current transaction (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); (bool token0isWeth, address token) = getToken(params.token0, params.token1); TokenPosition memory position = _positions[posKey(token, params.tickLower, params.tickUpper)]; if (liquidity == 0) { // create entry position = TokenPosition({ liquidity: liquidity, feeGrowthInside0LastX128: feeGrowthInside0LastX128, feeGrowthInside1LastX128: feeGrowthInside1LastX128 }); } else { // update entry updateFeesOwed(token0isWeth, token, uint128( FullMath.mulDiv( feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, position.liquidity, FixedPoint128.Q128 ) ), uint128( FullMath.mulDiv( feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, position.liquidity, FixedPoint128.Q128 ) )); position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; position.liquidity += liquidity; } emit IncreaseLiquidity(token, liquidity, amount0, amount1); } function decreaseLiquidity(DecreaseLiquidityParams calldata params) external checkDeadline(params.deadline) returns (uint256 amount0, uint256 amount1) { require(params.liquidity > 0); (bool token0isWeth, address token) = getToken(params.token0, params.token1); TokenPosition memory position = _positions[posKey(token, params.tickLower, params.tickUpper)]; uint128 positionLiquidity = position.liquidity; require(positionLiquidity >= params.liquidity); PoolAddress.PoolKey memory poolKey = PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: FEE}); IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); (amount0, amount1) = pool.burn(params.tickLower, params.tickUpper, params.liquidity); require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check'); bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper); // this is now updated to the current transaction (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); updateFeesOwed(token0isWeth, token, uint128(amount0) + uint128( FullMath.mulDiv( feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, positionLiquidity, FixedPoint128.Q128 ) ), uint128(amount1) + uint128( FullMath.mulDiv( feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, positionLiquidity, FixedPoint128.Q128 ) )); position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; // subtraction is safe because we checked positionLiquidity is gte params.liquidity position.liquidity = positionLiquidity - params.liquidity; emit DecreaseLiquidity(token, params.liquidity, amount0, amount1); } }