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));
require(_minOut > _amountClaim, "minOut too low");
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));
}
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));
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 {
require(minOut > _amount, "minOut too low");
IVeloRouter(router).swapExactTokensForTokens(...);
}
In StrategyArb._swapUnderlyingToAsset
function _swapUnderlyingToAsset(uint256 _amount, uint256 minOut, IRamsesRouter.route[] calldata _path) internal {
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
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
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");
+
+
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");
+
+
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");
+
+ require(underlying.balanceOf(address(this)) >= _amount, "insufficient balance");
IRamsesRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);
}