---
title: "The cheapest signed oracle on the EVM"
description: "5,158 gas per feed, measured. Four generations from one slot per feed to ten feeds in one slot, why every mainstream design still spends a whole word on a single price, and what we refused to give up to get here."
date: "2026-09-01"
tags: [oracle, protocol, research]
category: "research"
status: published
lang: en
aliases: [the-cheapest-verifiable-push-oracle]
motion: lanes
authors: [jake]
og: "/thumbnails/lanes-og-poster.png"
---

Every BTR pool prices against a pushed mark, so the cost of pushing marks is a line item on every quote we serve. This is what that line item cost across four generations of the contract, what each generation bought, and where the mainstream designs sit.

The short version: the whole field stores **one price per storage slot**. We store eight to ten, verify the quorum **once an hour** instead of once a push, and fire a push when the mark crosses a share of the edge the pool is currently quoting rather than a hand-set threshold.

| generation | storage | verification | gas per feed |
|---|---|---|---|
| V1 | 1 slot per feed | k-of-n per push | 7,422 |
| V2 | 8 feeds per slot | k-of-n per push | 11,208 |
| V3 | 10 feeds per slot | k-of-n per session | **5,158** |
| **V4** | **8 feeds per slot, 128x finer** | **k-of-n per session** | **5,828** |

*V3 is cheapest measured; Arc forum table's "V4 5,158" is this V3 measurement (10 feeds, 2-of-3). V4 trades density for 128x finer lanes and a clock with no end date: 5,828 at 10 feeds, 3,006 at 66.*

Full-transaction gas at 10 feeds, from `test/unit/OracleGasBench{,V3,V4}.t.sol`. Two rows go the wrong way and both are the same effect: packing pays only once slots fill, so V2 loses to V1 at ten feeds and V4 loses to V3 wherever eight lanes cross a slot boundary that ten did not. V4 buys 128x the price resolution and a clock with no end date for that. On the 26-feed production manifest the gap is +9.9%, 3,441 against 3,783.

## 1. Where the field puts a price

Four designs carry most of the EVM's oracle traffic. They differ in who submits, what gets verified, and what a price costs to store.

| | Chainlink OCR2 | Pyth | RedStone | Chronicle Scribe |
|---|---|---|---|---|
| Model | push, permissioned transmitter | pull, anyone | calldata injection + push relayer | push, validator |
| Trigger | deviation + heartbeat | on demand | deviation + heartbeat | deviation + heartbeat |
| Signatures verified | $f+1$ ECDSA | ECDSA on a Merkle root | 3-of-5 ECDSA per package | **1 Schnorr** over $k$ point-adds |
| Slots per feed | 1 (+ config, + payment) | 2 | 1 | 1 |
| Price encoding | fixed decimals | **mantissa + exponent** | fixed 8 dp | fixed 18 dp |
| Feeds sharing a slot | no | no | no | no |
| One signature covers N feeds | no | **yes** | capable, unused | no |

Two things in that table set up everything after it.

**Nobody packs across feeds.** All four pack *within* a feed, value beside timestamps, and stop there:

- Chainlink `Transmission`: `int192 answer | uint32 observationsTimestamp | uint32 transmissionTimestamp`, exactly 256 bits, exactly one price.
- Chronicle `PokeData`: `uint128 val | uint32 age`, 160 bits, one price, with `bar` declared last specifically so a downstream contract can pack the remainder.
- RedStone `DataFeedDetails`: carries a comment reading "this struct uses exactly one storage slot", again for one feed.
- Pyth: two slots, 32 bits of the first left unused rather than lent to a neighbour.

**Only Pyth uses a floating point.** `int64 price` with an `int32 expo`, documented as $x \cdot 10^{\text{expo}}$. The other three fix decimals at deploy (8, 8 and 18), exponent either immutable per contract or carried out of band in config.

A price does not need 256 bits. How few it needs is a real question with a defensible range: the packed venues in §3 chose 22 and 27 bits of mantissa, we shipped 18 in §5 and 25 in §7. Nobody who has looked at it seriously lands anywhere near 256. The four vendors above are unanimous about spending them anyway.

### What the field costs

Published gas figures in this category are mostly stale, vendor-supplied, or absent, so we measured them ourselves. Everything below is **full-transaction `gasUsed` from a mainnet receipt**, pulled over HyperSync on 2026-09-01, with the feed count per transaction read from the contract's own events. One row per provider; the transaction hashes and block ranges are in [the working notes](#appendix-measurement-notes).

| provider | model | median gas/tx | feeds/tx | gas per feed | basis |
|---|---|---|---|---|---|
| **BTR V3** | push, k-of-n per session | 51,588 / 178,579 | 10 / 66 | **5,158 / 2,705** | `OracleGasBenchV3.t.sol`, 2-of-3 |
| Chainlink OCR2 | push, permissioned | 182,121 | 1 | 182,121 | ETH/USD mainnet, n=309 |
| API3 | push, 7 signed beacons | 175,853 | 1 | 175,853 | Base `Api3ServerV1`, n=206 |
| Chronicle Scribe | push, Schnorr, `bar` 13 | 126,859 | 1 | 126,859 | ETH/USD mainnet, n=366 |
| Pyth | pull, batched | 115,535 → 301,251 | 1 → 8 | 115,535 → **37,656** | Base, n=639 |
| RedStone | calldata injection + relayer | 65,558 → 4,394,169 | 1 → 88 | 65,558 → 38,504 | Ethereum multi-feed, n=28,563 |
| ElfomoFi | trusted-writer blind store | 28,749 | 6 | **4,792** | Base price store, n=5,995 |
| Metric | namespaced blind store | 29,767 / 35,423 | 4 / 8 | 7,442 / **4,428** | Base `CompressedOracleV1`, n=2,231 |
| HumidiFi / Obric / SolFi / Tessera | Solana | not comparable | – | – | compute units do not convert to gas |

Five things about that table, because these numbers are easy to abuse.

