1/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.
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)
Guest lecturers:
Juliette Chevalier
Lead Developer relations at Aragon
Nader Dabit
Director of developer relations at EigenLayer
Ally Haire
Developer relations at Protocol Labs
Harrison
Founder at GasliteGG
Last updated on May 12, 2025
Solidity Developer
Advanced FoundryDuration: 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)
Guest lecturers:
Juliette Chevalier
Lead Developer relations at Aragon
Nader Dabit
Director of developer relations at EigenLayer
Ally Haire
Developer relations at Protocol Labs
Harrison
Founder at GasliteGG
Last updated on May 12, 2025