Use Case
In DeFi yield farming protocols, users can deposit assets into a pool and earn rewards. The process of claiming the compounded rewards is called “harvesting”. This assertion ensures that harvesting operations maintain protocol integrity by:
- Preventing harvests that decrease vault balances
- Ensuring price per share remains stable or increases
- Detecting potential yield farming exploits
- Maintaining protocol solvency by enforcing proper reward distribution
This is a critical security parameter because:
- Incorrect harvest implementations could lead to loss of user funds
- Price per share manipulation could enable flash loan attacks
- Balance decreases during harvest could indicate protocol insolvency
- Yield farming exploits often target harvest mechanisms
Applicable Protocols
- Yield farming protocols (e.g., Beefy, Yearn, Harvest)
- Liquidity mining programs
- Staking protocols with reward distribution
- Auto-compounding vaults
These protocols need this assertion because:
- Yield farming protocols rely on accurate reward distribution
- Liquidity mining programs must maintain proper reward accounting
- Staking protocols need to ensure rewards are properly distributed
- Auto-compounding vaults must maintain accurate share pricing
Explanation
The assertion implements a multi-layered approach to verify harvest operations:
-
Pre-harvest State Check:
- Uses
forkPreState()
to capture vault balance and price per share
- Establishes baseline metrics before harvest
-
Post-harvest Validation:
- Uses
forkPostState()
to verify metrics after harvest
- Ensures balance hasn’t decreased (can stay the same if harvested recently)
- Confirms price per share hasn’t decreased
-
Sequential State Change Validation:
- Uses
getStateChangesUint()
to monitor all balance changes
- Verifies each balance change is valid relative to the previous state
- Prevents manipulation through multiple harvest calls
- Ensures no balance decreases occur at any point during the transaction
The assertion uses the following cheatcodes:
forkPreState()
: Captures pre-harvest metrics
forkPostState()
: Verifies post-harvest metrics
getStateChangesUint()
: Monitors all balance changes
registerCallTrigger()
: Triggers on harvest function calls
This approach ensures that:
- Harvests never decrease vault balances
- Price per share remains stable or increases
- No unauthorized balance modifications occur
- Protocol solvency is maintained
- Multiple harvest calls in the same transaction are handled correctly
For more information about cheatcodes, see the Cheatcodes Documentation.
Code Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {Assertion} from "credible-std/Assertion.sol";
import {PhEvm} from "credible-std/PhEvm.sol";
interface IBeefyVault {
function balance() external view returns (uint256);
function getPricePerFullShare() external view returns (uint256);
function harvest(bool badHarvest) external;
}
// Inspired by https://github.com/beefyfinance/beefy-contracts/blob/master/forge/test/vault/ChainVaultsTest.t.sol#L77-L110
contract BeefyHarvestAssertion is Assertion {
IBeefyVault public vault;
constructor(address vault_) {
vault = IBeefyVault(vault_);
}
function triggers() external view override {
// Register trigger for harvest function calls
registerCallTrigger(this.assertionHarvestIncreasesBalance.selector, vault.harvest.selector);
}
// Assert that the balance of the vault doesn't decrease after a harvest
// and that the price per share doesn't decrease
function assertionHarvestIncreasesBalance() external {
// Check pre-harvest state
ph.forkPreState();
uint256 preBalance = vault.balance();
uint256 prePricePerShare = vault.getPricePerFullShare();
// Check post-harvest state
ph.forkPostState();
uint256 postBalance = vault.balance();
uint256 postPricePerShare = vault.getPricePerFullShare();
// Balance should not decrease after harvest (can stay the same if harvested recently)
require(postBalance >= preBalance, "Harvest decreased balance");
// Price per share should increase or stay the same
require(postPricePerShare >= prePricePerShare, "Price per share decreased after harvest");
// Get all state changes to the balance slot
uint256[] memory balanceChanges = getStateChangesUint(
address(vault),
bytes32(uint256(0)) // First storage slot for balance
);
// Verify that all intermediate balance changes are valid
// Each balance change should be >= the previous balance in the sequence
uint256 lastBalance = preBalance;
for (uint256 i = 0; i < balanceChanges.length; i++) {
require(balanceChanges[i] >= lastBalance, "Invalid balance decrease detected during harvest");
lastBalance = balanceChanges[i];
}
}
}
Note: This code example is maintained in the Phylax Assertion Examples Repository. For a full examples with mock protocol code and tests please refer to the repository.
Testing
To test this assertion:
- Deploy a test instance of the yield farming vault
- Set up initial vault state with assets and rewards
- Execute harvest operations with varying conditions:
- Single harvest with rewards
- Multiple harvests in same transaction
- Harvest with no new rewards
- Verify the assertion correctly:
- Allows harvests that maintain or increase balance
- Prevents harvests that decrease balance
- Maintains price per share stability
- Handles multiple harvest calls correctly
Assertion Best Practices
- Combine with other assertions like ERC20 Drain for comprehensive protection
- Use appropriate balance thresholds based on protocol reward rates
- Consider adding maximum balance increase limits to prevent reward manipulation
- Add checks for price per share throughout the callstack (could be a separate assertion for modularity)