Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Module 4: Account Abstraction

Difficulty: Intermediate

Estimated reading time: ~40 minutes | Exercises: ~4-5 hours

πŸ“š Table of Contents

ERC-4337 Architecture

EIP-7702 and DeFi Implications

Paymasters and Gas Abstraction


πŸ’‘ ERC-4337 Architecture

πŸ’‘ Concept: The Problem Account Abstraction Solves

Why this matters: As of 2025, over 40 million smart accounts are deployed on EVM chains. Major wallets (Coinbase Smart Wallet, Safe, Argent, Ambire) have migrated to ERC-4337. If your DeFi protocol doesn’t support account abstraction, you’re cutting off a massive and growing user base.

πŸ“Š The fundamental limitations of EOAs:

Ethereum’s account model has two types: EOAs (controlled by private keys) and smart contracts. EOAs are the only accounts that can initiate transactions. This creates severe UX limitations:

LimitationImpactReal-World Cost
Must hold ETH for gasUsers with USDC but no ETH can’t transactMassive onboarding friction
Lost key = lost fundsNo recovery mechanismBillions in lost crypto (estimates vary widely)
Single signature onlyNo multisig, no social recoveryEnterprise users forced to use external multisig
No batch operationsSeparate tx for approve + swap2x gas costs, poor UX

First proposed in EIP-4337 (September 2021), deployed to mainnet (March 2023)

✨ The promise of account abstraction:

Make smart contracts the primary account type, with programmable validation logic. ERC-4337 achieves this without any changes to the Ethereum protocol itselfβ€”everything is implemented at a higher layer.

πŸ” Deep dive: Cyfrin Updraft - Account Abstraction Course provides hands-on Foundry tutorials. QuickNode - ERC-4337 Guide covers fundamentals and implementation patterns.

πŸ”— DeFi Pattern Connection

Why DeFi protocol developers must understand account abstraction:

  1. User Onboarding β€” Lending protocols (Aave, Compound) and DEXes lose users at the β€œneed ETH for gas” step. Paymasters eliminate this entirely β€” new users deposit USDC without ever holding ETH.

  2. Batch DeFi Operations β€” Smart accounts can atomically: approve + deposit + borrow + swap in one UserOperation. Your protocol must handle these composite calls without reentrancy issues.

  3. Institutional DeFi β€” Enterprise users require multisig (3-of-5 signers to execute a trade). ERC-4337 makes this native instead of requiring external multisig contracts like Safe wrapping every interaction.

  4. Cross-Chain UX β€” Smart accounts + paymasters enable β€œswap on Arbitrum, pay gas in USDC on mainnet” patterns. Bridge protocols and aggregators are building this now.

The shift: DeFi is moving from β€œuser manages gas and approvals manually” to β€œprotocol handles everything under the hood.” Understanding this shift is essential for designing modern protocols.

πŸ’Ό Job Market Context

Interview question you WILL be asked:

β€œWhat is account abstraction and why does it matter for DeFi?”

What to say (30-second answer): β€œAccount abstraction makes smart contracts the primary account type, replacing EOA limitations with programmable validation. ERC-4337 achieves this without protocol changes through a system of UserOperations, Bundlers, an EntryPoint contract, and Paymasters. For DeFi, it means gasless onboarding, batch operations, custom signature schemes, and institutional-grade access controls. Over 40 million smart accounts are deployed β€” protocols that don’t support them are losing users.”

Follow-up question:

β€œWhat’s the difference between ERC-4337 and EIP-7702?”

What to say: β€œERC-4337 deploys new smart contract accounts β€” full flexibility but requires asset migration. EIP-7702, activated with Pectra in May 2025, lets existing EOAs delegate to smart contract code β€” same address, no migration. Delegation persists until explicitly revoked. They’re complementary: an EOA can use EIP-7702 to delegate to an ERC-4337-compatible implementation, getting the full bundler/paymaster ecosystem without changing addresses.”

Interview Red Flags:

  • 🚩 β€œAccount abstraction requires a hard fork” β€” ERC-4337 is entirely at the application layer
  • 🚩 Not knowing that msg.sender == tx.origin breaks with smart accounts
  • 🚩 Can’t name the ERC-4337 components (EntryPoint, Bundler, Paymaster)

Pro tip: Mention real adoption numbers β€” 40M+ smart accounts, Coinbase Smart Wallet, Safe migration to 4337. Show you track the ecosystem, not just the spec.

⚠️ Common Mistakes

// ❌ WRONG: Blocking smart accounts with EOA-only checks
function deposit() external {
    require(msg.sender == tx.origin, "No contracts");  // Breaks all smart wallets!
}

// βœ… CORRECT: Allow both EOAs and smart accounts
function deposit() external {
    // No msg.sender == tx.origin check β€” smart accounts welcome
}

πŸ’‘ Concept: The ERC-4337 Components

The actors in the system:

1. UserOperation

A pseudo-transaction object that describes what the user wants to do. It includes all the fields of a regular transaction (sender, calldata, gas limits) plus additional fields for smart account deployment, paymaster integration, and signature data.

Think of it as a β€œtransaction intent” rather than an actual transaction.

struct PackedUserOperation {
    address sender;              // The smart account
    uint256 nonce;
    bytes initCode;              // For deploying account if it doesn't exist
    bytes callData;              // The actual operation to execute
    bytes32 accountGasLimits;    // Packed: verificationGas | callGas
    uint256 preVerificationGas;  // Gas to compensate bundler
    bytes32 gasFees;             // Packed: maxPriorityFee | maxFeePerGas
    bytes paymasterAndData;      // Paymaster address + data (if sponsored)
    bytes signature;             // Smart account's signature
}

2. Bundler

An off-chain service that collects UserOperations from an alternative mempool, validates them, and bundles multiple UserOperations into a single real Ethereum transaction.

Bundlers compete with each otherβ€”it’s a decentralized market. They call handleOps() on the EntryPoint contract and get reimbursed for gas.

Who runs bundlers: Flashbots, Alchemy, Pimlico, Biconomy, and any party willing to operate one. Public bundler endpoints.

3. EntryPoint

A singleton contract (one per network, shared by all smart accounts) that orchestrates the entire flow. It receives bundled UserOperations, validates each one by calling the smart account’s validation function, executes the operations, and handles gas payment.

Deployed addresses:

4. Smart Account (Sender)

The user’s smart contract wallet. Must implement validateUserOp() which the EntryPoint calls during validation. This is where custom logic livesβ€”multisig, passkey verification, social recovery, spending limits.

