# EIP-8712 : Standard for Wrapping Non-Fungible Tokens
Author: Bernard Choo
Discussions-to: https://github.com/ethereum/eips/issues/8721
Status: Draft ( Review, Last Call, Final, Stagnant, Withdrawn, Living )
Type: Standard track
Category: ERC
Created: 2021-10-21
Requires: 721
# Table of Contents
[Simple Summary](#Simple-Summary)
[Abstract](#Abstract)
[Motivation](#Motivation)
[Specification](#Specification)
[Implementation](#Implementation)
[Rationale](#Rationale)
[Backwards Compatibility](#Backwards-Compatibility)
[Test Cases](#Test-Cases)
[References](#References)
[Security Considerations](#Security-Considerations)
[Copyright](#Copyright)
[Citation](#Citation)
# Simple Summary
This standard defines the inner-structure and implementation of a Wrapped-Non-Fungible Tokens (wNFTs).
# Abstract
Wrapping Multiple NFT together allows users to perform multiple repetitive action base on one single NFT. For instance, User can transfer the ownership of 50 NFTs with 1 transfer transaction.
# Motivation
Combining multiple NFTs into one wrapNFT increase the ease of use. When a WNFT is listed on sales, they are representing multiple NFTs on sales. User can freely purchase different collection of WrapNFT with less transaction.
# 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.
Implementations of the standard may support different NFT project.
Implementers of this standard must inherit ERC721Holder and ERC721 token standard from Openzeppelin.
Any wrap token contracts can implement this ERC to provide a standard entry point to wrap NFT.
```
pragma solidity ^0.8.0;
interface WrapNFTInterface {
event WrapAsset(
uint256 indexed tokenId,
address indexed user,
address[] addresses,
uint256[] ids,
bool indexed wrap
);
/// @notice Core function to wrap NFTs
/// @dev NFTs have to be approve Wrap NFT contract to use their NFTs.
/// @param _addresses The address of tokens of NFTs that will be wrap.
/// @param _tokenIDs The token IDs of NFTs that will be wrap.
/// @return _newItemId Return the new token ID of WrapNFT.
function wrap(address[] memory _addresses, uint256[] memory _tokenIDs) external returns (uint256 newTokenID);
/// @notice To display single index position.
/// @param _id ID of the WrapNFT.
/// @param _index Intrested stored index of the NFT.
/// @return tokenAddress , The address of the NFT stored in index.
/// @return tokenID , Token ID of the NFT stored in index.
function recordOfOwnerByIndex(uint256 _id, uint256 _index) external view returns (address tokenAddress, uint256 tokenID);
/// @notice Unwrap and return the caller the original NFTs before wrapping.
/// @dev Caller must be the owner and holder of the token ID.
/// @param _tokenId Token ID of WrapNFT that will be burn.
function unwrap(uint256 _tokenId) external;
}
```
# Implementation
In the following implementation, we will leverage on the Openzeppelin Smart contract template library to build out the core logics of a WrapNFT.
```
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import "./WrapNFTInterface.sol";
contract WrapNFT is ERC721, ERC721Holder , WrapNFTInterface{
struct Record {
address localTokenAddress;
uint256 localTokenID;
}
// Mapping from owner to list of owned token IDs
mapping(uint256 => mapping(uint256 => Record)) private _ownedTokens;
mapping(uint256 => uint256) private _ownedBalance;
uint256 public currentCount = 0;
event WrapAsset(
uint256 indexed tokenId,
address indexed user,
address[] addresses,
uint256[] ids,
bool indexed wrap
);
/// @notice Core function to wrap NFTs
/// @dev NFTs have to be approve Wrap NFT contract to use their NFTs.
/// @param _addresses The address of tokens of NFTs that will be wrap.
/// @param _tokenIDs The token IDs of NFTs that will be wrap.
/// @return _newItemId Return the new token ID of Wrapped NFT.
function wrap(address[] memory _addresses, uint256[] memory _tokenIDs,
) public returns (uint256) {
uint256 _newItemId = currentCount;
currentCount++;
Record storage localStruct;
for (uint256 j = 0; j < _tokenIDs.length; j++) {
address _localAddress = _addresses[j];
uint256 _localTokenId = _tokenIDs[j];
IERC721(_addresses[j]).safeTransferFrom(
_msgSender(),
address(this),
_tokenIDs[j]
);
localStruct = _ownedTokens[_newItemId][j];
localStruct.localTokenAddress = _localAddress;
localStruct.localTokenID = _localTokenId;
}
_ownedBalance[_newItemId] = uint256(_tokenIDs.length);
super._mint(msg.sender, _newItemId);
emit WrapAsset(
_newItemId,
msg.sender,
_addresses,
_tokenIDs,
true
)
return _newItemId;
}
/// @notice To display single index position.
/// @param _id ID of the WrapNFT.
/// @param _index Intrested stored index of the NFT.
/// @return localTokenAddress , The address of the NFT.
/// @return localTokenID , Token ID of the NFT.
function recordOfOwnerByIndex(uint256 _id, uint256 _index)
external view
returns (address, uint256)
{
return (
_ownedTokens[_id][_index].localTokenAddress,
_ownedTokens[_id][_index].localTokenID
);
}
/// @notice Unwrap and return the caller the original NFTs before wrapping.
/// @dev Caller must be the owner and holder of the token ID.
/// @param _tokenId Token ID of WrapNFT that will be burn.
function unwrap(uint256 _tokenId) external {
require(msg.sender == this.ownerOf(_tokenId), "not owned");
super._burn(_tokenId);
address _localAddress;
uint256 _localTokenId;
address[] memory _addresses = new address[](_ownedBalance[_tokenId]);
uint256[] memory _tokenIDs = new uint256[](_ownedBalance[_tokenId]);
for (uint256 j = 0; j < _ownedBalance[_tokenId]; j++) {
Record storage localStruct = _ownedTokens[_tokenId][j];
_localAddress = localStruct.localTokenAddress;
_localTokenId = localStruct.localTokenID;
IERC721(_localAddress).safeTransferFrom(address(this),msg.sender,
_localTokenId
);
}
emit WrapAsset(
_tokenId,
msg.sender,
_addresses,
_tokenIDs,
false
)
}
constructor() ERC721("WrapNFTs", "WNFTs") {
}
```
# Rationale
To faciliate transfer of NFTs and ease of grouping different NFTs into 1 NFT. It is convenience to transfer 1 WrapNFT with 50 underlying NFTs compared to transfering 50 NFTs with 50 transactions, wasting gas cost and precious time. This paticular usecase is useful for NFT portfolio migration.
Whenever the receiver unwrap the WrapNFT, the receiver will be able to retrieve back the underlying NFTs wrapped. Also, Users are able to utilize a huge collection of NFT with ease when they wrap them into a WrapNFT.
# Backwards Compatibility
This proposal is backwards compatible with the ERC-721 standard as it inherists the ERC-721 standard.
# Test Cases
A WrapNFT will always return all the underlying assets when unwrapped.
When unwrapping the WrapNFT , the WrapNFT have to be burned or send to zero address.
# References
William Entriken, Dieter Shirley, Jacob Evans, Nastassia Sachs, "EIP-721: Non-Fungible Token Standard," Ethereum Improvement Proposals, no. 721, January 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-721.
Zhenyu Sun, Xinqi Yang, "EIP-3589: Assemble assets into NFTs," Ethereum Improvement Proposals, no. 3589, May 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3589.
# Security Considerations
Backdoor or other fallback function to remove a NFT held by the wrapNFT contract without unwrapping will cause the unwrapping transaction to throw an error and render the WrapNFT irretrievable.
# Copyright
Copyright and related rights waived via CC0.
# Citation
Please cite this document as:
Bernard Choo, "EIP-8721: Standard for Wrapping Non-Fungible Tokens," Ethereum Improvement Proposals, no. 8721, October 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-8721.
The following is a proposed EIP to wrap different ERC-721 tokens into a WrapNFT. It will be submitted as a Draft once it has been revised according to community feedback. Questions of implementation are contained in Rationale.
The pull request is open here: