Skip to content
On this page

L4-1: Smart Wallets + Paymaster

1. Problem

Traditional Ethereum accounts (EOAs, Externally Owned Accounts) are controlled by a single private key. This model has fundamental limitations: (1) lost private key = permanently lost funds, with no recovery mechanism; (2) every operation requires ETH for gas, creating a "chicken and egg" problem for new users; (3) complex permission policies -- such as daily spending limits, timelocks, or multi-party co-signing -- cannot be implemented; (4) the user experience is clunky -- every transaction requires confirmation and waiting.

ERC-4337 (Account Abstraction) proposes a solution that does not modify the consensus layer: it introduces smart contract wallets (replacing EOAs), Bundlers (batching transactions), and Paymasters (sponsoring gas). User operations are no longer traditional Ethereum transactions but "UserOperations" -- data structures containing the operational intent, signed by the smart wallet and submitted by the Bundler in batches.

This challenge builds the core components of the complete ERC-4337 stack: a smart wallet contract (SmartWallet.sol) and a TokenPaymaster (TokenPaymaster.sol) that enables paying gas with ERC-20 tokens. Users can pay gas fees with ERC-20 tokens instead of ETH, while enjoying all the benefits of smart wallets.

2. Why

ERC-4337 has already been adopted by over 30 million smart accounts and is the de facto standard for Ethereum account abstraction. Vitalik Buterin has repeatedly emphasized that account abstraction is a necessary path to achieving mainstream Web3 adoption -- it eliminates the technical friction for new user onboarding. In 2026, EntryPoint v0.7 has been deployed via CREATE2 at the same address (0x0000000071727De22E5E9d8BAf0edAc6f37da032) on all major EVM chains, becoming a core component of Ethereum infrastructure.

Understanding ERC-4337 is critical for the entire Web3 ecosystem:

  • Frontend development: understanding how to construct a UserOperation and sign it with a smart wallet
  • Contract development: understanding the validateUserOp and execute interfaces and the EntryPoint lifecycle
  • Infrastructure development: building Bundlers and Paymasters is an emerging infrastructure domain
  • Security engineering: smart wallets introduce an attack surface different from EOAs, requiring different security considerations

Unlike EIP-7702 (which achieves account abstraction by delegating code from an EOA to a smart contract -- an "EOA upgrade" approach), ERC-4337 is a complete smart contract wallet solution that does not depend on the existence of an EOA. The two are complementary technical paths: EIP-7702 provides an upgrade path for existing EOA users, while ERC-4337 targets new users with native smart wallets.

3. Solution

The complete ERC-4337 architecture consists of four collaborating components. This challenge implements two of the core contracts:

3.1 Architecture Overview

User (potentially without ETH)
  |
  +--> Sign UserOperation
  |     {sender, nonce, callData, paymasterAndData, signature, ...}
  |
  v
Bundler (listens for UserOperations in the alt mempool)
  |
  +--> simulateValidation() -- simulate validation phase
  +--> Package multiple UserOperations into a bundle
  +--> handleOps([userOp1, userOp2, ...]) -- submit to EntryPoint
  |
  v
EntryPoint (0x0000000071727De22E5E9d8BAf0edAc6f37da032)
  |
  +--> Validation Phase:
  |      Wallet._validateSignature(userOp, userOpHash)
  |      Paymaster._validatePaymasterUserOp(userOp, userOpHash, maxCost)
  |
  +--> Execution Phase:
  |      Wallet.execute(to, value, data) or executeBatch(...)
  |      If validation phase succeeds, Bundler fronts the gas
  |
  +--> Post-Execution Phase:
         Paymaster._postOp(mode, context, actualGasCost)
         Deduct ERC-20 tokens from user to reimburse Bundler

3.2 SmartWallet

SmartWallet.sol is an ERC-4337-compatible smart contract wallet implementing two core interfaces:

Signature Validation (_validateSignature): Called by the EntryPoint during the validation phase. Validation logic:

  1. Recover the signer address from userOp.signature (65-byte ECDSA signature)
  2. Check that the recovered address matches owner
  3. Check that userOp.nonce matches the wallet's current nonce (replay prevention)
  4. Return 0 for validation success, 1 for failure (SIG_VALIDATION_FAILED)

The signature recovery implementation supports standard v-value normalization (adding 27 if v < 27) and is also compatible with EIP-155 replay-protected v-value encoding (chainId * 2 + 35).

Transaction Execution (execute / executeBatch):

  • execute(): Executes a single call. Can only be called by the EntryPoint (onlyEntryPoint modifier). Increments nonce on success.
  • executeBatch(): Atomic batch execution. All calls must succeed or all are reverted. The three arrays passed (to, values, datas) must have equal lengths.

Security Gating: All execution functions use the onlyEntryPoint modifier to ensure only the EntryPoint can trigger execution. This is the core security model of ERC-4337 -- user operations must pass through the complete validation pipeline (including gas payment, signature verification, and nonce checking) before execution.

