# Advanced Swaps

> Exact-output and multi-hop swaps, and how native ETH routes settle.

[Integration](/docs/developers/integration) covers the basic flow: quote one pool, sell an exact input, settle and take. This page extends it to the other swaps the app makes: buying an exact output, routing through two pools, and paying or receiving native ETH. The examples import `setup.ts` from Integration and build the same transactions as the app's own encoder; the viem and ethers v6 tabs send identical transactions.

The app routes up to two hops and sends one route per swap. Both exact input and exact output use the same Universal Router call, `execute` with an `INFI_SWAP` command (`0x10`); only the plan inside it changes.

## Exact Output

An exact-output swap fixes the amount you receive and lets the amount you pay float. The app uses it whenever you type into the Buy field.

**Quote.** `CLQuoter.quoteExactOutput` takes the output currency as `exactCurrency`, the output amount as `exactAmount`, and a path, and returns the input the swap needs. The app calls this path form even for one pool; `quoteExactOutputSingle` takes a pool key and direction instead and returns the same amount.

**Path order.** A path is a list of `PathKey`s, one per pool: the pool's fee, hooks, pool manager, `parameters` and hook data, plus `intermediateCurrency`, the currency on the far side of the hop. The pool is the one formed by that currency and its neighbour, so a `PathKey` never repeats both currencies. For an exact-output path, each `intermediateCurrency` is the currency the hop takes in, listed from the input end: to buy BTC with USDC, the single entry names USDC.

viem:

```ts
import { formatUnits, parseAbi, parseUnits } from "viem";
import { BTC, CONTRACTS, POOL_KEY, publicClient, USDC } from "./setup";

const quoterAbi = parseAbi([
  "struct PathKey { address intermediateCurrency; uint24 fee; address hooks; address poolManager; bytes hookData; bytes32 parameters; }",
  "struct QuoteExactParams { address exactCurrency; PathKey[] path; uint128 exactAmount; }",
  "function quoteExactOutput(QuoteExactParams params) returns (uint256 amountIn, uint256 gasEstimate)",
]);

export const amountOut = parseUnits("0.001", 8); // buy exactly 0.001 BTC

// An exact-output path lists each hop by the currency it takes in. One hop: USDC into BTC/USDC.
export const path = [
  {
    intermediateCurrency: USDC,
    fee: POOL_KEY.fee,
    hooks: POOL_KEY.hooks,
    poolManager: POOL_KEY.poolManager,
    hookData: "0x",
    parameters: POOL_KEY.parameters,
  },
] as const;

// exactCurrency is the output you fix. The quoter reverts internally, so call it, never send it.
const { result } = await publicClient.simulateContract({
  address: CONTRACTS.clQuoter,
  abi: quoterAbi,
  functionName: "quoteExactOutput",
  args: [{ exactCurrency: BTC, path, exactAmount: amountOut }],
});
export const amountIn = result[0];

console.log("0.001 BTC costs", formatUnits(amountIn, 6), "USDC");
```

ethers v6:

```ts
import { Contract, formatUnits, parseUnits } from "ethers";
import { BTC, CONTRACTS, POOL_KEY, provider, USDC } from "./setup";

const quoter = new Contract(
  CONTRACTS.clQuoter,
  [
    "function quoteExactOutput((address exactCurrency, (address intermediateCurrency, uint24 fee, address hooks, address poolManager, bytes hookData, bytes32 parameters)[] path, uint128 exactAmount) params) returns (uint256 amountIn, uint256 gasEstimate)",
  ],
  provider,
);

export const amountOut = parseUnits("0.001", 8); // buy exactly 0.001 BTC

// An exact-output path lists each hop by the currency it takes in. One hop: USDC into BTC/USDC.
export const path = [
  {
    intermediateCurrency: USDC,
    fee: POOL_KEY.fee,
    hooks: POOL_KEY.hooks,
    poolManager: POOL_KEY.poolManager,
    hookData: "0x",
    parameters: POOL_KEY.parameters,
  },
] as const;

// exactCurrency is the output you fix. The quoter reverts internally, so call it, never send it.
const [quotedIn]: [bigint, bigint] = await quoter.quoteExactOutput.staticCall({
  exactCurrency: BTC,
  path,
  exactAmount: amountOut,
});
export const amountIn = quotedIn;

console.log("0.001 BTC costs", formatUnits(amountIn, 6), "USDC");
```

