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);
}
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;
address public constant VELO_FACTORY = address(0x..);
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 {
}
}
}
_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.