news 2026/9/13 1:19:13

如何用 OpenZeppelin ERC7984 与 FHEVM 构建机密通证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
如何用 OpenZeppelin ERC7984 与 FHEVM 构建机密通证

如何用 OpenZeppelin ERC7984 与 FHEVM 构建机密通证

【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm

如果你想在链上发行一种余额和转账金额默认不可见、但仍然支持完整通证操作的代币,FHEVM 的文档提供了一个直接路径:基于 OpenZeppelin 的ERC7984机密通证基类,结合 FHEVM 的全同态加密(FHE)能力,构建一个转账后余额只以加密 handle 形式存在链上的通证。本文按文档给出的流程,从环境准备、合约编写、测试脚本到验证方式,完整走一遍这条集成路径。适用前提:你使用 Hardhat 工作流,并且可以访问 FHEVM-enabled 网络以及 Zama 的 gateway/relayer。

准备环境

按 OpenZeppelin 集成指南 的要求,开始前需要:

  • Node.js>= 20,并且使用偶数版本(v18.xv20.x这类);快速上手文档 说明 Hardhat 不支持奇数版本 Node.js,使用 v21.x、v23.x 等会持续报警且行为可能异常;
  • Hardhat^2.24;
  • 访问 FHEVM-enabled 网络和 Zama 的 gateway/relayer 的能力;
  • 基于FHEVM Hardhat template创建的项目(在 指南 中有模板说明)。

初始化项目在指南中给出的命令序列如下,在项目目录内依次执行:

# 1. 安装模板项目的依赖 npm ci # 2. 安装 OpenZeppelin 的机密合约库 npm i @openzeppelin/confidential-contracts # 3. 编译合约 npm run compile

安装完成后可以用npm test跑一遍模板自带的测试,确认环境本身没有问题,再开始加入你自己的合约。

编写 ERC7984Example 合约

在项目的contracts/目录下创建ERC7984Example.sol。合约继承三个部分:

  1. ERC7984—— OpenZeppelin 提供的机密通证基类;
  2. Ownable2Step—— 为铸币和管理功能提供访问控制;
  3. ZamaEthereumConfig—— FHEVM 的 FHE 配置,用于 Ethereum 主网或 Ethereum Sepolia 测试网(该抽象合约定义在本仓库的 ZamaConfig.sol 中)。

文档给出的完整合约如下:

// 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"; contract ERC7984Example is ZamaEthereumConfig, ERC7984, Ownable2Step { constructor( address owner, uint64 amount, string memory name_, string memory symbol_, string memory contractURI_ ) ERC7984(name_, symbol_, contractURI_) Ownable(owner) { euint64 encryptedAmount = FHE.asEuint64(amount); _mint(owner, encryptedAmount); } }

几个需要理解的行为:

  • 构造时以明文金额完成首次 mint(FHE.asEuint64(amount)把明文uint64就地加密,FHE.asEuint64(uint64)定义在 FHE.sol),用于建立代币的初始总量;
  • 首次 mint 只在构造时发生一次,之后的转账全部是加密的,链上余额以 handle 形式存在;
  • 文档同时提示:示例使用明文初始 mint 是为了简化,生产环境中可以考虑从创世起就用加密 mint、实现更复杂的 mint 计划,或覆盖部分隐私假设。

编写测试脚本

把测试文件放进项目的test/目录。文档中该示例标签页的文件名为confToken.test.ts,而文档给出的运行命令引用的是test/ERC7984Example.test.ts——文件名以你实际运行的命令为准,二者保持一致即可。测试覆盖初始化、转账流程和两类应 revert 的非法操作:

