Abstract

This standard defines an interface for ERC-721 or ERC-1155 tokens, known as “bindables”, to “bind” to ERC-721 NFTs.

When bindable tokens “bind” to an NFT, even though their ownership is transferred to the NFT, the NFT owner may “unbind” the tokens and claim their ownership. This enables bindable tokens to transfer with their bound NFTs without extra cost, offering a more effective way to create and transfer N:1 token-to-NFT bundles. Until an NFT owner decides to unbind them, bound tokens stay locked and resume their base token functionalities after unbinding.

This standard supports various use-cases such as:

  • NFT-bundled physical assets like microchipped streetwear, digitized car collections, and digitally twinned real estate.
  • NFT-bundled digital assets such as accessorizable virtual wardrobes, composable music tracks, and customizable metaverse land.

Motivation

A standard interface for NFT binding offers a seamless and efficient way to bundle and transfer tokens with NFTs, ensuring compatibility with wallets, marketplaces, and other NFT applications. It eliminates the need for rigid, implementation-specific strategies for token ownership.

In contrast with other standards that deal with token ownership at the account level, this standard aims to address token ownership at the NFT level. Its objective is to build a universal interface for token bundling, compatible with existing ERC-721 and ERC-1155 standards.

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 Bindable

Smart contracts implementing the ERC-721 bindable standard MUST implement the IERC721Bindable interface.

Implementers of the IER721Bindable interface MUST return true if 0x82a34a7d is passed as the identifier to the supportsInterface function.

/// @title ERC-721 Bindable Token Standard
/// @dev See https://eips.ethereum.org/ERCS/eip-5700
///  Note: the ERC-165 identifier for this interface is 0x82a34a7d.
interface IERC721Bindable /* is IERC721 */ {

    /// @notice This event emits when an unbound token is bound to an NFT.
    /// @param operator The address approved to perform the binding.
    /// @param from The address of the unbound token owner.
    /// @param bindAddress The contract address of the NFT being bound to.
    /// @param bindId The identifier of the NFT being bound to.
    /// @param tokenId The identifier of binding token.
    event Bind(
        address indexed operator,
        address indexed from,
        address indexed bindAddress,
        uint256 bindId,
        uint256 tokenId
    );

    /// @notice This event emits when an NFT-bound token is unbound.
    /// @param operator The address approved to perform the unbinding.
    /// @param from The owner of the NFT the token is bound to.
    /// @param to The address of the new unbound token owner.
    /// @param bindAddress The contract address of the NFT being unbound from.
    /// @param bindId The identifier of the NFT being unbound from.
    /// @param tokenId The identifier of the unbinding token.
    event Unbind(
        address indexed operator,
        address indexed from,
        address to,
        address indexed bindAddress,
        uint256 bindId,
        uint256 tokenId
    );

    /// @notice Binds token `tokenId` to NFT `bindId` at address `bindAddress`.
    /// @dev The function MUST throw unless `msg.sender` is the current owner, 
    ///  an authorized operator, or the approved address for the token. It also
    ///  MUST throw if the token is already bound or if `from` is not the token
    ///  owner. Finally, it MUST throw if the NFT contract does not support the
    ///  ERC-721 interface or if the NFT being bound to does not exist. Before 
    ///  binding, token ownership MUST be transferred to the contract address of
    ///  the NFT. On bind completion, the function MUST emit `Transfer` & `Bind` 
    ///  events to reflect the implicit token transfer and subsequent bind.
    /// @param from The address of the unbound token owner.
    /// @param bindAddress The contract address of the NFT being bound to.
    /// @param bindId The identifier of the NFT being bound to.
    /// @param tokenId The identifier of the binding token.
    function bind(
        address from,
        address bindAddress,
        uint256 bindId,
        uint256 tokenId
    ) external;

