@layerzerolabs/protocol-stellar-v2 0.2.35 → 0.2.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/.turbo/turbo-build.log +274 -268
  2. package/.turbo/turbo-lint.log +216 -213
  3. package/.turbo/turbo-test.log +1735 -1994
  4. package/contracts/common-macros/src/auth.rs +5 -5
  5. package/contracts/common-macros/src/lib.rs +69 -0
  6. package/contracts/common-macros/src/rbac.rs +90 -0
  7. package/contracts/common-macros/src/tests/lz_contract.rs +5 -7
  8. package/contracts/common-macros/src/tests/mod.rs +1 -0
  9. package/contracts/common-macros/src/tests/rbac.rs +420 -0
  10. package/contracts/common-macros/src/tests/snapshots/common_macros__tests__auth__snapshot_generated_multisig_code.snap +4 -4
  11. package/contracts/common-macros/src/tests/snapshots/common_macros__tests__auth__snapshot_generated_ownable_code.snap +5 -12
  12. package/contracts/common-macros/src/tests/snapshots/common_macros__tests__rbac__snapshot_preserve_function_signature.snap +17 -0
  13. package/contracts/common-macros/src/tests/storage/parse_name.rs +0 -1
  14. package/contracts/macro-integration-tests/tests/runtime/lz_contract/wrapper_default.rs +1 -1
  15. package/contracts/macro-integration-tests/tests/runtime/lz_contract/wrapper_multisig.rs +1 -1
  16. package/contracts/macro-integration-tests/tests/runtime/lz_contract/wrapper_multisig_upgradeable.rs +1 -1
  17. package/contracts/macro-integration-tests/tests/runtime/multisig/self_auth.rs +1 -1
  18. package/contracts/macro-integration-tests/tests/runtime/ownable/initialization.rs +8 -5
  19. package/contracts/macro-integration-tests/tests/runtime/ownable/ownership_transfer.rs +2 -2
  20. package/contracts/macro-integration-tests/tests/runtime/rbac/guard_behavior.rs +91 -0
  21. package/contracts/macro-integration-tests/tests/runtime/rbac/mod.rs +30 -0
  22. package/contracts/macro-integration-tests/tests/runtime/ttl_configurable/configuration.rs +2 -2
  23. package/contracts/macro-integration-tests/tests/runtime/upgradeable/migrate_guard_and_state.rs +4 -4
  24. package/contracts/macro-integration-tests/tests/ui/lz_contract/pass/basic.rs +1 -1
  25. package/contracts/macro-integration-tests/tests/ui/ownable/pass/basic.rs +1 -1
  26. package/contracts/macro-integration-tests/tests/ui/rbac/fail/missing_env.rs +18 -0
  27. package/contracts/macro-integration-tests/tests/ui/rbac/fail/missing_env.stderr +16 -0
  28. package/contracts/macro-integration-tests/tests/ui/rbac/fail/param_not_address.rs +18 -0
  29. package/contracts/macro-integration-tests/tests/ui/rbac/fail/param_not_address.stderr +24 -0
  30. package/contracts/macro-integration-tests/tests/ui/rbac/fail/param_not_found.rs +18 -0
  31. package/contracts/macro-integration-tests/tests/ui/rbac/fail/param_not_found.stderr +24 -0
  32. package/contracts/macro-integration-tests/tests/ui/rbac/pass/basic.rs +71 -0
  33. package/contracts/macro-integration-tests/tests/ui_rbac.rs +12 -0
  34. package/contracts/oapps/oft/src/interfaces/mintable.rs +2 -2
  35. package/contracts/oapps/oft/src/tests/extensions/oft_fee.rs +2 -2
  36. package/contracts/oapps/oft/src/tests/extensions/pausable.rs +2 -2
  37. package/contracts/oapps/oft/src/tests/extensions/rate_limiter.rs +2 -2
  38. package/contracts/oapps/sac-manager/Cargo.toml +0 -1
  39. package/contracts/oapps/sac-manager/src/interfaces/mod.rs +3 -0
  40. package/contracts/oapps/sac-manager/src/interfaces/sac_admin_wrapper.rs +49 -0
  41. package/contracts/oapps/sac-manager/src/lib.rs +3 -3
  42. package/contracts/oapps/sac-manager/src/sac_manager.rs +45 -73
  43. package/contracts/oapps/sac-manager/src/storage.rs +2 -9
  44. package/contracts/oapps/sac-manager/src/tests/sac_manager/clawback.rs +8 -10
  45. package/contracts/oapps/sac-manager/src/tests/sac_manager/mint.rs +13 -18
  46. package/contracts/oapps/sac-manager/src/tests/sac_manager/mod.rs +0 -1
  47. package/contracts/oapps/sac-manager/src/tests/sac_manager/set_admin.rs +22 -12
  48. package/contracts/oapps/sac-manager/src/tests/sac_manager/set_authorized.rs +19 -9
  49. package/contracts/oapps/sac-manager/src/tests/sac_manager/test_helper.rs +27 -10
  50. package/contracts/oapps/sac-manager/src/tests/sac_manager/view_functions.rs +0 -15
  51. package/contracts/oapps/sac-manager/src/tests/test_helper.rs +19 -28
  52. package/contracts/upgrader/src/lib.rs +5 -2
  53. package/contracts/utils/src/auth.rs +6 -2
  54. package/contracts/utils/src/errors.rs +18 -0
  55. package/contracts/utils/src/lib.rs +1 -0
  56. package/contracts/utils/src/multisig.rs +5 -1
  57. package/contracts/utils/src/ownable.rs +1 -1
  58. package/contracts/utils/src/rbac.rs +428 -0
  59. package/contracts/utils/src/tests/auth.rs +2 -2
  60. package/contracts/utils/src/tests/mod.rs +1 -0
  61. package/contracts/utils/src/tests/multisig.rs +2 -2
  62. package/contracts/utils/src/tests/ownable.rs +4 -5
  63. package/contracts/utils/src/tests/rbac.rs +559 -0
  64. package/contracts/utils/src/tests/ttl_configurable.rs +5 -6
  65. package/contracts/utils/src/tests/upgradeable.rs +4 -5
  66. package/contracts/workers/worker/src/worker.rs +1 -1
  67. package/package.json +3 -3
  68. package/sdk/.turbo/turbo-test.log +368 -366
  69. package/sdk/dist/generated/bml.d.ts +53 -3
  70. package/sdk/dist/generated/bml.js +27 -3
  71. package/sdk/dist/generated/counter.d.ts +55 -5
  72. package/sdk/dist/generated/counter.js +28 -4
  73. package/sdk/dist/generated/dvn.d.ts +55 -5
  74. package/sdk/dist/generated/dvn.js +28 -4
  75. package/sdk/dist/generated/dvn_fee_lib.d.ts +55 -5
  76. package/sdk/dist/generated/dvn_fee_lib.js +28 -4
  77. package/sdk/dist/generated/endpoint.d.ts +55 -5
  78. package/sdk/dist/generated/endpoint.js +28 -4
  79. package/sdk/dist/generated/executor.d.ts +55 -5
  80. package/sdk/dist/generated/executor.js +28 -4
  81. package/sdk/dist/generated/executor_fee_lib.d.ts +55 -5
  82. package/sdk/dist/generated/executor_fee_lib.js +28 -4
  83. package/sdk/dist/generated/executor_helper.d.ts +53 -3
  84. package/sdk/dist/generated/executor_helper.js +27 -3
  85. package/sdk/dist/generated/layerzero_view.d.ts +55 -5
  86. package/sdk/dist/generated/layerzero_view.js +28 -4
  87. package/sdk/dist/generated/oft.d.ts +55 -5
  88. package/sdk/dist/generated/oft.js +28 -4
  89. package/sdk/dist/generated/price_feed.d.ts +55 -5
  90. package/sdk/dist/generated/price_feed.js +28 -4
  91. package/sdk/dist/generated/sac_manager.d.ts +213 -687
  92. package/sdk/dist/generated/sac_manager.js +57 -239
  93. package/sdk/dist/generated/sml.d.ts +55 -5
  94. package/sdk/dist/generated/sml.js +28 -4
  95. package/sdk/dist/generated/treasury.d.ts +55 -5
  96. package/sdk/dist/generated/treasury.js +28 -4
  97. package/sdk/dist/generated/uln302.d.ts +55 -5
  98. package/sdk/dist/generated/uln302.js +28 -4
  99. package/sdk/dist/generated/upgrader.d.ts +53 -3
  100. package/sdk/dist/generated/upgrader.js +27 -3
  101. package/sdk/package.json +1 -1
  102. package/sdk/test/oft-sml.test.ts +10 -9
  103. package/sdk/test/{sac-manager-redistribution.test.ts → sac-manager.test.ts} +49 -25
  104. package/contracts/oapps/sac-manager/src/errors.rs +0 -14
  105. package/contracts/oapps/sac-manager/src/tests/sac_manager/set_minter.rs +0 -69
