// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; /** * @title IUniswapV3PositionManager * @dev Interface for Uniswap V3 Position Manager */ interface IUniswapV3PositionManager { struct MintParams { address token0; address token1; uint24 fee; int24 tickLower; int24 tickUpper; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; address recipient; uint256 deadline; } struct IncreaseLiquidityParams { uint256 tokenId; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; uint256 deadline; } struct DecreaseLiquidityParams { uint256 tokenId; uint128 liquidity; uint256 amount0Min; uint256 amount1Min; uint256 deadline; } struct CollectParams { uint256 tokenId; address recipient; uint128 amount0Max; uint128 amount1Max; } function mint(MintParams calldata params) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); function increaseLiquidity(IncreaseLiquidityParams calldata params) external payable returns (uint128 liquidity, uint256 amount0, uint256 amount1); function decreaseLiquidity(DecreaseLiquidityParams calldata params) external returns (uint256 amount0, uint256 amount1); function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1); function positions(uint256 tokenId) external view returns ( uint96 nonce, address operator, address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1 ); function burn(uint256 tokenId) external; } /** * @title IUniswapV3Pool * @dev Interface for Uniswap V3 Pool */ interface IUniswapV3Pool { function slot0() external view returns ( uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked ); function liquidity() external view returns (uint128); } /** * @title IPriceOracle * @dev Interface for price oracle */ interface IPriceOracle { function getPrice(address token) external view returns (uint256); } /** * @title BaseStrategy * @dev Abstract base class for all strategies */ abstract contract BaseStrategy is ReentrancyGuard, Ownable { using SafeERC20 for IERC20; IERC20 public want; address public vault; uint256 public minReportDelay; uint256 public maxReportDelay; uint256 public lastReport; uint256 public totalDebt; uint256 public totalGain; uint256 public totalLoss; event StrategyHarvested(uint256 profit, uint256 loss); event StrategyMigrated(address indexed newStrategy); constructor(address _vault, address _want) { require(_vault != address(0), "Invalid vault"); require(_want != address(0), "Invalid want token"); vault = _vault; want = IERC20(_want); minReportDelay = 1 days; maxReportDelay = 30 days; lastReport = block.timestamp; } function estimatedTotalAssets() public view virtual returns (uint256) { return want.balanceOf(address(this)); } function prepareReturn(uint256 debtOutstanding) public view virtual returns ( uint256 profit, uint256 loss, uint256 debtPayment ) { // Override in subclass } function adjustPosition(uint256 debtOutstanding) public virtual onlyVault { // Override in subclass } function liquidatePosition(uint256 amountNeeded) public virtual returns (uint256 liquidatedAmount, uint256 loss) { // Override in subclass } function harvest() public nonReentrant onlyVault returns (uint256 profit, uint256 loss) { require( block.timestamp >= lastReport + minReportDelay, "Too soon to report" ); (profit, loss,) = prepareReturn(0); if (profit > 0) { totalGain += profit; want.safeTransfer(vault, profit); } if (loss > 0) { totalLoss += loss; } lastReport = block.timestamp; emit StrategyHarvested(profit, loss); return (profit, loss); } modifier onlyVault() { require(msg.sender == vault, "Only vault can call"); _; } } /** * @title LiquidityStrategy * @dev Strategy for providing liquidity on Uniswap V3 * * This strategy provides liquidity on Uniswap V3 and earns trading fees. * It manages a concentrated liquidity position with configurable tick ranges. * * Example usage: * - Deploy with USDC/ETH: LiquidityStrategy(vault, USDC, POSITION_MANAGER, POOL, -887220, 887220) */ contract LiquidityStrategy is BaseStrategy { using SafeERC20 for IERC20; address public positionManager; address public pool; address public pairedToken; address public priceOracle; uint256 public positionTokenId; int24 public tickLower; int24 public tickUpper; uint24 public poolFee; uint256 public minLiquidityAmount; uint256 public maxLiquidityAmount; uint256 public feeThreshold; // Minimum fee to harvest event LiquidityPositionOpened(uint256 tokenId, uint128 liquidity); event LiquidityPositionIncreased(uint256 tokenId, uint128 liquidity); event LiquidityPositionDecreased(uint256 tokenId, uint128 liquidity); event FeesHarvested(uint256 amount0, uint256 amount1); event PositionRebalanced(int24 newTickLower, int24 newTickUpper); /** * @dev Initialize liquidity strategy * @param _vault Vault contract address * @param _want Token to provide liquidity with (USDC, DAI, etc.) * @param _positionManager Uniswap V3 Position Manager address * @param _pool Uniswap V3 Pool address * @param _pairedToken The other token in the pair * @param _tickLower Lower tick boundary * @param _tickUpper Upper tick boundary * @param _poolFee Pool fee (500, 3000, 10000) * @param _priceOracle Price oracle for token conversion */ constructor( address _vault, address _want, address _positionManager, address _pool, address _pairedToken, int24 _tickLower, int24 _tickUpper, uint24 _poolFee, address _priceOracle ) BaseStrategy(_vault, _want) { require(_positionManager != address(0), "Invalid position manager"); require(_pool != address(0), "Invalid pool"); require(_pairedToken != address(0), "Invalid paired token"); require(_priceOracle != address(0), "Invalid price oracle"); require(_tickLower < _tickUpper, "Invalid tick range"); positionManager = _positionManager; pool = _pool; pairedToken = _pairedToken; priceOracle = _priceOracle; tickLower = _tickLower; tickUpper = _tickUpper; poolFee = _poolFee; minLiquidityAmount = 1e18; maxLiquidityAmount = type(uint256).max; feeThreshold = 1e16; // 0.01 tokens // Approve position manager want.safeApprove(_positionManager, type(uint256).max); IERC20(_pairedToken).safeApprove(_positionManager, type(uint256).max); } /** * @dev Get total assets including liquidity position and unclaimed fees * @return Total assets in want tokens */ function estimatedTotalAssets() public view override returns (uint256) { uint256 balance = want.balanceOf(address(this)); uint256 positionValue = getPositionValue(); uint256 unclaimedFees = getUnclaimedFees(); uint256 unclaimedFeesInWant = convertToWant(address(pairedToken), unclaimedFees); return balance + positionValue + unclaimedFeesInWant; } /** * @dev Get the value of the liquidity position * @return Value in want tokens */ function getPositionValue() public view returns (uint256) { if (positionTokenId == 0) { return 0; } IUniswapV3PositionManager manager = IUniswapV3PositionManager(positionManager); ( , , address token0, address token1, , , , uint128 liquidity, , , uint128 tokensOwed0, uint128 tokensOwed1 ) = manager.positions(positionTokenId); if (liquidity == 0) { return 0; } // Get current tick IUniswapV3Pool poolContract = IUniswapV3Pool(pool); (uint160 sqrtPriceX96, int24 currentTick, , , , , ) = poolContract.slot0(); // Calculate token amounts from liquidity (uint256 amount0, uint256 amount1) = getTokenAmountsFromLiquidity( liquidity, currentTick ); // Add unclaimed fees amount0 += tokensOwed0; amount1 += tokensOwed1; // Convert to want token value uint256 value0 = (token0 == address(want)) ? amount0 : convertToWant(token0, amount0); uint256 value1 = (token1 == address(want)) ? amount1 : convertToWant(token1, amount1); return value0 + value1; } /** * @dev Get unclaimed fees from position * @return Total unclaimed fees in want tokens */ function getUnclaimedFees() public view returns (uint256) { if (positionTokenId == 0) { return 0; } IUniswapV3PositionManager manager = IUniswapV3PositionManager(positionManager); ( , , , , , , , , , , uint128 tokensOwed0, uint128 tokensOwed1 ) = manager.positions(positionTokenId); // Convert fees to want token value uint256 fee0Value = (address(want) == address(want)) ? tokensOwed0 : convertToWant(address(want), tokensOwed0); uint256 fee1Value = convertToWant(pairedToken, tokensOwed1); return fee0Value + fee1Value; } /** * @dev Calculate token amounts from liquidity using Uniswap V3 math * @param liquidity Liquidity amount * @param currentTick Current pool tick * @return amount0 Amount of token0 * @return amount1 Amount of token1 */ function getTokenAmountsFromLiquidity( uint128 liquidity, int24 currentTick ) internal view returns (uint256 amount0, uint256 amount1) { if (currentTick < tickLower) { // All liquidity is in token0 amount0 = liquidityToAmount0(liquidity, tickLower, tickUpper); amount1 = 0; } else if (currentTick >= tickUpper) { // All liquidity is in token1 amount0 = 0; amount1 = liquidityToAmount1(liquidity, tickLower, tickUpper); } else { // Liquidity is split between both tokens amount0 = liquidityToAmount0(liquidity, currentTick, tickUpper); amount1 = liquidityToAmount1(liquidity, tickLower, currentTick); } } /** * @dev Calculate amount0 from liquidity * Using Uniswap V3 math: amount0 = liquidity / sqrt(priceUpper) * @param liquidity Liquidity amount * @param tickA Lower tick * @param tickB Upper tick * @return Amount of token0 */ function liquidityToAmount0( uint128 liquidity, int24 tickA, int24 tickB ) internal pure returns (uint256) { // Simplified calculation - full implementation would use precise tick math // Production implementations may use Uniswap V3's precise tick math library for higher accuracy. uint256 tickDiff = uint256(int256(tickB - tickA)); return (uint256(liquidity) * 1e18) / (1e18 + tickDiff); } /** * @dev Calculate amount1 from liquidity * Using Uniswap V3 math: amount1 = liquidity * sqrt(priceLower) * @param liquidity Liquidity amount * @param tickA Lower tick * @param tickB Upper tick * @return Amount of token1 */ function liquidityToAmount1( uint128 liquidity, int24 tickA, int24 tickB ) internal pure returns (uint256) { // Simplified calculation - full implementation would use precise tick math uint256 tickDiff = uint256(int256(tickB - tickA)); return (uint256(liquidity) * tickDiff) / 1e18; } /** * @dev Convert token amount to want tokens using price oracle * @param token Token address * @param amount Amount of token * @return Amount in want tokens */ function convertToWant(address token, uint256 amount) internal view returns (uint256) { if (token == address(want)) { return amount; } if (amount == 0) { return 0; } // Get prices from oracle uint256 tokenPrice = IPriceOracle(priceOracle).getPrice(token); uint256 wantPrice = IPriceOracle(priceOracle).getPrice(address(want)); // Calculate: amount * (tokenPrice / wantPrice) return (amount * tokenPrice) / wantPrice; } /** * @dev Prepare return for harvest * @param debtOutstanding Amount of debt to repay * @return profit Profit from liquidity provision * @return loss Loss from liquidity provision * @return debtPayment Amount of debt paid */ function prepareReturn(uint256 debtOutstanding) public view override returns ( uint256 profit, uint256 loss, uint256 debtPayment ) { uint256 totalAssets = estimatedTotalAssets(); uint256 totalDebt_ = totalDebt; if (totalAssets > totalDebt_) { profit = totalAssets - totalDebt_; } else { loss = totalDebt_ - totalAssets; } debtPayment = Math.min( want.balanceOf(address(this)), debtOutstanding ); return (profit, loss, debtPayment); } /** * @dev Adjust position to match debt * @param debtOutstanding Amount of debt to manage */ function adjustPosition(uint256 debtOutstanding) public override onlyVault { uint256 balance = want.balanceOf(address(this)); if (balance > debtOutstanding) { uint256 amountToProvide = balance - debtOutstanding; _provideLiquidity(amountToProvide); } else if (balance < debtOutstanding) { uint256 amountToRemove = debtOutstanding - balance; _removeLiquidity(amountToRemove); } } /** * @dev Liquidate position to pay debt * @param amountNeeded Amount needed to pay debt * @return liquidatedAmount Amount liquidated * @return loss Loss from liquidation */ function liquidatePosition(uint256 amountNeeded) public override returns (uint256 liquidatedAmount, uint256 loss) { uint256 balance = want.balanceOf(address(this)); if (balance >= amountNeeded) { return (amountNeeded, 0); } uint256 amountToRemove = amountNeeded - balance; _removeLiquidity(amountToRemove); balance = want.balanceOf(address(this)); liquidatedAmount = Math.min(balance, amountNeeded); if (liquidatedAmount < amountNeeded) { loss = amountNeeded - liquidatedAmount; } return (liquidatedAmount, loss); } /** * @dev Provide liquidity to Uniswap V3 * @param amount Amount to provide */ function _provideLiquidity(uint256 amount) internal { if (amount < minLiquidityAmount) { return; } if (amount > maxLiquidityAmount) { amount = maxLiquidityAmount; } if (positionTokenId == 0) { _openPosition(amount); } else { _increasePosition(amount); } emit LiquidityPositionOpened(positionTokenId, 0); } /** * @dev Open new liquidity position * @param amount Amount to provide */ function _openPosition(uint256 amount) internal { IUniswapV3PositionManager manager = IUniswapV3PositionManager(positionManager); IUniswapV3PositionManager.MintParams memory params = IUniswapV3PositionManager.MintParams({ token0: address(want), token1: pairedToken, fee: poolFee, tickLower: tickLower, tickUpper: tickUpper, amount0Desired: amount, amount1Desired: 0, amount0Min: 0, amount1Min: 0, recipient: address(this), deadline: block.timestamp + 60 }); (uint256 tokenId, , , ) = manager.mint(params); positionTokenId = tokenId; } /** * @dev Increase existing liquidity position * @param amount Amount to add */ function _increasePosition(uint256 amount) internal { IUniswapV3PositionManager manager = IUniswapV3PositionManager(positionManager); IUniswapV3PositionManager.IncreaseLiquidityParams memory params = IUniswapV3PositionManager.IncreaseLiquidityParams({ tokenId: positionTokenId, amount0Desired: amount, amount1Desired: 0, amount0Min: 0, amount1Min: 0, deadline: block.timestamp + 60 }); manager.increaseLiquidity(params); } /** * @dev Remove liquidity from Uniswap V3 * @param amount Amount to remove */ function _removeLiquidity(uint256 amount) internal { if (positionTokenId == 0 || amount == 0) { return; } IUniswapV3PositionManager manager = IUniswapV3PositionManager(positionManager); // Calculate liquidity to remove based on amount uint128 liquidityToRemove = uint128((amount * 1e18) / 1e18); IUniswapV3PositionManager.DecreaseLiquidityParams memory params = IUniswapV3PositionManager.DecreaseLiquidityParams({ tokenId: positionTokenId, liquidity: liquidityToRemove, amount0Min: 0, amount1Min: 0, deadline: block.timestamp + 60 }); manager.decreaseLiquidity(params); // Collect tokens IUniswapV3PositionManager.CollectParams memory collectParams = IUniswapV3PositionManager.CollectParams({ tokenId: positionTokenId, recipient: address(this), amount0Max: type(uint128).max, amount1Max: type(uint128).max }); manager.collect(collectParams); emit LiquidityPositionDecreased(positionTokenId, liquidityToRemove); } /** * @dev Harvest fees from position */ function harvestFees() external onlyVault nonReentrant { if (positionTokenId == 0) { return; } uint256 unclaimedFees = getUnclaimedFees(); require(unclaimedFees >= feeThreshold, "Fees below threshold"); IUniswapV3PositionManager manager = IUniswapV3PositionManager(positionManager); // Collect fees IUniswapV3PositionManager.CollectParams memory params = IUniswapV3PositionManager.CollectParams({ tokenId: positionTokenId, recipient: address(this), amount0Max: type(uint128).max, amount1Max: type(uint128).max }); (uint256 amount0, uint256 amount1) = manager.collect(params); emit FeesHarvested(amount0, amount1); } /** * @dev Rebalance position to new tick range * @param _tickLower New lower tick * @param _tickUpper New upper tick */ function rebalancePosition(int24 _tickLower, int24 _tickUpper) external onlyOwner { require(_tickLower < _tickUpper, "Invalid tick range"); // Remove current position if (positionTokenId != 0) { _removeLiquidity(type(uint256).max); IUniswapV3PositionManager manager = IUniswapV3PositionManager(positionManager); manager.burn(positionTokenId); positionTokenId = 0; } // Update tick range tickLower = _tickLower; tickUpper = _tickUpper; // Provide liquidity at new range uint256 balance = want.balanceOf(address(this)); if (balance > 0) { _provideLiquidity(balance); } emit PositionRebalanced(_tickLower, _tickUpper); } /** * @dev Emergency remove all liquidity */ function emergencyRemoveLiquidity() external onlyOwner { if (positionTokenId != 0) { _removeLiquidity(type(uint256).max); IUniswapV3PositionManager manager = IUniswapV3PositionManager(positionManager); manager.burn(positionTokenId); positionTokenId = 0; } } /** * @dev Set fee threshold * @param _threshold Minimum fee to harvest */ function setFeeThreshold(uint256 _threshold) external onlyOwner { feeThreshold = _threshold; } /** * @dev Set price oracle * @param _priceOracle Price oracle address */ function setPriceOracle(address _priceOracle) external onlyOwner { require(_priceOracle != address(0), "Invalid oracle"); priceOracle = _priceOracle; } }