    /// @notice Unbinds token `tokenId` from NFT `bindId` at address `bindAddress`.
    /// @dev The function MUST throw unless `msg.sender` is the current owner, 
    ///  an authorized operator, or the approved address for the NFT the token
    ///  is bound to. It also MUST throw if the token is unbound, if `from` is
    ///  not the owner of the bound NFT, or if `to` is the zero address. After
    ///  unbinding, token ownership MUST be transferred to `to`, during which
    ///  the function MUST check if `to` is a valid contract (code size > 0),
    ///  and if so, call `onERC721Received`, throwing if the wrong identifier is
    ///  returned. On unbind completion, the function MUST emit `Unbind` &
    ///  `Transfer` events to reflect the unbind and subsequent transfer.
    /// @param from The address of the owner of the NFT the token is bound to.
    /// @param to The address of the unbound token new owner.
    /// @param bindAddress The contract address of the NFT being unbound from.
    /// @param bindId The identifier of the NFT being unbound from.
    /// @param tokenId The identifier of the unbinding token.
    function unbind(
        address from,
        address to,
        address bindAddress,
        uint256 bindId,
        uint256 tokenId
    ) external;

    /// @notice Gets the NFT address and identifier token `tokenId` is bound to.
    /// @dev When the token is unbound, this function MUST return the zero
    ///  address for the address portion to indicate no binding exists.
    /// @param tokenId The identifier of the token being queried.
    /// @return The token-bound NFT contract address and numerical identifier.
    function binderOf(uint256 tokenId) external view returns (address, uint256);

    /// @notice Gets total tokens bound to NFT `bindId` at address `bindAddress`.
    /// @param bindAddress The contract address of the NFT being queried.
    /// @param bindId The identifier of the NFT being queried.
    /// @return The total number of tokens bound to the queried NFT.
    function boundBalanceOf(address bindAddress, uint256 bindId) external view returns (uint256);

ERC-1155 Bindable

Smart contracts implementing the ERC-1155 Bindable standard MUST implement the IERC1155Bindable interface.

Implementers of the IER1155Bindable interface MUST return true if 0xd0d55c6 is passed as the identifier to the supportsInterface function.

/// @title ERC-1155 Bindable Token Standard
/// @dev See https://eips.ethereum.org/ERCS/eip-5700
///  Note: the ERC-165 identifier for this interface is 0xd0d555c6.
interface IERC1155Bindable /* is IERC1155 */ {

    /// @notice This event emits when token(s) are bound to an NFT.
    /// @param operator The address approved to perform the binding.
    /// @param from The owner address of the unbound tokens.
    /// @param bindAddress The contract address of the NFT being bound to.
    /// @param bindId The identifier of the NFT being bound to.
    /// @param tokenId The identifier of the binding token type.
    /// @param amount The number of tokens binding to the NFT.
    event Bind(
        address indexed operator,
        address indexed from,
        address indexed bindAddress,
        uint256 bindId,
        uint256 tokenId,
        uint256 amount
    );

    /// @notice This event emits when token(s) of different types are bound to an NFT.
    /// @param operator The address approved to perform the batch binding.
    /// @param from The owner address of the unbound tokens.
    /// @param bindAddress The contract address of the NFTs being bound to.
    /// @param bindId The identifier of the NFT being bound to.
    /// @param tokenIds The identifiers of the binding token types.
    /// @param amounts The number of tokens per type binding to the NFTs.
    event BindBatch(
        address indexed operator,
        address indexed from,
        address indexed bindAddress,
        uint256 bindId,
        uint256[] tokenIds,
        uint256[] amounts
    );

    /// @notice This event emits when token(s) are unbound from an NFT.
    /// @param operator The address approved to perform the unbinding.
    /// @param from The owner address of the NFT the tokens are bound to.
    /// @param to The address of the unbound tokens' new owner.
    /// @param bindAddress The contract address of the NFT being unbound from.
    /// @param bindId The identifier of the NFT being unbound from.
    /// @param tokenId The identifier of the unbinding token type.
    /// @param amount The number of tokens unbinding from the NFT.
    event Unbind(
        address indexed operator,
        address indexed from,
        address to,
        address indexed bindAddress,
        uint256 bindId,
        uint256 tokenId,
        uint256 amount
    );

