# [ERC7946] ウォレット接続なしでQR/NFCからバッチトランザクションを実行するUWULinkの仕組みを理解しよう！

> ERC7946は、Universal Wallet Uplink（UWULink）と呼ばれるプロトコルです。

- URL: https://cardene.dev/articles/74cb1edb51abdcb19b29
- 公開日: 2026-03-30
- 更新日: 2026-03-30
- トピック: Blockchain
- タグ: solidity, smartcontract, ブロックチェーン, walletconnect

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください！

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://twitter.com/cardene777

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

今回は、ウォレット接続なしにQRコードやNFCだけでバッチトランザクションを実行できるプロトコルを提案している「**ERC7946**」についてまとめていきます。

:::message
以下にまとめられているものを解説しながらまとめていきます。
:::

https://eips.ethereum.org/EIPS/eip-7946

dAppがユーザーのアドレスを知ることなく、複数のコントラクト呼び出しを1つのアトミックなトランザクションとしてウォレットに送信できるプロトコルです。

## 概要

**ERC7946**は、Universal Wallet Uplink（UWULink）と呼ばれるプロトコルです。

dAppからウォレットへの一方向通信で、複数のコントラクト呼び出しを1つのアトミックなバッチトランザクションとして実行できます。
QRコードやNFCタグのような低帯域チャネルに適したProtocol Buffersベースのコンパクトなバイナリメッセージ形式を使います。

コール一覧を直接含めるStatic Modeと、オンチェーンのResolverコントラクトを参照して動的にコール一覧を生成するProgrammable Modeの2つのモードが用意されています。

![uwulink-overview.png](/images/qiita-d4d8219f-205f-4154-b504-f1ac29a15462.webp)

:::message
**EIP7702**については以下の記事を参考にしてください。
:::

[\[EIP7702\] EOAをコントラクトアカウントに変換する仕組みを理解しよう！](/articles/13a828c97786aab953e4)

:::message
**EIP5792**については以下の記事を参考にしてください。
:::

[\[EIP5792\] バッチトランザクションを実行するJSON-RPCメソッドの仕組みを理解しよう！](/articles/45da7ebe31ff0ee860a2)

## 動機

dAppではトークンのapproveとswapのように、複数のステップを踏む操作が必要になることが多くあります。
従来はステップごとにユーザーの確認が必要で、ウォレット接続も前提でした。
**EIP5792**や**EIP7702**でバッチ実行が可能になりましたが、dAppとウォレットの双方向接続を前提としているため、UXの摩擦やプライバシーの問題が残っていました。

#### アトミックマルチコール

**EIP7702**の登場により、EOA（外部所有アカウント）でもスマートコントラクトウォレットのようにバッチトランザクションを実行できるようになりました。
トークンのapprove・swap・Transferを1回の確認で済ませられるため、部分的な失敗のリスクがなくなります。

#### プライバシー保護

現在のウォレット接続フロー（**EIP1193** / **EIP1102**）では、トランザクションを送る前からdAppにユーザーのアドレスが渡ります。
UWULinkは一方向通信なので、dAppはユーザーのアドレスやウォレット情報を一切知ることができません。
トランザクションがブロックチェーンにブロードキャストされて初めてアドレスが公開されます。

#### オフライン・一方向通信

デスクトップからモバイルへのQRコード読み取り、店頭のNFCタップ、印刷物のQRコードなど、セッションを確立せずにトランザクションリクエストを伝えたいケースがあります。
WalletConnectのようなセッション型プロトコルは、こういった一回限りのやり取りには重すぎます。
UWULinkなら、QRコードをスキャンするだけでオンチェーンのアクションを完結できます。

#### コンパクトなエンコーディング

QRコードやNFCにはデータサイズの制限があります。
**EIP681**のようなURI形式ではhexエンコードされたアドレスやcalldataでサイズが膨らみますが、UWULinkはProtocol Buffers（Protobuf）によるバイナリシリアライゼーションを採用しており、同じ情報をはるかに小さなサイズで表現できます。

:::message
**EIP681**については以下の記事を参考にしてください。
:::

