Back to Blog

AI Wrote It. AI Caught It. Moonwell Lost $1.78M Anyway.

|Odin Scan Team
AI Wrote It. AI Caught It. Moonwell Lost $1.78M Anyway.

On February 15, Moonwell Finance lost $1.78 million in a matter of minutes. Not to a novel exploit. Not to a flash loan sandwich. To a price oracle that returned $1.12 for an asset worth $2,200.

What made this story travel across every crypto security feed was not just the loss. It was the PR. The commits that introduced the vulnerability were co-authored by Claude Opus 4.6.

AI wrote the code. The code wrecked the protocol.

Here is the part that does not get mentioned enough: Odin Scan scanned that same PR and flagged the exact bug as Critical, with a precise explanation of the attack path, before a single dollar was lost.


What Happened

Moonwell is a DeFi lending protocol running on Base and Optimism. On February 15, governance proposal MIP-X43 was executed. It was supposed to enable Chainlink OEV wrapper contracts across core markets, a routine upgrade.

Buried in that proposal was a misconfigured oracle for cbETH (Coinbase Wrapped ETH).

The oracle used was cbETHETH_ORACLE, which returns the cbETH/ETH exchange rate. That rate hovers around 1.12, reflecting the cbETH/ETH staking accumulation. It is not a USD price. It was never meant to be used as a USD price.

But it was. Directly. Without multiplying by the ETH/USD feed.

The protocol now believed cbETH was worth $1.12.

Liquidation bots are not patient. They do not wait for a post-mortem. Within minutes, they were repaying tiny amounts of debt to seize cbETH collateral at a 2000x discount. By the time it was over, 1,096 cbETH had been liquidated and the protocol held $1,779,044.83 in bad debt.


The Technical Root Cause

This class of vulnerability has a name: LST/LRT Composite Oracle Vulnerability.

LSTs (Liquid Staking Tokens) like cbETH, wstETH, and rETH have two different exchange rates:

  • cbETH/ETH: how much ETH you get per cbETH. This grows slowly over time as staking rewards accumulate. Currently around 1.12.
  • cbETH/USD: the actual dollar value of cbETH. This tracks ETH/USD and is around $2,200.

To price cbETH in USD, you need both. You multiply the cbETH/ETH rate by the ETH/USD rate.

The Moonwell configuration skipped the second step. It plugged the cbETH/ETH rate directly into a slot expecting USD. The oracle name, cbETHETH_ORACLE, is almost a breadcrumb, it tells you exactly what you are getting if you read it carefully. Nobody did.

// proposals/ChainlinkOracleConfigs.sol, line 42
// This is a cbETH/ETH feed, NOT a cbETH/USD feed.
// Using it directly as a USD price values cbETH at ~$1.12 instead of ~$2,200.
address cbETH_ORACLE = 0x...cbETHETH_ORACLE...;

The fix is straightforward: compose the two feeds. Multiply cbETH/ETH by ETH/USD. This is a well-documented pattern. It is also one of the most common misconfiguration patterns in DeFi oracle setups.


Why the Tests Did Not Catch It

This is the uncomfortable part, because Moonwell is not a fly-by-night protocol. They have audits. They have tests. And yet.

The problem is what most DeFi test suites actually test. They check that the system behaves correctly given valid inputs. The oracle returns a price, the math runs, the liquidation threshold is hit or not. Those tests all passed.

What they almost never test is: is the oracle itself returning the right kind of value?

Testing that cbETH's price is "reasonable" would require either:

  1. A test that asserts cbETH/USD must be within some range of ETH/USD times the LST ratio
  2. A property-based check that flags any collateral priced below a sanity threshold relative to its underlying

Neither of those are standard in a Hardhat or Foundry test suite. Developers test the code paths, not the configuration semantics.

There is also a deployment gap. The oracle configuration lives in a governance proposal, not the core contract code. Many test suites fork mainnet and simulate the existing state but do not simulate the exact post-proposal state for every new asset that gets listed. The cbETH oracle was new. It was not in scope for tests written before it existed.

