はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、複数のウォレット拡張が同時に存在してもユーザーが使うウォレットを正しく選択できる仕組みを提案しているEIP6963についてまとめていきます!

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

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

EIP6963は、Webページ上で複数のウォレットプロバイダーを安全かつ公平に発見するための仕組みです。 ここでいうウォレットプロバイダーとは、MetaMaskのようなブラウザ拡張型ウォレットがWebページに注入するEthereum用のインターフェースを指します。

従来、Ethereum対応のWebアプリケーションは、window.ethereum というグローバルオブジェクトを通じてウォレットと通信してきました。 この window.ethereum は、EIP1193という仕様で定義された「Ethereum Provider」をWebページに提供するための共通インターフェースです。

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

EIP6963では、この window.ethereum に依存しない、別のウォレット検出方法を定義しています。 具体的には、JavaScriptのwindowイベントを利用した双方向の通信プロトコルを導入します。

これにより、以下が可能になります。

  • Webページ上で複数のウォレット拡張を同時に検出できます。
  • 利用者がどのウォレットを使うかを明示的に選択できます。
  • Ethereumライブラリとウォレット拡張が、標準化された方法でやり取りできます。

つまり、EIP6963は「どのウォレットが使われるか」を拡張機能の都合ではなく、ユーザーの選択に委ねるための仕組みです。

動機

現在のウォレット注入方式の問題点

現在、ブラウザ拡張型ウォレットは、Ethereumのプロバイダー(EIP1193)を window.ethereum に直接注入します。 この方式には、構造的な問題があります。

まず、複数のウォレット拡張を同時にインストールすると競合が発生します。 理由は、すべてのウォレットが同じ window.ethereum を使おうとするためです。

さらに、ブラウザ拡張がWebページに読み込まれる順番は不定です。 これはブラウザや環境によって変わり、安定しません。

その結果、次のような状況が起こります。

  • どのウォレットが window.ethereum を占有するかは制御できません。
  • 最後に読み込まれたウォレットが勝つ、という状態になります。
  • ユーザーは自分がどのウォレットを使っているのか把握しづらくなります。

これは、レースコンディションと呼ばれる問題です。 レースコンディションとは、複数の処理が競合し、実行順によって結果が変わってしまう状態を指します。

ユーザー体験と新規ウォレットへの影響

この問題は、単に技術的な不具合にとどまりません。

まず、ユーザー体験が悪化します。

  • 複数のウォレットを使い分けたい場合でも、実質的に1つしか使えません。
  • 意図しないウォレットが選ばれる可能性があります。
  • Webアプリ側も、どのウォレットと接続しているのか判断しにくくなります。

次に、新しいウォレットプロバイダーにとって大きな障壁になります。

  • ユーザーは「他のウォレットをアンインストールしないと使えない」状況になります。
  • 結果として、有名なウォレットだけが使われ続けます。
  • 新規参入が難しくなり、健全な競争が阻害されます。

不公平な対策と相互運用性の欠如

一部のウォレット拡張は、この問題を回避するために独自の対策を行っています。

例えば、以下のような方法です。

  • あえて注入タイミングを遅らせる
  • 既存の window.ethereum を上書きする

しかし、これは根本的な解決ではありません。

  • 他のウォレットを排除する動きになります。
  • ウォレット間で公平な競争が成り立ちません。
  • 標準化されていないため、相互運用性がありません。

結果として、エコシステム全体の健全性が損なわれます。

EIP6963が目指す解決策

EIP6963は、こうした問題を解消するために設計されています。

中心となる考え方は以下です。

  • 単一の window.ethereum に依存しません。
  • windowイベントを使って、ウォレットとWebアプリが通信します。
  • 複数のウォレットを同時に検出できます。
  • 利用者がウォレットを選択できます。

これにより、以下の効果が期待されます。

観点 改善内容
ユーザー体験 利用者自身が使うウォレットを選べます
公平性 どのウォレットも同じ条件で検出されます
拡張性 新しいウォレットが参入しやすくなります
相互運用性 Ethereumライブラリと拡張が標準化された方法で連携できます