    /// @notice This event emits when token(s) of different types are unbound from an NFT.
    /// @param operator The address approved to perform the batch binding.
    /// @param from The owner address of the unbound tokens.
    /// @param to The address of the unbound tokens' new owner.
    /// @param bindAddress The contract address of the NFTs being unbound from.
    /// @param bindId The identifier of the NFT being unbound from.
    /// @param tokenIds The identifiers of the unbinding token types.
    /// @param amounts The number of tokens per type unbinding from the NFTs.
    event UnbindBatch(
        address indexed operator,
        address indexed from,
        address to,
        address indexed bindAddress,
        uint256 bindId,
        uint256[] tokenIds,
        uint256[] amounts
    );

    /// @notice Binds `amount` tokens of `tokenId` to NFT `bindId` at address `bindAddress`.
    /// @dev The function MUST throw unless `msg.sender` is an approved operator
    ///  for `from`. It also MUST throw if the `from` owns fewer than `amount`
    ///  tokens. Finally, it MUST throw if the NFT contract does not support the
    ///  ERC-721 interface or if the NFT being bound to does not exist. Before 
    ///  binding, tokens MUST be transferred to the contract address of the NFT. 
    ///  On bind completion, the function MUST emit `Transfer` & `Bind` events 
    ///  to reflect the implicit token transfers and subsequent bind.
    /// @param from The owner address of the unbound tokens.
    /// @param bindAddress The contract address of the NFT being bound to.
    /// @param bindId The identifier of the NFT being bound to.
    /// @param tokenId The identifier of the binding token type.
    /// @param amount The number of tokens binding to the NFT.
    function bind(
        address from,
        address bindAddress,
        uint256 bindId,
        uint256 tokenId,
        uint256 amount
    ) external;

    /// @notice Binds `amounts` tokens of `tokenIds` to NFT `bindId` at address `bindAddress`.
    /// @dev The function MUST throw unless `msg.sender` is an approved operator
    ///  for `from`. It also MUST throw if the length of `amounts` is not the 
    ///  same as `tokenIds`, or if any balances of `tokenIds` for `from` is less
    ///  than that of `amounts`. Finally, it MUST throw if the NFT contract does 
    ///  not support the ERC-721 interface or if the bound NFT does not exist. 
    ///  Before binding, tokens MUST be transferred to the contract address of 
    ///  the NFT. On bind completion, the function MUST emit `TransferBatch` and
    ///  `BindBatch` events to reflect the batch token transfers and bind.
    /// @param from The owner address of the unbound tokens.
    /// @param bindAddress The contract address of the NFTs being bound to.
    /// @param bindId The identifier of the NFT being bound to.
    /// @param tokenIds The identifiers of the binding token types.
    /// @param amounts The number of tokens per type binding to the NFTs.
    function batchBind(
        address from,
        address bindAddress,
        uint256 bindId,
        uint256[] calldata tokenIds,
        uint256[] calldata amounts
    ) external;

    /// @notice Unbinds `amount` tokens of `tokenId` from NFT `bindId` at address `bindAddress`.
    /// @dev The function MUST throw unless `msg.sender` is an approved operator
    ///  for `from`. It also MUST throw if `from` is not the owner of the bound
    ///  NFT, if the NFT's token balance is fewer than `amount`, or if `to` is 
    ///  the zero address. After unbinding, tokens MUST be transferred to `to`,
    ///  during which the function MUST check if `to` is a valid contract (code 
    ///  size > 0), and if so, call `onERC1155Received`, throwing if the wrong \
    ///  identifier is returned. On unbind completion, the function MUST emit 
    ///  `Unbind` & `Transfer` events to reflect the unbind and transfers.
    /// @param from The owner address of the NFT the tokens are bound to.
    /// @param to The address of the unbound tokens' new owner.
    /// @param bindAddress The contract address of the NFT being unbound from.
    /// @param bindId The identifier of the NFT being unbound from.
    /// @param tokenId The identifier of the unbinding token type.
    /// @param amount The number of tokens unbinding from the NFT.
    function unbind(
        address from,
        address to,
        address bindAddress,
        uint256 bindId,
        uint256 tokenId,
        uint256 amount
    ) external;

