Ethereum-lommeboken din har i praksis ikke endret seg siden 2015. En privatnøkkel signerer en transaksjon, og mister du den nøkkelen, mister du alt. ERC-4337 (kontoabstraksjon) bryter med den modellen ved å gjøre selve lommeboken til en smart kontrakt med egen logikk for godkjenning, gjenoppretting og gassbetaling. Standarden gikk live på Ethereum mainnet i mars 2023 uten å kreve endringer i konsensuslaget, og i 2026 bygger store aktører som Coinbase, Safe og Alchemy produksjonslommebøker på den. Denne guiden viser deg, steg for steg, hvordan du setter opp, koder, tester og sikrer en ERC-4337-smartkonto fra bunnen av.

Du trenger grunnleggende kjennskap til Solidity og JavaScript for å følge med, men vi forklarer hvert konsept underveis. Målet er et fungerende prosjekt du kan deploye til et testnett innen du er ferdig, pluss en forståelse av hvorfor de fleste ERC-4337-sårbarheter oppstår i valideringslogikken, ikke i selve standarden.

Guiden er delt i tolv konkrete steg, fra oppsett av utviklingsmiljøet til et komplett, deployerbart prosjekt med factory-kontrakt og tester. Underveis peker vi også på fem tilbakevendende feil og åtte konkrete feilmeldinger du sannsynligvis vil støte på, sammen med hvordan du løser dem raskt i stedet for å lete deg frem selv.

Hva er kontoabstraksjon, og hvorfor betyr det noe i 2026

En vanlig Ethereum-konto (EOA, Externally Owned Account) kan bare gjøre én ting: signere en transaksjon med én privatnøkkel. Den kan ikke kreve to signaturer, sette et daglig utbetalingstak, eller la noen andre betale gassen for deg. ERC-4337 løser dette ved å innføre “smarte kontoer”: kontrakter som selv definerer hva som teller som en gyldig godkjenning. Ifølge den offisielle ERC-4337-dokumentasjonen “muliggjør det programmerbare, sikre og fleksible smarte lommebøker, uten å kreve noen endringer i Ethereums konsensuslag” (docs.erc4337.io).

I praksis betyr det at kontoen din kan bruke passordnøkler (passkeys) i stedet for seed-fraser, la et sosialt gjenopprettingsnettverk hjelpe deg om du mister tilgang, eller la en tredjepart (en “paymaster”) betale gassen slik at brukeren aldri trenger å eie ETH for å komme i gang. Coinbase sin Smart Wallet og Safe sin 4337-modul er begge bygget på nøyaktig denne mekanismen, og begge er i aktiv drift i 2026.

Kjernen i systemet er fire komponenter som jobber sammen: en UserOperation (en pseudo-transaksjon brukeren signerer), en bundler (en node som samler UserOperations og sender dem videre), en EntryPoint-kontrakt (en felles inngangsport som validerer og kjører operasjonene på kjeden) og valgfrie paymaster-kontrakter (som kan sponse gassen). Forstår du disse fire delene, forstår du 80 prosent av angrepsflaten i ERC-4337.

Historisk sett ble kontoabstraksjon diskutert som en mulig endring i selve Ethereum-protokollen i årevis før noen fant en løsning som ikke krevde en hard fork. ERC-4337 sitt gjennombrudd var å flytte hele mekanismen opp på applikasjonslaget, slik at bundlere og EntryPoint-kontrakten gjør jobben en konsensusendring ellers ville krevd. Det er også grunnen til at standarden kunne rulles ut på mainnet allerede i mars 2023 og deretter spres til lag 2-nettverk som Arbitrum, Base og Optimism uten koordinert oppgradering av selve kjedene. For deg som utvikler betyr det at koden du skriver i denne guiden, i prinsippet fungerer likt uansett hvilken EVM-kompatibel kjede du deployer til, så lenge en EntryPoint-kontrakt og minst én bundler finnes der.

Forutsetninger: verktøy og versjoner du trenger

Sett av rundt 60 minutter til denne guiden hvis du følger alle stegene og kjører testene selv. Du trenger følgende installert før du starter:

VerktøyAnbefalt versjonFormål
Node.js20 LTS eller nyereKjøre skript og bundler-klienter
Foundry (forge/cast/anvil)nyeste stabile utgivelseKompilere, teste og deploye Solidity
Solidity0.8.23 eller nyereSkrive smartkonto-kontrakten
Git2.40 eller nyereVersjonskontroll av prosjektet
Testnett-lommebokMetaMask eller tilsvarendeSignere UserOperations under testing
Testnett-ETHSepoliaBetale gass under utvikling

Du trenger også tilgang til en bundler-tjeneste. Du kan kjøre din egen med eth-infinitism sin referanseimplementasjon, men de fleste utviklere bruker en hostet tjeneste under utvikling. Pimlico, Alchemy og Stackup tilbyr alle gratis testnett-tilgang, og vi bruker et generisk eksempel i denne guiden som fungerer med hvilken som helst av dem.

