All incidents

Aave AMM LP Oracle Manipulation Through Delegated Recursive LP Looping

Share
Jan 11, 2023 15:28 UTCAttackLoss: 2.29 WBTC, 39,982.13 USDCPending manual check1 exploit txWindow: Atomic
Estimated Impact
2.29 WBTC, 39,982.13 USDC
Label
Attack
Exploit Tx
1
Addresses
3
Attack Window
Atomic
Jan 11, 2023 15:28 UTC → Jan 11, 2023 15:28 UTC

Exploit Transactions

TX 1Ethereum
0x927b784148b60d5233e57287671cdf67d38e3e69e5b6d0ecacc7c1aeaa98985b
Jan 11, 2023 15:28 UTCExplorer

Victim Addresses

0x5F360c6b7B25DfBfA4F10039ea0F7ecfB9B02E60Ethereum
0x8A4236F5eF6158546C34Bd7BC2908B8106Ab1Ea1Ethereum
0x004375Dff511095CC5A197A54140a24eFEF3A416Ethereum

Loss Breakdown

2.29WBTC
39,982.13USDC

Similar Incidents

Root Cause Analysis

Aave AMM LP Oracle Manipulation Through Delegated Recursive LP Looping

1. Incident Overview TL;DR

On Ethereum mainnet block 16384470, tx 0x927b784148b60d5233e57287671cdf67d38e3e69e5b6d0ecacc7c1aeaa98985b used a single adversary-crafted call from EOA 0x67a909f2953fb1138bea4b60894b51291d2d0795 into helper contract 0x3a5b7db0be9f74324370fbd65b75850a5c82d176. The helper took a zero-fee Balancer flash loan of 5,673,090.338021 USDC, deposited that USDC into the Aave AMM market on behalf of the borrower EOA, recursively borrowed the listed WBTC/USDC Uniswap V2 LP token against delegated debt, and redeposited the LP onto itself as collateral. After building that collateral stack, the helper burned one LP tranche, donated USDC back into the pair, and synced the pair before calling the final Aave USDC borrow. The validated all_relevant_txs set for this incident contains only this adversary-crafted transaction.

That sequence changed the live pair state that Aave's LP oracle read during borrow validation. The LP price seen by Aave rose from 3495450576387056244740 before the exploit to 4320806762049972060102 at the final borrow check, which let the helper borrow the full Aave USDC reserve of 5673090338021 base units. The attacker-controlled helper finished the block with 229462904 WBTC satoshis and 39982134101 USDC base units, while the attacker EOA paid 532165187372027622 wei in gas. Using the same block's Chainlink BTC/USD and ETH/USD answers, the net profit was 79246.202439 USD.

The root cause is an oracle design failure in the Aave AMM market. Aave treated a thin Uniswap V2 LP token as both collateral and a borrowable reserve while valuing it from synchronous pair reserves and totalSupply() inside the same transaction. Because the borrow path had no TWAP, cooldown, or anti-self-referential guard, the attacker could create temporary borrowing power by rewriting the LP pair state immediately before the final borrow validation.

2. Key Background

Aave V2 borrow validation values every active collateral and debt position through the configured price oracle. In the Aave V2 GenericLogic and ValidationLogic libraries, the protocol converts balances into ETH-equivalent values with getAssetPrice(asset) and then checks whether the requested borrow is still covered by the user's collateral:

vars.reserveUnitPrice = IPriceOracleGetter(oracle).getAssetPrice(vars.currentReserveAddress);
...
vars.amountOfCollateralNeededETH = vars.userBorrowBalanceETH.add(amountInETH).percentDiv(
  vars.currentLtv
);
require(
  vars.amountOfCollateralNeededETH <= vars.userCollateralBalanceETH,
  Errors.VL_COLLATERAL_CANNOT_COVER_NEW_BORROW
);

The exploit also relies on Aave credit delegation. DebtTokenBase.approveDelegation() allows one account to delegate borrow capacity to another account without transferring collateral ownership:

function approveDelegation(address delegatee, uint256 amount) external override {
  _borrowAllowances[_msgSender()][delegatee] = amount;
  emit BorrowAllowanceDelegated(_msgSender(), delegatee, _getUnderlyingAssetAddress(), amount);
}

