Skip to main content

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 = 2an open set.
Exact-output paths are encoded in reverse — token bought first, token sold last — exactly as in Uniswap V3. Encoding them forwards silently prices the wrong trade.

The router

SwapRouter exposes the four V3-shaped entries; the difference from V3 is the poolType field where fee was:
Behavior worth knowing:
  • Deadline: Expired() past it. Slippage: TooLittleReceived() / TooMuchRequested().
  • Price limit: 0 means “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 reverts InvalidSqrtPriceLimit(). 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).
  • Amounts above int256.max revert AmountOutOfRange() — beyond it the cast flips exact-input into a monstrous exact-output.
  • Gasless approvals: selfPermit / selfPermitIfNecessary on the router.

Native coin, multicall, and the one hard requirement

The router batches via multicall and settles native through its own balance. Three consequences:
  • msg.value is 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 by msg.value.
  • The router must hold nothing between transactions. The sweep helpers (refundNative, unwrapNative, sweepToken) send the whole balance to anyone who calls. Therefore: every entry point that can leave a balance must be called inside one atomic multicall that 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): refundNative and unwrapNative revert NoNativeWrapper(); use sweepToken. The router’s receive() rejects unsolicited native.

The quoter

  • Not a view — it prices by starting the swap and aborting it from the callback (revert-and-catch). Call it with eth_call; on-chain it burns a swap’s gas.
  • amountOutReceived on 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:
The pool sends the output, then calls back for payment — pay the positive delta before returning. Verify the caller is a pool from the canonical factory (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).