πŸ—οΈ Popular implementations:

πŸ“ Modular Account Standards (2024-2025):

The ecosystem is converging on standardized module interfaces so plugins can work across different smart accounts:

  • ERC-6900 β€” Modular Smart Contract Accounts. Defines a standard plugin interface (validation, execution, hooks) so modules are portable across account implementations. Led by Alchemy (Modular Account).
  • ERC-7579 β€” Minimal Modular Smart Accounts. A lighter alternative to ERC-6900 with fewer constraints, adopted by Rhinestone and Biconomy. Defines four module types: validators, executors, hooks, and fallback handlers.

Why this matters for DeFi: Modular accounts enable session keys (temporary authorization for specific protocols), spending limits (auto-DCA without full key access), and recovery modules. Your protocol may need to interact with these modules for advanced integrations.

5. Paymaster

An optional contract that sponsors gas on behalf of users. When a UserOperation includes paymaster data, the EntryPoint calls the paymaster to verify it agrees to pay, then charges the paymaster instead of the user.

This enables gasless interactionsβ€”a dApp can pay its users’ gas costs, or accept ERC-20 tokens as gas payment. ✨

6. Aggregator

An optional component for signature aggregationβ€”multiple UserOperations can share a single aggregate signature (e.g., BLS signatures), reducing on-chain verification cost.

πŸ” Deep Dive: Packed Fields in ERC-4337

Why this matters: ERC-4337 v0.7 aggressively packs data to minimize calldata costs (which dominate L2 gas). If you misunderstand the packing, your smart account won’t work.

PackedUserOperation β€” accountGasLimits (bytes32):

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   verificationGasLimit         β”‚      callGasLimit              β”‚
β”‚   (128 bits / 16 bytes)        β”‚   (128 bits / 16 bytes)        β”‚
β”‚   Gas for validateUserOp()     β”‚   Gas for execution phase      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   high 128 bits                β”‚   low 128 bits                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      bytes32 (256 bits)

PackedUserOperation β€” gasFees (bytes32):

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   maxPriorityFeePerGas         β”‚      maxFeePerGas              β”‚
β”‚   (128 bits / 16 bytes)        β”‚   (128 bits / 16 bytes)        β”‚
β”‚   Tip for the bundler          β”‚   Max total gas price          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   high 128 bits                β”‚   low 128 bits                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      bytes32 (256 bits)

validationData return value β€” the trickiest packing:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     validAfter       β”‚     validUntil       β”‚  aggregator / sigFailed      β”‚
β”‚     (48 bits)        β”‚     (48 bits)        β”‚  (160 bits)                  β”‚
β”‚  Not-before timestampβ”‚  Expiration timestampβ”‚  0 = no aggregator, valid    β”‚
β”‚  0 = immediately     β”‚  0 = no expiration   β”‚  1 = SIG_VALIDATION_FAILED   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  bits 208-255        β”‚  bits 160-207        β”‚  bits 0-159                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              uint256 (256 bits)

Common return values:

return 0;    // βœ… Signature valid, no time bounds, no aggregator
return 1;    // ❌ Signature invalid (SIG_VALIDATION_FAILED in aggregator field)

// With time bounds:
uint256 validAfter = block.timestamp;
uint256 validUntil = block.timestamp + 1 hours;
return (validUntil << 160) | (validAfter << 208);
// This creates a 1-hour validity window

πŸ” Deep Dive: validationData Packing β€” Worked Example

Let’s trace through a concrete example. Say your smart account wants to return: β€œsignature valid, usable from timestamp 1700000000, expires at 1700003600 (1 hour later).”

Given:
  sigFailed  = 0 (valid signature)
  validAfter = 1700000000 = 0x6553_F100
  validUntil = 1700003600 = 0x6554_0110

Step 1: Pack sigFailed into bits 0-159
  Since sigFailed = 0, the low 160 bits are all zeros.
  Result so far: 0x00000000...0000 (160 bits)

Step 2: Shift validUntil left by 160 bits (into bits 160-207)
  0x65540110 << 160
  = 0x0000006554_0110_000000000000000000000000000000000000000000

Step 3: Shift validAfter left by 208 bits (into bits 208-255)
  0x6553F100 << 208
  = 0x6553F100_0000000000000000000000000000000000000000000000000000

Step 4: OR them together:
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  0x6553F100      β”‚  0x65540110      β”‚  0x00...00                   β”‚
  β”‚  validAfter      β”‚  validUntil      β”‚  sigFailed (0 = valid)       β”‚
  β”‚  bits 208-255    β”‚  bits 160-207    β”‚  bits 0-159                  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

In Solidity:

// Packing:
uint256 validationData = (uint256(1700003600) << 160) | (uint256(1700000000) << 208);

// Unpacking (how EntryPoint reads it):
address aggregator = address(uint160(validationData));        // bits 0-159
uint48 validUntil  = uint48(validationData >> 160);           // bits 160-207
uint48 validAfter  = uint48(validationData >> 208);           // bits 208-255
bool sigFailed     = aggregator == address(1);                // special sentinel

// If validUntil == 0, EntryPoint treats it as "no expiration" (type(uint48).max)

Common mistake: Swapping validAfter and validUntil positions. The layout is validAfter | validUntil | sigFailed from high to low bits β€” counterintuitive because you’d expect β€œuntil” (the upper bound) at higher bits, but the packing follows the EntryPoint’s _parseValidationData order.

Connection to Module 1: This is the same bit-packing pattern as BalanceDelta (Module 1) and PackedAllowance (Module 3) β€” multiple values squeezed into a single uint256 to save gas. The pattern is everywhere in production DeFi.

πŸ’» Quick Try:

Check the EntryPoint contract on Etherscan to see ERC-4337 in action:

  1. Go to EntryPoint v0.7 on Etherscan
  2. Click β€œInternal Txns” β€” each one is a UserOperation being executed
  3. Click any transaction β†’ β€œLogs” tab β†’ look for UserOperationEvent
  4. You’ll see: sender (smart account), paymaster (who paid gas), actualGasCost, success
  5. Compare a sponsored tx (paymaster β‰  0x0) vs a self-paid one (paymaster = 0x0)

This gives you a concrete feel for how the system works in production.


πŸ’‘ Concept: The Flow

