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)
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)
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:
- 34 accounts fell below their liquidation thresholds
- Liquidation bots (MEV searchers) immediately seized the collateral at a discount
- 10,938 wstETH (~$26M) was liquidated in minutes
- 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:
- Immediate: Reduced wstETH borrow cap to 1 token to prevent further leverage
- Short-term: Corrected the oracle configuration and restored accurate pricing
- 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
}
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}%"
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...
}
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(())
}
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)