はじめに
『DApps開発入門』という本や色々記事を書いているかるでねです。
今回は、構造化データを人間が理解できる形で表示して安全に署名確認できる仕組みを提案しているERC7730についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。
概要
ERC7730は、ウォレット画面で人間が内容を正しく確認できる形で表示したり、トランザクションシミュレーターなどの機械が意味を理解できる形で処理したりするために必要な「追加の意味情報」をJSON形式で定義する仕様です。
ブロックチェーンでは、トランザクションのデータや署名対象のメッセージは、ABIやEIP712の型定義のような「構造化されたデータ」として表現されています。
例えば、EVMトランザクションの calldata や、構造化署名メッセージがこれに当たります。
しかし、これらは「型」や「値」は分かっても、「人間にとってどういう意味を持つ操作なのか」までは表現できません。
EIP712については以下の記事を参考にして下さい。
ERC7730は、そうした構造化データに対して以下のような補足情報を付与します。
- 表示用のフォーマット情報
例として、Solidityのuint256で表現された数量が「トークン量」なのか「割合」なのか「時刻」なのかを区別し、表示形式を決められるようにします。 - 動的な値の埋め込み(インターポレーション)
トークン量であれば、対応するdecimalsを考慮して人間が読める単位に変換し、シンボル(例:USDC、ETHなど)を付けて表示できます。
これにより、単なる数値 1000000 ではなく、「1.0 USDC」のように意味のある表示が可能になります。
同じ情報は、トランザクションシミュレーションエンジンなどの自動システムにとっても、機械的に解釈しやすい形式で利用できます。
ウォレットや自動化システムは、署名対象となる生データ(ABIやメッセージ)と一緒に、このERC7730形式の定義ファイルを参照します。
これによって、それぞれの用途に適したユーザーインターフェースや解析処理を構築できます。
結果として、署名時に表示される情報の理解度が大きく向上し、フロントエンドの改ざんやフィッシングによって意図しないトランザクションに署名してしまうリスクを下げることができます。
動機
ハードウェアウォレットの画面上で、ユーザーが署名内容を自分の目で確認する行為は「クリアサイニング(Clear Signing)」と呼ばれ、ブロックチェーン利用における基本的かつ重要なセキュリティ対策です。
しかし現状では、署名対象のデータは以下のような問題を抱えています。
まず、関数名やメッセージ型の名前は開発者向けの命名であり、ユーザーの意図をそのまま表すとは限りません。
例えば、executeSwap や commit のような名前だけでは、「何を」、「いくら」、「どこへ」送るのかが直感的に分かりません。
次に、データの各フィールドは基本的にプリミティブ型(uint256、address など)で表現されますが、これらは文脈によって意味が大きく変わります。
整数ひとつを取っても、トークン数量、パーセンテージ、タイムスタンプなど、表示方法はまったく異なります。
さらに、トークン量の表示には追加のメタデータが必要です。
具体的には以下の情報がなければ、人間にとって正しい表示ができません。
| 必要な情報 | 説明 |
|---|---|
decimals |
最小単位を人間が読める単位に変換するための桁数です。 |
ticker(シンボル) |
USDC、DAI、ETHなどの通貨記号です。 |
| トークンコントラクトアドレス | どのトークンの情報かを特定するために必要です。 |
ABIやEIP712の型定義だけでは、これらの表示に必要な文脈情報が不足しています。
そのため、ウォレットは「数値とアドレスの羅列」をそのまま表示するしかなく、ユーザーが本当に意図した操作かどうかを判断するのが困難になります。
ERC7730は、この問題を解決するために、構造化データに対する「意味づけ」と「表示ルール」を標準化された形式で提供します。
どのフィールドが何を表し、どのように整形して表示すべきかをJSONで記述することで、ウォレットはそれを解釈し、ユーザーにとって理解しやすい形に変換できます。
このような詳細なフォーマット情報は、スマートコントラクトやメッセージの利用方法を深く理解しているアプリケーション開発者が最も適切に記述できます。
ERC7730をオープンな標準として定めることで、その定義ファイルを一度用意すれば、多くの対応ウォレットで共通して利用できるようになります。
その結果、署名体験はより直感的で安全になり、フロントエンド改ざんやフィッシングによって誤った内容に署名させられるリスクを大きく低減できます。
仕様
簡単な例
以下は、ERC20トークンの transfer 関数呼び出しを「クリアサイン」するための ERC7730 定義ファイルの例です。
{
"$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v1.schema.json",
"context": {
"$id": "Example ERC-20",
"contract" : {
"deployments": [
{
"chainId": 1,
"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7"
},
{
"chainId": 137,
"address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"
},
{
"chainId": 42161,
"address": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"
}
]
}
},
"metadata": {
"owner": "Example",
"contractName": "Example Token",
"info": {
"url": "https://example.io/",
"deploymentDate": "2017-11-28T12:41:21Z"
}
},
"display": {
"formats": {
"transfer(address to,uint256 value)": {
"intent": "Send",
"interpolatedIntent": "Send {value} to {to}",
"fields": [
{
"path": "value",
"label": "Amount",
"format": "tokenAmount",
"params": {
"tokenPath": "@.to"
}
},
{
"path": "to",
"label": "To",
"format": "addressName"
}
]
}
}
}
}
スキーマ指定
$schemaキーは、このJSONがどのバージョンのERC7730仕様に従っているかを示します。
ここではバージョン1の公式JSON SchemaへのURLが指定されており、ウォレットやツールはこれを参照して構造の正当性を検証します。
コンテキスト
contextは、この ERC7730 ファイルが「どのデータに対して有効なのか」を定義するための拘束条件です。
ウォレットは、ここに書かれた条件が満たされている場合にのみ、このファイルの表示ルールを適用する必要があります。
この例では、特定のスマートコントラクトに対してのみ有効であることを示しています。
| 項目 | 意味 |
|---|---|
chainId |
チェーンID(Ethereum Mainnet, Polygon, Arbitrumなど) |
address |
そのチェーン上でのコントラクトアドレス |
つまり、「このERC7730定義は、指定されたチェーン上の指定されたアドレスにデプロイされた同一トークンコントラクトにのみ適用する」という制約を与えています。
ウォレットは、トランザクションの宛先コントラクトがこのリストと一致した場合にだけ、後述の display ルールを使って表示を行います。
メタデータ
metadataは、コンテキストが一致した場合に信頼してよい定数情報をまとめた領域です。主な用途は次の通りです。
- コントラクトやメッセージの受信者を人間が読める形で表示する
- 列挙値やIDの意味を人間向け名称に変換する
- 表示フォーマットで共通して使う定数を定義する
この例では、以下のような情報が定義されています。
| キー | 説明 |
|---|---|
owner |
表示用の運営主体名や組織名です。 |
contractName |
ユーザー向けのトークン名です。 |
info |
公式サイトURLやデプロイ日時などの補足情報です。 |
ウォレットはこれらを使って、「どのトークンに送金しようとしているのか」をユーザーに分かりやすく表示できます。
表示定義
displayセクションは、実際に署名画面でどのように内容を表示するかを定義する中核部分です。
対象関数の識別
formatsのキーには、ABIの人間可読形式が使われています。
transfer(address to,uint256 value)
ウォレットはここから型名だけを抜き出して
transfer(address,uint256)
というシグネチャを作り、Keccak-256でハッシュ化して関数セレクタ 0xa9059cbb を計算します。
そして、トランザクションの calldata の先頭4バイトと一致するかどうかで、この定義を適用すべきかを判断します。
意図の表示
intentには、ユーザーに見せる操作の要約が書かれています。
"intent": "Send"
これは「この操作は送金である」という意味を、開発者向け関数名ではなく人間向けの言葉で示すためのものです。
ウォレットはこの文字列を表示することが推奨されています。
動的な意図文
interpolatedIntentは、パラメータを埋め込んだ短い文章テンプレートです。
"interpolatedIntent": "Send {value} to {to}"
ウォレットは {value} や {to} を実際の引数値で置き換えて、「Send 1.5 USDC to alice.eth」のような1行表示を作ることもできます。
専用レイアウトがない場合の簡易表示として利用可能です。
フィールド定義(fields)
fieldsでは、各パラメータをどのように表示するかを細かく指定します。
| path | label | format | 意味 |
|---|---|---|---|
value |
Amount |
tokenAmount |
トークン量としてdecimalsとシンボルを考慮して表示 |
to |
To |
addressName |
アドレスをENS名などの信頼できる名称に解決して表示 |
tokenPathでトークンコントラクトの参照先を指定することで、正しい decimals とティッカーを取得し、人間が理解できる金額表記に変換します。
バージョン管理
ERC7730では、どの仕様バージョンに従っているかを $schema のURLで明示します。
現在の仕様バージョンは「1」であり、ウォレットやツールはこのスキーマを基準にして、以下を行います。
- JSON構造の検証
- 将来のバージョン差分への対応
これにより、仕様が進化しても互換性と安全性を保ちながら、表示ルールを標準化して共有できる仕組みになっています。
共通概念
キー名の命名規則
ERC7730では、キー名が$で始まるものは「この仕様ファイルを読みやすくするための内部用の目印」です。
内部用なので、ウォレットが署名内容の表示UIを作るときに、$で始まるキーを「表示ルールとして使う」ことはしてはいけません。
例えば、この後に出てくる$schemaや、パス参照で使う$($.metadata...)は、仕様ファイルの中身を参照するための仕組みであって、「ユーザーに見せるフィールド」そのものではありません。
構造化データ
ERC7730が対象にする「構造化データ」は、ざっくり言うと「型と構造が決まっていて、フィールド単位に分解できるデータ」です。
仕様では、最低限次の条件を満たすデータを指します。
| 条件 | 意味 |
|---|---|
| 型システムが明確 | データ全体が「あるトップレベル型」に属している(例:ある関数の引数セット、あるメッセージ型など) |
| スキーマがある | どんなフィールドがあり、どう辿ればそのフィールドに到達できるかが決まっている |
| フィールドをパスで特定できる |
params.amountInのような文字列のパスで、特定の値を指せる |
ウォレットが署名前に確認したいのは、この「構造化データの中身」です。
ただし実際に署名されるのは、構造化データそのものではなく、構造化データを包んでいる外側の「コンテナ構造」です。
構造化データとコンテナ
ここが初見で一番つまずきやすいので、用語を整理します。
| 用語 | 何か | 具体例 |
|---|---|---|
| 構造化データ | ユーザーが意味を確認したい「中身」 | コントラクト呼び出しの引数、EIP712メッセージのフィールド |
| コンテナ構造 | 署名アルゴリズムが対象にする「外側」 | EVMトランザクション(RLPでシリアライズ)、EIP712の署名対象形式 |
RLP(Recursive Length Prefix)は、Ethereumでトランザクションなどをバイト列に変換(シリアライズ)するための形式です。
ここでは「EVMトランザクションの外側の入れ物を、署名できる形にする方式」だと思ってください。
仕様が言っている流れはこうです。
- ウォレットは「コンテナ構造」全体を受け取ります(例:EVMトランザクション)。
- ウォレットはコンテナ構造に定義された署名手順で、全体に署名します。
- そのうえで、コンテナの中から「構造化データ」を取り出してデコードし、ユーザー確認用に表示します。

