Skip to content

Instantly share code, notes, and snippets.

@Signor1
Created June 25, 2025 14:34
Show Gist options
  • Select an option

  • Save Signor1/83b47ff7fc1f832dfc15375ff78192f4 to your computer and use it in GitHub Desktop.

Select an option

Save Signor1/83b47ff7fc1f832dfc15375ff78192f4 to your computer and use it in GitHub Desktop.
Implementing ERC20 Permit

React Hooks (JavaScript)

  1. useERC20Permit Hook

For tokens that already inherit ERC20Permit (EIP-2612) Single transaction: No need for separate approve + transferFrom Functions:

generatePermitSignature() - Creates the permit signature depositWithPermit() - Deposits in one transaction

  1. usePermit2 Hook

For any ERC20 token using the canonical Permit2 contract Functions:

checkPermit2Allowance() - Check if Permit2 has allowance approvePermit2() - One-time approval to Permit2 (user does this once) generatePermit2Signature() - Creates Permit2 signature depositWithPermit2() - Deposits with Permit2

  1. Utility Functions

getDeadline() - Generate deadline timestamps generateNonce() - Generate random nonces for Permit2 supportsERC20Permit() - Check if token supports EIP-2612

Solidity Contracts

  1. TokenVault Contract

Standard deposit: Traditional approve + transferFrom ERC20Permit deposit: Single transaction for EIP-2612 tokens Permit2 deposit: Single transaction for any ERC20 token Batch deposits: Multiple tokens in one transaction

  1. AdvancedTokenVault Contract

All features from TokenVault Time-locked deposits with both permit methods Lock duration enforcement on withdrawals

Key Benefits For ERC20Permit tokens:

✅ Single transaction (no separate approve needed) ✅ Better UX - users sign once and deposit ✅ No dangling allowances - permit is consumed immediately ❌ Limited to tokens that implement EIP-2612

For Permit2:

✅ **

