Smashing ERC4337 Wallets For Fun and Profit – The EntryPoint

Smashing ERC4337 Wallets For Fun and Profit – The EntryPoint

In The previous article, we gave a brief overview of the ERC4337 standard and how accounts are constructed under the hood.

In this article, we are going to dive into the core contract in this standard, the EntryPoint

In most cases, you will not be dealing with the EntryPoint contract directly in your audits, but understanding how to work will solidify your intuition of the whole standard.

So let’s begin.

The core object the EntryPoint contract operates on is the UserOperation. It has multiple parameters. The ones that stand out are:

  1. sender, the address that sent this object

  2. signature, the result of signing the UserOperation by the sender private key.

  3. nonce, to prevent signature reply

  4. initCode, which is used to initialize a wallet contract if needed. ( check this article for a refresher )

  5. callData, what the sender wants to execute.

  6. The rest of the parameters like related to the gas usage and limits the operation specifies.

There are two flows the EntryPoint contract handles:

  • The handleOps() flow

  • The simulateHandleOp() flow

There is also a variation of these two flows that deal with aggregated user operations. But for simplicity, we will focus on a single userOperation.

The interface for these two functions looks like this:

A UserOperation is first simulated through the function simulateHandleOp. The simulation executes the operation of the user on the wallet and keeps track of the amount of gas used.

Then, it compares it with the gas parameters set in UserOperation. Finally, simulateHandleOp reverts with ExecutionResult() Indicating that the simulation was successful.

Notice that simulateHandleOp and handleOps are very similar in that, they both execute the UserOperation. The only difference is that simulateHandleOp reverts with ExecutionResult() while handleOps changes the state of the wallet.

Other than that, they both have the same flow. A core function in that flow is the _validatePrepayment(). This function validates that the verification logic will not consume more than the verification gas limit specified by the sender.

But it has another crucial job, _validatePrepayment() calls _validateAccountPrepayment() which is in charge of the authorization of the calldata the sender wants to execute on the wallet.

It does that by calling the function validateUserOp() on the WALLET. The logic of the wallet then checks if the sender of the UserOperation can execute the calldata on the wallet. Here is the interface:

This is a perfect place to start looking for issues because broken authorization here can allow any user to execute arbitrarily calldata on any wallet.

For instance, is the signature passed from UserOperation belongs to the sender of the operation?

If that’s true, does it sender has a role in the wallet? Just passing a correctly signed payload does not mean it should be allowed to execute!

In the next part, we will look into paymasters which allow for the gasless execution of operations!