57331 sc medium conditional eth wrapping logic causes withdrawal dos in moonwellweth and stargateeth strategies

#57331 [SC-Medium] Conditional ETH Wrapping Logic Causes Withdrawal DoS in MoonwellWETH and StargateETH Strategies

Submitted on Oct 25th 2025 at 09:50:56 UTC by @Max36935 for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57331

  • Report Type: Smart Contract

  • Report severity: Medium

  • Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/strategies/optimism/MoonwellWETHStrategy.sol

  • Impacts:

    • Temporary freezing of funds for at least 1 hour

Description

Conditional ETH Wrapping Logic Causes Withdrawal DoS in MoonwellWETH and StargateETH Strategies

Summary

Both MoonwellWETHStrategy.sol and StargateEthPoolStrategy.sol on Optimism contain flawed conditional logic in their _deallocate() functions that prevents ETH from being wrapped to WETH when withdrawals experience any slippage or loss. This causes the functions to revert, blocking users from withdrawing funds during normal market conditions.

Vulnerability Details

Location

  • File 1: src/strategies/optimism/MoonwellWETHStrategy.sol

    • Function: _deallocate(uint256 amount)

    • Lines: 64-67

  • File 2: src/strategies/optimism/StargateEthPoolStrategy.sol

    • Function: _deallocate(uint256 amount)

    • Lines: 74-77

The Bug

Both strategies implement the same problematic pattern. Let me walk through MoonwellWETH as the example (Stargate has identical logic):

Lines 54-70 from MoonwellWETHStrategy.sol:

The Issue is on Line 64:

This conditional check only wraps the ETH if the total amount (redeemed + any pre-existing balance) is sufficient. In normal operation, strategies don't accumulate ETH balances - they start with 0 ETH.

Execution Flow When Loss Occurs:

Scenario: User requests to deallocate 100 WETH, but receives 98 ETH due to 2% slippage.

Result: Transaction fails with 98 ETH stuck unwrapped in the strategy contract.

Comment vs Code Contradiction

Look at line 58 comment in MoonwellWETH:

The comment explicitly states "wrap any ETH received", but the code implements conditional wrapping that only wraps if there's no loss. This is a clear contradiction between intent and implementation.

Similarly, in StargateEthPoolStrategy line 65:

It says "then wrap back to WETH" - not "wrap back if we have enough". The comment implies unconditional wrapping.

Why Strategies Don't Have Leftover ETH

I verified that in normal operation, strategies should not accumulate ETH:

  1. _allocate() doesn't leave ETH: Takes WETH as input, converts if needed, deposits to protocol

  2. _deallocate() should wrap immediately: Receives ETH from protocol, supposed to wrap to WETH

  3. receive() exists only for protocol redemptions: Not for general ETH deposits

The only source of ETH is from the redemption itself. The conditional assumes there might be leftover ETH from previous operations, but this shouldn't happen in normal flow.

Comparison with Other Strategies

I checked all other WETH strategies in the codebase and none use this conditional pattern:

TokeAutoEth (mainnet) - Lines 77-82:

No conditional wrapping - just measures, emits loss event if needed, then continues to require check.

PeapodsETH (mainnet) - Lines 37-44:

Same pattern - no conditional wrapping.

MoonwellUSDC (same protocol, USDC version) - Lines 56-64:

Even the USDC version from the same Moonwell protocol doesn't have the conditional - it just measures and emits loss.

Finding: Only MoonwellWETH and StargateETH have this conditional wrapping bug. All other strategies follow the simpler, correct pattern.

Impact

Direct Impact

  • Users cannot withdraw from MoonwellWETH or StargateETH strategies when there's any slippage or loss

  • Protocol cannot rebalance funds allocated to these strategies during volatile markets

  • Withdrawals that would normally succeed with a small loss completely fail

Steps to Reproduce

  1. Deploy strategy with 0 initial ETH balance (normal state)

  2. Allocate 100 WETH to strategy

  3. Market conditions cause 2% slippage

  4. Call deallocate(100 WETH)

  5. Strategy receives 98 ETH from protocol

  6. Conditional check: 98 + 0 >= 100 = FALSE

  7. ETH not wrapped

  8. require(wethBalance >= 100) fails

  9. Transaction reverts

Recommendation

Remove the conditional and wrap all received ETH:

Current (buggy):

Fixed:

Proof of Concept

Proof of Concept

Save And Run The Test

  • Save in src/test/Audit_MytSharesNotDecrementedOnLiquidation.t.sol

  • Run it forge test --match-contract MoonwellWETHBugTest -vvv

Was this helpful?