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

Incorrect total balance deposit in `claimAndSwap`

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");
// @audit: deposits entire balance, not just swap proceeds(balAfter - balBefore)
@> transmuter.deposit(asset.balanceOf(address(this)), address(this));
}

PoC

Add the following test on Operation.t.soland 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;
// Initial strategy deposit
mintAndDepositIntoStrategy(strategy, user, amount);
// Setup conditions for strategy to have claimable WETH
deployMockYieldToken();
addMockYieldToken();
depositToAlchemist(amount);
airdropToMockYield(amount / 2);
// First claim setup
airdrop(asset, user2, amount);
vm.prank(user2);
asset.approve(address(transmuter), amount);
vm.prank(user2);
transmuter.deposit(amount/2, user2);
// Generate claimable amount
harvestMockYield();
vm.prank(address(transmuterKeeper));
transmuterBuffer.exchange(address(underlying));
skip(7 days);
// Send additional alETH directly to strategy
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");
// Perform claim and swap - we need minOut > claimable amount to match the check in the contract
vm.prank(keeper);
if (block.chainid == 1) {
strategy.claimAndSwap(
claimable,
claimable + 1e17, // slightly more than input amount
0
);
}
// @audit Check balances - the loose balance should not have been sent to transmuter
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:

  1. All loose alETH gets locked in the transmuter.

  2. Strategy loses liquidity needed for potential withdrawals.

  3. 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));
}
Updates

Appeal created

inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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