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,以提供组装资产的标准方法。
mint
和 safeMint
将资产组装成一个 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++], "");
}
}
}
安全考虑
在使用 mint
或 safeMint
函数之前,用户应该意识到某些 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.