EIP6963は、Ethereumのウォレット体験をより安全で、公平で、拡張性のあるものにするための提案です。 既存の仕組みを壊すのではなく、共存と選択を可能にする方向で問題を解決しようとしています。

仕様

定義

EIP6963の文章中で使う言葉を、ここで定義しています。

Wallet Provider

ユーザーの鍵(秘密鍵)を管理し、Ethereum上のトランザクション(送金やコントラクト呼び出し)を実行できるユーザーエージェントです。 ユーザーエージェントは、ブラウザ拡張やモバイルアプリなど「ユーザーの代わりに処理をするソフト」のことです。

Decentralized Application(DApp)

ウォレットがWebページに公開するWeb3 API(Ethereumに接続するAPIなど)を使うWebページです。 つまり、ウォレットを通じてEthereumとやり取りするWebアプリを指しています。

Provider Discovery Library

DAppがウォレットとやり取りするのを助けるライブラリやソフトウェアです。 具体例としては、DApp側で「見つかった複数のウォレットを一覧表示して、ユーザーに選ばせる」ような処理をまとめる役割を担います。

プロバイダー情報

EIP6963では、各ウォレットプロバイダーを「この情報で名乗ってください」という形式で定義しています。 その形式が EIP6963ProviderInfo です。

EIP6963ProviderInfoに含めるべき項目

EIP6963ProviderInfo の必須フィールドは以下です。 EIP6963では、これらが必ず入っている必要があります。

フィールド 型 役割
uuid string そのページの生存期間中に、同一っぽいプロバイダーを区別できるようにする一意ID
name string DAppに表示するウォレット名(人が読める名前)
icon string ウォレットのアイコン画像を指すURI
rdns string 安定した識別子として使う逆DNS形式の文字列

また、追加プロパティを入れることも許されています。 DAppが知らない追加プロパティは無視するべきです。

uuid の意味

uuid はUUIDv4に準拠しなければなりません。 UUIDv4はランダム性が高く、別々に生成されたIDが衝突しにくい仕組みです。 これにより、似たようなプロパイダー情報が複数あっても、別物として識別できます。

ここで重要なのは、「同じページの表示中(ページの生存期間中)」に、同じような情報を持つ複数のEIP-1193のProviderセッションを区別できることです。

name の意味

ユーザーに表示するためのローカルな別名です。 例として「Example Wallet Extension」などが挙げられています。 DAppのUIでウォレット選択画面に出す用途です。

icon の意味

画像を指すURIです。 後述の「Images/Icons」で追加の制約があります。

rdns の意味と注意点

rdns はReverse-DNS(逆DNS)形式の識別子です。com.example.subdomain のようにドメインを逆順に並べます。 ウォレット側はこの値を必ず提供する必要があります。

ただし、これは自己申告の値です。 ブラウザのユーザーエージェント文字列と同じで、次のようなことが起こり得ると明記されています。

  • 値が不明な場合がある
  • 無効な形式の場合がある
  • 正しくない場合がある
  • 別のウォレットになりすます値が入る場合がある

そのためDAppは、これらの失敗ケースでもDAppの機能が大きく壊れないように扱うべきです。

インターフェース定義

ts
/**
 * Represents the assets needed to display a wallet
 */
interface EIP6963ProviderInfo {
  uuid: string;
  name: string;
  icon: string;
  rdns: string;
}

画像・アイコン(Images/Icons)

EIP6963では、アイコンを「URIエンコードされた画像」にする方針を取っています。 これにより、取得方法をHTTPに限定せず、data URIなど色々な方法で扱えます。

例として、SVGとPNGのdata URIが示されています。

txt
# svg (data uri)
data:image/svg+xml,<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" viewBox="0 0 32 32"><circle fill="red" cx="16" cy="16" r="12"/></svg>
# png (data uri)
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==

icon の必須要件

  • icon はRFC-2397で定義された data URI でなければなりません。 data URIは、画像データそのものを文字列として埋め込む形式です。
  • 画像は正方形で、最低96x96pxが望ましいです。
  • フォーマットは、PNG / WebP / SVG のような「可逆圧縮」または「ベクター画像」が推奨です。 可逆圧縮は、画質を落とさずに圧縮できる形式です。

SVGの安全性要件