Versjonene i tabellen over er ikke tilfeldig valgt. Solidity 0.8.23 var det første stabile utgaven med full støtte for de nyere opkodene referansekontraktene fra eth-infinitism benytter, og eldre kompilatorversjoner kan feile stille på enkelte assembly-blokker i biblioteket. Tilsvarende krever nyere versjoner av Foundry sin cheatcode makeAddrAndKey, som testene senere i guiden bruker for å generere deterministiske nøkkelpar. Bruker du en eldre installasjon, får du kryptiske kompileringsfeil i stedet for en tydelig versjonsadvarsel, så det er verdt å oppdatere før du starter fremfor å feilsøke underveis.

Steg 1–2: Sett opp prosjektet og utviklingsmiljøet

Start med å opprette et nytt Foundry-prosjekt og installer ERC-4337-referansekontraktene fra eth-infinitism, som er samlingen de fleste produksjonslommebøker bygger videre på.

mkdir erc4337-smartkonto && cd erc4337-smartkonto
forge init --no-commit

forge install eth-infinitism/account-abstraction --no-commit
forge install OpenZeppelin/openzeppelin-contracts --no-commit

# Legg til remappings slik at importer fungerer
echo "@account-abstraction/=lib/account-abstraction/contracts/" >> remappings.txt
echo "@openzeppelin/=lib/openzeppelin-contracts/" >> remappings.txt

forge build

Steg to er å sette opp et lokalt testnett med Anvil slik at du kan iterere raskt uten å bruke ekte testnett-ETH for hver eneste endring:

anvil --chain-id 31337 --block-time 2

# I et annet terminalvindu:
export RPC_URL="http://127.0.0.1:8545"
export PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"

Nøkkelen over er Anvil sin offentlig kjente testnøkkel nummer 0, den samme som følger med i alle Foundry-installasjoner. Bruk den aldri på et nettverk med ekte verdier.

Steg 3–4: Forstå arkitekturen – EntryPoint, bundler og paymaster

Før du skriver kode, er det verdt å forstå rekkefølgen ting skjer i. Når en bruker vil utføre en handling, bygger lommebok-klienten en UserOperation, en datastruktur som ligner en transaksjon, men som ikke er en gyldig Ethereum-transaksjon i seg selv. Denne sendes til en bundler i stedet for direkte til mempoolen.

Bundleren simulerer operasjonen lokalt, sjekker at den følger valideringsreglene i ERC-7562 (som blant annet forbyr visse opkoder og begrenser tilgang til lagring under valideringsfasen), og pakker den sammen med andre UserOperations i én vanlig Ethereum-transaksjon som kaller handleOps på EntryPoint-kontrakten. Ifølge ERC-4337-dokumentasjonen “innkapsler smarte kontoer egendefinert logikk for autentisering, autorisasjon, betaling av nettverksgebyr, nonce-håndtering og utførelse” (docs.erc4337.io).

EntryPoint-kontrakten er en delt, uforanderlig singleton-kontrakt som alle smartkontoer forholder seg til. Den kaller validateUserOp på din kontrakt, sjekker at kontoen (eller en paymaster) har dekning for gassen, og utfører deretter selve kallet. Dette er stedet hvor mesteparten av sikkerhetsarbeidet ditt havner, fordi det er her du bestemmer hva som teller som en gyldig signatur.

LeverandørBundlerPaymaster-tilbudKontotype
AlchemyRundler (åpen kildekode, Rust)Gas ManagerModular Account, Light Account
PimlicoAlto (åpen kildekode, TypeScript)Verifiserende + ERC-20-paymasterKernel, Safe, Nexus (ERC-7579)
BiconomyBundler v3Sponsor- og token-paymasterSmart Account v2 (modulær)
ZeroDevUltra RelayGas PoliciesKernel (ERC-7579)
Coinbase Developer PlatformCoinbase BundlerSponset gass på Base (med tak)Coinbase Smart Wallet (passkey)
StackupStackup Bundler (åpen kildekode, Go)Paymaster ServiceFlere kontotyper (Kernel, Safe)

Merk at du ikke er avhengig av én enkelt bundler-leverandør for at kontoen din skal fungere. Fordi en UserOperation er signert av deg selv, ikke av bundleren, kan du i prinsippet sende samme operasjon til flere bundlere samtidig, eller bytte leverandør hvis en av dem opplever driftsproblemer. Dette er en viktig forskjell fra en sentralisert relayer-tjeneste: bundleren kan ikke endre innholdet i operasjonen din uten å ugyldiggjøre signaturen, den kan bare velge å inkludere den eller la være. Vil du unngå avhengighet til én enkelt leverandør i produksjon, er det vanlig praksis å konfigurere klienten til å prøve to eller tre bundlere i rekkefølge før den gir opp.

Steg 5–6: Skriv den minimale smartkonto-kontrakten

Nå bygger vi selve kontoen. Vi starter minimalt: én eier, én signaturkontroll, og et kall til EntryPoint. Legg denne i src/MinimalAccount.sol.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {IAccount} from "@account-abstraction/interfaces/IAccount.sol";
import {PackedUserOperation} from "@account-abstraction/interfaces/PackedUserOperation.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

