Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-5700: Bindable Token Interface

Interface for binding fungible and non-fungible tokens to assets.

Authors Leeren (@leeren)
Created 2022-09-22
Discussion Link https://ethereum-magicians.org/t/eip-5700-bindable-token-standard/11077
Requires EIP-165, EIP-721, EIP-1155

Abstract

The proposed standard defines an interface by which fungible and non-fungible tokens may be bound to arbitrary assets (typically represented as NFTs themselves), enabling token ownership and transfer attribution to be proxied through the assets they are bound to.

A bindable token (“bindable”) is an EIP-721 or EIP-1155 token which, when bound to an asset, delegates ownership and tracking through its bound asset, remaining locked for direct transfers until it is unbound. When unbound, bindable tokens function normally according to their base token implementations.

A bound asset (“binder”) has few restrictions on how it is represented, except that it be unique and expose an interface for ownership queries. A binder would most commonly be represented as an EIP-721 NFT. Binders and bindables form a one-to-many relationship.

Below are example use-cases that benefit from such a standard:

  • NFT-bundled physical assets: microchipped streetwear bundles, digitized automobile collections, digitally-twinned real-estate property
  • NFT-bundled digital assets: accessorizable virtual wardrobes, composable music tracks, customizable metaverse land

Motivation

A standard interface for token binding allows tokens to be bundled and transferred with other assets in a way that is easily integrable with wallets, marketplaces, and other NFT applications, and avoids the need for ad-hoc ownership attribution strategies that are neither flexible nor backwards-compatible.

Unlike other standards tackling delegated ownership attribution, which look at composability on the account level, this standard addresses composability on the asset level, with the goal of creating a universal interface for token modularity that is compatible with existing EIP-721 and EIP-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.

EIP-721 Bindable

Smart contracts implementing the EIP-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/EIPS/eip-5700
///  Note: the ERC-165 identifier for this interface is 0x82a34a7d.
interface IERC721Bindable /* is IERC721 */ {

    /// @notice The `Bind` event MUST emit when NFT ownership is delegated
    ///  through an asset and when minting an NFT bound to an existing asset.
    /// @dev When minting bound NFTs, `from` MUST be set to the zero address.
    /// @param operator The address calling the bind.
    /// @param from The unbound NFT owner address.
    /// @param to The bound NFT owner delegate address.
    /// @param tokenId The identifier of the NFT being bound.
    /// @param bindId The identifier of the asset being bound to.
    /// @param bindAddress The contract address handling asset ownership.
    event Bind(
        address indexed operator,
        address indexed from,
        address to,
        uint256 tokenId,
        uint256 bindId,
        address indexed bindAddress
    );

    /// @notice The `Unbind` event MUST emit when asset-bound NFT ownership is
    ///  revoked, as well as when burning an NFT bound to an existing asset.
    /// @dev When burning bound NFTs, `to` MUST be set to the zero address.
    /// @param operator The address calling the unbind.
    /// @param from The bound asset owner address.
    /// @param to The unbound NFT owner address.
    /// @param tokenId The identifier of the NFT being unbound.
    /// @param bindId The identifier of the asset being unbound from.
    /// @param bindAddress The contract address handling bound asset ownership.
    event Unbind(
        address indexed operator,
        address indexed from,
        address to,
        uint256 tokenId,
        uint256 bindId,
        address indexed bindAddress
    );

    /// @notice Binds NFT `tokenId` owned by `from` to asset `bindId` at address
    ///  `bindAddress`, delegating NFT-bound ownership to `to`.
    /// @dev The function MUST throw unless `msg.sender` is the current owner, 
    ///  an authorized operator, or the approved address for the NFT. It also
    ///  MUST throw if the NFT is already bound, if `from` is not the NFT owner, 
    ///  or if `to` is not `bindAddress` or its asset owner. After binding, the
    ///  function MUST check if `bindAddress` is a valid contract /  (code size 
    ///  > 0), and if so, call `onERC721Bind` on it, throwing if the wrong 
    ///  identifier is returned (see "Binding Rules") or if the contract is 
    ///  invalid. On bind completion, the function MUST emit `Bind` & `Transfer`
    ///  events to reflect delegated ownership change.
    /// @param from The unbound NFT original owner address.
    /// @param to The bound NFT delegate owner address (SHOULD be `bindAddress`).
    /// @param tokenId The identifier of the NFT being bound.
    /// @param bindId The identifier of the asset being bound to.
    /// @param bindAddress The contract address handling asset ownership.
    /// @param data Additional data sent with the `onERC721Bind` hook.
    function bind(
        address from,
        address to,
        uint256 tokenId,
        uint256 amount,
        uint256 bindId,
        address bindAddress,
        bytes calldata data
    ) external;

    /// @notice Unbinds NFT `tokenId` from asset `bindId` owned by `from` at 
    ///  address `bindAddress`, assigning unbound NFT ownership to `to`.
    /// @dev The function MUST throw unless `msg.sender` is the asset owner or
    ///  an approved operator. It also MUST throw if NFT `tokenId` is not bound, 
    ///  if `from` is not the asset owner, or if `to` is the zero address. After 
    ///  unbinding, the function MUST check if `bindAddress` is a valid contract 
    ///  (code size > 0), and if so, call `onERC721Unbind` on it, throwing if 
    ///  the wrong identifier is returned (see "Binding Rules") or if the 
    ///  contract is invalid. The function also MUST check if `to` is a valid 
    ///  contract, and if so, call `onERC721Received`, throwing if the wrong 
    ///  identifier is returned. On unbind completion, the function MUST emit 
    ///  `Unbind` & `Transfer` events to reflect delegated ownership change.
    /// @param from The bound asset owner address.
    /// @param to The unbound NFT owner address.
    /// @param tokenId The identifier of the NFT being unbound.
    /// @param bindId The identifier of the asset being unbound from.
    /// @param bindAddress The contract address handling bound asset ownership.
    /// @param data Additional data sent with the `onERC721Unbind` hook.
    function unbind(
        address from,
        address to,
        uint256 tokenId,
        uint256 bindId,
        address bindAddress,
        bytes calldata data
    ) external;

    /// @notice Gets the asset identifier and address which an NFT is bound to.
    /// @param tokenId The identifier of the NFT being queried.
    /// @return The bound asset identifier and contract address.
    function binderOf(uint256 tokenId) external returns (uint256, address);

    /// @notice Counts NFTs bound to asset `bindId` at address `bindAddress`.
    /// @param bindAddress The contract address handling bound asset ownership.
    /// @param bindId The identifier of the bound asset.
    /// @return The total number of NFTs bound to the asset.
    function boundBalanceOf(address bindAddress, uint256 bindId) external returns (uint256);

Smart contracts managing assets MUST implement the IERC721Binder interface if they are to accept binds from EIP-721 bindables.

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

/// @dev Note: the ERC-165 identifier for this interface is 0x2ac2d2bc.
interface IERC721Binder /* is IERC165 */ {

    /// @notice Handles the binding of an IERC721Bindable-compliant NFT.
    /// @dev An IERC721Bindable-compliant smart contract MUST call this function
    ///  at the end of a `bind` after ownership is delegated through an asset.
    ///  The function MUST revert if `to` is not the asset owner or the binder
    ///  address. The function MUST revert if it rejects the bind. If accepting 
    ///  the bind, the function MUST return `bytes4(keccak256("onERC721Bind(address,address,address,uint256,uint256,bytes)"))`
    ///  Caller MUST revert the transaction if the above value is not returned.
    ///  Note: The contract address of the binding NFT is `msg.sender`.
    /// @param operator The address initiating the bind.
    /// @param from The unbound NFT owner address.
    /// @param to The bound NFT owner delegate address.
    /// @param tokenId The identifier of the NFT being bound.
    /// @param bindId The identifier of the asset being bound to.
    /// @param data Additional data sent along with no specified format.
    /// @return `bytes4(keccak256("onERC721Bind(address,address,address,uint256,uint256,bytes)"))`
    function onERC721Bind(
        address operator,
        address from,
        address to,
        uint256 tokenId,
        uint256 bindId,
        bytes calldata data
  ) external returns (bytes4);

    /// @notice Handles the unbinding of an IERC721Bindable-compliant NFT.
    /// @dev An IERC721Bindable-compliant smart contract MUST call this function
    ///  at the end of an `unbind` after revoking asset-delegated ownership.
    ///  The function MUST revert if `from` is not the asset owner of `bindId`.
    ///  The function MUST revert if it rejects the unbind. If accepting the 
    ///  unbind, the function MUST return `bytes4(keccak256("onERC721Unbind(address,address,address,uint256,uint256,bytes)"))`
    ///  Caller MUST revert the transaction if the above value is not returned.
    ///  Note: The contract address of the unbinding NFT is `msg.sender`.
    /// @param from The bound asset owner address.
    /// @param to The unbound NFT owner address.
    /// @param tokenId The identifier of the NFT being unbound.
    /// @param bindId The identifier of the asset being unbound from.
    /// @param data Additional data with no specified format.
    /// @return `bytes4(keccak256("onERC721Unbind(address,address,address,uint256,uint256,bytes)"))`
    function onERC721Unbind(
        address operator,
        address from,
        address to,
        uint256 tokenId,
        uint256 bindId,
        bytes calldata data
    ) external returns (bytes4);

    /// @notice Gets the owner address of the asset identified by `bindId`.
    /// @dev This function MUST throw for assets assigned to the zero address.
    /// @param bindId The identifier of the asset whose owner is being queried.
    /// @return The address of the owner of the asset.
    function ownerOf(uint256 bindId) external view returns (address);

    /// @notice Checks if an operator can act on behalf of an asset owner.
    /// @param owner The address that owns an asset.
    /// @param operator The address that can act on behalf of the asset owner.
    /// @return True if `operator` can act on behalf of `owner`, else False.
    function isApprovedForAll(address owner, address operator) external view returns (bool);

}

EIP-1155 Bindable

Smart contracts implementing the EIP-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/EIPS/eip-5700
///  Note: the ERC-165 identifier for this interface is 0xd0d555c6.
interface IERC1155Bindable /* is IERC1155 */ {

    /// @notice The `Bind` event MUST emit when token ownership is delegated
    ///  through an asset and when minting tokens bound to an existing asset.
    /// @dev When minting bound tokens, `from` MUST be set to the zero address.
    /// @param operator The address calling the bind.
    /// @param from The owner address of the unbound tokens.
    /// @param to The delegate owner address of the bound tokens.
    /// @param tokenId The identifier of the token type being bound.
    /// @param amount The number of tokens of type `tokenId` being bound.
    /// @param bindId The identifier of the asset being bound to.
    /// @param bindAddress The contract address handling asset ownership.
    event Bind(
        address indexed operator,
        address indexed from,
        address to,
        uint256 tokenId,
        uint256 amount,
        uint256 bindId,
        address indexed bindAddress
    );

    /// @notice The `BindBatch` event MUST emit when token ownership of 
    ///  different token types are delegated through multiple assets and when
    ///  minting different token types bound to multiple existing assets.
    /// @dev When minting bound tokens, `from` MUST be set to the zero address.
    /// @param operator The address calling the bind.
    /// @param from The owner address of the unbound tokens.
    /// @param to The delegate owner address of the bound tokens.
    /// @param tokenIds The identifiers of the token types being bound.
    /// @param amounts The number of tokens for each token type being bound.
    /// @param bindIds The identifiers of the assets being bound to.
    /// @param bindAddress The contract address handling asset ownership.
    event BindBatch(
        address indexed operator,
        address indexed from,
        address to,
        uint256[] tokenIds,
        uint256[] amounts,
        uint256[] bindIds,
        address indexed bindAddress
    );

    /// @notice The `Unbind` event MUST emit when asset-delegated token 
    ///  ownership is revoked and when burning tokens bound to existing assets.
    /// @dev When burning bound tokens, `to` MUST be set to the zero address.
    /// @param operator The address calling the unbind.
    /// @param from The owner address of the bound asset.
    /// @param to The owner address of the unbound tokens.
    /// @param tokenId The identifier of the token type being unbound.
    /// @param amount The number of tokens of type `tokenId` being unbound.
    /// @param bindId The identifier of the asset being unbound from.
    /// @param bindAddress The contract address handling bound asset ownership.
    event Unbind(
        address indexed operator,
        address indexed from,
        address to,
        uint256 tokenId,
        uint256 amount,
        uint256 bindId,
        address indexed bindAddress
    );

    /// @notice The `UnbindBatch` event MUST emit when asset-delegated token 
    ///  ownership is revoked for different token types and when burning 
    ///  different token types bound to multiple existing assets.
    /// @dev When burning bound tokens, `to` MUST be set to the zero address.
    /// @param operator The address calling the unbind.
    /// @param from The owner address of the bound asset.
    /// @param to The owner address of the unbound tokens.
    /// @param tokenIds The identifiers of the token types being unbound.
    /// @param amounts The number of tokens for each token type being unbound.
    /// @param bindIds The identifiers of the assets being unbound from.
    /// @param bindAddress The contract address handling bound asset ownership.
    event UnbindBatch(
        address indexed operator,
        address indexed from,
        address to,
        uint256[] tokenIds,
        uint256[] amounts,
        uint256[] bindIds,
        address indexed bindAddress
    );

    /// @notice Binds `amount` tokens of type `tokenId` owned by `from` to asset
    ///  `bindId` at `bindAddress`, delegating token-bound ownership to `to`.
    /// @dev The function MUST throw unless `msg.sender` is an approved operator
    ///  for `from`. The function also MUST throw if `from` owns fewer than 
    ///  `amount` tokens, or if `to` is not `bindAddress` or its asset owner. 
    ///  After binding, the function MUST check if `bindAddress` is a valid 
    ///  contract (code size > 0), and if so, call `onERC1155Bind` on it, 
    ///  throwing if the wrong identifier is returned (see "Binding Rules") or
    ///  if the contract is invalid. On bind completion, the function MUST emit
    ///  `Bind` & `TransferSingle` events to reflect delegated ownership change.
    /// @param from The owner address of the unbound tokens.
    /// @param to The delegate owner address of the bound tokens (SHOULD be `bindAddress`).
    /// @param tokenId The identifier of the token type being bound.
    /// @param amount The number of tokens of type `tokenId` being bound.
    /// @param bindId The identifier of the asset being bound to.
    /// @param bindAddress The contract address handling asset ownership.
    /// @param data Additional data sent with the `onERC1155Bind` hook.
    function bind(
        address from,
        address to,
        uint256 tokenId,
        uint256 amount,
        uint256 bindId,
        address bindAddress,
        bytes calldata data
    ) external;

    /// @notice Binds `amounts` tokens of types `tokenIds` owned by `from` to 
    ///   assets `bindIds` at `bindAddress`, delegating bound ownership to `to`.
    /// @dev The function MUST throw unless `msg.sender` is an approved operator
    ///  for `from`. The function also MUST throw if length of `amounts` is not 
    ///  the same as `tokenIds` or `bindIds`, if any balances of `tokenIds` for
    ///  `from` is less than that of `amounts`, or if `to` is not `bindAddress`
    ///  or the asset owner. After delegating ownership, the function MUST check 
    ///  if `bindAddress` is a valid contract (code size > 0), and if so, call 
    ///  `onERC1155BatchBind` on it, throwing if the wrong identifier is 
    ///  returned (see "Binding Rules") or if the contract is invalid. On bind
    ///  completion, the function MUST emit `BindBatch` & `TransferBatch` events
    ///  to reflect delegated ownership changes.
    /// @param from The owner address of the unbound tokens.
    /// @param to The delegate owner address of the bound tokens (SHOULD be `bindAddress`).
    /// @param tokenIds The identifiers of the token types being bound.
    /// @param amounts The number of tokens for each token type being bound.
    /// @param bindIds The identifiers of the assets being bound to.
    /// @param bindAddress The contract address handling asset ownership.
    /// @param data Additional data sent with the `onERC1155BatchBind` hook.
    function batchBind(
        address from,
        address to,
        uint256[] calldata tokenIds,
        uint256[] calldata amounts,
        uint256[] calldata bindIds,
        address bindAddress,
        bytes calldata data
    ) external;

    /// @notice Revokes delegated ownership of `amount` tokens of type `tokenId`
    ///  owned by `from` bound to `bindId`, switching ownership to `to`.
    /// @dev The function MUST throw unless `msg.sender` is the asset owner or
    ///  an approved operator. It also MUST throw if `from` is not the asset 
    ///  owner, if fewer than `amount` tokens are bound to the asset, or if `to`
    ///  is the zero address. Once delegated ownership is revoked, the function 
    ///  MUST check if `bindAddress` is a valid contract (code size > 0), and if 
    ///  so, call `onERC1155Unbind` on it, throwing if the wrong identifier is 
    ///  returned (see "Binding Rules") or if the contract is invalid. The 
    ///  function also MUST check if `to` is a contract, and if so, call on it 
    ///  `onERC1155Received`, throwing if the wrong identifier is returned. On 
    ///  unbind completion, the function MUST emit `Unbind` & `TransferSingle` 
    ///  events to reflect delegated ownership change.
    /// @param from The owner address of the bound asset.
    /// @param to The owner address of the unbound tokens.
    /// @param tokenId The identifier of the token type being unbound.
    /// @param amount The number of tokens of type `tokenId` being unbound.
    /// @param bindId The identifier of the asset being unbound from.
    /// @param bindAddress The contract address handling bound asset ownership.
    /// @param data Additional data sent with the `onERC1155Unbind` hook.
    function unbind(
        address from,
        address to,
        uint256 tokenId,
        uint256 amount,
        uint256 bindId,
        address bindAddress,
        bytes calldata data
    ) external;

    /// @notice Revokes delegated ownership of `amounts` tokens of `tokenIds`
    ///  owned by `from` bound to assets `bindIds`, switching ownership to `to`.
    /// @dev The function MUST throw unless `msg.sender` is the assets' owner or
    ///  approved operator. It also MUST throw if the length of `amounts` is not
    ///  the same as `tokenIds` or `bindIds`, if `from` is not the owner of all
    ///  assets, if any count in `amounts` is fewer than the number of tokens
    ///  bound for the corresponding token-asset pair given by `tokenIds` and
    ///  `bindIds`, or if `to` is the zero address. Once delegated ownership is 
    ///  revoked for all tokens, the function MUST check if `bindAddress` is a 
    ///  valid contract (code size >  0), and if so, call `onERC1155BatchUnbind` 
    ///  on it, throwing if a wrong identifier is returned (see "Binding Rules") 
    ///  or if the contract is invalid. The function also MUST check if `to` is 
    ///  valid contract, and if so, call `onERC1155BatchReceived` on it, 
    ///  throwing if the wrong identifier is returned. On unbind completion, the 
    ///  function MUST emit `BatchUnbind` and `TransferBatch` events to reflect 
    ///  delegated ownership change.
    /// @param from The owner address of the bound asset.
    /// @param to The owner address of the unbound tokens.
    /// @param tokenIds The identifiers of the token types being unbound.
    /// @param amounts The number of tokens for each token type being unbound.
    /// @param bindIds The identifier of the assets being unbound from.
    /// @param bindAddress The contract address handling bound asset ownership.
    /// @param data Additional data sent with the `onERC1155BatchUnbind` hook.
    function batchUnbind(
        address from,
        address to,
        uint256[] calldata tokenIds,
        uint256[] calldata amounts,
        uint256[] calldata bindIds,
        address bindAddress,
        bytes calldata data
    ) external;

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

    /// @notice Gets the balance of bound tokens for multiple token types given
    ///  by `tokenIds` bound to assets `bindIds` at address `bindAddress`.
    /// @param bindAddress The contract address handling bound asset ownership.
    /// @param bindIds List of bound asset identifiers.
    /// @param tokenIds The identifiers of the counted bound token types.
    /// @return balances The bound balances for each asset / token type pair.
    function boundBalanceOfBatch(
        address bindAddress,
        uint256[] calldata bindIds,
        uint256[] calldata tokenIds
    ) external returns (uint256[] memory balances);

}

Smart contracts managing assets MUST implement the IERC1155Binder interface if they are to accept binds from EIP-1155 bindables.

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

pragma solidity ^0.8.16;

/// @dev Note: the ERC-165 identifier for this interface is 0x6fc97e78.
interface IERC1155Binder /* is IERC165 */ {

    /// @notice Handles binding of an IERC1155Bindable-compliant token type.
    /// @dev An IERC1155Bindable-compliant smart contract MUST call this 
    ///  function at the end of a `bind` after ownership is delegated through an
    ///  asset. The function MUST revert if `to` is not the asset owner or 
    ///  binder address. The function MUST revert if it rejects the bind. If 
    ///  accepting the bind, the function MUST return `bytes4(keccak256("onERC1155Bind(address,address,address,uint256,uint256,uint256,bytes)"))`
    ///  Caller MUST revert the transaction if the above value is not returned.
    ///  Note: The contract address of the binding token is `msg.sender`.
    /// @param operator The address responsible for binding.
    /// @param from The owner address of the unbound tokens.
    /// @param to The delegate owner address of the bound tokens.
    /// @param tokenId The identifier of the token type being bound.
    /// @param bindId The identifier of the asset being bound to.
    /// @param data Additional data sent along with no specified format.
    /// @return `bytes4(keccak256("onERC1155Bind(address,address,address,uint256,uint256,uint256,bytes)"))`
    function onERC1155Bind(
        address operator,
        address from,
        address to,
        uint256 tokenId,
        uint256 amount,
        uint256 bindId,
        bytes calldata data
    ) external returns (bytes4);

    /// @notice Handles binding of multiple IERC1155Bindable-compliant tokens 
    ///  `tokenIds` to multiple assets `bindIds`.
    /// @dev An IERC1155Bindable-compliant smart contract MUST call this 
    ///  function at the end of a `batchBind` after delegating ownership of 
    ///  multiple token types to the asset owner. The function MUST revert if 
    ///  `to` is not the asset owner or binder address. The function MUST revert
    ///  if it rejects the bind. If accepting the bind, the function MUST return 
    ///  `bytes4(keccak256("onERC1155BatchBind(address,address,address,uint256[],uint256[],uint256[],bytes)"))`
    ///  Caller MUST revert the transaction if the above value is not returned.
    ///  Note: The contract address of the binding token is `msg.sender`.
    /// @param operator The address responsible for performing the binds.
    /// @param from The unbound tokens' original owner address.
    /// @param to The bound tokens' delegate owner address (SHOULD be `bindAddress`).
    /// @param tokenIds The list of token types being bound.
    /// @param amounts The number of tokens for each token type being bound.
    /// @param bindIds The identifiers of the assets being bound to.
    /// @param data Additional data sent along with no specified format.
    /// @return `bytes4(keccak256("onERC1155Bind(address,address,address,uint256[],uint256[],uint256[],bytes)"))`
    function onERC1155BatchBind(
        address operator,
        address from,
        address to,
        uint256[] calldata tokenIds,
        uint256[] calldata amounts,
        uint256[] calldata bindIds,
        bytes calldata data
    ) external returns (bytes4);

    /// @notice Handles unbinding of an IERC1155Bindable-compliant token type.
    /// @dev An IERC1155Bindable-compliant contract MUST call this function at
    ///  the end of an `unbind` after revoking delegated asset ownership. The 
    ///  function MUST revert if `from` is not the asset owner. The function 
    ///  MUST revert if it rejects the unbind. If accepting the unbind, the 
    ///  function MUST return `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256,uint256,uint256,bytes)"))`
    ///  Caller MUST revert the transaction if the above value is not returned.
    ///  Note: The contract address of the unbinding token is `msg.sender`.
    /// @param operator The address responsible for performing the unbind.
    /// @param from The owner address of the bound asset.
    /// @param to The owner address of the unbound tokens.
    /// @param tokenId The token type being unbound.
    /// @param amount The number of tokens of type `tokenId` being unbound.
    /// @param bindId The identifier of the asset being unbound from.
    /// @param data Additional data sent along with no specified format.
    /// @return `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256,uint256,uint256,bytes)"))`
    function onERC1155Unbind(
        address operator,
        address from,
        address to,
        uint256 tokenId,
        uint256 amount,
        uint256 bindId,
        bytes calldata data
    ) external returns (bytes4);

    /// @notice Handles unbinding of multiple IERC1155Bindable-compliant token types.
    /// @dev An IERC1155Bindable-compliant contract MUST call this function at
    ///  the end of a `batchUnbind` after revoking asset-delegated ownership.
    ///  The function MUST revert if `from` is not the asset owner. The function 
    ///  MUST revert if it rejects the unbinds. If accepting the unbinds, the 
    ///  function MUST return `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256[],uint256[],uint256[],bytes)"))`
    ///  Caller MUST revert the transaction if the above value is not returned.
    ///  Note: The contract address of the unbinding token is `msg.sender`.
    /// @param operator The address responsible for performing the unbinds.
    /// @param from The owner address of the bound asset.
    /// @param to The owner address of the unbound tokens.
    /// @param tokenIds The list of token types being unbound.
    /// @param amounts The number of tokens for each token type being unbound.
    /// @param bindIds The identifiers of the assets being unbound from.
    /// @param data Additional data sent along with no specified format.
    /// @return `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256[],uint256[],uint256[],bytes)"))`
    function onERC1155BatchUnbind(
        address operator,
        address from,
        address to,
        uint256[] calldata tokenIds,
        uint256[] calldata amounts,
        uint256[] calldata bindIds,
        bytes calldata data
    ) external returns (bytes4);

    /// @notice Gets the owner address of the asset represented by id `bindId`.
    /// @param bindId The identifier of the asset whose owner is being queried.
    /// @return The address of the owner of the asset.
    function ownerOf(uint256 bindId) external view returns (address);

    /// @notice Checks if an operator can act on behalf of an asset owner.
    /// @param owner The owner address of an asset.
    /// @param operator The address operating on behalf of the asset owner.
    /// @return True if `operator` can act on behalf of `owner`, else False.
    function isApprovedForAll(address owner, address operator) external view returns (bool);

}

Rules

This standard supports two modes of binding, depending on whether ownership is delegated to the asset owner or binder address.

