diff --git a/ERCS/erc-6551.md b/ERCS/erc-6551.md index 7bf3d13f8f..830723cbc2 100644 --- a/ERCS/erc-6551.md +++ b/ERCS/erc-6551.md @@ -2,13 +2,13 @@ eip: 6551 title: Non-fungible Token Bound Accounts description: An interface and registry for smart contract accounts owned by non-fungible tokens -author: Jayden Windle (@jaydenwindle), Benny Giang , Steve Jang, Druzy Downs (@druzydowns), Raymond Huynh (@huynhr), Alanah Lam , Wilkins Chung (@wwhchung) , Paul Sullivan (@sullivph) , Auryn Macmillan (@auryn-macmillan), Jan-Felix Schwarz (@jfschwarz), Anton Bukov (@k06a), Mikhail Melnik (@ZumZoom), Josh Weintraub (@jhweintraub) , Rob Montgomery (@RobAnon) +author: Jayden Windle (@jaydenwindle), Benny Giang , Steve Jang, Druzy Downs (@druzydowns), Raymond Huynh (@huynhr), Alanah Lam , Wilkins Chung (@wwhchung) , Paul Sullivan (@sullivph) , Auryn Macmillan (@auryn-macmillan), Jan-Felix Schwarz (@jfschwarz), Anton Bukov (@k06a), Mikhail Melnik (@ZumZoom), Josh Weintraub (@jhweintraub) , Rob Montgomery (@RobAnon) , vectorized (@vectorized) discussions-to: https://ethereum-magicians.org/t/non-fungible-token-bound-accounts/13030 status: Review type: Standards Track category: ERC created: 2023-02-23 -requires: 155, 165, 721, 1167, 1271 +requires: 165, 721, 1167, 1271 --- ## Abstract @@ -50,20 +50,32 @@ The following diagram illustrates the relationship between NFTs, NFT holders, to ### Registry -The registry serves as a single entry point for all token bound account address queries. It has two functions: +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 an `implementation` address - `account` - computes the token bound account address for an NFT given an `implementation` address -The registry SHALL deploy each token bound account as an [ERC-1167](./eip-1167.md) minimal proxy with immutable constant data appended to the bytecode. +The registry is permissionless, immutable, and has no owner. The complete source code for the registry can be found in the [Registry Implementation](#registry-implementation) section. The registry MUST be deployed at address `0x000000006551c19487814612e58FE06813775758` using Nick's Factory (`0x4e59b44847b379578588920cA78FbF26c0B4956C`) with salt `0x0000000000000000000000000000000000000000fd8eb4e1dca713016c518e31`. -The deployed bytecode of each token bound account SHALL have the following structure: +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](./eip-1167.md) 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) (20 bytes) ERC-1167 Footer (15 bytes) - (32 bytes) + (32 bytes) (32 bytes) (32 bytes) (32 bytes) @@ -75,67 +87,66 @@ For example, the token bound account with implementation address `0xbebebebebebe 363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcf000000000000000000000000000000000000000000000000000000000000007b ``` -Each token bound account proxy SHALL delegate execution to a contract that implements the `IERC6551Account` interface. +Each token bound account proxy MUST delegate execution to a contract that implements the `IERC6551Account` interface. -The registry contract is permissionless, immutable, and has no owner. The complete source code for the registry can be found in the [Registry Implementation](#registry-implementation) section below. The registry SHALL be deployed at address `TBD` using Nick's Factory (`0x4e59b44847b379578588920cA78FbF26c0B4956C`) with salt `0x6551655165516551655165516551655165516551655165516551655165516551`. +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 SHALL 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, [EIP-155](./eip-155.md) chain ID, and salt. - -The registry SHALL implement the following interface: +The registry MUST implement the following interface: ```solidity interface IERC6551Registry { /** - * @dev The registry SHALL emit the AccountCreated event upon successful account creation + * @dev The registry MUST emit the ERC6551AccountCreated event upon successful account creation. */ - event AccountCreated( + event ERC6551AccountCreated( address account, address indexed implementation, + bytes32 salt, uint256 chainId, address indexed tokenContract, - uint256 indexed tokenId, - uint256 salt + 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. * - * If initData is not empty and account has not yet been created, calls account with - * provided initData after creation. - * - * Emits AccountCreated event. + * Emits ERC6551AccountCreated event. * - * @return the address of the account + * @return account The address of the token bound account */ function createAccount( address implementation, + bytes32 salt, uint256 chainId, address tokenContract, - uint256 tokenId, - uint256 salt, - bytes calldata initData - ) external returns (address); + uint256 tokenId + ) external returns (address account); /** - * @dev Returns the computed token bound account address for a non-fungible token + * @dev Returns the computed token bound account address for a non-fungible token. * - * @return The computed address of the token bound account + * @return account The address of the token bound account */ function account( address implementation, + bytes32 salt, uint256 chainId, address tokenContract, - uint256 tokenId, - uint256 salt - ) external view returns (address); + uint256 tokenId + ) external view returns (address account); } ``` ### Account Interface -All token bound accounts SHOULD be created via the registry. +All token bound accounts SHOULD be created via the singleton registry. All token bound account implementations MUST implement [ERC-165](./eip-165.md) interface detection. @@ -147,7 +158,7 @@ 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 + * @dev Allows the account to receive Ether. * * Accounts MUST implement a `receive` function. * @@ -157,41 +168,37 @@ interface IERC6551Account { receive() external payable; /** - * @dev Returns the identifier of the non-fungible token which owns the account + * @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 + * The return value of this function MUST be constant - it MUST NOT change over time. * - * @return chainId The EIP-155 ID of the chain the token exists on + * @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 - ); + returns (uint256 chainId, address tokenContract, uint256 tokenId); /** - * @dev Returns a value that SHOULD be modified each time the account changes state + * @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 + * @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 + * 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 + * 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 + * 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 @@ -202,6 +209,7 @@ interface IERC6551Account { view returns (bytes4 magicValue); } + ``` ### Execution Interface @@ -211,17 +219,12 @@ All token bound accounts MUST implement an execution interface which allows vali Token bound accounts MAY support the following execution interface: ```solidity -/// @dev the ERC-165 identifier for this interface is `0x74420f4c` +/// @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 + * @dev Executes a low-level operation if the caller is a valid signer on the account. * - * @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 + * Reverts and bubbles up error if operation fails. * * Accounts implementing this interface MUST accept the following operation parameter values: * - 0 = CALL @@ -230,16 +233,18 @@ interface IERC6551Executable { * - 3 = CREATE2 * * Accounts implementing this interface MAY support additional operations or restrict a signer's - * ability to execute certain operations + * 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, - uint256 operation - ) external payable returns (bytes memory); + function execute(address to, uint256 value, bytes calldata data, uint8 operation) + external + payable + returns (bytes memory); } ``` @@ -295,9 +300,9 @@ Finally, this proposal seeks to grant NFTs the ability to act as agents on-chain 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. -### EIP-155 Support +### Chain Identifier -This proposal uses an EIP-155 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. +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 @@ -319,17 +324,40 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/interfaces/IERC1271.sol"; import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; -contract ExampleERC6551Account is IERC165, IERC1271, IERC6551Account, IERC6551Executable { +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 public state; receive() external payable {} - function execute( - address to, - uint256 value, - bytes calldata data, - uint256 operation - ) external payable returns (bytes memory result) { + 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"); @@ -345,7 +373,7 @@ contract ExampleERC6551Account is IERC165, IERC1271, IERC6551Account, IERC6551Ex } } - function isValidSigner(address signer, bytes calldata) external view returns (bytes4) { + function isValidSigner(address signer, bytes calldata) external view virtual returns (bytes4) { if (_isValidSigner(signer)) { return IERC6551Account.isValidSigner.selector; } @@ -356,6 +384,7 @@ contract ExampleERC6551Account is IERC165, IERC1271, IERC6551Account, IERC6551Ex function isValidSignature(bytes32 hash, bytes memory signature) external view + virtual returns (bytes4 magicValue) { bool isValid = SignatureChecker.isValidSignatureNow(owner(), hash, signature); @@ -364,24 +393,16 @@ contract ExampleERC6551Account is IERC165, IERC1271, IERC6551Account, IERC6551Ex return IERC1271.isValidSignature.selector; } - return ""; + return bytes4(0); } - function supportsInterface(bytes4 interfaceId) external pure returns (bool) { - return (interfaceId == type(IERC165).interfaceId || - interfaceId == type(IERC6551Account).interfaceId || - interfaceId == type(IERC6551Executable).interfaceId); + 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 - returns ( - uint256, - address, - uint256 - ) - { + function token() public view virtual returns (uint256, address, uint256) { bytes memory footer = new bytes(0x60); assembly { @@ -391,14 +412,14 @@ contract ExampleERC6551Account is IERC165, IERC1271, IERC6551Account, IERC6551Ex return abi.decode(footer, (uint256, address, uint256)); } - function owner() public view returns (address) { + function owner() public view virtual returns (address) { (uint256 chainId, address tokenContract, uint256 tokenId) = token(); if (chainId != block.chainid) return address(0); return IERC721(tokenContract).ownerOf(tokenId); } - function _isValidSigner(address signer) internal view returns (bool) { + function _isValidSigner(address signer) internal view virtual returns (bool) { return signer == owner(); } } @@ -407,90 +428,166 @@ contract ExampleERC6551Account is IERC165, IERC1271, IERC6551Account, IERC6551Ex ### Registry Implementation ```solidity -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; -import "@openzeppelin/contracts/utils/Create2.sol"; - -library ERC6551BytecodeLib { - function getCreationCode( - address implementation_, - uint256 chainId_, - address tokenContract_, - uint256 tokenId_, - uint256 salt_ - ) internal pure returns (bytes memory) { - return - abi.encodePacked( - hex"3d60ad80600a3d3981f3363d3d373d3d3d363d73", - implementation_, - hex"5af43d82803e903d91602b57fd5bf3", - abi.encode(salt_, chainId_, tokenContract_, tokenId_) - ); - } -} +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 + ); -contract ERC6551Registry is IERC6551Registry { + /** + * @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, - uint256 salt, - bytes calldata initData - ) external returns (address) { - bytes memory code = ERC6551BytecodeLib.getCreationCode( - implementation, - chainId, - tokenContract, - tokenId, - salt - ); - - address _account = Create2.computeAddress(bytes32(salt), keccak256(code)); - - if (_account.code.length != 0) return _account; + uint256 tokenId + ) external returns (address account); - emit AccountCreated(_account, implementation, chainId, tokenContract, tokenId, salt); + /** + * @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 { - _account := create2(0, add(code, 0x20), mload(code), salt) - } - - if (_account == address(0)) revert AccountCreationFailed(); - - if (initData.length != 0) { - (bool success, bytes memory result) = _account.call(initData); - - if (!success) { - assembly { - revert(add(result, 32), mload(result)) + // 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) } - } - return _account; + // 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, - uint256 salt + uint256 tokenId ) external view returns (address) { - bytes32 bytecodeHash = keccak256( - ERC6551BytecodeLib.getCreationCode( - implementation, - chainId, - tokenContract, - tokenId, - salt - ) - ); - - return Create2.computeAddress(bytes32(salt), bytecodeHash); + 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) + } } } ```