Summary
In claimAndSwap function, the profitability checks for token swaps, the balanceDeployed() function (which correctly accounts for both tokens) can decrease after a swap
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));
}
because the profitability check uses an incorrect accounting model:
The check only verifies that received alETH (asset) meets minOut
It completely ignores the value of WETH (underlying) being spent
This creates a mathematical flaw where total value can decrease while still passing the check
For example:
Initial: 100 WETH ($100k) + 0 alETH
Swap: 100 WETH for 90 alETH
Check passes because alETH increased by 90 > minOut
But total value decreased from $100k to $90k
Vulnerability Details
In claimAndSwap() function of StrategyOp.sol across all three strategy implementations (StrategyOp.sol, StrategyMainnet.sol, and StrategyArb.sol) in their claimAndSwap functions.
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));
}
It's because of an incorrect economic assumption in the profitability verification. The strategies track total value through balanceDeployed():
function balanceDeployed() public view returns (uint256) {
return transmuter.getUnexchangedBalance(address(this)) +
underlying.balanceOf(address(this)) +
asset.balanceOf(address(this));
}
But the claimAndSwap#L87 check only verifies:
require((balAfter - balBefore) >= _minOut, "Slippage too high");
+------------------+----------+--------------------------------+--------------------------------+
| Implementation | DEX Used | Key Issues | Impact |
+------------------+----------+--------------------------------+--------------------------------+
| StrategyOp | Velodrome| • Velodrome swaps | • Depositors lose value |
| (Optimism) | | • Unprofitable WETH->alETH | • Keepers extract value |
| | | swaps by keepers | • minOut param insufficient |
+------------------+----------+--------------------------------+--------------------------------+
| StrategyMainnet | Curve | • Curve swaps | • Depositors lose value |
| (Ethereum) | | • Swap verification | • Keepers extract value |
| | | vulnerability | • minOut param insufficient |
+------------------+----------+--------------------------------+--------------------------------+
| StrategyArb | Ramses | • Ramses swaps | • Depositors lose value |
| (Arbitrum) | | • Profitability check | • Keepers extract value |
| | | issue | • minOut param insufficient |
+------------------+----------+--------------------------------+--------------------------------+
*/
Impact
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));
}
Economic Impact:
Strategy can lose value while passing all checks
Example: Trade 100 WETH ($100k) for 95 alETH ($95k)
Check passes because 95 alETH > minOut
Strategy loses $5k in value
Systemic Risk:
Compromises Yearn V3's tokenized strategy value preservation
Affects all depositors' shares value
Creates arbitrage opportunities against the strategy
Because the code assumes checking alETH increase is sufficient, but fails to verify the economic relationship between WETH spent and alETH received. This creates a mathematical gap where value can be extracted while satisfying all existing checks.
Recommendations
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");
+
+ uint256 totalValueBefore = balanceDeployed();
+
+
+ transmuter.claim(_amountClaim, address(this));
+ _swapUnderlyingToAsset(_amountClaim, _minOut, _path);
+
+
+ uint256 totalValueAfter = balanceDeployed();
+ require(totalValueAfter > totalValueBefore, "Trade must be profitable");
+
+
+ require(_minOut >= _amountClaim * 101 / 100, "Minimum 1% premium required");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
+
+ function _verifyProfitablePrice(uint256 amountIn, uint256 amountOut) internal view {
+
+ uint256 oraclePrice = getOraclePrice();
+ uint256 executionPrice = (amountOut * 1e18) / amountIn;
+ require(executionPrice >= oraclePrice, "Price below oracle");
+ }