Aave V3 Fork Oracle Misconfiguration: USDC Priced as BTC

On February 26, 2026, an attacker exploited a misconfigured Aave V3 fork lending pool on Ethereum mainnet (block 24,538,897). The root cause was a deployment-time oracle misconfiguration in the AaveOracle contract at 0x9dce7a180c34203fee8ce8ca62f244feeb67bd30, where the constructor arguments contained an off-by-one index error that mapped the USDC price feed to the Chainlink BTC/USD aggregator instead of the correct USDC/USD aggregator. This caused the protocol to value 8.879192 USDC as if it were worth 8.879192 BTC ($608,705 at the BTC/USD price of $68,554 at block 24,538,897), allowing the attacker to borrow 187.366746 WETH ($389,811 at $2,080.47/ETH) against effectively zero collateral. The attacker profited approximately 181.75 ETH after fees and flash swap costs.

Root Cause

Vulnerable Contract

  • Contract: AaveOracle (Aave V3 fork)
  • Address: 0x9dce7a180c34203fee8ce8ca62f244feeb67bd30
  • Proxy status: Not a proxy (standalone contract)
  • Source type: Verified (Etherscan)

Vulnerable Function

  • Function: getAssetPrice(address asset)
  • Selector: 0xb3596f07
  • File: @aave/core-v3/contracts/misc/AaveOracle.sol

The getAssetPrice function itself is correct code from the canonical Aave V3 codebase. The vulnerability is in the state of the contract: the assetsSources mapping was misconfigured such that assetsSources[USDC] pointed to the Chainlink BTC/USD aggregator instead of the USDC/USD aggregator.

Vulnerable Code

// From AaveOracle.sol (verified source)
function getAssetPrice(address asset) public view override returns (uint256) {
    AggregatorInterface source = assetsSources[asset]; // <-- VULNERABILITY: reads misconfigured mapping

    if (asset == BASE_CURRENCY) {
        return BASE_CURRENCY_UNIT;
    } else if (address(source) == address(0)) {
        return _fallbackOracle.getAssetPrice(asset);
    } else {
        int256 price = source.latestAnswer(); // <-- Returns BTC/USD price (~$68,554 at block 24,538,897) when queried for USDC
        if (price > 0) {
            return uint256(price);
        } else {
            return _fallbackOracle.getAssetPrice(asset);
        }
    }
}

The assetsSources mapping is set via setAssetSources(), which is guarded by onlyAssetListingOrPoolAdmins:

function setAssetSources(
    address[] calldata assets,
    address[] calldata sources
) external override onlyAssetListingOrPoolAdmins {
    _setAssetsSources(assets, sources);
}

function _setAssetsSources(address[] memory assets, address[] memory sources) internal {
    require(assets.length == sources.length, Errors.INCONSISTENT_PARAMS_LENGTH);
    for (uint256 i = 0; i < assets.length; i++) {
        assetsSources[assets[i]] = AggregatorInterface(sources[i]); // <-- VULNERABILITY: no validation of source
        emit AssetSourceUpdated(assets[i], sources[i]);
    }
}

Why It’s Vulnerable

Expected behavior: getAssetPrice(USDC) should return the USDC/USD price from Chainlink (approximately 1e8, representing $1.00 in 8-decimal base currency units). The assetsSources[USDC] mapping should point to the Chainlink USDC/USD aggregator at 0x8fffffd4afb6115b954bd326cbe7b4ba576818f6.

Actual behavior: At the time of the exploit, assetsSources[USDC] pointed to the Chainlink BTC/USD aggregator at 0xf4030086522a5beea4988f8ca5b36dbc97bee88c, which returned 6,855,405,329,514 (representing ~$68,554 in 8-decimal base currency units at block 24,538,897). This is confirmed by the trace: when getAssetPrice(0xa0b86991c...USDC) is called, the oracle at 0x9dce7a18 makes a STATICCALL to the BTC/USD Chainlink proxy 0xf4030086 (selector 0x50d25bcd = latestAnswer()), not the USDC/USD feed.

