EIP-7702 - Set EOA account code

Created 2024-05-07
Status Review
Category Core
Type Standards Track
Authors
Requires

Abstract

Add a new transaction type that adds a list of [chain_id, address, nonce, y_parity, r, s] authorization tuples. For each tuple, write a delegation designator (0xef0100 ++ address) to the signing account's code. All code executing operations must load the code pointed to by the designator.

Motivation

There is a lot of interest in adding short-term functionality improvements to EOAs, increasing the usability of applications and in some cases allowing improved security. Three particular applications include:

Specification

Parameters

Parameter Value
SET_CODE_TX_TYPE 0x04
MAGIC 0x05
PER_AUTH_BASE_COST 12500
PER_EMPTY_ACCOUNT_COST 25000

Set Code Transaction

We introduce a new EIP-2718 transaction, "set code transaction", where the TransactionType is SET_CODE_TX_TYPE and the TransactionPayload is the RLP serialization of the following:

rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, value, data, access_list, authorization_list, signature_y_parity, signature_r, signature_s])

authorization_list = [[chain_id, address, nonce, y_parity, r, s], ...]

The fields chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, value, data, and access_list of the outer transaction follow the same semantics as EIP-4844. Note, this means a null destination is not valid.

The authorization_list is a list of tuples that store the address to code which the signer desires to execute in the context of their EOA. The transaction is considered invalid if the length of authorization_list is zero.

The transaction is also considered invalid when any field in an authorization tuple cannot fit within the following bounds:

assert auth.chain_id < 2**256
assert auth.nonce < 2**64
assert len(auth.address) == 20
assert auth.y_parity < 2**8
assert auth.r < 2**256
assert auth.s < 2**256

The EIP-2718 ReceiptPayload for this transaction is rlp([status, cumulative_transaction_gas_used, logs_bloom, logs]).

Behavior

At the start of executing the transaction, after incrementing the sender's nonce, for each [chain_id, address, nonce, y_parity, r, s] tuple do the following:

  1. Verify the chain id is either 0 or the chain's current ID.
  2. Verify the nonce is less than 2**64 - 1.
  3. authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]
    • s value must be less than or equal to secp256k1n/2, as specified in EIP-2.
  4. Add authority to accessed_addresses (as defined in EIP-2929.)
  5. Verify the code of authority is either empty or already delegated.
  6. Verify the nonce of authority is equal to nonce. In case authority does not exist in the trie, verify that nonce is equal to 0.
  7. Add PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST gas to the global refund counter if authority exists in the trie.
  8. Set the code of authority to be 0xef0100 || address. This is a delegation designation.
    • As a special case, if address is 0x0000000000000000000000000000000000000000 do not write the designation. Clear the account's code and reset the account's code hash to the empty hash 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470.
  9. Increase the nonce of authority by one.

If any of the above steps fail, immediately stop processing that tuple and continue to the next tuple in the list. It will in the case of multiple tuples for the same authority, set the code using the address in the last valid occurrence.

Note that the signer of an authorization tuple may be different than tx.origin of the transaction.

If transaction execution results in failure (any exceptional condition or code reverting), setting delegation designations is not rolled back.

Delegation Designation

The delegation designation uses the banned opcode 0xef from EIP-3541 to designate the code has a special purpose. This designator requires all code executing operations to follow the address pointer to get the account's executable code, and requires all other code reading operations to act only on the first 2 bytes of the designator (0xef01). The following reading instructions are impacted: EXTCODESIZE, EXTCODECOPY, EXTCODEHASH, and the following executing instructions are impacted: CALL, CALLCODE, STATICCALL, DELEGATECALL, as well as transactions with destination targeting the code with delegation designation.

For example, EXTCODESIZE would return 2 (the size of 0xef01) instead of 23 which would represent the delegation designation, EXTCODEHASH would return 0xeadcdba66a79ab5dce91622d1d75c8cff5cff0b96944c3bf1072cd08ce018329 (keccak256(0xef01)), and CALL would load the code from address and execute it in the context of authority.

