Permit2 is a token approval mechanism introduced to enhance the user experience of decentralized token transactions. It allows users to smart contracts without requiring on-chain approvals. It shifts the intensive work on to smart contracts. Users express their intents to modify their permissions.
Usually in a normal ERC-20 transfer transaction, an approval is given to the spender smart contract by modify allowances. The token smart contract has an allowance mapping that manages every given approval issued by the sender. Every application that has been approved has their permissions remain indefinitely unless the user revokes the permission granted to the application.
Instead of granting this permission to the application indefinitely, we could grant permission to a smart contract which has infinite amount permitted to spend on behalf of the user that makes on-chain transactions efficient, and reduces the computation over-head required - a normal approval ERC-20 transaction would first require the application to initiate a transfer from which triggers the approval thus requiring two separate transactions.
1. The user initiates a normal token approval to the permit2 contract and by doing so, they have granted permission to the permit2 contract to be able to spend their tokens on their behalf.
2. The user signs a gasless off-chain permit message expressing their intents to modify their permission.
3. The application takes the permit message and delivers to the permit2 contract informing it of the user’s intent.
4. The permit2 contract then verifies that signature is from the actual signer and does transfer of tokens to the actual spender which is the application.
Permit2 smart contract has two token approval mechanisms - allowance transfer and signature transfer.
In this section, we will explore the signature transfer mechanism and build a simple permit2 smart contract to understand the flow of the perrmit2 contract.
This section requires the user to have a knowledge of EIP-712 which is a signature standard for typed structured data for the off-chain signature mechanism to express intents.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/EIP712.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol";
contract SimplePermit2 is EIP712 {
using ECDSA for bytes32;
struct TokenPermissions {
address token;
uint256 amount;
}
struct PermitTransferFrom {
TokenPermissions permitted;
uint256 nonce;
uint256 deadline;
}
struct SignatureTransferDetails {
address to;
uint256 requestedAmount;
}
bytes32 private constant _PERMIT_TRANSFER_FROM_TYPEHASH =
keccak256("PermitTransferFrom(TokenPermissions permitted, address spender, uint256 nonce, uint256 deadline)TokenPermissions(address token,uint256 amount)");
bytes32 public constant _TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)");
constructor() EIP712("SimplePermit2", "1") {}
function permitTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails calldata transferDetails,
address owner,
uint8 v,
bytes32 r,
bytes32 s
) public view {
bytes memory signature = bytes.concat(r, s, bytes1(v));
uint256 requestedAmount = transferDetails.requestedAmount;
if (block.timestamp > permit.deadline) revert SignatureExpired(permit.deadline);
if (requestedAmount > permit.permitted.amount) revert InvalidAmount(permit.permitted.amount);
permit.nonce++;
signature.verify( _hashTypedData(getHashData(permit)), owner);
ERC20(permit.permitted.token).safeTransferFrom(owner, transferDetails.to, requestedAmount);
}
function getHashData(
PermitTransferFrom memory permit
) public view returns (bytes32) {
bytes32 tokenPermissionHash = keccak256(
abi.encode(_TOKEN_PERMISSIONS_TYPEHASH, permit.permitted)
);
return keccak256(
abi.encode(
_PERMIT_TRANSFER_FROM_TYPEHASH,
keccak256(abi.encodePacked(tokenPermissionHash)),
msg.sender,
permit.nonce,
permit.deadline
)
);
}
function verify(
bytes calldata signature,
bytes32 digest,
address claimedSigner,
) internal pure returns (bool) {
bytes32 r;
bytes32 s;
uint8 v;
(r, s) = abi.decode(signature, (bytes32, bytes32));
v = uint8(signature[64]);
(address signer, , ) = digest.tryRecover(v, r, s);
if (signer == address(0)) revert InvalidSignature();
if (signer != claimedSigner) revert InvalidSigner();
return signer == claimedSigner;
}
}
The frontend integration of our SimplePermit2
contract using ethers.js. This is similar to how Uniswap Permit2-sdk is written with a little of abstraction. But we will be writing an integration code of our contract using ethers.js.
import { ethers } from 'ethers';
async function generatePermitSignature(
ownerAddress: string,
tokenAddress: string,
amount: bigint,
nonce: bigint,
deadline: bigint,
spenderAddress: string,
permit2ContractAddress: string
) {
// 1. Connect to the user's wallet
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// 2. Prepare the domain and types for EIP-712 signature
const domain = {
name: "SimplePermit2",
version: "1",
chainId: await signer.getChainId(),
verifyingContract: permit2ContractAddress
};
const types = {
TokenPermissions: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" }
],
PermitTransferFrom: [
{ name: "permitted", type: "TokenPermissions" },
{ name: "spender", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
};
// 3. Create the value object
const value = {
permitted: {
token: tokenAddress,
amount: amount.toString()
},
spender: spenderAddress,
nonce: nonce.toString(),
deadline: deadline.toString()
};
// 4. Request the signature from the user's wallet
try {
const signature = await signer.signTypedData(domain, types, value);
// Split the signature into v, r, s components
const sig = ethers.Signature.from(signature);
return {
v: sig.v,
r: sig.r,
s: sig.s,
fullSignature: signature
};
} catch (error) {
console.error("Error signing typed data:", error);
throw error;
}
}
Gas Cost Savings:
Traditional ERC-20 token approvals require a separate transaction for each allowance, incurring gas fees. Permit2 eliminates the need for these redundant approval transactions, saving users gas costs.
Simplified User Experience:
Users can sign a single message off-chain instead of interacting with the blockchain twice (approval + transaction). This reduces complexity and enhances accessibility, especially for new users.
Reduced Approval Risks:
Traditional token approvals often involve granting large or unlimited allowances to smart contracts, exposing users to potential security risks. Permit2 enables users to set:
Custom Allowance Limits: Users can specify exact amounts to be approved.
Time-Limited Approvals: Permissions can expire automatically after a defined period, reducing exposure.
Cross-Platform Compatibility:
Permit2 is designed to work seamlessly across multiple decentralized applications (dApps) and protocols. This makes it a versatile solution for trading platforms, DeFi protocols, and wallet integrations.
Batch Operations:
Permit2 allows bulk approvals and transfers in a single transaction, optimizing workflows for advanced use cases such as portfolio management and multi-token swaps.
Permit2 is a significant step forward in enhancing the efficiency, security, and user experience of token approvals for decentralized trading. By reducing gas costs, simplifying workflows, and offering advanced features such as batch operations and time-limited allowances, Permit2 provides a powerful tool for DeFi protocols and traders alike.
For developers and platforms, integrating Permit2 into their systems can unlock better user experiences while optimizing transaction workflows. To integrate Permit2 into your solidity project, click here.