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