ERC-4337: Account Abstraction via Entry Point Contract specification

Disagree here with providing complex ways of finding a UserOperation .

What has provided the best dapp ux is submitting a UserOperation and the bundler returning the transactionHash. This allows the dapp to await success and handle an error efficiently.

You can potentially also be optimistic about a UserOperation for certain operations that are non-blocking.

Agree with returning transactionHash by eth_sendUserOperation, which can help retrieve the UserOp, and this does not conflict with adding transactionHash tag in eth_getUserOperationReceipt, or maybe directly using a new API such as eth_getUserOperationReceiptByTransactionHash. Furthermore, returning transactionHash can also solve the issue mentioned by @alex-forshtat-tbk that The end-user, dapp, or wallet likely do not know the transactionHash of a transaction containing the UserOperation.

1 Like

I’ve been working on PrepaidGas, a decentralized, ZK-powered paymaster, and have identified a potential issue while implementing the decentralized paymaster solution. I’d appreciate your thoughts on:

The Challenge: DDoS Vector against Bundlers (without fund loss) When a user effectively acts as their own paymaster service (as in our decentralized model), a malicious actor could theoretically spam bundlers simultaneously with the same valid gas ticket (i.e., a UserOperation with a valid ZKP and paymaster context).

  • Result: Bundlers would successfully validate these identical UserOps (as the ZKP is initially valid), but only the first one to hit the chain would successfully execute (e.g., consuming the nullifier). The subsequent ones would fail during execution.
  • Impact: While no funds are lost from the paymaster, this wastes significant bundler resources. It’s akin to the UserOperation nonce preventing account-level replays, but there’s no equivalent “paymaster nonce” for such concurrent spam. This could damage the paymaster’s reputation and lead to bundlers deprioritizing or censoring its UserOps.

Why a Solution is Important (and respects separation): I believe introducing a mechanism like a “paymaster nonce” within the UserOperation could mitigate this. This wouldn’t violate the Bundler-Paymaster separation; it would simply be:

  1. Bundler Infrastructure Protection: A crucial anti-spam primitive for bundlers, protecting their vital infrastructure.
  2. Enabling Decentralized Paymasters: It would empower truly decentralized paymaster models like PrepaidGas to operate robustly without relying on centralized off-chain components solely for anti-spam.

I’m keen to hear any concerns or thoughts on this. Is this something already considered, or are there existing ideas/solutions in the pipeline for such scenarios without a centralized party?

Hello @karandeep,

Thanks for an interesting question. It would be great to learn more about the architecture of your paymaster, is it open-source? If so, could you please share a link to the repository.

Regarding the issue with a DDoS vector, you are correct that in the current ERC-4337/ERC-7562 design it is the Paymaster’s responsibility to provide an on-chain mechanism to identify and rate limit the end users, and even further, a Paymaster that fails to do so may be blacklisted by bundlers in case it is used by an attacker to DoS the bundler.

Clearly this is a challenge for the Paymaster that has no advance information about its users.
As far as I can understand the situation, this means that your Paymaster should not be “staked” in the definition of ERC-7562.
Not being staked will limit the number of UserOperations using this Pyamaster in the mempool to SAME_UNSTAKED_ENTITY_MEMPOOL_COUNT = 10, so an attacker will be able to create and broadcast 10 UserOps using the same gas ticket, and only pay one of these.

If you really need your Paymaster to be staked, you will potentially need to add an additional DoS prevention mechanism.

However, it is not clear yet if the “paymaster nonce” is necessary here, as the existing UserOperation nonce parameter already consists of two components. Introducing extra fields to the UserOperation would require a major breaking change at this point.

The nonce field is considered to be related to the sender but it is visible to the Paymaster in the validatePaymasterUserOp callback.
Do you think you would be able to solve the DDoS issue by using the uint192 key component of the nonce field?

1 Like

I don’t think it is an attack against the bundler:
as @alex-forshtat-tbk said, there can only be so many UserOps for that paymaster in the mempool (currently 10), so that’s the number of UserOps that can be invalidated.
In order to have more UserOps in the mempool, the paymaster has to be staked. But if an attack can still cause invalidation of a large # of UserOps, then the paymaster gets DoS’ed , not the bundler…
In order to prevent such DoS, the paymaster must use its own storage.
e.g: in most ZK consructs, there is a “nullifier”, which can be checked whether the proof was already spent.
As long as these nullifiers are independent (that is, have 1-1 relation to a proof, and no 2 proofs can invalidate the same nullifier), the paymaster is safe from such a DoS attack.

1 Like

Hello @alex-forshtat-tbk and @dror,

Thank you both for your valuable insights in the context of ERC-4337 and ERC-7562. Here are the links to the prepaid-gas paymaster repository and a demo video, for those interested in exploring further:

Regarding the DDoS concern for PrepaidGas:

As both of you highlighted, and Dror explicitly mentioned, implementing nullifier tracking in paymaster storage is key. PrepaidGas Paymaster does exactly that with nullifierGasUsage and usedNullifiers, ensuring our funds are safe from on-chain replays and double-spending of ZKP-backed gas tickets.

Regarding Alex’s suggestion of using the userOp.nonce.key to leverage existing fields, my primary concern is that some smart account implementations use the userOp.nonce field for their own internal logic, like in the case of a modular smart account representing a module address. Forcing our Paymaster’s UserOperations to use this field for a different purpose could create compatibility issues with a wide range of existing Smart Accounts.

As highlighted, for an unstaked Paymaster, the SAME_UNSTAKED_ENTITY_MEMPOOL_COUNT (currently 10) directly limits the number of UserOps a single bundler will hold for us. While this offers excellent anti-DDoS protection for bundlers, does it risk throttling legitimate traffic for decentralized paymasters like PrepaidGas?

