# Withdraw and swap

> Leave the pool as a different token, on this chain or another, with the swap route frozen into the proof so the relayer cannot change it.

A withdraw-and-swap spends notes and delivers a *different* token to the recipient: shielded USDC out as USDT, shielded ETH out as USDC on another chain. It is a [withdrawal](/operations/withdraw) whose processor is [RelaySwaps](/protocol/contracts/relay-swaps) rather than PrivacyPoolRelay, and whose payout routing contains the full calldata for a DEX aggregator. Because that routing is bound in the proof's `context`, the relayer that submits the transaction can execute exactly the route you quoted or nothing at all.

The shipped aggregator is LiFi. The SDK abstracts it behind `ISwapQuoteProvider`, so another provider can be supplied; `LiFiSwapQuoteProvider` is the implementation in the box.

## How it works

The flow is quote-heavy because the route has to be fixed before the proof is made, and the proof takes tens of seconds.

1.  **Relayer fee estimate.** Ask the relayer what it will charge to relay this asset and amount. The fee comes out of the withdrawn token before the swap. If you ask for `extraGas`, the relayer adds an ETH top-up for the recipient (priced from gas, clamped at a per-chain ceiling) and folds it into the fee, so you pre-pay it and the relayer is reimbursed.
2.  **Swap quote.** Fetch the aggregator's calldata for `withdrawAmount - feeAmount` of the input token into `outputToken`, from RelaySwaps to the recipient. The response includes a minimum output.
3.  **Committed relayer quote.** Send the calldata back to the relayer, which ABI-encodes the complete `SwapRouting` (target, approval address, output token, recipient, fee, output floor, gas top-up, dust recipient, calldata) and signs it with an expiry.
4.  **Validate.** The SDK decodes the signed commitment once and checks every field against what you asked for, including that the embedded calldata equals yours, that `amountSent = amount + fee`, that the fee is within the Entrypoint's `maxRelayFee`, and that the chain matches the pool.
5.  **Prove.** The transact proof's `amountOut` is `amountSent`, and its `context` commits to the routing, calldata included.
6.  **Relay.** The relayer calls `RelaySwaps.execute` (or `executeUnwrapped` for a yield-share note). The contract re-reads `maxRelayFee` on-chain, pulls the withdrawal, pays the fee, runs the swap, requires the recipient's balance rose by at least the floor, forwards the gas top-up, and sweeps dust.

Quotes are short-lived and proof generation is not. If the quote expires while you are proving, nothing has failed; re-quote and prove again.

## How to swap out

Configure the session with a swap quote provider, then run the two calls:

```ts
// At session build time:
//   builder.withSwapQuoteProvider(new LiFiSwapQuoteProvider({ integrator, apiKey }))

const quote = await session.prepareSwapQuote({
    relayerInfo,
    amount: withdrawAmount,        // Hex, in the input token
    tokenId: usdcAddress,          // the shielded asset you are spending
    outputToken: usdtAddress,      // what the recipient receives
    recipientAddress: publicDestination,
    slippage: 0.005,               // optional, fraction
    extraGas: false,               // optional ETH top-up for the recipient
});

console.log("estimated output:", quote.estimatedOutput, "min:", quote.minOutputAmount);

const receipt = await session.relayWithdrawAndSwap({
    inputCommitments: [noteToSpend.commitment],
    amount: withdrawAmount,
    tokenId: usdcAddress,
    recipientAddress: publicDestination,
    outputToken: usdtAddress,
    minOutputAmount: quote.minOutputAmount,
    swapCalldata: quote.swapCalldata,
    selectedQuote: quote.selectedQuote,
    swapTarget: quote.swapTarget,
    approvalAddress: quote.approvalAddress,
    dustRecipient: quote.dustRecipient,
});
console.log("swap tx:", receipt.txHash);
```

Pass `toChainId` to `prepareSwapQuote` for a cross-chain delivery. The aggregator then bridges, and because no output lands on the source chain the on-chain output check is skipped: the routing carries a zero `recipient`, and delivery is enforced by the aggregator's destination-side executor against `destinationMinOutputAmount`. Keep `fallbackAddress` set for cross-chain routes; it is where a failed bridge leg refunds.

## Constraints

-   **The route is frozen at proving time.** That is the guarantee, and it is also why a stale quote means re-proving rather than resubmitting.
-   **Allowlisted routers only.** `swapTarget` and `approvalAddress` must be on the RelaySwaps owner's `approvedRouters`. The SDK checks this before proving and refuses an address outside the list (`UnallowedLiFiAddress`); the contract checks again on-chain.
-   **A real output floor is mandatory on same-chain swaps.** The contract rejects a named recipient with a zero `minOutputAmount`, because a zero floor would let the calldata route output anywhere. The SDK always sets one from the quote.
-   **Yield-share notes unwrap first.** Spending ppUSDC through this path uses `executeUnwrapped`: the relayer supplies the wrapper's zap from an allowlist it derives on-chain at boot, the shares are redeemed to USDC after the vault pays them, and the swap runs on the underlying. Your call is the same; the relayer chooses the entry point from the asset.
-   **Unsupported output tokens are withdrawals.** Swapping into a token the pool does not list is simply a withdrawal that changes asset on the way out. There is no way to hold an unlisted asset privately; to stay in the pool, use [reshield](reshield).
-   **Layout drift is fatal by design.** The SDK and relayer assert the contract's `ROUTING_TYPEHASH` against their own `SwapRouting` encoding (`RelaySwapsLayoutMismatch`), so an outdated client cannot produce a routing the contract decodes differently.

## Behind the scenes

SDK call `session.prepareSwapQuote(...)` then `session.relayWithdrawAndSwap(...)`. Requires `withSwapQuoteProvider(...)` at build time.
Contract method [`RelaySwaps`](/protocol/contracts/relay-swaps)`.execute(proof, transactParams, noteData)`, or `.executeUnwrapped(..., zap)` for a yield share; either calls `PoolVault.transact` and then the aggregator.
Relayer routes `POST /v1/quote/evm/{chainId}/swap` for both the estimate and the committed quote, then `POST /v1/relay/evm/{chainId}/relay-swap`. The relayer publishes its RelaySwaps address as `contracts.relaySwaps` in `/v1/details`; it is absent on a chain without swaps.
Circuit [`transact_NxM`](/protocol/circuits/transact) with `amountOut = amountSent` and `context = keccak(processor, routing)`. Nothing swap-specific is proven; the binding does the work.

## What's next

-   [Reshield](reshield): swap and land back in the pool as a fresh note.
-   [Swap and deposit](swap-and-deposit): the inbound sibling, from any wallet token into a shielded note.
-   [Relaying](relaying): quotes, expiry, and what a relayer can and cannot do.