In case a delegation designator points to a precompile address, retrieved code is considered empty and CALL, CALLCODE, STATICCALL, DELEGATECALL instructions targeting this account will execute empty code, i.e. succeed with no execution given enough gas.

In case a delegation designator points to another designator, creating a potential chain or loop of designators, clients must retrieve only the first code and then stop following the designator chain.

Gas Costs

The intrinsic cost of the new transaction is inherited from EIP-2930, specifically 21000 + 16 * non-zero calldata bytes + 4 * zero calldata bytes + 1900 * access list storage key count + 2400 * access list address count. Additionally, we add a cost of PER_EMPTY_ACCOUNT_COST * authorization list length.

The transaction sender will pay for all authorization tuples, regardless of validity or duplication.

If a code reading instruction accesses a cold account during the resolution of delegated code, add an additional EIP-2929 COLD_ACCOUNT_READ_COST cost of 2600 gas to the normal cost and add the account to accessed_addresses. Otherwise, assess a WARM_STORAGE_READ_COST cost of 100.

Transaction Origination

Modify the restriction put in place by EIP-3607 to allow EOAs whose code is a valid delegation designation, i.e., 0xef0100 || address, to continue to originate transactions. Accounts with any other code values may not originate transactions.

Additionally, if a transaction's destination has a delegation designation, add the target of the delegation to accessed_addresses.

Rationale

Cost of Delegation

The PER_AUTH_BASE_COST is the cost to process the authorization tuple and set the delegation destination. We are able to compute a fair cost for this operation by reviewing its impact on the system:

The impact-based assessment leaves us with 12016 gas for the operation. We round up to 12500 to account for miscellaneous costs associated with shuttling data around the state transition.

No initcode

Running initcode is not desirable for many reasons. The chief concern is it's unnatural. Initcode is intended to initialize and deploy contracts. With this EIP, it will take on a new role of determining whether it is appropriate to deploy code to the EOA. Suppose a user only wants code deployed to their account if they also have an operation bundled with the general transaction calldata. This gives EOAs a unique power to control when and what code executes in their account. Although EIP-7702 as written still allows this to a degree, the lack of programmability in the decision will force wallets to not sign many authorization tuples and instead focus on signing only a tuple pointing to a configurable proxy. This affords EOAs a similar experience to smart contract wallets.

Additionally, initcode in a transaction tends to be propagated inside the transaction. That means it would need to be included in the authorization tuple and signed over. The minimum initcode would be around 15 bytes, and that would simply copy the contract code from an external address. The total cost would be 16 * 15 = 240 calldata cost, plus the EIP-3860 cost of 2 * 15 = 30, plus the runtime costs of around 150. So nearly 500 additional gas would be spent simply preparing the account; and even more likely, 1200+ gas if not copying from an external account.

Creation by template

Initcode or not, there is a question of how users should specify the code they intend to run in their account. The two main options are to specify the bytecode directly in the transaction or to specify a pointer to the code. The simplest pointer would just be the address of some code deployed on-chain.

The cost analysis makes the answer clear. The smallest proxy would be around 50 bytes, and an address is 20 bytes. The 30 byte difference provides no useful additional functionality and will be inefficiently replicated billions of times on the chain.

Furthermore, specifying code directly would again make it possible for EOAs to have a new, unique ability to execute arbitrary code specified in the transaction calldata.

Lack of instruction prohibition

Consistency is a valuable property in the EVM, both from an implementation perspective and a user understanding perspective. Despite considering bans on several families of instructions in the context of EOAs, the authors feel there is not a compelling reason to do so. It will force smart contract wallets and EOA smart contract wallets to proceed down distinct paths of contract development.

The main families of instructions where a ban was considered were storage related and contract creation related. The decision to not ban storage instructions hinged mostly on their importance to smart contract wallets. Although it's possible to have an external storage contract that the smart contract wallet calls into, it is unnecessarily inefficient. In the future, new state schemes may even allow substantially cheaper access to certain storage slots. This is something smart contract wallets will very much want to take advantage of that a storage contract wouldn't support.

Creation instructions were considered for a ban on other similar EIPs, however because this EIP allows EOAs to spend value intra-transaction, the concern with bumping the nonce intra-transaction and invalidating pending transactions is not significant.

