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

Not claiming the rewards from the Transmuter in `_harvestAndReport()` might lead to inaccurate yield reported.

Summary

In the current implementation of the strategies, the _harvestAndReport() is only calculating the total assets under management after claimAndSwap() is executed. However, it doesn't account for any yield that can be generated between the execution of claimAndSwap() and the execution of _harvestAndReport().

Vulnerability Details

In order for the strategy to account the yield, that the strategy is generated, report() is called. However, the current implementation of the _harvestAndReport() expects that claimAndSwap() is executed beforehand to account for the yield, generated by the Transmuter.

This approach is not following the recommended implementation by Yearn ref:

Called during every report. This should harvest and sell any rewards, reinvest any proceeds, perform any position maintenance and return a full accounting of a trusted amount denominated in the underlying asset the strategy holds.

and ref

report: Called by management or keepers to accrue all profits or losses, charge fees, and lock profit to be distributed.

Moreover, this approach will not account for the yield generated between the call of claimAndSwap() and _harvestAndReport(), which will result in wrong value of total assets in the Tokenized strategy.

Impact

The reported amount will not account for the yieldyield generated between the call of claimAndSwap() and _harvestAndReport()

PoC

NOTE that for this PoC to correctly show the yield lost, the fix where the claimable amount is included in the calculation of total assets should be applied into all Strategies:

function _harvestAndReport()
internal
override
returns (uint256 _totalAssets)
{
uint256 claimable = transmuter.getClaimableBalance(address(this));
uint256 unexchanged = transmuter.getUnexchangedBalance(address(this));
// NOTE : possible some dormant WETH that isn't swapped yet
uint256 underlyingBalance = underlying.balanceOf(address(this));
@> _totalAssets = unexchanged + asset.balanceOf(address(this)) + claimable;
}
pragma solidity ^0.8.18;
import "forge-std/console.sol";
import {Setup, ERC20, IStrategyInterface} from "./utils/Setup.sol";
import {IStrategyInterfaceVelo} from "../interfaces/IStrategyInterface.sol";
import {IStrategyInterfaceRamses} from "../interfaces/IStrategyInterface.sol";
import {IVeloRouter} from "../interfaces/IVelo.sol";
import {IRamsesRouter} from "../interfaces/IRamses.sol";
contract PoCTest is Setup {
function setUp() public virtual override {
super.setUp();
deployMockYieldToken();
addMockYieldToken();
}
function claimAndSwap(uint256 claimable) public {
vm.prank(keeper);
if (block.chainid == 1) {
// Mainnet
IStrategyInterface(address(strategy)).claimAndSwap(claimable, claimable * 101 / 100, 0);
} else if (block.chainid == 10) {
// NOTE on OP we swap directly to WETH
IVeloRouter.route[] memory veloRoute = new IVeloRouter.route[]();
veloRoute[0] =
IVeloRouter.route(address(underlying), address(asset), true, 0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a);
// Velo Iterface
IStrategyInterfaceVelo(address(strategy)).claimAndSwap(claimable, claimable * 101 / 100, veloRoute);
} else if (block.chainid == 42161) {
// ARB
// NOTE we swap first to eFrax and then to WETH
IRamsesRouter.route[] memory ramsesRoute = new IRamsesRouter.route[]();
address eFrax = 0x178412e79c25968a32e89b11f63B33F733770c2A;
ramsesRoute[0] = IRamsesRouter.route(address(underlying), eFrax, true);
ramsesRoute[1] = IRamsesRouter.route(eFrax, address(asset), true);
IStrategyInterfaceRamses(address(strategy)).claimAndSwap(claimable, claimable * 101 / 100, ramsesRoute);
} else {
revert("Chain ID not supported");
}
}
function claimAndSwapThatReverts() public view {
console.log("price should be better than 1:1");
}
function alchemistDepositYieldIntoTransmuter(uint256 amount) public {
airdrop(underlying, address(transmuterBuffer), amount);
vm.prank(address(transmuterKeeper));
transmuterBuffer.exchange(address(underlying));
}
function logStrategyData() public view {
console.log("------------------Strategy Data------------------");
console.log("Total Assets:", strategy.totalAssets());
console.log("Claimable:", strategy.claimableBalance());
console.log("Unexchanged Balance:", strategy.unexchangedBalance());
console.log("Exchangable Balance:", transmuter.getExchangedBalance(address(strategy)));
console.log("\n");
}
function testPoC() public {
// Loss of yield, because we are not accounting for it.
// Harvest and report doesn't account for the yield between the claimAndSwap() and report()
// Deposit
uint256 amount = 1 ether;
mintAndDepositIntoStrategy(strategy, user, amount);
console.log("Amount deposited:", amount);
logStrategyData();
// Mock alchemic harvesting yield and depositing it in transmuterBuffer and then calling exchange
alchemistDepositYieldIntoTransmuter(amount);
// Show that there is balance to be claimed
console.log("Amount deposited:", amount);
logStrategyData();
// Swap
console.log("> Claim and Swap the claimable balance");
uint256 claimableBalance = strategy.claimableBalance();
claimAndSwap(claimableBalance);
logStrategyData();
// Execute exchange again - some yield is deposited between claimAndSwap and report!
console.log("> Alchemist Deposits Yield into Transmuter \n");
alchemistDepositYieldIntoTransmuter(amount);
// Harvest and report
console.log("> Harvest and Report");
vm.prank(keeper);
strategy.report();
logStrategyData();
uint256 totalAssetsBefore = strategy.totalAssets();
// Swap again and report right after as if they are in the same tx.
console.log("> Claim and Swap right before Harvest and Report");
claimableBalance = strategy.claimableBalance();
claimAndSwap(claimableBalance);
vm.prank(keeper);
strategy.report();
logStrategyData();
uint256 totalAssetsAfter = strategy.totalAssets();
console.log("Yield not accounted for: ", totalAssetsAfter - totalAssetsBefore);
}
}

Logs:

Amount deposited: 1000000000000000000
------------------Strategy Data------------------
Total Assets: 1000000000000000000
Claimable: 0
Unexchanged Balance: 1000000000000000000
Exchangable Balance: 0
Amount deposited: 1000000000000000000
------------------Strategy Data------------------
Total Assets: 1000000000000000000
Claimable: 22526036641519837
Unexchanged Balance: 977473963358480163
Exchangable Balance: 22526036641519837
> Claim and Swap the claimable balance
------------------Strategy Data------------------
Total Assets: 1000000000000000000
Claimable: 0
Unexchanged Balance: 1000310338400702524
Exchangable Balance: 0
> Alchemist Deposits Yield into Transmuter
> Harvest and Report
------------------Strategy Data------------------
Total Assets: 1000310338400702524
Claimable: 22532869815303332
Unexchanged Balance: 977777468585399192
Exchangable Balance: 22532869815303332
> Claim and Swap right before Harvest and Report
------------------Strategy Data------------------
Total Assets: 1000620618544195983
Claimable: 0
Unexchanged Balance: 1000620618544195983
Exchangable Balance: 0
Yield not accounted for: 310280143493459

Tools Used

Manual review

Recommendations

Call claimAndSwap in the _harvestAndReport() function, to account for the total yield generated until the call of report().

Updates

Lead Judging Commences

inallhonesty Lead Judge
7 months ago

Appeal created

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Incorrect accounting in `_harvestAndReport` claimable should be included

dobrevaleri Submitter
7 months ago
inallhonesty Lead Judge
7 months ago
inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Incorrect accounting in `_harvestAndReport` claimable should be included

Support

FAQs

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