// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "@aperture/uni-v3-lib/TickMath.sol"; import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; import {Math} from "@openzeppelin/utils/math/Math.sol"; import "../libraries/UniswapMath.sol"; import "../VWAPTracker.sol"; /** * @title ThreePositionStrategy * @notice Abstract contract implementing the three-position liquidity strategy (Floor, Anchor, Discovery) * @dev Provides the core logic for anti-arbitrage asymmetric slippage profile */ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { using Math for uint256; /// @notice Tick spacing for the pool int24 internal constant TICK_SPACING = 200; /// @notice Discovery spacing (3x current price in ticks) int24 internal constant DISCOVERY_SPACING = 11000; /// @notice Minimum discovery depth multiplier uint128 internal constant MIN_DISCOVERY_DEPTH = 200; /// @notice The three liquidity position types enum Stage { FLOOR, ANCHOR, DISCOVERY } /// @notice Structure representing a liquidity position struct TokenPosition { uint128 liquidity; int24 tickLower; int24 tickUpper; } /// @notice Parameters for position strategy struct PositionParams { uint256 capitalInefficiency; uint256 anchorShare; uint24 anchorWidth; uint256 discoveryDepth; } /// @notice Storage for the three positions mapping(Stage => TokenPosition) public positions; /// @notice Events for tracking ETH abundance/scarcity scenarios event EthScarcity(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, int24 vwapTick); event EthAbundance(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, int24 vwapTick); /// @notice Abstract functions that must be implemented by inheriting contracts function _getHarbToken() internal view virtual returns (address); function _getWethToken() internal view virtual returns (address); function _isToken0Weth() internal view virtual returns (bool); function _mintPosition(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal virtual; function _getEthBalance() internal view virtual returns (uint256); function _getOutstandingSupply() internal view virtual returns (uint256); /// @notice Sets all three positions according to the asymmetric slippage strategy /// @param currentTick The current market tick /// @param params Position parameters from optimizer function _setPositions(int24 currentTick, PositionParams memory params) internal { uint256 ethBalance = _getEthBalance(); // Calculate floor ETH allocation (75% to 95% of total) uint256 floorEthBalance = (19 * ethBalance / 20) - (2 * params.anchorShare * ethBalance / 10 ** 19); // Step 1: Set ANCHOR position (shallow liquidity for fast price movement) (uint256 pulledHarb, uint128 anchorLiquidity) = _setAnchorPosition(currentTick, ethBalance - floorEthBalance, params); // Step 2: Set DISCOVERY position (depends on anchor's liquidity) uint256 discoveryAmount = _setDiscoveryPosition(currentTick, anchorLiquidity, params); // Step 3: Set FLOOR position (deep liquidity, uses VWAP for historical memory) _setFloorPosition(currentTick, floorEthBalance, pulledHarb, discoveryAmount, params); } /// @notice Sets the anchor position around current price (shallow liquidity) /// @param currentTick Current market tick /// @param anchorEthBalance ETH allocated to anchor position /// @param params Position parameters /// @return pulledHarb Amount of HARB pulled for this position /// @return anchorLiquidity The liquidity amount for the anchor position function _setAnchorPosition( int24 currentTick, uint256 anchorEthBalance, PositionParams memory params ) internal returns (uint256 pulledHarb, uint128 anchorLiquidity) { // Enforce anchor range of 1% to 100% of the price int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100); int24 tickLower = _clampToTickSpacing(currentTick - anchorSpacing, TICK_SPACING); int24 tickUpper = _clampToTickSpacing(currentTick + anchorSpacing, TICK_SPACING); uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(currentTick); uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); bool token0isWeth = _isToken0Weth(); 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); } _mintPosition(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity); } /// @notice Sets the discovery position (deep edge liquidity) /// @param currentTick Current market tick (normalized to tick spacing) /// @param anchorLiquidity Liquidity amount from anchor position /// @param params Position parameters /// @return discoveryAmount Amount of HARB used for discovery function _setDiscoveryPosition( int24 currentTick, uint128 anchorLiquidity, PositionParams memory params ) internal returns (uint256 discoveryAmount) { currentTick = currentTick / TICK_SPACING * TICK_SPACING; bool token0isWeth = _isToken0Weth(); // Calculate anchor spacing (same as in anchor position) int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100); int24 tickLower = _clampToTickSpacing( token0isWeth ? currentTick - DISCOVERY_SPACING - anchorSpacing : currentTick + anchorSpacing, TICK_SPACING ); int24 tickUpper = _clampToTickSpacing( token0isWeth ? currentTick - anchorSpacing : currentTick + DISCOVERY_SPACING + anchorSpacing, TICK_SPACING ); uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); // Calculate discovery liquidity to ensure X times more liquidity per tick than anchor // Discovery should have 2x to 10x more liquidity per tick (not just total liquidity) uint256 discoveryMultiplier = 200 + (800 * params.discoveryDepth / 10 ** 18); // Calculate anchor width in ticks int24 anchorWidth = 2 * anchorSpacing; // Adjust for width difference: discovery liquidity = anchor liquidity * multiplier * (discovery width / anchor width) uint128 liquidity = uint128( uint256(anchorLiquidity) * discoveryMultiplier * uint256(int256(DISCOVERY_SPACING)) / (100 * uint256(int256(anchorWidth))) ); // Calculate discoveryAmount for floor position calculation if (token0isWeth) { discoveryAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); } else { discoveryAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); } _mintPosition(Stage.DISCOVERY, tickLower, tickUpper, liquidity); } /// @notice Sets the floor position using VWAP for historical price memory (deep edge liquidity) /// @param currentTick Current market tick /// @param floorEthBalance ETH allocated to floor position /// @param pulledHarb HARB amount from anchor position /// @param discoveryAmount HARB amount from discovery position /// @param params Position parameters function _setFloorPosition( int24 currentTick, uint256 floorEthBalance, uint256 pulledHarb, uint256 discoveryAmount, PositionParams memory params ) internal { bool token0isWeth = _isToken0Weth(); // Calculate outstanding supply after position minting uint256 outstandingSupply = _getOutstandingSupply(); outstandingSupply -= pulledHarb; outstandingSupply -= (outstandingSupply >= discoveryAmount) ? discoveryAmount : outstandingSupply; // Use VWAP for floor position (historical price memory for dormant whale protection) uint256 vwapX96 = getAdjustedVWAP(params.capitalInefficiency); uint256 ethBalance = _getEthBalance(); int24 vwapTick; if (vwapX96 > 0) { uint256 requiredEthForBuyback = outstandingSupply.mulDiv(vwapX96, (1 << 96)); if (floorEthBalance < requiredEthForBuyback) { // ETH scarcity: not enough ETH to buy back at VWAP price uint256 balancedCapital = (7 * outstandingSupply / 10) + (outstandingSupply * params.capitalInefficiency / 10 ** 18); vwapTick = _tickAtPrice(token0isWeth, balancedCapital, floorEthBalance); emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, vwapTick); } else { // ETH abundance: sufficient ETH reserves vwapTick = _tickAtPriceRatio(int128(int256(vwapX96 >> 32))); vwapTick = token0isWeth ? -vwapTick : vwapTick; emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, vwapTick); } } else { // No VWAP data available, use current tick vwapTick = currentTick; } // Ensure floor doesn't overlap with anchor position int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100); if (token0isWeth) { vwapTick = (vwapTick < currentTick + anchorSpacing) ? currentTick + anchorSpacing : vwapTick; } else { vwapTick = (vwapTick > currentTick - anchorSpacing) ? currentTick - anchorSpacing : vwapTick; } // Normalize and create floor position vwapTick = _clampToTickSpacing(vwapTick, TICK_SPACING); int24 floorTick = _clampToTickSpacing( token0isWeth ? vwapTick + TICK_SPACING : vwapTick - TICK_SPACING, TICK_SPACING ); // Use planned floor ETH balance, but fallback to remaining if insufficient uint256 remainingEthBalance = _getEthBalance(); uint256 actualFloorEthBalance = (remainingEthBalance >= floorEthBalance) ? floorEthBalance : remainingEthBalance; uint128 liquidity; if (token0isWeth) { liquidity = LiquidityAmounts.getLiquidityForAmount1( TickMath.getSqrtRatioAtTick(vwapTick), TickMath.getSqrtRatioAtTick(floorTick), actualFloorEthBalance ); } else { liquidity = LiquidityAmounts.getLiquidityForAmount0( TickMath.getSqrtRatioAtTick(vwapTick), TickMath.getSqrtRatioAtTick(floorTick), actualFloorEthBalance ); } _mintPosition(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity); } }