58409 sc high high arithmetic underflow in mytstrategy sol s deallocate check prevents yield withdrawal

Submitted on Nov 2nd 2025 at 02:08:55 UTC by @chief_hunter888 for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58409

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Theft of unclaimed yield

Description

[HIGH] Arithmetic Underflow in MYTStrategy.sol's deallocate() Prevents Yield Withdrawal

Summary

The MYTStrategy base contract is designed to work with yield-bearing strategies that wrap ERC4626 vaults (e.g., MorphoYearnOGWETH, EulerWETH). These strategies deposit assets (e.g., WETH) and receive shares that accrue value over time. When the Alchemix vault later deallocates funds (for user withdrawals, rebalancing, or other operations), it should be able to withdraw the original principal plus any earned yield.

However, the deallocate() function in MYTStrategy.sol contains a critical arithmetic flaw on line 130 that makes withdrawing yield impossible:

function deallocate(bytes memory data, uint256 assets, ...) external returns (...) {
    uint256 oldAllocation = abi.decode(data, (uint256));      // Line 128: Original deposit amount
    uint256 amountDeallocated = _deallocate(assets);          // Line 129: Amount withdrawn (can include yield)
    uint256 newAllocation = oldAllocation - amountDeallocated; // Line 130: ❌ UNDERFLOW!
    // ...
}

The Problem:

  • oldAllocation is decoded from the data parameter passed by the vault (e.g., 100 WETH)

  • The data parameter contains stale accounting - it reflects the allocation amount at some previous point in time. On-chain oldAllocation tracks the original deposit amount (e.g., 100 WETH). See allocate flow.

  • amountDeallocated is the actual amount withdrawn, which includes accrued yield (e.g., 105 WETH after 5% yield)

  • When yield exists: 100 WETH - 105 WETH causes arithmetic underflow and reverts

Critical Issue: The data parameter has no freshness guarantee. There's no timestamp or block number to validate that oldAllocation reflects current reality. Even if the vault could update data externally (via oracle or manual update), yield earned between updates remains permanently inaccessible because the accounting is always behind the actual accrued value.


Impact

Critical: This bug fundamentally breaks the core purpose of yield-bearing strategies - earned yield cannot be withdrawn by the protocol.

  1. Yield Permanently Frozen Between Updates: The vault can only deallocate up to the amount encoded in the data parameter. Any yield earned beyond that amount causes arithmetic underflow. Even if data could theoretically be updated externally:

    • There's no freshness validation (no block number or timestamp)

    • Yield accrues continuously, but data updates are discrete

    • The gap between actual value and recorded oldAllocation creates permanently locked yield

    • For strategies earning 5-20% APY, significant value becomes inaccessible

  2. Complete Protocol DoS When Attempting Full Withdrawal: Any attempt to deallocate the actual current value (principal + yield) causes an arithmetic underflow revert, making the following operations impossible:

    • Processing user withdrawal requests for full balances

    • Vault rebalancing operations to optimize yield

    • Emergency fund recovery during crises

    • Strategy migrations or upgrades

    • Liquidations requiring full collateral withdrawal

  3. Affects ALL Strategies: This bug is in the MYTStrategy.sol base contract, meaning it affects every strategy that inherits from it:

    • MorphoYearnOGWETHStrategy

    • EulerWETHStrategy / EulerUSDCStrategy

    • PeapodsETHStrategy / PeapodsUSDCStrategy

    • TokeAutoEthStrategy / TokeAutoUSDStrategy

    • And all other MYT strategies

  4. Loss of Funds: While the principal remains technically accessible (assuming data reflects at least the principal), the yield (which could represent millions of dollars across all strategies) is permanently or semi-permanently locked, depending on whether and how frequently data could be updated externally.

Furthermore, in addition to preventing the current programmatic underflow reverts that may occur during deallocation when the yield-bearing token snapshot is outdated, the code should be updated to handle this “out-of-sync” error more gracefully than a Solidity revert. Specifically, an early check should be introduced at the start of the deallocate function to verify that previousAllocationData is sufficiently fresh, using the block number at which it was last refreshed. This ensures all bookkeeping values are up-to-date and prevents the error from being triggered.

Implementing this check also guarantees that the maximum possible amount can be withdrawn from the strategy via deallocation at any given time. Without fresh off-chain oracle data, any strategy will always have a portion of yield effectively frozen, because the amount that can be deallocated is limited to the last snapshot recorded in the data field.


Proof of Concept

Scenario: Alchemix vault allocates to MorphoYearnOGWETH strategy, yield accrues, then protocol attempts to deallocate.