**The plan.** `CL_SWAP_EXACT_OUT` (`0x09`) takes `currencyOut`, the path, `amountOut` and `amountInMaximum`. Slippage moves to the input side: `amountInMaximum` is the quote plus your tolerance, rounded up, and the swap reverts if it would cost more.

`SETTLE_ALL` (`0x0c`) is given the same cap. It pays the Vault the swap's actual debt, reverting if that exceeds the cap, so Permit2 pulls only what the swap cost and nothing needs refunding. The Permit2 allowance must cover the cap, not the quote. `TAKE` with an amount of `0` withdraws the whole output, which is exactly `amountOut`.

The app sends exact-output swaps on one route only; it does not split them across pools.

viem:

```ts
import { encodeAbiParameters, erc20Abi, maxUint256, parseAbi, parseAbiParameters, type Hex } from "viem";
import { BTC, CONTRACTS, publicClient, USDC, walletClient } from "./setup";
import { amountIn, amountOut, path } from "./quote-exact-output";

const permit2Abi = parseAbi([
  "function approve(address token, address spender, uint160 amount, uint48 expiration)",
]);
const routerAbi = parseAbi([
  "function execute(bytes commands, bytes[] inputs, uint256 deadline) payable",
]);

// The input is what floats, so slippage caps it: the quote plus 0.5%, rounded up.
const slippageBps = 50n;
const amountInMaximum = (amountIn * (10_000n + slippageBps) + 9_999n) / 10_000n;

const block = await publicClient.getBlock();

// 1. Let Permit2 move USDC. Once per token; skip it when the allowance already covers you.
const approveHash = await walletClient.writeContract({
  address: USDC,
  abi: erc20Abi,
  functionName: "approve",
  args: [CONTRACTS.permit2, maxUint256],
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });

// 2. Let the Universal Router pull up to the cap through Permit2, for 30 days.
const permitHash = await walletClient.writeContract({
  address: CONTRACTS.permit2,
  abi: permit2Abi,
  functionName: "approve",
  args: [USDC, CONTRACTS.universalRouter, amountInMaximum, Number(block.timestamp + 30n * 86_400n)],
});
await publicClient.waitForTransactionReceipt({ hash: permitHash });

// 3. The plan: swap for an exact output, pay what it actually cost (at most the cap), take the BTC.
const CL_SWAP_EXACT_OUT = "09";
const SETTLE_ALL = "0c";
const TAKE = "0e";
const MSG_SENDER = "0x0000000000000000000000000000000000000001"; // the router reads this as "the caller"

const swap = encodeAbiParameters(
  parseAbiParameters(
    "(address currencyOut, (address intermediateCurrency, uint24 fee, address hooks, address poolManager, bytes hookData, bytes32 parameters)[] path, uint128 amountOut, uint128 amountInMaximum)",
  ),
  [{ currencyOut: BTC, path, amountOut, amountInMaximum }],
);
const settle = encodeAbiParameters(parseAbiParameters("address, uint256"), [USDC, amountInMaximum]);
const take = encodeAbiParameters(parseAbiParameters("address, address, uint256"), [
  BTC,
  MSG_SENDER,
  0n,
]);
const plan = encodeAbiParameters(parseAbiParameters("bytes, bytes[]"), [
  ("0x" + CL_SWAP_EXACT_OUT + SETTLE_ALL + TAKE) as Hex,
  [swap, settle, take],
]);

// 4. One INFI_SWAP (0x10) command carrying the plan. Reverts after 20 minutes.
const INFI_SWAP = "0x10";
const hash = await walletClient.writeContract({
  address: CONTRACTS.universalRouter,
  abi: routerAbi,
  functionName: "execute",
  args: [INFI_SWAP, [plan], block.timestamp + 1_200n],
});
console.log("swap:", (await publicClient.waitForTransactionReceipt({ hash })).status);
```