Receiving ETH: The receive() function allows the wallet to directly receive ETH transfers (e.g., from other users or contracts).

3.3 TokenPaymaster

TokenPaymaster.sol implements a Paymaster that allows users to pay gas with ERC-20 tokens. Core mechanism:

Validation (_validatePaymasterUserOp): Called by the EntryPoint during the validation phase, receiving three parameters:

  1. userOp: The complete user operation; the paymasterAndData field contains the encoded maxTokenCost (the maximum number of tokens the user authorizes to pay)
  2. userOpHash: The hash of the user operation (unused in this implementation, but used for additional signature verification in full implementations)
  3. maxCost: The maximum gas cost of this operation as estimated by the EntryPoint

Validation checks:

  • Parse maxTokenCost from paymasterAndData (reading 32-byte uint256 starting at byte offset 20)
  • Calculate tokenCost = maxCost * 2000 based on the fixed exchange rate TOKENS_PER_ETH = 2000
  • Verify that the user has approved sufficient token allowance (token.allowance(userOp.sender, address(this)))
  • Verify that the Paymaster has sufficient ETH deposited in the EntryPoint (entryPoint.balanceOf(address(this)))
  • Return context (encoded (sender, tokenCost)) for use by _postOp

Post-Execution (_postOp): Called by the EntryPoint after user operation execution:

  • Only deducts when the operation succeeded (mode == PostOpMode.opSucceeded)
  • Calculates the final token fee based on actual gas consumed: actualTokenCost = actualGasCost * TOKENS_PER_ETH
  • Capped at the pre-authorized tokenCost limit
  • Performs transferFrom from the user account to the Paymaster contract

