EIP-7702: Set EOA account code

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