// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.20; import "@uniswap-v3-periphery/libraries/PositionKey.sol"; import "@uniswap-v3-core/libraries/FixedPoint128.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "@aperture/uni-v3-lib/TickMath.sol"; import "@aperture/uni-v3-lib/LiquidityAmounts.sol"; import "@aperture/uni-v3-lib/PoolAddress.sol"; import "@aperture/uni-v3-lib/CallbackValidation.sol"; import "@openzeppelin/token/ERC20/IERC20.sol"; import "./interfaces/IWETH9.sol"; import {Harb} from "./Harb.sol"; /** * @title LiquidityManager - A contract that supports the harb ecosystem. It * protects the communities liquidity while allowing a manager role to * take strategic liqudity positions. */ contract BaseLineLP { // default fee of 1% uint24 constant FEE = uint24(10_000); // the address of the Uniswap V3 factory address immutable factory; IWETH9 immutable weth; Harb immutable harb; IUniswapV3Pool immutable pool; PoolKey private poolKey; bool immutable token0isWeth; struct TokenPosition { // the liquidity of the position uint128 liquidity; int24 tickLower; int24 tickUpper; // the fee growth of the aggregate position as of the last action on the individual position uint256 feeGrowthInside0LastX128; uint256 feeGrowthInside1LastX128; } TokenPosition public floor; TokenPosition public anchor; TokenPosition public discovery; 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(int24 indexed tickLower, int24 indexed tickUpper, 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 ethReceived The amount of WETH that was accounted for the decrease in liquidity event PositionLiquidated(int24 indexed tickLower, int24 indexed tickUpper, uint128 liquidity, uint256 ethReceived); constructor(address _factory, address _WETH9, address _harb) { factory = _factory; weth = IWETH9(_WETH9); poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE); pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); harb = Harb(_harb); token0isWeth = _WETH9 < _harb; } function posKey(int24 tickLower, int24 tickUpper) internal pure returns (bytes6 _posKey) { bytes memory _posKeyBytes = abi.encodePacked(tickLower, tickUpper); assembly { _posKey := mload(add(_posKeyBytes, 6)) } } function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { CallbackValidation.verifyCallback(factory, poolKey); if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed); if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed); } function createPosition(int24 tickLower, int24 tickUpper, uint256 amount) internal { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); uint128 liquidity = LiquidityAmounts.getLiquidityForAmount1( sqrtRatioAX96, sqrtRatioBX96, amount / 20 ); pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey)); // read position and start tracking in storage bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper); (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); discovery = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper, feeGrowthInside0LastX128: feeGrowthInside0LastX128, feeGrowthInside1LastX128: feeGrowthInside1LastX128 }); } // called once at the beginning function deployLiquidity(int24 startTick, uint256 amount) external { require(floor.liquidity == 0, "already set up"); require(anchor.liquidity == 0, "already set up"); require(discovery.liquidity == 0, "already set up"); harb.mint(amount); int24 tickLower; int24 tickUpper; // create floor if (token0isWeth) { tickLower = startTick; tickUpper = startTick + 200; createPosition(tickLower, tickUpper, amount / 10); } else { tickLower = startTick - 200; tickUpper = startTick; createPosition(tickLower, tickUpper, amount / 10); } // create anchor if (token0isWeth) { tickLower += 201; tickUpper += 601; createPosition(tickLower, tickUpper, amount / 20); } else { tickLower -= 601; tickUpper -= 201; createPosition(tickLower, tickUpper, amount / 10); } // create discovery if (token0isWeth) { tickLower += 601; tickUpper += 11001; createPosition(tickLower, tickUpper, harb.balanceOf(address(this))); } else { tickLower -= 11001; tickUpper -= 601; createPosition(tickLower, tickUpper, harb.balanceOf(address(this))); } } function getInverseAmountsForLiquidity( uint160 sqrtRatioX96, uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity ) internal pure returns (uint256 amount0, uint256 amount1) { if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); if (sqrtRatioX96 <= sqrtRatioAX96) { amount1 = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); } else if (sqrtRatioX96 < sqrtRatioBX96) { amount1 = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioX96, sqrtRatioBX96, liquidity); amount0 = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioX96, liquidity); } else { amount0 = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); } } function calculateCapacity() public view returns (uint256 capacity) { capacity = 0; (, int24 currentTick, , , , , ) = pool.slot0(); uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(currentTick); // handle floor { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floor.tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floor.tickUpper); (uint256 amount0, uint256 amount1) = getInverseAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, floor.liquidity); capacity += token0isWeth ? amount1 : amount0; } // handle anchor { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(anchor.tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(anchor.tickUpper); (uint256 amount0, uint256 amount1) = getInverseAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, anchor.liquidity); capacity += token0isWeth ? amount1 : amount0; } // handle discovery { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(discovery.tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(discovery.tickUpper); (uint256 amount0, uint256 amount1) = getInverseAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, discovery.liquidity); capacity += token0isWeth ? amount1 : amount0; } } }