ethers v6:

```ts
import { AbiCoder, Contract, MaxUint256 } from "ethers";
import { BTC, CONTRACTS, provider, signer, USDC } from "./setup";
import { amountIn, amountOut, path } from "./quote-exact-output";

const abi = AbiCoder.defaultAbiCoder();
const usdc = new Contract(USDC, ["function approve(address spender, uint256 amount) returns (bool)"], signer);
const permit2 = new Contract(
  CONTRACTS.permit2,
  ["function approve(address token, address spender, uint160 amount, uint48 expiration)"],
  signer,
);
const router = new Contract(
  CONTRACTS.universalRouter,
  ["function execute(bytes commands, bytes[] inputs, uint256 deadline) payable"],
  signer,
);

// The input is what floats, so slippage caps it: the quote plus 0.5%, rounded up.
const slippageBps = 50n;
const amountInMaximum = (amountIn * (10_000n + slippageBps) + 9_999n) / 10_000n;

const block = await provider.getBlock("latest");
if (!block) throw new Error("no latest block");
const now = BigInt(block.timestamp);

// 1. Let Permit2 move USDC. Once per token; skip it when the allowance already covers you.
await (await usdc.approve(CONTRACTS.permit2, MaxUint256)).wait();

// 2. Let the Universal Router pull up to the cap through Permit2, for 30 days.
await (await permit2.approve(USDC, CONTRACTS.universalRouter, amountInMaximum, now + 30n * 86_400n)).wait();

// 3. The plan: swap for an exact output, pay what it actually cost (at most the cap), take the BTC.
const CL_SWAP_EXACT_OUT = "09";
const SETTLE_ALL = "0c";
const TAKE = "0e";
const MSG_SENDER = "0x0000000000000000000000000000000000000001"; // the router reads this as "the caller"

const swap = abi.encode(
  [
    "tuple(address currencyOut, tuple(address intermediateCurrency, uint24 fee, address hooks, address poolManager, bytes hookData, bytes32 parameters)[] path, uint128 amountOut, uint128 amountInMaximum)",
  ],
  [{ currencyOut: BTC, path, amountOut, amountInMaximum }],
);
const settle = abi.encode(["address", "uint256"], [USDC, amountInMaximum]);
const take = abi.encode(["address", "address", "uint256"], [BTC, MSG_SENDER, 0n]);
const plan = abi.encode(
  ["bytes", "bytes[]"],
  ["0x" + CL_SWAP_EXACT_OUT + SETTLE_ALL + TAKE, [swap, settle, take]],
);

// 4. One INFI_SWAP (0x10) command carrying the plan. Reverts after 20 minutes.
const INFI_SWAP = "0x10";
const tx = await router.execute(INFI_SWAP, [plan], now + 1_200n);
console.log("swap:", (await tx.wait())?.status);
```

## Multi-Hop

A multi-hop swap passes through more than one pool in a single swap action. Every seeded pool pairs a token with USDC, so BTC to AAPL routes BTC → USDC through BTC/USDC 0.30%, then USDC → AAPL through AAPL/USDC 0.30%. The app searches routes of up to two hops and quotes each candidate.

**Path order.** For exact input, each `intermediateCurrency` is the currency the hop produces, listed from the input end: USDC, then AAPL. The same path array serves `quoteExactInput` and `CL_SWAP_EXACT_IN`. For exact output it is the input of each hop, still listed from the input end: buying AAPL with BTC uses BTC, then USDC, with AAPL as `exactCurrency`. The router walks an exact-output path backwards from the output.

viem:

```ts
import { formatUnits, parseAbi, parseUnits } from "viem";
import { BTC, CONTRACTS, POOL_KEY, publicClient, USDC } from "./setup";

export const AAPL = "0x410432915aB0c6BA27d3B4A86B1E01b62942219F"; // 18 decimals

// The AAPL/USDC 0.30% pool. AAPL sorts below USDC, so it is currency0 here.
export const AAPL_POOL_KEY = {
  currency0: AAPL,
  currency1: USDC,
  hooks: "0x0000000000000000000000000000000000000000",
  poolManager: CONTRACTS.clPoolManager,
  fee: 2011,
  parameters: "0x00000000000000000000000000000000000000000000000000000000003c0000",
} as const;

const quoterAbi = parseAbi([
  "struct PathKey { address intermediateCurrency; uint24 fee; address hooks; address poolManager; bytes hookData; bytes32 parameters; }",
  "struct QuoteExactParams { address exactCurrency; PathKey[] path; uint128 exactAmount; }",
  "function quoteExactInput(QuoteExactParams params) returns (uint256 amountOut, uint256 gasEstimate)",
]);

export const amountIn = parseUnits("0.001", 8); // sell 0.001 BTC

// An exact-input path lists each hop by the currency it produces: BTC -> USDC, then USDC -> AAPL.
export const path = [
  {
    intermediateCurrency: USDC,
    fee: POOL_KEY.fee,
    hooks: POOL_KEY.hooks,
    poolManager: POOL_KEY.poolManager,
    hookData: "0x",
    parameters: POOL_KEY.parameters,
  },
  {
    intermediateCurrency: AAPL,
    fee: AAPL_POOL_KEY.fee,
    hooks: AAPL_POOL_KEY.hooks,
    poolManager: AAPL_POOL_KEY.poolManager,
    hookData: "0x",
    parameters: AAPL_POOL_KEY.parameters,
  },
] as const;

const { result } = await publicClient.simulateContract({
  address: CONTRACTS.clQuoter,
  abi: quoterAbi,
  functionName: "quoteExactInput",
  args: [{ exactCurrency: BTC, path, exactAmount: amountIn }],
});
export const amountOut = result[0];

console.log("0.001 BTC ->", formatUnits(amountOut, 18), "AAPL");
```

ethers v6:

```ts
import { Contract, formatUnits, parseUnits } from "ethers";
import { BTC, CONTRACTS, POOL_KEY, provider, USDC } from "./setup";

export const AAPL = "0x410432915aB0c6BA27d3B4A86B1E01b62942219F"; // 18 decimals

// The AAPL/USDC 0.30% pool. AAPL sorts below USDC, so it is currency0 here.
export const AAPL_POOL_KEY = {
  currency0: AAPL,
  currency1: USDC,
  hooks: "0x0000000000000000000000000000000000000000",
  poolManager: CONTRACTS.clPoolManager,
  fee: 2011,
  parameters: "0x00000000000000000000000000000000000000000000000000000000003c0000",
} as const;

const quoter = new Contract(
  CONTRACTS.clQuoter,
  [
    "function quoteExactInput((address exactCurrency, (address intermediateCurrency, uint24 fee, address hooks, address poolManager, bytes hookData, bytes32 parameters)[] path, uint128 exactAmount) params) returns (uint256 amountOut, uint256 gasEstimate)",
  ],
  provider,
);

export const amountIn = parseUnits("0.001", 8); // sell 0.001 BTC

// An exact-input path lists each hop by the currency it produces: BTC -> USDC, then USDC -> AAPL.
export const path = [
  {
    intermediateCurrency: USDC,
    fee: POOL_KEY.fee,
    hooks: POOL_KEY.hooks,
    poolManager: POOL_KEY.poolManager,
    hookData: "0x",
    parameters: POOL_KEY.parameters,
  },
  {
    intermediateCurrency: AAPL,
    fee: AAPL_POOL_KEY.fee,
    hooks: AAPL_POOL_KEY.hooks,
    poolManager: AAPL_POOL_KEY.poolManager,
    hookData: "0x",
    parameters: AAPL_POOL_KEY.parameters,
  },
] as const;

const [quotedOut]: [bigint, bigint] = await quoter.quoteExactInput.staticCall({
  exactCurrency: BTC,
  path,
  exactAmount: amountIn,
});
export const amountOut = quotedOut;

console.log("0.001 BTC ->", formatUnits(amountOut, 18), "AAPL");
```

**Settlement.** The swap action runs both hops against the Vault's running balances. The first hop credits the plan with USDC and the second hop spends exactly that USDC, so it nets to zero and is never transferred. Only the first currency is settled and the last taken: `SETTLE_ALL` pays the BTC and `TAKE` withdraws the AAPL.