SVGはJavaScriptを実行できる場合があります。 そのため、アプリやライブラリはSVGを描画するときに、必ず <img> タグを使わなければなりません。 これにより、信頼できないJavaScriptが実行されないようにします。

RDNS(逆DNS)

rdns は「セッションをまたいでも安定する識別子」を提供するためのものです。 逆DNS形式を使う理由は、名前の衝突(同じ識別子を別の人が使ってしまうこと)を避けるためです。

形式としては次のイメージです。

  • 先頭が「プロバイダーが管理しているドメイン」を逆順にしたもの
  • その後ろにサブドメインや製品名などを付ける

例:com.example.MyBrowserWallet

rdns に関する要件

文章中の要件をそのまま整理すると以下です。

要件 レベル 内容
rdns はRFC-1034のDomain Nameとして有効であること MUST 正しいドメイン名として成立している必要があります
rdnsのDNS部分はプロバイダーが管理する有効なドメインであるべき SHOULD ただし必須ではありません
DAppは逆DNSの規約に従っていないプロバイダーを拒否してもよい MAY 受け入れない判断をしてもよい、という意味です
DAppは rdns で機能判定をするべきではない SHOULD NOT 自己申告で偽装も可能なので危険です
機能発見や検証はこの仕様の範囲外 - EIP6963では扱いません

プロバイダー詳細

EIP6963では、ウォレットを「情報」と「実際のProvider本体」をセットで通知します。 そのための形式が EIP6963ProviderDetail です。

必須構造は以下です。

  • info: EIP6963ProviderInfo
  • provider: EIP-1193で定義される EIP1193Provider

コードは以下です。

ts
interface EIP6963ProviderDetail {
  info: EIP6963ProviderInfo;
  provider: EIP1193Provider;
}

ウィンドウイベント

EIP6963は window.ethereum を奪い合う問題を避けるために、windowイベントを使った発見プロトコルを定義します。

ここでの前提は、DAppのコードとウォレット拡張のコードが「どちらが先に実行されるか保証できない」ことです。 そのため、レースコンディションが発生しても必ず発見できるようにイベントが設計されています。

イベント送信と受信は次のAPIで行います。

  • 送信:window.dispatchEvent
  • 受信:window.addEventListener

アナウンスイベントとリクエストイベント

EIP6963が使うイベントは2種類です。

  • ウォレット → DApp:eip6963:announceProvider
  • DApp → ウォレット:eip6963:requestProvider

この2つで「双方向の呼びかけ」を作り、どちらが先に実行されても最終的に合流できるようにします。

announceProvider(ウォレットが名乗るイベント)

EIP6963AnnounceProviderEvent は CustomEvent である必要があります。 type は "eip6963:announceProvider" 固定で、detail に EIP6963ProviderDetail を入れます。

さらに detail は Object.freeze() で凍結するのが望ましいです。 凍結とは、後から中身を変更できないようにして、通知内容の不変性を守ることです。

ts
// Announce Event dispatched by a Wallet
interface EIP6963AnnounceProviderEvent extends CustomEvent {
  type: "eip6963:announceProvider";
  detail: EIP6963ProviderDetail;
}

requestProvider(DAppが探すイベント)

EIP6963RequestProviderEvent は Event で、type は "eip6963:requestProvider" 固定です。

ts
// Request Event dispatched by a DApp
interface EIP6963RequestProviderEvent extends Event {
  type: "eip6963:requestProvider";
}

ウォレット側の必須動作

EIP6963でウォレットに求められる動作は以下です。

  • eip6963:announceProvider を window.dispatchEvent() で送る
  • eip6963:requestProvider を window.addEventListener() で監視する
  • requestを受け取ったら、もう一度 announce を送る

3が重要で、DAppがイベントリスナーを作る前にウォレットがannounceしてしまった場合でも、DAppがrequestを投げ直せば再通知されるため、取りこぼしが起きにくくなります。 これにより window.ethereum を汚染して上書き合戦をする必要がなくなり、ウォレットの選択が不安定になる状況を避けられます。

ウォレット側の例は以下です。

ts
let info: EIP6963ProviderInfo;
let provider: EIP1193Provider;

