ERC7984 Confidential Token

🟡 Intermediate | 🏗️ OpenZeppelin Contracts

A confidential ERC7984 token implementation using OpenZeppelin's confidential contracts library

Overview

This example demonstrates how to create a confidential token using OpenZeppelin's ERC7984 implementation. ERC7984 is the confidential token standard that keeps balances and transfer amounts encrypted. The token supports minting, burning, confidential transfers, and operator approvals with time-based expiry.

Quick Start

# Create new project from this template
npx labz create erc7984-token my-project

# Navigate and install
cd my-project
npm install

# Run tests
npx hardhat test

Contract

// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.27;

import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {FHE, externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol";
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {ERC7984} from "@openzeppelin/confidential-contracts/token/ERC7984/ERC7984.sol";

/// @title ERC7984Example - Confidential Fungible Token
/// @notice A confidential token where balances and transfers are encrypted using FHE
/// @dev Extends OpenZeppelin's ERC7984 base implementation with minting/burning capabilities
contract ERC7984Example is ZamaEthereumConfig, ERC7984, Ownable2Step {

    // ============ Events ============

    /// @notice Emitted when tokens are minted
    event TokensMinted(address indexed to, uint64 amount);

    /// @notice Emitted when tokens are burned (confidential amount)
    event TokensBurned(address indexed from);

    // ============ Constructor ============

    /// @param owner The initial owner of the contract
    /// @param initialSupply Initial token supply to mint to owner
    /// @param name_ Token name
    /// @param symbol_ Token symbol
    /// @param tokenURI_ Token metadata URI
    constructor(
        address owner,
        uint64 initialSupply,
        string memory name_,
        string memory symbol_,
        string memory tokenURI_
    ) ERC7984(name_, symbol_, tokenURI_) Ownable(owner) {
        if (initialSupply > 0) {
            euint64 encryptedAmount = FHE.asEuint64(initialSupply);
            _mint(owner, encryptedAmount);
        }
    }

    // ============ Minting Functions ============

    /// @notice Mint tokens with a visible amount (for public mints)
    /// @param to Recipient address
    /// @param amount Amount to mint (visible in calldata)
    function mint(address to, uint64 amount) external onlyOwner {
        _mint(to, FHE.asEuint64(amount));
        emit TokensMinted(to, amount);
    }

    /// @notice Mint tokens with an encrypted amount (for private mints)
    /// @param to Recipient address
    /// @param encryptedAmount Encrypted amount from client
    /// @param inputProof Proof validating the encrypted input
    function confidentialMint(
        address to,
        externalEuint64 encryptedAmount,
        bytes calldata inputProof
    ) external onlyOwner returns (euint64 transferred) {
        transferred = _mint(to, FHE.fromExternal(encryptedAmount, inputProof));
        emit TokensBurned(to);
    }

    // ============ Burning Functions ============

    /// @notice Burn tokens with a visible amount
    /// @param from Address to burn from
    /// @param amount Amount to burn (visible in calldata)
    function burn(address from, uint64 amount) external onlyOwner {
        _burn(from, FHE.asEuint64(amount));
        emit TokensBurned(from);
    }

    /// @notice Burn tokens with an encrypted amount
    /// @param from Address to burn from
    /// @param encryptedAmount Encrypted amount from client
    /// @param inputProof Proof validating the encrypted input
    function confidentialBurn(
        address from,
        externalEuint64 encryptedAmount,
        bytes calldata inputProof
    ) external onlyOwner returns (euint64 transferred) {
        transferred = _burn(from, FHE.fromExternal(encryptedAmount, inputProof));
        emit TokensBurned(from);
    }

    // ============ View Functions ============

    /// @notice Get token decimals (ERC7984 uses 6 decimals by default)
    function decimals() public view virtual override returns (uint8) {
        return 6;
    }

    /// @notice Check if an address has any balance
    /// @param account Address to check
    /// @return True if balance is initialized (may still be zero)
    function hasBalance(address account) external view returns (bool) {
        euint64 balance = confidentialBalanceOf(account);
        return FHE.isInitialized(balance);
    }
}

Code Explanation

Inheritance

Imports OpenZeppelin's ERC7984 base contract along with Ownable for access control. ERC7984 provides all the confidential token functionality.

Lines 1-10

Constructor

Constructor initializes the token with name, symbol, decimals, and contract URI. Optionally mints initial supply to the owner.

Lines 15-25

Mint

Owner-only function to mint new tokens. Supports both visible amount minting (for public allocations) and confidential minting (for private allocations).

Lines 27-35

Confidential Transfer

Transfer tokens with encrypted amount. The actual transfer amount is never revealed on-chain, only the sender and recipient can see it.

Lines 40-55

Operators

Operator system allows delegated transfers with time-based expiry. Unlike ERC20 allowances, operators have full transfer capability within their validity period.

Lines 60-75

FHE Operations Used

  • FHE.confidentialTransfer()

  • FHE.confidentialMint()

  • FHE.confidentialBurn()

  • FHE.setOperator()

FHE Types Used

  • euint64

Tags

ERC7984 token confidential OpenZeppelin transfer mint burn operator

Prerequisites

Before this example, you should understand:

Next Steps

After this example, check out:


Generated with Lab-Z

Last updated