contract MinimalAccount is IAccount {
    using ECDSA for bytes32;

    address public immutable owner;
    address public immutable entryPoint;

    error MinimalAccount__NotFromEntryPoint();
    error MinimalAccount__CallFailed(bytes reason);

    modifier onlyEntryPoint() {
        if (msg.sender != entryPoint) revert MinimalAccount__NotFromEntryPoint();
        _;
    }

    constructor(address _entryPoint, address _owner) {
        entryPoint = _entryPoint;
        owner = _owner;
    }

    function validateUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external onlyEntryPoint returns (uint256 validationData) {
        validationData = _validateSignature(userOp, userOpHash);
        if (missingAccountFunds > 0) {
            (bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
            require(success, "prefund failed");
        }
    }

    function _validateSignature(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash
    ) internal view returns (uint256) {
        bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(userOpHash);
        address signer = ethSignedHash.recover(userOp.signature);
        return signer == owner ? 0 : 1; // 0 = gyldig, 1 = ugyldig (SIG_VALIDATION_FAILED)
    }

    function execute(address target, uint256 value, bytes calldata data) external onlyEntryPoint {
        (bool success, bytes memory result) = target.call{value: value}(data);
        if (!success) revert MinimalAccount__CallFailed(result);
    }

    receive() external payable {}
}

Legg merke til at kontoen aldri signerer noe selv. En deltaker i den offentlige ERC-4337-diskusjonen på Ethereum Magicians formulerte det presist: “Kontoen er en kontrakt, og kan derfor ikke signere” (ethereum-magicians.org). Kontrakten kan bare verifisere en signatur som allerede finnes i UserOperation-dataene. All logikken din må derfor leve i verifiseringen, ikke i signeringen.

Steg 7–8: Sikre validateUserOp mot de vanligste feilene

Koden over er minimal, men den mangler flere ting en produksjonskonto trenger. For det første må adressen til kontoen henge sammen med eierens første signatur, ellers kan noen “kapre” en motregnet adresse før eieren rekker å deploye den selv. EIP-4337-spesifikasjonen er eksplisitt på dette punktet: “Av sikkerhetshensyn er det viktig at den genererte kontraktsadressen avhenger av den opprinnelige signaturen” (eips.ethereum.org). Løsningen er å bruke CREATE2 med eierens adresse som en del av salt-verdien, slik at adressen alltid er deterministisk knyttet til akkurat den eieren.

For det andre må du aldri skrive til lagring (storage) under selve valideringen utover det som er strengt nødvendig for nonce-sjekk. Bundlere simulerer validateUserOp lokalt for å avgjøre om en operasjon er verdt å inkludere, og hvis valideringen leser lagring som kan endres av andre transaksjoner mellom simulering og faktisk kjøring, kan bundleren bli lurt til å inkludere en operasjon som feiler på kjeden. Dette er nøyaktig den typen problem ERC-7562-regelsettet er laget for å fange opp.

OpenZeppelin har publisert en detaljert gjennomgang av nettopp dette regelsettet: “Regelsettet er også forbedret for å begrense angrepsflaten (for eksempel ved å forby ubrukte opkoder) og for å tilskrive valideringsfeil på kontoen til en registrert factory, der det er relevant” (openzeppelin.com). Her er en forbedret versjon av valideringen som legger til nonce-håndtering og en eksplisitt sjekk mot signaturmisbruk på tvers av kjeder:

function _validateSignature(
    PackedUserOperation calldata userOp,
    bytes32 userOpHash
) internal view returns (uint256 validationData) {
    // userOpHash inneholder allerede chainId og EntryPoint-adresse,
    // så vi trenger ikke å legge det til manuelt her.
    bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(userOpHash);
    address signer = ethSignedHash.recover(userOp.signature);

    if (signer != owner) {
        return 1; // SIG_VALIDATION_FAILED, aldri revert() her
    }

    // Pakk inn gyldighetsvindu om du vil begrense hvor lenge
    // en signert operasjon kan brukes (valgfritt, men anbefalt)
    uint48 validUntil = 0; // 0 = ingen utløp
    uint48 validAfter = 0;
    return _packValidationData(false, validUntil, validAfter);
}

Merk deg spesielt at funksjonen returnerer en feilkode i stedet for å kalle revert() når signaturen er ugyldig. Det er en av de vanligste kildene til feil i egenskrevne kontoer, fordi en revert midt i valideringsfasen kan gjøre at bundleren straffer hele operasjonen unødvendig i stedet for bare å avvise den.

Steg 9–10: Bygg og send din første UserOperation

Med kontrakten på plass trenger du en klient som kan bygge, signere og sende en UserOperation til en bundler. Under følger et eksempel med et generisk bundler-klientbibliotek. Bytt ut bundler-URL-en med den du fikk fra Pimlico, Alchemy eller Stackup.

import { createPublicClient, http, encodeFunctionData } from "viem";
import { sepolia } from "viem/chains";

const publicClient = createPublicClient({ chain: sepolia, transport: http() });

const ENTRY_POINT = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
const smartAccountAddress = "0xDinKontoAdresseHer";

async function buildUserOperation(target, value, callData, ownerSigner) {
  const nonce = await publicClient.readContract({
    address: ENTRY_POINT,
    abi: entryPointAbi,
    functionName: "getNonce",
    args: [smartAccountAddress, 0n],
  });

  const executeCallData = encodeFunctionData({
    abi: minimalAccountAbi,
    functionName: "execute",
    args: [target, value, callData],
  });

  const userOp = {
    sender: smartAccountAddress,
    nonce,
    callData: executeCallData,
    callGasLimit: 200000n,
    verificationGasLimit: 150000n,
    preVerificationGas: 50000n,
    maxFeePerGas: 2000000000n,
    maxPriorityFeePerGas: 1000000000n,
    paymasterAndData: "0x",
    signature: "0x",
  };

  const userOpHash = await publicClient.readContract({
    address: ENTRY_POINT,
    abi: entryPointAbi,
    functionName: "getUserOpHash",
    args: [userOp],
  });

  userOp.signature = await ownerSigner.signMessage({
    message: { raw: userOpHash },
  });

  return userOp;
}

Send deretter operasjonen til bundlerens JSON-RPC-endepunkt med metoden eth_sendUserOperation. Et typisk vellykket svar ser slik ut:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": "0x7a8f3e2b1c9d4a6f8e0b2c4d6e8f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f"
}
// Dette er UserOperation-hashen, ikke en transaksjonshash.
// Poll eth_getUserOperationReceipt med denne hashen for å se
// om operasjonen faktisk ble inkludert i en blokk.

