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

Keepers could execute unprofitable trades that reduce the total strategy value

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));
// @audit Value extraction vulnerability: Profitability check only considers alETH balance increase
// but ignores WETH reduction, allowing value-losing trades to pass the check.
// Example: Trading 100 WETH worth $100k for 90 alETH worth $90k would pass this check
// despite being a $10k loss, as it only verifies alETH increase >= minOut
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}

because the profitability check uses an incorrect accounting model:

  1. The check only verifies that received alETH (asset) meets minOut

  2. It completely ignores the value of WETH (underlying) being spent

  3. 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));
// @audit severe accounting flaw in swap profitability verification
// The check only verifies alETH increase but ignores WETH decrease
// This allows value-extracting trades where total value (WETH + alETH) decreases
// while still passing 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));
}

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));
// @audit Value extraction vector: The profitability check is mathematically flawed
// 1. Claims WETH from transmuter
// 2. Only checks if received alETH >= minOut
// 3. Ignores the value relationship between spent WETH and received alETH
_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

// StrategyOp.sol, StrategyMainnet.sol, StrategyArb.sol
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");
+ // Track total value before operation
+ uint256 totalValueBefore = balanceDeployed();
+
+ // Execute claim and swap
+ transmuter.claim(_amountClaim, address(this));
+ _swapUnderlyingToAsset(_amountClaim, _minOut, _path);
+
+ // Verify total value increased
+ uint256 totalValueAfter = balanceDeployed();
+ require(totalValueAfter > totalValueBefore, "Trade must be profitable");
+
+ // Additional slippage protection
+ require(_minOut >= _amountClaim * 101 / 100, "Minimum 1% premium required");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
+ // Add oracle price verification
+ function _verifyProfitablePrice(uint256 amountIn, uint256 amountOut) internal view {
+ // Implement oracle price check to ensure trade price is favorable
+ uint256 oraclePrice = getOraclePrice(); // Implementation specific
+ uint256 executionPrice = (amountOut * 1e18) / amountIn;
+ require(executionPrice >= oraclePrice, "Price below oracle");
+ }
Updates

Lead Judging Commences

inallhonesty Lead Judge
11 months ago

Appeal created

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

[INVALID] Keepers can execute swaps that appear profitable in isolation but actually decrease total strategy value

Support

FAQs

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