Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Implementing ERC-2612 Permits for Gasless Approvals

Hello! Let's dive into the second lesson of our "Fungible Tokens & DeFi Primitives" module.

Introduction

In our last lesson, we built a robust ERC-20 token with minting, burning, and pausable features using OpenZeppelin's modular contracts. This gave us a fully functional token, but it still relies on a standard interaction pattern that can be cumbersome for users.

When a user wants a decentralized application (DApp) to use their tokens (for example, to swap them on a Decentralized Exchange), they must first perform an approve transaction, wait for it to be confirmed, and then perform a second transaction (e.g., swap). This two-step process is a significant point of friction in the user experience and costs the user gas for two separate transactions.

Today, we will address this problem directly.

Learning Outcome: By the end of this lesson, you will be able to add permit functionality (ERC-2612) to an ERC-20 token for gasless approvals.

We'll explore EIP-2612, a standard that allows users to approve token spending by signing an off-chain message. This signature can then be submitted by another party (a "relayer") in a single transaction that also performs the main action, effectively making the approval step "gasless" for the user and creating a much smoother UX.

1. The "Gasless Approval" Concept

The term "gasless" can be slightly misleading. The gas for the transaction is still paid, but it's not paid by the user who is granting the approval. Instead, another account—either the DApp's backend, the person receiving the tokens, or a dedicated relayer service—pays the gas for a transaction that includes the approval.

This is achieved by separating the authorization (signing a message) from the execution (submitting the transaction).

To see how this works in practice, let's watch a short video that illustrates the two primary scenarios.

Gas-less Token Transfer

The video 'Gas-less Token Transfer' from the Smart Contract Programmer channel provides a clear, high-level explanation of the permit flow.

Please watch from the beginning until 04:36. The video covers: The Problem: A user has tokens but no ETH to pay for gas (00:00 - 00:33). Scenario 1: The token recipient pays the gas to execute the permit and transferFrom calls (00:33 - 02:00). Scenario 2: A third-party relayer pays the gas, often taking a small fee in the token being transferred (02:00 - 04:36). Focus on understanding the flow of information: who signs what, and who submits the transaction to the blockchain.

As you saw, the core idea is that the token owner (Alice) creates and signs a message authorizing a "spender" to access her tokens. This signed message is a valuable piece of data that can be used by anyone to gain that approval on-chain.

2. The Anatomy of a Permit

To make this off-chain signing secure, EIP-2612 relies on another standard, EIP-712: Typed structured data hashing and signing.

Instead of asking users to sign an unreadable string of hex characters, EIP-712 allows DApps to present a clearly structured, human-readable message in wallets like MetaMask. This greatly improves security and user trust.

The permit function itself, and the data signed for it, have several key components to ensure security.

ERC20 Permit Tutorial - Allow users to approve token transfers without spending gas (EIP 2612)

The video 'ERC20 Permit Tutorial' by Shobhit (Web3 Club) provides an excellent breakdown of the technical components of the permit signature.

Watch the section 'Key Specifications: Permit, Nonce, Domain Separator' from 02:21 to 04:00. Pay close attention to the explanation of each part of the signed message.

Let's summarize the critical components from the video:

  • permit function parameters:
    • owner: The address of the user who owns the tokens.
    • spender: The address being granted the approval.
    • value: The amount of tokens to approve.
    • deadline: A Unix timestamp after which the signature is no longer valid. This prevents someone from holding onto a signature and using it unexpectedly in the distant future.
    • v, r, s: The three components of the cryptographic signature itself.
  • nonce (Number used once): This is a counter specific to each token owner, which is incremented every time a permit is successfully used. The current nonce must be included in the signed message. This is the primary defense against a replay attack, ensuring a signature can only be used once.
  • domain separator: This is a unique hash generated from the token's name, version, chain ID, and the contract's address. It ensures that a signature created for one contract on one network cannot be replayed on a different contract or a different network (e.g., a testnet signature being used on mainnet).

3. Implementing ERC20Permit in Solidity

Thanks to OpenZeppelin, adding EIP-2612 support to our MyToken contract from the previous lesson is remarkably simple. We just need to inherit from the ERC20Permit contract.

EIP-2612 Permit: A Practical Guide | by VorobevSA - Medium

The article 'EIP-2612 Permit: A Practical Guide' by VorobevSA on Medium shows just how easy the Solidity part is.

Read the 'Practical Implementation' section. Start from the 'All that’s left is to add support for Permit...' paragraph. Notice that it only requires importing ERC20Permit.sol and adding ERC20Permit to the constructor.

Let's update the MyToken.sol contract we built in the last lesson.

Updated MyToken.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; // 1. Import ERC20Permit

