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
5000 (tentative)
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
For EOAs with delegated code (begins with the prefix 0xef0100), the following validations MUST be performed:
During the consensus rule’s transaction validity check before execution: transactions signed by the private key MUST be rejected if the delegated code indicates the private key is in the deactivated state (i.e., 24 bytes long). This ensures such transactions cannot be included in blocks.
The transaction pool MUST implement the same validation to prevent invalid transactions from propagating across the network.
Any EIP-7702 authorization from an authority with a deactivated delegated code MUST be considered invalid and skipped. The gas consumption rules remain unchanged, consistent with EIP-7702.
Gas Cost
The PER_EMPTY_ACCOUNT_COST and PER_AUTH_BASE_COST constants defined in EIP-7702 remain unchanged, since account code will be loaded during the authorization validation. This EIP only adds a code length check, which is a small overhead compared to existing logic.
Rationale
Using a precompiled contract
Alternative methods for implementing this feature 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 regular smart contract: A regular deployed contract could track the deactivated status of each address. However, invoking this contract, which executes bytecode and accesses storage, during deactivation and reactivation, would be more expensive than using a precompiled contract. Additionally, this approach “leaks” the design of a (widely used) programming language into the Ethereum core protocol. While it poses no obvious security risks, it is not ideal from a design perspective.
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.
5000 Gas PRECOMPILE_GAS_COST
The 5000 gas cost is sufficient to cover validation, computation, and storage updates for the delegated code.
Alternative EOA deprecation approach
One alternative deprecation approach involves using a hard fork to edit all existing and new EOAs to upgradeable smart contracts, which utilize the original EOA private key for authorization. Users can then upgrade these smart contracts to achieve more granular permission control. However, this approach is incompatible with EOAs already delegated to smart contracts, as it will overwrite the existing smart contract implementations. The EIP aims to fill this migration gap.
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 (e.g. to 0xef01ff) makes it unclear which prefix to restore upon reactivation.
Avoiding account state changes
Another alternative is to add a bool field deactivated 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.
Forwards compatibility for removing EOAs
After all existing and future EOAs have been migrated to smart contracts. It’s natural and also easy to deprecate this EIP with a single upgrade, which involves some clean-ups:
Removing the precompiled contract.
Removing all validation logic of the deactivation status since all EOAs are smart contracts.
Removing the appended 0x00 byte from the delegated code of deactivated EOAs, which this EIP introduces.
Backwards Compatibility
This EIP maintains backwards compatibility with existing EOAs and contracts.
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_addrstate_db.set_code(caller,active_code)error,gas_left=precompile.execute(caller,state_db,gas=10000)asserterror==b""assertstate_db.get_code(caller)==active_code+b"\x00"# Deactivated
assertgas_left==10000-PrecompiledContract.GAS_COSTerror,gas_left=precompile.execute(caller,state_db,gas=10000)asserterror==b""assertstate_db.get_code(caller)==active_code# Activated
assertgas_left==10000-PrecompiledContract.GAS_COST# Test 2: Error cases
error,gas_left=precompile.execute(caller,state_db,gas=10000,static=True)asserterror==b"cannot call in static context"assertgas_left==0error,gas_left=precompile.execute(caller,state_db,gas=PrecompiledContract.GAS_COST-1)asserterror==b"insufficient gas"assertgas_left==0# EOA without delegated code
caller="0x4567"error,gas_left=precompile.execute(caller,state_db,gas=10000)asserterror==b"invalid delegated code prefix"assertgas_left==0# Small contract code
caller="0x89ab"state_db.set_code(caller,bytes.fromhex("00"))# a fake contract
error,gas_left=precompile.execute(caller,state_db,gas=10000)asserterror==b"invalid delegated code prefix"assertgas_left==0
Reference Implementation
classPrecompiledContract:DELEGATED_CODE_PREFIX=bytes.fromhex("ef0100")# EIP-7702 prefix
GAS_COST=5000# PRECOMPILE_GAS_COST
defexecute(self,caller,state_db,gas,static=False):"""
Toggle EOA's private key authorization between active/deactivated states.
Parameters:
- caller: The address calling the contract
- state_db: The state database
- gas: Gas provided for execution
- static: Whether called in 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.GAS_COST:returnb"insufficient gas",0# Check static call
ifstatic:returnb"cannot call in static context",0# Get and validate caller's code
code=state_db.get_code(caller)ifnotcode.startswith(self.DELEGATED_CODE_PREFIX):returnb"invalid delegated code prefix",0# Update 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 code length",0returnb"",gas-self.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
Additional checks during transaction validation
The deactivation status is determined by checking the length of the delegated code. This check introduces an additional storage read and check comparable to account state read and checks, such as nonce validation. It is integrated into the transaction validity check of the transaction pool and consensus rules, ensuring that invalid transactions are filtered out before propagation and execution.
In applying authorizations of an EIP-7702 transaction, an additional check is introduced to validate the length of the delegated code. Since EIP-7702 already requires retrieving the code in authorization validation, the extra cost of verifying its length is negligible.
Risk of asset freezing
In the worst case, a malicious wallet, through bypassing permission controls, could steal assets, and deactivate the account by calling the precompiled contract, it could also block reactivation, and thus freeze the account. In this case, the risk is inherent to delegating control and not solely introduced by this EIP.
The risk also exists when the delegated wallet does not support reactivation or implements a flawed reactivation interface, combined with partially functional or non-functional asset transfers. These issues could prevent the user from reactivating the account and result in partial or complete asset freezing. Users can mitigate these risks using thoroughly audited wallets that support this EIP.
This EIP does not revoke ERC-2612 permissions. EOAs with deactivated private keys can still authorize transfers by calling the permit function of ERC-20 tokens supporting ERC-2612. This issue also exists with EIP-7702. To support deactivated EOAs, ERC-20 contracts should deprecate the permit function, or a new opcode could be introduced to help check the deactivated status of the EOA.
Message replay across EVM-compatible chains
For deactivation through EOA-signed transactions, the replay protection mechanism provided by EIP-155, if enabled, can effectively prevent cross-chain message replay.
For deactivation/reactivation called by the delegated contract, the contract should ensure that the chain ID is part of the message validation process (or implement alternative replay protection mechanisms) to prevent cross-chain message replay.