The three ERC-4337 components β smart account, paymaster, and a minimal bundler β implemented from scratch (no SDK) to understand account abstraction end to end. V3 adds passkeys: tap a button, approve Face ID / Touch ID / Windows Hello, and a real gasless transaction happens on Sepolia β no wallet extension, no seed phrase, no gas. The account even deploys itself on first use (counterfactual factory +
initCode).
Status: V3 β shipped, deployed + verified on Sepolia, and hosted live.
erc4337.onrender.com β open it, tap Increment the counter, and approve your device's biometric prompt. The first tap registers a passkey and prompts twice (register, then sign); later taps prompt once. You sign no Ethereum transaction, install no wallet, and pay no gas β that is the whole point of ERC-4337 account abstraction.
Requires a browser/device with a passkey authenticator (Face ID, Touch ID, Windows Hello, or a security key) and a secure (HTTPS) context. Works in Chrome, Edge, Firefox and Safari on desktop, and in Safari on iOS. On iPhone/iPad, use Safari β Apple restricts passkey access to Safari there, so third-party iOS browsers (Chrome, Firefox, Edge) return a permission error. Hosted on Render's free tier β the first visit after a while may take ~30-50s to wake the server.
A hands-on implementation of ERC-4337. Instead of an SDK
(Pimlico, Alchemy, permissionless.jsβ¦), each piece is built by hand to understand the full
machinery: how a smart account validates a UserOperation, how a paymaster sponsors gas, how
a bundler turns UserOperations into a real handleOps transaction, and how a factory deploys the
account on the fly.
The project evolves in milestones that keep the same architecture (a BaseAccount Template
Method) and swap only the validation logic:
- V1 β single-owner ECDSA account (auth by possession: you hold a key).
- V2 β auth by knowledge: 3 secret questions derive the signing key in the browser; the account stores only the derived address.
- V3 (current) β passkeys / WebAuthn (P-256): the device's authenticator signs; the account
stores only the public key
(x, y). Plus a CREATE2 factory for counterfactual deployment.
The EntryPoint is the only piece not written here β the canonical singleton deployed by the
Ethereum Foundation (0x4337084d9e255ff0702461cf8895ce9e3b5ff108 on Sepolia).
Two additions, both proving the BaseAccount refactor holds β a new scheme touches only two hooks:
PasskeyAccountβ validated by a WebAuthn passkey (P-256 / secp256r1). The private key is generated and held by the authenticator (Secure Enclave / TPM / security key) and never leaves the device. On-chain verification uses OpenZeppelin's auditedWebAuthn+P256libraries (EIP-7951 precompile at0x100, with a Solidity fallback).AccountFactoryβ CREATE2, idempotent. The account address is computed off-chain (factory.getAddress) before it exists, and the real deployment is bundled into the account's first UserOp viainitCode. This removed V2's/deployendpoint and its server-side deployer key: deployment is now paid out of the normalhandleOpsflow.
Browser (passkey signs userOpHash with Face ID / Touch ID / Windows Hello)
β POST /rpc (eth_sendUserOperation) β first op carries initCode (factory + createAccount)
βΌ
Bundler ββsimulate (eth_call), then handleOps txβββΆ EntryPoint
ββ(first op)ββΆ AccountFactory.createAccount (CREATE2 deploy)
ββββββββββββΆ PasskeyAccount.validate() (WebAuthn + P-256)
ββββββββββββΆ PasskeyAccount.execute() βββΆ Counter
(gas sponsored by the Paymaster's deposit)
The user never signs an Ethereum transaction β the bundler does that (and pays), reimbursed by the Paymaster. Deep dive: docs/v3-passkeys-factory.md.
A Template Method hierarchy isolates the one thing that changes between auth schemes:
BaseAccount (abstract) validateUserOp / execute / prefund / deposit + _validateSignature (hook)
ββ SmartAccount (V1) secp256k1 Β· _validateSignature = ECDSA recover == s_owner
ββ SecretQuestionAccount (V2) secp256k1 Β· _validateSignature = ECDSA recover == s_signerAddress
ββ PasskeyAccount (V3) secp256r1 Β· _validateSignature = WebAuthn.verify(userOpHash, (x, y))
AccountFactory (V3) CREATE2, idempotent β counterfactual deployment via initCode
- Solidity + Foundry β contracts, tests, deployment
- TypeScript + Node.js + viem β bundler + counterfactual UserOp flow
- React + Vite β frontend (single fused passkey button)
- OpenZeppelin β
WebAuthn+P256(V3), ECDSA (V1/V2), Create2 - @noble/curves β off-chain DER/low-s parsing
- WebAuthn (
navigator.credentials) β passkey registration & signing - Sepolia testnet Β· Render hosting
contracts/ Foundry β BaseAccount, SmartAccount, SecretQuestionAccount, PasskeyAccount, AccountFactory, Paymaster, Counter, tests
bundler/ Node.js β JSON-RPC bundler (/rpc) + serves the frontend build
frontend/ React + Vite β passkey wallet (single fused button) + WebAuthn library
docs/ Architecture & technical documentation
render.yaml Single-service hosting (bundler serves the frontend)
Makefile Common commands (build, test, deploy-v3, vector, bundler, front)
Prerequisites: Foundry, Node.js β₯ 18.
git clone --recursive https://github.com/TheBossMickael/erc4337.git # forge-std + OpenZeppelin are git submodules
cd erc4337
make install # submodules + bundler & frontend dependencies
make build # compile the contracts
make test # Solidity unit tests (local EVM)
make test-fork SEPOLIA_RPC_URL="<your RPC>" # integration test vs the real EntryPoint (+ P-256 precompile)- Solidity unit tests (local EVM, mocked EntryPoint): signature validation, access control,
execution, paymaster, deposits β for
SmartAccount,SecretQuestionAccount,PasskeyAccount, plusAccountFactory(idempotence, CREATE2 determinism). - Fork tests against the real EntryPoint v0.8 on a Sepolia fork (V1, V2, and V3's real P-256
precompile at
0x100) β the actualhandleOpsflow without spending sETH. - Locked WebAuthn vector cross-checked on both sides: the same P-256 assertion is asserted
on-chain (WebAuthnVector.sol) and off-chain
(webauthn.test.ts). Foundry has no P-256 signing cheatcode, so
the vector is generated once by
make vectorand locked β if the format changes, both worlds break. - Frontend Vitest: SPKIβ(x,y), DERβ(r,s) with low-s normalization, and the naked-tuple signature encoding.
| Contract | Address (verified on Etherscan) |
|---|---|
| AccountFactory | 0x28d7β¦Ac1a |
| Paymaster | 0x93baβ¦d314 |
| Counter | 0x3921β¦b48d |
PasskeyAccounts are counterfactual β each is created lazily inside its owner's first UserOp, so there is no single "demo account" address (one passkey = one account).
| Contract | Address (verified on Etherscan) |
|---|---|
| SecretQuestionAccount (demo) | 0xE4c8β¦324C |
| Paymaster | 0x73B4β¦C57D |
| Counter | 0xb73Dβ¦5F1D |
| Contract | Address (verified on Etherscan) |
|---|---|
| SmartAccount | 0x6f12β¦Aa87 |
| Paymaster | 0x317Eβ¦9e2c |
| Counter | 0x12dFβ¦d81d |
Hosted live on Render as a single web service β erc4337.onrender.com. Config: render.yaml. WebAuthn requires HTTPS, which the Render URL provides.
Recommended reading order β each builds on the previous one:
- Architecture β the full path of a transaction, who pays the gas
- Contracts β BaseAccount, the account hierarchy, signature convention
- Bundler β JSON-RPC server, simulation, userOpHash, serialization
- V2 β auth by knowledge β key derivation from answers
- V3 β passkeys + factory β WebAuthn/P-256, counterfactual deployment
- Limitations β deliberate simplifications (V1, V2 & V3)
- V1 β ECDSA validation, gasless via paymaster, minimal bundler β
- V2 β auth by knowledge (secret questions), React frontend, deployed + hosted β
- V3 (current) β passkeys / WebAuthn (P-256) + counterfactual factory, deployed + hosted β
MIT β see LICENSE.