# [EIP7002] ステーキングしたETHの出金要求を送る新しい仕組みを理解しよう！

> EIP7002 は、バリデータの退出や出金要求を、実行レイヤー側からリクエストできるようにする提案です。リクエスト自体は実行レイヤーのコントラクトに送られますが、最終的な処理はコンセンサスレイヤーで行われます。

- URL: https://cardene.dev/articles/fc917bdda0c98a05c7c8
- 公開日: 2026-05-04
- 更新日: 2026-05-04
- トピック: Blockchain
- タグ: eip, validator, ブロックチェーン, staking

## はじめに

初めまして。
『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

今回は、**ステーキングしたETHの出金先を示す情報**である`withdrawal credential`を使って、実行レイヤー側から退出や出金要求を起動できるようにする**EIP7002**についてまとめていきます！

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

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

実行レイヤーからコンセンサスレイヤーへ出金要求を渡すことで、退出開始の主導権を`withdrawal credential`、つまり**出金先を示す側**へ寄せます。
元のEIPの動機は`0x01`を中心に書かれていますが、ElectraのCL処理では`0x01`と`0x02`の`execution-layer withdrawal credential`が送信元アドレス認証の対象になります。
ただし`partial withdrawal`の経路はさらに条件があり、`amount > 0`を送れば何でも部分出金になるわけではありません。

## 概要

**EIP7002** は、バリデータの退出や出金要求を、実行レイヤー側からリクエストできるようにする提案です。
リクエスト自体は実行レイヤーのコントラクトに送られますが、最終的な処理はコンセンサスレイヤーで行われます。

いまのEthereumでは、バリデータには大きく2つの鍵があります。
1つは日常的に署名を行う`active key`で、もう1つは出金先を表す`withdrawal credential`です。
このうち`active key`はオンラインで使われやすい「**ホット**」な鍵ですが、`withdrawal credential`は資産の最終的な所有権に近い役割を持つため、より安全に管理される「**コールド**」な情報として扱えます。

![概要の図](/images/qiita-b929da35-2879-4a48-bfa2-73ba44721b75.webp)

この図では、`active key`が**日常運用のための鍵**で、`withdrawal credential`が**最終的な受取先を示す情報**であることを並べています。
つまり従来は「運用に使う側」が退出開始を握っていたのに対し、**EIP7002**は「**出金先を示す側**」にも要求の入口を持たせる提案です。

**EIP7002**がやりたいことは、この`withdrawal credential`側にも「**退出や出金要求を始める入口**」を与えることです。
これによって、`active key`を預けているオペレーターや別管理者が動かなくても、最終的な所有者が退出開始を進めやすくなります。

#### **EIP4895** との違い

似た文脈の仕様として**EIP4895**がありますが、役割は異なります。
**EIP4895**は、コンセンサスレイヤーで確定した出金をEVM側へ「**反映する**」仕組みです。
一方の**EIP7002**は、その前段である「**出金や退出を要求する**」入口を実行レイヤーに作る提案です。

![eip4895-vs-eip7002.png](/images/qiita-04e973bd-9ade-4fe6-a464-1388d21ae654.webp)

この図では、左側の**EIP4895**が「**CLで確定した出金結果をELに反映する**」流れであるのに対し、右側の**EIP7002**は「**ELからCLへ出金リクエストを渡す**」流れであることを並べて示しています。
つまり両者は似た出金文脈でも向きが逆で、**EIP4895** は結果の反映、**EIP7002** は要求の入口という役割分担になります。

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

[\[EIP4895\] ビーコンチェーンからEVMへのバリデーター出金サポートの提案を理解しよう！](/articles/f154a261447388d500f1)

#### **EIP7685** との関係

**EIP7002** は単独で新しいブロック形式を定義するのではなく、**EIP7685** の汎用リクエスト枠組みに乗る形で設計されています。
つまり**EIP7002**は「**出金要求という具体的なリクエスト種別**」を定義し、**EIP7685**は「**その要求データをELからCLへ運ぶ共通バス**」を提供します。
Electraでは、これらは`block.body.execution_requests`に入る`ExecutionRequests`のうち`withdrawals`リストとして扱われます。
その後、`get_execution_requests_list()`によって`type byte + SSZ list`のバイト列に変換され、リクエスト種別の昇順でengine側へ渡されます。

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

[\[EIP7685\] 実行レイヤーからコンセンサスレイヤーへリクエストを共有する汎用フレームワークの仕組みを理解しよう！](/articles/f19820fbecd8049e3885)

## 動機

現在の仕様では、バリデータの退出開始を指示できるのは`active key`側です。
Ethereumの仕様ではこの退出開始手続きを`voluntary exit`と呼びます。
しかし、ステークされたETHの所有権に近いのは`withdrawal credential`側です。

このずれがあると、以下のような問題が起こります。

| 問題 | 何が困るか |
|---|---|
| `active key`の管理者と資産の所有者が別 | 所有者が退出したくても、`active key`側が協力しないと動けない |
| custody関係が複雑 | オペレーターが止まると、資産が実質的に「**人質**」になりうる |
| `active key`を紛失した | 退出したくても通常フローを始められない |

ここで重要なのは、`withdrawal credential`はすでに「**そのステーク資産を最終的に受け取る先**」を表していることです。
それなのに退出開始だけはできない、という状態は役割分担として不自然です。

**EIP7002**はこの不一致を解消し、`execution-layer withdrawal credential`を持つEOAやスマートコントラクトが、自分で退出や出金要求を始められるようにします。

さらにEIP本文では、`0x00`の`withdrawal credential`しか持っていない場合でも、一度だけ署名して`0x01`へ切り替えれば、この仕組みを利用できると説明しています。
そのため、`0x01`だけの特殊機能というより、`withdrawal credential`側へ制御権を寄せる方向の仕様と考えると分かりやすいです。

![動機の図](/images/qiita-e3533716-58b6-40b8-bb71-17b29b49e2f7.webp)

この図では、最初から`0x01`である必要はなく、`0x00`しか持っていない場合でも**一度切り替えればこの仕組みに入れる**ことを示しています。
そのため、**EIP7002**を「**一部の特殊な出金先だけの話**」として見るより、**出金先側へ制御権を寄せる流れ**として捉えた方が全体像をつかみやすいです。

## 仕様

#### 全体の流れ

先に流れを掴むと読みやすいです。
この提案では、実行レイヤー上の**決まったアドレスにデプロイされる出金要求用コントラクト**が出金リクエストを受け取り、それをブロック末尾で取り出して**EIP7685**の`requests list`に載せ、コンセンサスレイヤーがそれを処理します。

![el-cl-responsibility-split.png](/images/qiita-ec77735f-f67a-40d8-84b1-fe3238b6ca13.webp)

この図では、左側の実行レイヤーで`withdrawal credential`を持つ側が出金要求用コントラクトへ要求を送り、そこで手数料付きのキューに積まれる様子を表しています。
その後、ブロック末尾のシステムコールがキューから要求を取り出し、**EIP7685** のリクエストとして右側のコンセンサスレイヤーへ受け渡します。
つまり**EIP7002**の本質は、退出そのものをELが完結させることではなく、ELが「**退出開始の入口**」を持ち、最終判断と実処理はCLが担うように責務分離する点にあります。

1. **リクエスト送信**
    `execution-layer withdrawal credential`に対応するアドレスが、決まったアドレスにデプロイされる出金要求用コントラクトに対して、バリデータの公開鍵と`amount`を送ります。
2. **キュー格納**
    コントラクトは手数料を確認し、リクエストをチェーン上のキューに積みます。
3. **ブロック末尾のシステムコール**
    ブロック処理の最後に`SYSTEM_ADDRESS`からコントラクトが呼ばれ、そのブロックでCLに渡すリクエスト群を取り出します。
4. **CL側処理**
    取り出されたリクエストは**EIP7685**のリクエストとして`block.body.execution_requests.withdrawals`に入り、CL側で退出や部分出金候補として評価されます。

#### 設定値

EIP本文で定義されている主要な定数は以下です。

| 項目 | 値 | 役割 |
|---|---|---|
| `WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS` | `0x00000961Ef480Eb55e80D19ad83579A64c007002` | 出金要求を受け取ってキューに保存する、決まったアドレスのコントラクト |
| `WITHDRAWAL_REQUEST_TYPE` | `0x01` | **EIP7685** 上でのリクエスト種別 |
| `SYSTEM_ADDRESS` | `0xfffffffffffffffffffffffffffffffffffffffe` | ブロック末尾のシステムコールに使う特別アドレス |
| `MAX_WITHDRAWAL_REQUESTS_PER_BLOCK` | `16` | 1ブロックでCLに渡せるリクエスト上限 |
| `TARGET_WITHDRAWAL_REQUESTS_PER_BLOCK` | `2` | 手数料調整が目標とする平均リクエスト数 |
| `MIN_WITHDRAWAL_REQUEST_FEE` | `1` | 最低手数料 |
| `WITHDRAWAL_REQUEST_FEE_UPDATE_FRACTION` | `17` | 動的手数料の変化率を制御する係数 |

