Pre-ERC: Adoption Profile and Reference Kernel for EOAs Adopting Code via SETCODEFROM

TL;DR

EIP-8298 (SETCODEFROM) lets an EOA adopt account code at its own address. EIP-8151 makes ecrecover stop returning addresses that hold regular code. Together, they stop a secp256k1 key from sending transactions or passing ecrecover for that account, without moving assets.

Adoption is one-way: there is no return to empty code, and that is what retires the key. So the adopted code, and the transaction that adopts it, must be right the first time. Neither EIP specifies that code.

I found seven failure modes. Most are irreversible, and one is a check that looks sound but proves nothing. For example, code that falls back to ecrecover(...) == address(this) locks the account out the moment it is adopted.

This pre-ERC proposes:

  1. An adoption profile. Requirements R1–R8, most tied to a failure mode. Existing accounts can be adapted to conform.
  2. A migration guard. MigrationCheck reverts the migration unless four things hold in one transaction: the old key signed this migration, the signed code was adopted, the signed initialization ran, and ecrecover now rejects the old key.
  3. A reference kernel (non-normative). A minimal conforming account with ERC-7913 verifiers for post-quantum (PQ) keys.

Where I most need input:

  • Do parts of this belong in EIP-8298 instead? I’m happy to open a PR.
  • Does EIP-8298’s mempool bytecode scan apply to the sender’s own code?
  • How would ERC-8286 and existing 4337/7579 accounts best conform?

