Skip to content

Commit ab1884a

Browse files
committed
refine: L7 content clarity and structure
1 parent 81ab59c commit ab1884a

File tree

1 file changed

+61
-23
lines changed
  • src/content/tutorial/6-gasless-transfers/7-usdc-permit

1 file changed

+61
-23
lines changed

src/content/tutorial/6-gasless-transfers/7-usdc-permit/content.md

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,31 @@ terminal:
1313

1414
# USDC with EIP-2612 Permit
1515

16-
In Lesson 6 you built gasless USDT transfers using meta-transactions. USDC uses a different approval method called **EIP-2612 Permit** - a standardized way to approve token spending with signatures. This lesson shows how to adapt your gasless pipeline for USDC.
16+
In Lesson 6 you built gasless USDT transfers using a custom meta-transaction approval. USDC ships with a standardized approval flow called **EIP-2612 Permit**. This lesson explains what that standard changes, why it exists, and how to adapt the gasless pipeline you already wrote so that USDC transfers go through the same relay infrastructure.
1717

1818
---
1919

2020
## Learning Goals
2121

22-
- Understand EIP-2612 Permit vs meta-transaction approvals
23-
- Sign permit messages with version-based domain separators
24-
- Use `transferWithPermit` instead of `transferWithApproval`
25-
- Adapt fee calculation for USDC-specific parameters
22+
- Understand when to prefer EIP-2612 Permit instead of custom meta-transaction approvals.
23+
- Sign permit messages that use the version + chainId domain separator defined by EIP-2612.
24+
- Swap the `transferWithApproval` call for the USDC-specific `transferWithPermit`.
25+
- Adjust fee calculations to respect USDC's 6-decimal precision and Polygon USD pricing.
2626

27-
The core flow stays the same: discover relays, calculate fees, sign approval, submit to relay. Only the approval signature changes.
27+
You already know the broader flow: discover relays, compute fees, sign an approval, relay the transaction. The only moving pieces are the approval signature and the calldata that consumes it. Everything else stays intact.
28+
29+
---
30+
31+
## Background: What EIP-2612 Adds
32+
33+
EIP-2612 is an extension of ERC-20 that lets a token holder authorize spending via an off-chain signature instead of an on-chain `approve()` transaction. The signature uses the shared EIP-712 typed-data format:
34+
35+
- **Domain separator** includes the token name, version, chainId, and contract address so signatures cannot be replayed across chains or forks.
36+
- **Permit struct** defines the spender, allowance value, and deadline in a predictable shape.
37+
38+
Tokens like USDC, DAI, and WETH adopted the standard because it enables wallets and relayers to cover approval gas costs while staying interoperable with any contract that understands permits (for example, Uniswap routers or Aave).
39+
40+
Older tokens such as USDT predate EIP-2612, so they expose custom meta-transaction logic instead. That is why Lesson 6 had to sign the entire `transferWithApproval` function payload, whereas USDC only needs the numeric values that describe the allowance.
2841

2942
---
3043

@@ -74,16 +87,17 @@ const types = {
7487

7588
## Key Differences
7689

77-
| Aspect | USDT Meta-Transaction | USDC Permit (EIP-2612) |
78-
|--------|----------------------|------------------------|
79-
| **Standard** | Custom implementation | EIP-2612 standard |
80-
| **Domain separator** | `salt` (chain-specific) | `version` + `chainId` |
81-
| **Message struct** | `MetaTransaction` | `Permit` |
82-
| **Approval encoding** | `functionSignature` (bytes) | Separate parameters |
83-
| **Expiry** | No deadline | `deadline` parameter |
84-
| **Transfer method** | `transferWithApproval` | `transferWithPermit` |
90+
| Aspect | USDT Meta-Transaction (Lesson 6) | USDC Permit (This Lesson) |
91+
|--------|---------------------------------|---------------------------|
92+
| **Standardization** | Custom, tether-specific | Formalized in EIP-2612 |
93+
| **Domain separator** | Uses `salt` derived from chain | Uses `version` plus `chainId` |
94+
| **Typed struct** | `MetaTransaction` with encoded bytes | `Permit` with discrete fields |
95+
| **Expiry control** | No expiration | Explicit `deadline` |
96+
| **Transfer helper** | `transferWithApproval` | `transferWithPermit` |
8597
| **Method selector** | `0x8d89149b` | `0x36efd16f` |
8698

99+
Keep this table nearby while refactoring; you will touch each of these rows as you migrate the code.
100+
87101
---
88102

89103
## Step 1: Update Contract Addresses
@@ -96,7 +110,7 @@ const USDC_WMATIC_POOL = '0xA374094527e1673A86dE625aa59517c5dE346d32'
96110
const METHOD_SELECTOR_TRANSFER_WITH_PERMIT = '0x36efd16f'
97111
```
98112

99-
USDC uses a different transfer contract and Uniswap pool than USDT. The method selector also changes.
113+
USDC relies on a different relay contract and Uniswap pool than USDT on Polygon. Updating the constants up front prevents subtle bugs later on. For example, querying gas data against the wrong selector yields an optimistic fee that fails on-chain.
100114

101115
---
102116

@@ -141,6 +155,12 @@ const signature = await wallet._signTypedData(domain, types, message)
141155
const { r, s, v } = ethers.utils.splitSignature(signature)
142156
```
143157

158+
Key callouts:
159+
160+
- Fetch the **nonce** from the USDC contract itself. EIP-2612 uses a per-owner nonce to prevent replay.
161+
- Calculate the **approval value** as `transfer + fee`. A permit is just an allowance, so the relay must be allowed to withdraw both the payment to the recipient and its compensation.
162+
- USDC accepts a `MaxUint256` deadline, but production systems usually set a shorter deadline (for example `Math.floor(Date.now() / 1000) + 3600`) to minimize replay windows.
163+
144164
---
145165

146166
## Step 3: Build transferWithPermit Call
@@ -164,7 +184,9 @@ const transferCalldata = transferContract.interface.encodeFunctionData('transfer
164184
])
165185
```
166186

167-
Notice the `deadline` parameter - this replaces USDT's `approval` parameter (which was the approval amount).
187+
`transferWithPermit` consumes the permit signature directly. Compare this to the USDT version: instead of passing an encoded `approve()` call, you now hand the relay the raw signature components plus a deadline.
188+
189+
If you changed the `deadline` value when signing the permit, make sure the same variable is used here. Hardcoding `MaxUint256` in one place and not the other invalidates the signature.
168190

169191
---
170192

@@ -183,7 +205,9 @@ const polPerUsdc = await getPolUsdcPrice(provider) // Query USDC pool
183205
const feeInUSDC = totalPOLCost.mul(1_000_000).div(polPerUsdc).mul(110).div(100)
184206
```
185207

