Permit & Meta-Transactions
ERC-20 tokens deployed with ERC20_VOTES or ERC20_SOLADY include EIP-2612 Permit support, enabling gasless approvals via off-chain signatures. Urblock also supports Uniswap's Permit2 for universal gasless approvals.
When to Use
- Gasless UX — let users approve token spending without paying gas
- Single-transaction flows — approve + transfer in one call
- Payment protocols — users sign off-chain, relayer submits on-chain
- Subscription services — time-limited permits with automatic expiry
How Permit Works
Traditional flow (2 transactions):
1. User calls approve(spender, amount) — pays gas
2. Spender calls transferFrom(owner, spender, amount)
Permit flow (1 transaction):
1. User signs EIP-712 message off-chain — FREE
2. Spender calls permit(owner, spender, value, deadline, v, r, s)
+ transferFrom(owner, spender, amount) — in one tx
Deploy a Permit-Enabled Token
// ERC20_VOTES includes EIP-2612 Permit
const token = await urblock.tokens.create({
name: "Permit Token",
symbol: "PMT",
standard: "ERC20_VOTES",
network: "polygon_amoy",
initial_supply: "1000000000000000000000000",
idempotency_key: "permit-token-001",
});
Get Permit Info
Retrieve the nonce and EIP-712 domain separator needed to build a permit signature:
curl https://api.urblock.io/v1/tokens/tok_abc123/permit_info \
-H "Authorization: Bearer sk_test_..."
{
"nonce": "0",
"domain_separator": "0x...",
"name": "Permit Token",
"version": "1",
"chain_id": 80002,
"verifying_contract": "0xTokenAddress..."
}
Sign a Permit (Off-Chain)
The token holder signs an EIP-712 typed message off-chain using their wallet (e.g., ethers.js, viem). This is a client-side operation — the backend does not sign permits:
import { ethers } from "ethers";
// Build the EIP-712 domain from permit_info
const domain = {
name: permitInfo.name,
version: permitInfo.version,
chainId: permitInfo.chain_id,
verifyingContract: permitInfo.verifying_contract,
};
const types = {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
};
const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour
const value = { owner, spender, value: amount, nonce: permitInfo.nonce, deadline };
const signature = await signer.signTypedData(domain, types, value);
const { v, r, s } = ethers.Signature.from(signature);
Submit a Permit On-Chain
The spender (or relayer) submits the signed permit via the Urblock API:
curl -X POST https://api.urblock.io/v1/tokens/tok_abc123/permit \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"owner": "0xTokenHolder...",
"spender": "0xSpenderContract...",
"value": "1000000000000000000000",
"deadline": 1718238167,
"v": 28,
"r": "0x...",
"s": "0x...",
"idempotency_key": "permit-submit-001"
}'
After the permit is confirmed on-chain, the spender has the allowance and can call transferFrom.
Permit2 (Universal Permits)
Permit2 extends gasless transfers to any ERC-20 token, not just those with built-in EIP-2612 support. Urblock exposes a permit2_transfer endpoint that executes a gasless transfer via the Permit2 contract:
curl -X POST https://api.urblock.io/v1/tokens/tok_abc123/permit2_transfer \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"from": "0xTokenHolder...",
"to": "0xRecipient...",
"amount": "1000000000000000000000",
"idempotency_key": "permit2-transfer-001"
}'
The SDK does not yet have wrapper methods for permit endpoints. Use the HTTP API directly as shown above, or use urblock.request() for a typed call.
Permit vs Permit2
| Feature | EIP-2612 Permit | Permit2 |
|---|---|---|
| Requires token support | Yes (ERC20_VOTES, ERC20_SOLADY) | No (any ERC-20) |
| On-chain contract | Built into token | Separate Permit2 contract |
| Nonce management | Per-address counter | Bitmap-based |
| Signed transfer | No | Yes (permitTransferFrom) |
| Batch permits | No | Yes |
Supported Standards
| Standard | EIP-2612 Permit | Permit2 Compatible |
|---|---|---|
ERC20_VOTES | ✅ | ✅ |
ERC20_SOLADY | ✅ | ✅ |
ERC20 | ❌ | ✅ |
ERC1363 | ❌ | ✅ |
ERC3643 | ❌ | ❌ (compliance checks) |
Error Codes
| Code | Meaning |
|---|---|
permit_expired | Deadline has passed |
invalid_signature | v/r/s values don't match the signer |
invalid_nonce | Nonce already used (replay protection) |
permit_not_supported | Token standard does not support EIP-2612 |
Best Practices
- Set short deadlines — permits should expire within hours, not days
- Track nonces — each address has an incrementing nonce; use the SDK to auto-resolve
- Prefer Permit2 for universal support — works with any ERC-20 token
- Verify signatures server-side before submitting to avoid wasting gas on invalid permits
Next Steps
- Deploy Your First Token — choose the right standard
- API: Tokens — full token endpoint reference
- Advanced Token Features — SBT, ERC-1363
- Gas Optimization — reduce costs further