ERC-1155

| 笔记 | 10068 | 26分钟 | ERC以太坊区块链BlockchainSolidity智能合约

来新活了,先研究一下什么是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 派上用场的地方!

具体例子

  1. 合约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”这个回调接口的身份标识。

  2. ERC-777 合约在转账时会自动查询 ERC-1820 注册表

    • 发现合约A声明了自己能接收 ERC-777 代币,且有 tokensReceived 回调。
    • 于是转账时会自动调用合约A的 tokensReceived 函数。
    • 如果没注册,则不会做回调。

结果

  • 合约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);
    }

检查参数 idsvalues 数组长度是否一致(即每种代币有且仅有一个对应的操作数量)。如果不一致则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 != 0to != 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)或“蜜罐追踪法”。

  1. 发现风险/黑产地址后,监管或风控方用一个特殊的代币(有时叫“污点币”、“风险标记币”)给目标地址转账。
  2. 链上所有人都能看到这些代币,但它们通常没实际价值,只用于追踪和标记地址
  3. 监管/风控系统扫描链上账户,如果某地址持有这种“标记币”,说明它被列入风险名单或参与可疑资金流。

3.3 为什么拒收可以免监管?

ERC-1155原生支持拒收,只要修改 onERC1155Received 这个函数的实现一直 revert 就行了。

黑产地址可以拒收监管机构发来的污点币。