That means one attacker-controlled address can accumulate the LP debt while another attacker-controlled address accumulates the LP aToken collateral.

Finally, Uniswap V2 LP value per share is a direct function of pair balances and LP totalSupply(). The pair contract's burn() and sync() functions update reserves from live token balances:

function burn(address to) external lock returns (uint amount0, uint amount1) {
  uint balance0 = IERC20(_token0).balanceOf(address(this));
  uint balance1 = IERC20(_token1).balanceOf(address(this));
  uint liquidity = balanceOf[address(this)];
  amount0 = liquidity.mul(balance0) / _totalSupply;
  amount1 = liquidity.mul(balance1) / _totalSupply;
  _burn(address(this), liquidity);
  _safeTransfer(_token0, to, amount0);
  _safeTransfer(_token1, to, amount1);
  _update(balance0, balance1, _reserve0, _reserve1);
}

function sync() external lock {
  _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}

Any oracle that reads pair reserves and totalSupply() synchronously is therefore manipulable if the attacker can burn LP, change one-sided balances, and force a reserve update before the borrow check.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK case, not a passive market event. The vulnerable condition was Aave AMM listing the WBTC/USDC Uniswap V2 LP token 0x004375Dff511095CC5A197A54140a24eFEF3A416 as collateral and debt while pricing it through AaveOracle 0x8A4236F5eF6158546C34Bd7BC2908B8106Ab1Ea1, which forwarded the LP lookup to source 0x849AF4b128be3317a694bFD262dEFF636AB84c1b. The seed trace shows that this source read UniswapV2Pair::getReserves() and UniswapV2Pair::totalSupply() during the same transaction in which the attacker modified those values.

The attacker first used delegated borrowing to separate debt from collateral: the borrower EOA carried LP variable debt while the helper contract built a large LP aToken balance. Once the helper had enough LP collateral, it burned one additional LP tranche, withdrew both underlying assets, donated USDC back into the pair, and synced the pool to write the manipulated balances into reserve storage. Aave then recalculated collateral using the inflated LP price during the final USDC borrow validation and accepted borrowing power that existed only because of the attacker's intra-transaction state changes. No privileged role, compromised key, private orderflow, or attacker-side artifact was required; every primitive used was permissionless.

The missing controls were exactly the ones a safe collateral oracle for thin LP tokens needs: a manipulation-resistant valuation source, temporal separation between collateral state changes and borrow authorization, or a circuit breaker that prevented the same LP from being both thinly borrowable and immediately mark-to-spot collateral. Because none of those controls existed, the attacker could self-create borrowing capacity and drain the seeded USDC liquidity.

4. Detailed Root Cause Analysis

4.1 ACT Opportunity Definition

The ACT opportunity existed in the Ethereum pre-state immediately before the exploit transaction in block 16384470. Relevant public conditions were already present:

  • Aave AMM LendingPool 0x5F360c6b7B25DfBfA4F10039ea0F7ecfB9B02E60 listed the WBTC/USDC LP reserve.
  • The LP price path was already wired through AaveOracle 0x8A4236... to source 0x849AF4....
  • The LP reserve was funded and borrowable.
  • Balancer's flash loan interface and Aave credit delegation were open to any unprivileged caller.

The root safety invariant is:

Collateral used to authorize borrowing must be priced with a manipulation-resistant valuation, so a borrower cannot increase the borrowing capacity of already-deposited collateral inside the same transaction by changing a thin LP's reserves or supply.

The concrete code-level breakpoint is Aave V2's collateral valuation path in GenericLogic.calculateUserAccountData(), which writes vars.reserveUnitPrice = IPriceOracleGetter(oracle).getAssetPrice(vars.currentReserveAddress), and the downstream ValidationLogic.validateBorrow() check that compares the requested borrow against vars.userCollateralBalanceETH. For this reserve, that price read reached 0x849AF4...::latestAnswer(), which the trace shows reading the manipulated pair state directly.

4.2 Bootstrap and Delegated Recursive LP Loop

