// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol"; import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import { IUniswapV3MintCallback } from "@uniswap-v3-core/interfaces/callback/IUniswapV3MintCallback.sol"; import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol"; // ───────────────────────────────────────────────────────────────────────────── // Full-Range LP Strategy // ───────────────────────────────────────────────────────────────────────────── /// @title FullRangeLPStrategy /// @notice Single LP position covering the entire valid tick range (MIN_TICK to MAX_TICK). /// Position is set once at initialization and never rebalanced, giving 100% time-in-range. contract FullRangeLPStrategy is IUniswapV3MintCallback { /// @dev Tick boundaries aligned to 200-spacing for the 1% fee pool int24 public constant TICK_LOWER = -887_200; int24 public constant TICK_UPPER = 887_200; IUniswapV3Pool public immutable pool; IERC20 public immutable token0; IERC20 public immutable token1; bool public immutable token0isWeth; uint128 public liquidity; uint256 public feesCollected0; uint256 public feesCollected1; uint256 public totalBlocks; bool public initialized; constructor(IUniswapV3Pool _pool, IERC20 _weth, IERC20 _token, bool _token0isWeth) { pool = _pool; token0isWeth = _token0isWeth; token0 = _token0isWeth ? _weth : _token; token1 = _token0isWeth ? _token : _weth; } /// @notice Initialize: use current token balances to mint a full-range position. /// Caller must transfer tokens to this contract before calling. function initialize() external { require(!initialized, "already initialized"); initialized = true; uint256 bal0 = token0.balanceOf(address(this)); uint256 bal1 = token1.balanceOf(address(this)); (, int24 currentTick,,,,,) = pool.slot0(); uint160 sqrtCurrent = TickMath.getSqrtRatioAtTick(currentTick); uint160 sqrtLower = TickMath.getSqrtRatioAtTick(TICK_LOWER); uint160 sqrtUpper = TickMath.getSqrtRatioAtTick(TICK_UPPER); liquidity = LiquidityAmounts.getLiquidityForAmounts(sqrtCurrent, sqrtLower, sqrtUpper, bal0, bal1); if (liquidity > 0) { pool.mint(address(this), TICK_LOWER, TICK_UPPER, liquidity, ""); } } /// @notice Record one simulation block. Full-range is always in-range, so this just increments totalBlocks. function recordBlock() external { totalBlocks++; } /// @notice Collect accrued fees without changing liquidity. function collectFees() external { if (liquidity > 0) { pool.burn(TICK_LOWER, TICK_UPPER, 0); } (uint256 f0, uint256 f1) = pool.collect(address(this), TICK_LOWER, TICK_UPPER, type(uint128).max, type(uint128).max); feesCollected0 += f0; feesCollected1 += f1; } /// @notice Burn entire position and collect all tokens + fees. function finalize() external { if (liquidity > 0) { pool.burn(TICK_LOWER, TICK_UPPER, liquidity); (uint256 f0, uint256 f1) = pool.collect(address(this), TICK_LOWER, TICK_UPPER, type(uint128).max, type(uint128).max); feesCollected0 += f0; feesCollected1 += f1; liquidity = 0; } } function getWethBalance() external view returns (uint256) { return token0isWeth ? token0.balanceOf(address(this)) : token1.balanceOf(address(this)); } function getTokenBalance() external view returns (uint256) { return token0isWeth ? token1.balanceOf(address(this)) : token0.balanceOf(address(this)); } /// @inheritdoc IUniswapV3MintCallback function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external override { require(msg.sender == address(pool), "not pool"); if (amount0Owed > 0) token0.transfer(msg.sender, amount0Owed); if (amount1Owed > 0) token1.transfer(msg.sender, amount1Owed); } } // ───────────────────────────────────────────────────────────────────────────── // Fixed-Width LP Strategy // ───────────────────────────────────────────────────────────────────────────── /// @title FixedWidthLPStrategy /// @notice LP position ±2000 ticks around current price (200-tick spacing). /// Rebalances (removes + remints) when price exits the range. contract FixedWidthLPStrategy is IUniswapV3MintCallback { int24 public constant HALF_WIDTH = 2_000; // ±2000 ticks from center int24 public constant TICK_SPACING = 200; int24 public constant MIN_TICK = -887_200; int24 public constant MAX_TICK = 887_200; IUniswapV3Pool public immutable pool; IERC20 public immutable token0; IERC20 public immutable token1; bool public immutable token0isWeth; int24 public tickLower; int24 public tickUpper; uint128 public liquidity; uint256 public rebalanceCount; uint256 public feesCollected0; uint256 public feesCollected1; uint256 public blocksInRange; uint256 public totalBlocks; bool public initialized; constructor(IUniswapV3Pool _pool, IERC20 _weth, IERC20 _token, bool _token0isWeth) { pool = _pool; token0isWeth = _token0isWeth; token0 = _token0isWeth ? _weth : _token; token1 = _token0isWeth ? _token : _weth; } /// @notice Initialize around the current pool price. /// Caller must transfer tokens to this contract before calling. function initialize() external { require(!initialized, "already initialized"); initialized = true; (, int24 currentTick,,,,,) = pool.slot0(); _createPosition(currentTick); } /// @notice Record one simulation block; tracks time-in-range. function recordBlock() external { totalBlocks++; (, int24 currentTick,,,,,) = pool.slot0(); if (currentTick >= tickLower && currentTick < tickUpper) { blocksInRange++; } } /// @notice Rebalance if current price is outside the position range. /// @return didRebalance true if a rebalance occurred. function maybeRebalance() external returns (bool didRebalance) { (, int24 currentTick,,,,,) = pool.slot0(); if (currentTick >= tickLower && currentTick < tickUpper) return false; _removePosition(); _createPosition(currentTick); rebalanceCount++; return true; } /// @notice Collect accrued fees without changing liquidity. function collectFees() external { if (liquidity > 0) { pool.burn(tickLower, tickUpper, 0); } (uint256 f0, uint256 f1) = pool.collect(address(this), tickLower, tickUpper, type(uint128).max, type(uint128).max); feesCollected0 += f0; feesCollected1 += f1; } /// @notice Burn current position and collect everything. function finalize() external { _removePosition(); } function getWethBalance() external view returns (uint256) { return token0isWeth ? token0.balanceOf(address(this)) : token1.balanceOf(address(this)); } function getTokenBalance() external view returns (uint256) { return token0isWeth ? token1.balanceOf(address(this)) : token0.balanceOf(address(this)); } // ── Internal ────────────────────────────────────────────────────────────── function _createPosition(int24 currentTick) internal { int24 center = (currentTick / TICK_SPACING) * TICK_SPACING; tickLower = center - HALF_WIDTH; tickUpper = center + HALF_WIDTH; // Clamp to pool boundaries if (tickLower < MIN_TICK) tickLower = ((MIN_TICK / TICK_SPACING) + 1) * TICK_SPACING; if (tickUpper > MAX_TICK) tickUpper = (MAX_TICK / TICK_SPACING) * TICK_SPACING; uint256 bal0 = token0.balanceOf(address(this)); uint256 bal1 = token1.balanceOf(address(this)); uint160 sqrtCurrent = TickMath.getSqrtRatioAtTick(currentTick); uint160 sqrtLow = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtHigh = TickMath.getSqrtRatioAtTick(tickUpper); liquidity = LiquidityAmounts.getLiquidityForAmounts(sqrtCurrent, sqrtLow, sqrtHigh, bal0, bal1); if (liquidity > 0) { pool.mint(address(this), tickLower, tickUpper, liquidity, ""); } } function _removePosition() internal { if (liquidity > 0) { pool.burn(tickLower, tickUpper, liquidity); (uint256 f0, uint256 f1) = pool.collect(address(this), tickLower, tickUpper, type(uint128).max, type(uint128).max); feesCollected0 += f0; feesCollected1 += f1; liquidity = 0; } } /// @inheritdoc IUniswapV3MintCallback function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external override { require(msg.sender == address(pool), "not pool"); if (amount0Owed > 0) token0.transfer(msg.sender, amount0Owed); if (amount1Owed > 0) token1.transfer(msg.sender, amount1Owed); } }