ERC-6909是ERC-1155标准的简化替代方案,旨在提高多代币合约的效率,其主要特点包括取消强制回调和批量转移逻辑,并引入混合权限管理模型,使得开发者能够更灵活地管理代币。此外,还提出了ERC-6909在DeFi应用中的实际使用,以及NFT系列的元数据处理。
ERC-6909 Token标准是 ERC-1155 Token标准的一种简化替代方案。
ERC-1155 标准引入了一种多Token接口,使得单个智能合约能够结合可替代的和不可替代的Token(即,ERC20 和 ERC721)。
ERC-1155 解决了多个挑战,例如降低部署成本、最小化以太坊区块链上的冗余字节码,以及简化多Token交易的Token批准流程。
然而,由于每次转账都强制要求回调、强制包含批量转账,以及缺乏对单操作员批准方案的细粒度控制,它引入了一些膨胀和气体低效问题。ERC-6909 通过消除合约级回调和批量转账,并用混合(限额-操作员)权限方案替代单操作员的凭证方案,从而解决了这些缺点,以实现颗粒化的Token管理。
注意: 以下部分假设读者对 ERC-1155 标准及其概念有一定的了解。如果你不熟悉,请在继续之前阅读相关内容。
ERC-1155 规范要求 safeTransferFrom
和 safeBatchTransferFrom
检查接收账户是否为合约。如果是,则必须在接收合约账户上调用 ERC1155TokenReceiver 接口函数(onERC1155Received
,onERC1155BatchReceived
)以检查其是否接受转账。
这些回调在某些情况下是有用的。然而,对于希望不使用这种行为的接收方,它们是不必要的外部调用。回调影响接收合约账户的Gas成本和代码大小,因为它们需要实现多个回调(即,通过 onERC1155Received
,onERC1155BatchReceived
)并返回魔术的 4 字节值以接收Token。相比之下,ERC-6909 的实现者可以自定义他们的回调架构。
尽管批量转账有时是有益的,但在 ERC-6909 标准中故意省略,以允许开发者根据特定执行环境实施批量转账逻辑。开发者可以根据自己的需要实现批量转账,而无需仅仅为了遵循标准而添加额外的批量转账函数。
下面展示的 safeBatchTransferFrom
函数在 ERC-1155 标准中执行批量转账。然而,其强制的包含为不需要它们的应用程序增加了膨胀:
// ERC-1155
function safeBatchTransferFrom(
address _from,
address _to,
uint256[] calldata _ids,
uint256[] calldata _values,
bytes calldata _data
) external;
以下是 ERC-6909 transferFrom
函数。我们可以看到批量特性和 _data
参数已被删除。
// ERC-6909
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();
if (senderAllowance != type(uint256).max) {
allowance[sender][msg.sender][id] = senderAllowance - amount;
}
}
if (balanceOf[sender][id] < amount) revert InsufficientBalance();
balanceOf[sender][id] -= amount;
balanceOf[receiver][id] += amount;
emit Transfer(msg.sender, sender, receiver, id, amount);
return true;
}
// 在 ERC-1155 →
function setApprovalForAll(
address _operator,
bool _approved
) external;
上面展示的 setApprovalForAll
函数是 ERC-1155 中的全局操作员模型,允许一个账户授权另一个账户管理(作为操作员)其所有Token ID 的操作。一旦被授权,操作员可以随意转移授权账户拥有的任何数量的任何Token ID。
虽然这种方法简化了委托,但缺乏细粒度控制:
为引入细粒度控制,ERC-6909 混合操作员权限方案包含以下内容:
在下面的 ERC-6909 setOperator
函数中,spender
变量被设置为操作员,并被授权无条件的权限,以转移账户所拥有的所有Token ID 而没有限额限制。
function setOperator(address spender, bool approved) public returns (bool) {
isOperator[msg.sender][spender] = approved;
emit OperatorSet(msg.sender, spender, approved);
return true;
}
限额模型引入了一种特定于Token和数量的控制系统,其中一个账户可以为特定Token ID 设置有限的限额。
例如,Alice 可以允许 Bob 转移 100 个 ID 为 42 的Token,而不授予对其他Token ID 或不受限制的数量的访问权限,使用下一个展示的 ERC-6909 中的 approve
函数。
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;
}
在 approve
中的 spender
变量是被授权代表Token所有者转移特定金额的特定Token ID 的账户。
例如,Token所有者可以允许 spender
转移 <= 100 个特定Token ID。或者,他们还可以通过将限额设置为 type(uint256).max
来为特定Token ID 授予无限制的批准。
ERC-6909 并没有指定是否应扣减设置为 type(uint256).max
的限额。相反,这种行为留给实现者的自由裁量权,类似于 ERC-20。
ERC-6909 实现使用三个映射来更新账户余额和批准状态。
balanceOf
映射跟踪特定Token ID 由地址 (owner
) 持有的余额。映射中的 owner => (id => amount)
结构表示单个所有者可以持有多个Token,并通过各自的 ID 跟踪其余额。
mapping(address owner => mapping(uint256 id => uint256 amount)) public balanceOf;
允许映射定义支出者在所有者的授权下可以转移多少特定Token(ID)。它促进了对Token支出的细粒度控制。
mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) public allowance;
例如,allowance[0xDEF...][0x123...][5]
将返回所有者 0xDEF...
允许支出者 0x123...
转移的Token(ID 为 5)的数量。
mapping(address owner => mapping(address operator => bool isOperator)) public isOperator;
该映射跟踪支出者是否被批准作为拥有地址的所有Token的操作员。例如,isOperator[0x123...][0xABC...]
返回 true
如果地址 0xABC...
被允许支出地址 0x123...
所拥有的Token;否则返回 false
。
该规范并未遵循 ERC-721 和 ERC-1155 中的“安全转账机制”,因为由于其向任意合约的外部调用被认为是误导性的。ERC-6909 使用 transfer
和 transferFrom
函数,详情如下。
ERC-6909 的 transfer
函数表现得与 ERC-20 的 transfer
相同,只不过它适用于特定的Token ID。该函数接受接收地址、Token的 ID 和要转移的金额作为输入参数,并使用 balanceOf
映射更新余额。与 ERC-20 转账函数类似,成功执行事务时返回 true 是必要的。
// ERC-20 接口转账函数
function transfer(address _to, uint256 _value) public returns (bool)
// ERC-6909 转账函数参考实现
// @notice 将数量 id 从调用者转移至接收者。
// @param receiver 接收者的地址。
// @param id Token的 ID。
// @param amount Token的数量。
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;
}
ERC-6909 的 transferFrom 函数与 ERC-20 的不同之处在于它要求提供一个Token ID。此外,它还检查操作员的批准以及限额。
该函数首先检查 if (sender != msg.sender && !isOperator[sender][msg.sender])
,确保调用者 (msg.sender
) 是:
sender
),或者isOperator[sender][msg.sender] == true
)。如果 msg.sender
不是所有者也不是已批准的操作员,函数将检查调用者是否具有 足够的限额 以进行转移。如果有限额但未设置为无限制(type(uint256).max
),则从限额中扣除转移的 amount
。
此外,标准规定如果调用者是操作员或 sender
,则该函数不应扣减调用者对于Token id
的 allowance
中的 amount
。
// ERC-6909 transferFrom
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();
if (senderAllowance != type(uint256).max) {
allowance[sender][msg.sender][id] = senderAllowance - amount;
}
}
if (balanceOf[sender][id] < amount) revert InsufficientBalance();
balanceOf[sender][id] -= amount;
balanceOf[receiver][id] += amount;
emit Transfer(msg.sender, sender, receiver, id, amount);
return true;
}
approve
函数允许调用者(msg.sender
)向支出者授予特定Token(ID)的特定限额。这会更新限额映射以反映新的限额并发出 Approval
事件。
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;
}
setOperator
函数允许调用者(msg.sender
)通过将批准参数设置为 true 或 false 来授予或撤销特定地址(spender
)的操作员权限。该函数会相应地更新 isOperator
映射,并发出 OperatorSet
事件,以通知外部监听器有关更改的情况。
function setOperator(address spender, bool approved) public returns (bool) {
isOperator[msg.sender][spender] = approved;
emit OperatorSet(msg.sender, spender, approved);
return true;
}
ERC-6909 定义了关键事件,以跟踪多Token合约中的Token转移、批准及操作员权限。
/// @notice 转移发生时发出的事件。
event Transfer(address caller, address indexed sender, address indexed receiver, uint256 indexed id, uint256 amount);
ERC-6909 中的 Transfer
事件用于跟踪Token的移动,必须在以下条件下发出:
id
的 amount
从一个账户到另一个账户时,将记录 sender
、receiver
、token ID
和转移的 amount
。sender
为零地址(0x0
)发出。0x0
)发出,以表示Token被移除。/// @notice 操作员被设置时发出的事件。
event OperatorSet(address indexed owner, address indexed spender, bool approved);
OperatorSet
事件每当所有者分配或撤销另一个地址的操作员权限时都会发出。事件记录所有者的地址、支出者的地址以及更新的批准状态(true
表示授予,false
表示撤销)。
/// @notice 批准发生时发出的事件。
event Approval(address indexed owner, address indexed spender, uint256 indexed id, uint256 amount);
当所有者设置或更新支出者转移特定金额的特定Token ID 的批准时,必须发出 Approval
事件。事件记录 owner
、spender
、Token id
和批准的 amount
。
现在我们已经探讨了 ERC-6909 与 ERC-1155 之间的差异,以及 ERC-6909 中的核心方法和事件,让我们看看标准的一些实际应用。
在 Uniswap v3 中,工厂/池模型通过使用 UniswapV3Factory 合约为每个池部署一个单独的合约来创建新的Token对。这种方法增加了Gas成本,因为每个新池都需要新的合约部署。
与此相比,Uniswap v4 引入了一个单例合约(PoolManager.sol
),该合约将所有流动性池管理作为其内部状态的一部分,而不是要求单独的合约部署。这一设计显著降低了池创建的Gas成本。
此外,以前版本中涉及 多个 Uniswap 池 的交易需要跨多个合约的Token转移和冗余状态更新。在 Uniswap v4 中,PoolManager
合约可以集中持有用户的 ERC-6909 表示的 ERC-20 Token,而不需要在池中往返转移 ERC-20 Token。
例如,如果用户为Token A 提供流动性,他们后来可以选择提取其股份并接收Token A 作为转移到其钱包的 ERC-20。然而,如果他们选择不提取Token,那么 Uniswap v4 的 PoolManager
可以 铸造其Token余额的 ERC-6909 表示,而无需从合约转移 ERC-20 Token——节省了跨合约调用。这些 ERC-6909 余额允许用户在协议中交易或互动,而无需在钱包之间移动Token。
这意味着当用户后来将Token A 兑换为Token B 时,Uniswap 只是更新他们在池中的 ERC-6909 余额。
注意:ERC-6909 在 Uniswap v4 中不作为 LP 代币使用。
以下是 IERC6909Metadata
接口,它定义了如何将 ERC-6909 标准和单独Token相关联的元数据,例如名称、符号和小数,其函数可以根据 id 的不同发展,允许 ERC-6909 中不同的Token具有不同的名称、符号和小数。
/// @notice 包含单个Token元数据的合同。
interface IERC6909Metadata is IERC6909 {
/// @notice 给定Token的名称。
/// @param id Token的 id。
/// @return name Token的名字。
function name(uint256 id) external view returns (string memory);
/// @notice 给定Token的 символ。
/// @param id Token的 id。
/// @return symbol Token的符号。
function symbol(uint256 id) external view returns (string memory);
/// @notice 给定Token的小数位数。
/// @param id Token的 id。
/// @return decimals Token的小数位数。
function decimals(uint256 id) external view returns (uint8);
}
对于 DeFi 协议,我们可能有多个 LP 代币,并且我们可能希望将其标准化为都具有相同的小数,例如 18。然而,我们可能希望名称和符号能够反映池中持有的不同资产。
相比之下,对于 NFT,decimals
值应始终设置为 1,因为 NFT 是不可分割的。
在典型的 NFT 系列(例如 ERC-721)中,所有Token共享相同的名字和符号,以表示整个系列(例如 "CryptoPunks" 和符号 "PUNK")。ERC-6909 使我们能够遵循 ERC-712 规范,其中所有 NFT 在同一系列中共享相同的元数据。
ERC-6909 规范并没有明确规定支持非同质化Token的独特方法。但是,可以使用 ERC-1155 规范中描述的 ID 位拆分技术在 ERC-6909 中实现非同质化Token。这种方法使用 位移和加法运算 将集合 ID 和项目 ID 编码在一个 uint256
Token ID 中。
function getTokenId(uint256 collectionId, uint256 itemId) public pure returns (uint256) {
return (collectionId << 128) + itemId;
}
下面的 ERC6909MultiCollectionNFT
合约是一个使用 getTokenId
根据 collectionId
和 itemId
生成Token ID 的非同质化Token(NFT)实现示例。
mintNFT
函数确保每个 tokenId
只能被铸造一次,无论地址如何。它使用 mintedTokens
映射跟踪 NFT tokenId
是否已被全球铸造。
由于在 mintNFT
中将 amount
变量设置为 1,因此函数中的 _mint(to, tokenId, amount)
调用将对 tokenId
铸造一份。在任何情况下,如果 amount
> 1,Token将变为可替代,而不是非同质化。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./ERC6909.sol";
contract ERC6909MultiCollectionNFT is ERC6909 {
struct NFT {
string uri;
}
mapping(uint256 => NFT) private _tokens;
mapping(uint256 => string) private _collectionURIs;
mapping(uint256 => bool) public mintedTokens;
event MintedNFT(address indexed to, uint256 indexed collectionId, uint256 indexed itemId, uint256 tokenId, string uri);
// 通过连接 collectionId 和 itemId 计算 Token ID
function getTokenId(uint256 collectionId, uint256 itemId) public pure returns (uint256) {
return (collectionId << 128) + itemId;
}
function _mint(address to, uint256 tokenId, uint256 amount) internal {
balanceOf[to][tokenId] += amount;
emit Transfer(msg.sender, address(0), to, tokenId, amount);
}
function mintNFT(address to, uint256 collectionId, uint256 itemId, string memory uri) external {
uint256 amount = 1;
uint256 tokenId = getTokenId(collectionId, itemId);
require(!mintedTokens[tokenId], "ERC6909MultiCollectionNFT: Token already minted");
require(amount == 1, "ERC6909MultiCollectionNFT: Token copies must be 1");
_tokens[tokenId] = NFT(uri);
mintedTokens[tokenId] = true; // 标记为已铸造
_mint(to, tokenId, amount); // amount 被定义为 1。
emit MintedNFT(to, collectionId, itemId, tokenId, uri);
}
function nftBalanceOf(address owner, uint256 tokenId) public view returns (uint256) {
return balanceOf[owner][tokenId];
}
}
请记住,上面 mintNFT
中的 _mint
调用将余额映射更新为 1,因为这些铸造的Token完全是非同质化的。因此,在此合约中,如果 owner
地址确实铸造了 tokenId
,nftBalanceOf
函数预计总是返回 1。
为了转移Token的所有权,下面的 nftTransfer
函数确保只有 NFT 所有者可以通过验证其余额启动转移,才能允许唯一存在的单位转移。
function nftTransfer(address to, uint256 tokenId) external {
require(balanceOf[tokenId][msg.sender] == 1, "ERC6909MultiCollectionNFT: This should be non-fungible.");
require(to != address(0), "ERC6909MultiCollectionNFT: transfer to zero address");
transfer(to, tokenId, 1);
// 在此情况下,数量等于 1。
emit Transfer(msg.sender, address(0), to, tokenId, 1);
}
为了标准化 ERC-6909 中的元数据访问,可选的 IERC6909ContentURI
接口为检索合约和Token层次的元数据定义了两个 URI 函数(contractURI
和 tokenURI
)。ERC-6909 标准并不强制Token需要关联 URI 元数据。然而,如果实现中包含这些 URI 函数,返回的 URI 应该指向遵循 ERC-6909 元数据 URI JSON 架构的 JSON 文件。
pragma solidity ^0.8.19;
import "./IERC6909.sol";
/// @title ERC6909 内容 URI 接口
interface IERC6909ContentURI is IERC6909 {
/// @notice 合同级别 URI
/// @return uri 合同级别的 URI。
function contractURI() external view returns (string memory);
/// @notice Token级别 URI
/// @param id Token的 ID。
/// @return uri Token级别的 URI。
function tokenURI(uint256 id) external view returns (string memory);
}
如上所示,ERC-6909 IERC6909ContentURI
接口定义了两个可选的 URI 函数,即 contractURI
和 tokenURI
;每个函数都有其相应的 URI JSON 模式。contractURI
函数(不带参数)返回指向合约级别元数据的单个 URI,而 tokenURI()
返回每个Token ID 特定的 URI。
以下是根据 ERC-6909 标准构造合约 URI JSON 架构的示例。
{
"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"]
}
tokenURI
函数则接受一个 uint256
参数 id
,并返回该Token的 URI。如果Token id
不存在,该函数可以回滚。与合约级别的 URI 一样,客户在与合约交互时 必须 替换 URI 中每处 {id}
的出现为实际的Token ID,以访问与该Token相关的正确元数据。
以下是 tokenURI
函数的实现,返回遵循占位符格式的 静态 URI 模板:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./ERC6909.sol";
import "./interfaces/IERC6909ContentURI.sol";
contract ERC6909ContentURI is ERC6909, IERC6909ContentURI {
/// @notice 合同级 URI。
string public contractURI;
/// @notice 每个 id 的 URI。
/// @return Token的 URI。
function tokenURI(uint256) public pure override returns (string memory) {
return "<baseuri>/{id}";
}
}
以下是根据 ERC-6909 标准构造 URI JSON 架构的示例。
{
"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"]
}
考虑一个场景,其中一个账户(A)授予另一个账户(B)操作员权限,并为 B 设置可转移特定金额的Token的限额。
如果 B 代表 A 发起转移,则实现必须确定检查的正确顺序以及限额如何与操作员权限互动。
歧义涉及检查的顺序。合约应该:
在下面的 allowanceFirst
合约中,如果账户 B 具有操作员权限,但限额不足,限额检查将失败,从而导致事务回滚。这可能是反直观的,因为操作员权限通常意味着无限制访问,用户可能会期望事务成功。
相反,在 operatorFirst
合约中,如果实现首先检查操作员权限,将绕过限额检查,事务将基于操作员的无限制访问而成功。
contract operatorFirst {
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;
}
// -- 剪切 --
}
}
contract allowanceFirst{
function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public {
// 首先检查限额是否充足
if (msg.sender != sender && allowance[sender][msg.sender][id] < amount) {
require(isOperator[sender][msg.sender], "insufficient allowance");
}
// 错误:当限额不足时,由于算术下溢而发生恐慌,无论调用者是否具有操作员权限。
allowance[sender][msg.sender][id] -= amount;
// -- 剪切 --
}
}
该标准故意将权限检查的决定留给实现者,这给实现者提供灵活性。在一个账户同时拥有操作员权限和不足的限额时,转账行为取决于检查的顺序。
ERC-6909 标准通过移除转账函数中的批量和强制回调,显著提高了 ERC-1155 的效率。去除批量处理允许逐个案例优化,特别是对于汇总或气体敏感的环境。
它还通过混合操作员权限方案引入了可扩展的Token批准控制。
我们要感谢 vectorized、jtriley 和 neodaoist(ERC-6909 的共同作者)审核本文。
- 原文链接: rareskills.io/post/erc-6...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!