Inactive Aave target supply can self-seal a full rebalance recovery
Anyone can supply to Aave on behalf of the still-inactive target adapter and consume the reserve's supply-cap headroom. The rebalance then fails into a recovery that retries the same over-cap supply, sealing the vault in recovery.
Description
Aave V3 lets any caller supply an asset for any onBehalfOf address, and separately rejects a
supply that would push reserve supply above the reserve's cap. A caller can therefore mint aTokens
directly to a registered Yieldcoin adapter without touching the adapter or the vault, consuming
capacity that a pending rebalance needs.
With P the position in the source adapter, C the target reserve's available headroom and U an
unsolicited supply credited to the still-inactive target adapter, the honest rebalance fits when
P <= C. An attacker chooses U > C - P.
executeRebalance then withdraws the full source position, activates the target, and attempts to
deposit P. The deposit fails on the cap. The source withdrawal and the adapter change are already
committed and are not rolled back, so the Child stores REBALANCE_DEPOSIT(P) and returns
successfully:
source adapter = 0target adapter = UChild loose balance = Precovery obligation = REBALANCE_DEPOSIT(P)Parent rebalance = REBALANCING (source -> target)
Recovery retries exactly P, which still exceeds the remaining headroom, so every retry fails. The
distinguishing property is that the blocking U belongs to the Yieldcoin target adapter itself: no
unrelated Aave supplier can exit to release it, and no Yieldcoin path can withdraw it either.
AaveV3Adapter.withdraw is vault-only, and both vault paths that withdraw — the epoch withdrawal
and a further rebalance — are excluded while recovery is pending. Recovery itself only deposits.
The retry is therefore self-sealing: its own precondition is destroyed by a balance the recovering vault holds and cannot release. Parent cannot close an epoch or start a different rebalance while its first rebalance remains in progress, so the position and all outstanding shares are immobilised. Repair requires an external Aave-governance cap increase, or a purpose-built upgrade that can unwind the target, restore the principal, clear Child recovery and reconcile Parent's pending rebalance.
An unprivileged account can immobilise the vault's entire strategy principal and every outstanding
share indefinitely, repairable only from outside the protocol, using capital only slightly greater
than the target reserve's headroom above the position. That overshoot is forfeited rather than
recoverable — against the smallest headroom currently targeted for Aave v3 USDC, roughly
16.5m USDC against a low initial position, an attacker would irrecoverably commit several orders
of magnitude more than the value they immobilise, which is what confines the trigger to targets
whose headroom sits close to the position. No trusted actor misbehaves: rebalance selection and the
TVL observation are truthful throughout, and the only adversarial entry point is Aave's public
supply.
Vulnerable scenario
- The position
Pis deployed in Child protocol A. Protocol B's Aave V3 adapter is registered but inactive, andP <= C. - A truthful observation reports the active source TVL.
ChildVault.getTVL()reads only the active adapter, and rebalance selection checks APY, not target reserve capacity. - An unprivileged account calls Aave
supply(asset, U, targetAdapter, 0)before the selected rebalance executes, withP + U > C. - Parent commits
REBALANCINGfrom A to B. - Child clears A, activates B, fails to deposit
P, and storesREBALANCE_DEPOSIT(P). - Every retry reproduces the same failure, and no Child or Parent transition can reroute.
Recovery attempts that do not resolve it
Pausing every component, force-cancelling the open deposit, unpausing in dependency order,
cancelling the open withdrawal intent, and retrying gives BaseVault__DepositFailed. The authorised
Child reroute gives BaseVault__RecoveryAlreadyPending. Parent close and Parent reroute both give
ParentVault__RebalanceInProgress. Custody and obligations are unchanged throughout. Calling
completeRebalance is not a safe escape either: it would make Parent authoritative for a target
that never received the principal.
Recommendation
Add an authenticated, state-bound abort for a failed asynchronous rebalance deposit. The Child half is straightforward; the Parent half is a new lifecycle transition. Both are needed.
Child side — roll back rather than latch an unsatisfiable retry. On a failed target deposit, withdraw whatever the target holds, restore the previous adapter as active, and redeposit the combined balance there:
bool success = _executeDeposit(tvlToRebalance, false, newAdapter);if (success) {emit RebalanceDepositSuccess(rebalanceNonce, tvlToRebalance);} else {(, uint256 reclaimed) = _executeWithdraw(type(uint256).max, false, newAdapter);_baseVaultStorage().s_activeProtocolAdapter = oldAdapter;_executeDeposit(tvlToRebalance + reclaimed, true, oldAdapter);emit RebalanceDepositFailure(rebalanceNonce, tvlToRebalance);}
Applied to the audited commit this prevents the terminal state above: the principal returns to a yielding position, no recovery is latched, and the unsolicited supply is absorbed into vault accounting instead of sealing the retry. It is a control rather than a shipping patch — it writes the active adapter directly instead of re-resolving it, does not distinguish unsolicited surplus from vault-originated principal, and is not bound to the stored rebalance nonce, source, target and amounts. A production version should do all three.
Parent side. Even with the rollback, Parent remains REBALANCING with no way to stand down,
which continues to block epoch settlement. Closing that requires an authenticated failure
acknowledgement letting Parent clear the pending rebalance without marking the failed target active.
This has not been prototyped here, because it is a new transition across both vaults whose
authentication needs designing rather than a local change.
Reverting the failed deposit instead of storing recovery is not sufficient on its own. It preserves
the source position, but the blocking supply remains and Parent is still REBALANCING, so it trades
an immobilised principal for an immobilised lifecycle without providing an abort.
Selection-time capacity check. Accounting for reserve caps, utilisation and available headroom before initiating a rebalance keeps the required overshoot large enough that the attack stays uneconomical, and is a change to target selection rather than to vault logic. Because the rating above depends on that margin holding, this is the mitigation the severity is conditioned on.
Interim measure. Until both halves exist, do not schedule asynchronous rebalances into adapters whose underlying protocol lets third parties create positions for the adapter address. An off-chain capacity preflight is not sufficient on its own, because the observation can go stale between selection and Child execution.
When to re-rate. The cost ratio moves against the protocol as it grows, because the attacker's
cost tracks the reserve's remaining headroom while the damage tracks the position. Below
position = headroom / 2 the attacker pays more than the value frozen; above it they pay less, and
against the tightest currently targeted headroom that crossover sits near 8m USDC of position on
that chain. Evaluate against live headroom at selection time rather than against the cap, since
ordinary third-party supply consumes headroom with no attacker involvement, and treat any candidate
target whose headroom is close to the position as in scope irrespective of absolute size.
Resolution
Acknowledged and documented as KI-027 in 7b97b61 and 3a8d0c0, which records the margin between the position and live headroom as the condition the rating rests on; nothing on-chain enforces that margin.
Affected files
evm/src/vaults/ChildVault.sol:296-337(executeRebalance,_rebalanceToNewStrategy)evm/src/vaults/ChildVault.sol:364-379(executeRecovery)evm/src/vaults/BaseVault.sol:359-399(recovery store and retry)evm/src/modules/adapters/AaveV3Adapter.sol:49-98