    /// @notice Unbinds `amount` tokens of `tokenId` from NFT `bindId` at address `bindAddress`.
    /// @dev The function MUST throw unless `msg.sender` is an approved operator
    ///  for `from`. It also MUST throw if the length of `amounts` is not the
    ///  same as `tokenIds`, if any balances of `tokenIds` for the NFT is less 
    ///  than that of `amounts`, or if `to` is the zero addresss. After 
    ///  unbinding, tokens MUST be transferred to `to`, during which the 
    ///  function MUST check if `to` is a valid contract (code size > 0), and if 
    ///  so, call `onERC1155BatchReceived`, throwing if the wrong identifier is 
    ///  returned. On unbind completion, the function MUST emit `UnbindBatch` & 
    ///  `TransferBatch` events to reflect the batch unbind and transfers.
    /// @param from The owner address of the unbound tokens.
    /// @param to The address of the unbound tokens' new owner.
    /// @param bindAddress The contract address of the NFTs being unbound from.
    /// @param bindId The identifier of the NFT being unbound from.
    /// @param tokenIds The identifiers of the unbinding token types.
    /// @param amounts The number of tokens per type unbinding from the NFTs.
    function batchUnbind(
        address from,
        address to,
        address bindAddress,
        uint256 bindId,
        uint256[] calldata tokenIds,
        uint256[] calldata amounts
    ) external;

    /// @notice Gets the number of tokens of type `tokenId` bound to NFT `bindId` at address `bindAddress`.
    /// @param bindAddress The contract address of the bound NFT.
    /// @param bindId The identifier of the bound NFT.
    /// @param tokenId The identifier of the token type bound to the NFT.
    /// @return The number of tokens of type `tokenId` bound to the NFT.
    function boundBalanceOf(
        address bindAddress,
        uint256 bindId,
        uint256 tokenId
    ) external view returns (uint256);

    /// @notice Gets the number of tokens of types `bindIds` bound to NFTs `bindIds` at address `bindAddress`.
    /// @param bindAddress The contract address of the bound NFTs.
    /// @param bindIds The identifiers of the bound NFTs.
    /// @param tokenIds The identifiers of the token types bound to the NFTs.
    /// @return balances The bound balances for each token type / NFT pair.
    function boundBalanceOfBatch(
        address bindAddress,
        uint256[] calldata bindIds,
        uint256[] calldata tokenIds
    ) external view returns (uint256[] memory balances);

}

Rationale

A standard for token binding unlocks a new layer of composability for allowing wallets, applications, and protocols to interact with, trade, and display bundled NFTs. One example use-case of this is at Dopamine, where streetwear garments may be bundled with digital assets such as music, avatars, or digital-twins of the garments, by representing these assets as bindable tokens and binding them to microchips represented as NFTs.

Binding Mechanism

During binding, a bindable token’s technical ownership is conferred to its bound NFT, while allowing the NFT owner to unbind at any time. A caveat of this lightweight design is that applications that have yet to adopt this standard will not show the bundled tokens as owned by the NFT owner.

Backwards Compatibility

The bindable token interface is designed to be compatible with existing ERC-721 and ERC-1155 standards.

Reference Implementation

Security Considerations

During binding, because ownership is conferred to the bound NFT contract, implementations should take caution in ensuring unbinding may only be performed by the designated NFT owner.

Copyright and related rights waived via CC0.