> ## Documentation Index
> Fetch the complete documentation index at: https://docs.windrose.market/llms.txt
> Use this file to discover all available pages before exploring further.

# Router

> The one contract traders approve: dollar-in and dollar-out trading on any curve, quote-asset trading with deadlines, refunds of untaken quote and automatic graduation.

`Router` lets a trader who only holds the chain's dollar buy or sell a launch quoted in any synthetic currency in one transaction: it mints the synthetic through the [vault](/protocol/contracts/fx-vault), trades on the [curve](/protocol/contracts/bonding-curve), and redeems whatever the curve did not take. It also wraps plain quote-asset trades with a deadline and triggers graduation after the buy that fills a curve. It has no owner, no state between calls and never holds funds.

Source: `contracts/src/Router.sol` (`ReentrancyGuard`). `registry` and `usdc` (`registry.usdc()`) are immutables. Addresses: [Addresses](/protocol/addresses).

## Dollar paths

```solidity theme={"system"}
function buyWithUSDC(address curve, uint256 usdcIn, uint256 minTokensOut, uint256 deadline, bytes[] calldata priceUpdate)
    external payable nonReentrant returns (uint256 tokensOut);

function sellForUSDC(address curve, uint256 tokensIn, uint256 minUsdcOut, uint256 deadline, bytes[] calldata priceUpdate)
    external payable nonReentrant returns (uint256 usdcOut);
```

### `buyWithUSDC`

<Steps>
  <Step title="Checks">
    `Expired` when `block.timestamp > deadline`, `ZeroAmount` for `usdcIn == 0`. The currency is `registry.get(curve.code())`.
  </Step>

  <Step title="Obtain the quote asset">
    `usdcIn` raw dollars are pulled from the caller (approve the dollar to the router first). If the quote asset is the dollar (a USD launch), `quoteIn = usdcIn` and the whole `msg.value` is refunded. If the currency is tier 2, the router computes `fee = registry.oracle().updateFee(priceUpdate)` (`InsufficientFee` if `msg.value` is lower), approves the vault and calls `vault.mint{value: fee}(usdcIn, 0, priceUpdate)`; the minted synthetic amount is `quoteIn`, and `msg.value - fee` is refunded. Any other tier-1 asset (EURC) reverts `UseQuoteAsset`.
  </Step>

  <Step title="Buy">
    The router approves the curve for `quoteIn` and calls `curve.buy(quoteIn, minTokensOut, msg.sender)`: tokens go straight to the trader, and `Slippage` from the curve enforces the floor. The allowance is reset to zero afterwards.
  </Step>

  <Step title="Return what the curve did not take">
    The last buy before graduation is capped. Any quote left on the router is redeemed through the vault (tier 2, with an empty price update) and the dollars are sent to the trader, or transferred back as-is for a USD launch.
  </Step>

  <Step title="Graduate">
    If the curve is now `ready()`, the router calls `graduate()` inside a `try` block: a failure there (for example a pool-side revert) never reverts the trade, and `graduate()` stays callable by anyone. `BoughtWithUSDC(curve, buyer, usdcIn, quoteIn - leftover, tokensOut)` is emitted.
  </Step>
</Steps>

`minTokensOut` is the only slippage floor on this path: the vault mint runs with `minSynthOut = 0`, so a stale or moving oracle rate is reflected in fewer tokens rather than a separate revert. Quote with `quoteBuyWithUSDC` and apply your tolerance to `tokensOut`.

### `sellForUSDC`

Pulls `tokensIn` launch tokens from the caller (approve the token to the router), sells them to the curve with `minQuoteOutRaw = 0` and the router as recipient, then converts: for a USD launch `usdcOut = quoteOut` (full `msg.value` refund); for tier 2 the router pays the oracle fee and calls `vault.redeem{value: fee}(quoteOut, 0, priceUpdate)`; EURC launches revert `UseQuoteAsset`. `Slippage` if `usdcOut < minUsdcOut`. The dollars are transferred to the caller and `SoldForUSDC(curve, seller, tokensIn, quoteOut, usdcOut)` is emitted. Selling cannot close a curve, so this path never graduates.

A redeem below 100% collateral ratio is haircut pro rata (see [FxVault](/protocol/contracts/fx-vault)); `minUsdcOut` catches that too.

## Quote-asset paths

```solidity theme={"system"}
function buy(address curve, uint256 quoteIn, uint256 minTokensOut, uint256 deadline) external nonReentrant returns (uint256 tokensOut);
function sell(address curve, uint256 tokensIn, uint256 minQuoteOut, uint256 deadline) external nonReentrant returns (uint256 quoteOut);
```

`buy` pulls `quoteIn` of `curve.quoteAsset()` (approve it to the router), buys for `msg.sender` with `minTokensOut`, returns any untaken quote, and tries to graduate when the curve is ready. `sell` pulls the tokens, sells with `minQuoteOut` and pays the caller directly from the curve. Both revert `Expired` past the deadline and `ZeroAmount` for zero input. These paths work for every currency, including EUR launches quoted in EURC.

Trading directly on the curve is equivalent, minus the deadline, the refund of untaken quote and the automatic graduation.

## Quotes

