Skip to main content

Overview

FHERC20 introduces a new permission model called operators that replaces the traditional ERC20 allowance system. Instead of approving specific amounts (which would leak information about balances), FHERC20 uses time-based operator permissions that grant full access until an expiration timestamp.

Privacy Preserving

No amount-specific approvals means no information leakage about how much you’re willing to let others spend.

Time-Based Expiration

Operators have automatic expiration using Unix timestamps, reducing the need for explicit revocation.

Full Access

Operators can move any amount of tokens (up to your balance) without needing separate approvals for each transaction.

Simple Management

One function to grant, extend, or revoke operator permissions with intuitive timestamp-based control.

Why Operators Instead of Allowances?

The Problem with Traditional Allowances

Standard ERC20 uses the approve() function to grant spending permissions:
// Standard ERC20 - LEAKS INFORMATION
token.approve(spender, 1000); // Everyone can see you approved 1000 tokens
This approach has privacy issues for confidential tokens:
  • ❌ Reveals how much you’re willing to let someone spend
  • ❌ Requires updating allowances frequently
  • ❌ Can leak information about your balance
  • ❌ Doesn’t work well with encrypted amounts

The Operator Solution

FHERC20 operators grant permission without revealing amounts:
// FHERC20 - NO INFORMATION LEAKAGE
token.setOperator(spender, block.timestamp + 1 days);
This approach is privacy-preserving:
  • ✅ No amount information revealed
  • ✅ Time-based expiration is automatic
  • ✅ Simple on/off permission model
  • ✅ Works perfectly with encrypted values

Setting Operators

Function Signature

function setOperator(address operator, uint48 until) external;
Parameters:
  • operator: Address to grant operator permissions to
  • until: Unix timestamp when the permission expires (uint48 supports dates until year 8921556)

Basic Usage

// Grant operator permission for 24 hours
token.setOperator(
    operatorAddress,
    uint48(block.timestamp + 1 days)
);
Use uint48(block.timestamp + duration) to calculate expiration times. The uint48 type is large enough for practical use while being gas-efficient.

Checking Operator Status

Function Signature

function isOperator(address holder, address spender)
    external view returns (bool);
Parameters:
  • holder: Address of the token holder
  • spender: Address to check operator status for
Returns:
  • true if spender is currently an authorized operator for holder
  • false if not authorized or permission has expired

Usage Examples

// Check if address is an operator
bool canOperate = token.isOperator(holderAddress, spenderAddress);

if (canOperate) {
    // Spender can transfer holder's tokens
    token.confidentialTransferFrom(holder, recipient, encryptedAmount);
}
// Off-chain checking
const isAuthorized = await token.isOperator(holderAddress, operatorAddress);

if (isAuthorized) {
    console.log("Operator is authorized");
} else {
    console.log("Operator permission expired or never granted");
}

Using Operator Permissions

Once granted operator status, an address can use confidentialTransferFrom() to move tokens:
function confidentialTransferFrom(
    address from,
    address to,
    InEuint64 memory inValue
) external returns (euint64 transferred);

Complete Example

// 1. Token holder grants operator permission
await token.connect(holder).setOperator(
    operatorAddress,
    Math.floor(Date.now() / 1000) + 86400  // 1 day from now
);

// 2. Operator can now transfer on behalf of holder
const encryptedAmount = await cofhe.encrypt(100);
await token.connect(operator).confidentialTransferFrom(
    holderAddress,
    recipientAddress,
    encryptedAmount
);

// 3. After expiration, operator can no longer transfer
// (automatically revoked when timestamp passes)

Internal Implementation

Storage

Operators are stored in a mapping with their expiration times:
mapping(address holder => mapping(address spender => uint48 until))
    private _operators;

Setting an Operator

function setOperator(address operator, uint48 until) external {
    address holder = msg.sender;

    // Update or revoke operator permission
    _operators[holder][operator] = until;

    // Emit event (implementation specific)
    emit OperatorSet(holder, operator, until);
}

Checking Operator Status

function isOperator(address holder, address spender)
    external view returns (bool)
{
    // Check if current time is before expiration
    return _operators[holder][spender] >= block.timestamp;
}

Transfer From Check

Before allowing a confidentialTransferFrom, the contract verifies operator status:
function confidentialTransferFrom(
    address from,
    address to,
    euint64 value
) external returns (euint64 transferred) {
    // Verify operator permission
    if (!isOperator(from, msg.sender)) {
        revert FHERC20InsufficientPermission(from, msg.sender);
    }

    // Perform the transfer
    return _transfer(from, to, value);
}

Operator Patterns

Pattern 1: Short-Lived Permissions

Grant operator permissions for specific transactions:
// Grant permission for a specific operation
function executeSwap(address tokenIn, uint64 amountIn) external {
    // Grant DEX operator permission for 5 minutes
    tokenIn.setOperator(dexAddress, uint48(block.timestamp + 5 minutes));

    // Execute swap
    dex.swap(tokenIn, tokenOut, amountIn);

    // Permission automatically expires after 5 minutes
}

Operator vs Allowance Comparison

FeatureERC20 AllowanceFHERC20 Operator
Privacy❌ Reveals approved amount✅ No amount revealed
Expiration⚠️ Manual revocation required✅ Automatic time-based
Flexibility✅ Can approve specific amounts⚠️ All-or-nothing access
Gas Efficiency⚠️ Multiple approvals costly✅ Single approval sufficient
Complexity✅ Simple amount-based✅ Simple time-based
Use with FHE❌ Doesn’t work with encryption✅ Designed for FHE

Security Considerations

Operator Has Full Access

An operator can transfer all of a holder’s tokens, not just a specific amount. Only grant operator permissions to trusted addresses.
// Operator can transfer entire balance
euint64 balance = token.confidentialBalanceOf(holder);
token.confidentialTransferFrom(holder, attacker, balance);
Best practices:
  • Use short expiration times when possible
  • Only authorize trusted contracts or addresses
  • Monitor operator grants in your UI
  • Consider implementing additional checks in contracts

Time-Based Expiration

Operator permissions automatically expire based on blockchain timestamp:
// Permission expires at specific timestamp
uint48 expiresAt = uint48(block.timestamp + 1 hours);
token.setOperator(operator, expiresAt);

// After expiration, operator cannot act
// No need for explicit revocation
Advantages:
  • Automatic cleanup
  • No gas cost for revocation
  • Predictable expiration
Considerations:
  • Block timestamps can vary slightly
  • Account for clock skew in time calculations
  • Use buffer time for critical operations

Front-Running Protection

Operator changes are atomic and immediate:
// This transaction either succeeds completely or reverts
token.setOperator(newOperator, expirationTime);
Unlike ERC20’s approve/transferFrom race condition, operator changes are safe from front-running because:
  • No amount is specified
  • Permission is binary (yes/no)
  • Time-based expiration is deterministic

Multiple Operators

A holder can have multiple operators simultaneously:
// Grant multiple operators
token.setOperator(operatorA, uint48(block.timestamp + 1 days));
token.setOperator(operatorB, uint48(block.timestamp + 7 days));
token.setOperator(operatorC, uint48(block.timestamp + 30 days));

// All can operate independently
Consider:
  • Each operator has full access
  • Permissions are independent
  • Track all active operators
  • Implement operator limits if needed