この提案の肝は、1ブロックあたりの処理上限を `16` 件に抑えつつ、実行レイヤー側ではそれ以上の送信をキューで受け止められる点にあります。
ここでキューを挟むことで、利用者の`transaction`を即座に失敗させず、後続ブロックで順に消化できるようにしています。

#### 出金リクエストのデータ構造

**EIP7002** のリクエストは **EIP7685** の `0x01` 型リクエストとして定義され、データ本体は以下の3つのフィールドを持ちます。

| フィールド | 型 | 説明 |
|---|---|---|
| `source_address` | `Bytes20` | リクエストを送った実行レイヤー側アドレス |
| `validator_pubkey` | `Bytes48` | 対象バリデータを特定するBLS公開鍵 |
| `amount` | `uint64` | 引き出したい量。CL側では`Gwei`単位で解釈される |

`validator_pubkey`を使うのは、同じ`withdrawal credential`に複数のバリデータが紐づくことがあるからです。
アドレスだけでは「どのバリデータを退出させたいか」が分からないため、公開鍵で対象を一意に絞ります。

![出金リクエストのデータ構造の図](/images/qiita-681c58b5-ca01-4410-963b-588522f49dff.webp)

この図では、**1つの出金先情報に複数のバリデータがぶら下がりうる**ことを示しています。
だからこそ、単に「この出金先から要求した」だけでは足りず、**どのバリデータに対する要求なのか**を`validator_pubkey`で明示する必要があります。

ここで実装上かなり重要なのが `amount` の扱いです。
コントラクトへ入れる`calldata`は`validator_pubkey || big-endian uint64 amount`ですが、キューから組み立てて返されるリクエストでは`amount`が`little-endian`で並びます。
さらにCL側ではこの`amount`を`Gwei`として解釈します。
そのため、単に`uint64`が入るだけではなく「**入力と出力でbyte orderが違う**」「**単位はGwei**」まで含めて覚えておく必要があります。

![byte-layout-and-endianness.png](/images/qiita-dd961151-91e9-4e0c-a599-7bc0f7125422.webp)

この図では、利用者が送る`56 byte`の`calldata`、キュー上の`3 slot`配置、そしてコントラクトが返す出金要求データの並びを横に並べています。
特に重要なのは、入力時の`amount`が`big-endian`なのに対し、キューから組み立てた返却値では`little-endian`になる点と、CL側ではそれを`Gwei`として解釈する点です。

#### コントラクトの3つのコードパス

出金リクエスト用コントラクトには、大きく3つの入り口があります。

| コードパス | 入力条件 | 役割 |
|---|---|---|
| Add Withdrawal Request | `calldata`が`56 byte` | キューにリクエストを追加する |
| Fee Getter | `calldata`長が`0` | 現在の必要手数料を返す |
| System Process | `SYSTEM_ADDRESS`から呼ばれる | キューからリクエストを取り出して内部状態を更新する |

ここで`56 byte`というのは、`validator_pubkey`の`48 byte`と**`big-endian`**の`amount 8 byte`をつなげた長さです。
送信元アドレスは`calldata`に含めず、EVMの`CALLER`、つまり`msg.sender`から取得します。

![contract-code-paths.png](/images/qiita-57fcacc3-19bc-4017-a8e7-5255d826a5eb.webp)

この図では、同じ出金要求用コントラクトが`56 byte calldata`、`calldataなし`、`SYSTEM_ADDRESS`からのブロック末尾呼び出しという3種類の入口で別々に振る舞うことをまとめています。
利用者が普段触るのはリクエスト追加と手数料取得ですが、プロトコルとして重要なのは3つ目のシステム処理で、ここがキューから要求を取り出してCLへ渡す中継点になります。

#### リクエスト追加処理

`Add Withdrawal Request`では、手数料を確認したうえでキューの末尾へリクエストを積みます。
EIP本文の擬似コードは以下です。

```python
def add_withdrawal_request(Bytes48: validator_pubkey, uint64: amount):
    fee = get_fee()
    require(msg.value >= fee, 'Insufficient value for fee')

    count = sload(WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_COUNT_STORAGE_SLOT)
    sstore(WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_COUNT_STORAGE_SLOT, count + 1)

    queue_tail_index = sload(WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_QUEUE_TAIL_STORAGE_SLOT)
    queue_storage_slot = WITHDRAWAL_REQUEST_QUEUE_STORAGE_OFFSET + queue_tail_index * 3
    sstore(WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot, msg.sender)
    sstore(WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 1, validator_pubkey[0:32])
    sstore(WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 2, validator_pubkey[32:48] ++ uint64_to_little_endian(amount))
    sstore(WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_QUEUE_TAIL_STORAGE_SLOT, queue_tail_index + 1)
```

この処理で見ておきたい点は以下です。

1. **手数料確認**
    `msg.value` がその時点の必要手数料以上でないとリクエストは受け付けられません。
2. **ブロック内カウント更新**
    そのブロックで何件リクエストが追加されたかを `WITHDRAWAL_REQUEST_COUNT_STORAGE_SLOT` に蓄積します。
3. **キューへの格納**
    `msg.sender`、`pubkey`前半`32 byte`、`pubkey`後半`16 byte`と`amount 8 byte`を3スロットに分けて保存します。
4. **tail 更新**
    キューの末尾ポインタを 1 つ進めます。

この設計だと、実行ブロック中にはいくらでもリクエストを受け付けられます。
ただしCLに渡す件数は後段の`dequeue`で上限がかかるため、受け付け速度と処理速度を分離できます。

#### 手数料取得と動的手数料

`calldata`長が`0`の呼び出しでは、コントラクトは現在の必要手数料を返します。
利用者は先に手数料を読み、その額を付けてリクエストを送る想定です。

手数料の計算は `get_fee()` と `fake_exponential()` で行われます。

```python
def get_fee() -> int:
    excess = sload(WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, EXCESS_WITHDRAWAL_REQUESTS_STORAGE_SLOT)
    require(excess != EXCESS_INHIBITOR, 'Inhibitor still active')
    return fake_exponential(
        MIN_WITHDRAWAL_REQUEST_FEE,
        excess,
        WITHDRAWAL_REQUEST_FEE_UPDATE_FRACTION
    )
```

考え方としては **EIP1559** に近く、平均で 1ブロックあたり 2 件程度なら安く、需要が集中すると指数的に高くなります。
そのため、悪意ある利用者が安い`gas`だけでキューを埋め尽くし続けることを防ぎやすくなります。

![queue-and-dynamic-fee.png](/images/qiita-93f97049-92ef-4430-9873-a986f3124591.webp)

この図では、利用者からの要求をいったんキューで受け止め、1 ブロックあたりの処理上限だけを後段でかける構造を表しています。
同時に、需要が強い時ほど必要手数料が上がるため、単に超過分を即失敗させるのではなく、呼び出し成功と後段処理を分けながら混雑と`griefing`を抑える設計になっています。

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

[\[EIP1559\] Ethereumの新たなガス価格設定メカニズムを理解しよう！](/articles/ce77124940010280717b)

#### システムコールと`dequeue`処理

各実行ブロックの最後には、`SYSTEM_ADDRESS`から、その決まったアドレスにデプロイされる出金要求用コントラクトへ`calldataなし`のシステムコールが行われます。
この時にコントラクトはキュー先頭からリクエストを取り出し、内部状態を更新して、CLに渡すリクエスト群を返します。

EIP本文の中核ロジックは以下です。

```python
def read_withdrawal_requests():
    reqs = dequeue_withdrawal_requests()
    update_excess_withdrawal_requests()
    reset_withdrawal_requests_count()
    return ssz.serialize(reqs)
```

このシステムコールにはいくつか強い制約があります。

| 項目 | 内容 |
|---|---|
| gas limit | 専用で`30_000_000` |
| gas accounting | この呼び出しで消費した`gas`は`block`全体の`gas`使用量に含めない |
| fee burn | **EIP1559**の`burn`対象にしない |
| codeが無い場合 | `block`を`invalid`とする |
| call failure | `block`を`invalid`とする |

ここはかなり重要です。
単なる補助的な`call`ではなく、`fork`後の`block validity`に直接関わるシステム操作になっています。
つまりチェーンが有効化の準備を終えていること、そしてそのアドレスで出金要求用コントラクトが正しく動くことが前提です。

#### CL側での処理

