EIP-7702でEOAはどう変わる?tx.origin・msg.senderをFoundryで確認する
EIP-7702でEOAへ委任コードを設定したとき、アドレス・残高・保存領域・msg.sender・tx.originがどう扱われるかを、Foundryの再現テストで確認します。

EIP-7702を一言でいうと、普段使っているウォレットアドレスのまま、処理だけを別のコントラクトへ任せられる仕組みです。
たとえば、店舗の住所、残高、帳簿はそのままにして、「この受付手順を使って処理してください」という案内を店頭へ置くイメージです。住所そのものが別店舗へ変わるわけではありません。
Ethereumでは、EIP-7702がPectraアップグレードの一部として2025年5月7日に有効化されました。これにより、EOAでも複数操作の一括実行や、別の送信者によるガス代の立て替えなどを実装しやすくなっています。
一方で、これまでの「EOAにはコードがない」「tx.origin == msg.senderなら最初の呼び出し階層である」という前提は、そのまま使えなくなりました。
この記事では、EOAのどの部分が残り、どの部分を委任先が担当するのかを整理した後、Foundryで保存領域と呼び出し元を確認します。
先に用語を普通の言葉で確認する
| 用語 | 普通の言葉でいうと | EIP-7702での役割 |
|---|---|---|
| EOA | 秘密鍵で署名して取引を送る通常のウォレットアドレス | アドレス、残高、送信順番号、保存領域を持つ |
| 委任先(implementation) | 実際の処理手順を書いたコントラクト | EOAが実行する関数のロジックを提供する |
| 委任を示すコード | 「この委任先の処理を使う」という案内 | 0xef0100と委任先アドレスを組み合わせた23バイト |
| 委任許可(authorization) | EOA本人が委任を認めた署名 | チェーンID、委任先、nonce、署名を含む |
msg.sender |
今の関数を直接呼んだアドレス | 呼び出し階層ごとに変わる |
tx.origin |
取引全体を開始したEOA | 取引の途中でも同じアドレスが続く |
専門用語をすべて暗記する必要はありません。まずは、住所と帳簿はEOA、処理手順は委任先と考えると追いやすくなります。
EOAへ置かれるのは実装コードではなく23バイトの案内
EIP-7702では、EOAへ委任先コントラクトの全コードをコピーしません。EOAのコード欄へ、次の形の短いデータを設定します。
0xef0100 | 20-byte implementation address
0xef0100が3バイト、アドレスが20バイトなので、合計は23バイトです。このデータは委任を示すコード(delegation indicator)と呼ばれます。
EOAが呼び出されると、EVMはこの案内を見て委任先のコードを取得します。ただし、処理は委任先自身のアドレスではなく、委任を設定したEOAの実行文脈で動きます。
| 実行時の要素 | 使われるもの |
|---|---|
address(this) |
委任を設定したEOAのアドレス |
| ETH残高 | EOAの残高 |
| 保存領域 | EOAの保存領域 |
| 関数のロジック | 委任先コントラクトのコード |
| 委任先自身の保存領域 | 通常は変更されない |
動き方はdelegatecallに近いものの、通常のdelegatecallをEOAが自分で実行するわけではありません。EVMが23バイトの案内を解決し、委任先コードをEOAの文脈で実行します。
委任は取引が終わっても残る
EIP-7702は新しい取引形式0x04を追加し、その取引へ委任許可の一覧を添付します。許可が正しければ、EOAのコード欄へ委任を示すコードが設定されます。
この設定は、その取引だけで消える一時的なものではありません。別の委任先へ置き換えるか、ゼロアドレスを指定して解除するまで残ります。
特に注意したいのは、委任を設定した後の関数実行が取り消されても(revert)、処理済みの委任設定は元に戻らない点です。アプリ側で「呼び出しに失敗したから委任も失敗した」と判断しないようにします。
また、委任を解除してもEOAの保存領域は自動では消えません。次の委任先が同じ保存場所を別の意味で読むと、権限や数値を誤って解釈する可能性があります。
FoundryでEOAの実行文脈を確認する
今回は次の環境で確認します。
| 項目 | バージョン・設定 |
|---|---|
| Foundry | v1.8.0 |
| Solidity | v0.8.36 |
| EVM | Prague |
| 外部RPC | 不要 |
| 実資産 | 不要 |
EIP-7702のチートコードを使うには、EVMのバージョンをprague以上にします。
[profile.default]
solc_version = "0.8.36"
evm_version = "prague"
optimizer = true
optimizer_runs = 200
検証では、委任先のDelegatedAccountと、呼び出し元を記録するContextTargetを使います。
contract ContextTarget {
address public lastSender;
address public lastOrigin;
bool public senderEqualsOrigin;
function capture() external {
lastSender = msg.sender;
lastOrigin = tx.origin;
senderEqualsOrigin = msg.sender == tx.origin;
}
}
contract DelegatedAccount {
uint256 public number;
address public executionAddress;
function setNumberAndCapture(uint256 newNumber, address target) external {
require(msg.sender == address(this), "only self");
number = newNumber;
executionAddress = address(this);
ContextTarget(target).capture();
}
}
numberとexecutionAddressは委任先コントラクトに宣言されています。ただし、委任実行時に書き込まれる場所はEOA側です。
委任を設定してEOA自身から呼び出す
FoundryのsignAndAttachDelegationは、指定した秘密鍵で委任許可へ署名し、テスト内の次の取引へ添付します。ここで使う鍵はローカルテスト専用です。
vm.signAndAttachDelegation(address(implementation), ALICE_PRIVATE_KEY);
require(alice.code.length == 23, "delegation indicator must be 23 bytes");
vm.prank(alice, alice);
DelegatedAccount(alice).setNumberAndCapture(42, address(target));
この呼び出しでは、取引の開始元と直接の呼び出し元をどちらもaliceにしています。委任先のコードは動きますが、address(this)は実装コントラクトではなくaliceです。
確認する結果は次の通りです。
require(DelegatedAccount(alice).number() == 42);
require(DelegatedAccount(alice).executionAddress() == alice);
require(implementation.number() == 0);
require(target.lastSender() == alice);
require(target.lastOrigin() == alice);
require(target.senderEqualsOrigin());
| 確認結果 | 意味 |
|---|---|
alice.code.length == 23 |
EOAへ委任を示す短いコードが設定された |
DelegatedAccount(alice).number() == 42 |
値はEOA側の保存領域へ書かれた |
implementation.number() == 0 |
委任先自身の保存領域は変わっていない |
executionAddress() == alice |
委任先コードがEOAのアドレスで実行された |
target.lastSender() == alice |
次のコントラクトから見る直接の呼び出し元はEOA |
target.lastOrigin() == alice |
取引全体の開始元も同じEOA |
最後の2項目が、従来の前提を見直す必要がある理由です。ContextTargetは入れ子になった次の呼び出し階層ですが、msg.sender == tx.originが成立しています。
tx.originとmsg.senderの等値だけでは最上位の呼び出しと断定できない
EIP-7702以前は、msg.sender == tx.originなら「途中にコントラクトの処理を挟んでいない最初の呼び出し」と考えるコードがありました。
委任されたEOAは、自分の文脈で複数のコントラクトを順番に呼び出せます。そのため、2つ目以降の呼び出し先でも、直接の呼び出し元と取引開始元が同じEOAになる場合があります。
require(msg.sender == tx.origin, "contracts not allowed");
この条件だけでは、次のことを証明できません。
- 途中でプログラム可能な処理が動いていない
- 1回の取引で前後に別の状態変更が行われない
- 再入可能な経路がない
- フラッシュローンなどの原子的な一連処理を使っていない
ただし、「msg.sender == tx.originを使うすべてのコードが同じ形で壊れる」という意味でもありません。何を保証する目的で比較しているのかを確認し、最上位の呼び出しや再入防止の代わりに使っている場合は修正します。
再入防止には専用のガード、状態更新の順序、必要に応じた一時保存領域など、目的に合った仕組みを使います。
誰がEOAを呼ぶかでmsg.senderは変わる
委任先のコードがEOAの文脈で動くことと、msg.senderが常にEOAになることは別です。
今回のテストは、aliceがalice自身を呼び出しています。そのため、委任先コードの中では次の関係になります。
address(this) = alice
msg.sender = alice
tx.origin = alice
一方、別の送信者やリレイヤーがaliceを呼べば、委任先コード内のmsg.senderはその呼び出し元になります。
address(this) = alice
msg.sender = relayer
tx.origin = relayer
委任先の関数を誰でも呼べる状態にすると、EOAの残高や保存領域を第三者が操作できる危険があります。実装側で署名、権限、nonce、期限、呼び出し対象を検証する必要があります。
安全な委任先を確認する7項目
1. 委任先アドレスとコードを確認する
署名対象のアドレスが、監査・検証した実装と一致するか確認します。proxy(プロキシ)を使う場合は、実装の変更権限も含めて評価します。
2. 初期化を第三者へ取られないようにする
委任だけを先に設定し、誰でも呼べる初期化関数を後から実行する設計は危険です。初期化を同じ流れで行うか、EOA本人だけが実行できる条件を入れます。
3. 保存領域の配置を固定する
委任先を変更しても、EOA側の保存領域は残ります。新旧実装が同じスロットを違う意味で使わないように、保存領域の設計と移行手順を決めます。
4. 操作署名へ必要な条件を含める
委任先が署名付き操作を受け付ける場合は、少なくともチェーンID、呼び出し先、入力データ、送るETH量、nonce、期限を署名対象へ含めます。
5. 2種類のnonceを混同しない
EIP-7702の委任許可には、同じ委任署名の再利用を防ぐEOAのnonceがあります。それとは別に、委任先が受け付ける操作署名にも再利用防止のnonceが必要です。
送信順番号の確認方法はnonce too low・replacement underpricedの原因と直し方でも説明しています。
6. 失敗後も委任状態を確認する
後続処理が取り消されても(revert)、委任設定が残る場合があります。トランザクション全体の調査では、実行結果だけでなくEOAのコードと委任先も確認します。失敗トランザクションの確認順と合わせると切り分けやすくなります。
7. 解除後の保存領域も確認する
ゼロアドレスへの委任でコードを解除しても、保存領域は消えません。再委任する前に、残っている権限、nonce、許可リスト、残高管理の値を確認します。
まとめ
EIP-7702では、EOAへ実装コントラクトの全コードをコピーするのではなく、委任先を示す23バイトの案内を設定します。
その結果、アドレス、残高、保存領域はEOAのものを使い、関数のロジックだけを委任先から取得する形になります。
Foundryのテストでは、値がEOA側へ保存され、委任先自身の保存領域は変わらないことを確認できました。また、委任されたEOAが別のコントラクトを呼ぶと、入れ子の階層でもmsg.sender == tx.originが成立する場合があります。
実装を確認するときは、委任先コードだけでなく、初期化、保存領域、操作署名、2種類のnonce、失敗後の委任状態、解除後に残るデータまで確認してください。
確認した一次情報
- EIP-7702 Set Code for EOAs確認日: 2026/8/28
- Ethereum Foundation Pectra Mainnet Announcement確認日: 2026/8/28
- Foundry signDelegation確認日: 2026/8/28
- Foundry v1.8.0確認日: 2026/8/28
- Solidity 0.8.36確認日: 2026/8/28



