- ERC4337 Account Abstraction
This is an educational implementation of an ERC-4337 (Account Abstraction) smart contract wallet that demonstrates how to build a basic smart contract account using the EntryPoint contract. It showcases signature validation, user operation handling, and fund management for decentralized account abstraction.
- ERC-4337 Compliant: Implements the
IAccountinterface for EntryPoint compatibility - Owner-Based Validation: Uses ECDSA signature validation with owner authorization
- Flexible Execution: Execute arbitrary transactions through the EntryPoint or directly as owner
- Fund Management: Receive ETH and withdraw funds with owner-only permissions
- Educational Design: Simple, well-commented code for learning Account Abstraction concepts
Tech Stack:
- Solidity 0.8.24
- Foundry (testing & deployment)
- OpenZeppelin Contracts (Ownable, ECDSA)
- ERC-4337 Account Abstraction Contracts
- Forge Standard Library
┌─────────────────────────────────────────────────────────────┐
│ EOA / User │
│ (Account Owner) │
└───────────┬─────────────────────────────────┬───────────────┘
│ │
│ 1. Sign UserOperation │ 2. Call execute()
│ │ directly (owner)
▼ │
┌───────────────────────────────┐ │
│ Bundler / Sequencer │ │
│ (Bundles multiple UserOps) │ │
└──────────────┬────────────────┘ │
│ │
│ 2. Call handleOps() │
│ │
▼ │
┌────────────────────────────────────────┐ │
│ EntryPoint Contract │ │
│ (ERC-4337 Core Hub) │ │
│ │ │
│ ┌─────────────────────────────┐ │ │
│ │ validateUserOp() │ │ │
│ │ (Signature Validation) │ │ │
│ └─────────────────────────────┘ │ │
│ │ │
│ ┌─────────────────────────────┐ │ │
│ │ execute() │ │ │
│ │ (Execution Phase) │ │ │
│ └─────────────────────────────┘ │ │
└──────────────┬─────────────────────────┘ │
│ │
│ 3. Validate & Execute │
│ │
▼ ▼
┌───────────────────────────────────────────────┐
│ BasicAccount (Smart Wallet) │
│ │
│ ┌──────────────────────────────┐ │
│ │ validateUserOp() │ │
│ │ - Recover signer from sig │ │
│ │ - Verify signer == owner │ │
│ │ - Pay entry point │ │
│ └──────────────────────────────┘ │
│ │
│ ┌──────────────────────────────┐ │
│ │ execute() │ │
│ │ - Call external contracts │ │
│ │ - Transfer funds │ │
│ └──────────────────────────────┘ │
│ │
│ ┌──────────────────────────────┐ │
│ │ withdraw() │ │
│ │ - Owner only withdraw funds │ │
│ └──────────────────────────────┘ │
└──────────────┬────────────────────────────────┘
│
│ 4. Execute Transactions
│
┌─────────┴──────────┐
│ │
▼ ▼
┌─────────────────┐ ┌──────────────┐
│ Target Contract│ │ Other Calls │
│ (USDC, etc.) │ │ (transfers) │
└─────────────────┘ └──────────────┘
Repository Structure:
erc4337-account-abstraction/
├── src/
│ └── BasicAccount.sol # ERC-4337 Smart Wallet Implementation
├── script/
│ ├── DeployBasicAccount.s.sol # Deployment script
│ ├── HelperConfig.s.sol # Network configuration
│ └── SendPackedUserOp.s.sol # UserOperation helper functions
├── test/
│ ├── unit/
│ │ └── BasicAccountTest.t.sol # Unit tests
│ ├── integration/
│ │ └── DeployBasicAccountTest.t.sol # Integration tests
│ ├── fuzz/
│ │ ├── Handler.t.sol # Fuzz testing handler
│ │ └── InvariantsTest.t.sol # Invariant tests
│ └── mocks/
│ └── InvalidReceiver.sol # Mock for testing failures
├── lib/
│ ├── account-abstraction/ # ERC-4337 core contracts
│ ├── forge-std/ # Foundry standard library
│ ├── foundry-devops/ # DevOps utilities
│ └── openzeppelin-contracts/ # OpenZeppelin utilities
├── foundry.toml # Foundry configuration
├── Makefile # Build automation
└── README.md # This file
git clone https://github.com/0xGearhart/erc4337-account-abstraction
cd erc4337-account-abstraction
make-
Copy the environment template:
cp .env.example .env
-
Configure your
.envfile:ETH_MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-api-key ETH_SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/your-api-key ARB_MAINNET_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/your-api-key ARB_SEPOLIA_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/your-api-key ETHERSCAN_API_KEY=your_etherscan_api_key_here DEFAULT_KEY_ADDRESS=public_address_of_your_encrypted_private_key_here
-
Get testnet ETH:
- Sepolia Faucet: cloud.google.com/application/web3/faucet/ethereum/sepolia
-
Configure Makefile
- Change account name in Makefile to the name of your desired encrypted key
- change "--account defaultKey" to "--account <YOUR_ENCRYPTED_KEY_NAME>"
- check encrypted key names stored locally with:
cast wallet list- If no encrypted keys found
- Encrypt private key to be used securely within foundry:
cast wallet import <account_name> --interactive- Never commit your
.envfile - Never use your mainnet private key for testing
- Use a separate wallet with only testnet funds
Compile the contracts:
forge buildRun the test suite:
forge testRun tests with verbosity:
forge test -vvvRun specific test:
forge test --mt testFunctionNameGenerate coverage report:
forge coverageCreate test coverage report and save to .txt file:
make coverage-reportStart a local Anvil node:
make anvilDeploy to local node (in another terminal):
make deploymake deploy ARGS="--network arb sepolia"You can interact with the BasicAccount contract using Foundry's cast command or through the provided scripts.
1. Fund the BasicAccount with ETH:
cast send <BASIC_ACCOUNT_ADDRESS> --value 1ether --rpc-url $SEPOLIA_RPC_URL --account defaultKey2. Check BasicAccount balance:
cast balance <BASIC_ACCOUNT_ADDRESS> --rpc-url $SEPOLIA_RPC_URL3. Withdraw funds (owner only):
cast send <BASIC_ACCOUNT_ADDRESS> "withdraw(uint256)" 500000000000000000 --rpc-url $SEPOLIA_RPC_URL --account defaultKey4. Send a packed user operation through EntryPoint:
# First, set your secondary address for approvals
export SECONDARY_ADDRESS=0x... # address to approve USDC to
# Run the script that creates and sends a user operation
forge script script/SendPackedUserOp.s.sol:SendPackedUserOp --rpc-url $ARB_SEPOLIA_RPC_URL --account defaultKey --broadcast -vvvv
# Or more simply with make commands
make send-packed-user-op ARGS="--network arb sepolia"5. Execute a transaction as owner:
# Call USDC mint through the BasicAccount
cast send <BASIC_ACCOUNT_ADDRESS> "execute(address,uint256,bytes)" <USDC_ADDRESS> 0 0x<ENCODED_MINT_DATA> \
--rpc-url $SEPOLIA_RPC_URL --account defaultKeyDeploy to Sepolia:
make deploy ARGS="--network sepolia"Or using forge directly:
forge script script/DeployContract.s.sol:DeployContract --rpc-url $SEPOLIA_RPC_URL --account defaultKey --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY -vvvvIf automatic verification fails:
forge verify-contract <CONTRACT_ADDRESS> src/MainContract.sol:MainContract --chain-id 11155111 --etherscan-api-key $ETHERSCAN_API_KEY| Network | Contract Address | Explorer |
|---|---|---|
| ARB Sepolia | 0xBD2cd0cEF56260d291fc55f4D112d55DAA495226 |
View on Etherscan |
| ARB Mainnet | TBD |
View on Etherscan |
For production use, consider:
- Professional security audit
- Bug bounty program
- Gradual rollout with monitoring
The BasicAccount implements two-level access control using OpenZeppelin's Ownable and ERC-4337's EntryPoint contract:
Owner Permissions (OpenZeppelin Ownable):
withdraw(uint256 amount): Only the account owner can withdraw ETH from the contracttransferOwnership(address newOwner): Owner can transfer ownership to another addressexecute(address, uint256, bytes)(called directly): Owner can directly execute transactions without going through EntryPoint
EntryPoint Permissions:
validateUserOp(PackedUserOperation, bytes32, uint256): EntryPoint validates user operations by checking the signature against the owner's addressexecute(address, uint256, bytes)(called via EntryPoint): EntryPoint can execute operations on behalf of the owner after validation
Signature Validation Scheme:
- Uses ECDSA signature recovery with EIP-191 message hash formatting
- Signer must match the account owner (no multi-sig support in basic implementation)
- Returns
SIG_VALIDATION_SUCCESS(0) if signature is valid,SIG_VALIDATION_FAILED(1) otherwise
Access Control Matrix:
| Function | Owner | EntryPoint | Other |
|---|---|---|---|
receive() |
✓ | ✓ | ✓ |
validateUserOp() |
✗ | ✓ | ✗ |
execute() |
✓ | ✓ | ✗ |
withdraw() |
✓ | ✗ | ✗ |
getEntryPoint() |
✓ | ✓ | ✓ |
Access Control Vulnerabilities & Mitigations:
- Mitigation: In production, consider implementing a multi-sig wallet as owner or using social recovery
validateUserOp())
- Mitigation: For production, implement nonce checking to prevent replay attacks
- Mitigation: Add timestamp validation if timing is critical for your use case
-
Single Signer Model: Only one owner can validate operations - no multi-sig support. Consider using a multi-sig wallet as the owner in production.
-
No Nonce Validation: The
validateUserOp()function doesn't validate nonces but the entry point contract ensures uniqueness. Not strictly needed but could add logic to ensure ordered execution or some other logic. -
Basic Signature Scheme: Uses simple ECDSA with EOA signatures. No support for account abstraction-specific features like batching or scheduled operations.
-
No Transaction Batching: Each user operation can only execute one transaction. For complex interactions, multiple transactions are required.
-
No Paymaster Support: No integration with paymasters. All transaction fees must be paid by the account itself.
-
Hardcoded Gas Limits: Gas limits in
SendPackedUserOp.s.solare hardcoded and may not be sufficient for complex transactions.
Centralization Risks:
- Full control by a single EOA owner - one compromised key means loss of all funds
- Owner can withdraw all funds at any time without restrictions
- No governance or community oversight in basic implementation
EntryPoint Dependencies:
- Relies completely on EntryPoint contract for transaction bundling and validation
- EntryPoint contract must be trusted and properly implemented
- Any bugs in EntryPoint could compromise account security
| Function | Operation | Typical Gas Cost |
|---|---|---|
validateUserOp() |
Signature validation | ~35-41k |
execute() |
Call execution (varies by target) | ~25k+ |
withdraw() |
Fund withdrawal | ~24-32k |
receive() |
Receive ETH | ~21k |
Generate gas report and save to .txt file:
make gas-reportGenerate gas snapshot:
forge snapshotCompare gas changes:
forge snapshot --diffUserOperation
- A user-signed transaction-like object sent by a user to a bundler
- Contains data needed for account validation and execution
- Bundlers collect multiple UserOperations and submit them to the EntryPoint
- Cannot directly interact with blockchain - requires EntryPoint processing
EntryPoint
- The singleton contract that handles UserOperation bundling and validation
- Acts as the main hub for all account abstraction operations
- Calls
validateUserOp()on the account to verify the signature - Calls
execute()to perform the actual transaction - Manages gas refunds and compensation for bundlers
Account / Smart Wallet
- A smart contract that acts like a user account
- Implements the
IAccountinterface withvalidateUserOp()andexecute()functions - Controls user funds and executes transactions
- Can have custom validation logic (signatures, multi-sig, biometric, etc.)
- In this project:
BasicAccountcontract
Bundler
- An external service that collects UserOperations from users
- Bundles multiple operations together for efficiency
- Submits the bundle to the EntryPoint
- Gets compensated for gas costs in the
verificationGasandcallGasLimitfields - Not part of the smart contracts - it's an off-chain service
Paymaster
- A contract that can sponsor gas fees for UserOperations
- Allows accounts to use any token (not just ETH) to pay for gas
- Interfaces with EntryPoint for validation and payment
- Not implemented in BasicAccount (all fees must be paid by the account in ETH)
initCode
- Code used to deploy the account contract on first use
- Contains the factory contract address and initialization data
- Executed only once (when
nonce == 0) - Empty for already-deployed accounts like in this project
callData
- The encoded function call to execute on the account
- Typically encodes a call to the
execute()function - Specifies the target address, value, and function data
ValidationData
- Return value from
validateUserOp() 0= signature valid and operation can proceed (SIG_VALIDATION_SUCCESS)1= signature invalid, operation will fail (SIG_VALIDATION_FAILED)
Q: How does BasicAccount validate transactions? A: It recovers the signer from the ECDSA signature and verifies the signer matches the account owner. Only the owner can authorize transactions.
Q: What happens if my account runs out of ETH? A: The EntryPoint will revert the operation. Your account must maintain enough ETH to cover gas costs. Consider using a Paymaster for sponsored transactions.
Q: Can I use this on Mainnet? A: Not recommended. This is an educational implementation and has not been audited. Only use on testnets or in development environments.
Q: How do I add multi-sig support?
A: Replace the signature validation logic in _validateSignature() to verify multiple signatures instead of just one. You'd also need to track which signers have approved the operation.
Q: What's the difference between execute() called by EntryPoint vs. owner?
A: Both paths execute the same function, but EntryPoint ensures proper validation happened first and handles gas accounting. Owner direct calls bypass validation (for convenience) but still require the owner key.
Q: Why is nonce validation commented out? A: It's a simplified example. In production, you MUST implement nonce validation to prevent replay attacks. The same UserOperation could be replayed multiple times without proper nonce handling.
Q: Can I batch multiple transactions in one UserOperation?
A: Not in BasicAccount's current implementation. Each UserOperation executes one execute() call. To batch, you'd need to use delegatecall or create a separate batching contract.
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
Disclaimer: This software is provided "as is", without warranty of any kind. Use at your own risk.
Built with Foundry