Set Code for EOAs
相关视频
正文
Abstract
Add a new EIP-2718 transaction type that allows Externally
Owned Accounts (EOAs) to set the code in their account. This is done by
attaching a list of authorization tuples -- individually formated as [chain_id, address, nonce, y_parity, r, s]
-- to the transaction. For each tuple, a
delegation indicator (0xef0100 || address)
is written to the authorizing
account's code. All code executing operations must load and execute the code
pointed to by the delegation.
Motivation
Despite great advances in the smart contract wallet ecosystem, EOAs have held back broad adoption of UX improvements across applications. This EIP therefore focuses on adding short-term functionality improvements to EOAs which will allow UX improvements to permeate through the entire application stack. Three particular features this EIP is designed around are:
- Batching: allowing multiple operations from the same user in one atomic transaction. One common example is an ERC-20 approval followed by spending that approval. This is a common workflow in DEXes that requires two transactions today. Advanced use cases of batching occasionally involve dependencies: the output of the first operation is part of the input to the second operation.
- Sponsorship: account X pays for a transaction on behalf of account Y. Account X could be paid in some other ERC-20 for this service, or it could be an application operator including the transactions of its users for free.
- Privilege de-escalation: users can sign sub-keys and give them specific permissions that are much weaker than global access to the account. For example, a permission to spend ERC-20 tokens but not ETH, or to spend up to 1% of the total balance per day, or to interact only with a specific application.
Specification
Parameters
Parameter | Value |
---|---|
SET_CODE_TX_TYPE | 0x04 |
MAGIC | 0x05 |
PER_AUTH_BASE_COST | 12500 |
PER_EMPTY_ACCOUNT_COST | 25000 |
Set code transaction
A new EIP-2718 transaction known as the "set code transaction"
is introduced, 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
implies a null destination is not valid.
The signature_y_parity, signature_r, signature_s
elements of this transaction
represent a secp256k1 signature over keccak256(SET_CODE_TX_TYPE || TransactionPayload)
.
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 perform the
following:
- Verify the chain ID is 0 or the ID of the current chain.
- Verify the
nonce
is less than2**64 - 1
. - Let
authority = ecrecover(msg, y_parity, r, s)
.- Where
msg = keccak(MAGIC || rlp([chain_id, address, nonce]))
. - Verify
s
is less than or equal tosecp256k1n/2
, as specified in EIP-2.
- Where
- Add
authority
toaccessed_addresses
, as defined in EIP-2929. - Verify the code of
authority
is empty or already delegated. - Verify the nonce of
authority
is equal tononce
. - Add
PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST
gas to the global refund counter ifauthority
is not empty. - Set the code of
authority
to be0xef0100 || address
. This is a delegation indicator.- If
address
is0x0000000000000000000000000000000000000000
, do not write the delegation indicator. Clear the account's code by reseting the account's code hash to the empty code hash0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
.
- If
- Increase the nonce of
authority
by one.
If any step above fails, immediately stop processing the tuple and continue to the next tuple in the list. When multiple tuples from the same authority are present, set the code using the address in the last valid occurrence.
Note, if transaction execution results in failure (e.g. any exceptional condition or code reverting), the processed delegation indicators is not rolled back.
Delegation indicator
Delegation indicators use the banned opcode 0xef
, defined in
EIP-3541, to indicate that the code must be handled differently
than regular code. The delegation forces all code executing operations to follow
the address pointer to obtain the code to execute. For example, CALL
loads the
code at address
and executes it in the context of authority
.
The affected executing operations are:
CALL
CALLCODE
DELEGATECALL
STATICCALL
- any transaction where
destination
points to an address with a delegation indicator present
For code reading, only CODESIZE
and CODECOPY
instructions are affected
reading. They operate directly on the executing code instead of the delegation.
For example, when executing a delegated account EXTCODESIZE
returns 23
(the
size of 0xef0100 || address
) whereas CODESIZE
returns the size of the code
residing at address
.
Note, this means during delegated execution CODESIZE
and CODECOPY
produce a
different result compared to calling EXTCODESIZE
and EXTCODECOPY
on the
authority.
Precompiles
When a precompile address is the target of a delegation, the retrieved code is
considered empty and CALL
, CALLCODE
, STATICCALL
, DELEGATECALL
instructions targeting this account will execute empty code, and therefore
succeed with no execution when given enough gas to initiate the call.
Loops
In case a delegation indicator points to another delegation, creating a potential chain or loop of delegations, clients must retrieve only the first code and then stop following the delegation 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, 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 indicator, i.e. 0xef0100 || address
, to
originate transactions. Accounts with any other code values may not originate
transactions.
Additionally, if a transaction's destination
has a delegation indicator, add
the target of the delegation to accessed_addresses
.
Rationale
Below is the rationale from both general design directions of the EIP, as well as specific technical choices.
General design philosphy
Persistence of code delegation
The first draft of this proposal had a clever idea to avoid disagreement on whether in-protocol revocation was needed or not. The idea was to temporarily set code in the account with the authorization. After the transaction the code would be completely cleared. This was a new design space enriching EOA functionality.
Even this approach was not without its flaws. Fundamentally, there was little friction for users including set code authorizations. This meant that some accounts and applications would opt to treat the extension as more of a scripting facility, rather than a full-fledged upgrade to a smart contract wallet. The outcome of this would be two somewhat competing workstreams for UX improvements: smart contract wallets and EOA scripts.
Previous proposals had been met with similar criticisms. To counteract this, persistent delegations were introduced. They create enough friction in deployment that users will not deploy new unique ones regularly. This will hopefully unify the workstreams and fragmentation in UX developments.
No initcode
Running initcode is not desirable for many reasons. It creates new mode of execution that needs extensive testing and may be used for purposes not possible with standard smart contract wallets. It forces developers perform initialization as a standard call to the EOA after delegation. The lack of atomicity in these operations is another factor in pushing users to complete smart contract wallet designs.
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 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.
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. It is for these reasons that creation by template is chosen.
Interaction with applications and wallets
While this EIP provides a lot of flexibility to applications and EOAs, there are incorrect ways of using it. Applications must not expect that they can suggest the user sign an authorization, and therefore it is the duty of the wallet to not provide an interface to do so.
There is no safe way to provide this interface. The code specified by an authorization has unrestricted access to the account and must always be closely audited by the wallet. Few users have the level of sophistication to reasonably verify the code they are delegating to.
It is also not possible to implement a system of permissions at this level to minimize the risk. If applications require custom wallet functionality, they must use standardized extension / module systems built on top of the delegated code that correctly implements permissions.
Forward-compatibility with future account abstraction
This EIP is designed to be forward-compatible with endgame account abstraction, without over-enshrining any fine-grained details of ERC-4337 or RIP-7560.
To start, the address
that users sign could directly point to existing
ERC-4337 wallet code. This essentially requires the "code pathways" that are
used are code pathways that would, in most cases, continue to make sense in a
pure-smart-contract-wallet world. Hence, it avoids the problem of creating two
separate UX workstreams because, to a large extent, they would be the same
ecosystem.
There will be some workflows that require kludges under this solution that would
be better done in some different "more native" under "endgame AA", but this is
relatively a small subset. The EIP does not require adding any opcodes, that
would become dangling and useless in a post-EOA world, and it allows EOAs to
masquerade as contracts to be included in ERC-4337 bundles, in a way that's
compatible with the existing EntryPoint
.
Self-sponsoring: allowing tx.origin
to set code
Allowing tx.origin
to set code enables, where the sender of the outer
transaction would be the signing account, is called self-sponsoring. It allows
users to take advantage of EIP-7702 without relying on any third party
infrastructure.
However, that means the EIP breaks the invariant that msg.sender == tx.origin
only happens in the topmost execution frame of a transaction. This will affect
smart contracts containing require(msg.sender == tx.origin)
checks. This check
is used for at least three purposes:
- Ensuring that
msg.sender
is an EOA (given thattx.origin
always has to be an EOA). This invariant does not depend on the execution layer depth and, therefore, is not affected. - 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. - 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:
- Determining if
msg.sender
is an EOA withouttx.origin
is difficult, if not impossible. - The only execution context which is safe from atomic sandwich attacks is the
topmost context, and
tx.origin == msg.sender
is the only way to detect that context. - In contrast, there are many direct and flexible ways of preventing reentrancy
(e.g., using a transient storage variable). Since
msg.sender == tx.origin
is only true in the topmost context, it would make an obscure tool for preventing reentrancy, rather than other more common approaches.
There are other approaches to mitigate this restriction which do not break the invariant:
- Set
tx.origin
to a constantENTRY_POINT
address when using theCALL*
instruction in the context of an EOA. - Set
tx.origin
to a special address derived from the sender or signer addresses. - Disallow
tx.origin
from setting code. This would make the simple batching use cases impossible, but could be relaxed in the future.
Rationale for technical details
Cost of delegation
The PER_AUTH_BASE_COST
is the cost to process the authorization tuple and set
the delegation destination. To compute a fair cost for this operation, the
authors review its impact on the system:
- ferry 101 bytes of calldata =
101 * non-zero cost (16) = 1616
- recovering the
authority
address =3000
- reading the nonce and code of
authority
=2600
- storing values in already warm account =
200
- cost to deploy code =
200 * 23 = 4600
The impact-based assessment identifies 12016
gas of comparable computation for
the operation. It is rounded up to 12500
to account for miscellaneous costs
associated with shuttling data around the state transition.
Clearing delegation indicators
A general design goal in 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 indicator.
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 indicator, most
operations that interact with that account will incur an additional
COLD_ACCOUNT_READ_COST
upon the first touch caused by attempting to load the
code at 0x0
.
For this reason, the authors have opted to include a special case which allow users to restore their EOA to its original purity.
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, as it would cause smart contract wallets and EOA smart contract wallets to proceed down distinct UX workstreams.
The main instruction families of 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 complicated and inefficient. In the future, new state schemes may allow substantially cheaper access to certain storage slots within an account. This is something smart contract wallets will want to take advantage of that a storage contract wouldn't support.
Creation instructions were considered for a ban as well 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.
Protection from mallebility cross-chain
One consideration when signing a code pointer is what code that address points to on another chain. While it is possible to create a deterministic deployment, i.e. via Nick's method, verifying such a deployment may not always be desirable. In such situations, the chain ID can be set to reduce the scope of the authorization. When universal deployment is preferred, simply set chain ID to 0.
An alternative to adding chain ID could be to substitute in the actual code for the address in the signature. This seems to have the benefit of both minimizing the on-chain size of auth tuples, by continuing to serialize only the address, while retaining specificity of the actual code running in the account, by pulling in the code for the signature. 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.
Delegation of code execution only
Other code retrieving operations like EXTCODEHASH
do not automatically follow
delegations, they operate on the delegation indicator 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 a definition 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
(which, as of Cancun, only applies in the same
transaction as contract creation), so choosing to follow delegations in
EXTCODE*
opcodes would have created a new type of account breaking prior
assumptions.
Charge maximum cost upfront
While computing the intrinsic gas cost, the transaction is charged the worst-case cost for each delegation. Later, while processing the authorization list, a refund is issued if the account already exists in state. This mechanism is designed to avoid state lookups for each authorization when computing the intrinsic gas and can quickly determine the validity of the transaction with only a state lookup on the sender's account.
No blobs, no contract creation
Transactions should be thought of as specialized tools and not necessarily a one-type-does-all solution. EIP-4844 is treated differently at the p2p level due to burden blobs place on a node's bandwidth. EIP-7702 has different implications on transaction gossiping and there is no need to complicate those rules unneccesarily by making it a superset of all possible functionality. The authors ultimately do not expect there to be much demand for atomic delegation and blob submission.
Contract creation is another specialized use case that has been grandfathered into several transaction types. It adds complexity to testing, because it is a new distinct branch of execution that needs to be tested when any change to the EVM occurs and verify the change works as expected in that context.
For these reasons, the authors have chosen to keep the scope of the EIP focused on improving UX.
Backwards Compatibility
This EIP breaks a few invariants:
- An account balance can only decrease as a result of a transaction originating
from that account.
- Once an account has been delegated, any call to the account may also cause the balance to decrease.
- An EOA nonce may not increase after transaction execution has begun.
- Once an account has been delegated, the account may call a create operation during execution, causing the nonce to increase.
tx.origin == msg.sender
can only be true in the topmost frame of execution.- Once an account has been delegated, it can invoke multiple calls per transaction.
Security Considerations
Implementation of secure delegate contracts
The following is a non-exhaustive list of pitfalls that delegate contracts should be wary of and require a signature over from the account's authority:
- Replay protection (e.g., a nonce) should be implemented by the delegate and signed over. Without it, a malicious actor can reuse a signature, repeating its effects.
value
-- without it, a malicious sponsor could cause unexpected effects in the callee.gas
-- without it, a malicious sponsor could cause the callee to run out of gas and fail, griefing the sponsee.target
/calldata
-- without them, a malicious actor may call arbitrary functions in arbitrary contracts.
A poorly implemented delegate can allow a malicious actor to take near complete control over a signer's EOA.
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.
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.
Setting code as tx.origin
Allowing the sender of an EIP-7702 to also set code has the possibility to:
- Break atomic sandwich protections which rely on
tx.origin
; - Break reentrancy guards of the style
require(tx.origin == msg.sender)
.
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.
Transaction propagation
Allowing EOAs to behave as smart contracts via the delegation indicator 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 sender's 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 indicator. This minimizes the number of transactions that can be invalidated by a single transaction.
An 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 concern. However, clients should be aware this behavior is possible and design their transaction propagation accordingly.
Copyright
Copyright and related rights waived via CC0.