// 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(uint256 => 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(uint256 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 ); (uint256 amount0, uint256 amount1) = 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(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 outstanding() public view returns (uint256 _outstanding) { harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this)); } function ethInAnchor() public view returns (uint256 _ethInAnchor) { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(positions[Stage.ANCHOR].tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(positions[Stage.ANCHOR].tickUpper); if (token0isWeth) { _ethInAnchor = LiquidityAmounts.getAmount0ForLiquidity( sqrtRatioAX96, sqrtRatioBX96, positions[Stage.ANCHOR].liquidity ); } else { _ethInAnchor = LiquidityAmounts.getAmount1ForLiquidity( sqrtRatioAX96, sqrtRatioBX96, positions[Stage.ANCHOR].liquidity ); } } /// @dev Calculates cycle tick from cycle price. function tickAtPrice(uint256 price) internal view returns (int24 tick_) { tick_ = TickMath.getTickAtSqrtRatio( uint160(int160(ABDKMath64x64.sqrt(int128(int256(price << 64))) << 32)) ); tick_ = tick_ / TICK_SPACING * TICK_SPACING; tick_ = token0isWeth ? tick_ : -tick_; } // 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 = anchor.tickLower; int24 anchorTickUpper = 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; if (token0isWeth) { require(currentTick > centerTick, "call slide(), not shift()"); // ### check price moved more than 15% of range require(currentTick > amplitudeTick, "amplitude not reached, come back later"); } else { // ### check price moved up enough and add hysteresis require(currentTick < centerTick, "call slide(), not shift()"); // ### check price moved more than 15% of range require(currentTick < amplitudeTick, "amplitude not reached, come back later"); } } // ## scrape positions uint256 ethAmountAnchor; for (uint256 i=Stage.FLOOR; i <= Stage.DISCOVERY; i++) { position = positions[i]; (uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity); if (i == ANCHOR) { ethAmountAnchor = token0isWeth ? amount0 : amount1; } } // TODO: handle fees // ## set new positions // move 10% of new ETH into Floor ethAmountAnchor -= (ethAmountAnchor - ethInAnchor()) * 10 / LIQUIDITY_RATIO_DIVISOR; // ### set Floor position uint256 anchorLiquidity; { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(currentTick - 300); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + 300); if (token0isWeth) { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0( sqrtRatioAX96, sqrtRatioBX96, ethAmountAnchor ); } else { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1( sqrtRatioAX96, sqrtRatioBX96, ethAmountAnchor ); } (uint256 amount0, uint256 amount1) = pool.mint(address(this), currentTick - 300, currentTick + 300, liquidity, abi.encode(poolKey)); bytes32 positionKey = PositionKey.compute(address(this), currentTick - 300, currentTick + 300); (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); positions[Stage.ANCHOR] = TokenPosition({ liquidity: anchorLiquidity, tickLower: currentTick - 300, tickUpper: currentTick + 300, feeGrowthInside0LastX128: feeGrowthInside0LastX128, feeGrowthInside1LastX128: feeGrowthInside1LastX128 }); } // ### set Floor position { uint256 ethAmountFloor = address(this).balance; int24 floorTick = tickAtPrice(outstanding() / ethAmountFloor); floorTick -= (currentTick - floorTick); uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorTick); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - 300); uint256 liquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, token0isWeth ? ethAmountFloor : 0, token0isWeth ? 0: ethAmountFloor ); pool.mint( address(this), floorTick, currentTick - 300, liquidity, abi.encode(poolKey) ); bytes32 positionKey = PositionKey.compute(address(this), floorTick, currentTick - 300); (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); positions[Stage.FLOOR] = TokenPosition({ liquidity: liquidity, tickLower: floorTick, tickUpper: currentTick - 300, feeGrowthInside0LastX128: feeGrowthInside0LastX128, feeGrowthInside1LastX128: feeGrowthInside1LastX128 }); } // ##set discovery position { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(currentTick + 300); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + 11300); // discovery with 1.5 times as much liquidity per tick as anchor uint256 liquidity = anchorLiquidity * 55 / 2; uint256 harbInDiscovery; if (token0isWeth) { harbInDiscovery = TickMath.getAmount1ForLiquidity( sqrtRatioAX96, sqrtRatioBX96, liquidity ); } else { harbInDiscovery = TickMath.getAmount0ForLiquidity( sqrtRatioAX96, sqrtRatioBX96, liquidity ); } pool.mint( address(this), currentTick + 300, currentTick + 11300, liquidity, abi.encode(poolKey) ); bytes32 positionKey = PositionKey.compute(address(this), currentTick + 300, currentTick + 11300); (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); positions[Stage.DISCOVERY] = TokenPosition({ liquidity: liquidity, tickLower: floorTick, tickUpper: currentTick - 300, feeGrowthInside0LastX128: feeGrowthInside0LastX128, feeGrowthInside1LastX128: feeGrowthInside1LastX128 }); } } function slide() external { // check price moved down // check price moved down enough // scrape positions // get outstanding upply and capacity // set new positions // burn harb } }