# Gasless Token Approvals with EIP-2612 Permit: A Developer's Implementation Guide

> Learn how to implement gasless token approvals using EIP-2612 permit signatures in your dApp, with code examples covering signing, verification, and Permit2.

- **Published:** 2026-06-08
- **Author:** Clixo
- **Reading time:** 6 min read
- **Tags:** eip-2612, gasless, permit, token-approvals, defi
- **Canonical URL:** https://clixo.sh/blog/gasless-transactions-eip-2612-permit-guide

The standard ERC-20 approval flow requires two transactions: one `approve` and one `transferFrom`. On a busy day, that is two gas payments, two wallet confirmations, and a meaningful drop-off rate before the user completes the action they came to your dApp to take. EIP-2612 is the cleanest way to collapse this into a single transaction, and it has been live in production on tokens like USDC, DAI, and UNI for years.

Here is how it works and how to implement it correctly.

## What EIP-2612 Permit Does

EIP-2612 adds a `permit()` function to the ERC-20 standard. This function lets a token holder authorize a spender by submitting a signed message rather than a separate `approve` transaction. The signed message is an off-chain EIP-712 typed data structure — it costs zero gas to create, because no chain interaction is needed.

The spender contract then calls `permit()` and `transferFrom()` in the same transaction. The user sees one wallet confirmation, pays gas once, and the action completes. The approval is atomic with the transfer — it cannot be front-run and does not leave a standing allowance that can be drained later.

```mermaid
sequenceDiagram
  participant U as User
  participant DA as dApp
  participant TC as Token Contract
  participant PC as Protocol Contract
  U->>DA: Request action
  DA->>TC: Read nonce
  TC-->>DA: Return nonce
  DA->>U: Sign EIP-712 permit message
  U-->>DA: Signature (v, r, s)
  DA->>PC: depositWithPermit(amount, deadline, sig)
  PC->>TC: permit(owner, spender, amount, sig)
  PC->>TC: transferFrom(owner, contract, amount)
  TC-->>PC: Transfer complete
```

## How to Implement Gasless Approvals with EIP-2612

### Step 1: Check if the Token Supports Permit

Not every ERC-20 token implements EIP-2612. Before building this flow, verify the token you are integrating has a `permit()` function and a `DOMAIN_SEPARATOR()` function.

```typescript
import { erc20Abi, publicClient } from 'viem';

const permitAbi = [
  {
    name: 'permit',
    type: 'function',
    inputs: [
      { name: 'owner', type: 'address' },
      { name: 'spender', type: 'address' },
      { name: 'value', type: 'uint256' },
      { name: 'deadline', type: 'uint256' },
      { name: 'v', type: 'uint8' },
      { name: 'r', type: 'bytes32' },
      { name: 's', type: 'bytes32' },
    ],
    outputs: [],
  },
  {
    name: 'nonces',
    type: 'function',
    inputs: [{ name: 'owner', type: 'address' }],
    outputs: [{ name: '', type: 'uint256' }],
  },
  {
    name: 'DOMAIN_SEPARATOR',
    type: 'function',
    inputs: [],
    outputs: [{ name: '', type: 'bytes32' }],
  },
] as const;
```

### Step 2: Build and Sign the Permit Message

The permit message is an EIP-712 typed data structure. The user signs this off-chain with zero gas cost.

```typescript
import { signTypedData } from 'viem/actions';

async function signPermit({
  walletClient,
  tokenAddress,
  ownerAddress,
  spenderAddress,
  value,
  chainId,
}) {
  // Fetch the current nonce for this owner from the token contract
  const nonce = await publicClient.readContract({
    address: tokenAddress,
    abi: permitAbi,
    functionName: 'nonces',
    args: [ownerAddress],
  });

  // Set a deadline 20 minutes from now
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 20);

  const signature = await walletClient.signTypedData({
    account: ownerAddress,
    domain: {
      name: 'USD Coin',       // must match the token's EIP-712 domain name
      version: '2',           // must match the token's domain version
      chainId,
      verifyingContract: tokenAddress,
    },
    types: {
      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: ownerAddress,
      spender: spenderAddress,
      value,
      nonce,
      deadline,
    },
  });

  return { signature, deadline };
}
```

The domain `name` and `version` must exactly match what the token contract returns from its `DOMAIN_SEPARATOR`. Mismatches produce invalid signatures that revert on-chain with no helpful error.

### Step 3: Submit Permit and Execute in One Transaction

On the contract side, call `permit()` first, then execute your action. This is typically done in a single function call on your protocol contract.

```solidity
function depositWithPermit(
    address token,
    uint256 amount,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) external {
    IERC20Permit(token).permit(
        msg.sender,
        address(this),
        amount,
        deadline,
        v,
        r,
        s
    );
    IERC20(token).transferFrom(msg.sender, address(this), amount);
    // ... rest of deposit logic
}
```

On the client, pass the signature components alongside the transaction:

```typescript
const { v, r, s } = parseSignature(signature);

await walletClient.writeContract({
  address: yourProtocolAddress,
  abi: yourProtocolAbi,
  functionName: 'depositWithPermit',
  args: [tokenAddress, amount, deadline, v, r, s],
});
```

## When Tokens Do Not Support EIP-2612: Permit2

Not all ERC-20 tokens have a `permit()` function — many were deployed before the standard existed. Uniswap's **Permit2** contract solves this by creating a generalized approval layer on top of any ERC-20 token.

The flow with Permit2:

1. The user approves Permit2 (a canonical, audited contract at the same address on all EVM chains) to spend the token. This is a one-time standard `approve`, done once per token regardless of how many protocols use Permit2.
2. From then on, any protocol that integrates Permit2 can request permit-style signatures from the user without requiring additional `approve` transactions.

For DeFi protocols handling many tokens, integrating Permit2 gives you the single-transaction UX of EIP-2612 across the full ERC-20 ecosystem.

## Key Implementation Caveats

**Domain name and version must be exact.** The EIP-712 domain parameters are part of the signed payload. If your domain name has a typo or the version does not match, the on-chain `permit()` call will revert. Fetch `DOMAIN_SEPARATOR()` directly from the token and cross-reference your domain parameters.

**Deadlines should be reasonable, not distant.** A permit signature with a deadline 10 years in the future is nearly as risky as unlimited approval. Use 15-30 minute deadlines for interactive flows.

**Nonce must be current at signing time.** The nonce in the signed message must match the current on-chain nonce. If the user has signed another permit for the same token between when you fetched the nonce and when they submit, the nonce will be stale and the transaction will revert. Fetch the nonce as close to the signing step as possible.

**Signature format varies by wallet.** Some wallets return a concatenated 65-byte signature; others return an object with `v`, `r`, `s` separately. Normalize the format before splitting into components.

---

EIP-2612 permit integration is one of the highest-leverage UX improvements available in DeFi. If your team is building a DeFi product and wants the permit flow implemented correctly end to end, [Start a build](https://clixo.sh/#contact) with Clixo.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
