// 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 {LiquidityAmounts} from "@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 "@openzeppelin/utils/math/SignedMath.sol"; import {ABDKMath64x64} from "@abdk/ABDKMath64x64.sol"; import "./interfaces/IWETH9.sol"; import {Harb} from "./Harb.sol"; /** * @title LiquidityManager - A contract that implements an automated market making strategy. * It maintains 3 positions: * - The floor position guarantees the capacity needed to maintain a minimum price of the HARB token It is a very tight liquidity range with enough reserve assets to buy back the circulating supply. * - The anchor range provides liquidity around the current market price, ensuring liquid trading conditions for the token, regardless of the market environment. * - The discovery range starts 1000 ticks above the current market price and increases from there. It consists solely of unissued tokens, which are sold as the market price increases. * The liquidity surplus obtained from selling tokens in the discovery range is directed back into the floor and anchor positions. */ contract BaseLineLP { int24 internal constant TICK_SPACING = 200; int24 internal constant ANCHOR_SPACING = 5 * TICK_SPACING; int24 internal constant DISCOVERY_SPACING = 11000; int24 internal constant MAX_TICK_DEVIATION = 50; // how much is that? // default fee of 1% uint24 internal constant FEE = uint24(10_000); uint160 internal constant MIN_SQRT_RATIO = 4295128739; uint256 internal constant ANCHOR_LIQ_SHARE = 5; // 5% uint256 internal constant CAPITAL_INEFFICIENCY = 120; // 20% enum Stage { FLOOR, ANCHOR, DISCOVERY } // the address of the Uniswap V3 factory address private immutable factory; IWETH9 private immutable weth; Harb private immutable harb; IUniswapV3Pool private immutable pool; bool private immutable token0isWeth; PoolKey private poolKey; struct TokenPosition { // the liquidity of the position uint128 liquidity; int24 tickLower; int24 tickUpper; } // State variables to track total ETH spent uint256 public cumulativeVolumeWeightedPrice; uint256 public cumulativeVolume; mapping(Stage => TokenPosition) public positions; address public feeDestination; error ZeroAddressInSetter(); error AddressAlreadySet(); // TODO: add events 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); // take care of harb harb.mint(token0isWeth ? amount1Owed : amount0Owed); // pack ETH uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed; if (weth.balanceOf(address(this)) < ethOwed) { weth.deposit{value: address(this).balance}(); } // do transfers if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed); if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed); } function setFeeDestination(address feeDestination_) external { if (address(0) == feeDestination_) revert ZeroAddressInSetter(); if (feeDestination != address(0)) revert AddressAlreadySet(); feeDestination = feeDestination_; } //TODO: what to do with stuck funds if slide/shift become inoperable? receive() external payable { } function tickAtPrice(bool t0isWeth, uint256 tokenAmount, uint256 ethAmount) internal pure 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) ); } tick_ = TickMath.getTickAtSqrtRatio(sqrtPriceX96); tick_ = t0isWeth ? tick_ : -tick_; } function tickToPrice(int24 tick) public pure returns (uint256 priceRatio) { uint160 sqrtRatio = TickMath.getSqrtRatioAtTick(tick); uint256 adjustedSqrtRatio = uint256(sqrtRatio) / (1 << 48); priceRatio = adjustedSqrtRatio * adjustedSqrtRatio; } function _mint(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal { // create position pool.mint( address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey) ); // put into storage positions[stage] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper }); } function _set(uint160 sqrtPriceX96, int24 currentTick) internal { // ### set Floor position int24 vwapTick; { uint256 outstandingSupply = harb.outstandingSupply(); uint256 vwap = 0; uint256 requiredEthForBuyback = 0; if (cumulativeVolume > 0) { vwap = cumulativeVolumeWeightedPrice / cumulativeVolume; requiredEthForBuyback = outstandingSupply * 10**18 / vwap; } uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))); // leave at least ANCHOR_LIQ_SHARE% of supply for anchor ethBalance = ethBalance * (100 - ANCHOR_LIQ_SHARE) / 100; if (ethBalance < requiredEthForBuyback) { // not enough ETH, find a lower price requiredEthForBuyback = ethBalance; outstandingSupply = outstandingSupply * CAPITAL_INEFFICIENCY / 100; vwapTick = tickAtPrice(token0isWeth, outstandingSupply , requiredEthForBuyback); } else if (vwap == 0) { requiredEthForBuyback = ethBalance; vwapTick = currentTick; } else { vwap = cumulativeVolumeWeightedPrice * CAPITAL_INEFFICIENCY / 100 / cumulativeVolume; // in harb/eth vwapTick = tickAtPrice(token0isWeth, token0isWeth ? vwap : 10**18, token0isWeth ? 10**18 : vwap); vwapTick = token0isWeth ? vwapTick : -vwapTick; if (requiredEthForBuyback < ethBalance) { // invest a majority of the ETH still in floor, even though not needed requiredEthForBuyback = (requiredEthForBuyback + (5 * ethBalance)) / 6; } } // move floor below anchor, if needed if (token0isWeth) { vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick; } else { vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick; } // normalize tick position for pool vwapTick = vwapTick / TICK_SPACING * TICK_SPACING; // calculate liquidity uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(vwapTick); int24 floorTick = token0isWeth ? vwapTick + TICK_SPACING: vwapTick - TICK_SPACING; uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorTick); uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, token0isWeth ? requiredEthForBuyback : 0, token0isWeth ? 0 : requiredEthForBuyback ); // mint _mint(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity); } // ### set Anchor position uint128 anchorLiquidity; { int24 tickLower = token0isWeth ? currentTick - ANCHOR_SPACING : vwapTick; int24 tickUpper = token0isWeth ? vwapTick : currentTick + ANCHOR_SPACING; tickLower = tickLower / TICK_SPACING * TICK_SPACING; tickUpper = tickUpper / TICK_SPACING * TICK_SPACING; uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(currentTick); uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))) * 98 / 100; anchorLiquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, token0isWeth ? ethBalance : 10**30, token0isWeth ? 10**30: ethBalance ); _mint(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity); } currentTick = currentTick / TICK_SPACING * TICK_SPACING; // ## set Discovery position { int24 tickLower = token0isWeth ? currentTick - DISCOVERY_SPACING - ANCHOR_SPACING : currentTick + ANCHOR_SPACING; int24 tickUpper = token0isWeth ? currentTick - ANCHOR_SPACING : currentTick + DISCOVERY_SPACING + 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.getAmount0ForLiquidity( sqrtRatioAX96, sqrtRatioBX96, liquidity ); } else { harbInDiscovery = LiquidityAmounts.getAmount1ForLiquidity( sqrtRatioAX96, sqrtRatioBX96, liquidity ); } harb.mint(harbInDiscovery); _mint(Stage.DISCOVERY, tickLower, tickUpper, liquidity); harb.burn(harb.balanceOf(address(this))); } } function _scrape() internal { uint256 fee0 = 0; uint256 fee1 = 0; uint256 currentPrice; for (uint256 i=uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) { TokenPosition storage position = positions[Stage(i)]; if (position.liquidity > 0) { (uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity); // Collect the maximum possible amounts which include fees (uint256 collected0, uint256 collected1) = pool.collect( address(this), position.tickLower, position.tickUpper, type(uint128).max, // Collect the max uint128 value, effectively trying to collect all type(uint128).max ); // Calculate the fees fee0 += collected0 - amount0; fee1 += collected1 - amount1; if (i == uint256(Stage.ANCHOR)) { int24 tick = token0isWeth ? -1 * (position.tickLower + ANCHOR_SPACING): position.tickUpper - ANCHOR_SPACING; currentPrice = tickToPrice(tick); } } } // Transfer fees to the fee destination // and record transaction totals if (fee0 > 0) { if (token0isWeth) { IERC20(address(weth)).transfer(feeDestination, fee0); uint256 volume = fee0 * 100; uint256 volumeWeightedPrice = currentPrice * volume; cumulativeVolumeWeightedPrice += volumeWeightedPrice; cumulativeVolume += volume; } else { IERC20(address(harb)).transfer(feeDestination, fee0); } } if (fee1 > 0) { if (token0isWeth) { IERC20(address(harb)).transfer(feeDestination, fee1); } else { IERC20(address(weth)).transfer(feeDestination, fee1); uint256 volume = fee1 * 100; uint256 volumeWeightedPrice = currentPrice * volume; cumulativeVolumeWeightedPrice += volumeWeightedPrice; cumulativeVolume += volume; } } } function _isPriceStable(int24 currentTick) internal view returns (bool) { uint32 timeInterval = 300; // 5 minutes in seconds uint32[] memory secondsAgo = new uint32[](2); secondsAgo[0] = timeInterval; // 5 minutes ago secondsAgo[1] = 0; // current block timestamp //(int56[] memory tickCumulatives,) = pool.observe(secondsAgo); int56 tickCumulativeDiff; int24 averageTick; try pool.observe(secondsAgo) returns (int56[] memory tickCumulatives, uint160[] memory) { tickCumulativeDiff = tickCumulatives[1] - tickCumulatives[0]; averageTick = int24(tickCumulativeDiff / int56(int32(timeInterval))); // Process the data } catch { // TODO: Handle the error, possibly by trying with a different time interval or providing a default response return true; } return (currentTick >= averageTick - MAX_TICK_DEVIATION && currentTick <= averageTick + MAX_TICK_DEVIATION); } // call this function when price has moved up x% // TODO: write a bot that calls this function regularly 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(); // check slippage with oracle require(_isPriceStable(currentTick), "price deviated from 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; int24 centerTick = token0isWeth ? anchorTickLower + ANCHOR_SPACING : anchorTickUpper - ANCHOR_SPACING; uint256 minAmplitude = uint256(uint24((anchorTickUpper - anchorTickLower) * 3 / 20)); // Determine the correct comparison direction based on token0isWeth bool isUp = token0isWeth ? currentTick < centerTick : currentTick > centerTick; bool isEnough = SignedMath.abs(currentTick - centerTick) > minAmplitude; // Check Conditions require(isEnough, "amplitude not reached, come back later!"); require(isUp, "call slide(), not shift()"); } // ## scrape positions _scrape(); harb.setPreviousTotalSupply(harb.totalSupply()); _set(sqrtPriceX96, currentTick); } function slide() external { // Fetch the current tick from the Uniswap V3 pool (uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0(); // check slippage with oracle require(_isPriceStable(currentTick), "price deviated from 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 = token0isWeth ? anchorTickLower + ANCHOR_SPACING : anchorTickUpper - ANCHOR_SPACING; uint256 minAmplitude = uint256(uint24((anchorTickUpper - anchorTickLower) * 3 / 20)); // Determine the correct comparison direction based on token0isWeth bool isDown = token0isWeth ? currentTick > centerTick : currentTick < centerTick; bool isEnough = SignedMath.abs(currentTick - centerTick) > minAmplitude; // Check Conditions require(isEnough, "amplitude not reached, diamond hands!"); require(isDown, "call shift(), not slide()"); } _scrape(); _set(sqrtPriceX96, currentTick); } }