HomeEIPs
EIPsERC-7656
ERC-7656

Generalized Token-Linked Services

Define a registry for generic services linked to a specific NFT
DraftStandards Track: ERC
Created: 2024-03-15
Requires: EIP-165, EIP-1167, EIP-5313, EIP-6551
Francesco Sullo (@sullof)
DiscussionsOriginal linkEdit
1 min read
Anyone may contribute to propose contents.
Go propose
Video
Anyone may contribute to propose contents.
Go propose
Original

Abstract

This proposal introduces a variation of ERC-6551 that extends to all types of services linked to non-fungible tokens (NFTs), i.e., contracts extending an NFT, owned by a single NFT and thus by the owner of the NFT. It achieves this goal using generic language for functions, errors, and events, and avoids conflicting with the strict restrictions imposed by the original proposal.

Motivation

ERC-6551 aims to bind smart accounts to tokens, allowing its registry to deploy accounts owned by a specific tokenID. The issue we attempt to address with this new proposal is that ERC-6551 explicitly requires any contract deployed via the ERC6551Registry to implement IERC6551Account and IERC6551Execute, i.e., it must be an account. This requirement is underscored by the choices for the names of functions and events in the interface. Additionally, ERC-6551 specifies that the ERC6551Registry smart contract is deployed as a singleton at a specific address on any chain. Due to this centralization of services, projects building on it are prone to consider any contract deployed via that registry that is not an account as spam or invalid.

With this new ERC, we propose a more generic registry that uses generic function/event names to allow the deployment of any kind of contract that makes sense when associated with an NFT, so that the contract is under the full control of the NFT's owner. In comparison with ERC-6551, since one of this proposal's goals is flexibility, there is no expectation for an ERC7656Registry contract to be deployed as a singleton, allowing any project to adjust it to their needs; consequently, we require that any registry explicitly supports the IERC7656Registry interface.

The expansion of the registry's capabilities to manage contracts implementing any kind of service beyond accounts provides several advantages:

  • Flexibility: Developers can allow NFTs to interact with a broader range of linked contracts, unlocking new use cases and functionalities (lending systems, vested asset distribution, fractional ownership, identity, etc.)
  • Compatibility: By ensuring that account-like contracts can still be identified as such, the proposal maintains backward compatibility with ERC-6551.
  • Innovation: This proposal encourages further innovation in the NFT space by removing limitations on the types of contracts that can be associated with NFTs, opening the door to pure-utility 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.

The interface IERC7656Registry is defined as follows:

// interfaceId 0xc6bdc908 interface IERC7656Registry { /** * @notice The registry MUST emit the Created event upon successful contract creation. * @param contractAddress The address of the created contract * @param implementation The address of the implementation contract * @param salt The salt to use for the create2 operation * @param chainId The chain id of the chain where the contract is being created * @param tokenContract The address of the token contract * @param tokenId The id of the token */ event Created( address contractAddress, address indexed implementation, bytes32 salt, uint256 chainId, address indexed tokenContract, uint256 indexed tokenId ); /** * The registry MUST revert with CreationFailed error if the create2 operation fails. */ error CreationFailed(); /** * @notice Creates a token linked account for a non-fungible token. * If account has already been created, returns the account address without calling create2. * @param implementation The address of the implementation contract * @param salt The salt to use for the create2 operation * @param chainId The chain id of the chain where the account is being created * @param tokenContract The address of the token contract * @param tokenId The id of the token * Emits Created event. * @return account The address of the token linked account */ function create( address implementation, bytes32 salt, uint256 chainId, address tokenContract, uint256 tokenId ) external returns (address account); /** * @notice Returns the computed token linked account address for a non-fungible token. * @param implementation The address of the implementation contract * @param salt The salt to use for the create2 operation * @param chainId The chain id of the chain where the account is being created * @param tokenContract The address of the token contract * @param tokenId The id of the token * @return account The address of the token linked account */ function compute( address implementation, bytes32 salt, uint256 chainId, address tokenContract, uint256 tokenId ) external view returns (address account); }

Any ERC7656Registry implementation MUST support the IERC7656Registry's interface ID, i.e., 0xc6bdc908.

Similarly to ERC-6551, The registry MUST deploy each token linked 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)

Any contract created using a ERC7656Registry SHOULD implement the IERC7656Service interface:

<<<<<<< HEAD interface IERC7656Service { ======= interface IERC7656Linked { >>>>>>> master /** * @notice Returns the token linked to the contract * @return chainId The chainId of the token * @return tokenContract The address of the token contract * @return tokenId The tokenId of the token */ function token() external view returns (uint256 chainId, address tokenContract, uint256 tokenId); }

or the IERC6551Account interface or both. This flexibility makes ERC-6551 accounts compatible with this proposal out-of-the-box.

Rationale

The technical foundation of ERC-7656 centers on the extension and generalization of contract types that can be associated with NFTs. This approach was chosen to address specific limitations and opportunities identified in the design and application of NFT-linked contracts. Key technical decisions in this proposal include:

  • Generic Function/Event Names: The choice to adopt a generic naming convention for functions, errors, and events is deliberate. This design decision enables the ERC-7656 registry to support a wide array of contract types beyond mere accounts. By not prescribing specific roles or functionalities, we allow for greater innovation and flexibility in the types of applications that can be built on this standard. This also simplifies the interface and makes it more adaptable to various use cases.

  • No Singleton Requirement for the Registry: Unlike its predecessor, ERC-7656 does not mandate that the registry be deployed as a singleton. This decision was influenced by the recognition that different projects may have unique requirements and constraints. By allowing for multiple instances of the registry, projects can customize and optimize the registry's deployment to fit their specific needs, enhancing the ecosystem's overall diversity and resilience.

  • Explicit Support for the IERC7656Registry Interface: Requiring that any registry explicitly supports the IERC7656Registry interface is a technical decision aimed at ensuring interoperability and recognition. This requirement facilitates the identification and interaction with compliant registries, promoting a more standardized and cohesive ecosystem.

  • Flexibility in Contract Association: The proposal is designed to accommodate not just accounts, but any contract that can meaningfully be associated with an NFT. This decision stems from a technical evaluation of the evolving landscape of NFT use cases, recognizing the need for a standard that can support a broader range of functionalities, from complex financial instruments to identity verification systems.

  • Backward Compatibility: The proposal includes provisions for backward compatibility, particularly with account-like contracts from ERC-6551. This technical choice ensures that projects built on the earlier standard can transition to or leverage the new standard without discarding existing infrastructure or investments.

These technical decisions collectively aim to broaden the scope and applicability of NFT-linked contracts, empower developers with more tools for innovation, and support a growing ecosystem of decentralized applications. By addressing both current limitations and future opportunities, ERC-7656 seeks to lay a flexible and robust foundation for the next generation of NFT technologies.

Reference Implementation

The reference implementation of ERC7656Registry is mutated from ERC6551Registry, with minor changes to rename over-specific functions, emit a different event and error, and a supportsInterface function that returns true only for the IERC7656Registry interfaceId.

contract ERC7656Registry is IERC7656Registry { function create( address implementation, bytes32 salt, uint256 /* chainId */, address tokenContract, uint256 tokenId ) external override returns (address) { // solhint-disable-next-line no-inline-assembly 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) // 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 mstore8(0x00, 0xff) // 0xFF mstore(0x35, keccak256(0x55, 0xb7)) // keccak256(bytecode) mstore(0x01, shl(96, address())) // registry address mstore(0x15, salt) // salt // 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, 0xd786d393) // `CreationFailed()` revert(0x1c, 0x04) } // Store account address in memory before salt and chainId mstore(0x6c, deployed) // Emit the Created event log4( 0x6c, 0x60, 0xc6989e4f290074742210cbd6491de7ded9cfe2cd247932a53d31005007a6341a, 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 compute( address implementation, bytes32 salt, uint256 /* chainId */, address /* tokenContract */, uint256 /* tokenId */ ) external view override returns (address) { // solhint-disable-next-line no-inline-assembly assembly { // 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 mstore8(0x00, 0xff) // 0xFF mstore(0x35, keccak256(0x55, 0xb7)) // keccak256(bytecode) mstore(0x01, shl(96, address())) // registry address mstore(0x15, salt) // salt // Store computed account address in memory mstore(0x00, shr(96, shl(96, keccak256(0x00, 0x55)))) // Return computed account address return(0x00, 0x20) } } /// @dev Returns true if interfaceId is IERC7656Registry's interfaceId /// This contract does not explicitly extend IERC165 to keep the bytecode as small as possible function supportsInterface(bytes4 interfaceId) external pure returns (bool) { return interfaceId == 0xc6bdc908; } }

An example of implementation of IERC7656Service:

contract LinkedService is IERC7656Service, EIP5313 { 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 override returns (address) { (uint256 chainId, address tokenContract_, uint256 tokenId_) = token(); if (chainId != block.chainid) return address(0); return IERC721(tokenContract_).ownerOf(tokenId_); } }

Security Considerations

This proposal does not introduce any new security considerations beyond those already addressed in ERC-6551.

Copyright and related rights waived via CC0.

Further reading
Anyone may contribute to propose contents.
Go propose
Adopted by projects
Anyone may contribute to propose contents.
Go propose

Not miss a beat of EIPs' update?

Subscribe EIPs Fun to receive the latest updates of EIPs Good for Buidlers to follow up.

View all
Serve Ethereum Builders, Scale the Community.
Resources
GitHub
Supported by