Alert Source Discuss
⚠️ Review Standards Track: ERC

ERC-6909: 最小化多代币接口

在单个合约中通过 ID 管理多个代币的最小化规范。

Authors JT Riley (@jtriley2p), Dillon (@d1ll0n), Sara (@snreynolds), Vectorized (@Vectorized), Neodaoist (@neodaoist)
Created 2023-04-19
Requires EIP-165

摘要

以下内容将多代币合约指定为 ERC-1155 多代币标准的简化替代方案。与 ERC-1155 相比,回调和批处理已从接口中删除,并且权限系统是一种混合的操作员批准方案,用于实现细粒度和可扩展的权限。从功能上讲,该接口已简化为在同一合约下管理多个代币所需的最基本要求。

动机

ERC-1155 标准包含不必要的功能,例如要求具有代码的接收者账户实现返回特定值的回调,以及规范中的批量调用。此外,单一操作员权限方案授予合约中每个代币 ID 的无限额度。仅在必要时才会有意删除向后兼容性。规范中有意省略了其他功能,例如批量调用、增加和减少额度的方法以及其他用户体验改进,以最大限度地减少所需的外部接口。

根据 ERC-1155,每次转移和批量转移到合约账户都需要回调。当接收者账户是合约账户时,这可能需要对接收者进行不必要的外部调用。虽然在某些情况下可能需要此行为,但无法选择退出此行为,就像 ERC-721 同时具有 transferFromsafeTransferFrom 一样。除了代币合约本身的运行时性能之外,它还会影响接收者合约账户的运行时性能和代码大小,从而需要多个回调函数和返回值来接收代币。

批量转移虽然有用,但本标准不包括批量转移,以便允许在不同的实现中进行opinionated 批量转移操作。例如,不同的 ABI 编码可以在不同的环境中提供不同的好处,例如对于具有 calldata 存储承诺的 rollups 的 calldata 大小优化,或对于具有昂贵 gas 费用的环境的运行时性能。

混合额度-操作员权限方案可以在代币批准上实现细粒度但可扩展的控制。 额度使外部账户可以通过 ID 代表用户转移单个代币 ID 的代币,而操作员则被授予对用户的所有代币 ID 的完全转移权限。

规范

本文档中的关键词“必须 (MUST)”,“不得 (MUST NOT)”,“必需 (REQUIRED)”,“应该 (SHALL)”,“不应该 (SHALL NOT)”,“应当 (SHOULD)”,“不应当 (SHOULD NOT)”,“推荐 (RECOMMENDED)”,“不推荐 (NOT RECOMMENDED)”,“可以 (MAY)”和“可选 (OPTIONAL)”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。

每个符合 ERC-6909 的合约除了以下接口外,还必须实现 ERC-165 接口。

定义

  • infinite: uint256 的最大值 (2 ** 256 - 1)。
  • caller: 当前上下文的调用者 (msg.sender)。
  • spender: 代表另一个账户转移代币的账户。
  • operator: 对另一个账户的所有代币 ID 具有无限转移权限的账户。
  • mint: 创建一定数量的代币。这可能发生在 mint 方法中,或者作为从零地址转移的情况发生。
  • burn: 移除一定数量的代币。这可能发生在 burn 方法中,或者作为转移到零地址的情况发生。

方法

balanceOf

owner 拥有的代币 id 的总 amount

- name: balanceOf
  type: function
  stateMutability: view

  inputs:
    - name: owner
      type: address
    - name: id
      type: uint256

  outputs:
    - name: amount
      type: uint256

allowance

spender 被允许代表 owner 转移的代币 id 的总 amount

- name: allowance
  type: function
  stateMutability: view

  inputs:
    - name: owner
      type: address
    - name: spender
      type: address
    - name: id
      type: uint256

  outputs:
    - name: amount
      type: uint256

isOperator

如果 spender 被批准为 owner 的操作员,则返回 true

- name: isOperator
  type: function
  stateMutability: view

  inputs:
    - name: owner
      type: address
    - name: spender
      type: address

  outputs:
    - name: status
      type: bool

transfer

amount 的代币 id 从调用者转移到 receiver

当调用者的代币 id 余额不足时,必须恢复。

必须记录 Transfer 事件。

必须返回 True。

- name: transfer
  type: function
  stateMutability: nonpayable

  inputs:
    - name: receiver
      type: address
    - name: id
      type: uint256
    - name: amount
      type: uint256

  outputs:
    - name: success
      type: bool

transferFrom

通过调用者,将 amount 的代币 idsender 转移到 receiver

当调用者既不是 sender 也不是 sender 的操作员,并且调用者对 sender 的代币 idallowance 不足时,必须恢复。

sender 的代币 id 余额不足时,必须恢复。

