Approve · Permit · Permit2
Symptom
You call commitToOffer on an ERC-20-priced offer and the tx reverts with InsufficientAllowance, ERC20: insufficient allowance, or a similar transfer error.
Causes
1. Spender mismatch
You approved the wrong contract. The spender must be the Diamond, not the voucher (rNFT) contract.
// WRONG
await token.approve(voucherContract, amount)
// RIGHT
await token.approve(diamondAddress, amount)
// EVEN BETTER (handled by the SDK)
await sdk.approveExchangeToken(offerId, amount)2. Amount too small
You approved price but didn't account for protocol/agent fees, or the offer has a buyer deposit.
For a typical offer:
- Buyer pays
offer.price. - Approval must be ≥
offer.price.
For offers with non-zero buyer deposit:
- Buyer pays
offer.price + offer.buyerCancelPenalty... no, wait:buyerCancelPenaltyonly matters on cancel. The amount transferred at commit isoffer.price.
Approve a comfortable surplus (e.g. offer.price * 2) to avoid this.
3. EIP-2612 Permit vs. ERC-3009 confusion
USDC uses ERC-3009 (receiveWithAuthorization), not EIP-2612 Permit. If you sign a Permit and send it for USDC, it'll either revert or be silently ignored.
Use:
ERC3009for USDC, EURC, and other ERC-3009-supporting tokens.Permit(EIP-2612) for DAI, USDS, and others.Permit2as a universal fallback.
See Concepts → Signing → Token-auth meta-tx and x402 → Token-auth strategies.
4. Permit2 not pre-approved
Permit2 requires a one-time approval of the Permit2 contract itself for each token. Once that's done, per-use authorizations are signed envelopes (no on-chain tx). If your buyer hasn't pre-approved Permit2 for the token, the per-use authorization will fail.
const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3"
await token.approve(PERMIT2, ethers.constants.MaxUint256)
// now Permit2-style signed envelopes work5. ERC-20 race conditions
Some tokens (USDT, classic implementations) refuse to update allowance from a non-zero value. You must approve 0 first, then the new amount:
await token.approve(spender, 0).then(t => t.wait())
await token.approve(spender, amount).then(t => t.wait())The SDK's approveExchangeToken handles this for known-problematic tokens.
Fix summary
For 95 % of cases:
await (await sdk.approveExchangeToken(offerId, /* amount */ offer.price)).wait()
await (await sdk.commitToOffer(buyer, offerId)).wait()For gas-optimized UX:
const signed = await sdk.metaTx.signMetaTxWithTokenTransferAuthorization({
functionName: "commitToOffer",
functionArgs: { buyer, offerId },
tokenAuth: { type: "ERC3009" }, // for USDC
})
await sdk.metaTx.relayForwardedMetaTransaction(signed)