import { expect } from 'chai'; import { ethers, fhevm } from 'hardhat'; describe('ERC7984Example', function () { let token: any; let owner: any; let recipient: any; let other: any; const INITIAL_AMOUNT = 1000; const TRANSFER_AMOUNT = 100; beforeEach(async function () { [owner, recipient, other] = await ethers.getSigners(); // Deploy ERC7984Example contract token = await ethers.deployContract('ERC7984Example', [ owner.address, INITIAL_AMOUNT, 'Confidential Token', 'CTKN', 'https://example.com/token' ]); }); describe('Initialization', function () { it('should set the correct name', async function () { expect(await token.name()).to.equal('Confidential Token'); }); it('should set the correct symbol', async function () { expect(await token.symbol()).to.equal('CTKN'); }); it('should set the correct contract URI', async function () { expect(await token.contractURI()).to.equal('https://example.com/token'); }); it('should mint initial amount to owner', async function () { // Verify that the owner has a balance (without decryption for now) const balanceHandle = await token.confidentialBalanceOf(owner.address); expect(balanceHandle).to.not.be.undefined; }); }); describe('Transfer Process', function () { it('should transfer tokens from owner to recipient', async function () { // Create encrypted input for transfer amount const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), owner.address) .add64(TRANSFER_AMOUNT) .encrypt(); // Perform the transfer await expect(token .connect(owner) 'confidentialTransfer(address,bytes32,bytes)').to.not.be.reverted; // Check that both addresses have balance handles (without decryption for now) const recipientBalanceHandle = await token.confidentialBalanceOf(recipient.address); const ownerBalanceHandle = await token.confidentialBalanceOf(owner.address); expect(recipientBalanceHandle).to.not.be.undefined; expect(ownerBalanceHandle).to.not.be.undefined; }); it('should allow recipient to transfer received tokens', async function () { // First transfer from owner to recipient const encryptedInput1 = await fhevm .createEncryptedInput(await token.getAddress(), owner.address) .add64(TRANSFER_AMOUNT) .encrypt(); await expect(token .connect(owner) 'confidentialTransfer(address,bytes32,bytes)').to.not.be.reverted; // Second transfer from recipient to other const encryptedInput2 = await fhevm .createEncryptedInput(await token.getAddress(), recipient.address) .add64(50) // Transfer half of what recipient received .encrypt(); await expect(token .connect(recipient) 'confidentialTransfer(address,bytes32,bytes)').to.not.be.reverted; // Check that all addresses have balance handles (without decryption for now) const otherBalanceHandle = await token.confidentialBalanceOf(other.address); const recipientBalanceHandle = await token.confidentialBalanceOf(recipient.address); expect(otherBalanceHandle).to.not.be.undefined; expect(recipientBalanceHandle).to.not.be.undefined; }); it('should revert when trying to transfer more than balance', async function () { const excessiveAmount = INITIAL_AMOUNT + 100; const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), recipient.address) .add64(excessiveAmount) .encrypt(); await expect( token .connect(recipient) 'confidentialTransfer(address,bytes32,bytes)' ).to.be.revertedWithCustomError(token, 'ERC7984ZeroBalance') .withArgs(recipient.address); }); it('should revert when transferring to zero address', async function () { const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), owner.address) .add64(TRANSFER_AMOUNT) .encrypt(); await expect( token .connect(owner) 'confidentialTransfer(address,bytes32,bytes)' ).to.be.revertedWithCustomError(token, 'ERC7984InvalidReceiver') .withArgs(ethers.ZeroAddress); }); }); });

其中两个关键 API 值得说明:

  • fhevm.createEncryptedInput(contractAddress, signerAddress)创建的加密值绑定到合约地址用户地址,只能由该用户在指定合约内使用,不能在其他用户或其他合约中复用;
  • confidentialTransfer(address,bytes32,bytes)的第三个参数inputProof是对加密输入完整性与来源的验证凭证,与 handle 一起传入合约。

运行测试并验证结果

在项目根目录执行文档给出的命令:

npx hardhat test test/ERC7984Example.test.ts

验证方式就是测试脚本里的断言本身,它们覆盖了四个层面:

  1. 初始化name()symbol()contractURI()返回部署时传入的值;confidentialBalanceOf(owner)返回的余额 handle 不为空,说明初始 mint 生效;
  2. 正常转账confidentialTransfer不 revert,且转出入双方都能取到余额 handle;收款方还能继续把收到的代币转出(链上密态计算在正常工作);
  3. 超额转账:期望触发自定义错误ERC7984ZeroBalance,参数为转账方地址;
  4. 转账到零地址:期望触发ERC7984InvalidReceiver,参数为ethers.ZeroAddress

这四组断言都通过,即说明机密通证的部署、加密输入、链上密态转账和错误路径都符合预期。

如果需要真正读出余额的明文值,快速上手测试文档 给出了fhevm.userDecryptEuint的用法:它接收四个参数——FHE 类型(FhevmType)、要解密的 handle、对该 handle 有权限的合约地址、有权限的用户 signer。handle 的访问权限通过链上的FHE.allow()授予,解密类型要与 handle 的实际类型一致(示例中uint32计数对应FhevmType.euint32)。ERC7984 的余额是euint64,对应的解密类型就是FhevmType.euint64。注意 ERC7984 示例测试本身只做 handle 存在性检查,没有解密余额;要解密需要先确保相应地址获得授权。

可选扩展:加密 mint/burn 与总量可见性

如果通证生命周期需要更多管理功能,文档 给出了三段可直接加入合约的扩展代码。

加密 mint(金额对链外保密):

function confidentialMint( address to, externalEuint64 encryptedAmount, bytes calldata inputProof ) external onlyOwner returns (euint64 transferred) { return _mint(to, FHE.fromExternal(encryptedAmount, inputProof)); }

对应的链外调用示例:

const enc = await fhevm .createEncryptedInput(await token.getAddress(), owner.address) .add64(1_000) .encrypt(); await token.confidentialMint(recipient.address, enc.handles[0], enc.inputProof);

文档提醒:encryptedAmountinputProof必须由 SDK 在链外生成,并对畸形输入做校验和 revert;机密操作 gas 成本更高,批量 mint 应克制、优先用少量大额 mint。

加密 burn

function confidentialBurn( address from, externalEuint64 encryptedAmount, bytes calldata inputProof ) external onlyOwner returns (euint64 transferred) { return _burn(from, FHE.fromExternal(encryptedAmount, inputProof)); }

文档同时给出明文版本的mintburn_mint(to, FHE.asEuint64(amount))/_burn(from, FHE.asEuint64(amount))),并说明明文 mint 的金额会出现在 calldata 和事件中,适合需要透明的公开铸造场景;从任意账户 burn 的权限很大,文档建议配合角色、多签或用户授权来控制。

总量可见性:允许 owner 解密最新总量的做法是覆写_update

function _update(address from, address to, euint64 amount) internal virtual override returns (euint64 transferred) { transferred = super._update(from, to, amount); FHE.allow(confidentialTotalSupply(), owner()); }

这样 owner 可以在每次状态变更后调用confidentialTotalSupply()并用其链外密钥材料解密返回的 handle。文档的提醒:Ownable2Step会自动授权当前owner(),所有权变更后只有新 owner 可解密;授予总量可见性属于特权访问,应记录密钥持有者及原因。

限制与继续深入

  • 本例的初始 mint 是明文的,链上可见;若要求从首次铸造起就保密,文档建议改用confidentialMint这类加密 mint 路径;
  • ZamaEthereumConfig面向 Ethereum 主网和 Sepolia 测试网,其他链的配置不在此文档范围内;
  • 围绕同一套库的更多集成示例(ERC-20 包装成 ERC-7984、反向 unwrap、机密 vesting)见 OpenZeppelin 集成目录,以及 erc7984 教程原文。

【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 1:19:02

Go Module依赖冲突解决方案与最佳实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 1:15:58

基于STM32F103的状态指示灯与呼吸灯实现:GPIO与PWM全解析

简介:面向嵌入式入门者与STM32开发者,这套工程基于Keil5 IDE与STM32F103VET6微控制器,实现LED呼吸灯与状态指示灯功能。工程涵盖GPIO初始化、定时器PWM配置及呼吸灯亮度渐变算法,可用于设备状态指示、用户界面反馈等场景&#xff…

作者头像 李华
网站建设 2026/9/13 1:12:29

AI崩溃排查与修复实战:从取证、根因定位到闭环防护

做AI应用这几年,我最怕的不是模型效果差,而是线上正跑着的对话机器人突然“精神分裂”:上一秒还在正常回答问题,下一秒就开始复读同一句话,或者吐出一堆毫无逻辑的乱码,更有甚者直接把系统提示词给“供”出…

作者头像 李华