Back to blogs
Written by
Alex Babits
Published on
August 28, 2024

How to Implement Permit2

Learn to implement Permit2, the token approval system compatible with all ERC-20 tokens that streamlines user experience and reduces their economic burden.

Table of Contents

This article intends to journey through the history of token approval systems and arrive at the modern day Permit2 technique. A review of past systems is a prerequisite to understand and appreciate the functionality that Permit2 provides. If the reader is already comfortable with token approvals and EIP-2612, they may want to jump straight into the Permit2 section.

In crypto, it's difficult to wander very far before interacting with a dApp that requires permission to move someone's tokens on their behalf. The only way to grant this permission is through an approval. All permission solutions require changing the allowance mapping for a user's specific token and a trusted spender to a non-zero amount. This is ultimately accomplished through the execution of the internal _approve() function natively found in all ERC20 tokens. 

Permit2 is a token approval system compatible with all ERC-20 tokens that streamlines user experience and reduces their economic burden. It shifts the intensive work onto smart contracts. User’s only need to sign a gasless off chain message to express their intent to modify their permissions. This means dApps can handle all the internal approval mechanisms necessary to transfer tokens for users.

Permission’s humble beginnings

Image illustrating that early permitting functionality was like living in the stone age.
The Stone Age - Before Permit

The first solution to the permission problem requires no extra code or special techniques. It’s the standard token approval process and it works as follows:

When interacting with a dApp that requires permission to transfer tokens, a user will call their token's public approve() function to increase the dApp contract (spender) allowance value for their token. 

The user must be the address executing this transaction because the public approve() function sets the owner to be the msg.sender. Only after the transaction is confirmed on chain can the dApp spender successfully call the transferFrom() function to move funds on the users behalf. 

Because two different actors are needed to execute the two transactions, the process explicitly requires two on chain transactions to complete.

//OpenZeppelin ERC20
//https://github.com/OpenZeppelin/openzeppelin-contracts/blob/c304b6710b4b5fcf2a319ad28c36c49df6caef14/contracts/token/ERC20/ERC20.sol#L128
    function approve(
        address spender,
        uint256 value
    ) public virtual returns (bool) {
        // explicitly sets the owner as msg.sender
        // for the actual `_approve()` call.
        address owner = _msgSender();
        // `allowance` mapping gets updated for
        // the spender and amount on behalf of the owner.
        _approve(owner, spender, value);  
        return true;
    }

The approach is simple, but two transactions to transfer funds is cumbersome. Additionally, there is a large attack surface for users that interact with many dApps. 

Every dApp a user has granted permission to will have a non-zero allowance mapping that lasts indefinitely; unless they manually revoke that dApp's permissions back to zero through another on chain approve() call. 

If any of the approved dApps become compromised with respect to user's approved tokens, the user loses all their tokens associated with that address.

One small step with EIP-2612

Image illustrating that with the introduction of permit() function, permitting functionality moved into the iron age.
Iron Age - approaching permit2

The second solution extends the vanilla token approval standard through EIP-2612

Notably, EIP-2612 introduces a permit() function.

As the reader might guess, understanding how permit() works is paramount. Its inputs can be visualized in two chunks: The allowance parameters {owner, spender, value, deadline}, and the signature parameters {v, r, s} that represent the elliptic curve points which express an encrypted relationship between the message data and private key used during the signature.

Specifically,  

  • `r` is related to the user's private key and randomness generated during signing. 
  • `s` is related to the combination of the private key, `r`, and the hash of the message. 
  • `v` is a single byte that prevents signature malleability by specifying which of the two valid elliptic curve solutions to use.

The permit() function should do four things:

  1. Verify the signature's deadline has not expired.
  2. Extract the signer address associated with the signature’s points (v, r, s) and the allowance details packaged as a message hash via ecrecover()
  3. Verify the inputted owner argument matches the signer address that was extracted. The signer requesting the allowance MUST be the owner of the tokens!
  4. Call the token's internal _approve() function on behalf of the user to satisfy their intention for the trusted spender and amount.
