Skip to content

Built-by-Sign/access-control-solana

Repository files navigation

Solana Access Control & Permissions Example

A production-ready Anchor program demonstrating hierarchical access control patterns, two-step ownership transfers, and secure permission management on Solana.

Overview

This program showcases enterprise-grade access control mechanisms without exposing business logic:

  • Two-Step Ownership Transfers: Prevents accidental transfers to wrong addresses
  • Hierarchical Permissions: Program admin and vault owner separation of concerns
  • Granular Error Handling: Specific errors for precise debugging
  • Event Monitoring: Timestamped events for all access control changes
  • Lazy Initialization: Efficient PDA and ATA creation patterns

Architecture

State Accounts

Config (Global)

pub struct Config {
    pub admin: Pubkey,       // Current program administrator
    pub new_admin: Pubkey,   // Pending admin (two-step transfer)
    pub bump: u8,
}
  • PDA Seeds: [b"config"]
  • Purpose: Global program administration
  • Access: Admin-only operations

Vault (Per-Project)

pub struct Vault {
    pub owner: Pubkey,              // Current vault owner
    pub pending_owner: Option<Pubkey>, // Pending owner (two-step transfer)
    pub vault_token: Pubkey,        // Associated token mint
    pub vault_id: String,           // Human-readable identifier (max 20 chars)
    pub bump: u8,
}
  • PDA Seeds: [b"vault", vault_id.as_bytes()]
  • Purpose: Project-level token vault with ownership controls
  • Access: Owner-only withdrawals, anyone can deposit

Access Control Model

┌─────────────────────────────────────────┐
│         Program Admin (Global)          │
│  - Transfer admin role                  │
│  - Global configuration                 │
└─────────────────────────────────────────┘
                  │
                  │ Cannot access individual vaults
                  ▼
┌─────────────────────────────────────────┐
│       Vault Owner (Per-Project)         │
│  - Withdraw deposits                    │
│  - Transfer ownership                   │
│  - Renounce ownership                   │
└─────────────────────────────────────────┘
                  │
                  │ Controls only their vault
                  ▼
┌─────────────────────────────────────────┐
│        Vault Token Account (PDA)        │
│  - Stores deposited tokens              │
│  - Authority: Vault PDA                 │
└─────────────────────────────────────────┘

Instructions

Program Lifecycle

deploy(admin: Pubkey)

Initialize the program with a program admin.

  • Access: Anyone (one-time initialization)
  • Effect: Creates Config PDA
  • Event: ProgramDeployed(admin, timestamp)

Vault Management

initialize(vault_id: String, vault_token: Pubkey)

Create a new vault with the caller as owner.

  • Access: Anyone
  • Validations: vault_id.len() <= 20
  • Effect: Creates Vault PDA, caller becomes owner
  • Event: VaultInitialized(vault_id, owner, vault_token, timestamp)

Two-Step Ownership Transfer

transfer_ownership(vault_id: String, new_owner: Pubkey)

Initiate ownership transfer (step 1 of 2).

  • Access: Current vault owner only
  • Effect: Sets pending_owner field
  • Event: OwnershipTransferred(vault_id, previous_owner, new_owner, timestamp)

receive_ownership(vault_id: String)

Accept ownership transfer (step 2 of 2).

  • Access: Pending owner only
  • Validations: Caller must match pending_owner
  • Effect: Updates owner, clears pending_owner
  • Error: InvalidPendingOwner if caller doesn't match

renounce_ownership(vault_id: String)

Permanently renounce ownership (irreversible).

  • Access: Current vault owner only
  • Effect: Sets owner to Pubkey::default()
  • Warning: Irreversible - vault becomes ownerless
  • Event: OwnershipRenounced(vault_id, previous_owner, timestamp)

Two-Step Admin Transfer

transfer_program_admin(new_admin: Pubkey)

Initiate program admin transfer (step 1 of 2).

  • Access: Current program admin only
  • Effect: Sets new_admin field
  • Event: ProgramAdminTransferred(previous_admin, new_admin, timestamp)

receive_program_admin()

Accept program admin transfer (step 2 of 2).

  • Access: Pending admin only
  • Validations: Caller must match new_admin
  • Effect: Updates admin, clears new_admin
  • Error: InvalidPendingAdmin if caller doesn't match

Token Operations

deposit(vault_id: String, amount: u64)

