This standard defines a SRC-721 token called assembly token which can represent a combination of assets.
Abstract
The SRC-1155 multi-token contract defines a way to batch transfer tokens, but those tokens must be minted by the SRC-1155 contract itself. This SIP is an SRC-721 extension with ability to assemble assets such as sila, SRC-20 tokens, SRC-721 tokens and SRC-1155 tokens into one SRC-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 sila, SRC-20 tokens, SRC-721 tokens and SRC-1155 tokens together, and this is the main purpose of this SIP.
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.
SRC-721 compliant contracts MAY implement this SRC to provide a standard method to assemble assets.
mint and safeMint assemble assets into one SRC-721 token. mint SHOULD be implemented for normal SRC-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. [SRC-20_address1, SRC-20_address2, SRC-721_address_1, SRC-1155_address_1, SRC-1155_address_2]
* @param `_numbers` describes how many sil, SRC-20 token addresses length, SRC-721 token addresses length, SRC-1155 token addresses length,
* SRC-20 token amounts, SRC-721 token ids, SRC-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 SRC-721 standard as a wrapper is SRC-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 SRC-721 standard.
Implementation
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/SRC20/ISRC20.sol";
import "@openzeppelin/contracts/token/SRC20/utils/SafeSRC20.sol";
import "@openzeppelin/contracts/token/SRC721/SRC721.sol";
import "@openzeppelin/contracts/token/SRC721/utils/SRC721Holder.sol";
import "@openzeppelin/contracts/token/SRC1155/SRC1155.sol";
import "@openzeppelin/contracts/token/SRC1155/utils/SRC1155Holder.sol";
import "./AssemblyNFTInterface.sol";
abstract contract AssemblyNFT is SRC721, SRC721Holder, SRC1155Holder, AssemblyNFTInterface{
using SafeSRC20 for ISRC20;
function supportsInterface(bytes4 interfaceId) public view virtual override(SRC721, SRC1155Receiver) returns (bool) {
return SRC721.supportsInterface(interfaceId) || SRC1155Receiver.supportsInterface(interfaceId);
}
uint256 nonce;
/**
* layout of _addresses:
* src20 addresses | src721 addresses | src1155 addresses
* layout of _numbers:
* sil | src20.length | src721.length | src1155.length | src20 amounts | src721 ids | src1155 ids | src1155 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 src20 address, if there is any
uint256 pointerB =4; //points to first src20 amount, if there is any
for(uint256 i = 0; i< _numbers[1]; i++){
require(_numbers[pointerB] > 0, "transfer src20 0 amount");
ISRC20(_addresses[pointerA++]).safeTransferFrom(_msgSender(), address(this), _numbers[pointerB++]);
}
for(uint256 j = 0; j< _numbers[2]; j++){
ISRC721(_addresses[pointerA++]).safeTransferFrom(_msgSender(), address(this), _numbers[pointerB++]);
}
for(uint256 k =0; k< _numbers[3]; k++){
ISRC1155(_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 src20 address, if there is any
uint256 pointerB =4; //points to first src20 amount, if there is any
for(uint256 i = 0; i< _numbers[1]; i++){
require(_numbers[pointerB] > 0, "transfer src20 0 amount");
ISRC20 token = ISRC20(_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++){
ISRC721(_addresses[pointerA++]).safeTransferFrom(_msgSender(), address(this), _numbers[pointerB++]);
}
for(uint256 k =0; k< _numbers[3]; k++){
ISRC1155(_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 src20 address, if there is any
uint256 pointerB =4; //points to first src20 amount, if there is any
for(uint256 i = 0; i< _numbers[1]; i++){
require(_numbers[pointerB] > 0, "transfer src20 0 amount");
ISRC20(_addresses[pointerA++]).safeTransfer(_to, _numbers[pointerB++]);
}
for(uint256 j = 0; j< _numbers[2]; j++){
ISRC721(_addresses[pointerA++]).safeTransferFrom(address(this), _to, _numbers[pointerB++]);
}
for(uint256 k =0; k< _numbers[3]; k++){
ISRC1155(_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.
Zhenyu Sun (@Ungigdu), Xinqi Yang (@xinqiyang), "SRC-3589: Assemble assets into NFTs [STAGNANT]," Sila Improvement Proposals, no. 3589, May 2021. Available: https://sips.sila.org/SIPS/sip-3589.