On February 15, 2026, Moonwell's cbETH oracle started reporting a price of $1.12. The actual market price was $2,200.
Within four minutes, liquidation bots had seized 1,096 cbETH from healthy positions. Malicious borrowers supplied pennies of collateral and borrowed thousands of dollars. By the time the team could react, $1.78 million in bad debt had accumulated — and the oracle couldn't be fixed for five days because the governance timelock wouldn't allow it.
The kicker: security researcher Pashov noted that the affected contracts were co-authored by an AI model. The "vibe coding" era had claimed its first DeFi victim.
The Anatomy of MIP-X43
Moonwell governance proposal MIP-X43 was supposed to be routine. It activated Chainlink OEV (Oracle Extractable Value) wrapper contracts across Moonwell's core markets on Base and Optimism — an upgrade designed to capture value from oracle updates that would otherwise leak to MEV searchers.
The proposal touched multiple oracle configurations. One of them — cbETH — contained a fatal error.
What Should Have Happened
cbETH's USD price requires two oracle feeds composed together:
cbETH_USD = cbETH_ETH × ETH_USD
The cbETH/ETH feed gives the exchange rate (~1.12 ETH per cbETH due to accrued staking rewards). The ETH/USD feed gives Ether's dollar price (~$2,000). Multiply them together: cbETH ≈ $2,240.
What Actually Happened
The oracle was configured to use only the cbETH/ETH feed — raw, without the ETH/USD multiplication step:
// WRONG: Returns the exchange rate in ETH, not USD
// cbETH oracle reports: 1.12 (meaning 1.12 ETH)
// Protocol interprets this as: $1.12
// CORRECT implementation:
// cbETH_price = cbETH_ETH_feed.latestAnswer() * ETH_USD_feed.latestAnswer()
// cbETH_price = 1.12 * 2000 = $2,240
The number 1.12 was technically correct — it is the cbETH/ETH rate. But the protocol interpreted it as $1.12, not 1.12 ETH. A 2,000x pricing error.
The Four-Minute Cascade
T+0 (18:01 UTC): MIP-X43 executes. Oracle begins reporting cbETH at $1.12.
T+1–2 minutes: Automated liquidation bots detect that every cbETH collateral position is now "undercollateralized" by ~99.95%. Liquidations begin. Bots repay ~$1 of debt per cbETH seized — getting $2,200 of real value for $1.
T+2–3 minutes: Opportunistic borrowers realize they can supply minimal collateral and borrow cbETH at $1.12. Someone supplies $100 and borrows 80+ cbETH ($176,000 of real value).
T+4 (18:05 UTC): Moonwell's monitoring detects the anomaly.
T+8 minutes: The team and risk manager Anthias Labs reduce cbETH supply/borrow caps to 0.01, halting new activity.
But liquidations continued. Because the oracle was still wrong, existing positions remained "undercollateralized" in the protocol's eyes. And fixing the oracle required a governance vote with a 5-day timelock.
Final damage: $1.78M in bad debt, 1,096.317 cbETH seized by liquidators at pennies on the dollar.
The AI Angle: What "Vibe Coding" Actually Means for DeFi
When Pashov flagged that Moonwell's commits were co-authored by Claude, the crypto security community erupted. "AI wrote the bug" became the headline. But the reality is more nuanced — and more concerning.
What AI Probably Did
The oracle configuration in MIP-X43 involves setting addresses, feed IDs, and composition logic for multiple assets. This is exactly the kind of boilerplate that developers use AI to generate:
// "Set up the cbETH oracle using the Chainlink cbETH/ETH feed"
// AI generates:
OracleConfig memory cbethConfig = OracleConfig({
asset: CBETH_ADDRESS,
feed: CHAINLINK_CBETH_ETH_FEED,
// Missing: secondaryFeed: CHAINLINK_ETH_USD_FEED,
// Missing: compositionType: MULTIPLY
});
The prompt was probably correct. The generated code was probably syntactically correct. But it missed the semantic requirement that cbETH needs a composed oracle, not a single feed. The AI doesn't understand that 1.12 is an exchange rate, not a dollar price.
Why AI Didn't Cause This Bug — Missing Tests Did
Here's the uncomfortable truth: a human developer could have made the identical mistake. Oracle composition is a well-known footgun. Chainlink's documentation explicitly warns about it. The real failure was downstream:
No end-to-end integration test verified that oracle prices matched market reality after MIP-X43 execution. A single test like this would have caught it:
// test/oracle-sanity.test.js
it("cbETH oracle reports price within 5% of market", async () => {
const oraclePrice = await moonwell.getUnderlyingPrice(cbETH.address);
const marketPrice = await chainlinkAggregator.latestRoundData(); // ETH/USD * cbETH/ETH
const deviation = Math.abs(oraclePrice - marketPrice) / marketPrice;
expect(deviation).to.be.lessThan(0.05); // 5% tolerance
});
No governance simulation tested MIP-X43 against a mainnet fork before execution. Tools like Tenderly or Foundry's fork-url make this trivial:
forge test --fork-url $BASE_RPC --fork-block-number $PRE_MIP_BLOCK
No price sanity circuit breaker in the protocol itself. If an oracle suddenly reports a 99.95% price drop, maybe don't liquidate everything immediately?
The Real "Vibe Coding" Risk
The danger isn't that AI writes bugs — humans have been writing oracle bugs for years. The danger is that AI makes developers confident in code they haven't fully reviewed.
When you write oracle configuration by hand, you're mentally modeling the price derivation. When you prompt an AI to "configure the cbETH oracle," you might skip that mental model entirely. The code compiles, the types check, and you move on.
"Vibe coding" isn't about AI quality. It's about human attention.
This Wasn't Moonwell's First Oracle Incident
In November 2025, Moonwell experienced a similar oracle-related issue. Two oracle misconfigurations within four months suggests a systemic problem:
- No oracle testing framework — Each deployment relies on manual verification
- No automated price sanity checks — Neither at deployment time nor runtime
- Governance can't react to emergencies — The 5-day timelock that protects against malicious proposals also prevents emergency fixes
This pattern appears across DeFi. The same governance timelock that prevented an attacker from draining the protocol also prevented the team from fixing the oracle. Security mechanisms designed for one threat model (malicious governance) amplify damage from another (configuration errors).
Defense Patterns: Oracle Security That Actually Works
1. Multi-Source Price Validation
Never rely on a single oracle feed without cross-validation:
function getPrice(address asset) external view returns (uint256) {
uint256 primaryPrice = primaryOracle.getPrice(asset);
uint256 secondaryPrice = secondaryOracle.getPrice(asset);
uint256 deviation = _abs(primaryPrice, secondaryPrice) * 10000 / primaryPrice;
require(deviation < MAX_DEVIATION_BPS, "Oracle price divergence");
return primaryPrice;
}
2. Historical Price Circuit Breakers
function validatePriceUpdate(address asset, uint256 newPrice) internal view {
uint256 lastPrice = lastKnownPrice[asset];
if (lastPrice > 0) {
uint256 changePercent = _abs(newPrice, lastPrice) * 100 / lastPrice;
require(
changePercent < MAX_PRICE_CHANGE_PERCENT,
"Price change exceeds circuit breaker threshold"
);
}
}
A 99.95% price drop should never flow through to liquidation logic without at least a pause.
3. Governance Simulation Pipeline
# .github/workflows/governance-simulation.yml
name: Simulate Governance Proposal
on:
pull_request:
paths: ['proposals/**']
jobs:
simulate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fork mainnet and execute proposal
run: |
forge script SimulateProposal \
--fork-url $BASE_RPC \
--fork-block-number $(cast block-number --rpc-url $BASE_RPC)
- name: Verify oracle sanity post-execution
run: |
forge test --match-test "testOracleSanity" \
--fork-url $BASE_RPC
Every governance proposal should be simulated against a mainnet fork before going to vote.
4. Emergency Oracle Override (With Guardrails)
// Guardian can pause oracle consumption without governance vote
// But CANNOT set arbitrary prices — only pause
function pauseOracle(address asset) external onlyGuardian {
oraclePaused[asset] = true;
emit OraclePaused(asset, msg.sender);
}
// Paused oracles use last known good price
function getPrice(address asset) external view returns (uint256) {
if (oraclePaused[asset]) {
return lastValidPrice[asset]; // Stale but not catastrophically wrong
}
return _fetchAndValidatePrice(asset);
}
5. Solana Equivalent: Pyth Price Confidence Intervals
Solana programs using Pyth oracles have a built-in defense that EVM protocols should learn from:
use pyth_sdk_solana::PriceFeed;
pub fn validate_pyth_price(price_feed: &PriceFeed) -> Result<u64> {
let price = price_feed.get_price_no_older_than(
Clock::get()?.unix_timestamp,
MAX_STALENESS_SECONDS
).ok_or(ErrorCode::StalePythPrice)?;
// Pyth provides a confidence interval — USE IT
// If confidence is wider than 5% of price, the data is unreliable
let confidence_ratio = (price.conf as u128) * 10000 / (price.price.unsigned_abs() as u128);
require!(confidence_ratio < 500, ErrorCode::LowConfidencePrice);
// Additional: reject negative or zero prices
require!(price.price > 0, ErrorCode::InvalidPrice);
Ok(price.price as u64)
}
The Uncomfortable Questions
For Protocol Teams:
- Do you simulate every governance proposal against a mainnet fork?
- Do your oracle configs have automated sanity tests?
- Can your team fix a critical oracle error in under 5 minutes?
- If not, do you have circuit breakers that auto-pause on impossible price movements?
For AI-Assisted Development:
- Do you test AI-generated oracle configurations with price validation tests?
- Does your review process treat AI-generated code with more scrutiny, not less?
- Do you have a "human must verify the math" step for any financial calculation?
For DeFi Users:
- Are you using protocols that have had multiple oracle incidents?
- Do you understand the governance timelock that prevents emergency fixes?
- Is your position leveraged enough that a 3% oracle error liquidates you?
The Bottom Line
The Moonwell exploit wasn't about AI being bad at coding. It was about a missing multiplication step that any developer — human or AI — could have introduced, combined with a testing infrastructure that wouldn't have caught it either way.
The real lesson: DeFi's testing culture is still inadequate for the amount of capital at risk. A $1.78M loss from a single missing price feed multiplication. No fuzzing needed. No flash loans. No reentrancy. Just one number that should have been multiplied by another.
The next oracle misconfiguration is already deployed somewhere. The question is whether there's a test that will catch it before governance executes.
This is part of the DeFi Security Research series. Previously: The Aave CAPO Oracle Incident, Custom Forta Detection Bots.
Top comments (0)