DeFiFoundrySolidity
16,653 OP
View results
Submission Details
Severity: medium
Invalid

_harvestAndReport Function Only Tracks Balances Without Executing Required BaseStrategy Harvesting Logic

Summary

The _harvestAndReport function in StrategyOp implements token accounting logic but fails to execute the core harvesting functionality expected of a BaseStrategy implementation. While the contract contains the necessary harvesting logic in the claimAndSwap function, this critical functionality is not integrated into the reporting cycle.

This architectural flaw means the strategy relies entirely on keeper-initiated manual harvesting rather than performing automated compounding during reports. As a result, rewards can accumulate in the transmuter without being claimed and redeployed, reducing the strategy's effective yield and creating operational friction that requires active keeper maintenance.

Proof of Concept

The current implementation only performs passive balance tracking:

https://github.com/Cyfrin/2024-12-alchemix/blob/main/src/StrategyOp.sol#L161

function _harvestAndReport() internal override returns (uint256 _totalAssets) {
uint256 claimable = transmuter.getClaimableBalance(address(this));
uint256 unexchanged = transmuter.getUnexchangedBalance(address(this));
uint256 underlyingBalance = underlying.balanceOf(address(this));
_totalAssets = unexchanged + asset.balanceOf(address(this)) + underlyingBalance;
}

The actual harvesting logic exists in a separate keeper-only function:

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
// ... slippage checks and deposits
}

This separation means harvesting only occurs through manual keeper intervention rather than automatically during the strategy's reporting cycle. The _harvestAndReport function inherited from BaseStrategy is specifically designed to handle harvesting operations during reports, as indicated by its documentation requiring "harvesting, rewards selling, accrual, redepositing etc."

Recommended mitigation steps

The _harvestAndReport function should be refactored to incorporate the existing harvesting logic while maintaining proper slippage controls and optimal routing. The function should automatically detect claimable rewards, execute the claim-swap-deposit cycle with appropriate safety checks, and return the final token accounting.

error InsufficientOutput();
error ZeroAmount();
uint256 public constant MIN_HARVEST_AMOUNT = 0.1 ether;
uint256 public slippageTolerance = 200; // 2% default slippage
address public constant VELO_FACTORY = address(0x..); // Need to add factory address
function setSlippageTolerance(uint256 _slippageTolerance) external onlyManagement {
require(_slippageTolerance > 0 && _slippageTolerance <= 1000, "Invalid slippage");
slippageTolerance = _slippageTolerance;
}
function _harvestAndReport() internal override returns (uint256 _totalAssets) {
uint256 claimableAmount = transmuter.getClaimableBalance(address(this));
if (claimableAmount >= MIN_HARVEST_AMOUNT) {
IVeloRouter.route[] memory path = _getOptimalSwapPath();
uint256 minOut = _calculateMinOutAmount(claimableAmount);
transmuter.claim(claimableAmount, address(this));
uint256 underlyingBalance = underlying.balanceOf(address(this));
if (underlyingBalance < claimableAmount) {
claimableAmount = underlyingBalance;
}
if (claimableAmount > 0) {
uint256 balanceBefore = asset.balanceOf(address(this));
try this._executeSwap(claimableAmount, minOut, path) {
uint256 swapOutput = asset.balanceOf(address(this)) - balanceBefore;
if (swapOutput < minOut) revert InsufficientOutput();
if (swapOutput > 0) {
transmuter.deposit(swapOutput, address(this));
}
} catch {
// Keep underlying for next harvest if swap fails
}
}
}
_totalAssets = transmuter.getUnexchangedBalance(address(this)) +
asset.balanceOf(address(this)) +
underlying.balanceOf(address(this));
}
function _executeSwap(
uint256 _amount,
uint256 _minOut,
IVeloRouter.route[] calldata _path
) external returns (uint256) {
require(msg.sender == address(this), "Not self");
IVeloRouter(router).swapExactTokensForTokens(
_amount,
_minOut,
_path,
address(this),
block.timestamp
);
return asset.balanceOf(address(this));
}
function _getOptimalSwapPath() internal view returns (IVeloRouter.route[] memory) {
IVeloRouter.route[] memory path = new IVeloRouter.route[]();
path[0] = IVeloRouter.route({
from: address(underlying),
to: address(asset),
stable: false,
factory: VELO_FACTORY
});
return path;
}
function _calculateMinOutAmount(uint256 _amountIn) internal view returns (uint256) {
if (_amountIn == 0) revert ZeroAmount();
uint256[] memory amounts = IVeloRouter(router).getAmountsOut(
_amountIn,
_getOptimalSwapPath()
);
return (amounts[amounts.length - 1] * (10000 - slippageTolerance)) / 10000;
}

This refactored implementation properly integrates harvesting into the reporting cycle while maintaining the strategy's risk controls and optimal execution parameters. Additional helper functions would need to be implemented to determine optimal swap routing and slippage parameters based on market conditions.

Updates

Appeal created

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.