EIP本文では、CL側の詳細仕様は`consensus-specs`を参照する形になっています。
Electraでは、実際には`block.body.execution_requests.withdrawals`に最大`16`件までの`WithdrawalRequest`が入り、`process_withdrawal_request()`で1件ずつ評価されます。

![execution-requests-transport.png](/images/qiita-77b135ce-c5ec-461c-9046-2e67fbc38fac.webp)

この図では、EL側の順番待ちとシステムコールから返る出金要求の一覧が、`0x01 + SSZ list`のバイト列として整形され、Electraの`BeaconBlockBody.execution_requests.withdrawals`まで運ばれる流れをまとめています。
ここで重要なのは、**EIP7685**がリクエスト受け渡しのルールを与え、Electra側がそれを`execution_requests`の`withdrawals`スロットとして受け取る、という役割分担です。

ここで重要なのは、CL側がまず「**このリクエストは有効な実行レイヤー側の出金先情報から来たか**」を見てから、その後で`full exit`と`partial path`を分岐することです。
Electraの`predicate`では`has_execution_withdrawal_credential()`が使われるため、送信元アドレス認証の対象は`0x01`だけでなく`0x02`の`compounding credential`も含みます。

![cl-withdrawal-branching.png](/images/qiita-aa78b7cc-7841-42c3-82ce-2b1d57e73599.webp)

この図では、CL側がまず出金先の種類と送信元アドレスを確認し、その後で`amount == 0`と`amount > 0`を分岐させる流れを表しています。
`amount == 0`は`full exit`のシグナルですが、`amount > 0`はあくまで`partial candidate`であり、`0x02`の`compounding credential`や`excess balance`などの条件を満たした時だけ`pending partial withdrawal`に積まれます。

分岐の骨子は以下です。

1. **`amount == 0`**
    `FULL_EXIT_REQUEST_AMOUNT` として扱われます。
    この場合は`full exit`のシグナルで、`pending partial withdrawal`が残っていなければ`validator exit`を開始します。
2. **`amount > 0`**
    `partial withdrawal`の候補として扱われます。
    ただし実際に`pending partial withdrawal`へ積まれるのは、`0x02`の`compounding credential`であること、十分な`effective balance`があること、`excess balance`があることなどを満たした時だけです。
3. **条件を満たさない`request`**
    `pubkey`が存在しない、送信元アドレスが一致しない、すでに`exit`済み、`partial queue`が満杯、といったケースではそのリクエストだけが無視されます。

つまり`amount > 0`を送ったから自動的に部分出金になるわけではありません。
ELはリクエストを渡すだけで、実際に`full exit`になるか、`pending partial withdrawal`に積まれるか、何も起こらないかはCLの条件判定に委ねられます。

## 補足

#### `validator_pubkey` を使う理由

一見すると`validator index`でも対象を指定できそうですが、ステーキングプールなどのスマートコントラクトは`validator`作成前の`index`を知りません。
一方で`pubkey`は事前に分かるため、EL側からリクエストを組み立てるには`pubkey`の方が扱いやすいです。

![validator_pubkey を使う理由の図](/images/qiita-452c8a90-097e-4610-bb6a-b19f879214f4.webp)

この図では、`validator index`が**後から決まる番号**であるのに対し、`validator pubkey`は**事前に分かる識別子**であることを並べています。
そのため、実行レイヤー側から要求を組み立てる入口としては、`index`より`pubkey`の方が自然です。

この仕様は「**CLに詳しい主体だけが使える**」形を避け、スマートコントラクトが自然に扱える入力へ寄せています。

#### キューを持つ理由

代替案として、そのブロック内で`MAX_WITHDRAWAL_REQUESTS_PER_BLOCK`件を超えたら`contract call`自体を失敗させる設計も考えられます。
ただしそれだと、混雑時に`honest`な利用者の`transaction`まで失敗しやすくなります。

キューを持てば、利用者の`call`は成功させつつ、CL側への受け渡しだけを`rate limit`できます。
EIP本文でも、この`UX`の方が望ましいとしてキュー方式を選んでいます。

#### 手数料による`griefing`対策

この提案が防ぎたい攻撃の1つは、安価に大量の出金リクエストを送りつけて、他の利用者の退出を遅らせることです。
`validator`ごとの厳密な回数制限をCL状態証明つきでやる方法もありますが、それには**EIP4788**のような別の`cross-layer`前提や、追加の`state tracking`が必要になります。

