跳到主要内容

构建机密 ERC-20

机密 ERC-20 隐藏两样标准 ERC-20 泄漏的东西:余额(只有持有者能读自己的)与转账金额(只有发送方知道)。这是 OXN 的典型应用。

设计

  • 隐藏余额。 balanceOf(user) 对除用户本人(或被授权者)之外的所有人返回 0。
  • 隐藏金额。 transfer(recipient, amount)amount 放在加密 calldata——观察者看到两个地址间发生了转账,但不知多少。
  • 兼容性。 尽可能保持标准 IERC20 接口,让钱包与其他工具照样工作。

合约

contracts/ConfidentialToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract ConfidentialToken {
string public name = "ConfidentialToken";
string public symbol = "CONF";
uint8 public decimals = 18;
uint256 public totalSupply;

mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
mapping(address => mapping(address => bool)) private _readApprovals;

event Transfer(address indexed from, address indexed to); // 不含金额
event Approval(address indexed owner, address indexed spender); // 不含金额

constructor(uint256 initialSupply) {
totalSupply = initialSupply;
_balances[msg.sender] = initialSupply;
emit Transfer(address(0), msg.sender);
}

// 只对账户本人或被授权的读者可见
function balanceOf(address account) external view returns (uint256) {
if (msg.sender == account || _readApprovals[account][msg.sender]) {
return _balances[account];
}
return 0;
}

function transfer(address to, uint256 amount) external returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}

function approve(address spender, uint256 amount) external returns (bool) {
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender);
return true;
}

function transferFrom(address from, address to, uint256 amount) external returns (bool) {
uint256 allowed = _allowances[from][msg.sender];
require(allowed >= amount, "insufficient allowance");
_allowances[from][msg.sender] = allowed - amount;
_transfer(from, to, amount);
return true;
}

function allowance(address owner, address spender) external view returns (uint256) {
if (msg.sender == owner || _readApprovals[owner][msg.sender]) {
return _allowances[owner][spender];
}
return 0;
}

function approveReader(address reader) external {
_readApprovals[msg.sender][reader] = true;
}

function _transfer(address from, address to, uint256 amount) internal {
require(_balances[from] >= amount, "insufficient balance");
_balances[from] -= amount;
_balances[to] += amount;
emit Transfer(from, to); // 不 emit 金额
}
}

什么机密什么可见

机密:

  • 单个余额
  • 转账金额
  • 授权金额

可见:

  • 交易发生在哪些地址间
  • 转账是否成功(状态)
  • 总供应量(公开状态)

多数隐私需求下这是好平衡——参与者可见(反正 tx.from / tx.to 也泄漏),值是藏起来的。

客户端交互

import { ethers } from "ethers";
import { wrapEthersProvider, wrapEthersSigner } from "@oasisprotocol/sapphire-ethers-v6";

const raw = new ethers.JsonRpcProvider("https://rpc.bout.network");
const provider = wrapEthersProvider(raw);
const signer = wrapEthersSigner(new ethers.Wallet(PRIVATE_KEY, raw));

const token = new ethers.Contract(tokenAddress, abi, signer); // signer 不是 provider

// 签名查询——返回调用者真实余额
const myBalance = await token.balanceOf(await signer.getAddress());

// 转账——amount 在 calldata 里加密
await token.transfer(recipient, ethers.parseUnits("100", 18), { gasLimit: 500_000n });

注意合约用 signer 构造,不是 provider。这确保 balanceOf 查询是签名查询,msg.sender 被填充。

与标准工具的兼容性

大部分钱包和浏览器看不到余额或金额。这正是重点——但意味着:

  • MetaMask 代币检测默认可能对这个代币显示 0 余额。
  • 浏览器 "Read Contract" UIbalanceOf 会返回 0,因为它用明文 eth_call
  • 代币索引器无法跨用户追踪余额。

想让钱包 UI 显示余额,钱包必须为持有者本人的余额发签名查询。OXN 网页钱包对它识别的代币正确处理这个。

接收方通知

因为 Transfer 事件不含金额、且只有发送方能解密,接收方不会自动知道收到转账。选项同发行 ERC-20

完整示例

带测试的可运行 Hardhat 项目见 OXN 示例仓库

下一步