ERC-6909: 最小化多代币接口
在单个合约中通过 ID 管理多个代币的最小化规范。
Authors | JT Riley (@jtriley2p), Dillon (@d1ll0n), Sara (@snreynolds), Vectorized (@Vectorized), Neodaoist (@neodaoist) |
---|---|
Created | 2023-04-19 |
Requires | EIP-165 |
Table of Contents
摘要
以下内容将多代币合约指定为 ERC-1155 多代币标准的简化替代方案。与 ERC-1155 相比,回调和批处理已从接口中删除,并且权限系统是一种混合的操作员批准方案,用于实现细粒度和可扩展的权限。从功能上讲,该接口已简化为在同一合约下管理多个代币所需的最基本要求。
动机
ERC-1155 标准包含不必要的功能,例如要求具有代码的接收者账户实现返回特定值的回调,以及规范中的批量调用。此外,单一操作员权限方案授予合约中每个代币 ID 的无限额度。仅在必要时才会有意删除向后兼容性。规范中有意省略了其他功能,例如批量调用、增加和减少额度的方法以及其他用户体验改进,以最大限度地减少所需的外部接口。
根据 ERC-1155,每次转移和批量转移到合约账户都需要回调。当接收者账户是合约账户时,这可能需要对接收者进行不必要的外部调用。虽然在某些情况下可能需要此行为,但无法选择退出此行为,就像 ERC-721 同时具有 transferFrom
和 safeTransferFrom
一样。除了代币合约本身的运行时性能之外,它还会影响接收者合约账户的运行时性能和代码大小,从而需要多个回调函数和返回值来接收代币。
批量转移虽然有用,但本标准不包括批量转移,以便允许在不同的实现中进行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
的代币 id
从 sender
转移到 receiver
。
当调用者既不是 sender
也不是 sender
的操作员,并且调用者对 sender
的代币 id
的 allowance
不足时,必须恢复。
当 sender
的代币 id 余额不足时,必须恢复。
必须记录 Transfer
事件。
如果调用者不是 sender
的操作员且调用者的 allowance
不是无限的,则必须将调用者的 allowance
减少与 sender
的余额减少的相同 amount
。
如果 allowance
是无限的,则不应减少调用者对 sender
的代币 id
的 allowance
。
如果调用者是操作员或 sender
,则不应减少调用者对 sender
的代币 id
的 allowance
。
必须返回 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
被允许代表调用者转移的代币 id
的 amount
。
必须将调用者的代币 id
的 spender
的 allowance
设置为 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
启动从 sender
到 receiver
的代币 id
的 amount
的转移。
当代币 id
的 amount
从一个账户转移到另一个账户时,必须记录。
当 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
,以代表所有者转移。
allowance
由 owner
设置时必须记录。
- 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
代币 id
的 name
。
- 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
代币 id
的 URI
。
如果代币 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
代币 id
的 totalSupply
。
- 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 效率和复杂性。
移除“安全”命名
safeTransfer
和 safeTransferFrom
命名约定具有误导性,尤其是在 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.