はじめに(Introduction)
前回、以下を翻訳したので記述してあるリファレンスコードを実際に実装してみようと思います。
コントラクト(Contract)
ERC6551Account
、ERC6551Registry
、ERC721Demo
、ERC20Demo
の4つのコントラクトを実装します。
ERC6551Account
Non-fungible Token Bound Accounts(以下、NFTBA)が実行時に呼び出すコントラクトです。
原文ではライセンス行が無いので追記しています。
// 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のアカウントアドレスを作成したり、計算したりするコントラクトです。
// 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です。
// 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)です。
// 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でデプロイするモジュールです。
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です。 |
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のトークンID1
をsigner2
にmint
します。
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
します。
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 | ※1 | エンコードされたオペレーションcalldataです。 |
operation | 0 | CALLを指定しています。 |
※1:ERC20のtransfer
を実行するcalldata
パラメータ | 設定値 | 概要 |
---|---|---|
to |
signer1 のアドレス |
送信先のアドレスです。 |
value | 1.0 |
送信するERC20のトークン値です。 |
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
結果:例
account
に100.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)の所有者が操作可能なアドレスというところで、いろいろな手段に使えそうです。
また、多段にすることで樹形図のような使い方もできそうだと思いました。