Skip to main content

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:

IssueTraditional ApprovalsPermit2
Approval scopeOften unlimitedBounded amount
ExpiryNever expiresConfigurable expiry
RevocationPer-protocolSingle revoke point
Gas costApproval per protocolOne approval, reusable
Phishing riskUnlimited drainCapped 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​

  1. One-time approval: User approves Permit2 contract to spend their tokens (standard ERC20 approve)
  2. 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:

  1. Check if Permit2 has approval for the token
  2. If not, request ERC20 approval to Permit2
  3. 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],
});
Subscription TypeRecommended Allowance
Monthly, indefinite12-24 months worth
Annual1-2 years worth
Fixed term (e.g., 6 months)Exact total + buffer
Agent creditsExpected batch count

Oversizing slightly is safer than undersizing. An expired or exhausted allowance causes payment failures.

Expiry Handling​

What Happens When Allowance Expires​

  1. Keeper attempts to execute payment
  2. PaymentProcessor checks Permit2 allowance
  3. expiration < block.timestamp → payment fails
  4. ExecutionFailed event emitted with AllowanceExpired reason
  5. Subscription becomes inactive

User Recovery​

Users must sign a new Permit2 allowance to continue:

  1. Dashboard shows "Allowance expired" status
  2. User clicks "Renew authorization"
  3. New Permit2 signature requested
  4. Allowance updated on-chain
  5. 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:

CheckFailure Mode
allowance >= amountInsufficientAllowance
expiration > nowAllowanceExpired
nonce not usedInvalidNonce
Token balance >= amountTransfer 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 GuaranteePermit2 Enablement
Non-custodialFunds stay in user wallet
BoundedAllowance caps exposure
RevocableUser can revoke anytime
Time-scopedExpiry 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.

Further Reading​