EIP-7702: Set EOA account code

A basic EIP-7702 account template that provides batching doesn’t need any storage or initialization. Here’s a possible example.

2 Likes

I reviewed the spec and have concerns with the following part:

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.

As the transaction originator, I would prefer the transaction to fail earlier if the result of applying the algorithm to authorization tuples does not yield a known good configuration. I should be able to specify what that configuration looks like, and if it’s not met after processing the authorization tuples, the transaction should:

a) Fail, and
b) Revert any changes made to delegated designations.

Here’s a scenario: An attacker signs an authorization to change a delegated designation to a known good contract. They then monitor the network and when the transaction that includes this authorization is sent they quickly send another transaction to switch the delegated designation to a malicious contract. In this case, when processing a transaction that includes an authorization to change delegation to a known good contract the alogirthm will not make a switch (because the nonce will be invalid) and transaction would execute in an environment where the delegated designation points to the malicious contract. As a transaction sender, I don’t want my call to execute under such conditions, with potentially unknown side effects. I’d much rather have the transaction fail early without execution.

To avoid this issue, I propose a simple solution: the transaction originator should be able to specify a set of expected (address, contract_address) pairs before code execution begins. If, after processing the delegation changes, the expected configuration isn’t met, the transaction should fail, preventing any further code execution.

1 Like

With my pending proposal it’s possible to verify the delegation target of an account before interacting with it, using EXTCODECOPY.

This opens up a more serious vulnerability where a single delegator can cause an entire batch of delegations to fail. I don’t think that would be the right trade-off.

1 Like

Good catch! I agree with @frangio that #8969 seems a good fit here. (Though I was thinking yesterday that maybe it would be a good idea to leave CODESIZE and CODECOPY transparent and only change the EXT* opcodes?)

It would add a bit of extra gas, but would checking the delegation target via EXTCODECOPY satisfy all the usecases you can think of? There are valid (esp. cross-chain) usecases for not failing on invalid designators, so it would be great if we could keep that functionality.

(To expand on those cross-chain usecases, one that comes to mind is that you could synchronize nonces across different chains by signing a series of null delegations with a 0 chainId and incrementing nonces, and then you can follow that up with a single chainId 0 delegation with the subsequent nonce.)

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.

Just to clarify, does this mean that when invoked, the EVM will attempt to execute 0xef0100 || address, and since 0xef is an invalid opcode, it will consume all the gas specified in the call and fail?

This requires the contract to have a verification code. I feel that a declarative approach is conceptually simpler, where, as a transaction originator, I can specify that certain addresses should have a particular code before execution begins.

Could you explain how reverting any changes made to delegated designations could cause an entire batch of delegations to fail? I assume this might happen if a subsequent transaction relies on authorization nonces already being incremented.

I’m not too concerned about reverting the authorization changes, as long as I can be sure that the transaction executes against the expected delegated designations. Initially, I thought there might be cases where it’s critical for delegated designations to be updated atomically—i.e., either both accounts update their delegated designation, or neither does—otherwise, there’s a potential security risk. However, I realize the current design doesn’t offer protection against this.

If such a vulnerability were to exist, an attacker could simply monitor transactions and unbundle the changes to the delegation designation.

Considering this, is there a security risk in the following scenario? The victim sends a transaction authorizing a change in delegated designation for an address to a particular contract. They also send a second transaction authorizing a change in delegated designation for the same address to another contract. Meanwhile, the attacker listens for these transactions and bundles both authorizations into their own transaction, executing it before the victim’s first transaction.

When the attacker’s transaction is executed, it performs two changes to the delegated designations. As a result, the victim loses the first authorization and cannot change the delegated designation to the first contract. Consequently, the victim is unable to execute their intended actions in the first transaction, which now executes with the delegated designation pointing to the second contract.

In reverse order:

If you build a TX with your own delegation authorization embedded, the nonce will bump once for the TX (signed with nonce n) and then a second time for the signature on the delegation designator (signed with nonce n+1). So an attacker couldn’t unbundle the designator and broadcast it themselves, since while they’re frontrunning you the nonce won’t equal n+1. This only works if a TX is submitted via the same EOA that’s delegating, though, meaning they would have to have Ether to cover the gas.

I was thinking more along the lines of using a generic “check delegations & dispatch” contract. A transaction submitter could delegate to such a contract and then call into it with require(msg.sender == address(this)) and a series of delegations to check via EXTCODEHASH and the target/value/calldata to use if they succeed. (One possible kink here is that while the submitter can easily ensure their delegation to such a contract is processed before the call, they can’t reset it afterwards without broadcasting another transaction.)

