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
u8binary 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
| Role | Value | Binary | Capabilities |
|---|---|---|---|
| Contract Admin | 1 | 0001 | Grant/revoke roles, set max supply, configure escrow |
| Reserve Admin | 2 | 0010 | Mint/burn securities, force transfers |
| Wallet Admin | 4 | 0100 | Freeze/thaw wallets |
| Transfer Admin | 8 | 1000 | Freeze/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
addExtraAccountMetasForExecutehelper 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 tokenargs.name(String): Token name for metadataargs.symbol(String): Token symbol for metadataargs.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 creationauthority(UncheckedAccount): Metadata update authoritymint(InterfaceAccount<Mint\>, init, signer): Token-2022 mint to createaccess_control(Account<AccessControl\>, init): Access control PDAwallet_role(Account<WalletRole\>, init): Initial wallet role for payersystem_program(Program): Solana System Programtoken_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 walletauthority_wallet_role(Account<WalletRole>): Authority's role accountaccess_control(Account<AccessControl>): Access control PDAsecurity_token(InterfaceAccount<Mint>): Token-2022 mintuser_wallet(AccountInfo): Wallet to grant role toauthority(Signer): Must have Contract Admin rolepayer(Signer, mut): Pays for account creation if neededsystem_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 roleAccessControlError::InvalidRole: Invalid role valueAccessControlError::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 walletauthority_wallet_role(Account<WalletRole>): Authority's role accountaccess_control(Account<AccessControl>): Access control PDAsecurity_token(InterfaceAccount<Mint>): Token-2022 mintuser_wallet(AccountInfo): Wallet to revoke role fromauthority(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 roleAccessControlError::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 roleauthority_wallet_role(Account<WalletRole>): Authority's role accountaccess_control(Account<AccessControl>): Access control PDAsecurity_mint(InterfaceAccount<Mint>, mut): Token-2022 mintdestination_account(InterfaceAccount<TokenAccount>, mut): Target token accountdestination_authority(UncheckedAccount): Owner of destination accounttoken_program(Program): SPL Token-2022 Programsecurity_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 roleAccessControlError::MintExceedsMaxTotalSupply: Would exceed max supplyAccessControlError::SecurityAssociatedAccountRequired: SAA not provided for non-escrow destinationAccessControlError::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 roleauthority_wallet_role(Account<WalletRole>): Authority's role accountaccess_control(Account<AccessControl>): Access control PDAsecurity_mint(InterfaceAccount<Mint>, mut): Token-2022 mintsource_account(InterfaceAccount<TokenAccount>, mut): Token account to burn fromsource_authority(UncheckedAccount): Owner of source accounttoken_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 roleAccessControlError::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 roleauthority_wallet_role(Account<WalletRole>): Authority's role accountaccess_control(Account<AccessControl>): Access control PDAsecurity_mint(InterfaceAccount<Mint>): Token-2022 mintsource_account(InterfaceAccount<TokenAccount>, mut): Source token accountsource_authority(UncheckedAccount): Owner of source accountdestination_account(InterfaceAccount<TokenAccount>, mut): Destination token accounttoken_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 roleAccessControlError::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 roleauthority_wallet_role(Account<WalletRole>): Authority's role accountaccess_control(Account<AccessControl>): Access control PDAsecurity_mint(InterfaceAccount<Mint>): Token-2022 mintfreeze_account(InterfaceAccount<TokenAccount>, mut): Token account to freezetoken_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 roleAccessControlError::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 roleauthority_wallet_role(Account<WalletRole>): Authority's role accountaccess_control(Account<AccessControl>): Access control PDAsecurity_mint(InterfaceAccount<Mint>): Token-2022 mintfreeze_account(InterfaceAccount<TokenAccount>, mut): Token account to thawtoken_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 roleauthority_wallet_role(Account<WalletRole>): Authority's role accountaccess_control(Account<AccessControl>, mut): Access control PDAescrow_account(InterfaceAccount<TokenAccount>): Escrow token accountsecurity_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 roleAccessControlError::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 roleauthority_wallet_role(Account<WalletRole>): Authority's role accountaccess_control(Account<AccessControl>, mut): Access control PDAsecurity_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 roleAccessControlError::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
| Error | Code | Description |
|---|---|---|
Unauthorized | 6000 | Caller lacks required role |
InvalidRole | 6001 | Invalid role value provided |
MintExceedsMaxTotalSupply | 6002 | Minting would exceed max total supply |
IncorrectTokenlockAccount | 6003 | Wrong tokenlock account provided |
MismatchedEscrowAccount | 6004 | Escrow account doesn't match expected value |
CantBurnSecuritiesWithinLockup | 6005 | Cannot burn tokens from lockup escrow |
CantForceTransferBetweenLockup | 6006 | Cannot force transfer involving lockup escrow |
NewMaxTotalSupplyMustExceedCurrentTotalSupply | 6007 | New max supply must be greater than current supply |
CannotFreezeLockupEscrowAccount | 6008 | Cannot freeze the lockup escrow account |
ValueUnchanged | 6009 | Provided value is already set |
InvalidSecurityAssociatedAccount | 6010 | Security associated account is invalid |
SecurityAssociatedAccountNotInitialized | 6011 | Security associated account not initialized |
TransferHookNotConfigured | 6012 | Transfer hook not configured on mint |
SecurityAssociatedAccountRequired | 6013 | Security associated account is required |
AlreadyHasRole | 6014 | Wallet already has this role |
CannotRevokeRole | 6015 | Cannot revoke role that wallet doesn't have |
InvalidAccessControl | 6016 | Invalid access control account |
InvalidWalletRoleAccountOwner | 6017 | Invalid wallet role account owner |
Integration Notes
With Transfer Restrictions Program
The Access Control program works closely with the Transfer Restrictions program:
initialize_access_controlrequires the Transfer Restrictions program ID ashook_program_idmint_securitiesrequires a security associated account initialized via Transfer Restrictionsforce_transfer_betweenrequires transfer hook extra accounts from Transfer Restrictions
With Tokenlock Program
- Use
set_lockup_escrow_accountto 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 mintgrant_role- Grant admin rolesrevoke_role- Revoke admin rolesmint_securities- Mint tokensburn_securities- Burn tokensforce_transfer_between- Emergency force transferfreeze_wallet- Freeze token accountthaw_wallet- Unfreeze token accountset_lockup_escrow_account- Configure escrowset_max_total_supply- Update supply cap