Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-3589: 将资产组装成 NFT

Authors Zhenyu Sun (@Ungigdu), Xinqi Yang (@xinqiyang)
Created 2021-05-24
Discussion Link https://github.com/ethereum/EIPs/issues/3590
Requires EIP-721

简单概述

此标准定义了一个名为 assembly token 的 ERC-721 Token,它可以代表资产的组合。

摘要

ERC-1155 多 Token 合约定义了一种批量转移 Token 的方法,但这些 Token 必须由 ERC-1155 合约本身铸造。此 EIP 是一个 ERC-721 扩展,能够将以太币、ERC-20 Token、ERC-721 Token 和 ERC-1155 Token 等资产组装成一个 ERC-721 Token,其 Token ID 也是资产的签名。当资产被组装成一个 Token 时,可以非常容易地实现批量转移或交换。

动机

随着 NFT 艺术品和收藏家的迅速增加,一些收藏家对传统的交易方式不满意。当两个收藏家想要交换他们的一些藏品时,目前他们可以将他们的 NFT 在市场上列出,并通知对方购买,但这效率低下且 Gas 消耗大。相反,一些收藏家转向社交媒体或聊天群组,寻找值得信赖的第三方来为他们交换 NFT。第三方从收藏家 A 和 B 那里获取 NFT,并将 A 的藏品转移给 B,将 B 的藏品转移给 A。这是非常危险的。

进行批量交换最安全的方法是将批量交换转换为原子交换,即一对一交换。但首先我们应该将这些以太币、ERC-20 Token、ERC-721 Token 和 ERC-1155 Token“组装”在一起,这是本 EIP 的主要目的。

规范

本文档中的关键词“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY”和“OPTIONAL”应按照 RFC 2119 中的描述进行解释。

符合 ERC-721 标准的合约 MAY 实现此 ERC,以提供组装资产的标准方法。

mintsafeMint 将资产组装成一个 ERC-721 Token。mint SHOULD 为 _transfer 无损的普通 ERC-20 Token 实现。safeMint MUST 负责处理有损 Token,例如 _transfer 函数被征税的 PIG Token。

hash 函数的 _salt MAY 以其他方式实现,甚至可以作为用户输入提供。但 Token ID MUST 由 hash 函数生成。

该标准的实现 MAY 支持不同的资产集合。

此标准的实现者 MUST 具有以下所有功能:

pragma solidity ^0.8.0;

interface AssemblyNFTInterface {

  event AssemblyAsset(address indexed firstHolder,
                    uint256 indexed tokenId,
                    uint256 salt,
                    address[] addresses,
                    uint256[] numbers);

  /**
  * @dev hash 函数将资产与 salt 的组合分配给 bytes32 签名,该签名也是 Token ID。
  * @param `_salt` 防止哈希冲突,可以由用户输入选择或从合约增加 nonce。
  * @param `_addresses` 连接资产地址,例如 [ERC-20_address1, ERC-20_address2, ERC-721_address_1, ERC-1155_address_1, ERC-1155_address_2]
  * @param `_numbers` 描述以太币数量、ERC-20 Token 地址长度、ERC-721 Token 地址长度、ERC-1155 Token 地址长度、
  * ERC-20 Token 数量、ERC-721 Token ID 以及 ERC-1155 Token ID 和数量。
  */
  function hash(uint256 _salt, address[] memory _addresses, uint256[] memory _numbers) external pure returns (uint256 tokenId);

  /// @dev 组装无损资产
  /// @param `_to` 组装 Token 的接收者
  function mint(address _to, address[] memory _addresses, uint256[] memory _numbers) payable external returns(uint256 tokenId);

  /// @dev 使用额外的逻辑进行铸造,该逻辑计算 Token 的实际接收值。
  function safeMint(address _to, address[] memory _addresses, uint256[] memory _numbers) payable external returns(uint256 tokenId);

  /// @dev 销毁此 Token 并释放组装的资产
  /// @param `_to` 资产释放到的地址
  function burn(address _to, uint256 _tokenId, uint256 _salt, address[] calldata _addresses, uint256[] calldata _numbers) external;

}

基本原理

人们想要将他们的 NFT 打包在一起有很多原因。例如,收藏家想要将一组足球运动员打包成一支足球队;收藏家拥有数百个没有类别来管理的 NFT;收藏家想要购买完整的 NFT 系列,否则就不买。他们都需要一种将这些 NFT 组装在一起的方法。

选择 ERC-721 标准作为包装器的原因是 ERC-721 Token 已经被广泛使用,并且 NFT 钱包也提供了良好的支持。并且组装 Token 本身也可以再次组装。在批量交易、批量交换或藏品交换等场景中,组装 Token 比一批资产更容易被智能合约使用。

此标准具有 AssemblyAsset 事件,该事件记录了组装 Token 代表的资产的确切种类和数量。钱包可以通过 Token ID 轻松地向用户显示这些 NFT。

向后兼容性

此提案结合了已有的 721 扩展,并且向后兼容 ERC-721 标准。

实现

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import "./AssemblyNFTInterface.sol";

