--- eip: 8151 title: ECDSA Authority Deactivation Aware ecRecover description: Modify ecRecover precompile to return 32 zero bytes for ECDSA authorities deactivated per EIP-7851 author: Liyi Guo (@colinlyguo), Nicolas Consigny (@nconsigny) discussions-to: https://ethereum-magicians.org/t/eip-8151-ecdsa-authority-deactivation-aware-ecrecover/27690 status: Draft type: Standards Track category: Core created: 2026-02-09 requires: 7851 --- ## Abstract This EIP modifies the `ecRecover` precompile at address `0x0000000000000000000000000000000000000001` to respect [EIP-7851](./eip-7851.md) ECDSA authority deactivation. After performing ECDSA public key recovery, the precompile checks whether the recovered address has deactivated ECDSA authority. If so, it returns 32 zero bytes (the existing failure sentinel of `ecRecover`) instead of the recovered address. ## Motivation [EIP-7851](./eip-7851.md) enables delegated EOAs (per [EIP-7702](./eip-7702.md)) to deactivate their ECDSA authority, preventing that authority from signing transactions or new delegation authorizations. However, the `ecRecover` precompile is currently a pure cryptographic function with no awareness of account state. Even after an EOA's ECDSA authority is deactivated, on-chain signature verification through `ecRecover` continues to succeed for that authority. This affects contracts that rely on `ecRecover` for signature-based authorization, such as ERC-20 contracts implementing `permit` ([ERC-2612](./eip-2612.md)). Since many such contracts are immutable and cannot be updated to add deactivation checks, modifying `ecRecover` at the protocol level is a practical path. ## Specification | Constant | Value | |----------------------------|----------------------------------------------| | `ECRECOVER_ADDRESS` | `0x0000000000000000000000000000000000000001` | | `COLD_ACCOUNT_ACCESS_COST` | 2600 | | `WARM_ACCOUNT_ACCESS_COST` | 100 | ### Modified `ecRecover` Behavior Starting at the activation of this EIP, the `ecRecover` precompile at address `ECRECOVER_ADDRESS` must perform the following steps: 1. Perform ECDSA public key recovery from the input `(hash, v, r, s)` as currently specified, yielding a `recovered_address`. 2. If recovery fails, return 32 zero bytes and consume `3000` gas. 3. If recovery succeeds, read the account code of `recovered_address` and determine whether ECDSA authority is deactivated per [EIP-7851](./eip-7851.md). 4. If `recovered_address` has deactivated ECDSA authority, return 32 zero bytes. 5. Otherwise, return `recovered_address` left-padded to 32 bytes. ### Deactivation Check An address is considered to have deactivated ECDSA authority if and only if its account code is exactly `0xef0101 || delegate_address`, consistent with the ECDSA-disabled delegation prefix defined in [EIP-7851](./eip-7851.md): ```python code = state.get_code(recovered_address) is_deactivated = len(code) == 23 and code[:3] == bytes.fromhex("ef0101") ``` ### Gas Cost When ECDSA recovery fails, no state access is performed and the gas cost remains `3000`. When ECDSA recovery succeeds, the precompile must access the account of `recovered_address` to read its code. This access must follow the [EIP-2929](./eip-2929.md) warm/cold rules: - If `recovered_address` is already in the transaction's `accessed_addresses` set, the additional cost is `WARM_ACCOUNT_ACCESS_COST` (100). - Otherwise, the additional cost is `COLD_ACCOUNT_ACCESS_COST` (2600), and `recovered_address` must be added to the `accessed_addresses` set, making it warm for subsequent operations within the same transaction. ## Rationale ### Protocol-Level Modification Modifying `ecRecover` at the protocol level is chosen because many deployed contracts that rely on `ecRecover` for signature-based authorization (e.g., ERC-20 `permit` implementations) are immutable and cannot be upgraded to incorporate deactivation checks. A protocol-level change ensures these existing contracts automatically benefit from ECDSA authority deactivation without requiring redeployment. ### Returning 32 Zero Bytes When deactivated ECDSA authority is detected, the precompile returns 32 zero bytes rather than triggering an execution failure (`success = 0`). Currently, malformed `v`, out of range `r`/`s`, and failed recovery all return 32 zero bytes with `success = 1`, and `ecRecover` has never used execution failure to signal invalid input. Treating deactivated authority as another form of "recovery failed" keeps this convention intact. Furthermore, introducing an execution failure path would break deployed contracts that wrap `ecRecover` with a low level `staticcall` and `require(success)`, turning a "signature not valid" result into an unexpected revert. Contracts that already check for a zero return will naturally reject deactivated authorities without any code changes. ### EIP-2929 Gas Accounting Since the precompile now reads account state, the additional gas cost follows the existing [EIP-2929](./eip-2929.md) warm/cold access pattern for consistency with the rest of the protocol. This avoids introducing a new gas model and ensures that the cost of state access is fairly accounted for. ## Backwards Compatibility For addresses whose ECDSA authority has been deactivated under [EIP-7851](./eip-7851.md), `ecRecover` now returns 32 zero bytes where it previously returned the recovered address. On every successful ECDSA recovery, the precompile now performs an additional account access, adding either `WARM_ACCOUNT_ACCESS_COST` (100) or `COLD_ACCOUNT_ACCESS_COST` (2600) to the base cost of `3000`. Transactions that invoke `ecRecover` near their gas limit may fail with an out-of-gas error after activation. ## Reference Implementation ```python COLD_ACCOUNT_ACCESS_COST = 2600 WARM_ACCOUNT_ACCESS_COST = 100 BASE_GAS = 3000 def ecrecover_gas(state, recovered_address): if recovered_address is None: return BASE_GAS if recovered_address in state.accessed_addresses: return BASE_GAS + WARM_ACCOUNT_ACCESS_COST state.accessed_addresses.add(recovered_address) return BASE_GAS + COLD_ACCOUNT_ACCESS_COST ZERO_BYTES32 = b'\x00' * 32 DEACTIVATED_DELEGATION_PREFIX = b'\xef\x01\x01' DEACTIVATED_CODE_LEN = 23 # len(0xef0101 || delegate_address) def ecrecover(state, hash: bytes, v: int, r: int, s: int) -> bytes: # None means recovery failure recovered_address = ecdsa_recover(hash, v, r, s) if recovered_address is None: return ZERO_BYTES32 # Check EIP-7851 deactivation code = state.get_code(recovered_address) if len(code) == DEACTIVATED_CODE_LEN and code[:3] == DEACTIVATED_DELEGATION_PREFIX: return ZERO_BYTES32 return recovered_address.rjust(32, b'\x00') ``` ## Security Considerations ### Contracts Not Checking for Zero Return Contracts that use `ecrecover` but do not verify the result is non-zero are already vulnerable to accepting invalid signatures. This EIP maps deactivated authorities to the same failure sentinel (32 zero bytes) and therefore does not introduce a new class of vulnerability. ### Application-Level ECDSA Verification This EIP only modifies the `ecrecover` precompile. Contracts that perform ECDSA recovery in application-level code (e.g., pure Solidity implementations) bypass the precompile and will not observe deactivation. ### Cross-Domain / L2 Fault Proof Implications Some systems re-execute `ecrecover` in a different context (different chain, different layer, or asynchronously at a later time) and assume it is a pure function of `(hash, v, r, s)`. Because this EIP introduces a state read (the recovered address's account code), that assumption no longer holds. In particular, L2 fault proof systems that accelerate `ecrecover` by executing the L1 precompile and caching the result may become incorrect. Mitigations include removing such acceleration, or migrating to a pure recovery primitive (e.g., implementing secp256k1 recovery inside the fault-proof VM or via a new dedicated pure-recovery precompile in L1), so that the accelerated operation remains a pure function of its inputs. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).