Constructor arguments reveal a deployment-time misconfiguration: Decoding the constructor arguments from info.json reveals that the oracle was misconfigured from the moment of deployment, not via a later admin call. The constructor sources array has an off-by-one shift error: USDC (assets[2]) was mapped to the BTC/USD aggregator (sources[2] = 0xf4030086), USDT (assets[3]) was mapped to the USDC/USD aggregator (sources[3] = 0x8fffffd4), and USDe (assets[4]) was mapped to the USDT/USD aggregator (sources[4] = 0x3e7d1eab). WBTC (assets[1]) received 0x0 (no oracle). This was a deployment script bug — the correct oracle addresses were present but associated with the wrong asset addresses due to an index offset error.

Note: The prestate trace (trace_prestateTracer.json) confirms the AaveOracle’s storage was not modified during this transaction (the contract does not appear in the pre diff section), confirming the misconfiguration pre-existed the exploit transaction.

Impact of the misconfiguration: The GenericLogic.calculateUserAccountData() function computes collateral value as scaledBalance * normalizedIncome * assetPrice / assetUnit. With the BTC/USD price (~68,554x the correct USDC/USD price at block 24,538,897), the attacker’s 8.879192 USDC deposit was valued at approximately $608,705 instead of $8.88 – an inflation factor of ~68,554x. This passed the health factor check in ValidationLogic.validateBorrow() and allowed borrowing 187.366746 WETH.

Normal flow vs Attack flow:

  • Normal flow: User deposits 8.879 USDC. Oracle returns $1/USDC. Collateral = $8.88. Maximum borrow at 80% LTV = ~$7.10 worth of assets.
  • Attack flow: Attacker deposits 8.879 USDC. Oracle returns ~$68,554/USDC (BTC/USD price at block 24,538,897). Collateral = ~$608,705. Maximum borrow = $487,000 worth of assets. Attacker borrows 187.366746 WETH ($389,811 at $2,080.47/ETH) and walks away.

Attack Execution

High-Level Flow

  1. Attacker EOA (0x3885...) calls attacker contract (0x3e47...) using a disguised approve() selector with extra calldata encoding the full attack plan.
  2. Attacker contract initiates a Uniswap V2 flash swap of 8.88 USDC from the USDC/WETH pair (0xb4e1...).
  3. Inside the uniswapV2Call callback, the attacker contract approves the LendingPool to spend the 8.88 USDC.
  4. Attacker contract calls LendingPool.deposit(USDC, 8879192, self, 0) to deposit 8.88 USDC as collateral.
  5. The deposit succeeds and the oracle misconfiguration values the 8.879192 USDC as ~$608,705 worth of collateral (BTC/USD price of ~$68,554 at block 24,538,897).
  6. Attacker contract calls LendingPool.borrow(WETH, 187.37e18, 2, 0, self) to borrow 187.37 WETH.
  7. The borrow passes the health factor check because the collateral is massively overvalued due to the oracle returning the BTC price for USDC.
  8. Attacker contract unwraps the borrowed WETH to ETH, repays ~0.0043 WETH to the Uniswap V2 pair as flash swap fee, sends ~5.61 ETH to the block builder, and transfers ~181.75 ETH profit to the attacker EOA.

Detailed Call Trace

