Uniswap Labs Blog

How to Integrate with Permit2
July 31, 2023

By Sara Reynolds and Zach Pomerantz

A safe swapping experience must balance security and speed. Token approvals are a great example of this tradeoff. Self-custody lets users specify who has permission to move funds from their wallets, but sending a token approval transaction takes time and costs gas. To improve this process and lower the number of clicks to swap, we released the Permit2 smart contract last year.

Instead of signing a token approval for each new smart contract, Permit2 allows token approvals to be shared and managed across different contracts, creating a more unified, cost-efficient, and safer UX. Permit2 is already improving Uniswap's product UX, but it was designed for anyone to use.

This guide will show you how. In it, we will:

  • Set up a sample demo contract to call the AllowanceTransfer function
  • Use Permit2 to permit and transfer funds
  • Construct Permit2 signatures on a frontend using the Permit2 SDK

image2

What can Permit2 do

Permit2 is a token approval contract that iterates on existing token approval mechanisms by introducing signature-based approvals and transfers for any ERC20 token, regardless of EIP-2612 support.

For each token, users have to submit a one-time traditional approval that sets Permit2 as an approved spender. Contracts that integrate with Permit2 can "borrow" the spender status for any token a user has approved on the canonical Permit2 contract. Any integrated contract can temporarily become a spender with a signed offchain Permit2 message. This effectively "shares" the one-time traditional token approval and skips the individual approval transaction for every protocol that supports Permit2 -- until the Permit2 message expires. It's a major UX upgrade with additional safety bumpers like expiring approvals and the lockdown feature.

image1

Permit2's design gives us a few helpful features. The full list of features can be found in our readme, but notably:

  1. Permits for any token. Applications can have a single transaction flow by sending a signature along with the transaction data for any token, including those not supporting a native permit method.
  2. Expiring approvals. Approvals can be time-bound, removing security concerns around hanging approvals on a wallet's entire token balance. Revoking approvals do not necessarily have to be a new transaction.
  3. Signature-based transfers. Users can bypass setting allowances entirely by releasing tokens to a permissioned spender through a one-time signature.
  4. Batch approvals and transfers. Users can set approvals on multiple tokens or execute multiple transfers with one transaction.
  5. Batch revoking allowances. Remove allowances on any number of tokens and spenders in one transaction.

Permit2 benefits greatly from network effects. As more teams integrate with Permit2, we'll begin to see network effects. So how do we do it?

How to accept Permit2 signatures in your contract

For app developers, Permit2 is an incredibly useful way to create a simpler, less expensive, and safer UX that your users will appreciate. In this guide, we’ll write a contract that integrates with Permit2 to make use of this new meta-transaction approval system.

AllowanceTransfer

The AllowanceTransfer contract shares and manages approvals with other spenders using a time-bounded expiry. It's functionally similar to traditional token approvals, handling allowances, transfers, and approval expiries. This style of approval works best if your application needs to access and transfer tokens frequently. With allowance transfer style approvals, an approved amount and expiry are set and saved in storage for each permitted spender. This allows the spender to transfer tokens as needed up until the spending limit is reached or until the approval expires.

SignatureTransfer

The SignatureTransfer contract introduces one-time permits for token transfers. Spenders are permitted to transfer tokens, but only in the same transaction where the signature is spent. To transfer funds again, you would need to request this signature again. The signature transfer method is great for applications that infrequently need tokens from users and want to ensure tighter restrictions on approvals. Persistent limits and expiries are not stored and never leave "hanging approvals." SignatureTransfer also lets you do a more advanced integration with Permit2 through permitWitnessTransferFrom, which we plan to detail in a later blog post.

Depending on the type of approval you need for your application, your contract will need to call one of these functions --- though these are not the only callable functions in Permit2:

Setting up your contract

In this example, we'll write a contract to use the allowance transfer style approval on Permit2. We'll assume that a user has already set a token approval to let Permit2 be a spender.

