stellar_access/access_control/
storage.rs

1use soroban_sdk::{contracttype, panic_with_error, Address, Env, Symbol};
2
3use crate::{
4    access_control::{
5        emit_admin_renounced, emit_admin_transfer_completed, emit_admin_transfer_initiated,
6        emit_role_admin_changed, emit_role_granted, emit_role_revoked, AccessControlError,
7        ROLE_EXTEND_AMOUNT, ROLE_TTL_THRESHOLD,
8    },
9    role_transfer::{accept_transfer, transfer_role},
10};
11
12/// Storage key for enumeration of accounts per role.
13#[contracttype]
14pub struct RoleAccountKey {
15    pub role: Symbol,
16    pub index: u32,
17}
18
19/// Storage keys for the data associated with the access control
20#[contracttype]
21pub enum AccessControlStorageKey {
22    RoleAccounts(RoleAccountKey), // (role, index) -> Address
23    HasRole(Address, Symbol),     // (account, role) -> index
24    RoleAccountsCount(Symbol),    // role -> count
25    RoleAdmin(Symbol),            // role -> the admin role
26    Admin,
27    PendingAdmin,
28}
29
30// ################## QUERY STATE ##################
31
32/// Returns `Some(index)` if the account has the specified role,
33/// where `index` is the position of the account for that role,
34/// and can be used to query [`get_role_member`].
35/// Returns `None` if the account does not have the specified role.
36///
37/// # Arguments
38///
39/// * `e` - Access to Soroban environment.
40/// * `account` - The account to check.
41/// * `role` - The role to check for.
42pub fn has_role(e: &Env, account: &Address, role: &Symbol) -> Option<u32> {
43    let key = AccessControlStorageKey::HasRole(account.clone(), role.clone());
44
45    // extend ttl if `Some(index)`
46    e.storage().persistent().get(&key).inspect(|_| {
47        e.storage().persistent().extend_ttl(&key, ROLE_TTL_THRESHOLD, ROLE_EXTEND_AMOUNT)
48    })
49}
50
51/// Returns the admin account.
52///
53/// # Arguments
54///
55/// * `e` - Access to Soroban environment.
56pub fn get_admin(e: &Env) -> Option<Address> {
57    e.storage().instance().get(&AccessControlStorageKey::Admin)
58}
59
60/// Returns the total number of accounts that have the specified role.
61/// If the role does not exist, it returns 0.
62///
63/// # Arguments
64///
65/// * `e` - Access to Soroban environment.
66/// * `role` - The role to get the count for.
67pub fn get_role_member_count(e: &Env, role: &Symbol) -> u32 {
68    let count_key = AccessControlStorageKey::RoleAccountsCount(role.clone());
69    if let Some(count) = e.storage().persistent().get(&count_key) {
70        e.storage().persistent().extend_ttl(&count_key, ROLE_TTL_THRESHOLD, ROLE_EXTEND_AMOUNT);
71        count
72    } else {
73        0
74    }
75}
76
77/// Returns the account at the specified index for a given role.
78///
79/// # Arguments
80///
81/// * `e` - Access to Soroban environment.
82/// * `role` - The role to query.
83/// * `index` - The index of the account to retrieve.
84///
85/// # Errors
86///
87/// * [`AccessControlError::IndexOutOfBounds`] - If the indexing is out of
88///   bounds.
89pub fn get_role_member(e: &Env, role: &Symbol, index: u32) -> Address {
90    let key = AccessControlStorageKey::RoleAccounts(RoleAccountKey { role: role.clone(), index });
91
92    if let Some(account) = e.storage().persistent().get(&key) {
93        e.storage().persistent().extend_ttl(&key, ROLE_TTL_THRESHOLD, ROLE_EXTEND_AMOUNT);
94        account
95    } else {
96        panic_with_error!(e, AccessControlError::IndexOutOfBounds)
97    }
98}
99
100/// Returns the admin role for a specific role.
101/// If no admin role is explicitly set, returns `None`.
102///
103/// # Arguments
104///
105/// * `e` - Access to Soroban environment.
106/// * `role` - The role to query the admin role for.
107pub fn get_role_admin(e: &Env, role: &Symbol) -> Option<Symbol> {
108    let key = AccessControlStorageKey::RoleAdmin(role.clone());
109    if let Some(admin_role) = e.storage().persistent().get(&key) {
110        e.storage().persistent().extend_ttl(&key, ROLE_TTL_THRESHOLD, ROLE_EXTEND_AMOUNT);
111        Some(admin_role)
112    } else {
113        None
114    }
115}
116
117// ################## CHANGE STATE ##################
118
119/// Sets the overarching admin role.
120///
121///
122/// # Arguments
123///
124/// * `e` - Access to Soroban environment.
125/// * `admin` - The account to grant the admin privilege.
126///
127/// # Errors
128///
129/// * [`AccessControlError::AdminAlreadySet`] - If the admin is already set.
130///
131/// **IMPORTANT**: this function lacks authorization checks.
132/// It is expected to call this function only in the constructor!
133pub fn set_admin(e: &Env, admin: &Address) {
134    // Check if admin is already set
135    if e.storage().instance().has(&AccessControlStorageKey::Admin) {
136        panic_with_error!(e, AccessControlError::AdminAlreadySet);
137    }
138    e.storage().instance().set(&AccessControlStorageKey::Admin, &admin);
139}
140
141/// Grants a role to an account.
142/// Creates the role if it does not exist.
143/// Returns early if the account already has the role.
144///
145/// # Arguments
146///
147/// * `e` - Access to Soroban environment.
148/// * `caller` - The address of the caller, must be the admin or has the
149///   `AdminRole` privileges for this role.
150/// * `account` - The account to grant the role to.
151/// * `role` - The role to grant.
152///
153/// # Errors
154///
155/// * refer to [`ensure_if_admin_or_admin_role`] errors.
156///
157/// # Events
158///
159/// * topics - `["role_granted", role: Symbol, account: Address]`
160/// * data - `[caller: Address]`
161///
162/// # Notes
163///
164/// * Authorization for `caller` is required.
165pub fn grant_role(e: &Env, caller: &Address, account: &Address, role: &Symbol) {
166    caller.require_auth();
167    ensure_if_admin_or_admin_role(e, caller, role);
168    grant_role_no_auth(e, caller, account, role);
169}
170
171/// Low-level function to grant a role to an account without performing
172/// authorization checks.
173/// Creates the role if it does not exist.
174/// Returns early if the account already has the role.
175///
176/// # Arguments
177///
178/// * `e` - Access to Soroban environment.
179/// * `caller` - The address of the caller.
180/// * `account` - The account to grant the role to.
181/// * `role` - The role to grant.
182///
183/// # Events
184///
185/// * topics - `["role_granted", role: Symbol, account: Address]`
186/// * data - `[caller: Address]`
187///
188/// # Security Warning
189///
190/// **IMPORTANT**: This function bypasses authorization checks and should only
191/// be used:
192/// - During contract initialization/construction
193/// - In admin functions that implement their own authorization logic
194///
195/// Using this function in public-facing methods creates significant security
196/// risks as it could allow unauthorized role assignments.
197pub fn grant_role_no_auth(e: &Env, caller: &Address, account: &Address, role: &Symbol) {
198    // Return early if account already has the role
199    if has_role(e, account, role).is_some() {
200        return;
201    }
202    add_to_role_enumeration(e, account, role);
203
204    emit_role_granted(e, role, account, caller);
205}
206
207/// Revokes a role from an account.
208///
209/// # Arguments
210///
211/// * `e` - Access to Soroban environment.
212/// * `caller` - The address of the caller, must be the admin or has the
213///   `AdminRole` privileges for this role.
214/// * `account` - The account to revoke the role from.
215/// * `role` - The role to revoke.
216///
217/// # Errors
218///
219/// * refer to [`ensure_if_admin_or_admin_role`] errors.
220/// * refer to [`revoke_role_no_auth`] errors.
221///
222/// # Events
223///
224/// * topics - `["role_revoked", role: Symbol, account: Address]`
225/// * data - `[caller: Address]`
226///
227/// # Notes
228///
229/// * Authorization for `caller` is required.
230pub fn revoke_role(e: &Env, caller: &Address, account: &Address, role: &Symbol) {
231    caller.require_auth();
232    ensure_if_admin_or_admin_role(e, caller, role);
233    revoke_role_no_auth(e, caller, account, role);
234}
235
236/// Low-level function to revoke a role from an account without performing
237/// authorization checks.
238///
239/// # Arguments
240///
241/// * `e` - Access to Soroban environment.
242/// * `caller` - The address of the caller.
243/// * `account` - The account to revoke the role from.
244/// * `role` - The role to revoke.
245///
246/// # Errors
247///
248/// * [`AccessControlError::RoleNotHeld`] - If the `account` doesn't have the
249///   role.
250/// * refer to [`remove_from_role_enumeration`] errors.
251///
252/// # Events
253///
254/// * topics - `["role_revoked", role: Symbol, account: Address]`
255/// * data - `[caller: Address]`
256///
257/// # Security Warning
258///
259/// **IMPORTANT**: This function bypasses authorization checks and should only
260/// be used:
261/// - During contract initialization/construction
262/// - In admin functions that implement their own authorization logic
263///
264/// Using this function in public-facing methods creates significant security
265/// risks as it could allow unauthorized role revocations.
266pub fn revoke_role_no_auth(e: &Env, caller: &Address, account: &Address, role: &Symbol) {
267    // Check if account has the role
268    if has_role(e, account, role).is_none() {
269        panic_with_error!(e, AccessControlError::RoleNotHeld);
270    }
271
272    remove_from_role_enumeration(e, account, role);
273
274    let key = AccessControlStorageKey::HasRole(account.clone(), role.clone());
275    e.storage().persistent().remove(&key);
276
277    emit_role_revoked(e, role, account, caller);
278}
279
280/// Allows an account to renounce a role assigned to itself.
281/// Users can only renounce roles for their own account.
282///
283/// # Arguments
284///
285/// * `e` - Access to Soroban environment.
286/// * `caller` - The address of the caller, must be the account that has the
287///   role.
288/// * `role` - The role to renounce.
289///
290/// # Errors
291///
292/// * [`AccessControlError::RoleNotHeld`] - If the `caller` doesn't have the
293///   role.
294/// * refer to [`remove_from_role_enumeration`] errors.
295///
296/// # Events
297///
298/// * topics - `["role_revoked", role: Symbol, account: Address]`
299/// * data - `[caller: Address]`
300///
301/// # Notes
302///
303/// * Authorization for `caller` is required.
304pub fn renounce_role(e: &Env, caller: &Address, role: &Symbol) {
305    caller.require_auth();
306
307    // Check if account has the role
308    if has_role(e, caller, role).is_none() {
309        panic_with_error!(e, AccessControlError::RoleNotHeld);
310    }
311
312    remove_from_role_enumeration(e, caller, role);
313
314    let key = AccessControlStorageKey::HasRole(caller.clone(), role.clone());
315    e.storage().persistent().remove(&key);
316
317    emit_role_revoked(e, role, caller, caller);
318}
319
320/// Initiates admin role transfer.
321/// Admin privileges for the current admin are not revoked until the
322/// recipient accepts the transfer.
323/// Overrides the previous pending transfer if there is one.
324///
325/// # Arguments
326///
327/// * `e` - Access to Soroban environment.
328/// * `new_admin` - The account to transfer the admin privileges to.
329/// * `live_until_ledger` - The ledger number at which the pending transfer
330///   expires. If `live_until_ledger` is `0`, the pending transfer is cancelled.
331///   `live_until_ledger` argument is implicitly bounded by the maximum allowed
332///   TTL extension for a temporary storage entry and specifying a higher value
333///   will cause the code to panic.
334///
335/// # Errors
336///
337/// * [`AccessControlError::AdminNotSet`] - If admin account is not set.
338/// * refer to [`transfer_role`] errors.
339///
340/// # Events
341///
342/// * topics - `["admin_transfer_initiated", current_admin: Address]`
343/// * data - `[new_admin: Address, live_until_ledger: u32]`
344///
345/// # Notes
346///
347/// * Authorization for the current admin is required.
348pub fn transfer_admin_role(e: &Env, new_admin: &Address, live_until_ledger: u32) {
349    let admin = enforce_admin_auth(e);
350
351    transfer_role(e, new_admin, &AccessControlStorageKey::PendingAdmin, live_until_ledger);
352
353    emit_admin_transfer_initiated(e, &admin, new_admin, live_until_ledger);
354}
355
356/// Completes the 2-step admin transfer.
357///
358/// # Arguments
359///
360/// * `e` - Access to Soroban environment.
361///
362/// # Errors
363///
364/// * [`AccessControlError::AdminNotSet`] - If admin account is not set.
365/// * refer to [`accept_transfer`] errors.
366///
367/// # Events
368///
369/// * topics - `["admin_transfer_completed", new_admin: Address]`
370/// * data - `[previous_admin: Address]`
371///
372/// # Notes
373///
374/// * Authorization for the pending admin is required.
375pub fn accept_admin_transfer(e: &Env) {
376    let Some(previous_admin) = get_admin(e) else {
377        panic_with_error!(e, AccessControlError::AdminNotSet);
378    };
379
380    let new_admin =
381        accept_transfer(e, &AccessControlStorageKey::Admin, &AccessControlStorageKey::PendingAdmin);
382
383    emit_admin_transfer_completed(e, &previous_admin, &new_admin);
384}
385
386/// Sets `admin_role` as the admin role for `role`.
387/// The admin role for a role controls who can grant and revoke that role.
388///
389/// # Arguments
390///
391/// * `e` - Access to Soroban environment.
392/// * `role` - The role to set the admin for.
393/// * `admin_role` - The role that will be the admin.
394///
395/// # Events
396///
397/// * topics - `["role_admin_changed", role: Symbol]`
398/// * data - `[previous_admin_role: Symbol, new_admin_role: Symbol]`
399///
400/// # Errors
401///
402/// * [`AccessControlError::AdminNotSet`] - If admin account is not set.
403///
404/// # Notes
405///
406/// * Authorization for the current admin is required.
407pub fn set_role_admin(e: &Env, role: &Symbol, admin_role: &Symbol) {
408    let Some(admin) = get_admin(e) else {
409        panic_with_error!(e, AccessControlError::AdminNotSet);
410    };
411    admin.require_auth();
412
413    set_role_admin_no_auth(e, role, admin_role);
414}
415
416/// Allows the current admin to renounce their role, making the contract
417/// permanently admin-less. This is useful for decentralization purposes or when
418/// the admin role is no longer needed. Once the admin is renounced, it cannot
419/// be reinstated.
420///
421/// # Arguments
422///
423/// * `e` - Access to Soroban environment.
424///
425/// # Errors
426///
427/// * [`AccessControlError::AdminNotSet`] - If no admin account is set.
428///
429/// # Events
430///
431/// * topics - `["admin_renounced", admin: Address]`
432/// * data - `[]`
433///
434/// # Notes
435///
436/// * Authorization for the current admin is required.
437pub fn renounce_admin(e: &Env) {
438    let admin = enforce_admin_auth(e);
439
440    e.storage().instance().remove(&AccessControlStorageKey::Admin);
441
442    emit_admin_renounced(e, &admin);
443}
444
445/// Low-level function to set the admin role for a specified role without
446/// performing authorization checks.
447///
448/// # Arguments
449///
450/// * `e` - Access to Soroban environment.
451/// * `role` - The role to set the admin for.
452/// * `admin_role` - The new admin role to set.
453///
454/// # Events
455///
456/// * topics - `["role_admin_changed", role: Symbol]`
457/// * data - `[previous_admin_role: Symbol, new_admin_role: Symbol]`
458///
459/// # Security Warning
460///
461/// **IMPORTANT**: This function bypasses authorization checks and should only
462/// be used:
463/// - During contract initialization/construction
464/// - In admin functions that implement their own authorization logic
465///
466/// Using this function in public-facing methods creates significant security
467/// risks as it could allow unauthorized admin role assignments.
468///
469/// # Circular Admin Warning
470///
471/// **CAUTION**: This function allows the creation of circular admin
472/// relationships between roles. For example, it's possible to assign MINT_ADMIN
473/// as the admin of MINT_ROLE while also making MINT_ROLE the admin of
474/// MINT_ADMIN. Such circular relationships can lead to unintended consequences,
475/// including:
476///
477/// - Race conditions where each role can revoke the other
478/// - Potential security vulnerabilities in role management
479/// - Confusing governance structures that are difficult to reason about
480///
481/// When designing your role hierarchy, carefully consider the relationships
482/// between roles and avoid creating circular dependencies.
483pub fn set_role_admin_no_auth(e: &Env, role: &Symbol, admin_role: &Symbol) {
484    let key = AccessControlStorageKey::RoleAdmin(role.clone());
485
486    // Get previous admin role if exists
487    let previous_admin_role =
488        e.storage().persistent().get::<_, Symbol>(&key).unwrap_or_else(|| Symbol::new(e, ""));
489
490    e.storage().persistent().set(&key, admin_role);
491
492    emit_role_admin_changed(e, role, &previous_admin_role, admin_role);
493}
494
495/// Removes the admin role for a specified role without performing authorization
496/// checks.
497///
498/// # Arguments
499///
500/// * `e` - Access to Soroban environment.
501/// * `role` - The role to remove the admin for.
502///
503/// # Security Warning
504///
505/// **IMPORTANT**: This function bypasses authorization checks and should only
506/// be used:
507/// - In admin functions that implement their own authorization logic
508/// - When cleaning up unused roles
509pub fn remove_role_admin_no_auth(e: &Env, role: &Symbol) {
510    let key = AccessControlStorageKey::RoleAdmin(role.clone());
511
512    // Check if the key exists before attempting to remove
513    if e.storage().persistent().has(&key) {
514        e.storage().persistent().remove(&key);
515    } else {
516        panic_with_error!(e, AccessControlError::AdminRoleNotFound);
517    }
518}
519
520/// Removes the role accounts count for a specified role without performing
521/// authorization checks.
522///
523/// # Arguments
524///
525/// * `e` - Access to Soroban environment.
526/// * `role` - The role to remove the accounts count for.
527///
528/// # Security Warning
529///
530/// **IMPORTANT**: This function bypasses authorization checks and should only
531/// be used:
532/// - In admin functions that implement their own authorization logic
533/// - When cleaning up unused roles with zero members
534pub fn remove_role_accounts_count_no_auth(e: &Env, role: &Symbol) {
535    let count_key = AccessControlStorageKey::RoleAccountsCount(role.clone());
536
537    // Check if the key exists and has a zero count before removing
538    if let Some(count) = e.storage().persistent().get::<_, u32>(&count_key) {
539        if count == 0 {
540            e.storage().persistent().remove(&count_key);
541        } else {
542            panic_with_error!(e, AccessControlError::RoleCountIsNotZero);
543        }
544    } else {
545        panic_with_error!(e, AccessControlError::RoleNotFound);
546    }
547}
548
549// ################## LOW-LEVEL HELPERS ##################
550
551/// Ensures that the caller is either the contract admin or has the admin role
552/// for the specified role.
553///
554/// # Arguments
555///
556/// * `e` - Access to Soroban environment.
557/// * `caller` - The address of the caller to check permissions for.
558/// * `role` - The role to check admin privileges for.
559///
560/// # Errors
561///
562/// * [`AccessControlError::Unauthorized`] - If the caller is neither the
563///   contract admin nor has the admin role.
564pub fn ensure_if_admin_or_admin_role(e: &Env, caller: &Address, role: &Symbol) {
565    // Check if caller is contract admin (if one is set)
566    let is_admin = match get_admin(e) {
567        Some(admin) => caller == &admin,
568        None => false,
569    };
570
571    // Check if caller has admin role for the specified role
572    let is_admin_role = match get_role_admin(e, role) {
573        Some(admin_role) => has_role(e, caller, &admin_role).is_some(),
574        None => false,
575    };
576
577    if !is_admin && !is_admin_role {
578        panic_with_error!(e, AccessControlError::Unauthorized);
579    }
580}
581
582/// Ensures that the caller has the specified role.
583/// This function is used to check if an account has a specific role.
584/// The main purpose of this function is to act as a helper for the
585/// `#[has_role]` macro.
586///
587/// # Arguments
588///
589/// * `e` - Access to Soroban environment.
590/// * `caller` - The address of the caller to check the role for.
591/// * `role` - The role to check for.
592///
593/// # Errors
594///
595/// * [`AccessControlError::Unauthorized`] - If the caller does not have the
596///   specified role.
597pub fn ensure_role(e: &Env, caller: &Address, role: &Symbol) {
598    if has_role(e, caller, role).is_none() {
599        panic_with_error!(e, AccessControlError::Unauthorized);
600    }
601}
602
603/// Retrieves the admin from storage, enforces authorization,
604/// and returns the admin address.
605///
606/// # Arguments
607///
608/// * `e` - Access to Soroban environment.
609///
610/// # Returns
611///
612/// The admin address if authorization is successful.
613///
614/// # Errors
615///
616/// * [`AccessControlError::AdminNotSet`] - If admin account is not set.
617pub fn enforce_admin_auth(e: &Env) -> Address {
618    let Some(admin) = get_admin(e) else {
619        panic_with_error!(e, AccessControlError::AdminNotSet);
620    };
621
622    admin.require_auth();
623    admin
624}
625
626/// Adds an account to role enumeration.
627///
628/// # Arguments
629///
630/// * `e` - Access to Soroban environment.
631/// * `account` - The account to add to the role.
632/// * `role` - The role to add the account to.
633pub fn add_to_role_enumeration(e: &Env, account: &Address, role: &Symbol) {
634    // Get the current count of accounts with this role
635    let count_key = AccessControlStorageKey::RoleAccountsCount(role.clone());
636    let count = e.storage().persistent().get(&count_key).unwrap_or(0);
637
638    // Add the account to the enumeration
639    let new_key =
640        AccessControlStorageKey::RoleAccounts(RoleAccountKey { role: role.clone(), index: count });
641    e.storage().persistent().set(&new_key, account);
642
643    // Store the index for the account in HasRole
644    let has_role_key = AccessControlStorageKey::HasRole(account.clone(), role.clone());
645    e.storage().persistent().set(&has_role_key, &count);
646
647    // Update the count
648    e.storage().persistent().set(&count_key, &(count + 1));
649}
650
651/// Removes an account from role enumeration.
652///
653/// # Arguments
654///
655/// * `e` - Access to Soroban environment.
656/// * `account` - The account to remove from the role.
657/// * `role` - The role to remove the account from.
658///
659/// # Errors
660///
661/// * [`AccessControlError::RoleIsEmpty`] - If the role has no members.
662/// * [`AccessControlError::RoleNotHeld`] - If the `account` doesn't have the
663///   role.
664pub fn remove_from_role_enumeration(e: &Env, account: &Address, role: &Symbol) {
665    // Get the current count of accounts with this role
666    let count_key = AccessControlStorageKey::RoleAccountsCount(role.clone());
667    let count = e.storage().persistent().get(&count_key).unwrap_or(0);
668    if count == 0 {
669        panic_with_error!(e, AccessControlError::RoleIsEmpty);
670    }
671
672    // Get the index of the account to remove
673    let to_be_removed_has_role_key =
674        AccessControlStorageKey::HasRole(account.clone(), role.clone());
675    let to_be_removed_index = e
676        .storage()
677        .persistent()
678        .get::<_, u32>(&to_be_removed_has_role_key)
679        .unwrap_or_else(|| panic_with_error!(e, AccessControlError::RoleNotHeld));
680
681    // Get the index of the last account for that role
682    let last_index = count - 1;
683    let last_key = AccessControlStorageKey::RoleAccounts(RoleAccountKey {
684        role: role.clone(),
685        index: last_index,
686    });
687
688    // Swap the to be removed account with the last account, then delete the last
689    // account
690    if to_be_removed_index != last_index {
691        let last_account = e
692            .storage()
693            .persistent()
694            .get::<_, Address>(&last_key)
695            .expect("we ensured count to be 1 at this point");
696
697        // Swap
698        let to_be_removed_key = AccessControlStorageKey::RoleAccounts(RoleAccountKey {
699            role: role.clone(),
700            index: to_be_removed_index,
701        });
702        e.storage().persistent().set(&to_be_removed_key, &last_account);
703
704        // Update HasRole for the swapped account
705        let last_account_has_role_key =
706            AccessControlStorageKey::HasRole(last_account.clone(), role.clone());
707        e.storage().persistent().set(&last_account_has_role_key, &to_be_removed_index);
708    }
709
710    // Remove the last account
711    e.storage().persistent().remove(&last_key);
712    e.storage().persistent().remove(&to_be_removed_has_role_key);
713
714    // Update the count
715    e.storage().persistent().set(&count_key, &last_index);
716}