EIP-8151 ecRecover restricted by account code (Account Code Restricted ecRecover)

Hegota Considered for Inclusion · Security & privacy · EIP status Draft · created 2026-02-09

The ecRecover precompile, which recovers an address from a signature, checks the code of the recovered account. It returns the address only when the code is empty or is an EIP-7702 delegation designator, and otherwise returns the same empty output as a failed signature.

Why it is needed

An externally owned account (EOA) can use EIP-7702 as a stepping stone to move to a post-quantum signature scheme. After the move, the account code differs from the EIP-7702 delegation format, so under the EIP-3607 rule the authority to send transactions with the old ECDSA private key is blocked at the protocol level. However, the ecRecover precompile, which recovers an address from a signature, does not look at account state. This means an attacker who has derived the old private key with a quantum computer could move tokens through contract signature features such as ERC-20 permit (ERC-2612). Most such contracts are already deployed and cannot be fixed. So the same restriction is placed on ecRecover itself, so that the old key of a migrated account can no longer be used for contract authentication either.

What changes

Today

After EIP-8151

Who is affected

In depth

Modified ecRecover execution order

After activation, ecRecover at address 0x0000000000000000000000000000000000000001 behaves in the following order. (1) It consumes 3000 gas and aborts with out of gas if gas is insufficient. (2) It recovers the ECDSA public key from the input (hash, v, r, s) as it does today, and returns empty output if recovery fails. (3) If recovery succeeds, it computes the code check cost under EIP-8038. Whether the account is cold or warm is decided from accessed_addresses before the account is read. This is the pre-state gas check of EIP-7928. If recovered_address is not in accessed_addresses, the cost is COLD_ACCOUNT_ACCESS (3000). If it is, the cost is WARM_ACCESS (100). One more WARM_ACCESS (100) is added as the code lookup fee. This fee is not waived even if the code is empty or the account is rejected by the later check. (4) It consumes that cost and aborts with out of gas if gas is insufficient, in which case no account access occurs. (5) It adds recovered_address to accessed_addresses, records the account read, and then reads the raw code. (6) If the code is empty or is exactly an EIP-7702 delegation indicator, it returns recovered_address left-padded to 32 bytes. (7) Otherwise it returns empty output. Both a recovery failure and a code check rejection yield success = 1 with empty output.

Account code check conditions

The check looks at the raw account code stored in the state of recovered_address as is. It does not follow the delegation indicator to read the target contract's code. A nonexistent account is treated as having empty code. The only allowed cases are len(code) == 0, or len(code) == 23 with code[:3] == 0xef0100. That is, the recovered address is returned only when the code is empty or exactly of the form 0xef0100 || delegate_address. The constants in the reference implementation are DELEGATION_PREFIX = 0xef0100 and DELEGATION_CODE_LEN = 23. An account that has migrated to a post-quantum scheme and whose code has changed to another form does not pass this check.

Gas cost

The base gas of 3000 is charged once before recovery. If recovery fails, it ends there with no state access and no additional cost. If recovery succeeds, the raw code must be read, so, in the same way as EXTCODESIZE and EXTCODECOPY, the code lookup WARM_ACCESS (100) is added to either COLD_ACCOUNT_ACCESS (3000) or WARM_ACCESS (100) from EIP-8038. The code check cost is 3100 cold and 200 warm, and the total cost is 6100 cold and 3200 warm. The account is already read before the check knows whether the code is empty or the account is to be rejected, so the fee is not waived in either outcome. This cost is charged before the address is warmed, recorded or read. If gas is insufficient, execution aborts with out of gas and no account access is added. Only after the cost is paid is the address added to accessed_addresses, and it stays warm for later operations unless the enclosing scope is reverted.

Block access list (EIP-7928) recording

