Early rate$2,400 of senior audit time for $500. Early members keep the rate as it climbs.$2,400 of senior audit time for $500See how →
F-2026-0002·denial-of-service

A one-unit Aave v4 deposit permanently pins a remote epoch

Fixedvaultcrosschainepoch-accountinggithub.com/contractlevel/yield-v2
TL;DR

Epoch netting can produce a one-base-unit remote deposit that the Aave v4 Hub rejects because it rounds to zero shares. The rejection is stored as replayable recovery that can never succeed, so roughly two USDC of intents freezes claims, cancellations, later epochs and rebalancing for every holder.

Severity
HIGH
Impact
HIGH
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
MEDIUM
Exploitability
MEDIUM
02Section · Description

Description

At frozen revision a08ecdfdb5aa603366ac9a342791cc532052ee52, AaveV4Adapter.deposit assumes every positive asset amount can be supplied. It approves the configured Spoke, calls supply(reserveId, amount, address(this)), and only checks the position delta after that call returns.

The real Aave v4 Hub has a stricter semantic boundary. Spoke.supply transfers the assets to the Hub and calls Hub.add; the Hub converts assets to added shares by rounding down and rejects zero shares:

solidity
uint120 shares = asset.toAddedSharesDown(amount).toUint120();
require(shares > 0, InvalidShares());

With Aave v4's virtual balances, the conversion is:

code
floor(amount * (addedShares + 1_000_000) / (addedAssets + 1_000_000))

The configured Ethereum USDC reserve already has addedAssets > addedShares, so a one-unit USDC supply produces zero shares and deployed Aave bytecode reverts InvalidShares(). The adapter's fixed 100-unit discrepancy tolerance cannot handle this because the upstream call reverts before the post-deposit check.

Yieldcoin can send this amount even though each user deposit must be at least one USDC. ParentVaultEpochLib aggregates deposits and floor-valued withdrawals and calculates netFlow = totalDepositAmount - totalWithdraw. A one-USDC deposit netted against a withdrawal worth 0.999999 USDC therefore creates a remote net deposit of exactly one base unit. Parent marks the epoch EXECUTING and bridges that unit.

When the Child receives it, _handleCCIPDeposit catches the adapter revert and stores EPOCH_DEPOSIT(epochNonce, 1). _recoverFailedEpochDeposit can only retry the same one-unit amount. Aave v4 maintains a non-decreasing added-asset/share exchange rate, so once the configured reserve is above 1:1, normal accrual and elapsed time cannot make one asset unit mint a share. Meanwhile _requireNoRecovery rejects later Child operations, and Parent refuses to close another epoch or initiate a rebalance while the prior epoch is executing.

Vulnerable scenario:

  1. The active strategy is the configured Ethereum Aave v4 USDC reserve.
  2. A user submits the minimum one-USDC deposit while a holder submits shares whose floor-valued withdrawal claim is 0.999999 USDC.
  3. The epoch operator closes the epoch with a truthful TVL report. Parent computes and sends one USDC base unit and records the epoch as EXECUTING.
  4. Production CCIP accepts the amount and delivers it to Child.
  5. The real Aave v4 Hub rounds the supply to zero shares and reverts InvalidShares().
  6. Child stores exact one-unit deposit recovery. Every recovery attempt reaches the same permanent share-conversion boundary, while the recovery and executing-epoch guards prevent all normal progress.

No Aave governance action, reserve cap exhaustion, donation, market illiquidity, or protocol insolvency is required.

KI-014 accepts a superficially similar shape: recovery retries the full original amount, cannot make incremental progress, and its stall "can nevertheless be indefinite." It is scoped to EPOCH_WITHDRAW and REBALANCE_WITHDRAW recovery, where a lending market cannot supply the requested liquidity. This is EPOCH_DEPOSIT recovery, and the distinction decides the outcome: KI-014's stall waits on market liquidity, which returns. Aave v4 maintains a non-decreasing added-asset to added-share exchange rate, so the conversion that rejects one base unit today rejects it strictly more firmly tomorrow. Waiting is a valid response to KI-014 and is not a response here.

03Section · Impact

Impact

An unprivileged user can indefinitely deny all holders access to the strategy principal with roughly two USDC of intents. The affected cohort cannot claim or cancel, later epochs cannot close, and the strategy cannot rebalance. Normal operation can resume only after a vault upgrade/state repair or an unsafe accounting override; waiting and exact recovery do not help.

The impact is a permanent lock of the entire strategy position combined with protocol-wide liveness denial, and the cost of causing it is approximately two USDC. No privileged role is involved, no external market condition is required, and nothing about the attacker's position is unusual beyond holding shares to withdraw. The one constraint on likelihood is that the epoch's aggregate net flow must land on exactly one base unit, which requires submitting intents against a known epoch aggregate rather than blindly — intents are public and the submission window is open until close, so this bounds the attack without preventing it.

04Section · Recommendation

Recommendation

The correction must happen before Parent commits an asynchronous remote deposit; changing the adapter's 100-unit tolerance is insufficient.

  1. Add an adapter preview/minimum-deposit interface. For Aave v4, resolve the reserve's Hub and asset ID and require the asset-to-added-share preview to return at least one share.
  2. Before marking a remote epoch executing, reject or reconcile a positive net flow that the target adapter cannot accept. A sub-minimum amount can remain explicitly accounted as loose dust or roll into a later epoch while the current cohort becomes claimable.
  3. Classify InvalidShares() as terminal rather than replayable on Child. Add an authenticated Child-to-Parent abort message that accounts for the loose asset, clears recovery, and safely finalizes or reopens the epoch.
  4. Add fork regressions for zero, one, and the first amount that previews to a nonzero Aave v4 share, including after the reserve exchange rate increases.

Pooling the amount with a later inbound message is not a recovery under the current implementation, because the singleton recovery guard blocks processing that later message.

05Section · Resolution

Resolution

Fixed in e7e38a6: the adapter previews the conversion and retains an unrepresentable amount as capped, tracked assets instead of letting the upstream reject it, applied to Aave v4 and v3 alike.

06Section · Affected files

Affected files

  • evm/src/modules/adapters/AaveV4Adapter.sol#L51-L60
  • evm/src/libraries/vaults/ParentVaultEpochLib.sol#L192-L275
  • evm/src/vaults/ChildVault.sol#L133-L145, #L390-L414
  • evm/src/vaults/BaseVault.sol#L278-L300, #L359-L364
  • evm/lib/aave-v4/src/spoke/Spoke.sol#L225-L240
  • evm/lib/aave-v4/src/hub/Hub.sol#L200-L220
  • evm/lib/aave-v4/src/hub/libraries/SharesMath.sol#L13-L27
Status
Fixed
Fix commit
e7e38a6
F-2026-0002