Since the permit entrypoint we want lives in the AllowanceTransfer contract, we'll first create our Demo contract to use the IAllowanceTransfer interface exposed in Permit2. In your constructor, you can pass the Permit2 address and save it as an immutable value. Permit2 is deployed to the same address across mainnet, Ethereum, Optimism, Arbitrum, Polygon, and Celo.

import {IAllowanceTransfer} from "Permit2/src/interfaces/IAllowanceTransfer.sol";

   contract Demo {
   IAllowanceTransfer public immutable permit2;

   constructor(address _permit2) {
       permit2 = IAllowanceTransfer(_permit2);
   }
}

Calling permit

Now that you've initialized with the canonical Permit2 contract, your contract can set an approval by calling permit. This permit function has a few parameters.

function permit(address owner, IAllowanceTransfer.PermitSingle memory permitSingle, bytes calldata signature) external

To call permit we need the owner's address, their signature, and the PermitSingle information. PermitSingle is a struct that holds the permit information that has been signed by the user. Before Permit2 can let our demo contract be a spender, it has to verify the user with their signature and PermitSingle data.

To call permit, we can create a permitThroughPermit2 function that will pass these parameters through. Later on, we will see how the application frontend requests and constructs the PermitSingle.

   function permitThroughPermit2(PermitSingle calldata permitSingle, bytes calldata signature) public {
       permit2.permit(msg.sender, permitSingle, signature);
   }

By passing msg.sender for the owner variable, we’re requiring that the caller of the function is also the one who signed the PermitSingle information. That’s one less parameter we need to collect for permitThroughPermit2.

Before we allow a contract to transfer a user’s tokens using the Permit2 approvals, we need to check that our demo contract is set to be spender. This checks if a user has called this function. If so, then we know we have spending permissions on our contract. We can add a custom error, InvalidSpender, and write a check that enforces that signatures passed through permitThroughtPermit2 construct our demo contract address as the spender.

Our function now looks something like this:

   function permitThroughPermit2(PermitSingle calldata permitSingle, bytes calldata signature) public {
       if (permitSingle.spender != address(this)) revert InvalidSpender();
       permit2.permit(msg.sender, permitSingle, signature);
   }

Transferring tokens

Permit2 uses a persistent mapping based design. This design stores token approvals information set by the user like the spender, amount, and expiration. Because this information is stored, this approval will last until the permitSingle.permitDetails.expiration. Until that timestamp, our demo contract can transfer the funds up to the allowance. Let’s write another function to send funds to this contract by calling the transferFrom function in Permit2.

function transferToMe(address token, uint160 amount) public {
       permit2.transferFrom(msg.sender, address(this), amount, token);
	// ...Do cool stuff ...
   }

By passing in msg.sender, we ensure that only the approved user can trigger this function to send some amount of a desired token from the sender to the demo contract. If this was instead a parameter that we pass in, then anyone could trigger the function pulling the funds from the user. After calling this function, our demo contract now has tokens that they can swap, deposit into a lending protocol, or do anything else with.

You may have noticed that permit and transferFrom are two separate entry points in our contract. If left like this, it would mean that the user has to initiate two transactions to get their tokens with any contract that is integrated with Permit2. We can instead combine the permit and transferFrom calls into one entrypoint:

   function permitAndTransferToMe(IAllowanceTransfer.PermitSingle calldata permitSingle, bytes calldata signature, uint160 amount) public {

       if (permitSingle.spender != address(this)) revert InvalidSpender();
       permit2.permit(msg.sender, permitSingle, signature);
       permit2.transferFrom(msg.sender, address(this), amount, permitSingle.details.token);
       //...Do cooler stuff ...
   }

If our demo contract is approved through a permit for longer than a single transaction, we do not need to pass in a signature each time to call transferFrom and pull tokens.

We used AllowanceTransfer as our entrypoint. SignatureAllowance lets us do the same thing, but the approval would not last beyond the single transaction. We could use the permitTransferFrom and permitWitnessTransferFrom functions to ensure even tighter restrictions at the expense of greater transaction costs.

