58329 sc low incorrect balance measurement in morphoyearnogweth deallocate leads to temporary freezing of funds via spurious loss events

Submitted on Nov 1st 2025 at 10:05:44 UTC by @rshackin for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58329

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Temporary freezing of funds for at least 1 hour

    • Temporary freezing of funds for at least 24 hour

Description

Summary:

MorphoYearnOGWETH strategy contains a logic error in _deallocate() that causes it to emit StrategyDeallocationLoss events on every successful deallocation despite funds arriving correctly. This occurs because balance measurements are taken after the vault withdrawal completes, making the computed delta always zero. The false loss signals can trigger automated circuit breakers that pause the strategy, blocking further deallocations and creating temporary freezing of funds for users relying on this route to redeem collateral.

  • Component: MorphoYearnOGWETH._deallocate()

  • Impact: Deterministic false loss events on normal operations and realistic automation-driven redemption freezing.

Vulnerability Details:

  • Root Cause: In MorphoYearnOGWETH._deallocate(), the balance delta calculation reads both snapshots after calling vault.withdraw():

  • The vault.withdraw() call on line 1 successfully transfers amount WETH to the strategy (as confirmed by test balance assertions), but because both balanceOf() calls on lines 4-5 happen after this transfer completes, they read the same post-transfer balance, making their difference (wethRedeemed) zero regardless of actual funds received. This deterministically forces the loss event to emit on every deallocation.

  • Since both reads occur after the external vault call transfers WETH to the strategy, balanceBefore == balanceAfter, forcing wethRedeemed = 0. The subsequent check if (wethRedeemed < amount) always triggers, emitting StrategyDeallocationLoss(message: "Strategy deallocation loss.", amountRequested: amount, actualAmountSent: 0) even though the strategy's balance increased by exactly amount.

  • Intended correct Behavior (from other strategies): EulerWETH, EulerUSDC, Peapods ETH/USDC all implement the correct pattern:

This confirms MorphoYearnOGWETH's ordering is an unintended implementation and this is flawed.

Impact Details:

  1. Direct Impact: Deterministic False Loss Signals.

  • Every deallocation through MorphoYearnOGWETH emits StrategyDeallocationLoss(actualAmountSent: 0) despite funds arriving, creating constant operational error and misleading monitoring systems about the data of this strategy.

  1. Primary Impact: Temporary Freezing of Funds (High):

  • Mechanism: Production teams commonly deploy automated keepers that monitor strategy events and execute circuit-breaker pauses when losses are detected to prevent cascading failures. The spurious StrategyDeallocationLoss can trigger such automation immediately after any deallocation.

  • Effect: Once paused, the strategy blocks further _deallocate() calls, preventing users from unwinding positions or redeeming collateral through this route until manual intervention/un-pause occurs.

Duration Justification for ≥24h Threshold:

  • Investigation phase (2-8 hours): On-call engineer reviews logs, identifies StrategyDeallocationLoss events, must distinguish false positives from genuine losses by comparing event history, vault state, and balance proofs

  • Coordination phase (4-12 hours): Gathering multisig/admin quorum or governance vote to execute unpause, especially during weekends/holidays when key holders are unavailable

  • Repeated cycles: Since the bug fires on every deallocation, even if first pause resolves quickly (best-case 6-8h), subsequent user attempts re-trigger false events and additional pauses, accumulating total freeze time beyond 24 hours

  • Realistic minimum: 24-48+ hours is standard operational response time for non-emergency investigations (distinguishing false vs. real threats), satisfying the "at least 24 hour" criterion.

References:

(https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/strategies/mainnet/MorphoYearnOGWETH.sol#L49-L56)

Move balanceBefore read to line before vault.withdraw() call, aligning with pattern used in all other MYT strategies.

This fix aligns the logic with the pattern used in all other MYT strategies (EulerWETH, EulerUSDC, PeapodsETH, PeapodsUSDC), eliminating false loss events while preserving intended safety monitoring when genuine shortfalls occur..

Proof of Concept

Proof of Concept:

Both PoCs run on a mainnet fork using real Morpho-Yearn OG WETH vault and WETH addresses, satisfying the competition's local-fork testing requirement.

  • PoC A: False Loss Event on Successful Deallocation:

  1. Place test file in test/MorphoYearnOGWETH_FalseLoss.t.sol

  2. Export RPC: export MAINNET_RPC_URL="https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"

  3. Execute: forge test --match-contract MorphoYearnOGWETHLossEvent -vvv

Results: Test passes; trace shows strategy balance increases by 0.5 ETH (from 1.0 to 1.5) while StrategyDeallocationLoss(actualAmountSent: 0) is emitted in the same transaction — proving the contradiction.

  • PoC B: Automated Pause Leading to Temporary Freezing.

  1. Place test file in test/MorphoYearnOGWETH_AutoFreeze.t.sol

  2. Use same RPC setup as PoC A

  3. Execute: forge test --match-contract MorphoYearnOGWETH_AutoPause -vvv

Results: Test passes; second deallocation reverts with Paused(), proving the automation-driven freeze path.

Was this helpful?