Files
zeekayandHanzo Dev 4e05024a06 Refuse a hop address, a foreign quote and a slippage outside a percentage
An adversarial pass over the calldata found three more ways a crafted quote
became a signable transaction.

A hop address was taken as given. An address is twenty bytes and the path packs
it against a uint24 with nothing marking the boundary, so a short one shifted
every byte after it — and the path still parsed, as a route through pools
nobody named. Width is the only thing separating the two, so both hop addresses
are now checked the way the swapper already was.

A quote says which chain priced it, and that was ignored. Building this chain's
router call from another chain's quote addresses pools by the same numbers on a
book that never quoted them, and these chains share deployment addresses, so it
is a real trade at a price nobody was shown.

Slippage is a percentage of an amount and lives between none of it and all of
it. Below zero the factor inverted and the bound grew a minus sign; above a
hundred it ran past a word. Clamping the percentage bounds both directions at
one place, where the alternative was clamping every product it reaches.

IsAddress is exported: whether a chain has an address to address a transaction
to is one question, and the process that mounts these handlers has to ask it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 17:30:19 -07:00

1107 lines
36 KiB
Go

package engine
import (
"context"
"encoding/json"
"fmt"
"math"
"math/big"
"net/http"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/luxfi/graph/storage"
"golang.org/x/crypto/sha3"
)
// What a swap would pay out, over the pools this process indexes.
//
// It was served by exchange-api, the last TypeScript service in front of this
// one. The shape below is that service's, because the swap form reads it today
// and a client is not something to break while moving a service.
//
// TWO QUESTIONS, TWO ANSWERERS
//
// A quote is really two questions, and they have different authorities:
//
// - "Which pools could carry this trade?" — the index answers. That is the
// whole reason the endpoint belongs in graphd: the book is already here,
// with every pool, its pair and its fee, so a route can be searched over
// the real graph instead of a hardcoded hub list. The service being retired
// could only try WLUX and LUSD as intermediaries; this tries every token
// the book says both sides connect to.
//
// - "What does that pool actually pay?" — the pool answers. For a V2 pair
// that is constant product over its two reserves, and it is computed here,
// exactly, in integers. For a V3 pool it is a walk across whatever ticks
// the trade crosses, and the pool's own quoter is the only thing holding
// the tick map, so that is what is asked.
//
// WHY V3 IS NOT COMPUTED HERE
//
// The tempting shortcut is to treat a V3 pool as constant-liquidity around its
// current price: slot0 gives sqrtPriceX96, liquidity() gives L, and one step of
// the standard formula gives an amount out. Measured against the pool on Lux
// mainnet that shortcut is exact while a trade stays inside the current tick and
// badly wrong the moment it does not — 15% high on CYRUS/LUSD, 44% on Z/LUSD,
// 89% on TRUMP/LUSD, always in the direction of promising output the pool cannot
// pay. Those are the pools whose liquidity is parked away from spot, which is
// most of a thin book. Closing that gap means the tick map, and the tick map is
// not indexed here: the Mint/Burn arm of processLog is deliberately inert
// because reserves are state, not history (see valuation.go's header). So the
// choice is between a number that is wrong by a factor and the pool's own
// arithmetic. The pool's own arithmetic is right.
//
// STALENESS
//
// Every amount below is read at head, per request, not from the indexed
// aggregates — those are recomputed on a 30s pass, and a quote a person is
// about to sign should not be half a minute old. blockNumber says which block
// the answer came from.
const (
// nativeSentinel is the zero address standing for the chain's own coin. It
// routes as the wrapped token and echoes back to the caller unwrapped.
nativeSentinel = "0x0000000000000000000000000000000000000000"
// marginalRef is the reference trade the price impact is measured against:
// small enough that it moves no price, so it reports the rate a trade would
// get if it were free of its own weight. Impact is the gap between that and
// what the real size gets.
marginalRef = 1_000_000_000_000_000 // 1e15 base units
// v2SwapGas is what a constant-product swap costs to execute. Unlike a V3
// leg, whose quoter measures the walk it just did, a V2 pair has nothing to
// ask — so this is a figure for the transfer-and-settle a pair does, not a
// reading. It exists because the field is an estimate and a route that
// reported nothing would read as free.
v2SwapGas = 104_000
// maxRoutes bounds the search. The book on Lux mainnet is 32 pools and every
// candidate is priced with a live call, so this is a guard against a chain
// with thousands of pools, not a view about how many routes are worth trying.
maxRoutes = 24
// quoteBudget bounds one request's conversation with the node.
quoteBudget = 20 * time.Second
)
// selector is the first four bytes of a function signature's hash — how the EVM
// names a method. Deriving them from the signatures keeps the ABI readable
// rather than a column of magic hex.
func selector(sig string) string {
h := sha3.NewLegacyKeccak256()
h.Write([]byte(sig))
return "0x" + fmt.Sprintf("%x", h.Sum(nil)[:4])
}
var (
selQuoteExactIn = selector("quoteExactInputSingle((address,address,uint256,uint24,uint160))")
selQuoteExactOut = selector("quoteExactOutputSingle((address,address,uint256,uint24,uint160))")
selGetReserves = selector("getReserves()")
selSlot0 = selector("slot0()")
selLiquidity = selector("liquidity()")
)
// ── the wire ──────────────────────────────────────────────────────────────
type quoteRequest struct {
TokenIn string `json:"tokenIn"`
TokenOut string `json:"tokenOut"`
Amount string `json:"amount"`
Type string `json:"type"`
Swapper string `json:"swapper"`
SlippageTolerance *float64 `json:"slippageTolerance"`
TokenInChainID *int64 `json:"tokenInChainId"`
TokenOutChainID *int64 `json:"tokenOutChainId"`
}
type quoteResponse struct {
RequestID string `json:"requestId"`
Routing string `json:"routing"`
PermitData any `json:"permitData"`
Quote classicQuote `json:"quote"`
}
type classicQuote struct {
ChainID int64 `json:"chainId"`
Swapper string `json:"swapper"`
Input quoteSide `json:"input"`
Output quoteSide `json:"output"`
TradeType string `json:"tradeType"`
Slippage float64 `json:"slippage"`
GasUseEstimate string `json:"gasUseEstimate"`
GasFee string `json:"gasFee"`
Route [][]hopJSON `json:"route"`
RouteString string `json:"routeString"`
QuoteID string `json:"quoteId"`
BlockNumber string `json:"blockNumber"`
PriceImpact float64 `json:"priceImpact"`
}
type quoteSide struct {
Token string `json:"token"`
Amount string `json:"amount"`
Recipient string `json:"recipient,omitempty"`
}
// hopJSON is one pool in a route. A V3 hop carries the pool's price state, a V2
// hop its two reserves; the swap form reads both shapes and keys on `type`.
type hopJSON struct {
Type string `json:"type"`
Address string `json:"address"`
TokenIn tokenJSON `json:"tokenIn"`
TokenOut tokenJSON `json:"tokenOut"`
SqrtRatioX96 string `json:"sqrtRatioX96,omitempty"`
Liquidity string `json:"liquidity,omitempty"`
TickCurrent string `json:"tickCurrent,omitempty"`
Fee string `json:"fee,omitempty"`
Reserve0 *reserveRef `json:"reserve0,omitempty"`
Reserve1 *reserveRef `json:"reserve1,omitempty"`
AmountIn string `json:"amountIn"`
AmountOut string `json:"amountOut"`
}
// tokenJSON renders decimals as text. The token list renders the same field as a
// number; both are exchange-api's, and the swap form parses each where it finds it.
type tokenJSON struct {
Address string `json:"address"`
ChainID int64 `json:"chainId"`
Symbol string `json:"symbol"`
Decimals string `json:"decimals"`
}
type reserveRef struct {
Token tokenJSON `json:"token"`
Quotient string `json:"quotient"`
}
// ── the book ──────────────────────────────────────────────────────────────
// pool is one indexed venue: its address, its pair, and the fee it charges.
type pool struct {
addr string // lower-cased
t0, t1 string // lower-cased
fee int64
venue string // "v2" or "v3", learned from the chain; empty until then
}
// other returns the far side of a pool from the token given, and whether the
// token is on it at all.
func (p *pool) other(token string) (string, bool) {
switch token {
case p.t0:
return p.t1, true
case p.t1:
return p.t0, true
}
return "", false
}
// leg is one pool crossed by a route, with whichever amount is known so far.
type leg struct {
p *pool
in, out string
amtIn *big.Int
amtOut *big.Int
gas int64
// what the pool looked like when it answered, echoed to the client
sqrtP, liq *big.Int
tick int64
r0, r1 *big.Int
// marginal is the rate a negligible trade would get through this pool,
// which is the baseline price impact is measured from. refIn is the size
// that rate was measured at, kept because the rate is a quotient and the
// divisor has to be the size actually asked about — for an exact output the
// leg's own input is not known until after the question has been sent.
marginal float64
refIn *big.Int
}
// route is an ordered chain of legs from the caller's token to the one wanted.
type route struct {
legs []*leg
}
func (r *route) amountIn() *big.Int { return r.legs[0].amtIn }
func (r *route) amountOut() *big.Int { return r.legs[len(r.legs)-1].amtOut }
func (r *route) gas() int64 {
var g int64
for _, l := range r.legs {
g += l.gas
}
return g
}
// priced reports whether every leg answered. A route with a silent leg is not a
// worse route, it is not a route.
func (r *route) priced() bool {
for _, l := range r.legs {
if l.amtIn == nil || l.amtOut == nil || l.amtIn.Sign() <= 0 || l.amtOut.Sign() <= 0 {
return false
}
}
return true
}
// ── the quoter ────────────────────────────────────────────────────────────
// Quoter prices a swap over the indexed book.
type Quoter struct {
store *storage.Store
chainID int64
node *node
quoterV2 string // the V3 periphery quoter; empty when the chain has no V3 venue
wrapped string // wrapped native, lower-cased
mu sync.Mutex
venues map[string]string // pool address → "v2"/"v3"; a pool's venue never changes
}
// NewQuoter builds the quote handler over the same store the GraphQL engine
// reads. Exported because the process that serves production is the explorer,
// not graphd, and a handler in package main is reachable by neither.
func NewQuoter(store *storage.Store, chainID int64, rpc, quoterV2, wrapped string) *Quoter {
return &Quoter{
store: store,
chainID: chainID,
node: dial(rpc),
quoterV2: strings.ToLower(strings.TrimSpace(quoterV2)),
wrapped: strings.ToLower(strings.TrimSpace(wrapped)),
venues: map[string]string{},
}
}
// HandleQuote is the swap form's price endpoint, over the same book.
func HandleQuote(q *Quoter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var req quoteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
badQuote(w, "body must be JSON")
return
}
q.serve(r.Context(), w, &req)
}
}
func badQuote(w http.ResponseWriter, detail string) {
writeJSON(w, http.StatusBadRequest, map[string]any{
"errorCode": "VALIDATION_ERROR",
"detail": detail,
})
}
func (q *Quoter) serve(ctx context.Context, w http.ResponseWriter, req *quoteRequest) {
// EXACT_INPUT is the default because it is the question the swap form asks
// almost always: here is what I have, what do I get.
tradeType := "EXACT_INPUT"
if req.Type == "EXACT_OUTPUT" {
tradeType = "EXACT_OUTPUT"
}
if !IsAddress(req.TokenIn) {
badQuote(w, "tokenIn must be an address")
return
}
if !IsAddress(req.TokenOut) {
badQuote(w, "tokenOut must be an address")
return
}
amount, ok := parseWei(req.Amount)
if !ok {
badQuote(w, "amount must be a decimal wei string")
return
}
if amount.Sign() <= 0 {
badQuote(w, "amount must be > 0")
return
}
// The caller may say which chain it believes it is trading on. This process
// indexes one chain, so a request naming another is a question it cannot
// answer rather than one it should answer wrongly.
for _, id := range []*int64{req.TokenInChainID, req.TokenOutChainID} {
if id != nil && *id != q.chainID {
writeJSON(w, http.StatusNotFound, map[string]any{
"errorCode": "QUOTE_ERROR",
"detail": "No quotes available",
})
return
}
}
swapper := nativeSentinel
if IsAddress(req.Swapper) {
swapper = checksumAddress(req.Swapper)
}
slippage := 0.5
if req.SlippageTolerance != nil {
slippage = *req.SlippageTolerance
}
// Native routes as wrapped: the pools hold the wrapped token, and a swap
// from the coin wraps on the way in.
in, out := q.unwrapNative(req.TokenIn), q.unwrapNative(req.TokenOut)
if in == out {
badQuote(w, "tokenIn and tokenOut resolve to the same token")
return
}
ctx, cancel := context.WithTimeout(ctx, quoteBudget)
defer cancel()
quote := noRoute(q.chainID, req, tradeType, swapper, slippage)
if best := q.best(ctx, in, out, amount, tradeType); best != nil {
head := q.decorate(ctx, best)
quote = q.render(req, best, tradeType, swapper, slippage, head)
}
writeJSON(w, http.StatusOK, quoteResponse{
RequestID: requestID(),
Routing: "CLASSIC",
Quote: quote,
})
}
// noRoute is the answer when nothing in the book can carry the trade.
//
// It is a 200 with an empty route rather than an error because that is what the
// swap form is built to read: it treats `route: []` as a quote that found
// nothing and shows no price, whereas a 404 makes it retry. The amounts are
// zero — there is no route, so there is no number, and zero here is the absence
// the empty route already states rather than a figure anyone should trade on.
func noRoute(chainID int64, req *quoteRequest, tradeType, swapper string, slippage float64) classicQuote {
return classicQuote{
ChainID: chainID,
Swapper: swapper,
Input: quoteSide{Token: echoToken(req.TokenIn), Amount: "0"},
Output: quoteSide{Token: echoToken(req.TokenOut), Amount: "0", Recipient: swapper},
TradeType: tradeType,
Slippage: slippage,
GasUseEstimate: "0",
GasFee: "0",
Route: [][]hopJSON{},
RouteString: "",
QuoteID: requestID(),
BlockNumber: "0",
PriceImpact: 0,
}
}
// ── routing ───────────────────────────────────────────────────────────────
// best searches the indexed book for the route that pays most (or, for an exact
// output, costs least) and returns it fully priced, or nil when none is.
func (q *Quoter) best(ctx context.Context, in, out string, amount *big.Int, tradeType string) *route {
pools := q.book()
if len(pools) == 0 {
return nil
}
if err := q.classify(ctx, pools); err != nil {
return nil
}
candidates := routes(pools, in, out)
if len(candidates) == 0 {
return nil
}
// Seed each candidate with the amount the caller fixed — the first leg's
// input for an exact input, the last leg's output for an exact output — and
// price outward from there. Two passes cover a two-hop route: one for the
// leg whose amount the caller gave, one for the leg the first pass fed.
exactIn := tradeType == "EXACT_INPUT"
for _, c := range candidates {
if exactIn {
c.legs[0].amtIn = amount
} else {
c.legs[len(c.legs)-1].amtOut = amount
}
}
for pass := 0; pass < 2; pass++ {
if err := q.priceReady(ctx, candidates, exactIn); err != nil {
return nil
}
}
var winner *route
for _, c := range candidates {
if !c.priced() {
continue
}
switch {
case winner == nil:
winner = c
case exactIn && c.amountOut().Cmp(winner.amountOut()) > 0:
winner = c
case !exactIn && c.amountIn().Cmp(winner.amountIn()) < 0:
winner = c
}
}
return winner
}
// book reads every indexed pool into the routing graph. A pool with a malformed
// address or a missing side cannot be crossed and is left out.
func (q *Quoter) book() []*pool {
raw := q.store.PoolsRaw()
out := make([]*pool, 0, len(raw))
for addr, p := range raw {
if p == nil || !IsAddress(addr) || !IsAddress(p.Token0) || !IsAddress(p.Token1) {
continue
}
t0, t1 := strings.ToLower(p.Token0), strings.ToLower(p.Token1)
if t0 == t1 {
continue
}
out = append(out, &pool{addr: strings.ToLower(addr), t0: t0, t1: t1, fee: p.FeeTier})
}
// A stable order so the same book searched twice tries routes in the same
// order, and a tie between two equal quotes resolves the same way each time.
sort.Slice(out, func(i, j int) bool { return out[i].addr < out[j].addr })
return out
}
// routes enumerates the ways the book connects in to out: every pool holding
// both, then every pair of pools meeting at a token in the middle.
//
// The middle token is discovered rather than configured. exchange-api tried two
// hardcoded hubs, which is exactly as much of the graph as someone remembered to
// list; the index knows the whole graph, so a pair that only meets through some
// third token is reachable here without anyone adding it to a list.
func routes(pools []*pool, in, out string) []*route {
var direct, hops []*route
byToken := map[string][]*pool{}
for _, p := range pools {
if (p.t0 == in && p.t1 == out) || (p.t0 == out && p.t1 == in) {
direct = append(direct, &route{legs: []*leg{{p: p, in: in, out: out}}})
continue
}
byToken[p.t0] = append(byToken[p.t0], p)
byToken[p.t1] = append(byToken[p.t1], p)
}
// Rank middles by how much of the book meets there. Depth is not knowable
// without asking every pool, but a token many pools hold is the one likeliest
// to carry a trade, and the cap has to fall somewhere.
type mid struct {
token string
first *pool
last *pool
deg int
}
var mids []mid
for _, first := range byToken[in] {
m, ok := first.other(in)
if !ok || m == out || m == in {
continue
}
for _, last := range byToken[m] {
if last == first {
continue
}
if far, ok := last.other(m); !ok || far != out {
continue
}
mids = append(mids, mid{token: m, first: first, last: last, deg: len(byToken[m])})
}
}
sort.SliceStable(mids, func(i, j int) bool {
if mids[i].deg != mids[j].deg {
return mids[i].deg > mids[j].deg
}
return mids[i].first.addr < mids[j].first.addr
})
for _, m := range mids {
hops = append(hops, &route{legs: []*leg{
{p: m.first, in: in, out: m.token},
{p: m.last, in: m.token, out: out},
}})
}
all := append(direct, hops...)
if len(all) > maxRoutes {
all = all[:maxRoutes]
}
return all
}
// ── pricing ───────────────────────────────────────────────────────────────
// classify learns which venue each pool is, by asking it. The store cannot say:
// a V2 pair and a V3 pool are the same row there, and the pair's fee is recorded
// as 3000 whether or not that is what it charges. So the pool is asked directly
// — getReserves answers on a pair, slot0 on a pool, and the one that answers is
// the one it is. A venue never changes, so this is asked once per pool.
func (q *Quoter) classify(ctx context.Context, pools []*pool) error {
var unknown []*pool
q.mu.Lock()
for _, p := range pools {
if v, ok := q.venues[p.addr]; ok {
p.venue = v
continue
}
unknown = append(unknown, p)
}
q.mu.Unlock()
if len(unknown) == 0 {
return nil
}
calls := make([]ethCall, 0, len(unknown)*2)
for _, p := range unknown {
calls = append(calls,
ethCall{To: p.addr, Data: selGetReserves},
ethCall{To: p.addr, Data: selSlot0})
}
res, err := q.node.call(ctx, calls)
if err != nil {
return err // the node did not answer; nothing is learned, nothing recorded
}
q.mu.Lock()
defer q.mu.Unlock()
for i, p := range unknown {
reserves, slot0 := res[i*2], res[i*2+1]
switch {
case len(reserves) >= 3*wordHex:
p.venue = "v2"
case len(slot0) >= 2*wordHex:
p.venue = "v3"
default:
continue // answered neither; leave it unknown rather than guess
}
q.venues[p.addr] = p.venue
}
return nil
}
// priceReady prices every leg whose input amount is now known, in one batch, and
// carries each answer to the leg that follows it.
func (q *Quoter) priceReady(ctx context.Context, candidates []*route, exactIn bool) error {
var ready []*leg
for _, c := range candidates {
for _, l := range c.legs {
if l.done() {
continue
}
if exactIn && l.amtIn != nil {
ready = append(ready, l)
} else if !exactIn && l.amtOut != nil {
ready = append(ready, l)
}
}
}
if len(ready) == 0 {
return nil
}
// Each leg asks for two things: what it pays, and what a negligible trade
// would pay. The second is the baseline for price impact, and asking for it
// here rides the batch that is going out anyway.
var calls []ethCall
index := make([][2]int, len(ready))
for i, l := range ready {
c := q.legCalls(l, exactIn)
index[i] = [2]int{len(calls), len(c)}
calls = append(calls, c...)
}
res, err := q.node.call(ctx, calls)
if err != nil {
return err
}
for i, l := range ready {
at, n := index[i][0], index[i][1]
l.absorb(res[at:at+n], exactIn)
}
// Feed each answer to the next leg along.
for _, c := range candidates {
for i := 0; i+1 < len(c.legs); i++ {
a, b := c.legs[i], c.legs[i+1]
if exactIn && b.amtIn == nil && a.amtOut != nil {
b.amtIn = a.amtOut
}
if !exactIn && a.amtOut == nil && b.amtIn != nil {
a.amtOut = b.amtIn
}
}
}
return nil
}
// done reports whether a leg has both its amounts and needs no further asking.
func (l *leg) done() bool { return l.amtIn != nil && l.amtOut != nil }
// legCalls builds the calls one leg needs: the quote itself, then the marginal
// reference. A V2 pair needs only its reserves — both numbers fall out of the
// same two integers, computed here.
func (q *Quoter) legCalls(l *leg, exactIn bool) []ethCall {
if l.p.venue == "v2" {
return []ethCall{{To: l.p.addr, Data: selGetReserves}}
}
if q.quoterV2 == "" {
return nil
}
sel := selQuoteExactIn
amount := l.amtIn
if !exactIn {
sel, amount = selQuoteExactOut, l.amtOut
}
if amount == nil {
return nil
}
quote := ethCall{To: q.quoterV2, Data: sel +
addrWord(l.in) + addrWord(l.out) + numWord(amount) + numWord(big.NewInt(l.p.fee)) + numWord(zero)}
// The marginal reference is always an exact-input quote, whichever direction
// the trade was asked in: it is a rate, and a rate reads the same either way.
l.refIn = marginalSize(l.amtIn)
refCall := ethCall{To: q.quoterV2, Data: selQuoteExactIn +
addrWord(l.in) + addrWord(l.out) + numWord(l.refIn) + numWord(big.NewInt(l.p.fee)) + numWord(zero)}
return []ethCall{quote, refCall}
}
// marginalSize is the trade the marginal rate is measured at: small enough not
// to move the price, and never larger than the trade being quoted, since a
// reference bigger than the trade would report the trade as an improvement.
func marginalSize(amtIn *big.Int) *big.Int {
ref := big.NewInt(marginalRef)
if amtIn != nil && amtIn.Sign() > 0 && amtIn.Cmp(ref) < 0 {
return amtIn
}
return ref
}
// absorb reads the answers legCalls asked for onto the leg.
func (l *leg) absorb(res []string, exactIn bool) {
if len(res) == 0 {
return
}
if l.p.venue == "v2" {
w := words(res[0])
if len(w) < 2 {
return
}
l.r0, l.r1 = w[0], w[1]
rIn, rOut := l.r0, l.r1
if l.in == l.p.t1 {
rIn, rOut = l.r1, l.r0
}
if exactIn {
l.amtOut = amountOut(l.amtIn, rIn, rOut, l.p.fee)
} else {
l.amtIn = amountIn(l.amtOut, rIn, rOut, l.p.fee)
}
if l.amtIn == nil || l.amtOut == nil {
return
}
l.gas = v2SwapGas
// A negligible trade through a constant product pays the pool ratio less
// the fee, and both numbers are already here — no second question.
l.refIn = marginalSize(l.amtIn)
if ref := amountOut(l.refIn, rIn, rOut, l.p.fee); ref != nil {
l.marginal = ratio(ref, l.refIn)
}
return
}
// A V3 quote answers with the amount, the price it left the pool at, how many
// initialised ticks it crossed getting there, and what that walk cost.
w := words(res[0])
if len(w) < 4 {
return
}
if exactIn {
l.amtOut = w[0]
} else {
l.amtIn = w[0]
}
l.gas = w[3].Int64()
if len(res) > 1 && l.refIn != nil {
if rw := words(res[1]); len(rw) >= 1 {
l.marginal = ratio(rw[0], l.refIn)
}
}
}
// ── constant product ──────────────────────────────────────────────────────
//
// The V2 pair's whole rule: the product of the two reserves may not fall. The
// fee is withheld from the input before it is swapped, which is why it appears
// on the numerator and again scaled on the denominator.
//
// Fees are parts per million here because that is how a pool records one, and at
// 3000 the arithmetic is identical to Uniswap's 997/1000 — the same integers,
// scaled by a thousand top and bottom.
var zero = new(big.Int)
// amountOut is what a pool pays for amountIn, given the reserve it receives into
// and the reserve it pays out of. Nil when the trade cannot happen at all.
func amountOut(amtIn, reserveIn, reserveOut *big.Int, feePPM int64) *big.Int {
if amtIn == nil || amtIn.Sign() <= 0 || reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
return nil
}
if feePPM < 0 || feePPM >= 1_000_000 {
return nil
}
million := big.NewInt(1_000_000)
afterFee := new(big.Int).Mul(amtIn, big.NewInt(1_000_000-feePPM))
num := new(big.Int).Mul(afterFee, reserveOut)
den := new(big.Int).Add(new(big.Int).Mul(reserveIn, million), afterFee)
if den.Sign() <= 0 {
return nil
}
return num.Quo(num, den)
}
// amountIn is what a pool must receive to pay amountOut. Nil when the pool does
// not hold that much: a constant product can be drained towards its reserve but
// never to it, so asking for the whole side has no answer.
func amountIn(amtOut, reserveIn, reserveOut *big.Int, feePPM int64) *big.Int {
if amtOut == nil || amtOut.Sign() <= 0 || reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
return nil
}
if feePPM < 0 || feePPM >= 1_000_000 || amtOut.Cmp(reserveOut) >= 0 {
return nil
}
million := big.NewInt(1_000_000)
num := new(big.Int).Mul(new(big.Int).Mul(reserveIn, amtOut), million)
den := new(big.Int).Mul(new(big.Int).Sub(reserveOut, amtOut), big.NewInt(1_000_000-feePPM))
// One more base unit than the division gives, because the pool checks its
// invariant after truncation and the exact quotient rounds the wrong way.
return num.Add(num.Quo(num, den), big.NewInt(1))
}
// ── price impact ──────────────────────────────────────────────────────────
// priceImpact is how much worse a trade does than a negligible one would, as a
// percentage, composed across the route: each leg contributes the ratio of what
// it paid to what it would have paid at the marginal rate.
//
// Zero when any leg has no marginal to compare against — an unknown gap is
// reported as none rather than guessed at, and a negative one is rounding.
func priceImpact(r *route) float64 {
exec, marginal := 1.0, 1.0
for _, l := range r.legs {
if l.marginal <= 0 || l.amtIn == nil || l.amtOut == nil || l.amtIn.Sign() <= 0 {
return 0
}
exec *= ratio(l.amtOut, l.amtIn)
marginal *= l.marginal
}
if marginal <= 0 || exec <= 0 || math.IsInf(exec, 0) || math.IsInf(marginal, 0) {
return 0
}
impact := (1 - exec/marginal) * 100
if impact < 0 {
return 0
}
if impact > 100 {
return 100
}
return impact
}
// ratio divides two integers as a rate. Both sides are raw base units, and the
// decimals cancel wherever a ratio is compared to another ratio.
func ratio(num, den *big.Int) float64 {
if num == nil || den == nil || den.Sign() == 0 {
return 0
}
f, _ := new(big.Float).Quo(new(big.Float).SetInt(num), new(big.Float).SetInt(den)).Float64()
return f
}
// ── rendering ─────────────────────────────────────────────────────────────
// decorate reads the price state of the pools the winning route crosses, and the
// block it all was read at, in one trip. Only the winner is asked: what a route
// that lost looked like is not in the answer.
func (q *Quoter) decorate(ctx context.Context, r *route) uint64 {
var reqs []rpcRequest
type at struct {
l *leg
slot int
}
var v3 []at
for _, l := range r.legs {
if l.p.venue != "v3" {
continue
}
v3 = append(v3, at{l, len(reqs)})
reqs = append(reqs,
ethCallReq(len(reqs), l.p.addr, selSlot0),
ethCallReq(len(reqs)+1, l.p.addr, selLiquidity))
}
headAt := len(reqs)
reqs = append(reqs, rpcRequest{JSONRPC: "2.0", ID: headAt, Method: "eth_blockNumber", Params: []any{}})
res, err := q.node.batch(ctx, reqs)
if err != nil {
return 0
}
for _, a := range v3 {
// slot0 opens with the price and the tick it sits in; the rest of the
// struct is oracle bookkeeping this does not read.
if w := words(res[a.slot]); len(w) >= 2 {
a.l.sqrtP, a.l.tick = w[0], signed(w[1])
}
if w := words(res[a.slot+1]); len(w) >= 1 {
a.l.liq = w[0]
}
}
n, ok := new(big.Int).SetString(strings.TrimPrefix(res[headAt], "0x"), 16)
if !ok {
return 0
}
return n.Uint64()
}
func (q *Quoter) render(req *quoteRequest, r *route, tradeType, swapper string, slippage float64, head uint64) classicQuote {
// One read of the token table for the whole answer: it is a scan, and the
// route asks about the same handful of tokens several times over.
rows := q.tokens()
last := len(r.legs) - 1
hops := make([]hopJSON, 0, len(r.legs))
for i, l := range r.legs {
// The caller's own tokens are echoed as the caller wrote them, so a swap
// from the native coin reads as the coin at both ends of the route rather
// than as the wrapped token it travelled through.
inTok := rows.describe(l.in, q.chainID)
outTok := rows.describe(l.out, q.chainID)
if i == 0 {
inTok.Address = echoToken(req.TokenIn)
}
if i == last {
outTok.Address = echoToken(req.TokenOut)
}
hops = append(hops, l.render(inTok, outTok))
}
names := make([]string, 0, len(r.legs)+1)
names = append(names, rows.symbol(r.legs[0].in))
for _, l := range r.legs {
names = append(names, rows.symbol(l.out))
}
return classicQuote{
ChainID: q.chainID,
Swapper: swapper,
Input: quoteSide{Token: echoToken(req.TokenIn), Amount: r.amountIn().String()},
Output: quoteSide{Token: echoToken(req.TokenOut), Amount: r.amountOut().String(), Recipient: swapper},
TradeType: tradeType,
Slippage: slippage,
GasUseEstimate: strconv.FormatInt(r.gas(), 10),
// What the gas costs in the chain's coin is a fee market question, and
// nothing here reads the fee market.
GasFee: "0",
Route: [][]hopJSON{hops},
RouteString: strings.Join(names, " -> "),
QuoteID: requestID(),
BlockNumber: strconv.FormatUint(head, 10),
PriceImpact: priceImpact(r),
}
}
func (l *leg) render(in, out tokenJSON) hopJSON {
h := hopJSON{
Address: checksumAddress(l.p.addr),
TokenIn: in,
TokenOut: out,
AmountIn: l.amtIn.String(),
AmountOut: l.amtOut.String(),
}
if l.p.venue == "v2" {
h.Type = "v2-pool"
t0, t1 := in, out
if l.in == l.p.t1 {
t0, t1 = out, in
}
h.Reserve0 = &reserveRef{Token: t0, Quotient: bigString(l.r0)}
h.Reserve1 = &reserveRef{Token: t1, Quotient: bigString(l.r1)}
return h
}
h.Type = "v3-pool"
h.SqrtRatioX96 = bigString(l.sqrtP)
h.Liquidity = bigString(l.liq)
h.TickCurrent = strconv.FormatInt(l.tick, 10)
h.Fee = strconv.FormatInt(l.p.fee, 10)
return h
}
// tokenTable is the index's token rows, keyed the one way this file looks them
// up. The indexer writes a row under whatever casing the log carried, so the
// key is folded here rather than hoped about at each call site.
type tokenTable map[string]*storage.SeedTokenData
func (q *Quoter) tokens() tokenTable {
rows := q.store.TokensRaw()
out := make(tokenTable, len(rows))
for addr, t := range rows {
out[strings.ToLower(addr)] = t
}
return out
}
// describe says what a token is, as the index knows it. Eighteen decimals is the
// default the indexer itself falls back to, so a token it has not read yet reads
// the same way here as everywhere else.
func (t tokenTable) describe(addr string, chainID int64) tokenJSON {
out := tokenJSON{Address: checksumAddress(addr), ChainID: chainID, Decimals: "18"}
if row := t[addr]; row != nil {
out.Symbol = row.Symbol
if row.Decimals > 0 {
out.Decimals = strconv.FormatInt(row.Decimals, 10)
}
}
return out
}
// symbol names a token for the human-readable route. A token with no symbol
// indexed is named by its address, which is at least true.
func (t tokenTable) symbol(addr string) string {
if row := t[addr]; row != nil && row.Symbol != "" {
return row.Symbol
}
return checksumAddress(addr)
}
// unwrapNative maps the native sentinel onto the wrapped token that the pools
// actually hold.
func (q *Quoter) unwrapNative(addr string) string {
a := strings.ToLower(addr)
if a == nativeSentinel && q.wrapped != "" {
return q.wrapped
}
return a
}
// echoToken gives the caller back the address it sent, checksummed. The native
// sentinel stays the sentinel: the caller asked about the coin, and the wrapping
// is this service's business, not theirs.
func echoToken(addr string) string {
if strings.ToLower(addr) == nativeSentinel {
return nativeSentinel
}
return checksumAddress(addr)
}
// wordHex is the length of one ABI word rendered as hex characters.
const wordHex = 64
// ── ABI ───────────────────────────────────────────────────────────────────
// numWord renders an integer as a 32-byte argument.
func numWord(n *big.Int) string {
return fmt.Sprintf("%064x", n)
}
// addrWord renders an address as a 32-byte argument, left-padded.
func addrWord(addr string) string {
return strings.Repeat("0", 24) + strings.ToLower(strings.TrimPrefix(addr, "0x"))
}
// words splits returned data into its 32-byte values. Everything read back here
// is a flat tuple of fixed-width values, so position is the whole decoding.
func words(data string) []*big.Int {
h := strings.TrimPrefix(data, "0x")
out := make([]*big.Int, 0, len(h)/wordHex)
for i := 0; i+wordHex <= len(h); i += wordHex {
n, ok := new(big.Int).SetString(h[i:i+wordHex], 16)
if !ok {
return out
}
out = append(out, n)
}
return out
}
// signed reinterprets a word as a two's-complement integer, which is how a tick
// — the only signed value read here — comes back.
func signed(w *big.Int) int64 {
limit := new(big.Int).Lsh(big.NewInt(1), 255)
if w.Cmp(limit) >= 0 {
return new(big.Int).Sub(w, new(big.Int).Lsh(big.NewInt(1), 256)).Int64()
}
return w.Int64()
}
func bigString(n *big.Int) string {
if n == nil {
return "0"
}
return n.String()
}
// ── addresses ─────────────────────────────────────────────────────────────
// IsAddress reports whether s is a twenty-byte hex address. Exported because
// the process that mounts these handlers decides whether a chain has a router
// to address a transaction to, and that question has one answer.
func IsAddress(s string) bool {
if len(s) != 42 || !strings.HasPrefix(s, "0x") {
return false
}
for _, c := range s[2:] {
if !strings.ContainsRune("0123456789abcdefABCDEF", c) {
return false
}
}
return true
}
// checksumAddress renders an address with EIP-55 capitalisation: the hash of the
// lower-cased digits decides which letters are upper. Wallets compare addresses
// in this form, so an address that goes out any other way looks like a different
// one to whatever reads it.
func checksumAddress(addr string) string {
a := strings.ToLower(strings.TrimPrefix(addr, "0x"))
h := sha3.NewLegacyKeccak256()
h.Write([]byte(a))
sum := h.Sum(nil)
out := []byte("0x" + a)
for i := 0; i < len(a); i++ {
c := a[i]
if c < 'a' || c > 'f' {
continue
}
if sum[i/2]>>(4-4*(i%2))&0xf >= 8 {
out[2+i] = c - ('a' - 'A')
}
}
return string(out)
}
// maxWord is the largest value an ABI word holds. An amount above it is not a
// large trade, it is not a number the chain has — and rendered as an argument
// it runs 65 hex characters instead of 64, shifting every argument after it, so
// the call would still be well-formed and would mean something else entirely.
var maxWord = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1))
// parseWei reads an amount as the caller must write it: base units, decimal
// digits only, inside a word. A float here would round someone's balance.
func parseWei(s string) (*big.Int, bool) {
if s == "" {
return nil, false
}
for _, c := range s {
if c < '0' || c > '9' {
return nil, false
}
}
n, ok := new(big.Int).SetString(s, 10)
if !ok || n.Cmp(maxWord) > 0 {
return nil, false
}
return n, true
}