Vær oppmerksom på gasskostnaden sammenlignet med en vanlig transaksjon. En målestudie av ERC-4337 fant at en enkel overføring via en SimpleAccount-kontrakt brukte 92 901 gass, omtrent fire ganger kostnaden til en tilsvarende EOA-overføring. Andre uavhengige analyser fra 2025 og 2026 anslår overheaden mer konservativt til mellom 15 000 og 42 000 ekstra gass per UserOperation, avhengig av kompleksiteten i valideringslogikken. Det er prisen du betaler for fleksibiliteten, og en av grunnene til at paymaster-sponset gass er så populært i konsumentrettede lommebøker.

Etter at operasjonen er sendt, bør du ikke bare vente passivt på en kvittering. Sett opp en enkel polling-løkke som spør eth_getUserOperationReceipt hvert par sekunder, og legg inn en timeout på rundt to minutter før du varsler brukeren om at noe kan ha gått galt. På mainnet bør du i tillegg overvåke paymasterens innskudd hos EntryPoint hvis du sponser gass for brukerne dine. Et innskudd som går tomt midt i en travel periode fører til at alle nye UserOperations med den paymasteren avvises, noe som ser ut som et driftsavbrudd for sluttbrukeren selv om kontraktene fungerer helt normalt.

Steg 11–12: Legg til gjenoppretting, passkeys og sesjonsnøkler

Den virkelige fordelen med kontoabstraksjon kommer når du utvider valideringslogikken utover én enkelt privatnøkkel. Tre mønstre dekker de fleste behovene:

  • Sosial gjenoppretting: utpek et sett med “guardians” (venner, andre enheter, eller en tjeneste) som sammen kan godkjenne bytte av eier etter en tidsforsinkelse, uten at noen av dem alene kan stjele kontoen.
  • Passkeys (WebAuthn): bytt ut ECDSA-signaturen med en P-256-signatur generert av enhetens sikre maskinvare (Face ID, Touch ID, Windows Hello). Coinbase sin Smart Wallet bruker nettopp dette som standard i 2026.
  • Sesjonsnøkler: gi en midlertidig, begrenset nøkkel lov til å signere for kontoen din innenfor strenge rammer (for eksempel kun mot én kontrakt, med et gitt beløpstak, i 24 timer), typisk brukt av spill og apper som ellers ville krevd en signatur per handling.

Alle tre mønstrene krever at du gjør valideringslogikken modulær i stedet for hardkodet, slik ERC-7579 (en utvidelse mange av leverandørene i tabellen over allerede støtter) legger opp til. Den praktiske konsekvensen for deg som utvikler er at hver ny valideringsmodul er en ny angrepsflate, og at hver modul bør revideres uavhengig av de andre. Å legge til gjenoppretting uten å tenke gjennom tidsforsinkelsen, for eksempel, kan gjøre kontoen sårbar for guardians som blir kompromittert samtidig.

Et konkret eksempel: ZeroDev sin Kernel-konto lar deg installere en sesjonsnøkkel-modul som begrenser en midlertidig nøkkel til bare å kalle én spesifikk funksjon på én spesifikk kontrakt, innenfor et gitt beløpstak per dag. Dette er nyttig for et spill som trenger å signere hundrevis av småtransaksjoner uten å avbryte brukeren hver gang, men det krever at du er nøye med hvilke funksjoner modulen faktisk har tilgang til. En sesjonsnøkkel-modul som ved en feil gir tilgang til en approve-funksjon på en token-kontrakt, kan i verste fall gi en kompromittert nøkkel langt mer makt enn tiltenkt, selv om beløpsgrensen på selve overføringen er lav.

