Developing a Permit2 Token Approvals System
Classic ERC-20 requires two transactions for any DeFi interaction: approve() and then transferFrom(). This is both a UX problem and a security problem. Endless approvals to third-party protocols have caused millions in losses when hacks occurred (approved protocol, it got hacked, attacker drained all approved tokens). EIP-2612 added permit() for signed approvals without transactions, but not all tokens support it. Uniswap's Permit2 solves both issues at the level of a single hub contract.
How Permit2 Works
The idea is simple: a user approves Permit2Address once with approve(Permit2Address, type(uint256).max) for each token. After that, the Permit2 contract manages permissions on their behalf — but through signed off-chain messages, without additional transactions.
Two modes of operation:
AllowanceTransfer — like standard ERC-20 approve, but through Permit2. Permission has an expiration. The protocol requests the user's signature for PermitSingle or PermitBatch, then can call transferFrom through Permit2.
SignatureTransfer — a one-time transfer by signature. No permanent permission — only a specific transaction, signed off-chain. Atomic: if the transaction reverts — the nonce is marked as used, but the transfer doesn't happen.
Integration into Smart Contract
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol";
contract MyProtocol {
IPermit2 public immutable permit2;
constructor(address _permit2) {
permit2 = IPermit2(_permit2);
}
// Accept tokens through SignatureTransfer (one-time transfer)
function deposit(
uint256 amount,
ISignatureTransfer.PermitTransferFrom memory permit,
bytes calldata signature
) external {
permit2.permitTransferFrom(
permit,
ISignatureTransfer.SignatureTransferDetails({
to: address(this),
requestedAmount: amount
}),
msg.sender,
signature
);
// amount tokens are now with us, continue logic
_processDeposit(msg.sender, amount);
}
}
On the frontend side (wagmi/viem):
import { signTypedData } from "wagmi/actions";
const permitData = {
permitted: {
token: tokenAddress,
amount: parseEther("100"),
},
nonce: await getPermit2Nonce(userAddress),
deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour
};
const signature = await signTypedData({
domain: {
name: "Permit2",
chainId: 1,
verifyingContract: PERMIT2_ADDRESS,
},
types: PERMIT_TRANSFER_FROM_TYPES,
primaryType: "PermitTransferFrom",
message: permitData,
});
// Send depositData + signature to contract
Managing Nonce in SignatureTransfer
Unlike standard EIP-2612, where nonce is linear (each signature = next nonce), Permit2 uses bitmap-based nonce. A nonce is a 256-bit number where wordPosition = nonce >> 8 and bitPosition = nonce & 0xFF. This allows invalidating specific permissions without needing to use all previous nonces in order.
Users can revoke a specific pending nonce through:
permit2.invalidateUnorderedNonces(wordPosition, mask);
This is critical for UX: if a user signed a permission with a long deadline, they can revoke it without a new approve transaction.
AllowanceTransfer: Permanent Permissions with Expiration
// Check and use existing AllowanceTransfer permission
function depositWithAllowance(address token, uint160 amount) external {
permit2.transferFrom(msg.sender, address(this), amount, token);
// Permit2 will check: is there permission, has it expired, is the amount sufficient
}
The advantage over standard transferFrom: the permission automatically expires on expiration. Users see in the interface what's approved: "you approved X tokens until Y date". This is more transparent than infinite ERC-20 approve.
Common Integration Mistakes
Not checking requestedAmount against the amount actually passed. If the contract accepts any amount from permit, an attacker could pass a permit for a large amount but transfer less — or vice versa if logic is incorrect.
Hardcoded Permit2 address. Permit2 is deployed at one address (0x000000000022D473030F116dDEE9F6B43aC78BA3) via CREATE2 on all EVM networks. Use it as a constant, not as a constructor parameter in production.
Not handling revert from permit2. If the signature is invalid or nonce is already used — permit2 reverts. Make sure the contract correctly propagates this revert rather than masking it.
Work Process
Analysis. Determine which mode is needed: AllowanceTransfer for protocols with persistent access to funds, SignatureTransfer for atomic operations. Usually we use a combination.
Development. Integration into smart contract + typed data signing types on frontend. Foundry tests with PermitSignature helper from permit2 repository.
Testing. Fork tests on mainnet with real Permit2 contract. Test scenarios of expired deadline, used nonce, invalid signature.
Timeline Estimates
Integrating Permit2 into an existing contract + frontend: 2-3 days. Includes both modes (AllowanceTransfer and SignatureTransfer), tests, and UI for viewing/revoking permissions.







