09/16/2025 | Press release | Archived content
USDC's superpower is programmability - you can choose how users authorize movements and how tokens interact with smart contracts. In this post, we'll explore four common authorization flows for EVM compatible chains: approve, permit (EIP 2612), transferWithAuthorization (EIP 3009), and Permit2, clarifying where allowances live (or don't) and when to use each. Permit2 is an external Uniswap contract, not part of the USDC token contract; its approvals are stored in Permit2, not in USDC.
Best for: repeated pulls, maximum compatibility, simplest mental model.
Allowance location: USDC token at allowances[owner][spender].
Trade offs: two onchain steps; educate users on revoking unlimited approvals.
constUSDC = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'; constOWNER_WALLET_ID = '...'; constSPENDER_WALLET_ID = '...'; constSPENDER = '0xYourSpender'; constOWNER = '0xOwner'; constRECIPIENT = '0xRecipient'; constAMOUNT = '1000000'; awaitclient.createContractExecutionTransaction({ walletId: OWNER_WALLET_ID, contractAddress: USDC, abiFunctionSignature: 'approve(address,uint256)', abiParameters: [SPENDER, AMOUNT], amount: '0', fee: { type: 'level', config: { feeLevel: 'HIGH'} }, });
What it does: user signs an EIP 712 Permit message with a chosen value (the allowance). A relayer submits permit(...) to the USDC contract, which writes the allowance just like approve.
Best for: allowance based integrations that want to avoid a user paid approval transaction.
Allowance location: USDC token. After permit(...), USDC.allowance(owner, spender) equals the signed value.
consttypedData = { domain: { name: 'USDC', version: '2', chainId: 84532, // Base Sepolia verifyingContract: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', }, types: { EIP712Domain: [ { name: 'name', type: 'string'}, { name: 'version', type: 'string'}, { name: 'chainId', type: 'uint256'}, { name: 'verifyingContract', type: 'address'}, ], Permit: [ { name: 'owner', type: 'address'}, { name: 'spender', type: 'address'}, { name: 'value', type: 'uint256'}, { name: 'nonce', type: 'uint256'}, { name: 'deadline', type: 'uint256'}, ], }, primaryType: 'Permit', message: { owner: '0xOwnerAddress', spender: '0xRelayerOrSpenderAddress', value: Number(ethers.parseUnits('3.0', 6)), nonce: 0, deadline: Math.floor(Date.now() / 1000) + 3600, }, }; const{ data } = awaitclient.signTypedData({ walletId: 'your-circle-wallet-id', data: JSON.stringify(typedData), memo: 'EIP 2612: Permit USDC allowance', }); constsignature = data.signature; console.log('signature:', signature);
To use the signature later, call permit(address,address,uint256,uint256,bytes) on the USDC contract, then pull with transferFrom.
What it does: user signs a one time EIP 712 authorization with a bytes32 nonce and validity window. A relayer submits transferWithAuthorization(...) to move tokens immediately. When the contract pulls funds from the user's wallet, use receiveWithAuthorization(...).
Best for: checkout or payment flows where you want gasless user experience without persistent allowances.
Allowance location: none. No change to USDC.allowance(...).
constCHAIN_ID = 84532; constUSDC = '0x036CbD53842c5426634e7929541eC2318f3dCF7e'; constRELAYER_ADDRESS = '0xRelayerSpenderAddress'; constOWNER = '0xOwnerAddress'; constVALUE_USDC = '3.00'; constVALID_AFTER = 0; constVALID_BEFORE = Math.floor(Date.now()/1000) + 3600; constvalueWei = Number(ethers.parseUnits(VALUE_USDC, 6)); constnonceBytes32 = ethers.hexlify(ethers.randomBytes(32)); consttypedData = { domain: { name: 'USDC', version: '2', chainId: CHAIN_ID, // Base Sepolia verifyingContract: USDC, }, types: { EIP712Domain: [ { name: 'name', type: 'string'}, { name: 'version', type: 'string'}, { name: 'chainId', type: 'uint256'}, { name: 'verifyingContract', type: 'address'}, ], TransferWithAuthorization: [ { name: 'from', type: 'address'}, { name: 'to', type: 'address'}, { name: 'value', type: 'uint256'}, { name: 'validAfter', type: 'uint256'}, { name: 'validBefore', type: 'uint256'}, { name: 'nonce', type: 'bytes32'}, ], }, primaryType: 'TransferWithAuthorization', message: { from: '0xOwnerAddress', to: '0xRelayerSpenderAddress', value: valueWei, validAfter: VALID_AFTER, validBefore: VALID_BEFORE, nonce: nonceBytes32, }, }; const{ data } = awaitclient.signTypedData({ walletId: 'your-circle-wallet-id', data: JSON.stringify(typedData), memo: 'EIP-3009 authorization', }); constsignature = data.signature;
Execution later is a single call to transferWithAuthorization(...), or your contract can call receiveWithAuthorization(...) to safely pull funds.
What it does: an external Uniswap contract that standardizes approvals and signed transfers across many tokens.
Best for: DEX and aggregator style apps that want consistent flows for many tokens.
constCHAIN_ID = 84532; constPERMIT2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3'; constSPENDER = '0xYourSpenderContract'; constOWNER = '0xOwnerAddress'; constTOKEN = '0xUSDCAddress'; constDEADLINE = Math.floor(Date.now()/1000) + 3600; constAMOUNT = 3_000_000; consttypes = { EIP712Domain: [ { name: 'name', type: 'string'}, { name: 'chainId', type: 'uint256'}, { name: 'verifyingContract', type: 'address'}, ], PermitDetails: [ { name: 'token', type: 'address'}, { name: 'amount', type: 'uint160'}, { name: 'expiration', type: 'uint48'}, { name: 'nonce', type: 'uint48'}, ], PermitBatch: [ { name: 'details', type: 'PermitDetails[]'}, { name: 'spender', type: 'address'}, { name: 'sigDeadline', type: 'uint256'}, ], }; constdetails = [{ token: TOKEN, amount: AMOUNT, expiration: DEADLINE, nonce: 0, }]; consttypedData = { domain: { name: 'Permit2', chainId: CHAIN_ID, verifyingContract: PERMIT2, }, types, primaryType: 'PermitBatch', message: { details, spender: SPENDER, sigDeadline: DEADLINE, }, }; const{ data } = awaitclient.signTypedData({ walletId: 'your-circle-wallet-id', data: JSON.stringify(typedData), memo: 'Permit2 batch approval', }); constsignature = data.signature; console.log('Permit2 signature:', signature);
To execute later, call Permit2.permitTransferFrom(...). For persistent approvals with Permit2, use its AllowanceTransfer flow and remember that the allowance is stored in Permit2, not in USDC.
That is the landscape for EVM chains. Pick the flow that matches your UX and security goals, and use the Circle Developer Controlled Wallets SDK to sign and execute with clean, repeatable patterns.