はじめに

初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、スマートコントラクトアカウント(SCA)で、複数処理の呼び出しをアトミックかつガスコストを節約する方法で実行する仕組みを提案しているERC7638についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIPについてまとめています。

概要

バッチコールエンコーディング(Batch Calls Encoding = BCE)は、スマートコントラクトアカウント(Smart Contract Account = SCA) ウォレットからのトランザクションを1つのトランザクションにまとめ、パラメータをバイト形式でエンコードしてオンチェーンデータを圧縮してガスコストを節約する手法です。

動機

例えば、AさんがNFTを保有しているときに、BさんがAさんのNFTを他のアドレスに送る必要があるとします。 この時、approveとtransferFromという2つのトランザクションを実行する必要があります。 SCAウォレットでは、この2つトランザクションを1回のトランザクションにまとめる方法を提案しています。 また、approveかtransferFromのどちらかが失敗した場合は全ての処理が失敗するアトミック性も確保しています。 複数のパラメーターをバイト形式にエンコードし、てトランザクションのデータを圧縮することでガスコストを節約します。

さらに、この方式ではアトミック性に加えてガス代の支払いを別のアドレスに委任することもできます。

  1. ユーザーはSCAを通じて複数のトランザクションを実行したい。
  2. ユーザーは手数料として10 USDTをバンドラーに送金。
  3. バンドラーは複数のトランザクションをまとめて送信して、ガス代をETHで支払って10 USDTを受け取ります。

バンドラーとは、複数のコントラクトから実行したいトランザクションをまとめて実行してくれるEOAアカウントです。 コントラクトは秘密鍵を持っていないため、現状トランザクションを実行することはできません。 そのため、EOAアドレスがトランザクションの実行を代理する必要があります。 この代理実行を行う存在がバンドラーです。 より詳しくは以下の記事などを参考にしてください。

ユーザーは実行するトランザクションデータをエンコードして署名を添付してバンドラーに送ります。 バンドラーは、ガスの支払いが不十分であるときトランザクションを送信しません。 トランザクションデータを承認した場合、署名されたトランザクションを送信して、実行後にバンドラーは手数料を受け取ることができます。

ERC4337でもガス支払いの委任も実装できますが、BCEではSCEとERC4337を同時に実行できます。

ERC4337については以下の記事を参考にしてください。

仕様

この提案では、SCAをコントラクトに実装する必要があります。 DappはSCAウォレット拡張機能と通信してユーザーが行いたい処理(インテント)をウォレットに伝え、ウォレットはBCEを使用して複数のトランザクションデータをユーザーのSCAコントラクトに送ります。 Batch Callは複数のCallバイトで構成され、各Callバイトは以下のように「to value \ data」のエンコーディングで定義されます。

text
graph LR
A["to (20bytes)"] --- B["value (32bytes)"] --- C["data length (32bytes)"] --- D["data (bytes)"]
  • to
    • 呼び出されたコントラクトのアドレス(20 バイト)。
  • value
    • コントラクトに送信された ETH の量 (wei 単位)。 data length データの長さ(バイト単位)。
  • data
    • コントラクトに送信されたエンコードされた関数データ。

複数のCallデータが連結されてBCEが形成されます。

補足

BCEでは複数の処理を効率的にまとめます。 各処理には以下のパラメータが含まれています。

  • to
    • 送付先アドレス。
  • value
    • transferするETH量。
  • data
    • 実行するCallデータ。

従来のアプローチでは、パラメータを以下のような構造体にまとめます。

js
struct Call {
    address to;
    uint256 value;
    bytes data;
}
Call[] calls;

ただ、このアプローチでは各パラメータのデータ型も一緒にエンコードされるため、データサイズが大きくなりガスコストが上がります。 BCEでは、各パラメータのデータ型が固定されているため、型情報をエンコードする必要がありません。 これにより、エンコードされたデータサイズが小さくなりガスコストが節約されます。 データを読み込むときは連結したデータを分割(slice)して各パラメータを読み取ります。