Integrer med Safes ERC-4337-modul for produksjonsklar sikkerhet

Å skrive alt selv er nyttig for læring, men de fleste produksjonslommebøker bygger heller på en revidert base som Safe. Safe sin Safe4337Module lar en eksisterende Safe-kontrakt fungere som en ERC-4337-smartkonto ved å implementere validateUserOp og rute godkjente operasjoner videre til execTransactionFromModule. Ifølge Safes egen dokumentasjon skal modulen kun brukes sammen med Safe versjon 1.4.1 eller nyere (docs.safe.global), fordi eldre versjoner mangler nødvendige hooks for modulen.

// Aktiver Safe4337Module på en eksisterende Safe (via Safe Transaction Builder
// eller et skript som kaller enableModule)
const safe4337ModuleAddress = "0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226";

const enableModuleTx = {
  to: safeAddress,
  value: 0,
  data: encodeFunctionData({
    abi: safeAbi,
    functionName: "enableModule",
    args: [safe4337ModuleAddress],
  }),
};

// Denne transaksjonen må godkjennes av Safe-eierne på vanlig måte
// (multisig-terskel) FØR kontoen kan motta UserOperations via EntryPoint.

Fordelen med denne tilnærmingen er at du arver Safes mangeårige revisjonshistorikk og multisig-logikk, og bare legger til ERC-4337-laget oppå. Ulempen er en ekstra kontraktshopp per operasjon, som legger til noe gass sammenlignet med en dedikert 4337-konto bygget fra bunnen av.

Test og revider smartkontoen din med Foundry

Ingen smartkonto bør deployes til mainnet uten grundig testing. Foundry sin fuzzing-motor er spesielt nyttig for å teste valideringslogikk, fordi den kan generere hundrevis av ugyldige signaturer og kombinasjoner av nonce-verdier automatisk.

// test/MinimalAccount.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Test} from "forge-std/Test.sol";
import {MinimalAccount} from "../src/MinimalAccount.sol";

contract MinimalAccountTest is Test {
    MinimalAccount account;
    address owner;
    uint256 ownerKey;
    address entryPoint = address(0x1234);

    function setUp() public {
        (owner, ownerKey) = makeAddrAndKey("owner");
        account = new MinimalAccount(entryPoint, owner);
    }

    function test_RevertWhen_CalledByNonEntryPoint() public {
        vm.expectRevert(MinimalAccount.MinimalAccount__NotFromEntryPoint.selector);
        vm.prank(address(0xBAD));
        account.execute(address(0), 0, "");
    }

    function testFuzz_RejectsWrongSigner(uint256 wrongKey) public {
        vm.assume(wrongKey != ownerKey && wrongKey != 0);
        vm.assume(wrongKey < type(uint256).max / 2); // gyldig secp256k1-omrade
        // Bygg en falsk signatur med feil nokkel og verifiser at
        // valideringen returnerer feilkode 1, ikke revert
        // (full implementasjon krever PackedUserOperation-oppsett)
    }
}

Kjør testene med forge test -vvv og se etter at alle negative tester faktisk feiler slik du forventer. Et vanlig nybegynnerfeil er å skrive en test som "består" fordi den aldri når frem til den kritiske sjekken i utgangspunktet. Bruk forge coverage for å bekrefte at valideringsgrenen din faktisk kjøres av testene.

Enhetstester i Foundry fanger opp mesteparten, men de tester kontrakten i isolasjon, ikke hele kjeden fra bundler til EntryPoint. Før du går til mainnet, bør du derfor også kjøre minst én fullstendig integrasjonstest på et offentlig testnett som Sepolia, der du faktisk sender en signert UserOperation gjennom en ekte bundler og venter på en reell kvittering. Dette fanger opp feil som simuleringsreglene i ERC-7562 håndhever (som lagringstilgang og forbudte opkoder), men som ikke nødvendigvis dukker opp i en lokal Foundry-test siden Anvil ikke håndhever de samme bundler-spesifikke restriksjonene.

Fem vanlige feil i ERC-4337-implementasjoner

De fleste sikkerhetsproblemer i egenbygde smartkontoer følger et lite knippe gjentagende mønstre. Her er de fem du bør sjekke først i din egen kode:

  1. Bruk av revert() i stedet for feilkode i validateUserOp. Når signaturen er ugyldig, skal funksjonen returnere verdien 1 (SIG_VALIDATION_FAILED), ikke kaste et unntak. En revert midt i valideringen kan gjøre at bundleren avviser hele batchen unødvendig.
  2. Manglende binding mellom userOpHash og chainId. Hvis du bygger din egen hash i stedet for å stole på verdien EntryPoint gir deg, risikerer du at en signatur signert for én kjede kan gjenbrukes på en annen (replay-angrep).
  3. Lagringsskriving under validering. Alt utover den nødvendige nonce-oppdateringen bryter med ERC-7562-reglene og kan gjøre at bundlere avviser operasjonen din i simulering, selv om den ville fungert på kjeden.
  4. Manglende EIP-712 for ERC-1271-signaturer. Kontrakts-til-kontrakt-signaturverifisering uten strukturert hashing åpner for at samme signatur kan misbrukes i en annen kontekst enn den var ment for.
  5. Stole blindt på paymaster-data uten å validere avsender. En kompromittert eller dårlig validert paymaster kan tømmes for hele sitt innskudd hos EntryPoint, fordi det er paymasterens saldo, ikke brukerens, som trekkes for gassen.

