EIP-7702: Set EOA account code

I meant if EXTCODE* doesn’t follow delegations and behaves as if there’s no code for EOAs with a delegation (what @frangio is proposing), EIP-6492 and EIP-1271 will not work.

In the current state of EIP-7702, both EIPs work fantastically thanks to the Delegation Designation.

@matt

The proposal is not for EXTCODE* to ignore delegations, just to not follow the delegation pointer, so for example EXTCODECOPY would copy the delegation designator itself. IMO this makes delegations less special (just built in proxies) and should be the default spec unless we have really good reasons to do more (like the current spec).

2 Likes

It would be nice to have some of these compromises made public. 0xth0mas raised a good point in asking if there has been any links to research on how things like EXTCODEHASH is being used in protocols / security implications. The EIP specifically only mentions msg.sender == tx.origin being broken, but no mention of any real other security threats. There are already concerns regarding EXTCODEHASH raised in here not mentioned in the EIP. Changing expected functionality of existing operations when any number of smart contracts could rely on them for security does not seem like something that should be thrown to the side due to a small group weighting them.

Do we really think a short testnet before including in next hard fork is an appropriate amount of time? To be honest, smart contract wallets exist currently and AA is not a pressing concern to move to production ASAP. AA is a great UX level up, but UX doesn’t matter if protocols are being exploited as a side-effect and users are presented transactions they’re not familiar with.

I don’t mean to be overly passionate about this being rushed, but Ethereum is a base protocol supporting over $40b in protocols, potentially written with assumptions being violated by this EIP. Extreme care should be taken with upgrades like this need to be taken with every opportunity granted to client facing applications to adapt to changes needing to be granted.

1 Like

Removing selfdestruct added a security guarantee that the code wouldn’t change. This was part of the rationale for both removing selfdestruct and blocking setcode. This objection is likely the end of this EIP for the same reason.

7702 seems to enable a lot of things that aren’t its express goals. They all need to be analyzed, however. 7702 is a massively powerful tool, so we have to get it right the first time. 7702 will be the only practical way to do EOA transaction batching; it would be unreasonable to assume it won’t be used that way. I submit that as the scope of 7702 is significantly larger than the sum of its goals, we should not use “not a goal” as a justification to ignore considerations as “out of scope” – especially as it seems practically impossible to prevent (or even substantially disadvantage) uses which do not align with the authors’ initial motivations.

The use of 7702 as a general-purpose protocol-level proxy system is another use case which seems inevitable due to economic considerations. Single-shot deterministic signatures can immutably create such a proxy but cannot initialize it, as the only frontrunner-safe way to handle initialization is to submit a TX from the EOA so that the enclosing TX will first bump the EOA’s nonce as a prerequisite to the delegation authorization’s validity and then call an initialization function. This requires two signatures from the same address, and is therefore impossible to do via a deterministic signature.

Paradoxically, this will leave mutable proxies with a security advantage over immutable ones, though at least immutable ones would be guaranteed to start with blank storage!

Another odd case is that an ordinary “safe” initialization transaction, sent from the EOA being delegated, is intercepted and censored. The user might well eventually “cancel” the “failed” TX by using the nonce in a zero-value standard transaction. This would allow the attacker to use the otherwise-invalid authorization to set the delegation themselves, at which point they could call into an uninitialized contract for undefined behavior (undefined, as redelegation could leave arbitrary leftover storage).

A solution to this problem might be for wallets to issue “cancellation” transactions with an extra no-op delegation embedded to double-bump the EOA nonce, ensuring a pending delegation would be invalidated, but that costs extra gas and would require the user to use an updated wallet for safety.

I’d like to see some official way to condition an authorization on the call of an initializer. I don’t know exactly what that would look like, but it would solve both these problems.

First of all, apologies – this is a long thread, and while I’ve skimmed the recent conversation, I have not read the entire thing. I came here from reviewing the PR in geth, and diving into the EIP itself.

The eip title is, literally “Add a new tx type that sets the code for an EOA during execution”, and the title of this discussion is “EIP-7702: Set EOA account code for one transaction” (emphasis mine). But from the EIP body, and the implementation, I now understand that these changes are permanent/global.

In earlier iterations, this setting of code was temporary - scoped to the transaction. Meaning that whoever who sent the transaction was the one who could to a degree decide what state he wanted to replace. If they control account X, they can decide to temporarily set some code there, and execute code on their own behalf. They could also combine other auths, and “synthesise” the environment which their transaction interacted with.

Change since then seems to be:

  • make the auth nonce mandatory. Nice for security!
  • make setting the code global/persistent. This, in my opinion, is a huge change! This totally throws previous guarantees about code immutability out the window, creating new insta-upgradable contracts. Contracts which change from one transaction to the next, without any form of selfdestruct in-between.

Seems to me as if a gigantic change which should be headlined was just silently slipped into the EIP.