import { useState, useCallback } from 'react';
import { ethers } from 'ethers';
// Canonical Permit2 contract address (same across all networks)
const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
// EIP-712 Domain for ERC20Permit
const EIP2612_DOMAIN = {
name: '', // Will be filled from token contract
version: '1',
chainId: 0, // Will be filled dynamically
verifyingContract: '' // Will be filled with token address
};
// EIP-712 Types for ERC20Permit
const EIP2612_TYPES = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
};
// EIP-712 Domain for Permit2
const PERMIT2_DOMAIN = {
name: 'Permit2',
chainId: 0, // Will be filled dynamically
verifyingContract: PERMIT2_ADDRESS
};
// EIP-712 Types for Permit2
const PERMIT2_TYPES = {
PermitTransferFrom: [
{ name: 'permitted', type: 'TokenPermissions' },
{ name: 'spender', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
],
TokenPermissions: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' }
]
};
/**
* Hook for ERC20Permit tokens (EIP-2612)
* For tokens that already inherit ERC20Permit functionality
*/
export const useERC20Permit = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const generatePermitSignature = useCallback(async ({
provider,
tokenAddress,
owner,
spender,
value,
deadline
}) => {
try {
setLoading(true);
setError(null);
const signer = provider.getSigner();
const network = await provider.getNetwork();
// Get token contract instance
const tokenContract = new ethers.Contract(
tokenAddress,
[
'function name() view returns (string)',
'function nonces(address) view returns (uint256)'
],
provider
);
// Get token name and user nonce
const [tokenName, nonce] = await Promise.all([
tokenContract.name(),
tokenContract.nonces(owner)
]);
// Prepare domain
const domain = {
...EIP2612_DOMAIN,
name: tokenName,
chainId: network.chainId,
verifyingContract: tokenAddress
};
// Prepare permit message
const message = {
owner,
spender,
value: value.toString(),
nonce: nonce.toString(),
deadline: deadline.toString()
};
// Sign the permit
const signature = await signer._signTypedData(domain, EIP2612_TYPES, message);
// Split signature
const sig = ethers.utils.splitSignature(signature);
setLoading(false);
return {
message,
signature,
v: sig.v,
r: sig.r,
s: sig.s,
nonce: nonce.toString(),
deadline: deadline.toString()
};
} catch (err) {
setError(err.message);
setLoading(false);
throw err;
}
}, []);
const depositWithPermit = useCallback(async ({
provider,
vaultAddress,
tokenAddress,
amount,
deadline
}) => {
try {
setLoading(true);
setError(null);
const signer = provider.getSigner();
const owner = await signer.getAddress();
// Generate permit signature
const permitData = await generatePermitSignature({
provider,
tokenAddress,
owner,
spender: vaultAddress,
value: amount,
deadline
});
// Call vault contract with permit
const vaultContract = new ethers.Contract(
vaultAddress,
[
'function depositWithPermit(address token, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s)'
],
signer
);
const tx = await vaultContract.depositWithPermit(
tokenAddress,
amount,
deadline,
permitData.v,
permitData.r,
permitData.s
);
setLoading(false);
return tx;
} catch (err) {
setError(err.message);
setLoading(false);
throw err;
}
}, [generatePermitSignature]);
return {
generatePermitSignature,
depositWithPermit,
loading,
error
};
};
/**
* Hook for Permit2 integration
* For vanilla ERC20 tokens using the canonical Permit2 contract
*/
export const usePermit2 = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const checkPermit2Allowance = useCallback(async ({
provider,
tokenAddress,
owner
}) => {
try {
const tokenContract = new ethers.Contract(
tokenAddress,
['function allowance(address owner, address spender) view returns (uint256)'],
provider
);
const allowance = await tokenContract.allowance(owner, PERMIT2_ADDRESS);
return allowance;
} catch (err) {
console.error('Error checking Permit2 allowance:', err);
return ethers.BigNumber.from(0);
}
}, []);
const approvePermit2 = useCallback(async ({
provider,
tokenAddress
}) => {
try {
setLoading(true);
setError(null);
const signer = provider.getSigner();
const tokenContract = new ethers.Contract(
tokenAddress,
['function approve(address spender, uint256 amount) returns (bool)'],
signer
);
// Approve max amount to Permit2
const tx = await tokenContract.approve(
PERMIT2_ADDRESS,
ethers.constants.MaxUint256
);
setLoading(false);
return tx;
} catch (err) {
setError(err.message);
setLoading(false);
throw err;
}
}, []);
const generatePermit2Signature = useCallback(async ({
provider,
tokenAddress,
owner,
spender,
amount,
deadline,
nonce
}) => {
try {
setLoading(true);
setError(null);
const signer = provider.getSigner();
const network = await provider.getNetwork();
// Prepare domain
const domain = {
...PERMIT2_DOMAIN,
chainId: network.chainId
};
// Prepare permit message
const message = {
permitted: {
token: tokenAddress,
amount: amount.toString()
},
spender,
nonce: nonce.toString(),
deadline: deadline.toString()
};
// Sign the permit
const signature = await signer._signTypedData(domain, PERMIT2_TYPES, message);
setLoading(false);
return {
permit: {
permitted: {
token: tokenAddress,
amount: amount.toString()
},
nonce: nonce.toString(),
deadline: deadline.toString()
},
signature,
owner
};
} catch (err) {
setError(err.message);
setLoading(false);
throw err;
}
}, []);
const depositWithPermit2 = useCallback(async ({
provider,
vaultAddress,
tokenAddress,
amount,
deadline,
nonce
}) => {
try {
setLoading(true);
setError(null);
const signer = provider.getSigner();
const owner = await signer.getAddress();
// Check if Permit2 allowance exists
const allowance = await checkPermit2Allowance({
provider,
tokenAddress,
owner
});
// If no allowance, user needs to approve Permit2 first
if (allowance.lt(amount)) {
throw new Error('Insufficient Permit2 allowance. Please approve Permit2 first.');
}
// Generate Permit2 signature
const permitData = await generatePermit2Signature({
provider,
tokenAddress,
owner,
spender: vaultAddress,
amount,
deadline,
nonce
});
// Call vault contract with Permit2
const vaultContract = new ethers.Contract(
vaultAddress,
[
`function depositWithPermit2(
address token,
uint256 amount,
uint256 nonce,
uint256 deadline,
bytes calldata signature
)`
],
signer
);
const tx = await vaultContract.depositWithPermit2(
tokenAddress,
amount,
nonce,
deadline,
permitData.signature
);
setLoading(false);
return tx;
} catch (err) {
setError(err.message);
setLoading(false);
throw err;
}
}, [generatePermit2Signature, checkPermit2Allowance]);
return {
checkPermit2Allowance,
approvePermit2,
generatePermit2Signature,
depositWithPermit2,
loading,
error,
PERMIT2_ADDRESS
};
};
/**
* Utility functions
*/
export const permitUtils = {
// Generate a deadline timestamp (current time + minutes)
getDeadline: (minutesFromNow = 30) => {
return Math.floor(Date.now() / 1000) + (minutesFromNow * 60);
},
// Generate a random nonce for Permit2
generateNonce: () => {
return ethers.BigNumber.from(ethers.utils.randomBytes(32)).toString();
},
// Check if a token supports ERC20Permit
supportsERC20Permit: async (provider, tokenAddress) => {
try {
const tokenContract = new ethers.Contract(
tokenAddress,
[
'function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)',
'function nonces(address owner) view returns (uint256)',
'function DOMAIN_SEPARATOR() view returns (bytes32)'
],
provider
);
// Try to call DOMAIN_SEPARATOR to check if permit is supported
await tokenContract.DOMAIN_SEPARATOR();
return true;
} catch {
return false;
}
}
};
/**
* Example usage component
*/
export const PermitExampleUsage = () => {
const erc20Permit = useERC20Permit();
const permit2 = usePermit2();
const handleERC20PermitDeposit = async () => {
try {
const deadline = permitUtils.getDeadline(30); // 30 minutes from now
const tx = await erc20Permit.depositWithPermit({
provider: window.ethereum, // or your provider
vaultAddress: '0x...', // Your vault contract address
tokenAddress: '0x...', // ERC20Permit token address
amount: ethers.utils.parseEther('100'), // Amount to deposit
deadline
});
console.log('ERC20Permit deposit transaction:', tx.hash);
await tx.wait();
console.log('Deposit confirmed!');
} catch (error) {
console.error('ERC20Permit deposit failed:', error);
}
};
const handlePermit2Deposit = async () => {
try {
const deadline = permitUtils.getDeadline(30);
const nonce = permitUtils.generateNonce();
const tx = await permit2.depositWithPermit2({
provider: window.ethereum, // or your provider
vaultAddress: '0x...', // Your vault contract address
tokenAddress: '0x...', // Any ERC20 token address
amount: ethers.utils.parseEther('100'), // Amount to deposit
deadline,
nonce
});
console.log('Permit2 deposit transaction:', tx.hash);
await tx.wait();
console.log('Deposit confirmed!');
} catch (error) {
console.error('Permit2 deposit failed:', error);
}
};
const handleApprovePermit2 = async () => {
try {
const tx = await permit2.approvePermit2({
provider: window.ethereum,
tokenAddress: '0x...' // Token address
});
console.log('Permit2 approval transaction:', tx.hash);
await tx.wait();
console.log('Permit2 approved!');
} catch (error) {
console.error('Permit2 approval failed:', error);
}
};
return (
<div>
<h3>ERC20Permit Deposit (Single Transaction)</h3>
<button
onClick={handleERC20PermitDeposit}
disabled={erc20Permit.loading}
>
{erc20Permit.loading ? 'Processing...' : 'Deposit with ERC20Permit'}
</button>
<h3>Permit2 Integration</h3>
<button
onClick={handleApprovePermit2}
disabled={permit2.loading}
>
{permit2.loading ? 'Processing...' : 'Approve Permit2 (One-time)'}
</button>
<button
onClick={handlePermit2Deposit}
disabled={permit2.loading}
>
{permit2.loading ? 'Processing...' : 'Deposit with Permit2'}
</button>
{erc20Permit.error && <p style={{color: 'red'}}>ERC20Permit Error: {erc20Permit.error}</p>}
{permit2.error && <p style={{color: 'red'}}>Permit2 Error: {permit2.error}</p>}
</div>
);
};
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
// Permit2 interface
interface IPermit2 {
struct TokenPermissions {
address token;
uint256 amount;
}
struct PermitTransferFrom {
TokenPermissions permitted;
uint256 nonce;
uint256 deadline;
}
struct SignatureTransferDetails {
address to;
uint256 requestedAmount;
}
function permitTransferFrom(
PermitTransferFrom calldata permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes calldata signature
) external;
}
/**
* @title TokenVault
* @dev A vault contract that supports both ERC20Permit and Permit2 for gasless approvals
*/
contract TokenVault is ReentrancyGuard {
// Canonical Permit2 contract address (same across all networks)
address public constant PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
// Events
event Deposit(address indexed user, address indexed token, uint256 amount);
event Withdrawal(address indexed user, address indexed token, uint256 amount);
// User balances: user => token => amount
mapping(address => mapping(address => uint256)) public balances;
// Total deposits per token
mapping(address => uint256) public totalDeposits;
/**
* @dev Standard deposit function (requires prior approval)
* @param token The ERC20 token address
* @param amount The amount to deposit
*/
function deposit(address token, uint256 amount) external nonReentrant {
require(amount > 0, "Amount must be greater than 0");
// Transfer tokens from user to vault
IERC20(token).transferFrom(msg.sender, address(this), amount);
// Update balances
balances[msg.sender][token] += amount;
totalDeposits[token] += amount;
emit Deposit(msg.sender, token, amount);
}
/**
* @dev Deposit using ERC20Permit (EIP-2612) - Single transaction
* @param token The ERC20Permit token address
* @param amount The amount to deposit
* @param deadline The permit deadline
* @param v The permit signature parameter
* @param r The permit signature parameter
* @param s The permit signature parameter
*/
function depositWithPermit(
address token,
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external nonReentrant {
require(amount > 0, "Amount must be greater than 0");
// Execute permit to grant allowance
IERC20Permit(token).permit(
msg.sender,
address(this),
amount,
deadline,
v,
r,
s
);
// Transfer tokens from user to vault
IERC20(token).transferFrom(msg.sender, address(this), amount);
// Update balances
balances[msg.sender][token] += amount;
totalDeposits[token] += amount;
emit Deposit(msg.sender, token, amount);
}
/**
* @dev Deposit using Permit2 - Works with any ERC20 token
* @param token The ERC20 token address
* @param amount The amount to deposit
* @param nonce The permit nonce
* @param deadline The permit deadline
* @param signature The permit signature
*/
function depositWithPermit2(
address token,
uint256 amount,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) external nonReentrant {
require(amount > 0, "Amount must be greater than 0");
// Prepare Permit2 structs
IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({
permitted: IPermit2.TokenPermissions({
token: token,
amount: amount
}),
nonce: nonce,
deadline: deadline
});
IPermit2.SignatureTransferDetails memory transferDetails = IPermit2.SignatureTransferDetails({
to: address(this),
requestedAmount: amount
});
// Execute permit transfer through Permit2
IPermit2(PERMIT2_ADDRESS).permitTransferFrom(
permit,
transferDetails,
msg.sender,
signature
);
// Update balances
balances[msg.sender][token] += amount;
totalDeposits[token] += amount;
emit Deposit(msg.sender, token, amount);
}
/**
* @dev Batch deposit using Permit2 - Deposit multiple tokens in one transaction
* @param tokens Array of ERC20 token addresses
* @param amounts Array of amounts to deposit
* @param nonce The permit nonce
* @param deadline The permit deadline
* @param signature The permit signature
*/
function batchDepositWithPermit2(
address[] calldata tokens,
uint256[] calldata amounts,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) external nonReentrant {
require(tokens.length == amounts.length, "Arrays length mismatch");
require(tokens.length > 0, "Empty arrays");
// Note: This is a simplified example. For actual batch transfers,
// you'd need to use the PermitBatchTransferFrom struct and
// the corresponding permitTransferFrom overload in Permit2
for (uint256 i = 0; i < tokens.length; i++) {
require(amounts[i] > 0, "Amount must be greater than 0");
// For each token, create individual permit and transfer
IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({
permitted: IPermit2.TokenPermissions({
token: tokens[i],
amount: amounts[i]
}),
nonce: nonce + i, // Different nonce for each permit
deadline: deadline
});
IPermit2.SignatureTransferDetails memory transferDetails = IPermit2.SignatureTransferDetails({
to: address(this),
requestedAmount: amounts[i]
});
// This is simplified - in practice you'd want to batch the permits
IPermit2(PERMIT2_ADDRESS).permitTransferFrom(
permit,
transferDetails,
msg.sender,
signature // In practice, you'd have separate signatures for each permit
);
// Update balances
balances[msg.sender][tokens[i]] += amounts[i];
totalDeposits[tokens[i]] += amounts[i];
emit Deposit(msg.sender, tokens[i], amounts[i]);
}
}
/**
* @dev Withdraw tokens from the vault
* @param token The ERC20 token address
* @param amount The amount to withdraw
*/
function withdraw(address token, uint256 amount) external nonReentrant {
require(amount > 0, "Amount must be greater than 0");
require(balances[msg.sender][token] >= amount, "Insufficient balance");
// Update balances
balances[msg.sender][token] -= amount;
totalDeposits[token] -= amount;
// Transfer tokens back to user
IERC20(token).transfer(msg.sender, amount);
emit Withdrawal(msg.sender, token, amount);
}
/**
* @dev Get user's balance for a specific token
* @param user The user address
* @param token The token address
* @return The user's balance
*/
function getBalance(address user, address token) external view returns (uint256) {
return balances[user][token];
}
/**
* @dev Get total deposits for a specific token
* @param token The token address
* @return The total deposits
*/
function getTotalDeposits(address token) external view returns (uint256) {
return totalDeposits[token];
}
/**
* @dev Emergency function to recover tokens (only for owner)
* This is a simplified example - in production, you'd want proper access control
*/
function emergencyWithdraw(address token) external {
// Add proper access control here (e.g., onlyOwner modifier)
uint256 balance = IERC20(token).balanceOf(address(this));
IERC20(token).transfer(msg.sender, balance);
}
}
/**
* @title AdvancedTokenVault
* @dev Enhanced version with additional features like time locks and yield farming
*/
contract AdvancedTokenVault is TokenVault {
// Time lock for deposits
mapping(address => mapping(address => uint256)) public lockUntil;
// Minimum lock duration (e.g., 1 day)
uint256 public constant MIN_LOCK_DURATION = 1 days;
event DepositLocked(address indexed user, address indexed token, uint256 amount, uint256 lockUntil);
/**
* @dev Deposit with time lock using ERC20Permit
*/
function depositWithPermitAndLock(
address token,
uint256 amount,
uint256 lockDuration,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external nonReentrant {
require(amount > 0, "Amount must be greater than 0");
require(lockDuration >= MIN_LOCK_DURATION, "Lock duration too short");
// Execute permit
IERC20Permit(token).permit(
msg.sender,
address(this),
amount,
deadline,
v,
r,
s
);
// Transfer tokens
IERC20(token).transferFrom(msg.sender, address(this), amount);
// Update balances and lock time
balances[msg.sender][token] += amount;
totalDeposits[token] += amount;
lockUntil[msg.sender][token] = block.timestamp + lockDuration;
emit Deposit(msg.sender, token, amount);
emit DepositLocked(msg.sender, token, amount, lockUntil[msg.sender][token]);
}
/**
* @dev Deposit with time lock using Permit2
*/
function depositWithPermit2AndLock(
address token,
uint256 amount,
uint256 lockDuration,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) external nonReentrant {
require(amount > 0, "Amount must be greater than 0");
require(lockDuration >= MIN_LOCK_DURATION, "Lock duration too short");
// Prepare Permit2 structs
IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({
permitted: IPermit2.TokenPermissions({
token: token,
amount: amount
}),
nonce: nonce,
deadline: deadline
});
IPermit2.SignatureTransferDetails memory transferDetails = IPermit2.SignatureTransferDetails({
to: address(this),
requestedAmount: amount
});
// Execute permit transfer
IPermit2(PERMIT2_ADDRESS).permitTransferFrom(
permit,
transferDetails,
msg.sender,
signature
);
// Update balances and lock time
balances[msg.sender][token] += amount;
totalDeposits[token] += amount;
lockUntil[msg.sender][token] = block.timestamp + lockDuration;
emit Deposit(msg.sender, token, amount);
emit DepositLocked(msg.sender, token, amount, lockUntil[msg.sender][token]);
}
/**
* @dev Override withdraw to check lock time
*/
function withdraw(address token, uint256 amount) external override nonReentrant {
require(amount > 0, "Amount must be greater than 0");
require(balances[msg.sender][token] >= amount, "Insufficient balance");
require(block.timestamp >= lockUntil[msg.sender][token], "Tokens are still locked");
// Update balances
balances[msg.sender][token] -= amount;
totalDeposits[token] -= amount;
// Transfer tokens back to user
IERC20(token).transfer(msg.sender, amount);
emit Withdrawal(msg.sender, token, amount);
}
/**
* @dev Get lock expiration time for user's tokens
*/
function getLockExpiration(address user, address token) external view returns (uint256) {
return lockUntil[user][token];
}
/**
* @dev Check if user's tokens are currently locked
*/
function isLocked(address user, address token) external view returns (bool) {
return block.timestamp < lockUntil[user][token];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment