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:
- An adoption profile. Requirements R1–R8, most tied to a failure mode. Existing accounts can be adapted to conform.
- A migration guard.
MigrationCheckreverts the migration unless four things hold in one transaction: the old key signed this migration, the signed code was adopted, the signed initialization ran, andecrecovernow rejects the old key. - 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 toAPPROVEinVERIFY. A post-migration ERC-1271 check goes through a different entry point. It can pass whileVERIFYis 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.
SETCODEFROMcopies 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
ecrecoverresult 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
Migrationtyped-data format they can recognize and display. It is the one signature that hands an address over irreversibly. - Wallets need one audited
Migratorthey 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
Registrationdigest. 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
ecrecoveror an EIP-8141SECP256K1entry 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.
- Never authorize via secp256k1 recovery against the account’s own address, through either
-
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, becauseAPPROVEchecks 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
VERIFYsupport MUST have a relay path instead. Whether the account can act on a given chain is a wallet check, not part of conformance (FM7). - check that the frame mode is
-
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
STATICCALLon every path, including proof checks during initialization.
- Every entry point that checks signatures (
-
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 beforeSETCODEFROM, and with its own hash afterwards. - It then runs the new code’s
initializein 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. SETCODEFROMis reachable only on this path, including throughDELEGATECALL.
-
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 inVERIFY(mempool). Replaceable code containsSETCODEFROM. 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,DELEGATECALLorCALLCODE, and not 7702-delegated. This covers helpers and key blobs too. - Canonical encodings (MUST). Reject trailing bytes, since
ARBITRARYsignature bytes are outside the signature hash. Reject empty keys. - Bytes at opcode positions (SHOULD). No
0xA7,0xF4or0xF2at an opcode position, including in compiler metadata.- A contract containing
0xA7is treated by EIP-8298 like a 7702-delegated account, which EIP-8141’s trace rules reject. - Contracts containing
0xF4/0xF2are tracked by code hash, with a cap on the pending transactions that depend on them. - The reference kernel’s verifiers avoid all three.
- A contract containing
- 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
Migratormeans 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:
Migratorenforces it, and bindingMigrationCheckto oneMigratoris what makes this part of the guard. - Route B:
beginchecks by frame introspection thatinitialize(initData)follows the set-code frame in the same atomic batch, andfinishchecks 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
MigrationandRegistration. 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, withsuccess = 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
returndatasizebefore 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 givessuccess = 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 theVERIFYwrapper 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,migratehits 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’sENTRY_POINT(0xaa), which is the caller of everyDEFAULTframe.- Re-delegating away does not revoke an intent. Any later delegation to
Migratorrevives it, including a delegation left behind by a failed attempt.
- Re-delegating away does not revoke an intent. Any later delegation to
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), andbeginrejectsDonce(account, D)is recorded. Nobody else can revoke an account’s intent. - Side effects.
Migratorkeeps no storage and calls nothing butMigrationCheckand the account’s owninitialize.MigrationCheckis 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
beginandfinish. They are linked by transient storage keyed by(account, D). - Failure. On failure, the call reverts but the 7702 delegation stays.
Migratortherefore exposes onlymigrateand 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,
initializereverts 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
Migrationfor an external origin, such aseth_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).
VERIFYneeds 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
VERIFYor 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
MigratorandMigrationCheckon 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 contains0xA7at an opcode position in about 80% of cases.
Relation to existing work
- ERC-8286 standardizes modules for frame-transaction accounts.
SETCODEFROMadoption 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
SECP256K1entries. 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 rejectsSECP256K1andP256entries 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 containsSETCODEFROM. - 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
payframe 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
Migrationtype,Migratorand 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 callingSETCODEFROMagain”. Is my reading the intent? It is load-bearing: any account that replaces itself withoutDELEGATECALLcontainsSETCODEFROM. - 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,
MigratorandMigrationCheck.- 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.