// 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; int24 constant ANCHOR_SPACING = 5 * TICK_SPACING; // 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; } // for minting limits uint256 private lastDay; uint256 private mintedToday; 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; } event UniCallback(uint256 indexed amount0Owed, uint256 indexed amount1Owed); function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { CallbackValidation.verifyCallback(factory, poolKey); // take care of harb harb.mint(token0isWeth ? amount1Owed : amount0Owed); // pack ETH weth.deposit{value: token0isWeth ? amount0Owed : amount1Owed}(); // do transfers if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed); if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed); } receive() external payable { } 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 }); } function outstanding() public view returns (uint256 _outstanding) { _outstanding = harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this)); } function spendingLimit() public view returns (uint256, uint256) { return (lastDay, mintedToday); } 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 ); } } uint160 internal constant MIN_SQRT_RATIO = 4295128739; function tickAtPrice(uint256 tokenAmount, uint256 ethAmount) internal view returns (int24 tick_) { require(ethAmount > 0, "ETH amount cannot be zero"); uint160 sqrtPriceX96; if (tokenAmount == 0) { sqrtPriceX96 = MIN_SQRT_RATIO; } else { // 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. 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 }); } /// @dev Returns if amount is within daily limit and resets spentToday after one day. /// @param amount Amount to withdraw. /// @return Returns if amount is under daily limit. function availableMint(uint256 amount) internal returns (uint256) { if (block.timestamp > lastDay + 24 hours) { lastDay = block.timestamp; mintedToday = 0; } uint256 mintLimit = harb.totalSupply() * 3 / 20; if (mintedToday + amount > mintLimit) { return mintLimit - mintedToday; } return amount; } event DEBUG(uint256 indexed eth, uint256 indexed outstanding, int24 indexed floorTick, int24 startTick, bool ethIs0); function _set(uint160 sqrtPriceX96, int24 currentTick, uint256 ethInNewAnchor) internal { // ### set Anchor position uint128 anchorLiquidity; { int24 tickLower = currentTick - ANCHOR_SPACING; int24 tickUpper = currentTick + ANCHOR_SPACING; uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); if (token0isWeth) { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0( sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor ); } else { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1( sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor ); } // TODO: calculate liquidity correctly // or make sure that we don't have to pay more than we have _mint(tickLower, tickUpper, anchorLiquidity * 2); } // ### set Floor position { int24 startTick = token0isWeth ? currentTick + ANCHOR_SPACING : currentTick - ANCHOR_SPACING; // all remaining eth will be put into this position uint256 ethInFloor = address(this).balance; int24 floorTick; // calculate price at which all HARB can be bought back uint256 _outstanding = outstanding(); if (_outstanding > 0) { floorTick = tickAtPrice(_outstanding, ethInFloor); emit DEBUG(ethInFloor,0,floorTick,startTick,token0isWeth); // put a position symetrically around the price, startTick being edge on one side floorTick = token0isWeth ? startTick + (floorTick - startTick) : floorTick - (startTick - floorTick); } else { floorTick = startTick + ((token0isWeth ? int24(1) : int24(-1)) * 400); } // 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 ); emit DEBUG(ethInFloor,uint256(liquidity),floorTick,startTick,token0isWeth); // mint _mint(startTick, floorTick, liquidity); } // ## set Discovery position { int24 tickLower = token0isWeth ? currentTick + ANCHOR_SPACING : currentTick - 11400; int24 tickUpper = token0isWeth ? currentTick + 11400 : currentTick - ANCHOR_SPACING; 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 ); } // manage minting limits of harb here if (harbInDiscovery <= harb.balanceOf(address(this))) { _mint(tickLower, tickUpper, liquidity); harb.burn(harb.balanceOf(address(this))); } else { uint256 amount = availableMint(harbInDiscovery - harb.balanceOf(address(this))); harb.mint(amount); mintedToday += amount; amount = harb.balanceOf(address(this)); if(amount < harbInDiscovery) { // calculate new ticks so that discovery liquidity is still // deeper than anchor, but less wide int24 tickWidth = int24(int256(11000 * amount / harbInDiscovery)) + ANCHOR_SPACING; tickWidth = tickWidth / TICK_SPACING * TICK_SPACING; tickWidth = (tickWidth <= ANCHOR_SPACING) ? tickWidth + TICK_SPACING : tickWidth; tickLower = token0isWeth ? currentTick - tickWidth : currentTick + ANCHOR_SPACING; tickUpper = token0isWeth ? currentTick - ANCHOR_SPACING : currentTick + tickWidth; sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); liquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, token0isWeth ? 0 : amount, token0isWeth ? amount : 0 ); } _mint(tickLower, tickUpper, liquidity); } } } // call this function when price has moved up 15% function shift() external { require(positions[Stage.ANCHOR].liquidity > 0, "Not initialized"); // 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; // cap anchor size at 10 % of total ETH uint256 ethBalance = address(this).balance; ethInAnchor = (ethInAnchor > ethBalance / 10) ? ethBalance / 10 : ethInAnchor; _set(sqrtPriceX96, currentTick, ethInAnchor); } function slide() external { // Fetch the current tick from the Uniswap V3 pool (uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0(); // TODO: check slippage with oracle // ## check price moved down if (positions[Stage.ANCHOR].liquidity > 0) { // 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 isDown = token0isWeth ? currentTick < centerTick : currentTick > centerTick; bool isEnough = token0isWeth ? currentTick < amplitudeTick : currentTick > amplitudeTick; // Check Conditions require(isDown, "call shift(), not slide()"); require(isEnough, "amplitude not reached, diamond hands!"); } // ## scrape positions for (uint256 i=uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) { TokenPosition storage position = positions[Stage(i)]; if (position.liquidity > 0) { pool.burn(position.tickLower, position.tickUpper, position.liquidity); // TODO: handle fees } } uint256 ethBalance = address(this).balance; if (ethBalance == 0) { // TODO: set only discovery return; } uint256 ethInAnchor = ethIn(Stage.ANCHOR); uint256 ethInFloor = ethIn(Stage.FLOOR); // use previous ration of Floor to Anchor uint256 ethInNewAnchor = ethBalance / 10; if (ethInFloor > 0) { ethInNewAnchor = ethBalance * ethInAnchor / (ethInAnchor + ethInFloor); } // but cap anchor size at 10 % of total ETH ethInNewAnchor = (ethInNewAnchor > ethBalance / 10) ? ethBalance / 10 : ethInNewAnchor; emit DEBUG(0, 0, currentTick, currentTick / TICK_SPACING * TICK_SPACING, token0isWeth); currentTick = currentTick / TICK_SPACING * TICK_SPACING; _set(sqrtPriceX96, currentTick, ethInNewAnchor); } }