Permit2
What Permit2 Is
Permit2 is Uniswap's universal token approval contract. It provides a single approval point for all protocols, with fine-grained control over spending limits and expiry.
amser uses Permit2 exclusively for all token transfers. Users approve Permit2 once, then grant bounded allowances to amser's PaymentProcessor.
Why amser Uses Permit2
Traditional ERC20 approvals have problems:
| Issue | Traditional Approvals | Permit2 |
|---|---|---|
| Approval scope | Often unlimited | Bounded amount |
| Expiry | Never expires | Configurable expiry |
| Revocation | Per-protocol | Single revoke point |
| Gas cost | Approval per protocol | One approval, reusable |
| Phishing risk | Unlimited drain | Capped exposure |
Permit2 enables amser to be non-custodial while still supporting recurring payments. Users control exactly how much can be spent and for how long.
How It Works
The Two-Step Model
- One-time approval: User approves Permit2 contract to spend their tokens (standard ERC20 approve)
- Per-protocol allowance: User grants amser a bounded allowance within Permit2
Allowance Parameters
When subscribing, users sign a Permit2 allowance with:
- amount: Maximum tokens that can be transferred (typically plan price × expected payments)
- expiration: Unix timestamp after which the allowance is invalid
- nonce: Replay protection
The PaymentProcessor checks these parameters before every payment. If the allowance is insufficient or expired, the payment fails cleanly.
Setting Up Allowances
For Integrators
When a user subscribes, you need to:
- Check if Permit2 has approval for the token
- If not, request ERC20 approval to Permit2
- Request Permit2 signature for amser allowance
import { createPublicClient, createWalletClient, http, maxUint256, parseAbi } from 'viem';
import { baseSepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
const account = privateKeyToAccount(process.env.MERCHANT_PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
const walletClient = createWalletClient({ account, chain: baseSepolia, transport: http() });
const erc20Abi = parseAbi([
'function allowance(address,address) view returns (uint256)',
'function approve(address,uint256) returns (bool)',
]);
// Step 1: Check/request ERC20 approval to Permit2
const currentAllowance = await publicClient.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'allowance',
args: [account.address, PERMIT2_ADDRESS],
});
if (currentAllowance < requiredAmount) {
await walletClient.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [PERMIT2_ADDRESS, maxUint256],
});
}
// Step 2: Build Permit2 allowance (spender = PaymentProcessor)
const permit = {
details: {
token: tokenAddress,
amount: planPrice * BigInt(expectedPayments),
expiration: BigInt(Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60), // 1 year
nonce: await getNextNonce(account.address),
},
spender: PAYMENT_PROCESSOR_ADDRESS,
sigDeadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour to submit
};
// Step 3: User signs the permit via EIP-712
const signature = await walletClient.signTypedData({
domain: permit2Domain,
types: permitTypes,
primaryType: 'PermitSingle',
message: permit,
});
// Step 4: Submit via the module (users never call PaymentProcessor directly)
await walletClient.writeContract({
address: moduleAddress,
abi: parseAbi([
'function subscribeWithPermit(uint256 planId, uint256 remainingExecutions, uint48 allowanceExpiry, bytes permitData, bytes signature)',
]),
functionName: 'subscribeWithPermit',
args: [BigInt(planId), BigInt(remainingExecutions), allowanceExpiry, permitData, signature],
});
Recommended Allowance Sizing
| Subscription Type | Recommended Allowance |
|---|---|
| Monthly, indefinite | 12-24 months worth |
| Annual | 1-2 years worth |
| Fixed term (e.g., 6 months) | Exact total + buffer |
| Agent credits | Expected batch count |
Oversizing slightly is safer than undersizing. An expired or exhausted allowance causes payment failures.
Expiry Handling
What Happens When Allowance Expires
- Keeper attempts to execute payment
- PaymentProcessor checks Permit2 allowance
expiration < block.timestamp→ payment failsExecutionFailedevent emitted withAllowanceExpiredreason- Subscription becomes inactive
User Recovery
Users must sign a new Permit2 allowance to continue:
- Dashboard shows "Allowance expired" status
- User clicks "Renew authorization"
- New Permit2 signature requested
- Allowance updated on-chain
- Next keeper execution succeeds
Best Practices
- Set expiry well beyond expected subscription duration
- Monitor allowance expiry in your frontend
- Prompt users to renew before expiry
- Consider 1-year minimum for subscriptions
What Permit2 Validates
On every payment, the PaymentProcessor checks:
| Check | Failure Mode |
|---|---|
allowance >= amount | InsufficientAllowance |
expiration > now | AllowanceExpired |
nonce not used | InvalidNonce |
| Token balance >= amount | Transfer reverts |
All checks happen atomically. If any fail, no tokens move.
Security Model
Why This Is Safer Than Infinite Approvals
Traditional "approve max" patterns expose users to:
- Protocol bugs draining entire balance
- Compromised contracts stealing funds
- No expiry means permanent risk
Permit2 with amser limits exposure:
- Allowance caps maximum loss
- Expiry automatically revokes access
- Single revocation point for all protocols
- Nonces prevent replay attacks
User Controls
Users always retain:
- Ability to revoke Permit2 approval entirely
- Ability to reduce amser's allowance to zero
- Ability to pause subscriptions (stops payments without revoking)
- Full custody of funds (never deposited anywhere)
What amser Cannot Do
Even with a valid Permit2 allowance, amser cannot:
- Transfer more than the allowance amount
- Transfer after expiry
- Transfer if user balance is insufficient
- Transfer to addresses other than the plan's merchant
- Transfer without a valid execution window (idempotency)
The PaymentProcessor enforces all constraints on-chain.
Permit2 Contract Addresses
Permit2 is deployed at the same address on all supported chains:
0x000000000022D473030F116dDEE9F6B43aC78BA3
This is a canonical deployment by Uniswap. amser does not deploy or control Permit2.
Relationship to RAI
Permit2 is the enablement layer for Recurring Authorization Infrastructure.
RAI's guarantees depend on Permit2:
| RAI Guarantee | Permit2 Enablement |
|---|---|
| Non-custodial | Funds stay in user wallet |
| Bounded | Allowance caps exposure |
| Revocable | User can revoke anytime |
| Time-scoped | Expiry enforces duration |
Without Permit2, recurring authorization would require either:
- Custody (funds in protocol)
- Unlimited approvals (unsafe)
- Per-payment signatures (poor UX)
Permit2 provides the middle ground: delegated, bounded, revocable authority.
Common Issues
"Insufficient Allowance" Errors
Causes:
- User set allowance too low
- Multiple payments exhausted allowance
- Allowance was for wrong token
Fix: Request new Permit2 signature with higher amount.
"Allowance Expired" Errors
Causes:
- Expiry timestamp passed
- User set very short expiry
Fix: Request new Permit2 signature with later expiry.
"User rejected signature"
Causes:
- User declined in wallet
- Wallet doesn't support EIP-712
- Domain mismatch
Fix: Ensure correct Permit2 domain parameters for the chain.