Skip to main content

Access Control

Overview

The Access Control program implements a role-based access control system for Solana RWA tokens using SPL Token-2022. It manages permissions for administrative functions and asset operations (minting, burning, freezing, and force transfers).

Architecture

  • Bitmask Role System: Uses u8 binary representation for efficient role management
  • Program Derived Addresses (PDAs): Leverages Solana PDAs for deterministic account generation
  • Token-2022 Integration: Manages Token-2022 mint authority, freeze authority, and extensions
  • Separation of Concerns: Distinct roles for different administrative responsibilities

Key Features

  • Role-based permission system with bitmask operations
  • Token minting and burning with supply cap enforcement
  • Wallet freezing and thawing capabilities
  • Force transfer functionality for emergency situations
  • Integration with Transfer Restrictions program
  • Lockup escrow account management

How It Works

Bitmask Role System

The program uses a u8 binary representation to efficiently store and check roles. Each role is represented by a specific bit position:

#[repr(u8)]
pub enum Roles {
ContractAdmin = 1, // 0001
ReserveAdmin = 2, // 0010
WalletAdmin = 4, // 0100
TransferAdmin = 8, // 1000
All = 15, // 1111
}

Example Combinations:

// Multiple roles can be granted using bitwise OR
WalletAndTransferAdmin = 12; // 1100 (WalletAdmin | TransferAdmin)

// Check if an address has a role using bitwise AND
if (wallet_role.role & Roles::WalletAdmin as u8) > 0 {
// Address has WalletAdmin role
}

Role Permissions

RoleValueBinaryCapabilities
Contract Admin10001Grant/revoke roles, set max supply, configure escrow
Reserve Admin20010Mint/burn securities, force transfers
Wallet Admin40100Freeze/thaw wallets
Transfer Admin81000Freeze/thaw wallets

Access Control Account Structure