A suggestion for EIP-7702:
Currently, if you’re trying to use EIP-7702 inside a higher level transaction (e.g. use ERC-4337 paymaster to sponsor your EIP-7702 account deployment and execution), you end up signing twice: once for the authlist item (to deploy the EIP-7702 delegate), and then another for the ERC4337’s UserOperation.

The suggestion is add a “hash” value into the authlist, which gets signed along with the delegate, chainid and nonce.
Other than being signed by 7702, it is left unused by it.
But this value can be used by the wrapper transaction (a UserOperation), and thus a single signature can be used both.

1 Like

How does this interact with:

That proposal has the text:

If a contract creation is attempted due to […] any other reason, and the destination address already has […] non-empty storage, then the creation MUST throw as if the first byte in the init code were an invalid opcode.

Arguably, EIP-7702 is a contract creation. As far as I can tell, EIP-7702 doesn’t clear storage. Therefore, if a contract writes to the EOA’s storage and later the EOA’s code is cleared, then no future code can be written to the EOA.

It’s a little fuzzy because 7702 doesn’t run initcode, but rules-as-intended it sounds like the second deployment should fail. If 7702 bypasses 7610, maybe the document should explicitly state it.

Summoning @holiman and @rjl493456442 for their thoughts.

It is interesting that you worry about 7610 but not 684. 7610 merely expands 684 to include storage. 7702 breaks from 7610 in the same way it breaks from 684.

Disregarding initcode, neither 7702 nor SETCODE are contract creation. The main distinction between setcode and creation is that only the code is changed. The account already exists; it has balance and nonce. Allowing EOAs to setcode is not a creation therefore. Changing the nonce, the balance, the code, the storage, or any other property of an existing account is not a recreation.

Seems like the EIP is still making progress, which is great. I have one specific question about the impacted opcodes and a more general question about the design of the EIP.

Starting with the specific question: the impacted operations are called out specifically: EXTCODESIZE, EXTCODECOPY, EXTCODEHASH, CALL, CALLCODE, STATICCALL, and DELEGATECALL, and examples are given for how the EXTCODESIZE and EXTCODEHASH are affected, but wouldn’t it be better to explicitly specify how each of these are impacted? We wouldn’t want any variation due to room for interpretation. In particular, its not clear to me from reading how should EXTCODECOPY work, though I think it seems to suggest that only the first 2 bytes should be copied (0xef01).

For the more general question about the design, I hope someone can help clarify and point out where my understanding is flawed. But it seems to me that the EIP creates a situation that is very dangerous by default for users and makes it extraordinarily difficult to perform the simple things that users might want to benefit from (e.g. batching or gasless). It seems that since the signature is only over keccak(MAGIC || rlp([chain_id, address, nonce])), there is always a requirement for an additional signature from the user for any particular operation. They are unable to simply sign once a batch of transactions to be executed. Further, delegated contract code would have to add in additional application level signature checks or the like for the EOA to not be completely opening their account up for anyone to access, making the default state appear incredibly dangerous for users (especially those wishing to do simple batch operations with a single signature). Am I missing something here, or is this really how it is designed?

No, that is how the EIP is designed. A delegation is an extremely sensitive operation. The intent is that wallet providers will develop account code and offer it as a way to upgrade an EOA with features such as batching. Advanced users may manually delegate to trusted code or implement their own, which obviously requires an understanding of the security model.

Thanks for clarifying. In that case, I have concerns about the current design. The complexity of building a contract wallet is high and error-prone, and in the best case this design will require a user to perform multiple signatures in order to do simple batching / gasless operations. By its nature, the first signature will require the user to over-provision trust in the contract code being delegated to. If instead the signature included the calldata (and gas, value), and if the authorization list included a boolean for whether or not the delegation should be transient, this would be vastly superior for most use cases, in my opinion. A user could then create single signatures with restricted capabilities without needing to trust/audit a potentially extremely complex contract (and one that by its nature invites many easy-to-produce vulnerabilities). Such a design would also allow that trust to be gained progressively, as wallet implementations build confidence, without requiring all-or-nothing (blind) leaps of faith from users.

Is this being actively discussed?
Reducing 2 signatures to 1 signature is a huge value add to the usage of this and should not be ignored. Much better UX with a single signature

Yeah, I should be worried about 684 as well :rofl: I had just read 7610, so it was on my mind at the time.

A wallet should be able to implement a UX where this is combined into a single interaction. Does something prevent that?