LazorKit LogoLazorKit
Concepts

Smart Wallet

How LazorKit smart wallets work — PDA structure, vault vs wallet PDA, authentication, roles, lifecycle.

A LazorKit smart wallet is a cluster of on-chain accounts controlled by the LazorKit program. The owner authenticates via WebAuthn (passkey) or Ed25519 — no seed phrase, no browser extension, no private key storage in your app.

Two PDAs you'll see everywhere

smartWallet / vault PDA — where SOL and tokens live. This is the address users see, share, and receive funds at.

walletPda — the wallet metadata account. Stores nothing you care about directly; it's the identity anchor used internally by the program.

In SDK v2 the hook returns smartWalletPubkey = vault and walletPdaPubkey = metadata, matching this mental model.


Account structure

AccountSeedsSizeWhat it holds
Wallet PDA["wallet", user_seed]8 bytesIdentity anchor; bump + version only.
Vault PDA["vault", wallet]0 bytesHolds all SOL and tokens. Program signs via PDA seeds.
Authority PDA["authority", wallet, credential_id_hash]80 (Ed25519) / 145 (Secp256r1)Per-key auth record + replay counter.
Session PDA["session", wallet, session_key]80+ bytesEphemeral signer + immutable action buffer.

Wallet PDA

Minimal 8-byte account. Chosen by a 32-byte user_seed at creation. You don't need to store it client-side — look it up via findWalletsByAuthority(credentialIdHash).

Vault PDA

Zero-data account that holds SOL and tokens. The program signs for it during Execute. No rent deposit required — the vault is itself a PDA with no allocated data.

Send funds here — not to the wallet PDA

Users should receive SOL/tokens at the vault (smartWallet / smartWalletPubkey). Sending to the wallet PDA won't fail, but the program won't recognize those funds during Execute — it only spends from the vault.

Authority PDA

One PDA per authorised key. Header (48 bytes) + tail:

  • authority_typeEd25519 (0) or Secp256r1 (1)
  • roleOwner (0) / Admin (1) / Spender (2)
  • counter — monotonic u32 for Secp256r1 replay protection
  • tail — 32-byte pubkey for Ed25519; credential_id_hash(32) + compressed_pubkey(33) + rpId_hash(32) for Secp256r1 (fixed 97 bytes)

Secp256r1 accounts are now fixed-size

Post Phase 2.1, Secp256r1 authorities store sha256(rpId) instead of the raw rpId string. That makes every Secp256r1 authority exactly 145 bytes, eliminating one sol_sha256 syscall per Execute. Clients still send rpId as a string when creating a wallet or adding an authority — the program hashes it once at write time.

Because each authority has its own PDA, different authorities on the same wallet execute in parallel — the only writable account during Execute is the caller's own authority PDA (counter increment). Wallet and vault are read-only.


Authentication

Secp256r1 (Passkey)

WebAuthn credentials bound to the device's Secure Enclave (Face ID, Touch ID, Windows Hello, security keys). The credential never leaves the device. On-chain verification uses the native Secp256r1 precompile and enforces:

  • Odometer counter — monotonic u32 per authority. Client submits stored_counter + 1. Committed only after successful verification.
  • Slot freshness — auth payload slot must be within 150 slots of Clock::get().
  • CPI protection — stack height check prevents passkey auth via CPI.
  • Accounts hash — signed payload binds to all account pubkeys, preventing account-swap attacks.

Ed25519

Standard Solana keypair. Verified by the Solana runtime. No counter required — Solana's tx-level duplicate detection handles replay.


Roles

RoleValuePermissions
Owner0Full control — add/remove any authority, transfer ownership, create/revoke sessions, execute
Admin1Add Spender authorities, create/revoke sessions, execute
Spender2Execute only

See RBAC for the full permission breakdown.


Wallet lifecycle

Create

const { instructions, walletPda, vaultPda, authorityPda } = await client.createWallet({
  payer: payer.publicKey,
  userSeed: crypto.randomBytes(32),
  owner: {
    type: 'secp256r1',
    credentialIdHash,     // 32-byte SHA256 of WebAuthn credential ID
    compressedPubkey,     // 33-byte compressed Secp256r1 public key
    rpId: 'your-app.com',
  },
});

Creates Wallet PDA, Vault PDA, and the first Authority PDA in one transaction.

Look up a returning user

No need to persist walletPda client-side:

const [wallet] = await client.findWalletsByAuthority(credentialIdHash);
// { walletPda, authorityPda, vaultPda, role, authorityType }

Add authority

Adds another key. Owner can add any role; Admin can only add Spender.

Remove authority

Closes the authority PDA and refunds rent to a specified destination. Prevents self-removal and removal of the last owner.

Transfer ownership

Atomically closes the old owner PDA and creates a new owner PDA. Refunds the old owner's rent to an explicit destination.


Costs

OperationCost
Wallet creation (Ed25519)0.002399 SOL
Wallet creation (Secp256r1)0.002713 SOL
Execute (per tx)0.000005 SOL
Session setup (one-time)0.001453 SOL

At $150/SOL, wallet creation is ~$0.36–$0.41. Session setup is ~$0.22, with subsequent session-signed executes at ~$0.00075 each.