Solidity 隐私代币揭秘:自动回调与操作员机制详解
目录
- 引言
- 一、Operator
-
-
- 代码解释
-
- 二、Callback
-
-
- 发送方合约(Token 合约)
- 接收方合约(Receiver)
- 代码解释
-
- 三、Swaps
- 总结
引言
在以太坊智能合约的开发中,传统的代币转账方式通常依赖于用户直接调用 transfer 或 transferFrom 方法。然而,这种方式在某些场景下可能不够灵活,尤其是当需要第三方代表用户执行代币操作时。为了解决这一问题,引入了“操作员(Operator)”机制,使得用户可以授权第三方在特定条件下代表自己进行代币操作。
在 OpenZeppelin 的 Confidential Fungible Token(CFT)实现中,除了传统的转账方式,还引入了带回调(callback)的转账函数 confidentialTransferWithCallback。这种方式允许在代币转账完成后,自动触发接收方合约的特定函数,从而实现更复杂的业务逻辑。
本文将详细介绍操作员机制的实现方式,以及带回调的转账函数如何在发送方和接收方合约中协同工作,实现代币的安全转移和自动处理。
一、Operator
在标准的 ERC-20 代币合约中,用户只能直接调用 transfer 和 transferFrom 方法来转移代币。然而,在某些场景下,我们希望允许第三方代表用户进行代币转移操作,这时就引入了操作员机制。操作员是被授权代表用户执行特定操作的地址,通常具有一定的权限和有效期限。
const alice: Wallet;const expirationTimestamp = Math.round(Date.now() / 1000) + 60 * 60 * 24; // 当前时间 + 24 小时await tokenContract.connect(alice).setOperator(bob, expirationTimestamp);
代码解释
创建一个钱包实例 alice,用于签名交易。
计算当前时间加上 24 小时后的时间戳,作为操作权限的过期时间。
通过 alice 钱包连接到代币合约,并调用 setOperator 方法,将 bob 设置为操作员,权限在指定的时间戳后过期。
二、Callback
回调就是转账完成后,“自动触发”对接收方合约的特定函数进行调用。
如果你选择使用带回调的转账函数,就不需要再手动调用中间授权(如“操作员审批”)步骤,系统会自动处理。
被回调函数调用的智能合约必须实现 IConfidentialFungibleTokenReceiver 接口。
发送方合约(Token 合约)
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;import {IConfidentialFungibleTokenReceiver} from \"@openzeppelin/confidential-contracts/interfaces/IConfidentialFungibleTokenReceiver.sol\";import {ConfidentialFungibleToken} from \"@openzeppelin/confidential-contracts/token/ConfidentialFungibleToken.sol\";contract ConfidentialToken is ConfidentialFungibleToken { constructor() ConfidentialFungibleToken(\"ConfidentialToken\", \"CFT\") {} function confidentialTransferWithCallback( address recipient, externalEuint64 encryptedAmount, bytes calldata inputProof, bytes calldata callbackData ) external { confidentialTransferFrom(msg.sender, address(this), encryptedAmount, inputProof); bool success = IConfidentialFungibleTokenReceiver(recipient) .onConfidentialTransferReceived( msg.sender, encryptedAmount, callbackData ); require(success, \"Callback failed\"); }}
接收方合约(Receiver)
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;import {IConfidentialFungibleTokenReceiver} from \"@openzeppelin/confidential-contracts/interfaces/IConfidentialFungibleTokenReceiver.sol\";contract Receiver is IConfidentialFungibleTokenReceiver { mapping(address => uint256) public balances; function onConfidentialTransferReceived( address from, bytes calldata ciphertext, bytes calldata data ) external override returns (ebool) { uint256 decryptedAmount = 100; // 假设解密后的金额为 100 balances[from] += decryptedAmount; return true; }}
代码解释
发送方
调用 confidentialTransferWithCallback:将加密金额转账给接收方合约。
Token 合约自动调用回调函数:在转账完成后,自动调用接收方合约
的 onConfidentialTransferReceived 方法。
接收方合约处理回调:接收方合约
在 onConfidentialTransferReceived 方法中进行加密金额的解密和验证。如果验证通过,更新接收方的余额并返回 true;否则,返回 false 或抛出异常,导致转账失败。
三、Swaps
(一)Swap ERC20 to ConfidentialFungibleToken
下面这段 Solidity 代码展示了一个将普通 ERC20 代币 **“包裹”(wrap)**成保密形式加密代币的过程。它主要做两件事:扣除用户的明文代币,然后 mint(铸造)等量的加密代币。
ERC20 代币仅需直接转入,若转入失败则交易自动回滚。随后,我们通过内部的 _mint 函数转出正确数量的 ConfidentialFungibleToken
,该函数保证转出操作必成功。
function wrap(address to, uint256 amount) public virtual { // take ownership of the tokens SafeERC20.safeTransferFrom(underlying(), msg.sender, address(this), amount - (amount % rate())); // mint confidential token _mint(to, (amount / rate()).toUint64().asEuint64());}
代码解释
1、函数签名及参数
wrap(address to, uint256 amount):
to 是接收新铸造的加密代币的地址;
amount 是用户想包裹的明文 ERC20 代币数量。
2、扣除用户明文代币
underlying()
返回被包裹的原始 ERC20 代币合约地址;
safeTransferFrom
从调用者 msg.sender
扣除 (amount - (amount % rate()))
数量的明文代币,转入当前合约;
amount % rate()
残余部分被抹去,确保只扣整倍的代币;
rate() 是一个兑换比例,比如 100:1 ——意味着你需要 100 个明文资产对应 1 个加密代币。
3、铸造对应加密代币
计算铸造数量:amount / rate()
,得到你能获取多少单位的加密代币;
toUint64()
:确保铸造数量不超过 uint64 的最大值;
.asEuint64()
:将普通整数转换为加密整数类型 euint64;
_mint(to, …)
将等量的加密代币(保密状态)铸造/记账给 to 地址。
(二)Swap ConfidentialFungibleToken to ConfidentialFungibleToken
Solidity 代码实现了一个基于 Zama FHE 协议的加密代币交换功能,允许用户在链上进行隐私保护的代币交换。在 Zama 的 FHE 协议中,所有数据都是加密的,只有授权的合约才能解密和操作这些数据。
function swapConfidentialForConfidential( IConfidentialFungibleToken fromToken, IConfidentialFungibleToken toToken, externalEuint64 amountInput, bytes calldata inputProof) public virtual { require(fromToken.isOperator(msg.sender, address(this))); euint64 amount = FHE.fromExternal(amountInput, inputProof); FHE.allowTransient(amount, address(fromToken)); euint64 amountTransferred = fromToken.confidentialTransferFrom(msg.sender, address(this), amount); FHE.allowTransient(amountTransferred, address(toToken)); toToken.confidentialTransfer(msg.sender, amountTransferred);}
代码解释
①require(fromToken.isOperator(msg.sender, address(this)));
作用:验证调用者(msg.sender)是否被授权为 fromToken 的操作员。
背景:在 Zama 的 FHE 协议中,只有被授权的操作员才能执行加密代币的转账操作。
② euint64 amount = FHE.fromExternal(amountInput, inputProof);
作用:将外部传入的加密数据(amountInput)和证明(inputProof)转换为内部可操作的加密整数类型 euint64。
背景:euint64 是 Zama FHE 协议中用于表示加密整数的类型。
③ FHE.allowTransient(amount, address(fromToken));
作用:允许对 fromToken 合约地址的临时访问权限,以便执行加密代币的转账操作。
背景:在 FHE 协议中,操作加密数据需要临时授权。
④euint64 amountTransferred = fromToken.confidentialTransferFrom(msg.sender, address(this), amount);
作用:从调用者(msg.sender)向当前合约(address(this))转移指定数量的加密代币。
背景:confidentialTransferFrom 是 Zama FHE 协议中用于执行加密代币转账的函数。
⑤ FHE.allowTransient(amountTransferred, address(toToken));
作用:允许对 toToken 合约地址的临时访问权限,以便将转账的加密代币发送给目标地址。
背景:在 FHE 协议中,操作加密数据需要临时授权。
⑥ toToken.confidentialTransfer(msg.sender, amountTransferred);
作用:将转账的加密代币发送到目标地址(msg.sender)。
背景:confidentialTransfer 是 Zama FHE 协议中用于执行加密代币转账的函数。
(三)Swap ConfidentialFungibleToken to ERC20
从保密代币切换到非保密代币是最复杂的操作,因为需要访问解密后的数据才能准确完成请求。在我们的示例中,解密操作将在链下进行,并通过 Zama 的网关将结果回传。以下是一个合约示例,演示如何将保密代币与 ERC20 代币进行 1:1 兑换。
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;// 导入 Zama FHE 库,用于处理加密整数类型import {FHE, externalEuint64, euint64} from \"@fhevm/solidity/lib/FHE.sol\";// 导入标准 ERC20 接口import {IERC20} from \"@openzeppelin/contracts/interfaces/IERC20.sol\";// 导入 SafeERC20,用于安全转账import {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";// 导入保密代币接口import {IConfidentialFungibleToken} from \"@openzeppelin/confidential-contracts/interfaces/IConfidentialFungibleToken.sol\";contract SwapConfidentialToERC20 { using FHE for *; // 自定义错误:无效的解密回调请求 ID error SwapConfidentialToERC20InvalidGatewayRequest(uint256 requestId); // 映射:存储每个请求 ID 对应的接收者地址 mapping(uint256 => address) private _receivers; // 保密代币合约(输入端) IConfidentialFungibleToken private _fromToken; // 普通 ERC-20 代币合约(输出端) IERC20 private _toToken; // 构造函数:初始化输入和输出代币合约地址 constructor(IConfidentialFungibleToken fromToken, IERC20 toToken) { _fromToken = fromToken; _toToken = toToken; } /** * @notice 用户调用此函数发起 swap,将加密代币提交; * @param encryptedInput 加密金额,externalEuint64 类型 * @param inputProof 加密金额的零知识证明 */ function swapConfidentialToERC20(externalEuint64 encryptedInput, bytes memory inputProof) public { // 1. 使用 proof 将加密输入转为链上可用的加密整数 euint64 amount = encryptedInput.fromExternal(inputProof); // 2. 授权保密代币合约读取 this 合约中的这笔加密金额 amount.allowTransient(address(_fromToken)); // 3. 从用户 transfer 保密代币到本合约 euint64 amountTransferred = _fromToken.confidentialTransferFrom(msg.sender, address(this), amount); // 4. 将 ciphertext 添加到解密请求列表 bytes32 ; cts[0] = euint64.unwrap(amountTransferred); // 5. 发出解密请求,并指定回调函数 finalizeSwap uint256 requestID = FHE.requestDecryption(cts, this.finalizeSwap.selector); // 6. 保存 requestID ↔ 用户地址 关联,等待回调发币 _receivers[requestID] = msg.sender; } /** * @notice 解密服务调用此函数,将加密金额转成明文后,合约会发放同等 ERC-20 代币 * @param requestID 与 swap 请求对应的唯一 ID * @param amount 解密后拿到的金额(uint64 明文) * @param signatures 多重签名验证解密合法性 */ function finalizeSwap(uint256 requestID, uint64 amount, bytes[] memory signatures) public virtual { // 1. 验证解密签名是否合法 FHE.checkSignatures(requestID, signatures); // 2. 获取之前保存的接收方 address to = _receivers[requestID]; if (to == address(0)) revert SwapConfidentialToERC20InvalidGatewayRequest(requestID); // 3. 清除记录避免重复提现 delete _receivers[requestID]; // 4. 如果解密金额大于 0,将 ERC-20 转给接收者 if (amount != 0) { SafeERC20.safeTransfer(_toToken, to, amount); } }}
代码解释
注释写在代码上啦。这段 Solidity 合约 SwapConfidentialToERC20 用于将保密代币(encrypted token)兑换成普通 ERC‑20 代币,核心思路是:用户先将加密金额转移到合约,触发链下解密过程,最后由合约释放等值的 ERC‑20。它做了三件事:接收保密代币、提交解密请求、解密成功后,释放相应 ERC‑20 代币。
总结
通过引入操作员机制和带回调的转账函数,智能合约的代币操作变得更加灵活和安全。
操作员机制
:用户可以通过 setOperator 方法授权第三方在指定时间内代表自己执行代币操作。这种方式避免了频繁的授权操作,提高了用户体验。
带回调的转账函数
:发送方合约在执行代币转账后,自动调用接收方合约的 onConfidentialTransferReceived 方法,实现自动处理。这种方式简化了授权和转账的流程,提高了系统的自动化程度。
通过上述机制的结合,智能合约不仅实现了代币的安全转移,还能够在转账完成后自动触发特定的业务逻辑,为去中心化应用(DApp)提供了更强大的功能支持。