EIP-7702: Set EOA account code for one transaction

It does, but why would a contract store a mapping of accounts’ codehashes when EXTCODEHASH is much cheaper than storage? Besides, making assumptions on user account’s behavior based on code hash would almost always be unsafe because most users use proxies and could change the implementation.

Yes, and my maxNonce proposal addresses that. Currently 63 pending transactions are allowed per EOA. Bumping the nonce should be expensive enough to compensate for that. Such invalidation is currently possible via replace-by-fee, sending 63 transactions and then replacing the first one at +10% cost to a transaction that drains the eth from the account. As long as bumping is at least as expensive as that, it doesn’t introduce a new issue. And since it’s a rare operation (you only do it when switching wallets or when a vulnerability is found in something you previously signed), it’s fine to make bumping expensive.

The 4337 integration of 7702 will prevent this by including UserOp.sender’s codehash in the signature verification. If someone tries to bundle your UserOp while replacing the account implementation with something else you previously signed, it’ll fail validation. Therefore it won’t propagate through the AA mempool, and if a bundler attempts to do it directly it’ll just result in an on-chain bundle revert at the attacker’s expense without achieving anything. Your UserOp stays in the mempool and another bundler will include it properly.

We’ll post something about how 7702 can be integrated into 4337 soon. I think it can be fairly lightweight and solve this kind of issues.

Yes, I thought that’s what people here wanted to do when proposing using an address instead of code. From security perspective the two options are equivalent as long as the signature covers the actual code. Just a different trade off: one time deployment of an implementation that is probably shared by many users, and in exchange we make all 7702 transactions cheaper due to using just 20 bytes.

I don’t have a strong opinion in which one is better, as long as the signature covers the code, but I think others in this thread are concerned about this cost. I have to say I do like the flexibility of including actual code that the wallet can change without deploying another contract. It enables some use case that would be expensive otherwise, for things like intent solvers. Maybe it’s worth the extra ~30 bytes cost.

1 Like

I’m not sure I follow. So the destination of the transaction would be responsible for checking the codehash of each signer? Against what?

I have to apologize again for not fully understanding all the 4337 specifics.

The scenario I’m worried about would be a bundler adding an item to the transaction without adding it to the bundle, so something is deployed at the token recipient’s address, even though the token recipient has not submitted a UserOp recently.

Not the destination of the transaction. UserOp.sender, during its validateUserOp. The account can get its own codehash and require that UserOp.signature covers it as well. If its implementation is not the one intended in the current UserOp, then it fails validation and the bundle would revert if it tries that. In that case the UserOperationEvent is not emitted and the UserOp stays in the mempool for some honest bundler to include it with the right implementation in the 7702 transaction.

because I’m only explaining a part of the 7702-4337 integration here. It’ll become clearer when we publish the whole integration. We’ll post about it soon.

1 Like

The purpose of this EIP was to let existing eoa to run code. Using different signature creates a new address, and enshrine yet another signature into the protocol.
Our goal is to gradually switch over to AA, where the contract controls the signature scheme

1 Like

Putting an address has the benefit of using shorter vanity addresses, say 16 bytes…

The point is that you’re using a code that was deployable on this network (you copy a code that used a constructor)

Is this not analogous to handing a signed (but unbroadcast) transaction to someone, and now that someone can also bump your nonce?

A signed (and not broadcasted) transaction must be signed, and can only be used one. In the case of 7702, the EOA doesn’t fully control if the signature is used multiple time. If the code signed includes (for any reason, accidentally or maliciously) a non restricted access to CREATE, then that be leverages for DoS attack on the EOA. Signed (and not broadcasted) transaction doesn’t present the same long-lasting risk

geth (and I assume other clients) protect against similar attacks by limiting the number of pending transactions per account.

I’m not sure how this is relevant. This issue can appen with just one or two pending transactions, which is definitelly within the limit of the clients.

Is using 7702+CREATE /CREATE2 /etc. worse than invalidating pending transactions using a private mempool and a number of normal transactions?

I’m not sure how the client update the mempool when processing new blocks. If it is only considering nonce increase for the origin of each tx in a block … then it would be needed to also observe nonce change that are more “hidden” in the execution trace.

Note that this will also mess up with the wallets that cache the nonce for the signer (I remember having many issues with this a few years ago, no sure if it is still a thing). For sure that is offchain code that can be updated …

If the contract_code has even the most basic nonce/replay protection, they are equivalent then? And if we have in-protocol revocation (which I’m not in support of), you can revoke the CREATE-ing code.

It’s relevant because clients already have protection against similar attacks, which would also protect against this one.

They’d just have to expand what’s under consideration to include the signers in these bundles (in addition to tx.origin.)

No worse than using two different wallets with the same seed phrase.

I very strongly support in-protocol revocation. I think the protocole should not assume that all contract_code will be be bug free. There will be code that has issue weither that is a malicious action of just by mistake.

If we go with a in-protocole revocation, it is likelly going to be based on some transaction signed by the EOA … which is exactly what the CREATE-ing code can DoS. In the worst case, an attacker would have a user sign a malicious code, and would put an open CREATE in it, so that if the user ever realise it signed something malicious and want to revoke it, the revocation can be front-run in a way that consumes the nonce.

1 Like

