# Approvals

Every entrypoint that moves your money pulls it with `transferFrom`, which means you must
`approve` the contract that does the pulling — and that contract is **not the same one you
are calling** in two of the five cases.

Getting this wrong is the most common integration failure, and the error message does not
help: you get a bare ERC-20 allowance revert naming neither the expected spender nor the
amount.

## The matrix

| You are calling | Approve | Asset | Amount |
|---|---|---|---|
| `factory.launchToken` | **the factory** | quote asset (USDC) | `launchFee()` |
| `launchAndBuy.launchAndBuy` | **the router** | quote asset (USDC) | `launchFee() + quoteIn` — one approval covers both |
| `curve.buy` | **the curve** | quote asset (USDC) | `quoteIn` |
| `curve.sell` | **the curve** | the **memecoin** (18 dp) | `tokensIn` |
| a graduated-pool swap | **Permit2, then the router** | whichever asset you spend | see below |
| `escrow.claim` / `claimToken` | nothing | — | claims are pull-only |

Two things people get wrong reading that table:

- **The two launch paths approve different contracts.** The factory pulls the fee from
  `msg.sender` itself, so a direct launch approves the *factory*. The router pulls the fee
  *and* the opening buy, so the atomic path approves the *router* — and approving the factory
  there does nothing.
- **Both curve legs approve the curve**, but for different assets. A buy spends the quote
  asset; a sell spends the memecoin. The spender is the same, the token is not.

## Pool swaps need two approvals, not one

UniversalRouter does not pull with a plain ERC-20 allowance. It pulls through **Permit2**, so
a pool swap needs two transactions before the swap itself:

```ts
// 1. one-time, per token: let Permit2 move this asset on your behalf
await writeContract({
  address: token, abi: erc20Abi, functionName: "approve",
  args: [PERMIT2, MAX_UINT160],
});

// 2. per spender, with an expiry: let the router draw from Permit2
await writeContract({
  address: PERMIT2, abi: permit2Abi, functionName: "approve",
  args: [token, UNIVERSAL_ROUTER, MAX_UINT160, Math.floor(Date.now() / 1000) + 30 * 86400],
});
```

Skipping the second step reverts with **`AllowanceExpired(uint256)`** — a name that reads
like a grant lapsed when the truth is that one was never made. If you see that selector
(`0xd81b2f2e`), you are missing the Permit2 approval, not the ERC-20 one.

> [!TIP]
> Check both allowances before offering a swap button. `permit2.allowance(owner, token,
> spender)` returns `(amount, expiration, nonce)` — treat an expiry in the past as
> unapproved, not as approved-with-zero.

## Zero launch fee needs no approval

If `factory.launchFee()` returns zero there is nothing to pull, and a direct launch needs no
approval at all. Read the fee rather than assuming it — it is owner-settable and it is part of
the economics digest, so it can change between your quote and your submit.