// OpenZeppelin ERC20Permit.sol
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/c304b6710b4b5fcf2a319ad28c36c49df6caef14/contracts/token/ERC20/extensions/ERC20Permit.sol#L44
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual {
       
        // deadline check
        if (block.timestamp > deadline) revert ERC2612ExpiredSignature(deadline);


        // hash of approval data
        bytes32 structHash = keccak256(abi.encode(
            PERMIT_TYPEHASH,
            owner,
            spender,
            value,
            _useNonce(owner),
            deadline
        ));


        // EIP-712 hash of hashed approval data
        bytes32 hash = _hashTypedDataV4(structHash);


        // signer extraction
        address signer = ECDSA.recover(hash, v, r, s);


        // signer must be owner
        if (signer != owner) revert ERC2612InvalidSigner(signer, owner);
       
        // internal approval call
        _approve(owner, spender, value);
    }

Without the existence of an efficient cryptographic curve and the ability to recover a signer address with message data and signature points, none of this would be possible.

So onto the real questions... Why are the nerds excited? Where is the beauty?

Remarkably, the total number of actors needed to interact on chain to complete the approval and transfer process is reduced from two to one. Because the user expresses the intent to modify their allowance mapping with an off chain signature, they never need to touch the chain, and their approval duty is gasless

The user's sole responsibility is to generate the needed approval data that gets passed along to the spender, so the spender can handle everything swiftly.

To reiterate, the user (owner) does not need to be the actor that calls approve(). The spender can leverage permit() to handle the given approval data on the user's behalf, and then call transferFrom() to move funds, all in a single transaction.

Because the owner argument is explicitly sanitized to be the signer with the correct approval data during permit(), anyone can call permit(), but only valid allowance updates will execute successfully.

Not only are the number of transactions reduced from two to one, but this technique includes deadlines for allowances that can expire. This decreases the likelihood of a loss of funds in the event of an exploited dApp, and eliminates unwanted spender transactions that may happen long after an initial intent was expressed. 

The downside is this technique lacks backwards compatibility because it is an extension of the ERC-20 standard. Only future tokens that include the EIP or historic tokens with upgradability that choose to upgrade will benefit from the feature.

Permit2 - Welcome to the party

Image illustrating that with the introduction of Permit2, developers are now living in the modern era.
Modern Day Permit2

The third solution, originally called PermitEverywhere, was created by MerkleJerk. 

Uniswap noticed the idea, adapted a solution, and named it Permit2. Permit2 enjoys all the same benefits as EIP-2612, using the permit() concept as a centerpiece, in addition to solving for backwards compatibility. This expands the capabilities to all ERC-20 tokens that interact with dApps integrated with Permit2.

Rather than forcing ERC-20 tokens themselves to extend EIP-2612 to enjoy the benefits, Permit2 abstracts the concept away into a standalone contract system. This allows for generalized allowance tracking and signature verification that make everything possible.

The name Permit2 reflects the concept that there are two ways to interact with the Permit2 contract to achieve permissioned transfers. Despite the distinction between allowance and signature based transfers, both interaction types use signatures:

  1. Allowance based transfers: Handles token allowance through signatures, where transfers check the allowed amounts. This is a more efficient solution when multiple transfers are expected over time.
  2. Signature based transfers: Handles token transfers directly through signatures. More efficient for one-time transfers.

How does permit2 work?

  1. Prerequisite step expected from users.
    • They must perform a traditional approval for their tokens to the Permit2 contract. 
    • Often done for uint256 max value and only needs to be done once by a user for their token. 
    • Once complete, any dApps that integrate with Permit2 only need to request an off chain signature from the user to leverage the already granted permissions. 
  2. Actions once Permit2 has gained approval of user tokens.
    • A user expresses their permission intention through an off chain signature to allow a specific dApp spender contract to move their tokens. 
    • The spender acts as a courier to deliver the intention to the Permit2 contract, which can be thought of as a gatekeeper or middleman between the spender and user. 
    • If the Permit2 contract verifies the signature which explicitly requires the correct data, it will then use the pre-approved allowance to transfer tokens on behalf of the user to the spender.
    • Once the spender receives the tokens, it can do the necessary actions the user requested.

There are some unique drawbacks to Permit2. First, the prerequisite step forces users to approve their tokens to the Permit2 contract, acting as static friction for the UX and adoption. Secondly, the attack surface is narrow, pointing directly at the Permit2 contracts. The good news is that these contracts are concise, well written, tested, and audited.

Integrating with Permit2

Image of the Milky Way, illustrating in a fun way, the reader is a small speck inside a large universe "learning about esoteric token approval techniques."
Permit2 Techniques

This is a Sepolia mock integration that demonstrates the different ways to integrate with Permit2. All solidity code will be shown explicitly, while the frontend JavaScript code will only be briefly explained. The entire repo can be found here: https://github.com/alexbabits/permit2-example.

