56498 sc low reserve drainage due to incorrect balance measurement

Submitted on Oct 16th 2025 at 20:45:09 UTC by @nem0thefinder for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #56498

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Permanent freezing of funds

Description

Summary

The _deallocate() function reads balances after the vault withdrawal instead of before and after. This makes wethRedeemed always equal zero, preventing validation of the actual amount received. When the external vault returns less than requested (due to fees or slippage), the strategy silently uses reserves to cover the shortfall. This drains accumulated yield until reserves are depleted, at which point all withdrawals fail permanently.

Description

The Bug

function _deallocate(uint256 amount) internal override returns (uint256) {
    // ... 
    vault.withdraw(amount, address(this), address(this)); // Withdrawal happens here
    
    //  BOTH reads happen AFTER the withdrawal
    uint256 wethBalanceBefore = TokenUtils.safeBalanceOf(address(weth), address(this));
    uint256 wethBalanceAfter = TokenUtils.safeBalanceOf(address(weth), address(this));
    
    uint256 wethRedeemed = wethBalanceAfter - wethBalanceBefore; // Always ≈ 0
    
    // This check is meaningless since wethRedeemed ≈ 0
    require(wethRedeemed + wethBalanceBefore >= amount, ...);
    
    // This only checks total balance, not what the vault actually returned
    require(TokenUtils.safeBalanceOf(address(weth), address(this)) >= amount, ...);
    
    return amount; // Claims success even if vault returned less
}

The Critical Flaw:

  • Both wethBalanceBefore and wethBalanceAfter are read after vault.withdraw() completes

  • wethRedeemed = wethBalanceAfter - wethBalanceBefore ≈ 0 (both values are identical)

  • The function cannot detect when the vault returns less than requested

  • Subsequent checks pass if the strategy has sufficient total balance (reserves + withdrawn amount)

  • This causes reserves to be silently consumed to cover vault shortfalls

How It Fails

Scenario: External vault has 2% withdrawal fee

Over multiple withdrawals, reserves drain completely:

Worst case: If vault returns 0 , entire deallocation comes from reserves if while allocated assets remain untouched.

Impact

1. Theft of Unclaimed Yield

Strategy reserves could contain accumulated yield, harvested rewards, and profits. The bug silently drains these reserves to cover vault shortfalls. Users lose all accumulated profits over time.

2. Temporary Freezing of Funds

Once reserves are depleted, all withdrawals requiring deallocation permanently fail:

User funds become temp frozen in the external vault till reserves are updated.

Mitigation

Fix: Measure Balance Before and After Withdrawal

Alternative: Slippage Tolerance

If the protocol wants to accept vault fees/slippage:

Proof of Concept

Proof of Concept

Import the following in MorphoYearnOGWETHStrategyTest.t.sol

Paste the test in MorphoYearnOGWETHStrategyTest.t.sol

Run it via `forge test --mt test_deallocate_drains_strategy_reserves_silently -vvv

`

Logs

Was this helpful?