depth 0: EOA 0x3885... -> AttackerContract 0x3e47... [CALL] approve(address,uint256) 0x095ea7b3 (804 bytes input)
  depth 1: AttackerContract -> UniV2Pair 0xb4e1... [STATICCALL] token1() 0xd21220a7
  depth 1: AttackerContract -> UniV2Pair 0xb4e1... [CALL] swap(uint256,uint256,address,bytes) 0x022c0d9f
    depth 2: UniV2Pair -> USDC 0xa0b8... [CALL] transfer(address,uint256) 0xa9059cbb  // 8,879,192 USDC to attacker
      depth 3: USDC -> USDCImpl 0x4350... [DELEGATECALL] transfer
    depth 2: UniV2Pair -> AttackerContract [CALL] uniswapV2Call(address,uint256,uint256,bytes) 0x10d1e85c
      depth 3: AttackerContract -> UniV2Pair [STATICCALL] getReserves() 0x0902f1ac
      depth 3: AttackerContract -> USDC [STATICCALL] balanceOf(address) 0x70a08231
      depth 3: AttackerContract -> USDC [CALL] approve(LendingPool, 0) 0x095ea7b3
      depth 3: AttackerContract -> USDC [CALL] approve(LendingPool, 8879192) 0x095ea7b3
      depth 3: AttackerContract -> LendingPool 0x7398... [CALL] deposit(USDC, 8879192, self, 0) 0xe8eda9df
        depth 4: LendingPool -> PoolLogic_Main 0xb86a... [DELEGATECALL] deposit 0xe8eda9df
          depth 5: LendingPool -> PoolLogic_Validation 0x8ce6... [DELEGATECALL] 0x1913f161
            depth 6: LendingPool -> StableDebtUSDC [STATICCALL] scaledTotalSupply
            depth 6: LendingPool -> OracleProxy_USDC [STATICCALL] getSupplyData
            depth 6: LendingPool -> aUSDC [STATICCALL] scaledTotalSupply
            depth 6: LendingPool -> InterestRateStrategy_USDC [STATICCALL] calculateInterestRates
          depth 5: LendingPool -> USDC [CALL] transferFrom(attacker, aUSDC, 8879192)
          depth 5: LendingPool -> aUSDC [CALL] mint(attacker, attacker, 8879192, index)
            depth 6: aUSDC -> ATokenImpl [DELEGATECALL] mint
              depth 7: aUSDC -> IncentivesController [CALL] handleAction
      depth 3: AttackerContract -> LendingPool [CALL] borrow(WETH, 187.37e18, 2, 0, self) 0xa415bcad
        depth 4: LendingPool -> PoolLogic_Main 0xb86a... [DELEGATECALL] borrow 0xa415bcad
          depth 5: LendingPool -> PoolAddressesProvider [STATICCALL] getPriceOracle
          depth 5: LendingPool -> PoolAddressesProvider [STATICCALL] getPriceOracleSentinel
          depth 5: LendingPool -> PoolLogic_UserAccountData 0x07b5... [DELEGATECALL] 0x1e6473f9  // calculateUserAccountData
            depth 6: LendingPool -> VariableDebtWETH [STATICCALL] scaledTotalSupply
            depth 6: LendingPool -> OracleProxy_WETH [STATICCALL] getSupplyData
            depth 6: LendingPool -> PriceOracle 0x9dce... [STATICCALL] getAssetPrice(USDC) 0xb3596f07
              depth 7: PriceOracle -> Chainlink BTC/USD 0xf403... [STATICCALL] latestAnswer  // WRONG FEED!
                depth 8: BTC/USD Proxy -> BTC/USD Impl [STATICCALL] latestAnswer
            depth 6: LendingPool -> aUSDC [STATICCALL] scaledBalanceOf(attacker)
            depth 6: LendingPool -> PriceOracle [STATICCALL] getAssetPrice(WETH) 0xb3596f07
              depth 7: PriceOracle -> Chainlink ETH/USD 0x5f4e... [STATICCALL] latestAnswer  // Correct
                depth 8: ETH/USD Proxy -> ETH/USD Impl [STATICCALL] latestAnswer
          depth 5: LendingPool -> VariableDebtWETH [CALL] mint(attacker, attacker, 187.37e18, index)
            depth 6: VariableDebtWETH -> DebtTokenImpl [DELEGATECALL] mint
              depth 7: VariableDebtWETH -> IncentivesController [CALL] handleAction
          depth 5: LendingPool -> InterestRateStrategy_WETH [STATICCALL] calculateInterestRates
          depth 5: LendingPool -> aWETH [CALL] transferUnderlyingTo(attacker, 187.37e18) 0x4efecaa5
            depth 6: aWETH -> ATokenImpl [DELEGATECALL] transferUnderlyingTo
              depth 7: aWETH -> WETH [CALL] transfer(attacker, 187.37e18)
      depth 3: AttackerContract -> UniV2Pair [STATICCALL] token1  // get WETH address
      depth 3: AttackerContract -> WETH [CALL] transfer(UniV2Pair, 0.004289 WETH)  // repay flash swap
    depth 2: UniV2Pair -> USDC [STATICCALL] balanceOf(self)  // verify K invariant
    depth 2: UniV2Pair -> WETH [STATICCALL] balanceOf(self)
  depth 1: AttackerContract -> WETH [STATICCALL] balanceOf(self)
  depth 1: AttackerContract -> WETH [CALL] withdraw(187.36e18) 0x2e1a7d4d  // unwrap WETH to ETH
    depth 2: WETH -> AttackerContract [CALL] (ETH transfer, 187.36 ETH)
  depth 1: AttackerContract -> FeeRecipient 0x4838... [CALL] (ETH transfer, 5.61 ETH)  // builder tip
  depth 1: AttackerContract -> AttackerEOA 0x3885... [CALL] (ETH transfer, 181.75 ETH)  // profit