pub struct AccessControl {
pub mint: Pubkey, // Associated Token-2022 mint
pub authority: Pubkey, // Metadata update authority
pub max_total_supply: u64, // Maximum supply cap
pub lockup_escrow_account: Option<Pubkey`, // Optional escrow for tokenlock
}

Usage

Initialization

The Access Control program must be initialized before any operations. This creates the Token-2022 mint with necessary extensions and the access control account.

TypeScript Example:

import * as anchor from "@coral-xyz/anchor";
import { TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";

const mintKeypair = anchor.web3.Keypair.generate();
const [accessControlPDA] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("ac"), mintKeypair.publicKey.toBuffer()],
program.programId
);

const [walletRolePDA] = anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("wallet_role"),
mintKeypair.publicKey.toBuffer(),
payer.publicKey.toBuffer(),
],
program.programId
);

await program.methods
.initializeAccessControl({
decimals: 6,
name: "Example RWA Token",
symbol: "ERWA",
uri: "https://example.com/metadata.json",
hookProgramId: transferRestrictionsProgramId,
maxTotalSupply: new anchor.BN(1_000_000_000),
})
.accountsStrict({
payer: payer.publicKey,
authority: authority.publicKey,
mint: mintKeypair.publicKey,
accessControl: accessControlPDA,
walletRole: walletRolePDA,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_2022_PROGRAM_ID,
})
.signers([payer, mintKeypair])
.rpc();

Granting Roles

Contract Admins can grant roles to other addresses using the grant_role instruction.

TypeScript Example:

const [userWalletRolePDA] = anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("wallet_role"),
mint.toBuffer(),
userWallet.publicKey.toBuffer(),
],
program.programId
);

const [authorityWalletRolePDA] = anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("wallet_role"),
mint.toBuffer(),
authority.publicKey.toBuffer(),
],
program.programId
);

// Grant Reserve Admin role (value: 2)
await program.methods
.grantRole(2)
.accountsStrict({
walletRole: userWalletRolePDA,
authorityWalletRole: authorityWalletRolePDA,
accessControl: accessControlPDA,
securityToken: mint,
userWallet: userWallet.publicKey,
authority: authority.publicKey,
payer: payer.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([authority, payer])
.rpc();

Minting Securities

Reserve Admins can mint tokens to any wallet, subject to the max_total_supply constraint.

TypeScript Example:

import { getAssociatedTokenAddressSync } from "@solana/spl-token";

const destinationAccount = getAssociatedTokenAddressSync(
mint,
destinationAuthority.publicKey,
false,
TOKEN_2022_PROGRAM_ID
);

await program.methods
.mintSecurities(new anchor.BN(1000 * 10 ** 6)) // 1000 tokens
.accountsStrict({
authority: reserveAdmin.publicKey,
authorityWalletRole: reserveAdminWalletRolePDA,
accessControl: accessControlPDA,
securityMint: mint,
destinationAccount,
destinationAuthority: destinationAuthority.publicKey,
tokenProgram: TOKEN_2022_PROGRAM_ID,
securityAssociatedAccount: securityAssociatedAccountPDA, // Required unless minting to lockup escrow
})
.signers([reserveAdmin])
.rpc();

Important: Before minting to a regular wallet, ensure the wallet has been initialized in the Transfer Restrictions program with a holder account, holder group, and security associated account.

Burning Securities

Reserve Admins can burn tokens from any wallet.

TypeScript Example:

await program.methods
.burnSecurities(new anchor.BN(500 * 10 ** 6)) // Burn 500 tokens
.accountsStrict({
authority: reserveAdmin.publicKey,
authorityWalletRole: reserveAdminWalletRolePDA,
accessControl: accessControlPDA,
securityMint: mint,
sourceAccount: sourceTokenAccount,
sourceAuthority: sourceAuthority.publicKey,
tokenProgram: TOKEN_2022_PROGRAM_ID,
})
.signers([reserveAdmin])
.rpc();

Note: Cannot burn tokens held in a tokenlock escrow account. The timelock must be canceled first.

Freezing and Thawing Wallets

Wallet Admins or Transfer Admins can freeze or thaw individual wallet accounts to prevent transfers.

Freeze Wallet:

await program.methods
.freezeWallet()
.accountsStrict({
authority: walletAdmin.publicKey,
authorityWalletRole: walletAdminWalletRolePDA,
accessControl: accessControlPDA,
securityMint: mint,
freezeAccount: targetTokenAccount,
tokenProgram: TOKEN_2022_PROGRAM_ID,
})
.signers([walletAdmin])
.rpc();

Thaw Wallet:

await program.methods
.thawWallet()
.accountsStrict({
authority: walletAdmin.publicKey,
authorityWalletRole: walletAdminWalletRolePDA,
accessControl: accessControlPDA,
securityMint: mint,
freezeAccount: targetTokenAccount,
tokenProgram: TOKEN_2022_PROGRAM_ID,
})
.signers([walletAdmin])
.rpc();

Note: Cannot freeze the lockup escrow account.

Force Transfer Between Wallets

Reserve Admins can force transfer tokens between wallets in emergency situations (e.g., lost keys, legal recovery).

TypeScript Example:

await program.methods
.forceTransferBetween(new anchor.BN(100 * 10 ** 6))
.accountsStrict({
authority: reserveAdmin.publicKey,
authorityWalletRole: reserveAdminWalletRolePDA,
accessControl: accessControlPDA,
securityMint: mint,
sourceAccount: fromTokenAccount,
sourceAuthority: fromAuthority.publicKey,
destinationAccount: toTokenAccount,
tokenProgram: TOKEN_2022_PROGRAM_ID,
})
.remainingAccounts([
/* Transfer hook extra accounts */
])
.signers([reserveAdmin])
.rpc();

Important:

  • Cannot force transfer from/to lockup escrow accounts
  • Transfer hook accounts must be added as remaining accounts
  • Use addExtraAccountMetasForExecute helper to add required accounts

Setting Lockup Escrow Account

Contract Admins can set the lockup escrow account address to enable integration with the Tokenlock program.

TypeScript Example:

await program.methods
.setLockupEscrowAccount()
.accountsStrict({
authority: contractAdmin.publicKey,
authorityWalletRole: contractAdminWalletRolePDA,
accessControl: accessControlPDA,
escrowAccount: escrowTokenAccount,
securityToken: mint,
})
.signers([contractAdmin])
.rpc();

Updating Max Total Supply

Reserve Admins can increase (but not decrease) the maximum total supply.

TypeScript Example:

await program.methods
.setMaxTotalSupply(new anchor.BN(2_000_000_000)) // New max supply
.accountsStrict({
authority: reserveAdmin.publicKey,
authorityWalletRole: reserveAdminWalletRolePDA,
accessControl: accessControlPDA,
securityToken: mint,
})
.signers([reserveAdmin])
.rpc();

Note: New max supply must be greater than the current total supply.

Revoking Roles

Contract Admins can revoke roles from addresses.

TypeScript Example:

await program.methods
.revokeRole(2) // Revoke Reserve Admin role
.accountsStrict({
walletRole: userWalletRolePDA,
authorityWalletRole: contractAdminWalletRolePDA,
accessControl: accessControlPDA,
securityToken: mint,
userWallet: userWallet.publicKey,
authority: contractAdmin.publicKey,
})
.signers([contractAdmin])
.rpc();

API Reference

Instructions

initialize_access_control(ctx: Context\<InitializeAccessControl\>, args: InitializeAccessControlArgs) -> Result<()>

Initializes the Access Control program with a new Token-2022 mint and access control account.

Parameters:

  • args.decimals (u8): Number of decimal places for the token
  • args.name (String): Token name for metadata
  • args.symbol (String): Token symbol for metadata
  • args.uri (String): URI for token metadata (e.g., JSON file)
  • args.hook_program_id (Pubkey): Transfer hook program ID (Transfer Restrictions program)
  • args.max_total_supply (u64): Maximum number of tokens that can be minted

Accounts:

  • payer (Signer, mut): Pays for account creation
  • authority (UncheckedAccount): Metadata update authority
  • mint (InterfaceAccount<Mint\>, init, signer): Token-2022 mint to create
  • access_control (Account<AccessControl\>, init): Access control PDA
  • wallet_role (Account<WalletRole\>, init): Initial wallet role for payer
  • system_program (Program): Solana System Program
  • token_program (Program): SPL Token-2022 Program

Requirements:

  • Mint must be a new keypair signed by the transaction
  • Payer receives Contract Admin role (value: 1) automatically
  • Access Control PDA seeds: ["ac", mint.key()]
  • Wallet Role PDA seeds: ["wallet_role", mint.key(), payer.key()]

Effects:

  • Creates Token-2022 mint with extensions:
    • Transfer Hook
    • Group Member Pointer
    • Group Pointer
    • Metadata Pointer
    • Permanent Delegate
  • Initializes token metadata
  • Grants payer Contract Admin role

grant_role(ctx: Context\<GrantRole\>, role: u8) -> Result<()>

Grants a role to a wallet address.

Parameters:

  • role (u8): Role bitmask to grant (1=Contract, 2=Reserve, 4=Wallet, 8=Transfer)

Accounts:

  • wallet_role (Account<WalletRole>, init_if_needed, mut): Role account for user wallet
  • authority_wallet_role (Account<WalletRole>): Authority's role account
  • access_control (Account<AccessControl>): Access control PDA
  • security_token (InterfaceAccount<Mint>): Token-2022 mint
  • user_wallet (AccountInfo): Wallet to grant role to
  • authority (Signer): Must have Contract Admin role
  • payer (Signer, mut): Pays for account creation if needed
  • system_program (Program): Solana System Program

Requirements:

  • Caller must have Contract Admin role (1)
  • Role must be valid (1, 2, 4, or 8)
  • User wallet cannot already have the exact role being granted

Errors:

  • AccessControlError::Unauthorized: Caller lacks Contract Admin role
  • AccessControlError::InvalidRole: Invalid role value
  • AccessControlError::AlreadyHasRole: User already has this role

revoke_role(ctx: Context\<RevokeRole\>, role: u8) -> Result<()>

Revokes a role from a wallet address.

Parameters:

  • role (u8): Role bitmask to revoke

Accounts:

  • wallet_role (Account<WalletRole>, mut): Role account for user wallet
  • authority_wallet_role (Account<WalletRole>): Authority's role account
  • access_control (Account<AccessControl>): Access control PDA
  • security_token (InterfaceAccount<Mint>): Token-2022 mint
  • user_wallet (AccountInfo): Wallet to revoke role from
  • authority (Signer): Must have Contract Admin role

Requirements:

  • Caller must have Contract Admin role (1)
  • User wallet must currently have the role being revoked

Errors:

  • AccessControlError::Unauthorized: Caller lacks Contract Admin role
  • AccessControlError::CannotRevokeRole: User doesn't have the role

mint_securities(ctx: Context\<MintSecurities\>, amount: u64) -> Result<()>

Mints new tokens to a destination account.

Parameters:

  • amount (u64): Amount of tokens to mint (in base units)

Accounts:

  • authority (Signer): Must have Reserve Admin role
  • authority_wallet_role (Account<WalletRole>): Authority's role account
  • access_control (Account<AccessControl>): Access control PDA
  • security_mint (InterfaceAccount<Mint>, mut): Token-2022 mint
  • destination_account (InterfaceAccount<TokenAccount>, mut): Target token account
  • destination_authority (UncheckedAccount): Owner of destination account
  • token_program (Program): SPL Token-2022 Program
  • security_associated_account (Option<UncheckedAccount>): Security associated account (required unless minting to lockup escrow)

Requirements:

  • Caller must have Reserve Admin role (2)
  • Total supply after mint must not exceed max_total_supply
  • Destination must have security associated account initialized (unless it's the lockup escrow account)

Errors:

  • AccessControlError::Unauthorized: Caller lacks Reserve Admin role
  • AccessControlError::MintExceedsMaxTotalSupply: Would exceed max supply
  • AccessControlError::SecurityAssociatedAccountRequired: SAA not provided for non-escrow destination
  • AccessControlError::SecurityAssociatedAccountNotInitialized: SAA not properly initialized

burn_securities(ctx: Context\<BurnSecurities\>, amount: u64) -> Result<()>

Burns tokens from a source account.

Parameters:

  • amount (u64): Amount of tokens to burn (in base units)

Accounts:

  • authority (Signer): Must have Reserve Admin role
  • authority_wallet_role (Account<WalletRole>): Authority's role account
  • access_control (Account<AccessControl>): Access control PDA
  • security_mint (InterfaceAccount<Mint>, mut): Token-2022 mint
  • source_account (InterfaceAccount<TokenAccount>, mut): Token account to burn from
  • source_authority (UncheckedAccount): Owner of source account
  • token_program (Program): SPL Token-2022 Program

Requirements:

  • Caller must have Reserve Admin role (2)
  • Source account must have sufficient balance
  • Source account cannot be a lockup escrow account (cancel timelock first)

Errors:

  • AccessControlError::Unauthorized: Caller lacks Reserve Admin role
  • AccessControlError::CantBurnSecuritiesWithinLockup: Attempting to burn from escrow

force_transfer_between(ctx: Context\<ForceTransferBetween\>, amount: u64) -> Result<()>

Forcibly transfers tokens between accounts (emergency use only).

Parameters:

  • amount (u64): Amount of tokens to transfer (in base units)

Accounts:

  • authority (Signer): Must have Reserve Admin role
  • authority_wallet_role (Account<WalletRole>): Authority's role account
  • access_control (Account<AccessControl>): Access control PDA
  • security_mint (InterfaceAccount<Mint>): Token-2022 mint
  • source_account (InterfaceAccount<TokenAccount>, mut): Source token account
  • source_authority (UncheckedAccount): Owner of source account
  • destination_account (InterfaceAccount<TokenAccount>, mut): Destination token account
  • token_program (Program): SPL Token-2022 Program

Remaining Accounts:

Must include transfer hook extra account metas (use addExtraAccountMetasForExecute helper).

Requirements:

  • Caller must have Reserve Admin role (2)
  • Neither source nor destination can be lockup escrow accounts
  • Must provide transfer hook extra accounts as remaining accounts

Errors:

  • AccessControlError::Unauthorized: Caller lacks Reserve Admin role
  • AccessControlError::CantForceTransferBetweenLockup: Source or destination is escrow account

Example with Extra Accounts:

import { addExtraAccountMetasForExecute } from "@solana-program/transfer-hook";

const instruction = await program.methods
.forceTransferBetween(amount)
.accountsStrict({
/* ... accounts ... */
})
.instruction();

await addExtraAccountMetasForExecute(
connection,
instruction,
transferHookProgramId,
sourceAccount,
mint,
destinationAccount,
sourceAuthority.publicKey,
amount,
commitment
);

await program.provider.sendAndConfirm(new Transaction().add(instruction), [
reserveAdmin,
]);

freeze_wallet(ctx: Context\<FreezeWallet\>) -> Result<()>

Freezes a wallet's token account, preventing all transfers.

Accounts:

  • authority (Signer): Must have Wallet Admin or Transfer Admin role
  • authority_wallet_role (Account<WalletRole>): Authority's role account
  • access_control (Account<AccessControl>): Access control PDA
  • security_mint (InterfaceAccount<Mint>): Token-2022 mint
  • freeze_account (InterfaceAccount<TokenAccount>, mut): Token account to freeze
  • token_program (Program): SPL Token-2022 Program

Requirements:

  • Caller must have Wallet Admin (4) or Transfer Admin (8) role
  • Cannot freeze the lockup escrow account

Errors:

  • AccessControlError::Unauthorized: Caller lacks required role
  • AccessControlError::CannotFreezeLockupEscrowAccount: Attempting to freeze escrow

thaw_wallet(ctx: Context\<ThawWallet\>) -> Result<()>

Unfreezes a wallet's token account, allowing transfers again.

Accounts:

  • authority (Signer): Must have Wallet Admin or Transfer Admin role
  • authority_wallet_role (Account<WalletRole>): Authority's role account
  • access_control (Account<AccessControl>): Access control PDA
  • security_mint (InterfaceAccount<Mint>): Token-2022 mint
  • freeze_account (InterfaceAccount<TokenAccount>, mut): Token account to thaw
  • token_program (Program): SPL Token-2022 Program

Requirements:

  • Caller must have Wallet Admin (4) or Transfer Admin (8) role

Errors:

  • AccessControlError::Unauthorized: Caller lacks required role

set_lockup_escrow_account(ctx: Context\<SetLockupEscrowAccount\>) -> Result<()>

Sets the lockup escrow account address for tokenlock integration.

Accounts:

  • authority (Signer): Must have Contract Admin role
  • authority_wallet_role (Account<WalletRole>): Authority's role account
  • access_control (Account<AccessControl>, mut): Access control PDA
  • escrow_account (InterfaceAccount<TokenAccount>): Escrow token account
  • security_token (InterfaceAccount<Mint>): Token-2022 mint

Requirements:

  • Caller must have Contract Admin role (1)
  • Can only be set once

Errors:

  • AccessControlError::Unauthorized: Caller lacks Contract Admin role
  • AccessControlError::ValueUnchanged: Escrow account already set to this value

set_max_total_supply(ctx: Context\<SetMaxTotalSupply\>, max_total_supply: u64) -> Result<()>

Updates the maximum total supply of tokens.

Parameters:

  • max_total_supply (u64): New maximum supply (must be greater than current supply)

Accounts:

  • authority (Signer): Must have Reserve Admin role
  • authority_wallet_role (Account<WalletRole>): Authority's role account
  • access_control (Account<AccessControl>, mut): Access control PDA
  • security_token (InterfaceAccount<Mint>): Token-2022 mint

Requirements:

  • Caller must have Reserve Admin role (2)
  • New max supply must exceed current total supply

Errors:

  • AccessControlError::Unauthorized: Caller lacks Reserve Admin role
  • AccessControlError::NewMaxTotalSupplyMustExceedCurrentTotalSupply: New value too low

Account Structures

AccessControl

#[account]
pub struct AccessControl {
pub mint: Pubkey, // Associated Token-2022 mint
pub authority: Pubkey, // Metadata update authority
pub max_total_supply: u64, // Maximum token supply
pub lockup_escrow_account: Option<Pubkey`, // Optional escrow for tokenlock program
}