https://eips.ethereum.org/EIPS/eip-7730
現在の仕様がカバーする対象
EVMスマートコントラクトの calldata
| 項目 | 内容 |
|---|---|
| 型の定義元 | Solidity |
| スキーマ | 関数ABI(display.formatsでマッチした関数フラグメントから導出) |
| コンテナ構造 | EVMトランザクション(RLPエンコード) |
ここで重要なのは、ERC7730が「関数ABIを元にフィールドのパスを確定できる」ようにしている点です。
どの引数がどのフィールドかが決まるので、toやvalueのようなパスで値を指せます。
EIP712メッセージ
| 項目 | 内容 |
|---|---|
| 型の定義元 | EIP712 |
| スキーマ |
display.formatsに書かれた型文字列表現から抽出 |
| コンテナ構造 | EIP712の仕様通りにハッシュ化したメッセージ自体(自己完結) |
EIP712は「メッセージ自体が署名対象として完結している」ので、EVMトランザクションのように別の外側形式(RLPトランザクション)に包まれている前提ではありません。
フォーマットと型
ERC7730の表示フォーマットは、構造化データの「型」に依存します。
つまり、同じuint256でも「トークン量として表示する」のか「単なる数として表示する」のかを、フォーマット側で決めます。
この「どんなフォーマットが使えるか」、「どんな型に対応しているか」は、仕様のReferenceセクションで定義されます。
コンテナ構造の値を参照する必要
フィールドの表示を作るとき、構造化データだけでは足りず、コンテナ構造の値を参照したいケースがあります。
例として、EVMトランザクションなら@.from(送信者)や@.value(ネイティブ通貨送金額)などがそれです。
これも、どんなコンテナ値が参照できるかはReferenceセクションに定義されます。
パス参照(Path references)
ERC7730は、複数のJSONドキュメント(構造化データ、ERC7730ファイル、コンテナ)をまたいで値を参照できるように、制限付きのJSONPath表記を使います。
パス表記の制限
仕様が求めている制限は以下です。
| ルール | 意味 |
|---|---|
| ドット記法のみ(配列も) | 配列要素はarray.[index]の形で書きます |
| 使えるセレクタ | name(キー名)、index(添字)、slice(範囲)のみ |
| sliceのstepは禁止 |
[start:end:step]のstepは使えません |
| sliceの範囲 | startは含む、endは含まない(start inclusive, end exclusive) |
| start/end省略可 |
[:20]や[-20:]のように書けます |
ルート識別子
複数の参照元を区別するため、先頭にルート記号を付けます。
| ルート | 参照先 | 短い説明 |
|---|---|---|
# |
構造化データ | デコード済みの関数引数/EIP712フィールド |
$ |
ERC7730ファイル | includeをマージした後の仕様ファイル自身 |
@ |
コンテナ構造 | トランザクションやメッセージのメタ情報 |
ルートを省略すると「今表示している構造化データを起点(相対パス)」として解釈されます。
また、囲っているコンテナが存在しない場合は、相対パスは#.から始まるのと同じ扱いになります。
# 構造化データ
#は「署名対象の中身(引数やフィールド)」を指します。
パスの名前は、display.formatsに書かれているスキーマ(関数シグネチャやメッセージ型)に一致します。
例:
| 例 | 意味 |
|---|---|
#.params.amountIn |
トップレベルparamsの中のamountIn
|
params.amountIn |
相対パスなので上と同じ意味 |
#.details.[] |
PermitBatchメッセージのPermit Details配列(配列そのものを指す説明) |
$ マージ後のERC7730ファイル
$は「ERC7730ファイル自身の中の値」を参照します。
includeを使っている場合は、利用者(ウォレット等)がマージした後の内容を参照します。
例:
| 例 | 意味 |
|---|---|
$.metadata.enums.interestRateMode |
仕様ファイル内で定義された列挙値 |
$.display.definitions.minReceiveAmount |
共有のフィールド定義 |
@ コンテナ
@は「構造化データを包む外側のコンテナの値」を参照します。
EVMトランザクションなら@.from @.to @.value @.chainIdなどが例です。
例:
| 例 | 意味 |
|---|---|
@.value |
EVMトランザクションのネイティブ通貨送金額 |
@.to |
トランザクションの宛先(多くはコントラクトアドレス) |
パスのスライス
構造化データのフィールドが可変長プリミティブ(Solidityのbytesやstring)の場合、パスの末尾にスライスを付けて「一部だけ」を参照できます。
配列に対しても同様に「一部分だけ」を対象にできます。
例:
| 例 | 意味 |
|---|---|
#.data.path.[0].path.[-1].to |
data.path配列の0番目要素の中のpath配列の最後(-1)要素のto
|
#.params.path.[:20] / #.params.path.[0:20]
|
params.pathの先頭20バイト |
#.params.path.[-20:] |
params.pathの末尾20バイト |
値の埋め込み
interpolatedIntentは、意図文(ユーザーが読む文)に値を埋め込むための仕組みです。
intentとfieldsを別々に並べる代わりに、1文で自然に読める表示を作れます。
何が嬉しいか
interpolatedIntentが役に立つ理由は以下の3点です。
| 効果 | 説明 |
|---|---|
| 認知負荷の低減 | 項目がバラバラに並ぶより、1文の方が理解が速い |
| 画面資源が少ないデバイス向き | ハードウェアウォレットなどで短く表示できる |
| バッチ取引の説明がしやすい | 例としてEIP5792のように複数操作を文として連結しやすい |
EIP5792については以下の記事を参考にして下さい。
埋め込み構文
埋め込みは {path} です。
pathは前述のパス参照ルールに従い、以下の参照先を使えます。
| 参照先 | 例 |
|---|---|
| 構造化データ |
{to} {params.amountIn}
|
| コンテナ値 |
{@.value} {@.from}
|
| メタデータ定数 |
{ $.metadata.constants.nativeAssetAddress }(例として本文の趣旨) |
また、interpolatedIntentで参照してよいのは「常に表示されるフィールド」だけです。
フォーマット処理の挙動
ウォレットがinterpolatedIntentを処理するときのルールは、明確に手順になっています。
- 文字列内のすべての
{path}を見つけます。 - 各
{path}について、パスを解決して値を探します。 - さらに、そのパスに対応する
fields内のフォーマット定義を特定します。 -
formatとparamsを適用して値を整形します。 -
{path}を整形済みの値で置換します。 - どれかが失敗した場合は、
intentの表示に切り替えます。
ここで「フォールバック」は、パスが見つからない・フォーマット適用に失敗した、などの場合に「最低限安全に読める表示」に戻すための動作です。
エスケープ
意図文の中で{や}そのものを表示したい場合は、波括弧を二重にしてエスケープします。
-
{{は{を表示 -
}}は}を表示
シンプルなトークン送金
{
"intent": "Send",
"interpolatedIntent": "Send {value} to {to}",
"fields": [
{"path": "to", "format": "addressName"},
{"path": "value", "format": "tokenAmount", "params": {"tokenPath": "@.to"}}
]
}
表示例:Send 100 USDT to cyberdrk.eth
ネイティブ通貨を含むスワップ
{
"intent": "Swap",
"interpolatedIntent": "Swap {amountIn} for at least {amountOutMinimum}",
"fields": [
{"path": "amountIn", "format": "tokenAmount", "params": {"tokenPath": "tokenIn"}},
{"path": "amountOutMinimum", "format": "tokenAmount", "params": {"tokenPath": "tokenOut"}}
]
}
表示例:Swap 1000 USDC for at least 0.25 WETH
コンテナ値を使う例
{
"intent": "Wrap ETH",
"interpolatedIntent": "Wrap {@.value} ETH for WETH",
"fields": [
{"path": "@.value", "format": "amount"}
]
}
表示例:Wrap 0.5 ETH for WETH
波括弧を文字として表示する例
{
"interpolatedIntent": "Execute {{function}} with {amount} tokens"
}
表示例:Execute {function} with 100 tokens
ファイルの整理
スマートコントラクトやEIP712メッセージでは、共通インターフェースや共通型を何度も使うことが多いです。
ERC7730は、その重複を減らすために「include(取り込み)」をサポートします。
includes の意味
includes は、別のERC7730ファイルへのURL(またはパス)です。
取り込まれる側のファイルも、ERC7730仕様に従っている必要があります。
"includes": "./example-erc20.json"
マージの基本ルール
ウォレットがincludeを扱うとき、「1つの参照ファイルにマージする」ことが推奨されています。
その時、同じキーが衝突した場合は「取り込む側(including file)」を優先して解決します。
fields 配列のマージ
fieldsは配列なので上書きすると順序や部分上書きが崩れます。
そのため、ウォレットは以下のルールでマージすることが推奨されています。
-
pathが同じ要素同士は1つにまとめます- included側の設定をベースにしつつ、including側のパラメータで上書きします
-
included側に存在しない
pathの要素は、結果の配列に追加します -
例
共通のERC20 approve 定義(汎用ファイル)
このファイルはapproveだけを定義しています。
ここには「特定のコントラクトに紐づける情報(context)」や「コントラクト固有のmetadata」がありません。
{
"display": {
"formats": {
"approve(address spender,uint256 value)": {
"intent": "Approve",
"fields": [
{
"path": "spender",
"label": "Spender",
"format": "addressName"
},
{
"path": "value",
"label": "Amount",
"format": "tokenAmount",
"params": {
"tokenPath": "@.to",
"threshold": "0x8000000000000000000000000000000000000000000000000000000000000000",
"thresholdLabel": "Unlimited"
}
}
]
}
}
}
}
ここでthresholdとthresholdLabelは、「一定値を超えた approve をUnlimitedのように表示する」といった表示上の工夫に使うパラメータです。
汎用ファイルを取り込み、特定コントラクトにバインドするファイル
以下のファイルは上の汎用定義を includes で取り込み、context でUSDTコントラクト(例)に結びつけます。
さらに、value フィールドの threshold だけを上書きしています。
{
"context": {
"$id": "Example Contract",
"contract" : {
"deployments": [
{
"chainId": 1,
"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7"
}
]
}
},
"includes": "./example-erc20.json",
"metadata": {
"owner": "Example",
"contractName": "Example Token",
"info": {
"url": "https://example.io/",
"deploymentDate": "2017-11-28T12:41:21Z"
},
"token": {
"ticker": "STABLE",
"name": "Example Stablecoin",
"decimals": 6
}
},
"display": {
"formats": {
"approve(address spender,uint256 value)": {
"fields": [
{
"path": "value",
"params" : {
"threshold": "0xFFFFFFFFFFFFFFFFFF"
}
}
]
}
}
}
}
コンテキストセクション
ERC7730のcontextセクションは、「このERC7730定義ファイルを、どのトランザクションやメッセージに適用してよいか」を厳密に制限するための条件集合です。
ウォレットは、署名対象の構造化データとそれを包むコンテナ構造が、このcontextで定義された条件を満たしているかを必ず検証する必要があります。
条件を満たさない場合、そのERC7730ファイルは適用してはいけません。
現在のバージョンのERC7730では、以下の2種類のコンテキストがサポートされています。
- EVMスマートコントラクト(calldata)
- EIP712メッセージ(messages)
すべてのcontextには、人間向け識別子として$idキーを持つことができます。
これは表示用の名前であり、検証ロジックには直接関係しません。
EVMスマートコントラクトのバインディング(calldata)
context.contractは、ERC7730ファイルを特定のスマートコントラクト呼び出しに結び付けるための条件を定義します。
contract.deployments
deploymentsは、「このERC7730定義が有効なコントラクトの配置先」のリストです。
ウォレットは、署名対象トランザクションである以下のリストのいずれかと完全一致することを必ず確認しなければなりません。
- チェーンID
- 宛先コントラクトアドレス
各要素は以下の構造を持ちます。
| キー | 意味 |
|---|---|
chainId |
EIP155で定義されたチェーン識別子 |
address |
そのチェーン上のコントラクトアドレス |
EIP155については以下の記事を参考にして下さい。
contract.abi(非推奨)
従来互換のために残されているABI定義用キーです。
現在はdisplay.formatsで型と表示を定義するのが正しい方法であり、新規作成では使用すべきではありません。将来の仕様で削除予定です。
contract.factory
factoryは、「ファクトリコントラクト経由でデプロイされたコントラクト群」を対象にするための仕組みです。
factoryは以下の2つの情報を持ちます。
| キー | 内容 |
|---|---|
deployEvent |
デプロイ時に発火するSolidityイベントのシグネチャ |
deployments |
ファクトリ自身がデプロイされているアドレス一覧 |
ウォレットは以下の条件をすべて満たすことを検証します。
- 現在のトランザクションの宛先アドレスが、
deployEventのイベントログの中に含まれている - そのイベントの発行元(emitter)が、
factory.deploymentsで指定されたファクトリアドレスのいずれかである
これにより、「このトランザクションの対象コントラクトは、正規のファクトリから生成されたものだ」と検証できます。
EIP712メッセージのバインディング(messages)
context.eip712は、特定のEIP712メッセージ型とドメインにERC7730定義を結び付けます。
eip712.schemas(非推奨)
旧形式のスキーマ定義で、将来削除予定です。
現在はdisplay.formats側で型定義を行います。
eip712.domain
domainは、EIP712のDomain Separatorに含まれるキーと値の制約を定義します。
ウォレットは、メッセージのdomainオブジェクトに含まれる値が、ここで指定されたすべてのキーと一致することを確認しなければなりません。
よく使われるドメインキーは以下です。
| キー | 意味 |
|---|---|
name |
メッセージ検証者名 |
version |
バージョン |
chainId |
対象チェーンID |
verifyingContract |
検証用コントラクトアドレス |
メッセージ側のdomainには、ここに書かれていないキーが追加で含まれていても問題ありません。
eip712.deployments
deploymentsは、ドメイン内のchainIdとverifyingContractの組を制約します。
ウォレットは以下を必ず検証します。
- メッセージdomainに
chainIdとverifyingContractが存在する - その組み合わせが、
eip712.deploymentsのいずれかと一致する
構造はEVMコントラクトと同じです。
| キー | 意味 |
|---|---|
chainId |
EIP-155チェーンID |
address |
verifyingContractのアドレス |
eip712.domainSeparator
domainSeparatorは、EIP712のドメインハッシュ値そのものを直接指定する方法です。
ウォレットは、以下を必ず検証します。
- メッセージのDomainをEIP712の手順でハッシュ化し
- その結果が
domainSeparatorと一致する
スマートコントラクトが「Domainの構成要素ではなく、ハッシュ値だけをコード中に持っている」場合でも、この方法で正しい検証先を特定できます。
EIP712コンテキストの例
{
"context" : {
"eip712": {
"domain": {
"name": "Permit2"
},
"deployments": [
{
"chainId": 1,
"address": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
},
{
"chainId": 42161,
"address": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
}
]
}
}
}
この定義は、以下を同時に満たすメッセージだけに、このERC7730ファイルを適用する、という意味になります。
-
domain.nameが「Permit2」であること -
domain.chainIdと domain.verifyingContract が
Ethereum Mainnet または Arbitrum 上の Permit2 コントラクトであること
メタデータセクション
metadataは、コンテキストで特定されたコントラクトやメッセージに関する「信頼できる定数情報」をまとめる領域です。
ウォレットは、UI表示や値検証、フォーマット補助にこの情報を利用できます。
metadata.owner
コントラクトまたは検証コントラクトの運営主体の表示名です。
ウォレットは署名画面で相手先を示すために使用できます。
metadata.contractName
コントラクトの表示名です。
トークン名やプロトコル名など、人間が理解しやすい名称を示します。
metadata.info
追加の構造化情報です。
| キー | 意味 |
|---|---|
deploymentDate |
デプロイ日時 |
url |
公式サイトURL |
metadata.token(ERC20専用)
ERC20コントラクトがname()、symbol()、decimals()を実装していない場合の補完情報です。
通常はオンチェーンから取得できるため、対応しているコントラクトでは記載すべきではありません。
| キー | 内容 |
|---|---|
name |
トークン名 |
ticker |
シンボル |
decimals |
小数桁数 |
metadata.constants
フォーマッタで再利用する定数の集合です。
$.metadata.constants.KEY_NAMEというパスで参照します。
{
"metadata": {
"constants": {
"nativeAssetAddress": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
}
}
}
これは「ネイティブ通貨を表す疑似アドレス」を定数として定義する例です。
metadata.maps(コンテキスト依存定数)
チェーンごとに異なる定数などを扱うための仕組みです。
構造は次の通りです。
| キー | 説明 |
|---|---|
keyPath |
どの値で分岐するか(例:@.chainId) |
values |
分岐キーと実際の定数値の対応表 |
例:
{
"metadata": {
"maps": {
"underlyingToken": {
"keyPath": "@.chainId",
"values" : {
"1": "0xaabbccddeeff...",
"17000": "0x112233445566..."
}
}
}
}
}
ウォレットは、@.chainIdの値に対応するアドレスを自動的に選択し、
$.metadata.maps.underlyingTokenとして利用します。
一致しない場合は、このERC7730ファイルは無効と判断されます。
metadata.enums(列挙値の表示名定義)
数値やコード値を、人間が読める文字列に変換するための対応表です。
{
"metadata": {
"enums": {
"interestRateMode": {
"1": "stable",
"2": "variable"
}
}
}
}
これをenumフォーマットで参照します。
{
"display": {
"formats": {
"repay(address asset,uint256 amount,uint256 interestRateMode)": {
"intent": "Repay loan",
"interpolatedIntent": "Repay {amount} of {interestRateMode} rate loan",
"fields": [
{
"path": "amount",
"format": "tokenAmount",
"params": { "tokenPath": "asset" }
},
{
"path": "interestRateMode",
"format": "enum",
"params": { "$ref": "$.metadata.enums.interestRateMode" }
}
]
}
}
}
}
この場合、interestRateModeが1なら「stable」、2なら「variable」と表示されます。
表示セクション(Display section)
ERC7730のdisplayセクションは、コンテキストで正しくバインドされた構造化データに対して、「どのフィールドを、どの順序で、どの形式に変換して人間に表示するか」を定義する中核部分です。
このセクションは大きく以下の2つに分かれます。
- 共通フォーマット定義をまとめる
display.definitions - 実際の関数や EIP712 メッセージごとの表示ルールを定義する
display.formats
共通定義
display.definitionsは、複数のフィールドで再利用される共通フォーマット部品を定義する場所です。
各サブキーは1つの「フィールド表示テンプレート」であり、$.display.definitions.名前というパスで参照されます。
ここでは通常、pathは書かず、ラベル・フォーマット・パラメータのみを定義します。
実際にどのフィールドに適用するかは、display.formats側で指定します。
例
{
"display": {
"definitions": {
"sendAmount": {
"label": "Amount to Send",
"format": "tokenAmount",
"params": {
"tokenPath": "fromToken",
"nativeCurrencyAddress": "$.display.constants.addressAsEth"
}
}
}
}
}
この例は以下を示しています。
- 「送金額」を表示する共通ルールを
sendAmountという名前で定義 - 実際のフィールドでは
$.display.definitions.sendAmountを参照するだけで再利用可能
フォーマット定義
display.formatsは、実際の構造化データ(スマートコントラクト関数や EIP712 メッセージ)ごとの表示仕様を定義します。
コントラクト関数キーの書き方
EVMコントラクトの場合、キーは必ず「人間が読めるABIフラグメント」です。
例:
transfer(address to,uint256 value)
submitOrder((address token,uint256 amount) order,bytes32 salt)
ルールは厳密で、以下を満たす必要があります。
| ルール | 内容 |
|---|---|
| 型名 |
uint256 address bytes32 など正式名称のみ(uintは禁止) |
| 書式 | 型と引数名の間は半角スペース1つ |
| カンマ | 後ろにスペースを入れない |
| 配列 |
type[] や type[N]
|
| タプル |
(type name,...) 形式 |
| 引数名 | 以降のpath指定と完全一致していなければならない |
セレクタの一致方法
ウォレットは以下の手順で、どのformatを使うかを決定します。
- キーから引数名を除いた型シグネチャを作る
例:transfer(address,uint256) - それをkeccak256でハッシュし、先頭4バイトを取得
- トランザクションcalldataの先頭4バイトと比較
- 一致した定義を採用
同じ型シグネチャが複数ある場合は、定義ファイル自体が不正と判断されます。
構造化データフォーマット定義
display.formatsの各値は、「1つの関数呼び出し」または「1つのEIP712メッセージ型」の表示仕様をまとめたオブジェクトです。
$id
人間向け識別子です。
表示処理には直接使われません。
intent
ユーザーに「何をしようとしているのか」を短い自然文で伝えるための項目です。
形式は2種類あります。
- 単純な文字列
"intent": "Send"
- 構造化されたラベル付き表示
"intent": {
"Native Staking": "Withdraw",
"Rewards": "Consensus & Exec"
}
この場合、ウォレットは以下のようにラベル付きで表示することが期待されます。
Native Staking: Withdraw
Rewards: Consensus & Exec
interpolatedIntent
intentにフィールド値を埋め込んだ1文形式です。
"interpolatedIntent": "Send {value} to {to}"
-
{value}や{to}はfieldsに定義されたパスと一致する必要があります - 表示時は、そのフィールドに指定されたformatとparamsで整形された値が埋め込まれます
- ウォレットは、
intent + fieldsかinterpolatedIntentのいずれか、または両方を表示します
例:
{
"intent": {
"Action": "Approve",
"Type": "Batch"
},
"interpolatedIntent": "Approve {spender} to spend up to {amount} on your behalf until {deadline}",
"fields": [
{"path": "spender", "format": "addressName"},
{"path": "amount", "format": "tokenAmount"},
{"path": "deadline", "format": "date"}
]
}
fields
fieldsは、個々の引数やメッセージフィールドをどう表示するかを定義する配列です。
要素は以下の3種類のいずれかになります。
- 単一フィールド定義
{ "path": "amount", "label": "Amount", "format": "tokenAmount" }
display.definitionsへの参照($ref)
{
"path": "account",
"$ref": "$.display.definitions.sendTo",
"params": { "type": "eoa" }
}
- フィールドグループ(再帰構造)
{
"path": "param",
"fields": [
{ "path": "name", "$ref": "$.display.definitions.itemName" },
{ "path": "value", "$ref": "$.display.definitions.itemReference" }
]
}
ただし仕様上、再帰構造は可読性のためのものであり、実際には以下のようにフラット化する形が推奨されています。
{ "path": "param.name", "$ref": "$.display.definitions.itemName" }
{ "path": "param.value", "$ref": "$.display.definitions.itemReference" }
フィールドフォーマット仕様
1つのフィールドに対して、以下の情報を持ちます。
| キー | 意味 |
|---|---|
path / value
|
構造化データ中の場所、または定数 |
label |
表示用ラベル |
format |
表示変換ルール(Referenceで定義) |
params |
フォーマット固有の追加設定 |
visible |
表示条件(always / never / optional / 条件式) |
separator |
配列表示時の区切り文字 |
$id |
内部識別子 |
visible の条件指定
| 値 | 意味 |
|---|---|
always |
常に表示 |
never |
表示しない |
optional |
表示してもよい |
ifNotIn |
特定値以外の時だけ表示 |
mustMatch |
特定値以外ならエラー |
グループ仕様
グループは、複数フィールドの表示順や、配列の展開方法を制御します。
iteration モード
| モード | 表示順 |
|---|---|
sequential |
配列Aを全部→配列Bを全部 |
bundled |
A[0],B[0],A[1],B[1]… |
bundled の場合、配列サイズが一致しないとエラーになります。
パスのスライス指定
スライスはパス末尾に付与されます。
バイト列へのスライス
"tokenPath": "params.path.[0:20]"
先頭20バイトだけをアドレスとして扱います。
配列へのスライス
"path": "pools.[-1]"
最後の要素だけを表示します。
全要素適用
"path": "details.[]"
配列の全要素に同じ表示ルールを適用します。
埋め込み calldata
関数の引数が「別の関数呼び出しの calldata」を含む場合、その中身をさらにデコードして表示できます。
{
"display": {
"formats": {
"permitAndCall(bytes permit,bytes action)": {
"intent": "Execute with permit",
"fields": [
{
"path": "action",
"label": "Swap",
"format": "calldata",
"params": {
"calleePath": "@.to"
}
}
],
"required": ["action"],
"excluded": ["permit"]
}
}
}
}
この指定により、以下を行います。
-
actionを「内部の関数呼び出し」として再帰的に解析 -
calleePathで呼び出し先コントラクトを特定 - そのコントラクト用のERC7730定義を再適用
ETH送金額や承認者が親トランザクション依存の場合は、以下のように補完します。
"params": {
"calleePath": "target",
"amountPath": "@.value",
"spenderPath": "@.to"
}
これにより、ハードウェアウォレット上でも「中で実行される取引の意味」まで正しく人間可読で検証できるようになります。
リファレンス
このセクションでは、ERC7730で参照できる「コンテナ構造の値」と、「各フィールドをどのように人間向け表示に変換するか」を定義するフォーマット一覧がまとめられています。
ウォレットはここで定義された参照パスと表示形式を用いて、トランザクションやEIP712メッセージを安全に可視化します。
コンテナ構造の値
EVMトランザクション
EVMトランザクションをコンテナとする場合、@ルート以下で次の値が参照できます。
| パス | 説明 |
|---|---|
@.from |
トランザクション送信者アドレス |
@.value |
ネイティブ通貨(ETHなど)の送金額 |
@.to |
宛先アドレス(呼び出し対象コントラクト) |
@.chainId |
EIP-155 チェーンID |
EIP712メッセージ
EIP712メッセージの場合も同様に@ルートで参照しますが、意味は以下のようになります。
| パス | 説明 |
|---|---|
@.from |
署名者アドレス |
@.value |
ネイティブ通貨送金は無いので通常は0として扱われます |
@.to |
検証コントラクト(verifyingContract)のアドレス。分からない場合はERC7730適用不可 |
@.chainId |
検証コントラクトのチェーンID。不明な場合はERC7730適用不可 |
フィールドフォーマット
ここからは、fields[].formatに指定できる表示変換ルールの一覧です。
整数型フォーマット(uint / int)
raw
整数をそのまま表示します。
| 内容 | 説明 |
|---|---|
| 表示 | ローカライズされた自然な整数表記 |
| 例 |
1000 → 1000
|
amount
ネイティブ通貨量として解釈し、適切な単位とティッカーを付けます。
| 例 | 表示 |
|---|---|
0x2c1c986f1c48000 |
0.19866144 ETH |
tokenAmount
ERC20トークン量として表示します。
decimalsで割り、シンボルを付加します。
| パラメータ | 意味 |
|---|---|
tokenPath / token
|
トークンコントラクトアドレス |
nativeCurrencyAddress |
ネイティブ通貨とみなすアドレス |
threshold |
これ以上なら特別表示 |
message |
threshold超過時の表示文言(既定は Unlimited) |
例:
| 条件 | 表示 |
|---|---|
| 1000000, DAI(6dec) | 1 DAI |
| 0xFFFFFFFF, threshold設定 | Unlimited DAI |
| ETH相当値 | 0.002 ETH |
nftName
NFTコレクション内のトークンIDを名前付きで表示します。
取得できない場合はIDのみ表示します。
date
整数を日時として表示します。
| encoding | 意味 |
|---|---|
timestamp |
Unix時刻 |
blockheight |
ブロック高を推定時刻に変換 |
duration
秒数を HH:MM:SS 形式で表示します。
unit
単位付き数値として表示します。
| パラメータ | 説明 |
|---|---|
base |
単位記号(h, s, %, bpsなど) |
decimals |
小数桁 |
prefix |
SI接頭辞を付けるか |
例:36000 s → 36 ks
enum
列挙値を metadata.enums の定義に従って人間可読文字列へ変換します。
文字列型
raw
UTF-8文字列としてそのまま表示します。
バイト列型
raw
16進文字列として表示します。
calldata
内部に別の関数呼び出しcalldataを含む場合の特別フォーマットです。
ウォレットはこの中身をさらにERC7730で再帰的に解釈します。
| パラメータ | 説明 |
|---|---|
calleePath / callee
|
呼び出し先コントラクト |
selectorPath / selector
|
関数セレクタ |
chainIdPath |
対象チェーン |
amountPath |
ネイティブ通貨額 |
spenderPath |
承認者アドレス |
アドレス型
raw
EIP55形式のアドレスとして表示します。
EIP55については以下の記事を参考にして下さい。
addressName
ENSなどの信頼できる名前があればそれを表示し、なければアドレスを表示します。
| パラメータ | 説明 |
|---|---|
types |
期待されるアドレス種別 |
sources |
名前解決元(ens, localなど) |
senderAddress |
送信者と同一の場合の特別扱い |
例:
| 値 | 表示 |
|---|---|
0xd8dA...6045 |
vitalik.eth |
| Uniswapペア | Uniswap V3: WBTC-USDC |
| 自分自身 | Sender |
tokenTicker
ERC20トークンアドレスをシンボル名で表示します。
例:0xaabbcc... → MYTOKEN
アドレス種別と信頼ソース
アドレス種別
| 種別 | 意味 |
|---|---|
wallet |
自分のウォレット |
eoa |
外部所有アカウント |
contract |
既知のスマートコントラクト |
token |
既知の ERC20 |
collection |
既知のNFTコレクション |
信頼ソース
| ソース | 説明 |
|---|---|
local |
ユーザーがローカル登録した名前 |
ens |
Ethereum Name Service |
ウォレットは、指定されたソース以外の名前解決を行ってはならず、指定された型と一致しない場合は警告または拒否することが求められます。
補足
人間が読めること
ERC7730の普及における最大の課題は、「この仕様ファイルを書く手間」に対して、「それを書こうとする動機」がどれだけ生まれるか、という点にあります。
単に仕組みを用意するだけではなく、開発者が実際に書きたくなる設計でなければ、エコシステムに広く浸透しません。
この前提から、ERC7730では次のような設計方針が取られています。
まず、ERCという標準仕様の形で定義することで、ハードウェアウォレット・ソフトウェアウォレットを問わず、あらゆるウォレットが同じ形式のファイルを扱えるようにしています。
これにより、アプリ開発者が一度ERC7730ファイルを書けば、多くのウォレットで共通して利用されるという強いインセンティブが生まれます。
次に、仕様そのものが「人間の開発者が直接読んで編集できる」ことを重視しています。
JSON構造は複雑ですが、型・パス・表示ルールが明示的に書かれており、ツールに頼らなくても内容を理解し、手で修正できるように設計されています。
さらに、将来的にはERC7730ファイルの可視化や編集を支援するツール群がオープンソースで提供される予定です。
これにより、エンドユーザーが実際にウォレット上でどう見えるかを、開発者が事前に確認しながら調整できるようになります。
ウォレットの制約
ERC7730は、すべてのウォレット、特にハードウェアウォレットでの利用を強く意識して設計されています。
ハードウェアウォレットは、画面サイズ、メモリ、計算能力が限られており、かつ「表示内容と署名対象の完全な一致」がセキュリティ上必須です。
そのため、複雑なUI表現をそのまま仕様に含めることはできません。
この制約から、ERC7730では以下のような判断がなされています。
まず、レイアウトや詳細なUI構成、フィールドの再配置といった複雑な表示制御は、将来の「ウォレット依存セクション」に委ねられています。
現時点では、最小限の共通表現だけを標準化し、各ウォレットが安全に実装できる範囲に留めています。
次に、複雑な構造を持つデータでも「フラットなフィールド列」に展開できる設計になっています。
再帰構造を持つメッセージであっても、最終的には一次元のリストとして表示できるため、特にハードウェアウォレットでの実装が容易になります。このフラット化は、初期段階では強く推奨される形式です。
また、calldataのように再帰的な解析を必要とする高度なフォーマッタについては、当初は機能制限付きでの対応になることが想定されています。
特にハードウェアウォレットでは、すべての入れ子構造を完全に表示するのではなく、安全に検証できる範囲での実装から始まることが前提になっています。
国際化
ERC7730は、あえて多言語対応やローカライズを扱っていません。
意図文(intent)、ラベル、フィールド名、列挙値など、すべての表示文字列は英語で記述されることを前提としています。
これは、まず「共通で解釈可能な標準」を安定させることを優先し、その上で必要性が明確になった段階で国際化を検討する、という段階的な方針です。
将来的に仕様が広く採用され、エコシステム側で多言語表示の必要性が明確になった場合には、標準化された国際化レイヤーが追加される可能性がありますが、現行仕様には含まれていません。
バッチトランザクション(EIP5792)
EIP5792で定義されるバッチトランザクションでは、複数の操作を1つの署名でまとめて実行します。
このとき、ユーザーが「全体として何が起きるのか」を直感的に理解できる表示が重要になります。
ERC7730では、interpolatedIntentを使って各操作の意図文を生成し、それらを " and " で連結する方法が推奨されています。
例えば、以下のような個別操作がある場合です。
[
{
"function": "permit",
"interpolatedIntent": "Approve {spender} to spend {value} USDC"
},
{
"function": "swapExactTokensForTokens",
"interpolatedIntent": "Swap {amountIn} for at least {amountOutMin}"
}
]
これらを連結すると、ウォレット上では次のような1文として表示されます。
Approve Uniswap Router to spend 1000 USDC and Swap 1000 USDC for at least 0.25 WETH
さらに複雑な例として、承認・スワップ・NFTミントをまとめた場合は、
Approve DEX Router to spend 2000 USDC and Swap 2000 USDC for at least 0.5 ETH and Mint 2 NFT(s) from NftProject
のように、人間が自然に読める1文に統合されます。
ウォレット実装者向けの指針としては、以下の流れが示されています。
- バッチ内の各操作について
interpolatedIntentを生成する - それらを
" and "で連結する - 1つの要約文として表示する
- 必要に応じて個別操作の詳細表示も提供する
- どれかの補間処理に失敗した場合は、通常の
intent表示にフォールバックする
これにより、限られた画面領域でも「全体像」と「詳細」の両方を安全に確認できます。
他の構造化データ形式への拡張性
ERC7730は将来、以下のような構造化データにも拡張されることが想定されています。
- EIP2771のメタトランザクション
- ERC4337のUserOperation
- EIP5792のバッチトランザクションペイロード
特にinterpolatedIntentは、複数操作を1文にまとめる用途に非常に適しており、バッチ実行やアカウント抽象化の世界でもそのまま活用できる設計になっています。
EIP2771については以下の記事を参考にして下さい。
ERC4337については以下の記事を参考にして下さい。
共通用語の標準化
DeFiの世界では、すでに「Swap」「Supply」「Borrow」「Repay」「Stake」「Unstake」「Mint」「Burn」など、意味がほぼ共通で理解されている行為が多数存在します。
ERC7730自体は、DEXやレンディングの操作名を標準化してはいませんが、ユーザーの安全性と可読性を高めるためには、同じ種類の操作には同じ言葉を使うことが望ましいとしています。
そのため、
- 類似プロトコル間で意図文の用語を揃える
- Clear Signing用レジストリで推奨用語集を定義する
- 将来的には用語の統一を検証・強制する仕組みを作る
といった、ERC7730の上位レイヤーでの標準化が推奨されています。
これにより、ユーザーはウォレット画面上で
「Approve」「Swap」「Stake」
といった表現を見た瞬間に、どのプロトコルでも同じ意味として理解でき、フィッシングや偽UIによる誤認リスクをさらに下げることができます。
セキュリティ
ERC7730は、トランザクションやEIP712メッセージの内容を人間に分かりやすく表示するための強力な仕組みを提供しますが、その一方で「表示の仕方そのもの」を悪用される新しい攻撃面も生み出します。
悪意ある第三者が、整形ルールを細工することで「危険な取引を安全そうに見せる」ことが理論上可能になります。
特に想定されている脅威は以下のようなものです。
- 有名なコントラクトの正規フォーマットを流用し、引数だけを差し替えるパラメータ注入攻撃
- 正しく構文は合っているが、意図的に誤解を招くERC7730ファイルを登録するレジストリ汚染
- フロントエンドやCDNの改ざんにより、ユーザーが想定していないコントラクトや引数で署名させられるケース
これらを前提に、ERC7730では複数の防御層が設計されています。
バインディングコンテキスト
contextセクションは、「このERC7730ファイルが適用されてよい対象」を厳密に制限するための仕組みです。
アプリ開発者は、適用対象をできる限り狭く定義しなければならず、ウォレットはその条件をすべて検証する責務があります。
具体的には次の点が必須です。
- コントラクトアドレスやチェーンID、EIP712ドメインなどが完全一致すること
- 表示ルールが、署名対象データと暗号学的に結び付いており、通信経路やストレージ上で改ざんされていないこと
- ハードウェアウォレットのような制約環境でも、検証ロジックが省略されないこと
これにより、「正規コントラクト用の表示定義を、別の悪意あるコントラクトに流用する」ことを防ぎます。
レジストリ汚染
ERC7730ファイルを集約・配布するレジストリ自体は仕様の範囲外ですが、実運用では必ず攻撃対象になります。
そのため、安全なレジストリには次の性質が求められます。
| 対策項目 | 内容 |
|---|---|
| 由来の検証 | 各ERC7730ファイルと管理者に暗号学的な署名・証明を付与する |
| 監査可能な履歴 | 追加・承認・取り消しの履歴を改ざん不能な形で公開 |
| 所有権の紐付け | コントラクトの所有者や管理権限とERC7730定義を検証可能な形で関連付ける |
| ガバナンス | 複数署名承認、異常検知、自動監視を備えた運用体制 |
これにより、正しく見えるが実際には誤誘導を狙った定義ファイルの大量登録や差し替えを、現実的に困難にします。
外部データ解決の信頼性
ERC7730の表示は、多くの場合「外部データの解決」に依存します。
主な例は以下です。
| 種類 | 具体例 |
|---|---|
| トークンメタデータ | トークン名、シンボル、decimals
|
| NFT情報 | コレクション名、トークン名 |
| アドレス解決 | ENSなどによる名前解決 |
| 列挙値 | 数値パラメータの意味表現 |
| ABI | 関数の型解釈 |
これらのデータソースが改ざんされると、以下のような被害が起こり得ます。
- 偽物トークンが本物のUSDTのように表示される
- 悪意あるアドレスが信頼できる名前で表示される
- NFTのコレクション名が偽装される
- 列挙値の意味がすり替えられる
- ABIの差し替えで引数の意味が誤解される
攻撃例
| シナリオ | 説明 |
|---|---|
| トークン名なりすまし | 偽トークンが「USDT」というtickerを返し、正規送金に見せかける |
| ENS改ざん | 攻撃者のアドレスが「vitalik.eth」のように解決される |
対策
ウォレットは次の対応を行うべきとされています。
| 要件 | 内容 |
|---|---|
| 既知リスト管理 | 有名トークン・コントラクト・NFTのホワイトリストを持つ |
| 併記表示 | 解決名とフルアドレスの両方を何らかの形で表示 |
| 信頼レベル表示 | 名前やメタデータの取得元と信頼度を明示 |
| 警告表示 | 解決不能・未検証の場合は必ず警告を出す |
| 偽装禁止 | 未検証データを「信頼済み」のように表示しない |
補間意図文の安全性
interpolatedIntentは非常に便利ですが、文字列補間であるがゆえに、表示改ざん攻撃の余地も生まれます。
想定される攻撃
- トークン名に「
to vitalik.eth」のような文字列を含め、文意を歪める - ENS名で本来の宛先を隠す
- 非常に長い文字列で重要部分を画面外に押し出す
必須対策
ウォレットは以下を必ず満たす必要があります。
| 項目 | 内容 |
|---|---|
| フォーマット統一 | 補間時も通常表示時と完全に同じフォーマッタを使用 |
| パス検証 | 存在するフィールドのみを参照し、循環や無限再帰を禁止 |
| フィールド定義必須 |
fieldsに定義されていないパスは補間不可 |
| フォールバック | 失敗時は必ずintent+個別フィールド表示に戻す |
| 部分表示禁止 | 中途半端な補間結果を表示しない |
表示フォールバック動作
補間に失敗した場合、ウォレットは以下を行います。
- 静的な
intentを表示 - 全フィールドを通常フォーマットで個別表示
- 必要に応じて「動的説明を生成できなかった」旨を通知
リソース制約デバイスへの配慮
ハードウェアウォレットなどでは、interpolatedIntent自体を実装しない選択も許容されています。
その場合でも、必ずintentは存在し、最低限の安全な表示が可能でなければなりません。
テストと検証の推奨事項
ERC7730ファイルの作成者は、少なくとも次のケースで表示確認を行うべきとされています。
- 最小値・最大値(0、最大uint、無制限Approveなど)
- 境界日時(期限切れ、遠い未来)
- 極端に長いENS名やトークン名
- 配列の最大要素数
将来の拡張
将来的には、
- フロントエンドとコントラクト定義を暗号学的に結び付ける仕組み
- ERC7730レジストリの多者署名承認と自動監査
- 不正UIや差し替えを検出する監視基盤
などを組み合わせることで、
- フィッシング
- パラメータ注入
- フロントエンド改ざん
といった攻撃のコストを大幅に引き上げることが想定されています。
引用
Laurent Castillo (@lcastillo-ledger), Derek Rein (@arein), Pierre Aoun (@paoun-ledger), Arik Galansky (@arikg), Bartosz Rozwarski (@llbartekll), Kaan Uzdogan (@kuzdogan), "ERC-7730: Structured Data Clear Signing Format [DRAFT]," Ethereum Improvement Proposals, no. 7730, February 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7730.
最後に
今回は「構造化データを人間が理解できる形で表示して安全に署名確認できる仕組みを提案しているERC7730」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!