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

Incorrect assumptions about state transitions and insufficient atomic validation between the strategy's requirements and the router's execution

Summary

function _swapUnderlyingToAsset(uint256 _amount, uint256 minOut, IVeloRouter.route[] calldata _path) internal {
// @FOUND The minOut check is performed locally but not enforced at the router level, allowing the swap to execute with any output amount
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);
}

From the disconnect between local validation and router-level enforcement. While the strategy performs a local check require(minOut > _amount), this value is not properly enforced in the actual swap execution. The router's swapExactTokensForTokens function receives the minOut parameter, but there's no guarantee that the router respects or enforces this value in the same way the strategy expects.

This creates a scenario where:

  1. Local validation passes (minOut > _amount)

  2. Swap executes through router

  3. Router could return any amount of tokens

  4. Strategy's post-swap balance check would fail, but the swap has already occurred

Vulnerability Details

In StrategyArb.claimAndSwap

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IRamsesRouter.route[] calldata _path) external onlyKeepers {
// @FOUND Critical vulnerability: The transmuter.claim() is called before validating if the swap will be profitable
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));
}

In StrategyOp.claimAndSwap

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
// @FOUND Same vulnerability: Claims WETH before ensuring swap profitability
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));
}

Because the strategy claims WETH from the transmuter before ensuring the swap will be profitable. This creates a critical state transition issue:

  1. WETH is claimed from transmuter

  2. If the swap fails or reverts due to slippage

  3. The strategy is left holding WETH instead of alETH

  4. This breaks the core invariant that funds should always be deployed in the transmuter unless actively being swapped

Impact

  • Strategy can be left with undeployed WETH if swap fails

  • Loss of yield generation potential

Recommendations

// StrategyArb.sol, StrategyOp.sol, StrategyMainnet.sol
- 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));
- }
+ function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
+ // @Mitigation - Validate profitability before claiming
+ require(_minOut > _amountClaim, "Unprofitable swap ratio");
+
+ // @Mitigation - Query expected output before state changes
+ uint256 expectedOut = router.getAmountsOut(_amountClaim, _path)[_path.length - 1];
+ require(expectedOut >= _minOut, "Insufficient expected output");
+
+ // @Mitigation - Execute operations in atomic transaction
+ transmuter.claim(_amountClaim, address(this));
+
+ uint256 balBefore = asset.balanceOf(address(this));
+ _swapUnderlyingToAsset(_amountClaim, _minOut, _path);
+ uint256 balAfter = asset.balanceOf(address(this));
+
+ // @Mitigation - Verify actual output meets requirements
+ require((balAfter - balBefore) >= _minOut, "Slippage too high");
+
+ // @Mitigation - Immediately redeposit to maintain maximum deployment
+ transmuter.deposit(asset.balanceOf(address(this)), address(this));
+
+ // @Mitigation - Emit event for monitoring
+ emit SwapExecuted(_amountClaim, balAfter - balBefore, _path[0], _path[_path.length-1]);
+ }
+ // @Mitigation - Add event for tracking swaps
+ event SwapExecuted(uint256 amountIn, uint256 amountOut, address tokenIn, address tokenOut);
Updates

Lead Judging Commences

inallhonesty Lead Judge
11 months ago

Appeal created

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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