# RelaySwaps contract

> The withdraw-and-swap processor: forwards a withdrawal to an allowlisted aggregator with proof-bound calldata, covering swap-out, reshield, and cross-chain delivery, with a zap path for yield-share notes.

RelaySwaps is the withdraw-and-swap processor and the on-chain half of the protocol's **intents integration**: intent-based routing through a DEX aggregator (LiFi in the shipped integration, abstracted so another provider can be used). Like [PrivacyPoolRelay](/protocol/contracts/privacy-pool-relay) it calls [`PoolVault`](/protocol/contracts/pool-vault)`.transact` and receives the public output, but instead of paying a recipient it hands the withdrawn value to the aggregator with calldata the user committed in their proof. That one shape covers [withdraw-and-swap](/operations/withdraw-and-swap) to any token, [reshield](/operations/reshield) (swap and redeposit in the same transaction), and cross-chain delivery through a bridge. It lives in `packages/swaps`, has its own deploy script, and is not part of the core deployment. In production it is deployed on [Ethereum mainnet and BNB Chain](/deployments) but not on Citrea, which is not a LiFi network.

## What it does

`execute` pulls the withdrawal from the pool, pays the relayer's fee in the input token, then forwards the remainder to an allowlisted swap router with the exact `swapCalldata` bound in the proof. The aggregator (LiFi in the shipped integration) performs every post-swap step: a direct transfer to the recipient, a `contractCalls` deposit back into the [Entrypoint](/protocol/contracts/entrypoint), or a bridge hop. RelaySwaps never parses that calldata; it cannot, for a generic aggregator. What stands in for parsing is two guarantees enforced on-chain:

-   **Allowlisted targets.** Both `swapTarget` (the router receiving the call) and `approvalAddress` (the address receiving the scoped ERC-20 approval, which for LiFi v4 is a separate `ERC20Proxy`) must be in the owner-curated `approvedRouters` set, else `RelaySwaps_InvalidSwapTarget` / `RelaySwaps_InvalidApprovalAddress`.
-   **Proof-bound routing.** The whole `SwapRouting` struct, calldata included, is ABI-encoded into `transactParams.data`, which the pool checks against the proof's `context`. A relayer that alters the route, the recipient, the slippage floor, or the fee after proving cannot land the transaction.

`executeUnwrapped(..., zap)` is the second entry point, for spending a [yield-share](/concepts/yield-shares) note. It shares the same body; the only difference is that after the pool pays the withdrawn ppUSDC, the contract redeems the shares to their underlying through an allowlisted [PPYieldTokenZap](/protocol/contracts/pp-yield-token-zap) before the swap runs. Passing `zap == address(0)` is the plain path.

## Order of operations

