LazorKit LogoLazorKit
Concepts

Ranks & policies

LazorKit's on-chain permission model — Owner, Admin and Delegate ranks, what each may manage, and the spending policy a Delegate carries.

Ranks & policies

Every authority on a wallet answers two separate questions:

  • its rank — what it may manage: Owner, Admin or Delegate;
  • its policy — what it may spend: an optional list of the same actions sessions use.

They used to be one field. In protocol v1, Execute never read the role, so a key named "Spender" could spend the whole vault. Protocol v2 keeps them apart, and a Delegate must carry a policy. This page describes v2 (devnet today; see Networks & versions).


Ranks

RankValueSDK constantMay addMay removeSpends
Owner0ROLE_OWNEROwner, Admin, DelegateOwner (not the last, not itself), Admin, Delegatewithout limits
Admin1ROLE_ADMINDelegateDelegatewithout limits
Delegate2ROLE_SPENDERnothingnothingonly within its policy

The SDK constant for a Delegate is still called ROLE_SPENDER.

What each instruction allows

InstructionOwnerAdminDelegate
ExecuteYesYesYes, within its policy
CreateSession / RevokeSessionYesYesNo
AddAuthorityany rankDelegate onlyNo
RemoveAuthorityany rank, not the last Owner, not itselfDelegate onlyNo
TransferOwnershipYesNoNo
Authorize (deferred TX1)Yes, passkey onlyYes, passkey onlyNo
ExecuteDeferred (TX2)anyone holding the payload
ReclaimDeferredthe payer that funded it
CloseExpiredSessionanyone, once the session has expired

A Delegate, the only rank that carries a policy, manages nothing: it cannot add or remove authorities, create or revoke sessions, transfer ownership or Authorize a deferred transaction. It spends through Execute, within its policy, and that is all. The program also refuses AddAuthority, CreateSession, TransferOwnership and Authorize to any authority with a policy, since none of them could enforce it.


Several Owners

A wallet may have several Owners. Each device holds its own passkey and passkeys cannot be copied, so multi-device means several authorities, and only if they are all Owners can a surviving device remove a lost one. The wallet account counts its Owners (owner_count): the last Owner cannot be removed, and an authority cannot remove itself.

TransferOwnership still swaps one Owner for another atomically. It is the way to hand ownership to a key that does not exist yet.

Policies

A policy is a list of actions — SolLimit, SolRecurringLimit, SolMaxPerTx, the Token* equivalents, ProgramWhitelist, ProgramBlacklist — run by the same engine as a session's (Session Keys › Actions).

  • Only a Delegate carries one. Adding a Delegate without a policy fails with DelegateRequiresPolicy (3033); a policy on an Owner or Admin fails with PolicyRankMismatch (3035).
  • A policy names what may leave the wallet. From the v2 program release that adds errors 3037 and 3038, nothing it does not name may: SOL leaves only with a Sol* action, rent the vault pays for a new account included, and a token only with a Token* action for its mint. That release is not deployed yet; on the program deployed today a policy bounds only what it names, so a whitelist without a SOL cap leaves SOL uncapped and a SOL cap leaves tokens uncapped. Either way, give every asset the Delegate should spend a limit. See Session Keys › What a policy bounds.
  • A policy must fit in its transaction. At most 16 actions, and when a passkey approves the AddAuthority, within about 244 bytes beside its response (How many actions fit).
  • Policies cannot be changed. Remove the authority and add it again with a new one.

Adding authorities with the SDKs

import { Keypair } from '@solana/web3.js';
import {
  Actions,
  ROLE_ADMIN,
  ROLE_OWNER,
  ROLE_SPENDER,
  ed25519,
  serializeActions,
} from '@lazorkit/sdk-legacy';

const delegateKp = Keypair.generate();

// A Delegate: needs a policy. It names SOL and USDC, so no other token can leave.
await client.addAuthority({
  payer: payer.publicKey,
  walletPda,
  adminSigner: ed25519(ownerKp.publicKey),
  newAuthority: { type: 'ed25519', publicKey: delegateKp.publicKey },
  role: ROLE_SPENDER,
  policy: serializeActions([
    Actions.solLimit(1_000_000_000n),
    Actions.solMaxPerTx(100_000_000n),
    Actions.tokenLimit({ mint: USDC_MINT, remaining: 100_000_000n }),
  ]),
});

// An Admin: no policy.
await client.addAuthority({
  payer: payer.publicKey,
  walletPda,
  adminSigner: ed25519(ownerKp.publicKey),
  newAuthority: { type: 'ed25519', publicKey: Keypair.generate().publicKey },
  role: ROLE_ADMIN,
});

// Another Owner (a second device): explicit on purpose.
await client.addAuthority({
  payer: payer.publicKey,
  walletPda,
  adminSigner: ed25519(ownerKp.publicKey),
  newAuthority: { type: 'secp256r1', credentialIdHash, compressedPubkey, rpId: 'your-app.com' },
  role: ROLE_OWNER,
  allowOwner: true,
});

The React and React Native SDKs take the same policy, and need the rank named: neither has a default role. A missing or unknown one throws before the passkey prompt, with a message that says what each rank may do, and so does ROLE_OWNER on a v2 wallet (neither passes allowOwner).

  • React: addAuthority({ role: ROLE_SPENDER, policy }); since 3.4.0 policy: serializeActions(spendingLimitsToActions(limits)) gives a Delegate the same limits as a session.
  • React Native: addAuthorityEd25519({ newEd25519Pubkey, role: ROLE_SPENDER, policy }, { redirectUrl }).

For a key your app holds, pass ROLE_SPENDER with a policy.

Changed in 3.3.0 / adapter 2.3.0

Breaking: role is required. Before, addAuthority defaulted to ROLE_ADMIN and addAuthorityEd25519 to ROLE_SPENDER.

v1 wallets

v1 has no policies and never checked rank at Execute: any authority on a v1 wallet can spend the whole vault. The SDKs therefore refuse a policy for a v1 wallet, and require unrestricted: true to add any key to one. Moving the wallet to v2 is the way to get bounded keys; see Migrating from v1.


Storage

Each authority has its own PDA. The rank is a u8 in the authority header; there is no in-place rank update — remove and re-add the authority.

AuthorityAccountHeader (48 bytes, v2)
  discriminator: u8   // 0x22 = Authority (v2)
  authority_type: u8  // 0 = Ed25519, 1 = Secp256r1
  role: u8            // rank: 0 = Owner, 1 = Admin, 2 = Delegate
  bump: u8
  version: u8
  _padding: [u8; 3]
  counter: u32        // Secp256r1 odometer
  policy_len: u16     // bytes of policy after the key material; 0 = unbounded
  _padding: [u8; 2]
  wallet: Pubkey      // the wallet this authority belongs to

Enforcement notes

  • Admin cannot self-escalate — adding an Owner or Admin requires an Owner signature.
  • Deferred execution (Authorize) is restricted to passkey Owners and Admins without a policy.
  • CPI reentrancy is blocked: passkey authentication and Execute refuse to run below the top level, and an inner instruction cannot call back into LazorKit.

Full rules and their reasons: docs/Architecture.md › Rank and policy.