Token Holder Extension for NFTs

We are proposing a token holder extension for NFTs. We are currently deciding on push vs pull mechanism for 721s and 1155s, and whether to use granular versions or a generic/universal one.


title: Token Holder Extension for NFTs
description: Extension to allow NFTs to receive and transfer ERC20, ERC721 and ERC1155 tokens.
author: Steven Pineda (@steven2308), Jan Turk (@ThunderDeliverer)
discussions-to: here
status: Draft
type: Standards Track
category: ERC
created: 2023-10-25
requires: 165, 20, 721, 1155

Abstract

This proposal suggests an extension to ERC721 to enable easy exchange of ERC20, ERC721, and ERC1155 tokens. By enhancing ERC721, it allows unique tokens to manage and trade the most common types of tokens bundled in a single NFT.

Motivation

In the ever-evolving landscape of blockchain technology and decentralized ecosystems, interoperability between diverse token standards has become a paramount concern. By enhancing ERC721 functionality, this proposal empowers non-fungible tokens (NFTs) to engage in complex transactions, facilitating the exchange of fungible tokens, unique assets, and multi-class assets within a single protocol.

This ERC introduces new utilities in the following areas:

  • Expanded use cases
  • Facilitating composite transactions
  • Market liquidity and value creation

Expanded Use Cases

Enabling ERC721 tokens to handle various token types opens the door to a wide array of innovative use cases. From gaming and digital collectibles to decentralized finance (DeFi) and supply chain management, this extension enhances the potential of NFTs by allowing them to participate in complex, multi-token transactions.

Facilitating composite transactions

With this extension, composite transactions involving both fungible and non-fungible assets become easier. This functionality is particularly valuable for applications requiring intricate transactions, such as gaming ecosystems where in-game assets may include a combination of fungible and unique tokens.

Market liquidity and value creation

By allowing ERC721 tokens to hold and trade different types tokens, it enhances liquidity for markets in all types of tokens.

Specification

ERC20Holder


interface IERC20Holder /*is IERC165, IERC721*/  {
    /**
     * @notice Used to notify listeners that the token received ERC-20 tokens.
     * @param erc20Contract The address of the ERC-20 smart contract
     * @param toTokenId The ID of the token receiving the ERC-20 tokens
     * @param from The address of the account from which the tokens are being transferred
     * @param amount The number of ERC-20 tokens received
     */
    event ReceivedERC20(
        address indexed erc20Contract,
        uint256 indexed toTokenId,
        address indexed from,
        uint256 amount
    );

    /**
     * @notice Used to notify the listeners that the ERC-20 tokens have been transferred.
     * @param erc20Contract The address of the ERC-20 smart contract
     * @param fromTokenId The ID of the token from which the ERC-20 tokens have been transferred
     * @param to The address receiving the ERC-20 tokens
     * @param amount The number of ERC-20 tokens transferred
     */
    event TransferredERC20(
        address indexed erc20Contract,
        uint256 indexed fromTokenId,
        address indexed to,
        uint256 amount
    );

    /**
     * @notice Used to retrieve the given token's specific ERC-20 balance
     * @param erc20Contract The address of the ERC-20 smart contract
     * @param tokenId The ID of the token being checked for ERC-20 balance
     * @return The amount of the specified ERC-20 tokens owned by a given token
     */
    function balanceOfERC20(
        address erc20Contract,
        uint256 tokenId
    ) external view returns (uint256);

    /**
     * @notice Transfer ERC-20 tokens from a specific token.
     * @dev The balance MUST be transferred from this smart contract.
     * @dev Implementers should validate that the `msg.sender` is either the token owner or approved to manage it before calling this.
     * @dev Must increase the transfer-out-nonce for the tokenId
     * @param erc20Contract The address of the ERC-20 smart contract
     * @param tokenId The ID of the token to transfer the ERC-20 tokens from
     * @param amount The number of ERC-20 tokens to transfer
     * @param data Additional data with no specified format, to allow for custom logic
     */
    function transferHeldERC20FromToken(
        address erc20Contract,
        uint256 tokenId,
        address to,
        uint256 amount,
        bytes memory data
    ) external;

    /**
     * @notice Transfer ERC-20 tokens to a specific token.
     * @dev The ERC-20 smart contract must have approval for this contract to transfer the ERC-20 tokens.
     * @dev The balance MUST be transferred from the `msg.sender`.
     * @param erc20Contract The address of the ERC-20 smart contract
     * @param tokenId The ID of the token to transfer ERC-20 tokens to
     * @param amount The number of ERC-20 tokens to transfer
     * @param data Additional data with no specified format, to allow for custom logic
     */
    function transferERC20ToToken(
        address erc20Contract,
        uint256 tokenId,
        uint256 amount,
        bytes memory data
    ) external;

