DEV Community

ohmygod
ohmygod

Posted on

The Aave CAPO Oracle Incident: How a 2.85% Price Error Triggered $26M in Wrongful Liquidations

On March 10, 2026, 34 Aave users woke up to find their wstETH positions liquidated — not because the market crashed, not because they were overleveraged, but because Aave's own oracle underpriced their collateral by 2.85%. The total damage: ~$26 million in wrongful liquidations, 10,938 wstETH seized, and 499 ETH extracted by third-party liquidation bots.

This wasn't an exploit. No attacker was involved. The protocol's oracle misconfigured itself — and that might be scarier than any hack.

What Is CAPO and Why Does It Exist?

Aave's Correlated Asset Price Oracle (CAPO) is a guardrail system for assets that should trade at a predictable ratio to each other. For wstETH (wrapped staked ETH), the exchange rate against ETH increases slowly and predictably as staking rewards accrue — roughly 3-4% per year.

CAPO caps how fast this exchange rate can move, protecting against oracle manipulation attacks. The logic is simple:

maxExchangeRate = snapshotRatio × (1 + maxYearlyGrowthPercent)^(timeSinceSnapshot / SECONDS_PER_YEAR)
Enter fullscreen mode Exit fullscreen mode

If the reported exchange rate exceeds maxExchangeRate, CAPO caps it. This prevents an attacker from flash-manipulating a Lido oracle to inflate wstETH's price and borrow against phantom collateral.

Good idea. Fatal implementation.

The Root Cause: Snapshot Desynchronization

The bug lived in the gap between two systems:

The Off-Chain Component

An algorithm periodically updates the snapshotRatio — the baseline exchange rate used in the CAPO formula. It intended to set this to the exchange rate from 7 days prior, providing a stable historical anchor.

The On-Chain Constraint

The smart contract enforced a hard limit: the snapshotRatio could only increase by 3% every 3 days. This was a safety cap to prevent dramatic parameter changes.

The Collision

When the off-chain system tried to jump the snapshot ratio forward by 7 days' worth of growth, the on-chain constraint only allowed a partial update. But the snapshotTimestamp was updated to reflect the full 7-day adjustment.

Result: the CAPO formula used an old ratio with a new timestamp, calculating a maxExchangeRate that was 2.85% below the actual market rate.

// What should have happened:
snapshotRatio = 1.228    // Current accurate rate
snapshotTimestamp = now - 7 days

// What actually happened:
snapshotRatio = 1.1939   // Partially updated (capped by on-chain limit)
snapshotTimestamp = now - 7 days  // But timestamp jumped the full distance

// CAPO calculation:
maxRate = 1.1939 × (1.0325)^(7/365) = ~1.194
// Actual market rate: 1.228
// Oracle reported: 1.194 (2.85% undervaluation)
Enter fullscreen mode Exit fullscreen mode

The Liquidation Cascade

Aave V3's Efficiency Mode (E-Mode) allows borrowers to take higher leverage when using correlated assets. wstETH/ETH positions in E-Mode could reach 90%+ loan-to-value ratios — meaning even a small price deviation triggers liquidation.

With wstETH suddenly "worth" 2.85% less than reality:

  1. 34 accounts fell below their liquidation thresholds
  2. Liquidation bots (MEV searchers) immediately seized the collateral at a discount
  3. 10,938 wstETH (~$26M) was liquidated in minutes
  4. Liquidators pocketed ~499 ETH in bonuses and arbitrage profits

The users did nothing wrong. Their positions were healthy. The protocol's own oracle misfired.

Why This Is Worse Than an Exploit

A traditional exploit has clear characteristics:

  • An attacker finds a bug and extracts funds
  • The protocol can blacklist the attacker's address
  • Recovery involves negotiation or law enforcement
  • It's a one-time event with a clear fix

The CAPO incident is different:

No attacker to blame. The oracle misconfigured itself through a design flaw in how two systems interact. There's no address to freeze, no funds to recover from a hacker.

Liquidators acted rationally. The bots that seized $26M in collateral were performing their intended function — liquidating undercollateralized positions. You can't call it theft when the protocol's own oracle said the positions were underwater.

It erodes trust in "safe" DeFi primitives. CAPO was specifically designed to make Aave safer. Instead, the safety mechanism itself caused the damage. If guardrails can misfire this catastrophically, what other "safety" systems are ticking time bombs?

The Fix and Compensation

Aave's response was swift:

  1. Immediate: Reduced wstETH borrow cap to 1 token to prevent further leverage
  2. Short-term: Corrected the oracle configuration and restored accurate pricing
  3. Compensation: ~141 ETH recovered via BuilderNet refunds + 13 ETH in liquidation fees, with the DAO treasury covering the ~345 ETH shortfall

Total reimbursement cost to the DAO: approximately 499 ETH (~$900K at the time).

Lessons for Protocol Developers

1. Never Desynchronize Coupled Parameters

The CAPO bug boils down to a timestamp and a ratio that must move in lockstep being allowed to drift apart. This pattern appears everywhere:

// DANGEROUS: Parameters updated independently
function updateSnapshot(uint256 newRatio, uint256 newTimestamp) external {
    // On-chain cap might prevent ratio from reaching target
    snapshotRatio = Math.min(newRatio, snapshotRatio * MAX_INCREASE);
    // But timestamp always updates fully
    snapshotTimestamp = newTimestamp;  // DESYNC!
}

