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}