Context: I’m working on the EELS implementation of EIP-8151 (ethereum/execution-specs#3860).

Background

  • Status. For Hegotá, EIP-8141 (frame transactions) is Scheduled for Inclusion. EIP-8151 and EIP-8298 have been Considered for Inclusion since ACDE #247 (2026-10-08). The reference kernel depends on all three; the profile and Route A need only EIP-8298 and EIP-8151.
  • Key authority. A 7702 delegation keeps the EOA key’s authority; adopting regular code via EIP-8298 retires it. Wallet code alone can’t stop ecrecover-based authorization in existing contracts, so EIP-8151 closes that path in the protocol. The guard below tests exactly this choice. (This framing is due to @colinlyguo.)
  • The gap. EIP-8298 sketches SETCODEFROM(PQ_WALLET_TEMPLATE) but leaves the template undefined. ethereum/EIPs#12506 (open) adds a set-code operation to EIP-8141’s default code, and it also leaves the adopted code open.
  • Prior art. I found no standard or implementation that targets adoption via SETCODEFROM. Pointers are welcome.
Where I looked
  • ethereum/ERCs and ethereum/EIPs, including open PRs
  • the EIP-8298 Magicians thread
  • Solady, OpenZeppelin, ZeroDev Kernel, Safe, Coinbase Smart Wallet, eth-infinitism, erc7579-implementation, Alchemy Modular Account, JARDIN and SPHINCs-

Failure modes (FM1–FM7)

Arrows point to the requirement (R) or guard check (P) that prevents each one.

  • FM1. Self-recovery locks the account out. Code that authorizes with ecrecover(...) == address(this) stops working once adopted, because EIP-8151 rejects addresses with code. A “fall back to the EOA key when no other key is set” design falls into this trap: it is code trying to keep a key the owner has just retired. (→ R1)
  • FM2. ERC-1271 passes, but VERIFY fails. Without a relay path (such as ERC-4337 validateUserOp), the account can initiate actions only as a frame-transaction sender, and that requires its code to APPROVE in VERIFY. A post-migration ERC-1271 check goes through a different entry point. It can pass while VERIFY is broken, and then the account can never act again, not even to replace its code. (→ R2, R3, simulation)
  • FM3. Adopted but not initialized. If initialization isn’t in the adopting transaction, the code has no key and can never approve anything, including its own initialization. If initialization is left open to anyone instead, it can be front-run. (→ R4, P3)
  • FM4. Receiving breaks. ETH transfers and ERC-721/1155 safe transfers now run the code, and they revert without a payable path and receiver hooks. This is repairable only if the code is replaceable. Senders that hard-code 21,000 gas can fail against almost any code. (→ R7)
  • FM5. The source can change. SETCODEFROM copies whatever code is at the source address when it runs, and a source can replace its own code. So the user signs a code hash, not an address. (→ R6, P2)
  • FM6. An empty ecrecover result proves nothing. An invalid signature and an EIP-8151 rejection give the same empty output. So the guard makes the same call twice: before adoption it must return the account, and after adoption it must return empty output. (→ P1, P4)
  • FM7. No EIP-8141, no way out. EIP-3607 rejects transactions from accounts with code. EIP-8298 does not require EIP-8141. On a chain with EIP-8298 but without EIP-8141, code without a relay path can never initiate anything again, and no on-chain check can detect the missing EIP. (→ wallet requirements)

Why an ERC, and what could go upstream

FM1, FM3, FM4, FM5 and FM7 are general guidance on using SETCODEFROM. I’d rather see them in EIP-8298’s Security Considerations. Draft text is in the first reply below, and I’m happy to open a PR. FM7 also stays here as a wallet requirement.

Three things need agreement between parties that don’t share code:

  • Signers need one Migration typed-data format they can recognize and display. It is the one signature that hands an address over irreversibly.
  • Wallets need one audited Migrator they can allowlist as a 7702 target, ideally at the same address on every chain.
  • Wallets and auditors need one conformance target to check a code hash against before the irreversible step: the profile plus a test suite.

Components

Part Role Status
Adoption profile (incl. verifier rules) Requirements for adopted code Normative
Migrator 7702 delegate that adopts and initializes in one call Normative behavior; canonical deployment recommended
MigrationCheck Guard for both routes (P1–P4), plus revocation Normative behavior; canonical deployment recommended
Reference kernel A minimal conforming account Non-normative

Terms

  • Old key: the EOA’s secp256k1 key.
  • Authority: anything the account registers that can approve, with a scope. In the reference kernel it is (ERC-7913 verifier, key, scope). v1 deliberately covers single keys only; composite authorities need their own proof format and will come as an extension.
  • Execution-capable: the authority’s scope lets it authorize the account’s own actions, through EIP-8141 execution approval or the equivalent on a relay path.
  • Registration proof: the key’s signature over an EIP-712 Registration digest. The digest binds the authority’s parameters, the chain and the account, but not the code version, so proofs survive replacement. Whether to drop the chain as well is Q10.
  • Relay path: an entry point through which a third party submits actions the account signed, such as ERC-4337 validateUserOp, so the account acts without sending transactions.

Adoption profile (draft requirements)

RFC 2119 keywords are used for precision, and everything is open for discussion. Tags: safety prevents an irreversible failure, mempool is for public-mempool use, interop lets others rely on the account. The only interface the profile defines is initialize(bytes).

Adopted code MUST:

  • R1. No self-recovery (safety, FM1).

    • Never authorize via secp256k1 recovery against the account’s own address, through either ecrecover or an EIP-8141 SECP256K1 entry naming the account.
    • Reject any authority it can recognize as satisfiable by the old key. Code can’t recognize every verifier, so wallets cover the rest.
  • R2. VERIFY approval (safety, FM2). Code that supports EIP-8141 MUST do the following before it calls APPROVE:

    • check that the frame mode is VERIFY, as ERC-8286 also requires, because APPROVE checks only the target and flags;
    • approve exactly the frame’s allowed scope, and only when one registered authority holds that whole scope and verifies a signature over the canonical signature hash.

    Code without VERIFY support MUST have a relay path instead. Whether the account can act on a given chain is a wallet check, not part of conformance (FM7).

  • R3. One verification (safety, FM2).

    • Every entry point that checks signatures (VERIFY, ERC-1271, registration proofs and any relay path) first builds its own digest and applies its own conditions: scope, domain, registration state and frame mode.
    • It then calls one shared verification of (authority, digest, signature), which gives the same result on every path for the same inputs.
    • Verifiers are called via STATICCALL on every path, including proof checks during initialization.
  • R4. Initialization (safety, FM3). initialize(bytes):

    • can be called only by the account itself;
    • reverts if the code hash recorded at the last initialization equals EXTCODEHASH(this), and otherwise runs and records the current hash. The record lives at a storage slot fixed by this ERC, so every conforming code reads the same one;
    • leaves the authority set exactly as it installed it, ignoring authority data already in storage (for example, from an earlier 7702 delegate);
    • verifies the registration proof of every execution-capable authority (R3), and reverts unless at least one exists.
  • R5. Authority changes (safety). Only by the account itself. Every execution-capable authority needs a verified proof, and at least one must always remain.

  • R6. Replacement (safety, FM5).

    • Only by the account itself.
    • The replacement call carries the expected code hash, and the signature that authorized it covers that hash.
    • The code compares the hash with EXTCODEHASH(source) immediately before SETCODEFROM, and with its own hash afterwards.
    • It then runs the new code’s initialize in the same call (R4), so replacement reverts unless the new code verifies the proof of an execution-capable authority. Replacing with the same code hash therefore always reverts.
    • SETCODEFROM is reachable only on this path, including through DELEGATECALL.
  • R7. Receiving (safety, FM4). Accept ETH with empty calldata, and ERC-721/1155 safe transfers.

  • R8. ERC-1271 (interop). Bind the hash to the account and chain before verifying, and accept only execution-capable authorities.

Adopted code SHOULD also receive ETH within the 2,300-gas stipend, and offer at least one configuration within EIP-8141’s MAX_VERIFY_GAS of 100,000.

Other SHOULDs
  • Declare via ERC-165 (interface ID TBD) whether the code is replaceable, and require an explicit freeze flag before replacing itself with code that isn’t.
  • Never CALL*/EXTCODE* its own address while in VERIFY (mempool). Replaceable code contains SETCODEFROM. EIP-8298 treats a contract containing it like a 7702-delegated account, and EIP-8141’s trace rules reject validation that reaches such an account.

Verifier rules. Verifiers called during validation, and any code they call or read, MUST be stateless and immutable. They MUST also accept only canonical encodings and reject empty keys. Under EIP-8151, ecrecover reads the recovered account’s code, so it is not stateless, and an ecrecover-based secp256k1 verifier therefore does not conform. The same problem affects inline ecrecover checks of an owner address, which are common in ERC-4337 accounts: if that address later adopts code, the authority stops verifying.

Verifier rules in full
  • Stateless (MUST). ERC-7913 only says SHOULD.
  • Immutable (MUST). No SETCODEFROM, SELFDESTRUCT, DELEGATECALL or CALLCODE, and not 7702-delegated. This covers helpers and key blobs too.
  • Canonical encodings (MUST). Reject trailing bytes, since ARBITRARY signature bytes are outside the signature hash. Reject empty keys.
  • Bytes at opcode positions (SHOULD). No 0xA7, 0xF4 or 0xF2 at an opcode position, including in compiler metadata.
    • A contract containing 0xA7 is treated by EIP-8298 like a 7702-delegated account, which EIP-8141’s trace rules reject.
    • Contracts containing 0xF4/0xF2 are tracked by code hash, with a cap on the pending transactions that depend on them.
    • The reference kernel’s verifiers avoid all three.
  • Gas (SHOULD). Publish worst-case gas.

Migration: guard and routes

D is the EIP-712 digest of Migration(source, codeHash, initDataHash, deadline, executor, check), where check names the MigrationCheck instance. ownerSig is the old key’s signature over D.

MigrationCheck reverts the migration unless all four checks hold in the same transaction:

Check What it shows
P1 ecrecover(D, ownerSig) succeeds and returns the account the old key signed this and still controlled the account
P2 EXTCODEHASH(account) == codeHash after adoption exactly the signed code was adopted
P3 initialize(initData) ran on the adopted code, and keccak256(initData) == initDataHash exactly the signed authorities were installed
P4 the same ecrecover call succeeds with empty output EIP-8151 rejects the old key on this chain

How begin tells the routes apart. MigrationCheck exposes begin and finish for both routes. begin looks at its caller’s code:

  • the delegation indicator for this instance’s Migrator means Route A;
  • empty code means Route B;
  • anything else reverts.

In both routes, begin checks p.check == address(this) (so an intent cannot run through another MigrationCheck deployment), the deadline, revocation and P1. It also requires executor == account in Route B, where the frame transaction is already signed by the old key. In Route A, Migrator checks msg.sender == p.executor instead.

P3 is enforced differently in each route:

  • Route A: Migrator enforces it, and binding MigrationCheck to one Migrator is what makes this part of the guard.
  • Route B: begin checks by frame introspection that initialize(initData) follows the set-code frame in the same atomic batch, and finish checks that it succeeded.
Typed data (draft)
EIP712Domain(string name, string version, uint256 chainId, address verifyingContract)
  name and version fixed by this ERC (TBD), verifyingContract = the account
Migration(address source, bytes32 codeHash, bytes32 initDataHash, uint256 deadline, address executor, address check)
Registration(address verifier, bytes32 keyHash, uint8 scope)   // for ERC-7913 authorities
  • Migration and Registration. This ERC would fix both, for ERC-7913 authorities.
  • initData. Its layout is implementation-defined in this draft. As a result, a signer cannot display what it installs (Q13).
P1/P4 details
  • Why two identical calls. An invalid signature already returns empty output, which Solidity reads as address(0). Since ethereum/EIPs#12540 (merged 2026-10-18), an EIP-8151 rejection returns the same empty output, with success = 1.
    • Before adoption, the call must return the account, which shows the signature is genuine.
    • Recovery is deterministic. So if the identical call afterwards returns empty output, the only thing that changed is the account’s code.
  • Delegated account during P1. In Route A, P1 runs while the account holds the 7702 delegation indicator. It passes only because EIP-8151 exempts delegation indicators. The exemption is intentional: a delegation keeps the key’s authority (see Key authority).
  • Older drafts. Earlier drafts of EIP-8151 returned 32 zero bytes, and EIP-8298’s Security Considerations still says so. Accepting that output as well would be sound, since P1 already showed the same input recovers.
  • Reading the output. Check returndatasize before reading memory, because with empty output the buffer still holds P1’s result.
  • Gas. Forward gas() in both P1 and P4. Under EIP-8151, a successful recovery costs 6,100 gas cold and 3,200 warm (EIP-8038 access costs), so a fixed 3,000 runs out of gas. Running out of gas gives success = 0, which never passes as a rejection.

Limits of the guard.

  • Inputs, not quality. The guard checks the user’s signed inputs, not the quality of the code. Conformance comes from choosing a code hash that has been checked against the profile and test suite.
  • What P3 shows. For conforming code, a successful P3 means every execution-capable key passed the same verification that ERC-1271 and VERIFY (or the relay path) use (R3, R4). Nothing on-chain runs the VERIFY wrapper itself; only fork simulation does.
  • Completion. Migrated(account, codeHash, D, route) is emitted only when all four checks pass. The submitting wallet should confirm by this event, not by transaction success, because if the 7702 authorization was skipped, migrate hits empty code and still “succeeds”.
More on the guard
  • Missing EIP-8151 is caught. On a chain without EIP-8151, P4 fails, so the migration is refused. A missing EIP-8141 (FM7), by contrast, cannot be detected.
  • The reference template is inert. The canonical reference kernel is deployed uninitialized. It has no authority and accepts initialization only from itself, so nobody can initialize or replace it, and its hash is stable in practice. FM5 matters mainly when the source is someone else’s live account.

Route A: EIP-7702 + Migrator (normative)

A single type-4 transaction authorizes the EOA → Migrator and calls migrate(p, initData, ownerSig), where p is the signed Migration struct. Migrator adopts and initializes the new code between MigrationCheck.begin and .finish. Migrator runs only for p.executor, and a signed intent can be revoked. Using the reference kernel afterwards requires EIP-8141 (FM7).

Route A details: executor, revocation, pseudo-code

Executor. Migrator requires msg.sender == p.executor.

  • Self-sent (executor = account). An intent runs only in a transaction the old key signs.
  • Sponsored. The sponsor can run any unexpired intent that names it, whenever the account is delegated to Migrator. The executor MUST NOT be a contract that forwards arbitrary calls, or EIP-8141’s ENTRY_POINT (0xaa), which is the caller of every DEFAULT frame.
    • Re-delegating away does not revoke an intent. Any later delegation to Migrator revives it, including a delegation left behind by a failed attempt.

Revocation.

  • Who. The account can still send ordinary transactions with the old key, so it can call MigrationCheck.revoke(D). That records (msg.sender, D), and begin rejects D once (account, D) is recorded. Nobody else can revoke an account’s intent.
  • Side effects. Migrator keeps no storage and calls nothing but MigrationCheck and the account’s own initialize. MigrationCheck is never on a validation path, so the revocation set has no mempool impact.
  • Races. A revocation can still lose a race with a sponsor who executes first, so deadlines SHOULD be short.
// SELF = Migrator's deployment address; CHECK = the MigrationCheck this Migrator is bound to
require address(this) != SELF                      // never run at Migrator's own address
require msg.sender == p.executor
require keccak256(initData) == p.initDataHash
CHECK.begin(p, ownerSig)                           // route A; p.check == CHECK; deadline; not revoked; P1
require EXTCODEHASH(p.source) == p.codeHash        // fail early
require SETCODEFROM(p.source) == 1
require address(this).call(initialize(initData))   // P3; the new code runs with msg.sender == account
CHECK.finish(p, ownerSig)                          // P2, P4; emits Migrated
  • Linking begin and finish. They are linked by transient storage keyed by (account, D).
  • Failure. On failure, the call reverts but the 7702 delegation stays. Migrator therefore exposes only migrate and receive hooks that make no outbound calls.
  • Already-delegated EOAs. For an EOA already delegated to a wallet’s delegate contract, the type-4 transaction replaces that delegation. The old delegate’s storage stays behind, and R4 ignores its authority data. If it happens to hold the new code’s hash in R4’s slot, initialize reverts and the whole migration reverts with it.

Route B: EIP-8141 default code (informative; needs ethereum/EIPs#12506)

Route B is a VERIFY frame followed by one atomic batch of four SENDER frames: begin, the set-code frame, initialize, and finish. Besides the Route B checks above, begin checks the frames after it by introspection, so that a revert in begin still skips the adoption. finish ties itself to the same batch, then checks P2 and P4.

Route B details

begin. It requires EXTCODEHASH(sender) == keccak256(""). Frame 0’s APPROVE has already bumped the nonce, so the result is never 0. An EOA with an active 7702 delegation must clear it first.

It then reads the next three frames with FRAMEPARAM and FRAMEDATACOPY. Together with begin itself, they must match rows 1–4 of the table below in mode, target, data and flags (‖ is byte concatenation), so the four frames form exactly one atomic batch.

Why forward. A revert unrolls only its own batch. If the set-code frame were outside finish’s batch, a revert in finish would leave the code adopted. When begin carries the flag, it shares a batch with the set-code frame, so a wrong structure reverts before the adoption and skips it. (Thanks to Daniil Ankushin for pointing this out.) Order matters too: initialize before the set-code frame would hit EIP-8141’s default code and still succeed.

finish. Transient storage doesn’t survive across frames, so finish checks that the frame three back is begin(p, ownerSig) on this instance, that it and the next two carry the flag, and that all three have status 1.

What remains. If begin lacks the flag and so does the set-code or initialize frame, no guard frame shares a batch with the adoption. The same holds for a set-code frame anywhere outside this batch: begin rejects a sender that already has code, but cannot unroll a frame it shares no batch with. Wallets must build the frames as in the table; simulation catches a mistake.

ownerSig. It is still needed, because the ECDSA bytes verified by the default code are not readable from the EVM.

# mode target data flags
0 VERIFY sender – 0x3 (APPROVE_EXECUTION_AND_PAYMENT)
1 SENDER MigrationCheck begin(p, ownerSig) atomic
2 SENDER sender 0x01 ‖ p.source ‖ p.codeHash atomic
3 SENDER sender initialize(initData) atomic
4 SENDER MigrationCheck finish(p, ownerSig) – (closes the batch)

Wallet requirements (proposed)

  • Initiation. Wallets MUST NOT sign Migration for an external origin, such as eth_signTypedData* from a dApp. Migration starts only from the wallet’s own UI.
  • Old key. Wallets MUST register authorities only under verifiers they recognize, and MUST check that the old key can satisfy none of them (R1).
  • Chains. Wallets MUST NOT adopt code on a chain where the code has no working way to act (FM7). VERIFY needs EIP-8141 on that chain, and a relay path needs its entry contract (for example, the ERC-4337 EntryPoint the code trusts) to be deployed there.
  • Simulation. Before submitting, wallets SHOULD simulate on a fork: first the whole migration, then an action signed with the new key (a frame transaction, or an operation on the relay path). This is the only test of the real VERIFY or relay path before the irreversible step.
Other wallet SHOULDs
  • Display. When signing, show the code hash and its implementation, every authority with its scope, the executor and the deadline.
  • Backup. Offer a second, separately held execution-capable authority. After migration, the old key is no longer a fallback.
  • Authorization. Sign the 7702 authorization for the target chain ID, never 0.
  • Deployments. Check the code hashes of Migrator and MigrationCheck on each chain.
  • Simulation caveats. The simulated frame transaction is really signed, so keep it off public networks. With few-time PQ schemes, it spends one signature.
  • Submission. Submit privately, because the transaction is signed with the old key (see Residual risks, Q-day).

Reference kernel

The kernel has no execute(): SENDER frames call their targets directly, which ERC-8286 calls the protocol-dispatched route. It also has no DELEGATECALL. Plugins are ERC-7913 verifiers called via STATICCALL, and larger changes adopt a new kernel. These are choices of the kernel, not requirements of the profile.

Kernel sketch, storage, PQ budget
// frame data: verify(uint8 authorityId, uint8 sigIndex), covered by the signature hash
assert FRAMEPARAM(cur, MODE) == VERIFY
assert SIGPARAM(sigIndex, SCHEME) == ARBITRARY
assert SIGPARAM(sigIndex, MSG) == 0                        // canonical tx signature hash
sig     = SIGDATACOPY(sigIndex)
allowed = FRAMEPARAM(cur, ALLOWED_SCOPE)
scope   = _authorize(authorityId, TXPARAM(SIG_HASH), sig)  // shared verification (R3)
assert allowed != 0 && (scope & allowed) == allowed
APPROVE(0, 0, allowed)
  • Storage. ERC-7201 namespaced. An active-id bitmap and the code hash recorded at initialization implement R4.
  • PQ budget. The 100k budget covers the whole validation prefix, not just the verifier. Two candidates to measure against it:
    • JARDIN’s compact path, at about 48K per the JARDIN note, or about 60K (an estimate) per its repository README;
    • SLH-DSA-Keccak-128-24, at about 94K for verification alone per the SPHINCs- repository README, which leaves little room.
  • Large PQ keys. They can live in a key-blob contract encoded as 0x00 ‖ (PUSH32 ‖ 32 bytes)*, so every byte is PUSH data. A raw 1,312-byte ML-DSA-44 key contains 0xA7 at an opcode position in about 80% of cases.

Relation to existing work

  • ERC-8286 standardizes modules for frame-transaction accounts. SETCODEFROM adoption and migration checks are outside its scope. I think a protocol-dispatched 8286 account could conform under R3; see Q3.
  • ethereum/ERCs#1695 (closed) is the closest prior work on standard 8141 verification code. It targets 7702/7819 delegation, where its fallback to the account’s own address (in its reference code) is correct. Adopted as regular code with no key set, that fallback would cause FM1.
  • ethereum/EIPs#12506 is Route B. This proposal is one candidate for what that route adopts.
More related work
  • ERC-8403 (ethereum/ERCs#1979) defines an account-authority lifecycle. It notes that no revoke removes a 7702-delegated EOA’s native key, and names two remedies: a new account, or adopting regular code at the same address with EIP-8298 and EIP-8151. This proposal covers the second.
  • JARDIN is a hash-based PQ account (a research prototype per its README). Its verifiers could be wrapped as ERC-7913 verifiers.
  • EIP-8425 (ethereum/EIPs#12383) installs a PQ wallet at the same address during recovery. The kernel could serve as that wallet. That path removes ecrecover, though, so P1 and P4 don’t apply.
  • ERC-8421 (ethereum/ERCs#2028) defines alternative mempools, the natural home for configurations over MAX_VERIFY_GAS.

Residual risks

  • Other chains. The old key stays valid wherever the account hasn’t migrated. A 7702 authorization with chain ID 0 is valid on every chain.
  • Off-chain and Solidity checks. EIP-8151 doesn’t reach off-chain verification (SIWE, order books) or secp256k1 code written in Solidity. Suggested practice for integrators: if an address holds code that is neither empty nor a 7702 delegation, use ERC-1271 instead of raw secp256k1 signatures. This works for every adoption route and needs no event.
  • EIP-8141 SECP256K1 entries. EIP-8151 doesn’t cover them. An entry naming a migrated account still validates with the old key, so any contract that trusts an entry’s resolved signer accepts it. That lasts until ethereum/EIPs#12505 (which rejects SECP256K1 and P256 entries whose signer has regular code), or an equivalent rule, is active on that chain. The kernel itself never accepts such entries (R1).
More residual risks
  • Q-day timing. The migration is signed with the old key, so this is a pre-Q-day procedure. Submit privately.
  • Admin rights. Any execution-capable authority can replace the code; v1 has no separate admin role.
  • Validation calling into the kernel. Another account’s validation may call into the kernel, for example to check its ERC-1271 signature. EIP-8141’s trace rules already reject that, because it reads storage outside tx.sender. EIP-8298’s rule would reject even a storage-free call, because the kernel’s code contains SETCODEFROM.
  • Sponsoring. When the kernel approves payment for another sender, it acts as a non-canonical paymaster, limited to one pending transaction in the public mempool. Its pay frame reads the kernel’s own storage. I read EIP-8141’s “gas account” rationale as allowing that, but the trace rules don’t say so explicitly.

Open questions

Corrections are especially welcome wherever I’ve misread EIP-8141, 8298 or 8151.

  • Q1. ERC or upstream? Is this split right? FM1, FM3, FM4, FM5 and FM7 would go to EIP-8298. The Migration type, Migrator and the conformance target would stay in an ERC.
  • Q2. EIP-8298’s scan and the sender’s own code. I read the scan as covering contracts reached via CALL*/EXTCODE*, not the sender’s own code running as the frame target. EIP-8298 says a migrated account “keeps transacting as the sender of EIP-8141 frame transactions” and can upgrade “by calling SETCODEFROM again”. Is my reading the intent? It is load-bearing: any account that replaces itself without DELEGATECALL contains SETCODEFROM.
  • Q3. ERC-8286 and existing accounts. Is R3 (one shared verification for every entry point) easy to meet for 8286 validators and for 4337/7579 relay paths? Would a view hook previewApprove(bytes32 hash, bytes sig) help show it from outside, or invite misuse as a check that isn’t bound to the account?

Q4–Q13, grouped by audience, are in the first reply below.

Next steps

  • PoC-1 on EELS (early November). Built on EIP-8298 + EIP-8151 (ethereum/execution-specs#3769, merged into the EIP-8298 branch, and #3860, still open), with a single-key kernel, Route A, Migrator and MigrationCheck.
    • Tests reproduce FM1 and FM3–FM6, and run the same migration with and without EIP-8151.
    • FM2 needs EIP-8141, so it comes with the 8141 integration.
  • Conformance suite and fork-simulation tool. A black-box suite that runs against any code hash.
  • Afterwards. EIP-8141 integration (FM2 tests, Route B), then a draft PR to ethereum/ERCs.

Draft text for EIP-8298 Security Considerations

Referenced in “Why an ERC, and what could go upstream” above.

Code adopted by an EOA for account migration becomes the only way for the account to act. With EIP-8151, it is also the only way to pass ecrecover for it. The adoption cannot be undone: the account can no longer originate ECDSA transactions, cannot be re-delegated under EIP-7702, and cannot return to empty code.

Such code should:

  • not authorize through secp256k1 recovery against the account’s own address, which EIP-8151 makes fail permanently;
  • initialize its authorization state in the same transaction as the adoption;
  • accept plain ETH transfers and implement the receiver hooks of the token standards it expects to hold.

On a chain without EIP-8141, such code should keep a relay path of its own, because the account can no longer send transactions.

A source whose code can reach SETCODEFROM can change its code hash. Migration code should therefore compare EXTCODEHASH(source) with the expected hash immediately before adopting.

More questions (Q4–Q13)

Continued from the open questions above.

For EIP-8141 authors

  • Q4. MAX_VERIFY_GAS. EIP-8141’s mempool rule 6 counts “the intrinsic cost of validating tx.signatures”. I assume this means only signature_verification_cost (100 per ARBITRARY entry), not signature calldata. Can you confirm?
  • Q5. P256 entries. Should protocol-validated P256 signature entries be an authority kind? They cost 6,700 gas and need no verifier call, so they leave the most room under 100k. But a contract sees only their resolved signer, and only inside the frame transaction that carries them, so ERC-1271 would need a separate path.
  • Q6. Stateless verifiers. Under EIP-8151, ecrecover reads the recovered account’s code. ecrecover-based verifiers are therefore no longer stateless, and EIP-8141’s trace and revalidation rules don’t track that read; the canonical paymaster’s signer check is one such case. Should EIP-8141’s mempool section say so? (ERC-7913 itself is Final.)
  • Q7. Large PQ keys. PUSH32 key blobs now, or wait for Public Key Aliases?

For wallet and account authors

  • Q8. ERC-1271 wrapping. Should v1 use a minimal AccountMessage(bytes32 hash) under the account’s domain, as Coinbase Smart Wallet does? Or ERC-7739, which is readable but heavier and still Draft?
  • Q9. Revocation. Are executor, per-account revoke(D) on the named MigrationCheck, and a short deadline enough?
  • Q10. Registration digests. Should they also exclude the chain ID, so that one slow PQ signature serves every chain?
  • Q11. The event. Is an on-chain completion event useful to anyone besides the migrating wallet?
  • Q12. Canonical addresses. Should Migrator and MigrationCheck stay specified by behavior, with a canonical deployment recommended, or be fixed as exact bytecode at fixed addresses?
  • Q13. What signers can see. initData is opaque to signers. Should the ERC fix an authority-list prefix for initData, or add an authoritiesHash to Migration, so that wallets can display what they install?