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

slippage check require(_minOut > _amountClaim, "minOut too low") is incorrect

Summary

the minimum output amount not met, in the claimAndSwap function

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
// VULNERABILITY: Double slippage check with incorrect logic
// First check assumes output must always be > input which is incorrect
require(_minOut > _amountClaim, "minOut too low");
router.exchange(
routes[_routeNumber],
swapParams[_routeNumber],
_amountClaim,
_minOut,
pools[_routeNumber],
address(this)
);
// Second check that actually enforces slippage protection correctly
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}

The function implements two separate slippage checks. The first check require(_minOut > _amountClaim) incorrectly assumes the output amount must always be greater than the input amount, this assumption breaks the core arbitrage functionality of the strategy since profitable trades can exist even when output < input in certain market conditions. The second check require((balAfter - balBefore) >= _minOut) is the correct implementation

The first check can prevent valid and profitable trades from executing, even when they would satisfy the actual slippage requirements checked by the second require statement.

This vulnerability effectively breaks the strategy's ability to perform arbitrage when market conditions would make it profitable to trade at rates where output < input, despite meeting the keeper's specified minimum output requirements.

Vulnerability Details

The strategies implement incorrect slippage validation in the claimAndSwap function that prevents legitimate arbitrage opportunities.

In StrategyMainnet.claimAndSwap()

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
// BUG: This check prevents valid arbitrage opportunities where output < input
// but the trade is still profitable due to market conditions
require(_minOut > _amountClaim, "minOut too low");
router.exchange(...);
}

In StrategyOp.claimAndSwap()

StrategyOp._swapUnderlyingToAsset()

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);
}
function _swapUnderlyingToAsset(uint256 _amount, uint256 minOut, IVeloRouter.route[] calldata _path) internal {
// BUG: Same incorrect validation that breaks arbitrage functionality
require(minOut > _amount, "minOut too low");
IVeloRouter(router).swapExactTokensForTokens(...);
}

In StrategyArb._swapUnderlyingToAsset

function _swapUnderlyingToAsset(uint256 _amount, uint256 minOut, IRamsesRouter.route[] calldata _path) internal {
// BUG: Same flawed assumption about arbitrage mechanics
require(minOut > _amount, "minOut too low");
IRamsesRouter(router).swapExactTokensForTokens(...);
}
/**
* Impact Analysis
* ==============
*
* +------------------------+------------------------------------------------+
* | Category | Details |
* +------------------------+------------------------------------------------+
* | Core Functionality | • Strategy arbitrage between WETH/alETH broken |
* | Breakdown | • Trades blocked when output < input |
* | • Arbitrage mechanism broken across chains |
* +------------------------+------------------------------------------------+
* | User Impact | • Keepers: Cannot execute valid arbitrage |
* | • Depositors: Miss yield opportunities |
* | • Managers: Cannot optimize returns |
* +------------------------+------------------------------------------------+
* | Chain Impact | • Ethereum: Curve operations affected |
* | • Optimism: Velodrome operations affected |
* | • Arbitrum: Ramses operations affected |
* +------------------------+------------------------------------------------+
*
* Root Cause:
* -----------
* Bug stems from fundamental misunderstanding of arbitrage mechanics:
* - Profitable opportunities can exist when output < input
* - This is especially true for WETH/alETH relationships
* - Current validation incorrectly blocks these opportunities
* - Impact spans all supported chains and DEX integrations
*/

Impact

The strategies enforce a strict requirement that the minimum output must always be greater than the input amount

// In StrategyMainnet.sol
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
// BUG: Forces minOut > input amount, breaking valid arbitrage opportunities
require(_minOut > _amountClaim, "minOut too low");
}

This requirement exists in all variants:

  • StrategyMainnet.sol (Ethereum) - Using Curve

  • StrategyOp.sol (Optimism) - Using Velodrome

  • StrategyArb.sol (Arbitrum) - Using Ramses

/**
* @notice Analysis of cross-chain arbitrage disruption
*
* +-----------------------+------------------------------------------+
* | Impact Category | Details |
* +-----------------------+------------------------------------------+
* | Arbitrage Disruption | • Blocks valid trades (output < input) |
* | • Example: WETH/alETH at 0.98 blocked |
* | • Affects all supported DEXs |
* +-----------------------+------------------------------------------+
* | Chain-Specific | • Ethereum: Curve arbitrage blocked |
* | Effects | • Optimism: Velodrome MM disrupted |
* | • Arbitrum: Ramses strategies broken |
* +-----------------------+------------------------------------------+
* | Economic Impact | • Lost depositor yield |
* | • Reduced WETH/alETH market efficiency |
* | • Missed cross-chain opportunities |
* +-----------------------+------------------------------------------+
* | User Impact | • Keepers: Valid trades blocked |
* | • Depositors: Yield opportunities lost |
* | • Protocol: Arbitrage system hampered |
* +-----------------------+------------------------------------------+
*
* @dev Systemic Implications:
* Bug fundamentally breaks the arbitrage system's core purpose within
* Yearn's V3 tokenized strategy system. Strategies should capture any
* profitable arbitrage opportunity that benefits the overall position,
* regardless of input/output ratio.
*
* @dev Strategy Purpose Contradiction:
* Current implementation prevents capturing price inefficiencies between
* WETH and alETH across multiple chains and DEXs, directly opposing the
* strategy's intended function in the ecosystem.
*/

Recommendations

For 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");
+ // Only enforce actual balance change validation
+ // This allows any profitable trade regardless of input/output ratio
router.exchange(
routes[_routeNumber],
swapParams[_routeNumber],
_amountClaim,
_minOut,
pools[_routeNumber],
address(this)
);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}

For StrategyOp.sol

function _swapUnderlyingToAsset(
uint256 _amount,
uint256 minOut,
IVeloRouter.route[] calldata _path
) internal {
- require(minOut > _amount, "minOut too low");
+ // Validate underlying balance availability only
+ // Let actual DEX trade determine if output meets requirements
uint256 underlyingBalance = underlying.balanceOf(address(this));
require(underlyingBalance >= _amount, "not enough underlying balance");
IVeloRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);
}

For StrategyArb.sol

function _swapUnderlyingToAsset(
uint256 _amount,
uint256 minOut,
IRamsesRouter.route[] calldata _path
) internal {
- require(minOut > _amount, "minOut too low");
+ // Add balance validation and rely on DEX slippage protection
+ require(underlying.balanceOf(address(this)) >= _amount, "insufficient balance");
IRamsesRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);
}
Updates

Appeal created

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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