@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.
- package/.turbo/turbo-build.log +274 -268
- package/.turbo/turbo-lint.log +216 -213
- package/.turbo/turbo-test.log +1735 -1994
- package/contracts/common-macros/src/auth.rs +5 -5
- package/contracts/common-macros/src/lib.rs +69 -0
- package/contracts/common-macros/src/rbac.rs +90 -0
- package/contracts/common-macros/src/tests/lz_contract.rs +5 -7
- package/contracts/common-macros/src/tests/mod.rs +1 -0
- package/contracts/common-macros/src/tests/rbac.rs +420 -0
- package/contracts/common-macros/src/tests/snapshots/common_macros__tests__auth__snapshot_generated_multisig_code.snap +4 -4
- package/contracts/common-macros/src/tests/snapshots/common_macros__tests__auth__snapshot_generated_ownable_code.snap +5 -12
- package/contracts/common-macros/src/tests/snapshots/common_macros__tests__rbac__snapshot_preserve_function_signature.snap +17 -0
- package/contracts/common-macros/src/tests/storage/parse_name.rs +0 -1
- package/contracts/macro-integration-tests/tests/runtime/lz_contract/wrapper_default.rs +1 -1
- package/contracts/macro-integration-tests/tests/runtime/lz_contract/wrapper_multisig.rs +1 -1
- package/contracts/macro-integration-tests/tests/runtime/lz_contract/wrapper_multisig_upgradeable.rs +1 -1
- package/contracts/macro-integration-tests/tests/runtime/multisig/self_auth.rs +1 -1
- package/contracts/macro-integration-tests/tests/runtime/ownable/initialization.rs +8 -5
- package/contracts/macro-integration-tests/tests/runtime/ownable/ownership_transfer.rs +2 -2
- package/contracts/macro-integration-tests/tests/runtime/rbac/guard_behavior.rs +91 -0
- package/contracts/macro-integration-tests/tests/runtime/rbac/mod.rs +30 -0
- package/contracts/macro-integration-tests/tests/runtime/ttl_configurable/configuration.rs +2 -2
- package/contracts/macro-integration-tests/tests/runtime/upgradeable/migrate_guard_and_state.rs +4 -4
- package/contracts/macro-integration-tests/tests/ui/lz_contract/pass/basic.rs +1 -1
- package/contracts/macro-integration-tests/tests/ui/ownable/pass/basic.rs +1 -1
- package/contracts/macro-integration-tests/tests/ui/rbac/fail/missing_env.rs +18 -0
- package/contracts/macro-integration-tests/tests/ui/rbac/fail/missing_env.stderr +16 -0
- package/contracts/macro-integration-tests/tests/ui/rbac/fail/param_not_address.rs +18 -0
- package/contracts/macro-integration-tests/tests/ui/rbac/fail/param_not_address.stderr +24 -0
- package/contracts/macro-integration-tests/tests/ui/rbac/fail/param_not_found.rs +18 -0
- package/contracts/macro-integration-tests/tests/ui/rbac/fail/param_not_found.stderr +24 -0
- package/contracts/macro-integration-tests/tests/ui/rbac/pass/basic.rs +71 -0
- package/contracts/macro-integration-tests/tests/ui_rbac.rs +12 -0
- package/contracts/oapps/oft/src/interfaces/mintable.rs +2 -2
- package/contracts/oapps/oft/src/tests/extensions/oft_fee.rs +2 -2
- package/contracts/oapps/oft/src/tests/extensions/pausable.rs +2 -2
- package/contracts/oapps/oft/src/tests/extensions/rate_limiter.rs +2 -2
- package/contracts/oapps/sac-manager/Cargo.toml +0 -1
- package/contracts/oapps/sac-manager/src/interfaces/mod.rs +3 -0
- package/contracts/oapps/sac-manager/src/interfaces/sac_admin_wrapper.rs +49 -0
- package/contracts/oapps/sac-manager/src/lib.rs +3 -3
- package/contracts/oapps/sac-manager/src/sac_manager.rs +45 -73
- package/contracts/oapps/sac-manager/src/storage.rs +2 -9
- package/contracts/oapps/sac-manager/src/tests/sac_manager/clawback.rs +8 -10
- package/contracts/oapps/sac-manager/src/tests/sac_manager/mint.rs +13 -18
- package/contracts/oapps/sac-manager/src/tests/sac_manager/mod.rs +0 -1
- package/contracts/oapps/sac-manager/src/tests/sac_manager/set_admin.rs +22 -12
- package/contracts/oapps/sac-manager/src/tests/sac_manager/set_authorized.rs +19 -9
- package/contracts/oapps/sac-manager/src/tests/sac_manager/test_helper.rs +27 -10
- package/contracts/oapps/sac-manager/src/tests/sac_manager/view_functions.rs +0 -15
- package/contracts/oapps/sac-manager/src/tests/test_helper.rs +19 -28
- package/contracts/upgrader/src/lib.rs +5 -2
- package/contracts/utils/src/auth.rs +6 -2
- package/contracts/utils/src/errors.rs +18 -0
- package/contracts/utils/src/lib.rs +1 -0
- package/contracts/utils/src/multisig.rs +5 -1
- package/contracts/utils/src/ownable.rs +1 -1
- package/contracts/utils/src/rbac.rs +428 -0
- package/contracts/utils/src/tests/auth.rs +2 -2
- package/contracts/utils/src/tests/mod.rs +1 -0
- package/contracts/utils/src/tests/multisig.rs +2 -2
- package/contracts/utils/src/tests/ownable.rs +4 -5
- package/contracts/utils/src/tests/rbac.rs +559 -0
- package/contracts/utils/src/tests/ttl_configurable.rs +5 -6
- package/contracts/utils/src/tests/upgradeable.rs +4 -5
- package/contracts/workers/worker/src/worker.rs +1 -1
- package/package.json +3 -3
- package/sdk/.turbo/turbo-test.log +368 -366
- package/sdk/dist/generated/bml.d.ts +53 -3
- package/sdk/dist/generated/bml.js +27 -3
- package/sdk/dist/generated/counter.d.ts +55 -5
- package/sdk/dist/generated/counter.js +28 -4
- package/sdk/dist/generated/dvn.d.ts +55 -5
- package/sdk/dist/generated/dvn.js +28 -4
- package/sdk/dist/generated/dvn_fee_lib.d.ts +55 -5
- package/sdk/dist/generated/dvn_fee_lib.js +28 -4
- package/sdk/dist/generated/endpoint.d.ts +55 -5
- package/sdk/dist/generated/endpoint.js +28 -4
- package/sdk/dist/generated/executor.d.ts +55 -5
- package/sdk/dist/generated/executor.js +28 -4
- package/sdk/dist/generated/executor_fee_lib.d.ts +55 -5
- package/sdk/dist/generated/executor_fee_lib.js +28 -4
- package/sdk/dist/generated/executor_helper.d.ts +53 -3
- package/sdk/dist/generated/executor_helper.js +27 -3
- package/sdk/dist/generated/layerzero_view.d.ts +55 -5
- package/sdk/dist/generated/layerzero_view.js +28 -4
- package/sdk/dist/generated/oft.d.ts +55 -5
- package/sdk/dist/generated/oft.js +28 -4
- package/sdk/dist/generated/price_feed.d.ts +55 -5
- package/sdk/dist/generated/price_feed.js +28 -4
- package/sdk/dist/generated/sac_manager.d.ts +213 -687
- package/sdk/dist/generated/sac_manager.js +57 -239
- package/sdk/dist/generated/sml.d.ts +55 -5
- package/sdk/dist/generated/sml.js +28 -4
- package/sdk/dist/generated/treasury.d.ts +55 -5
- package/sdk/dist/generated/treasury.js +28 -4
- package/sdk/dist/generated/uln302.d.ts +55 -5
- package/sdk/dist/generated/uln302.js +28 -4
- package/sdk/dist/generated/upgrader.d.ts +53 -3
- package/sdk/dist/generated/upgrader.js +27 -3
- package/sdk/package.json +1 -1
- package/sdk/test/oft-sml.test.ts +10 -9
- package/sdk/test/{sac-manager-redistribution.test.ts → sac-manager.test.ts} +49 -25
- package/contracts/oapps/sac-manager/src/errors.rs +0 -14
- 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::{
|
|
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
|
|
32
|
+
Self { manager_as_sac_admin: false }
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
/// Set the
|
|
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(
|
|
59
|
-
let sac_manager_client =
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
|
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:
|
|
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)
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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!(
|
|
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))
|
|
42
|
+
fn authorizer(env: &Env) -> Option<Address> {
|
|
43
|
+
env.storage().instance().get(&authorizer_key(env))
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -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
|
|