**EIP7002** はそこまで複雑な仕組みを持ち込まず、需要が高い時は手数料を上げるという経済的な手法を選んでいます。
平常時は安く、混雑時だけ高くすることで、実装複雑性を増やさず`abuse`を抑える狙いです。

#### **EIP7251** との関係

**EIP7002** は単体でも重要ですが、後続仕様とのつながりを見るとさらに分かりやすいです。
特に**EIP7251**では、バリデータの最大`effective balance`を引き上げると同時に、部分出金や統合の仕組みが重要になります。

**EIP7251** の中でも、部分出金は **EIP7002** で定義される実行レイヤーメッセージによってトリガーされます。
つまり**EIP7002**は、Electra系ステーキング改善の土台の1つです。

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

[\[EIP7251\] ステーク上限を2048 ETHの仕組みを理解しよう！](/articles/f501837f250747fad8b6)

## 互換性

**EIP7002**は`block structure`と`block validation rule`を変更するため、後方互換な変更ではありません。
`fork`後は、`execution client`も`consensus client`も、この新しいリクエスト処理を理解していなければ正しい`block`を検証できません。

ただし、既存ユーザーの通常のETH送受信やスマートコントラクト利用を壊す種類の変更ではありません。
影響があるのは、主にバリデータの退出と部分出金を支えるプロトコルルールの側です。

## セキュリティ

#### 既存`custody`関係への影響

この提案が入ると、`withdrawal credential`側が退出リクエストを始められるようになります。
そのため、もし既存の`custody`関係が「**`withdrawal credential`では退出開始できない**」という前提に依存していた場合、期待していた責務分担が変わる可能性があります。

EIP本文では、それでも`withdrawal credential`は最終的に資産を所有する側なのだから、退出権限を持つのは自然だと整理しています。
もし旧来の振る舞いを維持したいなら、`0x01`の`withdrawal credential`の先をスマートコントラクトにして、その中で制約を再現する方法が考えられます。

#### 手数料の払い過ぎ

このコントラクトは払い過ぎた手数料を返しません。
さらに、`transaction`作成時と`inclusion`時で`fee`が変わることもあります。
そのため、リクエストを送る側は「**いま必要なfeeを先に読む**」「**想定より高ければrevertする**」というチェックを入れる必要があります。

特にEOAから直接リクエストする場合は、厳密にちょうどの額を払うことが難しく、払い過ぎが起こりやすいです。
この点は`gas fee`と別の注意点として見ておく必要があります。

#### システムコール失敗時の扱い

`fork`後は、その出金要求用コントラクトへのシステムコール失敗が無効なブロックにつながります。
問題の`transaction`が`mempool`に残ったままだと、同じ失敗を次スロットでも繰り返す可能性があります。

EIP本文では、`block producer`実装が`transaction`集合をシャッフルするなどして、問題の`transaction`を除いた`valid block`を作りやすくすることを推奨しています。
単に「**失敗したblockを捨てれば終わり**」ではなく、`mempool`側の再試行も考慮する必要がある点が重要です。

#### コード未配置時の失敗

`WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS`に`code`が無い状態でこのEIPを有効化すると、最初の`block`以降がすべて`invalid`になります。
この仕様は「**そのアドレスにcontractがあること**」をプロトコルの前提にしているからです。

本文では回復策として、2つを挙げています。

1. **先に`contract code`を配置する**
    `transaction`で`code`を配置し、その`block`を最初の`valid block`にする方法です。
2. **`activation`を延期する**
    `client`実装の`fork timestamp`や`block number`を後ろへずらし、`contract`を準備してから有効化する方法です。

![invalid-block-failure-paths.png](/images/qiita-610c0455-e964-4397-862f-4d847b38e8f5.webp)

この図では、`fork`有効化後にそのコントラクトの`code`が無い場合や、システムコール自体が失敗した場合に、その`block`が`valid block`へ進めず`invalid block`になることを表しています。
つまりこの`contract`は単なる補助コントラクトではなく、`fork`後の`block validity`を支える前提要素です。

## 最後に

今回は「**EIP7002**」についてまとめてきました。

**EIP7002**は、`withdrawal credential`側に退出や部分出金の開始権限を寄せることで、ステーク資産の最終所有者がより自然に制御できるようにする提案です。
実行レイヤーの出金要求用コントラクト、キュー、動的`fee`、そして**EIP7685**経由のCL受け渡しまで含めて設計されており、**EIP7251**のような後続仕様の土台としても重要です。

元の仕様もぜひ読んでみてください。

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

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

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/
