3.11.7 Account Abstraction (ERC-4337)
Account Abstraction (AA) changes one of Ethereum's most fundamental assumptions: that every transaction originates from an externally-owned account (EOA) controlled by a secp256k1 private key. Under ERC-4337, ordinary user accounts can be smart contracts. The key that authorizes a transaction can be a multisig, a passkey, a session key with limited scope, or any other authentication scheme the account's code chooses to implement. The pattern unlocks substantial UX improvements — social recovery, gas sponsorship, batched operations, custom signature schemes — and changes the security surface in ways that affect every contract that interacts with users.
For a protocol developer, AA matters because the user accounts your protocol sees may not behave like EOAs. Assumptions that held implicitly when every caller was an EOA (an EOA can't reenter, an EOA can't have a contract identity, an EOA has a single private key, an EOA's transactions go through the public mempool) may now be false. A contract designed under EOA assumptions may behave incorrectly when its caller is a smart account. And a contract designing for AA users must reason about a more complex permissions model, a different mempool, and a new class of attacks specific to the AA architecture.
This subsection covers what changes from the protocol developer's perspective. ERC-4337 as a standard has substantial dedicated documentation (the canonical sources are eips.ethereum.org/EIPS/eip-4337 and docs.erc4337.io); this section focuses on the security implications and design choices that apply to protocols interacting with smart accounts.
The Architecture, in Brief
ERC-4337 was finalized in March 2023 and works without changes to Ethereum's consensus layer. Instead of modifying the protocol, it introduces a parallel transaction system:
- UserOperation: a struct representing a user's intent (sender, calldata, gas limits, signature, paymaster info)
- Smart Account: a contract implementing
validateUserOp()to authorize andexecute()to perform operations - Bundler: an off-chain actor that collects UserOperations from an alternate mempool, packages them, and submits them on-chain
- EntryPoint: a singleton contract that validates and executes UserOperations; users' smart accounts trust this specific address
- Paymaster (optional): a contract that sponsors gas for a UserOperation, enabling gasless UX or non-ETH gas payment
- Aggregator (optional): a contract that batch-validates multiple signatures, useful for BLS-style multisignatures
The flow:
- User signs a UserOperation off-chain
- UserOp is submitted to the alt-mempool
- A bundler picks it up, performs an off-chain validation simulation, and includes it in a bundle
- The bundler sends a transaction to the EntryPoint, calling
handleOps(userOps[]) - The EntryPoint iterates through the bundle: for each UserOp it calls the smart account's
validateUserOp, then (if applicable) the paymaster'svalidatePaymasterUserOp, then the smart account'sexecute(or whatever function the calldata targets) - Gas is settled — either from the account's pre-deposit, from the paymaster, or refunded to the bundler
The EntryPoint at 0x0000000071727De22E5E9d8BAf0edAc6f37da032 is the canonical v0.7 deployment on Ethereum mainnet and most major chains (Base, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche). Production smart wallets (Coinbase Smart Wallet, Safe, Argent, ZeroDev) all rely on it.
A more recent extension, EIP-7702 (Pectra upgrade, 2025), allows existing EOAs to temporarily delegate to smart contract code for the duration of a transaction without permanently becoming a smart account. ERC-4337 supports EIP-7702 authorization tuples alongside UserOperations. This further blurs the EOA/contract distinction: an address that is an EOA today may behave like a smart account in a specific transaction.
What Changes for Protocol Developers
The architecture's implications fall into several categories.
tx.origin Is No Longer a Meaningful Distinction
Pre-AA, tx.origin == msg.sender was a (broken) way to check "is the caller an EOA?" Under AA, every UserOp eventually flows through the EntryPoint contract, so tx.origin is the bundler's address — a contract or EOA you have no relationship with — and msg.sender in the call chain is whichever contract is currently making the call (usually the smart account, possibly via further internal calls).
The historical pattern:
// Broken under AA — and was broken before AA too
require(msg.sender == tx.origin, "no contracts");
This check was never a good idea (Section 3.11.2 covers the original reasons). Under AA it is actively harmful: legitimate users who use smart wallets fail the check. Protocols still using tx.origin == msg.sender for any security purpose should remove the check. If you need to gate operations, gate on permissions (signatures, roles, allowlists), not on caller type.
Signatures Don't Mean What They Used To
A signature on an Ethereum transaction has historically meant: "the holder of the secp256k1 private key authorized this." Under AA, a signature means whatever the smart account's validateUserOp decides it means. Possible interpretations:
- A traditional secp256k1 signature (the simplest case)
- A WebAuthn / passkey signature (P-256 curve, FIDO-format authenticator data)
- A multisig threshold signature (N independent signers, M required)
- A session-key signature (a key limited to specific contracts, specific function selectors, or time-bounded validity)
- An aggregated BLS signature (multiple participants in one signature)
- A signature backed by a remote attestation (e.g., trusted hardware proving a key never left it)
For protocols that consume signatures off-chain (permit-style flows, signed-message authorization, etc.), this matters operationally. A protocol verifying signatures against an EOA address can call ecrecover directly. A protocol verifying signatures against a smart account address must call the account's isValidSignature(hash, signature) function (ERC-1271):
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
contract SignatureVerifier {
using ECDSA for bytes32;
bytes4 internal constant ERC1271_MAGIC = 0x1626ba7e;
function verifySignature(address signer, bytes32 hash, bytes memory signature)
public view returns (bool)
{
// Smart accounts: forward to their isValidSignature
if (signer.code.length > 0) {
try IERC1271(signer).isValidSignature(hash, signature) returns (bytes4 result) {
return result == ERC1271_MAGIC;
} catch {
return false;
}
}
// EOAs: standard ecrecover
address recovered = hash.recover(signature);
return recovered == signer && recovered != address(0);
}
}
OpenZeppelin's SignatureChecker library implements this pattern. Protocols that handle off-chain signatures should use it or equivalent; rolling your own check often produces edge-case bugs.
Important caveat: ERC-1271 signatures are not always replay-protected the same way ECDSA signatures are. The smart account decides what counts as a valid signature for a given message hash; some implementations may accept the same signature multiple times across different contexts. Protocols consuming ERC-1271 signatures should include their own replay protection (nonces, chain ID binding, contract address binding) in the message itself, not rely on ECDSA's deterministic single-signer guarantee.
msg.sender Is Now a Contract
When a smart account calls into your protocol, msg.sender is the smart account's contract address, not the user's "human identity." This affects:
- Token approvals: the user grants approvals to the smart account; the smart account then operates with those approvals
- Per-user limits: tracked by smart account address; if a user has multiple smart accounts, each is treated independently
- Address-bound state: the smart account's address is the identity that matters; the secp256k1 key that ultimately signed the UserOp is invisible to your protocol
- Receiving callbacks: callbacks (ERC-721/1155 receivers, flash loan callbacks, etc.) go to the smart account, which must implement them — or they revert
For protocols that distinguish "is this a contract?" using msg.sender.code.length, the answer is now usually "yes" for legitimate users. This breaks several historical patterns that assumed contract-callers were less trusted than EOA-callers.
The User-Operation Mempool Is Not the Public Mempool
UserOperations live in a separate "alt-mempool." Most current production bundlers maintain private mempools — Pimlico, Stackup, Biconomy, Etherspot, and several others each run their own. A UserOp's submission to one bundler does not automatically reach others (though some bundlers gossip).
For MEV considerations (Section 3.11.3), this changes the dynamic substantially. A UserOp submitted to a private bundler is not visible to public-mempool searchers. Sandwich attacks against UserOperations are harder than against public-mempool transactions. The bundler itself, however, has full information and could in principle extract MEV — most reputable bundlers do not, but the trust model is "trust the bundler."
For visibility, protocols that monitor for incoming transactions need to account for both mempools. UserOps eventually hit the chain as part of bundle transactions to the EntryPoint, but the timing and content visibility differ from EOA-initiated transactions.
Security Pitfalls in Smart Accounts Themselves
For developers building smart accounts (rather than protocols that interact with them), several specific risks apply.
Validation Rules Are Strict
The EntryPoint's validation flow has strict storage-access rules — designed to make UserOp simulation deterministic so bundlers can safely accept UserOps without on-chain execution. The rules (formalized as ERC-7562):
validateUserOpmay only access storage in the sender's own contract (with limited exceptions for staked entities)- Forbidden opcodes include
TIMESTAMP,NUMBER,BLOCKHASH,GAS,GASPRICE,BLOBBASEFEE,BASEFEE(these can change between simulation and execution, creating griefing vectors) - External calls to unrelated contracts are restricted
- Random/oracle reads during validation are not allowed
A smart account whose validateUserOp violates these rules will not be accepted by bundlers. Many subtle bugs in early smart account implementations were rule violations — accessing storage on a related contract, using block.timestamp for time-bounded session keys without proper staking, etc.
The pattern: validateUserOp does the minimum work needed to authorize the operation, then returns. All complex logic happens in execute.
Account Initialization
Smart accounts are typically deployed when their first UserOp is processed — the UserOp's initCode field tells the EntryPoint how to deploy. If initialization is mishandled, the account can be deployed in an attacker-controlled state.
Specific patterns to avoid:
-
Predictable counterfactual addresses: the smart account's address can be computed before deployment (via CREATE2). If the deployment params can be manipulated by anyone, an attacker can race to deploy a different account at the expected address. The factory contract must ensure that only the legitimate owner can initialize at a given address — typically by including the owner's signature in the init data.
-
Initialization without
initializermodifier: the same Parity-pattern issue from Section 3.10.2. The factory deploys a minimal proxy that points at an implementation. The implementation must be_disableInitializers()-protected to prevent direct initialization on the implementation itself.
Session Keys and Limited Authority
A powerful AA pattern: short-lived signing keys with scope limited to specific contracts, specific function selectors, or specific operations. A user grants their wallet a "session key" that can only call a specific game's functions for the next hour, with a spending limit.
Implementation security:
- Session keys must be revocable (the user can disable them mid-session if compromised)
- Scope limits must be enforced during validation, not just at execution time (so bundlers reject invalid UserOps before paying for them)
- Time-based limits must use the smart account's own state rather than block timestamps (per the validation rules above) — typically a
validUntilfield signed by the master key
A session-key bug can grant broader authority than intended. The pattern of "this key can only call function X on contract Y" is easy to get wrong — for example, by validating the target but not the function selector, allowing the session key to call other functions on the intended contract.
Paymaster Risks
Paymasters expose substantial attack surface. A paymaster pays gas for sponsored UserOperations; if the validation logic is wrong, the paymaster pays for transactions that don't actually qualify for sponsorship. Several specific risks:
1. Paying for failed operations. If a UserOp passes paymaster validation but reverts during execution, the paymaster still pays the gas. A paymaster must simulate carefully and reject UserOps that are likely to fail.
2. Gas estimation errors. The paymaster's validatePaymasterUserOp returns a maximum cost it's willing to bear. If the actual cost exceeds this (e.g., because the bundler set higher gas prices than expected, or because the execution consumed more gas than estimated), the paymaster bears the loss.
3. Permitting paymaster-revert griefing. If postOp can be made to revert, the entire UserOp reverts, and the paymaster still pays. An attacker can craft UserOps that cause postOp to revert, draining the paymaster's deposit.
4. ERC-20-paid gas pricing. Paymasters that let users pay gas in ERC-20 tokens must accurately price the token at execution time. Manipulating the token's price during the UserOp (e.g., via a flash loan that affects the token's spot price) can let an attacker pay less than the actual cost.
5. Sponsorship rules. A paymaster that sponsors based on application-specific logic must validate that logic completely. "Whitelist-only" paymasters that omit one check have produced real losses. The Pimlico-style "verifying paymaster" requires a signature from the sponsor's off-chain key; the signature must bind to specific UserOp parameters, not just the sender.
For paymaster developers, several principles:
- Collect full payment during validation, not after execution. By the time
postOpruns, the UserOp may have failed. - Be conservative with gas estimation. Include safety margins.
- Test extensively against malicious UserOperations — what happens if the user passes garbage calldata? extreme gas values? recursive calls?
- Review new EntryPoint versions carefully — the v0.6 → v0.7 transition changed paymaster semantics in subtle ways. Future versions will continue to do so.
Bundler Trust
Bundlers can theoretically censor or reorder UserOperations. The current ecosystem has multiple competitive bundlers, so any single bundler censoring should not block users (the user can submit to a different bundler). But this is an operational property, not a cryptographic one. Bundler behavior is part of the trust model for AA-based applications.
Protocol Patterns for AA Compatibility
For protocols building to integrate with AA-using users, several patterns are well-established.
Use ERC-1271 for Off-Chain Signatures
As discussed above. Any signature-verifying protocol should use SignatureChecker.isValidSignatureNow() (OpenZeppelin) or equivalent rather than direct ecrecover. This single change covers most basic AA compatibility issues.
Don't Hardcode Permit Patterns
ERC-2612 permits sign a signature into a transaction's calldata, allowing token approvals and operations in a single transaction. Permits assume the signer is an EOA. Smart accounts can implement permit-like functionality via UserOps directly, often more flexibly. Protocols that hardcode the ECDSA permit pattern force users into EOAs.
The fix: provide permit-based flows as one of multiple authorization paths, not the only one.
Allow Smart Account Receivers
If your protocol sends tokens, NFTs, or callbacks to user-specified addresses, the receiver may be a smart account that needs to implement specific receiver functions:
- ERC-721:
onERC721Received(operator, from, tokenId, data) returns (bytes4) - ERC-1155:
onERC1155ReceivedandonERC1155BatchReceived - Flash loan:
executeOperationoronFlashLoandepending on the standard
Smart accounts in 2026 generally implement these standard receivers by default, but custom or older smart accounts may not. The pattern: when a token transfer to a contract recipient fails because of a missing receiver, this is the receiver's bug, not your protocol's — but documentation should make the requirements clear.
Batch Operations Are More Common
Smart accounts can batch multiple operations into a single UserOp. A user might in one UserOp: approve a token, deposit it into a vault, stake the receipt token, and claim historical rewards. For your protocol, this means a single transaction may interact with many of your functions in sequence, each call from the same msg.sender.
For protocols with assumptions about transaction structure ("a user can only deposit OR withdraw in a single transaction"), batched UserOps may break those assumptions. Test with batched scenarios.
Be Careful with msg.sender-Based State
If your protocol tracks state per msg.sender, a single user with multiple smart accounts has independent state in each. This is usually fine (the smart accounts are independent identities), but for protocols with one-per-user limits or KYC requirements, this means smart accounts cannot reliably identify the underlying human.
For protocols that need stable user identity, additional verification (signed attestations, identity oracles, social-graph verification) is required. The smart account's address alone does not identify the user.
EIP-7702 and Hybrid Identities
EIP-7702, included in the Pectra upgrade (May 2025), allows an EOA to temporarily delegate its execution to smart contract code within a single transaction. The mechanism: the EOA signs an "authorization tuple" pointing at a specific implementation contract; for that transaction, the EOA's code is treated as the implementation's code.
Implications:
- An address that is an EOA today may behave like a smart contract tomorrow, then return to being an EOA
- The "is this a contract?" check (
address.code.length > 0) becomes unreliable in flux — it may be true during a 7702-delegated transaction even if the underlying address is "really" an EOA - The same address could have different code in different transactions
For protocols, the practical impact is that address-based identity is becoming more flexible than the EOA/contract dichotomy historically allowed. Protocols that depend on a stable "this is an EOA" or "this is a contract" classification may need to adapt. The pragmatic guidance: treat caller identity as a permissions problem (what can this address do?), not a type problem (what kind of address is this?).
ERC-4337 v0.7 already supports EIP-7702 authorization tuples in UserOperations; the integration is increasingly mature.
Practical Checklist
For a protocol designing for AA compatibility:
-
No
tx.origin == msg.senderchecks anywhere in the contract -
All signature verification uses ERC-1271-compatible flows (e.g., OpenZeppelin's
SignatureChecker) - ERC-721 / ERC-1155 / flash loan receivers are not assumed to be implemented (transactions to non-implementing recipients should not silently fail)
- Address-based per-user limits / state account for the possibility that one user has multiple smart accounts
- Batched operations from the same caller are tested explicitly
- The protocol does not depend on a stable EOA/contract classification (EIP-7702 may change this per-transaction)
- Documentation indicates AA compatibility status (which flows work, which require additional handling)
For a protocol building a smart account:
-
validateUserOpfollows ERC-7562 storage-access rules -
Implementation contract calls
_disableInitializers()in its constructor - Factory contract verifies caller identity before initializing an account at a CREATE2-derived address
-
Session keys (if supported) include both target address AND function selector scope, plus a
validUntilfield - Session key revocation is testable and works
-
The account supports ERC-1271
isValidSignaturefor off-chain signature verification - Standard receiver functions (ERC-721, ERC-1155, ERC-3156) are implemented or explicitly noted as unsupported
For a protocol building a paymaster:
-
Sponsorship rules are validated completely during
validatePaymasterUserOp - Payment is collected during validation, not after execution
-
postOpis robust against revert-induced griefing - Gas estimation includes safety margins
- ERC-20-based pricing is sourced from a manipulation-resistant oracle (Section 3.11.1)
- Maximum exposure per UserOp is capped to prevent paymaster drains
- Tests cover malicious UserOps (garbage calldata, extreme gas values, recursive calls)
Cross-References
- Composability — Section 3.11.2 covers the broader caller-trust model that AA generalizes
- Anti-patterns — Section 3.7.7 covers
tx.originand other patterns that fail under AA - Defensive patterns — Section 3.7.3 covers role-based access control that works regardless of caller type
- Signature verification — Section 3.8.8 covers signature replay protection patterns; ERC-1271 signatures have specific considerations
- MEV — Section 3.11.3 covers the mempool implications of UserOperations vs. traditional transactions
- Upgradeability — Section 3.10.2 (Parity) covers the
_disableInitializers()discipline that applies to smart account implementations - ERC-4337 specification —
https://eips.ethereum.org/EIPS/eip-4337 - ERC-4337 implementation documentation —
https://docs.erc4337.io - EIP-7702 specification —
https://eips.ethereum.org/EIPS/eip-7702 - OpenZeppelin SignatureChecker —
https://docs.openzeppelin.com/contracts/utils/cryptography/SignatureChecker