0
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?

【EIP-7702 × Mizuhiki】SBT保持者のみから送金を受け取れる委任コントラクトを実装してみた

Last updated at Posted at 2025-10-01

はじめに

以前、Japan Smart ChainのMizuhikiを試してみた【EIP-7702】EOAをスマコン化!Pectraの新機能を実装してみた を投稿しました。

今回は、この2つを組み合わせて、Mizuhikiで認証されたアカウント(SBT保持者)からのみネイティブトークンを受け取れる委任コントラクトを作成し、実際にトランザクションを実行してみます。

これにより、本人確認済みのアカウントからのみの送金を受け付ける、より安全なスマートアカウントの実装が可能になります。

委任コントラクトの実装

コントラクトのベースは以前と同様に、tutorial-buildbear-eip-7702 のコントラクトを使用します。

デプロイされたコントラクトアドレス:0x2c4a8c7C0b12d78E2AeDc822c13dc1d70c25cE49

コントラクトコード

BatchCallAndSponsorForMizuhiki.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

/// @title BatchCallAndSponsorForMizuhiki
contract BatchCallAndSponsorForMizuhiki {
    using ECDSA for bytes32;

    /// @notice A nonce used for replay protection.
    uint256 public nonce;

    /// @notice Represents a single call within a batch.
    struct Call {
        address to;
        uint256 value;
        bytes data;
    }

    /// @notice Emitted for every individual call executed.
    event CallExecuted(
        address indexed sender,
        address indexed to,
        uint256 value,
        bytes data
    );
    /// @notice Emitted when a full batch is executed.
    event BatchExecuted(uint256 indexed nonce, Call[] calls);

    /**
     * @notice Executes a batch of calls using an off–chain signature.
     * @param calls An array of Call structs containing destination, ETH value, and calldata.
     * @param signature The ECDSA signature over the current nonce and the call data.
     *
     * The signature must be produced off–chain by signing:
     * The signing key should be the account’s key (which becomes the smart account’s own identity after upgrade).
     */
    function execute(
        Call[] calldata calls,
        bytes calldata signature
    ) external payable {
        // Compute the digest that the account was expected to sign.
        bytes memory encodedCalls;
        for (uint256 i = 0; i < calls.length; i++) {
            encodedCalls = abi.encodePacked(
                encodedCalls,
                calls[i].to,
                calls[i].value,
                calls[i].data
            );
        }
        bytes32 digest = keccak256(abi.encodePacked(nonce, encodedCalls));

        bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(
            digest
        );

        // Recover the signer from the provided signature.
        address recovered = ECDSA.recover(ethSignedMessageHash, signature);
        require(recovered == address(this), "Invalid signature");

        _executeBatch(calls);
    }

    /**
     * @notice Executes a batch of calls directly.
     * @dev This function is intended for use when the smart account itself (i.e. address(this))
     * calls the contract. It checks that msg.sender is the contract itself.
     * @param calls An array of Call structs containing destination, ETH value, and calldata.
     */
    function execute(Call[] calldata calls) external payable {
        require(msg.sender == address(this), "Invalid authority");
        _executeBatch(calls);
    }

    /**
     * @dev Internal function that handles batch execution and nonce incrementation.
     * @param calls An array of Call structs.
     */
    function _executeBatch(Call[] calldata calls) internal {
        uint256 currentNonce = nonce;
        nonce++; // Increment nonce to protect against replay attacks

        for (uint256 i = 0; i < calls.length; i++) {
            _executeCall(calls[i]);
        }

        emit BatchExecuted(currentNonce, calls);
    }

    /**
     * @dev Internal function to execute a single call.
     * @param callItem The Call struct containing destination, value, and calldata.
     */
    function _executeCall(Call calldata callItem) internal {
        (bool success, ) = callItem.to.call{value: callItem.value}(
            callItem.data
        );
        require(success, "Call reverted");
        emit CallExecuted(
            msg.sender,
            callItem.to,
            callItem.value,
            callItem.data
        );
    }

    // Allow the contract to receive ETH (e.g. from DEX swaps or other transfers).
    fallback() external payable {}

    /// @notice Receive function to accept ETH payments.
    /// @dev Only allows addresses holding at least one SBT (Soulbound Token) to send ETH to this contract.
    ///      Reverts if the sender does not hold any SBT.
    /// @custom:security Only SBT holders can send ETH to this contract.
    receive() external payable {
        if (msg.value == 0) {
            return;
        }
        require(
            IERC721(_sbt).balanceOf(msg.sender) > 0,
            "Only SBT holders can send ETH to this contract"
        );
    }

    /// @dev Address of the SBT (Soulbound Token) contract. This address is immutable and set during contract deployment.
    address private immutable _sbt;

    /// @dev Constructor for the BatchCallAndSponsorForMizuhiki contract.
    /// @param sbt_ The address of the Soulbound Token (SBT) contract.
    /// @notice Ensures that the provided SBT address is not the zero address and implements the IERC721 interface.
    /// @custom:throws Reverts if the SBT address is zero or does not support the IERC721 interface.
    constructor(address sbt_) {
        require(sbt_ != address(0), "Soulbound Token address cannot be zero");
        require(
            IERC165(sbt_).supportsInterface(type(IERC721).interfaceId),
            "Soulbound Token must implement IERC721"
        );
        _sbt = sbt_;
    }

    /// @notice Returns the address of the SBT (Soulbound Token) contract.
    /// @dev This is a getter function for the private _sbt variable.
    /// @return The address of the SBT contract.
    function sbt() external view returns (address) {
        return _sbt;
    }
}

