Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update EIP-7702: refine based on discussions #8561

Merged
merged 16 commits into from
Jun 6, 2024
151 changes: 118 additions & 33 deletions EIPS/eip-7702.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ requires: 2718, 2929, 2930

## Abstract

Add a new transaction type that adds a `contract_code` field and a signature, and converts the signing account (not necessarily the same as the `tx.origin`) into a smart contract wallet for the duration of that transaction. Intended to offer similar functionality to [EIP-3074](./eip-3074.md).
Add a new transaction type that adds a list of `[address, y_parity, r, s]` authorization tuples, and converts the signing accounts (not necessarily the same as the `tx.origin`) into smart contract wallets for the duration of that transaction.

## Motivation

Expand All @@ -23,63 +23,126 @@ There is a lot of interest in adding short-term functionality improvements to EO
* **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, you could imagine a permission to spend ERC-20 tokens but not ETH, or to spend up to 1% of total balance per day, or to interact only with a specific application.

[EIP-3074](./eip-3074.md) solves all of these use cases. However, it has forward-compatibility concerns:

* It introduces two opcodes, `AUTH` and `AUTHCALL`, that would have no use in an "endgame account abstraction" world where eventually all users are using smart contract wallets (which seems like it must happen eventually, at the least because eventually quantum computers will break the ECDSA that EOAs use)
* It leads to the development of an "invoker contract" ecosystem that would be separate from the "smart contract wallet" ecosystem, leading to possible fragmentation of effort.

The purpose of this EIP is to enable all of the use cases of EIP-3074, without these two weaknesses.

## Specification

The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

### Parameters

* `FORK_BLKNUM` = `TBD`
* `TX_TYPE` = `TBD`
* `MAGIC` = `TBD`
* `PER_CONTRACT_CODE_BASE_COST` = `5000`
| Parameter | Value |
| -------------------- | ------ |
| `SET_CODE_TX_TYPE` | `0x04` |
| `MAGIC` | `0x05` |
| `PER_AUTH_BASE_COST` | `2500` |

As of `FORK_BLOCK_NUMBER`, a new [EIP-2718](./eip-2718.md) transaction is introduced with `TransactionType` = `TX_TYPE(TBD)`.
### Set Code Transaction

The [EIP-2718](./eip-2718.md) `TransactionPayload` for this transaction is
We introduce a new [EIP-2718](./eip-2718.md) 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, data, access_list, [[contract_code, y_parity, r, s], ...], signature_y_parity, signature_r, signature_s])
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 = [[address, y_parity, r, s], ...]
```

The intrinsic cost of the new transaction is inherited from [EIP-2930](./eip-2930.md), 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 `16 * non-zero calldata bytes + 4 * zero calldata bytes` over each `contract_code`, plus `PER_CONTRACT_CODE_BASE_COST` times the length of the `contract_code` array.
The fields `chain_id`, `nonce`, `max_priority_fee_per_gas`, `max_fee_per_gas`, `gas_limit`, `destination`, `value`, `data`, and `access_list` follow the same semantics as [EIP-1559](./eip-1559.md).

The `authorization_list` is a list of tuples that store the address to code which the signer desires to set in their EOA temporarily.

At the start of executing the transaction, for each `[contract_code, y_parity, r, s]` tuple:
The [EIP-2718](./eip-2718.md) `ReceiptPayload` for this transaction is `rlp([status, cumulative_transaction_gas_used, logs_bloom, logs])`.

1. Let `signer = ecrecover(keccak(MAGIC + contract_code), y_parity, r, s]`.
2. Verify that the contract code of `signer` is empty.
3. Set the contract code of `signer` to `contract_code`.
4. Add the `signer` account to `accessed_addresses` (as defined in [EIP-2929](./eip-2929.md).)
#### Behavior

At the end of the transaction, set the `contract_code` of each `signer` back to empty.
At the start of executing the transaction, for each `[address, y_parity, r, s]` tuple:

Note that the signer of any of the `contract_code` signatures, and the `tx.origin` of the transaction, are allowed to be different.
1. Let `authority = ecrecover(keccak(MAGIC || address), y_parity, r, s]`.
2. Verify that the code of `authority` is empty.
3. Set the code of `authority` to code associated with `address`.
4. Add the `authority` account to `accessed_addresses` (as defined in [EIP-2929](./eip-2929.md).)

At the end of the transaction, set the code of each `authority` back to empty.

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

#### Gas Costs

The intrinsic cost of the new transaction is inherited from [EIP-2930](./eip-2930.md), 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_CONTRACT_CODE_BASE_COST * authorization list length`.