- **Per-transaction is not per-feed.** Chainlink, Chronicle and API3 run one contract per feed, so an update *is* a feed and the two columns collapse. Pyth and RedStone batch, and a batched number is only meaningful next to its width. The 21,000 intrinsic alone is 21,000 per feed at $N=1$ and 318 at $N=66$.
- **Batch width is not a free parameter.** RedStone's 88-feed transaction is real (`0x60f5afbf34d3f0fd9889733fc29c52b4cce142e2559bca7a1f14734664c689ca`, 4,394,169 gas), but it is 4 transactions out of 28,563. The modal batch is one feed, 66% of the time, because only feeds whose deviation or heartbeat fired go in. Weighted across the whole 150,000-block window, RedStone's Ethereum adapter spent 2,433,854,363 gas on 44,432 feed updates: **54,777 gas per feed** is the honest fleet number. Its per-feed cost also bottoms out at ~38,500 around $N=9$ and climbs back to ~49,900 at $N=88$, because the parsing loop is $O(\text{points} \times \text{feeds})$.
- **Chronicle's headline numbers describe a path mainnet is not using**, and the instance usually cited is dead. `0x64DE91F5…C7C6cE410C85` (`wat` = `ETHUSD`, `bar` = 13) has zero transactions and zero logs across 900,000 Ethereum blocks. The live feed is `0x46ef0071…3Ae5917f4E` (`wat` = `ETH/USD`, `bar` = 13), and across 500,000 blocks all 617 of its events are `Poked` and **none** are `OpPoked`: the widely cited 51k-65k figures are for an optimistic path that has a 600-second challenge period configured and no traffic. The vendor's own published curve for the non-optimistic path (81,025 / 134,342 / 585,993 gas at `bar` 5 / 15 / 100) puts the measured 126,859 at `bar` 13 exactly where it should be. 338 of its 366 successful pokes use a mined vanity selector, `0x00000082`, three zero bytes bought to shave calldata cost.
- **The bottom two rows are not oracles and do not push on deviation.** ElfomoFi and Metric are proprietary market makers writing their own book, and both rewrite the whole word every block whether or not a price moved: 5,995 pushes in 6,000 Base blocks for one, 2,231 pushes at a 2-second median for the other. Their gas per feed is therefore a *capacity* number, not a cost-per-informative-update. An oracle that pushes only when a mark crosses a threshold spends more per push and far fewer pushes. Both are in the table because they are the live floor of the category, and §3 is about what they do with the word.
- **Pyth is measurable.** The contracts are public and so is the traffic. On Base its cost is well fit by a fixed ~89,000 plus ~26,530 per feed, i.e. one router signature amortised over the batch, which is the same structural argument we make for our own blob. Its economics did change on 2026-08-26, when the EVM path moved from Wormhole guardians to a 5-router 3-of-5 quorum; everything above is post-change.

## 2. V1: a signed batch, one slot per feed

Our first oracle took the field's shape and fixed the two things that bothered us about it.

**Price authority is a signature, not a sender.** `batchPushSigned` takes a blob and $k$ concatenated ECDSA signatures over the EIP-712 digest of `keccak256(blob)`, recovered in strictly ascending address order so the count is a genuine $k$-of-$n$ proof rather than one signer repeated. The relayer is unpermissioned: anyone can land a blob, and landing it grants no authority over its contents. Chainlink's transmitter check and RedStone's whitelist both bind authority to the sender; this does not.

**One transaction carries many feeds.** All $k$ signatures cover the same digest, so a batch of $N$ feeds pays the quorum once rather than $N$ times. That single decision is why our per-feed numbers are in the thousands while a per-contract design's are in the hundreds of thousands.

The feed itself is one 256-bit word:

```bitfield 256
0..63     lastPriceB64     (B64 float, 52/5/7)
64..95    sigmaPbps        (u32)
96..127   updatedAtSecs    (u32)
128..143  ttlSecs          (u16)
144..159  confidenceBps    (u16)
160..175  flags            (u16, bit0 = paused)
176..191  maxDeviationBps  (u16)
192..239  sourceTsMs       (u48, monotonic replay nonce)
```

The mark is a B64 float: 52-bit mantissa, 5-bit decimals, 7-bit exponent, biased by 64. Worst-case relative step is about $2.2 \times 10^{-15}$, exact for all practical purposes. That buys a whole feed in one `SSTORE`, where the field spends a slot on the price alone and puts $\sigma$, confidence and TTL somewhere else or nowhere at all.

Note the 16 free bits at the end. V1 packs a whole feed into one word and still does not fill it, which is the observation the next generation is built on.

Measured, full transaction:

| shape | gas per feed |
|---|---|
| 66 feeds, cold | 7,998 |
| 10 feeds, warm | 7,422 |
| 10 feeds, cold | 11,421 |

The field's cheapest measured one-feed path is ~65.6k and its median vendor sits near 127k. 7.4k was not the frontier either, so we went and measured the frontier.

## 3. What the prop AMMs were doing, and what we took from them

The proprietary AMMs taking share through 2026 live or die on mark freshness, so their oracle plumbing is where their engineering went. They got to packed multi-price storage before we did, and V2 and V3 are built on their idea: **several prices and one shared timestamp in a single 32-byte word, written blind.** Read off the chain, they kept more precision than we did, not less.

| venue | chain | mechanism | price encoding | auth on the hot path | cadence | gas per push |
|---|---|---|---|---|---|---|
| **Metric** | Base (also deployed ETH, Arbitrum, HyperEVM) | selectorless fallback; word = `uint8` slot index + `uint56` ms timestamp + 4 × 48-bit lanes | 27-bit mantissa + 5-bit base-2 exponent, `uint64` at 8 decimals, plus two 8-bit bid/ask codes indexing a 256-entry bps table | **none**, namespaced blind write; `ecrecover` exists only on the one-off delegation registration | 2 s median, every Base block, via 3 round-robined senders | 29,767 (4 feeds) / 35,423 (8 feeds) |
| **ElfomoFi** | Base, BSC | selectorless fallback on Base, `updatePrices(uint256)` on BSC; one word = `uint32` timestamp + N × 28-bit lanes | 22-bit mantissa, fixed decimals per pair, plus a 6-bit signed micro-skew worth ±3.2 bp on the mid | **none**, hardcoded `PUSH20` sender allowlist, revert string `"pub"` | one push per Base block, one per 3 BSC blocks | 28,749 (6 feeds) / 28,407 (8 feeds, BSC) |
| **HumidiFi, SolFi, Obric, Tessera V** | Solana | trusted-signer instruction, either to the AMM program itself or to a satellite oracle program | not publicly documented for any of them; SolFi's 168-byte oracle account is actively padded with constant filler | **none**, the only signature is the Ed25519 check Solana runs on every transaction, which authenticates the writer, not the price | 3-16 pushes/s per feed measured, higher in bursts | 197-740 compute units; **not convertible to EVM gas** |

