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

state transition validation error where the wrong balance state is being verified against the minimum output requirement

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));
// @FOUND Incorrect balance state check - only verifies temporary asset balance
// instead of total deployed balance, allowing manipulation of the minOut check
_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 {
// @FOUND Critical validation flaw: minOut check is insufficient
// The check "minOut > _amount" doesn't account for potential price manipulation
// between the check and the actual swap execution
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:

  1. The minOut validation (minOut > _amount) assumes a simple 1:1+ ratio

  2. This validation occurs before the actual swap

  3. 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));
// @FOUND Same validation flaw: price check happens before swap execution
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;
+}
Updates

Appeal created

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

Support

FAQs

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