// SAFE: Atomic coupling
function updateSnapshot(uint256 newRatio, uint256 newTimestamp) external {
    uint256 cappedRatio = Math.min(newRatio, snapshotRatio * MAX_INCREASE);

    // Scale timestamp proportionally to how much ratio actually moved
    uint256 actualGrowth = cappedRatio - snapshotRatio;
    uint256 intendedGrowth = newRatio - snapshotRatio;
    uint256 scaledTimeDelta = (newTimestamp - snapshotTimestamp) 
                              * actualGrowth / intendedGrowth;

    snapshotRatio = cappedRatio;
    snapshotTimestamp = snapshotTimestamp + scaledTimeDelta;  // Proportional
}
Enter fullscreen mode Exit fullscreen mode

2. E-Mode Amplifies Oracle Risk

High-LTV modes like E-Mode are designed for correlated assets where price deviation is minimal. But they create a paradox: the very mechanism that makes positions "safe" (tight correlation) also means tiny oracle errors become liquidation triggers.

Rule of thumb: If your liquidation threshold is within 5% of LTV, your oracle must be accurate to within 1%. CAPO's 2.85% error exceeded this margin for most E-Mode positions.

3. Test Off-Chain + On-Chain Interactions as a System

The CAPO bug wouldn't surface in isolated unit tests of either component:

  • The off-chain algorithm correctly calculated the 7-day-old ratio
  • The on-chain cap correctly limited growth to 3% per 3 days
  • Together, they produced an impossible state
# Property-based test that would have caught this
def test_capo_sync_invariant():
    for _ in range(10000):
        ratio_growth = random.uniform(0, 0.05)  # up to 5% growth
        time_delta = random.randint(1, 30) * DAY

        # Simulate on-chain cap
        capped_ratio = min(target_ratio, 
                          current_ratio * (1 + MAX_INCREASE_PER_3D) ** (time_delta / (3 * DAY)))

        # Calculate CAPO output
        max_rate = capped_ratio * (1 + YEARLY_GROWTH) ** (time_delta / YEAR)

        # INVARIANT: CAPO max rate must never be below actual market rate
        # when market rate is within normal growth bounds
        actual_rate = current_ratio * (1 + YEARLY_GROWTH) ** (time_delta / YEAR)
        assert max_rate >= actual_rate * 0.99, \
            f"CAPO underpricing by {(1 - max_rate/actual_rate)*100:.2f}%"
Enter fullscreen mode Exit fullscreen mode

4. Implement Circuit Breakers for Oracle-Triggered Liquidations

Aave's liquidation engine treated the CAPO output as ground truth. A circuit breaker could have prevented mass liquidation:

// Before executing liquidation, sanity-check the price deviation
function liquidate(address user, ...) external {
    uint256 oraclePrice = capoOracle.getPrice(wstETH);
    uint256 chainlinkPrice = chainlink.getPrice(wstETH);

    // If CAPO and Chainlink diverge by >2%, pause liquidations
    uint256 deviation = abs(oraclePrice - chainlinkPrice) * 1e18 / chainlinkPrice;
    require(deviation < CIRCUIT_BREAKER_THRESHOLD, "Oracle divergence detected");

    // Proceed with liquidation...
}
Enter fullscreen mode Exit fullscreen mode

5. Solana Parallel: Clock-Dependent Oracle Anchoring

Solana programs face an analogous risk with Clock::get() dependencies. If an oracle program anchors price validity to slot timestamps and a validator runs Firedancer (producing blocks faster), the same desynchronization can occur:

// Solana: Same pattern, different runtime
pub fn validate_price(ctx: Context<ValidatePrice>) -> Result<()> {
    let clock = Clock::get()?;
    let oracle = &ctx.accounts.oracle;

    // If oracle update frequency doesn't match block production rate,
    // stale prices can trigger wrongful liquidations
    let staleness = clock.unix_timestamp - oracle.last_update;
    require!(staleness < MAX_STALENESS, OracleError::StalePrice);

    // Additional check: compare against backup oracle
    let backup_price = get_backup_oracle_price(&ctx.accounts.backup_oracle)?;
    let deviation = abs_diff(oracle.price, backup_price) * 10000 / backup_price;
    require!(deviation < MAX_DEVIATION_BPS, OracleError::PriceDivergence);

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

The Bigger Picture

The Aave CAPO incident is a category of bug that will become more common as DeFi protocols add layers of complexity:

  • Oracle wrappers (CAPO, TWAP circuits, Chainlink heartbeat adapters) add new failure modes
  • Hybrid on-chain/off-chain systems create state desynchronization opportunities
  • High-leverage modes amplify the impact of tiny errors
  • MEV infrastructure ensures any liquidation opportunity is captured in milliseconds, before humans can intervene

The protocol that handles the most value ($30B+ TVL for Aave) was brought down not by a sophisticated attacker, but by two numbers drifting apart by 2.85%.

Sometimes the most dangerous bugs aren't in the code that handles money — they're in the code that's supposed to keep it safe.


This analysis is based on the Chaos Labs post-mortem and on-chain data from the March 10, 2026 incident. All code examples are illustrative and simplified for clarity.

Top comments (0)