ERC-1155
来新活了,先研究一下什么是ERC-1155.
1 ERC-1155介绍
1.1 基本功能
用一个智能合约管理多个代币,同时支持同质化代币FT和非同质化代币NFT。
复习:FT和NFT
- FT:每一份代币完全等价,例如ETH、USDT。常用标准ERC-20。
- NFT:每一个代币都是独一无二的,例如数字艺术品、游戏装备、门票等。常用表混ERC-721。
简单理解成,FT是钱,NFT是物品。
易于用在区块链游戏中,金币、装备、材料等多种物品统一管理交易。
1.2 主要特性
- 批量操作,支持一次完成多个代币的转账,减少gas成本。
- 同时支持FT/NFT,每种资产用一个 id 标识。
- 安全机制:引入了safeTransferFrom和safeBatchTransferFrom,防止NFT被传入不兼容的合约。
1.3 ERC-165
用于让智能合约声明和检测自己支持哪些接口。
ERC-1155依赖于ERC-165。
bool isERC1155 = contractA.supportsInterface(0xd9b67a26);
0xd9b67a26 是 ERC-1155 所有函数签名做异或运算后得到的 bytes4 值。
1.4 ERC-1820
ERC-1820 是以太坊上的一个“通用合约接口注册表”标准。
- 允许任意地址(账户或合约)在链上注册和声明自己支持的接口/能力。
- 让外部合约、DApp、服务可以查询某地址支持哪些接口,实现“动态接口发现”。
- 解决了“合约升级”“多协议兼容”等场景下,运行时无法直接 introspection(自省)”的问题。
对比ERC-165
- ERC-165:只能让合约声明支持哪些“静态接口”,且只适用于智能合约自身。
- ERC-1820:支持任意地址注册,且接口声明和查询是链上全局、可升级、可委托的,适用面更广。
实际应用中,ERC-165只能被别的合约调用检查。而ERC-1820能直接让钱包调用合约代码来检查。
场景:ERC-777 代币合约的“合约账户回调”功能
背景
- ERC-777 是一种比 ERC-20 更高级的同质化代币标准。
- 它要求,如果你把代币转账给一个合约账户,合约必须实现特定回调函数(比如
tokensReceived)。 - 但怎么让 ERC-777 合约自动发现“目标合约有没有实现回调”?
这就是 ERC-1820 派上用场的地方!
具体例子
-
合约A 想接收 ERC-777 代币时回调处理
- 合约A实现了
tokensReceived函数。 - 合约A调用 ERC-1820 注册表,声明自己是
ERC777TokensRecipient接口的实现者。
// 假设 Registry 已部署在 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24 IERC1820Registry registry = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24); // 注册自己的回调能力 registry.setInterfaceImplementer(address(this), keccak256("ERC777TokensRecipient"), address(this));这里
keccak256("ERC777TokensRecipient")就是“tokensReceived”这个回调接口的身份标识。 - 合约A实现了
-
ERC-777 合约在转账时会自动查询 ERC-1820 注册表
- 发现合约A声明了自己能接收 ERC-777 代币,且有
tokensReceived回调。 - 于是转账时会自动调用合约A的
tokensReceived函数。 - 如果没注册,则不会做回调。
- 发现合约A声明了自己能接收 ERC-777 代币,且有
结果
- 合约A不需要和ERC-777合约“硬编码”对接,只需注册一次即可自动兼容各种ERC-777合约。
- 其他合约或钱包可以随时查询 ERC-1820 Registry,得知地址A支持哪些协议回调、有哪些功能。
ERC-1820 让“某合约/账户支持哪些功能”变成了链上可注册、可查询、可升级的标准机制。
2 ERC-1155代码速读
2.1 变量
_balance
mapping(uint256 id => mapping(address account => uint256)) private _balances;
每个账户在每种代币上的余额。
查询 A 用户持有的 X 号 token 数量,访问方式为 _balances[X][A]。
_operatorApprovals
mapping(address account => mapping(address operator => bool)) private _operatorApprovals;
用于记录某个账户是否授权了另一个账户(operator)对其所有的 token 进行操作。
如果 A 授权 B 作为 operator,那么 _operatorApprovals[A][B] == true。
_uri
string private _uri;
_uri 用于存储所有 token 类型共享的元数据模板 URI(统一资源标识符),即元数据(Metadata)访问地址的模板。
通常写成:https://token-cdn-domain/{id}.json ,id是占位符,每个代币对应一个id。
一个uri里的内容形式如下:
{
"name": "Sword of Power",
"description": "A legendary sword with immense power.",
"image": "https://nft.example.com/images/sword.png",
"attributes": [
{ "trait_type": "Attack", "value": 150 }
]
}
钱包可以获取ERC-1155合约的uri模板,然后自动替换id,就可以看到各种道具的图片、说明了。
2.2 接口
supportsInterface
/// @inheritdoc IERC165
function supportsInterface(bytes4 interfaceId)
public view virtual override(ERC165, IERC165) returns (bool) {
return
interfaceId == type(IERC1155).interfaceId ||
interfaceId == type(IERC1155MetadataURI).interfaceId ||
super.supportsInterface(interfaceId);
}
这个合约的函数签名符合输入的interfaceId。说明合约符合接口。
_update
(GPT生成)
/**
* @dev Transfers a `value` amount of tokens of type `id` from `from` to `to`. Will mint (or burn) if `from`
* (or `to`) is the zero address.
*
* Emits a {TransferSingle} event if the arrays contain one element, and {TransferBatch} otherwise.
*
* Requirements:
*
* - If `to` refers to a smart contract, it must implement either {IERC1155Receiver-onERC1155Received}
* or {IERC1155Receiver-onERC1155BatchReceived} and return the acceptance magic value.
* - `ids` and `values` must have the same length.
*
* NOTE: The ERC-1155 acceptance check is not performed in this function. See {_updateWithAcceptanceCheck} instead.
*/
function _update(address from, address to, uint256[] memory ids, uint256[] memory values) internal virtual {
这是一个内部(
internal)的虚函数,用于在ERC-1155合约内部执行代币的**批量转账、铸造(mint)、销毁(burn)**等核心账本操作。
if (ids.length != values.length) {
revert ERC1155InvalidArrayLength(ids.length, values.length);
}
检查参数
ids和values数组长度是否一致(即每种代币有且仅有一个对应的操作数量)。如果不一致则revert并抛出自定义错误。
address operator = _msgSender();
记录操作者(即本次操作的发起者,一般是
msg.sender,但有可能被代理转发时用_msgSender())。
for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids.unsafeMemoryAccess(i);
uint256 value = values.unsafeMemoryAccess(i);
遍历所有传入的id和数量,对每种代币类型分别处理。
unsafeMemoryAccess是 OpenZeppelin 的性能优化写法,等价于ids[i]、values[i]。
[分支1:from != address(0)]
if (from != address(0)) {
uint256 fromBalance = _balances[id][from];
if (fromBalance < value) {
revert ERC1155InsufficientBalance(from, fromBalance, value, id);
}
unchecked {
// Overflow not possible: value <= fromBalance
_balances[id][from] = fromBalance - value;
}
}
如果
from不是零地址(代表正常转账或销毁 burn):
- 查询
from账户下该id的余额;- 如果余额不足,revert 报错;
- 否则从余额中扣除相应数量(因为扣减前已做检查,所以用
unchecked节省Gas)。
[分支2:to != address(0)]
if (to != address(0)) {
_balances[id][to] += value;
}
}
如果
to不是零地址(代表正常转账或铸造 mint):
- 给
to账户下该id的余额加上相应数量。
[事件通知]
if (ids.length == 1) {
uint256 id = ids.unsafeMemoryAccess(0);
uint256 value = values.unsafeMemoryAccess(0);
emit TransferSingle(operator, from, to, id, value);
} else {
emit TransferBatch(operator, from, to, ids, values);
}
}
判断是单个类型代币的操作还是多个类型的批量操作:
- 如果只有一个id,触发
TransferSingle事件;- 否则触发
TransferBatch事件。
复习:emit + event
emit + event 就是给外部世界用来高效检索的“合约执行日志”。它专门用来记录合约内部发生的关键事件,方便链外用户查找和监听。
没有事件机制,只能看到“谁调用了什么合约,带了什么参数”
特殊场景说明
from == address(0)表示铸造(mint);to == address(0)表示销毁(burn);- 普通转账是
from != 0且to != 0; - 本函数本身不做合约接收方的安全性校验,需要调用
_updateWithAcceptanceCheck才会做。
_updateWithAcceptanceCheck
function _updateWithAcceptanceCheck(
address from,
address to,
uint256[] memory ids,
uint256[] memory values,
bytes memory data
) internal virtual {
_update(from, to, ids, values);
if (to != address(0)) {
address operator = _msgSender();
if (ids.length == 1) {
uint256 id = ids.unsafeMemoryAccess(0);
uint256 value = values.unsafeMemoryAccess(0);
ERC1155Utils.checkOnERC1155Received(operator, from, to, id, value, data);
} else {
ERC1155Utils.checkOnERC1155BatchReceived(operator, from, to, ids, values, data);
}
}
}
先执行 _update 更新钱包数量。
如果接收方不是 0 (即该操作不是销毁),则进行接收方检查。
接收方检查:checkOnERC1155Received
该函数用于确认目标合约真的支持接收 ERC-1155 资产。如果对方没实现或者拒绝接收,就立即回滚,防止 token 丢失到不可用合约。
(GPT)
function checkOnERC1155Received(
address operator,
address from,
address to,
uint256 id,
uint256 value,
bytes memory data
) internal {
- 这是一个库函数(library),只在合约内部被调用,不对外暴露。
- 参数含义:
operator:操作发起者(一般就是msg.sender)。from:转出代币的钱包或合约地址。to:接收方地址(重点,可能是合约,也可能是普通钱包)。id/value:本次转账的 token id 和数量。data:可附加传递的自定义数据。
if (to.code.length > 0) {
- 检查
to地址是否是合约(to.code.length > 0)。 - 如果
to是普通钱包(EOA),没有合约代码,这个检查直接跳过,函数不做任何事。
try IERC1155Receiver(to).onERC1155Received(operator, from, id, value, data) returns (bytes4 response) {
if (response != IERC1155Receiver.onERC1155Received.selector) {
// Tokens rejected
revert IERC1155Errors.ERC1155InvalidReceiver(to);
}
}
- 如果
to是合约,则尝试调用它的onERC1155Received回调函数。 - 如果调用正常返回,会把返回值
response拿出来检查。 - 合约必须返回固定魔法值(即
IERC1155Receiver.onERC1155Received.selector,这是接口要求),否则就表示它“拒绝”或者“不兼容”,此时直接 revert(回滚)交易。
catch (bytes memory reason) {
if (reason.length == 0) {
// non-IERC1155Receiver implementer
revert IERC1155Errors.ERC1155InvalidReceiver(to);
} else {
assembly ("memory-safe") {
revert(add(reason, 0x20), mload(reason))
}
}
}
}
- 如果调用
onERC1155Received时出错/回滚,进入 catch 分支。 - 如果
reason.length == 0,说明对方合约根本没实现这个回调,或者合约回退但没原因,直接认为它不是兼容 ERC-1155 的 receiver,再次回滚。 - 如果有 revert 原因(如错误消息),用内联汇编直接把错误原样抛出,方便追踪具体原因。
总结与意义
- 功能:保障 ERC-1155 代币不会被错误地发送到“不支持 ERC-1155”的合约,防止用户资产丢失。
- 执行场景:ERC-1155 合约在每次“转账给合约地址”时,都会调用这个函数做“兼容性检测”。
- 安全性:只要不是兼容合约、或者合约拒绝接收,都会回滚整个交易,资产不会丢。
_safeTransferFrom
function _safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) internal {
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
_updateWithAcceptanceCheck(from, to, ids, values, data);
}
安全转账,保证不会销毁/铸造代币。
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
这行的作用是把单个 id value 封装成数组。
_safeBatchTransferFrom
function _safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory values,
bytes memory data
) internal {
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
_updateWithAcceptanceCheck(from, to, ids, values, data);
}
同上。
_mint & _burn
function _mint(address to, uint256 id, uint256 value, bytes memory data) internal {
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
_updateWithAcceptanceCheck(address(0), to, ids, values, data);
}
function _burn(address from, uint256 id, uint256 value) internal {
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
_updateWithAcceptanceCheck(from, address(0), ids, values, "");
}
同上。
2.3 其他工具函数与扩展
另外还有一些工具函数和扩展。
工具:
- ERC1155Holder:一个最简ERC1155抽象合约,继承这个合约就可以兼容ERC1155标准
扩展:
- ERC1155Burnable:允许销毁(启用_burn函数)
- ERC1155Pausable:允许暂停,可以暂停合约,禁止交易
- ERC1155Supply:给 ERC-1155 合约增加“总供应量统计”功能。允许合约外部随时查询某种 token(id)的总供应量,以及所有 token 的全局总供应量。
- ERC1155URIStorage:允许每个 token(如某个道具、NFT、物品)有独立的 URI,而不是所有 token 共用一个 URI 模板。
- IERC1155MetadataURI:支持元数据访问
2.4 总结
代码真的很少很简单。
功能确实就是一句话:同时管理多种token的合约。
3 ERC-1155的监管挑战
首先,以太坊及兼容链上通用的 ERC-20[8]、ERC-721[9]、ERC-1155[10]等代币标准在协议层存在根本性缺陷,均未预留任何用于强制标记或链上跟踪黑产地址的原生能力,接收方合约通过回调即可选择性拒收任何代币,包括由监管机构为识别风险地址而生成的代币,进一步加剧了监管的盲点。
问题:
- 黑产地址通常是在做什么,他们如何非法获利?如何躲避监管?
- 监管机构通过给分享地址生成代币来监管?这是怎么操作的?
- 为什么黑产地址拒收代币就可以免监管?
3.1 黑产如何获利?如何躲避监管?
获利方法老生常谈:
- 洗钱:通过将非法所得(如诈骗、黑客攻击、勒索等)转入多个新生成的钱包地址(俗称“分币”),掩盖资金来源。
- 诈骗:如虚假项目募资、钓鱼网站、伪造NFT等,把用户资产骗到自己的地址。
- 勒索:如“勒索病毒”攻击后索要比特币、以太币等作为赎金。
- 盗币:黑客攻击交易所、项目合约后,将资产提走存入自己控制的地址。
- 链上赌博/洗白:利用DEX或链上赌博、NFT交易,模拟“正常交易”来混淆资金流。
至于如何躲避监管,区块链上原生的去中心化属性就使得监管非常困难。
后文说的监管指的是,如何标记出某个钱包地址可能在进行非法交易,从而使得其他合约拒绝与改地址交互。
3.2 监管机构如何监管?
业内“链上监管”一种理论方法:“标签转账”(tagging by token)或“蜜罐追踪法”。
- 发现风险/黑产地址后,监管或风控方用一个特殊的代币(有时叫“污点币”、“风险标记币”)给目标地址转账。
- 链上所有人都能看到这些代币,但它们通常没实际价值,只用于追踪和标记地址。
- 监管/风控系统扫描链上账户,如果某地址持有这种“标记币”,说明它被列入风险名单或参与可疑资金流。
3.3 为什么拒收可以免监管?
ERC-1155原生支持拒收,只要修改 onERC1155Received 这个函数的实现一直 revert 就行了。
黑产地址可以拒收监管机构发来的污点币。