Summary
The balance accounting and validation sequence in the claimAndSwap function
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));
}
The function performs balance validation AFTER executing state-changing operations (claim and swap). The balanceDeployed() calculation includes both underlying and asset tokens, this creates a window where the actual balance can be manipulated between the claim and final check
because the balance check happens after claiming funds, the balanceDeployed() function sums both underlying and asset balances. This means the minOut check can pass even if the swap fails or provides unfavorable rates
This allows for claiming funds from transmuter, manipulating swap conditions, passing the minOut check despite unfavorable swap rates. Resulting in asset value loss while still passing the balance validation
Vulnerability Details
The issue lies in the minimum output enforcement mechanism. The strategies incorrectly validate the minimum output amount by checking the balance difference after both the claim and swap operations have occurred, rather than enforcing the minimum output specifically for the swap operation.
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IRamsesRouter.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));
}
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(...);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
}
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");
}
Keepers can execute trades at unfavorable rates while still passing the minOut check. The strategy could receive less alETH than intended from the WETH->alETH swap, this affects all supported networks (Optimism, Ethereum, Arbitrum) since all strategy variants have the same issue.
Impact
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");
}
The issue is in balanceDeployed()
function balanceDeployed() public view returns (uint256) {
return transmuter.getUnexchangedBalance(address(this)) +
underlying.balanceOf(address(this)) +
asset.balanceOf(address(this));
}
Recommendations
function claimAndSwap(
uint256 _amountClaim,
uint256 _minOut,
IVeloRouter.route[] calldata _path
) external onlyKeepers {
+
+ require(transmuter.getClaimableBalance(address(this)) >= _amountClaim, "Insufficient claimable");
+
+
+ uint256 initialAssetBalance = asset.balanceOf(address(this));
+ uint256 initialUnderlyingBalance = underlying.balanceOf(address(this));
transmuter.claim(_amountClaim, address(this));
- uint256 balBefore = asset.balanceOf(address(this));
+
+
+ require(
+ underlying.balanceOf(address(this)) == initialUnderlyingBalance + _amountClaim,
+ "Claim amount mismatch"
+ );
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
- uint256 balAfter = asset.balanceOf(address(this));
+
+
+ uint256 swapOutput = asset.balanceOf(address(this)) - initialAssetBalance;
+ require(swapOutput >= _minOut, "Insufficient swap output");
+
+
+ require(
+ underlying.balanceOf(address(this)) == initialUnderlyingBalance,
+ "Underlying remainder"
+ );
- require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
+
+function validateSwapRate(uint256 amountIn, uint256 minOut) internal pure {
+
+ require(minOut > amountIn * 101 / 100, "Unprofitable swap rate");
+}