1. User creates UserOperation (off-chain)
2. User sends UserOp to Bundler (off-chain, via RPC)
3. Bundler validates UserOp locally (simulation)
4. Bundler batches multiple UserOps into one tx
5. Bundler calls EntryPoint.handleOps(userOps[])
6. For each UserOp:
   a. EntryPoint calls SmartAccount.validateUserOp() β†’ validation
   b. If paymaster: EntryPoint calls Paymaster.validatePaymasterUserOp() β†’ funding check
   c. EntryPoint calls SmartAccount with the operation callData β†’ execution
   d. If paymaster: EntryPoint calls Paymaster.postOp() β†’ post-execution accounting
7. EntryPoint reimburses Bundler for gas spent

The critical insight: Validation and execution are separated. Validation runs first for ALL UserOps in the bundle, then execution runs. This prevents one UserOp’s execution from invalidating another UserOp’s validation (which would waste the bundler’s gas).

πŸ” Deep dive: Read the ERC-4337 spec section on the validation/execution split and the β€œforbidden opcodes” during validation. The restricted opcodes include GASPRICE, GASLIMIT, DIFFICULTY/PREVRANDAO, TIMESTAMP, BASEFEE, BLOCKHASH, NUMBER, SELFBALANCE, BALANCE, ORIGIN, and COINBASE β€” essentially anything environment-dependent that could change between simulation and execution. Storage access is restricted (accounts can read/write their own storage; staked entities get broader access). CREATE is forbidden during validation except for account deployment via factories. The full rules are in the validation rules spec.


πŸ“– Read: SimpleAccount and BaseAccount

Source: eth-infinitism/account-abstraction

Read these contracts in order:

  1. contracts/interfaces/IAccount.sol β€” the minimal interface a smart account must implement
  2. contracts/core/BaseAccount.sol β€” helper base contract with validation logic
  3. contracts/samples/SimpleAccount.sol β€” a basic implementation with single-owner validation
  4. contracts/core/EntryPoint.sol β€” focus on handleOps, _validatePrepayment, and _executeUserOp (it’s complex, but understanding the flow is essential)
  5. contracts/core/BasePaymaster.sol β€” the interface for gas sponsorship

⚑ Common pitfall: The validation function returns a packed validationData uint256 that encodes three values: sigFailed (1 bit), validUntil (48 bits), validAfter (48 bits). Returning 0 means β€œsignature valid, no time bounds.” Returning 1 means β€œsignature invalid.” Get the packing wrong and your account won’t work. See the Deep Dive above for the bit layout.

πŸ“– How to Study ERC-4337 Source Code

Start here β€” the 5-step approach:

  1. Start with IAccount.sol β€” just one function: validateUserOp

    • Understand the inputs: PackedUserOperation, userOpHash, missingAccountFunds
    • Understand the return: packed validationData (draw the bit layout!)
  2. Read SimpleAccount.sol β€” the simplest implementation

    • How it stores the owner
    • How validateUserOp verifies the ECDSA signature
    • How execute and executeBatch handle the execution phase
    • Note the onlyOwnerOrEntryPoint pattern
  3. Skim EntryPoint.handleOps β€” the orchestrator

    • Don’t try to understand every line β€” focus on the flow
    • Find where it calls validateUserOp on each account
    • Find where it calls the execution calldata
    • Find where it handles paymaster logic
  4. Read BasePaymaster.sol β€” the paymaster interface

    • validatePaymasterUserOp β€” decide whether to sponsor
    • postOp β€” post-execution accounting
    • How context bytes flow between validate and postOp
  5. Study a production account (Safe or Kernel)

    • Compare to SimpleAccount β€” what’s different?
    • Look for: module systems, plugin hooks, access control
    • These represent where the industry is heading

Don’t get stuck on: The gas accounting internals in EntryPoint. Understand the flow first (validate β†’ execute β†’ postOp), then revisit the gas math later.


🎯 Build Exercise: SimpleSmartAccount

Workspace: workspace/src/part1/module4/exercise1-simple-smart-account/ β€” starter file: SimpleSmartAccount.sol, tests: SimpleSmartAccount.t.sol

  1. Create a minimal smart account that implements IAccount (just validateUserOp)
  2. The account should validate that the UserOperation was signed by a single owner (ECDSA signature via ecrecover)
  3. Implement basic execute(address dest, uint256 value, bytes calldata func) for the execution phase
  4. Test against the provided MockEntryPoint (simplified for learning)

Note on UserOperation versions: The exercise uses a simplified UserOperation struct with separate gas fields (inspired by v0.6). Production ERC-4337 v0.7 uses PackedUserOperation with packed bytes32 accountGasLimits and bytes32 gasFees (see the bit-packing diagrams above). The core flow (validate β†’ execute β†’ postOp) is identical β€” only the struct encoding differs.

Key concepts to implement:

  • Extract r, s, v from userOp.signature (65 bytes packed as r|s|v)
  • Recover signer using ecrecover(userOpHash, v, r, s) β€” raw hash, no EthSign prefix
  • Return 0 for valid signature, 1 for SIG_VALIDATION_FAILED
  • If missingAccountFunds > 0, pay the EntryPoint via low-level call

⚠️ Note: The exercise uses raw ecrecover against the userOpHash directly (no "\x19Ethereum Signed Message:\n32" prefix). This matches the simplified MockEntryPoint. Production ERC-4337 implementations typically use ECDSA.recover with the EthSign prefix or a typed data hash, but the raw approach keeps the exercise focused on the account abstraction flow rather than signature encoding details.

🎯 Goal: Understand the smart account contract interface from the builder’s perspective. You’re not building a wallet productβ€”you’re understanding how these accounts interact with DeFi protocols you’ll design.


πŸ“‹ Summary: ERC-4337 Architecture

βœ“ Covered:

  • EOA limitations β€” gas requirements, single key, no batch operations
  • ERC-4337 architecture β€” UserOperation, Bundler, EntryPoint, Smart Account, Paymaster
  • Validation/execution split β€” why it matters for security
  • SimpleAccount implementation β€” ECDSA validation and execution

Next: EIP-7702 and how smart accounts change DeFi protocol design


πŸ’‘ EIP-7702 and DeFi Implications

πŸ’‘ Concept: EIP-7702 β€” How It Differs from ERC-4337

Why this matters: EIP-7702 (Pectra upgrade, May 2025) unlocks account abstraction for the ~200 million existing EOAs without requiring migration. Your DeFi protocol will interact with both β€œnative” smart accounts (ERC-4337) and β€œupgraded” EOAs (EIP-7702).

Introduced in EIP-7702, activated with Pectra (May 2025)

πŸ“Š The two paths to account abstraction:

