ERC-4337 security in 2026: the state of account abstraction audits
ERC-4337 Security in 2026: Auditing Account Abstraction
By deivitto — April 2026
Account abstraction has moved from an interesting EIP to core infrastructure. Nearly every major wallet ships some form of AA, paymasters handle millions in gas sponsorship daily, and the EntryPoint contract on Ethereum mainnet processes a growing volume of UserOperations.
With adoption comes attack surface. And AA's attack surface is weird — it doesn't map cleanly to the mental models most auditors carry from traditional DeFi.
Let's break down where the real security risks are in 2026, what vulnerability patterns we keep finding in AA implementations, and how to actually test these systems.
The ERC-4337 Trust Model
Before we get into vulnerabilities, let's be clear about the trust assumptions. ERC-4337 intentionally avoids protocol-level changes, which means it relies on a set of off-chain and on-chain components working together:
EntryPoint. The singleton contract that executes UserOperations. It's the trust anchor. If the EntryPoint has a bug, every wallet using it has a bug.
Smart Account (Wallet). The user's on-chain account. Holds assets, validates signatures, executes transactions.
Bundler. Off-chain actor that bundles UserOperations into transactions. Semi-trusted: can censor but shouldn't be able to steal.
Paymaster. Optional contract that sponsors gas. Holds ETH, decides who gets sponsored.
Aggregator. Optional contract that aggregates signatures for batch verification. Highly sensitive.
Factory. Deploys new smart accounts. The initCode in a UserOperation triggers deployment.
Each of these components has distinct security properties and failure modes.
EntryPoint Risks
The EntryPoint is the most audited contract in the AA ecosystem, and for good reason. A bug here is catastrophic.
Reentrancy in execution phase. The EntryPoint calls into untrusted account and paymaster code. The execution flow is: validate → execute → postOp. Each phase must be isolated to prevent a malicious account from corrupting state during validation.
The v0.7 EntryPoint added better isolation between these phases, but integrations that build around the EntryPoint sometimes reintroduce reentrancy windows.
Gas accounting manipulation. The EntryPoint tracks gas usage carefully to ensure the correct party pays. But gas measurement on EVM is tricky:
// The EntryPoint measures gas like this (simplified):
uint256 preGas = gasleft();
// ... execute the operation ...
uint256 gasUsed = preGas - gasleft();
An attacker who can manipulate gasleft() behavior (through gas griefing or careful calldata sizing) might pay less than expected or force the paymaster to overpay.
Stake/unstake griefing. Entities that access global storage must stake ETH. The staking mechanism has a withdrawal delay. Attacks that trigger unexpected unstaking or delay manipulation can grief legitimate participants.
Smart Account Vulnerabilities
This is where most bugs live. Every team implements their own account logic, and the variation is enormous.
Signature Validation Edge Cases
The validateUserOp function is the security gatekeeper. Get it wrong and anyone can drain the wallet.
Missing chainId in signature hash. If the signature doesn't commit to the chain ID, a valid UserOperation on one chain can be replayed on another:
// VULNERABLE -- no chain ID binding
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external returns (uint256 validationData) {
// userOpHash already includes chainId via EntryPoint,
// but if you're doing custom hash construction:
// BAD -- cross-chain replay possible
bytes32 customHash = keccak256(abi.encode(
userOp.sender,
userOp.nonce,
userOp.callData
));
// GOOD -- chain-specific
bytes32 customHash = keccak256(abi.encode(
userOp.sender,
userOp.nonce,
userOp.callData,
block.chainid
));
}
Nonce management bugs. ERC-4337 uses a 2D nonce scheme: a 192-bit key and a 64-bit sequence number. Some implementations handle this incorrectly, either allowing nonce reuse across keys or not incrementing properly.
Signature malleability. If you're using ECDSA without checking s value canonicality, the same logical signature can produce multiple valid byte representations:
// Always use OpenZeppelin's ECDSA or verify s <= secp256k1n/2
// A malleable signature lets an attacker submit a "different"
// valid signature for the same operation
Module interaction bugs. Modular account architectures (ERC-6900, ERC-7579) let users install/remove validation modules. The security question is: can a malicious module override or bypass the primary validator? We've seen bugs where:
- A module could set itself as the fallback validator, then approve arbitrary operations
- Module removal didn't properly clean up storage, leaving ghost permissions
- Module installation wasn't gated, letting anyone add a malicious validator
Storage Access Rules
ERC-4337 restricts storage access during validation to prevent DoS attacks on bundlers. Validation code can only access:
- The account's own storage
- The account's associated storage in other contracts (mapped by the account's address)
- Staked entities' storage (with staking requirements)
Violations don't cause reverts in the EntryPoint, they cause the bundler to reject the UserOperation. This means:
- Your tests might pass (using a permissive local bundler) while production bundlers reject your operations
- Storage access patterns that work in
executeUserOpmight not work invalidateUserOp
// This works in execute phase but FAILS in validation phase
// if the oracle isn't staked:
function validateUserOp(...) external {
// Reading external contract storage during validation
uint256 price = oracle.getPrice(); // BANNED in validation
require(price > minPrice, "Price too low");
}
Testing for storage rule compliance requires a bundler that enforces ERC-7562 rules, not just a basic Foundry test.
Paymaster Security
Paymasters are essentially saying "I'll pay gas for this operation." They're holding ETH and making decisions about who to sponsor. That's a juicy target.
Common Paymaster Vulnerabilities
Insufficient validation. A paymaster that sponsors too broadly can be drained by anyone submitting expensive operations:
// VULNERABLE -- sponsors everything
function validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) external returns (bytes memory context, uint256 validationData) {
// No checks at all -- anyone can drain the deposit
return (abi.encode(userOp.sender), 0);
}
Signature replay on paymaster approvals. If the paymaster signs an approval for a specific UserOperation but the signature doesn't bind to enough fields, an attacker can reuse the approval:
// The paymaster signature MUST include:
// - sender
// - nonce (prevents replay)
// - callData (prevents operation substitution)
// - maxGasValues (prevents gas griefing)
// - validUntil/validAfter (time-bounds the approval)
bytes32 hash = keccak256(abi.encode(
userOp.sender,
userOp.nonce,
keccak256(userOp.callData),
userOp.accountGasLimits,
userOp.preVerificationGas,
userOp.gasFees,
block.chainid,
address(this),
validUntil,
validAfter
));
postOp manipulation. The postOp function runs after the UserOperation executes. If the paymaster uses postOp to charge the user (e.g., in ERC-20 tokens), the user's operation might manipulate token balances to avoid payment:
// Attack flow:
// 1. Paymaster sponsors gas, plans to charge USDC in postOp
// 2. User's operation transfers all USDC out of the account
// 3. postOp tries to charge USDC -- fails or charges 0
// 4. Paymaster paid the gas but got nothing in return
The v0.7 EntryPoint partially addresses this with a postOpReverted mode, but implementations still get the accounting wrong.
Bundler Trust Assumptions
Bundlers are semi-trusted. They can't steal funds directly, but they can:
- Censor operations. Simply not include your UserOperation.
- Front-run. See the UserOperation in the mempool and extract value.
- Grief. Submit operations with manipulated gas parameters.
- Reorder. Choose the order of operations in a bundle for profit.
The mitigation is bundler competition, if your bundler censors you, use a different one. But in practice, bundler diversity is still limited on most chains. This is an infrastructure risk more than a smart contract risk, but it affects security posture.
Factory Deployment Risks
The initCode field in a UserOperation can deploy a new smart account. Security issues here:
Counterfactual address manipulation. The CREATE2 address depends on the factory address, salt, and init code. If any of these can be manipulated, an attacker might deploy a different contract at the expected address:
// A factory should be deterministic and tamper-proof
function createAccount(
address owner,
uint256 salt
) external returns (address account) {
// The account address must be fully determined by (owner, salt)
// No other mutable state should influence deployment
account = address(new SmartAccount{salt: bytes32(salt)}(
entryPoint,
owner
));
}
Griefing through pre-deployment. An attacker can deploy someone's account before they do, potentially with different initialization parameters if the factory allows it.
Testing AA Systems with Fuzzing
Standard smart contract fuzzing doesn't capture the full AA attack surface. You need to model the UserOperation flow.
We've written a detailed guide on how to fuzz ERC-4337 account abstraction. Here's the high-level approach:
Model the full lifecycle:
function invariant_accountBalanceConsistency() public {
// After any sequence of UserOperations,
// the account's balance should match expected state
uint256 expectedBalance = ghost_initialBalance
- ghost_totalGasPaid
- ghost_totalTransferred
+ ghost_totalReceived;
assertGe(
account.balance,
expectedBalance,
"Account balance underflow -- possible gas accounting bug"
);
}
function handler_executeViaEntryPoint(
uint256 targetSeed,
uint256 valueSeed,
bytes calldata randomCalldata
) external {
// Build a UserOperation with fuzzed parameters
PackedUserOperation memory userOp = buildUserOp(
targetSeed, valueSeed, randomCalldata
);
// Sign it properly
bytes32 hash = entryPoint.getUserOpHash(userOp);
userOp.signature = signHash(hash, ownerKey);
// Execute through EntryPoint
PackedUserOperation[] memory ops = new PackedUserOperation[](1);
ops[0] = userOp;
try entryPoint.handleOps(ops, payable(bundler)) {
ghost_totalGasPaid += calculateGasCost(userOp);
} catch {
// Track failed ops separately
}
}
Key properties to test:
- No unauthorized execution. Operations with invalid signatures must revert.
- Gas accounting correctness. The correct party always pays the correct amount.
- Nonce uniqueness. No nonce can be used twice.
- Module isolation. One module can't affect another's validation.
- Paymaster solvency. The paymaster deposit always covers committed gas.
For more on stateful fuzzing, check our learning resources.
Current State of AA Security Tooling
The tooling has gotten better, but gaps remain.
What works:
- Foundry for unit testing account logic
- Slither and Aderyn for static analysis (with AA-specific detectors)
- Custom invariant testing frameworks for protocol-level properties
- Halmos for symbolic execution of validation logic
What's still rough:
- No great way to test bundler rule compliance in Foundry
- Paymaster economic simulations are mostly manual
- Cross-chain AA testing (same account on multiple chains) lacks tooling
- Module interaction testing for ERC-6900/7579 is still early
What we'd like to see:
- Bundler-in-the-loop testing frameworks
- Formal models of the AA mempool
- Automated storage access rule verification
- Better gas estimation testing tools
Practical Recommendations
If you're building or auditing an AA system:
-
Test validation separately from execution. They have different security properties and different EVM constraints.
-
Simulate bundler rejection. Don't just test if operations succeed, test if a real bundler would accept them.
-
Fuzz the paymaster. Generate random UserOperations and verify the paymaster never loses money. This is where property-based testing really shines.
-
Check signature binding. Every field that affects operation semantics should be included in the signed hash.
-
Test module combinations. If you support modular validation, test every possible combination of installed modules.
-
Verify upgrade paths. Account upgrades are especially sensitive, a bug in the upgrade function can brick every wallet.
The ERC-4337 learning page covers more foundational concepts, and our smart contract security resources apply broadly to AA development.
Where This Is Heading
AA security is maturing. The EntryPoint v0.7 fixed several classes of issues from v0.6. Bundler specifications (ERC-7562) are tightening. Module standards are stabilizing.
But the attack surface is growing faster than the defense. New paymaster designs, new module types, new account architectures, cross-chain AA, and intent-based execution layers, each adds complexity.
The teams that stay safe are the ones that treat AA security as an ongoing process, not a one-time audit. Write properties. Fuzz continuously. Monitor on-chain.
Get an ERC-4337 security review
Related Posts
How to fuzz ERC-4337 account abstraction wallets
ERC-4337 wallets validate their own transactions and manage gas accounting. This guide covers the in...
Mutation testing for smart contracts: measure your test suite quality
Your tests pass. But are they actually good? Mutation testing injects faults into your code and chec...