Summary
In the claimAndSwap function's balance verification logic. The vulnerability exists because the minOut check is performed on the wrong balance state. minOutEnforced verifies that the total deployed balance increases by at least minOut, but the contract checks only the temporary asset balance before the final transmuter deposit.In StrategyOp.claimAndSwap
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));
}
due to incorrect assumptions about state transitions and balance accounting. The code assumes checking the temporary asset balance difference is sufficient, but this doesn't account for the full state change including the transmuter deposit. This creates a disparity.
Vulnerability Details
In the claimAndSwap function's minimum output enforcement across all three strategy contracts (StrategyOp.sol, StrategyMainnet.sol, and StrategyArb.sol).
function _swapUnderlyingToAsset(uint256 _amount, uint256 minOut, IVeloRouter.route[] calldata _path) internal {
require(minOut > _amount, "minOut too low");
uint256 underlyingBalance = underlying.balanceOf(address(this));
require(underlyingBalance >= _amount, "not enough underlying balance");
IVeloRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);
}
The bug exists because:
The minOut validation (minOut > _amount) assumes a simple 1:1+ ratio
This validation occurs before the actual swap
The price could be manipulated between validation and execution
The same issue exists in StrategyMainnet.sol
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));
}
Impact
A malicious actor could manipulate the price between validation and swap execution. This could result in the strategy receiving less alETH than intended, the transmuter's balance accounting could be manipulated
The fundamental flaw in the assumption that a simple comparison between minOut and amount is sufficient to prevent unfavorable trades. The validation needs to incorporate price oracle checks or implement more sophisticated slippage protection mechanisms.
Recommendations
Concrete mitigations for the minimum output validation
// StrategyOp.sol
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
+ // @Mitigation - Track total balance before operations
+ uint256 totalBefore = balanceDeployed();
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));
+ // @Mitigation - Verify total balance increase meets minimum
+ require(balanceDeployed() - totalBefore >= _minOut, "Insufficient output");
}
function _swapUnderlyingToAsset(uint256 _amount, uint256 minOut, IVeloRouter.route[] calldata _path) internal {
- require(minOut > _amount, "minOut too low");
+ // @Mitigation - Add price oracle check
+ uint256 minAcceptableOut = _getMinimumAcceptableOutput(_amount);
+ require(minOut >= minAcceptableOut, "Output below minimum threshold");
uint256 underlyingBalance = underlying.balanceOf(address(this));
require(underlyingBalance >= _amount, "not enough underlying balance");
IVeloRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);
}
+// @Mitigation - Add oracle-based minimum output calculation
+function _getMinimumAcceptableOutput(uint256 amount) internal view returns (uint256) {
+ // Implement oracle price check with safety margin
+ uint256 oraclePrice = getOraclePrice(); // Implementation needed
+ uint256 minAcceptable = (amount * oraclePrice * 98) / 100; // 2% slippage tolerance
+ return minAcceptable;
+}