必须记录 Transfer 事件。

如果调用者不是 sender 的操作员且调用者的 allowance 不是无限的,则必须将调用者的 allowance 减少与 sender 的余额减少的相同 amount

如果 allowance 是无限的,则不应减少调用者对 sender 的代币 idallowance

如果调用者是操作员或 sender,则不应减少调用者对 sender 的代币 idallowance

必须返回 True。

- name: transferFrom
  type: function
  stateMutability: nonpayable

  inputs:
    - name: sender
      type: address
    - name: receiver
      type: address
    - name: id
      type: uint256
    - name: amount
      type: uint256

  outputs:
    - name: success
      type: bool

approve

批准 spender 被允许代表调用者转移的代币 idamount

必须将调用者的代币 idspenderallowance 设置为 amount

必须记录 Approval 事件。

必须返回 True。

- name: approve
  type: function
  stateMutability: nonpayable

  inputs:
    - name: spender
      type: address
    - name: id
      type: uint256
    - name: amount
      type: uint256

  outputs:
    - name: success
      type: bool

setOperator

代表调用者,授予或撤销 spender 对任何代币 id 的无限转移权限。

必须将操作员状态设置为 approved 值。

必须记录 OperatorSet 事件。

必须返回 True。

- name: setOperator
  type: function
  stateMutability: nonpayable

  inputs:
    - name: spender
      type: address
    - name: approved
      type: bool

  outputs:
    - name: success
      type: bool

事件

Transfer

caller 启动从 senderreceiver 的代币 idamount 的转移。

当代币 idamount 从一个账户转移到另一个账户时,必须记录。

amount 的代币 id 被铸造时,必须使用零地址作为 sender 地址记录。

amount 的代币 id 被销毁时,必须使用零地址作为 receiver 地址记录。

- name: Transfer
  type: event

  inputs:
    - name: caller
      indexed: false
      type: address
    - name: sender
      indexed: true
      type: address
    - name: receiver
      indexed: true
      type: address
    - name: id
      indexed: true
      type: uint256
    - name: amount
      indexed: false
      type: uint256

OperatorSet

owner 已将 approved 状态设置为 spender

设置操作员状态时必须记录。

当操作员状态设置为与当前调用之前的状态相同时,可以记录。

- name: OperatorSet
  type: event

  inputs:
    - name: owner
      indexed: true
      type: address
    - name: spender
      indexed: true
      type: address
    - name: approved
      indexed: false
      type: bool

Approval

owner 已批准 spender 转移一定 amount 的代币 id,以代表所有者转移。

allowanceowner 设置时必须记录。

- name: Approval
  type: event

  inputs:
    - name: owner
      indexed: true
      type: address
    - name: spender
      indexed: true
      type: address
    - name: id
      indexed: true
      type: uint256
    - name: amount
      indexed: false
      type: uint256

接口 ID

接口 ID 为 0x0f632fb3

元数据扩展

方法

name

代币 idname

- name: name
  type: function
  stateMutability: view

  inputs:
    - name: id
      type: uint256

  outputs:
    - name: name
      type: string
symbol

代币 id 的代码 symbol

- name: symbol
  type: function
  stateMutability: view

  inputs:
    - name: id
      type: uint256

  outputs:
    - name: symbol
      type: string
decimals

代币 id 的小数 amount

- name: decimals
  type: function
  stateMutability: view

  inputs:
    - name: id
      type: uint256

  outputs:
  - name: amount
    type: uint8

内容 URI 扩展

方法

contractURI

合约的 URI

- name: contractURI
  type: function
  stateMutability: view

  inputs: []

  outputs:
    - name: uri
      type: string
tokenURI

代币 idURI

如果代币 id 不存在,MAY 恢复。

客户端必须替换返回的 URI 字符串中出现的 {id}

- name: tokenURI
  type: function
  stateMutability: view

  inputs:
    - name: id
      type: uint256

  outputs:
    - name: uri
      type: string

元数据结构

合约 URI

JSON Schema:

{
  "title": "Contract Metadata",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "The name of the contract."
    },
    "description": {
      "type": "string",
      "description": "The description of the contract."
    },
    "image_url": {
      "type": "string",
      "format": "uri",
      "description": "The URL of the image representing the contract."
    },
    "banner_image_url": {
      "type": "string",
      "format": "uri",
      "description": "The URL of the banner image of the contract."
    },
    "external_link": {
      "type": "string",
      "format": "uri",
      "description": "The external link of the contract."
    },
    "editors": {
      "type": "array",
      "items": {
        "type": "string",
        "description": "An Ethereum address representing an authorized editor of the contract."
      },
      "description": "An array of Ethereum addresses representing editors (authorized editors) of the contract."
    },
    "animation_url": {
      "type": "string",
      "description": "An animation URL for the contract."
    }
  },
  "required": ["name"]
}