PDA Seeds: ["ac", mint.key()]

WalletRole

#[account]
pub struct WalletRole {
pub role: u8, // Bitmask representing granted roles
}

PDA Seeds: ["wallet_role", mint.key(), wallet.key()]


Errors

AccessControlError

ErrorCodeDescription
Unauthorized6000Caller lacks required role
InvalidRole6001Invalid role value provided
MintExceedsMaxTotalSupply6002Minting would exceed max total supply
IncorrectTokenlockAccount6003Wrong tokenlock account provided
MismatchedEscrowAccount6004Escrow account doesn't match expected value
CantBurnSecuritiesWithinLockup6005Cannot burn tokens from lockup escrow
CantForceTransferBetweenLockup6006Cannot force transfer involving lockup escrow
NewMaxTotalSupplyMustExceedCurrentTotalSupply6007New max supply must be greater than current supply
CannotFreezeLockupEscrowAccount6008Cannot freeze the lockup escrow account
ValueUnchanged6009Provided value is already set
InvalidSecurityAssociatedAccount6010Security associated account is invalid
SecurityAssociatedAccountNotInitialized6011Security associated account not initialized
TransferHookNotConfigured6012Transfer hook not configured on mint
SecurityAssociatedAccountRequired6013Security associated account is required
AlreadyHasRole6014Wallet already has this role
CannotRevokeRole6015Cannot revoke role that wallet doesn't have
InvalidAccessControl6016Invalid access control account
InvalidWalletRoleAccountOwner6017Invalid wallet role account owner