Five things fall out of that table.

**The word layout converged independently.** ElfomoFi's BSC feed is eight 28-bit lanes plus a 32-bit timestamp, $8 \times 28 + 32 = 256$, one full word. That is, bit for bit, the shape of our V2 slot in §4, arrived at by a desk that has never seen our contract. It is the strongest evidence we have that the layout is the right answer and not a local optimum.

**They kept more mantissa than we did, not less.**

| design | mantissa | relative step | in bps |
|---|---|---|---|
| Metric | 27 bits + 5-bit binary exponent | $1.5 \times 10^{-8}$ | ~0.00015 |
| ElfomoFi | 22 bits fixed-point | – | ~0.04 (top of a decade) |
| BTR V2 | 23 bits | $2^{-22}$ | 0.0024 |
| BTR V3 | **18 bits** | $2^{-17}$ | 0.076 |
| BTR V4 | 25 bits | $2^{-24}$ | 0.000596 |

The precision cut was ours, and §5 defends it on its own terms: 18 bits is lossless *against the grid our producer actually quantises to*, verified by sweeping 28 live marks with a worst round-trip error of 0.049 bps. It stopped being the right cut when the target spread narrowed, and §7 is what replaced it: 25 bits, finer than everything in this table but Metric.

**What they spend the spare bits on is instructive.** Both EVM venues put quoting information in the word next to the price. Metric carries two 8-bit codes per lane that index a 256-entry lookup table of bps spreads, with `0xff` as a no-quote sentinel; ElfomoFi carries a 6-bit signed skew that shifts the mid by up to 3.2 bp. Both are a desk pushing *its own quote*. Our lanes carry a mark and nothing else, because the quote is computed on chain from it, which is the whole architectural difference and the reason our $\sigma$ and confidence live in separate words that are only written when they move.

**Nothing in the category verifies anything on the hot path.** This is the axis we kept and every one of them gave up, and it is now measured rather than asserted.

- Metric's push is a bare fallback with no `require`. Authority is namespacing: you may write only slots keyed by your own address, or by one that delegated to you, and replay protection is a monotonic millisecond timestamp.
- ElfomoFi's push clears a hardcoded sender allowlist and nothing else. Every `CALL` in its store, pricing implementation and router was enumerated and none of them is precompile `0x01`.
- The Solana cohort is the same position with a different runtime, including Obric, which reads what look like external feed accounts but which are owned by bespoke trusted-writer programs and not by Pyth, so it inherits no attestation from the resemblance.
- The gas arithmetic corroborates it: ElfomoFi's 28,749 is 21,000 intrinsic plus 25 bytes of calldata plus one `SSTORE` and one `LOG1`, with no room left for an `ecrecover`.

**The floor is a ~29,000-gas transaction, and what it costs per feed is entirely a question of how many lanes you fit in the word.** The ~2,800 gas per feed we quote for an unsigned, unguarded push at ten lanes is 21,000 intrinsic plus one unconditional store, and it now has a live comparable:

| construction | lanes/word | gas/tx | gas per feed |
|---|---|---|---|
| ElfomoFi, live blind push | 6 | 28,749 | 4,792 |
| Metric, live blind push | 4 | 29,767 (1 slot), 35,423 (2 slots) | 7,442 |
| the same blind write at V3's width | 10 | – | ~2,900 |
| **V3, signed** | 10 | 51,588 | **5,158** |

Metric's marginal cost per additional slot is 5,656. The ~2,300 gas per feed between the blind floor at ten lanes and V3 is the entire product: the monotonic replay read, the session authorisation, the per-lane fail-soft, the deviation bands, and a quorum signature that anybody can check without asking us or the chain. Whether that is worth roughly doubling the floor is the only question this article is really about, and §9 is our answer.

**And the Solana speed does not port.** HumidiFi and Tessera push at rates no EVM chain will absorb, for three Solana-specific reasons at once:

1. programs written in Pinocchio or hand-rolled sBPF cost 143-740 compute units;
2. the base fee is charged per signature rather than per byte of state written;
3. Jito's auction ranks by tip *per compute unit*, so a tiny transaction buys priority for a trivial tip.

None of the three has an EVM analogue. What *is* structural, and does port, is the oracle-LVR advantage that motivates the cadence in the first place, which is why the correct EVM adaptation is a $\theta$-gated push sized to the edge the pool is quoting, as in §8, rather than an imitation of 1 Hz.

The venue-design side of this, what these desks put on chain, what they keep off it, and what that costs a taker, is [the subject of its own piece](/blog/opening-the-dealing-desk-everyone-hides). This one is about the word.

## 4. V2: eight feeds in one slot

If the cost is `SSTORE` count, and a mark quantised to a few hundredths of a basis point is indistinguishable from an exact one at the sizes anyone trades, a 256-bit word holds eight prices and a timestamp.

```bitfield 256
0..27     lane0   (28b)
28..55    lane1   (28b)
56..83    lane2   (28b)
84..111   lane3   (28b)
112..139  lane4   (28b)
140..167  lane5   (28b)
168..195  lane6   (28b)
196..223  lane7   (28b)
224..255  tsDs    (u32, slot timestamp)
```

$8 \times 28 + 32 = 256$, exactly. Each lane is a small binary float:

```bitfield 28
0..22   mantissa  (u23, MSB always set for a live price)
23..27  exp       (u5, 32 octaves)
```

Decode is $\text{mark} = \text{mantissa} \ll (\text{exp} + \text{expBias})$, with `expBias` a per-feed `int8` in the cold registry positioning the octave window over the asset's range. A 23-bit mantissa normalized to $[2^{22}, 2^{23})$ steps by $2^{-22}$, about **0.0024 bps**.

Three consequences, each a design property rather than a saving:

- **Feed identity becomes positional.** `globalIndex = slotId * 8 + laneIdx`. V1 spent 8 bytes per feed naming what a lane position now says for free.
- **The all-zero lane is a free stale sentinel.** The mantissa MSB is always set for a live price, so an unset feed decodes to nothing and the consumer gate fails closed. No flag bit spent.
- **Slots must be class-pure.** Our first lane map assigned feeds sequentially, then a weekend arrived: the producer emits a slot record only when every feed in it is live, so an equity sharing a word with a stable froze the whole word. One class per slot, and a closed market darkens only its own.

$\sigma$ and confidence live in their own words, written only when the packed word changes. On a typical push, zero extra `SSTORE`s.

**And V2 is slower than V1 at ten feeds:** 11,208 against 7,422. Ten feeds spread across two slots pay two full slot records, and V2's wire is a fixed 100 bytes per slot touched, carrying price, $\sigma$ and confidence whether or not the last two moved. The packing only pays once the slots fill: at 16 feeds V2 falls to 8,899. That is what V3 was built to fix.

## 5. V3: verify once per session, send only what changed

The quorum was not the problem. Running the same signed push twice, once with the quorum check and once with it stubbed, puts the two `ecrecover`s at **13,377 gas of execution** (37,269 against 23,892). Three other things were:

1. a per-lane config `SLOAD` on the hot path;
2. a fixed 100-byte record per slot;
3. $\sigma$/confidence words rewritten whether or not they had moved.

```mermaid
flowchart TB
  subgraph off["Off chain"]
    A[producer quantises to the mark grid]
    B[encode lanes]
    C["diff: only lanes that moved"]
  end
  subgraph auth["Authorisation, once per session"]
    D["k-of-n signs SessionGrant<br/>relay, expiry <= 1h, maxSeq, nonce"]
    E[contract verifies the quorum once]
  end
  subgraph chain["Per push"]
    F["relay submits blob<br/>header + one entry per moved feed"]
    G["auth: msg.sender == relay"]
    H[per-slot timestamp: skip if not newer]
    I[per-lane validation: fail-soft]
  end
  subgraph read["Consumer"]
    J["one config word per slot"]
    K[sigma-root-tau staleness premium]
  end
  A --> B --> C --> F
  D --> E --> G
  F --> G --> H --> I --> J --> K
```

### Session grants

The quorum signs a `SessionGrant{relay, expiresAt, maxSeq, nonce}`; the contract verifies those signatures **once**, then gates pushes on `msg.sender == relay` for the life of the session. The bounds are what keep that from being a downgrade:

| bound | value | what it stops |
|---|---|---|
| `expiresAt` | $\le$ 1 hour | standing authority cannot accumulate; a grant self-terminates |
| `maxSeq` | ceiling on the blob sequence | caps a hijacked relay's blast radius |
| `nonce` | must be exactly stored + 1 | grants cannot be replayed or reordered |
| revocation | any granted signer, guardian or admin, single tx | no quorum needed to shut a session down |

Every push still emits `SlotsPushed(seq, tsDs, acceptedMask, keccak256(blob))`, so anyone can check the batch off chain from the event, and `pushSignedV3` keeps the permissionless per-blob quorum path: a dead session degrades to the V2 model, not to an outage.

### Ten lanes and one config word

```bitfield 256
0..21     lane0   (22b)
22..43    lane1   (22b)
44..65    lane2   (22b)
66..87    lane3   (22b)
88..109   lane4   (22b)
110..131  lane5   (22b)
132..153  lane6   (22b)
154..175  lane7   (22b)
176..197  lane8   (22b)
198..219  lane9   (22b)
220..251  tsDs    (u32, slot timestamp)
```

```bitfield 22
0..17   mantissa  (u18, MSB set = live, all-zero = STALE)
18..21  exp       (u4, 16 octaves)
```

An 18-bit mantissa steps by $2^{-17} \approx 0.076$ bps, below the producer's own quantisation grid of roughly 0.15 to 0.3 bps: **lossless at the grid it is actually fed**, verified by sweeping 28 live marks, worst round-trip error 0.049 bps.

Per-slot config collapses into one word of ten 25-bit lanes, which is what removes V2's per-lane `SLOAD`:

```bitfield 25
0..15   maxDevBps  (u16)
16..23  expBias    (u8)
24..24  paused     (1b)
```

### The diff wire

```bitfield 96
0..7    version     (u8, = 4)
8..39   seq         (u32)
40..71  sourceTsDs  (u32)
72..79  nP          (u8, price entries)
80..87  nS          (u8, sigma entries)
88..95  nC          (u8, conf entries)
```

| section | bytes per entry | sent |
|---|---|---|
| price | 4 (`gi:u8 \| lane:u24`) | when the lane moves |
| $\sigma$ | 5 | only when it changes |
| confidence | 3 | only when it changes |

With no $\sigma$ entries in the blob the $\sigma$ word is never even `SLOAD`ed.

$$\text{blob bytes} = 12 + 4n_P + 5n_S + 3n_C$$

Against V2's $9 + 100 n_{\text{slots}}$, a ten-feed push carrying only prices is 52 bytes instead of 209.

## 6. Measured

`test/unit/OracleGasBenchV3.t.sol`, EIP-7623-aware, **full-transaction gas per feed**, 2-of-3 quorum. Reproduce with `forge test --mp test/unit/OracleGasBenchV3.t.sol -vv`.

| shape | V1 | V2 | V3 session | V3 signed fallback |
|---|---|---|---|---|
| 66 feeds | 3,999 | – | **2,705** | – |
| 16 feeds | – | 8,899 | **4,202** | – |
| 10 feeds | 7,422 | 11,208 | **5,158** | 6,499 |
| 8 feeds, 1 slot | – | 11,519 | 6,038 | – |
| 3 feeds, narrow | – | 22,313 | 13,321 | – |
| 10 feeds, no lane changed | – | – | 3,966 | – |

Read across a row, not down a column. The 66-feed row is the one shape where V1 looks competitive, and the reason is amortisation: the fixed 21,000 intrinsic is 318 gas per feed at 66 against 2,100 at ten. At equal width V3 is 32% cheaper than V1 and 62% cheaper than V2. There is no V2 arm at 66: the bench's lane helper builds two slots and V2 needs nine at that width.