重要なポイント

このコントラクトの核心となる機能は receive() 関数です:

receive() external payable {
    if (msg.value == 0) {
        return;
    }
    require(
        IERC721(_sbt).balanceOf(msg.sender) > 0,
        "Only SBT holders can send ETH to this contract"
    );
}

この実装により:

  • ネイティブトークン(JETH)の受信時に、送信者がSBTを保持しているかチェック
  • SBT保持者でない場合は、トランザクションがリバート
  • 本人確認済みアカウントからのみの送金を受取

EIP-7702による委任コントラクトの設定

EIP-7702を使用して、EOA(外部所有アカウント)に委任コントラクトのコードを設定します。

設定コード

    // 委任コントラクトのアドレス
    const CONTRACT_ADDRESS = "0x2c4a8c7C0b12d78E2AeDc822c13dc1d70c25cE49";
    console.log("Contract Address:", CONTRACT_ADDRESS);

    // Signer情報を取得
    const signers = await ethers.getSigners();
    const signer = signers[0];
    console.log("Account:", signer.address);

    // コントラクトデプロイ前のコードを確認
    console.log("Code:", await ethers.provider.getCode(signer.address));

    // Signerのノンスを取得
    const nonce = await signer.getNonce();
    console.log("Signer nonce:", nonce);

    // コントラクトアドレスとノンスを使って認可情報を作成
    const auth = await signer.authorize({
      address: CONTRACT_ADDRESS,
      nonce: nonce + 1,
    });

    // トランザクションデータを作成
    const txData = {
      to: signer.address,
      type: 4,
      authorizationList: [auth],
    };

    // トランザクションを送信
    const tx = await signer.sendTransaction(txData);
    console.log("Transaction hash:", tx.hash);

    // トランザクションのレシートを取得
    const receipt = await tx.wait();
    console.log("Transaction receipt:", receipt?.status);

    // コントラクトデプロイ後のコードを確認
    console.log("Code:", await ethers.provider.getCode(signer.address));

実行結果

Contract Address: 0x2c4a8c7C0b12d78E2AeDc822c13dc1d70c25cE49
Account: 0x326b9f6F4D0d68476547c3deFAD62AFec29961E1
Code: 0x  # 設定前は空
Signer nonce: 36
Transaction hash: 0xfaffbe23361f2825c714cea9c052dbc27a9b730d85d07bc7fb525805fa88098e
Transaction receipt: 1  # 成功
Code: 0xef01002c4a8c7c0b12d78e2aedc822c13dc1d70c25ce49  # 委任コントラクトのコードが設定された

設定前は Code: 0x だったアカウントに、委任コントラクトのコードが正常に設定されました。

エクスプローラーでも、Type4で送信されていることがわかります。

image.png

また、コードが設定されていることもわかります。

image.png

SBT制限の動作確認

委任コントラクトが正常に動作するか、実際にネイティブトークンの送信テストを行います。

テストコード

※1、※2には数値(Signerの番号)が入ります。

  try {
    // アカウントの一覧を取得
    const signers = await ethers.getSigners();

    // 各アカウントの情報を表示
    await viewSignerInfo(signers);

    // 送信元と送信先のアカウントを指定
    const from = signers[※1];
    const to = signers[※2];
    console.log(`Send 0.001 JETH from ${from.address} to ${to.address}`);

    // 0.001 JETH を送金
    const tx = await from.sendTransaction({
      to: to.address,
      value: ethers.parseEther("0.001"),
    });
    console.log("Transaction hash:", tx.hash);

    // トランザクションの完了を待機し、結果を表示
    const receipt = await tx.wait();
    console.log("Transaction receipt:", receipt?.status);

    // 送金後の各署名者の情報を再度表示
    await viewSignerInfo(signers);
  } catch (error) {
    // エラーが発生した場合は内容を表示
    console.error(error);
  }

ケース1: SBT保持者からの送金(成功例)

