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

balance check only verifies the immediate balance difference but fails to consider the opportunity cost of the claimed WETH

Summary

In the profitability checks of the claimAndSwap function across all strategy variants (StrategyArb.sol, StrategyMainnet.sol, StrategyOp.sol).

The affected code with the root cause indicator

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
// @check The profitability check is fundamentally flawed because it only compares the output amount
// to the input amount (minOut > amount) without considering the actual value relationship between
// WETH and alETH. This allows trades that appear profitable in nominal terms but result in value loss.
_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));
}

The vulnerability is because the contract assumes that receiving more alETH than the WETH input guarantees profitability. This assumption is incorrect because:

  1. The check minOut > amount only compares nominal amounts

  2. The balance comparison balAfter - balBefore >= _minOut fails to account for the true value relationship between WETH and alETH

  3. No oracle or price verification is implemented to ensure the exchange rate is favorable

This allows a keeper to execute trades that pass all current checks but actually decrease the total value of the strategy's holdings, breaking the core invariant that all keeper actions should be profitable.

Vulnerability Details

The bug incubets in the claimAndSwap function, which is implemented similarly across all chains.

// From StrategyArb.sol, StrategyMainnet.sol, StrategyOp.sol
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
// @check:- vulnerability: The profitability check is based on a flawed assumption
// that getting more alETH than WETH input equals profit. This ignores:
// 1. The actual market value relationship between WETH and alETH
// 2. The opportunity cost of the claimed WETH
// 3. The lack of oracle price validation
// This allows value-extracting trades that appear profitable but actually decrease strategy value
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 relies solely on comparing nominal amounts:

In StrategyOp.sol (Optimism)#_swapUnderlyingToAsset

function _swapUnderlyingToAsset(uint256 _amount, uint256 minOut, IVeloRouter.route[] calldata _path) internal {
// @check Missing crucial price validation before Velodrome swap
require(minOut > _amount, "minOut too low");
IVeloRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);
}

In StrategyMainnet.sol (Ethereum)#claimAndSwap

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
// @check Same vulnerability with Curve router - no proper value verification
require(_minOut > _amountClaim, "minOut too low");
router.exchange(routes[_routeNumber], swapParams[_routeNumber], _amountClaim, _minOut, pools[_routeNumber], address(this));
}

In StrategyArb.sol (Arbitrum)#_swapUnderlyingToAsset

function _swapUnderlyingToAsset(uint256 _amount, uint256 minOut, IRamsesRouter.route[] calldata _path) internal {
// @check Ramses router implementation has the same flaw
require(minOut > _amount, "minOut too low");
IRamsesRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);
}

This affects all three blockchain deployments (Optimism, Ethereum, Arbitrum) and allows:

  • Value extraction through seemingly profitable but actually loss-making trades

  • Gradual erosion of strategy value through repeated suboptimal swaps

  • Manipulation of swap routes to extract value while passing all current checks

The issue is particularly severe because it affects the core profit-making mechanism of these strategies across all supported chains and DEXes (Velodrome, Curve, Ramses).

Impact

The core issue lies in this assumption

require(minOut > _amount, "minOut too low");

This check assumes that receiving more alETH than the WETH input automatically means profit. However, this is incorrect because:

  • WETH and alETH can have different market values

  • The transmuter allows 1:1 redemption of alETH for WETH

  • The strategy should only swap when it can get a premium above the 1:1 rate

The balance check is also insufficient:

uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");

This only verifies the received amount matches expectations but doesn't validate if the trade was actually profitable compared to the 1:1 redemption rate available through the transmuter.

  1. The missing price validation: None of the implementations (Velodrome on Optimism, Curve on Mainnet, Ramses on Arbitrum) include proper price checks against:

  • Current market rates

  • Transmuter redemption rate

  • Minimum profitable premium threshold

This allows keepers to execute trades that pass the nominal amount checks but actually reduce the strategy's total value, breaking the core requirement that keeper actions must be profitable.

Recommendations

  1. Enforces a minimum premium requirement above the 1:1 transmuter rate

  2. Properly tracks and validates total value changes

  3. Ensures trades are profitable relative to the base redemption rate

  4. Provides transparency through a view function for minimum output calculation

The approach works across all implementations:

  • Velodrome (Optimism)

  • Curve (Mainnet)

  • Ramses (Arbitrum)

This solution maintains the strategy's core functionality while protecting against value-extracting trades.

// In StrategyArb.sol, StrategyMainnet.sol, StrategyOp.sol
+ // @Recommendation - Add minimum premium configuration
+ uint256 public constant MIN_PREMIUM_BPS = 50; // 0.5% minimum premium
+ uint256 public constant BPS_DENOMINATOR = 10000;
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");
+ // @Recommendation - Track total value before swap
+ uint256 initialBalance = asset.balanceOf(address(this));
+
+ // @Recommendation - Calculate minimum required output with premium
+ uint256 minRequiredOutput = _amountClaim + (_amountClaim * MIN_PREMIUM_BPS / BPS_DENOMINATOR);
+ require(_minOut >= minRequiredOutput, "Insufficient premium");
+
+ transmuter.claim(_amountClaim, address(this));
+ _swapUnderlyingToAsset(_amountClaim, _minOut, _path);
+
+ // @Recommendation - Verify actual profitability including premium
+ uint256 received = asset.balanceOf(address(this)) - initialBalance;
+ require(received >= minRequiredOutput, "Trade not profitable");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
+ // @Recommendation - Add view function to calculate minimum output with premium
+ function calculateMinimumOutput(uint256 amount) public pure returns (uint256) {
+ return amount + (amount * MIN_PREMIUM_BPS / BPS_DENOMINATOR);
+ }
Updates

Lead Judging Commences

inallhonesty Lead Judge
10 months ago

Appeal created

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

[INVALID]Lack of mechanism to ensure premium swaps

Support

FAQs

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