# Reading oracle prices on Starknet: lessons from the 4 September incident

**URL:** <https://community.starknet.io/t/reading-oracle-prices-on-starknet-lessons-from-the-4-september-incident/116444>\
**Category:** Starknet Technical Development\
**Created:** [October 25, 2026, 10:17am UTC](https://community.starknet.io/t/reading-oracle-prices-on-starknet-lessons-from-the-4-september-incident/116444 "2026-10-25T10:17:50Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![Zealynx](https://dub1.discourse-cdn.com/flex005/user_avatar/community.starknet.io/zealynx/32/86134_2.png) [@Zealynx](https://community.starknet.io/u/Zealynx)\
**Post date:** [October 25, 2026, 10:17am UTC](https://community.starknet.io/t/reading-oracle-prices-on-starknet-lessons-from-the-4-september-incident/116444/1 "2026-10-25T10:17:50Z")

</div>

On 4 September a bad price from Pragma’s publishing pipeline reached several Starknet feeds, and Vesu liquidated 47 positions across 42 wallets in seven pools in under two minutes. Pragma published a detailed post-mortem with the exact numbers and the remaining work, which I really appreciate. A write-up like that helps everyone building here.

I read it from the consumer side, as someone who reviews contracts that read oracle prices. The usual consumer check is three conditions: the price is not stale, enough sources were aggregated, and the price is not zero. All three can pass while the price is wrong. Here is why, with the code behind it.

### What happened, in short

1. A publisher’s token mapping pointed USDC to the old bridged USDC.e, so one USDT/USD observation came from an inactive pool at about $3.07.
2. Three healthy USDT/USD observations were older than the oracle’s one-hour freshness window. Only two were left, about $0.99995 and $3.07. The median of two values is their average, about $2.04.
3. The publisher software read that on-chain USDT/USD price and divided USDT-quoted exchange prices by it. Both active publishers ran the same software, so BTC, ETH, STRK and others showed up at about half their value.
4. wstETH was derived from ETH/USD on its own update schedule, so for a while wstETH collateral was still at half price while ETH debt had already recovered.

### 1. A source count describes the last step only

In Pragma’s oracle contract, `num_sources_aggregated` is the number of distinct sources that had a fresh entry for that pair. It says nothing about what those sources share upstream. The lending side’s minimum-source check rejected the USDT feed, which only had two sources left. The BTC and ETH feeds still had plenty of sources, so they passed, even though every one of them had gone through the same bad conversion factor before it was published.

One detail I found interesting: when the oracle contract does a conversion itself (the route through USD), it reports the smaller source count and the older timestamp of the two legs. The conversion here happened off-chain in the publisher, before the data reached the contract, so nothing on-chain could see it.

Two publishers running the same code are not two independent opinions. The post-mortem says it directly: publisher redundancy is not software independence.

### 2. Freshness is relative to the newest entry

The contract drops entries that are more than one hour older than the newest entry for the pair, and the timestamp it returns is the newest one. So a timeout check on that timestamp tells you the freshest source is recent. It does not tell you how many sources are still alive. When healthy sources go quiet, the feed shrinks to whatever is left, and a median of two inputs gives no protection against an outlier.

If your contract accepts a price above a minimum source count, pick that minimum with this in mind. Two or three is a very thin median.

### 3. A price that halves in one update deserves a second look

BTC did not lose half its value in a minute. A liquidation path can treat a move that size as a reason to wait, not to act. Two ways to do it: compare against an independent reference (a second oracle, or a TWAP from a deep pool), or cap how far a price can move between two accepted readings and pause liquidations for that asset until the next readings confirm the move.

The trade-off is real. In a genuine crash, a pause means late liquidations and possibly bad debt. I don’t think there is one right threshold, but with no threshold every bad tick is final.

### 4. Check derived prices against what they derive from

When collateral and debt share an underlying, like wstETH and ETH, a lending market can check that their ratio stays near the expected exchange rate and treat a big gap as an invalid price. Feeds on different update schedules will disagree for a while after any shock, not only after a bad one.

### Not only a lending question

Since v0.14.3 the minimum L2 gas base fee follows a STRK/USD price (SNIP-35), and the floating α proposal from 15 October sketches a staking mechanism that would read STRK and BTC prices every epoch and freeze on the last value when the oracle is unavailable. Freezing handles a missing price. It does not handle a wrong one, so the same questions about source counts, freshness and move limits apply there too.

Pragma’s full report: [4 September oracle incident: post-mortem and updates | Pragma](https://www.pragma.build/updates/vesu-incident)

For teams running liquidations or other price-dependent logic on Starknet: do you check against a second reference or cap price moves, and how did you pick the threshold so a real crash still gets liquidated in time?