MizuhikiのSBTを保持しているアカウントからの送金は正常に処理されます。

Account[0]: 0x326b9f6F4D0d68476547c3deFAD62AFec29961E1 - Balance: 1.553999999959445032 JETH - Code: 0xef01002c4a8c7c0b12d78e2aedc822c13dc1d70c25ce49
Account[1]: 0x45eb028ab5aE2177Ec12a2a28658292bA43CC0Ef - Balance: 0.542999999998059304 JETH - Code: 0x
Account[2]: 0x05214f3D672e006bC27e80D99a740ba608e72dE8 - Balance: 0.172999999995656368 JETH - Code: 0x
Account[3]: 0x750C70e4368eFa5A2Fbf0Cf2aa1Fd96847014ab1 - Balance: 0.097999999999664 JETH - Code: 0x
Account[4]: 0xF75e96E466533Eab82B70685D4a5E610B3ebee50 - Balance: 0.0 JETH - Code: 0x
Send 0.001 JETH from 0x05214f3D672e006bC27e80D99a740ba608e72dE8 to 0x326b9f6F4D0d68476547c3deFAD62AFec29961E1
Transaction hash: 0x73df5e855ec03f49652d11794bce2be135678af137ba5492832f24c72c843535
Transaction receipt: 1
Account[0]: 0x326b9f6F4D0d68476547c3deFAD62AFec29961E1 - Balance: 1.554999999959445032 JETH - Code: 0xef01002c4a8c7c0b12d78e2aedc822c13dc1d70c25ce49
Account[1]: 0x45eb028ab5aE2177Ec12a2a28658292bA43CC0Ef - Balance: 0.542999999998059304 JETH - Code: 0x
Account[2]: 0x05214f3D672e006bC27e80D99a740ba608e72dE8 - Balance: 0.171999999995403768 JETH - Code: 0x
Account[3]: 0x750C70e4368eFa5A2Fbf0Cf2aa1Fd96847014ab1 - Balance: 0.097999999999664 JETH - Code: 0x
Account[4]: 0xF75e96E466533Eab82B70685D4a5E610B3ebee50 - Balance: 0.0 JETH - Code: 0x

MizuhikiのSBTを保持していないアカウント

結果例は以下となります。

Only SBT holders can send ETH to this contract でリバートされることがわかります。

Account[0]: 0x326b9f6F4D0d68476547c3deFAD62AFec29961E1 - Balance: 1.554999999959445032 JETH - Code: 0xef01002c4a8c7c0b12d78e2aedc822c13dc1d70c25ce49
Account[1]: 0x45eb028ab5aE2177Ec12a2a28658292bA43CC0Ef - Balance: 0.542999999998059304 JETH - Code: 0x
Account[2]: 0x05214f3D672e006bC27e80D99a740ba608e72dE8 - Balance: 0.171999999995403768 JETH - Code: 0x
Account[3]: 0x750C70e4368eFa5A2Fbf0Cf2aa1Fd96847014ab1 - Balance: 0.097999999999664 JETH - Code: 0x
Account[4]: 0xF75e96E466533Eab82B70685D4a5E610B3ebee50 - Balance: 0.0 JETH - Code: 0x
Send 0.001 JETH from 0x750C70e4368eFa5A2Fbf0Cf2aa1Fd96847014ab1 to 0x326b9f6F4D0d68476547c3deFAD62AFec29961E1
ProviderError: execution reverted: Only SBT holders can send ETH to this contract
 <省略>
  code: 3,
  data: undefined
}

まとめ

本記事では、EIP-7702とJapan Smart ChainのMizuhikiを組み合わせて、本人確認済みアカウントからのみネイティブトークンを受け取れる委任コントラクトを実装し、実際に動作確認を行いました。

実現できたこと

  • EIP-7702による委任コントラクト化: EOAにスマートコントラクト機能を追加
  • SBTによる本人確認: Mizuhikiの認証システムと連携
  • セキュアな資金管理: SBT保持者のみからの送金を自動制御

技術的意義と応用

この実装により、従来のEOAでは不可能だった認証機能付きスマートアカウントを実現しました。KYC済みユーザー限定の金融サービスや、企業内決済システムなど、コンプライアンス要件のあるアプリケーションへの応用が期待されます。

EIP-7702とMizuhikiの組み合わせは、Web3における信頼性の高い身元確認システムの新しい可能性を示しており、今後のブロックチェーンエコシステムの発展に貢献すると考えられます。

注意事項

【EIP-7702】EOAをスマコン化!Pectraの新機能を実装してみた にも記載していますが、EOA からコードが取得できるため、コントラクトとみなされるようになり、以下のようなインターフェースへの対応を用意しておく必要があります。

非常に便利な機能ですが、上記のような点に注意して扱う必要があります。

0
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
0
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?