Deposit tokens into the vault.

  • Access: Anyone
  • Effect: Transfers tokens from caller to vault ATA
  • Features: Lazy ATA initialization with init_if_needed
  • Event: TokensDeposited(vault_id, depositor, amount, token_mint, timestamp)

withdraw_deposit(vault_id: String, amount: u64)

Withdraw tokens from the vault.

  • Access: Vault owner only
  • Validations: Sufficient balance check
  • Effect: Transfers tokens from vault ATA to caller
  • Error: InsufficientBalance if amount exceeds vault balance
  • Event: TokensWithdrawn(vault_id, recipient, amount, token_mint, timestamp)

Error Handling

pub enum AccessControlError {
    NotOwner,              // Operation requires vault ownership
    NotProgramAdmin,       // Operation requires program admin
    InvalidPendingOwner,   // Caller is not the pending owner
    InvalidPendingAdmin,   // Caller is not the pending admin
    VaultIdTooLong,        // Vault ID exceeds 20 characters
    InsufficientBalance,   // Vault balance too low for withdrawal
}

PDA Derivation Patterns

Config Account

seeds = [b"config"]
// One per program, stores global admin

Vault Account

seeds = [b"vault", vault_id.as_bytes()]
// One per project, isolated by vault_id

Vault Token Account (ATA)

// Standard Associated Token Account
mint = vault.vault_token
authority = vault (PDA)

Signer Seeds for CPI

let vault_id_bytes = vault_id.as_bytes();
let bump = &[vault.bump];
let signer_seeds: &[&[&[u8]]] = &[&[
    b"vault",
    vault_id_bytes,
    bump
]];

Security Patterns

1. Two-Step Transfer Pattern

Prevents accidental or malicious transfers:

Owner A                    Owner B (pending)
   │                              │
   ├──► transfer_ownership()      │
   │    (sets pending_owner=B)    │
   │                              │
   │                              ├──► receive_ownership()
   │                              │    (validates caller=B, updates owner)
   │                              │
   └──────────────────────────────┴───► Ownership transferred safely

2. Explicit Permission Checks

All sensitive operations use explicit require! statements:

require!(
    ctx.accounts.owner.key() == vault.owner,
    AccessControlError::NotOwner
);

3. PDA Authority Pattern

Vault PDA is the authority for token accounts:

  • User keys never hold authority
  • Program controls transfers via PDA signing
  • Prevents unauthorized token movements

4. Lazy Initialization

ATAs created only when first needed:

#[account(
    init_if_needed,  // Creates account only if missing
    payer = authority,
    associated_token::mint = token_mint,
    associated_token::authority = vault,
)]

5. Hierarchical Isolation

Program Admin ──(can't access)──> Individual Vaults
       │
       └───> Global Configuration Only

Events

All events include timestamp: i64 for audit trails:

ProgramDeployed { admin, timestamp }
VaultInitialized { vault_id, owner, vault_token, timestamp }
OwnershipTransferred { vault_id, previous_owner, new_owner, timestamp }
OwnershipRenounced { vault_id, previous_owner, timestamp }
ProgramAdminTransferred { previous_admin, new_admin, timestamp }
TokensDeposited { vault_id, depositor, amount, token_mint, timestamp }
TokensWithdrawn { vault_id, recipient, amount, token_mint, timestamp }

Use Cases

This pattern is suitable for:

Multi-Tenant SaaS Platforms

  • Each customer gets isolated vault
  • Platform admin can't access customer vaults
  • Customers fully control their assets

DAO Treasury Management

  • DAO tools managing multiple organization vaults
  • Each DAO has independent ownership
  • Safe ownership transfers between signers

Escrow Systems

  • Project-based token escrows
  • Time-based or condition-based releases
  • Secure ownership handoffs

Token Distribution Platforms

  • Isolated allocation per project
  • Withdrawal controls by project owner
  • Audit trail via events

Key Takeaways

  1. Two-Step Transfers: Prevents loss of control due to typos or malicious actors
  2. Separation of Concerns: Admin and owner roles are distinct with clear boundaries
  3. Event-Driven: All access control changes emit events for monitoring
  4. Granular Errors: Specific errors aid debugging and user experience
  5. PDA-Based Authority: Secure token control without exposing private keys
  6. Lazy Initialization: Gas-efficient account creation patterns

Building

anchor build

Testing

anchor test

License

MIT

About

An Anchor program demonstrating hierarchical access control patterns, two-step ownership transfers, and secure permission management on Solana.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors