# [ERC7739] EOAが複数スマートアカウントを管理する時の署名保護の仕組みを理解しよう！

> ERC7739は、1つの外部所有アカウント（Externally Owned Account, EOA）が複数のスマートアカウントを管理している場合に発生する「署名のリプレイ攻撃」を防ぐための標準を定義しています。

- URL: https://cardene.dev/articles/6bef153574454340c17c
- 公開日: 2025-08-05
- 更新日: 2025-08-05
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、1つのEOAが複数のスマートアカウントを管理している場合に、同じ署名が不正利用されないようにするため、署名内容を人間に読める形で再ハッシュして安全に検証できる仕組みを提案している**ERC7739**についてまとめていきます！

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

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

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

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

## 概要

**ERC7739**は、1つの外部所有アカウント（Externally Owned Account, EOA）が複数のスマートアカウントを管理している場合に発生する「**署名のリプレイ攻撃**」を防ぐための標準を定義しています。
リプレイ攻撃とは、ある署名が別の文脈でもそのまま利用されてしまう脆弱性のことです。例えば、同じEOAが複数のスマートアカウントを持っているとき、1つのアカウントで使用した署名が、他のアカウントでも有効と認識されてしまう危険があります。

この問題を解決するために、**ERC7739**では**ERC1271**による署名検証の際に「**防御的リハッシュ（defensive rehashing）**」という仕組みを導入します。
具体的には、**EIP712**形式のネストされた型構造を利用して再ハッシュを行い、署名の内容をウォレットクライアント上で可読な形に保ちながら安全性を確保します。
これにより、ユーザーは署名内容を透明に確認でき、アプリケーションやウォレット側に大きな変更を求めずに導入可能です。

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

[\[ERC1271\] コントラクトで署名を検証する仕組みを理解しよう！](/articles/da237b1b8cc185491512)

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

[\[EIP712\] 型付き構造化データのハッシュ化と署名の仕組みを理解しよう！](/articles/f54bc85cb66de5a54257)

## 動機

現在、スマートアカウントは**ERC1271**の`isValidSignature`関数を用いて署名の正当性を検証できます。
しかし、単純な実装では署名がそのまま別のスマートアカウントで再利用される危険があります。
以下に示される一般的なコード例では、EOAの署名がハッシュ値だけに基づいて検証されるため、異なるスマートアカウントでも同じ署名が通ってしまう可能性があります。

その理由は、署名検証に用いられるハッシュ値に「**どのスマートアカウントに属する署名なのか**」という情報が含まれていないためです。
この欠落によって、Permit2のような広く利用されているアプリケーションにおいてもリプレイ攻撃のリスクが存在します。

これを防ぐため、多くのスマートアカウント実装では独自に「**防御的リハッシュ**」を導入しています。

* 元のハッシュ値
* スマートアカウント自身のアドレス
* チェーンID

上記の情報を組み合わせて新しい最終ハッシュを生成し、そのハッシュに基づいて署名を検証します。

しかし、この方法を単純に導入すると**EIP712**で署名した内容がウォレット画面に不透明な形で表示されてしまい、ユーザーが署名内容を確認できなくなるという問題が生じます。

**ERC7739**は、この問題を解決するために「**防御的リハッシュを行いつつも、署名内容を可視化できる**」仕組みを提供します。
**EIP712**を活用し、ウォレットクライアント側でも署名内容が人間に読める形で表示されるため、ユーザー体験を損なうことなくセキュリティを高めることができます。
また、この仕組みはフロントエンドやウォレットクライアントが対応していなくても、ユーザー自身がクライアント側スクリプトを挿入することで有効にできるよう設計されており、導入障壁を極力低くしています。

## 仕様

この仕様は、**ERC1271**における署名検証の振る舞いを定義しています。
特に、防御的リハッシュ（defensive rehashing）を導入する時のルールを明確にすることで、スマートアカウントが安全に署名を扱えるようにしています。
ここで定義される仕組みは2つのワークフローから成り立っています。
1つは**EIP712**を用いた「`TypedDataSign`」ワークフローで、もう1つは**EIP191**を用いた「`PersonalSign`」ワークフローです。

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

[\[ERC191\] 署名済みデータを安全に使用する仕組みを理解しよう！](/articles/ee4edd15ad6fe7f58f7e)

また、この標準は以下の依存仕様に基づいています。

* **EIP712**
    * 型付きデータのハッシュ化と署名を定義する仕組み。
    * 最終ハッシュの構築に必要。
