1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ERC-6551: Non-fungible Token Bound Accounts(実装)

Posted at

はじめに(Introduction)

前回、以下を翻訳したので記述してあるリファレンスコードを実際に実装してみようと思います。

コントラクト(Contract)

ERC6551AccountERC6551RegistryERC721DemoERC20Demoの4つのコントラクトを実装します。

ERC6551Account

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

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のアカウントアドレスを作成したり、計算したりするコントラクトです。

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です。

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)です。

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でデプロイするモジュールです。

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です。
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のトークンID1signer2mintします。

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します。

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のトークンをsigner11.0送信します。

executeのパラメータ

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

※1:ERC20のtransferを実行するcalldata

パラメータ 設定値 概要
to signer1のアドレス 送信先のアドレスです。
value 1.0 送信するERC20のトークン値です。
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にあります。

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

準備

Git Cloneをします。

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

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

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

npm install

ローカルノード起動

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

npx hardhat node

デプロイ

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

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

ERC6551

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

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

$env:HARDHAT_NETWORK="localhost"

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

set HARDHAT_NETWORK=localhost

アカウント作成

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

node .\scripts\createAccount.js

結果:例

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

createAccount
account 0x9A4d8D916Ea34345B55Fe04E6B0B5b18461B7223

ERC721のmint

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

node .\scripts\erc721mint.js

結果:例

signer1にトークンID1をmintします。

mint 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 1
ownerOf 1 0x70997970C51812dc3A010C7d01b50e0d17dc79C8

ERC20のmint

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

node .\scripts\erc20mint.js 

結果:例

account100.0 DTCをmintします。

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

状態の確認

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

node .\scripts\info.js

結果:例

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

        | 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移転します。

node .\scripts\execute.js

状態の確認

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

node .\scripts\info.js

結果:例

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

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

まとめ(Conclusion)

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?