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.

Crypto Training2026-01-012 min read

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#

ComponentIncorrect assumptionCorrect 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#

SOLIDITY
// 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#

SOLIDITY
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.
SOLIDITY
function validatePaymasterUserOp(...) external returns (bytes memory context, uint256 validationData) {
    require(maxCost <= maxSponsoredCost, "cost cap");
    // strict policy checks...
}

Audit runbook#

  1. Model every call in validateUserOp and postOp as adversarially triggerable.
  2. Prove signature domain binding includes chain + account + nonce semantics.
  3. Fuzz bundles with alternating account/paymaster identities.
  4. Stress gas-penalty and sponsorship exhaustion cases.

Further reading#