2 Likes

I just updated the EIP description here, so should not have that confusion anymore.

However, I would push back pretty hard on saying this was silently slipped into the EIP. The PR to make the change to the spec was open for a month and was discussed in ACDE 190, 191, 192, in two breakout rooms on the topic (1, 2), and in many more chats on discord and telegram.

3 Likes

While I was not supporting the change itself (seeing the value in temporary delegation in some use cases), I do want to support Matt’s latest point - the topic was indeed discussed heavily and eventually the decisions was made to approve this change after extensive debate on the topic as it was considered as the best tradeoff option between multiple concerns.

2 Likes

My apologies, it was badly put. It was not silently slipped into it. However, it was such a fundamental change that it’s basically a new EIP, in my opinion.

2 Likes

Imagine I’m somebody with significant amount of crypto assets in my EOA. I stumble upon a bad site where I accidentally sign a message to permanently upgrade my EOA to a smart contract that does nothing, permanently bricking my EOA and locking my assets away forever.

Editing my reply since I just saw the comment above with the PR.

That was probably just a suboptimal word choice intended to contrast with the previous (incorrect) “during execution” wording. AFAIK no proposal has been made to introduce a way to allow a delegation to be made permanent.

I’m not sure - this seems pretty clear?

I think it means “permanent” as its used in “permanent residence,” and would perhaps be better described as “persistent.” Otherwise, the “behavior” section would also have had to have been updated to replace

Verify the code of authority is either empty or already delegated.

with simply

Verify the code of authority is empty.

This is a good point. I feel like we iterated quite a bit from where it started. Probably would have been good to freeze the EIP at some point and create a new number. But that also has some overhead cost to coordinate people around a single spec.

Random websites can’t request you sign 7702 txs, you would have to set up an entire wallet and give it your seed phrase.

But yes, you can change the delegation designation for you account or even remove it completely. The key retains control of what code lives in its account.

It’s not a good idea to make a security argument from the absence of a feature. A standard mechanism for requesting delegation signatures will have to exist at some level and at some point to support hardware wallet users, and I would be flabbergasted if it doesn’t get integrated into the standard web3 api. Sure, wallets will probably have a variety of hoops and huge flashing lights before letting you click the button, but its simply wrong to presume that websites won’t be able to request this type of signature. (And if that were a security-relevant issue, it should be in the EIP with a big fat MUST NOT!)

1 Like

We should make this clearer in the EIP that this is the expectation. It’s a free world and wallets can do what they want, however after considering the possibilities I think wallets will understand that they cannot allow this API in an unrestricted manner to their users. This as been a central piece of the 3074 and 7702 discussion for years.

Let me clarify that I don’t take issue with the premise that these signatures are security-critical and that wallets must keep the tightest control over whatever API is used to create these signatures; I just didn’t think the literal words you typed were accurate (that the only way to screw up and make a 7702 signature is to type in your seed phrase because an API for dApps to request one will never exist). I fully support the sentiment that giving someone root will be hard enough that if you screw up it’s your own fault.

Also, I’d like to say that while I’ve been (and will be!) critical of certain aspects of 7702, I’m also very much in favor of it. I want this to happen and I think the ecosystem needs it to happen. It’s potentially dangerous, but I think it can be done right and I want to actively help make sure it is, not by stopping progress but by pitching in to drive it forward.

Thank you for all the work you’ve put into this EIP and the slings and arrows you’ve undoubtedly weathered through the process.

1 Like

I’ve been doing a bunch of analysis of 7702; here’s a summary of the remaining issues I see. I hope it helps.

Delegate Initialization

The Problem

Most contracts need to be initialized. (Even contracts which can correctly handle starting with empty storage won’t be able to rely on that when they’ve been redelegated to.) 7702 currently has a section on the undesirability of initcode which says

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.

I don’t get this; even configurable proxies require initialization, which 7702 offers no simple way to ensure. (I really hope this isn’t just a case of me being obtuse.)


It’s correctly stated by 7702’s security considerations section:

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.

Due to the lack of the ability to atomically call initialization code, however, a contract will need to consider that it may be called in a context where is has not yet been correctly initialized. The only way to detect this state is to use at least one storage slot (a minimal implementation would just contain an “initialized” flag), meaning at lease extra 2103 gas to SLOAD and check the flag on every call. (That’s almost optimally inefficient; almost every call will can be expected to be made while the contract is properly initializated, but will need to pay to check each time.)

Worse, though, the location of this flag is necessarily deterministic. This makes it impossible for a contract author to be certain their initialization code has actually been run, as a user could have previously delegated to a contract which wrote to whatever storage slot is to be used for the flag. We don’t even need to consider the EOA key holder or previous delegation target to be an adversary for this to be an issue! A user could have previously delegated to a legitimate contract which (via bug, oversight, or justifiable exclusion from its own threat model) allowed an unauthenticated caller to set the storage slot that the new delegation target will be using.