* **ERC1271**
    * コントラクトによる署名検証の標準。
    * `isValidSignature`関数を提供。
* **ERC5267**
    * **EIP712**ドメイン情報を取得する仕様。
    * `eip712Domain()`関数を提供。

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

[\[ERC5267\] EIP712ドメインの生成に必要な情報を渡す仕組みを理解しよう！](/articles/3f0d49e6796d6f35d321)

### TypedDataSignワークフロー

このワークフローは、もともと**EIP712**でハッシュが計算されている場合に使われます。

#### 最終ハッシュの計算方法

`TypedDataSign`では、アプリケーション固有のドメインセパレーター（`APP_DOMAIN_SEPARATOR`）と**EIP712**のドメイン情報（名前、バージョン、チェーンID、コントラクトアドレス、ソルト）を組み合わせ、元のデータ構造を含む新しいハッシュを生成します。
これにより、スマートアカウント固有の要素を署名に組み込み、リプレイ攻撃を防ぎます。

ハッシュは次の形で表現されます。

```
keccak256(
    0x1901 || APP_DOMAIN_SEPARATOR || hashStruct(TypedDataSign({...}))
)
```

ここで、`contentsName`はデータ型の名前部分を抽出した文字列です。
安全のため、この値が不正な形式（空文字や不正な記号を含む場合）のときは署名を無効とすることが推奨されています。

#### 署名フォーマット

`isValidSignature`に渡す署名は、元の署名に以下を連結した形式になります。

```
originalSignature || APP_DOMAIN_SEPARATOR || contents || contentsDescription || length(contentsDescription)
```

ここで`contents`は元データのハッシュで、`contentsDescription`はデータ型の説明を保持します。
これによって、`isValidSignature`側で正しいハッシュを復元でき、TypedDataSignワークフローを安全に実行できます。

### PersonalSignワークフロー

`PersonalSign`は、**EIP191**に基づいてハッシュが計算されている場合に使われます。これは「**Ethereum Signed Message:\n...**」という形式のメッセージ署名です。

#### 最終ハッシュの計算方法

PersonalSignでは、`ACCOUNT_DOMAIN_SEPARATOR`を利用し、さらに**EIP712**形式で「`PersonalSign(bytes prefixed)`」という構造体を生成し、そのハッシュを計算します。

```
keccak256(
    0x1901 || ACCOUNT_DOMAIN_SEPARATOR || hashStruct(PersonalSign({...}))
)
```

ここで、`prefixed`は実際に`Ethereum Signed Message`形式で作られたメッセージハッシュです。
このワークフローでは、署名データに追加情報を付与する必要はありません。

### サポート検出

スマートアカウントは、この標準をサポートしていることを示すために、特定の「**マジックナンバー**」を返す仕様になっています。

```
isValidSignature(0x77397739...., "")
→ bytes4(0x77390001)
```

この数値は標準のバージョンを示しており、将来的にアップデートがあれば変更される可能性があります。

### ワークフローの判定

`isValidSignature`はシグネチャ形式を変えずに、渡されたデータからどのワークフローを使うかを自動的に判断します。
もし署名に`APP_DOMAIN_SEPARATOR`や元データのハッシュが含まれている場合はTypedDataSignワークフローを実行し、そうでない場合はPersonalSignワークフローを実行します。

### 防御的リハッシュの省略条件

スマートアカウントは、場合によってはリハッシュを省略することができます。
例えば、オフチェーンで`isValidSignature`が呼ばれる場合や、すでにハッシュにスマートアカウントのアドレスが含まれている場合です。

これは、多くのアプリケーションが新しい**EIP712**形式に対応していない可能性を考慮した柔軟性であり、安全に省略できる状況では従来通りの署名検証を行えるようになっています。

## 補足

### TypedDataSign構造

TypedDataSignの型ハッシュ（typedDataSignTypehash）は、コントラクト上でその都度生成される必要があります。
これは署名リクエストの時に、ユーザーが署名する内容が必ず可視化されるようにするためです。
署名対象の`contents`がユーザー定義型であることを強制することで、署名内容を隠すことなく表示できます。

また、`eip712Domain`に含まれるフィールドはTypedDataSign構造体に直接展開されます。
これは、署名検証に使われるコントラクトのドメイン型が異なる場合に衝突が起こるのを避けるためです。

さらに、**ERC5267**で定義されている`bitmap`（bytes1型）や`extensions`（uint256[]型）は省略されています。
フィールドが存在しない場合とゼロ値を持つ場合を区別しても、署名の安全性が高まるわけではないためです。
なお、`extensions`はオフチェーンでのシグナリング（EIP番号を示すため）に使われる要素です。

