What if I told you never have to do an “Approve” transaction again?! Ok, maybe not never, but fewer times than you currently have to do? What if you can reduce the number of times you have to trust some random DeFi smart contract or that you’ll spend less time on revoke.cash when you hear about yet another hack! Well, Permit2 is a DeFi UX primitive that’s simplifies token approvals, safely. In this guide, I’ll walk you through a Permit2 frontend dApp that makes token approvals a lot easier.
Quick Background Lesson
Uniswap Labs developed Permit2 to allow token approvals to be shared across different applications by unifying ERC20 and NFT swapping in a single swap router. That’s a bit of a mouthful, but let’s say dApp A integrates with Permit2 and a user approved Permit2 to swap USDC for them; dApp B doesn’t have to do a new approval. All that dApp B has to do is check if there’s an existing approval, and if there is one, they can simply ask the user to sign an off-chain message allowing them to spend that approval, and dappB can transfer USDC directly if they submit the off-chain permit message alongside the transfer request. It really is that easy!
The traditional token approval process requires users to trust a protocol or dapp and then approve that protocol. If that protocol is compromised, then the user's tokens may be lost. The current process goes as follows:
- User sends an approve() transaction authorizing a protocol or dapp to transfer their tokens
- The Dapp can now transfer tokens on behalf of the user
Now, haters will say EIP-2612 solves this problem already, and while the token approval process is similar to Permit2, there are some subtle differences. EIP-2612 adds three new functions to the ERC-20 token standard: permit(), nonces(), and DOMAIN_SEPARATOR() to combine the consumption of token approvals and other standard ERC20 functions such as transfers. You can read more about EIP 2612 here. EIP-2612 adoption has been stymied because it can only work for new or upgradeable tokens.
Permit2, however, works with existing ERC20 tokens; no upgrades are required, and new dApps or protocols can write Permit2 convenience functions or simply use the Permit2 canonical contract to permission approvals and transfer tokens directly and safely.
With Permit2 however, a user will have to approve the canonical Permit2 contract that lives at this address across multiple chains, namely mainnet Ethereum, Goerli, Polygon, Optimism, Arbitrum, and Celo. After which any integrating protocol or dapp can submit a users’s Permit2 msg and signature and have tokens transferred seamlessly.
Repo
Here's the link to the repo I use in this guide.
Technical Details
In other Permit2 guides, the writers assume that there’s an intermediate protocol that sits between Alice and the Permit2 canonical contract; for the purpose of this article, we’ll assume the underlying dapp or protocol is represented by an address and the dApp will communicate with the canonical Permit2 contract directly, this is a frontend guide after all. For some background, I’m using this frontend template that’s not Web3 enabled, we’ll install a few dependencies that work with the Permit2 SDK, out of the box. So let’s get started.
Install Next.JS
You can choose to start a new Next.JS implementation but I used this template to get me up and running a little bit faster so feel free to start one of your own or do something else.
gh repo clone sozonome/nextarter-chakra
Install the Permit2 SDK
The first step is to install Permit2 SDK, which provides a few helpful functions that allow anyone to get started as quickly as possible. The only caveat is that the Permit2 SDK only works with the Ethers 5.7.2. Fun times, I know! Because of the joys dependency graphs, we’ll restrict ourselves to using this for now but it’s not required, you can make the various contract calls independently, that would just be outside the scope of this guide.
yarn add @uniswap/permit2-sdk ethers@5.7.2
We also add ethers js 5.7.2 as a SDK dependency. We’ll need it later on to pass providers to the SDK.
Add a few state variables
Because we are building a frontend using React, we can add a few state variables that we can use to control the state of the frontend.
const [account, setAccount] = useState<string>('')
const [spender, setSpender] = useState<string>('')
const [provider, setProvider] = useState<ethers.providers.Web3Provider>()
const token = '0x4f34BF3352A701AEc924CE34d6CfC373eABb186c'
- The
account
variable stores the currently connected address -
spender
is a convenience function that we use to pull an additional address -
provider
holds the Ethers provider that we need to use to build a signer and also interact with the Permit2 SDK - Token represents the Polygon Ecosystem Token on Goerli. This will be the main token contract that we will be interacting with.
- Connect Wallet Feature
Next, we’ll add a Connect Wallet feature to the dApp that will toggle the state depending on if a wallet is connected.
<Flex
direction="column"
alignItems="center"
justifyContent="center"
minHeight="70vh"
gap={4}
mb={8}
w="full"
>
<Text mb={4}>Welcome</Text>
{account ? (
<>
<Button onClick={handleApprove} mb={4}>
Authorize Permit2
</Button>
<Button onClick={handlePermit}>Permit</Button>
<Text mb={4}>Account: {account}</Text>
</>
) : (
<Button onClick={connectWallet}>Connect Wallet</Button>
)}
</Flex>
Once we have the frontend elements, we can add the Connect Wallet function to connect to the user’s wallet.
const connectWallet = useCallback(async () => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const address = await (window as any).ethereum.request({
method: 'eth_requestAccounts',
})
setProvider(
new ethers.providers.Web3Provider(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).ethereum
)
)
setAccount(address[0])
setSpender(address[1])
} catch (e) {
// eslint-disable-next-line no-console
console.error(e)
}
}, [])
This code block connects to the window.ethereum
object which returns an array of addresses that the user has enabled.
NOTE: This guide assumes at least two connected Ethereum addresses, the second address is not required for Permit2 to work, it just allows us to mock a protocol address.
If you have configured everything correctly, once you connect your wallet, your frontend should look like the image below.
Approve Permit2
This isn’t a required step to use Permit2, but it helps to ensure that a user has approved the Permit2 canonical contract to spend tokens.
Let’s add a button to enable approvals
<Button onClick={handleApprove} mb={4}>
Authorize Permit2
</Button>
And the logic for the Approval is fairly straightforward. First, we can now import from the Permit2 SDK library.
import {
AllowanceProvider,
PERMIT2_ADDRESS,
MaxAllowanceTransferAmount,
AllowanceTransfer,
} from '@uniswap/Permit2-sdk'
The handleApprove
function assigns a signer, which will prompt the user to sign the approval message and then submit a transaction.
const handleApprove = useCallback(async () => {
try {
const signer = provider!.getSigner(account)
const permit2ContractAbi = [
'function approve(address spender,uint amount)',
]
const permit2Contract = new Contract(token, permit2ContractAbi, signer)
const tx = await permit2Contract.approve(
PERMIT2_ADDRESS,
MaxAllowanceTransferAmount
)
await tx.wait()
} catch (e) {
// eslint-disable-next-line no-console
console.error(e)
}
}, [account, provider])
Permit2 Config
Now we get to the main stuff. The handlePermit
function processes the main Permit2 transfer. There are a few important steps that we will go through.
const handlePermit = useCallback(async () => {
const processPermit = async () => {
const signer = provider!.getSigner(account)
const allowanceProvider = new AllowanceProvider(
provider!,
PERMIT2_ADDRESS
)
// Address of the protocol contract that is being approved to spend tokens.
const SPENDER_ADDRESS = spender
/**
* Get the current allowance amount, expiration, and nonce using the AllowanceProvider.
* This is the same data that would be used to create a PermitSingle object.
* You can check permitAmount or expiration on this data to determine whether you need to create a new permit.
*/
const {
// amount: permitAmount,
// expiration,
nonce,
} = await allowanceProvider.getAllowanceData(
token,
account,
SPENDER_ADDRESS
)
/**
* Create a PermitSingle object with the maximum allowance amount, and a deadline 30 days in the future.
*/
const permitSingle: PermitSingle = {
details: {
token,
amount: MaxAllowanceTransferAmount,
expiration: toDeadline(/* 30 days= */ 1000 * 60 * 60 * 24 * 30),
nonce,
},
spender: SPENDER_ADDRESS,
sigDeadline: toDeadline(/* 30 mins= */ 1000 * 60 * 60 * 30),
}
const { domain, types, values } = AllowanceTransfer.getPermitData(
permitSingle,
PERMIT2_ADDRESS,
provider!.network.chainId
)
const signature = await signTypedData(signer, domain, types, values)
const permitAbi = [
'function permit(address owner, tuple(tuple(address token,uint160 amount,uint48 expiration,uint48 nonce) details, address spender,uint256 sigDeadline) permitSingle, bytes calldata signature)',
'function transferFrom(address from, address to, uint160 amount, address token)',
]
const permitContract = new Contract(PERMIT2_ADDRESS, permitAbi, signer)
await permitContract.permit(account, permitSingle, signature)
}
processPermit()
}, [account, provider, spender])
We begin by creating an AllowanceProvider object, which will contain many of the features we need for Permit2 transactions. The AllowanceProvider class is instantiated by passing a provider and the canonical PERMIT2_ADDRESS.
const {
amount: permitAmount,
expiration,
nonce,
} = await allowanceProvider.getAllowanceData(
token,
account,
SPENDER_ADDRESS
)
We read the current Permit2 status by querying the canonical address for the specific token approval amount, expiration and nonce. You can run checks at this stage and adapt behaviour according to your preferred business logic. From this point, we begin to see the real magic of Permit2. We are constructing a PermitSingle request, but this can be done as part of a batch using PermitBatch, this opens the door to multiple token approvals.
From this stage, you need to sign the transaction details
const { domain, types, values } = AllowanceTransfer.getPermitData(
permitSingle,
PERMIT2_ADDRESS,
provider!.network.chainId
)
const signature = await signTypedData(signer, domain, types, values)
getPermitData()
takes the permitSingle
object, the canonical address and the chain ID to construct a signature. The signTypedData()
is a Uniswap library helper method designed to work around some limitations in wallet configurations, I’ve found it very useful when working with this version of ethers so I recommend using it.
> yarn add @uniswap/conedison
import { signTypedData } from '@uniswap/conedison/provider/index'
To be clear, once you have a valid permitSingle
and signature, you have all you need to perform any Permit2 operation. The remainder of this guide fleshes out our use case.
Permit2 Transaction
Once you have gotten the signature, you have the ability to perform a token transfer either via your dapp smart contract or directly interact with the canonical Permit2 address that exposes some useful functions.
const permitAbi = [
'function permit(address owner, tuple(tuple(address token,uint160 amount,uint48 expiration,uint48 nonce) details, address spender,uint256 sigDeadline) permitSingle, bytes calldata signature)',
'function transferFrom(address from, address to, uint160 amount, address token)',
]
const permitContract = new Contract(PERMIT2_ADDRESS, permitAbi, signer)
await permitContract.permit(account, permitSingle, signature)
Fun fact: I learned the hard way that when constructing your own ABI, a Solidity struct has to be structured as a tuple. This means you have to define the struct members as parameters of a function of the tuple and name it. The permitSingle
struct is a bit confusing, seeing as it’s a tuple by itself, and also has a child details
struct that is also represented by a tuple. Now I know some older heads are probably laughing at me at this point but this was the first time I was building an ABI manually and it was a fun rabbit hole to go down. If you don't understand what I mean with this tuple business, no worries, just read your ABIs the way you're used to. I just pull ABIs from Etherscan.
From this stage, you can submit the transaction and that’s all it takes to approve and transact with Permit2. Backwards ERC-20 compatibility for the win!!
I’ve included a few more convenience features on the frontend that pull a user's Permit2 token approval details so the UI is a little more feature rich than described in this guide as you can see below, nothing too fancy, just stuff that should help developers get started as soon as possible.
- I’d like to see Uniswap Labs and the ecosystem at large advocate for more Permit2 adoption; it’s great when no-brainer UX implementations like this can become more mainstream.
- Adding viem support to the SDK would be great.
References:
Top comments (2)
Wow.. amazing. Seens pancakeswap removed the permit2, I don't know why. But I do you have it on github?