Permit2 / EIP-2612 — One-Click Subscribe

Subscribe to a plan in a single transaction using EIP-2612 gasless approval. No separate approve() transaction needed.

The problem

Standard ERC-20 subscriptions require two transactions: first approve() to grant the registry an allowance, then subscribe(). This is two wallet popups and two gas payments — a poor UX.

The solution: EIP-2612 permit

Tokens that support EIP-2612 (USDC on Base, DAI, WETH on some chains) allow off-chain signature-based approvals. CronRegistry's subscribeWithPermit() function batches the approval and subscription into a single on-chain transaction. The user signs one message (off-chain) and submits one transaction.

Flow

  1. User signs an EIP-712 permit message off-chain (no gas)
  2. Frontend submits subscribeWithPermit() with the permit signature
  3. Contract calls permit() to set allowance, then immediately charges and subscribes

SDK usage

typescript
import { CronClient } from "@cron/sdk";

const client = new CronClient({
  chain: "base",
  walletClient,
  publicClient,
});

// Read current nonce from the token contract
const tokenNonce = await publicClient.readContract({
  address: USDC_BASE,
  abi: [{ name: "nonces", type: "function", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "uint256" }], stateMutability: "view" }],
  functionName: "nonces",
  args: [userAddress],
});

// One call — signs permit off-chain, submits single tx
const { transactionHash, subscription, permit } = await client.subscribeWithPermit(
  1n,           // planId
  "USD Coin",   // token name for EIP-712 domain
  tokenNonce,   // current nonce from token contract
  {
    mintNFT: true,
    tokenVersion: "2",    // USDC on Base uses version "2"
    // deadline defaults to 1 hour from now
    // permitAmount defaults to 12× the plan charge amount
  }
);

console.log("Subscribed in one tx!", transactionHash);

Low-level signPermit

You can also sign a permit manually for more control:

typescript
import { signPermit, recommendedPermitAmount } from "@cron/sdk";

const permit = await signPermit({
  walletClient,
  tokenAddress: USDC_BASE,
  tokenName: "USD Coin",
  tokenVersion: "2",
  chainId: 8453,
  spender: CRON_REGISTRY_ADDRESS,
  value: recommendedPermitAmount(planAmount),  // 12× plan amount
  nonce: tokenNonce,
  deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
});

// permit.deadline, permit.v, permit.r, permit.s

Token compatibility

  • USDC on Base — EIP-2612 supported, use tokenVersion: "2"
  • DAI — EIP-2612 supported, use tokenVersion: "1"
  • USDT — Does not support EIP-2612, use standard subscribe() with prior approval

[!] Permit replay protection

Each permit is single-use — the nonce increments after use. Always read the current nonce from the token contract immediately before signing to avoid failures from stale nonces.

Graceful fallback

The subscribeWithPermit() contract function wraps the permit() call in a try/catch. If the permit fails (e.g. already consumed or unsupported token), the function falls through and attempts the charge using any existing allowance. This means you can safely call it even if the user already has an approval in place.