Integration Notes

With Transfer Restrictions Program

The Access Control program works closely with the Transfer Restrictions program:

  • initialize_access_control requires the Transfer Restrictions program ID as hook_program_id
  • mint_securities requires a security associated account initialized via Transfer Restrictions
  • force_transfer_between requires transfer hook extra accounts from Transfer Restrictions

With Tokenlock Program

  • Use set_lockup_escrow_account to register the tokenlock escrow account
  • Minting to the escrow account bypasses security associated account requirements
  • Burning and force transfers are blocked for escrow accounts

Multi-Signature Recommendations

For production deployments, consider using multi-signature wallets (e.g., Squads Protocol) for:

  • Contract Admin: Control over roles and system configuration
  • Reserve Admin: Control over token supply and emergency transfers

Security Considerations

Emergency Powers

Reserve Admins have significant powers that could be abused:

  • Can mint up to max_total_supply
  • Can burn from any address
  • Can force transfer between any addresses
  • Can update max supply

Mitigation: Use multi-signature wallets for Reserve Admin role and implement off-chain governance processes.

Role Management

  • Contract Admins can grant themselves additional roles
  • Roles can be granted to program-derived addresses for automation

Best Practice: Separate role responsibilities across different entities with clear governance.

Freeze Functionality

  • Freezing is reversible and should only be used for compliance or security incidents
  • Cannot freeze the lockup escrow account to prevent interference with vesting

Common Patterns

Initial Deployment

Role Separation


Program ID

Mainnet/Devnet: 4X79YRjz9KNMhdjdxXg2ZNTS3YnMGYdwJkBHnezMJwr3

IDL

The Interface Definition Language (IDL) file contains the complete program interface including all instructions, accounts, types, and errors.

Download: access_control.json

Usage with Anchor:

import { Program, AnchorProvider } from "@coral-xyz/anchor";
import idl from "./access_control.json";

const programId = new PublicKey("4X79YRjz9KNMhdjdxXg2ZNTS3YnMGYdwJkBHnezMJwr3");
const program = new Program(idl, programId, provider);

Key Instructions in IDL:

  • initialize_access_control - Initialize with Token-2022 mint
  • grant_role - Grant admin roles
  • revoke_role - Revoke admin roles
  • mint_securities - Mint tokens
  • burn_securities - Burn tokens
  • force_transfer_between - Emergency force transfer
  • freeze_wallet - Freeze token account
  • thaw_wallet - Unfreeze token account
  • set_lockup_escrow_account - Configure escrow
  • set_max_total_supply - Update supply cap