57604 sc high nominal accounting mismatch in moonwell strategies leads to permanent locking of all generated yield
Submitted on Oct 27th 2025 at 14:35:40 UTC by @sus_bandicoot for Audit Comp | Alchemix V3
Report ID: #57604
Report Type: Smart Contract
Report severity: High
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/strategies/optimism/MoonwellWETHStrategy.sol
Impacts:
Permanent freezing of unclaimed yield
Description
Brief/Intro
The MoonwellWETHStrategy and MoonwellUSDCStrategy contracts deposit user assets into Moonwell's yield-bearing mTokens. However, the Alchemix V3 system (via the Morpho VaultV2) tracks these deposits using a fixed, nominal asset amount. When deallocating, the strategies are only able to redeem the original nominal amount of assets, not the full value of their appreciated mToken shares. This accounting mismatch means all generated yield becomes permanently trapped in the strategy contracts, resulting in a total loss of earnings for the users who deposited funds into the vault.
Vulnerability Details
The protocol's yield-generation process involves three main components:
AlchemistAllocator: The contract used by operators to allocate funds.VaultV2(Morpho): The vault that holds the assets and tracks nominal allocations per strategy.Strategy Adapters (e.g.,
MoonwellWETHStrategy): The contracts that interact with the external yield protocols.
Allocation Flow:
An operator calls
AlchemistAllocator.allocate(...)to send, for example,100 WETHto theMoonwellWETHStrategy.The
AlchemistAllocatorcallsVaultV2.allocate(...).The
VaultV2contract transfers100 WETH(which users previously deposited into the vault) to theMoonwellWETHStrategy. It then callsMoonwellWETHStrategy.allocate(...)and internally records the nominalallocation = 100 WETHfor this strategy.
The VaultV2 then calls the allocate(...) function on the MoonwellWETHStrategy, which implements the logic to mint mTokens:
The strategy now holds 100 mWETH (assuming a 1:1 minting ratio for simplicity).
Deallocation Flow: Over time, the 100 mWETH held by the strategy appreciate in value due to accruing yield. Let's say the exchange rate doubles (a 1:2 ratio), and those 100 mWETH are now worth 200 WETH.
The
VaultV2's recordedallocationfor the strategy is still the nominal100 WETH(set during the allocation flow).An operator wishing to withdraw must call
AlchemistAllocator.deallocate(...). The problem is that they can only deallocate up to the nominal amount,100 WETH. Attempting to deallocate anything above the nominal100 WETH, e.g. the full200 WETH(the strategy's real value) would fail, as the system's accounting logic would underflow. The following steps demonstrate this process.The operator calls
AlchemistAllocator.deallocate(adapter, 100 WETH). This function first fetches the current nominal allocation from the vault and packs it asbytes memory oldAllocation.
The
AlchemistAllocatorthen callsvault.deallocate(adapter, oldAllocation, 100 WETH), whereoldAllocationis the encoded100 WETHandamount(theassetsparameter) is also100 WETH.
The VaultV2 receives this call, calls the adapter's deallocate function, and prepares to update its internal nominal allocation based on the change returned.
The vault's call is received by the MYTStrategy.deallocate(...) function, which decodes the oldAllocation (from data) and calls the internal _deallocate(...) function.
This is the place that would cause the transaction to revert if the operator attempted to withdraw more than the initial nominal allocation of assets. The underflow in the subtraction blocks the deallocation of the strategy earnings.
Finally, the MoonwellWETHStrategy._deallocate(...) function executes the redemption logic. As can be seen, the return value of a call to _deallocate(...) in the snippet above, is simply the input argument to the function (shown below).
The call to mWETH.redeemUnderlying(100 WETH) successfully redeems 100 WETH by burning only a portion of the strategy's mWETH (e.g., 50 mWETH, given our 1:2 ratio). The 100 WETH are returned to the VaultV2.
The VaultV2's allocation for this strategy is now updated to 0 (100 + (-100)).
The remaining 50 mWETH (worth the other 100 WETH), which represent the entire yield, are now permanently locked in the MoonwellWETHStrategy contract. Since the VaultV2's allocation is 0, no further deallocate calls can be made to retrieve them. There is no other mechanism to withdraw these remaining shares.
Impact Details
The primary and direct impact is the permanent and total loss of all yield generated by the MoonwellWETHStrategy and MoonwellUSDCStrategy.
While users can recover their principal (the initial nominal deposit)*, the entire purpose of these strategies is defeated, as all profits are irrecoverably locked. This renders these two strategies unprofitable by design, causing a direct loss of all earned yield for the vault's depositors. This guaranteed outcome aligns with a High-severity rating.
*A secondary impact stems from the realAssets() view function, which will report a misleadingly high value.
This function calculates the value of all mTokens held, including the trapped, irrecoverable yield. This inflates the perceived value of the strategy and the vault's overall share price, misleading operators and users about the system's true performance.
Furthermore, this inflated share price creates the potential for a more severe, Critical-severity scenario. The first users to redeem their shares would be paid out at an artificially high price, drawing from the vault's actual recoverable principal. This comes at the direct expense of the last depositors, who would be left holding worthless shares backed only by the trapped yield.
This report focuses on the guaranteed High-severity impact (the permanent loss of yield), as the Critical-severity bank run scenario is conditional on a specific mass-exit event and is of lower likelihood.
Since MoonwellWETHStrategy and MoonwellUSDCStrategy are identical except for the underlying asset, the same issue is present in both contracts.
References
AlchemistAllocator: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistAllocator.solMYTStrategy: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/MYTStrategy.solMoonwellUSDCStrategy: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/strategies/optimism/MoonwellUSDCStrategy.solMoonwellWETHStrategy: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/strategies/optimism/MoonwellWETHStrategy.solVaultV2(Morpho): https://github.com/morpho-org/vault-v2/blob/main/src/VaultV2.solmWETH: https://optimistic.etherscan.io/address/0xb4104C02BBf4E9be85AAa41a62974E4e28D59A33#code
Proof of Concept
Proof of Concept
The test output demonstrates the bug. The logs show that after allocating 1000 WETH and letting yield accrue (20 days), the strategy's underlying value increased to 1000.32 WETH.
After the operator deallocates the original 1000 WETH, the vault's internal allocation for the strategy drops to 0. The remaining 0.32 WETH (the yield) is still in the strategy, as shown by the "Remaining trapped yield" log.
The final step proves these funds are locked. The attempt to withdraw this remaining yield reverts, as the VaultV2 contract (and MYTStrategy) no longer permits deallocation from a strategy it considers to have a zero balance.
PoC Instructions:
The test contract can be found below. Copy it and paste it into:
src/test/strategies/MoonwellWETHStrategy.t.solRun the test with the following command:
Logs:
Test file: src/test/strategies/MoonwellWETHStrategy.t.sol
Was this helpful?