Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-3589: Assemble assets into NFTs

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

Simple Summary

This standard defines a ERC-721 token called assembly token which can represent a combination of assets.

Abstract

The ERC-1155 multi-token contract defines a way to batch transfer tokens, but those tokens must be minted by the ERC-1155 contract itself. This EIP is an ERC-721 extension with ability to assemble assets such as ether, ERC-20 tokens, ERC-721 tokens and ERC-1155 tokens into one ERC-721 token whose token id is also the asset’s signature. As assets get assembled into one, batch transfer or swap can be implemented very easily.

Motivation

As NFT arts and collectors rapidly increases, some collectors are not satisfied with traditional trading methods. When two collectors want to swap some of their collections, currently they can list their NFTs on the market and notify the other party to buy, but this is inefficient and gas-intensive. Instead, some collectors turn to social media or chat group looking for a trustworthy third party to swap NFTs for them. The third party takes NFTs from both collector A and B, and transfer A’s collections to B and B’s to A. This is very risky.

The safest way to do batch swap, is to transform batch swap into atomic swap, i.e. one to one swap. But first we should “assemble” those ether, ERC-20 tokens, ERC-721 tokens and ERC-1155 tokens together, and this is the main purpose of this EIP.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

ERC-721 compliant contracts MAY implement this ERC to provide a standard method to assemble assets.

mint and safeMint assemble assets into one ERC-721 token. mint SHOULD be implemented for normal ERC-20 tokens whose _transfer is lossless. safeMint MUST takes care for lossy token such as PIG token whose _transfer function is taxed.

_salt of hash function MAY be implemented other way, even provided as user input. But the token id MUST be generated by hash function.

Implementations of the standard MAY supports different set of assets.

Implementers of this standard MUST have all of the following functions:

pragma solidity ^0.8.0;

interface AssemblyNFTInterface {

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

  /**
  * @dev hash function assigns the combination of assets with salt to bytes32 signature that is also the token id.
  * @param `_salt` prevents hash collision, can be chosen by user input or increasing nonce from contract.
  * @param `_addresses` concat assets addresses, e.g. [ERC-20_address1, ERC-20_address2, ERC-721_address_1, ERC-1155_address_1, ERC-1155_address_2]
  * @param `_numbers` describes how many eth, ERC-20 token addresses length, ERC-721 token addresses length, ERC-1155 token addresses length,
  * ERC-20 token amounts, ERC-721 token ids, ERC-1155 token ids and amounts.
  */
  function hash(uint256 _salt, address[] memory _addresses, uint256[] memory _numbers) external pure returns (uint256 tokenId);

  /// @dev to assemble lossless assets
  /// @param `_to` the receiver of the assembly token
  function mint(address _to, address[] memory _addresses, uint256[] memory _numbers) payable external returns(uint256 tokenId);

  /// @dev mint with additional logic that calculates the actual received value for tokens.
  function safeMint(address _to, address[] memory _addresses, uint256[] memory _numbers) payable external returns(uint256 tokenId);

  /// @dev burn this token and releases assembled assets
  /// @param `_to` to which address the assets is released
  function burn(address _to, uint256 _tokenId, uint256 _salt, address[] calldata _addresses, uint256[] calldata _numbers) external;

}

Rationale

There are many reasons why people want to pack their NFTs together. For example, a collector want to pack a set of football players into a football team; a collector has hundreds of of NFTs with no categories to manage them; a collector wants to buy a full collection of NFTs or none of them. They all need a way a assemble those NFTs together.

The reason for choosing ERC-721 standard as a wrapper is ERC-721 token is already widely used and well supported by NFT wallets. And assembly token itself can also be assembled again. Assembly token is easier for smart contract to use than a batch of assets, in scenarios like batch trade, batch swap or collections exchange.

This standard has AssemblyAsset event which records the exact kinds and amounts of assets the assembly token represents. The wallet can easily display those NFTs to user just by the token id.

Backwards Compatibility

This proposal combines already available 721 extensions and is backwards compatible with the ERC-721 standard.

Implementation

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;

  /**
  * layout of _addresses:
  *     erc20 addresses | erc721 addresses | erc1155 addresses
  * layout of _numbers:
  *     eth | erc20.length | erc721.length | erc1155.length | erc20 amounts | erc721 ids | erc1155 ids | erc1155 amounts
   */

  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; //points to first erc20 address, if there is any
      uint256 pointerB =4; //points to first erc20 amount, if there is any
      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; //points to first erc20 address, if there is any
      uint256 pointerB =4; //points to first erc20 amount, if there is any
      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; //points to first erc20 address, if there is any
      uint256 pointerB =4; //points to first erc20 amount, if there is any
      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++], "");
      }
  }

}

Security Considerations

Before using mint or safeMint functions, user should be aware that some implementations of tokens are pausable. If one of the assets get paused after assembled into one NFT, the burn function may not be executed successfully. Platforms using this standard should make support lists or block lists to avoid this situation.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Zhenyu Sun (@Ungigdu), Xinqi Yang (@xinqiyang), "ERC-3589: Assemble assets into NFTs [DRAFT]," Ethereum Improvement Proposals, no. 3589, May 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3589.