如何用 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.x、v20.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。合约继承三个部分:
ERC7984—— OpenZeppelin 提供的机密通证基类;Ownable2Step—— 为铸币和管理功能提供访问控制;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验证方式就是测试脚本里的断言本身,它们覆盖了四个层面:
- 初始化:
name()、symbol()、contractURI()返回部署时传入的值;confidentialBalanceOf(owner)返回的余额 handle 不为空,说明初始 mint 生效; - 正常转账:
confidentialTransfer不 revert,且转出入双方都能取到余额 handle;收款方还能继续把收到的代币转出(链上密态计算在正常工作); - 超额转账:期望触发自定义错误
ERC7984ZeroBalance,参数为转账方地址; - 转账到零地址:期望触发
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);文档提醒:encryptedAmount和inputProof必须由 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)); }文档同时给出明文版本的mint和burn(_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),仅供参考