5/5
## Testing EntryPoint Execution of UserOperations Welcome back to our deep dive into ERC-4337 Account Abstraction! In this lesson, we're moving beyond validating UserOperations to a critical aspect: verifying that the `EntryPoint` contract can correctly execute commands on behalf of a smart contract account. This is a foundational piece of the ERC-4337 puzzle, and mastering it will equip you with what I consider a "crazy skill" due to its immense and still largely untapped potential in the Web3 space. Our focus will be on a new test: `testEntryPointCanExecuteCommands`. We'll see how a bundler interacts with the `EntryPoint` to get a UserOperation processed and executed by the target smart contract account. ## Setting Up the Test: `testEntryPointCanExecuteCommands` To begin, we'll leverage some of the setup from our previous test, `testValidationOfUserOps`, as the initial arrangement shares common elements. Our "Arrange" section will perform the following steps: 1. **Initial Balance Assertion**: We first assert that our `minimalAccount` (the smart contract account) starts with a USDC balance of 0. This ensures we're starting from a known state. ```solidity // Assert initial state assertEq(usdc.balanceOf(address(minimalAccount)), 0); ``` 2. **Defining Call Parameters**: * `dest`: This is set to the address of our mock USDC contract. It's the target contract for the internal call our smart account will make. * `value`: This is set to 0, as the `mint` function we intend to call doesn't require any Ether to be sent with it. 3. **Crafting `functionData`**: This byte string represents the encoded call to the `ERC20Mock.mint` function. Our goal is for the `minimalAccount` to mint `AMOUNT` (a predefined constant) of USDC tokens to itself. ```solidity // bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT); ``` 4. **Creating `executeCallData`**: This is the data for the call that the `EntryPoint` will make to our `minimalAccount`. It's an encoded call to the `MinimalAccount.execute` function. The `execute` function, in turn, will use the `dest`, `value`, and `functionData` prepared above to make the actual call to the USDC contract's `mint` function. ```solidity // bytes memory executeCallData = abi.encodeWithSelector(MinimalAccount.execute.selector, dest, value, functionData); ``` 5. **Generating a `packedUserOp`**: We use our `sendPackedUserOp.generateSignedUserOperation` helper function. This utility takes the `executeCallData` and other necessary parameters (like nonce, gas limits, etc., handled by `helperConfig.getConfig()`) to construct and sign a `PackedUserOperation`. This `packedUserOp` is what a bundler would typically receive and submit. ```solidity // PackedUserOperation memory packedUserOp; // packedUserOp = sendPackedUserOp.generateSignedUserOperation(executeCallData, helperConfig.getConfig()); ``` 6. **Funding the Smart Contract Account**: This is a new and crucial step for this test. We must fund the `minimalAccount` with Ether. Why? Because in this basic ERC-4337 flow without a Paymaster, the `EntryPoint` contract needs to withdraw funds from the `minimalAccount` to compensate the bundler (represented by `randomUser` in our test) for the gas costs incurred in processing the UserOperation. We'll use Foundry's `vm.deal` cheatcode to send 1 ETH to the `minimalAccount`. ```solidity // In the Arrange section of testEntryPointCanExecuteCommands vm.deal(address(minimalAccount), 1e18); // Deals 1 ETH to minimalAccount ``` While `1e18` (1 Ether) might seem like a "magic number" here, it's chosen for simplicity to ensure sufficient funds are available. In a real-world scenario, this amount would be calculated more precisely or managed via pre-deposited funds. With this setup, our `minimalAccount` is funded, and we have a signed `UserOperation` ready to be processed. ## Executing the UserOperation: The "Act" Phase The "Act" phase of our test simulates the core interaction where a bundler submits the `UserOperation` to the `EntryPoint`. 1. **Simulating the Bundler**: We use Foundry's `vm.prank(randomUser)` cheatcode. This makes the subsequent contract call appear as if it's originating from `randomUser`. In the ERC-4337 ecosystem, `randomUser` represents any bundler or "Alt Mempool Node" that has picked up the `UserOperation`. ```solidity // Act vm.prank(randomUser); ``` 2. **Calling `handleOps` on the EntryPoint**: The bundler (`randomUser`) now calls the `handleOps` function on the deployed `EntryPoint` contract. The `EntryPoint.sol` contract defines `handleOps` as follows: ```solidity // Snippet from EntryPoint.sol function handleOps( PackedUserOperation[] calldata ops, address payable beneficiary ) public nonReentrant { // ... logic to validate and execute UserOps ... } ``` * `ops`: This parameter is an array of `PackedUserOperation` structs. Bundlers can submit multiple UserOperations in a single batch. For our test, we'll create a single-element array containing the `packedUserOp` we generated in the "Arrange" phase. ```solidity // Before the handleOps call PackedUserOperation[] memory ops = new PackedUserOperation[](1); ops[0] = packedUserOp; ``` * `beneficiary`: This is the address that will receive the gas fee compensation for successfully processing the UserOperation(s). In our test, this is the `randomUser` (our simulated bundler). The actual call in our test looks like this: ```solidity // Act (continued) IEntryPoint(helperConfig.getConfig().entryPoint).handleOps(ops, payable(randomUser)); ``` Here, `helperConfig.getConfig().entryPoint` provides the address of our deployed `EntryPoint` contract. We cast `randomUser` to `payable` because the `beneficiary` parameter expects a payable address to receive Ether. This `handleOps` call is the trigger for the `EntryPoint` to validate our `UserOperation` and, if valid, execute its `callData` on the `minimalAccount`. ## Verifying the Outcome: The "Assert" Phase After the `EntryPoint.handleOps` function has completed, we need to verify that the intended action – minting USDC tokens to our `minimalAccount` – has actually occurred. The "Assert" phase is straightforward: ```solidity // Assert assertEq(usdc.balanceOf(address(minimalAccount)), AMOUNT); ``` This line checks if the `minimalAccount`'s balance of USDC tokens is now equal to `AMOUNT`. If the `EntryPoint` successfully instructed the `minimalAccount` to execute the `mint` function, this assertion will pass. ## Code Refinements During the implementation, a couple of minor adjustments were made to the code, mostly carried over from the previous test: 1. **`userOperationHash`**: A variable `userOperationHash`, which was part of the copied "Arrange" section, was commented out. While the hash is fundamental to UserOperations, it's encapsulated within the `packedUserOp` and handled internally by the `EntryPoint` during the `handleOps` flow. It's not directly needed as a standalone variable in this specific test's logic. 2. **Payable Beneficiary**: We ensured that `randomUser` is correctly cast to `payable(randomUser)` when passed as the `beneficiary` argument to `handleOps`, as the function signature requires it. ## Running the Test and Facing a Challenge With the test written, the next step is to run it using Forge: `forge test --mt testEntryPointCanExecuteCommands -vvv` The `--mt testEntryPointCanExecuteCommands` flag specifically targets our new test, and `-vvv` provides verbose output, which is invaluable for debugging. Upon running this command, the test **fails** with an `EVMError: Revert`. The verbose trace reveals several internal calls, including `console.log` outputs (potentially from `EntryPoint.sol` or `MinimalAccount.sol`), a call to `validateUserOp(...)`, and finally, the revert: `[FAIL. Reason: EVMError: Revert] testEntryPointCanExecuteCommands()(gas: XXXXX)` (where XXXXX is the gas consumed). ## The Debugging Hurdle This `EVMError: Revert` presents us with a debugging challenge. This is a core part of software development, especially in the complex world of smart contracts. To tackle this, consider the following hints: * **Analyze the Foundry Trace**: The verbose output (`-vvv`) is your best friend. Look carefully at the sequence of calls, internal reverts, and any `console.log` messages. * **General Debugging Skills**: Don't hesitate to use your standard debugging toolkit. While generic tools like ChatGPT might struggle with highly specific `EntryPoint` nuances, searching for parts of error messages or understanding EVM revert reasons on Google or YouTube can sometimes provide clues. The video lesson concludes here, pausing before diving into the solution for this revert. This is an excellent opportunity for you to try and diagnose the issue yourself. What could be going wrong within the `EntryPoint`'s execution or validation logic that causes this revert, even though we've funded the account and provided a signed UserOperation? In the next segment, we'll explore the cause of this revert and how to fix it, furthering our understanding of the `EntryPoint`'s operational intricacies.
Welcome back to our deep dive into ERC-4337 Account Abstraction! In this lesson, we're moving beyond validating UserOperations to a critical aspect: verifying that the EntryPoint contract can correctly execute commands on behalf of a smart contract account. This is a foundational piece of the ERC-4337 puzzle, and mastering it will equip you with what I consider a "crazy skill" due to its immense and still largely untapped potential in the Web3 space.
Our focus will be on a new test: testEntryPointCanExecuteCommands. We'll see how a bundler interacts with the EntryPoint to get a UserOperation processed and executed by the target smart contract account.
testEntryPointCanExecuteCommandsTo begin, we'll leverage some of the setup from our previous test, testValidationOfUserOps, as the initial arrangement shares common elements.
Our "Arrange" section will perform the following steps:
Initial Balance Assertion: We first assert that our minimalAccount (the smart contract account) starts with a USDC balance of 0. This ensures we're starting from a known state.
Defining Call Parameters:
dest: This is set to the address of our mock USDC contract. It's the target contract for the internal call our smart account will make.
value: This is set to 0, as the mint function we intend to call doesn't require any Ether to be sent with it.
Crafting functionData: This byte string represents the encoded call to the ERC20Mock.mint function. Our goal is for the minimalAccount to mint AMOUNT (a predefined constant) of USDC tokens to itself.
Creating executeCallData: This is the data for the call that the EntryPoint will make to our minimalAccount. It's an encoded call to the MinimalAccount.execute function. The execute function, in turn, will use the dest, value, and functionData prepared above to make the actual call to the USDC contract's mint function.
Generating a packedUserOp: We use our sendPackedUserOp.generateSignedUserOperation helper function. This utility takes the executeCallData and other necessary parameters (like nonce, gas limits, etc., handled by helperConfig.getConfig()) to construct and sign a PackedUserOperation. This packedUserOp is what a bundler would typically receive and submit.
Funding the Smart Contract Account: This is a new and crucial step for this test. We must fund the minimalAccount with Ether. Why? Because in this basic ERC-4337 flow without a Paymaster, the EntryPoint contract needs to withdraw funds from the minimalAccount to compensate the bundler (represented by randomUser in our test) for the gas costs incurred in processing the UserOperation.
We'll use Foundry's vm.deal cheatcode to send 1 ETH to the minimalAccount.
While 1e18 (1 Ether) might seem like a "magic number" here, it's chosen for simplicity to ensure sufficient funds are available. In a real-world scenario, this amount would be calculated more precisely or managed via pre-deposited funds.
With this setup, our minimalAccount is funded, and we have a signed UserOperation ready to be processed.
The "Act" phase of our test simulates the core interaction where a bundler submits the UserOperation to the EntryPoint.
Simulating the Bundler: We use Foundry's vm.prank(randomUser) cheatcode. This makes the subsequent contract call appear as if it's originating from randomUser. In the ERC-4337 ecosystem, randomUser represents any bundler or "Alt Mempool Node" that has picked up the UserOperation.
Calling handleOps on the EntryPoint: The bundler (randomUser) now calls the handleOps function on the deployed EntryPoint contract. The EntryPoint.sol contract defines handleOps as follows:
ops: This parameter is an array of PackedUserOperation structs. Bundlers can submit multiple UserOperations in a single batch. For our test, we'll create a single-element array containing the packedUserOp we generated in the "Arrange" phase.
beneficiary: This is the address that will receive the gas fee compensation for successfully processing the UserOperation(s). In our test, this is the randomUser (our simulated bundler).
The actual call in our test looks like this:
Here, helperConfig.getConfig().entryPoint provides the address of our deployed EntryPoint contract. We cast randomUser to payable because the beneficiary parameter expects a payable address to receive Ether.
This handleOps call is the trigger for the EntryPoint to validate our UserOperation and, if valid, execute its callData on the minimalAccount.
After the EntryPoint.handleOps function has completed, we need to verify that the intended action – minting USDC tokens to our minimalAccount – has actually occurred.
The "Assert" phase is straightforward:
This line checks if the minimalAccount's balance of USDC tokens is now equal to AMOUNT. If the EntryPoint successfully instructed the minimalAccount to execute the mint function, this assertion will pass.
During the implementation, a couple of minor adjustments were made to the code, mostly carried over from the previous test:
userOperationHash: A variable userOperationHash, which was part of the copied "Arrange" section, was commented out. While the hash is fundamental to UserOperations, it's encapsulated within the packedUserOp and handled internally by the EntryPoint during the handleOps flow. It's not directly needed as a standalone variable in this specific test's logic.
Payable Beneficiary: We ensured that randomUser is correctly cast to payable(randomUser) when passed as the beneficiary argument to handleOps, as the function signature requires it.
With the test written, the next step is to run it using Forge:
forge test --mt testEntryPointCanExecuteCommands -vvv
The --mt testEntryPointCanExecuteCommands flag specifically targets our new test, and -vvv provides verbose output, which is invaluable for debugging.
Upon running this command, the test fails with an EVMError: Revert. The verbose trace reveals several internal calls, including console.log outputs (potentially from EntryPoint.sol or MinimalAccount.sol), a call to validateUserOp(...), and finally, the revert:
[FAIL. Reason: EVMError: Revert] testEntryPointCanExecuteCommands()(gas: XXXXX) (where XXXXX is the gas consumed).
This EVMError: Revert presents us with a debugging challenge. This is a core part of software development, especially in the complex world of smart contracts.
To tackle this, consider the following hints:
Analyze the Foundry Trace: The verbose output (-vvv) is your best friend. Look carefully at the sequence of calls, internal reverts, and any console.log messages.
General Debugging Skills: Don't hesitate to use your standard debugging toolkit. While generic tools like ChatGPT might struggle with highly specific EntryPoint nuances, searching for parts of error messages or understanding EVM revert reasons on Google or YouTube can sometimes provide clues.
The video lesson concludes here, pausing before diving into the solution for this revert. This is an excellent opportunity for you to try and diagnose the issue yourself. What could be going wrong within the EntryPoint's execution or validation logic that causes this revert, even though we've funded the account and provided a signed UserOperation?
In the next segment, we'll explore the cause of this revert and how to fix it, furthering our understanding of the EntryPoint's operational intricacies.
An advanced tutorial to Mastering EntryPoint Execution for ERC-4337 UserOperations - Unlock the "crazy skill" of verifying `EntryPoint` execution of UserOperations in ERC-4337. You'll set up tests for `handleOps`, fund smart accounts for bundler compensation, and diagnose an EVM revert, deepening your Account Abstraction expertise.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on August 27, 2025
Duration: 36min
Duration: 3h 06min
Duration: 5h 02min
Duration: 6h 02min
Duration: 2h 47min
Duration: 1h 23min
Duration: 4h 28min
Duration: 1h 19min
Duration: 1h 10min
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on August 27, 2025