This example demonstrates how to integrate Permit3 into a decentralized application, showing how different components interact to create a complete user experience.
We'll build a complete integration from frontend to smart contracts, using Permit3 to enable a seamless token approval and transfer flow.
Start by implementing a React component for permit creation:
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { MerkleTree } from 'merkletreejs';
import { PERMIT3_ABI } from './abis'; // Import your ABI definitions
// Token Approval Component
const TokenApproval = () => {
const [loading, setLoading] = useState(false);
const [amount, setAmount] = useState('0');
const [token, setToken] = useState('');
const [spender, setSpender] = useState('');
const [expiration, setExpiration] = useState(24); // Hours
const [success, setSuccess] = useState(false);
const [error, setError] = useState(null);
// Constants
const PERMIT3_ADDRESS = "0x0000000000000000000000000000000000000000"; // Replace with actual address
const TOKENS = [
{ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", symbol: "USDC", decimals: 6 },
{ address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", symbol: "WETH", decimals: 18 },
{ address: "0x6B175474E89094C44Da98b954EedeAC495271d0F", symbol: "DAI", decimals: 18 },
];
const SPENDERS = [
{ address: "0x1111111111111111111111111111111111111111", name: "DEX A" },
{ address: "0x2222222222222222222222222222222222222222", name: "Lending Protocol B" },
{ address: "0x3333333333333333333333333333333333333333", name: "Yield Aggregator C" },
];
// Connect to provider
async function getProvider() {
if (window.ethereum) {
await window.ethereum.request({ method: 'eth_requestAccounts' });
return new ethers.providers.Web3Provider(window.ethereum);
}
throw new Error("No web3 provider detected");
}
// Create a permit
async function createPermit() {
setLoading(true);
setError(null);
try {
const provider = await getProvider();
const signer = provider.getSigner();
const permit3 = new ethers.Contract(PERMIT3_ADDRESS, PERMIT3_ABI, signer);
const selectedToken = TOKENS.find(t => t.address === token);
if (!selectedToken) throw new Error("Invalid token");
// Calculate amount with decimals
const parsedAmount = ethers.utils.parseUnits(amount, selectedToken.decimals);
// Calculate expiration timestamp
const expirationTimestamp = Math.floor(Date.now() / 1000) + (expiration * 3600);
// Build permit data
const permitData = {
token: token,
amount: parsedAmount,
expiration: expirationTimestamp,
spender: spender
};
// Create permit with Permit3
const tx = await permit3.permit(
await signer.getAddress(),
permitData.token,
permitData.amount,
permitData.expiration,
permitData.spender
);
await tx.wait();
setSuccess(true);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
return (
<div className="permit-approval">
<h2>Create Token Permit</h2>
<div className="form-group">
<label>Token:</label>
<select value={token} onChange={e => setToken(e.target.value)}>
<option value="">Select a token</option>
{TOKENS.map(t => (
<option key={t.address} value={t.address}>{t.symbol}</option>
))}
</select>
</div>
<div className="form-group">
<label>Amount:</label>
<input
type="number"
value={amount}
onChange={e => setAmount(e.target.value)}
placeholder="0.0"
/>
</div>
<div className="form-group">
<label>Spender:</label>
<select value={spender} onChange={e => setSpender(e.target.value)}>
<option value="">Select a spender</option>
{SPENDERS.map(s => (
<option key={s.address} value={s.address}>{s.name}</option>
))}
</select>
</div>
<div className="form-group">
<label>Expiration (hours):</label>
<input
type="number"
value={expiration}
onChange={e => setExpiration(e.target.value)}
min="1"
max="720"
/>
</div>
<button onClick={createPermit} disabled={loading || !token || !spender}>
{loading ? 'Creating...' : 'Create Permit'}
</button>
{success && (
<div className="success">Permit created successfully!</div>
)}
{error && (
<div className="error">Error: {error}</div>
)}
</div>
);
};Here's a Node.js service that coordinates cross-chain permits using Unbalanced Merkle tree:
const { ethers } = require('ethers');
const { MerkleTree } = require('merkletreejs');
const keccak256 = require('keccak256');
// Merkle Tree Helper Class
class MerkleTreeHelper {
// Build merkle tree with ordered hashing
static buildTree(leaves) {
// Use ordered hashing for consistency
const hashFn = (data) => {
if (Buffer.isBuffer(data)) return data;
return Buffer.from(ethers.utils.keccak256(data).slice(2), 'hex');
};
const tree = new MerkleTree(leaves, hashFn, {
sortPairs: true // This ensures ordered hashing
});
return tree;
}
// Generate proof for a specific leaf
static getProof(tree, leaf) {
const proof = tree.getProof(leaf);
// Convert to bytes32[] format expected by the contract
return proof.map(p => '0x' + p.data.toString('hex'));
}
// Get the merkle root
static getRoot(tree) {
return '0x' + tree.getRoot().toString('hex');
}
}
// Cross-Chain Coordinator Service
class CrossChainCoordinator {
constructor() {
this.chains = {
ethereum: {
chainId: 1,
rpc: 'https://eth.llamarpc.com',
permit3Address: '0x0000000000000000000000000000000000000000'
},
arbitrum: {
chainId: 42161,
rpc: 'https://arb1.arbitrum.io/rpc',
permit3Address: '0x0000000000000000000000000000000000000000'
},
optimism: {
chainId: 10,
rpc: 'https://mainnet.optimism.io',
permit3Address: '0x0000000000000000000000000000000000000000'
}
};
this.providers = {};
this.permit3Contracts = {};
this.permits = {};
this.initializeProviders();
}
// Initialize providers and contracts
initializeProviders() {
Object.entries(this.chains).forEach(([chainName, config]) => {
this.providers[chainName] = new ethers.providers.JsonRpcProvider(config.rpc);
this.permit3Contracts[chainName] = new ethers.Contract(
config.permit3Address,
PERMIT3_ABI,
this.providers[chainName]
);
});
}
// Add a permit for a specific chain
addChainPermit(chainName, permitData) {
if (!this.chains[chainName]) {
throw new Error(`Chain ${chainName} not configured`);
}
this.permits[chainName] = permitData;
}
// Generate the cross-chain permit with standard merkle tree
async generateCrossChainPermit(wallet) {
// Ensure we have all required data
const chainNames = Object.keys(this.permits);
if (chainNames.length === 0) {
throw new Error("No chain permits configured");
}
// Order chains by chainId for consistency
const orderedChains = chainNames.sort(
(a, b) => this.chains[a].chainId - this.chains[b].chainId
);
// Generate leaves for the merkle tree
const leaves = [];
const chainToLeafMap = new Map();
for (const chainName of orderedChains) {
const permit = this.permits[chainName];
const leaf = await this.permit3Contracts[chainName].hashChainPermits(permit);
leaves.push(leaf);
chainToLeafMap.set(chainName, leaf);
}
// Build the merkle tree
const merkleTree = MerkleTreeHelper.buildTree(leaves);
const merkleRoot = MerkleTreeHelper.getRoot(merkleTree);
// Create signature elements
const salt = ethers.utils.randomBytes(32);
const timestamp = Math.floor(Date.now() / 1000);
const deadline = timestamp + 3600; // 1 hour
// Sign using any chain's domain (we'll use ethereum)
const domain = {
name: "Permit3",
version: "1",
chainId: 1, // ALWAYS 1 (CROSS_CHAIN_ID) for cross-chain compatibility
verifyingContract: this.chains.ethereum.permit3Address
};
const types = {
Permit3: [
{ name: "owner", type: "address" },
{ name: "salt", type: "bytes32" },
{ name: "deadline", type: "uint48" },
{ name: "timestamp", type: "uint48" },
{ name: "merkleRoot", type: "bytes32" }
]
};
const value = {
owner: await wallet.getAddress(),
salt,
deadline,
timestamp,
merkleRoot
};
// Sign the message
const signature = await wallet._signTypedData(domain, types, value);
// Generate merkle proofs for each chain
const proofs = {};
for (const chainName of orderedChains) {
const leaf = chainToLeafMap.get(chainName);
proofs[chainName] = {
permits: this.permits[chainName],
proof: MerkleTreeHelper.getProof(merkleTree, leaf)
};
}
return {
owner: await wallet.getAddress(),
salt,
deadline,
timestamp,
signature,
merkleRoot,
chains: orderedChains,
proofs
};
}
// Execute permit on a specific chain
async executeOnChain(chainName, permitData) {
if (!this.chains[chainName]) {
throw new Error(`Chain ${chainName} not configured`);
}
const { owner, salt, deadline, timestamp, signature, proofs } = permitData;
const chainProof = proofs[chainName];
if (!chainProof) {
throw new Error(`No proof found for chain ${chainName}`);
}
// Get the contract for this chain
const permit3 = this.permit3Contracts[chainName];
// Execute the unbalanced permit
const tx = await permit3.permit(
owner,
salt,
deadline,
timestamp,
chainProof,
signature
);
return tx;
}
}
// Example usage
async function exampleCrossChainPermit() {
// Initialize coordinator
const coordinator = new CrossChainCoordinator();
// Add permits for each chain
coordinator.addChainPermit('ethereum', {
chainId: 1,
permits: [
{
token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
amount: ethers.utils.parseUnits('100', 6),
expiration: Math.floor(Date.now() / 1000) + 86400,
spender: '0x1111111111111111111111111111111111111111'
}
]
});
coordinator.addChainPermit('arbitrum', {
chainId: 42161,
permits: [
{
token: '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8', // USDC.e
amount: ethers.utils.parseUnits('50', 6),
expiration: Math.floor(Date.now() / 1000) + 86400,
spender: '0x2222222222222222222222222222222222222222'
}
]
});
coordinator.addChainPermit('optimism', {
chainId: 10,
permits: [
{
token: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607', // USDC
amount: ethers.utils.parseUnits('75', 6),
expiration: Math.floor(Date.now() / 1000) + 86400,
spender: '0x3333333333333333333333333333333333333333'
}
]
});
// Generate cross-chain permit
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
const crossChainPermit = await coordinator.generateCrossChainPermit(wallet);
console.log('Cross-chain permit generated:', {
root: crossChainPermit.merkleRoot,
chains: crossChainPermit.chains,
proofs: Object.keys(crossChainPermit.proofs)
});
// Execute on each chain
for (const chainName of crossChainPermit.chains) {
console.log(`Executing on ${chainName}...`);
const tx = await coordinator.executeOnChain(chainName, crossChainPermit);
console.log(`Transaction hash: ${tx.hash}`);
}
}
module.exports = {
CrossChainCoordinator,
MerkleTreeHelper
};Here's how to interact with Permit3 from your smart contracts:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { IPermit3 } from "./interfaces/IPermit3.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract DeFiProtocol {
IPermit3 public immutable permit3;
// Events
event TokensReceived(address indexed from, address indexed token, uint256 amount);
event ActionExecuted(address indexed user, string action);
constructor(address _permit3) {
permit3 = IPermit3(_permit3);
}
// Use a single-chain permit to transfer tokens
function executeWithPermit(
address owner,
IPermit3.ChainPermits calldata chainPermits,
bytes32 salt,
uint48 deadline,
uint48 timestamp,
bytes calldata signature
) external {
// Use Permit3 to transfer tokens with the permit
permit3.permit(owner, salt, deadline, timestamp, chainPermits.permits, signature);
// Now we can transfer tokens from the owner
for (uint i = 0; i < chainPermits.permits.length; i++) {
IPermit3.AllowanceOrTransfer memory permit = chainPermits.permits[i];
// Check if this is a transfer (mode = 0)
if (permit.modeOrExpiration & 1 == 0) {
// Extract transfer details
uint160 amount = uint160(permit.modeOrExpiration >> 48);
// Permit3 has already validated and will transfer the tokens
emit TokensReceived(owner, permit.token, amount);
// Execute your protocol logic here
_executeProtocolAction(owner, permit.token, amount);
}
}
}
// Use a cross-chain permit
function executeWithUnbalancedPermit(
address owner,
bytes32 salt,
uint48 deadline,
uint48 timestamp,
IPermit3.ChainPermits calldata permits,
bytes32[] calldata proof,
bytes calldata signature
) external {
// Use Permit3 to process the cross-chain permit
permit3.permit(owner, salt, deadline, timestamp, permits, proof, signature);
// Process the permits for this chain
for (uint i = 0; i < permits.permits.length; i++) {
IPermit3.AllowanceOrTransfer memory permit = permits.permits[i];
// Check if this is a transfer (mode = 0)
if (permit.modeOrExpiration & 1 == 0) {
uint160 amount = uint160(permit.modeOrExpiration >> 48);
emit TokensReceived(owner, permit.token, amount);
_executeProtocolAction(owner, permit.token, amount);
}
}
}
// Internal protocol logic
function _executeProtocolAction(address user, address token, uint256 amount) internal {
// Your protocol logic here
// For example: lending, swapping, staking, etc.
emit ActionExecuted(user, "Protocol action completed");
}
}Here's a comprehensive test suite for your Permit3 integration:
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { MerkleTree } = require("merkletreejs");
const keccak256 = require("keccak256");
describe("Permit3 Integration Tests", function () {
let permit3;
let defiProtocol;
let token;
let owner;
let spender;
beforeEach(async function () {
[owner, spender] = await ethers.getSigners();
// Deploy contracts
const Permit3 = await ethers.getContractFactory("Permit3");
permit3 = await Permit3.deploy();
const DeFiProtocol = await ethers.getContractFactory("DeFiProtocol");
defiProtocol = await DeFiProtocol.deploy(permit3.address);
const Token = await ethers.getContractFactory("MockERC20");
token = await Token.deploy("Test Token", "TEST");
// Setup: mint tokens and approve Permit3
await token.mint(owner.address, ethers.utils.parseEther("1000"));
await token.connect(owner).approve(permit3.address, ethers.constants.MaxUint256);
});
describe("Single Chain Permits", function () {
it("Should create and execute a permit", async function () {
const amount = ethers.utils.parseEther("100");
const expiration = Math.floor(Date.now() / 1000) + 3600;
// Create permit data
const chainPermits = {
chainId: 1, // ALWAYS 1 (CROSS_CHAIN_ID) for cross-chain compatibility // Hardhat chainId
permits: [{
modeOrExpiration: (BigInt(amount) << 48n) | BigInt(expiration),
token: token.address,
account: defiProtocol.address
}]
};
// Generate signature
const salt = ethers.utils.randomBytes(32);
const timestamp = Math.floor(Date.now() / 1000);
const deadline = timestamp + 3600;
const domain = {
name: "Permit3",
version: "1",
chainId: 1, // ALWAYS 1 (CROSS_CHAIN_ID) for cross-chain compatibility
verifyingContract: permit3.address
};
const types = {
Permit3: [
{ name: "owner", type: "address" },
{ name: "salt", type: "bytes32" },
{ name: "deadline", type: "uint48" },
{ name: "timestamp", type: "uint48" },
{ name: "permitDataHash", type: "bytes32" }
]
};
const permitDataHash = await permit3.hashChainPermits(chainPermits);
const value = {
owner: owner.address,
salt,
deadline,
timestamp,
permitDataHash
};
const signature = await owner._signTypedData(domain, types, value);
// Execute the permit
await expect(
defiProtocol.executeWithPermit(
owner.address,
chainPermits,
salt,
deadline,
timestamp,
signature
)
).to.emit(defiProtocol, "TokensReceived")
.withArgs(owner.address, token.address, amount);
});
});
describe("Cross-Chain Permits", function () {
it("Should create and execute an unbalanced permit", async function () {
// Create permits for multiple chains
const permits = [
{
chainId: 1,
permits: [{
modeOrExpiration: (BigInt(ethers.utils.parseEther("50")) << 48n) | BigInt(Math.floor(Date.now() / 1000) + 3600),
token: token.address,
account: defiProtocol.address
}]
},
{
chainId: 42161,
permits: [{
modeOrExpiration: (BigInt(ethers.utils.parseEther("30")) << 48n) | BigInt(Math.floor(Date.now() / 1000) + 3600),
token: token.address,
account: spender.address
}]
},
{
chainId: 1, // ALWAYS 1 (CROSS_CHAIN_ID) for cross-chain compatibility // Our test chain
permits: [{
modeOrExpiration: (BigInt(ethers.utils.parseEther("20")) << 48n) | BigInt(Math.floor(Date.now() / 1000) + 3600),
token: token.address,
account: defiProtocol.address
}]
}
];
// Generate merkle tree
const leaves = [];
for (const permit of permits) {
const leaf = await permit3.hashChainPermits(permit);
leaves.push(leaf);
}
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
const root = tree.getRoot();
// Generate proof for our test chain (index 2)
const ourChainLeaf = leaves[2];
const proof = tree.getProof(ourChainLeaf).map(p => p.data);
// Create signature
const salt = ethers.utils.randomBytes(32);
const timestamp = Math.floor(Date.now() / 1000);
const deadline = timestamp + 3600;
const domain = {
name: "Permit3",
version: "1",
chainId: 1, // ALWAYS 1 (CROSS_CHAIN_ID) for cross-chain compatibility
verifyingContract: permit3.address
};
const types = {
Permit3: [
{ name: "owner", type: "address" },
{ name: "salt", type: "bytes32" },
{ name: "deadline", type: "uint48" },
{ name: "timestamp", type: "uint48" },
{ name: "merkleRoot", type: "bytes32" }
]
};
const value = {
owner: owner.address,
salt,
deadline,
timestamp,
merkleRoot: root
};
const signature = await owner._signTypedData(domain, types, value);
// Execute the cross-chain permit
const chainPermits = permits[2]; // Our chain's permits
const merkleProof = proof;
await expect(
defiProtocol.executeWithUnbalancedPermit(
owner.address,
salt,
deadline,
timestamp,
chainPermits,
merkleProof,
signature
)
).to.emit(defiProtocol, "TokensReceived")
.withArgs(owner.address, token.address, ethers.utils.parseEther("20"));
});
});
});- Always verify signatures on-chain
- Implement proper deadline checks
- Use nonces to prevent replay attacks
- Validate token addresses and amounts
- Batch multiple operations in a single permit
- Use cross-chain permits only when necessary
- Cache merkle proofs when possible
- Optimize proof size by ordering chains efficiently
- Provide clear feedback during signing
- Show gas estimates before execution
- Handle errors gracefully
- Implement retry mechanisms for failed transactions
- Use consistent chain ordering (by chainId)
- Implement proper error handling for each chain
- Monitor transaction status across chains
- Provide unified transaction history
-
"Invalid signature" error
- Ensure the domain separator matches exactly
- Check that all parameters are in the correct format
- Verify the signer address matches the owner
-
"Merkle proof verification failed"
- Ensure leaves are hashed correctly
- Check that the proof order matches the tree construction
- Verify the root calculation is consistent
-
"Deadline exceeded" error
- Increase the deadline buffer
- Check for time synchronization issues
- Consider network delays
// Debug merkle tree construction
console.log("Leaves:", leaves.map(l => ethers.utils.hexlify(l)));
console.log("Root:", ethers.utils.hexlify(tree.getRoot()));
console.log("Proof:", proof.map(p => ethers.utils.hexlify(p)));
// Verify proof locally
const verified = tree.verify(proof, ourChainLeaf, root);
console.log("Proof valid:", verified);
// Debug signature
console.log("Domain:", domain);
console.log("Types:", types);
console.log("Value:", value);
console.log("Signature:", signature);