1.  Read `tokenIdOut` and `amountOut` from the proof; reject a zero withdrawal.
2.  Decode `SwapRouting` from `transactParams.data` (already bound in the proof's `context`).
3.  Require non-zero `swapTarget`, `approvalAddress`, and `dustRecipient`, and that the first two are allowlisted.
4.  Require `msg.value == nativeGas`. If `recipient` is the zero address (reshield or cross-chain, where output is delivered elsewhere), `nativeGas` and `minOutputAmount` must both be zero. If `recipient` is set, `minOutputAmount` must be positive, because a zero floor would disable the output check and let the calldata route output anywhere.
5.  Require `feeAmount <= Entrypoint.assets(token).maxRelayFee` (a configured `0` means no cap) and `feeAmount <= amountOut`.
6.  Snapshot ETH, input-token, and output-token balances, and the recipient's output-token balance. For `executeUnwrapped`, require the zap is in `approvedZaps` and `zap.VAULT() == tokenIdOut`.
7.  Call `PoolVault.transact`; the pool pays `amountOut` to this contract.
8.  For `executeUnwrapped`, redeem the shares through the zap so the rest of the flow runs on the underlying; the fee, committed in shares, is converted alongside.
9.  Pay `feeAmount` to `feeRecipient` (`RelaySwaps_ZeroFeeRecipient` if unset while a fee is due).
10. If `swapValue > 0` (the B2 path for a bridge that needs native messaging value), run `gasSwapCalldata` against the same allowlisted router with an approval capped at `gasSwapMaxInput` (itself capped at `amountOut - feeAmount`), and require the contract's ETH rose by at least `swapValue`. Only valid on the ERC-20 input branch.
11. Execute the main swap: a native-input note forwards the value with the call; an ERC-20 note approves `approvalAddress` for the net amount, calls, then resets the approval to zero.
12. If a recipient was named, require its output-token balance rose by at least `minOutputAmount`, then forward `nativeGas` to it.
13. Sweep dust to `dustRecipient` using balance deltas against the step-6 snapshots, so concurrent executions cannot drain each other and pre-existing balances are never touched.

## SwapRouting

The struct the proof commits to. Every field is fixed at proving time.

`swapTarget`, `approvalAddress` The allowlisted aggregator entry point and the address that receives the scoped ERC-20 approval.
`outputToken`, `recipient`, `fallbackAddress` What the swap produces and where it lands. `recipient == 0` marks a flow whose output is delivered by the aggregator (reshield deposit or cross-chain).
`feeRecipient`, `feeAmount` The relayer's fee, in the input token (in shares for a yield note, converted at unwrap).
`minOutputAmount`, `nativeGas`, `dustRecipient` The recipient-side output floor, the ETH gas top-up forwarded to the recipient, and where residue goes.
`swapValue`, `gasSwapMaxInput`, `gasSwapCalldata` The B2 gas-swap leg: mint native messaging value for a bridge in-contract instead of asking the user to front source-chain ETH. `swapValue == 0` skips the leg entirely.
`swapCalldata` The full aggregator calldata, executed verbatim.
`dustRescue` A `DustRescue` blob (`chainId`, `tokenId`, `isCanonical`, `spendingPubKey`, `viewingPubKey`) that RelaySwaps never reads or validates. It rides in the struct purely to inherit its proof binding, so an off-chain dust-rescue service can deliver a cross-chain reshield's leftover to a [stealth address](/concepts/stealth-addresses) the user committed to.

`ROUTING_TYPEHASH()` returns the EIP-712 type hash of this struct. It exists so the relayer and SDK can assert at boot that their encoding of `SwapRouting` matches the contract's layout byte for byte (`RelaySwapsLayoutMismatch` on the SDK side), and it is generated from the compiler's own AST rather than hand-maintained.

## Owner functions

The owner is the only privileged role, and its surface is the two allowlists plus recovery.

`approveRouter(address)` / `removeRouter(address)` Maintain `approvedRouters`. The constructor seeds one router.
`approveZap(address)` / `removeZap(address)` Maintain `approvedZaps`. `approveZap` cross-checks the zap against its wrapper: `zap.A_TOKEN()` must be the wrapper's `asset()`, `zap.UNDERLYING()` must be that aToken's underlying and must be a deployed non-native token, and the share must be an enabled pool asset on the Entrypoint. `approvedZapList()` returns the whole set so a relayer can derive its share-to-zap map on-chain at boot.
`rescue(address _token, address _to, uint256 _amount)` Moves ETH or tokens stranded outside an `execute` window, for example an asynchronous bridge refund. `execute` is atomic and `nonReentrant`, so rescue cannot interleave with a live swap.
`changeOwnership(address)` Transfers ownership.
`renounceOwnership()` Always reverts (`RelaySwaps_RenounceDisabled`). Renouncing would freeze both allowlists with no way to add a router or zap again.

`receive()` accepts ETH from anyone: the pool on a native withdrawal, the aggregator on a refund or residue, and bridges on asynchronous refunds. Pre-existing ETH is never swept by `execute` (delta-only baseline); it is recovered with `rescue`.

## Errors you'll see

`RelaySwaps_InvalidSwapTarget()` / `RelaySwaps_InvalidApprovalAddress()` The quote named a router or approval address the owner has not allowlisted. The SDK checks `approvedRouters` before proving, so seeing this on-chain means the allowlist changed in between.
`RelaySwaps_SwapFailed()` The aggregator call reverted or returned false. The whole transaction reverts and the pool's state is unchanged, so the note is not spent; re-quote and retry.
`RelaySwaps_InsufficientOutput()` The recipient received less than `minOutputAmount`. Slippage moved past the floor between quote and execution.
`RelaySwaps_ZeroMinOutputWithRecipient()` A recipient was named but the output floor was zero. The SDK never produces this; it guards against a hand-built routing.
`RelaySwaps_NativeGasWithoutRecipient()` / `RelaySwaps_MinOutputWithoutRecipient()` A reshield or cross-chain routing (zero recipient) carried a gas top-up or an output floor, neither of which has anywhere to apply.
`RelaySwaps_FeeExceedsMax()` / `RelaySwaps_FeeExceedsWithdrawal()` The committed fee is above the asset's `maxRelayFee` or above the withdrawn amount.
`RelaySwaps_GasSwapNativeInput()` / `RelaySwaps_GasSwapMaxInputExceedsWithdrawal()` / `RelaySwaps_GasSwapInsufficient()` / `RelaySwaps_InputBalanceDeficit()` B2 gas-swap guards: the leg was requested on a native-input note, its approval cap exceeded the net proceeds, it minted less ETH than `swapValue`, or the two swaps together consumed more input than this withdrawal supplied.
`RelaySwaps_InvalidZap()` / `RelaySwaps_ZapShareMismatch()` / `RelaySwaps_UnwrapZeroOutput()` `executeUnwrapped` guards: the zap is not allowlisted, its `VAULT()` is not the withdrawn share token, or the redeem produced no underlying.
`RelaySwaps_ZapATokenMismatch()` / `RelaySwaps_ZapUnderlyingMismatch()` / `RelaySwaps_ZapShareNotPoolAsset()` / `RelaySwaps_ZapInvalidUnderlying()` `approveZap` cross-checks failed; the owner tried to allowlist a zap that does not match its wrapper or whose share is not an enabled pool asset.

## Trust model

The owner curates which aggregators may receive funds and which zaps may unwrap shares; a compromised owner could allowlist a malicious router, but could not change any routing a user has already proven, since that is fixed in the proof. Users trust the aggregator (LiFi) to execute the committed calldata honestly and the bridge, when one is involved, to deliver on the destination chain. The relayer that submits the transaction is trusted for liveness only: it cannot alter the route, redirect output, or take more than the committed fee.

Source: `v2-monorepo/packages/swaps/contracts/src/RelaySwaps.sol`
