// 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 ICompound * @dev Interface for Compound cToken contracts */ interface ICompound { function balanceOf(address account) external view returns (uint256); function exchangeRateStored() external view returns (uint256); function mint(uint256 mintAmount) external returns (uint256); function redeem(uint256 redeemTokens) external returns (uint256); function underlying() external view returns (address); } /** * @title IAave * @dev Interface for Aave aToken contracts */ interface IAave { function balanceOf(address user) external view returns (uint256); function UNDERLYING_ASSET_ADDRESS() external view returns (address); } /** * @title IAavePool * @dev Interface for Aave Pool contract */ interface IAavePool { function supply( address asset, uint256 amount, address onBehalfOf, uint16 referralCode ) external; function withdraw( address asset, uint256 amount, address to ) external 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; } /** * @dev Get estimated total assets managed by strategy */ function estimatedTotalAssets() public view virtual returns (uint256) { return want.balanceOf(address(this)); } /** * @dev Prepare return for harvest */ function prepareReturn(uint256 debtOutstanding) public view virtual returns ( uint256 profit, uint256 loss, uint256 debtPayment ) { // Override in subclass } /** * @dev Adjust position based on debt */ function adjustPosition(uint256 debtOutstanding) public virtual onlyVault { // Override in subclass } /** * @dev Liquidate position to pay debt */ function liquidatePosition(uint256 amountNeeded) public virtual returns (uint256 liquidatedAmount, uint256 loss) { // Override in subclass } /** * @dev Harvest profits and losses */ 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 LendingStrategy * @dev Strategy for lending tokens on Compound or Aave * * This strategy deposits tokens into lending protocols and earns interest. * It supports both Compound (cToken) and Aave (aToken) protocols. * * Example usage: * - Deploy with Compound: LendingStrategy(vault, USDC, COMPOUND_POOL, cUSDC, COMPOUND) * - Deploy with Aave: LendingStrategy(vault, USDC, AAVE_POOL, aUSDC, AAVE) */ contract LendingStrategy is BaseStrategy { using SafeERC20 for IERC20; enum ProtocolType { COMPOUND, AAVE } address public lendingProtocol; address public lendingToken; // cToken or aToken ProtocolType public protocolType; uint256 public minLendingAmount; uint256 public maxLendingAmount; event LendingPositionOpened(uint256 amount); event LendingPositionClosed(uint256 amount); event LendingPositionRebalanced(uint256 newAmount); /** * @dev Initialize lending strategy * @param _vault Vault contract address * @param _want Token to lend (USDC, DAI, etc.) * @param _lendingProtocol Lending protocol address (Compound pool or Aave pool) * @param _lendingToken cToken or aToken address * @param _protocolType Protocol type (COMPOUND or AAVE) */ constructor( address _vault, address _want, address _lendingProtocol, address _lendingToken, ProtocolType _protocolType ) BaseStrategy(_vault, _want) { require(_lendingProtocol != address(0), "Invalid protocol"); require(_lendingToken != address(0), "Invalid lending token"); lendingProtocol = _lendingProtocol; lendingToken = _lendingToken; protocolType = _protocolType; minLendingAmount = 1e18; // 1 token (assuming 18 decimals) maxLendingAmount = type(uint256).max; // Approve lending protocol to spend want tokens want.safeApprove(lendingProtocol, type(uint256).max); } /** * @dev Get total assets including lent amount * @return Total assets in want tokens */ function estimatedTotalAssets() public view override returns (uint256) { uint256 balance = want.balanceOf(address(this)); uint256 lent = getLentAmount(); return balance + lent; } /** * @dev Get the amount of tokens lent in the lending protocol * @return Amount of tokens lent (in want token units) */ function getLentAmount() public view returns (uint256) { if (protocolType == ProtocolType.COMPOUND) { return getCompoundBalance(); } else if (protocolType == ProtocolType.AAVE) { return getAaveBalance(); } else { revert("Unsupported protocol"); } } /** * @dev Get balance from Compound protocol * Converts cToken balance to underlying token amount using exchange rate * @return Amount of want tokens represented by cTokens */ function getCompoundBalance() internal view returns (uint256) { require(lendingToken != address(0), "cToken not set"); ICompound cTokenContract = ICompound(lendingToken); uint256 cTokenBalance = cTokenContract.balanceOf(address(this)); if (cTokenBalance == 0) { return 0; } // Get exchange rate (cToken amount per underlying token, scaled by 1e18) uint256 exchangeRate = cTokenContract.exchangeRateStored(); // Calculate underlying amount: cTokenBalance * exchangeRate / 1e18 // exchangeRate is in format: 1 cToken = exchangeRate / 1e18 underlying tokens return (cTokenBalance * exchangeRate) / 1e18; } /** * @dev Get balance from Aave protocol * aTokens are 1:1 with underlying tokens * @return Amount of want tokens represented by aTokens */ function getAaveBalance() internal view returns (uint256) { require(lendingToken != address(0), "aToken not set"); IAave aTokenContract = IAave(lendingToken); return aTokenContract.balanceOf(address(this)); } /** * @dev Prepare return for harvest * @param debtOutstanding Amount of debt to repay * @return profit Profit from lending * @return loss Loss from lending * @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 amountToLend = balance - debtOutstanding; _lend(amountToLend); } else if (balance < debtOutstanding) { uint256 amountToWithdraw = debtOutstanding - balance; _withdraw(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; _withdraw(amountToWithdraw); balance = want.balanceOf(address(this)); liquidatedAmount = Math.min(balance, amountNeeded); if (liquidatedAmount < amountNeeded) { loss = amountNeeded - liquidatedAmount; } return (liquidatedAmount, loss); } /** * @dev Lend tokens to the lending protocol * @param amount Amount to lend */ function _lend(uint256 amount) internal { if (amount < minLendingAmount) { return; } if (amount > maxLendingAmount) { amount = maxLendingAmount; } if (protocolType == ProtocolType.COMPOUND) { _lendCompound(amount); } else if (protocolType == ProtocolType.AAVE) { _lendAave(amount); } emit LendingPositionOpened(amount); } /** * @dev Lend to Compound * @param amount Amount to lend */ function _lendCompound(uint256 amount) internal { ICompound cToken = ICompound(lendingToken); uint256 result = cToken.mint(amount); require(result == 0, "Compound mint failed"); } /** * @dev Lend to Aave * @param amount Amount to lend */ function _lendAave(uint256 amount) internal { IAavePool pool = IAavePool(lendingProtocol); pool.supply(address(want), amount, address(this), 0); } /** * @dev Withdraw tokens from the lending protocol * @param amount Amount to withdraw */ function _withdraw(uint256 amount) internal { if (amount == 0) { return; } if (protocolType == ProtocolType.COMPOUND) { _withdrawCompound(amount); } else if (protocolType == ProtocolType.AAVE) { _withdrawAave(amount); } emit LendingPositionClosed(amount); } /** * @dev Withdraw from Compound * @param amount Amount to withdraw */ function _withdrawCompound(uint256 amount) internal { ICompound cToken = ICompound(lendingToken); // Calculate cTokens to redeem uint256 exchangeRate = cToken.exchangeRateStored(); uint256 cTokensToRedeem = (amount * 1e18) / exchangeRate; // Ensure we don't try to redeem more than we have uint256 cTokenBalance = cToken.balanceOf(address(this)); cTokensToRedeem = Math.min(cTokensToRedeem, cTokenBalance); uint256 result = cToken.redeem(cTokensToRedeem); require(result == 0, "Compound redeem failed"); } /** * @dev Withdraw from Aave * @param amount Amount to withdraw */ function _withdrawAave(uint256 amount) internal { IAavePool pool = IAavePool(lendingProtocol); // Aave will return the actual amount withdrawn pool.withdraw(address(want), amount, address(this)); } /** * @dev Emergency withdraw all funds */ function emergencyWithdraw() external onlyOwner { uint256 lent = getLentAmount(); if (lent > 0) { _withdraw(lent); } } /** * @dev Set minimum lending amount * @param _minAmount Minimum amount to lend */ function setMinLendingAmount(uint256 _minAmount) external onlyOwner { minLendingAmount = _minAmount; } /** * @dev Set maximum lending amount * @param _maxAmount Maximum amount to lend */ function setMaxLendingAmount(uint256 _maxAmount) external onlyOwner { maxLendingAmount = _maxAmount; } }