    /**
     * @notice Nonce increased every time an ERC20 token is transferred out of a token
     * @param tokenId The ID of the token to check the nonce for
     * @return The nonce of the token
     */
    function erc20TransferOutNonce(
        uint256 tokenId
    ) external view returns (uint256);
}

ERC721Holder

pragma solidity ^0.8.21;

interface IERC721Holder /*is IERC165, IERC721*/  {
    /**
     * @notice Used to notify listeners that the token received ERC-721 tokens.
     * @param erc721Contract The address of the ERC-721 smart contract
     * @param tokenHolderId The ID of the token receiving the ERC-721 tokens
     * @param tokenTransferredId The ID of the received token
     * @param from The address of the account from which the tokens are being transferred
     */
    event ReceivedERC721(
        address indexed erc721Contract,
        uint256 indexed tokenHolderId,
        uint256 tokenTransferredId,
        address indexed from
    );

    /**
     * @notice Used to notify the listeners that the ERC-721 tokens have been transferred.
     * @param erc721Contract The address of the ERC-721 smart contract
     * @param tokenHolderId The ID of the token from which the ERC-721 tokens have been transferred
     * @param tokenTransferredId The ID of the transferred token
     * @param to The address receiving the ERC-721 tokens
     */
    event TransferredERC721(
        address indexed erc721Contract,
        uint256 indexed tokenHolderId,
        uint256 tokenTransferredId,
        address indexed to
    );

    /**
     * @notice Used to retrieve the given token's specific ERC-721 balance
     * @param erc721Contract The address of the ERC-721 smart contract
     * @param tokenHolderId The ID of the token being checked for ERC-721 balance
     * @param tokenHeldId The ID of the held token
     */
    function balanceOfERC721(
        address erc721Contract,
        uint256 tokenHolderId,
        uint256 tokenHeldId
    ) external view returns (uint256);

    /**
     * @notice Transfer ERC-721 tokens from a specific token.
     * @dev The balance MUST be transferred from this smart contract.
     * @dev Implementers should validate that the `msg.sender` is either the token owner or approved to manage it before calling this.
     * @param erc721Contract The address of the ERC-721 smart contract
     * @param tokenHolderId The ID of the token to transfer the ERC-721 tokens from
     * @param tokenToTransferId The ID of the held token being sent
     * @param data Additional data with no specified format, to allow for custom logic
     */
    function transferHeldERC721FromToken(
        address erc721Contract,
        uint256 tokenHolderId,
        uint256 tokenToTransferId,
        address to,
        bytes memory data
    ) external;

    /**
     * @notice Transfer ERC-721 tokens to a specific token.
     * @dev The ERC-721 smart contract must have approval for this contract to transfer the ERC-721 tokens.
     * @dev The balance MUST be transferred from the `msg.sender`.
     * @param erc721Contract The address of the ERC-721 smart contract
     * @param tokenHolderId The ID of the token to transfer ERC-721 tokens to
     * @param tokenToTransferId The ID of the held token being received
     * @param data Additional data with no specified format, to allow for custom logic
     */
    function transferERC721ToToken(
        address erc721Contract,
        uint256 tokenHolderId,
        uint256 tokenToTransferId,
        bytes memory data
    ) external;

    /**
     * @notice Nonce increased every time an ERC721 token is transferred out of a token
     * @param tokenId The ID of the token to check the nonce for
     * @return The nonce of the token
     */
    function erc721TransferOutNonce(
        uint256 tokenId
    ) external view returns (uint256);
}

ERC1155Holder

pragma solidity ^0.8.21;

interface IERC1155Holder  /*is IERC165, IERC721*/  {
    /**
     * @notice Used to notify listeners that the token received ERC-1155 tokens.
     * @param erc1155Contract The address of the ERC-1155 smart contract
     * @param tokenHolderId The ID of the token receiving the ERC-1155 tokens
     * @param tokenTransferredId The ID of the received token
     * @param from The address of the account from which the tokens are being transferred
     * @param amount The number of ERC-1155 tokens received
     */
    event ReceivedERC1155(
        address indexed erc1155Contract,
        uint256 indexed tokenHolderId,
        uint256 tokenTransferredId,
        address indexed from,
        uint256 amount
    );

    /**
     * @notice Used to notify the listeners that the ERC-1155 tokens have been transferred.
     * @param erc1155Contract The address of the ERC-1155 smart contract
     * @param tokenHolderId The ID of the token from which the ERC-1155 tokens have been transferred
     * @param tokenTransferredId The ID of the transferred token
     * @param to The address receiving the ERC-1155 tokens
     * @param amount The number of ERC-1155 tokens transferred
     */
    event TransferredERC1155(
        address indexed erc1155Contract,
        uint256 indexed tokenHolderId,
        uint256 tokenTransferredId,
        address indexed to,
        uint256 amount
    );