NOTE: Uniswap Permit2 SDK will fail to instantiate AllowanceProvider if used with ethers.js v6. If using the SDK, you must use ethers v5.7.2. Notably, ethers v5 does not have Sepolia support with Alchemy RPC provider, so another provider that supports Sepolia is required. This example uses Infura as the RPC provider.

Start by installing permit2 Uniswap GitHub repo as a dependency via Foundry, import necessary interfaces, and set a reference to Permit2 via the constructor.

// Permit2App.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {IPermit2, IAllowanceTransfer, ISignatureTransfer } from "permit2/src/interfaces/IPermit2.sol";

contract Permit2App {

    IPermit2 public immutable permit2;
    
    error InvalidSpender();

    constructor(address _permit2) {
        permit2 = IPermit2(_permit2);
    }
    
// ...
}

Implementing permit2 through Allowance Transfers

The allowance transfer technique requires us to update the allowance mapping in the Permit2 contract by calling permit2.permit() before we can transfer funds on behalf of the user. Once accomplished, the spender can freely call permit2.transferFrom() as often as needed to move tokens, as long as the original permission has not expired and the sum of transfer amounts does not exceed the permitted allowance. 

To reiterate, after permit2.permit() has been called for a particular user with specific allowance data, it is redundant to call it again unless needed. Notably, the permit() call increments a nonce associated with a particular owner, token, and spender for each signature to prevent double spend type attacks. 

The following demonstrates allowance transfer integration with and without the need for calling permit().

// Permit2App.sol (Continued)
   
    // Allowance Transfer when permit has not yet been called
    // or needs to be refreshed.
    function allowanceTransferWithPermit(
        IAllowanceTransfer.PermitSingle calldata permitSingle,
        bytes calldata signature,
        uint160 amount
    ) public {
        _permitWithPermit2(permitSingle, signature);
        _receiveUserTokens(permitSingle.details.token, amount);
    }


    /**
     * Allowance Transfer when permit has already been called
     * and isn't expired and within allowed amount.
     * Note: `permit2._transfer()` performs
     * all the necessary security checks to ensure
     * the allowance mapping for the spender
     * is not expired and within allowed amount.
     */
    function allowanceTransferWithoutPermit(address token, uint160 amount) public {
        _receiveUserTokens(token, amount);
    }


    // Helper function that calls `permit2.permit()`
    function _permitWithPermit2(
        IAllowanceTransfer.PermitSingle calldata permitSingle,
        bytes calldata signature
    ) internal {
        // This contract must have spending permissions for the user.
        if (permitSingle.spender != address(this)) revert InvalidSpender();


        // owner is explicitly msg.sender
        permit2.permit(msg.sender, permitSingle, signature);
    }


    // Helper function that calls `permit2.transferFrom()`
    // Transfers the allowed tokens from user to spender (our contract)
    function _receiveUserTokens(address token, uint160 amount) internal {
        permit2.transferFrom(msg.sender, address(this), amount, token);
    }


    //Note: There are batch versions of allowance transfers that allow for multiple tokens and/or destinations in one transaction.

The corresponding frontend setup looks like this:

  • Uniswap Permit2 SDK installation and importing of useful files: AllowanceTransfer, SignatureTransfer, PERMIT2_ADDRESS, MaxAllowanceTransferAmount.
  • Instantiation of a signer (USE DEVELOPER PRIVATE KEY).
  • The Permit2App contract, example token contract, and Permit2 contract must be instantiated. 
  • The one time initialization step to approve the Permit2 contract for the user's example token must be executed through a simple approve() call.

Once the user has approved their tokens for the Permit2 contract, we must prepare the arguments needed to call our allowanceTransferWithPermit() function:

  • Craft the permitSingle object, detailing all the allowance data.
  • Obtain the EIP-712 structured return data from our permitSingle object with AllowanceTransfer.getPermitData().
  • Sign the returned structured permit data via _signeTypedData().
  • Call our function with the permitSingle object, signature, and the amount the user wants to transfer.

For calls to allowanceTransferWithoutPermit(), the permitSingle object is not needed. A straight forward call with a desired amount is sufficient.

Implementing permit2 through Signature Transfers

The signature transfer technique offers a different approach for integrating Permit2. Instead of changing an allowance mapping in Permit2, we can call permitTransferFrom() immediately, as long as the signature and permit data are successfully verified. This is more gas efficient due to fewer state updates, and best suited for situations where multiple transfers are not expected.