**5,158 gas per feed at ten feeds**, against 11,208 for V2 and 7,422 for V1; 2,705 against V1's 3,999 at 66; against a field whose cheapest single-feed update we could measure is RedStone's 65,558, with Chronicle at 126,859 and Chainlink at 182,121.

## 7. V4: a finer lane and a clock with no end date

V3 changed *when* the quorum is verified. V4 changes two fields and nothing else, the price word and the timestamp, and both changes were forced by measurement rather than review. Sessions, the diff wire, the $\sigma$/confidence words, fail-soft, the deviation bands and the quorum model are V3's, carried over untouched.

### The lane is too coarse for the spread we now intend to quote

V3's 0.076 bps step was sized against ~5 bp quotes. Against the 0.2 bp one-way stable spread we are targeting it is **±19% of the half-spread**, no longer noise. V4 spends the width:

```bitfield 29
0..24   mantissa  (u25, MSB set = live, all-zero = STALE)
25..28  exp       (u4, 16 octaves)
```

A 25-bit mantissa normalized to $[2^{24}, 2^{25})$ steps by $2^{-24} = 5.96 \times 10^{-8}$, **0.000596 bps**: 128× finer than V3. Eight of those lanes leave 24 bits of the word for the clock.

```bitfield 256
0..231    lanes    (8 x 29b)
232..251  ts       (u20, deciseconds since midnight UTC)
252..253  dayMod   (u2, source day mod 4)
254..255  unused   (2b)
```

On the wire this is version 5:

```bitfield 88
0..7    version     (u8, = 5)
8..39   seq         (u32)
40..63  tsDs        (u24, u20 value zero-padded)
64..71  nP          (u8, price entries)
72..79  nS          (u8, sigma entries)
80..87  nC          (u8, conf entries)
```

An 11-byte header and a **5-byte** price entry, `gi:u8 | lane:u32` with the top three bits zero, against V3's 12 and 4. $\sigma$ and confidence entries are unchanged at 5 B and 3 B, and still travel only when they move.

`exp:u4` is kept deliberately. Sixteen binary steps around a per-feed bias is ~65,000× of dynamic range for one asset, which covers JPYC through WBTC with room to spare; a fifth exponent bit would cost a mantissa bit and buy range no feed uses.

Where this lands against the packed venues of §3: Metric holds 27 mantissa bits at 4 lanes per slot, ElfomoFi 22 at 6–8. V4 is within ~4× of Metric's precision at twice its density, and unlike either it verifies a k-of-n quorum on chain.

### The exponent window, and where it was sitting

The encoder rescans the exponent on **every** push, renormalising the mantissa into its window with the MSB always set, so precision is always maximal for the current magnitude. That is unchanged since V2. `expBias` does not touch precision; it only *positions* the 16-step window over an asset's range, so an asset travelling from 0.001 to 1,000,000 stays encodable without a config change.

The bias was meant to track the asset and keep the window centred. Two things were true of V3 on the live fleet:

**One bias per encode class, not per feed.** Four constants (stable 34, FX 30, volatile 47, equity 43) covered 26 feeds, so each feed sat wherever its own magnitude happened to land inside its class window:

| feed | exponent | headroom |
|---|---|---|
| EURC | $e = 13$ of 15, two steps from the ceiling | 4× up |
| AUDF, QCAD, WBTC | $e = 12$ | 8× up |
| KRW1 | $e = 2$ | 4× from the floor |

**The bias moved by hand.** `setFeedExpBias` is a guardian call, so re-centring a feed took an operator noticing that it needed re-centring.

Neither cost a basis point of precision on any ordinary day. Both put a config change on the critical path of an event that does not wait for one: a redenomination, a de-peg, an equity split.

V4 derives the bias **per feed** from the asset's live magnitude,

$$\texttt{expBias} = \text{bit\_length}(\text{mark}_{1e18}) - (\text{mant\_bits} + 7) = \text{bit\_length}(\text{mark}_{1e18}) - 32$$

which pins $e = 7$ for every feed: eight exponent steps of upward headroom (256×) and seven down (128×) before a rebias is needed at all.

That is the production encoding, not a projection. Piping a live signed quote from the signer through the SDK's own `decodeBlobV5` reports **every price entry in the blob at exponent 7**, and the script exits non-zero if they are not:

```
$ curl -s '.../v1/quote/signed?version=5' | bun scripts/decode-live-v5.ts
version    5
seq        1788269141
prices     22  sigmas 0  confs 0
gi         0,1,2,3,4,8,9,10,11,12,16,17,18,19,20,21,24,26,27,32,33,36
exponents  7   <- all centred at 7
```

The entry count is not the claim and moves between runs: a feed whose mark is unavailable is excluded from the blob, an earlier sample minutes before carried 24. The exponent is the invariant. `sdk/scripts/decode-live-v5.ts` is committed so the check is re-derivable rather than quoted.

The contract exposes the same property directly, as a read of the deployed instance:

```
expHeadroom(EURC) -> (8, 7)      // V4, read 2026-09-01
                     (2, 13)     // the same feed under V3's class bias
```

The re-centring keeper is built, and it is quorum-signed rather than roled:

```solidity
BiasUpdate(bytes32 feedId, int8 newBias, uint48 expiresAt, uint16 nonce)
function setFeedExpBiasSigned(bytes32 feedId, int8 newBias, uint48 expiresAt, uint16 nonce, bytes calldata sigs)
```

**It is now armed on the primary tier.** The keeper watches per-feed headroom, requests a quorum-signed `BiasUpdate` when a feed drifts toward its window edge, and relays `setFeedExpBiasSigned` itself. The reference tier is deliberately still off: one tier moves at a time.

Arming it took one change on the producer side, and that change is the whole reason this did not ship with V4. **A rebias zeroes the lane.** The mantissa MSB is the live sentinel, so the moment a feed's exponent window moves the old lane value stops decoding: the getter reads STALE and every read derived from it goes dark. A config-learned bias let the producer's view and the chain's diverge, and that divergence is not slow drift, it is an instantaneous blackout, in the one window where every source you might cross-check against is also unavailable.