This is the category of bug that audits can catch when they specifically look for oracle configuration issues. It is the category that automated CI/CD scanning catches every single time, because it looks at every file that changes, including proposal configs.


Odin Scan Flagged It

We scanned the Moonwell PR through Odin Scan. Here is what came back:

Odin Scan finding: cbETH ETH-denominated oracle used without USD multiplication, Critical severity, Oracle category, proposals/ChainlinkOracleConfigs.sol:42

Critical severity. Oracle category. Exact file and line number.

The description reads:

In ChainlinkOracleConfigs.sol line 42, the cbETH market is configured to use cbETHETH_ORACLE directly as a USD price feed. The oracle name cbETHETH_ORACLE indicates this is a cbETH/ETH exchange rate feed (returns ~1.12), not a cbETH/USD feed. When this ETH-denominated exchange rate is used directly as a USD price without multiplying by ETH/USD, the protocol values cbETH at approximately 0.0005x of its actual value. This is the exact CRITICAL LST/LRT composite oracle vulnerability pattern. An attacker could borrow cbETH against collateral at the artificially low price, then immediately repay and profit from the spread, or liquidate positions undervalued by ~2000x.

That description is not a heuristic guess. It identifies the specific oracle name, infers the wrong value it returns, calculates the mispricing factor, and lays out the exact attack path. Before deployment. Before the governance proposal executed. Before any money moved.


The AI Coding Angle

We want to be careful here, because the story is more nuanced than "AI bad."

Claude Opus 4.6 co-authored the PR. That does not mean it introduced the bug. It might have written the surrounding boilerplate while a human specified the oracle address. We do not know the exact prompt that produced the configuration, and Moonwell has not released that context.

What we can say is this: AI coding assistants are fast, confident, and often wrong in subtle ways. They will write the code you describe. They will not always check whether the oracle address you hand them matches the semantic role you need it to fill. That is a configuration problem, not a code generation problem, and it requires a different kind of check.

The irony is real though. An AI model introduced a critical vulnerability. A different AI system caught it. The gap was deployment. A CI/CD gate between the PR merge and the governance proposal execution would have closed that gap.


What They Could Have Done Differently

1. Add a price sanity check on collateral listings.

For any new collateral asset, assert that the oracle price falls within a reasonable bound. For a cbETH listing, that means the price should be at least 90% of ETH/USD * 1.0 and no more than ETH/USD * 1.5. A one-line assertion would have reverted the proposal execution.

2. Test the post-proposal state, not just the pre-proposal state.

Integration tests should fork the chain after the governance transaction, then query all oracle prices and assert they are within expected ranges. If cbETH suddenly shows up at $1.12 in a post-proposal fork test, the test fails before mainnet does.

3. Run automated scanning on governance proposals, not just contract code.

The vulnerability was not in a Solidity function. It was in a configuration value inside a proposal script. Audit tools that only look at core contract files would miss this entirely. Odin Scan scans everything in the repository, including proposal scripts and deployment configs.

4. Use a staged rollout for governance proposals that add new collateral.

Deploy to testnet. Run the full state. Query oracle prices. Wait 24 hours. Then execute on mainnet. One extra day would have been enough for anyone reviewing the testnet deployment to notice cbETH was worth $1.


The Takeaway

Moonwell is a sophisticated team. This was not negligence. It was the natural failure mode of systems that rely on point-in-time audits and human review for configuration correctness.

Configuration bugs are not glamorous. They do not require deep protocol knowledge to exploit. They require only that someone notices before a liquidation bot does.

That is exactly what automated scanning is for. Every PR, every governance proposal, every deployment script. Not once before launch. Every single change, every single time.

The cost of running Odin Scan in your CI/CD pipeline is less than one hour of gas fees on a busy day. The cost of not running it is, in Moonwell's case, $1.78 million.


Want to add automated security scanning to your CI/CD pipeline? Sign up for a free trial and have Odin Scan running on your next PR in under five minutes. We support EVM, Solana, CosmWasm, and Cosmos SDK.

If you have questions or want to talk about your specific setup, reach out at support@odinscan.ai.