Should PrepaidGas Paymaster become staked, the challenge then becomes how to enable bundlers to accept higher volumes of UserOps without disproportionately burdening them with simulations of transactions that will ultimately revert due to an already-consumed nullifier.

Appreciate your feedback and looking forward to your thoughts!

The paymaster should be staked, so it can use a mapping of nullifiers.
and as long as a nullifier is “immutable” (that is, once issued can’t become invalid - unless it is explicitly withdran), it is safe from spamming bundlers (and from users to DoS the paymaster, by forcing it to spam bundlers…)

Hello everyone!

Please notice that ERC-4337 has been moved to Last Call, with Last Call Deadline = 2026-05-26.

If there is any change you would like to propose for the ERC-4337 specification, it will not be possible to do so after that date.

Please post here if there is anything that in your opinion should prevent ERC-4337 from being finalized.

Thank you!

2 Likes

Related to the earlier discussion here about using the nonce key’s uint192 component: under Glamsterdam, now live on Sepolia, the first use of any nonce key costs about 98,700 more gas than reusing one, and that cost lands inside verificationGasLimit. Numbers below.

EntryPoint v0.7 on Glamsterdam: the first use of a nonce key now costs ~99k gas inside verificationGasLimit

What changed

EIP-8037 reprices state creation. Writing a storage slot that was zero now costs 97,920 state gas (20,000 before). On the Sepolia node, each additional new slot in a call measures 110,026 gas in total, execution included.

Why it lands in verificationGasLimit

In EntryPoint v0.7 (0x0000000071727De22E5E9d8BAf0edAc6f37da032), _validatePrepayment measures gas from before account validation until after the nonce update, and only then checks the limit:

uint256 preGas = gasleft();
...
validationData = _validateAccountPrepayment(opIndex, userOp, outOpInfo, requiredPreFund, verificationGasLimit);
if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) {
    revert FailedOp(opIndex, "AA25 invalid account nonce");
}
unchecked {
    if (preGas - gasleft() > verificationGasLimit) {
        revert FailedOp(opIndex, "AA26 over verificationGasLimit");
    }
}

_validateAndUpdateNonce increments nonceSequenceNumber[sender][key]. The first userOp that uses a given 192-bit key (sequence 0) writes that slot for the first time. So any userOp that opens a new nonce key now needs about 98k more verification gas than its account’s own validation, and fails with AA26 if its limit doesn’t include it.

What I saw on Sepolia

8 smart accounts on one paymaster failed 164 bundles with AA26. Each op used a fresh nonce key, so after the fork every op paid for a new nonce slot inside verificationGasLimit. Their limits had no room for it. They recovered at 17:05 UTC when the limits went up.

  • 164 handleOps transactions reverted with FailedOp(0, "AA26 over verificationGasLimit") from one bundler’s executors. They were 73 distinct userOps from 8 smart accounts (Kernel via EIP-7702, Safe, SimpleAccount), resubmitted 1 to 14 times each (median 2); the executors paid about 0.045 Sepolia ETH for the failed bundles.
  • Every one of the 73 used a non-zero nonce key with sequence 0: the first use of that key. None had initCode, so account deployment was not the cause.
  • The failing ops carried verificationGasLimit values of 41,810 to 79,833, below the cost of the nonce write alone.
  • Ops from the same accounts with limits of 233k to 259k succeeded after the fork. 228 of 350 bundles failed between 13:54 and 17:02 UTC, 1 of 31 between 17:05 and 17:45 UTC.
  • Fresh bundler estimates were not the problem: the estimators I read (alto, rundler) simulate the EntryPoint’s own _validatePrepayment on the node, nonce update included, so on an Amsterdam node they cover the new slot.

What a new nonce key costs now

Measured on the Sepolia node with eth_estimateGas of EntryPoint.incrementNonce(key) (the same storage write a userOp’s validation makes), a fresh key against a reused one:

rules fresh key reused key extra for a fresh key
after the fork 127,095 28,438 98,657
before the fork (block 11856335) 44,370 27,236 17,134

Opening a new key now costs about 81,500 more gas than it did before the fork.

Who is affected

  • Apps that set verificationGasLimit without a fresh estimate (a fixed or cached value) and open a new nonce key per userOp: they fail with AA26 until the limit includes the slot.
  • Apps and SDKs that open a new key per op but estimate fresh: no failure, but every userOp pays the higher cost. viem’s toSmartAccount defaults the key source to Date.now(), and several account implementations built on it pass that key through, so their default is a new key on every op.

Fix

  • When building a userOp whose nonce key has sequence 0 (EntryPoint.getNonce(sender, key) returns key << 64), add the cost of the new nonce slot to verificationGasLimit (98,657 more than a reused key, measured on Sepolia), or re-estimate on an Amsterdam node with the exact nonce you will sign.
  • If parallel nonces are not needed, reuse a key: only the first use of a key pays for the slot.
  • Bundlers: an op that failed AA26 will fail again with the same limits; drop it or re-estimate instead of resubmitting.

How to check your own ops

Trace a reverted bundle on a Glamsterdam node (debug_traceTransaction with the callTracer): the output decodes to FailedOp(opIndex, "AA26 over verificationGasLimit"). Decode the bundle as handleOps(PackedUserOperation[],address) and read ops[opIndex].nonce: the high 192 bits are the key, the low 64 bits the sequence. Sequence 0 with a non-zero key is this case.

cc @dror @alex-forshtat-tbk, since this lands in EntryPoint v0.7’s validation path.

Write-up drafted with help from an AI assistant; every number comes from my own runs and the commands in the repo.

2 Likes