### contentsDescriptionのモード

**EIP712**では型の並び順を辞書順（lexicographical order）で処理します。
そのため、ネストされた型を含む場合には、`contentsName`が必ずしも型定義の先頭に配置されないことがあります。
この問題を解決するために、明示的に型名を指定できる「explicit mode」が導入されています。
これにより、署名内容が正しく表示されるよう保証されます。

### isValidSignatureによるサポート検出

モジュール型のスマートアカウントで容易に実装できるよう、新しい関数を追加するのではなく、既存の`isValidSignature`を利用して対応状況を検出する方法が採用されています。
この関数は特定のマジックナンバーを返すことで、対応しているかどうかを示します。

### contentsNameの制約

`contentsName`が小文字の7ビットASCII文字で始まることは禁止されています。
これは将来にわたって言語非依存かつ拡張性を保つためです。
例えば、`uint256`のような基本型は、他のプログラミング言語では異なる名称（例：`u256`）を持つ可能性があるため、標準を固定化しすぎないようにしています。

## 互換性

### 過去のドラフトとの互換性

以前のドラフトでは、対応状況を確認するために`supportsNestedTypedDataSign()`という関数を定義していました。
この関数は`0xd620c85a`という固定値を返すことで、サポートを示すものでした。
しかし現在の仕様では、この仕組みを廃止し、代わりに`isValidSignature`の戻り値にマジックナンバーを使う方式に変更されています。
この設計により、既存のインターフェースを拡張する必要がなくなり、モジュール型のアカウントに対しても導入しやすくなっています。

## 参考実装

**ERC7739**では、本番環境でそのまま利用できる最適化済みの参考実装が用意されています。
実装には、安全性、柔軟性、開発者体験、ユーザー体験を確保するために必要な追加機能が含まれています。
これは単なる動作確認用の最小限なコードではなく、現場で使えることを前提に設計されています。

この方針は、**ERC1271**のときに起きた誤解を避ける意図があります。
**ERC1271**では「**最小限の参考実装**」がそのまま安全な実装であると誤解され、十分な対策を講じないまま採用されてしまった事例がありました。
**ERC7739**では、そのような過ちを繰り返さないために、あえて実用的な形で参考実装が提示されています。

## セキュリティ

### 不正なcontentsNameの拒否

現在広く利用されている`eth_signTypedData`の実装は、カスタム型の名前（contentsName）を正しく検証していません。
この問題を悪用すると、フィッシングサイトが制御文字を混入させた`contentsName`を作り、PersonalSignの型定義から抜け出すように細工できます。
その結果、ウォレットクライアントはユーザーに「**読めないハッシュ**」への署名を求めてしまう危険があります。

この攻撃を防ぐために、コントラクト側で`contentsName`を検証し、不正な名前を拒否する仕組みを導入することが強く推奨されています。

### 複数の署名者をチェーンできない制約

この方式を使って**ERC1271**の署名リプレイ防止を実現しているアカウントは、同じ方式を使う署名者をさらに組み合わせることができません。
理由は、TypedDataSign構造体の内部に「**署名対象となるメッセージ**」が含まれているためです。
もしそのメッセージ自体が別のTypedDataSign構造体だった場合、最終的な**EIP712**メッセージには二重のTypedDataSign型が含まれることになります。
これは**EIP712**の仕様上、1つのリクエストで表現できないため、技術的に不可能です。

この制約により、複数段階の署名チェーンを作ることはできませんが、その代わりにリプレイ攻撃に強い堅牢な仕組みが保証されます。

## 引用

vectorized (@vectorized), Sihoon Lee (@push0ebp), Francisco Giordano (@frangio), Hadrien Croubois (@Amxx), Ernesto García (@ernestognw), Im Juno (@junomonster), howydev (@howydev), Atarpara (@Atarpara), 0xcuriousapple (@0xcuriousapple), "ERC-7739: Readable Typed Signatures for Smart Accounts [DRAFT]," Ethereum Improvement Proposals, no. 7739, May 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7739.

## 最後に

今回は「1つのEOAが複数のスマートアカウントを管理している場合に、同じ署名が不正利用されないようにするため、署名内容を人間に読める形で再ハッシュして安全に検証できる仕組みを提案している**ERC7739**」についてまとめてきました！
いかがだったでしょうか？

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

[Twitter @cardene777](https://twitter.com/cardene777)

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

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
