1/5
## Crafting an Unsigned PackedUserOperation for ERC-4337 This lesson focuses on a crucial preliminary step in interacting with ERC-4337 Account Abstraction: generating an *unsigned* `PackedUserOperation` struct. This forms the foundation for creating a fully signed operation, which is essential for testing functions like `validateUserOp` within a smart contract account. **Context and Motivation** Previously, you might have tested the `execute` function of a smart contract account, such as `MinimalAccount.sol`. The next logical step is to test the `validateUserOp` function. To do this effectively, we need to construct a valid `PackedUserOperation` struct, its corresponding hash (`userOpHash`), and potentially any `missingAccountFunds`. A complete `PackedUserOperation` requires a signature from the account owner. Instead of embedding the complex logic for creating and signing this operation directly within our test files (e.g., `MinimalAccountTest.t.sol`), we'll build this functionality within a Foundry script file (`SendPackedUserOp.s.sol`). This approach offers several advantages: 1. **Reusability:** A script for sending user operations will likely be necessary for interacting with the account on testnets or mainnet. 2. **Testability:** The logic developed in the script, especially for generating the operation data, can be imported and utilized within Foundry tests. 3. **Focused Testing:** This allows us to test the signature generation process itself within our test environment. **The `PackedUserOperation` Struct** The central data structure we're working with is `PackedUserOperation`. It's typically imported from an account abstraction library (e.g., `lib/account-abstraction/contracts/interfaces/PackedUserOperation.sol`). For this lesson, the relevant fields are: ```solidity struct PackedUserOperation { address sender; // The account sending the operation. uint256 nonce; // Anti-replay protection nonce. bytes initCode; // Code to deploy the account if it doesn't exist (ignored for now). bytes callData; // The actual transaction data (function call) to execute. bytes32 accountGasLimits; // Packed verificationGasLimit and callGasLimit. uint256 preVerificationGas; // Gas limit for pre-verification steps. bytes32 gasFees; // Packed maxFeePerGas and maxPriorityFeePerGas. bytes paymasterAndData; // Data for paymaster interaction (ignored for now). bytes signature; // The signature validating the operation (left blank for unsigned). } ``` **Building the `SendPackedUserOp.s.sol` Script** We'll create a new Foundry script file named `SendPackedUserOp.s.sol` to house our logic. 1. **Basic Script Structure:** The script begins with the standard SPDX license, pragma version, and necessary imports. We import `Script` from `forge-std` and the `PackedUserOperation` struct. ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Script} from "forge-std/Script.sol"; import {PackedUserOperation} from "lib/account-abstraction/contracts/interfaces/PackedUserOperation.sol"; contract SendPackedUserOp is Script { function run() public {} // Entry point for script execution, empty for now // Helper functions will be defined below } ``` 2. **Main Function: `generateSignedUserOperation` (Initial Version)** Our primary goal is to create a function that will eventually return a *signed* `PackedUserOperation`. For now, it will lay the groundwork and prepare the unsigned version. ```solidity function generatedSignedUserOperation(bytes memory callData, address sender) public // Not 'view' or 'pure' as it will use vm cheatcodes for nonce and later for signing returns (PackedUserOperation memory) { // Step 1: Generate the unsigned UserOperation data uint256 nonce = vm.getNonce(sender); // Fetch current nonce for the sender PackedUserOperation memory unsignedUserOp = _generateUnsignedUserOperation( callData, sender, nonce ); // Step 2: Sign the UserOperation (to be implemented later) // For now, we'll return the unsigned version return unsignedUserOp; } ``` In this function, we use the Foundry cheatcode `vm.getNonce(sender)` to retrieve the current nonce for the `sender` address. This is crucial for replay protection. 3. **Helper Function: `_generateUnsignedUserOperation`** To maintain clean and organized code, we'll create an internal helper function dedicated to populating the `PackedUserOperation` struct *without* the signature. ```solidity function _generateUnsignedUserOperation(bytes memory callData, address sender, uint256 nonce) internal pure // This function doesn't read state or use cheatcodes returns (PackedUserOperation memory) { // Example gas parameters (these may need tuning) uint128 verificationGasLimit = 16777216; uint128 callGasLimit = verificationGasLimit; // Often different in practice uint128 maxPriorityFeePerGas = 256; uint128 maxFeePerGas = maxPriorityFeePerGas; // Simplification for example // Pack accountGasLimits: (verificationGasLimit << 128) | callGasLimit bytes32 accountGasLimits = bytes32( (uint256(verificationGasLimit) << 128) | uint256(callGasLimit) ); // Pack gasFees: (maxFeePerGas << 128) | maxPriorityFeePerGas bytes32 gasFees = bytes32( (uint256(maxFeePerGas) << 128) | uint256(maxPriorityFeePerGas) ); return PackedUserOperation({ sender: sender, nonce: nonce, initCode: hex"", // Empty for existing accounts callData: callData, accountGasLimits: accountGasLimits, preVerificationGas: verificationGasLimit, // Often related to verificationGasLimit gasFees: gasFees, paymasterAndData: hex"", // Empty if not using a paymaster signature: hex"" // Crucially, the signature is blank for an unsigned operation }); } ``` **Key aspects of `_generateUnsignedUserOperation`:** * **Gas Parameters:** The `verificationGasLimit`, `callGasLimit`, `maxPriorityFeePerGas`, and `maxFeePerGas` are provided as example values. In a real-world scenario, these would require careful calculation or dynamic retrieval based on network conditions and operation complexity. * **Packing Gas Limits and Fees:** The `accountGasLimits` field (a `bytes32`) packs two `uint128` values: `verificationGasLimit` and `callGasLimit`. This is achieved by left-shifting the `verificationGasLimit` by 128 bits and then performing a bitwise OR operation with `callGasLimit`. A similar process is used for `gasFees`, packing `maxFeePerGas` and `maxPriorityFeePerGas`. Both `uint128` values are first cast to `uint256` before bitwise operations. * **Ignored Fields:** * `initCode`: This field is left empty (`hex""`) because we are assuming the smart contract account (`sender`) already exists. It's only used when deploying a new account via a user operation. * `paymasterAndData`: This is also left empty, assuming no paymaster is involved in sponsoring gas fees for this operation. * **Signature Field:** The `signature` field is explicitly set to `hex""`. This is the defining characteristic of an *unsigned* `PackedUserOperation`. By calling `_generateUnsignedUserOperation` from `generatedSignedUserOperation`, we now have a structured way to create the data payload of a user operation before it's signed. **Key Concepts Recap** * **ERC-4337 & Account Abstraction:** The overarching framework that necessitates `PackedUserOperation` for user-initiated actions. * **`PackedUserOperation`:** The core data structure that bundles a user's intent, replacing traditional transactions for smart contract accounts. * **Unsigned Operation:** The `PackedUserOperation` struct populated with all necessary data *except* the cryptographic signature. * **Foundry Scripts (`.s.sol`):** Used for deployment, interaction, and complex setup logic that can be reused in tests. * **Foundry Cheatcodes:** Tools like `vm.getNonce()` allow scripts and tests to interact with and query the blockchain virtual machine's state. * **Bit Shifting and Packing:** A technique to efficiently store multiple smaller data values within a single larger field (e.g., two `uint128` values in one `bytes32`). * **Helper Functions:** A good practice for modularizing code, making it more readable and maintainable. **Important Considerations** * **Gas Parameterization:** The hardcoded gas limits and fee values (`verificationGasLimit`, `callGasLimit`, `maxFeePerGas`, `maxPriorityFeePerGas`) are illustrative. In practice, these are critical and complex. Incorrect values can lead to operation failure. They often need to be estimated or determined dynamically. * **`initCode` Usage:** Only populate `initCode` if the user operation is intended to deploy a new smart contract account. For operations targeting existing accounts, it must be empty. * **Paymaster Logic:** The `paymasterAndData` field is only relevant when employing a paymaster to cover gas costs. * **Separation of Concerns:** Generating the unsigned data payload separately from the signing process is a clean architectural choice. **Next Steps** With the `_generateUnsignedUserOperation` helper in place and `generatedSignedUserOperation` set up to produce the unsigned struct, the next logical step (to be covered subsequently) involves: 1. Calculating the `userOpHash` of the `unsignedUserOp`. 2. Signing this hash using the `sender`'s private key (likely using Foundry cheatcodes like `vm.sign` or by managing private keys within the script). 3. Populating the `signature` field of the `PackedUserOperation` struct with this cryptographic signature. 4. Returning the fully populated, *signed* `PackedUserOperation`. This will complete the `generatedSignedUserOperation` function, enabling us to generate operations ready for validation by the `validateUserOp` function of our smart contract account.
An essential coding tutorial to Crafting an Unsigned PackedUserOperation for ERC-4337 - Learn to code a Foundry script that generates an unsigned `PackedUserOperation`, crucial for ERC-4337. This lesson covers populating key fields like `sender`, `nonce` (using `vm.getNonce`), `callData`, and packing gas parameters, preparing the operation for later signing.
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