// 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"; /** * @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 { using Math for uint256; // State variables to track total ETH spent uint256 public cumulativeVolumeWeightedPriceX96; uint256 public cumulativeVolume; // the minimum granularity of liquidity positions in the Uniswap V3 pool. this is a 1% pool. int24 internal constant TICK_SPACING = 200; // defines the width of the anchor position from the current price to discovery position. int24 internal constant ANCHOR_SPACING = 5 * TICK_SPACING; // 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 DISCOVERY_DEPTH = 200; // 500 // 500% // only working with UNI V3 1% fee tier pools uint24 internal constant FEE = uint24(10_000); // ANCHOR_LIQ_SHARE is the mininum share of total ETH in control // that will be left to put into anchor positon. uint256 internal constant MIN_ANCHOR_LIQ_SHARE = 5; // 5 = 5% uint256 internal constant MAX_ANCHOR_LIQ_SHARE = 25; // virtual liabilities that are added to push the calculated floor price down artificially, // creating a security margin for attacks on liquidity uint256 internal constant MIN_CAPITAL_INEFFICIENCY = 70; uint256 internal constant MAX_CAPITAL_INEFFICIENCY = 200; // 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; IUniswapV3Pool private immutable pool; bool private immutable token0isWeth; PoolKey private poolKey; // 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; // the minimum share of ETH that will be put into the anchor uint256 public anchorLiquidityShare; // the higher the inefficiency, the more conservative the positioning of floor uint256 public capitalInfefficiency; error ZeroAddressInSetter(); error AddressAlreadySet(); event EthScarcity(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, uint256 capitalInfefficiency, uint256 anchorLiquidityShare, int24 vwapTick); event EthAbundance(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, uint256 capitalInfefficiency, uint256 anchorLiquidityShare, 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) { factory = _factory; weth = IWETH9(_WETH9); poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE); pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); harb = Harberg(_harb); token0isWeth = _WETH9 < _harb; anchorLiquidityShare = MAX_ANCHOR_LIQ_SHARE; capitalInfefficiency = MIN_CAPITAL_INEFFICIENCY; // starting at 95% fuzzer passes tests } /// @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 setAnchorLiquidityShare(uint256 anchorLiquidityShare_) external onlyFeeDestination { require(anchorLiquidityShare_ >= MIN_ANCHOR_LIQ_SHARE, ""); require(anchorLiquidityShare_ <= MAX_ANCHOR_LIQ_SHARE, ""); anchorLiquidityShare = anchorLiquidityShare_; } function setCapitalInfefficiency(uint256 capitalInfefficiency_) external onlyFeeDestination { require(capitalInfefficiency_ >= MIN_CAPITAL_INEFFICIENCY, ""); require(capitalInfefficiency_ <= MAX_CAPITAL_INEFFICIENCY, ""); capitalInfefficiency = capitalInfefficiency_; } function setMinStakeSupplyFraction(uint256 mssf_) external onlyFeeDestination { harb.setMinStakeSupplyFraction(mssf_); } 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 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) internal { // estimate the lower tick of the anchor int24 vwapTick; uint256 outstandingSupply = harb.outstandingSupply(); uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))); uint256 floorEthBalance = ethBalance * (100 - anchorLiquidityShare) / 100; if (outstandingSupply > 0) { vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInfefficiency / 100 , floorEthBalance); } else { vwapTick = token0isWeth ? currentTick + ANCHOR_SPACING : currentTick - ANCHOR_SPACING; } // move vwapTick below currentTick, if needed if (token0isWeth) { vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick; } else { vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick; } // set Anchor position uint256 pulledHarb; { 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 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 = 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); discoveryAmount = pulledHarb * uint24(DISCOVERY_SPACING) * uint24(DISCOVERY_DEPTH) / uint24(ANCHOR_SPACING) / 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 { outstandingSupply = harb.outstandingSupply(); outstandingSupply -= pulledHarb; outstandingSupply -= (outstandingSupply >= discoveryAmount) ? discoveryAmount : outstandingSupply; uint256 vwapX96 = 0; uint256 requiredEthForBuyback = 0; if (cumulativeVolume > 0) { vwapX96 = cumulativeVolumeWeightedPriceX96 * capitalInfefficiency / 100 / cumulativeVolume; // in harb/eth 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; vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInfefficiency / 100 , requiredEthForBuyback); emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, 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, capitalInfefficiency, anchorLiquidityShare, vwapTick); } // 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); 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 _recordVolumeAndPrice(uint256 currentPriceX96, uint256 fee) internal { // assuming FEE is 1% uint256 volume = fee * 100; uint256 volumeWeightedPriceX96 = currentPriceX96 * volume; // Check for potential overflow. 10**70 is close to 2^256 if (cumulativeVolumeWeightedPriceX96 > 10**70) { uint256 zipFactor = 10**35; uint256 desiredPrecision = 10**5; while (zipFactor * desiredPrecision > cumulativeVolume) { zipFactor /= desiredPrecision; } // Handle overflow: zip historic trade data cumulativeVolumeWeightedPriceX96 = cumulativeVolumeWeightedPriceX96 / zipFactor; // cumulativeVolume should be well higer than zipFactor cumulativeVolume = cumulativeVolume / zipFactor; } cumulativeVolumeWeightedPriceX96 += volumeWeightedPriceX96; cumulativeVolume += volume; } 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 = token0isWeth ? -1 * (position.tickLower + ANCHOR_SPACING): position.tickUpper - ANCHOR_SPACING; currentPrice = priceAtTick(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 { // Fetch the current tick from the Uniswap V3 pool (, int24 currentTick, , , , , ) = pool.slot0(); // check slippage with oracle require(_isPriceStable(currentTick), "price deviated from oracle"); bool 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 = token0isWeth ? anchorTickLower + ANCHOR_SPACING : anchorTickUpper - ANCHOR_SPACING; 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()); } // set new positions _set(currentTick); } }