Documentation
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.senderitself, 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:
// 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.
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.