// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; 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 {ABDKMath64x64} from "@abdk/ABDKMath64x64.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 { int24 constant TICK_SPACING = 200; // default fee of 1% uint24 constant FEE = uint24(10_000); // uint256 constant FLOOR = 0; // uint256 constant ANCHOR = 1; // uint256 constant DISCOVERY = 2; enum Stage { FLOOR, ANCHOR, DISCOVERY } uint256 constant LIQUIDITY_RATIO_DIVISOR = 100; // 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; } mapping(Stage => TokenPosition) 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(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 uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { CallbackValidation.verifyCallback(factory, poolKey); // ## mint harb if needed if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed); if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed); } function createPosition(Stage positionIndex, int24 tickLower, int24 tickUpper, uint256 amount) internal { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); uint128 liquidity = LiquidityAmounts.getLiquidityForAmount1( sqrtRatioAX96, sqrtRatioBX96, amount ); pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey)); // TODO: check slippage // read position and start tracking in storage bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper); (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); positions[positionIndex] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper, feeGrowthInside0LastX128: feeGrowthInside0LastX128, feeGrowthInside1LastX128: feeGrowthInside1LastX128 }); } // called once at the beginning function deployLiquidity(int24 startTick, uint256 amount) external { require(positions[Stage.FLOOR].liquidity == 0, "already set up"); require(positions[Stage.ANCHOR].liquidity == 0, "already set up"); require(positions[Stage.DISCOVERY].liquidity == 0, "already set up"); harb.mint(amount); int24 tickLower; int24 tickUpper; // create floor if (token0isWeth) { tickLower = startTick; tickUpper = startTick + 200; createPosition(Stage.FLOOR, tickLower, tickUpper, amount / 10); } else { tickLower = startTick - 200; tickUpper = startTick; createPosition(Stage.FLOOR, tickLower, tickUpper, amount / 10); } // create anchor if (token0isWeth) { tickLower += 201; tickUpper += 601; createPosition(Stage.ANCHOR, tickLower, tickUpper, amount / 20); } else { tickLower -= 601; tickUpper -= 201; createPosition(Stage.ANCHOR, tickLower, tickUpper, amount / 10); } // create discovery if (token0isWeth) { tickLower += 601; tickUpper += 11001; createPosition(Stage.DISCOVERY, tickLower, tickUpper, harb.balanceOf(address(this))); } else { tickLower -= 11001; tickUpper -= 601; createPosition(Stage.DISCOVERY, tickLower, tickUpper, harb.balanceOf(address(this))); } } function outstanding() public view returns (uint256 _outstanding) { _outstanding = harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this)); } function ethIn(Stage s) public view returns (uint256 _ethInPosition) { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(positions[s].tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(positions[s].tickUpper); if (token0isWeth) { _ethInPosition = LiquidityAmounts.getAmount0ForLiquidity( sqrtRatioAX96, sqrtRatioBX96, positions[s].liquidity ); } else { _ethInPosition = LiquidityAmounts.getAmount1ForLiquidity( sqrtRatioAX96, sqrtRatioBX96, positions[s].liquidity ); } } function tickAtPrice(uint256 tokenAmount, uint256 ethAmount) internal view returns (int24 tick_) { require(ethAmount > 0, "ETH amount cannot be zero"); // Use a fixed-point library or more precise arithmetic for the division here. // For example, using ABDKMath64x64 for a more precise division and square root calculation. int128 priceRatio = ABDKMath64x64.div( int128(int256(tokenAmount)), int128(int256(ethAmount)) ); // Convert the price ratio into a sqrt price in the format expected by Uniswap's TickMath. uint160 sqrtPriceX96 = uint160( int160(ABDKMath64x64.sqrt(priceRatio) << 32) ); // Proceed as before. tick_ = TickMath.getTickAtSqrtRatio(sqrtPriceX96); tick_ = tick_ / TICK_SPACING * TICK_SPACING; tick_ = token0isWeth ? tick_ : -tick_; } function _mint(int24 tickLower, int24 tickUpper, uint128 liquidity) internal { // create position pool.mint( address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey) ); // get fee data bytes32 positionKey = PositionKey.compute( address(this), tickLower, tickUpper ); (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); // put into storage positions[Stage.ANCHOR] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper, feeGrowthInside0LastX128: feeGrowthInside0LastX128, feeGrowthInside1LastX128: feeGrowthInside1LastX128 }); } // call this function when price has moved up 15% function shift() external { // Fetch the current tick from the Uniswap V3 pool (uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0(); // TODO: check slippage with oracle // ## check price moved up { // Check if current tick is within the specified range int24 anchorTickLower = positions[Stage.ANCHOR].tickLower; int24 anchorTickUpper = positions[Stage.ANCHOR].tickUpper; // center tick can be calculated positive and negative numbers the same int24 centerTick = anchorTickLower + ((anchorTickUpper - anchorTickLower) / 2); int24 amplitudeTick = anchorTickLower + (anchorTickUpper - anchorTickLower) * 3 / 20; // Determine the correct comparison direction based on token0isWeth bool isUp = token0isWeth ? currentTick > centerTick : currentTick < centerTick; bool isEnough = token0isWeth ? currentTick > amplitudeTick : currentTick < amplitudeTick; // Check Conditions require(isUp, "call slide(), not shift()"); require(isEnough, "amplitude not reached, come back later"); } // ## scrape positions uint256 ethInAnchor; for (uint256 i=uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) { TokenPosition storage position = positions[Stage(i)]; (uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity); if (i == uint256(Stage.ANCHOR)) { ethInAnchor = token0isWeth ? amount0 : amount1; } } // TODO: handle fees // ## set new positions // reduce Anchor by 10% of new ETH. It will be moved into Floor uint256 initialEthInAnchor = ethIn(Stage.ANCHOR); ethInAnchor -= (ethInAnchor - initialEthInAnchor) * 10 / LIQUIDITY_RATIO_DIVISOR; // ### set Anchor position uint128 anchorLiquidity; { int24 tickLower = currentTick - 300; int24 tickUpper = currentTick + 300; uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); if (token0isWeth) { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0( sqrtRatioAX96, sqrtRatioBX96, ethInAnchor ); } else { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1( sqrtRatioAX96, sqrtRatioBX96, ethInAnchor ); } _mint(tickLower, tickUpper, anchorLiquidity); } // ### set Floor position { int24 startTick = token0isWeth ? currentTick - 301 : currentTick + 301; // all remaining eth will be put into this position uint256 ethInFloor = address(this).balance; // calculate price at which all HARB can be bought back int24 floorTick = tickAtPrice(outstanding(), ethInFloor); // put a position symetrically around the price, startTick being edge on one side floorTick = token0isWeth ? floorTick - (startTick - floorTick) : startTick + (floorTick - startTick); // calculate liquidity uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorTick); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(startTick); uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, token0isWeth ? ethInFloor : 0, token0isWeth ? 0: ethInFloor ); // mint _mint(floorTick, startTick, liquidity); } // ## set Discovery position { int24 tickLower = token0isWeth ? currentTick + 301 : currentTick - 11301; int24 tickUpper = token0isWeth ? currentTick + 11301 : currentTick - 301; uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); // discovery with 1.5 times as much liquidity per tick as anchor // A * 3 11000 A * 55 // D = ----- * ----- = ------ // 2 600 2 uint128 liquidity = anchorLiquidity * 55 / 2; uint256 harbInDiscovery; if (token0isWeth) { harbInDiscovery = LiquidityAmounts.getAmount1ForLiquidity( sqrtRatioAX96, sqrtRatioBX96, liquidity ); } else { harbInDiscovery = LiquidityAmounts.getAmount0ForLiquidity( sqrtRatioAX96, sqrtRatioBX96, liquidity ); } _mint(tickLower, tickUpper, liquidity); } } function slide() external { // check price moved down // check price moved down enough // scrape positions // get outstanding upply and capacity // set new positions // burn harb } }