*Part 7 of 12 in Web3 Security 101.*
The previous two parts were about keys and signers. This one is about a different kind of failure: nobody takes a key, no parameter is wrong, and the contract does exactly what it was written to do. It just does it at a price that is false.
The mechanism
A contract cannot see the outside world. Lending markets, perpetual exchanges and vaults still need to know what a token is worth, so they ask an oracle: a source of prices. The oracle is whatever the protocol decided to believe. There are a few common kinds:
- Spot price from a pool. The contract reads the ratio of two tokens in a DEX pool, for example from its reserves. It is cheap and always up to date, and it is exactly what the last trade made it.
- A time-weighted average (TWAP). The pool price averaged over a window. Harder to move, because the attacker would have to hold the distortion for many blocks.
- A push feed. An off-chain network such as Chainlink writes prices on chain: when the price moves by a set deviation, or when a heartbeat interval passes. The contract reads the last value.
- A price set by the protocol's own keeper or contract. Fast, and only as safe as whoever can call the function that sets it.
Price manipulation means making the oracle report a number that is wrong for a moment, and using the protocol during that moment: borrow against inflated collateral, open a position at a low price and close it at a high one, mint at a cheap rate.
Why flash loans matter
Moving a pool's price needs a lot of money, and until a few years ago that was a barrier. A flash loan removed it. A lending pool lends any amount with no collateral on one condition: the loan is repaid inside the same transaction. If it is not, the whole transaction reverts as if nothing happened.
The usual sequence, all in one transaction:
- Borrow a large amount with a flash loan.
- Swap it in a thin pool and push the price far from the real one.
- Use the victim protocol at the false price and take profit out of it.
- Swap back, repay the loan with a small fee, keep the difference.
The attacker risks only the gas. And because everything happens in one transaction, the chain shows no state in which the price is wrong: the transaction either succeeds as a whole or does not exist. There is no moment between step 2 and step 4 that a monitor can look at.
That is the main thing to carry into the next part. A manipulation inside one transaction leaves its trace after the money is gone.
KiloEx, April 2025
KiloEx is a perpetuals exchange that ran on BNB Chain, opBNB and Base. On April 14, 2025 an attacker manipulated its price oracle and took about $7.5M12. KiloEx's own post-mortem gives the root cause1; we do not retell it here, only what the chain showed.
At 18:52:27 UTC, on BNB Chain about $893K and on opBNB about $2.9M left the pools in the same second. At 18:53:27 another $3.13M left Base. Smaller transfers followed until 19:55:33. One attacker used one address across all three networks3.
Count the time. An alert on the first large outflow from a BNB Chain pool arrives in under 2 minutes. By that point nearly everything had left. By our count only the tail, about $0.4M, was still to go after such an alert. And SmartAlert does not index opBNB at all, so $2.9M of the loss was invisible to us in any case.
This is the typical shape of a manipulation with a short window. The signal is the loss, not a warning before it.
What a monitor can and cannot see
Cannot see: the price during the transaction. A rule reads confirmed blocks, 12 confirmations behind the head on Ethereum, and it reads a function's answer once a minute at best. A price that is false for one transaction is never in a confirmed block as a state. If your oracle is a pool, you cannot catch the manipulation by reading the price.
Can see:
- A price that stays wrong for blocks. If the attack changes the price of a feed or a keeper and it is not restored at once, a state rule sees the value. This is the case after a compromised price-setter key, or a feed that has stopped updating.
- The outflow: tokens leaving the pool or vault in dollars.
- The burst: a swap count far above the norm for that hour.
How to catch it
- Watch the price source you depend on with a "Contract state" rule. If your protocol reads a feed with a one-number view such as
latestAnswer(), create a rule on it: setscaleto the feed's decimals (8 for a Chainlink USD feed), fire above or below a sane range, or use "deviates from the norm" against the same hour of previous days. Check the current answer with "Check function" first. Pro and above, called every minute. This catches a stuck or wrong feed. It does not catch a one-transaction manipulation. - Watch the outflow in dollars. Transfer above an amount in $ on the pool's token,
from= the pool or vault. For native ETH or BNB, which has noTransferevent, add the address in "Addresses" and use "Total address balance in $ below threshold". - Watch for a burst of trading. Swap spike and liquidity removed on Uniswap V2-style pools compare each hour with the same hour on previous days. Manipulation on a thin pool shows as a spike.
- Add the same rules on every network the protocol runs on. KiloEx was on three networks at once. If you have a deployment on a network SmartAlert supports, it should be added there too; at KiloEx the gaps between networks were seconds, so this helps with coverage, not with time.
Where SmartAlert has no rule:
- Pool spot price.
getReserves()and Uniswap V3'sslot0()return several values at once, and contract-state rules support only a single bool, address or number as an answer. We cannot watch a pool's price directly. - Feed freshness.
latestRoundData(), which carries the time of the last update, returns several values too. We cannot alert when a feed has gone stale; we can only watch the price itself. - A manipulation inside one transaction, whatever the price source. That needs simulation before the transaction is confirmed, and a pause that fires by itself. It is a different class of product, and part 8 is about it.
The strongest protection against this class of attack is design, not monitoring: use a TWAP or an external feed instead of a spot price, bound how far the price can move in one update, and restrict who can set a price.
Next: part 8, single-transaction exploits and what monitoring cannot do.
Sources
- 1.KiloEx, Security incident root cause analysis: the cause, the $7.5M, the networks. ↩
- 2.KiloEx on X, incident statement. ↩
- 3.Rekt, KiloEx — Rekt: the attack across BNB Chain, opBNB and Base. Transaction times and amounts per network: SmartAlert, on-chain. ↩