Crypto Training
DeFi Incident Patterns: Oracle Games, Rounding Edges, and the Same Bug in Three Costumes
Cream, PancakeBunny, and bZx are not three unrelated stories. They are variations of one meta-pattern: untrusted external state crossing into accounting. Here's how to spot the variants.
If you read enough post-mortems, you eventually notice something uncomfortable:
Most “new” DeFi hacks are old hacks with new assets.
The surface changes:
- new chain
- new token
- new DEX
- new oracle design
The core failure stays the same:
the protocol trusted a number it did not control.
This post is a practical way to read incident writeups and turn them into audit instincts.
This post uses three classic narratives:
- Cream exploit analysis
- PancakeBunny exploit post-mortem
- bZx exploit analysis
The goal is not history trivia. The goal is pattern recognition.
The meta-pattern: "external state" crosses into accounting#
Protocols typically maintain an internal accounting system:
- shares
- debt
- collateral
- fees
- reserves
Then they consult something external:
- AMM price
- oracle
- token balance
- callback result
And then they update the internal accounting as if that external fact is:
- stable
- honest
- expensive to manipulate
Attackers win by violating exactly one of those assumptions.
A comparison table that helps during audits#
| Incident | External dependency | What the attacker controlled | Where it hit accounting |
|---|---|---|---|
| Cream-style lending incident | price / exchange rate | instantaneous on-chain price | borrow limit, collateral factor |
| PancakeBunny-style vault incident | AMM price | spot price via flash liquidity | share minting, vault accounting |
| bZx-style incident | AMM + oracle design | price and order of operations | margin, debt repayment, collateralization |
The “attacker controlled” column is the key.
Many teams say “we use an oracle”. The real question is: what does the attacker get to choose?
Case study 1: bZx (sequencing + oracle design)#
The bZx incidents are a good teaching example because they illustrate a subtle reality:
sometimes the bug is not one line of code. It is the protocol's economic interface.
When you read a bZx-style writeup, focus on:
- which price is used (spot vs averaged)
- when that price is read (before vs after a state update)
- whether the protocol implicitly assumes “the price you see is the price you get”
A common bZx-shaped failure mode is:
- attacker obtains temporary liquidity (often via flash loans)
- attacker moves price on an AMM that the protocol consults (directly or indirectly)
- attacker executes a protocol action that depends on that price (borrow, trade, collateralize)
- attacker reverses the price movement
- accounting now reflects a “price snapshot” that was never meant to be stable
The vulnerability is not “AMMs are bad”.
The vulnerability is: the protocol treated a manipulable surface as a safe oracle, without bounding the impact.
Case study 2: PancakeBunny (vault share minting meets spot price)#
PancakeBunny-style incidents are great for auditors because they show how protocol accounting and market microstructure collide.
Vaults often do some form of:
- compute value of assets under management
- mint shares
- distribute rewards
If “value” is derived from a manipulable price, share minting becomes a lever.
The common attack outline:
- use flash liquidity to push a pool price far from equilibrium
- trigger vault logic that mints or prices shares using that distorted price
- exit in a path that converts back to stable assets after the price normalizes
Even if the protocol uses a TWAP, the attacker can sometimes:
- manipulate within the window
- choose the exact block where the measurement is taken
This is why vault share logic needs the same rigor as lending protocols: it is a credit system.
Case study 3: CREAM (lending protocols are oracle protocols)#
Lending protocols are especially sensitive because the oracle often gates:
- how much you can borrow
- when you can be liquidated
- what collateral is considered sufficient
If the oracle is wrong for one block, the protocol can become insolvent in one block.
CREAM-style incidents tend to involve:
- thin-liquidity markets used as price sources
- price manipulation via large swaps or engineered liquidity
- borrow paths that do not cap exposure per block
When you audit lending, treat “oracle assumptions” as part of the core spec.
If the project cannot articulate those assumptions, you are auditing a system without a threat model.
What to extract from a post-mortem (beyond the storyline)#
When reading a post-mortem, I literally try to fill in these blanks:
- what was the protocol measuring?
- what did it assume about that measurement?
- how did the attacker cheaply invalidate the assumption?
- why did the protocol accept the invalid measurement as truth?
If you can answer those, you can usually derive the class of fix without reading every line of code.
Oracle manipulation is not just "they changed the price"#
When an attacker “manipulates the price”, they usually do one of these:
- spot price manipulation: move the pool, perform the action, reverse the pool
- window gaming: manipulate for only part of the TWAP window
- liquidity mirage: use a shallow pool that appears liquid due to routing
- sequencing: make your protocol read the price at a time favorable to them
If your protocol reads a price from an AMM:
- you are not reading “the market”
- you are reading “the marginal price of that specific pool given current reserves”
That can be a fine design. It just needs to be priced in and bounded.
A mitigation playbook that is actually usable#
When people ask “how do I prevent oracle manipulation”, they often want a single silver bullet.
There is no silver bullet.
There is a menu of bounds and tradeoffs.
| Mitigation | What it stops | What it does not stop | Cost |
|---|---|---|---|
| TWAP | pure spot manipulation | partial-window manipulation, low-liquidity TWAP | latency |
| external oracle (Chainlink) | AMM microstructure games | oracle outages, stale data, trust assumptions | dependency |
| min liquidity checks | thin-pool exploits | deep-pool manipulation | complexity |
| max deviation vs reference | large anomalies | slow drift manipulation | tuning |
| caps / rate limits | one-block drains | slow drains | UX + tuning |
| circuit breakers | catastrophic anomalies | subtle exploitation | ops |
The right design depends on:
- how quickly you can respond (governance speed)
- how much loss you can tolerate in one block
- how manipulable your chosen market is
Protocols that survive incidents usually have:
- at least two independent price signals, or
- a strong bound on how much one signal can move the system
Why naive fixes often fail#
After incidents, teams often ship “fixes” that do not address the attacker model.
Examples:
"We switched to a TWAP"#
Good move, but incomplete.
If the attacker can:
- manipulate for part of the window, or
- choose when your window starts, or
- exploit low liquidity in the measured pool,
then TWAP becomes “a slower oracle”, not “a safe oracle”.
"We added a slippage check"#
Slippage checks protect users in swaps. They do not automatically protect protocols that:
- mint shares
- issue debt
- liquidate positions
In protocol logic, you often need deviation bounds against a reference price, not just min-out.
"We rate-limited liquidations / borrows"#
This often helps. But if the limit is too high, it is only theater.
Rate limits work when they convert:
- one-block insolvency
into:
- multi-block exposure that can be detected and paused
The correct limit depends on total liquidity and how quickly governance can respond.
Rounding shows up inside incidents more often than you think#
Rounding is rarely the root cause in the headline.
But rounding often decides:
- how much profit is extractable
- whether a share minting edge is reversible
- whether a manipulation is self-financing
A common failure mode:
- attacker manipulates the oracle to create a favorable conversion rate
- protocol mints shares using rounding in attacker favor
- attacker exits when rounding flips direction on redemption or fees are socialized
This is why “rounding policy” belongs in your threat model, not in a comment.
A tiny "oracle manipulation" drill you can run in audits#
When I suspect a protocol is spot-sensitive, I do a drill:
- simulate a large AMM swap that moves price
- call the protocol path that reads price
- reverse the swap
- compare net profit to cost
In pseudo:
function drill() external {
flashLoan(bigAmount);
swapToMovePrice();
protocolActionThatReadsPrice();
swapBack();
repay();
}
If the protocol action mints value (shares or debt capacity) while the price is distorted, you have the skeleton of a real exploit.
You do not need perfect realism to learn whether the design is in the danger zone.
A short "audit lens" for oracle-dependent code#
Instead of repeating a checklist, here is a lens I apply to every oracle read:
Identify the oracle#
Is it:
- Chainlink-style signed data
- TWAP from a Uniswap pool
- spot price from a pool
- virtual price from a pool token
- “we compute a price from reserves”
Identify the manipulation budget#
Ask:
- how much liquidity backs this price?
- can the attacker borrow that liquidity for one block?
- can the attacker move it in a private bundle?
Identify the blast radius#
If the price is wrong, what can happen?
- borrow too much
- redeem too much
- mint too many shares
- avoid liquidation
- trigger liquidation on others
Add a bound#
If the blast radius is large, add one of:
- max deviation from an external oracle
- TWAP window and minimum liquidity
- per-block / per-user caps
- circuit breakers (pause on anomaly)
If you cannot add a bound, you are shipping a price oracle.
Code patterns that reduce oracle risk#
Pattern 1: use TWAP + minimum liquidity + max deviation#
Pseudo:
function safePrice() internal view returns (uint256 p) {
p = twapPrice(pool, window);
require(poolLiquidity(pool) >= MIN_LIQ, "thin-liquidity");
uint256 ref = chainlinkPrice();
uint256 devBps = deviationBps(p, ref);
require(devBps <= MAX_DEV_BPS, "deviation");
}
This is not perfect. It is a boundary.
Pattern 2: settle by balance deltas#
If you are integrating with tokens / hooks / routers, measure:
- balance before
- balance after
And base accounting updates on that delta, not on return values.
Pattern 3: rate limits on the path that matters#
If the “price-critical” path is borrow or mint:
- cap mint per block
- cap borrow per block
- cap per user
Rate limits are not “band-aids”.
They turn “one block exploit” into “multi-block exploit”, which is often enough to detect and stop.
Turning post-mortems into a reusable checklist (without copy-paste)#
Instead of copying “attack steps”, I suggest you extract reusable questions:
| Question | Why it matters |
|---|---|
| Can the attacker borrow the manipulation budget for one block? | flash liquidity collapses “cost” assumptions |
| Is the measured pool the deepest pool, or just the easiest pool? | routing can make thin pools look like market truth |
| Is the protocol sensitive to spot, or to average? | defines the minimum safe window |
| What is the maximum one-block loss? | caps and circuit breakers become rational design knobs |
| Are there asymmetric rounding paths? | dust becomes profit under repetition |
If you put these into your audit notes, every new incident improves your future audits.
Further reading#
- Zellic on audit discipline and what matters in practice: https://www.zellic.io/blog/the-auditooor-grindset/
- CREAM hack analysis: https://mudit.blog/cream-hack-analysis/
- Pancake Bunny exploit post-mortem: https://cmichel.io/bsc-pancake-bunny-exploit-post-mortem/
- bZx exploit walkthrough: https://www.palkeo.com/en/projets/ethereum/bzx.html
- Oracle manipulation basics: https://ethereum.org/en/developers/docs/oracles/