0/5
## Setting Up Your Account Abstraction Deployment Environment This lesson guides you through establishing the foundational Foundry deployment scripts for an ERC-4337 account abstraction project, specifically focusing on deploying a `MinimalAccount` smart contract. We'll transition from the `MinimalAccount.sol` contract code to creating and configuring the necessary `.s.sol` deployment scripts. Within your project's `script` folder, we will create three essential script files: 1. **`DeployMinimal.s.sol`**: This script will house the core logic required to deploy our `MinimalAccount` smart contract to the blockchain. 2. **`HelperConfig.s.sol`**: A crucial component for managing network-specific configurations. Its primary role is to handle variations in crucial addresses, such as the `EntryPoint` contract, which differs across various blockchain networks. This allows our deployment scripts to be versatile and reusable. 3. **`SendPackedUserOp.s.sol`**: This script is anticipated to be the most complex and will serve as the central hub for interacting with the account abstraction system. It will be responsible for constructing, signing, and dispatching `PackedUserOperation`s. A deep dive into the intricacies of signing and data creation for these user operations will occur within this script. The necessity for `HelperConfig.s.sol` arises from the fact that the `EntryPoint` contract, a cornerstone of the ERC-4337 standard, possesses distinct deployment addresses on different chains. By utilizing a helper configuration contract, our deployment scripts can dynamically retrieve the appropriate `EntryPoint` address for the target network, thereby avoiding hardcoded values and enhancing script portability. ## Building a Robust `HelperConfig` for Multi-Chain Deployments The `HelperConfig.s.sol` script is designed to centralize and manage network-specific parameters, making your deployment process adaptable to various blockchain environments. Let's begin by structuring `HelperConfig.s.sol`: ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Script} from "forge-std/Script.sol"; contract HelperConfig is Script { // Configuration struct struct NetworkConfig { address entryPoint; address account; // Deployer/burner wallet address } // Chain ID Constants uint256 constant ETH_SEPOLIA_CHAIN_ID = 11155111; uint256 constant ZKSYNC_SEPOLIA_CHAIN_ID = 300; uint256 constant LOCAL_CHAIN_ID = 31337; // Anvil default // Official Sepolia EntryPoint address: 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 address constant BURNER_WALLET = 0xYourBurnerWalletAddress; // Replace with your actual address // State Variables NetworkConfig public localNetworkConfig; mapping(uint256 chainId => NetworkConfig) public networkConfigs; constructor() { networkConfigs[ETH_SEPOLIA_CHAIN_ID] = getEthSepoliaConfig(); networkConfigs[ZKSYNC_SEPOLIA_CHAIN_ID] = getZkSyncSepoliaConfig(); } function getEthSepoliaConfig() public pure returns (NetworkConfig memory) { return NetworkConfig({ entryPoint: 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789, account: BURNER_WALLET }); } function getZkSyncSepoliaConfig() public pure returns (NetworkConfig memory) { // ZKSync Era has native account abstraction; an external EntryPoint might not be used in the same way. // address(0) is used as a placeholder or to indicate reliance on native mechanisms. return NetworkConfig({ entryPoint: address(0), account: BURNER_WALLET }); } function getOrCreateAnvilEthConfig() public returns (NetworkConfig memory) { if (localNetworkConfig.account != address(0)) { return localNetworkConfig; } // For local Anvil network, we might need to deploy a mock EntryPoint // address mockEntryPointAddress = deployMockEntryPoint(); // Placeholder // For now, let's use Sepolia's EntryPoint or a defined mock if available // This part would involve deploying a mock EntryPoint if one doesn't exist. // For simplicity in this example, we'll assume a mock or reuse Sepolia's for structure. // In a real scenario, you'd deploy a MockEntryPoint.sol here. // Example: localNetworkConfig = NetworkConfig({ entryPoint: mockEntryPointAddress, account: BURNER_WALLET }); // Fallback for this lesson (actual mock deployment not shown): NetworkConfig memory sepoliaConfig = getEthSepoliaConfig(); // Or a specific local mock entry point localNetworkConfig = NetworkConfig({ entryPoint: sepoliaConfig.entryPoint, // Replace with actual mock entry point if deployed account: BURNER_WALLET }); return localNetworkConfig; } function getConfigByChainId(uint256 chainId) public returns (NetworkConfig memory) { if (chainId == LOCAL_CHAIN_ID) { return getOrCreateAnvilEthConfig(); } if (networkConfigs[chainId].account != address(0)) { // Check if config exists return networkConfigs[chainId]; } revert("HelperConfig__InvalidChainId()"); } function getConfig() public returns (NetworkConfig memory) { return getConfigByChainId(block.chainid); } } ``` **Key elements of `HelperConfig.s.sol`:** * **`NetworkConfig` Struct:** Holds `entryPoint` and `account` (deployer/burner wallet) addresses for each network. * **Chain ID Constants:** Predefined IDs for commonly used testnets (Sepolia, ZKSync Sepolia) and the local Anvil network. * **`BURNER_WALLET`:** A constant for your deployer or test account address. **Remember to replace `0xYourBurnerWalletAddress` with your actual wallet address.** * **State Variables:** * `localNetworkConfig`: Stores configuration for the local Anvil environment. * `networkConfigs`: A mapping from chain IDs to their respective `NetworkConfig` structs. * **Constructor:** Initializes configurations for Sepolia and ZKSync Sepolia by calling their respective getter functions. * **`getEthSepoliaConfig()`/`getZkSyncSepoliaConfig()`:** These pure functions return hardcoded `NetworkConfig` structs. Note that for ZKSync Sepolia, `entryPoint` is set to `address(0)`, reflecting ZKSync's native account abstraction capabilities where a pre-deployed `EntryPoint` contract might not be necessary in the same way as on EVM chains. * **`getOrCreateAnvilEthConfig()`:** This function manages configuration for local Anvil testing. It checks if `localNetworkConfig` is already populated. If not (e.g., on the first run or if mocks are needed), it would ideally deploy mock contracts (like a mock `EntryPoint`). For this lesson, the full mock deployment logic is commented out, and it serves as a placeholder for such an implementation. * **`getConfigByChainId(uint256 chainId)`:** Retrieves the network configuration for a given chain ID. It handles the local chain ID by deferring to `getOrCreateAnvilEthConfig` and looks up other chains in the `networkConfigs` mapping. It includes a check to ensure a configuration exists for the requested `chainId`, reverting with an error if not. * **`getConfig()`:** A convenience function that fetches the configuration for the current chain by using `block.chainid`. Function visibility for getter functions like `getEthSepoliaConfig` is `public pure`, while functions like `getOrCreateAnvilEthConfig` and `getConfigByChainId` are `public` because they might interact with state or call other non-view/pure functions (e.g., `getOrCreateAnvilEthConfig` might eventually deploy mocks). ## Scripting the `MinimalAccount` Deployment with Foundry Now, let's populate `DeployMinimal.s.sol` to handle the deployment of our `MinimalAccount` contract, leveraging the `HelperConfig` we just created. ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Script} from "forge-std/Script.sol"; import {MinimalAccount} from "src/ethereum/MinimalAccount.sol"; // Adjust path as needed import {HelperConfig} from "./HelperConfig.s.sol"; // Relative path to HelperConfig contract DeployMinimal is Script { function deployMinimalAccount() public returns (HelperConfig helperConfigInstance, MinimalAccount minimalAccountContract) { HelperConfig helperConfig = new HelperConfig(); HelperConfig.NetworkConfig memory config = helperConfig.getConfig(); vm.startBroadcast(config.account); // Use the burner wallet from config for broadcasting MinimalAccount minimalAccount = new MinimalAccount(config.entryPoint); // The MinimalAccount constructor (if Ownable) might set config.account as owner if it's the broadcaster. // This explicit transfer ensures the script runner (msg.sender in script context) becomes the owner, // or reaffirms ownership if config.account == msg.sender. // It's often good practice for clarity and to ensure the intended final owner. if (minimalAccount.owner() != msg.sender) { minimalAccount.transferOwnership(msg.sender); } vm.stopBroadcast(); return (helperConfig, minimalAccount); } function run() public returns (HelperConfig, MinimalAccount) { return deployMinimalAccount(); } } ``` **Breakdown of `DeployMinimal.s.sol`:** 1. **Standard Setup:** Includes the SPDX license, Solidity pragma version, and imports `Script` from `forge-std`. 2. **Import `MinimalAccount`:** Imports the `MinimalAccount` contract that we intend to deploy. Ensure the path `src/ethereum/MinimalAccount.sol` matches your project structure. 3. **Import `HelperConfig`:** Imports the `HelperConfig` contract using a relative path. 4. **`deployMinimalAccount()` Function:** * **Instantiate `HelperConfig`:** Creates a new instance of `HelperConfig`. * **Fetch Network Configuration:** Calls `helperConfig.getConfig()` to retrieve the `NetworkConfig` (which includes the `entryPoint` address and the `account` for broadcasting) for the current chain. * **Broadcast Deployment:** * `vm.startBroadcast(config.account)`: Initiates a broadcast, ensuring that transactions are signed and sent by the `account` specified in our `HelperConfig` (the `BURNER_WALLET`). * `MinimalAccount minimalAccount = new MinimalAccount(config.entryPoint);`: Deploys the `MinimalAccount` contract, passing the chain-specific `entryPoint` address obtained from our configuration. * `minimalAccount.transferOwnership(msg.sender);`: After deployment, ownership of the `MinimalAccount` contract is explicitly transferred to `msg.sender` (the address executing the script). While the `Ownable` constructor in `MinimalAccount` might initially set the owner to `config.account` (if it's the broadcaster), this step ensures the script runner gains ownership, which can be useful for subsequent interactions or if `config.account` is a dedicated deployer distinct from the primary operator. * `vm.stopBroadcast()`: Concludes the broadcast. * **Return Values:** The function returns the instance of `HelperConfig` (in case its state or other functions are needed by the caller) and the newly deployed `MinimalAccount` contract instance. 5. **`run()` Function:** This is the main entry point for Foundry scripts. It simply calls `deployMinimalAccount()` and returns its results. **Compilation and Error Handling:** During the development of these scripts, you might encounter build errors. Common issues include: * **Remapping Issues:** If `forge build` fails due to incorrect paths for libraries like OpenZeppelin, ensure your `foundry.toml` file has the correct remappings. For example: `remappings = ['@openzeppelin/contracts=lib/openzeppelin-contracts/contracts']` * **Typos:** Simple typos in contract names or import statements (e.g., `MiniamAccount` instead of `MinimalAccount`) can cause compilation failures. * **Compiler Version Warnings:** Ensure all script files (`.s.sol`) specify a compatible `pragma solidity` version. It's good practice to run `forge build` frequently to catch and resolve these issues early in the development process. With these scripts in place, you have a robust and flexible system for deploying your `MinimalAccount` contract across different networks, paving the way for implementing the core account abstraction logic in `SendPackedUserOp.s.sol`.
A constructive walkthrough to Building Your ERC-4337 Deployment Foundation - Establish core Foundry deployment scripts (`DeployMinimal.s.sol`, `HelperConfig.s.sol`) for an ERC-4337 `MinimalAccount` contract. Learn to manage multi-chain `EntryPoint` configurations with `HelperConfig`, preparing for robust account abstraction deployments.
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