const announceEvent: EIP6963AnnounceProviderEvent = new CustomEvent(
  "eip6963:announceProvider",
  { detail: Object.freeze({ info, provider }) }
);

// The Wallet dispatches an announce event which is heard by
// the DApp code that had run earlier
window.dispatchEvent(announceEvent);

// The Wallet listens to the request events which may be
// dispatched later and re-dispatches the `EIP6963AnnounceProviderEvent`
window.addEventListener("eip6963:requestProvider", () => {
  window.dispatchEvent(announceEvent);
});

DApp側の必須動作

DApp側は以下の動作が必須です。

  • eip6963:announceProvider を window.addEventListener() で監視する
  • そのイベントリスナーはページの生存期間中、外してはいけない
  • イベントリスナーを用意した後に eip6963:requestProvider を window.dispatchEvent() で送る

「ページの生存期間中」と明記されているのは、初回ロード直後だけでなく、遅れてロードされたウォレットや再通知にも対応し続けるためです。

DApp側の例は以下です。

ts
// The DApp listens to announced providers
window.addEventListener(
  "eip6963:announceProvider",
  (event: EIP6963AnnounceProviderEvent) => {}
);

// The DApp dispatches a request event which will be heard by 
// Wallets' code that had run earlier
window.dispatchEvent(new Event("eip6963:requestProvider"));

複数ウォレットの扱いと再探索

EIP6963では、DAppが複数の EIP6963ProviderDetail を保存しておくことを許しています。 これにより、ユーザーが後から別のウォレットに切り替えたい場合でも、DAppはすぐにそのウォレットへトランザクション送信先を切り替えられます。

また、DAppは必要に応じて、再度 eip6963:requestProvider を送って探索をやり直してもよいです。 その場合、前回とは違うウォレット集合が見つかる可能性もあります。

イベント設計がレースコンディションを吸収する理由

EIP6963のイベント構成は、以下の状態どちらでも最終的にウォレットを発見できるようにしています。

  • ウォレットが先に動く(announce が先に飛ぶ)
  • DAppが先に動く(announce listener が先に準備される)

DAppは起動時に必ず request を投げ、ウォレットは request を受けたら必ず announce を再送するので、実行順が不定でも合流できます。

理解しやすいように流れを図にすると以下です。

sequenceDiagram
  participant Wallet as Wallet(拡張)
  participant DApp as DApp(Webページ)

  rect rgb(245,245,245)
  note over Wallet,DApp: どちらが先に動くか不定
  end

  DApp->>DApp: announceProvider のリスナーを登録
  DApp->>Wallet: requestProvider を dispatch
  Wallet->>DApp: announceProvider を dispatch(info + provider)
  DApp->>DApp: 受け取った provider を保存・UIに表示
  Wallet-->>Wallet: requestProvider を監視し続ける
  DApp-->>DApp: announceProvider を監視し続ける

補足

以前の案では、window.ethereum のような「1つのグローバルオブジェクト」に依存してウォレットを見つける仕組みが前提になっていました。 しかしこの方式は、複数の拡張機能が同じ場所に書き込むため、上書きが起きやすく、結果が不安定になります。

EIP6963がイベントベースを選んだ理由は、主に以下の問題を避けるためです。

まず、レースコンディションです。 ウォレット拡張とDAppのどちらが先に動くかは保証できないので、先に動いた側の情報が取りこぼされることがあります。

次に、名前空間の衝突です。 複数の拡張機能が同じ window.ethereum を奪い合うので、「どのウォレットが有効になっているのか」が安定しません。

さらに、「pollution攻撃」を避ける意図があります。 共有されるオブジェクト(ここでは window.ethereum)に意図しない値や偽の実装を混ぜて、他のコードの挙動を壊したり、誤動作させたりするタイプの攻撃です。 共有されていて書き換え可能(mutable)なオブジェクトは、こうした攻撃の影響を受けやすいです。

その点、イベントベースの方式は「共有オブジェクトを奪い合う」構造ではなく、DAppとウォレットがイベントで相互に呼びかける形になります。 イベントのやり取りで双方向チャネルを作り、必要に応じて何度でも「再探索・再通知」をやり直せるので、時間が経って状況が変わっても追従できます。

