Ethereum’s ERC-20 tokens power countless decentralized applications, from DeFi protocols to NFT marketplaces. However, a long-standing friction in user experience has been the need to approve token spending through on-chain transactions—each requiring gas fees. This two-step process (approve + transfer) adds complexity and cost, especially for users interacting with multiple dApps.
Enter EIP-2612, a game-changing Ethereum Improvement Proposal that enables gasless token approvals through signed messages. With EIP-2612, users can authorize token spending off-chain, reducing both transaction count and gas costs. In this guide, we’ll walk through how EIP-2612 works and implement it using Hardhat, OpenZeppelin, and Ethers.js on the Sepolia testnet.
What Is EIP-2612?
EIP-2612 introduces a standardized method called permit, which allows ERC-20 token holders to approve token spending via a digital signature instead of an on-chain transaction. This signature is later submitted by the recipient or a third party to execute the approval on-chain—without the token owner ever paying gas.
The permit function leverages Ethereum’s EIP-712 typed data signing standard, ensuring secure and human-readable message structures. This means users can sign a message authorizing token transfers directly from their wallet, and the dApp handles the rest.
Key Benefits of EIP-2612
Integrating EIP-2612 into your token contracts offers several advantages:
- Gasless Approvals: Users approve token spending off-chain, eliminating the need for a costly
approve()transaction. - Improved UX: Reduces user friction by cutting down interaction steps from two (approve + transfer) to one.
- Enhanced Security: Each permit includes a deadline and nonce, preventing replay attacks and unauthorized reuse.
- Standardization: As a widely adopted EIP, it ensures interoperability across wallets, dApps, and protocols.
👉 Discover how OKX Wallet supports seamless interaction with EIP-2612-enabled tokens.
Setting Up Your Development Environment
Before diving into code, let’s set up a local development environment using Hardhat, a powerful Ethereum development framework.
1. Create a Web3 Wallet & Get Testnet ETH
You’ll need two Ethereum accounts:
- One to deploy the token contract (owner)
- One to receive tokens (spender)
Use any non-custodial wallet like MetaMask or Torus. For this guide, ensure both wallets are funded with Sepolia ETH. You can get testnet tokens from the QuickNode Multi-Chain Faucet.
2. Set Up a QuickNode Endpoint
To interact with the Sepolia network, you’ll need a reliable RPC endpoint. While public nodes exist, they’re often slow or rate-limited. A better option is using QuickNode, which provides fast, stable access.
👉 Generate your free Ethereum Sepolia endpoint instantly with OKX.
After signing up, create an endpoint for Sepolia and copy the HTTP provider URL.
3. Initialize the Project
Create a new project directory and initialize it:
mkdir erc20permit && cd erc20permit
npm init -yInstall required dependencies:
npm install --save-dev hardhat
npm install --save [email protected] dotenv @nomicfoundation/hardhat-toolbox @openzeppelin/contractsInitialize Hardhat:
npx hardhatSelect “Create an empty hardhat.config.js” when prompted.
4. Configure Hardhat
Update hardhat.config.js with your network settings:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
networks: {
sepolia: {
url: process.env.RPC_URL,
accounts: [process.env.PRIVATE_KEY_DEPLOYER]
}
},
solidity: {
version: "0.8.9",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
}
};Create a .env file to store sensitive data:
RPC_URL=your_quicknode_endpoint
PRIVATE_KEY_DEPLOYER=your_owner_private_key
PRIVATE_KEY_ACCOUNT_2=your_receiver_private_keyBuilding an ERC-20 Token with Permit Support
We’ll use OpenZeppelin’s ERC20Permit extension to add EIP-2612 functionality.
Create contracts/MyToken.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
contract MyToken is ERC20, Ownable, ERC20Permit {
constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {
_mint(msg.sender, 1000 * 10 ** decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}This contract:
- Inherits
ERC20for basic token functionality - Uses
ERC20Permitfor gasless approvals - Allows the owner to mint new tokens
- Mints 1000 tokens to the deployer
Deploying the Contract
Create scripts/deploy.js:
const hre = require("hardhat");
async function main() {
const MyToken = await hre.ethers.getContractFactory("MyToken");
const myToken = await MyToken.deploy();
await myToken.deployed();
console.log("ERC20 Permit contract deployed at:", myToken.address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});Compile and deploy:
npx hardhat compile
npx hardhat run --network sepolia scripts/deploy.jsSave the deployed contract address—you’ll need it next.
Executing a Gasless Permit Approval
Now comes the core of EIP-2612: signing a permit off-chain and submitting it on-chain.
Create scripts/permit.js:
const { ethers } = require("hardhat");
const { abi } = require("../artifacts/contracts/MyToken.sol/MyToken.json");
require("dotenv").config();
function getTimestampInSeconds() {
return Math.floor(Date.now() / 1000);
}
async function main() {
const provider = new ethers.providers.StaticJsonRpcProvider(process.env.RPC_URL);
const chainId = (await provider.getNetwork()).chainId;
const tokenOwner = new ethers.Wallet(process.env.PRIVATE_KEY_DEPLOYER, provider);
const tokenReceiver = new ethers.Wallet(process.env.PRIVATE_KEY_ACCOUNT_2, provider);
const myToken = new ethers.Contract("YOUR_CONTRACT_ADDRESS", abi, provider);
console.log("Starting balances:");
console.log("Owner:", (await myToken.balanceOf(tokenOwner.address)).toString());
console.log("Receiver:", (await myToken.balanceOf(tokenReceiver.address)).toString());
const value = ethers.utils.parseEther("1");
const deadline = getTimestampInSeconds() + 4200;
const nonce = await myToken.nonces(tokenOwner.address);
const domain = {
name: await myToken.name(),
version: "1",
chainId,
verifyingContract: myToken.address,
};
const types = {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
};
const values = { owner: tokenOwner.address, spender: tokenReceiver.address, value, nonce, deadline };
const signature = await tokenOwner._signTypedData(domain, types, values);
const sig = ethers.utils.splitSignature(signature);
const tx = await myToken.connect(tokenReceiver).permit(
tokenOwner.address,
tokenReceiver.address,
value,
deadline,
sig.v,
sig.r,
sig.s,
{ gasLimit: 80000 }
);
await tx.wait(2);
console.log("Allowance set:", (await myToken.allowance(tokenOwner.address, tokenReceiver.address)).toString());
await myToken.connect(tokenReceiver).transferFrom(
tokenOwner.address,
tokenReceiver.address,
value,
{ gasLimit: 80000 }
);
console.log("Final balances:");
console.log("Owner:", (await myToken.balanceOf(tokenOwner.address)).toString());
console.log("Receiver:", (await myToken.balanceOf(tokenReceiver.address)).toString());
}
main().catch(console.error);Replace YOUR_CONTRACT_ADDRESS with your actual address and run:
npx hardhat run --network sepolia scripts/permit.jsYou’ll see output confirming:
- The receiver was granted allowance without the owner paying gas
- Tokens were successfully transferred
Frequently Asked Questions (FAQ)
What is EIP-2612?
EIP-2612 is an Ethereum Improvement Proposal that enables gasless ERC-20 token approvals through signed messages using EIP-712. It introduces the permit() function to replace traditional approve() transactions.
How does EIP-2612 save gas?
Instead of submitting an on-chain approve() transaction (which costs gas), users sign a message off-chain. The dApp or recipient submits this signature via permit(), bundling approval and transfer into one transaction.
Is EIP-2612 secure?
Yes. Each permit includes a unique nonce and expiration deadline, preventing replay attacks. Only the intended spender can use the signature before it expires.
Which tokens support EIP-2612?
Popular tokens like USDC, DAI, and WETH have adopted EIP-2612 or similar standards. Always check a token’s contract for the permit() function before integration.
Can I use EIP-2612 on other chains?
Yes! EIP-2612 works on any EVM-compatible blockchain (e.g., Polygon, Arbitrum, BSC) as long as the token contract implements the standard.
What happens if I reuse a permit signature?
The nonce system ensures each signature can only be used once. Attempting to replay it will result in a failed transaction.
Final Thoughts
EIP-2612 represents a major leap forward in Ethereum UX by eliminating gas costs for token approvals. By integrating ERC20Permit into your projects, you can offer smoother onboarding, reduce user friction, and align with modern dApp standards.
Whether you're building a DeFi platform or a token-gated application, leveraging EIP-2612 is a smart move toward better usability and broader adoption.
👉 Explore OKX’s developer tools to streamline your next Web3 project.