    /**
     * @notice Used to retrieve the given token's specific ERC-1155 balance
     * @param erc1155Contract The address of the ERC-1155 smart contract
     * @param tokenHolderId The ID of the token being checked for ERC-1155 balance
     * @param tokenHeldId The ID of the held token
     * @return The amount of the specified ERC-1155 tokens owned by a given token
     */
    function balanceOfERC1155(
        address erc1155Contract,
        uint256 tokenHolderId,
        uint256 tokenHeldId
    ) external view returns (uint256);

    /**
     * @notice Transfer ERC-1155 tokens from a specific token.
     * @dev The balance MUST be transferred from this smart contract.
     * @dev Implementers should validate that the `msg.sender` is either the token owner or approved to manage it before calling this.
     * @param erc1155Contract The address of the ERC-1155 smart contract
     * @param tokenHolderId The ID of the token to transfer the ERC-1155 tokens from
     * @param tokenToTransferId The ID of the held token being sent
     * @param amount The number of ERC-1155 tokens to transfer
     * @param data Additional data with no specified format, to allow for custom logic
     */
    function transferHeldERC1155FromToken(
        address erc1155Contract,
        uint256 tokenHolderId,
        uint256 tokenToTransferId,
        address to,
        uint256 amount,
        bytes memory data
    ) external;

    /**
     * @notice Transfer ERC-1155 tokens to a specific token.
     * @dev The ERC-1155 smart contract must have approval for this contract to transfer the ERC-1155 tokens.
     * @dev The balance MUST be transferred from the `msg.sender`.
     * @param erc1155Contract The address of the ERC-1155 smart contract
     * @param tokenHolderId The ID of the token to transfer ERC-1155 tokens to
     * @param tokenToTransferId The ID of the held token being received
     * @param amount The number of ERC-1155 tokens to transfer
     * @param data Additional data with no specified format, to allow for custom logic
     */
    function transferERC1155ToToken(
        address erc1155Contract,
        uint256 tokenHolderId,
        uint256 tokenToTransferId,
        uint256 amount,
        bytes memory data
    ) external;

    /**
     * @notice Nonce increased every time an ERC1155 token is transferred out of a token
     * @param tokenId The ID of the token to check the nonce for
     * @return The nonce of the token
     */
    function erc1155TransferOutNonce(
        uint256 tokenId
    ) external view returns (uint256);
}

GenericTokenHolder

pragma solidity ^0.8.21;

interface IGenericTokenHolder  /*is IERC165, IERC721*/  {
    enum TokenType {
        ERC20,
        ERC721,
        ERC1155
    }

    /**
     * @notice Used to notify listeners that the token received held tokens.
     * @dev If the token type is `ERC-20`, the `tokenToTransferId` MUST equal `0`
     * @dev If the token type is `ERC-721`, the `amount` MUST equal `1`.
     * @param tokenContract The address of the held token's smart contract
     * @param tokenType The type of the held token being received
     * @param toTokenId The ID of the token receiving the held tokens
     * @param tokenToTransferId The ID of the held token being received
     * @param from The address of the account from which the tokens are being transferred
     * @param amount The amount of held tokens received
     */
    event ReceivedToken(
        address indexed tokenContract,
        TokenType tokenType,
        uint256 indexed toTokenId,
        uint256 tokenToTransferId,
        address indexed from,
        uint256 amount
    );

    /**
     * @notice Used to notify the listeners that the ERC-20 tokens have been transferred.
     * @dev If the token type is `ERC-20`, the `tokenToTransferId` MUST equal `0`
     * @dev If the token type is `ERC-721`, the `amount` MUST equal `1`.
     * @param tokenContract The address of the smart contract of the token being transferred
     * @param tokenType The type of the token being transferred
     * @param fromTokenId The ID of the token from which the held tokens have been transferred
     * @param tokenToTransferId The Id of the held token being transferred
     * @param to The address receiving the ERC-20 tokens
     * @param amount The amount of held tokens transferred
     */
    event TransferredToken(
        address indexed tokenContract,
        TokenType tokenType,
        uint256 indexed fromTokenId,
        uint256 tokenToTransferId,
        address indexed to,
        uint256 amount
    );