@@ -4,11 +4,11 @@
4
4
 
5
5
  extern crate std;
6
6
 
7
- use crate::{SacManager, SacManagerClient};
7
+ use crate::{SACManager, SACManagerClient};
8
8
  use soroban_sdk::{
9
9
  testutils::{Address as _, MockAuth, MockAuthInvoke, StellarAssetContract},
10
10
  token::StellarAssetClient,
11
- Address, Env, IntoVal, Val,
11
+ Address, Env, IntoVal, Symbol, Val,
12
12
  };
13
13
 
14
14
  // =========================================================================
@@ -25,27 +25,19 @@ use soroban_sdk::{
25
25
  /// ```
26
26
  pub struct TestSetupBuilder {
27
27
  manager_as_sac_admin: bool,
28
- skip_minters_setup: bool,
29
28
  }
30
29
 
31
30
  impl TestSetupBuilder {
32
31
  fn new() -> Self {
33
- Self { manager_as_sac_admin: false, skip_minters_setup: false }
32
+ Self { manager_as_sac_admin: false }
34
33
  }
35
34
 
36
- /// Set the SacManager as SAC admin during setup.
35
+ /// Set the SACManager as SAC admin during setup.
37
36
  pub fn with_manager_as_sac_admin(mut self) -> Self {
38
37
  self.manager_as_sac_admin = true;
39
38
  self
40
39
  }
41
40
 
42
- /// Skip setting minters during setup.
43
- /// Use this to test `set_minter` auth or mint when no minter is set.
44
- pub fn with_empty_minters(mut self) -> Self {
45
- self.skip_minters_setup = true;
46
- self
47
- }
48
-
49
41
  /// Build the TestSetup with the configured options.
50
42
  pub fn build<'a>(self) -> TestSetup<'a> {
51
43
  let env = Env::default();
@@ -55,23 +47,22 @@ impl TestSetupBuilder {
55
47
  let sac_contract = env.register_stellar_asset_contract_v2(owner.clone());
56
48
  let sac = sac_contract.address();
57
49
 
58
- let sac_manager = env.register(SacManager, (&sac, &owner));
59
- let sac_manager_client = SacManagerClient::new(&env, &sac_manager);
50
+ let sac_manager = env.register(SACManager, (&sac, &owner));
51
+ let sac_manager_client = SACManagerClient::new(&env, &sac_manager);
60
52
  let sac_client = StellarAssetClient::new(&env, &sac);
61
53
 
62
- if !self.skip_minters_setup {
63
- env.mock_auths(&[MockAuth {
64
- address: &owner,
65
- invoke: &MockAuthInvoke {
66
- contract: &sac_manager,
67
- fn_name: "set_minter",
68
- args: (oft.clone(), true).into_val(&env),
69
- sub_invokes: &[],
70
- },
71
- }]);
72
- sac_manager_client.set_minter(&oft, &true);
54
+ // Grant all roles to owner (owner is the authorizer so can grant any role)
55
+ for role_str in ["ADMIN_MGR", "MINTER", "BLKLISTR", "CLAWBACK"] {
56
+ let role = Symbol::new(&env, role_str);
57
+ mock_auth(&env, &sac_manager, &owner, "grant_role", (owner.clone(), role.clone(), owner.clone()));
58
+ sac_manager_client.grant_role(&owner, &role, &owner);
73
59
  }
74
60
 
61
+ // Grant MINTER_ROLE to oft so it can call mint in tests
62
+ let minter_role = Symbol::new(&env, "MINTER");
63
+ mock_auth(&env, &sac_manager, &owner, "grant_role", (oft.clone(), minter_role.clone(), owner.clone()));
64
+ sac_manager_client.grant_role(&oft, &minter_role, &owner);
65
+
75
66
  if self.manager_as_sac_admin {
76
67
  env.mock_auths(&[MockAuth {
77
68
  address: &owner,
@@ -96,16 +87,16 @@ impl TestSetupBuilder {
96
87
  // TestSetup
97
88
  // =========================================================================
98
89
 
99
- /// Common test setup that creates a SAC and SacManager.
90
+ /// Common test setup that creates a SAC and SACManager.
100
91
  pub struct TestSetup<'a> {
101
92
  pub env: Env,
102
93
  pub owner: Address,
103
- /// Address used as a minter in default setup (in minters list).
94
+ /// Address that has MINTER_ROLE in default setup (used as operator for mint in tests).
104
95
  pub minter: Address,
105
96
  pub sac: Address,
106
97
  pub sac_contract: StellarAssetContract,
107
98
  pub sac_manager: Address,
108
- pub sac_manager_client: SacManagerClient<'a>,
99
+ pub sac_manager_client: SACManagerClient<'a>,
109
100
  pub sac_client: StellarAssetClient<'a>,
110
101
  }
111
102
 
@@ -20,7 +20,7 @@
20
20
  //! ```
21
21
 
22
22
  use soroban_sdk::{contract, contractimpl, xdr::ToXdr, Address, Bytes, BytesN, Env};
23
- use utils::{auth::AuthClient, upgradeable::UpgradeableClient};
23
+ use utils::{auth::AuthClient, errors::AuthError, option_ext::OptionExt, upgradeable::UpgradeableClient};
24
24
 
25
25
  /// Upgrader contract for managing upgrades of other contracts.
26
26
  ///
@@ -59,7 +59,10 @@ impl Upgrader {
59
59
  /// upgrader.upgrade_and_migrate(&contract_addr, &wasm_hash, &migration_data);
60
60
  /// ```
61
61
  pub fn upgrade_and_migrate(env: &Env, contract_address: &Address, wasm_hash: &BytesN<32>, migration_data: &Bytes) {
62
- AuthClient::new(env, contract_address).authorizer().require_auth();
62
+ AuthClient::new(env, contract_address)
63
+ .authorizer()
64
+ .unwrap_or_panic(env, AuthError::AuthorizerNotFound)
65
+ .require_auth();
63
66
  let client = UpgradeableClient::new(env, contract_address);
64
67
  client.upgrade(wasm_hash);
65
68
  client.migrate(migration_data);
@@ -3,6 +3,7 @@
3
3
  //! The `Auth` trait provides a common interface for authorization that can be
4
4
  //! implemented by different access control patterns (e.g., single owner, multisig).
5
5
 
6
+ use crate::{errors::AuthError, option_ext::OptionExt};
6
7
  use soroban_sdk::{contractclient, Address, Env};
7
8
 
8
9
  // ===========================================================================
@@ -20,7 +21,10 @@ pub trait Auth: Sized {
20
21
  /// For `Ownable` contracts, this returns the stored owner address.
21
22
  /// For `MultiSig` contracts, this returns the contract's own address
22
23
  /// (self-owning pattern).
23
- fn authorizer(env: &Env) -> Address;
24
+ ///
25
+ /// Returns `None` when there is no authorizer (for example, when an Ownable
26
+ /// contract's owner has been renounced).
27
+ fn authorizer(env: &Env) -> Option<Address>;
24
28
  }
25
29
 
26
30
  // ===========================================================================
@@ -31,7 +35,7 @@ pub trait Auth: Sized {
31
35
  ///
32
36
  /// Panics if the authorizer has not provided authorization for this invocation.
33
37
  pub fn enforce_auth<T: Auth>(env: &Env) -> Address {
34
- let authorizer = T::authorizer(env);
38
+ let authorizer = T::authorizer(env).unwrap_or_panic(env, AuthError::AuthorizerNotFound);
35
39
  authorizer.require_auth();
36
40
  authorizer
37
41
  }
@@ -62,3 +62,21 @@ pub enum MultiSigError {
62
62
  UnsortedSigners,
63
63
  ZeroThreshold,
64
64
  }
65
+
66
+ /// AuthError: 1070-1079
67
+ #[contract_error]
68
+ pub enum AuthError {
69
+ AuthorizerNotFound = 1070,
70
+ }
71
+
72
+ /// RbacError: 1080-1089
73
+ #[contract_error]
74
+ pub enum RbacError {
75
+ AdminRoleNotFound = 1080,
76
+ IndexOutOfBounds,
77
+ MaxRolesExceeded,
78
+ RoleIsEmpty,
79
+ RoleNotFound,
80
+ RoleNotHeld,
81
+ Unauthorized,
82
+ }
@@ -8,6 +8,7 @@ pub mod errors;
8
8
  pub mod multisig;
9
9
  pub mod option_ext;
10
10
  pub mod ownable;
11
+ pub mod rbac;
11
12
  pub mod ttl_configurable;
12
13
  pub mod ttl_extendable;
13
14
  pub mod upgradeable;
@@ -179,7 +179,11 @@ pub fn recover_signer(env: &Env, digest: &BytesN<32>, signature: &BytesN<65>) ->
179
179
  /// Panics with `InvalidAuthorizer` if the authorizer is not the contract's own address.
180
180
  pub fn enforce_multisig_auth<T: MultiSig>(env: &Env) {
181
181
  // Ensure the authorizer is the contract's own address
182
- assert_with_error!(env, T::authorizer(env) == env.current_contract_address(), MultiSigError::InvalidAuthorizer);
182
+ assert_with_error!(
183
+ env,
184
+ Some(env.current_contract_address()) == T::authorizer(env),
185
+ MultiSigError::InvalidAuthorizer
186
+ );
183
187
  env.current_contract_address().require_auth();
184
188
  }
185
189
 
@@ -204,7 +204,7 @@ pub trait OwnableInitializer {
204
204
  pub fn enforce_owner_auth<T: Ownable>(env: &Env) -> Address {
205
205
  let owner = T::owner(env).unwrap_or_panic(env, OwnableError::OwnerNotSet);
206
206
  // Ensure the owner is the same as the authorizer
207
- assert_with_error!(env, owner == T::authorizer(env), OwnableError::InvalidAuthorizer);
207
+ assert_with_error!(env, Some(&owner) == T::authorizer(env).as_ref(), OwnableError::InvalidAuthorizer);
208
208
  owner.require_auth();
209
209
  owner
210
210
  }
@@ -0,0 +1,428 @@
1
+ //! Role-Based Access Control (RBAC) for Soroban contracts.
2
+ //!
3
+ //! Combines OpenZeppelin-style role management with the Auth pattern.
4
+ //! The authorizer (e.g. owner from Ownable, or contract from MultiSig) replaces Admin.
5
+
6
+ use crate::{self as utils, auth::Auth, errors::RbacError, option_ext::OptionExt};
7
+ use common_macros::{contract_trait, only_auth, storage};
8
+ use soroban_sdk::{assert_with_error, contractevent, Address, Env, Symbol, Vec};
9
+
10
+ // ===========================================================================
11
+ // Constants
12
+ // ===========================================================================
13
+
14
+ /// Maximum number of roles that can exist simultaneously.
15
+ pub const MAX_ROLES: u32 = 256;
16
+
17
+ // ===========================================================================
18
+ // Events
19
+ // ===========================================================================
20
+
21
+ /// Event emitted when a role is granted.
22
+ #[contractevent]
23
+ #[derive(Clone, Debug, Eq, PartialEq)]
24
+ pub struct RoleGranted {
25
+ #[topic]
26
+ pub role: Symbol,
27
+ #[topic]
28
+ pub account: Address,
29
+ pub caller: Address,
30
+ }
31
+
32
+ /// Event emitted when a role is revoked.
33
+ #[contractevent]
34
+ #[derive(Clone, Debug, Eq, PartialEq)]
35
+ pub struct RoleRevoked {
36
+ #[topic]
37
+ pub role: Symbol,
38
+ #[topic]
39
+ pub account: Address,
40
+ pub caller: Address,
41
+ }
42
+
43
+ /// Event emitted when a role admin is changed.
44
+ #[contractevent]
45
+ #[derive(Clone, Debug, Eq, PartialEq)]
46
+ pub struct RoleAdminChanged {
47
+ #[topic]
48
+ pub role: Symbol,
49
+ pub previous_admin_role: Option<Symbol>,
50
+ pub new_admin_role: Option<Symbol>,
51
+ }
52
+
53
+ // ===========================================================================
54
+ // Storage
55
+ // ===========================================================================
56
+
57
+ #[storage]
58
+ pub enum RbacStorage {
59
+ /// All roles that have at least one member
60
+ #[persistent(Vec<Symbol>)]
61
+ #[default(Vec::new(env))]
62
+ ExistingRoles,
63
+
64
+ /// (role, index) -> Address
65
+ #[persistent(Address)]
66
+ RoleIndexToAccount { role: Symbol, index: u32 },
67
+
68
+ /// (role, account) -> index
69
+ #[persistent(u32)]
70
+ RoleAccountToIndex { role: Symbol, account: Address },
71
+
72
+ /// role -> count of accounts
73
+ #[persistent(u32)]
74
+ #[default(0)]
75
+ RoleAccountsCount { role: Symbol },
76
+
77
+ /// role -> admin role (who can grant/revoke this role). Key removed when no admin.
78
+ #[persistent(Symbol)]
79
+ RoleAdmin { role: Symbol },
80
+ }
81
+
82
+ // ===========================================================================
83
+ // Trait
84
+ // ===========================================================================
85
+
86
+ /// Trait for contracts with role-based access control.
87
+ ///
88
+ /// Extends `Auth` — the authorizer replaces the traditional admin and can grant/revoke
89
+ /// any role. Each role can also have an admin role for hierarchical control.
90
+ #[contract_trait]
91
+ pub trait RoleBasedAccessControl: Auth {
92
+ // ===========================================================================
93
+ // State-changing
94
+ // ===========================================================================
95
+
96
+ /// Grants a role to an account. Caller must be owner or have the role's admin role.
97
+ ///
98
+ /// # Arguments
99
+ /// * `account` - The account to grant the role to.
100
+ /// * `role` - The role to grant.
101
+ /// * `caller` - The account that is granting the role. Must be owner or have the role's admin role.
102
+ fn grant_role(
103
+ env: &soroban_sdk::Env,
104
+ account: &soroban_sdk::Address,
105
+ role: &soroban_sdk::Symbol,
106
+ caller: &soroban_sdk::Address,
107
+ ) {
108
+ caller.require_auth();
109
+ ensure_if_authorizer_or_role_admin::<Self>(env, role, caller);
110
+ grant_role_no_auth(env, account, role, caller);
111
+ }
112
+
113
+ /// Revokes a role from an account. Caller must be owner or have the role's admin role.
114
+ ///
115
+ /// # Arguments
116
+ /// * `account` - The account to revoke the role from.
117
+ /// * `role` - The role to revoke.
118
+ /// * `caller` - The account that is revoking the role. Must be owner or have the role's admin role.
119
+ fn revoke_role(
120
+ env: &soroban_sdk::Env,
121
+ account: &soroban_sdk::Address,
122
+ role: &soroban_sdk::Symbol,
123
+ caller: &soroban_sdk::Address,
124
+ ) {
125
+ caller.require_auth();
126
+ ensure_if_authorizer_or_role_admin::<Self>(env, role, caller);
127
+ revoke_role_no_auth(env, account, role, caller);
128
+ }
129
+
130
+ /// Allows an account to renounce a role assigned to itself.
131
+ /// Users can only renounce roles for their own account.
132
+ ///
133
+ /// # Arguments
134
+ /// * `role` - The role to renounce.
135
+ /// * `caller` - The account that is renouncing the role. Must be the account itself.
136
+ fn renounce_role(env: &soroban_sdk::Env, role: &soroban_sdk::Symbol, caller: &soroban_sdk::Address) {
137
+ caller.require_auth();
138
+ revoke_role_no_auth(env, caller, role, caller);
139
+ }
140
+
141
+ /// Sets `admin_role` as the admin role of `role`. Caller must be the authorizer.
142
+ ///
143
+ /// # Arguments
144
+ /// * `role` - The role to set the admin for.
145
+ /// * `admin_role` - The admin role to set for the role.
146
+ ///
147
+ /// # Notes
148
+ ///
149
+ /// The admin role can be any `Symbol`, including one with no members. If the admin
150
+ /// role has no members, only the authorizer can grant/revoke the role.
151
+ #[only_auth]
152
+ fn set_role_admin(env: &soroban_sdk::Env, role: &soroban_sdk::Symbol, admin_role: &soroban_sdk::Symbol) {
153
+ set_role_admin_no_auth(env, role, admin_role);
154
+ }
155
+
156
+ /// Removes the admin role for a specified role. Caller must be the authorizer.
157
+ ///
158
+ /// # Arguments
159
+ /// * `role` - The role to remove the admin for.
160
+ ///
161
+ /// # Errors
162
+ /// * `RbacError::AdminRoleNotFound` - If no admin role is set for the role.
163
+ #[only_auth]
164
+ fn remove_role_admin(env: &soroban_sdk::Env, role: &soroban_sdk::Symbol) {
165
+ remove_role_admin_no_auth(env, role);
166
+ }
167
+
168
+ // ===========================================================================
169
+ // View functions
170
+ // ===========================================================================
171
+
172
+ /// Returns `Some(index)` if the account has the specified role, where `index`
173
+ /// is the index of the account in the role. Returns `None` if not.
174
+ ///
175
+ /// # Arguments
176
+ /// * `account` - The account to check the role for.
177
+ /// * `role` - The role to check the account for.
178
+ fn has_role(env: &soroban_sdk::Env, account: &soroban_sdk::Address, role: &soroban_sdk::Symbol) -> Option<u32> {
179
+ RbacStorage::role_account_to_index(env, role, account)
180
+ }
181
+
182
+ /// Returns the admin role for a specific role, or None if not set.
183
+ ///
184
+ /// # Arguments
185
+ /// * `role` - The role to get the admin for.
186
+ fn get_role_admin(env: &soroban_sdk::Env, role: &soroban_sdk::Symbol) -> Option<soroban_sdk::Symbol> {
187
+ RbacStorage::role_admin(env, role)
188
+ }
189
+
190
+ /// Returns the number of accounts that have the specified role.
191
+ ///
192
+ /// # Arguments
193
+ /// * `role` - The role to get the member count for.
194
+ fn get_role_member_count(env: &soroban_sdk::Env, role: &soroban_sdk::Symbol) -> u32 {
195
+ RbacStorage::role_accounts_count(env, role)
196
+ }
197
+
198
+ /// Returns the account at the specified index for a given role.
199
+ ///
200
+ /// # Arguments
201
+ /// * `role` - The role to get the member for.
202
+ /// * `index` - The index of the member to get.
203
+ ///
204
+ /// # Errors
205
+ /// * `RbacError::IndexOutOfBounds` if the index is out of bounds.
206
+ fn get_role_member(env: &soroban_sdk::Env, role: &soroban_sdk::Symbol, index: u32) -> soroban_sdk::Address {
207
+ RbacStorage::role_index_to_account(env, role, index).unwrap_or_panic(env, RbacError::IndexOutOfBounds)
208
+ }
209
+
210
+ /// Returns all roles that currently have at least one member.
211
+ /// Defaults to empty vector if no roles exist.
212
+ ///
213
+ /// # Notes
214
+ ///
215
+ /// This function returns all roles that currently have at least one member.
216
+ /// The maximum number of roles is limited by [`MAX_ROLES`].
217
+ fn get_existing_roles(env: &soroban_sdk::Env) -> soroban_sdk::Vec<soroban_sdk::Symbol> {
218
+ RbacStorage::existing_roles(env)
219
+ }
220
+ }
221
+
222
+ // ===========================================================================
223
+ // Public helpers
224
+ // ===========================================================================
225
+
226
+ /// Ensures the caller has the specified role.
227
+ ///
228
+ /// # Arguments
229
+ /// * `role` - The role to check the caller for.
230
+ /// * `caller` - The account that is being checked. Must have the role.
231
+ ///
232
+ /// # Errors
233
+ /// * `Unauthorized` - If the caller does not have the role.
234
+ pub fn ensure_role(env: &Env, role: &Symbol, caller: &Address) {
235
+ assert_with_error!(env, RbacStorage::has_role_account_to_index(env, role, caller), RbacError::Unauthorized);
236
+ }
237
+
238
+ /// Grants a role to an account without auth check.
239
+ ///
240
+ /// # Arguments
241
+ /// * `account` - The account to grant the role to.
242
+ /// * `role` - The role to grant.
243
+ /// * `caller` - The account that is granting the role. Must be owner or have the role's admin role.
244
+ ///
245
+ /// # Security Warning
246
+ ///
247
+ /// **IMPORTANT**: This function bypasses authorization checks and should only
248
+ /// be used:
249
+ /// - During contract initialization/construction
250
+ /// - In admin functions that implement their own authorization logic
251
+ ///
252
+ /// Using this function in public-facing methods creates significant security
253
+ /// risks as it could allow unauthorized role assignments.
254
+ pub fn grant_role_no_auth(env: &Env, account: &Address, role: &Symbol, caller: &Address) {
255
+ if RbacStorage::has_role_account_to_index(env, role, account) {
256
+ return;
257
+ }
258
+ add_to_role_enumeration(env, account, role);
259
+ RoleGranted { role: role.clone(), account: account.clone(), caller: caller.clone() }.publish(env);
260
+ }
261
+
262
+ /// Revokes a role from an account without auth check.
263
+ ///
264
+ /// # Arguments
265
+ /// * `account` - The account to revoke the role from.
266
+ /// * `role` - The role to revoke.
267
+ /// * `caller` - The account that is revoking the role. Must be owner or have the role's admin role.
268
+ ///
269
+ /// # Security Warning
270
+ ///
271
+ /// **IMPORTANT**: This function bypasses authorization checks and should only
272
+ /// be used:
273
+ /// - During contract initialization/construction
274
+ /// - In admin functions that implement their own authorization logic
275
+ ///
276
+ /// Using this function in public-facing methods creates significant security
277
+ /// risks as it could allow unauthorized role revocations.
278
+ pub fn revoke_role_no_auth(env: &Env, account: &Address, role: &Symbol, caller: &Address) {
279
+ assert_with_error!(env, RbacStorage::has_role_account_to_index(env, role, account), RbacError::RoleNotHeld);
280
+ remove_from_role_enumeration(env, account, role);
281
+ RoleRevoked { role: role.clone(), account: account.clone(), caller: caller.clone() }.publish(env);
282
+ }
283
+
284
+ /// Sets the admin role for a role without auth check. For constructor/init or when caller enforces own auth.
285
+ ///
286
+ /// # Arguments
287
+ /// * `role` - The role to set the admin for.
288
+ /// * `admin_role` - The admin role to set for the role.
289
+ ///
290
+ /// # Security Warning
291
+ ///
292
+ /// **IMPORTANT**: This function bypasses authorization checks and should only
293
+ /// be used:
294
+ /// - During contract initialization/construction
295
+ /// - In admin functions that implement their own authorization logic
296
+ ///
297
+ /// Using this function in public-facing methods creates significant security
298
+ /// risks as it could allow unauthorized admin role assignments.
299
+ ///
300
+ /// # Circular Admin Warning
301
+ ///
302
+ /// **CAUTION**: This function allows the creation of circular admin
303
+ /// relationships between roles. For example, it's possible to assign MINT_ADMIN
304
+ /// as the admin of MINT_ROLE while also making MINT_ROLE the admin of
305
+ /// MINT_ADMIN. Such circular relationships can lead to unintended consequences,
306
+ /// including:
307
+ ///
308
+ /// - Race conditions where each role can revoke the other
309
+ /// - Potential security vulnerabilities in role management
310
+ /// - Confusing governance structures that are difficult to reason about
311
+ ///
312
+ /// When designing your role hierarchy, carefully consider the relationships
313
+ /// between roles and avoid creating circular dependencies.
314
+ pub fn set_role_admin_no_auth(env: &Env, role: &Symbol, admin_role: &Symbol) {
315
+ let previous = RbacStorage::role_admin(env, role);
316
+ RbacStorage::set_role_admin(env, role, admin_role);
317
+ RoleAdminChanged { role: role.clone(), previous_admin_role: previous, new_admin_role: Some(admin_role.clone()) }
318
+ .publish(env);
319
+ }
320
+
321
+ /// Removes the admin role for a specified role without auth check.
322
+ ///
323
+ /// For use in admin functions that implement their own authorization logic,
324
+ /// or when cleaning up unused roles.
325
+ ///
326
+ /// # Arguments
327
+ /// * `role` - The role to remove the admin for.
328
+ ///
329
+ /// # Errors
330
+ /// * `RbacError::AdminRoleNotFound` - If no admin role is set for the role.
331
+ ///
332
+ /// # Security Warning
333
+ ///
334
+ /// **IMPORTANT**: This function bypasses authorization checks and should only
335
+ /// be used:
336
+ /// - In admin functions that implement their own authorization logic
337
+ /// - When cleaning up unused roles
338
+ pub fn remove_role_admin_no_auth(env: &Env, role: &Symbol) {
339
+ let previous = RbacStorage::role_admin(env, role);
340
+ assert_with_error!(env, previous.is_some(), RbacError::AdminRoleNotFound);
341
+ RbacStorage::remove_role_admin(env, role);
342
+ RoleAdminChanged { role: role.clone(), previous_admin_role: previous, new_admin_role: None }.publish(env);
343
+ }
344
+
345
+ // ===========================================================================
346
+ // Private helpers
347
+ // ===========================================================================
348
+
349
+ /// Ensures the caller is the authorizer or has the role's admin role.
350
+ ///
351
+ /// # Arguments
352
+ /// * `role` - The role to check the caller for.
353
+ /// * `caller` - The account that is being checked. Must be the authorizer or have the role's admin role.
354
+ ///
355
+ /// # Errors
356
+ /// * `Unauthorized` - If the caller is neither the authorizer nor has the role's admin role.
357
+ fn ensure_if_authorizer_or_role_admin<T: RoleBasedAccessControl>(env: &Env, role: &Symbol, caller: &Address) {
358
+ assert_with_error!(
359
+ env,
360
+ T::get_role_admin(env, role).is_some_and(|admin_role| T::has_role(env, caller, &admin_role).is_some())
361
+ || Some(caller) == T::authorizer(env).as_ref(),
362
+ RbacError::Unauthorized
363
+ );
364
+ }
365
+
366
+ /// Adds an account to the role enumeration.
367
+ ///
368
+ /// # Arguments
369
+ /// * `account` - The account to add to the role enumeration.
370
+ /// * `role` - The role to add the account to.
371
+ fn add_to_role_enumeration(env: &Env, account: &Address, role: &Symbol) {
372
+ let count = RbacStorage::role_accounts_count(env, role);
373
+
374
+ // If the role has no accounts, add it to the existing roles
375
+ if count == 0 {
376
+ let mut existing = RbacStorage::existing_roles(env);
377
+ assert_with_error!(env, existing.len() < MAX_ROLES, RbacError::MaxRolesExceeded);
378
+ existing.push_back(role.clone());
379
+ RbacStorage::set_existing_roles(env, &existing);
380
+ }
381
+
382
+ RbacStorage::set_role_index_to_account(env, role, count, account);
383
+ RbacStorage::set_role_account_to_index(env, role, account, &count);
384
+ RbacStorage::set_role_accounts_count(env, role, &(count + 1));
385
+ }
386
+
387
+ /// Removes an account from the role enumeration.
388
+ ///
389
+ /// # Arguments
390
+ /// * `account` - The account to remove from the role enumeration.
391
+ /// * `role` - The role to remove the account from.
392
+ fn remove_from_role_enumeration(env: &Env, account: &Address, role: &Symbol) {
393
+ let count = RbacStorage::role_accounts_count(env, role);
394
+ assert_with_error!(env, count > 0, RbacError::RoleIsEmpty);
395
+
396
+ // Get the index of the account to remove
397
+ let to_remove_idx =
398
+ RbacStorage::role_account_to_index(env, role, account).unwrap_or_panic(env, RbacError::RoleNotHeld);
399
+
400
+ // Get the index of the last account for the role
401
+ let last_idx = count - 1;
402
+
403
+ // Remove the target account's mappings
404
+ RbacStorage::remove_role_index_to_account(env, role, to_remove_idx);
405
+ RbacStorage::remove_role_account_to_index(env, role, account);
406
+
407
+ // If the removed account wasn't the last, move the last account into the vacated slot
408
+ if to_remove_idx != last_idx {
409
+ // Get the last account and remove the mapping from index to account
410
+ let last_account =
411
+ RbacStorage::role_index_to_account(env, role, last_idx).unwrap_or_panic(env, RbacError::IndexOutOfBounds);
412
+ RbacStorage::remove_role_index_to_account(env, role, last_idx);
413
+
414
+ // Move the last account into the vacated slot
415
+ RbacStorage::set_role_index_to_account(env, role, to_remove_idx, &last_account);
416
+ RbacStorage::set_role_account_to_index(env, role, &last_account, &to_remove_idx);
417
+ }
418
+
419
+ RbacStorage::set_role_accounts_count(env, role, &last_idx);
420
+
421
+ // If this was the last account with this role, remove the role from the existing roles
422
+ if last_idx == 0 {
423
+ let mut existing = RbacStorage::existing_roles(env);
424
+ let pos = existing.first_index_of(role).unwrap_or_panic(env, RbacError::RoleNotFound);
425
+ existing.remove(pos);
426
+ RbacStorage::set_existing_roles(env, &existing);
427
+ }
428
+ }
@@ -39,8 +39,8 @@ impl AuthTestContract {
39
39
 
40
40
  /// `Auth` implementation for the test contract - uses a stored address as the authorizer.
41
41
  impl Auth for AuthTestContract {
42
- fn authorizer(env: &Env) -> Address {
43
- env.storage().instance().get(&authorizer_key(env)).expect("authorizer must be set in tests")
42
+ fn authorizer(env: &Env) -> Option<Address> {
43
+ env.storage().instance().get(&authorizer_key(env))
44
44
  }
45
45
  }
46
46
 
@@ -5,6 +5,7 @@ mod bytes_ext;
5
5
  mod multisig;
6
6
  mod option_ext;
7
7
  mod ownable;
8
+ mod rbac;
8
9
  mod test_helper;
9
10
  mod testing_utils;
10
11
  mod ttl_configurable;
@@ -23,8 +23,8 @@ impl TestContract {
23
23
  /// This allows multisig quorum approval to serve as the authorizer.
24
24
  #[contractimpl]
25
25
  impl Auth for TestContract {
26
- fn authorizer(env: &Env) -> Address {
27
- env.current_contract_address()
26
+ fn authorizer(env: &Env) -> Option<Address> {
27
+ Some(env.current_contract_address())
28
28
  }
29
29
  }
30
30