Importantly, signatures associated with a specific permission request cannot be reused because upon transfer completion the associated nonce is flipped from 0 to 1. 

Notably, there is no method to "get the current nonce" as with Allowance Transfers because nonces are stored as bits in an unordered manner within a bitmap. You can generate nonces in any way you wish on the frontend as long as the generation technique does not cause collisions. Incrementation or randomness (with a sufficiently large range) are two valid methods. 

Additionally, custom data called the "witness" can be added to signatures. Witness data can be passed along through the permitWitnessTransferFrom() function. This is useful when using relayers or specifying custom order details. The added wrinkle of witness data is that it must be handled very precisely. The witness data requires the creation of a custom witness struct, along with the associated type string and type hash.

Below shows both a normal signature transfer function and one that includes extra witness data.

/// Normal SignatureTransfer
    function signatureTransfer(
        address token,
        uint256 amount,
        uint256 nonce,
        uint256 deadline,
        bytes calldata signature
    ) public {
        permit2.permitTransferFrom(
            // The permit message. Spender is the caller (this contract)
            ISignatureTransfer.PermitTransferFrom({
                permitted: ISignatureTransfer.TokenPermissions({
                    token: token,
                    amount: amount
                }),
                nonce: nonce,
                deadline: deadline
            }),
            ISignatureTransfer.SignatureTransferDetails({
                to: address(this),
                requestedAmount: amount
            }),
            msg.sender, // The owner of the tokens has to be the signer
            signature // The resulting signature from signing hash of permit data per EIP-712 standards
        );
    }


 
    // State needed for `signatureTransferWithWitness()`.
    // Unconventionally placed here as to not clutter the other examples.
    struct Witness {
        address user;
    }


    // The full type string with witness,
    // notice structs are alphabetical:
    // "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Witness witness)TokenPermissions(address token,uint256 amount)Witness(address user)"


    // However, we only want to REMAINING EIP-712 structured type definition,
    // starting exactly with the witness.
    string constant WITNESS_TYPE_STRING = "Witness witness)TokenPermissions(address token,uint256 amount)Witness(address user)";


    // The type hash must hash our created witness struct.
    bytes32 constant WITNESS_TYPEHASH = keccak256("Witness(address user)");




    // SignatureTransfer technique with extra witness data
    function signatureTransferWithWitness(
        address token,
        uint256 amount,
        uint256 nonce,
        uint256 deadline,
        address user, // example extra witness data
        bytes calldata signature
    ) public {
        bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, Witness(user)));


        permit2.permitWitnessTransferFrom(
            ISignatureTransfer.PermitTransferFrom({
                permitted: ISignatureTransfer.TokenPermissions({
                    token: token,
                    amount: amount
                }),
                nonce: nonce,
                deadline: deadline
            }),
            ISignatureTransfer.SignatureTransferDetails({
                to: address(this),
                requestedAmount: amount
            }),
            msg.sender, // The owner of the tokens has to be the signer
            witness, // Extra data to include when checking the signature
            WITNESS_TYPE_STRING, // EIP-712 type definition for REMAINING string stub of the typehash
            signature // The resulting signature from signing hash of permit data per EIP-712 standards
        );
    }
   
   // Note: There are batch versions of signature transfers that allow for multiple tokens and/or destinations in one transaction.

The corresponding frontend setup looks like this:

  • Requires the same instantiations and Permit2 token approval found in the Allowance Transfer frontend section. After which, most core tasks are similar to allowance transfers.
  • Craft the permit object, detailing all the allowance data. Nonces must be generated without causing collisions.
  • Craft the witness object if extra witness data is required.
  • Obtain the EIP-712 structured return data from our permit object with SignatureTransfer.getPermitData().
  • Sign the returned structured permit data with _signeTypedData().
  • Call our function with the permission data, signature, and witness data if applicable.

With the conclusion of this Permit2 code integration example, that marks the end of the road for today's journey. Hopefully this has shed enough light on the inner workings of Permit2, so that developers can give their users a better token approval experience, and white hats can keep the integrating protocols safe.

Happy hacking and building!

Secure your protocol today

Join some of the biggest protocols and companies in creating a better internet. Our security researchers will help you throughout the whole process.
Stay on the bleeding edge of security
Carefully crafted, short smart contract security tips and news freshly delivered every week.