互換性

この提案ではコンセンサスレイヤーや他のERC標準を変更しないため、Ethereum全体として互換性の問題はありません。

参考実装

この規格では、Batch Callのエンコーディングのみを提案しており、具体的な実装などは実装者にユダめられています。 以下はSCAコントラクトの例です。 ユーザーは複数の処理をアトミックに署名し、バンドラーがユーザーに代わってガスを支払います。

SmartWallet.sol

js
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract SmartWallet {
    using ECDSA for bytes32;

    uint32 public valid = 1; //to make AtomSign invalid

    address private immutable original;
    address public owner;
    address public bundler;

    mapping(bytes32 => bool) public usedMsgHashes;

    modifier onlyBundler() {
        require(
            bundler == msg.sender,
            "onlyBundler: caller is not the bundler"
        );
        _;
    }

    modifier onlyOwnerAndOriginal() {
        require(
            owner == msg.sender || original == msg.sender,
            "onlyOwnerAndOriginal: caller is not the owner"
        );
        _;
    }

    constructor(address _bundler) {
        original = address(this);
        owner = msg.sender;
        bundler = _bundler;
    }

    function atomSignCall(
        bytes calldata atomCallbytes,
        uint32 deadline,
        bytes calldata signature
    ) external onlyBundler {
        require(deadline >= block.timestamp, "atomSignCall: Expired");
        bytes32 msgHash = keccak256(
            bytes.concat(
                msg.data[:msg.data.length - signature.length - 32],
                bytes32(block.chainid),
                bytes20(address(this)),
                bytes4(valid)
            )
        );
        require(!usedMsgHashes[msgHash], "atomSignCall: Used msgHash");
        require(
            owner == msgHash.toEthSignedMessageHash().recover(signature),
            "atomSignCall: Invalid Signature"
        );

        //do calls
        uint i;
        while(i < atomCallbytes.length) {
            address to = address(uint160(bytes20(atomCallbytes[i:i+20])));
            uint value = uint(bytes32(atomCallbytes[i+20:i+52]));
            uint len = uint(bytes32(atomCallbytes[i+52:i+84]));

            (bool success, bytes memory result) = to.call{value: value}(atomCallbytes[i+84:i+84+len]);
            if (!success) {
                assembly {
                    revert(add(result, 32), mload(result))
                }
            }

            i += 84 + len;
        }

        usedMsgHashes[msgHash] = true;
    }

    /**
     * if you signed something then regretted, make it invalid
     */
    function makeAtomSignInvalid() public onlyOwnerAndOriginal {
        valid = uint32(uint(blockhash(block.number)));
    }
}

上記コントラクトは以下の処理を実行します。

  • 複数のコールを1つのトランザクションでまとめて実行。
  • 署名によってトランザクションの正当性を検証。
  • ガスコストを節約。
  • トランザクションが有効であることを検証。

変数とマッピング

  • valid
    • atomSignの有効性を管理する変数。
  • original
    • コントラクトのアドレス。
  • owner
    • コントラクトの所有者アドレス。
  • bundler
    • バンドラーのアドレス。
  • usedMsgHashes
    • 既に使用されたメッセージハッシュを管理するマッピング。

Modifiers

  • onlyBundler
    • 関数呼び出し元がバンドラーであることを検証。
  • onlyOwnerAndOriginal
    • 関数呼び出し元が所有者またはオリジナルコントラクトであることを検証。

コンストラクタ

コントラクトがデプロイされたときに所有者とバンドラーのアドレスを設定します。

atomSignCall

署名を検証して複数のトランザクションをバッチ処理で実行する関数です。 また、関数の実行はバンドラー(特定の権限を持つアカウント)のみ行えます。

  • atomCallbytes
    • 実行するコールのデータを含むバイト配列。
  • deadline
    • トランザクションが有効である期限を示すタイムスタンプ。
  • signature
    • トランザクションの正当性を確認するための所有者の署名。
