HomeEIPsNewsletter
EIPsERC-3589
ERC-3589

Assemble assets into NFTs

StagnantStandards Track: ERC
Created: 2021-05-24
Requires: EIP-721
Zhenyu Sun (@Ungigdu), Xinqi Yang (@xinqiyang)
Discussions ForumOriginal Proposal LinkEdit
1 min read

EIP-3589 proposes the creation of a new ERC-721 token called an assembly token, which can represent a combination of assets such as ether, ERC-20 tokens, ERC-721 tokens, and ERC-1155 tokens. This would allow for batch transfer or swap of multiple assets in a single transaction, making it more efficient and less gas-intensive than current methods. The motivation behind this proposal is to provide a safer and more efficient way for collectors to swap NFTs, as currently they often rely on third parties or inefficient market listings. The specification includes a generic system for the creation of non-fungible tokens tied to physical assets, with a focus on ensuring the authenticity of the asset and avoiding identity theft. The proposal also includes recommendations for the creation and management of assembly tokens, such as only allowing the manufacturer of the asset to create its associated token. Overall, EIP-3589 aims to improve the functionality and efficiency of NFT trading by allowing for the assembly of multiple assets into a single token.

Video
Anyone may contribute to propose contents.
Go propose
Original

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.

Further reading
Anyone may contribute to propose contents.
Go propose
Adopted by projects
Anyone may contribute to propose contents.
Go propose

Not miss a beat of EIPs' update?

Subscribe EIPs Fun to receive the latest updates of EIPs Good for Buidlers to follow up.

View all
Serve Ethereum Builders, Scale the Community.
Resources
GitHub
Supported by