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();