Access Control
Overview
The Access Control module provides a comprehensive role-based access control system for Soroban contracts. It enables developers to manage permissions through a hierarchical role system, with a renounceable single overarching admin and customizable role assignments.
Key Concepts
Admin Management
The system features a single top-level admin with privileges to call any function in the AccessControl trait. This admin must be set during contract initialization for the module to function properly. This overarching admin can renounce themselves for decentralization purposes.
Admin transfers are implemented as a two-step process to prevent accidental or malicious takeovers:
- The current admin initiates the transfer by specifying the new admin and an expiration time (
live_until_ledger). - The designated new admin must explicitly accept the transfer to complete it.
 
Until the transfer is accepted, the original admin retains full control and can override or cancel the transfer by initiating a new one or using a live_until_ledger of 0.
Role Hierarchy
The module supports a hierarchical role system where each role can have an "admin role" assigned to it. For example:
- Create roles 
minterandminter_admin - Assign 
minter_adminas the admin role for theminterrole - Accounts with the 
minter_adminrole can grant/revoke theminterrole to other accounts 
This allows for creating complex organizational structures with chains of command and delegated authority.
Setting Up Role Hierarchies
Here's how to establish and use role hierarchies in practice:
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Symbol};
use stellar_access::access_control::{self as access_control, AccessControl};
const MANAGER_ROLE: Symbol = symbol_short!("manager");
const GUARDIAN_ROLE: Symbol = symbol_short!("guardian");
#[contract]
pub struct MyContract;
#[contractimpl]
impl MyContract {
    pub fn __constructor(e: &Env, admin: Address, manager: Address) {
        // Set the contract admin
        access_control::set_admin(e, &admin);
        
        // 1. Set MANAGER_ROLE as the admin role for GUARDIAN_ROLE:
        // accounts with MANAGER_ROLE can manage accounts with GUARDIAN_ROLE
        access_control::set_role_admin_no_auth(e, &admin, &GUARDIAN_ROLE, &MANAGER_ROLE);
        // 2. Admin grants MANAGER_ROLE to the manager account
        access_control::grant_role_no_auth(e, &admin, &manager, &MANAGER_ROLE);
    }
    pub fn manage_guardians(e: &Env, manager: Address, guardian1: Address, guardian2: Address) {
        // Manager must be authorized
        manager.require_auth();
        
        // 3. Now the manager can grant GUARDIAN_ROLE to other accounts
        access_control::grant_role_no_auth(e, &manager, &guardian1, &GUARDIAN_ROLE);
        access_control::grant_role_no_auth(e, &manager, &guardian2, &GUARDIAN_ROLE);
        
        // Manager can also revoke GUARDIAN_ROLE
        access_control::revoke_role_no_auth(e, &manager, &guardian1, &GUARDIAN_ROLE);
    }
}In this example:
- The 
adminsetsMANAGER_ROLEas the admin role forGUARDIAN_ROLEusingset_role_admin() - The 
admingrants theMANAGER_ROLErole to themanageraccount - The 
managercan now grant/revoke theGUARDIAN_ROLErole to other accounts without requiring admin intervention 
Role Enumeration
The system tracks account-role pairs in storage with additional enumeration logic:
- When a role is granted to an account, the pair is stored and added to enumeration storage
 - When a role is revoked, the pair is removed from storage and enumeration
 - If all accounts are removed from a role, the helper storage items become empty or 0
 
Roles exist only through their relationships with accounts, so a role with zero accounts is indistinguishable from a role that never existed.
Procedural Macros
The module includes several procedural macros to simplify authorization checks in your contract functions. These macros are divided into two categories:
Authorization-Enforcing Macros
These macros automatically call require_auth() on the specified account before executing the function:
@only_admin
Restricts access to the contract admin only:
#[only_admin]
pub fn admin_function(e: &Env) {
    // Only the admin can call this function
    // require_auth() is automatically called
}@only_role
Restricts access to accounts with a specific role:
#[only_role(caller, "minter")]
pub fn mint(e: &Env, caller: Address, to: Address, token_id: u32) {
    // Only accounts with the "minter" role can call this
    // require_auth() is automatically called on caller
}@only_any_role
Restricts access to accounts with any of the specified roles:
#[only_any_role(caller, ["minter", "burner"])]
pub fn multi_role_action(e: &Env, caller: Address) {
    // Accounts with either "minter" or "burner" role can call this
    // require_auth() is automatically called on caller
}Role-Checking Macros
These macros check role membership but do not enforce authorization. You must manually call require_auth() if needed:
@has_role
Checks if an account has a specific role:
#[has_role(caller, "minter")]
pub fn conditional_mint(e: &Env, caller: Address, to: Address, token_id: u32) {
    // Checks if caller has "minter" role, but doesn't call require_auth()
    caller.require_auth(); // Must manually authorize if needed
}@has_any_role
Checks if an account has any of the specified roles:
#[has_any_role(caller, ["minter", "burner"])]
pub fn multi_role_check(e: &Env, caller: Address) {
    // Checks if caller has either role, but doesn't call require_auth()
    caller.require_auth(); // Must manually authorize if needed
}Usage Example
Here’s a simple example of using the Access Control module:
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env};
use stellar_access::access_control::{self as access_control, AccessControl};
use stellar_macros::{has_role, only_admin};
#[contract]
pub struct MyContract;
#[contractimpl]
impl MyContract {
    pub fn __constructor(e: &Env, admin: Address) {
        // Set the contract admin
        access_control::set_admin(e, &admin);
        // Create a "minter" role with admin as its admin
        access_control::set_role_admin_no_auth(e, &symbol_short!("minter"), &symbol_short!("admin"));
    }
    #[only_admin]
    pub fn admin_restricted_function(e: &Env) -> Vec<String> {
        vec![&e, String::from_str(e, "seems sus")]
    }
    // we want `require_auth()` provided by the macro, since there is no
    // `require_auth()` in `Base::mint`.
    #[only_role(caller, "minter")]
    pub fn mint(e: &Env, caller: Address, to: Address, token_id: u32) {
        Base::mint(e, &to, token_id)
    }
    // allows either minter or burner role, does not enforce `require_auth` in the macro
    #[has_any_role(caller, ["minter", "burner"])]
    pub fn multi_role_action(e: &Env, caller: Address) -> String {
        caller.require_auth();
        String::from_str(e, "multi_role_action_success")
    }
    // allows either minter or burner role AND enforces `require_auth` in the macro
    #[only_any_role(caller, ["minter", "burner"])]
    pub fn multi_role_auth_action(e: &Env, caller: Address) -> String {
        String::from_str(e, "multi_role_auth_action_success")
    }
}Benefits and Trade-offs
Benefits
- Flexible role-based permission system
 - Hierarchical role management
 - Secure admin transfer process
 - Admin is renounceable
 - Easy integration with procedural macros
 
Trade-offs
- More complex than single-owner models like Ownable