@layerzerolabs/common-utils-stellar-contracts 0.2.122
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/Cargo.toml +28 -0
- package/LICENSE +23 -0
- package/clippy.toml +7 -0
- package/package.json +43 -0
- package/rust-toolchain.toml +4 -0
- package/rustfmt.toml +15 -0
- package/src/auth.rs +48 -0
- package/src/buffer_reader.rs +194 -0
- package/src/buffer_writer.rs +138 -0
- package/src/bytes_ext.rs +18 -0
- package/src/errors.rs +82 -0
- package/src/lib.rs +20 -0
- package/src/multisig.rs +247 -0
- package/src/option_ext.rs +38 -0
- package/src/ownable.rs +227 -0
- package/src/rbac.rs +438 -0
- package/src/testing_utils.rs +180 -0
- package/src/tests/auth.rs +179 -0
- package/src/tests/buffer_reader.rs +969 -0
- package/src/tests/buffer_writer.rs +689 -0
- package/src/tests/bytes_ext.rs +160 -0
- package/src/tests/mod.rs +13 -0
- package/src/tests/multisig.rs +760 -0
- package/src/tests/option_ext.rs +18 -0
- package/src/tests/ownable.rs +822 -0
- package/src/tests/rbac.rs +559 -0
- package/src/tests/test_helper.rs +67 -0
- package/src/tests/testing_utils.rs +522 -0
- package/src/tests/ttl_configurable.rs +586 -0
- package/src/tests/ttl_extendable.rs +64 -0
- package/src/tests/upgradeable.rs +529 -0
- package/src/ttl_configurable.rs +169 -0
- package/src/ttl_extendable.rs +25 -0
- package/src/upgradeable.rs +103 -0
package/src/rbac.rs
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
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
|
+
/// Role representing the contract's authorizer.
|
|
18
|
+
pub const AUTHORIZER: &str = "AUTHORIZER";
|
|
19
|
+
|
|
20
|
+
// ===========================================================================
|
|
21
|
+
// Events
|
|
22
|
+
// ===========================================================================
|
|
23
|
+
|
|
24
|
+
/// Event emitted when a role is granted.
|
|
25
|
+
#[contractevent]
|
|
26
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
27
|
+
pub struct RoleGranted {
|
|
28
|
+
#[topic]
|
|
29
|
+
pub role: Symbol,
|
|
30
|
+
#[topic]
|
|
31
|
+
pub account: Address,
|
|
32
|
+
pub caller: Address,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Event emitted when a role is revoked.
|
|
36
|
+
#[contractevent]
|
|
37
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
38
|
+
pub struct RoleRevoked {
|
|
39
|
+
#[topic]
|
|
40
|
+
pub role: Symbol,
|
|
41
|
+
#[topic]
|
|
42
|
+
pub account: Address,
|
|
43
|
+
pub caller: Address,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Event emitted when a role admin is changed.
|
|
47
|
+
#[contractevent]
|
|
48
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
49
|
+
pub struct RoleAdminChanged {
|
|
50
|
+
#[topic]
|
|
51
|
+
pub role: Symbol,
|
|
52
|
+
pub previous_admin_role: Option<Symbol>,
|
|
53
|
+
pub new_admin_role: Option<Symbol>,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ===========================================================================
|
|
57
|
+
// Storage
|
|
58
|
+
// ===========================================================================
|
|
59
|
+
|
|
60
|
+
#[storage]
|
|
61
|
+
pub enum RbacStorage {
|
|
62
|
+
/// All roles that have at least one member
|
|
63
|
+
#[persistent(Vec<Symbol>)]
|
|
64
|
+
#[default(Vec::new(env))]
|
|
65
|
+
ExistingRoles,
|
|
66
|
+
|
|
67
|
+
/// (role, index) -> Address
|
|
68
|
+
#[persistent(Address)]
|
|
69
|
+
RoleIndexToAccount { role: Symbol, index: u32 },
|
|
70
|
+
|
|
71
|
+
/// (role, account) -> index
|
|
72
|
+
#[persistent(u32)]
|
|
73
|
+
RoleAccountToIndex { role: Symbol, account: Address },
|
|
74
|
+
|
|
75
|
+
/// role -> count of accounts
|
|
76
|
+
#[persistent(u32)]
|
|
77
|
+
#[default(0)]
|
|
78
|
+
RoleAccountsCount { role: Symbol },
|
|
79
|
+
|
|
80
|
+
/// role -> admin role (who can grant/revoke this role). Key removed when no admin.
|
|
81
|
+
#[persistent(Symbol)]
|
|
82
|
+
RoleAdmin { role: Symbol },
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ===========================================================================
|
|
86
|
+
// Trait
|
|
87
|
+
// ===========================================================================
|
|
88
|
+
|
|
89
|
+
/// Trait for contracts with role-based access control.
|
|
90
|
+
///
|
|
91
|
+
/// Extends `Auth` — the authorizer replaces the traditional admin and can grant/revoke
|
|
92
|
+
/// any role. Each role can also have an admin role for hierarchical control.
|
|
93
|
+
#[contract_trait]
|
|
94
|
+
pub trait RoleBasedAccessControl: Auth {
|
|
95
|
+
// ===========================================================================
|
|
96
|
+
// State-changing
|
|
97
|
+
// ===========================================================================
|
|
98
|
+
|
|
99
|
+
/// Grants a role to an account. Caller must be owner or have the role's admin role.
|
|
100
|
+
///
|
|
101
|
+
/// # Arguments
|
|
102
|
+
/// * `account` - The account to grant the role to.
|
|
103
|
+
/// * `role` - The role to grant.
|
|
104
|
+
/// * `caller` - The account that is granting the role. Must be owner or have the role's admin role.
|
|
105
|
+
fn grant_role(
|
|
106
|
+
env: &soroban_sdk::Env,
|
|
107
|
+
account: &soroban_sdk::Address,
|
|
108
|
+
role: &soroban_sdk::Symbol,
|
|
109
|
+
caller: &soroban_sdk::Address,
|
|
110
|
+
) {
|
|
111
|
+
caller.require_auth();
|
|
112
|
+
ensure_if_authorizer_or_role_admin::<Self>(env, role, caller);
|
|
113
|
+
grant_role_no_auth(env, account, role, caller);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Revokes a role from an account. Caller must be owner or have the role's admin role.
|
|
117
|
+
///
|
|
118
|
+
/// # Arguments
|
|
119
|
+
/// * `account` - The account to revoke the role from.
|
|
120
|
+
/// * `role` - The role to revoke.
|
|
121
|
+
/// * `caller` - The account that is revoking the role. Must be owner or have the role's admin role.
|
|
122
|
+
fn revoke_role(
|
|
123
|
+
env: &soroban_sdk::Env,
|
|
124
|
+
account: &soroban_sdk::Address,
|
|
125
|
+
role: &soroban_sdk::Symbol,
|
|
126
|
+
caller: &soroban_sdk::Address,
|
|
127
|
+
) {
|
|
128
|
+
caller.require_auth();
|
|
129
|
+
ensure_if_authorizer_or_role_admin::<Self>(env, role, caller);
|
|
130
|
+
revoke_role_no_auth(env, account, role, caller);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Allows an account to renounce a role assigned to itself.
|
|
134
|
+
/// Users can only renounce roles for their own account.
|
|
135
|
+
///
|
|
136
|
+
/// # Arguments
|
|
137
|
+
/// * `role` - The role to renounce.
|
|
138
|
+
/// * `caller` - The account that is renouncing the role. Must be the account itself.
|
|
139
|
+
fn renounce_role(env: &soroban_sdk::Env, role: &soroban_sdk::Symbol, caller: &soroban_sdk::Address) {
|
|
140
|
+
caller.require_auth();
|
|
141
|
+
revoke_role_no_auth(env, caller, role, caller);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Sets `admin_role` as the admin role of `role`. Caller must be the authorizer.
|
|
145
|
+
///
|
|
146
|
+
/// # Arguments
|
|
147
|
+
/// * `role` - The role to set the admin for.
|
|
148
|
+
/// * `admin_role` - The admin role to set for the role.
|
|
149
|
+
///
|
|
150
|
+
/// # Notes
|
|
151
|
+
///
|
|
152
|
+
/// The admin role can be any `Symbol`, including one with no members. If the admin
|
|
153
|
+
/// role has no members, only the authorizer can grant/revoke the role.
|
|
154
|
+
#[only_auth]
|
|
155
|
+
fn set_role_admin(env: &soroban_sdk::Env, role: &soroban_sdk::Symbol, admin_role: &soroban_sdk::Symbol) {
|
|
156
|
+
set_role_admin_no_auth(env, role, admin_role);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Removes the admin role for a specified role. Caller must be the authorizer.
|
|
160
|
+
///
|
|
161
|
+
/// # Arguments
|
|
162
|
+
/// * `role` - The role to remove the admin for.
|
|
163
|
+
///
|
|
164
|
+
/// # Errors
|
|
165
|
+
/// * `RbacError::AdminRoleNotFound` - If no admin role is set for the role.
|
|
166
|
+
#[only_auth]
|
|
167
|
+
fn remove_role_admin(env: &soroban_sdk::Env, role: &soroban_sdk::Symbol) {
|
|
168
|
+
remove_role_admin_no_auth(env, role);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ===========================================================================
|
|
172
|
+
// View functions
|
|
173
|
+
// ===========================================================================
|
|
174
|
+
|
|
175
|
+
/// Returns `Some(index)` if the account has the specified role, where `index`
|
|
176
|
+
/// is the index of the account in the role. Returns `None` if not.
|
|
177
|
+
///
|
|
178
|
+
/// # Arguments
|
|
179
|
+
/// * `account` - The account to check the role for.
|
|
180
|
+
/// * `role` - The role to check the account for.
|
|
181
|
+
fn has_role(env: &soroban_sdk::Env, account: &soroban_sdk::Address, role: &soroban_sdk::Symbol) -> Option<u32> {
|
|
182
|
+
RbacStorage::role_account_to_index(env, role, account)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// Returns the admin role for a specific role, or None if not set.
|
|
186
|
+
///
|
|
187
|
+
/// # Arguments
|
|
188
|
+
/// * `role` - The role to get the admin for.
|
|
189
|
+
fn get_role_admin(env: &soroban_sdk::Env, role: &soroban_sdk::Symbol) -> Option<soroban_sdk::Symbol> {
|
|
190
|
+
RbacStorage::role_admin(env, role)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Returns the number of accounts that have the specified role.
|
|
194
|
+
///
|
|
195
|
+
/// # Arguments
|
|
196
|
+
/// * `role` - The role to get the member count for.
|
|
197
|
+
fn get_role_member_count(env: &soroban_sdk::Env, role: &soroban_sdk::Symbol) -> u32 {
|
|
198
|
+
RbacStorage::role_accounts_count(env, role)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// Returns the account at the specified index for a given role.
|
|
202
|
+
///
|
|
203
|
+
/// # Arguments
|
|
204
|
+
/// * `role` - The role to get the member for.
|
|
205
|
+
/// * `index` - The index of the member to get.
|
|
206
|
+
///
|
|
207
|
+
/// # Errors
|
|
208
|
+
/// * `RbacError::IndexOutOfBounds` if the index is out of bounds.
|
|
209
|
+
fn get_role_member(env: &soroban_sdk::Env, role: &soroban_sdk::Symbol, index: u32) -> soroban_sdk::Address {
|
|
210
|
+
RbacStorage::role_index_to_account(env, role, index).unwrap_or_panic(env, RbacError::IndexOutOfBounds)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Returns all roles that currently have at least one member.
|
|
214
|
+
/// Defaults to empty vector if no roles exist.
|
|
215
|
+
///
|
|
216
|
+
/// # Notes
|
|
217
|
+
///
|
|
218
|
+
/// This function returns all roles that currently have at least one member.
|
|
219
|
+
/// The maximum number of roles is limited by [`MAX_ROLES`].
|
|
220
|
+
fn get_existing_roles(env: &soroban_sdk::Env) -> soroban_sdk::Vec<soroban_sdk::Symbol> {
|
|
221
|
+
RbacStorage::existing_roles(env)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ===========================================================================
|
|
226
|
+
// Public helpers
|
|
227
|
+
// ===========================================================================
|
|
228
|
+
|
|
229
|
+
/// Ensures the caller has the specified role.
|
|
230
|
+
///
|
|
231
|
+
/// When `role` matches [`AUTHORIZER`], verifies that `caller` is the contract's
|
|
232
|
+
/// authorizer (via [`Auth::authorizer`]) instead of checking RBAC storage.
|
|
233
|
+
///
|
|
234
|
+
/// # Arguments
|
|
235
|
+
/// * `role` - The role to check the caller for.
|
|
236
|
+
/// * `caller` - The account that is being checked. Must have the role.
|
|
237
|
+
///
|
|
238
|
+
/// # Errors
|
|
239
|
+
/// * `Unauthorized` - If the caller does not have the role (or is not the authorizer).
|
|
240
|
+
pub fn ensure_role<T: RoleBasedAccessControl>(env: &Env, role: &Symbol, caller: &Address) {
|
|
241
|
+
if *role == Symbol::new(env, AUTHORIZER) {
|
|
242
|
+
assert_with_error!(env, T::authorizer(env).as_ref() == Some(caller), RbacError::Unauthorized);
|
|
243
|
+
} else {
|
|
244
|
+
assert_with_error!(env, T::has_role(env, caller, role).is_some(), RbacError::Unauthorized);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// Grants a role to an account without auth check.
|
|
249
|
+
///
|
|
250
|
+
/// # Arguments
|
|
251
|
+
/// * `account` - The account to grant the role to.
|
|
252
|
+
/// * `role` - The role to grant.
|
|
253
|
+
/// * `caller` - The account that is granting the role. Must be owner or have the role's admin role.
|
|
254
|
+
///
|
|
255
|
+
/// # Security Warning
|
|
256
|
+
///
|
|
257
|
+
/// **IMPORTANT**: This function bypasses authorization checks and should only
|
|
258
|
+
/// be used:
|
|
259
|
+
/// - During contract initialization/construction
|
|
260
|
+
/// - In admin functions that implement their own authorization logic
|
|
261
|
+
///
|
|
262
|
+
/// Using this function in public-facing methods creates significant security
|
|
263
|
+
/// risks as it could allow unauthorized role assignments.
|
|
264
|
+
pub fn grant_role_no_auth(env: &Env, account: &Address, role: &Symbol, caller: &Address) {
|
|
265
|
+
if RbacStorage::has_role_account_to_index(env, role, account) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
add_to_role_enumeration(env, account, role);
|
|
269
|
+
RoleGranted { role: role.clone(), account: account.clone(), caller: caller.clone() }.publish(env);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Revokes a role from an account without auth check.
|
|
273
|
+
///
|
|
274
|
+
/// # Arguments
|
|
275
|
+
/// * `account` - The account to revoke the role from.
|
|
276
|
+
/// * `role` - The role to revoke.
|
|
277
|
+
/// * `caller` - The account that is revoking the role. Must be owner or have the role's admin role.
|
|
278
|
+
///
|
|
279
|
+
/// # Security Warning
|
|
280
|
+
///
|
|
281
|
+
/// **IMPORTANT**: This function bypasses authorization checks and should only
|
|
282
|
+
/// be used:
|
|
283
|
+
/// - During contract initialization/construction
|
|
284
|
+
/// - In admin functions that implement their own authorization logic
|
|
285
|
+
///
|
|
286
|
+
/// Using this function in public-facing methods creates significant security
|
|
287
|
+
/// risks as it could allow unauthorized role revocations.
|
|
288
|
+
pub fn revoke_role_no_auth(env: &Env, account: &Address, role: &Symbol, caller: &Address) {
|
|
289
|
+
assert_with_error!(env, RbacStorage::has_role_account_to_index(env, role, account), RbacError::RoleNotHeld);
|
|
290
|
+
remove_from_role_enumeration(env, account, role);
|
|
291
|
+
RoleRevoked { role: role.clone(), account: account.clone(), caller: caller.clone() }.publish(env);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// Sets the admin role for a role without auth check. For constructor/init or when caller enforces own auth.
|
|
295
|
+
///
|
|
296
|
+
/// # Arguments
|
|
297
|
+
/// * `role` - The role to set the admin for.
|
|
298
|
+
/// * `admin_role` - The admin role to set for the role.
|
|
299
|
+
///
|
|
300
|
+
/// # Security Warning
|
|
301
|
+
///
|
|
302
|
+
/// **IMPORTANT**: This function bypasses authorization checks and should only
|
|
303
|
+
/// be used:
|
|
304
|
+
/// - During contract initialization/construction
|
|
305
|
+
/// - In admin functions that implement their own authorization logic
|
|
306
|
+
///
|
|
307
|
+
/// Using this function in public-facing methods creates significant security
|
|
308
|
+
/// risks as it could allow unauthorized admin role assignments.
|
|
309
|
+
///
|
|
310
|
+
/// # Circular Admin Warning
|
|
311
|
+
///
|
|
312
|
+
/// **CAUTION**: This function allows the creation of circular admin
|
|
313
|
+
/// relationships between roles. For example, it's possible to assign MINT_ADMIN
|
|
314
|
+
/// as the admin of MINT_ROLE while also making MINT_ROLE the admin of
|
|
315
|
+
/// MINT_ADMIN. Such circular relationships can lead to unintended consequences,
|
|
316
|
+
/// including:
|
|
317
|
+
///
|
|
318
|
+
/// - Race conditions where each role can revoke the other
|
|
319
|
+
/// - Potential security vulnerabilities in role management
|
|
320
|
+
/// - Confusing governance structures that are difficult to reason about
|
|
321
|
+
///
|
|
322
|
+
/// When designing your role hierarchy, carefully consider the relationships
|
|
323
|
+
/// between roles and avoid creating circular dependencies.
|
|
324
|
+
pub fn set_role_admin_no_auth(env: &Env, role: &Symbol, admin_role: &Symbol) {
|
|
325
|
+
let previous = RbacStorage::role_admin(env, role);
|
|
326
|
+
RbacStorage::set_role_admin(env, role, admin_role);
|
|
327
|
+
RoleAdminChanged { role: role.clone(), previous_admin_role: previous, new_admin_role: Some(admin_role.clone()) }
|
|
328
|
+
.publish(env);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/// Removes the admin role for a specified role without auth check.
|
|
332
|
+
///
|
|
333
|
+
/// For use in admin functions that implement their own authorization logic,
|
|
334
|
+
/// or when cleaning up unused roles.
|
|
335
|
+
///
|
|
336
|
+
/// # Arguments
|
|
337
|
+
/// * `role` - The role to remove the admin for.
|
|
338
|
+
///
|
|
339
|
+
/// # Errors
|
|
340
|
+
/// * `RbacError::AdminRoleNotFound` - If no admin role is set for the role.
|
|
341
|
+
///
|
|
342
|
+
/// # Security Warning
|
|
343
|
+
///
|
|
344
|
+
/// **IMPORTANT**: This function bypasses authorization checks and should only
|
|
345
|
+
/// be used:
|
|
346
|
+
/// - In admin functions that implement their own authorization logic
|
|
347
|
+
/// - When cleaning up unused roles
|
|
348
|
+
pub fn remove_role_admin_no_auth(env: &Env, role: &Symbol) {
|
|
349
|
+
let previous = RbacStorage::role_admin(env, role);
|
|
350
|
+
assert_with_error!(env, previous.is_some(), RbacError::AdminRoleNotFound);
|
|
351
|
+
RbacStorage::remove_role_admin(env, role);
|
|
352
|
+
RoleAdminChanged { role: role.clone(), previous_admin_role: previous, new_admin_role: None }.publish(env);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ===========================================================================
|
|
356
|
+
// Private helpers
|
|
357
|
+
// ===========================================================================
|
|
358
|
+
|
|
359
|
+
/// Ensures the caller is the authorizer or has the role's admin role.
|
|
360
|
+
///
|
|
361
|
+
/// # Arguments
|
|
362
|
+
/// * `role` - The role to check the caller for.
|
|
363
|
+
/// * `caller` - The account that is being checked. Must be the authorizer or have the role's admin role.
|
|
364
|
+
///
|
|
365
|
+
/// # Errors
|
|
366
|
+
/// * `Unauthorized` - If the caller is neither the authorizer nor has the role's admin role.
|
|
367
|
+
fn ensure_if_authorizer_or_role_admin<T: RoleBasedAccessControl>(env: &Env, role: &Symbol, caller: &Address) {
|
|
368
|
+
assert_with_error!(
|
|
369
|
+
env,
|
|
370
|
+
T::get_role_admin(env, role).is_some_and(|admin_role| T::has_role(env, caller, &admin_role).is_some())
|
|
371
|
+
|| Some(caller) == T::authorizer(env).as_ref(),
|
|
372
|
+
RbacError::Unauthorized
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/// Adds an account to the role enumeration.
|
|
377
|
+
///
|
|
378
|
+
/// # Arguments
|
|
379
|
+
/// * `account` - The account to add to the role enumeration.
|
|
380
|
+
/// * `role` - The role to add the account to.
|
|
381
|
+
fn add_to_role_enumeration(env: &Env, account: &Address, role: &Symbol) {
|
|
382
|
+
let count = RbacStorage::role_accounts_count(env, role);
|
|
383
|
+
|
|
384
|
+
// If the role has no accounts, add it to the existing roles
|
|
385
|
+
if count == 0 {
|
|
386
|
+
let mut existing = RbacStorage::existing_roles(env);
|
|
387
|
+
assert_with_error!(env, existing.len() < MAX_ROLES, RbacError::MaxRolesExceeded);
|
|
388
|
+
existing.push_back(role.clone());
|
|
389
|
+
RbacStorage::set_existing_roles(env, &existing);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
RbacStorage::set_role_index_to_account(env, role, count, account);
|
|
393
|
+
RbacStorage::set_role_account_to_index(env, role, account, &count);
|
|
394
|
+
RbacStorage::set_role_accounts_count(env, role, &(count + 1));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/// Removes an account from the role enumeration.
|
|
398
|
+
///
|
|
399
|
+
/// # Arguments
|
|
400
|
+
/// * `account` - The account to remove from the role enumeration.
|
|
401
|
+
/// * `role` - The role to remove the account from.
|
|
402
|
+
fn remove_from_role_enumeration(env: &Env, account: &Address, role: &Symbol) {
|
|
403
|
+
let count = RbacStorage::role_accounts_count(env, role);
|
|
404
|
+
assert_with_error!(env, count > 0, RbacError::RoleIsEmpty);
|
|
405
|
+
|
|
406
|
+
// Get the index of the account to remove
|
|
407
|
+
let to_remove_idx =
|
|
408
|
+
RbacStorage::role_account_to_index(env, role, account).unwrap_or_panic(env, RbacError::RoleNotHeld);
|
|
409
|
+
|
|
410
|
+
// Get the index of the last account for the role
|
|
411
|
+
let last_idx = count - 1;
|
|
412
|
+
|
|
413
|
+
// Remove the target account's mappings
|
|
414
|
+
RbacStorage::remove_role_index_to_account(env, role, to_remove_idx);
|
|
415
|
+
RbacStorage::remove_role_account_to_index(env, role, account);
|
|
416
|
+
|
|
417
|
+
// If the removed account wasn't the last, move the last account into the vacated slot
|
|
418
|
+
if to_remove_idx != last_idx {
|
|
419
|
+
// Get the last account and remove the mapping from index to account
|
|
420
|
+
let last_account =
|
|
421
|
+
RbacStorage::role_index_to_account(env, role, last_idx).unwrap_or_panic(env, RbacError::IndexOutOfBounds);
|
|
422
|
+
RbacStorage::remove_role_index_to_account(env, role, last_idx);
|
|
423
|
+
|
|
424
|
+
// Move the last account into the vacated slot
|
|
425
|
+
RbacStorage::set_role_index_to_account(env, role, to_remove_idx, &last_account);
|
|
426
|
+
RbacStorage::set_role_account_to_index(env, role, &last_account, &to_remove_idx);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
RbacStorage::set_role_accounts_count(env, role, &last_idx);
|
|
430
|
+
|
|
431
|
+
// If this was the last account with this role, remove the role from the existing roles
|
|
432
|
+
if last_idx == 0 {
|
|
433
|
+
let mut existing = RbacStorage::existing_roles(env);
|
|
434
|
+
let pos = existing.first_index_of(role).unwrap_or_panic(env, RbacError::RoleNotFound);
|
|
435
|
+
existing.remove(pos);
|
|
436
|
+
RbacStorage::set_existing_roles(env, &existing);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
use soroban_sdk::{testutils::Events, xdr, Address, Env, Event, TryFromVal, Val, Vec};
|
|
2
|
+
|
|
3
|
+
/// Decodes a raw emitted contract event (XDR) into `(topics, data)`.
|
|
4
|
+
///
|
|
5
|
+
/// Soroban testutils exposes events as [`ContractEvents`](soroban_sdk::testutils::ContractEvents),
|
|
6
|
+
/// which internally stores XDR [`ContractEvent`](soroban_sdk::xdr::ContractEvent) values.
|
|
7
|
+
/// This helper converts the XDR topics/data into `soroban_sdk::Val` so tests can inspect
|
|
8
|
+
/// topics and data using normal Soroban conversions (`TryFromVal` / `IntoVal`).
|
|
9
|
+
///
|
|
10
|
+
/// Returns `None` if any topic or the data payload can't be converted into `Val` for the
|
|
11
|
+
/// provided `Env`.
|
|
12
|
+
///
|
|
13
|
+
/// # Example
|
|
14
|
+
/// ```ignore
|
|
15
|
+
/// let events = env.events().all().filter_by_contract(&contract);
|
|
16
|
+
/// for ev in events.events().iter() {
|
|
17
|
+
/// let (topics, data) = utils::testing_utils::decode_event_topics_data(&env, ev).unwrap();
|
|
18
|
+
/// // ... inspect topics/data ...
|
|
19
|
+
/// }
|
|
20
|
+
/// ```
|
|
21
|
+
pub fn decode_event_topics_data(env: &Env, event: &xdr::ContractEvent) -> Option<(Vec<Val>, Val)> {
|
|
22
|
+
// In the current Soroban SDK version this is always `V0`.
|
|
23
|
+
// Using a single-variant match avoids "irrefutable let-else" warnings.
|
|
24
|
+
let v0 = match &event.body {
|
|
25
|
+
xdr::ContractEventBody::V0(v0) => v0,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let mut topics = Vec::<Val>::new(env);
|
|
29
|
+
for t in v0.topics.iter() {
|
|
30
|
+
topics.push_back(Val::try_from_val(env, t).ok()?);
|
|
31
|
+
}
|
|
32
|
+
let data = Val::try_from_val(env, &v0.data).ok()?;
|
|
33
|
+
Some((topics, data))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Asserts that the environment emitted exactly one event: `expected`.
|
|
37
|
+
///
|
|
38
|
+
/// This is a **strict equality** assertion over `env.events().all()`:
|
|
39
|
+
/// the full event list must be exactly `[expected]` (no extra events).
|
|
40
|
+
///
|
|
41
|
+
/// Uses the event struct's `.topics()` and `.data()` methods (generated by `#[contractevent]`)
|
|
42
|
+
/// to compare against emitted events. No more hardcoding topic names or field keys!
|
|
43
|
+
///
|
|
44
|
+
/// # Example
|
|
45
|
+
/// ```ignore
|
|
46
|
+
/// use crate::events::OwnershipTransferred;
|
|
47
|
+
///
|
|
48
|
+
/// let expected = OwnershipTransferred {
|
|
49
|
+
/// previous_owner: owner.clone(),
|
|
50
|
+
/// new_owner: new_owner.clone(),
|
|
51
|
+
/// };
|
|
52
|
+
/// assert_eq_event(&env, &contract, expected);
|
|
53
|
+
/// ```
|
|
54
|
+
pub fn assert_eq_event<E: Event>(env: &Env, contract: &Address, expected: E) {
|
|
55
|
+
// Compare against the emitted `xdr::ContractEvent` directly. This avoids the
|
|
56
|
+
// `xdr -> Val` roundtrip and ensures we match exactly what was emitted.
|
|
57
|
+
//
|
|
58
|
+
// IMPORTANT: This asserts *equality* (not "contains").
|
|
59
|
+
extern crate std;
|
|
60
|
+
|
|
61
|
+
assert_eq!(
|
|
62
|
+
env.events().all(),
|
|
63
|
+
std::vec![expected.to_xdr(env, contract)],
|
|
64
|
+
"Expected exactly one event. Expected topics: {:?}, data: {:?}",
|
|
65
|
+
expected.topics(env),
|
|
66
|
+
expected.data(env),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Asserts that the contract emitted an event equal to `expected`.
|
|
71
|
+
///
|
|
72
|
+
/// This is a **contains** assertion: after filtering events by `contract`,
|
|
73
|
+
/// `expected` must appear at least once (order doesn't matter; extra events allowed).
|
|
74
|
+
///
|
|
75
|
+
/// Uses the event struct's `.topics()` and `.data()` methods (generated by `#[contractevent]`)
|
|
76
|
+
/// to compare against emitted events. No more hardcoding topic names or field keys!
|
|
77
|
+
///
|
|
78
|
+
/// # Example
|
|
79
|
+
/// ```ignore
|
|
80
|
+
/// use crate::events::OwnershipTransferred;
|
|
81
|
+
///
|
|
82
|
+
/// let expected = OwnershipTransferred {
|
|
83
|
+
/// previous_owner: owner.clone(),
|
|
84
|
+
/// new_owner: new_owner.clone(),
|
|
85
|
+
/// };
|
|
86
|
+
/// assert_eq_event(&env, &contract, expected);
|
|
87
|
+
/// ```
|
|
88
|
+
pub fn assert_contains_event<E: Event>(env: &Env, contract: &Address, expected: E) {
|
|
89
|
+
// Compare against the emitted `xdr::ContractEvent` directly. This avoids the
|
|
90
|
+
// `xdr -> Val` roundtrip and ensures we match exactly what was emitted.
|
|
91
|
+
let expected_xdr = expected.to_xdr(env, contract);
|
|
92
|
+
|
|
93
|
+
let events = env.events().all().filter_by_contract(contract);
|
|
94
|
+
let mut found = false;
|
|
95
|
+
for event in events.events().iter() {
|
|
96
|
+
if *event == expected_xdr {
|
|
97
|
+
found = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
assert!(
|
|
103
|
+
found,
|
|
104
|
+
"Expected event not found. Expected topics: {:?}, data: {:?}",
|
|
105
|
+
expected.topics(env),
|
|
106
|
+
expected.data(env)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Asserts that the environment emitted events that match `expected_events` exactly.
|
|
111
|
+
///
|
|
112
|
+
/// This is a **strict equality** assertion over `env.events().all()`:
|
|
113
|
+
/// the full event list must match `expected_events` **in order** and **count**
|
|
114
|
+
/// (no missing/extra events).
|
|
115
|
+
///
|
|
116
|
+
/// # Example
|
|
117
|
+
/// ```ignore
|
|
118
|
+
/// assert_eq_events(env, &contract, &[
|
|
119
|
+
/// &Event1 { ... },
|
|
120
|
+
/// &Event2 { ... },
|
|
121
|
+
/// ]);
|
|
122
|
+
/// ```
|
|
123
|
+
|
|
124
|
+
pub fn assert_eq_events(env: &Env, contract: &Address, expected_events: &[&dyn Event]) {
|
|
125
|
+
// Note: this module is only compiled for tests / testutils usage, where `std` is available.
|
|
126
|
+
extern crate std;
|
|
127
|
+
let expected_xdrs: std::vec::Vec<xdr::ContractEvent> =
|
|
128
|
+
expected_events.iter().map(|e| e.to_xdr(env, contract)).collect();
|
|
129
|
+
|
|
130
|
+
assert_eq!(env.events().all(), expected_xdrs, "Expected events to match exactly");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Asserts that all `expected_events` were emitted by the contract (in any order).
|
|
134
|
+
///
|
|
135
|
+
/// This is a **contains** assertion with multiset semantics: after filtering events by
|
|
136
|
+
/// `contract`, every expected event must be present, and duplicates in `expected_events`
|
|
137
|
+
/// require duplicate emissions. Extra emitted events are allowed.
|
|
138
|
+
///
|
|
139
|
+
/// # Example
|
|
140
|
+
/// ```ignore
|
|
141
|
+
/// assert_eq_events(env, &contract, &[
|
|
142
|
+
/// &Event1 { ... },
|
|
143
|
+
/// &Event2 { ... },
|
|
144
|
+
/// ]);
|
|
145
|
+
/// ```
|
|
146
|
+
|
|
147
|
+
pub fn assert_contains_events(env: &Env, contract: &Address, expected_events: &[&dyn Event]) {
|
|
148
|
+
// Note: this module is only compiled for tests / testutils usage, where `std` is available.
|
|
149
|
+
extern crate std;
|
|
150
|
+
let events = env.events().all().filter_by_contract(contract);
|
|
151
|
+
let raw_events = events.events();
|
|
152
|
+
// Track which emitted events have already been matched so that duplicate expectations
|
|
153
|
+
// require duplicate emissions (multiset semantics).
|
|
154
|
+
// We use a `std::vec::Vec<bool>` as a "used" mask to implement multiset matching.
|
|
155
|
+
let mut used: std::vec::Vec<bool> = std::vec![false; raw_events.len()];
|
|
156
|
+
|
|
157
|
+
for (i, expected) in expected_events.iter().enumerate() {
|
|
158
|
+
let expected_xdr = expected.to_xdr(env, contract);
|
|
159
|
+
|
|
160
|
+
let mut found = false;
|
|
161
|
+
for (idx, event) in raw_events.iter().enumerate() {
|
|
162
|
+
if used[idx] {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if *event == expected_xdr {
|
|
166
|
+
used[idx] = true;
|
|
167
|
+
found = true;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
assert!(
|
|
173
|
+
found,
|
|
174
|
+
"Expected event #{} not found. Expected topics: {:?}, data: {:?}",
|
|
175
|
+
i,
|
|
176
|
+
expected.topics(env),
|
|
177
|
+
expected.data(env)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|