[\[EIP2681\] Etherumアカウントのnonceの範囲を制限する提案を理解しよう！](/articles/5c48b751358eb85d6821)

#### プログラマビリティ

コール一覧がオンチェーンのステートやユーザー固有のデータに依存する場合（例えば対象ユーザーが受け取れるエアドロップの一覧など）、すべてのデータを静的にエンコードするのは非現実的です。
Programmable Modeを使えば、dAppはResolverコントラクトのアドレスと入力データだけをペイロードに含めればよく、QR/NFCのデータサイズを抑えながら複雑なロジックを処理できます。

## 仕様

#### プロトコルの概要

UWULinkは以下の特性を持つプロトコルです。

| 特性 | 詳細 |
|------|------|
| 一方向通信 | dApp→ウォレットのみ。ウォレットからdAppへのデータ返送なし |
| アトミック実行 | バッチ内のすべてのコールが成功するか、すべてリバートする |
| 認証不要 | ウォレットがdAppに対して事前の認証やアドレス開示をしない |
| バイナリエンコーディング | Protocol Buffersでバイナリシリアライズし、URL-safe Base64でURIに変換 |
| チェーンID指定 | メッセージにチェーンIDを含み、対象チェーンを明示 |

URIのフォーマットは`uwulink:`スキームの後にBase64エンコードされたメッセージを付与します。

```
uwulink:<base64_of_UWULink_message>
```

#### Protobufスキーマ

UWULinkのメッセージは以下のProtobufスキーマで定義されています。

```protobuf
syntax = "proto3";
package org.ethereum.uwulink;

message UWULinkRequest {
  uint64 chain_id = 1;
  oneof request_type {
    Batch batch = 2;
    ResolverReference resolver = 3;
  }
}

message Batch {
  repeated Call calls = 1;
}

message Call {
  bytes to = 1;
  optional bytes value = 2;
  optional bytes data = 3;
}

message ResolverReference {
  bytes resolver_address = 1;
  bytes resolver_data = 2;
}
```

`UWULinkRequest`がトップレベルのメッセージで、`chain_id`と`request_type`（`Batch`または`ResolverReference`のいずれか）を含みます。

`Call`の`to`フィールドと`ResolverReference`の`resolver_address`は20バイトのアドレスです。
ウォレットはこれらが正確に20バイトであることを検証しなければなりません。
`value`フィールドは省略可能（最大32バイト）で、省略または空の場合は0 ETHとして扱います。
エンコード時は先頭の0バイトを取り除くため、例えば1 weiは`0x01`の1バイトで表現されます。

:::message
**Protobufを使う理由**

Protocol Buffers（Protobuf）はGoogleが開発したバイナリシリアライゼーション形式です。
JSONのようなテキスト形式と比べて、同じデータをはるかに小さなサイズで表現できます。
例えばチェーンID 1のようなよく使われる値は1〜2バイトで表現でき、20バイトのアドレスもそのまま格納されます。
QRコードやNFCタグのようにデータ容量が限られるチャネルでは、この圧縮効果が大きな意味を持ちます。
:::

#### Static Mode

Static Modeは、dAppが実行したいコールの一覧をリクエストに直接含めるモードです。

:::message
DEXでのトークンスワップを例にStatic Modeの流れを説明します。

Aliceがモバイルウォレットを使ってDEXでトークンを交換したいとします。
プライバシーを気にするAliceは、ウォレットをDEXに接続したくありません。

DEXはUWULinkのQRコードを表示します。
QRコードには以下の2つのコールがバッチとして含まれています。

1. トークンコントラクトの`approve`を呼び出し、DEXルーターにトークンの使用を許可
2. DEXルーターの`swapExactTokensForTokens`を呼び出してスワップを実行

AliceはQRコードをスキャンし、ウォレットに表示される内容を確認して承認します。
2つのコールは1つのアトミックなトランザクションとして実行されるため、スワップが失敗した場合はapproveもリバートされます。

DEXはAliceのアドレスを事前に知ることなく、トランザクションがブロックチェーンに記録されて初めてアドレスが公開されます。

