A one-unit Aave v4 deposit permanently pins a remote epoch
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.
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:
uint120 shares = asset.toAddedSharesDown(amount).toUint120();require(shares > 0, InvalidShares());
With Aave v4's virtual balances, the conversion is:
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:
- The active strategy is the configured Ethereum Aave v4 USDC reserve.
- A user submits the minimum one-USDC deposit while a holder submits shares whose floor-valued
withdrawal claim is
0.999999USDC. - 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. - Production CCIP accepts the amount and delivers it to Child.
- The real Aave v4 Hub rounds the supply to zero shares and reverts
InvalidShares(). - 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.
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.
Recommendation
The correction must happen before Parent commits an asynchronous remote deposit; changing the adapter's 100-unit tolerance is insufficient.
- 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.
- 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.
- 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. - 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.
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.
Affected files
evm/src/modules/adapters/AaveV4Adapter.sol#L51-L60evm/src/libraries/vaults/ParentVaultEpochLib.sol#L192-L275evm/src/vaults/ChildVault.sol#L133-L145,#L390-L414evm/src/vaults/BaseVault.sol#L278-L300,#L359-L364evm/lib/aave-v4/src/spoke/Spoke.sol#L225-L240evm/lib/aave-v4/src/hub/Hub.sol#L200-L220evm/lib/aave-v4/src/hub/libraries/SharesMath.sol#L13-L27