    /**
     * @notice Used to retrieve the given token's balance of given token
     * @dev When retrieving the balance of an ERC-20 token, the `tokenToTransferId` parameter MUST be ignored.
     * @param tokenContract The address of the held token's smart contract
     * @param tokenType The type of the token being checked for balance
     * @param tokenId The ID of the token being checked for balance
     * @param tokenToTransferId The ID of the held token of which the balance is being retrieved
     * @return The amount of the specified ERC-20 tokens owned by a given token
     */
    function balanceOfToken(
        address tokenContract,
        TokenType tokenType,
        uint256 tokenId,
        uint256 tokenToTransferId
    ) external view returns (uint256);

    /**
     * @notice Transfer held tokens from a specific token.
     * @dev The balance MUST be transferred from this smart contract.
     * @dev Implementers should validate that the `msg.sender` is either the token owner or approved to manage it before
     *  calling this.
     * @dev If the token type is `ERC-20`, the `tokenToTransferId` MUST be ignored.
     * @dev IF the token type is `ERC-721`, the `amount` MUST be ignored.
     * @param tokenContract The address of the held token's smart contract
     * @param tokenType The type of the token being transferred
     * @param tokenId The ID of the token to transfer the held token from
     * @param tokenToTransferId The ID of the held token to transfer
     * @param amount The number of held tokens to transfer
     * @param to The address to transfer the held tokens to
     * @param data Additional data with no specified format, to allow for custom logic
     */
    function transferHeldTokenFromToken(
        address tokenContract,
        TokenType tokenType,
        uint256 tokenId,
        uint256 tokenToTransferId,
        uint256 amount,
        address to,
        bytes memory data
    ) external;

    /**
     * @notice Transfer tokens to a specific holder token.
     * @dev The token smart contract must have approval for this contract to transfer the tokens.
     * @dev The balance MUST be transferred from the `msg.sender`.
     * @dev If the token type is `ERC-20`, the `tokenToTransferId` MUST be ignored.
     * @dev If the token type is `ERC-721`, the `amount` MUST be ignored.
     * @param tokenContract The address of the token smart contract
     * @param tokenType The type of the token being transferred
     * @param tokenId The ID of the token to transfer the tokens to
     * @param tokenToTransferId The ID of the held token to transfer
     * @param amount The number of ERC-20 tokens to transfer
     * @param data Additional data with no specified format, to allow for custom logic
     */
    function transferHeldTokenToToken(
        address tokenContract,
        TokenType tokenType,
        uint256 tokenId,
        uint256 tokenToTransferId,
        uint256 amount,
        bytes memory data
    ) external;

    /**
     * @notice Nonce increased every time a held token is transferred out of a token
     * @param tokenId The ID of the token to check the nonce for
     * @return The nonce of the token
     */
    function transferOutNonce(uint256 tokenId) external view returns (uint256);
}

Rationale

Pull mechanism

We suggest to use pull mechanism, where the contract transfers the token to itself, instead of receiving it via “safe tranfer” for 2 reasons:

  1. Customizability with Hooks. By initiating the
    process this way, smart contract developers have the flexibility to execute specific actions before and after transfering the tokens.

  2. Uniformity and Safety Across Token Standards:
    ERC20 tokens lack a standardized “safe transfer” mechanism, which means there is no reliable way to notify the receiver of a successful transfer. By adopting a pull mechanism for all tokens, a consistent approach is established across different token standards.

This has the disadvantage of requiring approval of the token to be transferred before actually transferring it into an NFT. It is also not written in stone, we are open to change the behavior for 721 and 1515 holders to work via “safe transfer”.

Granular vs Generic

Currently we are trying to decide whether to go with a granular approach where there is an independent interface for each type of held token, versus a universal token holder. The 3 granular versions and the generic one are included in the specification.

Granular is slightly cheaper in gas, and if you’re using just one ore two types, it’s smaller in contract size. The generic version is smaller in size and have single methods to send or receive, but it also adds some complexity by always requiring Id and amount on transfer methods. Id not being necessary for ERC20s and amount not being necessary for ERC721s.

If we go with the granular version, we might need to split it into 3 ERCs.

The results of this discussion will be included in this section.

Backwards Compatibility

No backward compatibility issues found.

Test Cases

Will be added.

Reference Implementation

Will be added.

Security Considerations

The same security considerations as with ERC-721 apply: hidden logic may be present in any of the functions, including burn, add resource, accept resource, and more.

Caution is advised when dealing with non-audited contracts.

Implementations MUST use the message sender as from parameter when they are transferring tokens into an NFT. Otherwise, since the current contract needs approval, it could potentially pull the external tokens into a different NFT.

Copyright

Copyright and related rights waived via CC0.

1 Like

Hello,

How can I help or How can be forward this opportunity?

Thanks a lot for the support.

Kindly regards,

Juan David

1 Like

It would be great if you shared your thoughts on Pull vs Push for 721s and 1155s, and on Granular vs Generic.
Also, if you have any suggestions on the proposed methods or events