--- eip: 5700 title: Bindable Token Interface description: Interface for binding fungible and non-fungible tokens to assets. author: Leeren (@leeren) discussions-to: https://ethereum-magicians.org/t/eip-5700-bindable-token-standard/11077 status: Draft type: Standards Track category: ERC created: 2022-09-22 requires: 165, 721, 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](./eip-721.md) or [EIP-1155](./eip-1155.md) 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](./eip-721.md) 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](./eip-721.md) and [EIP-1155](./eip-1155.md) 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.** ```solidity /// @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.** ```solidity /// @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.** ```solidity /// @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.** ```solidity 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: - [EIP-721 Bindable](../assets/eip-5700/erc721/ERC721Bindable.sol). - [EIP-721 Binder](../assets/eip-5700/erc721/ERC721Binder.sol). For reference EIP-1155 implementations supporting only the "delegated" binding mode: - [EIP-1155 Bindable](../assets/eip-5700/erc1155/ERC1155Bindable.sol). - [EIP-1155 Binder](../assets/eip-5700/erc1155/ERC1155Binder.sol). ## 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 Copyright and related rights waived via [CC0](../LICENSE.md).