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.
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.
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.
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,
The permit()
function should do four things:
ecrecover()
. owner
argument matches the signer address that was extracted. The signer requesting the allowance MUST be the owner of the tokens!_approve()
function on behalf of the user to satisfy their intention for the trusted spender and amount.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.
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:
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.
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.
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()
.
The corresponding frontend setup looks like this:
approve()
call.Once the user has approved their tokens for the Permit2 contract, we must prepare the arguments needed to call our allowanceTransferWithPermit() function:
permitSingle
object, detailing all the allowance data.permitSingle
object with AllowanceTransfer.getPermitData()
._signeTypedData()
.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.
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.
The corresponding frontend setup looks like this:
permit
object, detailing all the allowance data. Nonces must be generated without causing collisions.witness
object if extra witness data is required.permit
object with SignatureTransfer.getPermitData()
._signeTypedData()
.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!