```mermaid
sequenceDiagram
    participant D as DEX dApp
    participant Q as QRコード
    participant W as Aliceのウォレット
    participant C as ブロックチェーン

    D->>Q: UWULinkリクエストをエンコード
    Note over Q: approve + swap の2コール
    W->>Q: QRコードをスキャン
    Q-->>W: UWULinkRequest（Static Mode）
    W->>W: デコード・表示・ユーザー確認
    W->>C: アトミックバッチトランザクション送信
    C-->>W: 実行結果
```
:::

![static-vs-programmable.png](/images/qiita-4bb1ae21-223e-4f12-8cbc-819d8b756e25.webp)

#### Programmable Mode

Programmable Modeは、オンチェーンのResolverコントラクトを参照してコール一覧を動的に生成するモードです。
リクエストにはResolverコントラクトのアドレスと入力データのみを含むため、QR/NFCのペイロードサイズを抑えられます。

Resolverコントラクトは以下のインターフェースを実装しなければなりません。

```javascript
interface UWUResolver {
    struct Call {
        address target;
        uint256 value;
        bytes data;
    }

    error CallGenerationFailure(uint256 errorCode);

    function getCalls(address requester, bytes calldata data)
        external returns (Call[] memory calls);

    function getErrorCodeDetails(uint256 errorCode)
        external returns (string memory information);
}
```

##### `getCalls`

ウォレットのアドレス（`requester`）と入力データ（`data`）を受け取り、実行するコールの一覧を返す関数です。

- **引数**
    - `requester`
        ウォレットが自分自身のアドレスを渡します。ユーザー固有の結果を返すために使用されます。このアドレスはdAppサーバーやUIには見えず、ウォレットが使用するRPCサーバーにのみ渡されます。
    - `data`
        dAppが設定した任意のデータ。アクションの種類やキャンペーンIDなどを含みます。
- **戻り値**
    - 実行するコールの一覧（`Call[]`）。
- この関数はステートを変更する場合があります。ウォレットはオフチェーン（`eth_call`）で呼び出します。

##### `getErrorCodeDetails`

`CallGenerationFailure`エラーのエラーコードの詳細を返す関数です。

- 開発者向けの情報であり、ウォレットはこの情報をユーザーに表示すべきではありません。

ウォレットがProgrammable Modeのリクエストを受け取った時の処理フローは以下の通りです。

1. `resolver_address`に対して`from = address(0)`として`eth_call`で`getCalls(userAddress, resolver_data)`を実行（最新ブロック時点）
2. 成功した場合、返されたコール一覧をStatic Modeと同様にユーザーに表示して確認を求める
3. 失敗した場合（リバートまたは未実装）、ウォレットはリクエストを中止し、ユーザーにエラーを表示します

:::message
エアドロップの一括請求を例にProgrammable Modeの流れを説明します。

BobがエアドロップdAppを訪れます。
dAppはウォレット接続を要求せず、UWULinkのQRコードを表示します。

QRコードにはResolverコントラクトのアドレスとキャンペーンIDだけが含まれています。
Bobがスキャンすると、ウォレットがResolverコントラクトの`getCalls`をBobのアドレスで呼び出します。
Resolverコントラクトは、BobがTokenA・TokenB・TokenCの3種類のトークンを請求できると判定し、3つの`claim`コールを返します。

Bobはウォレットに表示された内容を確認して承認すると、3つの請求が1つのトランザクションで実行されます。
dAppはBobのアドレスを一度も知ることなく、オンチェーンのコントラクトがBobに合わせたコール一覧を生成しました。
:::

## 補足

#### 後方互換性

UWULinkは追加的なプロトコルであり、既存の標準を壊しません。
`uwulink:`スキームを認識しないウォレットはリクエストを無視するだけです。
**EIP681**のQRコードとフォーマットが異なるため、誤って処理される心配もありません。

Protobufスキーマは、将来の拡張に対して前方互換性があります。
新しいフィールド（有効期限やオリジンドメイン等）が追加されても、古いウォレットはそれらを無視してコアのリクエストを処理できます。