It will also tempting for ownable contracts to use the owner != 0x0 as the initialization flag, which would cause problems because the storage slot used for storing an owner value is often the same between contracts. Improving this (not actually making it safe, just disarming the footgun) would require salting the storage slot used for the initialization value with some combination of ADDRESS and CODEHASH (and probably an extra KECCAK256) on every single call.

Best Current Workaround

A user will need to sign two messages to correctly delegate: one 7702 delegation authorization and one covering an initialization payload. This will be a pain for hardware wallet users, and probably leads additional attack surface area; what might happen if the initialization payload signatures and delegation designator signatures can get mixed up? Since a contract can’t rely on storage values before initialization, and can’t access the EOA’s nonce value, the available anti-replay protection for an initialization call is limited to verifying an expiration timestamp in the signed initialization data.

Unfortunately, now we’re stacking workarounds on top of workarounds to fix a fairly fundamental issue, and it would be very very easy to get some of this wrong.

Ironically, one of the only cases where this can be done correctly would be deterministic-signature deployment of a correctly-initialized proxy. The arbitrary data used in the signature fields could be repurposed as a commitment to the calldata of a subsequent initialization function, and such an account can only ever generate one signature, ensuring that no other signed initialization data which can be replayed could ever exist.

Possible Solutions

  1. EOAs submitting their own delegation TX can atomically call a setup function which uses ecrecover to check its calldata. If this is made, by convention, the “correct” way to deploy a certain contract, the contract will never be in a bad state unless the user has, by definition, screwed up. This would avoid the cost of loading and checking an initialization flag after each call, but is incompatible with gas sponsorship, because it only works if the EOA submits the delegation in its own transaction (since the TX itself will bump the nonce value, preventing the delegation from being used by frontrunners).

  2. 7702 could define a well-known “initialization flag” slot and guarantee that setting a delegation resets the slot to a known value. An ecrecover-based technique would still be required to authenticate initialization, which would still require two separate signatures for “real” EOAs, and it would still require the overhead of loading & checking an initialization flag on every call. However, this would be a relatively minimal change to the EIP with limited side-effects and would support gas sponsorship. (For bonus points, I could imagine the slot could be marked “warm” after traversing a delegation designator.)

  3. 7702 could either always clear the account’s storage when setting a delegation or offer a authorization flag which did so. This would share most of #1’s advantages and disadvantages but would be the smallest possible change to the EIP and also ensure contracts started with a known state and couldn’t e.g. forget to initialize leftover security-critical slots.

  4. As #3, but add an extra bytes32 value as part of the authorization tuple which could be used as (or as a commitment to) initialization data and set the well-known initialization flag slot to the provided value on processing the delegation. This would avoid the need to generate two signatures and avoid an ecrecover cost in the initialization function, but it would not fix the need to load and check the flag for each call.

  5. 7702 could define an (optional?) bytes calldata value as part of the authorization tuple and call into the contract with it (if it’s non-empty?) after setting the value. This would avoid the double-signature issue, support gas sponsorship, ensure atomicity of initialization, and avoid the need to check an initialization flag. It would, however, make the gas cost of processing a delegation record variable, make it possible for the call to revert, and generally go against many of the decisions already taken for this EIP.

  6. A second type of delegation designator (say, 0xef0101) could be created which would represent a delegation to an as-yet-uninitialized contract. Such a delegation designator could only be used as the target of a CALL or the destination of a transaction, and only calldata starting with a well-known string (practically, the selector for a well-known initialization function) would be accepted. Such a call would swap the delegation designator from the 0xef0101 to the normal 0xef0100 type. This would avoid the need for contracts to keep track of their initialized/uninitialized state, while remaining compatible with gas sponsorship and suchlike. (Frankly, I’m spitballing on this one; all I can say is it’s less inelegant than some other options.)

Conclusions

I feel that something’s gotta be done because initializing code is such a common usecase. None of these solutions are, however, ideal – hence the braindump. I look forward to someone coming up with a better solution (or even figuring out some convincing argument that it’s not a problem at all).

Re-delegation

A user who wishes to delegate to a contract wallet which is itself upgradable will either be required to accept the extra per-operaton overhead associated with delegating to an 1167-style proxy or the need to break out their EOA key to sign each re-delegation pointing to the new code. Perhaps if we solved this problem by allowing programmatic redelegation we could use it to also solve the initialization issue? A user could initially delegate to a contract that only contained setup code, which could then redelegate to the full contract. (As I understand it, much of the pushback on SETCODE was that we should instead be implementing a first-class protocol proxy mechanism, which is what 7702 effectively is.)

Delegation Introspection

Edit: So if I’d noticed PR #8969 I would have just said I think it’s a good idea and saved everyone reading the next 1k words. Sorry about that.

