57288 sc high flawed rounding logic in tokeautoeth deallocate function causes permanent freezing of funds

Submitted on Oct 25th 2025 at 01:13:10 UTC by @terrah for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57288

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Permanent freezing of funds

Description

Bug Description

The _deallocate function in TokeAutoEth.sol contains a flawed rounding condition that withdraws all shares from the rewarder when the difference between actual shares held and shares needed is less than or equal to 1 share (1e18). This causes the function to withdraw significantly more shares than requested, approve only the requested amount to the vault, and strand the excess WETH in the strategy contract. The stranded funds become permanently inaccessible because subsequent deallocate calls will underflow and revert.

Brief/Intro

When deallocating more than approximately 99% of the strategy's position, the TokeAutoEth strategy withdraws all shares from the rewarder instead of only the amount needed. The function returns the requested amount to the vault for accounting purposes but actually withdraws more, leaving excess WETH stranded in the strategy contract. This stranded amount cannot be recovered through any available mechanism in the codebase, resulting in permanent freezing of funds. Additionally, the accounting mismatch causes all subsequent deallocate attempts to revert on underflow.

Details

The vulnerable code is in TokeAutoEth.sol _deallocate function:

The problem occurs when shareDiff is less than or equal to 1e18 (one full share). In this case, the function sets sharesNeeded to actualSharesHeld, withdrawing all shares. However, the function only approves the originally requested amount to the vault and returns the requested amount for accounting. This creates a critical discrepancy.

Consider a scenario where the strategy holds 100e18 shares worth approximately 100 WETH. When the vault calls deallocate with 99.5 WETH:

The function calculates sharesNeeded as 99.5e18 shares. The actualSharesHeld is 100e18 shares. The shareDiff becomes 0.5e18, which is less than 1e18. The condition triggers and sharesNeeded is set to 100e18. The function withdraws all 100e18 shares from the rewarder and redeems them for approximately 100 WETH. However, it only approves 99.5 WETH to the vault and returns 99.5. The vault's accounting records a deallocation of 99.5 WETH, believing 0.5 WETH worth of allocation remains.

After this operation, the strategy has zero shares in the rewarder but approximately 0.5 WETH sitting as an ERC20 balance in the contract. The vault's internal accounting believes the strategy still has 0.5 WETH allocated.

When any subsequent deallocate call attempts to withdraw this remaining 0.5 WETH allocation, the function will calculate sharesNeeded as approximately 0.5e18, but actualSharesHeld will be 0. The line shareDiff = actualSharesHeld - sharesNeeded will underflow in Solidity 0.8.28, causing the transaction to revert. This makes the deallocate function permanently unusable.

The stranded WETH cannot be recovered through any mechanism. The MYTStrategy contract has no sweep or rescue function. The removeStrategy function in AlchemistCurator only affects the vault's adapter registry and does not transfer tokens. The owner has no special token recovery powers. The funds remain permanently locked in the strategy contract.

The same bug exists in TokeAutoUSDStrategy.sol with identical logic.

Impact

This vulnerability causes permanent freezing of funds. Each time a deallocation of more than approximately 99% of the strategy's position occurs, a portion of funds (the remainder) becomes permanently inaccessible. The stranded amount ranges from a fraction of a share up to nearly one full share worth of value.

The funds cannot be recovered by the admin, the vault, or any other party. There is no sweep function, no emergency withdrawal mechanism, and no way to force the strategy to release the stranded tokens. The deallocate function becomes permanently broken after the first trigger, preventing any future rebalancing or withdrawals from that strategy.

Users who have deposited into the vault expecting to be able to withdraw their funds will find that the strategy cannot deallocate to fulfill withdrawal requests. The vault must route all future activity through other strategies, reducing overall capital efficiency and potentially causing liquidity issues during periods of high withdrawal demand.

Risk Breakdown

Difficulty to Exploit: Low

The vulnerability triggers automatically during normal protocol operations. No special privileges, external dependencies, or complex transaction sequences are required. Any call to deallocate that requests more than approximately 99% of the strategy's current position will trigger the bug. Vault operators, allocators, and admins performing routine rebalancing operations will inadvertently cause this issue.

The bug is deterministic and does not depend on market conditions, oracle prices, or external protocol states. It will occur every time the specific condition is met.

Recommendation

Remove the flawed rounding logic entirely. The condition checking if shareDiff is less than or equal to 1e18 serves no valid purpose and causes severe accounting issues. Replace the vulnerable section with proper bounds checking:

This ensures the function only withdraws available shares and maintains correct accounting. If dust avoidance is truly necessary, use a much smaller threshold like 1e12 (representing 0.000001 shares or approximately $0.003 worth of value), and ensure the return value matches the actual amount withdrawn.

Apply the same fix to TokeAutoUSDStrategy.sol which contains identical vulnerable logic.

References

Vulnerable file: TokeAutoEth.sol, function _deallocate Duplicate vulnerable file: TokeAutoUSDStrategy.sol, function _deallocate

For comparison, other strategy contracts like EulerARBWETHStrategy.sol, AaveV3ARBWETHStrategy.sol, and MorphoYearnOGWETHStrategy.sol do not implement this flawed logic and function correctly.

Proof of Concept

Proof of Concept

Was this helpful?