The producer now reads `FeedExpBiasUpdated` logs from chain at 12 confirmations and takes the chain as the sole authority on the current bias. Chain state is the one view every replica and every cosigner can reach independently, so a replica that fell behind converges instead of diverging, and the `lane_map_hash` cosign commitment no longer carries a value the chain can change underneath it.

A role looked like the smaller grant and was not. Decode is $\text{mark} = \text{mant} \ll (\text{exp} + \text{bias})$, so **a bias write is a price write**: a "minimal" steward role would have carried full price authority under a name implying routine maintenance, a *larger* grant than the guardian lever it was meant to avoid. Rebias therefore clears the push bar: the same k-of-n, `feedId` and `newBias` inside the signed struct so a signature for one feed cannot be redirected at another, expiry $\le$ 1 h, nonce spent on use.

### The clock has a hard end date, and the end date bricks

V3 stores `ts:u32` as deciseconds since a fixed `EPOCH` immutable. It wraps in 2038, and that field **is** the per-slot monotonic replay guard, so the wrap does not degrade the oracle: it rejects every subsequent push.

V4 removes the epoch. `ts` is deciseconds since midnight UTC, range $[0,\ 864{,}000)$, which fits u20; absolute time is always derived from `block.timestamp`, so there is no end date to reach. Deciseconds rather than milliseconds because ms-since-midnight needs 27 bits and would drag the lane back to 28 (0.0024 bps) for resolution nothing needs: 100 ms already beats a sub-second block time and is what makes two pushes inside the same second orderable.

Reconstruction is a nearest-candidate-day pick, unambiguous for any true age under ±12 h:

$$\Delta_{ds} = \texttt{storedDs} - \texttt{nowDs},\quad
\Delta_{ds} \mathrel{-}= 864{,}000 \text{ if } \Delta_{ds} > 432{,}000,\quad
\Delta_{ds} \mathrel{+}= 864{,}000 \text{ if } \Delta_{ds} < -432{,}000$$

$$\texttt{absSecs} = \texttt{block.timestamp} + \Delta_{ds}/10$$

A cyclic field breaks a raw comparison, so the replay guard becomes **reconstruct, bound, then compare**:

```mermaid
flowchart TB
  A["ts:u20 from the slot word"] --> B["reconstruct absSecs<br/>nearest candidate day"]
  B --> C{"absSecs in<br/>[now - MAX_RECON_AGE,<br/>now + SOURCE_TS_FUTURE_SKEW]?"}
  C -- no --> X["fail closed<br/>StaleTimestamp / FutureTimestamp"]
  C -- yes --> D{"dayMod(absSecs)<br/>== stored dayMod:u2?"}
  D -- no --> X
  D -- yes --> E{"absSecs newer than<br/>the slot's previous absSecs?"}
  E -- no --> X
  E -- yes --> F["accept"]
```

**Step C is the whole security argument.** Without it, a stale timestamp that has wrapped reconstructs as *newer* than it is, which is exactly the direction that admits a replayed blob. With it, every wrap misread lands outside the window and fails closed.

| bound | value | relation |
|---|---|---|
| `MAX_RECON_AGE` | 6 h | two orders of magnitude above every deployed ttl ($\le$ 7,200 s) and `MAX_HEARTBEAT_S` (3,600 s); a clean 2× inside the 12 h ambiguity bound |
| `SOURCE_TS_FUTURE_SKEW_SECS` | 5 s | future-dated pushes rejected |
| `dayMod` alias period | 96 h | 16× `MAX_RECON_AGE`, ~48× the longest deployed ttl |

The read side applies the same bound and reports the feed as older than `MAX_RECON_AGE` rather than handing a consumer a wrongly-reconstructed, falsely fresh observation time.

The ±12 h window alone leaves one fail-**open** case, and it is the interesting one: a feed dark for *exactly* ~24 h reconstructs to `now` and reads **fresh**, serving a day-old mark with no staleness premium and no revert. `dayMod:u2` closes it. Every write tags the source day mod 4; every read recomputes the candidate day from the reconstruction and rejects on mismatch, never searching for another candidate. The two spare bits at $[254, 256)$ would push the alias to 16 days if that is ever wanted.

The tag must be derived from the reconstructed **source** day, never from `block.timestamp`. A push landing at 00:00:03 carrying a 23:59:58 mark is day $D$ under a `block.timestamp` rule while the reader reconstructs it to day $D-1$: the tags mismatch and a perfectly live feed reads falsely stale at every midnight. `test_dayTag_writtenFromSourceDayNotBlockTimestamp` fails under the other rule.

### What it costs

`test/unit/OracleGasBenchV4.t.sol`, same methodology as §6: EIP-7623-aware full-transaction gas per feed, 2-of-3 quorum, both designs benched side by side in the same test.

| shape | V3 session | V4 session | delta |
|---|---|---|---|
| 26 feeds (production manifest) | 3,441 | **3,783** | +9.9% |
| 66 feeds | 2,705 | **3,006** | +11.1% |
| 16 feeds | 4,202 | **4,291** | +2.1% |
| 10 feeds | 5,158 | **5,828** | +13.0% |
| 8 feeds, 1 slot | 6,038 | **6,090** | +0.9% |

The delta is a storage slot, and only a storage slot: eight lanes per word instead of ten means a 26-feed manifest spans four slots in the bench where V3 spanned three, and the live class-pure map spans five where V3 spanned four. The 10 equities are the whole story, filling a V3 slot exactly and overflowing a V4 slot by two. Intrinsic and calldata amortise identically; where the feed count does not cross a slot boundary the cost is inside 1% (8 feeds, +0.9%).

**The non-finding is the more useful measurement.** We expected the wider mantissa to cost calldata: more bits per lane means more lanes crossing their diff threshold, and the diff wire is what makes V3 cheap. On a real 26-minute tape, 1,905 snapshots at ~1.2 Hz of live NX Rates marks across all 26 feeds, widening the mantissa costs **+1.9% entries per push** (20.56 → 20.94 at a 10 s gap). The diff is already saturated at any realistic push gap: 21 of 26 feeds move within 10 s and 24 of 26 within 40 s, so lane width barely moves it. Almost all of the +9.9% is the extra slot plus one byte per entry, not the diff.

### The grid can quantise the whole upgrade away

