An ERC-4337 smart account owned by a passkey (WebAuthn / secp256r1) instead of a raw private key, that can delegate a scoped, time-boxed session key so a dApp can send a burst of transactions with zero further wallet prompts. Demoed with a tiny on-chain clicker game: create your wallet once (Face ID / Touch ID), grant a session key once, then click away.
Every wallet interaction today means a signature prompt. That's fine for a single high-value transfer, but it kills UX for anything interactive — games, trading bots, subscriptions. Session keys let a user pre-authorize a narrow slice of what a dApp can do (which contract, which function, until when, how many times) and then get out of the way.
Browser (passkey) EntryPoint (ERC-4337, v0.6) Chain
| | |
|--WebAuthn assertion (owner sig)------->| |
| mode 0: passkey/P-256 |--validateUserOp--> SmartAccount
| | |
|--ECDSA sig (session key)-------------->| |--scoped to
| mode 1: secp256k1 | | target+selector
| | | +expiry+usage cap
| |--execute()----------->GameContract.click()
SmartAccount.sol— ERC-4337IAccount. Signature ismode byte || payload:0x00: WebAuthn assertion, verified on-chain against the owner's P-256 public key via [base-org/webauthn-sol] (RIP-7212 precompile with a FreshCryptoLib fallback).0x01: plain ECDSA from a session key, checked against that key'sSessionKeyPermission(target contract, function selector,validUntil,maxUses). Only reachable throughexecute(), so a session key can never touch anything outside its scope.
SmartAccountFactory.sol— CREATE2 factory, deterministic account address per owner pubkey + salt (standard ERC-4337 counterfactual-deploy pattern).GameContract.sol— trivial on-chain clicker used as the demo target.- Mock bundler (
frontend/src/app/api/bundler) — for local/demo reliability this relays a signedUserOperationstraight toEntryPoint.handleOpsusing a funded Anvil dev key, instead of depending on a real bundler network. Swapping in Pimlico (eth_sendUserOperation) is the natural next step for a testnet deployment.
- Passkey-native account abstraction: no seed phrase, no browser extension — the owner key lives in the device's secure enclave.
- Scoped session keys: not just "any tx for N hours" but target + selector +
expiry + usage cap, enforced entirely inside
_validateSignature— no separate session-key registry contract needed. - From-scratch ERC-4337 account (built on the reference
EntryPoint/BaseAccount, not a forked wallet SDK), so the validation logic is fully auditable in ~200 lines.
contracts/ Foundry project (SmartAccount, SmartAccountFactory, GameContract, tests)
frontend/ Next.js app: passkey creation, session key demo, clicker UI
# 0. Install contract dependencies (vendored libs are gitignored)
cd contracts
forge install eth-infinitism/account-abstraction@v0.6.0 --no-git
forge install OpenZeppelin/openzeppelin-contracts --no-git
forge install base-org/webauthn-sol --no-git
git clone --branch v4.9.3 --depth 1 https://github.com/OpenZeppelin/openzeppelin-contracts lib/openzeppelin-contracts-v4
git -C lib/webauthn-sol submodule update --init --recursive
# if FreshCryptoLib fails to check out (broken submodule ref upstream at time of
# writing), clone it manually and pin to the commit webauthn-sol expects:
# git clone https://github.com/rdubois-crypto/FreshCryptoLib lib/webauthn-sol/lib/FreshCryptoLib
# git -C lib/webauthn-sol/lib/FreshCryptoLib checkout 76f3f135b7b27d2aa519f265b56bfc49a2573ab5
# 1. Local chain
anvil
# 2. Deploy contracts
forge test # 4 tests covering grant/click/expiry/scope/revocation
forge script script/Deploy.s.sol \
--rpc-url http://127.0.0.1:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--broadcast
# copy the printed EntryPoint / SmartAccountFactory / GameContract addresses into
# frontend/.env.local if they differ from the defaults already there
# 3. Frontend
cd ../frontend
npm install
npm run dev
# open http://localhost:3000 in a browser with a platform authenticator
# (Touch ID / Face ID / Windows Hello / a security key) — WebAuthn requires
# a real authenticator and can't be driven from curl or headless tooling.Demo flow in the browser:
- Create wallet — triggers a passkey creation prompt, deploys a
SmartAccountowned by that passkey, funds it. - Grant session key — one more passkey prompt, authorizing a session key
(generated locally, stored in
localStoragefor the demo) to callGameContract.click()up to 50 times over the next 24h. - Click — sends a session-key-signed
UserOperationthrough the mock bundler. No further prompts.
- Session key material is kept in
localStoragein cleartext — fine for a demo, not for production (would live in memory / be wrapped by the OS keystore). - The "bundler" is a single relayer key calling
handleOpsdirectly, not a real bundler mempool — good enough to prove the account + session-key logic end-to-end without adding external infra dependency risk to a hackathon demo. - No paymaster: the account pre-funds its own
EntryPointdeposit. Sponsoring gas via a paymaster is the natural next feature. SmartAccountFactory.getAddressdoesn't fold owner pubkey into the CREATE2 salt computation (onlyentryPoint+saltare), since the owner key is set viainitialize()after deployment — fine for a single demo account per salt, would need revisiting for multi-owner factories.