Feilsøking: 8 vanlige problemer og hvordan du løser dem

ProblemSannsynlig årsakLøsning
Bundleren avviser operasjonen med "AA23 reverted"validateUserOp reverter i stedet for å returnere feilkodeEndre logikken til å returnere 1 ved ugyldig signatur
"AA21 didn't pay prefund"Kontoen mangler ETH til å dekke gassforskuddSend ETH til kontoadressen eller bruk en paymaster
Operasjonen henger uten kvitteringFeil nonce-nøkkel (key) brukt i getNonce-kalletBruk samme nonce-nøkkel konsekvent, vanligvis 0 for enkle kontoer
"AA13 initCode failed or OOG"Factory-kontrakten feiler under kontoopprettelseTest factory-deployment isolert med forge test før du sender via bundler
Signaturvalidering feiler i simulering, men fungerer lokaltUlik chainId eller EntryPoint-adresse brukt i hash-beregningenHent alltid userOpHash direkte fra EntryPoint-kontrakten, ikke egen beregning
Paymaster avviser operasjonenpaymasterAndData mangler gyldig signatur eller er utløptSjekk gyldighetsvinduet (validUntil/validAfter) paymasteren satte
Gasskostnaden er høyere enn forventetFor høy verificationGasLimit satt manueltBruk bundlerens eth_estimateUserOperationGas før innsending
"AA25 invalid account nonce"To operasjoner sendt med samme nonce samtidigVent på kvittering for forrige operasjon før du sender neste med samme nøkkel

Avanserte tips: kryssnettverk-replay, paymaster-risiko og EIP-7702

Når grunnmuren står, er det noen mer avanserte tema verdt å sette seg inn i. Test alltid at en signert UserOperation fra ett nettverk faktisk avvises på et annet. Den vanligste måten å skrive en automatisert test på, er å forke to forskjellige kjeder i Foundry og forsøke å spille av samme signerte operasjon på begge. Hvis den andre kjeden godtar den, mangler du chainId-binding i valideringen.

Paymaster-risiko fortjener egen oppmerksomhet. Fordi en paymaster sponser gassen ved å sette et innskudd hos EntryPoint, og dette innskuddet debiteres etter at operasjonen er utført, kan svak validering i paymaster-kontrakten føre til at hele innskuddet tømmes, uavhengig av brukerens egen konto. Design paymasteren din slik at den validerer avsenderadresse, beløpsgrenser og gyldighetsvindu, ikke bare at en signatur eksisterer.

Til slutt bør du følge med på EIP-7702, som ble innført i forbindelse med Pectra-oppgraderingen på Ethereum i 2025. Der ERC-4337 krever en helt separat smartkonto, lar EIP-7702 en vanlig EOA midlertidig "låne" kode fra en kontrakt, og dermed få mye av den samme fleksibiliteten uten å måtte migrere til en ny adresse. De to standardene konkurrerer ikke nødvendigvis. Flere av leverandørene i tabellen tidligere i artikkelen støtter allerede begge deler, men de har ulike avveininger når det gjelder gasskostnad og kompatibilitet med eksisterende infrastruktur. Bygger du noe nytt i 2026, er det verdt å vurdere begge før du låser arkitekturen.

ERC-4337 mot tradisjonell EOA-lommebok og multisig: hva bør du velge

Ikke alle prosjekter trenger full kontoabstraksjon. Valget handler i praksis om hvem sluttbrukeren din er, og hvor mye friksjon de tåler før de gir opp. En erfaren kryptobruker som allerede har en hardware-lommebok og forstår seed-fraser, får sjelden noe ekstra ut av en smartkonto utover bekvemmelighet. En helt ny bruker som aldri har eid kryptovaluta før, kan derimot falle fra helt hvis de må kjøpe ETH bare for å betale gassen på sin aller første handling i appen din. Tabellen under oppsummerer avveiningene mellom de tre vanligste modellene for lommeboksikkerhet i 2026.

EgenskapTradisjonell EOAMultisig (f.eks. Safe)ERC-4337 smartkonto
Gasskostnad per operasjonLavest (basiskostnad ~21 000 gass)Middels (flere signaturkall)Høyere (typisk 15 000–42 000 gass i overhead)
Gjenoppretting ved tapt nøkkelIngen, nøkkelen er altMulig hvis andre eiere er tilgjengeligeKonfigurerbar (sosial gjenoppretting, guardians)
Sponset gass (gasless UX)Ikke muligKrever relayer-oppsett utenfor standardenInnebygd via paymaster-mekanismen
Passkey/biometrisk påloggingIkke støttet nativtIkke støttet nativtStøttet via P-256-validering
Modenhet og revisjonshistorikkSvært moden (siden 2015)Moden (flere års produksjonsbruk)Voksende, men yngre angrepsflate
Kompleksitet å implementere selvLavMiddelsHøy uten en revidert base som Safe eller Kernel

