mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
1000 ZLUX -> 900,000 LUX
1000 LZOO -> 47,142,857 ZOO
1000 LBTC -> 64,267,132 cbBTC
1000 LUX -> 1.089 ZLUX
measured against production. Every one should be 990. This is not a display
bug: on an exit, handlerUtilaPayoutAction sets payoutAmount from the stored
quote's receive_amount and bridgeMints exactly it, so the first three rows are
an unbacked mint and the fourth silently destroys 99.9% of the user's balance.
The quote was `amount * sourcePrice / destinationPrice`, priced per request off
Coinbase, and getTokenPrice answered 1 on any failure. Coinbase lists 29 of the
38 symbols the bridge asks about; the other 9 threw, so a third of lookups
returned an invented $1.00 that is indistinguishable from a real dollar peg.
A pair with one real price and one invented one gives a ratio that inverts when
you swap the legs — which is why the same route was wrong in opposite
directions, and why that looked like two bugs instead of one.
The formula was the mistake, not the feed. Take the connected components of the
pair table and all 33 are one asset wearing three names — {BTC, LBTC, WBTC,
ZBTC, cbBTC}, {ETH, LETH, WETH, ZETH}, {LUX, ZLUX}. There is no edge between
two different assets anywhere in it, so the rate is 1, exactly, both ways, and
only the fee comes off. No price is consulted, which retires the whole class:
an outage, a delisting, a rate-limit or a renamed ticker now change nothing
here, because nothing here asks. The executor already agreed — its entry path
is 1:1 and its fallback is amount*(1-fee); only the quote disputed it.
One `convert` in quote.ts; getQuote, getRate and the stored swap row project
from it. Three copies of the arithmetic could not promise that a swap settles
at the number the user was shown.
Also, because they were the same defect wearing other masks:
- Quoting an unroutable pair returned a confident number. `convert` refuses,
and the routes answer 400 rather than inventing a trade the bridge cannot
make.
- getTokenPrice returns undefined, not 1, when nobody quotes an asset. It is
display-only now, but a caller still has to be able to tell a price from the
absence of one.
- min_receive_amount advertised a 2.5% slippage band on an exact wrap, asking
the user to consent to less than the bridge can ever pay.
The peg classes are pinned in the test rather than derived: recovering them
means stripping an L/Z/W/cb prefix and comparing the remainder, and inferring
an asset's identity from the spelling of its ticker is the exact reasoning that
caused this (it also reads LUX as a wrapped "UX").
Co-authored-by: Hanzo Dev <[email protected]>