ERC 4337: Account Abstraction via Entry Point Contract specification

Yes, the wallet could check its own balance this way, much like it could check its own storage. The user would be able to invalidate an op that is already in the mempool by using a non-op transaction to change the wallet’s balance.

However, the wallet can only check its own value this way, so it can only be used to invalidate the op of a single wallet at a time. The wallet could similarly use a separate transaction to update its nonce and invalidate the op.

Your question does highlight an important point - that the client should treat the value as part of the account state, and not just the storage. I.e. if a wallet calls a function in a 3rd party contract, which doesn’t access storage but does attempt to send value, the clients shouldn’t accept it. Otherwise this could be used to invalidate ops of multiple wallets.

The EIP specifies this condition: The first call does not access mutable state of any contract except the wallet itself and its deposit in the entry point contract.
I now edited it to clarify that mutable state includes both value and storage. Thanks for bringing it up.

The simulation is performed against the latest state while building the block. It is the block proposer’s responsibility to ensure that an earlier transaction in the block doesn’t invalidate an op. The simplest way to do it is what you suggested - make the handleOps call in the first transaction in the block. A client could simulate it against a mid-block state or use access lists to prevent conflicts, but making it first is easier. The current client implementations already do this, but I now added a comment to the EIP to make this requirement clearer.

Thanks for highlighting these points! Client developers need to take them into account.

@tjade273 thinking some more about value-bearing calls, I realized that our current protection (dropping validations that change any account balance except the wallet and the entry point) is not good enough. The value-bearing call you suggested could be a self-call by some 3rd party account, so there’s no balance change. So the current protection won’t stop this DoS:

  1. Wallets call EvilContract.func() during validation.
  2. EvilContract.func() attempts to call its own receive function with 1 wei, reverting if it fails. When it has 1 wei it is not caught by the current protection because the balance remains 1 wei.
  3. Attacker sends ops from 1000 wallets with this validation function while EvilContract has 1 wei. Validations succeed and the ops are accepted to the mempool.
  4. Attacker tells EvilContract to send the 1 wei elsewhere.
  5. All ops fail validation in the 2nd simulation.

We’ll update the EIP to also ban value-bearing calls during validation, except from the wallet to the entry point.

Thanks again for your valuable comments.

@yoavw yes, this is essentially the attack I had in mind. I assumed that executing a value-bearing call in some sense triggered the

does not access mutable state of any contract except the wallet itself

in your last post but yes it bears being specific that this applies also to implicit reads via value-bearing calls.

Another interesting corner case I’ve been thinking about is:

Wallet calls C1, passing it say 500 gas. C1 attempts to call C2 (which just returns immediately). Since gas cost for first accesses is metered at a higher rate, the call to C1 succeeds exactly when C2 has been accessed before. So in simulation if the calls are simulated separately as they arrive at the mempool, they will behave differently from when they are all run together.

It seems like this can be used to form a large scale DoS, unless all the userOps are simulated together in a batch (is this the case?).

1 Like

The ops are also simulated together as eth_estimateGas of the entire handleOps transaction, but we do try to avoid cases where this simulation fails, because it creates more work. If it does fail, the simulation returns the index of the op in the bundle as an arg of the FailedOp error, and the bundler removes this op.

The method you described would indeed pass the single-op simulation and then fail the eth_estimateGas, which will cause it to revert with a FailedOp and specify that op and retry.

It’s not as effective as the previous attack (value-bearing self calls) since it only invalidates ops in the current bundle rather than the entire mempool, and the attacker ends up paying for the first op in every bundle because this one is valid and not removed. But it is still a nuisance to the bundler which has to eth_estimateGas multiple times and only gets paid for one valid op.

I wonder if we should mitigate. We could require all calls during validation to provide max gas, which would prevent this vector and hopefully shouldn’t break any valid use case. On the other hand, the attack doesn’t scale well for the attacker because it only causes some off-chain work while paying on-chain costs in each bundle. What do you think?

The attacker may be able to get away with less than 1 successful op per batch if they test for previous accesses to an account that is likely to be called regardless of the attacker’s userVerify activity.

For example, a common paymaster that is likely to be used early in the batch, or even the externally owned account of the bundler themselves.

I’m trying to reason through the expected burden on a bundler due to this: is there any rate-limiting we can do to prevent users from filling up the mempool with a huge number of these invalid userOps? If the attacker sends 100k requests with the same wallet but different nonces, do these all get added to the mempool and crowd out real transactions? If so, maybe some sort of wallet blacklisting is warranted.

No, each wallet can only have one op in mempool at a time. The attacker needs to use 100k contract wallets in order to send 100k concurrent ops.

But… it may be possible to implement your attack without actually paying for these deployments. When a new wallet is deployed via EntryPoint, its validation is immediately called and the deployment is reverted if validation fails. You could craft a wallet that almost always reverts its own deployment op, costing the attacker nothing.

So you convinced me, we should add a max-gas rule during validation. We already require the use of fixed gas since we ban the GAS opcode, so we might as well require it to be max.

Thanks & keep them coming! :slight_smile:

1 Like

I think even with the max gas limitation there’s a similar issue.

The attacker just needs to calculate and set the verificationGas such that C1 runs out of gas only when address C2 is not primed. Then if the call to C1 fails, pay the EntryPoint and if it succeeds, revert. There should be enough gas left over for this due to the 63/64ths rule (we can make the leftover gas as large as necessary by burning a bunch in C1).

One potential mitigation is to simulate the operation, then resimulate it with all of the called addresses primed, and make sure the contract pays in both cases.

Alternatively you could disallow all reverted calls, or just OOG calls, in the stack. This should work since the “real life” calls will always take at most as much gas as the simulated calls, so if no calls run out of gas in the simulation then they shouldn’t in “real life” either.

I’m not sure the 63/64 rule leaves enough gas to do anything in this case, since nodes won’t accept a high verificationGas op due to the risk of unpaid work. But you’re right, there’s a risk that this could be exploited, and the max gas change doesn’t mitigate it.

That won’t solve the problem either, because the contract could use a combination, expecting some addresses to be primed and others not. E.g. succeed if no addresses are primed or all addresses are primed, but fail if 2 are and 3 aren’t.

I think this is the way to go. The client should drop the op if there’s an OOG revert in any context. Thanks for suggesting that.

PEEPanEIP-4337: Account Abstraction via Entry Point Contract specs. with @yoavw @kristofgazso

1 Like

Reference to another great presentation about this proposal:

Slides: ETHAmsterdam ERC 4337 - Google Slides

Main links from the slides:
Contract code: account-abstraction/contracts at main · eth-infinitism/account-abstraction · GitHub
Audit blog post: EIP-4337 - Ethereum Account Abstraction Audit - OpenZeppelin blog

4 Likes