3MIKAN
仮想通貨直コン

EIP-7702 delegationのライフサイクル:設定・変更・解除・storageを検証する

EIP-7702のdelegationを同じEOAへ設定し、AからBへ変更して解除するまでを、code・storage・nonce・balance・allowanceと失敗ケースに分けて検証します。

3MIKANのブランドキャラクターが同じ保管棚の処理bookを差し替え、棚と保存物はそのまま残るEIP-7702の記事画像

EIP-7702のdelegationを解除すると、EOAへ置かれた23-byteの案内は消えます。しかし、EOAのstorage、ETH balance、token contractが持つallowance、delegate独自のsessionやmoduleまで一括で消えるわけではありません。

先に結論を固定すると、delegationをclearしても、storage、allowance、session stateは同時には消えません。緊急時に「codeが0xへ戻ったから全権限を回収できた」と判断しないことが大切です。

この記事は、EIP-7702でEOAの実行文脈を確認する基礎記事の続編です。同じローカルEOAへdelegate Aを設定し、compatibleなBへ変更し、zero addressでclearするまでを、FoundryとAnvil Pragueで追います。

実在wallet、public RPC、mainnet account、実tokenは使いません。掲載addressとprivate keyはすべてAnvil既定のローカル検証値、またはテスト専用値です。

まず、clearで変わるものと残るものを分ける

