Summary
The _harvestAndReport
function in StrategyMainnet
, StrategyOp
, and StrategyArb
do not implement Yearn's V3 tokenized strategy requirements for harvesting and deploying idle funds, potentially impacting capital efficiency and yield generation.
Vulnerability Details
According to Yearn's documentation for _harvestAndReport
:
https://docs.yearn.fi/developers/v3/strategy_writing_guide#_harvestandreport
https://github.com/Cyfrin/2024-12-alchemix/blob/82798f4891e41959eef866bd1d4cb44fc1e26439/src/StrategyMainnet.sol#L151-L156
@> @dev Internal function to harvest all rewards, redeploy any idle
@> funds and return an accurate accounting of all funds currently
@> held by the Strategy.
@> This should do any needed harvesting, rewards selling, accrual,
@> redepositing etc. to get the most accurate view of current assets.
Current implementation in all three strategies:
https://github.com/Cyfrin/2024-12-alchemix/blob/82798f4891e41959eef866bd1d4cb44fc1e26439/src/StrategyMainnet.sol#L172-L192
https://github.com/Cyfrin/2024-12-alchemix/blob/82798f4891e41959eef866bd1d4cb44fc1e26439/src/StrategyOp.sol#L161-L174
https://github.com/Cyfrin/2024-12-alchemix/blob/82798f4891e41959eef866bd1d4cb44fc1e26439/src/StrategyArb.sol#L148-L171
function _harvestAndReport() internal override returns (uint256 _totalAssets) {
uint256 claimable = transmuter.getClaimableBalance(address(this));
if (claimable > 0) {
}
uint256 unexchanged = transmuter.getUnexchangedBalance(address(this));
uint256 underlyingBalance = underlying.balanceOf(address(this));
_totalAssets = unexchanged + asset.balanceOf(address(this)) + underlyingBalance;
}
This implementation:
Fails to deploy idle funds as required by Yearn's specification.
Fails to claim WETH.
Doesn't match test expectations in Operation.t.sol
vm.prank(keeper);
(uint256 profit, uint256 loss) = strategy.report();
assertEq(strategy.claimableBalance(), 0, "!claimableBalance");
Notice that the report
function is the one responsible for triggering harvestAndReport
. Now let's look at this report
function:
https://github.com/yearn/tokenized-strategy/blob/9ef68041bd034353d39941e487499d111c3d3901/src/TokenizedStrategy.sol#L1090-L1095
function report()
external
nonReentrant
onlyKeepers
returns (uint256 profit, uint256 loss)
{
StrategyData storage S = _strategyStorage();
@>
@>
@>
@>
@>
uint256 newTotalAssets = IBaseStrategy(address(this))
.harvestAndReport();
Impact
Funds remain idle when they should be deployed and WETH claims are not processed during harvests, directly reducing yields for all depositors.
Every harvest cycle misses yield generation on 100% of idle balances and fails to make underlying WETH available for profitable depeg opportunities.
Likelihood: High(100% of the time) and Impact High(loss of funds/yield generation), break the protocol invariant of providing yield on users deposits.
Tools Used
Manual Review
Recommendations
Function has to deploy idle assets and claim available WETH. Implement the following across all three strategies:
function _harvestAndReport() internal override returns (uint256 _totalAssets) {
if (!TokenizedStrategy.isShutdown()) {
uint256 idleBalance = asset.balanceOf(address(this));
if (idleBalance > 0) {
transmuter.deposit(idleBalance, address(this));
}
uint256 claimable = transmuter.getClaimableBalance(address(this));
if (claimable > 0) {
transmuter.claim(claimable, address(this));
}
}
uint256 unexchanged = transmuter.getUnexchangedBalance(address(this));
uint256 underlyingBalance = underlying.balanceOf(address(this));
_totalAssets = unexchanged +
asset.balanceOf(address(this)) +
underlyingBalance;
}