What is ERC-6551?

BlockGG
5 min readJul 17, 2023

--

What is ERC-6551?

Suppose in the game, my address A owns a character Bob, which itself is an ERC721 NFT, and it also has many props on it, such as hats, shoes, weapons, etc., and may also have some assets such as gold ingots, etc. These assets themselves are also ERC20, ERC721 and other types of tokens. These props and assets belong to my character Bob in the game logic, but in the actual underlying contract implementation, they all belong to my address A. If I want to sell my character Bob to someone else, I need to separately transfer Bob and all the assets he owns to the buyer. This is not very logical and operationally sound.

The purpose of the 6551 standard is to create a wallet for the character Bob, so that all the props it owns belong to the character Bob, which seems much more reasonable. We can look at the sample diagram given in the official EIP:

There are two NFTs in the user account, namely A#123 and B#456, where A#123 has two accounts (A and B), and B#456 has one account ©.

Let’s take a look at what the piece on the right in the figure means. The 6551 protocol provides a Registry contract, which is a contract used to create an NFT wallet. By calling its createAccount function, a contract wallet can be created for the NFT. Since the contract wallet can be written in various ways, this function requires us to provide an Implementation of the contract wallet. That is to say, we can customize the contract of the wallet, for example, it can be an AA wallet or a Safe wallet. There is also the keyword proxies in the figure, which means that the Registry created a proxy of the Implementation when creating the wallet, instead of copying it from the original model. The purpose of this is to save Gas. This piece of knowledge is applied to EIP-1167, friends who are not familiar with it can read my previous article.

The wallets created for NFT are called token bound accounts in 6551 EIP. It has a very important feature that it can be forward compatible with NFT, and it can be compatible with 6551 for the standard NFT contracts that have been deployed on the previous chain.

The wallets we usually use, whether it is EOA or contract wallets, are assets owned by the wallet itself. But in the NFT account, the NFT itself does not own assets, but it has a contract wallet, which is the subject of assets. In other words, NFT itself is more similar to the role of a human being. It should be clearer with a diagram:

Code

The 6551 standard recommends that Registry implement the IERC6551Registry interface:

interface IERC6551Registry {
/// @dev The registry SHALL emit the AccountCreated event upon successful account creation
event AccountCreated(
address account,
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt
);

/// @dev Creates a token bound account for an ERC-721 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.
///
/// @return the address of the account
function createAccount(
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt,
bytes calldata initData
) external returns (address);

/// @dev Returns the computed address of a token bound account
///
/// @return The computed address of the account
function account(
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt
) external view returns (address);
}

The createAccount function is used to create a contract wallet for NFT, and the account function is used to calculate the contract wallet address. They both make use of the create2 mechanism, so a definite address can be returned. Friends who don’t know about create2 can read my previous article.

6551 officially provides a deployed Registry implementation for reference.

The getCreationCode function is worth noting:

function getCreationCode(
address implementation_,
uint256 chainId_,
address tokenContract_,
uint256 tokenId_,
uint256 salt_
) internal pure returns (bytes memory) {
// 将 salt, chainId, tokenContract, tokenId 拼接在 bytecode 后面
return
abi.encodePacked(
hex"3d60ad80600a3d3981f3363d3d373d3d3d363d73",
implementation_,
hex"5af43d82803e903d91602b57fd5bf3",
abi.encode(salt_, chainId_, tokenContract_, tokenId_)
);
}

It is a function used to assemble the proxy bytecode. It can be seen that the salt_, chainId_, tokenContract_, tokenId_ and other data are spliced behind the proxy bytecode of EIP-1167 at the end. The purpose here is to read the data directly through the bytecode in the created contract wallet. The SudoSwap code we have learned before also uses a similar writing method, refer to here.

For the created NFT wallet contract (i.e. the Implementation contract), 6551 also made some requirements:

Should be created via the Registry

Must implement the ERC-165 interface

Must implement the ERC-1271 interface

The following IERC6551Account interface must be implemented:

/// @dev the ERC-165 identifier for this interface is `0x400a0398`
interface IERC6551Account {
/// @dev Token bound accounts MUST implement a `receive` function.
///
/// Token bound accounts MAY perform arbitrary logic to restrict conditions
/// under which Ether can be received.
receive() external payable;

/// @dev Executes `call` on address `to`, with value `value` and calldata
/// `data`.
///
/// MUST revert and bubble up errors if call fails.
///
/// By default, token bound accounts MUST allow the owner of the ERC-721 token
/// which owns the account to execute arbitrary calls using `executeCall`.
///
/// Token bound accounts MAY implement additional authorization mechanisms
/// which limit the ability of the ERC-721 token holder to execute calls.
///
/// Token bound accounts MAY implement additional execution functions which
/// grant execution permissions to other non-owner accounts.
///
/// @return The result of the call
function executeCall(
address to,
uint256 value,
bytes calldata data
) external payable returns (bytes memory);

/// @dev Returns identifier of the ERC-721 token which owns the
/// account
///
/// 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 ERC-721 token exists on
/// @return tokenContract The contract address of the ERC-721 token
/// @return tokenId The ID of the ERC-721 token
function token()
external
view
returns (
uint256 chainId,
address tokenContract,
uint256 tokenId
);

/// @dev Returns the owner of the ERC-721 token which controls the account
/// if the token exists.
///
/// This is value is obtained by calling `ownerOf` on the ERC-721 contract.
///
/// @return Address of the owner of the ERC-721 token which owns the account
function owner() external view returns (address);

/// @dev Returns a nonce value that is updated on every successful transaction
///
/// @return The current account nonce
function nonce() external view returns (uint256);
}

--

--