Crypto Training
ERC-4337 Auditor Threat Model: Bundles, Paymasters, and Signature Boundaries
Account abstraction audits fail when they treat UserOperation validation as a normal transfer flow. This post maps the real trust boundaries for EntryPoint systems.
ERC-4337 changes where security-critical checks happen.
If you audit it like ERC-20 flows, you miss the dangerous edges.
flowchart LR
U[UserOperation] --> B[Bundler simulation]
B --> E[EntryPoint validation]
E --> A[Account validateUserOp]
E --> P[Paymaster validatePaymasterUserOp]
E --> X[Execution]
X --> O[postOp settlement]
Key trust boundaries#
| Component | Incorrect assumption | Correct model |
|---|---|---|
| Bundler | “honest pre-check only” | adversarial relayer economics |
| Paymaster | “postOp always settles cleanly” | griefing + revert-driven drain paths |
| Account contract | “signature check is enough” | domain/account binding is mandatory |
| Factory | “counterfactual address is safe by default” | salt design can enable takeover |
Signature binding failure pattern#
// bad pattern
function isValidSignature(bytes32 hash, bytes memory sig) public view returns (bytes4) {
address signer = ECDSA.recover(hash, sig);
return signer == owner ? 0x1626ba7e : 0xffffffff;
}
If hash is not domain-bound to this account and chain, replay across sibling wallets can occur.
Safer pattern#
bytes32 digest = _hashTypedDataV4(
keccak256(abi.encode(
USER_OP_TYPEHASH,
address(this),
block.chainid,
userOp.nonce,
keccak256(userOp.callData)
))
);
Bundle-level state contamination#
Transient and mutable state in multi-operation bundles must be cleared deterministically.
sequenceDiagram
participant EP as EntryPoint
participant A as Account A
participant B as Account B
EP->>A: validate
A-->>EP: writes transient flags
EP->>B: validate
B-->>EP: reads stale transient flag (bug)
Paymaster grief paths#
Watch for:
- high gas limits with low actual use,
- revert-heavy
postOp, - insufficient bounds on sponsorship policy.
function validatePaymasterUserOp(...) external returns (bytes memory context, uint256 validationData) {
require(maxCost <= maxSponsoredCost, "cost cap");
// strict policy checks...
}
Audit runbook#
- Model every call in
validateUserOpandpostOpas adversarially triggerable. - Prove signature domain binding includes chain + account + nonce semantics.
- Fuzz bundles with alternating account/paymaster identities.
- Stress gas-penalty and sponsorship exhaustion cases.