ERC-6909 最小多代币标准

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-6909 和 ERC-1155 标准的对比

ERC-6909 移除转账的回调要求

ERC-1155 规范要求 safeTransferFromsafeBatchTransferFrom 检查接收账户是否为合约。如果是,则必须在接收合约账户上调用 ERC1155TokenReceiver 接口函数(onERC1155ReceivedonERC1155BatchReceived)以检查其是否接受转账。

这些回调在某些情况下是有用的。然而,对于希望不使用这种行为的接收方,它们是不必要的外部调用。回调影响接收合约账户的Gas成本和代码大小,因为它们需要实现多个回调(即,通过 onERC1155ReceivedonERC1155BatchReceived)并返回魔术的 4 字节值以接收Token。相比之下,ERC-6909 的实现者可以自定义他们的回调架构。

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-6909 同时支持全局批准和细粒度限额

// 在 ERC-1155 →
function setApprovalForAll(
    address _operator,
    bool _approved
) external;

上面展示的 setApprovalForAll 函数是 ERC-1155 中的全局操作员模型,允许一个账户授权另一个账户管理(作为操作员)其所有Token ID 的操作。一旦被授权,操作员可以随意转移授权账户拥有的任何数量的任何Token ID。

虽然这种方法简化了委托,但缺乏细粒度控制:

  • 没有办法授予特定于单个Token ID 或数量的权限。
  • 这种全有或全无的方法不适合需要受控权限的场景。

为引入细粒度控制,ERC-6909 混合操作员权限方案包含以下内容:

  • 来自 ERC-1155 的操作员模型,
  • 和受 ERC-20 启发的限额模型。

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;
}

ERC-6909 中的限额模型

限额模型引入了一种特定于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:ID 的所有者余额

balanceOf 映射跟踪特定Token ID 由地址 (owner) 持有的余额。映射中的 owner => (id => amount) 结构表示单个所有者可以持有多个Token,并通过各自的 ID 跟踪其余额。

mapping(address owner => mapping(uint256 id => uint256 amount)) public balanceOf;

allowance:ID 的支出者限额

允许映射定义支出者在所有者的授权下可以转移多少特定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)的数量。

isOperator:操作员批准状态

mapping(address owner => mapping(address operator => bool isOperator)) public isOperator;

该映射跟踪支出者是否被批准作为拥有地址的所有Token的操作员。例如,isOperator[0x123...][0xABC...] 返回 true 如果地址 0xABC... 被允许支出地址 0x123... 所拥有的Token;否则返回 false

核心 ERC-6909 功能及其数据参数

转账函数

该规范并未遵循 ERC-721 和 ERC-1155 中的“安全转账机制”,因为由于其向任意合约的外部调用被认为是误导性的。ERC-6909 使用 transfertransferFrom 函数,详情如下。

转账:

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] &lt; 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;
    }

transferFrom:

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 idallowance 中的 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 &lt; amount) revert InsufficientPermission();
        if (senderAllowance != type(uint256).max) {
            allowance[sender][msg.sender][id] = senderAllowance - amount;
        }
    }

    if (balanceOf[sender][id] &lt; amount) revert InsufficientBalance();

    balanceOf[sender][id] -= amount;
    balanceOf[receiver][id] += amount;
    emit Transfer(msg.sender, sender, receiver, id, amount);
    return true;
}

approve:

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:

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 中的事件与日志

ERC-6909 定义了关键事件,以跟踪多Token合约中的Token转移、批准及操作员权限。

1. 转移事件:

/// @notice 转移发生时发出的事件。

event Transfer(address caller, address indexed sender, address indexed receiver, uint256 indexed id, uint256 amount);

ERC-6909 中的 Transfer 事件用于跟踪Token的移动,必须在以下条件下发出:

  • 转移Token idamount 从一个账户到另一个账户时,将记录 senderreceivertoken ID 和转移的 amount
  • 当创建新Token时,事件必须以 sender 为零地址(0x0)发出。
  • 当Token被销毁时,事件必须以接收方为零地址(0x0)发出,以表示Token被移除。

2. OperatorSet 事件:

/// @notice 操作员被设置时发出的事件。
event OperatorSet(address indexed owner, address indexed spender, bool approved);

OperatorSet 事件每当所有者分配或撤销另一个地址的操作员权限时都会发出。事件记录所有者的地址、支出者的地址以及更新的批准状态(true 表示授予,false 表示撤销)。

3. Approval 事件:

/// @notice 批准发生时发出的事件。
event Approval(address indexed owner, address indexed spender, uint256 indexed id, uint256 amount);

当所有者设置或更新支出者转移特定金额的特定Token ID 的批准时,必须发出 Approval 事件。事件记录 ownerspender、Token id 和批准的 amount

现在我们已经探讨了 ERC-6909 与 ERC-1155 之间的差异,以及 ERC-6909 中的核心方法和事件,让我们看看标准的一些实际应用。

Uniswap v4 PoolManager 怎样实现 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 代币使用。

ERC-6909 元数据在单例 DeFi 架构和 NFT 系列中的考虑

以下是 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-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 &lt;&lt; 128) + itemId;
}

下面的 ERC6909MultiCollectionNFT 合约是一个使用 getTokenId 根据 collectionIditemId 生成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 &lt;&lt; 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 地址确实铸造了 tokenIdnftBalanceOf 函数预计总是返回 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 内容 URI 扩展与元数据 URI JSON 模式

为了标准化 ERC-6909 中的元数据访问,可选的 IERC6909ContentURI 接口为检索合约和Token层次的元数据定义了两个 URI 函数(contractURItokenURI)。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 函数,即 contractURItokenURI;每个函数都有其相应的 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 "&lt;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"]
}

ERC-6909 规范中的限额和操作员歧义。

考虑一个场景,其中一个账户(A)授予另一个账户(B)操作员权限,并为 B 设置可转移特定金额的Token的限额。

如果 B 代表 A 发起转移,则实现必须确定检查的正确顺序以及限额如何与操作员权限互动。

歧义涉及检查的顺序。合约应该:

  1. 首先检查限额,如果不足则回滚,即使 B 具有操作员权限。
  2. 首先检查操作员权限,无论限额如何都允许转移。

在下面的 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] &lt; amount) {
        require(isOperator[sender][msg.sender], "insufficient allowance");
    }

    // 错误:当限额不足时,由于算术下溢而发生恐慌,无论调用者是否具有操作员权限。
    allowance[sender][msg.sender][id] -= amount;

    // -- 剪切 --
 }
}

该标准故意将权限检查的决定留给实现者,这给实现者提供灵活性。在一个账户同时拥有操作员权限和不足的限额时,转账行为取决于检查的顺序。

结论

ERC-6909 标准通过移除转账函数中的批量和强制回调,显著提高了 ERC-1155 的效率。去除批量处理允许逐个案例优化,特别是对于汇总或气体敏感的环境。

它还通过混合操作员权限方案引入了可扩展的Token批准控制。

感谢

我们要感谢 vectorizedjtrileyneodaoist(ERC-6909 的共同作者)审核本文。

  • 原文链接: rareskills.io/post/erc-6...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/