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.
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.
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.
Constant | Value |
---|---|
PRECOMPILE_ADDRESS |
0xTBD |
PRECOMPILE_GAS_COST |
5000 |
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:
0xef0100 || address
. The private key is active and can sign transactions.0xef0100 || address || 0x00
. The private key is deactivated and cannot sign transactions or EIP-7702 delegation authorizations.A new precompiled contract is introduced at address PRECOMPILE_ADDRESS
. It costs PRECOMPILE_GAS_COST
and executes the following logic:
STATICCALL
(i.e. in a read-only context).0xef0100
as per EIP-7702).0xef0100 || address
): Appends 0x00
to deactivate private key authorization.0xef0100 || address || 0x00
): Removes last byte 0x00
to activate private key authorization.For EOAs with delegated code (begins with the prefix 0xef0100
), the following validations MUST be performed:
24
bytes long). This ensures such transactions cannot be included in blocks.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.
Alternative methods for deactivating and reactivating EOA private keys include:
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 for the precompiled contract's validation logic and storage updates.
One 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.
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.
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.
This EIP maintains backwards compatibility with existing EOAs and contracts.
# 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=10000)
assert error == b""
assert state_db.get_code(caller) == active_code + b"\x00" # Deactivated state
assert gas_left == 10000 - PrecompiledContract.PRECOMPILE_GAS_COST
error, gas_left = precompile.execute(caller, state_db, gas=10000)
assert error == b""
assert state_db.get_code(caller) == active_code # Reactivated state
assert gas_left == 10000 - PrecompiledContract.PRECOMPILE_GAS_COST
# Test 2: Error cases
error, gas_left = precompile.execute(caller, state_db, gas=10000, read_only=True)
assert error == b"STATICCALL disallows state modification"
assert gas_left == 0
error, gas_left = precompile.execute(caller, state_db, gas=PrecompiledContract.PRECOMPILE_GAS_COST-1)
assert error == b"insufficient gas"
assert gas_left == 0
# EOA without delegated code
caller = "0x4567"
error, gas_left = precompile.execute(caller, state_db, gas=10000)
assert error == b"invalid delegated code prefix"
assert gas_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)
assert error == b"invalid delegated code prefix"
assert gas_left == 0
class PrecompiledContract:
DELEGATED_CODE_PREFIX = bytes.fromhex("ef0100")
PRECOMPILE_GAS_COST = 5000
def execute(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 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
if gas < self.PRECOMPILE_GAS_COST:
return b"insufficient gas", 0
# Check STATICCALL
if read_only:
return b"STATICCALL disallows state modification", 0
# Get and validate caller's code
code = state_db.get_code(caller)
if not code.startswith(self.DELEGATED_CODE_PREFIX):
return b"invalid delegated code prefix", 0
# Update delegated code based on length
if len(code) == 23: # Active state
state_db.set_code(caller, code + b"\x00") # Deactivate
elif len(code) == 24: # Deactivated state
state_db.set_code(caller, code[:-1]) # Activate
else: # Although this is not possible, it's added for completeness
return b"invalid delegated code length", 0
return b"", gas - self.PRECOMPILE_GAS_COST
class StateDB:
"""Simplified state database, omitting other account fields"""
def __init__(self):
self.accounts = {}
def get_code(self, addr):
return self.accounts.get(addr, {}).get("code", b"")
def set_code(self, addr, value):
if addr not in self.accounts:
self.accounts[addr] = {}
self.accounts[addr]["code"] = value
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.
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.
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 or 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.
Copyright and related rights waived via CC0.