Smart Contract Wallet Development (Account Abstraction)
Account Abstraction is an architectural shift that transforms a wallet from a passive key storage into a programmable agent. EIP-4337 standardized this approach without changes at the Ethereum protocol level: all logic lives in smart contracts, and specialized mempool infrastructure (Bundlers and Paymasters) processes UserOperation objects instead of regular transactions. If you're building a product where users shouldn't worry about gas, seed phrases, and approvals — you can't do without AA.
EIP-4337 Architecture: How It Actually Works
System Components
A classic EOA transaction goes directly to mempool and is executed by a node. In an AA system, the flow is different:
-
UserOperation — a pseudo-transaction signed by the user. Contains
callData,sender(smart wallet address),signature, gas limits and Paymaster parameters. - Bundler — an offchain agent that collects UserOperations from an alternative mempool, packages them into a single on-chain transaction, and calls EntryPoint. Existing implementations: Stackup, Pimlico, Alchemy Rundler (written in Rust, orders of magnitude faster than reference implementation).
- EntryPoint — a singleton contract (0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 on most EVM networks), deployed by the ERC-4337 team. It verifies and executes a batch of operations. You cannot deploy your own EntryPoint — the ecosystem is tied to this address.
-
Account Contract — the user's smart wallet itself. Must implement the
IAccountinterface with avalidateUserOpmethod. All custom logic goes here. - Paymaster — an optional contract that pays gas for the user or accepts ERC-20 tokens as payment instead of ETH.
UserOperation Lifecycle
User → sign UserOp → send to Bundler RPC
Bundler → simulate via eth_estimateUserOperationGas → validate signature + paymaster
Bundler → batch multiple UserOps → call EntryPoint.handleOps()
EntryPoint → validateUserOp() on each Account Contract
EntryPoint → Paymaster.validatePaymasterUserOp()
EntryPoint → execute callData
EntryPoint → postOp() on Paymaster (for gas accounting)
Important: simulation and execution are separate. The Bundler simulates via eth_callStateOverride and rejects operations that may fail on-chain. This protects the Bundler from losing ETH on failed transactions.
Implementing Account Contract
Basic Structure
Use SimpleAccount from eth-infinitism or SafeAccount from Safe (formerly Gnosis Safe) as a base. For production — Safe v1.4.1 with 4337 module, because it's battle-tested with $100B+ TVL.
contract SmartWallet is IAccount, Initializable, UUPSUpgradeable {
address public owner;
IEntryPoint private immutable _entryPoint;
function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external override returns (uint256 validationData) {
_requireFromEntryPoint();
validationData = _validateSignature(userOp, userOpHash);
_validateNonce(userOp.nonce);
_payPrefund(missingAccountFunds);
}
function _validateSignature(
UserOperation calldata userOp,
bytes32 userOpHash
) internal view returns (uint256) {
bytes32 hash = userOpHash.toEthSignedMessageHash();
if (owner != hash.recover(userOp.signature))
return SIG_VALIDATION_FAILED;
return 0; // SIG_VALIDATION_SUCCESS
}
}
validationData encodes three things: validation result (0 = success, 1 = failure), validAfter and validUntil timestamps. This enables time-bounded operations directly in the signature.
Advanced Logic Patterns
Multisig in a single wallet. Instead of owner, a threshold and list of authorized signers are stored. validateUserOp checks that signatures contains sufficient correct signatures.
Session keys. A restricted key (e.g., generated by the browser without seed phrase exposure) that's allowed to operate only within a specific contract, spending limit, and time window:
struct SessionKey {
address key;
address allowedContract;
uint256 spendingLimit;
uint48 validUntil;
bool enabled;
}
mapping(address => SessionKey) public sessionKeys;
This is the foundation for "gasless gaming" — the user signs a session to a game contract once, then the game makes transactions on their behalf.
Social recovery. Guardians — trusted addresses that can change owner through a timelock (usually 72 hours). Argent's implementation is a good reference: threshold from M-of-N guardians, cancellation within the timelock window if the owner is online.
Paymaster: Gasless and ERC-20 Payment
Sponsoring Paymaster
contract SponsoringPaymaster is IPaymaster {
mapping(address => bool) public whitelistedContracts;
function validatePaymasterUserOp(
UserOperation calldata userOp,
bytes32,
uint256 maxCost
) external returns (bytes memory context, uint256 validationData) {
// Sponsor only calls to whitelisted contracts
address target = address(bytes20(userOp.callData[16:36]));
require(whitelistedContracts[target], "Not whitelisted");
require(deposit() >= maxCost, "Insufficient deposit");
return (abi.encode(userOp.sender), 0);
}
}
The Paymaster must maintain a deposit in EntryPoint. The staking mechanism prevents DoS: a Paymaster without stake can sponsor at most one operation per bundle.
ERC-20 Paymaster
Accepts any ERC-20 as gas payment. An oracle is needed for conversion: Chainlink price feed or Uniswap V3 TWAP pool. The workflow: before execution, we lock maxCost * exchangeRate tokens; after, we deduct the actual cost via postOp.
Ready-made solutions: Pimlico ERC-20 Paymaster (open source), Stackup Paymaster SDK.
Factory and Counterfactual Deployment
One of AA's key properties is that a wallet exists as an address before deployment. CREATE2 with a deterministic salt (usually a hash of the owner address) gives a predictable address. The user gets a wallet address before the first transaction, can receive funds — the wallet deploys automatically on first use.
contract WalletFactory {
function getAddress(address owner, uint256 salt) public view returns (address) {
return Create2.computeAddress(
bytes32(salt),
keccak256(abi.encodePacked(
type(ERC1967Proxy).creationCode,
abi.encode(address(implementation), initData(owner))
))
);
}
function createAccount(address owner, uint256 salt) external returns (SmartWallet) {
address addr = getAddress(owner, salt);
if (addr.code.length > 0) return SmartWallet(payable(addr)); // already deployed
return SmartWallet(payable(new ERC1967Proxy{salt: bytes32(salt)}(
address(implementation), initData(owner)
)));
}
}
Frontend Integration
Viem + permissionless.js — the most current stack (2024–2025). permissionless is built on Viem and provides abstractions for working with Bundler and Paymaster RPC:
import { createSmartAccountClient } from "permissionless";
import { signerToSimpleSmartAccount } from "permissionless/accounts";
import { createPimlicoBundlerClient } from "permissionless/clients/pimlico";
const smartAccount = await signerToSimpleSmartAccount(publicClient, {
signer: walletClient,
factoryAddress: FACTORY_ADDRESS,
entryPoint: ENTRY_POINT_ADDRESS,
});
const smartAccountClient = createSmartAccountClient({
account: smartAccount,
chain: optimism,
bundlerTransport: http(BUNDLER_RPC_URL),
middleware: {
sponsorUserOperation: paymasterClient.sponsorUserOperation,
},
});
// Sending a transaction — identical to a regular wallet for the user
const txHash = await smartAccountClient.sendTransaction({
to: contractAddress,
data: encodeFunctionData({ abi, functionName: "doSomething" }),
});
ZeroDev SDK — an alternative with a higher level of abstraction, built-in session keys and Kernel account (a popular Account Contract with a plugin system).
Alternatives to EIP-4337
zkSync Native AA — on zkSync Era, AA is built into the protocol, no separate EntryPoint needed. Every account can be a smart contract out of the box. More efficient on gas, but tied to zkSync.
EIP-7702 (Prague/Electra) — the upcoming Ethereum hard fork. Allows EOA to temporarily delegate execution to a smart contract through a special transaction type. Doesn't fully replace 4337, but covers some use cases more simply.
Development Stages and Estimation
| Component | Complexity | Duration |
|---|---|---|
| Basic Account Contract (single owner) | Medium | 1–2 weeks |
| Factory + counterfactual deploy | Low | 3–5 days |
| Sponsoring Paymaster | Medium | 1 week |
| ERC-20 Paymaster + oracle | High | 1–2 weeks |
| Session keys | High | 1–2 weeks |
| Social recovery | High | 1–2 weeks |
| Frontend SDK integration | Medium | 1 week |
| Audit + fixes | — | 3–6 weeks |
Minimum production-ready wallet (single owner + sponsoring paymaster + frontend) — 4–6 weeks of development. Full-featured product with social recovery, session keys and multi-chain — 3–5 months.
Key point when choosing a contractor: the validateUserOp implementation must be audited. An error in this function is direct loss of user funds. Saving on an audit here = conscious risk.