How to construct Permit2 signatures on the frontend

To integrate Permit2 on the frontend we will be using the Permit2 SDK. We'll be calling into our demo contract, using permitAndTransferToMe.

First, in order to get the signature for a PermitSingle, you'll need the next valid nonce, which you can get using the SDK.

import { AllowanceProvider, PERMIT2_ADDRESS } from '@uniswap/Permit2-sdk'

const allowanceProvider = new AllowanceProvider(ethersProvider, PERMIT2_ADDRESS)
const { amount: permitAmount, expiration, nonce } = allowanceProvider.getAllowanceData(user, token, ROUTER_ADDRESS);

// You may also check amount/expiration here to see if you are already permitted -
// you may not need to generate a new signature.

Once you have that, you can construct the PermitSingle object which we’ll need to pass as our first parameter. This object is defined by the SDK as a typed interface to ensure it matches the contract’s struct. To construct it, we’ll need a few things:

  • The address of the token
  • The maximum amount of the token that will be permitted to be transferred
  • The deadline after which the permit will no longer be valid. At Uniswap Labs, we use 30 days.
  • The address of the spender that will be permitted to transfer funds on behalf of the user

These are the same input variables that our demo contract sends to the Permit2 contract.

import { MaxAllowanceTransferAmount, PermitSingle } from '@uniswap/Permit2-sdk'

const PERMIT_EXPIRATION = ms`30d`
const PERMIT_SIG_EXPIRATION = ms`30m`

/**
* Converts an expiration (in milliseconds) to a deadline (in seconds) suitable for the EVM.
* Permit2 expresses expirations as deadlines, but JavaScript usually uses milliseconds,
* so this is provided as a convenience function.
*/
function toDeadline(expiration: number): number {
return Math.floor((Date.now() + expiration) / 1000)
}

const permitSingle: PermitSingle = {
details: {
token: tokenAddress,
amount: MaxAllowanceTransferAmount,
// You may set your own deadline - we use 30 days.
expiration: toDeadline(/* 30 days= */ 1000 * 60 * 60 * 24 * 30),
nonce,
},
spender: spenderAddress,
// You may set your own deadline - we use 30 minutes.
sigDeadline: toDeadline(/* 30 minutes= */ 1000 * 60 * 60 * 30),
}

Now that you have your PermitSingle object you can pass it to the SDK to get back typed data for the user to sign, using AllowanceTransfer.getPermitData. This takes the PermitSingle, the address off the Permit2 contract on the chain, and the chain ID and returns typed data that you can pass directly to a library like ethers to request the user's signature.

import { AllowanceTransfer, PERMIT2_ADDRESS } from '@uniswap/Permit2-sdk'

const { domain, types, values } = AllowanceTransfer.getPermitData(permitSingle, PERMIT2_ADDRESS, chainId)

// We use an ethers signer to sign this data:
const signature = await provider.getSigner().signTypedData(domain, types, value)

// This assumes ethers@6. Our web app uses ethers@5, and we've run into a few cross-wallet compatibility issues, so we actually sign using a helper method: https://github.com/Uniswap/conedison/blob/6fdf4baf13a799ca6d37f6d3222f9194e1750007/src/provider/signing.ts#L32.

You should get a signature back from ethers, which you can pass onto other methods which use it. For another example, check out how we pass ours to the Universal Router.

Finally, we'll instantiate our contract using ethers, and use it to call permitAndTransferToMe.

import { Contract } from ‘ethers’

// We only need to provide the ABI that we will use.
const demoAbi = [function permitAndTransferToMe(PermitSingle calldata permitSingle, bytes calldata signature, uint160 amount),
]
const demoContract = new Contract(demoContractAddress, demoAbi, provider.getSigner())

await demoContract.permitAndTransferToMe(permitSingle, signature, amount)

Conclusion

You made it to the end! While it may initially seem daunting, we hope this blog makes it easier. And remember to check out the Permit2 code, our SDK, and docs for more.

Related posts