57041 sc high deallocation accounting mismatch between vault and adapter

Submitted on Oct 22nd 2025 at 22:40:22 UTC by @nem0thefinder for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57041

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Smart contract unable to operate due to lack of token funds

    • Permanent freezing of funds

Description

Summary

The adapter's deallocate function returns the requested deallocation amount instead of the actual amount received from the external strategy, causing the vault to pull more funds than the adapter actually withdrew. This creates an accounting mismatch where the allocation cap decreases by the requested amount, but the adapter must cover slippage losses from its own balance.

Description

Note!!

This apply for all strategies When deallocating funds from an external strategy:

  1. The vault calls deallocateInternal requesting withdrawal of assets amount (e.g., 1000 USDC)

  2. The adapter withdraws from the external strategy via _deallocate(amount)

  3. The external strategy may return less than requested due to slippage (e.g., request 1000 USDC, receive 980 USDC)

  4. The vault correctly updates the allocation cap by the requested amount (decreases by 1000 USDC)

  5. The adapter emits a loss event but still returns withdrawReturn = amount (the full 1000 USDC)

  6. Then there are two Execution Paths

    1. The require statement that check the adapter balance will revert if there's no balance cover the amount (DoS deallocation)

    2. the require statement pass since the adapter balance cover the amount (there was prev.balance or team manual intervention)

  7. The adapter approves the full requested amount for transfer (1000 USDC)

  8. The vault pulls the full requested amount via SafeERC20Lib.safeTransferFrom (1000 USDC)

The Problem:

  • Allocation cap update is CORRECT: Decreases by 1000 USDC (the amount requested from the strategy)

  • Transfer amount is WRONG: Vault pulls 1000 USDC but adapter only received 980 USDC from the external strategy

  • The adapter must have sufficient USDC balance to cover the slippage loss (20 USDC in the example), otherwise the require(TokenUtils.safeBalanceOf(address(usdc), address(this)) >= amount, ...) check will fail

  • Expected behavior: Vault should update allocation cap by requested amount (1000 USDC) but pull only the actual amount received (980 USDC) VaultV2::_dealocateInternalarrow-up-right

AaveV3ARBUSDCStrategy::_deallocatearrow-up-right

The protocol acknowledges slippage is expected (via slippageBPS and _previewAdjustedWithdraw), but the implementation forces the adapter to cover losses rather than properly accounting for them.

Impact

  1. Fragile Dependency: Adapter requires spare USDC balance to cover slippage, creating an implicit requirement that's not guaranteed

  2. Incorrect Loss Attribution: Slippage losses are absorbed by the adapter's balance rather than properly attributed to vault depositors. The vault's accounting is correct (allocation decreases by requested amount), but the actual transfer forces the adapter to make up the difference

  3. Potential DoS: If adapter doesn't have sufficient balance to cover slippage, deallocations will revert

Mitigation:

Strategies/Adapters

  1. Calculate slippage-adjusted minimum: minAcceptable = amount - (amount * slippageBPS / 10000)

  2. Validate received amount meets minimum: require(redeemedAmount >= minAcceptable, "Slippage exceeded")

  3. Return allocation change based on requested amount: Keep current logic where change = int256(newAllocation) - int256(oldAllocation) (based on amount requested, not redeemedAmount)

  4. Approve only the actual redeemed amount: TokenUtils.safeApprove(address(usdc), msg.sender, redeemedAmount)

  5. Communicate actual redeemed amount to vault: The adapter needs a way to return redeemedAmount separately from the allocation change

MorpheusVault

  1. Update allocation cap by the change (already implemented correctly): Based on requested assets amount

  2. Transfer only the actual redeemed amount: SafeERC20Lib.safeTransferFrom(asset, adapter, address(this), redeemedAmount) instead of assets

Proof of Concept

Proof of Concept

1. Paste the following interfact in FluidARBUSDCStrategy.t.sol

2. Paste the following test in FluidARBUSDCStrategy.t.sol

3.Run it via forge test --mc FluidARBUSDCStrategyTest --mt test_POC_DeallocateAccountingMismatch -vvv

Logs

Was this helpful?