// 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 IUniswapV3Staking * @dev Interface for Uniswap V3 staking rewards */ interface IUniswapV3Staking { function rewards(address token, address owner) external view returns (uint256); function claimRewards(address[] calldata rewardTokens, uint256 amount) external; } /** * @title ICurveGauge * @dev Interface for Curve gauge (liquidity mining) */ interface ICurveGauge { function claimable_tokens(address user) external view returns (uint256); function claim_rewards(address user) external; function deposit(uint256 amount) external; function withdraw(uint256 amount) external; function balanceOf(address user) external view returns (uint256); } /** * @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 YieldFarmingStrategy * @dev Strategy for yield farming on Uniswap V3 or Curve * * This strategy deposits tokens into yield farming protocols and earns rewards. * It supports both Uniswap V3 (staking rewards) and Curve (gauge rewards). * * Example usage: * - Deploy with Uniswap V3: YieldFarmingStrategy(vault, USDC, UNISWAP_STAKING, UNI, UNISWAP_V3) * - Deploy with Curve: YieldFarmingStrategy(vault, USDC, CURVE_GAUGE, CRV, CURVE) */ contract YieldFarmingStrategy is BaseStrategy { using SafeERC20 for IERC20; enum FarmingProtocol { UNISWAP_V3, CURVE } address public farmingProtocol; address public rewardToken; address public priceOracle; FarmingProtocol public protocolType; uint256 public minFarmingAmount; uint256 public maxFarmingAmount; uint256 public harvestThreshold; // Minimum reward amount to harvest event YieldPositionOpened(uint256 amount); event YieldPositionClosed(uint256 amount); event RewardsHarvested(uint256 rewardAmount); event RewardsCompounded(uint256 rewardAmount, uint256 wantAmount); /** * @dev Initialize yield farming strategy * @param _vault Vault contract address * @param _want Token to farm with (USDC, DAI, etc.) * @param _farmingProtocol Farming protocol address (Uniswap staking or Curve gauge) * @param _rewardToken Reward token address (UNI, CRV, etc.) * @param _protocolType Protocol type (UNISWAP_V3 or CURVE) * @param _priceOracle Price oracle for reward token conversion */ constructor( address _vault, address _want, address _farmingProtocol, address _rewardToken, FarmingProtocol _protocolType, address _priceOracle ) BaseStrategy(_vault, _want) { require(_farmingProtocol != address(0), "Invalid protocol"); require(_rewardToken != address(0), "Invalid reward token"); require(_priceOracle != address(0), "Invalid price oracle"); farmingProtocol = _farmingProtocol; rewardToken = _rewardToken; priceOracle = _priceOracle; protocolType = _protocolType; minFarmingAmount = 1e18; // 1 token maxFarmingAmount = type(uint256).max; harvestThreshold = 1e18; // 1 reward token // Approve farming protocol to spend want tokens want.safeApprove(farmingProtocol, type(uint256).max); // Approve conversion of reward tokens IERC20(rewardToken).safeApprove(farmingProtocol, type(uint256).max); } /** * @dev Get total assets including farming position and pending rewards * @return Total assets in want tokens */ function estimatedTotalAssets() public view override returns (uint256) { uint256 balance = want.balanceOf(address(this)); uint256 farmed = getFarmedAmount(); uint256 pendingRewards = getPendingRewards(); uint256 pendingRewardsInWant = convertRewardsToWant(pendingRewards); return balance + farmed + pendingRewardsInWant; } /** * @dev Get the amount of tokens currently farming * @return Amount of tokens in farming position */ function getFarmedAmount() public view returns (uint256) { if (protocolType == FarmingProtocol.UNISWAP_V3) { // Uniswap V3 staking doesn't track position amount directly // This would need to be tracked separately return 0; } else if (protocolType == FarmingProtocol.CURVE) { return getCurveGaugeBalance(); } else { revert("Unsupported protocol"); } } /** * @dev Get balance in Curve gauge * @return Amount of tokens in gauge */ function getCurveGaugeBalance() internal view returns (uint256) { require(farmingProtocol != address(0), "Gauge not set"); ICurveGauge gauge = ICurveGauge(farmingProtocol); return gauge.balanceOf(address(this)); } /** * @dev Get pending rewards from farming protocol * @return Amount of reward tokens earned */ function getPendingRewards() public view returns (uint256) { if (protocolType == FarmingProtocol.UNISWAP_V3) { return getUniswapV3Rewards(); } else if (protocolType == FarmingProtocol.CURVE) { return getCurveRewards(); } else { revert("Unsupported protocol"); } } /** * @dev Get pending rewards from Uniswap V3 * @return Amount of reward tokens */ function getUniswapV3Rewards() internal view returns (uint256) { require(farmingProtocol != address(0), "Staking not set"); IUniswapV3Staking staking = IUniswapV3Staking(farmingProtocol); return staking.rewards(rewardToken, address(this)); } /** * @dev Get claimable tokens from Curve gauge * @return Amount of reward tokens */ function getCurveRewards() internal view returns (uint256) { require(farmingProtocol != address(0), "Gauge not set"); ICurveGauge gauge = ICurveGauge(farmingProtocol); return gauge.claimable_tokens(address(this)); } /** * @dev Convert reward tokens to want tokens using price oracle * @param rewardAmount Amount of reward tokens * @return Amount in want tokens */ function convertRewardsToWant(uint256 rewardAmount) internal view returns (uint256) { if (rewardAmount == 0) { return 0; } if (address(rewardToken) == address(want)) { return rewardAmount; } // Get price of reward token in want token uint256 rewardPrice = IPriceOracle(priceOracle).getPrice(rewardToken); uint256 wantPrice = IPriceOracle(priceOracle).getPrice(address(want)); // Calculate: rewardAmount * (rewardPrice / wantPrice) return (rewardAmount * rewardPrice) / wantPrice; } /** * @dev Prepare return for harvest * @param debtOutstanding Amount of debt to repay * @return profit Profit from farming * @return loss Loss from farming * @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 amountToFarm = balance - debtOutstanding; _farm(amountToFarm); } else if (balance < debtOutstanding) { uint256 amountToWithdraw = debtOutstanding - balance; _unfarm(amountToWithdraw); } } /** * @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 amountToWithdraw = amountNeeded - balance; _unfarm(amountToWithdraw); balance = want.balanceOf(address(this)); liquidatedAmount = Math.min(balance, amountNeeded); if (liquidatedAmount < amountNeeded) { loss = amountNeeded - liquidatedAmount; } return (liquidatedAmount, loss); } /** * @dev Farm tokens in the farming protocol * @param amount Amount to farm */ function _farm(uint256 amount) internal { if (amount < minFarmingAmount) { return; } if (amount > maxFarmingAmount) { amount = maxFarmingAmount; } if (protocolType == FarmingProtocol.UNISWAP_V3) { _farmUniswapV3(amount); } else if (protocolType == FarmingProtocol.CURVE) { _farmCurve(amount); } emit YieldPositionOpened(amount); } /** * @dev Farm on Uniswap V3 * @param amount Amount to farm */ function _farmUniswapV3(uint256 amount) internal { // Uniswap V3 staking implementation // This would depend on the specific staking contract // This implementation provides the base validation and can be extended with specific staking logic. require(amount > 0, "Amount must be greater than 0"); } /** * @dev Farm on Curve * @param amount Amount to farm */ function _farmCurve(uint256 amount) internal { ICurveGauge gauge = ICurveGauge(farmingProtocol); gauge.deposit(amount); } /** * @dev Unfarm tokens from the farming protocol * @param amount Amount to unfarm */ function _unfarm(uint256 amount) internal { if (amount == 0) { return; } if (protocolType == FarmingProtocol.UNISWAP_V3) { _unfarmUniswapV3(amount); } else if (protocolType == FarmingProtocol.CURVE) { _unfarmCurve(amount); } emit YieldPositionClosed(amount); } /** * @dev Unfarm from Uniswap V3 * @param amount Amount to unfarm */ function _unfarmUniswapV3(uint256 amount) internal { // Uniswap V3 unstaking implementation require(amount > 0, "Amount must be greater than 0"); } /** * @dev Unfarm from Curve * @param amount Amount to unfarm */ function _unfarmCurve(uint256 amount) internal { ICurveGauge gauge = ICurveGauge(farmingProtocol); gauge.withdraw(amount); } /** * @dev Harvest and compound rewards */ function harvestRewards() external onlyVault nonReentrant { uint256 pendingRewards = getPendingRewards(); require(pendingRewards >= harvestThreshold, "Rewards below threshold"); // Claim rewards if (protocolType == FarmingProtocol.CURVE) { ICurveGauge gauge = ICurveGauge(farmingProtocol); gauge.claim_rewards(address(this)); } // Convert rewards to want tokens uint256 rewardBalance = IERC20(rewardToken).balanceOf(address(this)); if (rewardBalance > 0) { uint256 wantAmount = convertRewardsToWant(rewardBalance); emit RewardsCompounded(rewardBalance, wantAmount); } emit RewardsHarvested(pendingRewards); } /** * @dev Emergency unfarm all positions */ function emergencyUnfarm() external onlyOwner { uint256 farmed = getFarmedAmount(); if (farmed > 0) { _unfarm(farmed); } } /** * @dev Set minimum farming amount * @param _minAmount Minimum amount to farm */ function setMinFarmingAmount(uint256 _minAmount) external onlyOwner { minFarmingAmount = _minAmount; } /** * @dev Set maximum farming amount * @param _maxAmount Maximum amount to farm */ function setMaxFarmingAmount(uint256 _maxAmount) external onlyOwner { maxFarmingAmount = _maxAmount; } /** * @dev Set harvest threshold * @param _threshold Minimum reward amount to harvest */ function setHarvestThreshold(uint256 _threshold) external onlyOwner { harvestThreshold = _threshold; } /** * @dev Set price oracle * @param _priceOracle Price oracle address */ function setPriceOracle(address _priceOracle) external onlyOwner { require(_priceOracle != address(0), "Invalid oracle"); priceOracle = _priceOracle; } }