イベント名については、JavaScriptのイベント命名の慣習に合わせています。 具体的には、現在形(announce / request のような形)で書き、さらに接頭辞として eip6963: を付けています。 これは、イベント名が他の用途と衝突しないようにするためです。

インターフェース

ProviderInfoを標準化する理由

EIP6963が EIP6963ProviderInfo を標準化するのは、DAppが「ユーザーに見せるためのウォレット選択UI」を作れるようにするためです。 ウォレット名(name)やアイコン(icon)など、選択画面に必要な情報が揃っていれば、DAppはウォレット一覧を人にとって分かりやすい形で表示できます。

特に、Web3Modal、RainbowKit、Web3-Onboard、ConnectKit のような「ウォレット選択モーダルを自動生成するライブラリ」を使うDAppにとっては重要です。 これらのライブラリは、ウォレットのメタ情報が揃っているほど、一覧表示や選択体験をきれいに作れます。

ProviderDetailでEIP1193を触らない理由

EIP6963では、ウォレットが通知する内容として EIP6963ProviderDetail を使います。 これは info(表示用メタ情報)と provider(実体のProvider)をまとめたものです。

ここで重要なのは、provider の型としてEIP1193のProviderをそのまま使い、EIP1193自体には手を入れていない点です。 理由は互換性(昔のDAppが壊れないこと)を守るためです。

この構造のおかげで、以下が成立します。

  • EIP6963に対応したDAppは、EIP6963対応ウォレットとイベント経由で接続できる
  • EIP6963対応ウォレットは、従来どおり window.ethereum に EIP1193のProviderを注入して、古いDAppにも対応できる

ただし注意点も明記されています。 古いDApp、または EIP6963対応DAppであっても「古いウォレット」に繋ぐ場合、複数ウォレットが同時にいる状況では、正しいウォレットが選ばれる保証はできません。 イベントベースの選択ができない組み合わせでは、結局 window.ethereum の奪い合い問題に戻るからです。

互換性

EIP6963は window.ethereum を置き換えることを要求していません。 そのため、既存のアプリがすぐ壊れるような変更にはなりません。 更新できないDAppもそのまま動かせます。

一方で、推奨される運用はかなりはっきり書かれています。

DApp側に推奨される方針

DAppは EIP6963を実装することが推奨されています。 理由は、複数ウォレットを発見できるようにして、ユーザーが選べる体験を作るためです。

さらに、DAppは window.ethereum の利用を基本的に無効化し、EIP6963で発見に失敗したときだけフォールバックとして使うべきです。 フォールバックは「代替手段」です。 つまり、イベント探索がうまくいかなかった場合だけ window.ethereum に頼る、という方針です。

ウォレット側に推奨される方針

ウォレットも window.ethereum との互換性を維持するべきです。 EIP6963未対応のDAppはまだ多く、互換性を切ると利用できないサイトが増えるからです。

しかし window.ethereum をそのまま直接注入すると、過去の名前空間衝突問題が再発します。 そこで、ウォレット側には次の運用も推奨されています。

  • まず、ウォレット固有の名前空間(例:window.<walletSpecific>)にproviderを注入する
  • そのオブジェクトを window.ethereum に「プロキシ(proxy)」して提供する

プロキシは「中継する仕組み」です。 直接同じオブジェクトを奪い合うのではなく、ウォレット固有の場所を正本として持ちつつ、互換性のために window.ethereum を提供する、という考え方です。

推奨される運用の整理

対象 推奨内容 ねらい
DApp EIP6963を実装する 複数ウォレット発見と選択を可能にする
DApp window.ethereum は探索失敗時のみ使う 競合や不安定さを避ける
ウォレット window.ethereum 互換を維持する 未対応DAppでも動くようにする
ウォレット 固有namespaceに注入し、window.ethereumへプロキシする 名前空間衝突を減らす

補足:実行順が不定でも成り立つ理由

EIP6963の設計思想として、「DAppとウォレットのどちらが先に動くか分からない」前提が強く意識されています。 イベントの再通知が前提に入っているので、DAppが後から起動しても request を投げれば拾えますし、ウォレットが後から起動しても announce を投げれば拾えます。 この性質が「時間が経っても再オーケストレーションできる」という説明につながっています。

