Vault Harvester

Harvesting and compounding is key to maximize yield in DeFi yield aggregators. Automate harvesting and compounding using Chainlink Automation's decentralized automation network.

View the template here.

Below is the main contract KeeperCompatibleHarvester.sol:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../libraries/UpkeepLibrary.sol";
import "../interfaces/IKeeperRegistry.sol";
import "../interfaces/IHarvester.sol";

abstract contract KeeperCompatibleHarvester is IHarvester, Ownable {
  using SafeERC20 for IERC20;

  // Contracts.
  IKeeperRegistry public keeperRegistry;

  // Configuration state variables.
  uint256 public performUpkeepGasLimit;
  uint256 public performUpkeepGasLimitBuffer;
  uint256 public vaultHarvestFunctionGasOverhead; // Estimated average gas cost of calling harvest()
  uint256 public keeperRegistryGasOverhead; // Gas cost of upstream contract that calls performUpkeep(). This is a private variable on KeeperRegistry.
  uint256 public chainlinkUpkeepTxPremiumFactor; // Tx premium factor/multiplier scaled by 1 gwei (10**9).
  address public callFeeRecipient;

  // State variables that will change across upkeeps.
  uint256 public startIndex;

  constructor(
    address _keeperRegistry,
    uint256 _performUpkeepGasLimit,
    uint256 _performUpkeepGasLimitBuffer,
    uint256 _vaultHarvestFunctionGasOverhead,
    uint256 _keeperRegistryGasOverhead
  ) {
    // Set contract references.
    keeperRegistry = IKeeperRegistry(_keeperRegistry);

    // Initialize state variables from initialize() arguments.
    performUpkeepGasLimit = _performUpkeepGasLimit;
    performUpkeepGasLimitBuffer = _performUpkeepGasLimitBuffer;
    vaultHarvestFunctionGasOverhead = _vaultHarvestFunctionGasOverhead;
    keeperRegistryGasOverhead = _keeperRegistryGasOverhead;

    // Initialize state variables derived from initialize() arguments.
    (, OnchainConfig memory config, , , ) = keeperRegistry.getState();
    chainlinkUpkeepTxPremiumFactor = uint256(config.paymentPremiumPPB);
  }

  /*             */
  /* checkUpkeep */
  /*             */

  function checkUpkeep(
    bytes calldata _checkData // unused
  )
    external
    view
    override
    returns (
      bool upkeepNeeded,
      bytes memory performData // array of vaults +
    )
  {
    _checkData; // dummy reference to get rid of unused parameter warning

    // get vaults to iterate over
    address[] memory vaults = _getVaultAddresses();

    // count vaults to harvest that will fit within gas limit
    (HarvestInfo[] memory harvestInfo, uint256 numberOfVaultsToHarvest, uint256 newStartIndex) = _countVaultsToHarvest(
      vaults
    );
    if (numberOfVaultsToHarvest == 0) return (false, bytes("No vaults to harvest"));

    (address[] memory vaultsToHarvest, uint256 heuristicEstimatedTxCost, uint256 callRewards) = _buildVaultsToHarvest(
      vaults,
      harvestInfo,
      numberOfVaultsToHarvest
    );

    uint256 nonHeuristicEstimatedTxCost = _calculateExpectedTotalUpkeepTxCost(numberOfVaultsToHarvest);

    performData = abi.encode(
      vaultsToHarvest,
      newStartIndex,
      heuristicEstimatedTxCost,
      nonHeuristicEstimatedTxCost,
      callRewards
    );

    return (true, performData);
  }

  function _buildVaultsToHarvest(
    address[] memory _vaults,
    HarvestInfo[] memory _willHarvestVaults,
    uint256 _numberOfVaultsToHarvest
  )
    internal
    view
    returns (address[] memory vaultsToHarvest, uint256 heuristicEstimatedTxCost, uint256 totalCallRewards)
  {
    uint256 vaultPositionInArray;
    vaultsToHarvest = new address[](_numberOfVaultsToHarvest);

    // create array of vaults to harvest. Could reduce code duplication from _countVaultsToHarvest via a another function parameter called _loopPostProcess
    for (uint256 offset; offset < _vaults.length; ++offset) {
      uint256 vaultIndexToCheck = UpkeepLibrary._getCircularIndex(startIndex, offset, _vaults.length);
      address vaultAddress = _vaults[vaultIndexToCheck];

      HarvestInfo memory harvestInfo = _willHarvestVaults[offset];

      if (harvestInfo.willHarvest) {
        vaultsToHarvest[vaultPositionInArray] = vaultAddress;
        heuristicEstimatedTxCost += harvestInfo.estimatedTxCost;
        totalCallRewards += harvestInfo.callRewardsAmount;
        vaultPositionInArray += 1;
      }

      // no need to keep going if we're past last index
      if (vaultPositionInArray == _numberOfVaultsToHarvest) break;
    }

    return (vaultsToHarvest, heuristicEstimatedTxCost, totalCallRewards);
  }

  function _countVaultsToHarvest(
    address[] memory _vaults
  ) internal view returns (HarvestInfo[] memory harvestInfo, uint256 numberOfVaultsToHarvest, uint256 newStartIndex) {
    uint256 gasLeft = _calculateAdjustedGasCap();
    uint256 vaultIndexToCheck; // hoisted up to be able to set newStartIndex
    harvestInfo = new HarvestInfo[](_vaults.length);

    // count the number of vaults to harvest.
    for (uint256 offset; offset < _vaults.length; ++offset) {
      // _startIndex is where to start in the _vaultRegistry array, offset is position from start index (in other words, number of vaults we've checked so far),
      // then modulo to wrap around to the start of the array, until we've checked all vaults, or break early due to hitting gas limit
      // this logic is contained in _getCircularIndex()
      vaultIndexToCheck = UpkeepLibrary._getCircularIndex(startIndex, offset, _vaults.length);
      address vaultAddress = _vaults[vaultIndexToCheck];

      (bool willHarvest, uint256 estimatedTxCost, uint256 callRewardsAmount) = _willHarvestVault(vaultAddress);

      if (willHarvest && gasLeft >= vaultHarvestFunctionGasOverhead) {
        gasLeft -= vaultHarvestFunctionGasOverhead;
        numberOfVaultsToHarvest += 1;
        harvestInfo[offset] = HarvestInfo(true, estimatedTxCost, callRewardsAmount);
      }

      if (gasLeft < vaultHarvestFunctionGasOverhead) {
        break;
      }
    }

    newStartIndex = UpkeepLibrary._getCircularIndex(vaultIndexToCheck, 1, _vaults.length);

    return (harvestInfo, numberOfVaultsToHarvest, newStartIndex);
  }

  function _willHarvestVault(address _vaultAddress) internal view returns (bool willHarvestVault, uint256, uint256) {
    (bool shouldHarvestVault, uint256 estimatedTxCost, uint256 callRewardAmount) = _shouldHarvestVault(_vaultAddress);
    bool canHarvestVault = _canHarvestVault(_vaultAddress);

    willHarvestVault = canHarvestVault && shouldHarvestVault;

    return (willHarvestVault, estimatedTxCost, callRewardAmount);
  }

  function _canHarvestVault(address _vaultAddress) internal view virtual returns (bool canHarvest);

  function _shouldHarvestVault(
    address _vaultAddress
  ) internal view virtual returns (bool shouldHarvestVault, uint256 txCostWithPremium, uint256 callRewardAmount);

  /*               */
  /* performUpkeep */
  /*               */

  function performUpkeep(bytes calldata _performData) external override {
    (
      address[] memory vaultsToHarvest,
      uint256 newStartIndex,
      uint256 heuristicEstimatedTxCost,
      uint256 nonHeuristicEstimatedTxCost,
      uint256 estimatedCallRewards
    ) = abi.decode(_performData, (address[], uint256, uint256, uint256, uint256));

    _runUpkeep(
      vaultsToHarvest,
      newStartIndex,
      heuristicEstimatedTxCost,
      nonHeuristicEstimatedTxCost,
      estimatedCallRewards
    );
  }

  function _runUpkeep(
    address[] memory _vaults,
    uint256 _newStartIndex,
    uint256 _heuristicEstimatedTxCost,
    uint256 _nonHeuristicEstimatedTxCost,
    uint256 _estimatedCallRewards
  ) internal {
    // Make sure estimate looks good.
    if (_estimatedCallRewards < _nonHeuristicEstimatedTxCost) {
      emit HeuristicFailed(
        block.number,
        _heuristicEstimatedTxCost,
        _nonHeuristicEstimatedTxCost,
        _estimatedCallRewards
      );
    }

    uint256 gasBefore = gasleft();
    // multi harvest
    require(_vaults.length > 0, "No vaults to harvest");
    (uint256 numberOfSuccessfulHarvests, uint256 numberOfFailedHarvests, uint256 calculatedCallRewards) = _multiHarvest(
      _vaults
    );

    // ensure _newStartIndex is valid and set startIndex
    uint256 vaultCount = _getVaultAddresses().length;
    require(_newStartIndex >= 0 && _newStartIndex < vaultCount, "_newStartIndex out of range.");
    startIndex = _newStartIndex;

    uint256 gasAfter = gasleft();
    uint256 gasUsedByPerformUpkeep = gasBefore - gasAfter;

    // split these into their own functions to avoid `Stack too deep`
    _reportProfitSummary(
      gasUsedByPerformUpkeep,
      _nonHeuristicEstimatedTxCost,
      _estimatedCallRewards,
      calculatedCallRewards
    );
    _reportHarvestSummary(_newStartIndex, gasUsedByPerformUpkeep, numberOfSuccessfulHarvests, numberOfFailedHarvests);
  }

  function _reportHarvestSummary(
    uint256 _newStartIndex,
    uint256 _gasUsedByPerformUpkeep,
    uint256 _numberOfSuccessfulHarvests,
    uint256 _numberOfFailedHarvests
  ) internal {
    emit HarvestSummary(
      block.number,
      // state variables
      startIndex,
      _newStartIndex,
      // gas metrics
      tx.gasprice,
      _gasUsedByPerformUpkeep,
      // summary metrics
      _numberOfSuccessfulHarvests,
      _numberOfFailedHarvests
    );
  }

  function _reportProfitSummary(
    uint256 _gasUsedByPerformUpkeep,
    uint256 _nonHeuristicEstimatedTxCost,
    uint256 _estimatedCallRewards,
    uint256 _calculatedCallRewards
  ) internal {
    uint256 estimatedTxCost = _nonHeuristicEstimatedTxCost; // use nonHeuristic here as its more accurate
    uint256 estimatedProfit = UpkeepLibrary._calculateProfit(_estimatedCallRewards, estimatedTxCost);

    uint256 calculatedTxCost = _calculateTxCostWithOverheadWithPremium(_gasUsedByPerformUpkeep);
    uint256 calculatedProfit = UpkeepLibrary._calculateProfit(_calculatedCallRewards, calculatedTxCost);

    emit ProfitSummary(
      // predicted values
      estimatedTxCost,
      _estimatedCallRewards,
      estimatedProfit,
      // calculated values
      calculatedTxCost,
      _calculatedCallRewards,
      calculatedProfit
    );
  }

  function _multiHarvest(
    address[] memory _vaults
  )
    internal
    returns (uint256 numberOfSuccessfulHarvests, uint256 numberOfFailedHarvests, uint256 cumulativeCallRewards)
  {
    bool[] memory isSuccessfulHarvest = new bool[](_vaults.length);
    for (uint256 i = 0; i < _vaults.length; ++i) {
      (bool didHarvest, uint256 callRewards) = _harvestVault(_vaults[i]);
      // Add rewards to cumulative tracker.
      if (didHarvest) {
        isSuccessfulHarvest[i] = true;
        cumulativeCallRewards += callRewards;
      }
    }

    (address[] memory successfulHarvests, address[] memory failedHarvests) = _getSuccessfulAndFailedVaults(
      _vaults,
      isSuccessfulHarvest
    );

    emit SuccessfulHarvests(block.number, successfulHarvests);
    emit FailedHarvests(block.number, failedHarvests);

    numberOfSuccessfulHarvests = successfulHarvests.length;
    numberOfFailedHarvests = failedHarvests.length;
    return (numberOfSuccessfulHarvests, numberOfFailedHarvests, cumulativeCallRewards);
  }

  function _harvestVault(address _vault) internal virtual returns (bool didHarvest, uint256 callRewards);

  function _getSuccessfulAndFailedVaults(
    address[] memory _vaults,
    bool[] memory _isSuccessfulHarvest
  ) internal pure returns (address[] memory successfulHarvests, address[] memory failedHarvests) {
    uint256 successfulCount;
    for (uint256 i = 0; i < _vaults.length; i++) {
      if (_isSuccessfulHarvest[i]) {
        successfulCount += 1;
      }
    }

    successfulHarvests = new address[](successfulCount);
    failedHarvests = new address[](_vaults.length - successfulCount);
    uint256 successfulHarvestsIndex;
    uint256 failedHarvestIndex;
    for (uint256 i = 0; i < _vaults.length; i++) {
      if (_isSuccessfulHarvest[i]) {
        successfulHarvests[successfulHarvestsIndex++] = _vaults[i];
      } else {
        failedHarvests[failedHarvestIndex++] = _vaults[i];
      }
    }

    return (successfulHarvests, failedHarvests);
  }

  /*     */
  /* Set */
  /*     */

  function setPerformUpkeepGasLimit(uint256 _performUpkeepGasLimit) external override onlyOwner {
    performUpkeepGasLimit = _performUpkeepGasLimit;
  }

  function setPerformUpkeepGasLimitBuffer(uint256 _performUpkeepGasLimitBuffer) external override onlyOwner {
    performUpkeepGasLimitBuffer = _performUpkeepGasLimitBuffer;
  }

  function setHarvestGasConsumption(uint256 _harvestGasConsumption) external override onlyOwner {
    vaultHarvestFunctionGasOverhead = _harvestGasConsumption;
  }

  /*      */
  /* View */
  /*      */

  function _getVaultAddresses() internal view virtual returns (address[] memory);

  function _getVaultHarvestGasOverhead(address _vault) internal view virtual returns (uint256);

  function _calculateAdjustedGasCap() internal view returns (uint256 adjustedPerformUpkeepGasLimit) {
    return performUpkeepGasLimit - performUpkeepGasLimitBuffer;
  }

  function _calculateTxCostWithPremium(uint256 _gasOverhead) internal view returns (uint256 txCost) {
    return UpkeepLibrary._calculateUpkeepTxCost(tx.gasprice, _gasOverhead, chainlinkUpkeepTxPremiumFactor);
  }

  function _calculateTxCostWithOverheadWithPremium(
    uint256 _totalVaultHarvestOverhead
  ) internal view returns (uint256 txCost) {
    return
      UpkeepLibrary._calculateUpkeepTxCostFromTotalVaultHarvestOverhead(
        tx.gasprice,
        _totalVaultHarvestOverhead,
        keeperRegistryGasOverhead,
        chainlinkUpkeepTxPremiumFactor
      );
  }

  function _calculateExpectedTotalUpkeepTxCost(
    uint256 _numberOfVaultsToHarvest
  ) internal view returns (uint256 txCost) {
    uint256 totalVaultHarvestGasOverhead = vaultHarvestFunctionGasOverhead * _numberOfVaultsToHarvest;
    return
      UpkeepLibrary._calculateUpkeepTxCostFromTotalVaultHarvestOverhead(
        tx.gasprice,
        totalVaultHarvestGasOverhead,
        keeperRegistryGasOverhead,
        chainlinkUpkeepTxPremiumFactor
      );
  }

  function _estimateSingleVaultHarvestGasOverhead(
    uint256 _vaultHarvestFunctionGasOverhead
  ) internal view returns (uint256 totalGasOverhead) {
    totalGasOverhead = _vaultHarvestFunctionGasOverhead + keeperRegistryGasOverhead;
  }

  /*      */
  /* Misc */
  /*      */

  /**
   * @dev Rescues random funds stuck.
   * @param _token address of the token to rescue.
   */
  function inCaseTokensGetStuck(address _token) external onlyOwner {
    IERC20 token = IERC20(_token);

    uint256 amount = token.balanceOf(address(this));
    token.safeTransfer(msg.sender, amount);
  }
}

This is an abstract contract that iterates vaults from a provided list and fits vaults within upkeep gas limit. The contract also provides helper functions to calculate gas consumption and estimate profit and contains a trigger mechanism can be time-based, profit-based or custom. Finally, the cotract reports profits, successful harvests, and failed harvests.

What's next

Get the latest Chainlink content straight to your inbox.