AspectERC-4337EIP-7702
Account typeFull smart account with new addressEOA keeps its address
MigrationRequires moving assetsNo migration needed
FlexibilityMaximum (custom validation, storage)Limited (persistent delegation until revoked)
Adoption~40M+ deployed as of 2025Native to protocol (all EOAs)
Use caseNew users, enterprisesExisting users, wallets

Combined approach:

An EOA can use EIP-7702 to delegate to an ERC-4337-compatible smart account implementation, gaining access to the full bundler/paymaster ecosystem without changing addresses. ✨

πŸ—οΈ Real adoption:

⚠️ Common Mistakes

// ❌ WRONG: Confusing EIP-7702 and ERC-4337 in your integration
// EIP-7702 = EOA delegates to code (no new account needed)
// ERC-4337 = deploy a new smart account contract

// ❌ WRONG: Assuming delegation is temporary per-transaction
// Delegation PERSISTS across transactions until explicitly revoked!
// Don't assume a user's EOA will behave like a plain EOA next block

// βœ… CORRECT: Design protocols to handle both transparently
// Check neither msg.sender.code.length nor tx.origin β€” just work with msg.sender

πŸ’‘ Concept: DeFi Protocol Implications

Why this matters: As a DeFi protocol designer, account abstraction changes your core assumptions. Code that worked for 5 years breaks with smart accounts.

1. msg.sender is now a contract

When interacting with your protocol, msg.sender might be a smart account, not an EOA. If your protocol assumes msg.sender == tx.origin (to check for EOA), this breaks.

Example of broken code:

// ❌ DON'T DO THIS
function deposit() external {
    require(msg.sender == tx.origin, "Only EOAs");  // BREAKS with smart accounts
    // ...
}

Some older protocols used this as a β€œreentrancy guard”—it’s no longer reliable.

⚑ Common pitfall: Protocols that whitelist β€œknown EOAs” or blacklist contracts. With EIP-7702, the same address can be an EOA one block and a contract the next.

2. tx.origin is unreliable

With bundlers submitting transactions, tx.origin is the bundler’s address, not the user’s. Never use tx.origin for authentication.

Example of broken code:

// ❌ DON'T DO THIS
function withdraw() external {
    require(tx.origin == owner, "Not owner");  // tx.origin is the bundler!
    // ...
}

3. Gas patterns change

Paymasters mean users don’t need ETH for gas. If your protocol requires users to hold ETH (e.g., for refund mechanisms), consider that smart account users might not have any.

4. Batch transactions are common

Smart accounts naturally batch operations. A single handleOps call might:

  • Deposit collateral
  • Borrow USDC
  • Swap USDC for ETH
  • All atomically ✨

Your protocol should handle this gracefully (no unexpected reentrancy, proper event emissions).

5. Signatures are non-standard

Smart accounts can use any signature scheme:

  • Passkeys (WebAuthn)
  • Multisig (m-of-n threshold)
  • MPC (distributed key generation)
  • Session keys (temporary authorization)

If your protocol requires EIP-712 signatures from users (e.g., for permit or off-chain orders), you need to support EIP-1271 (contract signature verification) in addition to ecrecover.

πŸ”— DeFi Pattern Connection

Where these implications hit real protocols:

  1. Uniswap V4 + Smart Accounts

    • Permit2’s SignatureVerification already handles EIP-1271 β†’ smart accounts can sign Permit2 permits
    • Flash accounting (Module 2) works identically for EOAs and smart accounts
    • But custom hooks might assume EOA behavior β€” audit carefully
  2. Aave V3 + Batch Liquidations

    • Smart accounts enable atomic batch liquidations: scan undercollateralized positions β†’ liquidate multiple β†’ swap rewards β†’ all in one UserOp
    • This creates a new class of liquidation MEV that’s more efficient than current flashbot bundles
  3. Curve/Balancer + Gas Abstraction

    • LP providers who hold only stablecoins can now add/remove liquidity without ETH
    • Protocol-sponsored paymasters can subsidize LP actions to attract TVL
  4. Governance + Multisig

    • DAOs using smart accounts can vote with m-of-n signatures natively
    • No more wrapping governance calls through external Safe contracts

The pattern: Every require(msg.sender == tx.origin) and ecrecover-only validation is now a compatibility bug. Modern DeFi protocols must be account-abstraction-aware from day one.

⚠️ Common Mistakes

Mistakes that break with smart accounts:

  1. Using msg.sender == tx.origin as a security check

    // ❌ BREAKS: Smart accounts have msg.sender β‰  tx.origin always
    require(msg.sender == tx.origin, "No contracts");
    
    // βœ… If you need reentrancy protection, use a proper guard
    // (ReentrancyGuard or transient storage from Module 1)
    
  2. Assuming all signatures are ECDSA

    // ❌ BREAKS: Smart accounts use EIP-1271, not ecrecover
    address signer = ecrecover(hash, v, r, s);
    require(signer == expectedSigner);
    
    // βœ… Use SignatureChecker that handles both
    // (see EIP-1271 section below)
    
  3. Assuming msg.sender.code.length == 0 means EOA

    // ❌ BREAKS: With EIP-7702, an EOA can have code temporarily
    // And during construction, contracts also have code.length == 0
    require(msg.sender.code.length == 0, "Only EOAs");
    
  4. Hardcoding gas refund to tx.origin

    // ❌ BREAKS: tx.origin is the bundler, not the user
    payable(tx.origin).transfer(refund);
    
    // βœ… Refund to msg.sender (the smart account)
    payable(msg.sender).transfer(refund);
    

πŸ’Ό Job Market Context

Interview question you WILL be asked:

β€œHow does account abstraction affect DeFi protocol design?”

What to say (30-second answer): β€œFive major changes: msg.sender can be a contract, so tx.origin checks break; tx.origin is the bundler, so authentication must use msg.sender; gas patterns change because paymasters mean users might not hold ETH; batch transactions are common so reentrancy protection matters more; and signatures are non-standard because smart accounts use passkeys, multisig, or session keys instead of ECDSA, requiring EIP-1271 support for any signature verification.”

Follow-up question:

β€œHow would you audit a protocol for smart account compatibility?”

What to say: β€œI’d search for three red flags: any msg.sender == tx.origin checks, any ecrecover-only signature verification without EIP-1271 fallback, and any assumption that msg.sender can’t be a contract. Then I’d verify reentrancy guards work correctly with batch operations, and check that gas refund patterns send to msg.sender, not tx.origin.”

Interview Red Flags:

  • 🚩 Using tx.origin for any authentication purpose
  • 🚩 β€œWe only support EOAs” β€” excludes 40M+ smart accounts
  • 🚩 Not knowing what EIP-1271 is