state delegationをclearした直後 保存している場所
delegation indicator `0xef0100
EOA address 同じ Ethereum account state
EOA nonce clear authorizationがvalidなら増える authority EOA
ETH balance clearだけでは移動しない。gas支払いは別 authority EOA
delegateが使ったstorage 自動では消えない authority EOAのstorage
ERC-20 allowance 自動では消えない token contractのowner → spender state
session key / module 保存先と無効化規則による EOA storageまたは外部contract
delegate implementation code clearの対象ではない delegate address側

「delegationを解除する」と「token approvalをrevokeする」は別操作です。allowanceのowner、token、spenderを確認する手順は、Spending cap・approve・revokeの確認方法で分けています。

同じEOAでdelegationをAへ設定し、Bへ変更し、解除する4段階

authorization tupleはchain ID・delegate・nonceへ署名する

EIP-7702のtype 0x04 transactionは、次のauthorization tupleを持ちます。

[chain_id, address, nonce, y_parity, r, s]

署名対象のhashは次の形です。

keccak256(0x05 || rlp([chain_id, address, nonce]))
  • chain_id: authorizationを有効にするchain。0ならcross-chain扱い
  • address: EOAが処理を委任するcode address。zero addressならclear
  • nonce: authority EOAの現在nonceと一致させる再利用防止値
  • y_parity / r / s: authorityを復元するsecp256k1署名

これは外側のtype 0x04 transaction署名とは別です。transactionを送るaccountと、authorizationへ署名するauthorityは異なっていても構いません。

clientはauthorization listをtransaction executionより先に、ただしtransaction senderのnonceを増やした後に処理します。tupleがwrong chainやnonce mismatchなら、そのtupleをskipして次へ進みます。複数のvalid tupleが同じauthorityを対象にする場合は、最後のvalid occurrenceが最終codeを決めます。

self executorではnonceが2回増える

今回のlifecycleは、authority自身がtype 0x04 transactionを送っています。そのため1回のphaseで、次の2回が起きます。

  1. transaction senderとしてauthority nonceが1増える
  2. valid authorizationの処理でauthority nonceがもう1増える

viem 2.56.0ではexecutor: 'self'を指定すると、この順序を見越してauthorization nonceをtransaction nonceより1大きく準備します。

const authorization = await authorityClient.signAuthorization({
  contractAddress: delegateA,
  executor: 'self',
})

await authorityClient.sendTransaction({
  to: authority.address,
  authorizationList: [authorization],
  data: configureAndApproveData,
})

relayerがtransactionを送る場合、authorityはtransaction senderではないため、valid authorizationによる増加は通常1回です。EIP-7702なら常にnonceが2増えるわけではありません。

同じEOAをbefore → A → B → clearで読む

実行環境を固定しました。

項目 固定値
EIP-7702 Final、source commit bbc3f95844c37612a2f1b9e7477990bb717ecfa0
execution spec Prague、commit c4deda5b3cfc5c1c8429dcd9159a6fb5636d8486
Foundry / Anvil 1.8.0、commit 61ae26af36320d4fa1020f7db53785885e29eeb5
Solidity 0.8.36、EVM prague
viem 2.56.0、tag commit 063faa52a247233c32445c5eda735ade21ca5801
chain ID local 31337
base fee / gas price 0。balance境界だけを分離する再現設定
外部RPC / 実資産 なし

phaseごとに、eth_getCode相当、EOA nonce、balance、slot 0、slot 1、mock ERC-20 allowanceを同じ列で読みました。

phase code / target nonce balance slot 0 session slot 1 allowance
before 0x / none 0 10,000 ETH 0 zero address 0
delegate A 23 bytes / A 2 10,000 ETH 41 0x…bEEF 250
delegate B 23 bytes / B 4 10,000 ETH 42 0x…bEEF 250
clear 0x / none 6 10,000 ETH 42 0x…bEEF 250

10,000 ETHという値はAnvilの初期balanceです。base feeとgas priceを0へ固定したため変わりません。実networkではtransaction senderがgasを支払えばbalanceは減ります。

再現値はpublic/fixtures/eip-7702-lifecycle-6017.jsonへ保存しています。code targetやtest-only addressを含みますが、mainnet deploymentや利用推奨addressではありません。

delegate Aを設定してstorageとallowanceを作る

delegate Aはslot 0へnumber、slot 1へsessionKeyを置きます。同じself transaction内でmock tokenへapproveも呼びました。

contract LifecycleDelegateA {
    uint256 public number;
    address public sessionKey;

    modifier onlySelf() {
        require(msg.sender == address(this), "only self");
        _;
    }

    function configureAndApprove(
        uint256 newNumber,
        address newSessionKey,
        address token,
        address spender,
        uint256 amount
    ) external onlySelf {
        number = newNumber;
        sessionKey = newSessionKey;
        require(IApprovalToken(token).approve(spender, amount));
    }
}

delegated codeはauthority EOAの文脈で動きます。したがってnumbersessionKeyはimplementation A自身ではなくauthority EOAのstorageへ入り、token contractが見るmsg.senderもauthorityです。

allowanceはEOAのslotへ保存されません。mock token contract側のmappingへ、次の組み合わせで保存されます。

token.allowance(authority, spender) = 250

この保存先の違いが、clear後にもallowanceが残る理由です。

compatibleなdelegate Bは同じstorageを引き継ぐ

delegate BはAと同じ順序・同じ型でstate variableを宣言しています。

contract LifecycleDelegateB {
    uint256 public number;
    address public sessionKey;

    function incrementNumber() external onlySelf {
        number += 1;
    }
}

AからBへdesignationを変えた直後、Bはslot 0の41とslot 1のsession keyを読みました。incrementNumber()後はslot 0が42になり、allowanceは250のままです。

これは、この小さなA/Bが偶然ではなく意図して同じlayoutを共有した結果です。任意の2 implementationを差し替えて安全という意味ではありません。

incompatibleなlayoutは残存値を別の意味で読む

失敗再現では、同じslotを別の型と意味へ割り当てました。

contract IncompatibleLifecycleDelegate {
    address public admin;      // slot 0: Aではnumber
    uint256 public dailyLimit; // slot 1: AではsessionKey
}

Aがslot 0へ保存した41は、incompatible delegateからaddress(0x29)に見えます。slot 1の0x…bEEFは、dailyLimit = 48879として読めます。EVMは「名前が違うから危険」と自動判定せず、同じ256-bit slotをそのまま解釈します。

ローカル再現では期待したadminと一致しないためstorage collisionでrevertしました。しかし、実装が明示的に検査しなければ、誤った値のまま処理を続ける可能性があります。

ERC-7201は、namespace IDからstorage rootを導く規約を定義しています。EIP-7702自身もdelegate変更時のcollision対策としてnamespaced storageに触れています。ただし、annotationを書くだけでcompilerが移行安全性を保証するわけではありません。

変更前には少なくとも次を固定します。

  • old / new implementationのstorage layoutとnamespace ID
  • 追加・削除・型変更したfield
  • mapping、array、packed fieldのrootと意味
  • initializer / migrationを誰がいつ実行するか
  • old session、module、nonce、permissionを新実装がどう読むか
  • rollbackしたときにnew stateをold codeがどう読むか

zero addressでclearしてもstorageは消えない

clear authorizationでは、delegate addressへzero addressを指定します。validならclientはauthority code hashをempty codeへ戻し、authority nonceを増やします。

const clearAuthorization = await authorityClient.signAuthorization({
  contractAddress: zeroAddress,
  executor: 'self',
})

Anvilでclear後に確認した結果は次の通りです。

code.length       = 0
nonce             = 6
storage slot 0    = 42
storage slot 1    = 0x...bEEF
ETH balance       = 10000 ETH  // zero-fee local run
token allowance   = 250

EIP-7702には、clearと同時に全storage keyを消すprotocol処理はありません。仕様は、必要ならstorageを消すための専用delegateを設計できると説明しています。

ただし、「専用delegateを一度呼べば、任意のaccount storageを完全に消せる」と一般化もできません。mappingや動的配列のすべてのkeyを列挙できるか、誰がclear関数を呼べるか、途中revertやgas上限をどう扱うかを実装ごとに検証します。

delegation解除後もEOA storageと外部token allowanceが別stateとして残る境界

allowance・session key・moduleは保存先ごとに止める

delegationのclearは、権限回収の一項目です。少なくとも次を別々に確認します。

ERC-20 allowance

allowanceはtoken contractがownerspenderの組み合わせで返します。clear後もallowance(authority, spender)を読み、必要なら同じtoken・spenderのrevokeを別transactionとして検討します。

EOA storage内のsession key

codeがemptyの間は、そのsession keyを読むdelegate functionもありません。しかし値はslotに残るため、同じlayoutのdelegateを再設定すると再び有効と解釈される可能性があります。

session keyを無効化したというためには、key record、permission範囲、expiry、delegate独自nonceを、実装が定義する方法で更新した証拠が必要です。

外部registryやmodule

module、guardian、permission registryが別contractにある場合、EOAのcode変更ではそのcontract stateは変わりません。外部contract側のdisable / remove結果をreadし直します。

application nonce

EIP-7702 authorization nonceはaccount nonceです。delegateが署名付き操作へ使うapplication nonce、session nonce、ERC-4337 nonce keyとは別です。UserOperation失敗をvalidation・paymaster・executionに分ける方法でも、EntryPointとaccount内部の状態を分けて確認しています。

failure 4ケースはreceiptだけで判定しない

case local result 読み方
wrong chain outer receiptはsuccess、authority code 0 bytes、nonce 0 invalid tupleがskipされた。receipt successはdesignation成功を意味しない
stale nonce replay 1回目・replayともouter receiptはsuccess、nonceは1のまま 2回目のtupleがskipされ、A designationは変わらない
incompatible storage 意味検査がstorage collisionでrevert code変更自体とlayout安全性は別
delegate call revert receiptはreverted、reverting delegateへのdesignationとnonce 1は残る valid authorizationはexecution revertでrollbackされない

wrong chain

chain 31338へ署名したtupleをchain 31337のtype 0x04へ入れました。transaction自体はblockへ入りましたが、authorityのcodeとnonceは変わりませんでした。

nonce replay

nonce 0のauthorizationをrelayerが一度使うと、authority nonceは1になりました。同じsigned tupleをもう一度含めたouter transactionもsuccessでしたが、tupleはnonce mismatchでskipされ、nonceとdesignationは変わりませんでした。

「replay transactionがrevertするはず」と決め打ちせず、authority code、target、nonceを最終stateで確認します。

delegate call revert

reverting delegateへのvalid authorizationとfail() callを同じtype 0x04へ入れると、execution receiptはrevertedになりました。それでもauthority codeはreverting delegateを指し、nonceは1です。

後続callが失敗したからdelegationも設定されなかった、とは判断できません。transaction調査ではreceipt・revert・logs・final stateの確認順と合わせ、authority codeを独立して読みます。

Forge helperとAnvil transactionの役割を分ける

Foundry 1.8.0のsignDelegationattachDelegationsignAndAttachDelegationは、Prague以降のtestでdelegated codeを扱えます。今回のForge testは6/6で、次を確認しました。

  • EOA storageへ書き、implementation storageは変わらない
  • A → B → clearでraw storageとallowanceが残る
  • stale nonceのattachを拒否する
  • wrong-chain signatureがauthorityを変えない
  • incompatible layoutが別の意味でslotを読む
  • delegate callがrevertしても設定済みdesignationが残る

ただしattachDelegationはtest VMへcodeを直接反映する便宜helperで、単独では実type 0x04のauthorization nonce transitionを再現しません。そこでAnvil 1.8.0へviemでraw transactionを送り、RPCから0 → 2 → 4 → 6を別に確認しました。

ローカル検証の実行は次の一つにまとめています。

CI_REQUIRE_FOUNDRY=1 npm run test:contracts

delegate addressが同じでもcodeの意味は固定とは限らない

delegation indicatorが保持するのはdelegate addressで、runtime code hashではありません。targetがproxyなら、EOA側の23 bytesが同じままimplementationやbehaviorが変わる場合があります。

proxy targetの実行先とauthorityを追うときは、ERC-1967のimplementation・admin・beacon slot、upgrade event、storage layoutを読む方法で、delegate addressの先をさらに分解できます。

確認時は次を分けます。

  1. authorityの23-byte codeとdelegate target
  2. target addressのcurrent runtime code / code hash
  3. proxyならimplementation、admin、upgrade event
  4. storage layoutとmigration version
  5. 確認日時、chain、client

EIP-6780以後、既存contractのSELFDESTRUCTは、同じtransactionで作成された例外を除きcodeやstorageを削除しません。SELFDESTRUCTを一般的なdelegate code消去・差し替え手段として扱わず、authority側のdesignation clearとtarget側のcode lifecycleを別々に読みます。

batch callをdelegateへ持たせる場合も、wallet requestとaccount executionは同じ層ではありません。wallet_sendCallsとERC-7821の実装・検証で、EIP-7702 self-call、atomic rollback、wallet statusを分けています。

緊急時に確認する順番

実在accountで不審なdelegationが疑われる場合、この記事の検証用private keyやaddressを使わず、利用walletとchainの公式手順を確認してください。少なくとも次を一つずつ記録します。

  1. 新しい署名・transaction・dapp接続を止める
  2. chain IDと対象accountを固定する
  3. account codeがemptyか23 bytesか、delegate targetはどこか読む
  4. account nonce、pending / replaced transactionを確認する
  5. target code、proxy implementation、upgrade権限、verificationを確認する
  6. storage namespace、session key、module、guardian、application nonceを確認する
  7. tokenごとのowner・spender・allowanceを確認する
  8. walletが対応するclear手順のcall内容とfeeを確認する
  9. receipt後にcode、nonce、allowance、session / module stateを再度読む

実在walletのdelegation表示やclear UIは、生成画像で代用していません。画面captureが必要な検証はブラウザ実機検証の集約Issueで、wallet version、chain、確認日、個人情報のredactionを含めて扱います。

delegation authorizationへ署名する前に、eth_sign、typed data、Permit、SIWEとの違いから確認する場合は、wallet署名要求の見分け方と停止フローでmethod、origin、chain、delegate、nonceを同じrequestへ対応させてください。

walletのsite permissionを外した後に、allowance、server session、session key、module、delegationのどれが残るかを横断確認する場合は、DApp切断と権限解除の違いで保存場所、read方法、transaction要否を一件ずつ対応させてください。

まとめ

  • zero address authorizationはEOAのdelegation indicatorをempty codeへ戻す
  • clearでもEOA storage、balance、token allowanceは自動で消えない
  • compatibleなA / Bはstateを引き継げるが、incompatible layoutは同じslotを別の意味で読む
  • self executorではouter transactionとauthorizationでnonceが2回増える。relayer経路と同一視しない
  • wrong-chainやstale tupleはskipされるため、outer receipt successだけで設定成功を断定しない
  • delegate callがrevertしても、先に処理されたvalid designationは残る
  • target address、runtime code、proxy upgrade、storage layout、external permissionを別々に確認する

delegationのclearは重要ですが、権限回収の完了条件そのものではありません。code、storage、nonce、balance、allowance、session / moduleを、それぞれの保存先で読み直すことが最後の確認です。

この記事とローカル検証は特定wallet、delegate、revoke手順、安全性を推奨・保証するものではありません。将来wallet security、smart-account SDK、monitoring、audit serviceのsponsorを掲載する場合も、広告と仕様・検証結果・緊急時確認項目を分離します。

確認した一次情報