ERC 4337: Account Abstraction via Entry Point Contract specification

Exactly. The wallet is the flexibility (and responsibility) to use whatever signature scheme.
The “SampleWallet” we provide use this EIP191 “Ethereum Signed Message” signature. We also add reference implementation that uses BLS signatures

Is the current draft (EIP-4337: Account Abstraction via Entry Point Contract specification) up to date? I’m working on implementing 4337, but I see some inconsistencies between the EIP and the EntryPoint implementation.

For example, the EIP states the following rule:

Any GAS opcode is followed immediately by one of { CALL, DELEGATECALL, CALLCODE, STATICCALL }.

however, in the implementation repository, the example contains the following code:

  //pay required prefund. make sure NOT to use the "gas" opcode, which is banned during validateUserOp
  // (and used by default by the "call")
  (bool success,) = payable(msg.sender).call{value : requiredPrefund, gas : type(uint).max}("");
  (success);
  //ignore failure (its EntryPoint's job to verify, not wallet.)

So is not clear if GAS is allowed or not (when used before CALL, DELECATECALL, etc.). This is an important detail because proxy contracts (see EIP-1167) use the GAS opcode when forwarding the call. If this exception to the rule doesn’t exist, then wallets that use these proxies wouldn’t be compatible.


An unrelated thing:

Also while simulating the op there are a list of rules the client must enforce, but I see CALL to external contracts is allowed as long as value = 0, I think this is not enough to stop a 3rd contract from invalidating a large set of user operations:

  1. During validation CALL address X with value = 0 and any data, address X is a non-deployed contract so it doesn’t have code, the call doesn’t fail.
  2. Deploy a contract at address X, the contract calls address Y and it reverts if the call doesn’t fail.
  3. Deploy a contract at address Y, the contract calls address Z and it reverts if the call doesn’t fail.
  4. This enables toggling an arbitrary number of operations between valid and invalid.

This can be built as an ever expanding chain of NOT gates, I think a way to mitigate this issue is adding a rule that during validation the wallet is not allowed to call addresses with empty code.

The latest changes haven’t been merged to the official repo yet. They’re mainly related to signature aggregation but contain a few other minor changes. It’ll be merged very soon. The place to see the latest pending changes is https://github.com/eth-infinitism/account-abstraction/blob/develop/eip/EIPS/eip-4337.md

It is allowed. GAS is a forbidden opcode but there’s an exception to allow it just before *CALL which immediately consume it from the stack. The rationale is that the code should not be able to access this information and change the flow based on it, but calls are fine. Since the GAS value is not available to the code, the only way it could affect the flow is if a function runs out of gas. But rule 7 precludes that, by banning out-of-gas calls:

  1. No CALL, DELEGATECALL, CALLCODE, STATICCALL results in an out-of-gas revert.

So the exception described in the EIP is the correct one. The EntryPoint code also doesn’t enforce it since it’s handled by the bundler. What’s wrong is the comment in the code, which doesn’t mention the *CALL exception.

At that point rule 9 kicks in:

  1. EXTCODEHASH of every address accessed (by any opcode) does not change between first and second simulations of the op.

Any op that accesses address X gets dropped from mempool without simulation. What the EIP tries to avoid is having to resimulate a large number of ops in order to drop them. During the first simulation, the bundler saves a list of accessed addresses. Since any code change would trigger rule 9, these ops would be invalidated without additional work.

That actually wouldn’t mitigate the issue, since the contract at address X can be selfdestructed and redeployed differently each time by using some well known constructor tricks. The attack would start with a contract that doesn’t revert at address X, then toggle it by selfdestructing and recreating (in a single transaction) in order to invalidate a large number of ops. But I think rule 9 above does offer sufficient mitigation. Do you see a way around it?

1 Like

What’s stopping the wallet from re-broadcasting the transaction? you could mutate the transaction a bit, and re-broadcast, thus spamming the mempool without any additional costs. I assume the the client can block the wallet, but then a sort of “per-wallet reputation” starts to play out too.

Yes, but I’m assuming that SELFDESTRUCT will get deactivated soon enough.

We’d rather avoid per-wallet reputation. We only have that for paymasters. But this attack does have a cost to the attacker, probably higher than the damage it causes. In order to propagate through the mempool, the op must be valid at the time of propagation. The attacker has to invalidate it after it has been propagated, by deploying a contract. The attacker also has to deploy a large number of wallets in order to fill the mempool, because each wallet can have only one op in the mempool at any given time.

So the attacker has a one-time setup cost of O(concurrent_ops) for setting up the wallets, and then O(iterations) for deploying a new contract on each iteration. And the damage is a single off-chain simulation for each iteration of the attack, since the 2nd simulation never happens due to rule 9.

I agree it would be better to mitigate it entirely, rather than relying on the cost and unprofitability of the attack. But how would you block it without breaking too much functionality? Preventing calls to accounts without code is a good idea and shouldn’t break anything, but doesn’t block the attack due to selfdestruct and recreate.

It’ll be deactivated, but I don’t know about “soon enough”. We did consider having a rule where contracts touched during validation must not have the selfdestruct opcode, but it wasn’t good enough because the contract could delegatecall to an unknown address (specified in the op rather than in the code), and that address could selfdestruct. Preventing delegatecall to unknown addresses seems too harsh.

1 Like

Thanks :slight_smile:

Cautiously optimistic.