Pro tip: If you can articulate the five protocol design changes fluently, you signal deep understanding. Most candidates know β€œaccount abstraction exists” but can’t explain concrete protocol implications.


πŸ’‘ Concept: EIP-1271 β€” Contract Signature Verification

Why this matters: Every protocol that uses signatures (Permit2, OpenSea, Uniswap limit orders, governance proposals) must support EIP-1271 for smart account compatibility.

Defined in EIP-1271 (April 2019)

The interface:

interface IERC1271 {
    // Standard method name
    function isValidSignature(
        bytes32 hash,      // The hash of the data that was signed
        bytes memory signature
    ) external view returns (bytes4 magicValue);
}

How it works:

  1. Instead of calling ecrecover(hash, signature), you check if msg.sender is a contract
  2. If it’s a contract, call IERC1271(msg.sender).isValidSignature(hash, signature)
  3. If the return value is 0x1626ba7e (the function selector itself), the signature is valid βœ…
  4. If it’s anything else, the signature is invalid ❌

Standard pattern:

// βœ… CORRECT: Supports both EOA and smart account signatures
function verifySignature(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) {
    // Check if signer is a contract
    if (signer.code.length > 0) {
        // EIP-1271 contract signature verification
        try IERC1271(signer).isValidSignature(hash, signature) returns (bytes4 magicValue) {
            return magicValue == 0x1626ba7e;
        } catch {
            return false;
        }
    } else {
        // EOA signature verification
        address recovered = ECDSA.recover(hash, signature);
        return recovered == signer;
    }
}

πŸ” Deep dive: Permit2’s SignatureVerification.sol is the production reference for handling both EOA and EIP-1271 signatures. Ethereum.org - EIP-1271 Tutorial provides step-by-step implementation. Alchemy - Smart Contract Wallet Compatibility covers dApp integration patterns.

πŸ’» Quick Try:

See EIP-1271 in action with a Safe multisig:

  1. Go to any Safe wallet on Etherscan (the Safe singleton implementation)
  2. Search for the isValidSignature function in the β€œRead Contract” tab
  3. Notice the function signature β€” this is the EIP-1271 interface that every protocol calls
  4. Now look at OpenZeppelin’s SignatureChecker.sol β€” see how it branches between ecrecover and isValidSignature based on signer.code.length

πŸŽ“ Intermediate Example: Universal Signature Verification

Before the exercise, here’s a reusable pattern that handles both EOA and smart account signatures β€” the same approach used by OpenZeppelin’s SignatureChecker:

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";

library UniversalSigVerifier {
    bytes4 constant EIP1271_MAGIC = 0x1626ba7e;

    function isValidSignature(
        address signer,
        bytes32 hash,
        bytes memory signature
    ) internal view returns (bool) {
        // Path 1: Smart account β†’ EIP-1271
        if (signer.code.length > 0) {
            try IERC1271(signer).isValidSignature(hash, signature) returns (bytes4 magic) {
                return magic == EIP1271_MAGIC;
            } catch {
                return false;
            }
        }

        // Path 2: EOA β†’ ECDSA
        (address recovered, ECDSA.RecoverError error,) = ECDSA.tryRecover(hash, signature);
        return error == ECDSA.RecoverError.NoError && recovered == signer;
    }
}

Key decisions in this pattern:

  • signer.code.length > 0 β†’ check if it’s a contract (imperfect with EIP-7702, but standard practice)
  • try/catch β†’ protect against malicious isValidSignature implementations that revert or consume gas
  • tryRecover β†’ safer than recover because it doesn’t revert on bad signatures
  • 0x1626ba7e β†’ this magic value is the isValidSignature function selector itself

Where you’ll use this:

  • Any protocol that accepts off-chain signatures (permits, orders, votes)
  • Any protocol that integrates with Permit2 (which handles this internally)
  • Governance systems that accept delegated votes

Connection to Module 3: This is exactly what Permit2’s SignatureVerification.sol does internally. The pattern you learned in Module 3 (Permit2 source code reading) connects directly here β€” SignatureVerification is the bridge between permit signatures and smart accounts.

πŸ”— DeFi Pattern Connection

Where EIP-1271 is required across DeFi:

  1. Permit2 β€” already supports EIP-1271 via SignatureVerification.sol

    • Smart accounts can sign Permit2 permits
    • Your vault from Module 3 works with smart accounts out of the box (if using Permit2)
  2. OpenSea / NFT Marketplaces β€” order signatures must support contract wallets

    • Safe users listing NFTs sign via EIP-1271
    • Marketplaces that only support ecrecover exclude enterprise users
  3. Governance (Compound Governor, OpenZeppelin Governor)

    • castVoteBySig must verify both EOA and contract signatures
    • DAOs with Safe treasuries need EIP-1271 to vote
  4. UniswapX / Intent Systems

    • Swap orders signed by smart accounts β†’ verified via EIP-1271
    • Witness data (Module 3) + EIP-1271 = smart accounts participating in intent-based trading

The pattern: If your protocol accepts any kind of off-chain signature, add EIP-1271 support. Use OpenZeppelin’s SignatureChecker library β€” it’s a one-line change that makes your protocol compatible with all smart accounts.

πŸ’Ό Job Market Context

Interview question you WILL be asked:

β€œHow do you verify signatures from smart contract wallets?”

What to say (30-second answer): β€œUse EIP-1271. Check if the signer has code β€” if yes, call isValidSignature(hash, signature) on the signer contract and verify it returns the magic value 0x1626ba7e. If no code, fall back to standard ECDSA recovery with ecrecover. Wrap the EIP-1271 call in try/catch to handle malicious implementations. OpenZeppelin’s SignatureChecker library implements this pattern, and Permit2 uses it internally.”

Follow-up question:

β€œWhat’s the security risk of EIP-1271?”

What to say: β€œThe main risk is that isValidSignature is an external call to an arbitrary contract. A malicious implementation could: consume all gas (griefing), return the magic value for any input (always-valid), or have side effects. That’s why you always use try/catch with a gas limit, and never trust that a valid EIP-1271 response means the signer actually authorized the action β€” it only means the contract says it did.”

Interview Red Flags:

  • 🚩 Only using ecrecover without EIP-1271 fallback
  • 🚩 Not knowing the magic value 0x1626ba7e
  • 🚩 Calling isValidSignature without try/catch

