构建机密 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" UI 对
balanceOf会返回 0,因为它用明文eth_call。 - 代币索引器无法跨用户追踪余额。
想让钱包 UI 显示余额,钱包必须为持有者本人的余额发签名查询。OXN 网页钱包对它识别的代币正确处理这个。
接收方通知
因为 Transfer 事件不含金额、且只有发送方能解密,接收方不会自动知道收到转账。选项同发行 ERC-20。
完整示例
带测试的可运行 Hardhat 项目见 OXN 示例仓库。