## Rationale

### Conversion of EIP-3074 use cases
### 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 determine 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 EOA a unique power to control when and what code executes in their account. Although [EIP-7702](./eip-7702.md) 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 signing only a tuple pointing to a configurable proxy. This affords EOAs a similar experience to smart contract wallets

Additionally, initcode in 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](./eip-3860.md) 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 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 time 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. A neat byproduct of this is that by combining EIP-7702 and CREATE2 it will be possible to commit to deploy specific bytecode to an address without committing to any fee market parameters. This solves the long standing issue of universal cross-chain contract deployment.

### Signature structure

The EIP proposes a signature format of `keccak(MAGIC || address)`. This allows for maximally flexible design which allows full delegation to `address`.

#### Code pointer

In this design, it requires fairly little work to convert an existing EIP-3074 workflow. Specifically, AUTH and AUTHCALL would get replaced by calls into the EOA. One way to do this is that the `contract_code` would be a user wallet (which could be a `DELEGATECALL` forwarder to save gas), and would expose two functions, `verify` and `execute`.
One consideration when signing a code pointer is what code might that address point to on another chain. For that reason, some have requested that the signature also include the chain which it is intended to be included on. This is unnecessary, because it's possible to have deterministic contract deployments across chain. Plus, considering the anticipated use of EIP-7702 is to sign a single message delegating to a proxy, it seems reasonable that wallet teams will be able to hardcode this single EIP-7702 authorization message into their wallet so that cross-chain code malleability never becomes a concern.

* AUTH would be replaced by a code to `verify`, which would use TSTORE to locally set `authorized[msg.sender, ...] = True`.
* AUTHCALL would be replaced by a call to `execute`, which would use TLOAD to verify `authorized[msg.sender, ...]`, and then execute from there.
A possible compromise would be to include the address in the authorization tuple, but still 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 address directly.

Hence, there is a very simple transformation from "existing EIP-3074 workflows" into workflows under this new scheme.
#### In-protocol revocation

A hotly debated element of this EIP is the need for in-protocol revocation. Although it is possible to implement revocation logic within delegated code, some say this isn't sufficient and it is the duty of the protocol to provide a revocation option of last resort.

From a first-principles perspective, users should only delegate control of their account to code they trust. Trust is often by proxy because most users are unable to verify smart contract code for themselves. This framing is similar to trusting client software to behave correctly, wallet software to not steal keys, and hardware wallets to not have backdoors. So long as the components are open source and the organizations behind the software have some reputation at stake, this will be a perfectly safe environment for users.

Writing safe and bug-free smart contracts is difficult. However, not all code paths are create equal. There are certain areas of code that are both simple and cannot fail. Revocation logic is one. Authentication logic is another. These are the core components of a wallet. One can imagine extending a wallet further with plugins in way were failure is more tolerable. Suppose a user adds a handful of plugins which bit rot over time. Revoking that plugin should be simple and possible via the core wallet implementation.

It's important to note that revocation is only useful as a mitigation against bugs that have not yet been found. As soon as a vulnerability exists in a wallet, the funds in the account must be considered as MEV and value of the account will be auctioned off.

This is not to say that revocation isn't critical. It is simply that smart contracts and EOA wallets are capable of providing the same functionality. Even if the there is no cost to the user for in-protocol revocation, it is undue complexity in the core protocol. We must resist all except which is absolutely necessary to encode in the core protocol.

### Setting code as `tx.origin`

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, that 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:

* Determining if `msg.sender` is an EOA without `tx.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 (ex. 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 constant `ENTRY_POINT` address when using `CALL*` 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.

### 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](./eip-4337.md) or RIP-7560.

Specifically:

* The contract code that users would need to sign could literally be existing ERC-4337 wallet code.
* The code that users would need to sign could literally be existing ERC-4337 wallet code.
* The "code pathways" that are used are code pathways that would, in many cases (though perhaps not all), continue to "make sense" in a pure-smart-contract-wallet world.
* Hence, it avoids the problem of "creating two separate code ecosystems", because to a large extent they would be the same ecosystem. There would 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.
* It does not require adding any opcodes, that would become dangling and useless in a post-EOA world.
Expand All @@ -88,11 +151,33 @@ Specifically:

## Backwards Compatibility
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing from the list of broken assumptions is signature malleability:
Specifically, a user might sign an unverified experimental 7702 contract on a testnet under the assumption (which was correct until now) that it is a sandbox, and nothing affects other network.
This becomes disastrous, as this contract can be used anywhere - and currently is un-revocable.
The user never opt-in to this new feature: he merely continued his work with an existing (though updated) wallet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@drortirosh chainId variable will be added to the signature. Also, this type of signed data has a specific magic. Wallets are expected to warn the user heavily about what they are about to sign.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signing an unverified / experimental 7702 contract would never happen with a wallet that correctly implements 7702.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any problem with "wallet that correctly implements the standard".

Leaving the chainid out of signature EIP breaks the invariant that "testnet are sandbox and can't affect any other network".
Please add that text into your security consideration, if you think that's acceptable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signing an unverified / experimental 7702 contract would never happen with a wallet that correctly implements 7702.

The only way to implement this is by completely blocking (by the wallet) from ever signing anything un-approved by the wallet, not even with a warning message.
That's the only way you can block this concern.

Even with a warning message such as:

Beware of this signature: it opens your account to malicious operations not only on this network, but on any other network, and this operation is un-revocable. are you sure you want to continue

And I'm afraid that on testnets, some people will STILL sign this message, as the current narrative is "testnets are sandbox"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which networks are banning chain id 0?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which networks are banning chain id 0?

The question is, on which network you can submit a non-EIP-155 transaction? All block builders on all networks I know of block them.
That's a de-facto standard (that all TXs are EIP-155).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're conflating protocol functionality with protections offered at the RPC level.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm saying that there is an implicit assumptions today by users, that this EIP breaks:

  1. That signing data on testnet stays in testnet and doesn't affect other networks.
  2. That signing anything has a one-shot effect, not long-lasting (or permanent)

And I think that breaking these assumptions AT LEAST deserves being mentioned in the "security consideration" (or backward compatibility) section.
After specifying them, you can add your argument why you think it is OK to break them - but at least acknowledge that we're introducing a network change that alters existing users assumptions.
This is not a change that affect new users that use those new features, or new wallets: it affect all existing users that happen to upgrade their existing wallet, and are unaware of such changes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That signing data on testnet stays in testnet and doesn't affect other networks.

Again, this is not correct. The protocol allows for non EIP-155 txs to be replayed on any chain. If users think they're safe because the wallet says they're on a testnet today, it's because apps, wallets, and RPC providers have done well to create this feeling of security. They could have just as easily continued signing and allowing txs w/o chain id baked in. I have no reason to believe post-7702 things will be any different.

That signing anything has a one-shot effect, not long-lasting (or permanent)

This would be true for 7702 auths. Happy to put reword the security considerations if it is not clear there.


This EIP breaks the invariant that an account balance can only decrease as a result of transactions originating from that account. This has consequences for mempool design, and for other EIPs such as inclusion lists. However, these issues are common to any proposal that provides similar functionality, including EIP-3074.
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

Many security considerations with EIP-3074 are shared. Particularly, user wallets need to be very careful about which `contract_code` they sign.
### 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:

* Replay protection -- (ex. 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*.

### 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.

## Copyright

Expand Down
Loading