---
title: "ERC-4337 UserOperation packing vulnerability"
description: "On March 7th, 2023, Alchemy and members of the open source community, identified calldata decoding issues with the ERC-4337 EntryPoint contract and the example VerifyingPaymaster contract."
---

# ERC-4337 UserOperation packing vulnerability

**Additional Contributors**:
[fangting@alchemy.com](mailto:fangting@alchemy.com), @drortirosh, @Gooong, @taylorjdawson, @leekt, @livingrockrises

## Overview

On March 7th, 2023, Alchemy and other members of the open source developer community, including [@Gooong](https://github.com/Gooong), [@taylorjdawson](https://github.com/taylorjdawson), [@leekt](https://github.com/leekt), and [@livingrockrises](https://github.com/livingrockrises), identified calldata decoding issues with the ERC-4337 EntryPoint contract and the example VerifyingPaymaster contract.

These contracts are currently deployed to several chains and generate hashes over user operations. The implementation resulted in inconsistent hashes depending on the signing method, which can lead to several second order effects like divergent hashes for the same UserOperations and colliding hashes for differing UserOperations.

Discussion was facilitated by [@drortirosh ](https://github.com/drortirosh)and is documented in [this Github issue](https://github.com/eth-infinitism/account-abstraction/issues/237#issuecomment-1466686252).

## Detailed breakdown

Below is a breakdown of the affected code, explanations of the EntryPoint Packing Vulnerability, the VerifyingPaymaster Packing Vulnerability, and their respective impact.

### Affected code

The code segment in question is the following:

#### ‍[UserOperation.sol:61-75](https://github.com/eth-infinitism/account-abstraction/blob/556f03fadcaba0d7d04cd901e6eb9601db50a998/contracts/interfaces/UserOperation.sol#L61-l75)

#### [VerifyingPaymaster.sol:35-49](https://github.com/eth-infinitism/account-abstraction/blob/556f03fadcaba0d7d04cd901e6eb9601db50a998/contracts/samples/VerifyingPaymaster.sol#L35-l49)

For context, the `UserOperation` struct is defined as:

Both of these code segments use assembly to copy a large portion of the calldata into memory, intending to capture part of a user operation to hash.

The `pack` method in `UserOperationLib` intends to capture all fields of the user operation from `sender` to `maxPriorityFeePerGas`, including the variable-size fields \(called dynamic fields in ABI encoding\) `initCode`, `callData`, and `paymasterAndData`.

The `pack` method in `VerifyingPaymaster` includes all of those fields except the `paymasterAndData` field, since that is not yet defined.

To implement this, both methods use a convenience field in [Yul](https://docs.soliditylang.org/en/v0.8.17/yul.html) provided to dynamic types in calldata, named `.offset`. This refers to the value provided in the ABI-encoding of a struct, which is defined [here in the Solidity spec](https://docs.soliditylang.org/en/v0.8.18/abi-spec.html#formal-specification-of-the-encoding). **\(It actually refers to the memory word after the offset, but that’s just for convenience when loading\).**

A standard ABI-encoder will encode the values for dynamic fields \(called their tail in the ABI coder\) in the order which they appear.

Consider the following encoding of a user operation in calldata that might be generated:

<CalloutBlock>

💡 **Note:** This example shows a user operation where all dynamic fields are less than one word in length for brevity.

</CalloutBlock>

<CalloutBlock>

💡‍ **\*‍**\***Note:** The memory address space here is within the user operation struct itself. In actual calldata, it will be placed elsewhere due to space occupied by method selecter and the arguments tuple.

</CalloutBlock>

In this example, following `pnd.offset` to generate a packing of the user operation will result in this “slice” of calldata:

This contains exactly what we want!

However, contracts that use ABI-encoded arguments do not validate what order fields are defined in, or even that the offsets are valid.

Using `signature.offset` or `pnd.offset` will read the corresponding “offset” value directly from calldata.

By using that as a boundary, it is possible to construct valid representations of user operations in calldata that have unusual hash properties.

Let’s explore how this affects the EntryPoint and VerifyingPaymaster independently.

### EntryPoint packing vulnerability

To demonstrate this vulnerability, we must consider a wallet contract that is different from the provided `SimpleAccount.sol`, because that sample re-uses the vulnerable code from `EntryPoint`.

The hash divergence becomes material when a different hashing scheme is used between the EntryPoint and the wallet contract, or if the wallet signs a non-standard user operation encoding.

This risk introduced to `EntryPoint` are that a single user operation can be represented by multiple “user op hashes” and that the same “user op hash” can represent multiple user operations.

Consider this account, called `ExampleAccount`, that has it’s own `ExampleAccountFactory`. The example account uses a single signer to validate user operations.

To grant permission to run a user operation, a hash over all fields in the user operation, except the signature itself, is generated and signed.

The `validateUserOp` method is defined as follows:

This is a relatively simple implementation of signature validation, as it checks all fields of `userOp`, along with the entrypoint address and the chain id.

As one of the goals of account abstraction, the `validateUserOp` method can contain arbitrary logic \(though bounded by limitations to storage access\), since this method represents the conditions under which a user operation can originate.

For this example account, user operations start from a signature by the owner. More generally, however, user operations can originate from arbitrary conditions: on-chain state, multiple signatures, or app-specific signatures – it’s a feature of account abstraction.

To demonstrate this vulnerability, let’s construct malicious calldata to `EntryPoint.handleOps` such that the `UserOperationEvent` emitted by `EntryPoint` will have an unexpected value.

After defining a sample `UserOperation memory uo` struct, here is how we can construct the calldata:

`rightPadBytes` is a helper function written to align `bytes` types to the nearest full word length.

It is defined as follows:

Now, when calling `handleOps`, the emitted event and the result of `EntryPoint.getUserOpHash\(\)` will be different.

#### Impact

Malicious bundlers, or non-bundler EOAs calling `EntryPoint.handleOps`, can modify their representation of a `UserOp` in calldata to change the UO hash in emitted events. This can break off-chain systems integrating with the emitted events, since the events are now revealed to be non-deterministic for a given UO.

Additionally, the bundler will have to deal with non-determinism when reading emitted userOpHashes from the `EntryPoint` contract. To see if an emitted `UserOperationEvent` from the `EntryPoint` corresponds to a user operation in the bundler’s local mempool, a comparison of the hash value is no longer enough, as the calldata to `handleOps` can be modified to change hash values.

Instead, bundlers will have to look up transaction receipts, fetch the calldata sent to `handleOps`, decode the calldata, then get the “canonical” hashes by re-encoding via a the standard ABI coder and calling `EntryPoint.getUserOpHash\(...\)`. This is needed to determine whether or not user operations in the local mempool have been mined. Additionally, since calls to `EntryPoint.handleOps` can happen from within other contract calls, the decoding can be deep in the call stack.

This divergence will also affect the implementation of bundler RPC methods, as a user op hash is used for identification in `eth\_getUserOperationByHash` and `eth\_getUserOperationReceipt`.

Bundlers will need to perform expensive searches, parsing, and decoding of calldata to `EntryPoint.handleOps\(...\)` to translate the emitted hashes from events into “canonical” hashes from `EntryPoint.getUserOpHash\(...\)`.

<CalloutBlock>

💡 **Note:** This vulnerability is distinct from the fact that rogue SCWs can reuse user operations. Reused user operations, and more generally, all user operations, should have a deterministic hash. Other applications and services that build on top of ERC-4337 will have to implement their own mitigation unless this is resolved.

</CalloutBlock>

Since ERC-4337 is at the early stages of adoption overall, it is hard to describe the potential impact of this vulnerability on the broader ecosystem. The scope of impact depends on the implementations of bundlers, user operation explorers, indexers, and other offchain services.

At a **minimum**, it would cause a confusing user experience, as the user operation hash \(similar to the transaction hash\) can change between submission and inclusion time, so some wallets might not account for that difference and fail to display updates to their users.

In a **medium risk case**, wallets can be designed such that they intentionally avoid indexing by setting all of their user op hashes to be the same \(see the [example of this provided by @leekt](https://github.com/leekt/foundry-4337/compare/PoC/sameUserOpHash)\).

In a **high risk case**, an offchain service monitoring user op inclusion could miss the inclusion of a given user operation, and attempt to resend or otherwise mishandle data and keys.

#### Proof of concept

See the full proof of concept in [this repo](https://github.com/alchemyplatform/entrypoint-hash-poc).

### VerifyingPaymaster packing vulnerability

The risks introduced to `VerifyingPaymaster` are that a user operation may contain different contents between signing time and inclusion on-chain. This can happen when two different user operations return the same hash from `VerifyingPaymaster.getHash\(UserOperation userOp, uint48 validUntil, uint48 validAfter\)`.

Let’s construct calldata for this function to show how this can be the case:

#### Calldata 1
Note how this encoding changes the order of the dynamic fields, but is otherwise still valid - offsets point to the correct locations and lengths are all valid. But, because the offset of `paymasterAndData` is an unexpected value, the slice we get from `pack\(\)` will be the following:

See how `initCode` and `callData` are excluded from the slice!

Let’s construct a second input calldata, this time maliciously modifying both fields:

- `initCode` will go from `1517c0de` to `1517c0de02`
- `callData` will go from `ca11dada1` to `ca11data02`

This gives us the following:

#### Calldata 2
If we perform the same `pack` operation on this different calldata, it will result in the same slice as before! And we can verify that the hashes are the same with the following:

Running this in a test environment in foundry reveals that both user ops have the same hash: `0x736f86d224bab46a95ae119947e172efa694379d9ac682d4ca780b7640a89b06`

See [this test file](https://github.com/alchemyplatform/entrypoint-hash-poc/blob/main/test/VerifyingPaymasterHashPOC.t.sol) for the full POC.

#### Impact

In this vulnerability, the hash can be modified to cover fewer elements than expected, allowing for initCode, callData, and possibly other static fields to be excluded from the hash, and thus vary between signing and usage. This can result in paymaster sponsorship signatures being used for different purposes than intended.

For instance, the wallet contract’s deployer factory and the call to a wallet’s `execute` function can be substituted. If a paymaster previously wanted to only sponsor users of their wallet, and only sponsor when they mint a specific NFT, those rules could be bypassed.

To bypass the rules, the sender would change `userOp.initCode` and `userOp.callData` after getting a signature. Then, the paymaster’s native token \(ETH or otherwise\) will be used for some other purpose than their intention of a gasless NFT mint.

Offchain signers which receive user operations to sign in an ABI-encoded format, or signers that have contract integrations to prepare data for signature, are vulnerable. This is a limited scope, as they are essentially “exploiting themselves”, but it presents a risk to operating a paymaster service.

Defensive measures against this include deploying an updated version of `VerifyingPaymaster`, or handling the process of ABI encoding themselves from user input.

## Conclusion

After several excellent conversations with ecosystem members, **@drortirosh** merged an optimized, readable [patch to the Entrypoint contract](https://github.com/eth-infinitism/account-abstraction/commit/6a7cbe7c6c786d6302d52b94405b88c6596f95cb) to address this vulnerability. Once redeployed, the Entrypoint contracts will no longer be exhibiting the behavior documented above.

Additionally, there is a [proposal](https://docs.google.com/document/d/1MywdH_TCkyEjD3QusLZ_kUZg4ZEI00qp97mBze9JI4k/edit#heading=h.gyhqxhuyd59n) to abstract nonce support in the Entrypoint that hardens this system as well. Given the risk to paymasters is limited, no official upstream patch has been made and paymaster operators can decide how to handle this as needed for their implementations.

We want to thank the 4337 community here, including **@drortirosh**,** @Gooong**,** @taylorjdawson**,** @leekt**, and** @livingrockrises** for working through this vulnerability with us.

Have any questions or topics you want to discuss?

Reach us at account-abstraction@alchemy.com.