Tommelfingerregelen: bruk en vanlig EOA for enkle personlige lommebøker der du selv har full kontroll og ikke trenger gjenoppretting. Velg multisig for behandlingssikre treasury-oppsett med flere ansvarlige parter. Velg ERC-4337 når du bygger en applikasjon der sluttbrukeren ikke skal måtte forstå seed-fraser eller eie ETH før de kan bruke produktet.

Det komplette prosjektet: struktur og deployment-skript

Sett sammen blir prosjektet ditt seende slik ut, klart til å deployes til Sepolia testnett:

Før deployment-skriptet trenger du en factory-kontrakt som løser problemet vi nevnte i steg 7–8: kontoadressen må avhenge deterministisk av eierens nøkkel, ikke av deployment-rekkefølgen. Dette gjør du med CREATE2, slik at samme eier alltid får samme kontoadresse uansett hvem som utløser deploymentet.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {MinimalAccount} from "./MinimalAccount.sol";

contract MinimalAccountFactory {
    event AccountCreated(address indexed owner, address indexed account);

    function createAccount(address entryPoint, address owner, uint256 salt)
        external
        returns (MinimalAccount account)
    {
        address predicted = getAddress(entryPoint, owner, salt);
        if (predicted.code.length > 0) {
            return MinimalAccount(payable(predicted));
        }

        account = new MinimalAccount{salt: bytes32(salt)}(entryPoint, owner);
        emit AccountCreated(owner, address(account));
    }

    function getAddress(address entryPoint, address owner, uint256 salt)
        public
        view
        returns (address)
    {
        bytes memory bytecode = abi.encodePacked(
            type(MinimalAccount).creationCode,
            abi.encode(entryPoint, owner)
        );
        return address(
            uint160(uint256(keccak256(
                abi.encodePacked(bytes1(0xff), address(this), bytes32(salt), keccak256(bytecode))
            )))
        );
    }
}

Legg merke til at createAccount sjekker om kontoen allerede finnes før den forsøker å deploye på nytt. Bundlere kaller ofte denne funksjonen som en del av initCode når en konto brukes for første gang, og hvis funksjonen reverter fordi kontoen allerede eksisterer, vil hele UserOperation feile unødvendig. Denne idempotente oppførselen er en av de tingene som ser trivielle ut på papiret, men som er ansvarlig for en god andel av "AA13 initCode failed"-feilene utviklere støter på i praksis.

erc4337-smartkonto/
├── src/
│   ├── MinimalAccount.sol
│   └── MinimalAccountFactory.sol
├── test/
│   └── MinimalAccount.t.sol
├── script/
│   └── DeployMinimalAccount.s.sol
├── lib/
│   ├── account-abstraction/
│   └── openzeppelin-contracts/
└── foundry.toml
// script/DeployMinimalAccount.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Script} from "forge-std/Script.sol";
import {MinimalAccount} from "../src/MinimalAccount.sol";

contract DeployMinimalAccount is Script {
    address constant ENTRY_POINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032;

    function run() external returns (MinimalAccount) {
        vm.startBroadcast();
        MinimalAccount account = new MinimalAccount(ENTRY_POINT, msg.sender);
        vm.stopBroadcast();
        return account;
    }
}

Deploy med følgende kommando, og pass på at RPC_URL peker mot Sepolia, ikke ditt lokale Anvil-nettverk, når du er klar for et ekte testnett:

forge script script/DeployMinimalAccount.s.sol \
  --rpc-url $SEPOLIA_RPC_URL \
  --private-key $PRIVATE_KEY \
  --broadcast \
  --verify

# Forventet output:
# [Success] Hash: 0x8f3a2b1c...
#   Contract Address: 0x5aC9...4E12
#   Block: 6821345
#   Gas Used: 412903

Med kontrakten verifisert på Etherscan kan du deretter bygge en factory rundt den (for CREATE2-basert deployment ved første bruk), koble den til en bundler, og begynne å sende UserOperations slik vi viste tidligere i guiden.

Sikkerhetssjekkliste før du deployer til mainnet

Når koden fungerer på testnett, gjenstår det viktigste steget: å bekrefte at den også tåler et fiendtlig miljø med ekte verdier på spill. Gå gjennom denne listen før du flytter noe til mainnet, uansett hvor liten eller midlertidig kontoen virker.

  • Kjør statisk analyse. Verktøy som Slither fanger opp åpenbare mønstre som reentrancy og manglende tilgangskontroll før du bruker tid på manuell gjennomgang.
  • Fuzz-test valideringsfunksjonen spesifikt. Ikke bare de positive stiene, men også ugyldige signaturer, feil nonce-rekkefølge og manipulert paymasterAndData.
  • Verifiser at chainId er en del av userOpHash. Test dette ved å forsøke å spille av en signert operasjon på et annet nettverk i en fork-test.
  • Sjekk at validateUserOp aldri reverter ved ugyldig signatur. Bruk feilkoder, ikke unntak, slik vi viste tidligere i guiden.
  • Test factory-kontraktens idempotens. Kall createAccount to ganger for samme eier og bekreft at andre kall ikke reverter.
  • Sett realistiske gassgrenser. Bruk bundlerens eth_estimateUserOperationGas i stedet for å gjette verificationGasLimit og callGasLimit manuelt.
  • Vurder en ekstern revisjon for alt som håndterer mer enn du har råd til å tape. Selv erfarne team overser detaljer i valideringslogikk, og en andre person med frisk blikk fanger ofte opp ting forfatteren har blitt blind for.
  • Dokumenter gjenopprettingsprosessen for sluttbrukeren. En teknisk sikker konto som ingen klarer å gjenopprette i praksis, er ikke reelt sikrere enn en tapt seed-frase.

