diff --git a/EIPS/eip-7851.md b/EIPS/eip-7851.md new file mode 100644 index 0000000000000..922f4b4ff265e --- /dev/null +++ b/EIPS/eip-7851.md @@ -0,0 +1,247 @@ +--- +eip: 7851 +title: Deactivate/Reactivate a Delegated EOA's Key +description: Introduce a new precompiled contract for EOAs with delegated code to deactivate or reactivate private keys. +author: Liyi Guo (@colinlyguo) +discussions-to: https://ethereum-magicians.org/t/eip-7851-eoa-private-key-deactivation-reactivation/22344 +status: Draft +type: Standards Track +category: Core +created: 2024-12-27 +requires: 7702 +--- + +## Abstract + +This EIP introduces a precompiled contract that enables Externally Owned Accounts (EOAs) with delegated control to smart contracts via [EIP-7702](./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](./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](./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](./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](./eip-7702)). +- Updates caller's delegated code based on length: + - 23 bytes (`0xef0100 || address`): Appends `0x00` to deactivate private key authorization. + - 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](./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](./eip-7702). + +### Gas Cost + +The `PER_EMPTY_ACCOUNT_COST` and `PER_AUTH_BASE_COST` constants defined in [EIP-7702](./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](./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](./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 + +```python +# 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 + +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 +assert gas_left == 10000 - PrecompiledContract.GAS_COST + +error, gas_left = precompile.execute(caller, state_db, gas=10000) +assert error == b"" +assert state_db.get_code(caller) == active_code # Activated +assert gas_left == 10000 - PrecompiledContract.GAS_COST + +# Test 2: Error cases +error, gas_left = precompile.execute(caller, state_db, gas=10000, static=True) +assert error == b"cannot call in static context" +assert gas_left == 0 + +error, gas_left = precompile.execute(caller, state_db, gas=PrecompiledContract.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 +``` + +## Reference Implementation + +```python +class PrecompiledContract: + DELEGATED_CODE_PREFIX = bytes.fromhex("ef0100") # EIP-7702 prefix + GAS_COST = 5000 # PRECOMPILE_GAS_COST + + def execute(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 + if gas < self.GAS_COST: + return b"insufficient gas", 0 + + # Check static call + if static: + return b"cannot call in static context", 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 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 code length", 0 + + return b"", gas - self.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 +``` + +## 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. + +### Additional checks for [EIP-7702](./eip-7702) transactions + +In applying authorizations of an [EIP-7702](./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. + +### Permit extension for [ERC-20](./eip-20) + +This EIP does not revoke [ERC-2612](./eip-2612) permissions. EOAs with deactivated private keys can still authorize transfers by calling the `permit` function of [ERC-20](./eip-20) tokens supporting [ERC-2612](./eip-2612). This issue also exists with [EIP-7702](./eip-7702). To support deactivated EOAs, [ERC-20](./eip-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](./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. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md).