Path encoding
A route is bytes:tokenA | poolType | tokenB | poolType | tokenC | … — addresses of 20 bytes joined by a single byte of pool type where Uniswap V3 has a three-byte fee tier. The route names the curve, not the price: fees here are governance state a plugin can override per swap, so a fee-bearing route would name a pool that stops existing the moment a fee moved.
Pool type values: CL = 0, CP = 1, STABLE = 2 — an open set.
The router
SwapRouter exposes the four V3-shaped entries; the difference from V3 is the poolType field where fee was:
- Deadline:
Expired()past it. Slippage:TooLittleReceived()/TooMuchRequested(). - Price limit:
0means “the widest the pool accepts” (MIN_SQRT_RATIO+1/MAX_SQRT_RATIO−1) — a real bound, not “no limit”. A limit at or beyond the current price revertsInvalidSqrtPriceLimit(). Multi-hop entries always pass the widest per hop; there is no per-hop limit. - A binding limit under-delivers instead of failing — and unlike upstream, this router checks the shortfall itself: an exact-output swap that cannot fill reverts
TooLittleReceived()rather than silently delivering less. - Exact-output pulls only what the swap cost — no ERC-20 change is left behind. Native over-send must be reclaimed by you (below).
- Fee taken on the output side: on a pool whose fee coin is the token you buy, the pool grosses an exact-output request up, so you still receive exactly
amountOutunless the price limit binds. Swap deltas are always net, so slippage checks are unaffected. - Amounts above
int256.maxrevertAmountOutOfRange()— beyond it the cast flips exact-input into a monstrous exact-output. - Gasless approvals:
selfPermit/selfPermitIfNecessaryon the router.
Native coin, multicall, and the one hard requirement
The router batches viamulticall and settles native through its own balance. Three consequences:
msg.valueis not a budget: every batched call sees the full amount. Payment logic spends the contract’s actual balance, so batching stays safe — but do not meter bymsg.value.- The router must hold nothing between transactions. The sweep helpers (
refundNative,unwrapNative,sweepToken) send the whole balance to anyone who calls (anything a periphery contract reserves for others is left behind — on the router, that is nothing), and they revertNotWhileWorking()if called in the middle of an operation. Therefore: every entry point that can leave a balance must be called inside one atomicmulticallthat ends with the matching sweep. This is a hard requirement of the API, not a recommendation — a payable swap sent bare, stopped at its price limit, leaves coin the next caller takes. - On a chain with no native wrapper (
wrappedNative() == 0— the Arc case, where the gas token is the quote ERC-20 seen at 18 decimals):refundNativeandunwrapNativerevertNoNativeWrapper(); usesweepToken. The router’sreceive()rejects unsolicited native. Send nomsg.valueat all: every payable entry point,multicallincluded, revertsValueNotAccepted()when value arrives on such a chain.
The quoter
- Not a
view— it prices by starting the swap and aborting it from the callback (revert-and-catch). Call it witheth_call; on-chain it burns a swap’s gas. amountOutReceivedon the exact-output quoters reports what would actually arrive when a price limit cuts the swap short — compare routes on it, or you will pick one that cannot fill. V3’s quoter has no equivalent.- A route may not visit the same pool twice (
PoolVisitedTwice()): each simulated hop rolls back before the next, so a second visit would price against pre-trade reserves. Split such routes or price them yourself. - Quotes are bids, not promises: dynamic fees price on measured volatility (and may price on the swap’s own size), so a quote ages faster than on a static-fee venue.
The pool-level swap
For integrators who settle themselves:getPool, not an init-code-hash recomputation). amountSpecified of 0 or int256.min reverts on every pool type. The signature — including the price limit — is uniform across CL, CP and STABLE; a STABLE swap simply resolves in one step instead of a tick loop.
Sibling callbacks for liquidity and flash: lunyaMintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes) and lunyaFlashCallback(uint256 fee0, uint256 fee1, bytes).