abstract contract AssemblyNFT is ERC721, ERC721Holder, ERC1155Holder, AssemblyNFTInterface{
  using SafeERC20 for IERC20;

  function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC1155Receiver) returns (bool) {
        return ERC721.supportsInterface(interfaceId) || ERC1155Receiver.supportsInterface(interfaceId);
  }

  uint256 nonce;

  /**
  * _addresses 的布局:
  *     erc20 地址 | erc721 地址 | erc1155 地址
  * _numbers 的布局:
  *     eth | erc20.length | erc721.length | erc1155.length | erc20 数量 | erc721 ID | erc1155 ID | erc1155 数量
   */

  function hash(uint256 _salt, address[] memory _addresses, uint256[] memory _numbers) public pure override returns (uint256 tokenId){
      bytes32 signature = keccak256(abi.encodePacked(_salt));
      for(uint256 i=0; i< _addresses.length; i++){
        signature = keccak256(abi.encodePacked(signature, _addresses[i]));
      }
      for(uint256 j=0; j<_numbers.length; j++){
        signature = keccak256(abi.encodePacked(signature, _numbers[j]));
      }
      assembly {
        tokenId := signature
      }
  }

  function mint(address _to, address[] memory _addresses, uint256[] memory _numbers) payable external override returns(uint256 tokenId){
      require(_to != address(0), "can't mint to address(0)");
      require(msg.value == _numbers[0], "value not match");
      require(_addresses.length == _numbers[1] + _numbers[2] + _numbers[3], "2 array length not match");
      require(_addresses.length == _numbers.length -4 - _numbers[3], "numbers length not match");
      uint256 pointerA; // 指向第一个 erc20 地址,如果有的话
      uint256 pointerB =4; // 指向第一个 erc20 数量,如果有的话
      for(uint256 i = 0; i< _numbers[1]; i++){
        require(_numbers[pointerB] > 0, "transfer erc20 0 amount");
        IERC20(_addresses[pointerA++]).safeTransferFrom(_msgSender(), address(this), _numbers[pointerB++]);
      }
      for(uint256 j = 0; j< _numbers[2]; j++){
        IERC721(_addresses[pointerA++]).safeTransferFrom(_msgSender(), address(this), _numbers[pointerB++]);
      }
      for(uint256 k =0; k< _numbers[3]; k++){
        IERC1155(_addresses[pointerA++]).safeTransferFrom(_msgSender(), address(this), _numbers[pointerB], _numbers[_numbers[3] + pointerB++], "");
      }
      tokenId = hash(nonce, _addresses, _numbers);
      super._mint(_to, tokenId);
      emit AssemblyAsset(_to, tokenId, nonce, _addresses, _numbers);
      nonce ++;
  }

  function safeMint(address _to, address[] memory _addresses, uint256[] memory _numbers) payable external override returns(uint256 tokenId){
      require(_to != address(0), "can't mint to address(0)");
      require(msg.value == _numbers[0], "value not match");
      require(_addresses.length == _numbers[1] + _numbers[2] + _numbers[3], "2 array length not match");
      require(_addresses.length == _numbers.length -4 - _numbers[3], "numbers length not match");
      uint256 pointerA; // 指向第一个 erc20 地址,如果有的话
      uint256 pointerB =4; // 指向第一个 erc20 数量,如果有的话
      for(uint256 i = 0; i< _numbers[1]; i++){
        require(_numbers[pointerB] > 0, "transfer erc20 0 amount");
        IERC20 token = IERC20(_addresses[pointerA++]);
        uint256 orgBalance = token.balanceOf(address(this));
        token.safeTransferFrom(_msgSender(), address(this), _numbers[pointerB]);
        _numbers[pointerB++] = token.balanceOf(address(this)) - orgBalance;
      }
      for(uint256 j = 0; j< _numbers[2]; j++){
        IERC721(_addresses[pointerA++]).safeTransferFrom(_msgSender(), address(this), _numbers[pointerB++]);
      }
      for(uint256 k =0; k< _numbers[3]; k++){
        IERC1155(_addresses[pointerA++]).safeTransferFrom(_msgSender(), address(this), _numbers[pointerB], _numbers[_numbers[3] + pointerB++], "");
      }
      tokenId = hash(nonce, _addresses, _numbers);
      super._mint(_to, tokenId);
      emit AssemblyAsset(_to, tokenId, nonce, _addresses, _numbers);
      nonce ++;
  }

  function burn(address _to, uint256 _tokenId, uint256 _salt, address[] calldata _addresses, uint256[] calldata _numbers) override external {
      require(_msgSender() == ownerOf(_tokenId), "not owned");
      require(_tokenId == hash(_salt, _addresses, _numbers));
      super._burn(_tokenId);
      payable(_to).transfer(_numbers[0]);
      uint256 pointerA; // 指向第一个 erc20 地址,如果有的话
      uint256 pointerB =4; // 指向第一个 erc20 数量,如果有的话
      for(uint256 i = 0; i< _numbers[1]; i++){
        require(_numbers[pointerB] > 0, "transfer erc20 0 amount");
        IERC20(_addresses[pointerA++]).safeTransfer(_to, _numbers[pointerB++]);
      }
      for(uint256 j = 0; j< _numbers[2]; j++){
        IERC721(_addresses[pointerA++]).safeTransferFrom(address(this), _to, _numbers[pointerB++]);
      }
      for(uint256 k =0; k< _numbers[3]; k++){
        IERC1155(_addresses[pointerA++]).safeTransferFrom(address(this), _to, _numbers[pointerB], _numbers[_numbers[3] + pointerB++], "");
      }
  }

}

安全考虑

在使用 mintsafeMint 函数之前,用户应该意识到某些 Token 的实现是可暂停的。如果其中一项资产在组装成一个 NFT 后被暂停,则 burn 函数可能无法成功执行。使用此标准的平台应创建支持列表或阻止列表,以避免这种情况。

版权

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

Citation

Please cite this document as:

Zhenyu Sun (@Ungigdu), Xinqi Yang (@xinqiyang), "ERC-3589: 将资产组装成 NFT [DRAFT]," Ethereum Improvement Proposals, no. 3589, May 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3589.