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

The balance comparison doesn't accurately reflect the true economic value of the position before and after the swap.

Summary

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

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
// @check Balance check is performed after claim, allowing the balance comparison to be manipulated
// since claimed amounts affect balBefore. This makes the profitability check unreliable
_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 balance check is performed after claiming WETH from the transmuter, this means balBefore includes only alETH balance but not the claimed WETH, the profitability comparison becomes unreliable because it doesn't account for the total value before the swap. A keeper could execute a swap that appears profitable due to this accounting error but actually results in a loss

Vulnerability Details

StrategyOp#claimAndSwap function

// In all three strategy contracts:
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
// @check: Transmuter claim happens before balance check, making the profitability comparison invalid
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
uint256 balAfter = asset.balanceOf(address(this));
// @check Invalid profitability check: (balAfter - balBefore) doesn't account for the claimed WETH value
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}

The profitability check is fundamentally flawed because:

  1. It claims WETH from the transmuter before recording balBefore

  2. The balance comparison (balAfter - balBefore) only tracks alETH changes

  3. The true economic value of the claimed WETH is not factored into the profitability calculation

Impact Across Implementations:

These strategies handle WETH/alETH pairs which should maintain specific price relationships based on the Alchemix protocol's design.

  1. Arbitrum (StrategyArb.sol):

  • Ramses Router swaps could execute at unfavorable rates while appearing profitable

  1. Optimism (StrategyOp.sol):

  • Velodrome Router swaps could result in value loss while passing the check

  1. Mainnet (StrategyMainnet.sol):

  • Curve Router swaps could lead to economic losses despite satisfying the requirement

Affects all three chains because the core logic of claiming before balance comparison is shared across implementations. This allows keepers to execute technically valid but economically unfavorable swaps, potentially leading to value extraction from the strategy.

Impact

In the strategy contracts, the profitability verification is broken because

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
// @check Step 1: Claims WETH from transmuter, changing the strategy's total value
transmuter.claim(_amountClaim, address(this));
// @check Step 2: Records only alETH balance, ignoring the just-claimed WETH
uint256 balBefore = asset.balanceOf(address(this));
// @check Step 3: Swaps WETH to alETH using an AMM
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
// @check Step 4: Compares new alETH balance to old alETH balance
// This comparison is invalid because it doesn't account for the WETH that was claimed
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
}

The root cause is that the balance comparison ignores the economic value of the claimed WETH. Since WETH is claimed before recording balBefore, the profitability check becomes

Profitability Check = (New alETH Balance - Old alETH Balance) >= minOut

When it should actually be

Profitability Check = (Value of New alETH Balance) > (Value of Old alETH Balance + Value of Claimed WETH)

This incorrect accounting allows trades that appear profitable in terms of alETH balance changes but are actually losing value when considering the full position.

Recommendations

This ensures keepers can only execute genuinely profitable trades that benefit the strategy's total value position.

// In StrategyOp.sol, StrategyMainnet.sol, and StrategyArb.sol:
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
+ // @Recommendation - Record total value before any operations
+ uint256 totalValueBefore = asset.balanceOf(address(this)) +
+ (transmuter.getClaimableBalance(address(this)));
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 - Compare total value after swap to ensure true profitability
+ uint256 totalValueAfter = asset.balanceOf(address(this));
+ require(totalValueAfter > totalValueBefore, "Trade not profitable");
+ require(totalValueAfter >= totalValueBefore + (_amountClaim * 101 / 100), "Insufficient premium");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
+ // @Recommendation - Add helper function to calculate total value
+ function getTotalValue() public view returns (uint256) {
+ return asset.balanceOf(address(this)) +
+ transmuter.getClaimableBalance(address(this)) +
+ transmuter.getUnexchangedBalance(address(this));
+ }
  1. Tracks total economic value before and after the swap

  2. Enforces a minimum 1% premium on trades

  3. Considers all forms of value (alETH, claimable WETH, unexchanged balance)

  4. Prevents value-losing trades by requiring strict profitability

The solution works across all implementations (Arbitrum, Optimism, Mainnet) because it focuses on the fundamental economic relationship between WETH and alETH rather than specific AMM implementations.

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.