Pro tip: Mention that EIP-1271 enables passkey-based wallets (WebAuthn signatures verified on-chain). Coinbase Smart Wallet uses this β€” passkey signs, wallet contract verifies via isValidSignature. This is the future of DeFi UX.

⚠️ Common Mistakes

// ❌ WRONG: Only supporting EOA signatures (ecrecover)
function verifySignature(bytes32 hash, bytes memory sig) internal view returns (address) {
    return ECDSA.recover(hash, sig);  // Fails for ALL smart wallets!
}

// βœ… CORRECT: Support both EOA and contract signatures
function verifySignature(address signer, bytes32 hash, bytes memory sig) internal view returns (bool) {
    if (signer.code.length > 0) {
        // Smart account β€” use EIP-1271
        try IERC1271(signer).isValidSignature(hash, sig) returns (bytes4 magic) {
            return magic == IERC1271.isValidSignature.selector;
        } catch {
            return false;
        }
    } else {
        // EOA β€” use ecrecover
        return ECDSA.recover(hash, sig) == signer;
    }
}
// ❌ WRONG: Calling isValidSignature without gas limit
(bool success, bytes memory result) = signer.staticcall(
    abi.encodeCall(IERC1271.isValidSignature, (hash, sig))
);  // Malicious contract could consume ALL remaining gas!

// βœ… CORRECT: Set a gas limit for the external call
(bool success, bytes memory result) = signer.staticcall{gas: 50_000}(
    abi.encodeCall(IERC1271.isValidSignature, (hash, sig))
);

🎯 Build Exercise: SmartAccountEIP1271

Workspace: workspace/src/part1/module4/exercise2-smart-account-eip1271/ β€” starter file: SmartAccountEIP1271.sol, tests: SmartAccountEIP1271.t.sol

  1. Extend your SimpleSmartAccount to support EIP-1271:
    • Implement isValidSignature(bytes32 hash, bytes signature) that verifies the owner’s ECDSA signature
    • Return 0x1626ba7e if valid βœ…, 0xffffffff if invalid ❌
    • Handle edge cases: invalid signature length, recovery to address(0)

Note: This exercise depends on completing Exercise 1 first. SmartAccountEIP1271 inherits from SimpleSmartAccount.

🎯 Goal: Understand how EIP-1271 bridges smart accounts and signature-based DeFi protocols. The isValidSignature function is what Permit2, OpenSea, and governance systems call to verify signatures from contract wallets.

πŸ”— Stretch goal (Permit2 integration): After completing the tests, consider how you’d modify the Permit2 Vault from Module 3 to support contract signatures β€” check signer.code.length > 0, then call isValidSignature instead of ecrecover. Permit2 already does this internally via its SignatureVerification library.


πŸ“‹ Summary: EIP-7702 and DeFi Implications

βœ“ Covered:

  • EIP-7702 vs ERC-4337 β€” persistent delegation vs full smart accounts
  • DeFi protocol implications β€” msg.sender, tx.origin, batch transactions
  • EIP-1271 β€” contract signature verification for smart account compatibility
  • Real-world patterns β€” Permit2 integration with smart accounts

Next: Paymasters and how to sponsor gas for users


πŸ’‘ Paymasters and Gas Abstraction

πŸ’‘ Concept: Paymaster Design Patterns

Why this matters: Paymasters are where DeFi and account abstraction intersect most directly. Protocols can subsidize onboarding (Coinbase pays gas for new users), accept stablecoins for gas (pay in USDC instead of ETH), or implement novel gas markets.

πŸ“Š Three common patterns:

1. Verifying Paymaster

Requires an off-chain signature from a trusted signer authorizing the sponsorship. The dApp’s backend signs each UserOperation it wants to sponsor.

Use case: β€œFree gas” onboarding flows. New users interact with your DeFi protocol without needing ETH first. ✨

Implementation:

contract VerifyingPaymaster is BasePaymaster {
    address public verifyingSigner;

    function validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) external override returns (bytes memory context, uint256 validationData) {
        // Extract signature from paymasterAndData
        // v0.7 layout: [0:20] paymaster addr, [20:36] verificationGasLimit,
        //              [36:52] postOpGasLimit, [52:] custom data
        bytes memory signature = userOp.paymasterAndData[52:];

        // Verify backend signed this UserOp
        bytes32 hash = keccak256(abi.encodePacked(userOpHash, block.chainid, address(this)));
        address recovered = ECDSA.recover(hash, signature);

        if (recovered != verifyingSigner) return ("", 1);  // ❌ Signature failed

        return ("", 0);  // βœ… Will sponsor this UserOp
    }
}

2. ERC-20 Paymaster

Accepts ERC-20 tokens as gas payment. The user pays in USDC or the protocol’s native token, and the paymaster converts to ETH to reimburse the bundler.

Use case: Users hold stablecoins but no ETH. Protocol accepts USDC for gas.

Requires a price oracle (Chainlink or similar) to determine the exchange rate.

Implementation sketch:

contract ERC20Paymaster is BasePaymaster {
    IERC20 public token;
    IChainlinkOracle public oracle;

    function validatePaymasterUserOp(...)
        external override returns (bytes memory context, uint256 validationData)
    {
        uint256 tokenPrice = oracle.getPrice();  // Token/ETH price
        uint256 tokenCost = (maxCost * 1e18) / tokenPrice;

        // Check user has enough tokens
        require(token.balanceOf(userOp.sender) >= tokenCost, "Insufficient token balance");

        // Return context with tokenCost for postOp
        return (abi.encode(userOp.sender, tokenCost), 0);
    }

    function postOp(
        PostOpMode mode,
        bytes calldata context,
        uint256 actualGasCost,
        uint256 actualUserOpFeePerGas
    ) external override {
        (address user, uint256 estimatedTokenCost) = abi.decode(context, (address, uint256));

        // Calculate actual token cost based on actual gas used
        uint256 tokenPrice = oracle.getPrice();
        uint256 actualTokenCost = (actualGasCost * 1e18) / tokenPrice;

        // Transfer tokens from user to paymaster
        token.transferFrom(user, address(this), actualTokenCost);
    }
}

⚑ Common pitfall: Oracle price updates can lag, leading to over/underpayment. Add a buffer (e.g., charge 105% of oracle price) and refund excess in postOp.

πŸ’» Quick Try:

See paymaster-sponsored transactions live:

  1. Go to JiffyScan β€” an ERC-4337 UserOperation explorer
  2. Pick any recent UserOperation on a supported chain
  3. Look at the β€œPaymaster” field β€” if non-zero, the paymaster sponsored gas
  4. Compare gas costs between sponsored (paymaster β‰  0x0) and self-paid (paymaster = 0x0) UserOperations
  5. Click into a paymaster address to see how many UserOps it has sponsored β€” some have sponsored millions