Test Evidence: See test_MYTStrategy_arithmetic_underflow_with_yield() in:

  • src/test/strategies/MockYieldStrategy.t.sol (clean POC)

  • src/test/strategies/MorphoYearnOGWETHStrategy.t.sol (real strategy)

How to run


Likelihood

Certainty: 100% — This will occur in every yield-bearing strategy deployment:

  1. Yield Accrual is Expected: The entire purpose of these strategies is to earn yield (5-20% APY). Yield accrues continuously in the underlying ERC4626 vaults.

  2. Data Staleness is Inevitable: The data parameter passed to deallocate() contains a snapshot of allocation at some previous time. Unless there's a mechanism to update this data on every block (which would be prohibitively expensive), the encoded oldAllocation will always lag behind the actual realAssets() value.

  3. Affects Normal Operations: Any protocol operation attempting to deallocate the full current value (principal + accrued yield) will hit this underflow:

    • User withdrawal requests for full balances

    • Vault rebalancing to move funds between strategies

    • Emergency deallocations during market stress

    • Strategy migrations

  4. No Freshness Validation: The contract has no mechanism to verify that data is fresh. There's no block number, timestamp, or staleness check. Even if an external system could update data, the contract would blindly trust any value passed in.

  5. Already Deployed: If these strategies are in production, yield is accruing right now and the gap between oldAllocation and realAssets() is growing with every block.


Recommendation

Option 1: Add Freshness Validation (Recommended)

Update the data structure to include freshness metadata and validate it:

Option 2: Fix Arithmetic Logic (Simpler, but doesn't address staleness)

Option 3: Vault-Level Solution

Implement a mechanism in the vault to query strategy.realAssets() and update the allocation data before calling deallocate(), ensuring oldAllocation always reflects current value including yield.


Root Cause

Two interrelated design flaws:

  1. Stale Data Without Freshness Validation: The data parameter encodes allocation amounts but has no mechanism to ensure this data is current:

    • No block number or timestamp to validate freshness

    • No requirement that oldAllocation reflects realAssets() before deallocation

    • The vault could theoretically update data externally (oracle, manual update), but the contract has no way to enforce or verify this

  2. Arithmetic Assumes No Yield: The subtraction oldAllocation - amountDeallocated (line 130) assumes amountDeallocated ≤ oldAllocation. This works for:

    • Non-yield-bearing strategies

    • Partial withdrawals less than original deposit

    • But fails for yield-bearing strategies where amountDeallocated can include accrued yield

The fundamental issue: The contract treats allocations as static amounts, but yield-bearing strategies have dynamic, appreciating values. Without fresh accounting data, any yield earned since the last update becomes inaccessible.


Severity

HIGH / CRITICAL

  • Impact: Critical - Yield permanently or semi-permanently frozen, protocol-level DoS for operations requiring full withdrawals

  • Likelihood: Certain - Data staleness is inevitable unless updated every block (prohibitively expensive); affects 100% of yield-bearing strategy usage

  • Affected Code: Base contract affecting all inheriting strategies (10+ production strategies)

  • Protocol Impact:

    • Cannot process user withdrawal requests for full balances

    • Cannot fully rebalance between strategies

    • Yield earned between data updates is inaccessible

    • Emergency operations blocked

  • Financial Impact: Yield losses proportional to update frequency and APY rates (5-20% APY on potentially millions of TVL)


References

Vulnerable Code:

  • File: src/MYTStrategy.sol

  • Function: deallocate() (lines 119-133)

  • Bug Location: Line 130

Affected Strategies (Inheriting from MYTStrategy):

  • src/strategies/mainnet/MorphoYearnOGWETH.sol

  • src/strategies/mainnet/EulerWETHStrategy.sol

  • src/strategies/mainnet/EulerUSDCStrategy.sol

  • src/strategies/mainnet/PeapodsETH.sol

  • src/strategies/mainnet/PeapodsUSDC.sol

  • src/strategies/mainnet/TokeAutoEth.sol

  • src/strategies/mainnet/TokeAutoUSDStrategy.sol

  • All other strategies inheriting MYTStrategy

Proof of Concept Tests:

  • src/test/strategies/MockYieldStrategy.t.sol::test_MYTStrategy_arithmetic_underflow_with_yield()

  • src/test/strategies/MorphoYearnOGWETHStrategy.t.sol::test_MYTStrategy_arithmetic_underflow_with_yield()

Documentation:

Proof of Concept

Proof of Concept

Mock Contract that mocks a Yield Accruing Strategy for testing:

the mock yield strategy test:

Just 2 tests to add to MorphoYearnOGWETHStrategy.sol to illustrate the limitation in the withdrawal cycle

Was this helpful?