This EIP introduces a precompiled contract that enables Externally Owned Accounts (EOAs) with delegated control to smart contracts via EIP-7702 to deactivate or reactivate their private keys. This design does not require additional storage fields or account state changes. By leveraging delegated code, reactivation can be performed securely through mechanisms such as social recovery.
Motivation
EIP-7702 enables EOAs to gain smart contract capabilities, but the private key of the EOA still retains full control over the account.
With this EIP, EOAs can fully migrate to smart contract wallets, while retaining private key recovery options with reactivation. The flexible deactivate and reactivate design also paves the way for native account abstraction. e.g., EIP-7701.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Parameters
Constant
Value
PRECOMPILE_ADDRESS
0xTBD
PRECOMPILE_GAS_COST
13000
Definitions
|| is the byte/byte-array concatenation operator.
Delegated Code Encoding
The deactivation status is encoded by appending or removing the 0x00 byte at the end of the delegated code. The transitions between two states are as follows:
Active state: 0xef0100 || address. The private key is active and can sign transactions.
Deactivated state: 0xef0100 || address || 0x00. The private key is deactivated and cannot sign transactions or EIP-7702 delegation authorizations.
Precompiled Contract
A new precompiled contract is introduced at address PRECOMPILE_ADDRESS. It costs PRECOMPILE_GAS_COST and executes the following logic:
Returns a precompile contract error, consumes all gas, and no state changes are made if:
Gas is insufficient.
Called via STATICCALL (i.e., in a read-only context).
Caller is not an EOA with delegated code (prefix 0xef0100 as per EIP-7702).
24 bytes (0xef0100 || address || 0x00): Removes last byte 0x00 to activate private key authorization.
Saves updated code as caller’s new account code.
Transaction Validation
Validation to check whether a private key is deactivated (identified by a code prefix 0xef0100 and a code length of 24) MUST be performed in the following scenarios:
During transaction validity checks before execution, nodes MUST reject transactions signed with deactivated private keys to ensure such transactions cannot be included in blocks.
The same validation MUST be applied to the transaction pool when receiving new transactions to prevent invalid transactions from propagating through the network.
Any EIP-7702 authorization issued by an authority with a deactivated private key MUST be treated as invalid and skipped.
Rationale
Cost of Precompiled Contract
The PRECOMPILE_GAS_COST represents the gas required to validate and potentially update an account’s code. A fair cost for calling this precompiled contract can be determined by analyzing its impact on the node:
Reading the code of the address: 2600.
Changing the code hash (from non-zero to non-zero): 5000.
Deploying at most 24 bytes of code: 200 * 24 = 4800.
The primary operation consumes a total of 12400 gas. To account for additional overhead, such as context switching during state transitions, as well as code prefix and length validations, the cost is rounded up to 13000.
Additional Transaction Validation Overhead
Due to EIP-3607 and EIP-7702, nodes already load the account code during transaction validation to verify whether the sender is an EOA (empty code or code with prefix 0xef0100). This EIP introduces only a code length check after loading the code, resulting in minimal additional overhead. Similarly, EIP-7702’s authorization validation already involves retrieving the account code, with this EIP adding only a code length check, which is negligible compared to code reading.
Transaction pools already perform state-based checks, such as those for nonce and balance. This EIP adds an account code read and a length check, which together are comparable to nonce and balance validations. DoS prevention mechanisms, expected in transaction pools, also support these additional checks.
An Alternative EOA Deprecation Approach
An alternative deprecation approach involves using a hard fork to edit all existing and new EOAs to pre-written upgradeable smart contracts, which utilize the original EOA private key for authorization. Users can add and replace keys, or upgrade the smart contracts to other implementations. However, this approach is incompatible with EOAs already delegated to smart contracts, as it will overwrite the existing smart contract implementations. This EIP aims to fill this migration gap.
Using a Precompiled Contract
Alternative methods for deactivating and reactivating EOA private keys include:
Adding a new transaction type: Introducing a new transaction type could provide a mechanism to deactivate and reactivate EOA private keys. However, reactivating the private key would rely on a delegated contract as the authorizer, which complicates defining the rules for the new transaction type.
Deploying a smart contract: Compared to reusing the code field, using a smart contract has higher costs: (i) it requires new contract storage slots to track the deactivation status of each address (ii) executing bytecode increases the overhead of the node compared to precompiled contracts, (iii) during transaction validation, reusing the code field allows deactivation status check to be combined with existing code loading in some scenarios, thereby reducing the need for additional storage reads, as discussed in Additional Transaction Validation Overhead section.
In-Protocol Reactivation
This approach ensures maximum compatibility with future migrations. EOAs can reactivate their private keys, delegate their accounts to an EIP-7701 contract, and then deactivate their private keys again. This avoids the limitations of contract upgrades. e.g., to remove legacy proxy contracts (reducing gas overhead) when upgrading to EOF contracts, one can reactivate the EOA and redelegate it to an EOF proxy contract.
Reactivation can only be performed by the delegated contract. Since reactivating the private key grants full control over the wallet, including the ability to replace the delegated wallet, wallets must implement this interface with strict security measures. These measures should treat reactivation as the highest level of authority, equivalent to full ownership of the wallet.
The reactivation process is recommended to include a signed authorization from the private key. This signature payload should consist of the chain ID and a random challenge (e.g., a hash value of other signatures) to ensure that: (i) the request is non-replayable in other chains, and (ii) the the private key owner is also included in the reactivation process.
Users should delegate their EOAs only to wallets that have been thoroughly audited and follow best practices for security.
Avoiding Delegated Code Prefix Modification
This EIP appends a byte (0x00) to the delegated code instead of modifying the prefix (0xef0100) of EIP-7702 to ensure forward compatibility. If new prefixes such as 0xef0101 are introduced in the future, changing the prefix to represent the deactivated status (e.g., 0xef01ff) makes it unclear which prefix to restore (0xef0100 or 0xef0101) upon reactivation.
Avoiding Account State Changes
An alternative is to add a deactivated field in the account state to track the status. However, this approach will introduce backward compatibility logic and more test vectors related to this optional field when enabling this EIP, because the field is not present in existing accounts.
Backwards Compatibility
When the private key is deactivated, this EIP introduces: (i) an extra byte (0x00) to be appended to the end of the delegated code, and (ii) the delegated code length becomes 24 bytes (e.g., EXTCODESIZE would return 24).
These changes are not breaking. However, they require protocol, application, and contract implementations to use strict offsets to parse the delegated address correctly. Implementations must also check the delegated code prefix 0xef01 to determine whether it represents an EOA with delegated code, while avoiding over-restrictive checks, such as asserting the code length to be exactly 23.
Test Cases
# Initialize the state database and precompiled contract
state_db=StateDB()precompile=PrecompiledContract()# Test 1: Valid activation and deactivation
caller="0x0123"delegated_addr=bytes.fromhex("1122334455667788990011223344556677889900")active_code=PrecompiledContract.DELEGATED_CODE_PREFIX+delegated_addr# Active state
state_db.set_code(caller,active_code)error,gas_left=precompile.execute(caller,state_db,gas=15000)asserterror==b""assertstate_db.get_code(caller)==active_code+b"\x00"# Deactivated state
assertgas_left==15000-PrecompiledContract.PRECOMPILE_GAS_COSTerror,gas_left=precompile.execute(caller,state_db,gas=15000)asserterror==b""assertstate_db.get_code(caller)==active_code# Turns to the active state again
assertgas_left==15000-PrecompiledContract.PRECOMPILE_GAS_COST# Test 2: Error cases
error,gas_left=precompile.execute(caller,state_db,gas=15000,read_only=True)asserterror==b"STATICCALL disallows state modification"assertgas_left==0error,gas_left=precompile.execute(caller,state_db,gas=PrecompiledContract.PRECOMPILE_GAS_COST-1)asserterror==b"insufficient gas"assertgas_left==0caller="0x4567"# EOA that is not delegated to code
error,gas_left=precompile.execute(caller,state_db,gas=15000)asserterror==b"the address is not an EOA delegated to code"assertgas_left==0caller="0x89ab"state_db.set_code(caller,bytes.fromhex("00"))# Not a delegated code prefix
error,gas_left=precompile.execute(caller,state_db,gas=15000)asserterror==b"the address is not an EOA delegated to code"assertgas_left==0
Reference Implementation
classPrecompiledContract:DELEGATED_CODE_PREFIX=bytes.fromhex("ef0100")PRECOMPILE_GAS_COST=13000defexecute(self,caller,state_db,gas,read_only=False):"""
Switch the private key state of delegated EOAs between active and deactivated.
Parameters:
- caller: The address calling the contract
- state_db: The state database
- gas: Gas provided for execution
- read_only: Whether called in a read-only context
Returns:
- Tuple of (result, gas_left)
result: error bytes on failure, empty bytes on success
gas_left: remaining gas, 0 on error
"""# Check gas
ifgas<self.PRECOMPILE_GAS_COST:returnb"insufficient gas",0# Check STATICCALL
ifread_only:returnb"STATICCALL disallows state modification",0# Get and validate caller's code
code=state_db.get_code(caller)ifnotcode.startswith(self.DELEGATED_CODE_PREFIX):returnb"the address is not an EOA delegated to code",0# Update delegated code based on length
iflen(code)==23:# Active state
state_db.set_code(caller,code+b"\x00")# Deactivate
eliflen(code)==24:# Deactivated state
state_db.set_code(caller,code[:-1])# Activate
else:# Although this is not possible, it's added for completeness
returnb"invalid delegated code length",0returnb"",gas-self.PRECOMPILE_GAS_COSTclassStateDB:"""Simplified state database, omitting other account fields"""def__init__(self):self.accounts={}defget_code(self,addr):returnself.accounts.get(addr,{}).get("code",b"")defset_code(self,addr,value):ifaddrnotinself.accounts:self.accounts[addr]={}self.accounts[addr]["code"]=value
Security Considerations
Contracts Using ECDSA Secp256k1 Signatures
Contracts that have already been deployed and use ECDSA secp256k1 signatures outside of transaction signatures (e.g., ERC-20 tokens that support ERC-2612, the permit function) will not be able to verify the deactivated status of the EOA. This means that signatures signed by private keys will remain valid in these functions.
To handle deactivated EOAs, new or upgradeable contracts can check the signing address’s deactivation status, for example:
Use EXTCODESIZE to get the account’s code length.
If the code length is 24 bytes, use EXTCODECOPY to verify the code starts with 0xef0100 (delegated code prefix).
If both conditions are satisfied (i.e., the code size is 24 bytes and the code starts with 0xef0100), the private key is confirmed to be deactivated, and the signature should be rejected.
For non-upgradeable contracts, the above method cannot be applied directly. Another potential solution, at the protocol level, is to modify ecRecover precompiled contract: if the private key of the recovered address is deactivated, the ecRecover precompiled contract could return a precompile contract error (or, if not adding an error return path, return a zero address or a collision-resistant address like 0x1). However, this approach also has limitations, as it cannot cover cases where contracts implement their own signature verification logic without relying on ecRecover.
Irreversible Deactivation
Delegating to a wallet that lacks reactivation support (e.g., by calling the precompiled contract through an appropriate interface) may result in irreversible deactivation. To mitigate this risk, users should only delegate their EOAs to implementations that have been thoroughly audited and explicitly support this EIP.
Deactivation and Reactivation Replay
Replay attacks can occur in two scenarios: (i) replaying the same authorization multiple times on a single chain, or (ii) replaying the authorization across different chains.
For deactivation through EOA-signed transactions, the use of nonces in transactions ensures the same message cannot be replayed multiple times on the same chain. Additionally, the replay protection mechanism provided by EIP-155, if enabled, effectively prevents cross-chain message replay.
For deactivation or reactivation via delegated contracts, the contract should implement robust replay protection mechanism(s) (e.g., adding a customized nonce and the chain ID to the signed message) to safeguard against replay attacks both within the same chain and across different chains. Especially when the EOA has been (or will be) delegated to the same implementation on multiple chains.