/**
 * @title MyToken
 * @dev A feature-rich ERC20 token with mint, burn, pausable, and permit functionalities.
 */
contract MyToken is ERC20, ERC20Burnable, ERC20Pausable, Ownable, ERC20Permit { // 2. Add to inheritance
    constructor(string memory name, string memory symbol, address initialOwner) 
        ERC20(name, symbol)
        ERC20Permit(name) // 3. Call the ERC20Permit constructor
    {
        _transferOwnership(initialOwner);
    }

    // ... (mint, pause, unpause, _beforeTokenTransfer functions remain the same)
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }

    function _beforeTokenTransfer(address from, address to, uint256 amount)
        internal
        override(ERC20, ERC20Pausable)
    {
        super._beforeTokenTransfer(from, to, amount);
    }
}

That's it. The ERC20Permit contract from OpenZeppelin handles all the underlying logic for the permit function, nonce management, and the domain separator. The simplicity on the contract side highlights how the main complexity shifts to the client-side application that prepares the signature.

4. Client-Side: Creating the Signature

This is where your front-end development experience is highly relevant. To use the permit function, a DApp's front-end must:

  1. Gather the necessary data (owner, spender, value, deadline).
  2. Fetch the user's current nonce from the token contract.
  3. Fetch the domain data from the token contract.
  4. Construct the EIP-712 typed data object.
  5. Prompt the user to sign this data using their wallet (e.g., via ethers.signer.signTypedData).
  6. Pass the resulting signature to the relayer who will call the permit function.

The Hardhat testing environment provides a perfect way to simulate this client-side logic.

EIP-2612 Permit: A Practical Guide | by VorobevSA - Medium

Let's return to the Medium article 'EIP-2612 Permit: A Practical Guide'. The Hardhat test script is a perfect, self-contained example of the client-side logic.

Read the section with the heading 'Now comes the most important part — testing Permit.' Study the code block for the test named 'Mint with Permint'. Focus on how the domain, types, and value objects are constructed before being passed to initialOwner.signTypedData(...).

Let's break down the key steps from that test script, which you would implement in a DApp using a library like ethers.js:

  1. Define the Domain: This object contains information that scopes the signature to your specific contract and network.

    const domain = {
        name: await myToken.name(),
        version: "1", // The version from the ERC20Permit constructor
        chainId: (await ethers.provider.getNetwork()).chainId,
        verifyingContract: await myToken.getAddress(),
    };
    
  2. Define the Types: This describes the structure of your Permit message, matching the EIP-2612 specification.

    const types = {
        Permit: [
            { name: "owner", type: "address" },
            { name: "spender", type: "address" },
            { name: "value", type: "uint256" },
            { name: "nonce", type: "uint256" },
            { name: "deadline", type: "uint256" },
        ],
    };
    
  3. Define the Value: This object contains the actual data for the specific approval being signed.

    const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now
    const value = {
        owner: owner.address,
        spender: spender.address,
        value: amountToApprove,
        nonce: await myToken.nonces(owner.address), // Crucial: get the current nonce
        deadline: deadline,
    };
    
  4. Sign and Split: The user's signer object is used to create the signature, which is then split into its v, r, and s components.

    const signature = await owner.signTypedData(domain, types, value);
    const sig = ethers.Signature.from(signature);
    
    // Now you have sig.v, sig.r, and sig.s
    

These components (owner, spender, value, deadline, sig.v, sig.r, sig.s) are everything a relayer needs to call the permit function on the token contract.

Conclusion

You have now mastered a powerful technique for improving DApp user experience. By implementing EIP-2612, you've replaced a clunky two-transaction process with a seamless one-click interaction for the user.

Key Takeaways:

  • Gasless Approvals: The permit function allows a user to approve token spending by signing an off-chain message, letting a third party pay the gas to submit the approval.
  • EIP-712 is Key: This standard provides a secure and user-friendly way to sign structured data, making the permit flow transparent to users.
  • Security Primitives: nonce, deadline, and the domain separator are essential for preventing replay attacks and ensuring signatures are not misused.
  • OpenZeppelin Simplifies: The ERC20Permit contract handles all the complex on-chain logic, allowing you to focus on the client-side implementation.
  • Client-Side Responsibility: The main work for permit lies in the front-end or client script, which must correctly construct, sign, and relay the typed data message.

Next Steps:

Now that we have a fully-featured token with a user-friendly approval mechanism, we are ready to use it in a core DeFi protocol. In the next lesson, we will build the heart of a decentralized exchange: the swap logic of an Automated Market Maker (AMM) using the constant product formula (x * y = k).

Can't find a good explanation? Sign up and we'll make it for you

Sign up