`grid_mask_bits` on the producer side is **relative**, not absolute, so a flat mark grid masks a fixed *fraction* of the mantissa and eats every bit a wider lane adds. Measured under the old flat 0.5 bps default, entries per push are identical at every mantissa width from 18 to 25 bits (19.01, +0.0%) and so is the delivered precision: the quantiser simply gives back what the lane gained. Shipping the V4 lane against a flat grid buys exactly nothing, and that is a measurement rather than an argument.

The grid is now wire-aware and derived from measured 30-minute realized volatility instead of from a fee, with the divisor scaled 128× on the v5 wire. That only works if the $\sigma$ feeding it is a measurement, which for most of this year it was not: the class floors were priors set high enough to bind almost always. They were recalibrated against 17.6 days of tape:

| class | floor, before → after (pbps) | share of observations the old floor bound |
|---|---|---|
| FX | 2000 → 250 | 99.7% |
| crypto | 4000 → 800 | 84.5% |
| commodity | 2500 → 1400 | |

Every feed on chain now carries a measured $\sigma$. The single exception is `USDC-USD` at 208, sitting on its floor because it is a genuinely pegged pair, which is what a backstop is for.

Delivered precision is $\max(\text{lane step},\ \text{grid})$: on USDT-USDC at its live $\sigma$ of 288 pbps that is **0.000596 bps at wire v5 against 0.0763 bps at v4**, with an identical mask profile across every class, so the elision the diff wire depends on is unchanged. A v4-era divisor applied at 25 mantissa bits would have masked 9 bits on volatiles and 5 on stables, handing back all but 4× of the 128×. Where $\sigma$ is a degraded class prior rather than a measurement the grid refuses it and falls back to the flat default, counted.

### Status

**V4 is authoritative on both tiers**, primary and reference, since 2026-09-01: 26 feeds each, five class-pure slots on the primary, wire v5, a 3-relay rotation on the primary and 2 on the reference, and the rebias keeper armed. Every pool leg bands and fail-closes against the V4 reference. V3 stays as the rollback and its primary keeper still runs, because a rollback that is not fresh is not a rollback; the V3 reference keepers are retired, because nothing bands against that instance any more.

