230 lines
9.8 KiB
Solidity
230 lines
9.8 KiB
Solidity
|
|
// 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);
|
||
|
|
}
|
||
|
|
}
|