Deposit Management: The Paymaster needs to deposit ETH in the EntryPoint to pay the Bundler's gas:

  • deposit(): Anyone can deposit ETH into the EntryPoint (marked as the Paymaster's deposit)
  • withdraw(): Only the owner can withdraw ETH from the EntryPoint back to the owner address

Exchange Rate Model: Currently uses a hardcoded exchange rate TOKENS_PER_ETH = 2000. This is a simplification -- production Paymasters need to integrate Chainlink or other oracles to obtain real-time ETH/token exchange rates.

Reference source files: src/level4/SmartWallet.sol, src/level4/TokenPaymaster.sol

3.4 Bundler Logic (Conceptual Level)

The Bundler is a critical infrastructure component in ERC-4337, responsible for:

  1. Listening to the alt mempool: Collecting UserOperations (distinct from the traditional transaction mempool)
  2. Simulation validation: Calling EntryPoint.simulateValidation() to check whether each UserOperation will succeed
  3. Gas ordering: Sorting UserOperations by gas price (maxPriorityFeePerGas) in descending order (higher priority fee first)
  4. Batch submission: Calling EntryPoint.handleOps() to submit all UserOperations in a batch
  5. Gas reimbursement: The Bundler fronts all gas, then receives compensation from the Paymaster or the wallet's EntryPoint deposit

3.5 UserOperation Data Structure

struct UserOperation {
    address sender;              // Smart wallet address
    uint256 nonce;               // Replay protection counter
    bytes   initCode;            // Factory deployment code (first operation for new wallets)
    bytes   callData;            // Calldata to execute
    uint256 callGasLimit;        // Gas limit for the execution phase
    uint256 verificationGasLimit;// Gas limit for the validation phase
    uint256 preVerificationGas;  // Bundler compensation
    uint256 maxFeePerGas;        // EIP-1559 max fee
    uint256 maxPriorityFeePerGas;// EIP-1559 priority fee
    bytes   paymasterAndData;    // Paymaster address (20B) + custom data
    bytes   signature;           // Wallet's signature of userOpHash
}

4. Pitfalls Encountered

  • Gas separation between validation and execution phases: ERC-4337 separates the validation phase (signature checks, Paymaster validation) from the execution phase. If the validation phase consumes too much gas and exceeds verificationGasLimit, the entire operation fails directly -- and the Bundler receives no gas compensation
  • Insufficient ETH deposit in Paymaster: If the deposit check in _validatePaymasterUserOp fails, the Bundler is denied compensation -- and the gas already consumed by _validatePaymasterUserOp itself is also unrecoverable
  • Token exchange rate volatility: TokenPaymaster.sol uses a hardcoded exchange rate TOKENS_PER_ETH = 2000; if the ETH/token price fluctuates significantly, users may pay too much or too little in tokens
  • paymasterAndData decoding: _decodeMaxTokenCost() uses inline assembly to read a uint256 at byte offset 20 from calldata; if paymasterAndData is malformed (shorter than 52 bytes), it will read garbage or out-of-bounds data
  • Nonce synchronization issues: When the Bundler submits multiple UserOperations, if one UserOperation's nonce is non-sequential with the others, the entire handleOps call may fail
  • Smart wallet without recovery mechanism: The current implementation has only a single owner; if the owner's private key is lost, funds in the wallet are permanently locked -- there is no social recovery or timelock mechanism
  • Hardcoded EntryPoint address: If the EntryPoint is upgraded (from v0.6 to v0.7), wallets and Paymasters need to be redeployed to point to the new EntryPoint

5. Why the Pitfalls Occur

Validation phase gas management is the most distinctive challenge of ERC-4337. In traditional transactions, gas is consumed continuously. In ERC-4337, the validation phase gas is independently priced by verificationGasLimit: the Bundler pays the validation gas before performing any operations. If validation fails, the Bundler not only receives no compensation but also bears the validation phase gas cost itself. This creates an incentive problem: Bundlers are unwilling to accept UserOperations with complex (high-gas) validation logic.

More specifically, the token.allowance() and entryPoint.balanceOf() calls in _validatePaymasterUserOp are both external contract calls that consume significantly more gas than simple storage reads. If the Paymaster's validation logic is too complex, Bundlers may refuse to package these UserOperations due to potential losses. This forces Paymaster developers to trade off between validation efficiency and functional completeness.

Insufficient Paymaster deposits represent a cascading failure: if the deposit passes the check in _validatePaymasterUserOp but becomes insufficient during actual execution (which may be seconds to minutes later) because other UserOperations consumed the deposit, the Bundler will be unable to receive compensation during the _postOp phase. ERC-4337 addresses this by requiring the Paymaster to hold sufficient deposits in the EntryPoint -- deposits are effectively "locked" during the validation phase and cannot be consumed by other UserOperations.

The danger of hardcoded exchange rates lies in arbitrage: if TOKENS_PER_ETH = 2000 but the market price is 2500, users can pay gas with tokens below market price, and the Paymaster bears the loss. If the market price is 1500, users must overpay in tokens, potentially degrading user experience. Production Paymasters must use real-time oracle prices.

6. How to Resolve the Pitfalls

Validation Gas Optimization:

  • Minimize external calls in _validatePaymasterUserOp -- consider caching entryPoint.balanceOf() results
  • Use SLOAD instead of CALL to check whitelist information
  • For complex validation logic, move some checks to the execution phase (borne by the wallet contract itself)
  • Build a profitability expectation model for Bundlers -- calculate whether compensation covers costs after simulation

Exchange Rate Oracle Integration (production code example):

solidity
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract TokenPaymaster {
    AggregatorV3Interface public tokenPriceFeed; // Token/ETH price

    function getTokenCost(uint256 ethCost) public view returns (uint256) {
        (, int256 price,,,) = tokenPriceFeed.latestRoundData();
        // price is token/ETH (scaled by 1e8), calculates how many tokens are needed to cover ethCost
        return (ethCost * uint256(price)) / 1e8;
    }
}

Nonce Management Improvements: Use ERC-4337's key-based nonce management (EntryPoint v0.7 supports two-dimensional nonces: (key, sequence)). This allows multiple independent nonce sequences, supporting concurrent UserOperations:

Wallet Recovery Mechanism: Add an ownership transfer function to SmartWallet (callable through the EntryPoint or by the owner themselves):

solidity
function transferOwnership(address newOwner) external {
    require(msg.sender == owner || msg.sender == entryPoint, "Not authorized");
    owner = newOwner;
}

More advanced implementations can add social recovery modules, allowing multiple guardians to replace the owner through voting.

Deposit Monitoring: Build automated deposit replenishment scripts -- automatically call deposit() when the Paymaster's balance in the EntryPoint falls below a safe threshold.

javascript
async function monitorPaymasterDeposit(paymaster, entryPoint, minBalance) {
  const balance = await entryPoint.balanceOf(paymaster.address);
  if (balance < minBalance) {
    await paymaster.deposit({ value: topUpAmount });
  }
}

7. Technical Highlights

TechniqueDescription
ERC-4337Account abstraction standard; UserOperation replaces traditional transactions
EntryPointGlobal singleton contract, same address across all EVM chains (CREATE2)
UserOperationContains sender/nonce/callData/signature/paymasterAndData
Signature Validation (_validateSignature)Called by EntryPoint during validation phase; returns 0=success, 1=failure
Batch Execution (executeBatch)Atomic batch calls; all succeed or all revert
PaymasterSponsors gas; users can pay with ERC-20 tokens
Validate -> Execute -> PostOpThree-phase pipeline: validate -> execute -> postOp
Validation Phase GasverificationGasLimit priced independently; Bundler bears cost on failure
paymasterAndData20-byte Paymaster address + custom data (e.g., maxTokenCost)
EntryPoint DepositPaymaster deposits ETH in EntryPoint to compensate Bundlers
PostOpModeopSucceeded/opReverted/postOpReverted; controls post-execution fee deduction
ECDSA Signature Recoveryv-value normalization (v<27 then +27), ecrecover precompile

Built with AiAda