Denne listen dekker ikke alt, men den fanger opp de feilene som går igjen oftest i offentlig dokumenterte gjennomganger av ERC-4337-kontoer. De fleste alvorlige hendelser skyldes ikke eksotiske kryptografiske svakheter, men grunnleggende feil i hvem som får kalle hva, og når.

Ofte stilte spørsmål om ERC-4337 og smartkonto-sikkerhet

Er ERC-4337 tryggere enn en vanlig lommebok?

Det kommer an på implementasjonen. Standarden selv gir deg verktøyene til å bygge inn beskyttelse som gjenoppretting og utgiftsgrenser, men en dårlig skrevet smartkonto kan være mindre trygg enn en vanlig EOA. Bruk reviderte baser som Safe eller Kernel fremfor å skrive alt fra bunnen av i produksjon.

Koster ERC-4337-transaksjoner mer i gass?

Ja, typisk noe mer enn en ren EOA-transaksjon. Uavhengige analyser fra 2025 og 2026 anslår overheaden til mellom 15 000 og 42 000 ekstra gass per UserOperation, avhengig av hvor kompleks valideringslogikken er. Paymaster-sponset gass kan flytte denne kostnaden bort fra sluttbrukeren.

Hva er forskjellen på ERC-4337 og EIP-7702?

ERC-4337 krever en egen smartkontokontrakt med egen adresse. EIP-7702, innført med Pectra-oppgraderingen i 2025, lar en eksisterende EOA midlertidig kjøre kode fra en kontrakt uten å bytte adresse. De løser overlappende problemer, men med ulike avveininger for gasskostnad og migrering.

Kan jeg miste tilgangen til en ERC-4337-konto?

Ja, hvis valideringslogikken ikke inkluderer noen form for gjenoppretting og du mister den eneste godkjente nøkkelen. Fordelen med kontoabstraksjon er nettopp at du kan bygge inn sosial gjenoppretting eller flere godkjente signeringsmetoder, men det er noe du selv må designe inn. Det kommer ikke gratis med standarden.

Hva er en paymaster, og må jeg bruke en?

En paymaster er en valgfri kontrakt som kan betale gassen for brukerens UserOperation, enten sponset gratis eller mot betaling i en annen token enn ETH. Den er ikke påkrevd. Uten en paymaster må kontoen selv ha nok ETH til å dekke gassen.

Er det trygt å bruke passkeys i stedet for en seed-frase?

Passkeys basert på P-256-signaturer og enhetens sikre maskinvare (som Face ID eller Windows Hello) fjerner risikoen for at en seed-frase blir stjålet via phishing eller skjermbilder. Risikoen flyttes i stedet til enheten og til hvordan gjenoppretting er satt opp hvis du mister enheten. Gjenopprettingsdesignet er derfor minst like viktig som selve signeringsmetoden.

Hvor mange UserOperations er behandlet på Ethereum totalt?

Tallene spriker mellom kilder. En sammenligning av EIP-4337 og EIP-7702 oppga minst 50 millioner UserOperations kumulativt på tvers av mainnet og lag 2-nettverk innen midten av 2025, mens enkelte 2026-rapporter oppgir over 100 millioner. Les slike tall som grove størrelsesordener, ikke eksakte fasitsvar, siden metodikken varierer fra kilde til kilde.

Må jeg revidere min egen smartkonto før jeg bruker den i produksjon?

Ja. Selv små avvik i valideringslogikken, som å bruke revert() på feil sted eller glemme chainId-binding, kan åpne for tap av midler. Bruk fuzzing-tester i Foundry, kjør statisk analyse, og vurder en ekstern revisjon før du håndterer ekte verdier, uansett hvor liten kontrakten virker.

Fungerer ERC-4337 på lag 2-nettverk som Arbitrum og Base?

Ja. Fordi ERC-4337 er implementert utelukkende gjennom smarte kontrakter og ikke krever endringer i konsensuslaget, fungerer den samme EntryPoint-modellen på enhver EVM-kompatibel kjede der noen har deployet kontraktene og driftet en bundler. Coinbase sin Smart Wallet kjører for eksempel primært på Base, mens flere av leverandørene nevnt tidligere i artikkelen (Alchemy, Pimlico, ZeroDev) tilbyr bundler-tjenester på tvers av både Ethereum mainnet og de største lag 2-nettverkene.