**Slippage.** `amountOutMinimum` applies to the final output only. Intermediate amounts have no limit of their own; a move in either pool shows up in the AAPL you receive.

viem:

```ts
import { encodeAbiParameters, erc20Abi, maxUint256, parseAbi, parseAbiParameters, type Hex } from "viem";
import { BTC, CONTRACTS, publicClient, walletClient } from "./setup";
import { AAPL, amountIn, amountOut, path } from "./quote-multi-hop";

const permit2Abi = parseAbi([
  "function approve(address token, address spender, uint160 amount, uint48 expiration)",
]);
const routerAbi = parseAbi([
  "function execute(bytes commands, bytes[] inputs, uint256 deadline) payable",
]);

const block = await publicClient.getBlock();

// 1. Let Permit2 move BTC. Once per token; skip it when the allowance already covers you.
const approveHash = await walletClient.writeContract({
  address: BTC,
  abi: erc20Abi,
  functionName: "approve",
  args: [CONTRACTS.permit2, maxUint256],
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });

// 2. Let the Universal Router pull this amount through Permit2, for 30 days.
const permitHash = await walletClient.writeContract({
  address: CONTRACTS.permit2,
  abi: permit2Abi,
  functionName: "approve",
  args: [BTC, CONTRACTS.universalRouter, amountIn, Number(block.timestamp + 30n * 86_400n)],
});
await publicClient.waitForTransactionReceipt({ hash: permitHash });

// 3. The plan: one swap action walks both hops; pay the BTC, take the AAPL.
//    The USDC between the hops nets to zero inside the Vault and is never settled.
const CL_SWAP_EXACT_IN = "07";
const SETTLE_ALL = "0c";
const TAKE = "0e";
const MSG_SENDER = "0x0000000000000000000000000000000000000001"; // the router reads this as "the caller"

const slippageBps = 50n; // 0.5%, on the final output only
const amountOutMinimum = (amountOut * (10_000n - slippageBps)) / 10_000n;

const swap = encodeAbiParameters(
  parseAbiParameters(
    "(address currencyIn, (address intermediateCurrency, uint24 fee, address hooks, address poolManager, bytes hookData, bytes32 parameters)[] path, uint128 amountIn, uint128 amountOutMinimum)",
  ),
  [{ currencyIn: BTC, path, amountIn, amountOutMinimum }],
);
const settle = encodeAbiParameters(parseAbiParameters("address, uint256"), [BTC, amountIn]);
const take = encodeAbiParameters(parseAbiParameters("address, address, uint256"), [
  AAPL,
  MSG_SENDER,
  0n,
]);
const plan = encodeAbiParameters(parseAbiParameters("bytes, bytes[]"), [
  ("0x" + CL_SWAP_EXACT_IN + SETTLE_ALL + TAKE) as Hex,
  [swap, settle, take],
]);

// 4. One INFI_SWAP (0x10) command carrying the plan. Reverts after 20 minutes.
const INFI_SWAP = "0x10";
const hash = await walletClient.writeContract({
  address: CONTRACTS.universalRouter,
  abi: routerAbi,
  functionName: "execute",
  args: [INFI_SWAP, [plan], block.timestamp + 1_200n],
});
console.log("swap:", (await publicClient.waitForTransactionReceipt({ hash })).status);
```

ethers v6:

```ts
import { AbiCoder, Contract, MaxUint256 } from "ethers";
import { BTC, CONTRACTS, provider, signer } from "./setup";
import { AAPL, amountIn, amountOut, path } from "./quote-multi-hop";

const abi = AbiCoder.defaultAbiCoder();
const btc = new Contract(BTC, ["function approve(address spender, uint256 amount) returns (bool)"], signer);
const permit2 = new Contract(
  CONTRACTS.permit2,
  ["function approve(address token, address spender, uint160 amount, uint48 expiration)"],
  signer,
);
const router = new Contract(
  CONTRACTS.universalRouter,
  ["function execute(bytes commands, bytes[] inputs, uint256 deadline) payable"],
  signer,
);

const block = await provider.getBlock("latest");
if (!block) throw new Error("no latest block");
const now = BigInt(block.timestamp);

// 1. Let Permit2 move BTC. Once per token; skip it when the allowance already covers you.
await (await btc.approve(CONTRACTS.permit2, MaxUint256)).wait();

// 2. Let the Universal Router pull this amount through Permit2, for 30 days.
await (await permit2.approve(BTC, CONTRACTS.universalRouter, amountIn, now + 30n * 86_400n)).wait();

// 3. The plan: one swap action walks both hops; pay the BTC, take the AAPL.
//    The USDC between the hops nets to zero inside the Vault and is never settled.
const CL_SWAP_EXACT_IN = "07";
const SETTLE_ALL = "0c";
const TAKE = "0e";
const MSG_SENDER = "0x0000000000000000000000000000000000000001"; // the router reads this as "the caller"

const slippageBps = 50n; // 0.5%, on the final output only
const amountOutMinimum = (amountOut * (10_000n - slippageBps)) / 10_000n;

const swap = abi.encode(
  [
    "tuple(address currencyIn, tuple(address intermediateCurrency, uint24 fee, address hooks, address poolManager, bytes hookData, bytes32 parameters)[] path, uint128 amountIn, uint128 amountOutMinimum)",
  ],
  [{ currencyIn: BTC, path, amountIn, amountOutMinimum }],
);
const settle = abi.encode(["address", "uint256"], [BTC, amountIn]);
const take = abi.encode(["address", "address", "uint256"], [AAPL, MSG_SENDER, 0n]);
const plan = abi.encode(
  ["bytes", "bytes[]"],
  ["0x" + CL_SWAP_EXACT_IN + SETTLE_ALL + TAKE, [swap, settle, take]],
);

// 4. One INFI_SWAP (0x10) command carrying the plan. Reverts after 20 minutes.
const INFI_SWAP = "0x10";
const tx = await router.execute(INFI_SWAP, [plan], now + 1_200n);
console.log("swap:", (await tx.wait())?.status);
```

## Native ETH

None of the seeded pools uses native ETH or WETH9, so the examples above trade only ERC-20 tokens. Infinity supports both, and the app's encoder handles both, choosing the plan from the pool's currency.

**Native currency.** In a pool key, native ETH is `Currency` `0x0000…0000`. It is always `currency0`, since no address sorts below zero. It never goes through Permit2: you pay it as the transaction's `value`, and the router settles the Vault from that. The app sends `value` equal to `amountIn` for an exact-input swap and `amountInMaximum` for an exact-output one.

**WETH9 is a different pool.** WETH9 (`0x4200…0006`) is an ordinary ERC-20. An ETH/USDC pool and a WETH9/USDC pool have different keys, different IDs and separate liquidity. To pay ETH into a WETH9 pool, the router wraps it first.

**What the app sends.** Universal Router commands and plan actions are separate ID spaces, so `0x0b` and `0x0c` below each mean two things.

- ETH in, native pool: `INFI_SWAP` with `SETTLE_ALL` on `0x0`, paid from `value`. For exact output, a trailing `SWEEP` command (`0x04`) on `0x0` returns the ETH the swap did not use.
- ETH in, WETH9 pool: `WRAP_ETH` (`0x0b`) wraps `value` into the router's own balance, then the plan uses `SETTLE` (`0x0b`) on WETH9 with amount `0` (the full debt) and `payerIsUser` false, paying from that balance. For exact output, a trailing `UNWRAP_WETH` (`0x0c`) returns the unused WETH9 as ETH.
- ETH out, native pool: `TAKE` on `0x0` sends ETH straight to you.
- ETH out, WETH9 pool: `TAKE` sends WETH9 to the router (`ADDRESS_THIS`, `0x…02`), then `UNWRAP_WETH` unwraps it to you, with the minimum output (exact input) or the exact amount (exact output) as its floor.

Without the trailing `SWEEP` or `UNWRAP_WETH`, unused ETH stays in the router, where the next caller can sweep it.