![standard-comparison.png](/images/qiita-61423da3-b549-4afb-95cc-c15faad09339.webp)

#### 既存標準との比較

| 比較項目 | **EIP681** | WalletConnect | **EIP5792** | UWULink |
|---------|---------|---------------|---------|---------|
| 通信方向 | 一方向 | 双方向 | 双方向 | 一方向 |
| バッチ対応 | 不可（単一トランザクション） | 可 | 可 | 可 |
| プライバシー | 低（URIにアドレス含む場合あり） | 中（リレー経由） | 低（接続必須） | 高（アドレス非開示） |
| エンコーディング | テキスト（URI） | JSON | JSON-RPC | バイナリ（Protobuf） |
| QR/NFC適性 | 中 | 低（セッション必要） | 低（接続必要） | 高 |
| 動的コール生成 | 不可 | 可（dApp側） | 可（dApp側） | 可（オンチェーン） |

## セキュリティ

#### プライバシー

UWULinkではウォレットがdAppに対してアドレスやウォレット情報を開示しません。
ただし、Programmable Modeではウォレットが`eth_call`でResolverコントラクトにユーザーのアドレスを渡すため、RPCプロバイダにはそのアドレスが見えます。
これは通常のdApp利用と同程度のリスクですが、プライバシーを重視するユーザーはStatic Modeの使用やプライバシー対応RPCの利用を検討すべきです。

#### フィッシングと悪意のあるQRコード

悪意のあるQRコードをスキャンしてそのまま承認すると、攻撃者に資金をTransferしたりトークンをapproveしてしまう可能性があります。
ウォレットはコントラクトアドレスのENS名の表示、関数セレクタのデコード、大きなvalue Transferやトークンallowanceへの警告表示など、読みやすい情報を表示する必要があります。

#### Resolverコントラクトの信頼性

Programmable Modeでは、ユーザーはResolverコントラクトが正当なコール一覧を返すことを信頼しています。
悪意のあるResolverは、入力データを無視してユーザーのETHをすべて攻撃者に送信するコールを返す可能性があります。
ウォレットはResolverから返されたコールをStatic Modeと同じようにユーザーに表示し、確認を求めます。
Resolverコントラクトはオープンソースで検証済みであることが望ましいです。

#### 大容量ペイロードによる障害

悪意のあるdAppが非常に大きなUWULinkペイロードを作成し、ウォレットアプリをクラッシュさせたり動作を遅くさせたりする可能性があります。
ウォレットはペイロードのサイズ制限を設けるべきです。
同様に、Resolverコントラクトが大量の結果を返す場合もあるため、`eth_call`に提供するガス量を制限することで対策できます。

#### リクエストの送信元が不明

従来のウォレット接続フローでは、リクエストの送信元ドメインをウォレットが認識でき、ホワイトリストやブロックリストを適用できました。
UWULinkではリクエストの送信元は「ユーザーがスキャンしたQRコード」であり、ドメイン情報がないため、ウォレットはドメインベースのフィルタリングを適用できません。
ユーザー自身がリクエストの内容を検証して承認する必要があり、ウォレットが正確な情報を表示することがより重要になります。

## 引用

https://eips.ethereum.org/EIPS/eip-7946

https://eips.ethereum.org/EIPS/eip-7702

https://eips.ethereum.org/EIPS/eip-5792

https://eips.ethereum.org/EIPS/eip-681

## 最後に

今回は「**ERC7946（UWULink）**」についてまとめてきました。

ウォレット接続なしにQRコードやNFCだけでバッチトランザクションを実行できるプロトコルです。
プライバシーを保護しながら、複数のコントラクト呼び出しを1回のスキャンで完結できるUXを提供します。
Static ModeとProgrammable Modeの2つのモードにより、シンプルな決済から複雑なエアドロップ請求まで幅広いユースケースに対応しています。

他でも色々記事を書いているのでぜひよろしければ読んでいってください！

https://amzn.asia/d/gxvJ0Pw

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://twitter.com/cardene777

https://cardene.substack.com/
