Assemble assets into NFTs
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
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
Copyright and related rights waived via CC0.
Adopted by projects
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