Non-fungible Token Bound Accounts
相关视频
正文
Abstract
This proposal defines a system which assigns Ethereum accounts to all non-fungible tokens. These token bound accounts allow NFTs to own assets and interact with applications, without requiring changes to existing smart contracts or infrastructure.
Motivation
The ERC-721 standard enabled an explosion of non-fungible token applications. Some notable use cases have included breedable cats, generative artwork, and exchange liquidity positions.
However, NFTs cannot act as agents or associate with other on-chain assets. This limitation makes it difficult to represent many real-world non-fungible assets as NFTs. For example:
- A character in a role-playing game that accumulates assets and abilities over time based on actions they have taken
- An automobile composed of many fungible and non-fungible components
- An investment portfolio composed of multiple fungible assets
- A punch pass membership card granting access to an establishment and recording a history of past interactions
This proposal aims to give every NFT the same rights as an Ethereum user. This includes the ability to self-custody assets, execute arbitrary operations, control multiple independent accounts, and use accounts across multiple chains. By doing so, this proposal allows complex real-world assets to be represented as NFTs using a common pattern that mirrors Etherem's existing ownership model.
This is accomplished by defining a singleton registry which assigns unique, deterministic smart contract account addresses to all existing and future NFTs. Each account is permanently bound to a single NFT, with control of the account granted to the holder of that NFT.
The pattern defined in this proposal does not require any changes to existing NFT smart contracts. It is also compatible out of the box with nearly all existing infrastructure that supports Ethereum accounts, from on-chain protocols to off-chain indexers. Token bound accounts are compatible with every existing on-chain asset standard, and can be extended to support new asset standards created in the future.
By giving every NFT the full capabilities of an Ethereum account, this proposal enables many novel use cases for existing and future NFTs.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Overview
The system outlined in this proposal has two main components:
- A singleton registry for token bound accounts
- A common interface for token bound account implementations
The following diagram illustrates the relationship between NFTs, NFT holders, token bound accounts, and the Registry:
Registry
The registry is a singleton contract that serves as the entry point for all token bound account address queries. It has two functions:
createAccount
- creates the token bound account for an NFT given animplementation
addressaccount
- computes the token bound account address for an NFT given animplementation
address
The registry is permissionless, immutable, and has no owner. The complete source code for the registry can be found in the Registry Implementation section. The registry MUST be deployed at address 0x000000006551c19487814612e58FE06813775758
using Nick's Factory (0x4e59b44847b379578588920cA78FbF26c0B4956C
) with salt 0x0000000000000000000000000000000000000000fd8eb4e1dca713016c518e31
.
The registry can be deployed to any EVM-compatible chain using the following transaction:
{
"to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
"value": "0x0",
"data": "0x0000000000000000000000000000000000000000fd8eb4e1dca713016c518e31608060405234801561001057600080fd5b5061023b806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063246a00211461003b5780638a54c52f1461006a575b600080fd5b61004e6100493660046101b7565b61007d565b6040516001600160a01b03909116815260200160405180910390f35b61004e6100783660046101b7565b6100e1565b600060806024608c376e5af43d82803e903d91602b57fd5bf3606c5285605d52733d60ad80600a3d3981f3363d3d373d3d3d363d7360495260ff60005360b76055206035523060601b60015284601552605560002060601b60601c60005260206000f35b600060806024608c376e5af43d82803e903d91602b57fd5bf3606c5285605d52733d60ad80600a3d3981f3363d3d373d3d3d363d7360495260ff60005360b76055206035523060601b600152846015526055600020803b61018b578560b760556000f580610157576320188a596000526004601cfd5b80606c52508284887f79f19b3655ee38b1ce526556b7731a20c8f218fbda4a3990b6cc4172fdf887226060606ca46020606cf35b8060601b60601c60005260206000f35b80356001600160a01b03811681146101b257600080fd5b919050565b600080600080600060a086880312156101cf57600080fd5b6101d88661019b565b945060208601359350604086013592506101f46060870161019b565b94979396509194608001359291505056fea2646970667358221220ea2fe53af507453c64dd7c1db05549fa47a298dfb825d6d11e1689856135f16764736f6c63430008110033",
}
The registry MUST deploy each token bound account as an ERC-1167 minimal proxy with immutable constant data appended to the bytecode.
The deployed bytecode of each token bound account MUST have the following structure:
ERC-1167 Header (10 bytes)
<implementation (address)> (20 bytes)
ERC-1167 Footer (15 bytes)
<salt (bytes32)> (32 bytes)
<chainId (uint256)> (32 bytes)
<tokenContract (address)> (32 bytes)
<tokenId (uint256)> (32 bytes)
For example, the token bound account with implementation address 0xbebebebebebebebebebebebebebebebebebebebe
, salt 0
, chain ID 1
, token contract 0xcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcf
and token ID 123
would have the following deployed bytecode:
363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcf000000000000000000000000000000000000000000000000000000000000007b
Each token bound account proxy MUST delegate execution to a contract that implements the IERC6551Account
interface.
The registry MUST deploy all token bound accounts using the create2
opcode so that each account address is deterministic. Each token bound account address SHALL be derived from the unique combination of its implementation address, token contract address, token ID, chain ID, and salt.
The registry MUST implement the following interface:
interface IERC6551Registry { /** * @dev The registry MUST emit the ERC6551AccountCreated event upon successful account creation. */ event ERC6551AccountCreated( address account, address indexed implementation, bytes32 salt, uint256 chainId, address indexed tokenContract, uint256 indexed tokenId ); /** * @dev The registry MUST revert with AccountCreationFailed error if the create2 operation fails. */ error AccountCreationFailed(); /** * @dev Creates a token bound account for a non-fungible token. * * If account has already been created, returns the account address without calling create2. * * Emits ERC6551AccountCreated event. * * @return account The address of the token bound account */ function createAccount( address implementation, bytes32 salt, uint256 chainId, address tokenContract, uint256 tokenId ) external returns (address account); /** * @dev Returns the computed token bound account address for a non-fungible token. * * @return account The address of the token bound account */ function account( address implementation, bytes32 salt, uint256 chainId, address tokenContract, uint256 tokenId ) external view returns (address account); }
Account Interface
All token bound accounts SHOULD be created via the singleton registry.
All token bound account implementations MUST implement ERC-165 interface detection.
All token bound account implementations MUST implement ERC-1271 signature validation.
All token bound account implementations MUST implement the following interface:
/// @dev the ERC-165 identifier for this interface is `0x6faff5f1` interface IERC6551Account { /** * @dev Allows the account to receive Ether. * * Accounts MUST implement a `receive` function. * * Accounts MAY perform arbitrary logic to restrict conditions * under which Ether can be received. */ receive() external payable; /** * @dev Returns the identifier of the non-fungible token which owns the account. * * The return value of this function MUST be constant - it MUST NOT change over time. * * @return chainId The chain ID of the chain the token exists on * @return tokenContract The contract address of the token * @return tokenId The ID of the token */ function token() external view returns (uint256 chainId, address tokenContract, uint256 tokenId); /** * @dev Returns a value that SHOULD be modified each time the account changes state. * * @return The current account state */ function state() external view returns (uint256); /** * @dev Returns a magic value indicating whether a given signer is authorized to act on behalf * of the account. * * MUST return the bytes4 magic value 0x523e3260 if the given signer is valid. * * By default, the holder of the non-fungible token the account is bound to MUST be considered * a valid signer. * * Accounts MAY implement additional authorization logic which invalidates the holder as a * signer or grants signing permissions to other non-holder accounts. * * @param signer The address to check signing authorization for * @param context Additional data used to determine whether the signer is valid * @return magicValue Magic value indicating whether the signer is valid */ function isValidSigner(address signer, bytes calldata context) external view returns (bytes4 magicValue); }
Execution Interface
All token bound accounts MUST implement an execution interface which allows valid signers to execute arbitrary operations on behalf of the account. Support for an execution interface MUST be signaled by the account using ERC-165 interface detection.
Token bound accounts MAY support the following execution interface:
/// @dev the ERC-165 identifier for this interface is `0x51945447` interface IERC6551Executable { /** * @dev Executes a low-level operation if the caller is a valid signer on the account. * * Reverts and bubbles up error if operation fails. * * Accounts implementing this interface MUST accept the following operation parameter values: * - 0 = CALL * - 1 = DELEGATECALL * - 2 = CREATE * - 3 = CREATE2 * * Accounts implementing this interface MAY support additional operations or restrict a signer's * ability to execute certain operations. * * @param to The target address of the operation * @param value The Ether value to be sent to the target * @param data The encoded operation calldata * @param operation A value indicating the type of operation to perform * @return The result of the operation */ function execute(address to, uint256 value, bytes calldata data, uint8 operation) external payable returns (bytes memory); }
Rationale
Singleton Registry
This proposal specifies a single, canonical registry that can be permissionlessly deployed to any chain at a known address. It purposefully does not specify a common interface that can be implemented by multiple registry contracts. This approach enables several critical properties.
Counterfactual Accounts
All token bound accounts are created using the create2 opcode, enabling accounts to exist in a counterfactual state prior to their creation. This allows token bound accounts to receive assets prior to contract creation. A singleton account registry ensures a common addressing scheme is used for all token bound account addresses.
Trustless Deployments
A single ownerless registry ensures that the only trusted contract for any token bound account is the implementation. This guarantees the holder of a token access to all assets stored within a counterfactual account using a trusted implementation.
Without a canonical registry, some token bound accounts may be deployed using an owned or upgradable registry. This may lead to loss of assets stored in counterfactual accounts, and increases the scope of the security model that applications supporting this proposal must consider.
Cross-chain Compatibility
A singleton registry with a known address enables each token bound account to exist on multiple chains. The inclusion of chainId
as a parameter to createAccount
allows the contract for a token bound account to be deployed at the same address on any supported chain. Account implementations are therefore able to support cross-chain account execution, where an NFT on one chain can control its token bound account on another chain.
Single Entry Point
A single entry point for querying account addresses and AccountCreated
events simplifies the complex task of indexing token bound accounts in applications which support this proposal.
Implementation Diversity
A singleton registry allows diverse account implementations to share a common addressing scheme. This gives developers significant freedom to implement both account-specific features (e.g. delegation) as well as alternative account models (e.g. ephemeral accounts) in a way that can be easily supported by client applications.
Registry vs Factory
The term "registry" was chosen instead of "factory" to highlight the canonical nature of the contract and emphasize the act of querying account addresses (which occurs regularly) over the creation of accounts (which occurs only once per account).
Variable Execution Interface
This proposal does not require accounts to implement a specific execution interface in order to be compatible, so long as they signal support for at least one execution interface via ERC-165 interface detection. Allowing account developers to choose their own execution interface allows this proposal to support the wide variety of existing execution interfaces and maintain forward compatibility with likely future standardized interfaces.
Account Ambiguity
The specification proposed above allows NFTs to have multiple token bound accounts. During the development of this proposal, alternative architectures were considered which would have assigned a single token bound account to each NFT, making each token bound account address an unambiguous identifier.
However, these alternatives present several trade offs.
First, due to the permissionless nature of smart contracts, it is impossible to enforce a limit of one token bound account per NFT. Anyone wishing to utilize multiple token bound accounts per NFT could do so by deploying an additional registry contract.
Second, limiting each NFT to a single token bound account would require a static, trusted account implementation to be included in this proposal. This implementation would inevitably impose specific constraints on the capabilities of token bound accounts. Given the number of unexplored use cases this proposal enables and the benefit that diverse account implementations could bring to the non-fungible token ecosystem, it is the authors' opinion that defining a canonical and constrained implementation in this proposal is premature.
Finally, this proposal seeks to grant NFTs the ability to act as agents on-chain. In current practice, on-chain agents often utilize multiple accounts. A common example is individuals who use a "hot" account for daily use and a "cold" account for storing valuables. If on-chain agents commonly use multiple accounts, it stands to reason that NFTs ought to inherit the same ability.
Proxy Implementation
ERC-1167 minimal proxies are well supported by existing infrastructure and are a common smart contract pattern. This proposal deploys each token bound account using a custom ERC-1167 proxy implementation that stores the salt, chain id, token contract address, and token ID as ABI-encoded constant data appended to the contract bytecode. This allows token bound account implementations to easily query this data while ensuring it remains constant. This approach was taken to maximize compatibility with existing infrastructure while also giving smart contract developers full flexibility when creating custom token bound account implementations.
Chain Identifier
This proposal uses the chain ID to identify each NFT along with its contract address and token ID. Token identifiers are globally unique on a single Ethereum chain, but may not be unique across multiple Ethereum chains.
Backwards Compatibility
This proposal seeks to be maximally backwards compatible with existing non-fungible token contracts. As such, it does not extend the ERC-721 standard.
Additionally, this proposal does not require the registry to perform an ERC-165 interface check for ERC-721 compatibility prior to account creation. This maximizes compatibility with non-fungible token contracts that pre-date the ERC-721 standard (such as CryptoKitties) or only implement a subset of the ERC-721 interface (such as ENS NameWrapper names). It also allows the system described in this proposal to be used with semi-fungible or fungible tokens, although these use cases are outside the scope of the proposal.
Smart contract authors may optionally choose to enforce interface detection for ERC-721 in their account implementations.
Reference Implementation
Example Account Implementation
pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/interfaces/IERC1271.sol"; import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; interface IERC6551Account { receive() external payable; function token() external view returns (uint256 chainId, address tokenContract, uint256 tokenId); function state() external view returns (uint256); function isValidSigner(address signer, bytes calldata context) external view returns (bytes4 magicValue); } interface IERC6551Executable { function execute(address to, uint256 value, bytes calldata data, uint8 operation) external payable returns (bytes memory); } contract ERC6551Account is IERC165, IERC1271, IERC6551Account, IERC6551Executable { uint256 immutable deploymentChainId = block.chainid; uint256 public state; receive() external payable {} function execute(address to, uint256 value, bytes calldata data, uint8 operation) external payable virtual returns (bytes memory result) { require(_isValidSigner(msg.sender), "Invalid signer"); require(operation == 0, "Only call operations are supported"); ++state; bool success; (success, result) = to.call{value: value}(data); if (!success) { assembly { revert(add(result, 32), mload(result)) } } } function isValidSigner(address signer, bytes calldata) external view virtual returns (bytes4) { if (_isValidSigner(signer)) { return IERC6551Account.isValidSigner.selector; } return bytes4(0); } function isValidSignature(bytes32 hash, bytes memory signature) external view virtual returns (bytes4 magicValue) { bool isValid = SignatureChecker.isValidSignatureNow(owner(), hash, signature); if (isValid) { return IERC1271.isValidSignature.selector; } return bytes4(0); } function supportsInterface(bytes4 interfaceId) external pure virtual returns (bool) { return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC6551Account).interfaceId || interfaceId == type(IERC6551Executable).interfaceId; } function token() public view virtual returns (uint256, address, uint256) { bytes memory footer = new bytes(0x60); assembly { extcodecopy(address(), add(footer, 0x20), 0x4d, 0x60) } return abi.decode(footer, (uint256, address, uint256)); } function owner() public view virtual returns (address) { (uint256 chainId, address tokenContract, uint256 tokenId) = token(); if (chainId != deploymentChainId) return address(0); return IERC721(tokenContract).ownerOf(tokenId); } function _isValidSigner(address signer) internal view virtual returns (bool) { return signer == owner(); } }
Registry Implementation
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; interface IERC6551Registry { /** * @dev The registry MUST emit the ERC6551AccountCreated event upon successful account creation. */ event ERC6551AccountCreated( address account, address indexed implementation, bytes32 salt, uint256 chainId, address indexed tokenContract, uint256 indexed tokenId ); /** * @dev The registry MUST revert with AccountCreationFailed error if the create2 operation fails. */ error AccountCreationFailed(); /** * @dev Creates a token bound account for a non-fungible token. * * If account has already been created, returns the account address without calling create2. * * Emits ERC6551AccountCreated event. * * @return account The address of the token bound account */ function createAccount( address implementation, bytes32 salt, uint256 chainId, address tokenContract, uint256 tokenId ) external returns (address account); /** * @dev Returns the computed token bound account address for a non-fungible token. * * @return account The address of the token bound account */ function account( address implementation, bytes32 salt, uint256 chainId, address tokenContract, uint256 tokenId ) external view returns (address account); } contract ERC6551Registry is IERC6551Registry { function createAccount( address implementation, bytes32 salt, uint256 chainId, address tokenContract, uint256 tokenId ) external returns (address) { assembly { // Memory Layout: // ---- // 0x00 0xff (1 byte) // 0x01 registry (address) (20 bytes) // 0x15 salt (bytes32) (32 bytes) // 0x35 Bytecode Hash (bytes32) (32 bytes) // ---- // 0x55 ERC-1167 Constructor + Header (20 bytes) // 0x69 implementation (address) (20 bytes) // 0x5D ERC-1167 Footer (15 bytes) // 0x8C salt (uint256) (32 bytes) // 0xAC chainId (uint256) (32 bytes) // 0xCC tokenContract (address) (32 bytes) // 0xEC tokenId (uint256) (32 bytes) // Silence unused variable warnings pop(chainId) // Copy bytecode + constant data to memory calldatacopy(0x8c, 0x24, 0x80) // salt, chainId, tokenContract, tokenId mstore(0x6c, 0x5af43d82803e903d91602b57fd5bf3) // ERC-1167 footer mstore(0x5d, implementation) // implementation mstore(0x49, 0x3d60ad80600a3d3981f3363d3d373d3d3d363d73) // ERC-1167 constructor + header // Copy create2 computation data to memory mstore(0x35, keccak256(0x55, 0xb7)) // keccak256(bytecode) mstore(0x15, salt) // salt mstore(0x01, shl(96, address())) // registry address mstore8(0x00, 0xff) // 0xFF // Compute account address let computed := keccak256(0x00, 0x55) // If the account has not yet been deployed if iszero(extcodesize(computed)) { // Deploy account contract let deployed := create2(0, 0x55, 0xb7, salt) // Revert if the deployment fails if iszero(deployed) { mstore(0x00, 0x20188a59) // `AccountCreationFailed()` revert(0x1c, 0x04) } // Store account address in memory before salt and chainId mstore(0x6c, deployed) // Emit the ERC6551AccountCreated event log4( 0x6c, 0x60, // `ERC6551AccountCreated(address,address,bytes32,uint256,address,uint256)` 0x79f19b3655ee38b1ce526556b7731a20c8f218fbda4a3990b6cc4172fdf88722, implementation, tokenContract, tokenId ) // Return the account address return(0x6c, 0x20) } // Otherwise, return the computed account address mstore(0x00, shr(96, shl(96, computed))) return(0x00, 0x20) } } function account( address implementation, bytes32 salt, uint256 chainId, address tokenContract, uint256 tokenId ) external view returns (address) { assembly { // Silence unused variable warnings pop(chainId) pop(tokenContract) pop(tokenId) // Copy bytecode + constant data to memory calldatacopy(0x8c, 0x24, 0x80) // salt, chainId, tokenContract, tokenId mstore(0x6c, 0x5af43d82803e903d91602b57fd5bf3) // ERC-1167 footer mstore(0x5d, implementation) // implementation mstore(0x49, 0x3d60ad80600a3d3981f3363d3d373d3d3d363d73) // ERC-1167 constructor + header // Copy create2 computation data to memory mstore(0x35, keccak256(0x55, 0xb7)) // keccak256(bytecode) mstore(0x15, salt) // salt mstore(0x01, shl(96, address())) // registry address mstore8(0x00, 0xff) // 0xFF // Store computed account address in memory mstore(0x00, shr(96, shl(96, keccak256(0x00, 0x55)))) // Return computed account address return(0x00, 0x20) } } }
Security Considerations
Fraud Prevention
In order to enable trustless sales of token bound accounts, decentralized marketplaces will need to implement safeguards against fraudulent behavior by malicious account owners.
Consider the following potential scam:
- Alice owns an ERC-721 token X, which owns token bound account Y.
- Alice deposits 10ETH into account Y
- Bob offers to purchase token X for 11ETH via a decentralized marketplace, assuming he will receive the 10ETH stored in account Y along with the token
- Alice withdraws 10ETH from the token bound account, and immediately accepts Bob's offer
- Bob receives token X, but account Y is empty
To mitigate fraudulent behavior by malicious account owners, decentralized marketplaces SHOULD implement protection against these sorts of scams at the marketplace level. Contracts which implement this EIP MAY also implement certain protections against fraudulent behavior.
Here are a few mitigations strategies to be considered:
- Attach the current token bound account state to the marketplace order. If the state of the account has changed since the order was placed, consider the offer void. This functionality would need to be supported at the marketplace level.
- Attach a list of asset commitments to the marketplace order that are expected to remain in the token bound account when the order is fulfilled. If any of the committed assets have been removed from the account since the order was placed, consider the offer void. This would also need to be implemented by the marketplace.
- Submit the order to the decentralized market via an external smart contract which performs the above logic before validating the order signature. This allows for safe transfers to be implemented without marketplace support.
- Implement a locking mechanism on the token bound account implementation that prevents malicious owners from extracting assets from the account while locked
Preventing fraud is outside the scope of this proposal.
Ownership Cycles
All assets held in a token bound account may be rendered inaccessible if an ownership cycle is created. The simplest example is the case of an ERC-721 token being transferred to its own token bound account. If this occurs, both the ERC-721 token and all of the assets stored in the token bound account would be permanently inaccessible, since the token bound account is incapable of executing a transaction which transfers the ERC-721 token.
Ownership cycles can be introduced in any graph of n>0 token bound accounts. On-chain prevention of cycles with depth>1 is difficult to enforce given the infinite search space required, and as such is outside the scope of this proposal. Application clients and account implementations wishing to adopt this proposal are encouraged to implement measures that limit the possibility of ownership cycles.
Copyright
Copyright and related rights waived via CC0.