署名前の取引シミュレーション|ウォレットで無制限approveを見抜く

コラム

/約14分で読めます

コラム

/約14分

署名前の取引シミュレーション|ウォレットで無制限approveを見抜く
目次(タップで折りたたみ)

    あるウォレットの開発チームが、DeFiの取引画面に「署名前の確認」を付けようとしています。対象は、EVM系のDeFiでの2段階の操作です。まずトークンの使用許可(approve)を出し、続けてトークンを交換(swap)します。

    チームが止めたいのは、次のような事故です。画面ではswapに見えたのに、署名した中身は無制限のapproveだった。受取額が、利用者の指定した最低量を下回っていた。想定外のコントラクトで、管理者が書き換えられていた。

    これを署名の前に見抜くのが、取引シミュレーションです。署名前の入力を特定ブロックの状態で試しに実行し、資産の増減や権限の変化を調べます。

    ただ、試してから実際に送るまでの間にも、ほかの取引は次々に入ります。プールの価格やnonceが変われば、結果も変わります。つまり、試した結果は、送ったときの成功までは保証しません。そこで、どのブロックの状態で試したかを記録します。送るまでに状態が変わった場合や、ノードに照会できない場合も、判定に含めます。

    チームが決める順番は次のとおりです。まず止めたい事故と、どこまでの誤検知なら許すかを決めます。続いて、再現する状態、実行環境、結果の判定を決めます。最後が、署名・送信の前のチェックと、監視と復旧です。以下では、approve→swapの例を最後まで通して見ていきます。

    この記事で使う言葉

    • calldata:取引でコントラクトに渡す入力データ。どの関数を、どの引数で呼ぶか
    • ABI:コントラクトの関数や引数の定義。calldataを人が読める形に戻すのに使う
    • spender/operator:他人のトークンを動かす権限を渡された相手(ERC-20ではspender、NFTではoperator)
    • revert:取引が途中で失敗し、変更がすべて取り消されること
    • 再編(reorg):チェーンの直近のブロックが別のものに置き換わること

    何がそろえば「合格」にするか

    eth_callで分かるのは、戻り値と、revertせずに実行できるかどうかです。受取額が少なすぎる取引も、無制限のapproveが混ざった取引も、実行さえできれば「成功」で返ってきます。そのため、エラーなしで通っても、利用者が思ったとおりの結果とは限りません。

    合格にするには、次の5つを確かめます。

    確かめること見る内容拒否する例
    実行できるかrevert、ガス不足(out of gas)、nonce、残高、期限スリッページ条件でrevert
    どの状態が変わるかストレージ、イベント、内部呼び出し想定外のコントラクトで管理者を変更
    どの資産が増減するかネイティブ通貨、ERC-20、NFTの増減受取額が最低量未満
    誰にどんな権限が渡るかspender、allowance、operator権限無制限のapprove、第三者へのoperator付与
    結果が古くなっていないかチェーンID、ブロックハッシュ/番号、経過時間価格・nonce・プールの状態が更新済み

    それぞれで何を記録するかは、後半の「実装する人向けの詳細」にまとめました。

    どこまで厳しく見るかは、使い道で変わります。使い道は3つです。

    • 利用者に見せるプレビュー:利用者が理解できる資産・権限の変化が中心
    • バックエンドの送信前チェック:送ってよいかを自動で決める
    • 開発者向けのデバッグ:原因を調べる

    自動で送る場合は、判定できないものの扱いを決めておきます。対象は、中身を読み取れない呼び出し、価格影響、nonceの競合、シミュレーション不能です。これらを「判定できなければ止める(fail-closed)」とするかは、業務のリスクごとに決めます。

    署名するものと同じ入力・同じブロックで試す

    流れは次の6段階です。

    依頼受付 → 入力の正規化 → ブロックの固定 → 隔離環境で実行 → 差分・トレースの読み取り → 送信可否の判定

    合格(PASS)→ 署名前に最新ブロックを再確認 / 要確認(REVIEW)→ 人手確認 / 拒否(REJECT)→ 送信しない

    確かめたいのは、実際に署名される中身です。そのため、試す入力には、画面表示用の見積もり(quote)ではなく、実際に署名するfrom、to、data、value、チェーンIDを、正規化して使います。

    さきほどのswapなら、次のものも同じ依頼IDに結び付けます。

    • プロキシの実装コントラクトとABI
    • トークンの小数桁(decimals)
    • 許可したspender

    結果は、試したその入力に対するものです。試した後でcalldataを作り直したら、前の結果は使えません。

    Gethのeth名前空間の仕様では、eth_callで指定したブロック時点の状態で取引を試せます。読み取り専用の実行です。残高などの状態(state override)や、ブロック情報(block override)を一時的に書き換えて試すこともできます。

    latestが指すブロックは、再編や新しいブロックの追加で入れ替わります。そのため、ブロックをlatestとだけ記録すると、あとで再現できません。応答には、ブロック番号とブロックハッシュを残します。

    状態の書き換えは強力な機能です。残高、nonce、コード、ストレージを一時的に差し替えられます。GethのState Override Setの仕様のとおり、実在しない残高やコードでも成功結果を作れてしまいます。

    画面の都合で残高やallowanceを補って試すことはあります。その結果を「今の状態で実行できる」という判定に混ぜてはいけません。仮定付きの試算として別に表示し、別の判定ルールで扱います。

    どの方式で試すか

    方式によって、分かることが違います。表のとおり、普段の軽い確認にはeth_call、原因調査にはdebug_traceCall、approve→swapのような連続した操作にはeth_simulateV1が向きます。本番の送信前チェックやプレビューには、フォークしたEVMや外部のシミュレーションAPIも使えます。

    方式分かること主な用途
    eth_call戻り値、revert、単一呼び出しの実行可否軽量な事前確認、コントラクトの読み取り
    debug_traceCall呼び出しの木構造、オペコード、ガス、失敗箇所原因の特定、内部呼び出しの検査
    eth_simulateV1複数ブロック・複数呼び出し、状態/ブロック情報の上書き、検証approve→swapなどの連続した操作
    フォークしたEVM/外部のシミュレーションAPI読み取り済みの差分、独自の判定ルール、過去時点の再現本番の送信前チェック、ウォレットのプレビュー、障害の再現

    どの方式にも足りない点があります。

    • eth_call:内部呼び出しや資産差分の解釈は別に要る
    • debug_traceCall:ノード負荷、RPCの公開範囲、ノードソフトウェアごとの差がある
    • eth_simulateV1:対応するノードソフトウェア・事業者の確認が要る
    • フォークしたEVM/外部API:実行環境・ABI・価格データへの依存が増える

    Gethのdebug_traceCallは、指定ブロックの文脈でeth_callを実行し、その実行トレースを返します。トレースAPIを外に公開するなら、次の上限を設けます。

    • タイムアウトとガス上限
    • 応答サイズ
    • 同時実行数

    シミュレーションを担うワーカーは、送信APIや秘密鍵を扱うサービスから切り離しておきます。

    approve→swapの例では、approveがswapの前提を変えます。そのため、2回の別々のeth_callでは再現できません。Gethのeth_simulateV1なら、前の状態変化を引き継いで、複数のブロックと呼び出しを試せます。

    対応しないノードを使う場合は、フォークした状態へ取引を順番に当て、各段階の差分を保存します。バッチ取引そのものの設計はバッチトランザクションの実装と導入判断も参照してください。

    「成功した」を「何が増えて何が減ったか」に読み替える

    シミュレーションの生の結果は、呼び出しのトレース、ログ、状態差分です。これをアドレスとABIの版に対応づけて、中身を読み取ります。そのうえで、同じ持ち主ごとに次の変化をまとめます。

    • ネイティブ通貨・ERC-20・ERC-721/1155の増減
    • allowanceとoperator
    • プロトコル上のポジション

    プロキシは、試した時点の実装コントラクトをたどります。ABIのハッシュとコードのハッシュも保存します。未検証のコントラクトや、読み取れない呼び出しは、中身が分からないだけで、変化がないとは限りません。「変化なし」として扱ってはいけません。

    トークンには、同じシンボルを名乗る偽物があります。そのため、金額を表示するときは、トークンのコントラクト、小数桁、表示用シンボルの取得元も持たせます。transferイベントを出さない独自実装のトークンもあるので、イベントだけで残高の変化を断定しないでください。

    DeFiのポジションは、単純なトークンの受け取りに見えないことがあります。そのプロトコル専用の読み取り処理がないときは、裏付け資産の相当額を推測値として分けて示します。

    試してから送るまでに状態が変わったら、どうするか

    試した後も、ほかの取引が入れば結果は変わります。変わるのは、プール価格、nonce、allowance、オラクル、期限などです。ブロックのタイムスタンプや基本手数料(base fee)も変わります。

    そこで送る前に最新ブロックを取得し、次のどれかに当たれば試し直します。

    • 試したブロックから、許容するブロック数または秒数を超えた。
    • 監視しているコントラクトのコードハッシュ、プロキシの実装先、重要なストレージが変わった。
    • 送信者のnonce、残高、allowance、見積もり、オラクルの更新回(round)が変わった。
    • calldata、value、ガス条件、送信先、チェーンIDを変えた。
    • 同じアカウントから、未確定の取引が追加・置換された。

    試した結果と署名するものは、input_hashで結び付けます。署名サービスは、合格(PASS)したハッシュ以外を拒否します。

    合格の後で、フロントエンドや中継サーバ(relayer)がtoやdataを組み直せる作りだとします。すると、署名するものが、試したものと変わってしまいます。これでは、このチェックは効きません。

    高額の操作では、非公開の送信経路や条件付き送信も検討できます。ただ、特定の事業者を使うだけで、実行結果が保証されるわけではありません。送信経路の選び方はMEV被害を抑える取引送信設計で扱っています。

    一方、スマートアカウントを使うERC-4337では、似た言葉が別の意味で使われます。UserOperationのシミュレーション仕様では、取引をまとめて送るbundlerが、検証処理をオフチェーンで試します。そして、revertするUserOperation(スマートアカウント用の取引依頼)を捨てます。

    これは、ウォレットが利用者に見せる資産差分のプレビューとは目的が違います。アカウント、factory、Paymaster(手数料を肩代わりする仕組み)の検証と、呼び出しの結果判定は、別の段階として記録します。全体像はERC-4337の仕組みと4コンポーネントの役割をご覧ください。

    失敗やタイムアウトのとき、どう扱うか

    シミュレーションの処理には、送信してよい状態と、そうでない状態があります。送信できるのは、すべての判定と鮮度の条件を満たした「合格」だけです。それも、署名前に最新ブロックを再確認するのが条件です。状態の一覧は後半の表にまとめました。

    タイムアウトを「失敗」と決めつけ、同じ重い処理を何度も再実行すると、ノード障害を広げます。依頼ID、入力ハッシュ、ブロックハッシュを冪等キーにします。冪等キーは、同じ依頼を何度送っても1回分しか処理しないための識別子です。ワーカーの完了記録を確かめてから再実行します。

    複数の事業者を使うと、結果が食い違うことがあります。そのときは多数決で決めず、次の差を比べます。食い違いは、人手の確認(REVIEW)へ回します。

    • ノードソフトウェアの版とブロックハッシュ
    • トレースの設定
    • 読み取り処理の版

    本番前にどんな障害を試すか

    さきほどのウォレットなら、次の場面を受け入れ試験に入れます。

    • 正常なswap、スリッページ超過、期限切れ、残高不足、allowance不足を見分けられる。
    • 無制限のapprove、operatorの追加、delegatecall先の変更を、資産の移転とは別に警告できる。
    • 試した後にプールの状態、nonce、プロキシの実装先を変えると、古い結果(STALE)になる。
    • 再編後にブロックハッシュの不一致を見つけ、古いPASSを送信に使わない。
    • 未知のABI、プロキシの解決失敗、トレースのタイムアウトを「変化なし」にせず、REVIEW/ERRORにする。
    • 署名する入力のハッシュが試した結果と違えば、署名サービスが拒否する。
    • ノード停止時に、高リスクの取引を未検証のまま迂回して送らない。
    • 試した結果、判定ルールの版、実行環境の版から、あとで判定を再現できる。

    監視では、成功率のほかに次の指標を見ます。

    • 試してから送るまでの時間、STALEの割合、ブロックの遅れ
    • PASSの後にオンチェーンでrevertした割合、読み取れなかった割合
    • 事業者間の不一致率、p95/p99の応答時間(95%・99%の依頼が収まる時間)

    PASS後のrevertが増えたら、ガス上限だけを疑わないでください。鮮度の条件、依存する状態、MEVによる状態変化、読み取り処理の誤判定も調べます。

    重い仕組みが要らないのはどんなときか

    固定額の単純な送金のように、コントラクトを呼ばない処理もあります。送信側の残高・nonce・宛先の許可リストでリスクを十分に抑えられるなら、重いトレース基盤は費用に見合わないことがあります。

    一方、次のものを扱うなら、eth_estimateGas1回だけでは足りません。利用者の受け取りや権限の変化を判定できないからです。

    • 任意のコントラクト呼び出し、DeFiの経路選択
    • スマートアカウント
    • 第三者が作るcalldata

    役割も分けて考えます。シミュレーションの仕組みが出すのは「この入力を、この状態で実行した結果」です。どこまで許すかの判定ルールと最終承認は、ウォレットや業務システムが持ちます。状態と実行環境は、ノードやRPC事業者が出します。

    コントラクト監査、オラクル設計、秘密鍵管理の代わりには、シミュレーションはなりません。

    よくある質問

    取引シミュレーションとは何をすることですか

    署名・送信する前の取引を、特定ブロックの状態で試しに実行することです。revertするか、どの資産がどれだけ増減するか、どの権限が誰に渡るかを確かめます。答えは「その時点の状態で実行した場合」のものです。送信時の成功は保証しないため、参照したブロックと鮮度を一緒に記録します。

    eth_estimateGasだけでは足りないのはなぜですか

    eth_estimateGasが答えるのは、その取引がどれだけのガスで実行できそうかです。利用者にとっての結果は分かりません。たとえば受取額が最低量を下回っていないか。無制限のapproveや第三者へのoperator付与が含まれていないか。想定外のコントラクトの管理者が変わらないか。そのため、資産と権限の変化を別に読み取って判定します。

    外部のシミュレーションAPIを使う場合も、自社で持つべき判定はありますか

    あります。外部APIは、読み取り済みの差分や過去時点の再現を出してくれます。それでも、送る側が持つべきものが3つあります。

    • どの差分なら送ってよいかという判定ルール
    • 署名するものと結果を結ぶinput_hashの突き合わせ
    • 鮮度切れや照会できないときに止める条件

    複数の事業者の結果が食い違ったときの扱いも、自社の状態遷移として決めておきます。

    実装する人向けの詳細

    5つの判定で記録する項目

    判定ごとに、次の項目を残します。

    判定記録する項目
    実行可能性エラーのセレクタ(エラーの種類を示す識別子)、失敗した呼び出し
    状態差分アドレス、スロット/イベント、変更前後の値
    資産差分保有者、トークン、数量、増減の向き
    許可範囲権限を持つ主体、対象、上限、期限
    鮮度実行時刻(simulated_at)、最新ブロックとの差

    結果の記録例

    1件のシミュレーション結果は、たとえば次の形で保存します。

    {
      "request_id": "sim_...",
      "chain_id": 1,
      "block": {"number": 0, "hash": "0x..."},
      "input_hash": "0x...",
      "engine": {"client": "geth", "version": "..."},
      "execution": {"status": "success", "gas_used": "..."},
      "asset_changes": [],
      "permission_changes": [],
      "unknown_calls": [],
      "decision": {"result": "review", "policy_version": "2026-08-13"}
    }

    処理の状態と、送信の可否

    送信できるのはPASSだけで、それも条件付きです。

    状態遷移する条件送信の可否復旧の方法
    待機中(QUEUED)入力の正規化・重複排除が済んだ不可依頼IDで再照会
    実行中(RUNNING)ブロックの固定・実行環境の割り当てが済んだ不可タイムアウト後は保存済みの結果を確認
    合格(PASS)すべての判定ルールと鮮度条件を満たす条件付きで可署名前に最新ブロックを再確認
    要確認(REVIEW)未知の呼び出し、しきい値超過、読み取り不足不可根拠と差分を人手で確認
    拒否(REJECT)revert、禁止した権限、損失条件不可入力を直したら新しい依頼にする
    古い結果(STALE)ブロック・入力・依存する状態が更新された不可最新状態で再シミュレーション
    エラー(ERROR)ノードのタイムアウト、トレースの制限、読み取り処理の障害原則不可同じブロックで別の実行環境と突き合わせる

    XTELAができること

    私たちは、扱う取引をリスクごとに分類するところから始め、ノード・解読処理・送信可否の関門の構成を設計し、実装まで進めます。鮮度切れやノード障害を再現する試験、監視と復旧手順の整備、ウォレットのプレビューを小さく試すPoCも行います。署名前の確認を実装に落としたい場合は、お問い合わせフォームからご相談ください。

    参考資料

    資料の確認日と注意:公開仕様は2026年8月〜9月24日に確認しました。それにもとづく一般的な技術設計の解説です。ノードやRPCの仕様は変わり得るため、実装時点の公式資料を確認してください。会計・法的な判断は各分野の専門家と進めてください。

    お問い合わせ

    どんなフェーズからでも、お持ちのアイデアや企画をもとにご提案可能です。
    まずはお気軽にご相談下さい!