// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol"; import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol"; import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; /** * @title LiquidityBoundaryHelper * @notice Helper library for calculating safe trade sizes within liquidity boundaries * @dev Prevents trades that would exceed available liquidity and cause SPL errors */ library LiquidityBoundaryHelper { /** * @notice Calculates the ETH required to push price to the outer discovery bound */ function calculateBuyLimit(IUniswapV3Pool pool, ThreePositionStrategy liquidityManager, bool token0isWeth) internal view returns (uint256) { (, int24 currentTick,,,,,) = pool.slot0(); (uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR); (uint128 discoveryLiquidity, int24 discoveryLower, int24 discoveryUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.DISCOVERY); if (anchorLiquidity == 0 && discoveryLiquidity == 0) { return 0; } if (token0isWeth) { return _calculateBuyLimitToken0IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, discoveryLiquidity, discoveryLower, discoveryUpper); } return _calculateBuyLimitToken1IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, discoveryLiquidity, discoveryLower, discoveryUpper); } /** * @notice Calculates the HARB required to push price to the outer floor bound */ function calculateSellLimit(IUniswapV3Pool pool, ThreePositionStrategy liquidityManager, bool token0isWeth) internal view returns (uint256) { (, int24 currentTick,,,,,) = pool.slot0(); (uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR); (uint128 floorLiquidity, int24 floorLower, int24 floorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.FLOOR); if (anchorLiquidity == 0 && floorLiquidity == 0) { return 0; } if (token0isWeth) { return _calculateSellLimitToken0IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, floorLiquidity, floorLower, floorUpper); } return _calculateSellLimitToken1IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, floorLiquidity, floorLower, floorUpper); } function _calculateBuyLimitToken0IsWeth( int24 currentTick, uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper, uint128 discoveryLiquidity, int24 discoveryLower, int24 discoveryUpper ) private pure returns (uint256) { if (discoveryLiquidity == 0) { return type(uint256).max; } int24 targetTick = discoveryUpper > anchorUpper ? discoveryUpper : anchorUpper; if (currentTick >= targetTick) { return 0; } uint256 totalEthNeeded = 0; if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiquidity > 0) { int24 anchorEndTick = targetTick < anchorUpper ? targetTick : anchorUpper; totalEthNeeded += _calculateEthToMoveBetweenTicks(currentTick, anchorEndTick, anchorLiquidity); } if (targetTick > anchorUpper && discoveryLiquidity > 0) { int24 discoveryStartTick = currentTick > discoveryLower ? currentTick : discoveryLower; if (discoveryStartTick < discoveryUpper) { totalEthNeeded += _calculateEthToMoveBetweenTicks(discoveryStartTick, targetTick, discoveryLiquidity); } } return totalEthNeeded; } function _calculateBuyLimitToken1IsWeth( int24 currentTick, uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper, uint128 discoveryLiquidity, int24 discoveryLower, int24 discoveryUpper ) private pure returns (uint256) { if (discoveryLiquidity == 0) { return type(uint256).max; } int24 targetTick = discoveryLower < anchorLower ? discoveryLower : anchorLower; if (currentTick <= targetTick) { return 0; } uint256 totalEthNeeded = 0; if (currentTick <= anchorUpper && currentTick > anchorLower && anchorLiquidity > 0) { int24 anchorEndTick = targetTick > anchorLower ? targetTick : anchorLower; totalEthNeeded += _calculateEthToMoveBetweenTicksDown(currentTick, anchorEndTick, anchorLiquidity); } if (targetTick < anchorLower && discoveryLiquidity > 0) { int24 discoveryStartTick = currentTick < discoveryUpper ? currentTick : discoveryUpper; if (discoveryStartTick > discoveryLower) { totalEthNeeded += _calculateEthToMoveBetweenTicksDown(discoveryStartTick, targetTick, discoveryLiquidity); } } return totalEthNeeded; } function _calculateSellLimitToken0IsWeth( int24 currentTick, uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper, uint128 floorLiquidity, int24 floorLower, int24 floorUpper ) private pure returns (uint256) { if (floorLiquidity == 0) { return type(uint256).max; } int24 targetTick = floorLower < anchorLower ? floorLower : anchorLower; if (currentTick <= targetTick) { return 0; } uint256 totalHarbNeeded = 0; if (currentTick <= anchorUpper && currentTick > anchorLower && anchorLiquidity > 0) { int24 anchorEndTick = targetTick > anchorLower ? targetTick : anchorLower; totalHarbNeeded += _calculateHarbToMoveBetweenTicks(currentTick, anchorEndTick, anchorLiquidity); } if (targetTick < anchorLower && floorLiquidity > 0) { int24 floorStartTick = currentTick < floorUpper ? currentTick : floorUpper; if (floorStartTick > floorLower) { totalHarbNeeded += _calculateHarbToMoveBetweenTicks(floorStartTick, targetTick, floorLiquidity); } } return totalHarbNeeded; } function _calculateSellLimitToken1IsWeth( int24 currentTick, uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper, uint128 floorLiquidity, int24 floorLower, int24 floorUpper ) private pure returns (uint256) { if (floorLiquidity == 0) { return type(uint256).max; } int24 targetTick = floorUpper > anchorUpper ? floorUpper : anchorUpper; if (currentTick >= targetTick) { return 0; } uint256 totalHarbNeeded = 0; if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiquidity > 0) { int24 anchorEndTick = targetTick < anchorUpper ? targetTick : anchorUpper; totalHarbNeeded += _calculateHarbToMoveUpBetweenTicks(currentTick, anchorEndTick, anchorLiquidity); } if (targetTick > anchorUpper && floorLiquidity > 0) { int24 floorStartTick = currentTick > floorLower ? currentTick : floorLower; if (floorStartTick < floorUpper) { totalHarbNeeded += _calculateHarbToMoveUpBetweenTicks(floorStartTick, targetTick, floorLiquidity); } } return totalHarbNeeded; } function _calculateEthToMoveBetweenTicks(int24 fromTick, int24 toTick, uint128 liquidity) private pure returns (uint256) { if (fromTick >= toTick || liquidity == 0) { return 0; } uint160 sqrtPriceFromX96 = TickMath.getSqrtRatioAtTick(fromTick); uint160 sqrtPriceToX96 = TickMath.getSqrtRatioAtTick(toTick); return LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceFromX96, sqrtPriceToX96, liquidity); } function _calculateEthToMoveBetweenTicksDown(int24 fromTick, int24 toTick, uint128 liquidity) private pure returns (uint256) { if (fromTick <= toTick || liquidity == 0) { return 0; } uint160 sqrtPriceFromX96 = TickMath.getSqrtRatioAtTick(fromTick); uint160 sqrtPriceToX96 = TickMath.getSqrtRatioAtTick(toTick); return LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceToX96, sqrtPriceFromX96, liquidity); } function _calculateHarbToMoveBetweenTicks(int24 fromTick, int24 toTick, uint128 liquidity) private pure returns (uint256) { if (fromTick <= toTick || liquidity == 0) { return 0; } uint160 sqrtPriceFromX96 = TickMath.getSqrtRatioAtTick(fromTick); uint160 sqrtPriceToX96 = TickMath.getSqrtRatioAtTick(toTick); return LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceToX96, sqrtPriceFromX96, liquidity); } function _calculateHarbToMoveUpBetweenTicks(int24 fromTick, int24 toTick, uint128 liquidity) private pure returns (uint256) { if (fromTick >= toTick || liquidity == 0) { return 0; } uint160 sqrtPriceFromX96 = TickMath.getSqrtRatioAtTick(fromTick); uint160 sqrtPriceToX96 = TickMath.getSqrtRatioAtTick(toTick); return LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceFromX96, sqrtPriceToX96, liquidity); } }