Zerochord
Concepts

Routing

The four stage quote engine, the exact split solve, and the guarantee that a routed quote is never worse than one path used alone.

The quote engine turns a set of pools read at one chain state into a route and a set of numbers a transaction can be held to. It runs in four stages, in order.

Stages one to three choose an allocation. Stage four prices it. Every figure the user is shown comes from stage four, which is integer arithmetic that matches the validators.

Stage 1: candidate path enumeration

Nodes are the registered stablecoins plus ADA. Edges are pools.

With that few nodes the enumeration of all simple paths up to routing.maxPathLength is exhaustive rather than heuristic, so nothing is missed by a search cutoff. A path never revisits an asset, so it cannot loop, and every intermediate asset must be in routing.intermediateAssets. The user's own input and output are never intermediates.

Each path carries how it would settle, computed by pathAtomicity:

  • A single pool takes its own venue's class.
  • A multi hop path where every pool is Kernel is atomic.
  • A multi hop path inside one Commitment venue that supports multi routing is single-order, because it becomes one order with one end to end minimum.
  • Anything else is needs-settlement.

Paths that would settle leg by leg are dropped unless the request passes allowNonAtomic.

Stage 2: reduction to effective pools

Every venue Zerochord routes across is constant product once you look at the right reserves. Minswap V2 is constant product on the reserves in its datum. Dano Finance's concentrated liquidity pool is constant product on its virtual reserves, and the concentrated part only decides what those virtual reserves are.

So a path of any length collapses to one triple of (reserveIn, reserveOut, fee), exactly. For pool 1 then pool 2 with fee multipliers g1 and g2:

reserveIn  = R1in * R2in        / (R2in + g1 * R1out)
reserveOut = g1 * R1out * R2out / (R2in + g1 * R1out)
g          = g1 * g2

The reserves are carried as a rational with a shared denominator, and the fee multiplier as an exact rational. Flooring the composed reserves introduced a ranking error of about thirty basis points on small pools, which is enough to pick the wrong path.

Stage 3: exact split solve

Maximising total output over an allocation of one input across concave paths is a convex problem solved by equalising marginal outputs, which is water filling. Fixed per-order costs make it mixed integer, and at this size mixed integer means evaluating all subsets.

Each candidate carries a fixedCostOut: the cost of using that path at all, converted into output base units. A venue that charges a non-refundable execution fee makes a small allocation not worth taking, and the subset enumeration is what decides that.

Because every path reduced to a constant product triple in stage two, the water filling has a closed form and the whole solve is a bisection on one scalar per subset. That bisection is the only place a floating point number appears in the engine, and its answer is an allocation rather than a quoted output.

routing.maxCandidatePaths bounds how many candidates enter the enumeration.

Stage 4: integer requote

The winning allocation is walked hop by hop, calling each venue's own quote with that venue's own arithmetic and rounding. That result is the quote.

Nothing from stage three survives into the numbers on screen. An off-by-one in rounding produces an order the validator rejects, not a slightly worse price, so the requote uses the same integers the validator will.

When a venue refuses the allocation

The reduction in stage two models a pool's curve but not its capacity. A concentrated liquidity pool stops filling once the price leaves its range, and the reduced form does not know that. So an allocation can be chosen that a venue then refuses to quote.

Dropping that branch would be wrong twice over: its share of the input would vanish, so the route would deliver less than the user paid for, and the comparison that chose the split would no longer describe the route being offered.

Instead the offending path is removed from the candidate set and the split is solved again. The whole input is always allocated to paths that can take it, and the engine refuses to return a quote that does not spend the whole input.

The floor: never worse than one path

Stages three and four can disagree. Composing a multi hop path floors its reserves, so the reduction can predict a few base units more than walking the pools really yields. Where the disagreement crosses the gap between two candidates, the solver can hand back a split that prices worse than one path used alone.

So the engine checks rather than trusting the two stages to agree. Every candidate the solver considered is requoted alone, at the full input, with the same integer arithmetic the quote is made of. If any of them beats the chosen route, the route becomes that single path.

The comparison is on the solver's own objective, output net of each path's fixed cost, not on gross output. Comparing gross would let the floor replace a route that pays one venue fee with one that pays three, on the strength of an output difference the extra fees more than consume.

The guarantee this buys, stated exactly: the quote is never worse than the best single candidate path the solver considered. It is not a claim about paths the pre-filter dropped.

Price impact

Price impact is the shortfall against the price a vanishingly small trade would get on the very same route, in hundredths of a basis point.

The reference is the best marginal rate among the paths the route actually uses, read straight off the exact reduction as a rational:

lim x->0 out(x)/x = reserveOut * gammaNumerator / (reserveIn * gammaDenominator)

It is not measured by quoting a small probe amount. Probing was the first implementation and it was wrong: a probe of a ten thousandth of the branch floors to one base unit on a small trade, and one base unit through a pool floors its output to zero or one. The reported impact then either sat at zero or moved with the trade size in the wrong direction. The closed form has no probe, no floating point and no such regime.

The minimum output

minimumOut = amountOut - ceil(amountOut * slippageBps / 10 000)

The deduction rounds up, so the on-chain bound is never looser than the quote implies.

slippageBps comes from the request, or from routing.defaultSlippageBps when the request omits it.

Pools whose fee can move

A pool that can change its fee after the quote cannot honour a fill-at-the-quoted-rate promise. routing.dynamicFeePolicy decides what happens:

  • exclude drops such pools from both the route and the baseline.
  • label keeps them and relies on the risk flag reaching the interface.

On this page