Every leg reads fresh and all four pools quote off it: USDT to USDCB at **0.815 bp one-way**, WETH 2,446.95, WBTC 78,011, NVDA 219.37, EURC 1.1587. All **26 of 26** feeds decode and render on the [transparency page](/oracle). Addresses are deterministic per fleet and resolve per chain: see [Consuming Price Feeds §2](/docs/5-1-5-consuming-price-feeds#2-addresses).

Reading the feeds from your own contract is three functions and has [its own guide](/docs/5-1-5-consuming-price-feeds).

## 8. The other lever: when to push

Everything above makes one push cheap. The other lever is deciding which pushes to send at all, because a push that carries no price information costs exactly what an informative one does. Measured over a 45-minute tape on 2026-08-31, half the pushes were heartbeats and the deviation threshold that fired the rest was a hand-set constant per feed.

**The trigger is now a share of the edge the pool is quoting.** The keeper mirrors the pool's own `Pricing._pathSpread` per leg:

$$E_i(t) = \text{minFee}_i + \frac{\sigma_i \cdot \text{vega}_i}{100 \cdot \text{BPS}} + \frac{Z \cdot \sigma_i \sqrt{\max(0,\; \tau_i - g_i)}}{\text{BPS}}
\qquad\text{push when}\quad |m_{\text{now}} - m_{\text{chain}}| \ge \kappa E_i(t)$$

| term | meaning | value |
|---|---|---|
| $\tau_i$ | age since the last push | measured per feed |
| $g_i$ | grace window | $\min(\text{ttl}_i/2,\ 30\text{s})$ |
| $Z$ | staleness coefficient, mirrored from `Pricing` | 100 |
| $\kappa$ | share of the edge that fires a push | 0.5 |

A stable quoting 1 bp pushes at 0.5 bp; an equity quoting 15 bp pushes at 7.5 bp. One constant replaces a table of per-feed thresholds, and when a fee floor or a risk parameter moves the boundary moves with it. As a leg ages past the grace window the contract is *already* charging $Z\sigma\sqrt{\tau}$ for that age, so the boundary widens by exactly what the quote has already sold.

The pool's quote also carries a confidence term, and the trigger **deliberately leaves it out**. Our confidence input is a freshness proxy, not a dispersion measure, and folding one into a push boundary inverts the trigger: a feed whose data has gone stale reports a higher confidence interval, quotes a wider edge, and would therefore push *less*, precisely when it should push more. Excluding it makes the keeper claim less protection than the pool charges, so the error is over-pushing rather than under-defending. It goes back in when confidence is rebuilt on cross-venue dispersion, which it is not yet.

**One heartbeat refreshes every seeded leg.** Twenty-six feeds each running their own liveness clock forced a blob every ~11.5 s no matter how still the market was. Coalescing restarts all of them in one blob, and on the diff wire the marginal leg is ~4.2k gas, cheaper than the second blob it avoids. A frozen record gets no heartbeat at all: when mark, $\sigma$ and confidence are bit-identical to what was last relayed, the source has said nothing, and re-publishing a dead price on a clock is pure cost. The feed ages out and the pool fails closed on staleness, which is the right outcome for a market that is not trading.

**The rate limit is a budget, not a refractory.** The old gate spaced every push of a feed 36 s apart to honour a 100-per-hour per-asset cap, which meant a fast mover could not be re-marked however far it ran: an extractable-value gap dressed as a rate limit. The cap is now a trailing-hour token bucket:

- a feed holding budget may re-push after a one-second burst floor (the contract admits one mark per feed per block);
- a feed out of tokens falls back to the full gap;
- a scarcity multiplier widens the boundary as the budget drains, so the last tokens of an hour go to the largest edges;
- worst-case hourly spend is unchanged by construction.

The fleet-wide manifest cap, 360 blobs/hour, is an **average**, not a spacing rule.

**Relays rotate.** Five relay EOAs are live, three on the primary oracle and two on the reference. The leader for a push is `keeper_set[keccak(slot) % N]` over a 10-second **wall-clock** slot, not over the blob's own timestamp: replicas poll independently and therefore hold different blobs, so a leader elected on blob contents was routinely a replica that had never fetched that blob and could not land it. Standbys arm at 8 s, staggered 4 s per index, and relay only if the leader's push has not landed. The contract holds one session at a time, so a relay that is not the session holder simply lands on the fully signed path instead.

Fleet cadence, measured from relay nonce deltas: **260 transactions/hour**, against 188/h before this work. Up, not down, and that is the intended direction: the old limiter spent its budget on evenly spaced heartbeats, the new one spends a larger budget on moves that cross the edge a taker would otherwise pick off. For scale, a design that pushes bid and ask directly at ~1 Hz runs ~3,600/hour; because BTR computes both sides on chain from a single mark, the mark only has to be right to within the spread quoted around it.

## 9. Why we still sign

The cheapest oracles in this class drop on-chain verification entirely. RedStone's own Bolt path checks `msg.sender` against five authorised updaters with **no signatures at all**. That is defensible: if you assume the operator's key custody is competent, the marginal risk looks small.

Two things make it not the trade we want.

**Liveness becomes key custody.** With an operator-key oracle the only party who can update the price is the party holding the key. Key ceremony breaks, ops box dies, operator gets rate limited, and the feed stops. A quorum-signed blob can be relayed by anyone, so liveness is not one team's uptime.

**Verifiability does not stop at the chain.** The blob is a self-contained EIP-712 artifact, so anything can check it: an integrator, a monitoring service, an off-chain risk engine. All of them get the same cryptographic answer to "did the quorum really say this", with no chain access and no trust in our relay.

And when the quorum itself fails, the damage is bounded. Suppose a full signer-set compromise and valid signatures on a hostile price: the on-chain deviation band still caps how far a push can move the mark per interval, scaled to the feed's own volatility, and any single granted signer can revoke the session in one transaction without needing a quorum to agree.

That is the division of labour in one sentence: **the signature authorises authenticity, the band bounds magnitude, and the session bounds duration.** The unsigned designs have the third and neither of the first two.

## 10. What is next

The wire is frozen and pinned by a golden vector shared byte-exact between the contract, the producer and the keeper: the same test vector runs in Solidity and Rust.

The upgrade we are sizing is **Schnorr aggregation**, which is the one place a competitor is ahead of us on design rather than on precision. Chronicle verifies $k$ signers with $k$ elliptic-curve point additions and a *single* signature check, rather than $k$ `ecrecover`s. At the session cadence the quorum is already amortised to near nothing, so this matters less than it did at V2's per-push cadence, which is exactly why it is next rather than now.

Automatic exponent re-centring (§7) is armed on the primary tier, gated on the producer reading the bias from `FeedExpBiasUpdated` on chain rather than learning it from config. What remains is arming it on the reference tier too.

The oracle is [on chain](/oracle): V4 authoritative on both tiers since 2026-09-01, all 26 feeds rendering live on the transparency page, every pool leg banding against the V4 reference, and the mechanism written up in [Oracles](/docs/3-4-oracles). To consume the feeds rather than read about them, the [integration guide](/docs/5-1-5-consuming-price-feeds) is the short version. Every gas number above reproduces from one `forge test` command; every competitor number is either a cited vendor figure or our own measurement with the transaction hash in the source. [Break it if you can.](/docs/3-7-bug-bounty)

## Appendix: measurement notes

Method, so the table can be checked rather than believed. Receipts and logs pulled over HyperSync on 2026-09-01; feeds per transaction counted from each contract's own events, or from the calldata array length where the contract emits none. Every figure is a median over the stated $n$, full-transaction `gasUsed`.

| provider | contract | entrypoint | window | $n$ | example tx |
|---|---|---|---|---|---|
| Chainlink | agg `0x7d4E7420…B21de6Fb5` via per-transmitter forwarders | `forward(address,bytes)` `0x6fadcf72` | ETH 25,820,587-25,880,587 | 309 | `0x772a03641fc3d7d8a639d22290dd72a16bc867325403798958d8a3df87528ca1` |
| API3 | `0x709944a4…905FB3920a` | `tryMulticall(bytes[])` `0x437b9116` | Base 50,687,327-50,727,327 | 206 | `0xb98f6f0c832ed7c9195f882e9a914b531bd334b58a690c248b943ca16e2d5d80` |
| Chronicle | `0x46ef0071…3Ae5917f4E` | `poke_optimized_7136211` `0x00000082` | ETH 25,380,626-25,880,626 | 366 | `0x4bb30bffced8e95f51ffbde077966f31f31be70b577e7ada8604ab81c2b0d999` |
| Pyth | `0x8250f4aF…DeB1487a` | `updatePriceFeedsIfNecessary` `0xb9256d28` | Base 50,667,010-50,727,010 | 639 | `0x0814d27e5e82b555074e693ae35ff5440a1a19c953909224b69863a8a97271df` (8 feeds) |
| RedStone | `0xd72a6BA4…0911fC6C` | `updateDataFeedsValuesPartial(bytes32[])` `0xb7a16251` | ETH 25,730,614-25,880,614 | 28,563 | `0x60f5afbf34d3f0fd9889733fc29c52b4cce142e2559bca7a1f14734664c689ca` (88 feeds) |
| ElfomoFi | `0x099097bF…F75ee289` | none: raw fallback, 25-byte calldata | Base 50,721,135-50,727,135 | 5,995 | `0x22ba1e8f909a637716abfff66bc23e10a36f058b421416ee63705a4cb1c54bcf` |

Two traps worth naming, because both would have produced a wrong table.

**Chainlink transmissions do not arrive at the aggregator.** Filtering on `to == aggregator` returns only `requestNewRound()` and misses every update. Each transmitter EOA calls its own forwarder (31 distinct addresses in this window) which then calls `transmit`. The correct filter is the `NewTransmission` log.

**The Chronicle instance in most write-ups is dormant.** It has a live `bar` and a live `wat`, and nothing pokes it. Reading a contract's configuration tells you nothing about whether it is used; only the transaction history does.

And one that nearly cost us the Metric row: a 200,000-block lookback on Base found no traffic at all on `CompressedOracleV1`, which is true and misleading. Its push window sits about 1.8 million blocks behind the tip. A recent-window scan answers "is it active now", never "has it ever been"; the second question needs `from_block: 0`.
