LST/LRT Oracle Pricing: The Pattern Behind $100M+ in DeFi Losses

The Moonwell hack in February 2025 cost $1.78 million. An LST oracle returned $1.12 instead of $2,200. Liquidation bots did the rest in minutes.
It was not the first time this happened. It will not be the last. The LST/LRT composite oracle vulnerability is one of the most consistently exploited patterns in DeFi, and the fix is a single multiplication.
This post explains why the bug happens, which protocols it has hit, how to detect it, and how to prevent it.
Background: What LSTs and LRTs Are
A Liquid Staking Token (LST) represents staked ETH plus accumulated rewards. Examples: cbETH, wstETH, rETH, stETH. You deposit ETH, you get an LST back. Over time, the LST accrues staking yield.
A Liquid Restaking Token (LRT) represents restaked ETH across EigenLayer or similar protocols. Examples: eETH, weETH, rsETH. Same concept, different underlying mechanism.
Both LSTs and LRTs have two distinct values:
Exchange rate: How much ETH one unit of the LST is worth. This grows slowly over time as yield accumulates. For cbETH it is around 1.05 to 1.12 depending on when you check. For wstETH it is around 1.18.
USD price: The actual dollar value. This is the exchange rate multiplied by the ETH/USD spot price. For cbETH at an exchange rate of 1.12 and ETH at $2,000: $2,240.
These are completely different numbers. One is dimensionless (or in ETH). The other is in USD. Using the exchange rate where you need the USD price prices cbETH at $1.12 instead of $2,240. A 2000x error.
The Attack
When a lending protocol misprices collateral this way, the attack is mechanical:
- The protocol thinks cbETH is worth $1.12
- An attacker deposits $1.12 worth of other collateral
- The attacker borrows 1 cbETH, which the protocol values at $1.12
- The attacker sells that cbETH on the open market for $2,240
- The attacker repays the original collateral
- Net profit: $2,238.88 per cbETH, limited only by available liquidity
Alternatively, if other users have cbETH as collateral, liquidation bots see those positions as undercollateralized at the fake price and liquidate them at a massive discount. The borrowers lose collateral; the protocol accumulates bad debt.
In the Moonwell case, 1,096 cbETH was liquidated this way before the oracle was corrected. $1.78 million gone.
How the Misconfiguration Happens
Most lending protocols integrate price oracles through a configuration step: you specify which feed to use for each asset. For ETH, you use an ETH/USD Chainlink feed. For USDC, you use a USDC/USD feed.
For cbETH, you need a cbETH/USD price. There is no single Chainlink feed that gives you cbETH/USD directly. You have to compose two feeds:
cbETH/USD = cbETH/ETH (exchange rate) * ETH/USD (spot price)
The mistake is using only the cbETH/ETH feed and treating it as cbETH/USD. This happens because:
- The oracle name (
cbETHETH_ORACLE) looks like a cbETH price feed at a glance - The configuration step is often done separately from contract development, with less review
- AI coding assistants copy simpler single-feed patterns from training data
- Governance proposals that add new collateral types go through less scrutiny than core contract changes
The Chainlink feed for cbETH/ETH has an address. The Chainlink feed for ETH/USD has a different address. Plugging the first address into a slot expecting USD output is the entire bug.
Known Incidents
This is not a theoretical vulnerability. It has been exploited repeatedly:
Moonwell Finance (February 2025): $1.78M. cbETH oracle used cbETH/ETH exchange rate directly as USD price. Introduced via governance proposal MIP-X43.
Mango Markets (October 2022): $114M. MNGO price was manipulated via a low-liquidity oracle, not strictly the same bug, but the same class of "oracle returns wrong semantic value" issue. The attacker inflated their own collateral price to borrow against it.
Inverse Finance (April 2022): $15.6M. Price oracle for INV was manipulated through a low-liquidity Uniswap pair used as the price source, again in the category of "wrong oracle type for the semantic role."
Multiple smaller protocols: Dozens of smaller exploits have used oracle composition errors or wrong feed types as the attack entry point. They rarely make major headlines but the pattern is the same.
The Correct Implementation
For any LST or LRT, you need to compose two feeds. Here is the pattern in Solidity:
/// @notice Returns the USD price of an LST asset by composing
/// the LST/ETH exchange rate with the ETH/USD spot price.
/// @param lstEthFeed The Chainlink feed returning LST/ETH (e.g. cbETH/ETH)
/// @param ethUsdFeed The Chainlink feed returning ETH/USD
/// @return price The LST price in USD, scaled to 8 decimals
function getLSTPrice(
AggregatorV3Interface lstEthFeed,
AggregatorV3Interface ethUsdFeed
) internal view returns (uint256 price) {
(, int256 lstEthRate,, uint256 lstEthUpdatedAt,) = lstEthFeed.latestRoundData();
(, int256 ethUsdPrice,, uint256 ethUsdUpdatedAt,) = ethUsdFeed.latestRoundData();
require(lstEthRate > 0 && ethUsdPrice > 0, "Invalid oracle data");
require(block.timestamp - lstEthUpdatedAt <= MAX_STALENESS, "LST feed stale");
require(block.timestamp - ethUsdUpdatedAt <= MAX_STALENESS, "ETH feed stale");
// Compose: LST/USD = LST/ETH * ETH/USD
// Both feeds return 8-decimal values, so divide by 1e8 to normalize
price = uint256(lstEthRate) * uint256(ethUsdPrice) / 1e8;
}
Notice the staleness checks. Stale oracle data is a separate vulnerability class that compounds oracle misconfiguration bugs.
How to Test for This
Add these assertions to your test suite for any protocol that lists LST or LRT collateral:
function test_cbETH_oracle_sanity() public {
uint256 cbEthPrice = oracle.getPrice(CBETH);
uint256 ethPrice = oracle.getPrice(WETH);
// cbETH should be priced between 90% and 150% of ETH
// If it is priced at $1.12, this assertion catches it immediately
assertGt(cbEthPrice, ethPrice * 9 / 10, "cbETH price too low");
assertLt(cbEthPrice, ethPrice * 15 / 10, "cbETH price too high");
}
This test would have caught the Moonwell bug. The assertion cbEthPrice > ethPrice * 0.9 fails immediately when cbEthPrice is $1.12 and ethPrice is $2,000.
What Odin Scan Checks
Odin Scan detects this vulnerability class by:
- Identifying oracle feed variable names that include tokens like
ETH,LST,cbETH,wstETH,rETH, suggesting an exchange rate feed - Checking whether that feed is composed with an ETH/USD feed before being used as a USD price
- Flagging any direct use of an ETH-denominated feed in a USD price context as Critical severity
This is exactly how it caught the Moonwell misconfiguration before deployment, in proposals/ChainlinkOracleConfigs.sol at line 42.
Checklist for Protocols Listing LST/LRT Collateral
Before any governance proposal that adds a new LST or LRT asset:
- Confirm the oracle feed type: does it return ETH-denominated or USD-denominated values?
- If ETH-denominated: verify it is composed with an ETH/USD feed before use
- Add a sanity check test asserting the asset's price is within a reasonable multiple of ETH/USD
- Run the proposal script through an automated scanner before on-chain execution
- Have at least one human reviewer read every oracle address and confirm the feed type matches the expected denomination
The multiplication takes one line of code. The check takes one line of test. The failure mode is a nine-figure loss.
Odin Scan detects LST/LRT oracle misconfiguration as part of its standard EVM scan. Try it on your contracts, including governance proposal scripts.