```solidity theme={"system"}
function quoteBuyWithUSDC(address curve, uint256 usdcIn) external view returns (uint256 tokensOut, uint256 quoteIn, uint256 quoteUsed);
function quoteSellForUSDC(address curve, uint256 tokensIn) external view returns (uint256 usdcOut, uint256 quoteOut);
```

`quoteBuyWithUSDC` previews the vault mint (`previewMint`, so the current fresh or stale fee is included) and then `curve.quoteBuy`; `quoteIn` is the synthetic that would be minted and `quoteUsed` what the curve would take. It returns all zeros for a tier-1 non-dollar launch. `quoteSellForUSDC` previews `curve.quoteSell` and then `previewRedeem` (haircut included); `usdcOut` is zero for EURC launches. Both revert with the oracle's `StalePrice` when the rate is older than the stale limit, since `previewMint` and `previewRedeem` call `oracle.rate`.

For quote-asset trades use `curve.quoteBuy` and `curve.quoteSell` directly.

## Approvals, value and deadlines

| Call          | Approve to the router        | `msg.value`                                                            |
| ------------- | ---------------------------- | ---------------------------------------------------------------------- |
| `buyWithUSDC` | the dollar, `usdcIn`         | `registry.oracle().updateFee(priceUpdate)`; `0` with the keeper oracle |
| `sellForUSDC` | the launch token, `tokensIn` | same                                                                   |
| `buy`         | the quote asset, `quoteIn`   | not payable                                                            |
| `sell`        | the launch token, `tokensIn` | not payable                                                            |

With `KeeperFxOracle` (every deployment today) pass `priceUpdate = []` and `value = 0`. If the registry is ever pointed at `PythFxOracle`, pass the Hermes update data and `value = updateFee(priceUpdate)`; excess value is always refunded (`RefundFailed` if the caller rejects it). Note that the vault forwards the fee to its own oracle: see the caveat in [Vault operations](/protocol/integrate/vault-operations).

Deadlines are unix timestamps. The app uses now + 600 seconds.

## Events and errors

```solidity theme={"system"}
event BoughtWithUSDC(address indexed curve, address indexed buyer, uint256 usdcIn, uint256 quoteIn, uint256 tokensOut);
event SoldForUSDC(address indexed curve, address indexed seller, uint256 tokensIn, uint256 quoteOut, uint256 usdcOut);
```

| Error               | When                                                                      |
| ------------------- | ------------------------------------------------------------------------- |
| `Expired()`         | `block.timestamp > deadline`                                              |
| `ZeroAmount()`      | zero `usdcIn`, `quoteIn` or `tokensIn`                                    |
| `UseQuoteAsset()`   | dollar path on a tier-1 launch whose quote asset is not the dollar (EURC) |
| `Slippage()`        | `sellForUSDC` below `minUsdcOut` (buys rely on the curve's `Slippage`)    |
| `InsufficientFee()` | `msg.value` below the oracle update fee on a tier-2 dollar path           |
| `RefundFailed()`    | the caller rejected a value refund                                        |

Curve errors (`CurveClosed`, `Slippage`, `ExceedsSold`) and vault errors (`StalePrice`, `LiabilityCapExceeded`, `CollateralRatioTooLow`, paused) bubble up unchanged. `Router.t.sol` covers the synthetic and USD dollar paths, the overshoot refund with auto-graduation, the EURC revert, the quote-asset paths, expiry and slippage.

## Example

<CodeGroup>
  ```ts viem theme={"system"}
  import { createWalletClient, createPublicClient, http, parseUnits } from "viem";
  import { routerAbi, erc20Abi } from "@launchpad/abis";

  const router = "0xae4E359895B7a232eEA55238256dcbDa9851c1CF"; // Robinhood Chain
  const usdg = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168";
  const curve = "0x..."; // from LaunchCreated or LaunchFactory.curveOfToken(token)

  const usdcIn = parseUnits("25", 6);
  const [tokensOut] = await publicClient.readContract({ address: router, abi: routerAbi, functionName: "quoteBuyWithUSDC", args: [curve, usdcIn] });
  const minTokensOut = (tokensOut * 98n) / 100n; // 2% tolerance
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 600);

  await walletClient.writeContract({ address: usdg, abi: erc20Abi, functionName: "approve", args: [router, usdcIn] });
  await walletClient.writeContract({
    address: router, abi: routerAbi, functionName: "buyWithUSDC",
    args: [curve, usdcIn, minTokensOut, deadline, []], value: 0n,
  });
  ```

  ```bash cast theme={"system"}
  ROUTER=0xae4E359895B7a232eEA55238256dcbDa9851c1CF
  USDG=0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
  RPC=https://rpc.mainnet.chain.robinhood.com
  cast call $ROUTER "quoteBuyWithUSDC(address,uint256)(uint256,uint256,uint256)" $CURVE 25000000 --rpc-url $RPC
  cast send $USDG "approve(address,uint256)" $ROUTER 25000000 --rpc-url $RPC --account windrose
  cast send $ROUTER "buyWithUSDC(address,uint256,uint256,uint256,bytes[])" $CURVE 25000000 $MIN_OUT $(( $(date +%s) + 600 )) "[]" --rpc-url $RPC --account windrose
  ```
</CodeGroup>

The full flow, including creation and post-graduation reads, is in [Launch lifecycle](/protocol/integrate/launch-lifecycle).
