Yul Router
The Yul Router is a gas-focused router written in Yul that executes Ekubo swaps on EVM chains. It is how swaps are executed in production today: the interface encodes routes from Quoter API results and sends them to the router.
The router is deployed deterministically on every supported network. Always use YUL_ROUTER_ADDRESS exported by the same installed version of @ekubo/yul-router-sdk that you use to encode calldata. This keeps the router destination compatible with that version’s encoding; do not copy or hard-code an address from documentation.
Design
Section titled “Design”The router deliberately carries everything it needs — token addresses, pool configs, extension forwardee addresses, and token wrapper addresses — in calldata. There are no token or extension jump tables and no stored routes. There is also no public ABI selector: any call that does not come from Ekubo Core is interpreted directly as packed route data. Calls from Core are reserved for the lock callback (selector 0x00000000); the forward callback (selector 0x00000001) always reverts.
A single transaction can contain many multi-hop routes. The router executes all of them under one Core lock, aggregates the specified and calculated amounts, applies one slippage check against the aggregate, and settles token transfers once.
Supported hop types:
| Hop type | Executes |
|---|---|
core |
A direct Core.swap against the pool key |
forwarded |
Core.forward(forwardee, ...) for forward-only swap extensions such as MEVCapture and Ve33 |
signedExclusiveSwap |
A controller-signed swap on a SignedExclusiveSwap pool (pool key, params, signed meta, minimum balance update, and signature) |
wrapper |
Wrapping or unwrapping through an Ekubo token wrapper |
Excluded by design, as a security posture:
- No delegatecall routing (the router checks an immutable self address and rejects delegatecall execution)
- No routing through
Core.forward(router, ...)— the forward callback reverts - No protocol or integration fee collection and no fee claiming
The router has been audited, and CI continuously verifies it against production: live mainnet quotes from the Quoter API are converted to calldata with the SDK and executed against canonical Core on a mainnet fork, covering ETH↔ERC20, ERC20↔ERC20, and exact-output swaps.
The SDK: @ekubo/yul-router-sdk
Section titled “The SDK: @ekubo/yul-router-sdk”Routes are encoded with @ekubo/yul-router-sdk (published with npm provenance from the repository’s release workflow):
npm install @ekubo/yul-router-sdkEncoding a swap
Section titled “Encoding a swap”encodeRoutes(...) is the primary surface. Each entry in multiHops is an independent path from specifiedToken to calculatedToken with its own specifiedAmount; the router aggregates them all under one lock and one slippage check. Splitting a trade across multiple multi-hops is how split routes execute atomically.
import { encodeRoutes, YUL_ROUTER_ADDRESS } from "@ekubo/yul-router-sdk";
const calldata = encodeRoutes({ // positive specifiedAmount = exact-in; negative = exact-out specifiedToken: WETH, calculatedToken: USDC, // slippage protection: minimum output (exact-in) or maximum input // (exact-out). Required — pass `false` only to explicitly opt into an // unbounded threshold. calculatedAmountThreshold: minUsdcOut, recipient, // optional; defaults to the sender multiHops: [ { specifiedAmount: 10n ** 18n, hops: [{ type: "core", poolKey }] }, // e.g. a second split through a MEVCapture pool: // { specifiedAmount: ..., hops: [{ type: "forwarded", forwardee: MEV_CAPTURE, poolKey: otherPoolKey }] }, ],});
// send directly to the router — the calldata IS the routeawait wallet.sendTransaction({ to: YUL_ROUTER_ADDRESS, data: calldata });Notes:
- Native ETH is
address(0)(alwaystoken0); attachvalueto the transaction when the input is native ETH. - All multi-hops in one call must agree on direction — mixing exact-in and exact-out throws.
encodeRoute(...)is a convenience wrapper for a single path;generateCalldata(...)is an alias ofencodeRoutes(...).- Limits: up to 256 multi-hops per call and 256 hops per multi-hop.
Signed exclusive swaps
Section titled “Signed exclusive swaps”For signedExclusiveSwap hops, encodeSignedSwapMeta({ deadline, fee, nonce, authorizedLocker }) packs the signed metadata word. deadline and fee are uint32 numbers; nonce must be a bigint (a JavaScript number is rejected so uint64 nonces cannot lose precision). encodePoolBalanceUpdate(delta0, delta1) packs the signed minimum balance update.
Other exports
Section titled “Other exports”YUL_ROUTER_ADDRESS— the router compatible with this SDK version’s encodingMIN_SQRT_RATIO/MAX_SQRT_RATIO— bounds forsqrtRatioLimiton hops (see Price representation)PoolKey,Hop,MultiHop, and parameter types for TypeScript consumerscalldataSize(data)— helper for estimating calldata cost
Typical flow
Section titled “Typical flow”- Fetch a quote from the Quoter API — it returns block-pinned split routes in exactly the shape the SDK consumes.
- Convert each split and hop into
multiHopsentries and callencodeRoutes(...)with your slippage threshold. - Send the calldata to
YUL_ROUTER_ADDRESSpromptly (quotes are pinned to a block).
To deploy the router to a new chain, use the repository’s Foundry deploy script — it deploys through the canonical deterministic deployer against the canonical Core address, so the router lands at the same address everywhere.