// 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 {Math} from "@openzeppelin/utils/math/Math.sol"; import {ABDKMath64x64} from "@abdk/ABDKMath64x64.sol"; import "./interfaces/IWETH9.sol"; import {Harberg} from "./Harberg.sol"; import {Optimizer} from "./Optimizer.sol"; import {VWAPTracker} from "./VWAPTracker.sol"; /** * @title LiquidityManager for Harberg Token on Uniswap V3 * @notice Manages liquidity provisioning on Uniswap V3 for the Harberg token by maintaining three distinct positions: * - Floor Position: Ensures a minimum price support by having enough reserve assets to potentially buy back the circulating supply of Harberg. * - Anchor Position: Provides liquidity around the current market price to facilitate trading and maintain market stability. * - Discovery Position: Expands liquidity by minting new Harberg tokens as the price rises, capturing potential growth in the ecosystem. * The contract dynamically adjusts these positions in response to market movements to maintain strategic liquidity levels and support the Harberg token's price. * It also collects and transfers fees generated from trading activities to a designated fee destination. * @dev Utilizes Uniswap V3's concentrated liquidity feature, enabling highly efficient use of capital. */ contract LiquidityManager is VWAPTracker { using Math for uint256; // the minimum granularity of liquidity positions in the Uniswap V3 pool. this is a 1% pool. int24 internal constant TICK_SPACING = 200; // DISCOVERY_SPACING determines the range above the current price where new tokens are minted and sold. // 11000 ticks represent 3x the current price int24 internal constant DISCOVERY_SPACING = 11000; // how much more liquidity per tick discovery is holding over anchor uint128 internal constant MIN_DISCOVERY_DEPTH = 200; // 500 // 500% // only working with UNI V3 1% fee tier pools uint24 internal constant FEE = uint24(10_000); // used to double-check price with uni oracle uint32 internal constant PRICE_STABILITY_INTERVAL = 300; // 5 minutes in seconds int24 internal constant MAX_TICK_DEVIATION = 50; // how much is that? // the address of the Uniswap V3 factory address private immutable factory; IWETH9 private immutable weth; Harberg private immutable harb; Optimizer private immutable optimizer; IUniswapV3Pool private immutable pool; bool private immutable token0isWeth; PoolKey private poolKey; address private recenterAccess; // the 3 positions this contract is managing enum Stage { FLOOR, ANCHOR, DISCOVERY } struct TokenPosition { // the liquidity of the position uint128 liquidity; int24 tickLower; int24 tickUpper; } mapping(Stage => TokenPosition) public positions; // the address where liquidity fees will be sent address public feeDestination; error ZeroAddressInSetter(); error AddressAlreadySet(); event EthScarcity(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, int24 vwapTick); event EthAbundance(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, int24 vwapTick); /// @dev Function modifier to ensure that the caller is the feeDestination modifier onlyFeeDestination() { require(msg.sender == address(feeDestination), "only callable by feeDestination"); _; } /// @notice Creates a liquidity manager for managing Harberg token liquidity on Uniswap V3. /// @param _factory The address of the Uniswap V3 factory. /// @param _WETH9 The address of the WETH contract for handling ETH in trades. /// @param _harb The address of the Harberg token contract. /// @dev Computes the Uniswap pool address for the Harberg-WETH pair and sets up the initial configuration for the liquidity manager. constructor(address _factory, address _WETH9, address _harb, address _optimizer) { factory = _factory; weth = IWETH9(_WETH9); poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE); pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); harb = Harberg(_harb); token0isWeth = _WETH9 < _harb; optimizer = Optimizer(_optimizer); } /// @notice Callback function that Uniswap V3 calls for liquidity actions requiring minting or burning of tokens. /// @param amount0Owed The amount of token0 owed for the liquidity provision. /// @param amount1Owed The amount of token1 owed for the liquidity provision. /// @dev This function mints Harberg tokens as needed and handles WETH deposits for ETH conversions during liquidity interactions. function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { CallbackValidation.verifyCallback(factory, poolKey); // take care of harb uint256 harbPulled = token0isWeth ? amount1Owed : amount0Owed; harb.mint(harbPulled); // 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); } /// @notice Sets the address to which trading fees are transferred. /// @param feeDestination_ The address that will receive the collected trading fees. /// @dev Can only be called once to set the fee destination, further attempts will revert. function setFeeDestination(address feeDestination_) external { if (address(0) == feeDestination_) revert ZeroAddressInSetter(); if (feeDestination != address(0)) revert AddressAlreadySet(); feeDestination = feeDestination_; } function setRecenterAccess(address addr) external onlyFeeDestination { recenterAccess = addr; } function revokeRecenterAccess() external onlyFeeDestination { recenterAccess = address(0); } receive() external payable {} /// @notice Calculates the Uniswap V3 tick corresponding to a given price ratio between Harberg and ETH. /// @param t0isWeth Boolean flag indicating if token0 is WETH. /// @param tokenAmount Amount of the Harberg token. /// @param ethAmount Amount of Ethereum. /// @return tick_ The calculated tick for the given price ratio. function tickAtPrice(bool t0isWeth, uint256 tokenAmount, uint256 ethAmount) internal pure returns (int24 tick_) { require(ethAmount > 0, "ETH amount cannot be zero"); if (tokenAmount == 0) { // HARB/ETH tick_ = TickMath.MAX_TICK; } 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 priceRatioX64 = ABDKMath64x64.div(int128(int256(tokenAmount)), int128(int256(ethAmount))); // HARB/ETH tick_ = tickAtPriceRatio(priceRatioX64); } // convert to tick in a pool tick_ = t0isWeth ? tick_ : -tick_; } function tickAtPriceRatio(int128 priceRatioX64) internal pure returns (int24 tick_) { // Convert the price ratio into a sqrt price in the format expected by Uniswap's TickMath. uint160 sqrtPriceX96 = uint160(int160(ABDKMath64x64.sqrt(priceRatioX64) << 32)); tick_ = TickMath.getTickAtSqrtRatio(sqrtPriceX96); } /// @notice Calculates the price ratio from a given Uniswap V3 tick as HARB/ETH. /// @param tick The tick for which to calculate the price ratio. /// @return priceRatioX96 The price ratio corresponding to the given tick. function priceAtTick(int24 tick) private pure returns (uint256 priceRatioX96) { //tick = (tick < 0) ? -tick : tick; uint256 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick); priceRatioX96 = sqrtRatioX96.mulDiv(sqrtRatioX96, (1 << 96)); } /// @notice Internal function to mint liquidity positions in the Uniswap V3 pool. /// @param stage The liquidity stage (floor, anchor, discovery) being adjusted. /// @param tickLower The lower bound of the tick range for the position. /// @param tickUpper The upper bound of the tick range for the position. /// @param liquidity The amount of liquidity to mint at the specified range. 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}); } /// @notice Clamps tick to valid range and aligns to tick spacing /// @param tick The tick to clamp /// @return clampedTick The clamped and aligned tick function _clampToTickSpacing(int24 tick) internal pure returns (int24 clampedTick) { // Align to tick spacing first clampedTick = tick / TICK_SPACING * TICK_SPACING; // Ensure tick is within valid bounds (this should rarely be needed due to extreme price checks) if (clampedTick < TickMath.MIN_TICK) clampedTick = TickMath.MIN_TICK; if (clampedTick > TickMath.MAX_TICK) clampedTick = TickMath.MAX_TICK; } /// @notice Internal function to set or adjust the floor, anchor, and discovery positions based on current market conditions and the manager's strategy. /// @param currentTick The current market tick. /// @dev Recalculates and realigns all liquidity positions according to the latest market data and strategic requirements. function _set( int24 currentTick, uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth ) internal { uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))); // this enforces an floor liquidity share of 75% to 95 %; uint256 floorEthBalance = (19 * ethBalance / 20) - (2 * anchorShare * ethBalance / 10 ** 19); // set Anchor position uint256 pulledHarb; // this enforces a anchor range of 1% to 100% of the price int24 anchorSpacing = TICK_SPACING + (34 * int24(anchorWidth) * TICK_SPACING / 100); { int24 tickLower = _clampToTickSpacing(currentTick - anchorSpacing); int24 tickUpper = _clampToTickSpacing(currentTick + anchorSpacing); uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(currentTick); uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); uint256 anchorEthBalance = ethBalance - floorEthBalance; uint128 anchorLiquidity; if (token0isWeth) { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, anchorEthBalance); pulledHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, anchorLiquidity); } else { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, anchorEthBalance); pulledHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, anchorLiquidity); } _mint(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity); } currentTick = currentTick / TICK_SPACING * TICK_SPACING; // set Discovery position uint256 discoveryAmount; { int24 tickLower = _clampToTickSpacing( token0isWeth ? currentTick - DISCOVERY_SPACING - anchorSpacing : currentTick + anchorSpacing ); int24 tickUpper = _clampToTickSpacing( token0isWeth ? currentTick - anchorSpacing : currentTick + DISCOVERY_SPACING + anchorSpacing ); uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); discoveryDepth = MIN_DISCOVERY_DEPTH + (4 * discoveryDepth * MIN_DISCOVERY_DEPTH / 10 ** 18); discoveryAmount = pulledHarb * uint24(DISCOVERY_SPACING) * uint24(discoveryDepth) / uint24(anchorSpacing) / 100; uint128 liquidity; if (token0isWeth) { liquidity = LiquidityAmounts.getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, discoveryAmount); } else { liquidity = LiquidityAmounts.getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, discoveryAmount); } _mint(Stage.DISCOVERY, tickLower, tickUpper, liquidity); harb.burn(harb.balanceOf(address(this))); } // set Floor position { int24 vwapTick; uint256 outstandingSupply = harb.outstandingSupply(); outstandingSupply -= pulledHarb; outstandingSupply -= (outstandingSupply >= discoveryAmount) ? discoveryAmount : outstandingSupply; uint256 vwapX96 = getAdjustedVWAP(capitalInefficiency); uint256 requiredEthForBuyback = 0; if (vwapX96 > 0) { requiredEthForBuyback = outstandingSupply.mulDiv(vwapX96, (1 << 96)); } // make a new calculation of the vwapTick, having updated outstandingSupply if (floorEthBalance < requiredEthForBuyback) { // not enough ETH, find a lower price requiredEthForBuyback = floorEthBalance; uint256 balancedCapital = (7 * outstandingSupply / 10) + (outstandingSupply * capitalInefficiency / 10 ** 18); vwapTick = tickAtPrice(token0isWeth, balancedCapital, requiredEthForBuyback); emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, vwapTick); } else if (vwapX96 == 0) { requiredEthForBuyback = floorEthBalance; vwapTick = currentTick; } else { // ETH/HARB tick vwapTick = tickAtPriceRatio(int128(int256(vwapX96 >> 32))); // convert to pool tick vwapTick = token0isWeth ? -vwapTick : vwapTick; emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, vwapTick); } // move floor below anchor, if needed if (token0isWeth) { vwapTick = (vwapTick < currentTick + anchorSpacing) ? currentTick + anchorSpacing : vwapTick; } else { vwapTick = (vwapTick > currentTick - anchorSpacing) ? currentTick - anchorSpacing : vwapTick; } // normalize tick position for pool vwapTick = _clampToTickSpacing(vwapTick); // calculate liquidity uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(vwapTick); int24 floorTick = _clampToTickSpacing(token0isWeth ? vwapTick + TICK_SPACING : vwapTick - TICK_SPACING); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorTick); floorEthBalance = (address(this).balance + weth.balanceOf(address(this))); uint128 liquidity; if (token0isWeth) { liquidity = LiquidityAmounts.getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, floorEthBalance); } else { liquidity = LiquidityAmounts.getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, floorEthBalance); } _mint(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity); } } 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)) { // the historic archor position is only an approximation for the price int24 tick = position.tickLower + (position.tickUpper - position.tickLower / 2); currentPrice = priceAtTick(token0isWeth ? -1 * tick : tick); } } } // Transfer fees to the fee destination // and record transaction totals if (fee0 > 0) { if (token0isWeth) { IERC20(address(weth)).transfer(feeDestination, fee0); _recordVolumeAndPrice(currentPrice, fee0); } 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); _recordVolumeAndPrice(currentPrice, fee1); } } } function _isPriceStable(int24 currentTick) internal view returns (bool) { uint32[] memory secondsAgo = new uint32[](2); secondsAgo[0] = PRICE_STABILITY_INTERVAL; // 5 minutes ago secondsAgo[1] = 0; // current block timestamp int56 tickCumulativeDiff; int24 averageTick; try pool.observe(secondsAgo) returns (int56[] memory tickCumulatives, uint160[] memory) { tickCumulativeDiff = tickCumulatives[1] - tickCumulatives[0]; averageTick = int24(tickCumulativeDiff / int56(int32(PRICE_STABILITY_INTERVAL))); } catch { // try with a higher timeframe secondsAgo[0] = PRICE_STABILITY_INTERVAL * 200; (int56[] memory tickCumulatives,) = pool.observe(secondsAgo); tickCumulativeDiff = tickCumulatives[1] - tickCumulatives[0]; averageTick = int24(tickCumulativeDiff / int56(int32(PRICE_STABILITY_INTERVAL))); } return (currentTick >= averageTick - MAX_TICK_DEVIATION && currentTick <= averageTick + MAX_TICK_DEVIATION); } /// @notice Adjusts liquidity positions in response to an increase or decrease in the Harberg token's price. /// @dev This function should be called when significant price movement is detected. It recalibrates the liquidity ranges to align with the new market conditions. function recenter() external returns (bool isUp) { // Fetch the current tick from the Uniswap V3 pool (, int24 currentTick,,,,,) = pool.slot0(); if (recenterAccess != address(0)) { require(msg.sender == recenterAccess, "access denied"); } else { // check slippage with oracle require(_isPriceStable(currentTick), "price deviated from oracle"); } isUp = false; // check how price moved if (positions[Stage.ANCHOR].liquidity > 0) { // get the anchor position 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); uint256 minAmplitude = uint24(TICK_SPACING) * 2; // Determine the correct comparison direction based on token0isWeth isUp = token0isWeth ? currentTick < centerTick : currentTick > centerTick; bool isEnough = SignedMath.abs(currentTick - centerTick) > minAmplitude; // Check Conditions require(isEnough, "amplitude not reached."); } // take out all old positions _scrape(); if (isUp) { harb.setPreviousTotalSupply(harb.totalSupply()); } try optimizer.getLiquidityParams() returns ( uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth ) { capitalInefficiency = (capitalInefficiency > 10 ** 18) ? 10 ** 18 : capitalInefficiency; anchorShare = (anchorShare > 10 ** 18) ? 10 ** 18 : anchorShare; anchorWidth = (anchorWidth > 100) ? 100 : anchorWidth; discoveryDepth = (discoveryDepth > 10 ** 18) ? 10 ** 18 : discoveryDepth; // set new positions _set(currentTick, capitalInefficiency, anchorShare, anchorWidth, discoveryDepth); } catch { // set new positions with default, average parameters _set(currentTick, 5 * 10 ** 17, 5 * 10 ** 17, 5 * 10, 5 * 10 ** 17); } } }