The attacker used credit delegation exactly as intended by Aave's debt token design, but in a composition that Aave's LP oracle model could not safely tolerate. The exploit tx starts with the helper taking a zero-fee Balancer flash loan and depositing the flash-loaned USDC into Aave on behalf of the borrower EOA. The helper then repeatedly borrows the LP token on behalf of that borrower and redeposits the LP onto itself.

By the end of the transaction:

  • the borrower EOA held 145820303250 LP variable debt units,
  • the helper held 143308665997 LP aToken units as collateral,
  • the helper later opened 5673090338021 USDC variable debt,
  • the Aave LP reserve itself had been fully emptied of immediately borrowable LP.

Those balances are not inferred; they are the post-state deltas recorded in the collector artifacts. The loop was permissionless because the only special action was approveDelegation, which the borrower EOA can call directly.

4.3 LP Burn, Single-Sided Donation, and Oracle Manipulation

The exploitable LP oracle behavior appears when the helper burns one borrowed LP tranche and then rewrites the pair reserve state before the final USDC borrow. The Uniswap pair source code above explains why this works: burn() pays out underlying based on live balances and sync() snapshots live balances back into reserves. The seed trace shows that exact state transition:

UniswapV2Pair::burn(0x3a5b7DB0BE9F74324370FBD65b75850A5c82D176)
emit Sync(reserve0: 282538140, reserve1: 49288009792)
emit Burn(sender: 0x3a5b7DB0BE9F74324370FBD65b75850A5c82D176, amount0: 296141905, amount1: 51661149896, to: 0x3a5b7DB0BE9F74324370FBD65b75850A5c82D176)
UniswapV2Pair::sync()
emit Sync(reserve0: 282538140, reserve1: 75312078962)
LendingPool::borrow(FiatTokenProxy, 5673090338021, 2, 0, 0x3a5b7DB0BE9F74324370FBD65b75850A5c82D176)
0x849AF4b128be3317a694bFD262dEFF636AB84c1b::latestAnswer()
  UniswapV2Pair::getReserves() -> (282538140, 75312078962, ...)
  UniswapV2Pair::totalSupply() -> 2818151713
  -> 4320806762049972060102

That trace matters for two reasons.

First, it proves the attacker's pair-state rewrite happened before the final Aave borrow validation, not as a later accounting artifact. Second, it proves the LP oracle was consuming the manipulated reserves and reduced LP supply synchronously. The LP oracle value seen by Aave during the final borrow rose from 3495450576387056244740 before the attack to 4320806762049972060102 at the validation point, and then to 4322044803711754187326 after the block. That is exactly the behavior a manipulation-resistant collateral oracle must prevent.

4.4 Why the Final Borrow Passed

Once the helper's LP collateral was repriced upward, ValidationLogic.validateBorrow() treated that inflated LP balance as real collateral and approved the full USDC reserve borrow. The attack did not need to forge state or bypass access control. It simply ensured that when Aave asked, "what is this user's LP collateral worth right now?", the answer came from the attacker's own temporary, same-tx rewrite of the pair's reserves and supply.

This also explains why delegated borrowing was only an execution primitive, not the root cause. Credit delegation let the attacker separate who carried the LP debt from who carried the LP collateral, but the loss came from Aave trusting a same-tx spot price for that LP collateral. Without the oracle flaw, the delegated loop would not have created extractable borrowing power.

The exploit conditions were therefore:

  • the LP token had to be listed in the Aave AMM market,
  • Aave had to price it from the live LP reserve/supply path,
  • public flash liquidity and delegation had to be available,
  • the LP pair had to be thin enough that burning one tranche and donating one-sided reserves materially changed the price.

The violated security principles were equally direct:

  • collateral oracles must be manipulation-resistant at the borrow decision point,
  • a protocol must not allow users to self-create borrowing power from their own intra-tx state changes,
  • thin LP tokens should not be both borrowable and collateralizable against a synchronous spot oracle.

5. Adversary Flow Analysis