sequenceDiagram
  participant Wallet as Wallet(拡張)
  participant DApp as DApp(Webページ)

  note over Wallet,DApp: 実行順が不定でも成立させる
  DApp->>DApp: announceProvider のリスナー登録
  DApp->>Wallet: requestProvider を dispatch
  Wallet->>DApp: announceProvider を dispatch(再通知も含む)
  DApp->>DApp: 候補を保存してユーザーに選ばせる

参考実装

Wallet Provider

既存のパターンと並行してこの新しいインターフェースをサポートするために、ウォレットプロバイダーによって注入されるスクリプトの参照実装は以下です。

ts
function onPageLoad() {
  let provider: EIP1193Provider;

  window.ethereum = provider;

  function announceProvider() {
    const info: EIP6963ProviderInfo = {
      uuid: "350670db-19fa-4704-a166-e52e178b59d2",
      name: "Example Wallet",
      icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
      rdns: "com.example.wallet"
};
    window.dispatchEvent(
      new CustomEvent("eip6963:announceProvider", {
        detail: Object.freeze({ info, provider }),
      })
    );
  }

  window.addEventListener(
    "eip6963:requestProvider",
    (event: EIP6963RequestProviderEvent) => {
      announceProvider();
    }
  );

  announceProvider();
}

DAppの参考実装

ブラウザ拡張機能によって注入される複数のウォレットプロバイダーを表示・追跡するDAppの参照実装です。

ts
const providers: EIP6963ProviderDetail[];

function onPageLoad() {

  window.addEventListener(
    "eip6963:announceProvider",
    (event: EIP6963AnnounceProviderEvent) => {
      providers.push(event.detail);
    }
  );

  window.dispatchEvent(new Event("eip6963:requestProvider"));
}

セキュリティ

EIP1193のセキュリティ考慮事項

つまり、EIP6963を実装する側(DApp、ウォレット、ライブラリ)は、EIP1193が想定している脅威や注意点を理解したうえで実装する必要があります。

また、実装者は「利用するウォレットプロバイダーが提示しているガイダンス」も考慮して従うことが期待されています。 ウォレットごとに実装の癖や、推奨する接続・署名フローがあるため、EIP1193だけ読んで終わりでは足りないという意味です。

ウォレットプロバイダーオブジェクトのプロトタイプ汚染

ブラウザ拡張(ウォレット拡張を含む)は、設計上、Webページの内容を変更できます。 同じ理由で、ウォレットが提供するProviderオブジェクトの中身も、ページ側や別の拡張から変更され得ます。

Providerオブジェクトは、トランザクションデータ(送金先、金額、コントラクト呼び出し内容など)をやり取りするための「非常に信頼度が高いインターフェース」と見なされています。 ここが意図せず書き換えられると、DAppとウォレットの間のやり取りが予想外の形で改変される可能性があります。

その対策として、ベストプラクティスは次です。

  • ウォレットが eip6963:announceProvider を送る前に、Object.freeze() でEIP1193のProviderオブジェクトを凍結する 凍結とは、後からプロパティを追加・変更できないようにすることです。

ただし、ここには現実的な難しさがあります。 Webの互換性の都合で、ページ側がProviderオブジェクトに「モンキーパッチ」を当てたい場合があります。

モンキーパッチは、既存のオブジェクトや関数を後から上書きして動作を変えることです。 古いサイト互換や計測・ラップなどで使われがちです。 凍結してしまうと、こうした上書きができなくなります。

そのため、ウォレット実装者は「セキュリティ」と「Web互換性」のトレードオフを考慮する必要があると書かれています。

整理すると以下です。

目的 手段 得られるもの 失う可能性があるもの
Provider改変を防ぐ Object.freeze(provider) 予期しない書き換え耐性が上がる 互換のためのモンキーパッチができない場合がある

ウォレットの偽装と改ざん

ページ側や別拡張が、他のウォレットを妨害したり、別のウォレットになりすましたりする可能性も考慮すべきだと書かれています。 そのためDApp側は、プロパティや関数が改変される「不正な挙動」を能動的に検知することが期待されています。