Financial Impact

  • Total loss: 187.366746 WETH drained from the aWETH pool (0xd060ebd4f56be8866376a3616b6e5aef87f945d2), representing the lending pool’s WETH reserves (~$389,811 at $2,080.47/ETH at block 24,538,897).
  • Attacker profit: 181.749547 ETH to the attacker EOA (0x3885869b0f4526806b468a0c64a89bb860a18cee), plus 5.612910 ETH paid as a builder tip to 0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97.
  • Attacker costs: ~0.004289 WETH Uniswap V2 flash swap repayment + gas costs. Total costs negligible.
  • Who lost funds: WETH liquidity providers in the Aave V3 fork pool. The attacker’s debt position of 187.366746 variableDebtWETH is backed by only 8.879192 USDC of actual collateral, making it effectively uncollectable.
  • Protocol solvency: The pool lost 187.366746 WETH of liquidity. The bad debt (187.366746 WETH - 8.879192 USDC worth) is irrecoverable unless the protocol has a backstop fund. The pool is likely insolvent for WETH withdrawals.
  • Source of attacker_gains (from funds_flow.json): Attacker gained 181.749547183310220532 ETH net (authoritative figure from on-chain logs).

Evidence

Oracle misconfiguration (trace indices 40-42):

  • Index 40: LendingPool -> PriceOracle.getAssetPrice(0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48) [USDC address]
  • Index 41: PriceOracle -> 0xf4030086522a5beea4988f8ca5b36dbc97bee88c.latestAnswer() [Chainlink BTC/USD proxy – WRONG]
  • Index 42: 0xf4030086 -> 0x4a3411ac2948b33c69666b35cc6d055b27ea84f1.latestAnswer() [BTC/USD aggregator implementation]

Correct WETH price feed (trace indices 45-47):

  • Index 45: LendingPool -> PriceOracle.getAssetPrice(0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) [WETH address]
  • Index 46: PriceOracle -> 0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419.latestAnswer() [Chainlink ETH/USD proxy – CORRECT]

Constructor arguments from info.json reveal the AaveOracle was deployed with a pre-existing off-by-one error in the sources array: USDC (assets[2]) was already mapped to BTC/USD (sources[2] = 0xf4030086), not USDC/USD. The USDC/USD aggregator (0x8fffffd4afb6115b954bd326cbe7b4ba576818f6) was mistakenly mapped to USDT (assets[3]), and the USDT/USD aggregator was mapped to USDe (assets[4]). The prestate trace confirms no setAssetSources() was called during the exploit transaction — the misconfiguration was a deployment-time bug.

Note: The oracle’s latestAnswer() for USDC (via the BTC/USD feed) returned 6,855,405,329,514 at block 24,538,897, corresponding to a BTC/USD price of ~$68,554 (not ~$86,000 as initially estimated). Actual ETH/USD price was ~$2,080 (not ~$2,700).

Key log events:

  • Log index 0: USDC Transfer 8,879,192 from UniV2Pair to AttackerContract (flash swap)
  • Log index 5: aUSDC mint 8,879,192 to AttackerContract (collateral deposit)
  • Log index 9: variableDebtWETH mint 187,366,746,326,704,993,556 to AttackerContract (borrow)
  • Log index 12: WETH Transfer 187.37e18 from aWETH to AttackerContract (borrow disbursement)

Receipt status: 0x1 (success)

Block miner/builder: 0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97 (Titan Builder), same address that received the 5.61 ETH tip from the attacker.