🌽The Harvest Functionality in Vaults: Issues and Best Practices

In decentralized finance (DeFi) vaults, the harvest function is a critical feature responsible for collecting rewards or yields generated by the vault’s underlying strategies. The proper implementation of this function ensures that users can maximize the returns from their deposited assets. However, forgetting to implement the harvest function, calling it incorrectly, or invoking it at the wrong time can lead to significant issues such as loss of yield, unclaimed rewards, or inequitable distribution among users.

This section outlines the importance of the harvest function, common issues that arise from its misuse, and best practices to avoid these problems, using examples from real-world vulnerabilities. Understanding the Harvest Function

In a DeFi vault, the harvest() function typically interacts with the underlying strategies of the vault to:

  1. Collect Yields: The vault earns yields from staked tokens or investments in external protocols (e.g., liquidity pools, yield farms). The harvest function aggregates these returns for distribution to users.

  2. Reinvest Rewards: The collected yields are often reinvested back into the strategy to compound returns, generating additional rewards for vault participants.

  3. Fee Distribution: In some vaults, harvest can also involve distributing a portion of the fees to the protocol or stakeholders.

If the harvest function is not called properly, it can lead to yield loss, user funds being diluted, or even the inability to claim the rewards. The next sections explain key issues related to calling harvest at the wrong time, failing to call harvest during critical operations, and potential ways to mitigate these problems. Issue 1: Calling Harvest After Deposits and Withdrawals

One common problem is when the harvest function is called after a user deposits or withdraws from the vault, which can lead to diluted rewards for existing users or loss of yield for withdrawing users.

How It Happens:

  1. Deposits: When a new user deposits into the vault, if the harvest function is called after calculating the shares to be minted for the depositor, the existing yield (that hasn't been harvested yet) gets shared with the new depositor. This dilutes the rewards for existing depositors since the yield is divided among a larger number of shares.

  2. Withdrawals: When a user withdraws, if the harvest function is called after the withdrawal transaction, the withdrawing user loses their share of the yield that wasn’t yet harvested. The vault holds the unharvested yield, and the withdrawing user gets less than what they’re entitled to.

Proof of Concept:

// Vulnerable harvest logic: called after share calculation
function deposit(uint256 amount) external {
    uint256 sharesToMint = (totalShares == 0)
        ? amount
        : (amount * totalShares) / totalAssets;

    shares[msg.sender] += sharesToMint;
    totalShares += sharesToMint;
    totalAssets += amount;

    // Harvest is called AFTER share calculation, causing dilution
    harvest();  

    asset.transferFrom(msg.sender, address(this), amount);
}

function withdraw(uint256 shareAmount) external {
    uint256 amountToWithdraw = (shareAmount * totalAssets) / totalShares;

    shares[msg.sender] -= shareAmount;
    totalShares -= shareAmount;
    totalAssets -= amountToWithdraw;

    // Harvest is called AFTER withdrawal, user loses yield
    harvest(); 

    asset.transfer(msg.sender, amountToWithdraw);
}

Impact:

  • For Deposits: Previous depositors share their earned yield with the new depositor.

  • For Withdrawals: Withdrawing users do not receive their full share of the yield.

Mitigation:

To avoid this issue, the harvest function should be called before calculating the user’s shares during deposit and withdrawal operations. This ensures that the vault’s total asset balance is updated before minting new shares or withdrawing assets.

Recommended Fix:

function deposit(uint256 amount) external {
    // Call harvest before share calculation to prevent dilution
    harvest();

    uint256 sharesToMint = (totalShares == 0)
        ? amount
        : (amount * totalShares) / totalAssets;

    shares[msg.sender] += sharesToMint;
    totalShares += sharesToMint;
    totalAssets += amount;

    asset.transferFrom(msg.sender, address(this), amount);
}

function withdraw(uint256 shareAmount) external {
    // Call harvest before withdrawal to update user's share of the yield
    harvest();

    uint256 amountToWithdraw = (shareAmount * totalAssets) / totalShares;

    shares[msg.sender] -= shareAmount;
    totalShares -= shareAmount;
    totalAssets -= amountToWithdraw;

    asset.transfer(msg.sender, amountToWithdraw);
}

Issue 2: Forgetting to Harvest During Adapter Changes

Another problem can occur when changing the vault's adapter (i.e., the module responsible for connecting the vault to a specific strategy). If the harvest function is not called during the adapter change process, any unharvested rewards stored in the previous strategy may be lost, as the vault switches to the new adapter without claiming these rewards.

How It Happens:

  1. The vault’s adapter is changed by the owner or governance.

  2. If the harvest() function is on cooldown or fails to execute, the vault switches adapters without claiming the unharvested rewards from the previous strategy.

  3. Users lose their share of the unclaimed rewards.

Proof of Concept:

// Vulnerable adapter change logic: forgets to harvest
function changeAdapter(address newAdapter) external onlyOwner {
    require(newAdapter != address(0), "Invalid adapter");

    // Adapter switch without harvesting the unclaimed rewards
    adapter.redeem(); 
    adapter = newAdapter;
}

Impact:

  • Users lose the yield or rewards stored in the previous adapter’s strategy.

Mitigation:

Always ensure that the harvest() function is called before switching adapters to collect the outstanding rewards from the old strategy. This ensures that no rewards are left unclaimed.

Issue 3: Timing of Harvest and Cooldown Issues

The timing of calling the harvest function can also lead to issues if it is not properly handled. Some strategies might have a cooldown period for harvest, meaning the harvest function can only be called once every certain period. If the harvest is not executed within the allowed timeframe or if it is called too early, it can fail or revert, causing unclaimed rewards to be lost.

How It Happens:

  1. Cooldown Period: Some vaults implement a cooldown on the harvest function to avoid excessive gas fees from calling it too frequently. If the cooldown isn’t respected, the harvest function will revert, and rewards may remain unclaimed.

  2. Early Harvest: If the harvest is called too early (before enough rewards have accrued), it can result in wasted gas and inefficient reward collection.

Conclusion:

The harvest function is critical to maximizing yield in vaults, and proper implementation ensures equitable distribution of rewards. Misusing this function—by forgetting to call it, calling it at the wrong time, or calling it in the wrong sequence—can result in loss of yield, user dilution, or inefficient gas usage. By implementing harvest before share calculations, ensuring it is called before changing adapters, and respecting cooldowns, vault developers can prevent these issues and maintain the integrity of the vault's yield-generating operations.

Last updated