Vulnerability Details
The claimAndSwap
function deposits the entire alETH balance of the strategy to the transmuter after performing a swap, instead of only depositing the newly acquired alETH from the swap operation.
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path ) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
@> uint256 balBefore = asset.balanceOf(address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
@> uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
@> transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
PoC
Add the following test on Operation.t.sol
and run: make test-test test=test_claim_and_swap_balance_deposit_the_entire_balance
function test_claim_and_swap_balance_deposit_the_entire_balance() public {
uint256 amount = 100e18;
mintAndDepositIntoStrategy(strategy, user, amount);
deployMockYieldToken();
addMockYieldToken();
depositToAlchemist(amount);
airdropToMockYield(amount / 2);
airdrop(asset, user2, amount);
vm.prank(user2);
asset.approve(address(transmuter), amount);
vm.prank(user2);
transmuter.deposit(amount/2, user2);
harvestMockYield();
vm.prank(address(transmuterKeeper));
transmuterBuffer.exchange(address(underlying));
skip(7 days);
uint256 looseBalance = 50e18;
airdrop(asset, address(strategy), looseBalance);
uint256 strategyBalanceBefore = asset.balanceOf(address(strategy));
uint256 transmuterBalanceBefore = transmuter.getUnexchangedBalance(address(strategy));
uint256 claimable = strategy.claimableBalance();
assertEq(strategyBalanceBefore, looseBalance, "Strategy should have loose balance");
assertGt(claimable, 0, "Should have claimable amount");
vm.prank(keeper);
if (block.chainid == 1) {
strategy.claimAndSwap(
claimable,
claimable + 1e17,
0
);
}
uint256 transmutedDiff = transmuter.getUnexchangedBalance(address(strategy)) - transmuterBalanceBefore;
assertGt(transmutedDiff, looseBalance, "Loose balance incorrectly deposited");
}
Output:
The entire balance is deposited.
[PASS] test_claim_and_swap_balance_deposit_the_entire_balance() (gas: 3966957)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 30.96s (25.98s CPU time)
Impact
When the strategy has existing alETH balance before claim and swap:
All loose alETH gets locked in the transmuter.
Strategy loses liquidity needed for potential withdrawals.
Assets get double-counted in accounting since they were already part of totalAssets
.
Tools Used
Manual Review & Foundry
Recommendations
Only deposit the newly swapped amount to transmuter.
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path ) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
- transmuter.deposit(asset.balanceOf(address(this)), address(this));
+ transmuter.deposit(balAfter - balBefore), address(this));
}