  • Delegated (RECOMMENDED):
    • Bindable ownership is delegated to the binder address (to is bindAddress in a bind).
    • Bindable ownership queries return the binder address.
    • Bindable transfers MUST always throw.
  • Legacy (NOT RECOMMENDED):
    • Bindable ownership is delegated to the asset owner address (to is the asset owner address in a bind).
    • Bindable ownership queries return the asset owner address.
    • Bindable transfers MUST always throw, except when invoked as a result of bound assets being transferred.
    • Transferrable bound assets MUST keep track of bound tokens following this binding mode.
    • On transfer, bound assets MUST invoke ownership transfers for bound tokens following this binding mode.

Binders SHOULD choose to only support the “delegated” binding mode by throwing if to is not bindAddress, otherwise both modes MAY be supported.

bind rules:

  • When binding an EIP-721 bindable to an asset:
    • MUST throw if caller is not the current NFT owner, the approved address for the NFT, or an approved operator for from.
    • MUST throw if NFT tokenId is already bound.
    • MUST throw if from is not the NFT owner.
    • MUST throw if to is not bindAddress or the asset owner.
    • After above conditions are met, MUST check if bindAddress is a smart contract (code size > 0). If so, it MUST call onERC721Bind on bindAddress with data passed unaltered and act appropriately (see “Hook Rules”).
    • MUST emit the Bind event to reflect asset-bound ownership delegation.
    • MUST emit the Transfer event if from is different than to to reflect delegated ownership change.
  • When binding an EIP-1155 bindable to an asset:
    • MUST throw if caller is not an approved operator for from.
    • MUST throw if from owns fewer than amount unbound tokens of type tokenId.
    • MUST throw if to is not bindAddress or the asset owner.
    • After above conditions are met, MUST check if bindAddress is a smart contract (code size > 0). If so, it MUST call onERC1155Bind on bindAddress with data passed unaltered and act appropriately (see “Hook Rules”).
    • MUST emit the Bind event to reflect asset-bound ownership delegation.
    • MUST emit the TransferSingle event if from is different than to to reflect delegated ownership change.

unbind rules:

  • When unbinding an EIP-721 bindable from an asset:
    • MUST throw if caller is not the owner of the asset or an approved asset operator for from.
    • MUST throw if NFT tokenId is not bound.
    • MUST throw if from is not the asset owner.
    • MUST throw if to is the zero address.
    • After above conditions are met, MUST check if bindAddress is a smart contract (code size > 0). If so, it MUST call onERC721Unbind on bindAddress with data passed unaltered and act appropriately (see “Hook Rules”).
    • In addition, it MUST check if to is a smart contract (code size > 0), and call onERC721Received on to with data passed unaltered and act appropriately (see “Hook Rules”).
    • MUST emit the Unbind event to reflect asset-bound ownership revocation.
    • MUST emit the Transfer event if from is different than to to reflect delegated ownership change.
  • When unbinding a an EIP-1155 bindable from an asset:
    • MUST throw if caller is not the owner of the asset or an approved asset operator for from.
    • MUST throw if from is not the asset owner.
    • MUST throw if fewer than amount tokens of type tokenId are bound to bindId.
    • MUST throw if to is the zero address.
    • After above conditions are met, MUST check if bindAddress is a smart contract (code size > 0). If so, it MUST call onERC1155Unbind on bindAddress with data passed unaltered and act appropriately (see “Hook Rules”).
    • In addition, it MUST check if to is a smart contract (code size > 0), and call onERC1155Received on to with data passed unaltered and act appropriately (see “Hook Rules”).
    • MUST emit the Unbind event to reflect asset-bound ownership revocation.
    • MUST emit the TransferSingle event if from is different than to to reflect delegated ownership change.

batchBind & batchUnbind rules:

  • When performing a batchBind on EIP-1155 bindables:
    • MUST throw if caller is not an approved operator for from.
    • MUST throw if length of tokenIds is not the same as that of amounts or bindIds.
    • MUST throw if any unbound token balances of tokenIds for from are less than that of amounts.
    • MUST throw if to is not bindAddress or the asset owner.
    • After above conditions are met, MUST check if bindAddress is a smart contract (code size > 0). If so, it MUST call onERC1155BatchBind on bindAddress with data passed unaltered and act appropriately (see “Hook Rules”).
    • MUST emit either Bind or BindBatch events to properly reflect asset-delegated ownership attribution for all bound tokens.
    • MUST emit either TransferSingle or TransferBatch events if from is different than to to reflect delegated ownership changes for all tokens.
  • When performing a batchUnbind on EIP-1155 bindables:
    • MUST throw if caller is not the owner of all assets or an approved asset operator for from.
    • MUST throw if length of tokenIds is not the same as that of amounts or bindIds.
    • MUST throw if from is not the owner of all assets.
  • MUST throw if any count in amounts is fewer than the number of tokens bound for the corresponding token-asset pair given by tokenIds and bindIds.
    • MUST throw if to is the zero address.
    • After above conditions are met, MUST check if bindAddress is a smart contract (code size > 0). If so, it MUST call onERC1155Unbind on bindAddress with data passed unaltered and act appropriately (see “Hook Rules”).
    • In addition, it MUST check if to is a smart contract (code size > 0), and call onERC1155Received on to with data passed unaltered and act appropriately (see “Hook Rules”).
    • MUST emit Bind event to reflect asset-bound ownership revocation.
    • MUST emit the TransferSingle event if from is different than to to reflect delegated ownership change.

Bind event rules:

  • When emitting an EIP-721 bindable Bind event:
    • SHOULD be emitted to indicate a single bind has occurred between a tokenId and bindId pair.
    • MAY be emitted multiple times to indicate multiple binds have occurred in a single transaction.
    • The operator argument MUST be the owner of the NFT tokenId, the approved address for the NFT, or the authorized operator of from.
    • The from argument MUST be the owner of the NFT tokenId.
    • The to argument MUST be binderAddress (indicates “delegated” bind) or the owner of the bound asset (indicates “legacy” bind).
    • The tokenId argument MUST be the NFT being bound.
    • The bindId argument MUST be the identifier of the asset being bound to.
    • The bindAddress argument MUST be the contract address of the asset being bound to.
    • When minting NFTs bound to an asset, the Bind event must be emitted with the from argument set to 0x0.
    • Bind events MUST be emitted to reflect asset-bound ownership delegation before calls to onERC721Bind.
  • When emitting an EIP-1155 bindable Bind event:
    • SHOULD be emitted to indicate a bind has occurred between a single tokenId type and binderId pair.
    • MAY be emitted multiple times to indicate multiple binds have occurred in a single transaction, but BindBatch should be preferred in this case to reduce gas consumption.
    • The operator argument MUST be an authorized operator for from.
    • The from argument MUST be the owner of the unbound tokens.
    • The to argument MUST be binderAddress (indicates “delegated” bind) or the owner of the bound asset bindId (indicates “legacy” bind).
    • The tokenId argument MUST be the token type being bound.
    • The amount argument MUST be the number of tokens of type tokenId being bound.
    • The bindId argument MUST be the identifier of the asset being bound to.
    • The bindAddress argument MUST be the contract address of the asset being bound to.
    • When minting NFTs bound to an asset, the Bind event must be emitted with the from argument set to 0x0.
    • Bind events MUST be emitted to reflect asset-bound ownership delegation before calls to onERC1155Bind or onERC1155BindBatch.

Unbind event rules:

  • When emitting an EIP-721 bindable Unbind event:
    • SHOULD be emitted to indicate a single unbind has occurred between a tokenId and bindId pair.
    • MAY be emitted multiple times to indicate multiple unbinds have occurred in a single transaction.
    • The operator argument MUST be the owner of the asset or an approved asset operator for from.
    • The from argument MUST be the owner of the asset.
    • The to argument MUST be the recipient address of the unbound NFT.
    • The tokenId argument MUST be the NFT being unbound.
    • The bindId argument MUST be the identifier of the asset being unbound from.
    • The bindAddress argument MUST be the contract address of the asset being unbound from.
    • When burning NFTs bound to an asset, the Bind event must be emitted with the to argument set to 0x0.
    • Bind events MUST be emitted to reflect delegated ownership revocation changes before calls to onERC721Unbind.
  • When emitting an EIP-1155 bindable Unbind event:
    • SHOULD be emitted to indicate an unbind has occurred between a single tokenId type and binderId pair.
    • MAY be emitted multiple times to indicate multiple unbinds have occurred in a single transaction, but UnbindBatch should be preferred in this case to reduce gas consumption.
    • The operator argument MUST be the owner of the asset or an approved asset operator for from.
    • The from argument MUST be the asset owner.
    • The to argument MUST be the recipient address of the unbound tokens.
    • The tokenId argument MUST be the token type being unbound.
    • The amount argument MUST be the number of tokens of type tokenId being unbound.
    • The bindId argument MUST be the identifier of the asset being unbound from.
    • The bindAddress argument MUST be the contract address of the asset being unbound from.
    • When burning NFTs bound to an asset, the Bind event must be emitted with the to argument set to 0x0.
    • Bind events MUST be emitted to reflect delegated ownership revocation changes before calls to onERC1155Unbind or onERC1155UnbindBatch

BindBatch & UnbindBatch event rules:

  • When emitting a BindBatch event:
    • SHOULD be emitted to indicate a bind has occurred between multiple tokenId and binderId pairs.
    • The operator argument MUST be an authorized operator for from.
    • The from argument MUST be the owner of the unbound tokens.
    • The to argument MUST be binderAddress (indicates “delegated” bind) or the owner of the bound asset (indicates “legacy” bind).
    • The tokenIds argument MUST be the identifiers of the token types being bound.
    • The amounts argument MUST be the number of tokens for each type in tokenIds being bound.
    • The bindIds argument MUST be the identifiers for all assets being bound to.
    • The bindAddress argument MUST be the contract address of the assets being bound to.
    • When batch minting NFTs bound to an asset, the BindBatch event must be emitted with the from argument set to 0x0.
    • BindBatch events MUST be emitted to reflect asset-bound ownership delegation before calls to onERC1155BindBatch
  • When emitting a batchUnbind event:
    • SHOULD be emitted to indicate an unbind has occurred between multiple tokenId and binderId pairs.
    • The operator argument MUST be an authorized operator or owner of the asset.
    • The from argument MUST be the owner of all assets.
    • The to argument MUST be the recipient address of the unbound tokens.
    • The tokenIds argument MUST be the identifiers of the token types being unbound.
    • The amounts argument MUST be the number of tokens for each type tokenId being unbound.
    • The bindIds argument MUST be the identifiers for the assets being unbound from.
    • The bindAddress argument MUST be the contract address of the assets being unbound from.
    • When burning tokens bound to an asset, the UnbindBatch event must be emitted with the to argument set to 0x0.
    • UnbindBatch events MUST be emitted to reflect asset-delegated ownership changes before calls to onERC1155UnbindBatch

bind hook rules:

  • The operator argument MUST be the address calling the bind hook.
  • The from argument MUST be the owner of the NFT or token type being bound.
    • FROM must be 0x0 for a mint.
  • The to argument MUST be binderAddress (indicates “delegated” bind) or the owner of the bound asset (indicates “legacy” bind).
    • The binder contract MAY choose to reject legacy binds.
  • For onERC721Bind / onERC1155Bind, the tokenId argument MUST be the NFT / token type being bound.
  • For onERC1155BatchBind, tokenIds MUST be the list of token types being bound.
  • For onERC1155Bind, the amount argument MUST be the number of tokens of type tokenId being bound.
  • For onERC1155BatchBind, the amounts argument MUST be a list of the number of tokens of each token type being bound.
  • For onERC721Bind / onERC1155Bind, the bindId argument MUST be the identifier for the asset being bound to.
  • For onERC1155BatchBind, bindIds MUST be the list of assets being bound to.
  • The data argument MUST contain data provided by the caller for the bind with contents unaltered.
  • The binder contract MAY accept the bind by returning the binder call’s designated magic value, in which case the bind MUST complete or revert if any other conditions for success are not met:
    • onERC721Bind: bytes4(keccak256("onERC721Bind(address,address,address,uint256,uint256,bytes)"))
    • onERC1155Bind: bytes4(keccak256("onERC1155Bind(address,address,address,uint256,uint256,uint256,bytes)"))
    • onERC1155BindBatch: bytes4(keccak256("onERC1155BindBatch(address,address,address,uint256[],uint256[],uint256[],bytes)"))
  • The binder contract MAY reject the bind by calling revert.
  • A return of any other value than the designated magic value MUST result in the transaction being reverted by the caller.

unbind hook rules:

  • The operator argument MUST be the address calling the unbind hook.
  • The from argument MUST be the asset owner.
  • The to argument MUST the the recipient address of the unbound NFT or token type.
    • TO must be 0x0 for a burn.
  • For onERC721Unbind / onERC1155Unbind, the tokenId argument MUST be the NFT / token type being unbound.
  • For onERC1155BatchUnbind, tokenIds MUST be the list of token types being unbound.
  • For onERC1155Unbind, the amount argument MUST be the number of tokens of type tokenId being unbound.
  • For onERC1155BatchUnbind, the amounts argument MUST be a list of the number of tokens of each token type being unbound.
  • For onERC721Bind / onERC1155Bind, the bindId argument MUST be the identifier for the asset being unbound from.
  • For onERC1155BatchBind, bindIds MUST be the list of assets being unbound from.
  • The data argument MUST contain data provided by the caller for the bind with contents unaltered.
  • The binder contract MAY accept the unbind by returning the binder call’s designated magic value, in which case the unbind MUST complete or MUST revert if any other conditions for success are not met:
    • onERC721Unbind: bytes4(keccak256("onERC721Unbind(address,address,address,uint256,uint256,bytes)"))
    • onERC1155Unbind: bytes4(keccak256("onERC1155Unbind(address,address,address,uint256,uint256,uint256,bytes)"))
    • onERC1155UnbindBatch: bytes4(keccak256("onERC1155UnbindBatch(address,address,address,uint256[],uint256[],uint256[],bytes)"))
  • The binder contract MAY reject the bind by calling revert.
  • A return of any other value than the designated magic value MUST result in the transaction being reverted by the caller.

Rationale

A backwards-compatible standard for token binding unlocks a new layer of composability for allowing wallets, applications, and protocols to interact with, trade and display bundled assets. One example use-case of this is at Dopamine, where microchipped streetwear garments may be bundled with NFTs such as music, avatars, or digital-twins of the garments themselves, by linking chips to binder smart contracts capable of accepting token binds.

Binding Mechanism

In the “delegated” mode, because token ownership is attributed to the contract address of the asset it is bound to, asset ownership modifications are completely decoupled from bound tokens, making bundled transfers efficient as no state management overhead is imposed. This is the recommended binding mode.

The “legacy” binding mode was included purely for backwards-compatibility purposes, so that existing applications that have yet to integrate the standard can still display bundled tokens out-of-the-box. Here, since token ownership is attributed to the owner of the bound asset, asset ownership modifications are coupled to that of its bound tokens, making bundled transfers inefficient as binder contracts are required to track all bound tokens.

Binder and bindable implementations MAY choose to support both modes of binding.

Transfer Mechanism

One important consideration was whether binds should support transfers or not. Indeed, it would be much simpler for binds and unbinds to be processed only by addresses who owns both the bindable tokens and assets being bound to. Going this route, binds would not require any dependence on transfers, as asset-delegated ownership would not change, and applications could simply transfer the assets themselves following prescribed asset transfer rules. However, this was ruled out due to the lack of flexibility offered, especially around friction added for consumers wishing to bind their tokens to unowned assets.

Backwards Compatibility

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

Reference Implementation

For reference EIP-721 implementations supporting “delegated” and “legacy” binding modes:

For reference EIP-1155 implementations supporting only the “delegated” binding mode:

Security Considerations

Bindable contracts supporting the “legacy” binding mode should be cautious with authorizing transfers once their tokens are bound. These should only be authorized as a result of their bound assets being transferred, and careful consideration must be taken when ensuring account balances are properly processed.

Binder contracts supporting the “legacy” binding mode must ensure that any accepted binds are tracked, and that asset transfers result in proper changing of bound token ownership.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Leeren (@leeren), "EIP-5700: Bindable Token Interface [DRAFT]," Ethereum Improvement Proposals, no. 5700, September 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5700.