πŸ” Deep dive: OSEC - ERC-4337 Paymasters: Better UX, Hidden Risks analyzes security vulnerabilities including post-execution charging risks. Encrypthos - Security Risks of EIP-4337 covers common attack vectors. OpenZeppelin - Account Abstraction Impact on Security provides security best practices.

3. Deposit Paymaster

Users pre-deposit ETH or tokens into the paymaster contract. Gas is deducted from the deposit.

Use case: Subscription-like models. Users deposit once, protocol deducts gas over time.

πŸ”— DeFi Pattern Connection

How paymasters transform DeFi economics:

  1. Protocol-Subsidized Onboarding

    • Aave could sponsor first-time deposits: user deposits USDC, Aave pays gas
    • Cost to protocol: ~$0.50 per new user on L2s
    • ROI: retained TVL from users who would have abandoned at β€œneed ETH” step
  2. Token-Gated Gas Markets

    • Protocol tokens as gas: hold $UNI β†’ pay gas in $UNI for Uniswap swaps
    • Creates native demand for the protocol token
    • Pimlico and Alchemy already offer this as a service
  3. Cross-Protocol Gas Sponsorship

    • Aggregators (1inch, Paraswap) can sponsor gas for users routing through them
    • β€œFree gas” becomes a competitive advantage for attracting order flow
    • Similar to how CEXes offer zero-fee trading
  4. Conditional Sponsorship

    • Sponsor gas only for trades above $1000 (whale onboarding)
    • Sponsor gas only during low-activity hours (incentivize off-peak usage)
    • Sponsor gas for LP deposits but not withdrawals (encourage TVL)

The pattern: Paymasters turn gas from a user cost into a protocol design lever. The question isn’t β€œdoes your protocol support paymasters?” β€” it’s β€œwhat’s your gas sponsorship strategy?”

πŸ’Ό Job Market Context

Interview question you WILL be asked:

β€œHow would you implement gasless DeFi interactions?”

What to say (30-second answer): β€œUsing ERC-4337 paymasters. Three patterns: a verifying paymaster where the protocol backend signs each UserOperation it wants to sponsor β€” good for controlled onboarding. An ERC-20 paymaster that accepts stablecoins for gas, using a Chainlink oracle for the exchange rate β€” good for users who hold tokens but not ETH. Or a deposit paymaster where users pre-fund a gas balance. The paymaster’s validatePaymasterUserOp decides whether to sponsor, and postOp handles accounting after execution.”

Follow-up question:

β€œWhat are the security risks of paymasters?”

What to say: β€œGriefing is the main risk β€” a malicious user could submit expensive UserOperations that the paymaster sponsors, draining its balance. Mitigations include: off-chain validation before signing (verifying paymaster), rate limiting per user, gas caps per UserOp, and requiring token pre-approval before sponsoring (ERC-20 paymaster). Also, oracle manipulation for ERC-20 paymasters β€” if the price feed is stale, the paymaster could underprice gas and lose money.”

Interview Red Flags:

  • 🚩 β€œJust use meta-transactions” β€” ERC-4337 paymasters are the modern standard
  • 🚩 Not understanding the validate β†’ execute β†’ postOp flow
  • 🚩 Can’t explain paymaster griefing risks

Pro tip: Knowing specific paymaster services (Pimlico, Alchemy Gas Manager, Biconomy) shows you’ve worked with the ecosystem practically, not just theoretically.

⚠️ Common Mistakes

// ❌ WRONG: Paymaster without griefing protection
function _validatePaymasterUserOp(PackedUserOperation calldata userOp, ...)
    internal returns (bytes memory, uint256) {
    return ("", 0);  // Sponsors everything β€” will be drained!
}

// βœ… CORRECT: Validate user eligibility and set limits
function _validatePaymasterUserOp(PackedUserOperation calldata userOp, ...)
    internal returns (bytes memory, uint256) {
    address sender = userOp.getSender();
    require(isWhitelisted[sender], "Not eligible");
    require(dailyUsage[sender] < MAX_DAILY_GAS, "Daily limit reached");
    return (abi.encode(sender), 0);
}
// ❌ WRONG: ERC-20 paymaster with no oracle staleness check
uint256 tokenAmount = gasUsed * gasPrice / tokenPrice;  // tokenPrice could be stale!

// βœ… CORRECT: Check oracle freshness
(, int256 price, , uint256 updatedAt, ) = priceFeed.latestRoundData();
require(block.timestamp - updatedAt < 1 hours, "Stale price feed");

πŸ’‘ Concept: Paymaster Flow in Detail

validatePaymasterUserOp(userOp, userOpHash, maxCost)
    β†’ Paymaster checks if it will sponsor this UserOp
    β†’ Returns context (arbitrary bytes) and validationData
    β†’ EntryPoint locks paymaster's deposit for maxCost

// ... UserOp executes ...

postOp(mode, context, actualGasCost, actualUserOpFeePerGas)
    β†’ Paymaster performs post-execution accounting
    β†’ mode indicates: success, execution revert, or postOp revert
    β†’ Can charge user in ERC-20, update internal accounting, etc.

⚠️ Critical detail: The postOp is called even if the UserOp execution reverts (in PostOpMode.opReverted), giving the paymaster a chance to still charge the user for the gas consumed.


πŸ“– Read: Paymaster Implementations

Source: eth-infinitism/account-abstraction

πŸ—οΈ Production paymasters:

πŸ“– How to Study Paymaster Implementations:

  1. Start with BasePaymaster.sol β€” the abstract base

    • Two functions to understand: validatePaymasterUserOp and postOp
    • The context bytes are the bridge between them β€” data from validation flows to post-execution
    • Notice: postOp is called even on execution revert (the paymaster still gets to charge)
  2. Read VerifyingPaymaster.sol β€” the simpler implementation

    • Focus on: how paymasterAndData is unpacked (paymaster address + custom data)
    • The validation logic: extract signature, verify against trusted signer
    • Notice: no postOp override β€” the simplest paymaster doesn’t need post-execution logic
  3. Read TokenPaymaster.sol β€” the complex implementation

    • Follow the flow: validate β†’ estimate token cost β†’ store in context β†’ execute β†’ postOp charges actual cost
    • The oracle integration: how does it get the ETH/token exchange rate?
    • The refund mechanism: estimated cost vs actual cost, refund difference
  4. Compare with production paymasters β€” Pimlico and Alchemy

    • These add: rate limiting, gas caps, off-chain pre-validation
    • Notice what’s missing from the reference implementations (griefing protection, fee margins)
    • This gap between reference and production is where security bugs hide
  5. Trace one complete sponsored transaction

    • UserOp submitted β†’ Bundler validates β†’ EntryPoint calls validatePaymasterUserOp β†’ execution β†’ EntryPoint calls postOp β†’ gas reimbursement
    • Key question at each step: who pays, and how much?