JSON 示例 (Minimal):

{
  "name": "示例合约名称",
}
代币 URI

客户端必须替换返回的 URI 字符串中出现的 {id}

JSON Schema:

{
  "title": "Asset Metadata",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Identifies the token"
    },
    "description": {
      "type": "string",
      "description": "Describes the token"
    },
    "image": {
      "type": "string",
      "description": "A URI pointing to an image resource."
    },
    "animation_url": {
      "type": "string",
      "description": "An animation URL for the token."
    }
  },
  "required": ["name", "description", "image"]
}

JSON 示例 (Minimal):

{
  "name": "示例代币名称",
  "description": "示例代币描述",
  "image": "exampleurl/{id}"
}

代币供应扩展

方法

totalSupply

代币 idtotalSupply

- name: totalSupply
  type: function
  stateMutability: view

  inputs:
    - name: id
      type: uint256

  outputs:
    - name: supply
      type: uint256

理由

细粒度批准

虽然 ERC-1155 标准中的“操作员模型”允许账户将另一个账户设置为操作员,从而授予完全权限以代表所有者转移任何数量的任何代币 ID,但这可能并不总是所需的权限方案。ERC-20 中的“额度模型”允许账户设置另一个账户可以代表所有者花费的代币的明确数量。本标准要求两者都实现,唯一的修改是“额度模型”,其中还必须指定代币 ID。这允许账户向特定代币 ID 授予特定批准,向特定代币 ID 授予无限批准,或向所有代币 ID 授予无限批准。

移除批处理

虽然批处理操作很有用,但不应将其置于标准本身中,而应根据具体情况进行处理。这允许在 calldata 布局方面做出不同的权衡,这对于某些特定应用(例如将 calldata 提交到全局存储的 roll-up)可能特别有用。

移除所需的回调

要求回调不必要地增加了实施者的负担,他们要么没有回调的特定用例,要么更喜欢定制的回调机制。最大限度地减少此类要求可以节省合约大小、gas 效率和复杂性。

移除“安全”命名

safeTransfersafeTransferFrom 命名约定具有误导性,尤其是在 ERC-1155 和 ERC-721 标准的上下文中,因为它们需要对具有代码的接收者账户进行外部调用,将执行流程传递给任意合约,前提是接收者合约返回特定值。通过默认删除强制性回调并从所有方法名称中删除“safe”一词,可以提高控制流程的安全性。

向后兼容性

这与 ERC-1155 不向后兼容,因为某些方法已被删除。但是,可以为 ERC-20、ERC-721 和 ERC-1155 标准实现包装器。

参考实现

// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.19;

