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:
sender
, the address that sent this objectsignature
, the result of signing theUserOperation
by thesender
private key.nonce
, to prevent signature replyinitCode
, which is used to initialize a wallet contract if needed. ( check this article for a refresher )callData
, what thesender
wants to execute.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()
flowThe
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!