When EIP-7928 is active, what the code check cost pays for is the account read of recovered_address. The read is recorded only after the cost is paid, and a code check that runs out of gas leaves no account access. The target of the read is the raw code of recovered_address, and the delegation target is not loaded. If the account has no other changes, it is included with an empty change list. Even if the code check rejects the account, the read record remains, and it remains even if the enclosing scope is reverted later. A reverted scope restores accessed_addresses to its contents at the time the scope was entered, and accesses that already existed are kept. The precompile only reads, so an account with no other changes has an empty change list.

Why at the protocol level, and why empty output

Many contracts that rely on ecRecover, such as ERC-20 permit, are immutable and cannot be fixed to check the code of the recovered account. Changing the precompile applies the same restriction automatically without redeployment. The reason for making the rejection result empty output is to match existing behavior. Today's ecRecover returns empty output with RETURNDATASIZE == 0 when v is invalid, when r or s is out of range, or when recovery fails. Returning 32 zero bytes for a rejected account would change this result. A caller that compares mload(returndatasize()) with an authenticated address would accept 32 zero bytes when that address is the zero address, and a caller that used to reject the signature because RETURNDATASIZE was 0 would accept the same 32 bytes. Empty output puts a rejected account on the same path as a recovery failure. Treating it as an execution failure (success = 0) was also ruled out. This is because a deployed contract that wraps ecRecover in a low-level staticcall and applies require(success) would see an invalid signature turn into a revert.

Backward compatibility and fixed-gas calls

For an address whose raw code is neither empty nor exactly an EIP-7702 delegation indicator, ecRecover returns empty output where it previously returned the recovered address. A recovery failure is still empty output, and its cost stays at 3000. Every time recovery succeeds, 3100 cold or 200 warm is added on top of the base 3000, for a total of 6100 or 3200. A transaction that called ecRecover close to its gas limit may run out of gas after activation. In particular, a call that passes a fixed 3000 gas, such as staticcall(3000, 1, ...), always runs out of gas whenever recovery succeeds, even if the signature is valid, and raising the gas of the whole transaction does not fix it. When EIP-7928 is active, a paid code check records the recovered address in the block access list, and that record remains even if the check rejects the address or a later scope is reverted.

Security considerations

An account rejected by the code check receives the same empty output as an invalid signature, so a contract that treats that result as a failure behaves as it does today. A caller that does not properly reject a recovery failure may accept an invalid signature. This EIP changes only the ecrecover precompile. A contract that performs ECDSA recovery directly in application code, such as a pure Solidity implementation, does not go through the precompile and so is not subject to the account code restriction. On other chains, other layers, or in systems that later re-run ecrecover asynchronously and assume it is a pure function of (hash, v, r, s) alone, that assumption breaks. This is because it reads the current raw code of the recovered account. An L2 fault proof system that runs the L1 precompile to obtain the final result can be wrong when L1 and L2 state differ. An L2 that adopts this EIP must, after recovering the address, apply the code check and the EIP-8038 access cost using its own raw account code and transaction access state. A system that needs recovery independent of account state must use a separate secp256k1 recovery implementation instead of this precompile.

Current status and fork discussion

The status of the source is Draft and it was created on 2026-02-09. The specification, gas figures, and block access list rules may still change. In core developer calls, it was only discussed at 09.16 AMA #15, and no decision was made. At 10.08 ACDE #247, EIP-8151 (ecRecover restriction) and EIP-8298 (SETCODEFROM) were bundled as a pair and moved to CFI (Considered for Inclusion) for the Hegotá fork. Together they open a path for ordinary accounts to migrate to post-quantum schemes without relying on ECDSA. At 10.22 ACDE #248, the scope of bogota-devnet-2 was fixed to four EIPs, EIP-8253, EIP-4758, EIP-8298 and EIP-8151, and it was agreed to ship it the following week with three clients: Nethermind, Geth and ethrex. It is a testnet for running Hegotá candidate EIPs on a real network. The call records contain no final decision on inclusion in the fork.

Original EIP · ← EIPs