ERC-6551 Non-fungible Token Bound Accounts

ERC-6551 Non-fungible Token Bound Accounts

Tags
Web3
EIP
NFT
Published
April 28, 2024
Author
Senn

Introduction

EIP-6551 proposes a system that assigns Ethereum accounts to non-fungible tokens (NFTs), allowing NFTs to own assets and interact with applications via the account. This is achieved without needing changes to existing smart contracts or infrastructure.
The proposal enables real-world non-fungible assets to be represented as NFTs, with the ability to manage assets, execute operations, and work across multiple chains, using a pattern that mirrors Ethereum's ownership model.
A singleton registry is defined to assign unique, deterministic smart contract account addresses to all NFTs, which would have control granted to the NFT holder.
 
For instance, consider a character NFT in a blockchain game. With EIP-6551, this character could accumulate in-game assets like weapons or coins. Normally, these assets would be held by the player's account, but with token bound accounts, the NFT itself can own these assets via an account whose owner is the NFT. This makes the character an autonomous agent that can interact with other contracts, execute transactions, and potentially even participate in DeFi protocols directly, rather than just being a static token. And owner of the NFT can trade this NFT to trade all assets belonged to the NFT,
notion image
 

Code analysis

notion image

ERC6551Registry

User can call ERC6551Registry.createAccount to create a deterministic acocunt of an NFT.
In the ERC6551Registry.createAccount, it uses ERC-1167 to deploy a proxy delegates to implementation. Note that it appends chainId, tokenContract and tokenId to contract’s bytecode. So that implementation's token method can read proxy’s code to get corresponding NFT’s information, and in turn get the owner of NFT.
// ERC6551Registry.sol 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) } }

ERC6551Account

ERC6551Account implemets _isValidSigner, which calls token method to read proxy code’s NFT contract address and ID. (Note it uses extcodecopy to read proxy’s bytecode.)
With NFT’s contract and ID, it calls NFT contract to get the owner of NFT.
With the NFT owner information, account contract can implement various methods only can be called by NFT owner or by addresses with NFT owner’s signature.
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 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 != block.chainid) return address(0); return IERC721(tokenContract).ownerOf(tokenId); } function _isValidSigner(address signer) internal view virtual returns (bool) { return signer == owner(); }

Reference