Understood, yeah I can’t think of a way to make the nonceManager solution safe to use without enforcing associativity.

My concern with the maxNonce solution is that it introduces a bumpNonce operation specifically for revocations and I’m unsure of how we’ll decide on an appropriate maxNonce, but if that’s significantly less effort than enforcing associativity then perhaps it’s the more practical solution.

No, because it closes off security avenues.

If the code/initcode is run it can include things like chainId or other parameter constraints (.e.g timestamp expiry) which is then signed and they can be set with TSTORE to form constraints for the outer signer which then pays for and passes data to (proxy) contract.

If its just contract address, then the inner signer cannot sign any additional constraints on the invocation. (While leaving what those additional fields are flexible and up to ERC standardisation)

I feel like I’m not communicating my concern well enough :sweat_smile:

When you transfer an ERC-1155 token, if the recipient account has code, the token contract will call onERC1155Received on the recipient account.

If onERC1155Received reverts or returns the wrong value, the token transfer reverts.

The attack is as follows:

  1. I sign any 7702 operation that doesn’t contain an onERC1155Received implementation.
  2. You try to send me an ERC-1155 token using a 7702 operation.
  3. The bundler includes my operation from (1) followed by your operation from (3) in the 7702 transaction.
  4. Your token transfer reverts because my account has code.

My EOA is not submitting a 4337 UserOp in (3) or (4).

The bundler can’t include your operation if it’s not a UserOp.

But I understand your concern, even though not exactly with the flow you described. You’re right that the bundler could include your previously-signed contract code in the 7702 transaction it is sending, even though it doesn’t include any operation from you. So if someone else submitted a UserOp that is making an ERC-1155 transfer to your account, a malicious bundler could make the transfer revert. As a user you have no way to defend yourself against such attacks, other than revoking the signature on that contract you previously signed.

It could be mitigated with some best practices. For example, wallets should not sign implementations that miss common callbacks such as onERC1155Received.

I think choosing a maxNonce is not too tricky. Any 32 bits number of transactions that you’re unlikely to ever reach will do. It is probably safe to assume at a user will never send 100 million transactions, and you can still safely bump the nonce by 100 million for revocation without getting anywhere close to the 64 bits nonce limit.

Some confusion around the maxNonce proposal arises from the fact we’re overloading the nonce for something other than replay protection. Revocation and replay protection are two separate features and ideally we would have used two separate values. The reason we’re using nonce is because it’s already in the account trie and it’ll be best not to add another value to it. You can look at it as if we’re using the high bits of the nonce for revocation, and the low bits for replay protection.

Similar to the topic of disallowing storage writes in the context of an EOA (which I am in favor of), are there things that could break if EOAs can emit events? In my mind this would only be offchain infrastructure but there could nonetheless be dangerous edgecases, such as wallets thinking a certain signer is installed since an event was emitted but that signer actually not having been set. Arguably, the offchain infra should change but I wanted to flag this anyways.

2 Likes

It could be mitigated with some best practices. For example, wallets should not sign implementations that miss common callbacks such as onERC1155Received.

This could nonetheless still be an issue if an account implements logic to block some transfers in certain cases

Yes, that’s exactly what I was trying to describe. Included in the 7702 transaction without being in the 4337 bundle.

You cannot mitigate against callbacks that haven’t been invented yet.

You have to actively revoke, even though you might not know it’s a problem. Even worse with multiple chains.

1 Like

Right. You can revoke and sign a new implementation when new ERCs with callbacks become common, but you might not know it.

That’s a caveat when implementing something with replayable signatures. Do you see a way to mitigate it without adding replay protection?

Right, an implementation may intentionally block some callback, although it’s hard for me to find a good example in the 7702 case. The contract only exists during the transaction. Suppose you want to prevent ERC-1155 transfers from blacklisted addresses - then you’re only blocking them for the duration of the current transaction but anyone can transfer it to you in a separate transaction. If your account needs to prevent certain transfers it should have permanent code.

1 Like

Yes, use initcode instead of deployed bytecode. The contract can check whatever conditions it wants before choosing to deploy.

4337 bundlers can have a list of known-safe initcodes, do simulations, or some other mechanism I think?

Suggested revocation mechanism:

  • In EIP-7702 transaction type, add a flag for each account’s tupple e.g (data,r,s,v,bump)
  • If the flag is set, bump the nonce (e.g. by 2^32) after the execution of this transaction.
    This way, this transaction is only one that can use this signature, since the nonce was bumped.
    Obviously, it revokes all other signed signatures as well

This model has several benefits:

  • no extra opcode or transaction type added.
  • the “bump nonce” operation itself can use gas-abstraction framework.
  • it is clearly visible in the transaction itself that it is a nonce-changer, so block builder can infer the validity of other transactions used by the same account.
1 Like

What conditions would initcode check in this case to refuse deployment? It gets called before anything else happens in the transaction so it doesn’t know that it’s going to be used in a bundle that triggers such a transfer. A concrete example would help here.

And if it could somehow check, it would involve state access and would therefore be unusable in the 4337 context.

Nothing in 4337 is whitelisted/permissioned and that’s not going to change with 7702. Bundlers should not whitelist account implementations and exclude others. Any account that adheres to the ERC-7562 rules should be served.