処理の流れ
  1. 期限の確認

    • deadline が現在のブロックタイムスタンプ以上であることを確認します。
    • 期限切れの場合はエラーを返します。
  2. メッセージハッシュの生成

    • トランザクションデータ (msg.data) の末尾から署名の長さと32バイトを除いた部分に、チェーンID、コントラクトアドレス、valid値を結合してハッシュを生成します。
  3. メッセージハッシュの未使用確認

    • 生成したメッセージハッシュが未使用であることを確認します。
    • 既に使用されている場合はエラーを返します。
  4. 署名の検証

    • メッセージハッシュに基づいて所有者の署名を検証します。
    • 署名が正当でない場合はエラーを返します。
  5. コールの実行

    • atomCallbytes を解析して、複数のコールを順に実行します。
    • 各コールは、宛先 (to)、送信するETHの量 (value)、コールデータ (data) を含みます。
    • コールが失敗した場合は、エラーメッセージを返してrevertします。
  6. メッセージハッシュの記録

    • 使用済みのメッセージハッシュとしてデータを記録します。

makeAtomSignInvalid

署名の有効性を無効にする関数です。 これにより、誤って署名したトランザクションを後で無効化します。 この関数は、コントラクトの所有者またはオリジナルのコントラクト(デプロイ時のアドレス)のみが呼び出すことができます。

処理の流れ
  1. 所有者またはオリジナルコントラクトの確認

    • 呼び出し元が所有者またはオリジナルコントラクトであることを確認します。
  2. 有効性の無効化

    • valid 値を現在のブロックハッシュに基づく値に更新します。

Bundler.sol

js
pragma solidity ^0.8.0;

contract Bundler {

    address public owner;

    modifier onlyOwner() {
        require(
            owner == msg.sender,
            "onlyOwner: caller is not the owner"
        );
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function executeOperation(
        address wallet,
        bytes calldata data
    ) public onlyOwner {
        (bool success, bytes memory result) = _callTo.call{value: 0}(data);

        if (!success) {
            assembly {
                revert(add(result, 32), mload(result))
            }
        }
    }
}

executeOperation

コントラクト所有者が指定したウォレットアドレスに対して任意の処理(Call)を実行する関数です。 実行結果が成功したかどうかを確認し、失敗した場合はエラーメッセージを返してrevertします。。

  • wallet
    • 操作を実行する対象のウォレットアドレス。
  • data
    • 実行する操作を指定するバイトコードデータ。

処理の流れ

  1. 所有者の確認

    • onlyOwner 修飾子を使って、関数呼び出し元がコントラクトの所有者であることを確認します。
  2. コールの実行

    • 指定された wallet アドレスに対して、data を用いてコールを実行します。
    • コールは value が0の場合に行われます(= ETHを送らない)。
    • 実行結果として success と result が返されます。
      • success
        • Callが成功したかどうかの真偽値。
      • result
        • Callの結果として返されるデータ。
  3. エラーハンドリング

    • success が false の場合、Callは失敗したことを意味します。
    • この場合、アセンブリ言語を使って result 内のエラーメッセージを取り出し、revert(例外を発生させてトランザクションを元に戻す)します。

Solidityのアセンブリについては以下の記事を参考にしてください。

セキュリティ

この提案では、データ圧縮を目的としたデータエンコーディングスキームを導入しています。 これはデータ圧縮のみに焦点を当てており、データの損失や個人データの隠蔽については焦点を当てていません。

引用

George (@JXRow), Zisu (@lazy1523), "ERC-7638: Batch Calls Encoding in SCA [DRAFT]," Ethereum Improvement Proposals, no. 7638, February 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7638.

最後に

今回は「スマートコントラクトアカウント(SCA)で、複数処理の呼び出しをアトミックかつガスコストを節約する方法で実行する仕組みを提案しているERC7638」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!