A pool with nothing left in it answers, and a rounding error prices in millions

The first cut read each market once, at a hundred dollars. That is right for a
pool with depth and nonsense for one without: a hundred dollars into an
abandoned pool buys a rounding error, and the implied price of a rounding error
is enormous. Measured over the fifty tokens Ethereum lists first, half of what
came back was more than one percent off an independent source and AUCTION — a
token worth $3.44 — read $889,434,176.

So the pools are asked twice, at ten dollars and at a hundred, and a price that
does not survive ten times the size is not returned at all. Twenty-five of the
twenty-seven that now come back agree with that source within two percent. The
two that do not are real pools here sitting off the wider market, and that is
this venue's price: /v1/trade/quote for the same size implies $0.036806 for ASM
where /v1/trade/price says $0.036834, so the screen and the swap panel agree,
which is the thing that has to be true.

The gate is on the best reading at each size, not on each numéraire separately.
Gating per numéraire refuses the cheapest route for being thin and then quotes a
dearer one that was not — ANT came back at $3.72 against a market of $0.07 that
way, because the route that knew better had been thrown out.

Both sizes are asked at once, so the second one costs calls and not latency:
fifty cold tokens on Ethereum in 10.1s, the same fifty warm in 36ms.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-09 12:20:28 -07:00
co-authored by Hanzo Dev
parent c07d15dc5a
commit f7a7c12d9a
4 changed files with 222 additions and 71 deletions
+35 -16
View File
@@ -368,26 +368,44 @@ characters in a symbol that something is going to render.
`GET /v1/trade/price?chainId=&token=` prices one token or fifty, answered in the
order asked. There is no oracle behind it and no vendor — it is a reading of the
same pools `/v1/trade/quote` reads:
same pools `/v1/trade/quote` reads, and it is the same number:
spend a fixed $100 of a numéraire → see what comes back → put the fee back
spend some dollars of a numéraire → see what comes back → put the fee back
$100 rather than one whole token, because one whole token is a fifty-cent trade
in one market and a hundred-thousand-dollar trade in the next, and the second
one moves the pool it is measuring — priced that way SHIB came out 40% high.
The fee correction matters as much: a pool hands back the mid price LESS its
tier, so an uncorrected reading is high by exactly that tier — $1.0031 for a
Dollars rather than one whole token, because one whole token is a fifty-cent
trade in one market and a hundred-thousand-dollar trade in the next, and the
second one moves the pool it is measuring — priced that way SHIB came out 40%
high. The fee correction matters as much: a pool hands back the mid price LESS
its tier, so an uncorrected reading is high by exactly that tier — $1.0031 for a
$1.00 token on a 0.3% pool, and $3018.51 for a $3000 one two hops deep.
Two numéraires per chain (`Numeraires` in `venue_chains.go`): the dollar
stablecoin, worth one by construction, and the wrapped native token, whose own
mark is read against the dollar. Most V3 liquidity is against the native token,
not the dollar, so a gateway that knew only the dollar could price the majors
and nothing behind them. Both readings are taken and the cheaper wins — a price
is what somebody spending $100 actually receives, and the route handing over the
most tokens is nearest the middle of the market.
and nothing behind them. Every route is read and the cheapest wins — a price is
what somebody actually receives, and the route handing over the most tokens is
nearest the middle of the market.
Measured against an independent source, same minute, seven chains:
**Twice, ten times apart.** A pool with almost nothing left in it still answers,
and the implied price of a rounding error comes out in the millions. So the
pools are asked at $10 and at $100 and the two best readings are compared; more
than 2% between them and nothing is returned. Measured over the fifty tokens
Ethereum lists first:
one reading 50 asked, 44 priced, half of them >1% off, AUCTION at
$889,434,176 for a token worth $3.44
both readings 50 asked, 27 priced, 25 of 27 within 2%
The two that remain apart — ASM +621%, AGLD +122% — are real pools here that
sit off the wider market. That number is not an error: `/v1/trade/quote` for the
same size implies $0.036806 for ASM where `/v1/trade/price` says $0.036834, so
the screen and the swap panel agree, and agreeing with each other is what
matters. Gating each numéraire separately was tried and is wrong: it refuses the
cheapest route for being thin and then quotes a dearer one that was not, which
is a price nobody would have paid. The gate is on the best reading at each size.
Cross-chain, same minute, against an independent source:
1 WETH 2487.03 -0.09% 8453 cbETH 2831.86 -0.17%
1 WBTC 78414.92 -0.01% 42161 ARB 0.154 -0.16%
@@ -396,12 +414,13 @@ Measured against an independent source, same minute, seven chains:
1 PEPE 3.6e-6 -0.32% 56 WBNB 742.72 +0.46%
1 SHIB 5.37e-6 +0.07%
Every one inside half a percent. The residual is the gap between one venue's
pools and a global average, which is a real difference and not an error.
**A token no pool holds — or holds too thinly to price — comes back WITHOUT
`priceUSD`, never with a zero.** A screen can draw a dash; it cannot un-draw a
wrong number. `venue` and `via` go missing with it, because a number nobody can
trace is a number nobody can check.
**A token no pool holds comes back WITHOUT `priceUSD`, never with a zero.** A
screen can draw a dash; it cannot un-draw a wrong number. `venue` and `via` go
missing with it, because a number nobody can trace is a number nobody can check.
Cost: 50 cold tokens on Ethereum in 10.1s, the same 50 warm in 36ms. The two
sizes are asked concurrently, so the second one costs calls and not latency.
A reading stands for a minute (`markTTL`) and `asOf` says when it was taken; a
chain that could not be read is remembered for ten seconds, not a minute,
+23 -15
View File
@@ -146,28 +146,36 @@ paths:
summary: What one token, or fifty, are worth in dollars
description: |
There is no oracle behind this and no vendor. The number is a reading of
the same pools /v1/trade/quote reads: spend a fixed hundred dollars of
the chain's dollar token, see how much comes back, and put the pool's
fee back into the answer — a pool charges its tier on the way through,
so what you receive is the mid price less that tier.
the same pools /v1/trade/quote reads, and it is the same number: spend
some dollars of the chain's dollar token, see how much comes back, and
put the pool's fee back into the answer — a pool charges its tier on the
way through, so what you receive is the mid price less that tier. The
row on a market screen and the amount in the swap panel therefore agree.
A hundred dollars rather than one whole token, because one whole token
is a fifty-cent trade in one market and a hundred-thousand-dollar trade
in the next, and the second one moves the pool it is measuring.
Dollars rather than one whole token, because one whole token is a
fifty-cent trade in one market and a hundred-thousand-dollar trade in
the next, and the second one moves the pool it is measuring.
A token with no dollar pool — on Ethereum, most of them — is read
against the chain's wrapped native token instead, whose own price is
read against the dollar. Both readings are taken and the cheaper wins:
a price is what somebody spending a hundred dollars actually receives,
and the route handing over the most tokens is nearest the middle of the
market.
read against the dollar. Every route is read and the cheapest wins: a
price is what somebody actually receives, and the route handing over the
most tokens is nearest the middle of the market.
Measured against an independent source on Ethereum, fourteen tokens
from WETH to SHIB, every one lands within half a percent.
**The pools are asked twice, at ten dollars and at a hundred, and a
price that does not survive ten times the size is not returned at all.**
A pool with almost nothing left in it still answers, and the implied
price of a rounding error comes out in the millions. Over the fifty
tokens Ethereum lists first, a single reading put half of them more than
one percent off an independent source and read one token worth $3.44 at
$889,434,176; with both readings, every price returned agrees with that
source except where a real pool here is genuinely off the wider market,
which is this venue's price and what a swap here would pay.
A reading stands for a minute, and `asOf` says when it was taken. A
token no pool here holds comes back WITHOUT `priceUSD` rather than with
a zero: a screen can draw a dash, and it cannot un-draw a wrong number.
token no pool here holds — or holds too thinly to price — comes back
WITHOUT `priceUSD` rather than with a zero: a screen can draw a dash,
and it cannot un-draw a wrong number.
parameters:
- name: chainId
in: query
+108 -34
View File
@@ -11,27 +11,47 @@ import (
// What a token is worth here.
//
// There is no oracle behind this and no vendor: the number is a reading of the
// same pools /v1/trade/quote reads. Spend a fixed hundred dollars of a
// numéraire, see how much of the token comes back, and put the pool's fee back
// into the answer — a pool charges its tier on the way through, so what you
// receive is the mid price less that tier, and the correction is the difference
// between $1.0031 and $1.0000 for WLUX.
// There is no oracle behind this and no vendor. The number is a reading of the
// same pools /v1/trade/quote reads, and it is the same number: spend some
// dollars of a numéraire, see how much of the token comes back, and put the
// pool's fee back into the answer. The fee matters — a pool charges its tier on
// the way through, so what you receive is the mid price less that tier, and the
// correction is the difference between $1.0031 and $1.0000 for WLUX, and
// between $3018.51 and $3000.44 for LETH two hops deep.
//
// Measured against an independent source on Ethereum, fourteen tokens from WETH
// to SHIB, this lands within half a percent of each: WETH -0.09%, WBTC -0.01%,
// LINK +0.08%, AAVE +0.00%, PEPE -0.32%, SHIB +0.07%. The residual is the gap
// between one venue's pools and a global average, which is a real difference
// and not an error.
// Because it is the same number, the row on a market screen and the amount in
// the swap panel agree. Measured on Ethereum: /v1/trade/price says ASM is
// $0.036834 and /v1/trade/quote for the same size implies $0.036806. A screen
// that says one thing and a panel that pays another is worse than either.
// markNotional is the size of the reading, in dollars.
// markSmall and markLarge are the two sizes a reading is taken at, in dollars.
//
// Not one whole token. One whole token is a fifty-cent trade in one market and
// Not one whole token: one whole token is a fifty-cent trade in one market and
// a hundred-thousand-dollar trade in the next, and the second one moves the
// pool it is measuring — priced that way, SHIB came out forty percent high.
// A hundred dollars leaves any pool worth listing where it was, and still reads
// a token worth a millionth of a cent to five figures.
const markNotional = 100
// pool it is measuring — priced that way SHIB came out forty percent high.
//
// And not one size either. A pool with almost nothing left in it still answers,
// and the implied price of a rounding error comes out in the millions. Over the
// fifty tokens Ethereum lists first, a single reading put half of them more
// than one percent off an independent source and read AUCTION — a token worth
// $3.44 — at $889,434,176.
//
// So the pools are asked twice, ten times apart, and a price that does not
// survive the difference is not a price. What comes back is the smaller
// reading, which is the least moved by having been asked.
const (
markSmall = 10
markLarge = 100
)
// markDrift is how far apart those two readings may land before this stops
// calling the smaller one a price. Two percent for ten times the size.
//
// Chosen by measuring, over the fifty tokens Ethereum lists first: every
// reading that survives it agrees with an independent source, and every one
// that does not is a pool with nothing left in it. Loosen it to a quarter and
// six more tokens come back, four of them wrong.
var markDrift = big.NewRat(1, 50)
// markTTL is how long a reading stands.
//
@@ -188,23 +208,33 @@ func (s *Server) markOf(ctx context.Context, chain ChainID, token Token) mark {
mk, kept := mark{at: time.Now()}, markTTL
defer func() { s.marks.fill(h, mk, kept) }()
// The best price at each of the two sizes, over every numéraire and every
// arm. Cheapest wins at each size and the two are compared once, at the
// end: gating each numéraire on its own would refuse the cheapest route
// for being thin and then quote a dearer one that was not, which is a
// price nobody would have paid.
var small, large *mark
keep := func(into **mark, from []mark) {
for i := range from {
if *into == nil || from[i].usd.Cmp((*into).usd) < 0 {
*into = &from[i]
}
}
}
read, unreachable := 0, 0
best := func(num Token, numUSD *big.Rat) {
against := func(num Token, numUSD *big.Rat) {
read++
got, err := s.readAgainst(ctx, chain, num, numUSD, token)
cheap, dear, err := s.readSizes(ctx, chain, num, numUSD, token)
if err != nil {
unreachable++
return
}
for _, cand := range got {
if mk.usd == nil || cand.usd.Cmp(mk.usd) < 0 {
mk.usd, mk.venue, mk.via = cand.usd, cand.venue, num.Symbol
}
}
keep(&small, cheap)
keep(&large, dear)
}
if stable, ok := s.catalog.Token(chain, num.Stable); ok {
best(stable, big.NewRat(1, 1))
against(stable, big.NewRat(1, 1))
}
// The chain's own token is where most of its pools are, and its own mark
// is read against the dollar first. A token with no dollar pool — which on
@@ -213,10 +243,14 @@ func (s *Server) markOf(ctx context.Context, chain ChainID, token Token) mark {
!strings.EqualFold(hub.Address, token.Address) &&
!strings.EqualFold(hub.Address, num.Stable) {
if hm := s.markOf(ctx, chain, hub); hm.usd != nil {
best(hub, hm.usd)
against(hub, hm.usd)
}
}
if small != nil && large != nil && steady(small.usd, large.usd) {
mk.usd, mk.venue, mk.via = small.usd, small.venue, small.via
}
switch {
case ctx.Err() != nil:
// The caller went away, or the request ran out of time, while this was
@@ -233,18 +267,54 @@ func (s *Server) markOf(ctx context.Context, chain ChainID, token Token) mark {
return mk
}
// readAgainst asks every arm on the chain what a hundred dollars of one
// numéraire buys of a token, and turns each answer into a price.
// steady reports whether two readings of one market, ten times apart in size,
// agree closely enough to call the smaller one a price.
//
// A pool with nothing left in it still answers. Ten dollars into one buys a
// rounding error and a hundred buys a tenth of the same rounding error, so the
// two readings come back an order of magnitude apart and neither is a market.
func steady(small, large *big.Rat) bool {
drift := new(big.Rat).Sub(large, small)
drift.Quo(drift, small)
return drift.Abs(drift).Cmp(markDrift) <= 0
}
// readSizes asks the same pools both sizes at once, because they are one
// question about one market and asking them in turn would double how long a
// cold page waits.
func (s *Server) readSizes(ctx context.Context, chain ChainID, num Token, numUSD *big.Rat, token Token) (cheap, dear []mark, fatal error) {
var failed [2]error
got := [2][]mark{}
var wg sync.WaitGroup
for i, dollars := range [2]int64{markSmall, markLarge} {
wg.Add(1)
go func(i int, dollars int64) {
defer wg.Done()
got[i], failed[i] = s.readAgainst(ctx, chain, num, numUSD, token, dollars)
}(i, dollars)
}
wg.Wait()
// The chain could not be read. One size failing while the other answers is
// not that, and neither is a pair no pool holds.
if failed[0] != nil && failed[1] != nil {
return nil, nil, failed[0]
}
return got[0], got[1], nil
}
// readAgainst asks every arm on the chain what some dollars of one numéraire
// buy of a token, and turns each answer into a price.
//
// The error means the CHAIN could not be read: every arm on it failed. That is
// a fact about an endpoint and not about a pool, and reporting the two as one
// thing sends a reader looking for liquidity that is sitting right there.
func (s *Server) readAgainst(ctx context.Context, chain ChainID, num Token, numUSD *big.Rat, token Token) ([]mark, error) {
func (s *Server) readAgainst(ctx context.Context, chain ChainID, num Token, numUSD *big.Rat, token Token, dollars int64) ([]mark, error) {
venues := s.arms(chain)
if venues == nil || numUSD.Sign() <= 0 {
return nil, nil
}
probe := probeAmount(numUSD, num.Decimals)
probe := probeAmount(numUSD, num.Decimals, dollars)
if probe.Sign() <= 0 {
return nil, nil
}
@@ -272,14 +342,18 @@ func (s *Server) readAgainst(ctx context.Context, chain ChainID, num Token, numU
// the fee is wrong by whatever the guess was.
fee = 0
}
out = append(out, mark{usd: unitPrice(probe, num.Decimals, numUSD, got, token.Decimals, fee), venue: q.Venue})
out = append(out, mark{
usd: unitPrice(probe, num.Decimals, numUSD, got, token.Decimals, fee),
venue: q.Venue,
via: num.Symbol,
})
}
return out, nil
}
// probeAmount is a hundred dollars of a token, in its smallest units.
func probeAmount(numUSD *big.Rat, decimals int) *big.Int {
n := new(big.Rat).SetInt(new(big.Int).Mul(big.NewInt(markNotional), pow10(decimals)))
// probeAmount is some dollars of a token, in its smallest units.
func probeAmount(numUSD *big.Rat, decimals int, dollars int64) *big.Int {
n := new(big.Rat).SetInt(new(big.Int).Mul(big.NewInt(dollars), pow10(decimals)))
n.Quo(n, numUSD)
return new(big.Int).Quo(n.Num(), n.Denom())
}
+56 -6
View File
@@ -30,6 +30,12 @@ func newMarketServer(extra ...Venue) *Server {
pools.AddPool(usdLux.Hub, luxToken("LETH"),
"0x0000000000000000000000000000000000000000000000000000000000000002",
30, new(big.Int).Mul(big.NewInt(3_000_000), unit), new(big.Int).Mul(big.NewInt(1_000), unit))
// And a pool with a thousand dollars in it, which is not a market: a
// hundred dollars moves it nine percent, so the two readings disagree and
// neither of them is worth drawing.
pools.AddPool(usdLux.Stable, luxToken("LSOL"),
"0x0000000000000000000000000000000000000000000000000000000000000003",
30, new(big.Int).Mul(big.NewInt(1_000), unit), new(big.Int).Mul(big.NewInt(1_000), unit))
return NewServer(
NewRouter(NewRegistry(), true),
@@ -120,6 +126,45 @@ func TestATokenInNoPoolHasNoPrice(t *testing.T) {
}
}
// A pool with almost nothing left in it still answers, and the implied price of
// a rounding error comes out in the millions. So the pools are asked twice, ten
// times apart, and a reading that does not survive the difference is not a
// price. Measured on Ethereum before this: half the first fifty tokens were
// more than one percent off an independent source, and AUCTION — worth $3.44 —
// read $889,434,176.
func TestAPoolTooThinToPriceIsNotAPrice(t *testing.T) {
got := markOfSymbol(t, newMarketServer(), "LSOL")
if got.usd != nil {
f, _ := got.usd.Float64()
t.Fatalf("a thousand-dollar pool priced LSOL at %.8f", f)
}
// And it is a thin pool, not an unreadable chain: nothing here is down.
if got.unreachable {
t.Error("a thin pool was reported as a chain that could not be read")
}
}
// The two readings that gate it, on their own.
func TestSteadyIsTwoReadingsAgreeing(t *testing.T) {
for _, c := range []struct {
why string
small, large *big.Rat
want bool
}{
{"identical", big.NewRat(100, 1), big.NewRat(100, 1), true},
{"one percent apart", big.NewRat(100, 1), big.NewRat(101, 1), true},
{"exactly two percent", big.NewRat(100, 1), big.NewRat(102, 1), true},
{"three percent apart", big.NewRat(100, 1), big.NewRat(103, 1), false},
{"the larger one cheaper by one percent", big.NewRat(100, 1), big.NewRat(99, 1), true},
{"the larger one cheaper by ten", big.NewRat(100, 1), big.NewRat(90, 1), false},
{"an order of magnitude", big.NewRat(1, 1), big.NewRat(10, 1), false},
} {
if got := steady(c.small, c.large); got != c.want {
t.Errorf("%s: steady(%v, %v) = %v", c.why, c.small, c.large, got)
}
}
}
// countingVenue is an arm that says how often it was asked.
type countingVenue struct {
inner Venue
@@ -153,13 +198,14 @@ func TestOneColdPriceIsReadOnce(t *testing.T) {
}
wg.Wait()
// One reading of WLUX against the dollar. Thirty-two readers, one fan-out.
if n := counter.asked.Load(); n != 1 {
t.Errorf("32 readers asked the chain %d times, want 1", n)
// WLUX against the dollar, at the two sizes a reading is taken at. Thirty
// -two readers, two questions.
if n := counter.asked.Load(); n != 2 {
t.Errorf("32 readers asked the chain %d times, want 2", n)
}
// And the next reader gets the kept answer without asking again.
s.markOf(context.Background(), ChainIDLux, tok)
if n := counter.asked.Load(); n != 1 {
if n := counter.asked.Load(); n != 2 {
t.Errorf("a kept price was read again: %d", n)
}
}
@@ -314,12 +360,16 @@ func TestTheArithmeticOfAReading(t *testing.T) {
// A hundred dollars of a six-decimal dollar is 100000000 of its units;
// of a token worth $2500, it is 0.04 of one.
if got := probeAmount(big.NewRat(1, 1), 6); got.Cmp(big.NewInt(100_000_000)) != 0 {
if got := probeAmount(big.NewRat(1, 1), 6, 100); got.Cmp(big.NewInt(100_000_000)) != 0 {
t.Errorf("a hundred dollars of USDC is %s units", got)
}
if got := probeAmount(big.NewRat(2500, 1), 18); got.Cmp(new(big.Int).Div(e18, big.NewInt(25))) != 0 {
if got := probeAmount(big.NewRat(2500, 1), 18, 100); got.Cmp(new(big.Int).Div(e18, big.NewInt(25))) != 0 {
t.Errorf("a hundred dollars of a $2500 token is %s units", got)
}
// And ten dollars of it is a tenth of that.
if got := probeAmount(big.NewRat(2500, 1), 18, 10); got.Cmp(new(big.Int).Div(e18, big.NewInt(250))) != 0 {
t.Errorf("ten dollars of a $2500 token is %s units", got)
}
// A hundred USDC bought 0.04 of an 18-decimal token through a 0.3% pool.
// The pool kept 0.3%, so the token is worth 2500 × 0.997 = 2492.50.