Skip to main content

Overview

FHERC20Permit extends the base FHERC20 contract with signature-based operator approval, allowing users to grant operator permissions without sending a transaction. This is similar to EIP-2612 (Permit for ERC20) but adapted for the operator model.

Gasless Approvals

Users can approve operators by signing messages off-chain, eliminating the need for approval transactions and saving gas.

EIP-712 Standard

Uses the battle-tested EIP-712 standard for structured data signing, providing security and wallet compatibility.

Meta-Transactions

Enable meta-transaction patterns where relayers can submit permits on behalf of users.

Improved UX

Users can approve and transfer in a single transaction, or approve without needing ETH for gas.

How It Works

Traditional Operator Approval (2 transactions)

// Transaction 1: Approve operator (costs gas)
await token.setOperator(operatorAddress, expirationTime);

// Transaction 2: Operator performs action
await token.connect(operator).confidentialTransferFrom(user, recipient, amount);

With Permit (1 transaction)

// Off-chain: User signs approval (no gas)
const signature = await user.signTypedData(domain, types, value);

// On-chain: Operator submits permit + action (one transaction)
await token.connect(operator).permit(
    userAddress,
    operatorAddress,
    until,
    deadline,
    v, r, s
);
await token.connect(operator).confidentialTransferFrom(user, recipient, amount);

The Permit Function

Function Signature

function permit(
    address owner,
    address spender,
    uint48 until,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) external;
Parameters:
  • owner: Address that is granting operator permission
  • spender: Address receiving operator permission
  • until: Unix timestamp when operator permission expires (uint48)
  • deadline: Unix timestamp when the signature itself expires (uint256)
  • v, r, s: ECDSA signature components

How Permit Works

1

User Signs Off-Chain

User signs a structured message containing owner, spender, until, nonce, and deadline using their wallet.
2

Signature Submitted

Anyone (usually the operator or a relayer) submits the signature along with the permit parameters to the blockchain.
3

Signature Verification

Contract verifies the signature matches the owner’s address and hasn’t expired.
4

Operator Granted

If valid, the contract calls setOperator(spender, until) on behalf of the owner.
5

Nonce Incremented

The owner’s nonce is incremented to prevent signature replay.

EIP-712 Domain and Types

Domain Separator

function DOMAIN_SEPARATOR() external view returns (bytes32);
The domain separator uniquely identifies this contract:
const domain = {
    name: await token.name(),
    version: "1",
    chainId: await ethers.provider.getNetwork().then(n => n.chainId),
    verifyingContract: token.address
};

Permit Type Hash

bytes32 private constant PERMIT_TYPEHASH = keccak256(
    "Permit(address owner,address spender,uint48 until,uint256 nonce,uint256 deadline)"
);
The structured data for signing:
const types = {
    Permit: [
        { name: "owner", type: "address" },
        { name: "spender", type: "address" },
        { name: "until", type: "uint48" },
        { name: "nonce", type: "uint256" },
        { name: "deadline", type: "uint256" }
    ]
};

Nonces

Function Signature

function nonces(address owner) public view returns (uint256);
Each address has a nonce that increments with each permit, preventing replay attacks.
// Get current nonce
const nonce = await token.nonces(userAddress);

// Nonce increments after each permit
await token.permit(...);
const newNonce = await token.nonces(userAddress); // nonce + 1

Meta-Transaction Pattern

Permits enable meta-transactions where users sign approvals and a relayer submits them:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@fhenixprotocol/contracts/FHERC20Permit.sol";

contract TokenRelayer {
    FHERC20Permit public immutable token;

    constructor(address _token) {
        token = FHERC20Permit(_token);
    }

    // Relayer submits permit + executes transfer in one transaction
    function permitAndTransferFrom(
        address owner,
        address to,
        euint64 amount,
        uint48 until,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        // Submit permit (owner approved relayer)
        token.permit(owner, address(this), until, deadline, v, r, s);

        // Execute transfer
        token.confidentialTransferFrom(owner, to, amount);
    }
}

Security Considerations

Signature Expiration

Always set a reasonable deadline for signatures:
// ✅ Good: 1 hour expiration
const deadline = Math.floor(Date.now() / 1000) + 3600;

// ❌ Bad: Far future expiration
const deadline = Math.floor(Date.now() / 1000) + (365 * 24 * 3600);
Expired signatures are automatically rejected by the contract.

Nonce Management

Each permit increments the user’s nonce. If a user signs multiple permits, only the one with the current nonce will be valid. Later signatures become invalid if an earlier one is used first.
// User signs permit A with nonce 5
const permitA = await signPermit({nonce: 5, ...});

// User signs permit B with nonce 5 (same nonce!)
const permitB = await signPermit({nonce: 5, ...});

// Only one will work - whichever is submitted first
Always fetch the current nonce before signing.

Front-Running

Since permits can be submitted by anyone, there’s a risk of front-running:
// User signs permit for operator A
const sig = await user.signPermit(operatorA, ...);

// Malicious operator B sees the signature in mempool
// and submits it before operator A

// Now operator B has the approval instead of A!
Mitigation: Ensure the spender parameter in your signature is the intended operator.

Signature Malleability

ECDSA signatures can be malleable. FHERC20Permit uses the standard EIP-712 approach which includes built-in protections, but always:
  • Validate v is 27 or 28
  • Use ethers.js or viem signature utilities
  • Don’t manually construct signatures

Permit vs SetOperator

FeaturesetOperator()permit()
Gas CostUser paysRelayer or operator pays
Transactions1 (from user)1 (from anyone)
UXRequires user transactionUser signs off-chain
ImmediateYesYes (once submitted)
ExpirationOperator until timeSignature deadline + Operator until time
Best ForDirect user interactionsRelayed/batched transactions

Complete Example Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@fhenixprotocol/contracts/FHERC20Permit.sol";

contract MyPermitToken is FHERC20Permit {
    constructor() FHERC20Permit("My Permit Token", "MPT", 18) {}

    function mint(address to, uint64 amount) external {
        _mint(to, amount);
    }
}
Usage pattern:
// 1. User signs permit off-chain (no gas)
const sig = await user.signPermit(operatorAddress, until, deadline);

// 2. Operator submits permit + transfer (one transaction)
await token.connect(operator).permit(
    user.address, operatorAddress, until, deadline,
    sig.v, sig.r, sig.s
);

const amount = await cofhe.encrypt(100);
await token.connect(operator).confidentialTransferFrom(
    user.address, recipient.address, amount
);