186-
The gas limit for `transferWithPermit` is typically the same as `transferWithApproval` (~72,000), but query the contract to be sure.
208+
- `getRequiredRelayGas` evaluates the gas buffer the forwarder demands for `transferWithPermit`. It usually matches the USDT value (~72,000 gas) but querying removes guesswork.
209+
- USDC keeps **6 decimals**, so multiply/divide by `1_000_000` when converting between POL and USDC. Avoid using `ethers.utils.parseUnits(..., 18)` out of habit.
210+
- The `polPerUsdc` helper should target the USDC/WMATIC pool; pricing against USDT would skew the fee at times when the two stablecoins diverge.
187211

188212
---
189213

@@ -197,6 +221,8 @@ After building the transfer calldata, the rest is identical to Lesson 6:
197221
4. Submit to relay via `HttpClient`
198222
5. Broadcast transaction
199223

224+
If you already wrapped these steps in helper functions, you should not need to touch them. The permit signature simply slots into the existing request payload where the USDT approval bytes previously sat.
225+
200226
---
201227

202228
## ABI Changes
@@ -210,6 +236,8 @@ const TRANSFER_ABI = [
210236
]
211237
```
212238

239+
`transferWithPermit` mirrors OpenZeppelin's relay helper, so the ABI change is straightforward. Keeping the ABI narrowly scoped makes tree-shaking easier if you bundle the tutorial for production later.
240+
213241
---
214242

215243
## Why Two Approval Methods?
@@ -228,23 +256,33 @@ Most modern tokens (DAI, USDC, WBTC on some chains) support EIP-2612. Older toke
228256

229257
---
230258

259+
## Testing and Troubleshooting
260+
261+
- **Signature mismatch**: Double-check that `domain.name` exactly matches the on-chain token name. For USDC on Polygon it is `"USD Coin"`; capitalization matters.
262+
- **Invalid deadline**: If the relay says the permit expired, inspect the value you passed to `deadline` and ensure your local clock is not skewed.
263+
- **Allowance too low**: If the recipient receives funds but the relay reverts, print the computed `feeAmount` and make sure the permit covered both transfer and fee.
264+
265+
Running `npm run usdc` after each change keeps the feedback loop tight and mirrors how the Nimiq wallet tests the same flow.
266+
267+
---
268+
231269
## Production Considerations
232270

233271
1. **Check token support**: Not all ERC20s have permit. Fallback to standard `approve()` + `transferFrom()` if needed.
234272
2. **Deadline vs MaxUint256**: Production systems often use block-based deadlines (e.g., `currentBlock + 100`) for tighter security.
235273
3. **Domain parameters**: Always verify `name` and `version` match the token contract - wrong values = invalid signature.
236274
4. **Method selector lookup**: Store selectors in config per token to avoid hardcoding.
275+
5. **Permit reuse policy**: Decide whether to reuse a permit for multiple transfers or issue a fresh one per relay request. Fresh permits simplify accounting but require re-signing each time.
237276

238277
---
239278

240279
## Wrap-Up
241280

242-
You've now implemented gasless transfers for both USDT (meta-transaction) and USDC (EIP-2612 permit). The key takeaways:
281+
You now support gasless transfers for both USDT (custom meta-transaction) and USDC (EIP-2612 permit). Keep these takeaways in mind:
243282

244-
- ✅ Approval strategies vary by token implementation
245-
- ✅ EIP-2612 is the standard, but many tokens predate it
246-
- ✅ Domain separators differ (salt vs version+chainId)
247-
- ✅ Transfer method changes (`transferWithApproval` vs `transferWithPermit`)
248-
- ✅ Fee calculation and relay logic stay consistent
283+
- ✅ Approval strategies vary across tokens; detect the capability before deciding on the flow.
284+
- ✅ EIP-2612 standardizes the permit format: domain fields and struct definitions must match exactly.
285+
-`transferWithPermit` lets you drop the bulky encoded function signature and pass raw signature parts instead.
286+
- ✅ Fee and relay logic remain unchanged once the calldata is assembled correctly.
249287

250288
You now have a complete gasless transaction system matching the Nimiq wallet implementation, ready for production use on Polygon mainnet.

0 commit comments

Comments
 (0)