/// @title ERC6909 多代币参考实现
/// @author jtriley.eth
contract ERC6909 {
    /// @dev 当 id 的所有者余额不足时抛出。
    /// @param owner 所有者的地址。
    /// @param id 代币的 id。
    error InsufficientBalance(address owner, uint256 id);

    /// @dev 当 id 的消费者的授权不足时抛出。
    /// @param spender 消费者的地址。
    /// @param id 代币的 id。
    error InsufficientPermission(address spender, uint256 id);

    /// @notice 发生转移时发出的事件。
    /// @param sender 发送者的地址。
    /// @param receiver 接收者的地址。
    /// @param id 代币的 id。
    /// @param amount 代币的数量。
    event Transfer(address caller, address indexed sender, address indexed receiver, uint256 indexed id, uint256 amount);

    /// @notice 设置操作员时发出的事件。
    /// @param owner 所有者的地址。
    /// @param spender 消费者的地址。
    /// @param approved 批准状态。
    event OperatorSet(address indexed owner, address indexed spender, bool approved);

    /// @notice 发生批准时发出的事件。
    /// @param owner 所有者的地址。
    /// @param spender 消费者的地址。
    /// @param id 代币的 id。
    /// @param amount 代币的数量。
    event Approval(address indexed owner, address indexed spender, uint256 indexed id, uint256 amount);

    /// @notice 所有者对 id 的余额。
    mapping(address owner => mapping(uint256 id => uint256 amount)) public balanceOf;

    /// @notice 消费者对 id 的授权。
    mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) public allowance;

    /// @notice 检查消费者是否被所有者批准为操作员。
    mapping(address owner => mapping(address spender => bool)) public isOperator;

    /// @notice 将一定数量的 id 从调用者转移到接收者。
    /// @param receiver 接收者的地址。
    /// @param id 代币的 id。
    /// @param amount 代币的数量。
    function transfer(address receiver, uint256 id, uint256 amount) public returns (bool) {
        if (balanceOf[msg.sender][id] < amount) revert InsufficientBalance(msg.sender, id);
        balanceOf[msg.sender][id] -= amount;
        balanceOf[receiver][id] += amount;
        emit Transfer(msg.sender, msg.sender, receiver, id, amount);
        return true;
    }

    /// @notice 将一定数量的 id 从发送者转移到接收者。
    /// @param sender 发送者的地址。
    /// @param receiver 接收者的地址。
    /// @param id 代币的 id。
    /// @param amount 代币的数量。
    function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public returns (bool) {
        if (sender != msg.sender && !isOperator[sender][msg.sender]) {
            uint256 senderAllowance = allowance[sender][msg.sender][id];
            if (senderAllowance < amount) revert InsufficientPermission(msg.sender, id);
            if (senderAllowance != type(uint256).max) {
                allowance[sender][msg.sender][id] = senderAllowance - amount;
            }
        }
        if (balanceOf[sender][id] < amount) revert InsufficientBalance(sender, id);
        balanceOf[sender][id] -= amount;
        balanceOf[receiver][id] += amount;
        emit Transfer(msg.sender, sender, receiver, id, amount);
        return true;
    }

    /// @notice 批准一定数量的 id 给消费者。
    /// @param spender 消费者的地址。
    /// @param id 代币的 id。
    /// @param amount 代币的数量。
    function approve(address spender, uint256 id, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender][id] = amount;
        emit Approval(msg.sender, spender, id, amount);
        return true;
    }


    /// @notice 设置或移除调用者的消费者的操作员身份。
    /// @param spender 消费者的地址。
    /// @param approved 批准状态。
    function setOperator(address spender, bool approved) public returns (bool) {
        isOperator[msg.sender][spender] = approved;
        emit OperatorSet(msg.sender, spender, approved);
        return true;
    }

    /// @notice 检查合约是否实现接口。
    /// @param interfaceId 接口标识符,如 ERC-165 中指定。
    /// @return supported 如果合约实现了 `interfaceId`,则为 True。
    function supportsInterface(bytes4 interfaceId) public pure returns (bool supported) {
        return interfaceId == 0x0f632fb3 || interfaceId == 0x01ffc9a7;
    }

    function _mint(address receiver, uint256 id, uint256 amount) internal {
      // 警告:重要的安全检查应先于对此方法的调用。
      balanceOf[receiver][id] += amount;
      emit Transfer(msg.sender, address(0), receiver, id, amount);
    }

    function _burn(address sender, uint256 id, uint256 amount) internal {
      // 警告:重要的安全检查应先于对此方法的调用。
      balanceOf[sender][id] -= amount;
      emit Transfer(msg.sender, sender, address(0), id, amount);
    }
}

安全考虑

批准和操作员

该规范包括两个代币转移权限系统,“allowance”和“operator”模型。在委托转移权限方面,有两个安全考虑因素。

第一个考虑因素与所有委托权限模型一致。任何具有 allowance 的账户都可以随时出于任何原因转移全部 allowance,直到 allowance 被撤销。任何具有操作员权限的账户都可以代表所有者转移任何数量的任何代币 ID,直到操作员权限被撤销。

第二个考虑因素是具有两个委托权限模型的系统所独有的。如果一个账户同时具有操作员权限和给定转移的不足 allowance,则在操作员检查之前执行 allowance 检查会导致恢复,而在 allowance 检查之前执行操作员检查则不会。该规范有意地将此保持不受约束,以便实施者可能跟踪 allowance,即使存在操作员状态。尽管如此,这是一个值得注意的考虑因素。

contract ERC6909OperatorPrecedence {
  // -- snip --

  function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public {
    // 首先检查 `isOperator`
    if (msg.sender != sender && !isOperator[sender][msg.sender]) {
      require(allowance[sender][msg.sender][id] >= amount, "insufficient allowance");
      allowance[sender][msg.sender][id] -= amount;
    }
  
    // -- snip --
  }
}

contract ERC6909AllowancePrecedence {
  // -- snip --

  function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public {
    // 首先检查 allowance 是否足够
    if (msg.sender != sender && allowance[sender][msg.sender][id] < amount) {
      require(isOperator[sender][msg.sender], "insufficient allowance");
    }

    // 错误:当 allowance 不足时,无论调用者是否具有操作员权限,这都会因算术下溢而崩溃。
    allowance[sender][msg.sender][id] -= amount;

    // -- snip
  }
}

版权

版权及相关权利已通过 CC0 放弃。

Citation

Please cite this document as:

JT Riley (@jtriley2p), Dillon (@d1ll0n), Sara (@snreynolds), Vectorized (@Vectorized), Neodaoist (@neodaoist), "ERC-6909: 最小化多代币接口 [DRAFT]," Ethereum Improvement Proposals, no. 6909, April 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6909.