ERC-6551: Non-fungible Token Bound Accounts（実装）

# はじめに（Introduction）

[前回](https://qiita.com/tnakagawa/items/bdbfb7f47dc7b193420f)、以下を翻訳したので記述してあるリファレンスコードを実際に実装してみようと思います。

https://eips.ethereum.org/EIPS/eip-6551

# コントラクト（Contract）

`ERC6551Account`、`ERC6551Registry`、`ERC721Demo`、`ERC20Demo`の４つのコントラクトを実装します。

## ERC6551Account

Non-fungible Token Bound Accounts（以下、NFTBA）が実行時に呼び出すコントラクトです。
原文ではライセンス行が無いので追記しています。

```javascript:ERC6551Account.sol
// SPDX-License-Identifier: MIT
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 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 != block.chainid) return address(0);

        return IERC721(tokenContract).ownerOf(tokenId);
    }

    function _isValidSigner(
        address signer
    ) internal view virtual returns (bool) {
        return signer == owner();
    }
}
```

## ERC6551Registry

NFTBAのアカウントアドレスを作成したり、計算したりするコントラクトです。

```javascript:ERC6551Registry.sol
// 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)
        }
    }
}
```

## ERC721Demo

NFTBAの所有者が保持するNFTです。

```javascript:ERC721Demo.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract ERC721Demo is ERC721, Ownable {
    constructor() ERC721("Demo NFT", "DNT") Ownable(msg.sender) {}

    function mint(address to, uint256 tokenId) public onlyOwner {
        _mint(to, tokenId);
    }
}
```

## ERC20Demo

NFTBAなどに付与するトークン（ERC20）です。

```javascript:ERC20Demo
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract ERC20Demo is ERC20, Ownable {
    constructor() ERC20("Demo Token", "DTC") Ownable(msg.sender) {}

    function mint(address account, uint256 value) public onlyOwner {
        _mint(account, value);
    }
}
```

# デプロイモジュール

Hardhatのignitionでデプロイするモジュールです。

```javascript:ERC6551Demo.js
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");

module.exports = buildModule("ERC6551Demo", (m) => {
    const ERC6551Account = m.contract("ERC6551Account", []);
    const ERC6551Registry = m.contract("ERC6551Registry", []);
    const ERC20Demo = m.contract("ERC20Demo", []);
    const ERC721Demo = m.contract("ERC721Demo", []);

    return { ERC6551Account, ERC6551Registry, ERC20Demo, ERC721Demo };
});
```

# スクリプト

実際にデモとして動かすスクリプトです。
全文書くとながいので重要な部分だけ記述します。

## createAccount

`ERC6551Registry`でNFTBAのアカウントを作成します。

|パラメータ|設定値|概要|
|:--|:--|:--|
|implementation|`ERC6551Account`のアドレス|NFTBAが実際に実行を行うコントラクトのアドレスです。|
|salt|0|ソルトです。|
|tokenContract|`ERC721Demo`のアドレス|NFTBAが所有者を判定する際に使うERC721（NFT）コントラクトのアドレスです。|
|tokenId|1|上記ERC721（NFT）コントラクトのトークンIDです。|

```javascript:createAcount.js（一部）
async function main() {
    const { provider, chainId, ERC6551Account, ERC6551Registry, ERC20Demo, ERC721Demo, signer1, signer2 } = await loadFixture();
    const implementation = ERC6551Account.target;
    const salt = "0x0000000000000000000000000000000000000000000000000000000000000000";
    const tokenContract = ERC721Demo.target;
    const tokenId = 1;
    const account = await ERC6551Registry.account(implementation, salt, chainId, tokenContract, tokenId);
    const code = await provider.getCode(account);
    if (code == "0x") {
        const tx = await ERC6551Registry.createAccount(implementation, salt, chainId, tokenContract, tokenId);
        const receipt = await tx.wait();
        console.log("createAccount");
    }
    console.log("account", account);
}
```

## ERC721のmint

ERC721のトークンID`1`を`signer2`に`mint`します。

```javascript:erc721mint.js（一部）
async function main() {
    const { provider, chainId, ERC6551Account, ERC6551Registry, ERC20Demo, ERC721Demo, signer1, signer2 } = await loadFixture();
    const tokenId = 1;
    const balance = await ERC721Demo.balanceOf(signer2.address);
    if (balance == 0) {
        const tx = await ERC721Demo.mint(signer2.address, tokenId);
        const receipt = await tx.wait();
        console.log("mint", signer2.address, tokenId);
    }
    const owner = await ERC721Demo.ownerOf(tokenId);
    console.log("ownerOf", tokenId, owner);
}
```

### ERC20のmint

NFTBAに`100.0`のERC20トークンを`mint`します。

```javascript:erc20mint.js（一部）
async function main() {
    const { provider, chainId, ERC6551Account, ERC6551Registry, ERC20Demo, ERC721Demo, signer1, signer2 } = await loadFixture();
    const implementation = ERC6551Account.target;
    const salt = "0x0000000000000000000000000000000000000000000000000000000000000000";
    const tokenContract = ERC721Demo.target;
    const tokenId = 1;
    const account = await ERC6551Registry.account(implementation, salt, chainId, tokenContract, tokenId);
    let balance = await ERC20Demo.balanceOf(account);
    if (balance == 0) {
        const value = ethers.parseEther("100");
        const tx = await ERC20Demo.mint(account, value);
        const receipt = await tx.wait();
        console.log("mint", account, ethers.formatEther(value), await ERC20Demo.symbol());
    }
    balance = await ERC20Demo.balanceOf(account);
    console.log("ERC20Demo#balanceOf", account, ethers.formatEther(balance), await ERC20Demo.symbol());
}
```

### execute

`account`に対して`signer2`がERC20のトークンを`signer1`に`1.0`送信します。

executeのパラメータ

|パラメータ|設定値|概要|
|:--|:--|:--|
|to|`ERC20Demo`のアドレス|操作のターゲットアドレスです。|
|value|0|ターゲットに送信する Ether 値です。|
|data|※１|エンコードされたオペレーションcalldataです。|
|operation|0|CALLを指定しています。|

※１：ERC20の`transfer`を実行するcalldata

|パラメータ|設定値|概要|
|:--|:--|:--|
|to|`signer1`のアドレス|送信先のアドレスです。|
|value|`1.0`|送信するERC20のトークン値です。|

```javascript:execute.js（一部）
async function main() {
    const { provider, chainId, ERC6551Account, ERC6551Registry, ERC20Demo, ERC721Demo, signer1, signer2 } = await loadFixture();
    const implementation = ERC6551Account.target;
    const salt = "0x0000000000000000000000000000000000000000000000000000000000000000";
    const tokenContract = ERC721Demo.target;
    const tokenId = 1;
    const account = await ERC6551Registry.account(implementation, salt, chainId, tokenContract, tokenId);
    const Account = new ethers.Contract(account, EXECUTE_ABI, signer2);
    const to = ERC20Demo.target;
    const value = 0;
    const iface = new ethers.Interface(TRANSFER_ABI);
    const data = iface.encodeFunctionData("transfer", [signer1.address, ethers.parseEther("1.0")]);
    const operation = 0;
    const tx = await Account.execute(to, value, data, operation);
    const receipt = await tx.wait();
}
```

## デモ（Demo）

ソースコードは[Github](https://github.com/tnakagawa/erc6551demo)にあります。

デモを体験するには以下が必要です。

- [Node](https://nodejs.org/en)(version 20)
- [VSCode](https://code.visualstudio.com/)

### 準備

Git Cloneをします。

```bat
git clone https://github.com/tnakagawa/erc6551demo.git
```

`erc6551demo`フォルダをVSCodeで開きます。

以下のコマンドで、Node関連のインストールを行います。

```bat
npm install
```

### ローカルノード起動

以下のコマンドで、ローカルノードを起動します。

```sh
npx hardhat node
```

### デプロイ

別のターミナルを開きます。
以下のコマンドで`localhost`にデプロイをします。

```bat
npx hardhat ignition deploy .\ignition\modules\ERC6551Demo.js --network localhost
```

### ERC6551

ターミナルの環境変数にネットワークが`localhost`であることを設定します。

PowerShellの場合は以下のコマンドです。

```sh
$env:HARDHAT_NETWORK="localhost"
```

Window Shellの場合は以下のコマンドです。

```sh
set HARDHAT_NETWORK=localhost
```

#### アカウント作成

以下のコマンドでERC6551のアカウントを作成します。

```bat
node .\scripts\createAccount.js
```

結果：例

アカウントが作成されます。

```text
createAccount
account 0x9A4d8D916Ea34345B55Fe04E6B0B5b18461B7223
```

#### ERC721のmint

以下のコマンドでERC721を`signer1`にmintします。

```bat
node .\scripts\erc721mint.js
```

結果：例

`signer1`にトークンID`1`をmintします。

```text
mint 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 1
ownerOf 1 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
```

#### ERC20のmint

以下のコマンドでERC20をaccountにmintします。

```bat
node .\scripts\erc20mint.js 
```

結果：例

`account`に`100.0 DTC`をmintします。

```text
mint 0x9A4d8D916Ea34345B55Fe04E6B0B5b18461B7223 100.0 DTC
ERC20Demo#balanceOf 0x9A4d8D916Ea34345B55Fe04E6B0B5b18461B7223 100.0 DTC
```

#### 状態の確認

以下のコマンドで状態を確認します。

```bat
node .\scripts\info.js
```

結果：例

`account`にERC20を`100.0 DTC`保持していることがわかります。

```text
        | address                                    | ERC20
signer1 | 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 | 0.0
signer2 | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 | 0.0
account | 0x9A4d8D916Ea34345B55Fe04E6B0B5b18461B7223 | 100.0
----------------------------------------------------------------------
Create  | true
ERC721  | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 1n
```

#### Execute

`account`から`signer1`へERC20を`1.0 DTC`移転します。

```bat
node .\scripts\execute.js
```

#### 状態の確認

以下のコマンドで状態を確認します。

```bat
node .\scripts\info.js
```

結果：例

`account`から`signer1`へERC20を`1.0 DTC`移転していることがわかります。

```text
        | address                                    | ERC20
signer1 | 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 | 1.0
signer2 | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 | 0.0
account | 0x9A4d8D916Ea34345B55Fe04E6B0B5b18461B7223 | 99.0
----------------------------------------------------------------------
Create  | true
ERC721  | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 1n
```

# まとめ（Conclusion）

リファレンスの実装を用いて簡単な操作をしてみました。
ERC721（NFT）の所有者が操作可能なアドレスというところで、いろいろな手段に使えそうです。
また、多段にすることで樹形図のような使い方もできそうだと思いました。