Conversation
Arc's WETH/USDC 1% pool — empty at launch, so the chain shipped with WETH honestly unpriceable (#793) — woke up holding about $18 of USDC. One whole WETH in drains it, so `usdPerToken` answered $18.25 per WETH against the same pool's ~$2,485 marginal price. That number values holdings, sizes "$50 of X" asks, and is the figure a spend cap and a policy gate are checked against, and it is wrong in the direction that makes a real position look like pocket change. The fence is general, not an Arc-WETH denylist: every v3 fee tier (and the v4 fallback) is quoted at the probe size AND half of it, and trusted only when `out(full)` is within MAX_PROBE_DECAY_BPS of `2 × out(half)`. The fee rate cancels in that ratio, so what is left is pure slippage — how much of the book the probe ate. Constant-product arithmetic maps 5% decay to a price within ~10% of the pool's marginal price, the same band lib/stock-tape holds 4663 stock fills to. A tier that fails is dropped as if it had no pool; when none survives the probe returns null and callers ask for a token amount instead (they already fail closed — an unpriced leg refuses under a spend policy rather than bypassing it, and never reads as $0). Judged per TIER, not per token: a thin tier that quoted HIGH would otherwise win the max() below. Base's cbBTC 0.01%, Ethereum's WBTC 0.01%/1%, Arbitrum's LINK 0.01%/0.05% and Optimism's OP 0.01%/0.05% tiers are all drained today next to healthy siblings. Measured live across every app chain 2026-09-18: every traded pool decays under 0.4% at one whole token, the thinnest legitimate tier seen was 3.88%, and every drained pool decays 23.8% or more. No price on any chain changed. Harness: the old pin asserted today's market state ("WETH is unpriceable") and so went red on a market move. It is replaced by two pins that assert the RULE — one pure (a drained pool is refused, an at-the-fence pool passes and one bp over does not, a sub-resolution quote steps aside rather than accusing a healthy pool, negative decay clamps), and one live that derives the invariant from fresh 1/1000-size quotes WITHOUT the fence's own arithmetic: every price returned is within 15% of the pool's marginal price, and every refusal is earned. A puddle that fills up flips the answer without flipping the pin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Gates —
|
| mutation | pin |
|---|---|
trusted: decayBps <= MAX_PROBE_DECAY_BPS → trusted: true |
RED |
drop the Math.max(0, decay) clamp |
RED |
drop the half < MIN_DEPTH_QUOTE_UNITS precision floor |
RED |
And the live pin, which deliberately does not call judgePoolDepth, catches a fence that stops fencing:
unmutated [{"sym":"WETH","got":null, "bestMarginal":2476.14,"ok":true}, …] GREEN
fence off [{"sym":"WETH","got":18.2516, "bestMarginal":2476.14,"ok":false}, …] RED
(My first draft of that pin re-derived expected through judgePoolDepth and stayed GREEN under the same mutation — both sides moved together. That's why it now reads the marginal price from fresh 1/1000-size quotes instead.)
Live before/after, every app chain
Read-only, on a prod build. One price changed:
Arc WETH $18.2516 → null (unpriceable) ← the bug
Arc BTC $77,868 → $77,868 (0.3% tier dropped, 0.01% tier already won)
Arc EURC $1.1467 → $1.1467
Base ETH $2,509.71 → $2,509.65 Base cbBTC $77,998 → $77,987
Base DEGEN $0.001012 → $0.001012 Base USDT $0.9991 → $0.9991
Ethereum ETH $2,511.61 → $2,510.26 Ethereum WBTC $77,781 → $77,781
Ethereum SHIB $0.000005 → $0.000005 ← under the precision floor, steps aside
Arbitrum LINK $11.8146 → $11.8146 Optimism OP $0.1104 → $0.1102
4663 AAPL $336.00 → $336.00 (tape, never a pool — unchanged path)
Deltas are tick drift between reads, not the fence. Every dropped tier logs why:
[usd-probe] CBBTC/USDC on Base: dropping the 0.01% tier — one whole token decays
50.0% (fence 5%), so its $104.92 is the pool being drained, not a price.
Cost: the quoter scan goes from 4 calls to 8 per chain, in parallel — 263ms → 331ms on the slowest path measured.
Nate — the Arc pin that went red today was not a code regression, and it was not really about Arc either. It caught a hole that had been open on every chain since the probe was written.
What happened
Arc's WETH/USDC 1% pool was empty when Arc shipped, which is why
#793pinned WETH there as honestly unpriceable. It has since woken up holding about $18 of USDC in total. One whole WETH in drains it, so the quoter answered:usdPerTokenquotes exactly one whole token, takes the best amount out across fee tiers, and returns it. So it handed $18.25 per WETH to everything downstream. That number values holdings, sizes"$50 of X"asks, and is the figure a spend cap and a policy gate are checked against — wrong by two orders of magnitude, in the direction that makes a real position look like pocket change.The fix: a depth fence, not an Arc-WETH denylist
I went with the general option in the brief. Every v3 fee tier — and the v4 fallback — is now quoted at the probe size and half of it, and trusted only when
out(full)is withinMAX_PROBE_DECAY_BPSof2 × out(half).Two properties make this the right measure:
x = probe ÷ reserve, constant-product givesdecay = 1 − (1 + x/2)/(1 + x), and the average price understates the marginal price by1/(1 + x). So 5% decay ⟺ a price within ~10% of the pool's marginal price — the same 10% bandlib/stock-tapeholds 4663 stock fills to.A tier that fails is dropped as if it had no pool. When no tier survives, the probe returns
nulland callers ask for a token amount. Failing closed, as the brief asked.Why 5%
Measured live across every app chain, 2026-09-18 (decay at one whole token, per tier; bold = the tier that wins the max):
Every pool that is really traded decays under 0.4%. The thinnest legitimate tier anywhere was 3.88% (and it is not a winner). Every drained pool decays 23.76% or more — 50% in the constant-product limit, which is where Arc's WETH pool sits. 5% is the middle of that gap with room on both sides, and the harness pins that the constant stays inside it.
Judged per tier, not per token
This matters more than the Arc case. Today the probe takes
max(amountOut), so a thin tier is usually harmless — a drained pool cannot out-quote a deep one. But it only takes a thin pool that is mispriced upward formax()to pick it, and drained sibling tiers are everywhere: Base cbBTC 0.01% and 1%, Ethereum WBTC 0.01% and 1%, Arbitrum LINK 0.01% and 0.05%, Optimism OP 0.01% and 0.05%. Each tier now carries its own verdict, and a dropped one logs why:No price on any chain changed. Every winner above was already the winner; the fence only removes tiers
max()was ignoring anyway — except on Arc WETH, where the only tier was the bad one.The precision guard
SHIB's half-token quote is 2 raw units of USDC, and
1 − 5/(2×2)reads as −25% "decay" on a perfectly healthy pool. So belowMIN_DEPTH_QUOTE_UNITS(100 units = $0.0001 of a 6-decimal stable) the measure is rounding noise and the check steps aside rather than accusing a healthy pool. That covers every token worth more than ~$0.0002 a whole token — DEGEN at $0.001 clears it by 5× — and steps aside only where a whole token is worth less than the quoter can resolve, which is also where a thin pool cannot hide a real position. Negative decay clamps to zero: only a price that collapses with size is evidence of thinness.Item 3 — what else reads Arc WETH, and does a refusal read as $0?
Grepped every
usdPerTokencaller. Nothing coerces a null probe to $0, and every path names the refusal:$N of Xswap sizinglib/transfer-exec(spend cap)valueUsd = null→policyCheckblocks: "Spend policy is ON but this action has no priceable leg — refusing rather than bypassing your caps"lib/funding-planlib/spot-guard-execlib/dca-auto-execlib/lifi-bridgegas leggasLeg: false, so it never runs; the branch degrades to a warn anywaylib/wallet-view/api/markets/routesSo the fence makes every one of these more honest, not silently zero.
Re-pinning: the rule, not the market
The old pin said "WETH is unpriceable", which is a fact about a pool on one day. Two pins replace it.
1. Pure — the fence's arithmetic. A drained pool (~50%) is refused; an exactly-at-the-fence pool passes and one bp over does not; a sub-resolution quote steps aside; negative decay clamps; a dead pool is refused; and
MAX_PROBE_DECAY_BPSmust sit inside the measured gap (400 < it < 2300). Mutation-tested: removing the threshold, removing the clamp, or removing the precision floor each turns it red.2. Live — the invariant, derived without the fence. For each Arc token it takes fresh 1/1000-size quotes per tier and asserts: every price the probe returns is within 15% of the pool's own marginal price, and every refusal is earned — no tier was healthy enough to price. It deliberately does not call
judgePoolDepth; a pin that re-used the fence's own arithmetic would move with it under a mutation (I checked — it did, which is why I rewrote it this way). Neutering the fence turns it red:Which tiers are thin today is market state and is not asserted anywhere. A puddle that fills up flips the answer without flipping the pin.
Calls for you
buildUniswapSwapstill takesmax(amountOut)across tiers with no depth check; on 4663 stockscheckFillAgainstTape(Stock swaps on Robinhood Chain are checked against the tape #796) covers it, and everywhere else max-out naturally routes away from thin pools at real trade sizes (a drained pool cannot out-quote a deep one for $50). The probe was exposed precisely because it always quotes a whole token. I've filed a task chip rather than widen this PR.Gates
npx tsc --noEmitcleannpm run buildcleannpm run test:api— see the comment below (run alone, againstnext starton this branch)$18.25 → null; Base/Ethereum/Arbitrum/Optimism/Arc/4663 prices unchanged to 3 decimal places.No DDL, no env, no new dependency.
🤖 Generated with Claude Code