The adversary strategy was a single-transaction, multi-stage attack that combined public flash liquidity, delegated recursive LP borrowing, LP oracle manipulation, and a final reserve drain.

  1. Bootstrap and funding The tx sender 0x67a909... called helper 0x3a5b7d..., which pulled a zero-fee Balancer flash loan of 5673090338021 USDC.

  2. Deposit and delegation setup The helper deposited the flash-loaned USDC into Aave on behalf of the borrower EOA. The attacker had already granted delegation on the LP variable debt token, so the helper could borrow LP against the borrower's credit line.

  3. Recursive LP loop The helper repeatedly called LendingPool::borrow(LP_PAIR, ..., onBehalfOf = borrower) and then LendingPool::deposit(LP_PAIR, ..., onBehalfOf = helper). This stacked LP aToken collateral on the helper while leaving the borrower with the matching LP variable debt.

  4. LP manipulation The helper borrowed one more LP tranche, transferred it into the Uniswap pair, called burn(), and received 296141905 WBTC satoshis plus 51661149896 USDC base units. It then returned USDC to the pair and called sync(), updating the pair reserves to a state that produced a higher LP oracle value.

  5. Final Aave borrow Immediately after the sync, the helper borrowed the full Aave USDC liquidity of 5673090338021 base units. The borrow passed because Aave recalculated LP collateral using the manipulated price.

  6. Profit realization The helper repaid the Balancer flash loan principal and kept the residual assets. The post-state balance diff shows the helper retaining 229462904 WBTC satoshis and 39982134101 USDC base units.

This execution satisfies the ACT model. The sequence uses only public contracts, public state, public code, and a permissionless adversary-controlled EOA plus freshly deployed helper contract. No privileged consensus power, private key compromise, or non-public dependency is required.

6. Impact & Losses

The measurable impact from the exploit transaction is:

  • 2.29462904 WBTC (229462904 satoshis) captured by the attacker-controlled helper.
  • 39982.134101 USDC (39982134101 base units) captured by the attacker-controlled helper.
  • 709.158007 USD of gas paid by the attacker EOA, against 79955.360446 USD gross profit.
  • 79246.202439 USD net profit after gas.

Measured in the report's reference asset, the adversary moved from 0 USD before the transaction to 79955.360446 USD after the transaction, with the full delta explained by the post-state WBTC and USDC balances minus gas. The protocol impact is broader than the immediate token balances. Aave AMM's borrow-capacity calculation accepted a temporary, attacker-manufactured increase in LP collateral value as if it were legitimate collateral appreciation. The final USDC borrow also drained the seeded Aave USDC liquidity, leaving the USDC aToken with zero immediately available USDC after the exploit.

7. References

  • Seed exploit transaction: 0x927b784148b60d5233e57287671cdf67d38e3e69e5b6d0ecacc7c1aeaa98985b
  • Collector metadata: /workspace/session/artifacts/collector/seed/1/0x927b784148b60d5233e57287671cdf67d38e3e69e5b6d0ecacc7c1aeaa98985b/metadata.json
  • Collector trace: /workspace/session/artifacts/collector/seed/1/0x927b784148b60d5233e57287671cdf67d38e3e69e5b6d0ecacc7c1aeaa98985b/trace.cast.log
  • Collector balance diff: /workspace/session/artifacts/collector/seed/1/0x927b784148b60d5233e57287671cdf67d38e3e69e5b6d0ecacc7c1aeaa98985b/balance_diff.json
  • Historical oracle and profit observations: /workspace/session/artifacts/auditor/iter_0/rpc_observations.json
  • Aave V2 GenericLogic.sol: https://raw.githubusercontent.com/aave/protocol-v2/master/contracts/protocol/libraries/logic/GenericLogic.sol
  • Aave V2 ValidationLogic.sol: https://raw.githubusercontent.com/aave/protocol-v2/master/contracts/protocol/libraries/logic/ValidationLogic.sol
  • Aave DebtTokenBase.sol source artifact: /workspace/session/artifacts/collector/seed/1/0xb19dd5dad35af36cf2d80d1a9060f1949b11fcb0/src/DebtTokenBase.sol
  • Uniswap V2 pair source artifact: /workspace/session/artifacts/collector/seed/1/0x004375dff511095cc5a197a54140a24efef3a416/src/Contract.sol