# Which pool's quote to believe when quotes are free to forge: three rules, three attacks, one depth-weighted median

**URL:** https://collective.flashbots.net/t/which-pools-quote-to-believe-when-quotes-are-free-to-forge-three-rules-three-attacks-one-depth-weighted-median/6122
**Category:** Research
**Created:** [November 3, 2026, 1:55pm UTC](https://collective.flashbots.net/t/which-pools-quote-to-believe-when-quotes-are-free-to-forge-three-rules-three-attacks-one-depth-weighted-median/6122 "2026-11-03T13:55:38Z")
**Posts on this page:** 1
**Page:** 1

<div class="post-metadata">

### Author: ![BlazePhoenix](https://collective.flashbots.net/letter_avatar_proxy/v4/letter/b/9de0a6/32.png) [@BlazePhoenix](https://collective.flashbots.net/u/BlazePhoenix)
#### Post date: [November 3, 2026, 1:55pm UTC](https://collective.flashbots.net/t/which-pools-quote-to-believe-when-quotes-are-free-to-forge-three-rules-three-attacks-one-depth-weighted-median/6122/1 "2026-11-03T13:55:38Z")

</div>

Disclosure: I work on BlazePhoenix, an on-chain DEX aggregator (pre-launch, independent external audit scheduled, nothing deployed for V2). This is a research note about a design question we got wrong once, posted here for criticism rather than promotion.

 ![Three rules for whose quote counts](https://collective.flashbots.net/uploads/default/original/2X/f/f27c166177065fce49b5460504a6ca929b6327ee.png)

**Question: when an aggregator/solver asks N pools for a price, what should decide whose quote is believed, given that a quote costs nothing to forge?**

A pool contract can answer a quote request with any number, because a quote is arithmetic over whatever the contract says its reserves are. “Trust the majority” is then a vote where votes are free. I tried three rules against three attacks (toy numbers for reserves; the 910 / 1,633 pair is a measured case from our [phantom liquidity write-up](https://blazephoenix.xyz/learn/phantom-liquidity/?utm_source=flashbots&utm_medium=article&utm_campaign=depth-weighted-median)).

**Rules**

1. Median by count (one pool, one vote).
2. Anchor the band on the pool with the largest `balanceOf`.
3. Depth-weighted median: sort by rate, carry depth (reserves / active liquidity, not `balanceOf`), return the first rate where cumulative depth \>= ceil(total/2); then drop candidates more than ±500 bps from that centre. This is equation E25 in our whitepaper (v2.3.0, §10.3, DOI 10.5281/zenodo.23084091).

**Attacks**

- Case 1 (measured): two dead pools agree on ~910, one pool with ~$401k says 1,633. Rule 1 centres on 910 and admits only the dead pools.
- Case 2: a pool quotes 2,000 with 1,000 in reserves, then receives a 5,000,000 plain transfer (recoverable via skim / LP claim, so not capital at risk). Rule 2 picks it as the anchor. Our whitepaper records that an external researcher showed an earlier balance-anchored design being captured this way; it was replaced.
- Case 3: six dust pools (100 units each) quote the same forged 2,000. Rule 1 follows them.

> **Toy script (python3 phantom.py)**
>
> ```python
> """phantom.py — three ways to decide whose price to believe. Toy numbers, real rules.
> Rules copied from: Whitepaper v2.3.0 E25 / section 10.3 (depth-weighted median, band +-500 bps)."""
> from dataclasses import dataclass
> 
> @dataclass
> class Pool:
> name: str
> rate: float # what the pool QUOTES (free to forge)
> reserve: float # depth in the pool's own accounting (a plain transfer does not move it)
> balance: float # balanceOf(pool): a plain transfer (donation) DOES move it
> 
> def band(pools, centre, bps):
> lo, hi = centre * (1 - bps / 1e4), centre * (1 + bps / 1e4)
> return [p.name for p in pools if lo <= p.rate <= hi]
> 
> def median_by_count(pools): # one pool, one vote
> r = sorted(p.rate for p in pools)
> return r[(len(r) - 1) // 2]
> 
> def anchor_largest_balance(pools): # "balanceOf cannot be faked without capital"
> return max(pools, key=lambda p: p.balance).rate
> 
> def depth_weighted_median(pools): # E25: first rate where cumulative depth >= ceil(total/2)
> ps = sorted(pools, key=lambda p: p.rate)
> total, acc = sum(p.reserve for p in ps), 0
> for p in ps:
> acc += p.reserve
> if acc >= -(-total // 2):
> return p.rate
> 
> def report(title, pools, bps_anchor=200):
> print(f"\n{title}")
> for label, centre, bps in [
> ("median by count ", median_by_count(pools), 500),
> ("anchor = largest balanceOf ", anchor_largest_balance(pools), bps_anchor),
> ("depth-weighted median (E25)", depth_weighted_median(pools), 500),
> ]:
> print(f" {label} centre {centre:>7.0f} admits {band(pools, centre, bps)}")
> 
> # Case 1 -- the measured case from the learn article: two dead pools agree on ~910, one deep pool says 1633.
> # (910 and 1633 are the article's numbers; the reserves/balances are mine, only their ORDER of size matters.)
> report("Case 1: two dead pools agree on 910, the deep pool quotes 1633", [
> Pool("dead-1", 910, 2_000, 2_000),
> Pool("dead-2", 910, 3_000, 3_000),
> Pool("deep", 1633, 401_000, 401_000),
> ])
> 
> # Case 2 -- a donation: the attacker's pool quotes 2000, holds almost nothing in its reserves,
> # and is sent 5,000,000 by a plain transfer, recoverable later, so it costs nothing at risk.
> report("Case 2: a forged 2000 quote plus a donated balance", [
> Pool("honest-a", 1633, 401_000, 401_000),
> Pool("honest-b", 1630, 120_000, 120_000),
> Pool("attacker", 2000, 1_000, 5_000_000),
> ])
> 
> # Case 3 -- count is free to fake: six dust pools quote the same forged 2000.
> report("Case 3: six dust pools agree on a forged 2000", [
> Pool("honest-a", 1633, 401_000, 401_000),
> Pool("honest-b", 1630, 120_000, 120_000),
> ] + [Pool(f"dust-{i}", 2000, 100, 100) for i in range(6)])
> 
> ```
> 
> Output:
> 
> ```plaintext
> Case 1: two dead pools agree on 910, the deep pool quotes 1633
> median by count centre 910 admits ['dead-1', 'dead-2']
> anchor = largest balanceOf centre 1633 admits ['deep']
> depth-weighted median (E25) centre 1633 admits ['deep']
> 
> Case 2: a forged 2000 quote plus a donated balance
> median by count centre 1633 admits ['honest-a', 'honest-b']
> anchor = largest balanceOf centre 2000 admits ['attacker']
> depth-weighted median (E25) centre 1633 admits ['honest-a', 'honest-b']
> 
> Case 3: six dust pools agree on a forged 2000
> median by count centre 2000 admits ['dust-0', 'dust-1', 'dust-2', 'dust-3', 'dust-4', 'dust-5']
> anchor = largest balanceOf centre 1633 admits ['honest-a', 'honest-b']
> depth-weighted median (E25) centre 1633 admits ['honest-a', 'honest-b']
> 
> ```

Only rule 3 survives all three: its breakdown point is half the depth _mass_, so to move the centre you must out-weigh half of the real liquidity of the pair. Caveats I’d like your view on:

- The ±5% band is symmetric, so a pool 5% _worse_ is admitted too, and since weights follow depth, a deep pool that is 5% worse can take a large share.
- `balanceOf` is still used elsewhere, for a _physical_ question (can this pool pay this much?): concentrated legs are clamped to 30% of the pool’s output-token balance (`MAX_CONC_DRAIN_BPS = 3000`, §10.4), because there a donation really does raise what the pool can pay. A single-tick CL formula on a thin pool once promised 117x more than the pool ever held.
- For a long time our docs said “median” while the code used the single deepest pool’s rate (breakdown point zero); fixed on 2026-08-21.

Questions for the searchers/builders here: (a) is depth measured from reserves/active liquidity still gameable by just-in-time liquidity added within the same block? (b) would you weight by depth, or by simulated output at the actual trade size? (c) any known donation or skim variants that move reserve-based depth?

Sources: BlazePhoenix whitepaper v2.3.0 ([https://blazephoenix.xyz/docs/BlazePhoenix\_Whitepaper.pdf](https://blazephoenix.xyz/docs/BlazePhoenix_Whitepaper.pdf), text CC BY 4.0) and [The two-tier capacity clamp: refusing trades a pool cannot absorb](https://blazephoenix.xyz/learn/capacity-clamp/?utm_source=flashbots&utm_medium=article&utm_campaign=depth-weighted-median)