Don’t get stuck on: The PostOpMode enum details initially. Just know that opSucceeded = everything worked, opReverted = user’s call failed but paymaster still charges, postOpReverted = rare edge case.


🎯 Build Exercise: Paymasters

Workspace: workspace/src/part1/module4/exercise3-paymasters/ β€” starter file: Paymasters.sol, tests: Paymasters.t.sol

  1. Implement a simple verifying paymaster that sponsors UserOperations if they carry a valid signature from a trusted signer:

    • Add a verifyingSigner address
    • In validatePaymasterUserOp, verify the signature in userOp.paymasterAndData
    • Return 0 for valid βœ…, 1 for invalid ❌
  2. Implement an ERC-20 paymaster that accepts a mock stablecoin as gas payment:

    • In validatePaymasterUserOp:
      • Verify the user has sufficient token balance
      • Return context with user address and estimated token cost
    • In postOp:
      • Calculate actual token cost based on actualGasCost
      • Transfer tokens from user to paymaster
    • Use a simple fixed exchange rate for now (1 USDC = 0.0005 ETH as mock rate)
    • In Part 2, you’ll integrate Chainlink for real pricing
  3. Write tests demonstrating the full flow:

    • User submits UserOp with no ETH
    • Paymaster sponsors gas
    • User pays in tokens
    • Verify user’s token balance decreased by correct amount
  4. Test edge cases:

    • User has insufficient tokens (paymaster should reject in validation)
    • UserOp execution reverts (paymaster should still charge in postOp)
    • Different gas prices (verify postOp correctly adjusts token cost)

🎯 Goal: Understand paymaster economics and how DeFi protocols can use them to remove gas friction for users.


πŸ“‹ Summary: Paymasters and Gas Abstraction

βœ“ Covered:

  • Paymaster patterns β€” verifying, ERC-20, deposit models
  • Paymaster flow β€” validation, context passing, postOp accounting
  • Real implementations β€” Pimlico, Alchemy gas managers
  • Edge cases β€” reverted UserOps, oracle pricing, insufficient balances

Key takeaway: Paymasters enable gasless DeFi interactions, making protocols accessible to users without ETH. Understanding paymaster economics is essential for modern protocol design.


Backward references (← concepts from earlier modules):

Module 4 ConceptBuilds onWhere
PackedUserOperation + validationData packingBalanceDelta bit-packing, uint256 slot layoutM1 β€” BalanceDelta
UserOp validation errorsCustom errors for clear revert reasonsM1 β€” Custom Errors
Type-safe EntryPoint callsabi.encodeCall for compile-time type checkingM1 β€” abi.encodeCall
EIP-7702 + ERC-4337 combined approachDelegation designator format, DELEGATECALL semanticsM2 β€” EIP-7702
EIP-1271 signature verificationPermit2’s SignatureVerification handles EOA + contract sigsM3 β€” Permit2 Source Code
Smart account permit supportPermit2 works with smart accounts via EIP-1271M3 β€” EIP-2612 Permit

Forward references (β†’ concepts you’ll use later):

Module 4 ConceptUsed inWhere
UserOp signature testingvm.sign, vm.addr, fork testing for EntryPointM5 β€” Foundry
Smart account upgradeabilityUUPS proxy pattern β€” Kernel, Safe are upgradeable proxiesM6 β€” Proxy Patterns
EntryPoint singleton deploymentCREATE2 deterministic addresses across chainsM7 β€” Deployment

Part 2 connections:

Module 4 ConceptPart 2 ModuleHow it connects
EIP-1271 + smart account signaturesM2 β€” AMMsSmart accounts using Permit2 for swaps β€” EIP-1271 verifies the permit signature
Paymaster oracle pricingM3 β€” OraclesERC-20 paymasters need Chainlink feeds for ETH/token exchange rates
Batch liquidations via smart accountsM4 β€” LendingAtomic batch liquidation: scan β†’ liquidate multiple β†’ swap rewards in one UserOp
Gasless flash loan executionM5 β€” Flash LoansPaymasters can sponsor flash loan arb execution for users
Gas sponsorship for vault depositsM7 β€” Vaults & YieldProtocol-sponsored gasless deposits to attract TVL
AA security implicationsM8 β€” DeFi Securitymsg.sender == tx.origin checks, EIP-1271 griefing, paymaster draining
Full AA integrationM9 β€” Integration CapstoneCapstone should support smart account users with paymaster option

πŸ“– Production Study Order

Read these files in order to build progressive understanding of account abstraction in production:

#FileWhyLines
1IAccount.solOne function: validateUserOp β€” the minimal smart account interface~15
2BaseAccount.solValidation helper β€” see how _validateSignature is separated from nonce/payment handling~50
3SimpleAccount.solReference implementation β€” ECDSA owner validation, execute/executeBatch~100
4EntryPoint.sol β€” handleOpsThe orchestrator β€” follow validate β†’ execute β†’ postOp flow (skim, don’t deep-read)~500
5BasePaymaster.solPaymaster interface β€” validatePaymasterUserOp + postOp with context passing~60
6VerifyingPaymaster.solSimplest paymaster β€” off-chain signature verification~80
7TokenPaymaster.solERC-20 gas payment β€” oracle integration, postOp accounting~200
8OZ SignatureChecker.solUniversal sig verification β€” the bridge between EOA and smart account signatures~30
9Kernel (ZeroDev)Production modular account β€” plugins, session keys, how the industry builds on top of ERC-4337~300

Reading strategy: Files 1–3 build the smart account from interface β†’ reference implementation. File 4 is the orchestrator (skim the flow, don’t memorize). Files 5–7 cover paymasters from simple β†’ complex. File 8 is the EIP-1271 bridge. File 9 shows where the industry is heading β€” modular, pluggable account architecture.


πŸ“š Resources

ERC-4337

Smart Account Implementations

EIP-7702

EIP-1271

Paymasters

Modular Accounts

Deployment Data


Navigation: ← Module 3: Token Approvals & Permits | Module 5: Foundry Testing β†’