How price oracle manipulation drains a lending market
A lending market runs on one number it does not produce itself: the price of the collateral. Every decision that matters, how much you can borrow, when you get liquidated, whether the protocol stays solvent, is downstream of that number. So the most direct way to rob a lending market is not to break its math. It is to lie to it about a price, and have it believe you.
The attack does not require stolen keys or a reentrancy trick. It requires only that the price the protocol reads can be moved cheaply, within a single transaction, by the same person asking to borrow against it.
Where the price comes from
Many protocols, especially early ones, read the price straight from an on-chain pool. The pool holds reserves of two tokens, and the ratio of those reserves is the price:
// "what is one unit of the collateral worth?"
uint256 price = reserveQuote * 1e18 / reserveBase; // spot price, this block
This is the spot price, and it is honest only when nobody is actively distorting it. The reserves are public state that anyone can change by trading into the pool. A large enough trade moves the ratio, and the protocol, reading the ratio, reports a price that reflects the trade rather than the market.
The attack, with a flash loan
Flash loans remove the one barrier that used to make this hard, which was needing the capital up front. An attacker borrows a vast amount for the length of one transaction, on the condition that it is repaid before the transaction ends. Inside that window:
- Borrow a large sum through a flash loan.
- Swap it into the pool the protocol uses as its oracle, pushing the reserve ratio hard in one direction. The reported price of the collateral is now wildly off.
- Call the lending market while the price is distorted. Deposit a modest amount of the now-overpriced collateral and borrow far more than it is really worth, or trigger liquidations of healthy positions priced against the false number.
- Reverse the swap to recover the tokens, repay the flash loan, and keep the difference.
Every step is a legitimate operation. No function was tricked into running when it should not have. The protocol did exactly what it was told, against a price that was true for one block and false in every way that mattered.
The rule
Never make a financial decision on a price that the caller can move inside the same transaction. If the source is cheap to distort, so is everything that trusts it.
The defenses all raise the cost or the time needed to move the number:
- Time-weighted prices. A TWAP averages the price over a window of blocks. Moving a multi-block average means holding the distortion across many blocks, which exposes the attacker to arbitrage and real cost, instead of a free round trip in one transaction.
- Independent oracles with sanity checks. A dedicated price feed, cross-checked for staleness and for deviation from a second source, removes the single manipulable pool from the critical path. Reject a price that is too old or that disagrees with its backup by more than a set bound.
- Liquidity-aware limits. A price from a shallow pool is easy to move, so the depth of the source matters as much as the number it returns. Thin liquidity behind an oracle is itself a warning sign.
How we test it
We model the attacker's capital directly. In a fork test, we take a flash-loan-sized swing against the oracle source and assert the protocol's reported price barely moves:
// after a large adversarial swap into the oracle source,
// the price the protocol reads must not move enough to be profitable
uint256 before = protocol.collateralPrice();
attackerSwap(hugeAmount);
uint256 after_ = protocol.collateralPrice();
assertLt(absDiff(before, after_), maxTolerableDeviation);
We also confirm the feed rejects stale and out-of-band values, and we check the depth of any pool in the price path, because an oracle is only as trustworthy as the cost of moving its inputs.
Why it hides
Oracle manipulation passes review because the price function is correct. It reads the reserves, does the arithmetic, returns the right answer for the state it sees. Unit tests confirm it, because they set prices to fixed values and never supply an adversary with a flash loan. The flaw is not in reading the price. It is in trusting a price that an attacker is paying to set, and that only becomes visible when you ask what it costs to move the number, and find the answer is almost nothing.
If your protocol prices collateral, computes a redemption, or liquidates against an on-chain number, the oracle path is one of the first places we probe. Related: rounding direction is a security property.