How would this EIP in your mind fit into the context of MEV-boost and current builder market dynamics? Could this also potentially be used to implement/augment some form of “re-staking”?

Can the graph in the EIP be simplified to this?

It can, but then you’d miss the fact these are separate calls to separate wallets

2 Likes

Is there any room for adding the transaction types in this proposal? For instance, each transaction will be described as a blob of bytes, where the first byte denotes the type of the transaction. This way it will be easier to keep backward compatibility in case some new types will be added. The question is risen mostly because L2s have different fee model: the price for L1 pubdata is dynamic.

Thus, for instance, zkSync has a separate field ergs_price_per_pubdata.
Arbitrum has similar plans as well.

And the base L1 itself is far from being fully finalized. Thus, new transaction types/execution variables may still be introduced. This way it would make the protocol more future-proof.

1 Like

Hello there!
I’m trying to understand the motivation of the bundler to do his job.
An I correct in assuming the preVerificationGas is always paid entirely so it can be arbitrarily large and used as a tip for the bundler?

The motivation is equivalent for a block builder to include any transactions: gas fees premiums.
The builder pays the “outer transaction” fee (probably to itself, if it is the build), and receives from each UserOp a fee based on userOp.priorityFee (and userOp.maxFeePerGas)
preVerificationGas can be used to tip the bundler a fixed amount, but its original purpose was to cover the gas cost that can’t be computed on-chain - the calldata cost, the fixed overhead (21000) and some EntryPoint contract overhead.

1 Like

The whole idea of the protocol is to check as much as possible from the UserOp in the EntryPoint contract, including the actual gas calculation.
Introducing a byte blobs defeats that purpose.
There is one “placeholder” for external gas calculation per network, namely preVerificationGas. It is assumed to be a function on the data in the UserOp and some network-specific parameters (like the 21000 stipend and the zero/nonzero byte costs)

Thanks you for the answer. It turns out that in order to benefit from the execution of UserOp, the value of UserOpGasPrice must exceed the gas price was used when sending the transaction (except of expenses that can’t be computed on-chain), am I understanding correctly?

Is there any calculations of the potential benefits of bundlers?

Hello friends.

Can you tell me what is my proof of ownership of this or that abstraction account? Isn’t my EOA private key?

I suggest using EIP-2535 Diamonds to implement the smart contract wallet for EIP-4337.

EIP-2535 is a proxy contract standard that can use more than one implementation contract (facet). This makes it relatively easy for uses to add additional contract functionality.

For example you could have a smart contract wallet implemented as an EIP-2535 Diamond that implements all the basic functionality needed and required by ERC-4337. Now if a user wants their smart contract wallet to have additional functionality the user can approve an upgrade on their diamond to add functions from another facet or facets to their diamond that has the functionality they want. Developers can develop different facets with different functionality for smart contract wallets. Users can then pick and choose and approve which facets and functionality they want to add to their smart contract wallet.

The facets of diamonds can be deployed one time and and then reused on-chain. So for example it is possible to create a registry of safe and compatible ERC 4337 account abstraction smart contract wallet facets that are deployed once. Each user can then deploy a diamond that uses a facet that provides the standard/required smart contract wallet functionality. From there each user can then choose what other functionality he or she wants their smart contract wallet to have and add the appropriate facets from the registry to their smart contract wallet diamond. The facets are reused-on chain, not deployed over and over again.

A diamond provides unlimited smart contract functionality at a single Ethereum address. Smart contract wallets can leverage this to provide functionality needed/desired by users without hitting the max contract size limit. EIP-2535 provides other benefits such as tooling, interoperability, multi-contract transparency, gas-efficiency, and code organization, which can be utilized.

What I am describing here is not just theory. Diamonds are being used by more than 70 projects.

A user interface that uses the standard to show deployed diamonds is here: https://louper.dev/

An introduction to EIP-2535 is here: Introduction to the Diamond Standard, EIP-2535 Diamonds

More resources and information about EIP-2535 are here: Awesome Diamonds.

1 Like

There is no EOA involved. Your proof of ownership is whatever you set up in the account’s validateUserOp() function. If you implement this function to ecrecover the signature (from UserOp.signature), then it works similarly to an EOA (except that you can rotate the key without changing your address. If you use a different signature scheme, then your proof of ownership is the key of whatever signature scheme you used. If you implement a multisig or any other complex “signature”, then your proof of ownership is whatever the logic requires.

2 Likes

Yes. ERC-4337 wallets could greatly benefit from using DiamodStorage and I hope to see wallet developers use it.

The benefit goes beyond just adding functionality via new facets. It also keeps accounts safe when switching them to a new implementation. Wallets are likely to use proxies, and the users may want to switch implementations. For example, a user might start with a simple wallet, and later decide to switch it to a multisig or a wallet with social recovery. There’s a risk that the previous implementation’s storage will break the security of the new implementation by leaving unexpected storage behind. I demonstrated it by leaving shadow signers in Gnosis Safe.

If both implementations were EIP-2535 facets that use DiamondStorage with different base positions, switching between implementations becomes safe.

5 Likes

The main concern I have is that facets used as extensions should use their own storage slot, and if using the common functionality’s slot, that it not modify the slot’s storage structure, even in a way that should be safe. It could play badly with another available facet that makes its own ‘safe’ changes to the structure.

Yes, I agree. As @yoavw said, it makes sense for different facets to use their own Diamond Storage with their own separate slot positions.