検知方法の一例として、uuid の一致が挙げられています。 具体的には、2つの EIP6963ProviderInfo の uuid が同じなら「何かがおかしい可能性がある」と判断する材料になります。

ただし、uuid チェックだけで十分とは書かれていません。 DAppやDiscoveryライブラリは、他にも EIP6963ProviderInfo が改ざんされる可能性を考慮し、追加の対策も検討すべき、とされています。 ここは「手段はこれに限らないので、実装側で広く考えてください」という書き方です。

SVGのJavaScript実行を防ぐ

アイコンとしてSVGを使える設計は便利ですが、SVGにはJavaScriptコードを含められることがあります。 このJavaScriptはページのコンテキストで実行され得るため、ページ内容や他のウォレット関連の要素を改変できてしまいます。これはXSS(クロスサイトスクリプティング)相当のリスクです。

そのためDAppは、アイコンを描画する際にこのリスクを考慮し、悪意ある画像が「ページの改ざんを隠すための手段(難読化の道具)」として使われないように対策を取る必要があります。

仕様の前の部分でも触れたとおり、SVGを扱う際に <img> タグで描画する、という制約はこのリスクと強く関係します。 innerHTML で埋め込むなどのやり方だと、SVG内のスクリプトが実行される方向に寄りやすく、危険になりがちです。

DApp側の安全寄りの描画例は以下です。

html
<!-- SVG icon は <img> で描画する -->
<img alt="wallet icon" src="data:image/svg+xml,..." />

ウォレットのフィンガープリンティングを防ぐ

フィンガープリンティングは、ユーザー環境を特定するための手法です。 ここでは「window.ethereum を注入しているかどうか」や「どのウォレットが入っているか」が、ユーザー識別の材料になる問題を指しています。

EIP6963のイベントループ設計には利点があり、DAppとウォレットのどちらが開始しても成立します。 この性質により、ウォレット実装者は「すべてのページに対して常に自分を名乗るかどうか」を再検討できます。

代替案の例として以下が挙げられています。

  • DAppが eip6963:requestProvider を送るまで、providerオブジェクトの注入自体を待つ
  • requestを受け取った段階で、ユーザーに「アドレス共有してよいか」をUIで同意取得する
  • これにより、ウォレットは「private connect(プライベート接続)」のような機能を提供できる

プライベート接続は、ページを開いた瞬間にウォレットの存在やアドレスを匂わせず、ユーザーが明示的に許可したときだけ公開する考え方です。

ただし、この方式を取る場合でも注意点があります。 EIP6963に対応していないDAppに対する後方互換性をどう維持するかを、ウォレット側は考える必要があります。 つまり「フィンガープリンティング耐性を上げたい」だけで window.ethereum を完全にやめると、古いDAppが動かなくなる可能性があります。

流れを図にすると以下です。

sequenceDiagram
  participant DApp as DApp
  participant Wallet as Wallet(拡張)
  participant User as User

  DApp->>Wallet: eip6963:requestProvider
  Wallet->>User: アドレス共有の同意UIを表示
  User-->>Wallet: 同意 / 拒否
  alt 同意
    Wallet->>DApp: eip6963:announceProvider(providerを通知)
  else 拒否
    Wallet-->>DApp: 何も通知しない、または限定的に通知
  end
  note over Wallet: ただし非対応DApp向け互換性も検討が必要

引用

Pedro Gomes (@pedrouid), Kosala Hemachandra (@kvhnuke), Richard Moore (@ricmoo), Gregory Markou (@GregTheGreek), Kyle Den Hartog (@kdenhartog), Glitch (@glitch-txs), Jake Moxey (@jxom), Pierre Bertet (@bpierre), Darryl Yeo (@darrylyeo), Yaroslav Sergievsky (@everdimension), "EIP-6963: Multi Injected Provider Discovery," Ethereum Improvement Proposals, no. 6963, May 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6963.

最後に

今回は「複数のウォレット拡張が同時に存在してもユーザーが使うウォレットを正しく選択できる仕組みを提案しているEIP6963」についてまとめてきました! いかがだったでしょうか?

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

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