Signature structure

The signature scheme in this EIP supports flexible design patterns, allowing for both full delegation to address and more protected delegations to address.

Code pointer

One consideration when signing a code pointer is what code might that address point to on another chain. For some use cases, it may not be desirable to expend the effort verifying the deployment was deterministic. In such situations, the chain ID can be set to reduce the scope of the authorization. For other situations where universal deployment is preferred, e.g., delegating to a wallet proxy, it's possible to set chain ID to 0 for validity on all EIP-7702 chains. Wallet maintainers will be able to hard code a single EIP-7702 authorization message into their wallet so that cross-chain code malleability never becomes a concern.

An alternative to adding chain ID could be to sign over the code the address points to. This seems to have the benefit of both minimizing the on-chain size of auth tuples while retaining specificity of the actual code running in the account. One unfortunate issue of this format, though, is that it imposes a database lookup to determine the signer of each auth tuple. This imposition itself seems to create enough complexity in transaction propagation that it is decided to avoid and simply sign over the address directly.

In-protocol revocation

Unlike previous versions of this EIP and similar EIPs, the delegation designation can be revoked at any time by signing and sending an EIP-7702 authorization to a new target with the account's current nonce. Without such action, a delegation will remain valid in perpetuity.

Self-sponsoring: allowing tx.origin to set code

Allowing tx.origin to set code enables simple transaction batching, where the sender of the outer transaction would be the signing account. The ERC-20 approve-then-transfer pattern, which currently requires two separate transactions, could be completed in a single transaction with this proposal.

Once code exists in the EOA, it's possible for self-sponsored EIP-7702 transactions to have msg.sender == tx.origin anytime the code in the EOA dispatches a call. Without EIP-7702, this situation can only ever arise in the topmost execution layer of a transaction. Therefore, this EIP breaks that invariant and so it affects smart contracts containing require(msg.sender == tx.origin) checks. This check is used for at least three purposes:

  1. Ensuring that msg.sender is an EOA (given that tx.origin always has to be an EOA). This invariant does not depend on the execution layer depth and, therefore, is not affected.
  2. Protecting against atomic sandwich attacks like flash loans, which rely on the ability to modify state before and after the execution of the target contract as part of the same atomic transaction. This protection would be broken by this EIP. However, relying on tx.origin in this way is considered bad practice, and can already be circumvented by miners conditionally including transactions in a block.
  3. Preventing reentrancy.

Examples of (1) and (2) can be found in contracts deployed on Ethereum mainnet, with (1) being more common (and unaffected by this proposal). On the other hand, use case (3) is more severely affected by this proposal, but the authors of this EIP did not find any examples of this form of reentrancy protection, though the search was non-exhaustive.

This distribution of occurrences—many (1), some (2), and no (3)—is exactly what the authors of this EIP expect because:

There are other approaches to mitigate this restriction which do not break the invariant:

Forward-compatibility with future account abstraction

This EIP is designed to be very forward-compatible with endgame account abstraction, without over-enshrining any fine-grained details of ERC-4337 or RIP-7560.

Specifically:

Clearing Delegation Designations

A general design goal of state transition changes is to minimize the number of special cases an EIP has. In early iterations, this EIP resisted a special case for clearing an account's delegation designation.

For most intents and purposes, an account delegated to 0x0 is indistinguishable from a true EOA. However, one particular unfortunate case is unavoidable. Even if a user has a zeroed out delegation designation, most operations that interact with that account will encounter an additional COLD_ACCOUNT_READ_COST upon the first touch.

This is not ideal and may be a significant enough concern to impact the overall adoption of the EIP. For these reasons, we have opted to include a mechanism which allow users to restore their EOA to its original pureness.

Delegation of code execution only

Other code retrieving operations like EXTCODEHASH do not automatically follow delegations, they operate on the delegation designator itself. If instead delegations were followed, an account would be able to temporarily masquerade as having a particular codehash, which would break contracts that rely on codehashes as an indicator of possible account behavior. A change of behavior in a contract is currently only possible if its code explicitly allows it (in particular via DELEGATECALL), and a change of codehash is only possible in the presence of SELFDESTRUCT (and as of Cancun only in the same transaction as contract creation), so choosing to follow delegations in EXTCODE* opcodes would have created a new type of account that broke prior assumptions.

Backwards Compatibility

This EIP breaks the invariant that an account balance can only decrease as a result of transactions originating from that account. It also breaks the invariant that an EOA nonce may not increase after transaction execution has begun. These breakages have consequences for mempool design, and for other EIPs such as inclusion lists. However, because the accounts are listed statically in the outer transaction, it is possible to modify transaction propagation rules so that conflicting transactions are not forwarded.

Security Considerations

Secure delegation

The following is a non-exhaustive list of checks/pitfalls/conditions that delegate contracts should be wary of and require a signature over from the account's authority:

A poorly implemented delegate can allow a malicious actor to take near complete control over a signer's EOA.

Setting code as tx.origin

Allowing the sender of an EIP-7702 to also set code has the possibility to:

The authors of this EIP believe the risks of allowing this are acceptable for the reasons outlined in the Rationale section.

Sponsored transaction relayers

It is possible for the authorized account to cause sponsored transaction relayers to spend gas without being reimbursed by either invalidating the authorization (i.e., increasing the account's nonce) or by sweeping the relevant assets out of the account. Relayers should be designed with these cases in mind, possibly by requiring a bond to be deposited or by implementing a reputation system.

Front running initialization

Smart contract wallet developers must consider the implications of setting code in an account without execution. Contracts are normally deployed by executing initcode to determine the exact code to be placed in the account. This gives developers the opportunity to initialize storage slots at the same time. The initial values of the account cannot be replaced by an observer, because they are either signed over by an EOA in the case of a creation transaction or they are committed to by computing the contract's address deterministically from the hash of the initcode.

This EIP does not provide developers the opportunity to run initcode and set storage slots during delegation. To secure the account from an observer front-running the initialization of the delegation with an account they control, smart contract wallet developers must verify the initial calldata to the account for setup purposes be signed by the EOA's key using ecrecover. This ensures the account can only be initialized with desirable values.

Transaction propagation

Allowing EOAs to behave as smart contracts via the delegation designation poses some challenges for transaction propagation. Traditionally, EOAs have only be able to send value via a transaction. This invariant allows nodes to statically determine the validity of transactions for that account. In other words, a single transaction has only been able to invalidate transactions pending from the senders account.

With this EIP, it becomes possible to cause transactions from other accounts to become stale. This is due to the fact that once an EOA has delegated to code, that code can be called by anyone at any point in a transaction. It becomes impossible to know if the balance of the account has been sweeped in a static manner.

While there are a few mitigations for this, the authors recommend that clients do not accept more than one pending transaction for any EOA with a non-zero delegation designator. This minimizes the number of transactions that can be invalidated by a single transaction. Another alternative would be to expand the EIP-7702 transaction with a list of accounts the caller wishes to "hydrate" during the transaction. Those accounts behave as the delegated code only for EIP-7702 transactions which include them in such a list, thus returning to clients the ability to statically analyze and reason about pending transactions.

A related issue is that an EOA's nonce maybe incremented more than once per transaction. Because clients already need to be robust in a worse scenario (described above), it isn't a major security concern. However, clients should be aware this behavior is possible and design their transaction propagation accordingly.

Storage management

Changing an account's delegation is a security-critical operation that should not be done lightly, especially if the newly delegated code is not purposely designed and tested as an upgrade to the old one.

In particular, in order to ensure a safe migration of an account from one delegate contract to another, it's important for these contracts to use storage in a way that avoids accidental collisions among them. For example, using ERC-7201 a contract may root its storage layout at a slot dependent on a unique identifier. To simplify this, smart contract languages may provide a way of re-rooting the entire storage layout of existing contract source code.

If all contracts previously delegated to by the account used the approach described above, a migration should not cause any issues. However, if there is any doubt, it is recommended to first clear all account storage, an operation that is not natively offered by the protocol but that a special-purpose delegate contract can be designed to implement.

Copyright

Copyright and related rights waived via CC0.