It’s totally doable

Consider the following arrangement:

A → B → C → D

  • A is an EOA initialized via deterministic delegation signature to point to B
  • B is an EOA with a well-known private key which anyone can use to set a delegation from
  • C is the EOA being introspected for delegation
  • D is a contract being delegated to by C

A contract interested in verifying the delegation target of C runs EXTCODECOPY on A and receives the contents of the delegation designator at B. It verifies that this designator is points to C, and rejects if it does not, possibly with a error message structured to indicate to someone simulating the transaction which delegation authorization needs to be set. The verifying contract can then use EXTCODECOPY on B to retrieve C’s delegation designator, revealing that C delegates to D.

A 7702 transaction can be built, possibly for submission via a 4337-style transaction submission market, which contains the B → C delegation and thereby ensures that introspection is possible atomically.

Conclusions

Introspection via this mechanism is possible. Therefore, there is technically no need to add an “official” introspection mechanism. Future contracts which really care about checking delegations can technically roll their own introspection machinery independently of this EIP.

Current contracts may, however, be broken without the ability to check a delegation designator. A variety of systems use code hash whitelisting – it’s literally the reason EXTCODEHASH was added! Sure, lots of those systems embed a incorrect assumption that the code they can check was correctly initialized by initcode which they can’t, but it is still possible to set up systems that use EXTCODEHASH as a security mechanism and work correctly.

Take, for example, a DeFi vault which can allocate funds to investment strategies represented by contracts. Each of these contracts’ code is whitelisted, and deployment of a new investment strategy consists of CREATE2-ing a fresh EIP-1167 proxy to an account with the known-good code. (I’ve seen a similar scheme in practice before.) One advantage to this solution is bypassing the problem of cross-chain contract deployment having nondeterministic addresses; the code will be the same on each chain, so a signed whitelist permit can be used on every chain even though the new proxy will point to the code via an address which may be different on each chain.

If the demand for this sort of introspection is inevitable, we must accept (or, not to put too fine a point on it, will be tacitly accepting) one of the following things:

  • Permanent on-chain storage of the repeated delegations and re-delegations needed for these checks, as well as one of the following:

    • Increased fragmentation caused by competing implementations of external TX building schemes which support setting 7702 delegations, and the associated loss of decentralization associated with the reliance of a protocol or contract on an possibly limited set of external transaction builders compatible with a fragmented scheme
    • The added complexity in 4337 (or another new EIP) needed to support setting 7702 delegations via a standard external-TX-building mechanism
    • The added complexity in 7702 needed to support setting delegations programmatically (which would enabling introspection via this technique but without relying on external TX builders)
  • The added complexity in 7702 needed to support direct introspection of delegation designators.(Perhaps this could be a new opcode, but it could also be a compromise on the operation of EXTCODESIZE/EXTCODECOPY/EXTCODEHASH for delegation designators).

For this and other storage slot reuse concerns, the Solidity team has been working on a way to compile contracts whose storage layout starts at a location other than 0: ethereum/solidity#597

The idea is that different account templates would use non-overlapping storage layouts.

There’s definitely a big risk of screwing things up. I would basically never recommend someone switch from one account implementation to an entirely different one. At least not without first sweeping the entire storage clean…

This is unlikely to cost this much. The flag can probably be packed with other values that need to be accessed to authenticate UserOps.

I’d also say it’s very possible to have account templates that don’t use storage at all. I’d prefer to use those myself. The signing key can be the EOA itself and accessed via address(this), and nonces are handled by the ERC-4337 Entrypoint. Not sure if I’m missing anything else one would put in storage.

It would be nice to atomically bundle an initialization call with the delegation itself though.

I’m not even sure there’s a practical way to do that on-chain as things stand. Best I can figure, you’d need to redelegate to a stateless slot-wiper contract, enumerate all used storage slots via RPC calls on the client side, build a TX to call into the wiper with the slot keys to clear them, and then redelegate. (That’s a heck of a lot of overhead for (part of) what SELFDESTRUCT used to be able to do for 5000 gas!)

Surely resetting storage (or at least running an initializer of some kind!) is a basic thing that should be possible to keep redelegation from being generally regarded as impractical and unsafe. I can, however, imagine both cases in which storage should be wiped on redelegation (major upgrades, switching wallet “brands”) and cases where it should not (minor updates within the same wallet “brand”).

Maybe the special case of delegation to 0x0, which clears code entirely, should also wipe storage? That would allow a redelegation to be done either directly (keeping storage) or via an intermediate undelegation (wiping storage) and require only a minimal update to the EIP. It wouldn’t be the same thing as atomic initialization, but it would be a significant start!

Can you elaborate on this for me? I can definitely imagine a contract which doesn’t use storage, but the techniques I’m imagining seem like they would imply significant functional limitations.