Summary
In StrategyMainnet, StrategyArb, and StrategyOp, the claimAndSwap function immediately deposits all received alETH (including the premium/profit) back into the transmuter, preventing the strategy from recording any profit. This leads to inaccurate profit reporting.
Vulnerability Details
The claimAndSwap function claims WETH, swaps it for alETH at a premium, but then deposits all received alETH back into the transmuter:
function claimAndSwap(
uint256 _amountClaim,
uint256 _minOut,
uint256 _routeNumber
) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
require(_minOut > _amountClaim, "minOut too low");
router.exchange(
routes[_routeNumber],
swapParams[_routeNumber],
_amountClaim,
_minOut,
pools[_routeNumber],
address(this)
);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
The issue arises because:
-
The strategy swaps WETH for alETH at a premium (profit)
-
Instead of keeping the premium as profit, all alETH is deposited into the transmuter
-
And, during withdrawal the TokenizedStrategy.solaccount for idle by checking the balance of asset which means any directly transferred profit will be seen as idle cash.
uint256 idle = _asset.balanceOf(address(this));
uint256 loss;
if (idle < assets) {
unchecked {
IBaseStrategy(address(this)).freeFunds(assets - idle);
}
idle = _asset.balanceOf(address(this));
if (idle < assets) {
unchecked {
loss = assets - idle;
}
if (maxLoss < MAX_BPS) {
require(
loss <= (assets * maxLoss) / MAX_BPS,
"too much loss"
);
}
assets = idle;
}
}
-
When _harvestAndReport() is called, it only sees:
Impact
Loss of Profit for strategy users and protocol
Impact = High
Likelihood = High
Tools Used
Manual
Recommendations
Modify claimAndSwap to retain the premium and exclude totalProfitsfrom availableWithdrawLimit()
uint256 public totalProfits;
function claimAndSwap(
uint256 _amountClaim,
uint256 _minOut,
uint256 _routeNumber
) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
router.exchange(
routes[_routeNumber],
swapParams[_routeNumber],
_amountClaim,
_minOut,
pools[_routeNumber],
address(this)
);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
uint256 profit = balAfter - balBefore - _amountClaim;
totalProfits += profit;
transmuter.deposit(_amountClaim, address(this));
}
function _harvestAndReport()
internal
override
returns (uint256 _totalAssets)
{
uint256 unexchanged = transmuter.getUnexchangedBalance(address(this));
_totalAssets = unexchanged + asset.balanceOf(address(this)) + underlying.balanceOf(address(this)) + totalProfits;
totalProfits = 0;
}