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));
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) {
IStrategyInterface(address(strategy)).claimAndSwap(claimable, claimable * 101 / 100, 0);
} else if (block.chainid == 10) {
IVeloRouter.route[] memory veloRoute = new IVeloRouter.route[]();
veloRoute[0] =
IVeloRouter.route(address(underlying), address(asset), true, 0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a);
IStrategyInterfaceVelo(address(strategy)).claimAndSwap(claimable, claimable * 101 / 100, veloRoute);
} else if (block.chainid == 42161) {
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 {
uint256 amount = 1 ether;
mintAndDepositIntoStrategy(strategy, user, amount);
console.log("Amount deposited:", amount);
logStrategyData();
alchemistDepositYieldIntoTransmuter(amount);
console.log("Amount deposited:", amount);
logStrategyData();
console.log("> Claim and Swap the claimable balance");
uint256 claimableBalance = strategy.claimableBalance();
claimAndSwap(claimableBalance);
logStrategyData();
console.log("> Alchemist Deposits Yield into Transmuter \n");
alchemistDepositYieldIntoTransmuter(amount);
console.log("> Harvest and Report");
vm.prank(keeper);
strategy.report();
logStrategyData();
uint256 totalAssetsBefore = strategy.totalAssets();
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()
.