@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
|
@@ -27,14 +27,14 @@ pub fn generate_ownable_impl(input: TokenStream) -> TokenStream {
|
|
|
27
27
|
quote! {
|
|
28
28
|
#item_struct
|
|
29
29
|
|
|
30
|
-
use utils::{auth::Auth as _,
|
|
30
|
+
use utils::{auth::Auth as _, ownable::{Ownable as _, OwnableInitializer as _}};
|
|
31
31
|
|
|
32
32
|
impl utils::ownable::OwnableInitializer for #name {}
|
|
33
33
|
|
|
34
34
|
#[common_macros::contract_impl]
|
|
35
35
|
impl utils::auth::Auth for #name {
|
|
36
|
-
fn authorizer(env: &soroban_sdk::Env) -> soroban_sdk::Address {
|
|
37
|
-
<Self as utils::ownable::Ownable>::owner(env)
|
|
36
|
+
fn authorizer(env: &soroban_sdk::Env) -> Option<soroban_sdk::Address> {
|
|
37
|
+
<Self as utils::ownable::Ownable>::owner(env)
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -63,8 +63,8 @@ pub fn generate_multisig_impl(input: TokenStream) -> TokenStream {
|
|
|
63
63
|
|
|
64
64
|
#[common_macros::contract_impl]
|
|
65
65
|
impl utils::auth::Auth for #name {
|
|
66
|
-
fn authorizer(env: &soroban_sdk::Env) -> soroban_sdk::Address {
|
|
67
|
-
env.current_contract_address()
|
|
66
|
+
fn authorizer(env: &soroban_sdk::Env) -> Option<soroban_sdk::Address> {
|
|
67
|
+
Some(env.current_contract_address())
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
//! - [`lz_contract`] - Wrapper macro combining common LayerZero contract attributes
|
|
8
8
|
//! - [`multisig`] - MultiSig trait implementation macro
|
|
9
9
|
//! - [`only_auth`] - Auth-based access control attribute macro
|
|
10
|
+
//! - [`only_role`] - RBAC role check with auth attribute macro
|
|
11
|
+
//! - [`has_role`] - RBAC role check attribute macro
|
|
10
12
|
//! - [`ownable`] - Ownable trait implementation macro
|
|
11
13
|
//! - [`storage`] - Storage enum to API macro
|
|
12
14
|
//! - [`ttl_configurable`] - TTL configuration with freeze support
|
|
@@ -20,6 +22,7 @@ mod auth;
|
|
|
20
22
|
mod contract_ttl;
|
|
21
23
|
mod error;
|
|
22
24
|
mod lz_contract;
|
|
25
|
+
mod rbac;
|
|
23
26
|
mod storage;
|
|
24
27
|
mod ttl_configurable;
|
|
25
28
|
mod ttl_extendable;
|
|
@@ -232,6 +235,72 @@ pub fn only_auth(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
|
232
235
|
auth::prepend_only_auth_check(item.into()).into()
|
|
233
236
|
}
|
|
234
237
|
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// RBAC Macros
|
|
240
|
+
// ============================================================================
|
|
241
|
+
|
|
242
|
+
/// Checks that the given account has the specified role.
|
|
243
|
+
///
|
|
244
|
+
/// Injects a role check at the start of the function. Panics with
|
|
245
|
+
/// `RbacError::Unauthorized` if the account does not have the role (aligns with OpenZeppelin).
|
|
246
|
+
///
|
|
247
|
+
/// # Requirements
|
|
248
|
+
/// - The function must have an `Env` parameter
|
|
249
|
+
/// - The function must have a parameter matching the first macro arg (of type `Address` or `&Address`)
|
|
250
|
+
/// - The contract must use `utils::rbac` (e.g. `RbacStorage` or `RoleBasedAccessControl`)
|
|
251
|
+
///
|
|
252
|
+
/// # Example
|
|
253
|
+
/// ```ignore
|
|
254
|
+
/// #[has_role(caller, "minter")]
|
|
255
|
+
/// pub fn mint(env: Env, caller: Address, amount: i128) { ... }
|
|
256
|
+
///
|
|
257
|
+
/// // Or with a &str constant:
|
|
258
|
+
/// const MINTER_ROLE: &str = "minter";
|
|
259
|
+
/// #[has_role(caller, MINTER_ROLE)]
|
|
260
|
+
/// pub fn mint(env: Env, caller: Address, amount: i128) { ... }
|
|
261
|
+
/// ```
|
|
262
|
+
///
|
|
263
|
+
/// # Generated code
|
|
264
|
+
/// ```ignore
|
|
265
|
+
/// pub fn mint(env: Env, caller: Address, amount: i128) {
|
|
266
|
+
/// utils::rbac::ensure_role(&env, &soroban_sdk::Symbol::new(&env, "minter"), &caller);
|
|
267
|
+
/// // Original function body
|
|
268
|
+
/// }
|
|
269
|
+
/// ```
|
|
270
|
+
#[proc_macro_attribute]
|
|
271
|
+
pub fn has_role(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
272
|
+
rbac::generate_role_check(attr.into(), item.into(), false).into()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// Checks that the given account has the specified role and requires auth.
|
|
276
|
+
///
|
|
277
|
+
/// Same as `#[has_role]` but also calls `account.require_auth()` to ensure
|
|
278
|
+
/// the caller has authorized the transaction.
|
|
279
|
+
///
|
|
280
|
+
/// # Requirements
|
|
281
|
+
/// Same as `#[has_role]`.
|
|
282
|
+
///
|
|
283
|
+
/// # Example
|
|
284
|
+
/// ```ignore
|
|
285
|
+
/// #[only_role(caller, "minter")]
|
|
286
|
+
/// pub fn mint(env: Env, caller: Address, amount: i128) { ... }
|
|
287
|
+
///
|
|
288
|
+
/// // Or with a &str constant: #[only_role(caller, MINTER_ROLE)]
|
|
289
|
+
/// ```
|
|
290
|
+
///
|
|
291
|
+
/// # Generated code
|
|
292
|
+
/// ```ignore
|
|
293
|
+
/// pub fn mint(env: Env, caller: Address, amount: i128) {
|
|
294
|
+
/// utils::rbac::ensure_role(&env, &soroban_sdk::Symbol::new(&env, "minter"), &caller);
|
|
295
|
+
/// caller.require_auth();
|
|
296
|
+
/// // Original function body
|
|
297
|
+
/// }
|
|
298
|
+
/// ```
|
|
299
|
+
#[proc_macro_attribute]
|
|
300
|
+
pub fn only_role(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
301
|
+
rbac::generate_role_check(attr.into(), item.into(), true).into()
|
|
302
|
+
}
|
|
303
|
+
|
|
235
304
|
// ============================================================================
|
|
236
305
|
// TTL Configuration Macro
|
|
237
306
|
// ============================================================================
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//! RBAC attribute macros for Stellar contracts.
|
|
2
|
+
//!
|
|
3
|
+
//! Provides `#[has_role]` and `#[only_role]` for role-based access control,
|
|
4
|
+
//! delegating to `utils::rbac::ensure_role`.
|
|
5
|
+
|
|
6
|
+
use crate::utils;
|
|
7
|
+
use proc_macro2::TokenStream;
|
|
8
|
+
use quote::{quote, ToTokens};
|
|
9
|
+
use syn::parse_quote;
|
|
10
|
+
use syn::{
|
|
11
|
+
parse::{Parse, ParseStream},
|
|
12
|
+
Expr, FnArg, Ident, ItemFn, Pat, Token, Type,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/// Helper that generates the role check for both `has_role` and `only_role`.
|
|
16
|
+
/// If `require_auth` is true, also injects `account.require_auth()`.
|
|
17
|
+
pub fn generate_role_check(args: TokenStream, input: TokenStream, require_auth: bool) -> TokenStream {
|
|
18
|
+
let HasRoleArgs { param, role } =
|
|
19
|
+
syn::parse2(args).unwrap_or_else(|e| panic!("failed to parse has_role/only_role args: {}", e));
|
|
20
|
+
let mut input_fn: ItemFn = syn::parse2(input).unwrap_or_else(|e| panic!("failed to parse function: {}", e));
|
|
21
|
+
|
|
22
|
+
let is_address_ref = validate_address_type(&input_fn, ¶m);
|
|
23
|
+
let param_ref = if is_address_ref { quote!(#param) } else { quote!(&#param) };
|
|
24
|
+
|
|
25
|
+
let env_param = utils::expect_env_param(&input_fn.sig.inputs);
|
|
26
|
+
let env_ref = env_param.as_ref_tokens();
|
|
27
|
+
|
|
28
|
+
// Insert the role check at the beginning of the function body
|
|
29
|
+
input_fn.block.stmts.insert(
|
|
30
|
+
0,
|
|
31
|
+
parse_quote!(utils::rbac::ensure_role(#env_ref, &soroban_sdk::Symbol::new(#env_ref, #role), #param_ref);),
|
|
32
|
+
);
|
|
33
|
+
if require_auth {
|
|
34
|
+
input_fn.block.stmts.insert(1, parse_quote!(#param.require_auth();));
|
|
35
|
+
}
|
|
36
|
+
input_fn.into_token_stream()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
struct HasRoleArgs {
|
|
40
|
+
param: Ident,
|
|
41
|
+
role: Expr,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl Parse for HasRoleArgs {
|
|
45
|
+
fn parse(input: ParseStream) -> syn::Result<Self> {
|
|
46
|
+
// Parse the parameter name (the account identifier to check)
|
|
47
|
+
let param: Ident = input.parse()?;
|
|
48
|
+
// Expect a comma separator between param and role
|
|
49
|
+
input.parse::<Token![,]>()?;
|
|
50
|
+
// Parse the role expression (e.g., a string literal or constant)
|
|
51
|
+
let role: Expr = input.parse()?;
|
|
52
|
+
Ok(HasRoleArgs { param, role })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Looks up `param_name` in the function signature and validates that its type
|
|
57
|
+
/// is `Address` or `&Address`. Returns `true` when the parameter is a reference,
|
|
58
|
+
/// so the caller knows whether an extra `&` is needed when forwarding it.
|
|
59
|
+
///
|
|
60
|
+
/// Panics at macro-expansion time if the parameter doesn't exist.
|
|
61
|
+
fn validate_address_type(func: &ItemFn, param_name: &Ident) -> bool {
|
|
62
|
+
for arg in &func.sig.inputs {
|
|
63
|
+
let FnArg::Typed(pat_type) = arg else { continue };
|
|
64
|
+
let Pat::Ident(pat_ident) = &*pat_type.pat else { continue };
|
|
65
|
+
if pat_ident.ident != *param_name {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
return match &*pat_type.ty {
|
|
69
|
+
Type::Reference(r) => {
|
|
70
|
+
assert_is_address(&r.elem, param_name);
|
|
71
|
+
true
|
|
72
|
+
}
|
|
73
|
+
ty => {
|
|
74
|
+
assert_is_address(ty, param_name);
|
|
75
|
+
false
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
panic!("Parameter `{param_name}` not found in function signature");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Asserts that the type path resolves to `Address`, panicking otherwise.
|
|
83
|
+
fn assert_is_address(ty: &Type, param_name: &Ident) {
|
|
84
|
+
let Type::Path(tp) = ty else {
|
|
85
|
+
panic!("Parameter `{param_name}` must be of type `Address` or `&Address`");
|
|
86
|
+
};
|
|
87
|
+
if tp.path.segments.last().is_none_or(|s| s.ident != "Address") {
|
|
88
|
+
panic!("Parameter `{param_name}` must be of type `Address` or `&Address`");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -22,8 +22,10 @@ fn snapshot_generated_lz_contract_code() {
|
|
|
22
22
|
&syn::parse2::<syn::File>(multisig_upgradeable_result).expect("failed to parse generated code"),
|
|
23
23
|
);
|
|
24
24
|
|
|
25
|
-
let upgradeable_no_migration_result =
|
|
26
|
-
|
|
25
|
+
let upgradeable_no_migration_result = crate::lz_contract::generate_lz_contract(
|
|
26
|
+
quote! { upgradeable(no_migration) },
|
|
27
|
+
quote! { pub struct MyContract; },
|
|
28
|
+
);
|
|
27
29
|
let upgradeable_no_migration_formatted = prettyplease::unparse(
|
|
28
30
|
&syn::parse2::<syn::File>(upgradeable_no_migration_result).expect("failed to parse generated code"),
|
|
29
31
|
);
|
|
@@ -45,11 +47,7 @@ fn test_lz_contract_invalid_config_table_driven() {
|
|
|
45
47
|
let input = quote! { pub struct MyContract; };
|
|
46
48
|
|
|
47
49
|
let cases: Vec<(&str, TokenStream, &str)> = vec![
|
|
48
|
-
(
|
|
49
|
-
"unknown option",
|
|
50
|
-
quote! { not_a_real_option },
|
|
51
|
-
"expected one of `upgradeable`, `multisig`",
|
|
52
|
-
),
|
|
50
|
+
("unknown option", quote! { not_a_real_option }, "expected one of `upgradeable`, `multisig`"),
|
|
53
51
|
("invalid attr syntax", quote! { 123 }, "failed to parse lz_contract config"),
|
|
54
52
|
("upgradeable(bad_inner)", quote! { upgradeable(not_migration) }, "expected `no_migration`"),
|
|
55
53
|
(
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
use proc_macro2::TokenStream;
|
|
2
|
+
use quote::quote;
|
|
3
|
+
|
|
4
|
+
use crate::tests::test_helpers::{assert_panics_contains, filter_item_inputs_excluding_labels};
|
|
5
|
+
|
|
6
|
+
// ============================================
|
|
7
|
+
// Snapshot Test: has_role and only_role
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
#[test]
|
|
11
|
+
fn snapshot_preserve_function_signature() {
|
|
12
|
+
let args = quote! { caller, "minter" };
|
|
13
|
+
let input = quote! {
|
|
14
|
+
pub fn mint(env: Env, caller: Address, amount: i128) {
|
|
15
|
+
// mint logic
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let has_role_result = crate::rbac::generate_role_check(args.clone(), input.clone(), false);
|
|
20
|
+
let has_role_formatted =
|
|
21
|
+
prettyplease::unparse(&syn::parse2::<syn::File>(has_role_result).expect("failed to parse generated code"));
|
|
22
|
+
|
|
23
|
+
let only_role_result = crate::rbac::generate_role_check(args, input, true);
|
|
24
|
+
let only_role_formatted =
|
|
25
|
+
prettyplease::unparse(&syn::parse2::<syn::File>(only_role_result).expect("failed to parse generated code"));
|
|
26
|
+
|
|
27
|
+
let combined =
|
|
28
|
+
format!("// === has_role ===\n\n{}\n\n// === only_role ===\n\n{}", has_role_formatted, only_role_formatted);
|
|
29
|
+
|
|
30
|
+
insta::assert_snapshot!(combined);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================
|
|
34
|
+
// generate_role_check assertion tests
|
|
35
|
+
// ============================================
|
|
36
|
+
|
|
37
|
+
fn assert_stmt_eq(stmt: &syn::Stmt, expected: &str, test_name: &str) {
|
|
38
|
+
let actual = quote::quote!(#stmt).to_string().replace(" ", "");
|
|
39
|
+
assert_eq!(actual, expected, "{test_name}: expected '{expected}', got '{actual}'");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn assert_role_check_exact_stmts(
|
|
43
|
+
args: TokenStream,
|
|
44
|
+
input: TokenStream,
|
|
45
|
+
require_auth: bool,
|
|
46
|
+
expected_ensure_stmt_no_spaces: &str,
|
|
47
|
+
expected_auth_stmt_no_spaces: Option<&str>,
|
|
48
|
+
expected_stmt_count: Option<usize>,
|
|
49
|
+
test_name: &str,
|
|
50
|
+
) -> syn::ItemFn {
|
|
51
|
+
let result_tokens = crate::rbac::generate_role_check(args, input, require_auth);
|
|
52
|
+
let output_fn: syn::ItemFn =
|
|
53
|
+
syn::parse2(result_tokens).unwrap_or_else(|e| panic!("{test_name}: failed to parse output function: {e}"));
|
|
54
|
+
|
|
55
|
+
assert!(!output_fn.block.stmts.is_empty(), "{test_name}: function body should contain at least one statement");
|
|
56
|
+
|
|
57
|
+
assert_stmt_eq(&output_fn.block.stmts[0], expected_ensure_stmt_no_spaces, test_name);
|
|
58
|
+
|
|
59
|
+
if let Some(expected_auth) = expected_auth_stmt_no_spaces {
|
|
60
|
+
assert!(output_fn.block.stmts.len() >= 2, "{test_name}: expected at least two statements");
|
|
61
|
+
assert_stmt_eq(&output_fn.block.stmts[1], expected_auth, test_name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if let Some(expected_count) = expected_stmt_count {
|
|
65
|
+
assert_eq!(
|
|
66
|
+
output_fn.block.stmts.len(),
|
|
67
|
+
expected_count,
|
|
68
|
+
"{test_name}: expected {expected_count} statements, got {}",
|
|
69
|
+
output_fn.block.stmts.len()
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
output_fn
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[test]
|
|
77
|
+
fn test_role_check_inserts_expected_statements_table_driven() {
|
|
78
|
+
struct Case {
|
|
79
|
+
name: &'static str,
|
|
80
|
+
args: TokenStream,
|
|
81
|
+
input: TokenStream,
|
|
82
|
+
require_auth: bool,
|
|
83
|
+
expected_ensure_stmt: &'static str,
|
|
84
|
+
expected_auth_stmt: Option<&'static str>,
|
|
85
|
+
expected_stmt_count: usize,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let cases = vec![
|
|
89
|
+
Case {
|
|
90
|
+
name: "has_role: Env ref + Address value",
|
|
91
|
+
args: quote! { caller, "minter" },
|
|
92
|
+
input: quote! { pub fn mint(env: &Env, caller: Address, amount: i128) {} },
|
|
93
|
+
require_auth: false,
|
|
94
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,\"minter\"),&caller);",
|
|
95
|
+
expected_auth_stmt: None,
|
|
96
|
+
expected_stmt_count: 1,
|
|
97
|
+
},
|
|
98
|
+
Case {
|
|
99
|
+
name: "only_role: Env owned + Address value",
|
|
100
|
+
args: quote! { caller, "minter" },
|
|
101
|
+
input: quote! { pub fn mint(env: Env, caller: Address, amount: i128) {} },
|
|
102
|
+
require_auth: true,
|
|
103
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(&env,&soroban_sdk::Symbol::new(&env,\"minter\"),&caller);",
|
|
104
|
+
expected_auth_stmt: Some("caller.require_auth();"),
|
|
105
|
+
expected_stmt_count: 2,
|
|
106
|
+
},
|
|
107
|
+
Case {
|
|
108
|
+
name: "only_role: Env ref + Address value",
|
|
109
|
+
args: quote! { caller, "minter" },
|
|
110
|
+
input: quote! { pub fn mint(env: &Env, caller: Address, amount: i128) {} },
|
|
111
|
+
require_auth: true,
|
|
112
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,\"minter\"),&caller);",
|
|
113
|
+
expected_auth_stmt: Some("caller.require_auth();"),
|
|
114
|
+
expected_stmt_count: 2,
|
|
115
|
+
},
|
|
116
|
+
Case {
|
|
117
|
+
name: "has_role: &Address param uses account directly",
|
|
118
|
+
args: quote! { account, "admin" },
|
|
119
|
+
input: quote! { pub fn admin_action(env: Env, account: &Address) {} },
|
|
120
|
+
require_auth: false,
|
|
121
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(&env,&soroban_sdk::Symbol::new(&env,\"admin\"),account);",
|
|
122
|
+
expected_auth_stmt: None,
|
|
123
|
+
expected_stmt_count: 1,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
for c in cases {
|
|
128
|
+
assert_role_check_exact_stmts(
|
|
129
|
+
c.args,
|
|
130
|
+
c.input,
|
|
131
|
+
c.require_auth,
|
|
132
|
+
c.expected_ensure_stmt,
|
|
133
|
+
c.expected_auth_stmt,
|
|
134
|
+
Some(c.expected_stmt_count),
|
|
135
|
+
c.name,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[test]
|
|
141
|
+
fn test_has_role_role_arg_variants_generate_expected_symbol_new_table_driven() {
|
|
142
|
+
struct Case {
|
|
143
|
+
name: &'static str,
|
|
144
|
+
args: TokenStream,
|
|
145
|
+
input: TokenStream,
|
|
146
|
+
expected_ensure_stmt: &'static str,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let cases = vec![
|
|
150
|
+
Case {
|
|
151
|
+
name: "role string literal passed to Symbol::new (Env ref)",
|
|
152
|
+
args: quote! { caller, "minter" },
|
|
153
|
+
input: quote! { pub fn mint(env: &Env, caller: Address) {} },
|
|
154
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,\"minter\"),&caller);",
|
|
155
|
+
},
|
|
156
|
+
Case {
|
|
157
|
+
name: "role const expr passed to Symbol::new (Env ref)",
|
|
158
|
+
args: quote! { caller, MINTER_ROLE },
|
|
159
|
+
input: quote! { pub fn mint(env: &Env, caller: Address) {} },
|
|
160
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,MINTER_ROLE),&caller);",
|
|
161
|
+
},
|
|
162
|
+
Case {
|
|
163
|
+
name: "role const expr passed to Symbol::new (Env owned)",
|
|
164
|
+
args: quote! { caller, MINTER_ROLE },
|
|
165
|
+
input: quote! { pub fn mint(env: Env, caller: Address) {} },
|
|
166
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(&env,&soroban_sdk::Symbol::new(&env,MINTER_ROLE),&caller);",
|
|
167
|
+
},
|
|
168
|
+
Case {
|
|
169
|
+
name: "role path expr passed to Symbol::new",
|
|
170
|
+
args: quote! { caller, roles::MINTER_ROLE },
|
|
171
|
+
input: quote! { pub fn mint(env: &Env, caller: Address) {} },
|
|
172
|
+
expected_ensure_stmt:
|
|
173
|
+
"utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,roles::MINTER_ROLE),&caller);",
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
for c in cases {
|
|
178
|
+
assert_role_check_exact_stmts(c.args, c.input, false, c.expected_ensure_stmt, None, Some(1), c.name);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================
|
|
183
|
+
// Error cases: invalid args (table-driven)
|
|
184
|
+
// ============================================
|
|
185
|
+
|
|
186
|
+
#[test]
|
|
187
|
+
fn test_has_role_rejects_invalid_args_table_driven() {
|
|
188
|
+
struct Case {
|
|
189
|
+
name: &'static str,
|
|
190
|
+
args: TokenStream,
|
|
191
|
+
expected_substring: &'static str,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let input = quote! { pub fn mint(env: Env, caller: Address) {} };
|
|
195
|
+
let cases = vec![
|
|
196
|
+
Case {
|
|
197
|
+
name: "missing comma in args",
|
|
198
|
+
args: quote! { caller "minter" },
|
|
199
|
+
expected_substring: "failed to parse has_role/only_role args",
|
|
200
|
+
},
|
|
201
|
+
Case {
|
|
202
|
+
name: "missing role",
|
|
203
|
+
args: quote! { caller, },
|
|
204
|
+
expected_substring: "failed to parse has_role/only_role args",
|
|
205
|
+
},
|
|
206
|
+
Case {
|
|
207
|
+
name: "extra tokens in args",
|
|
208
|
+
args: quote! { caller, "minter", extra },
|
|
209
|
+
expected_substring: "failed to parse has_role/only_role args",
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
for c in cases {
|
|
214
|
+
assert_panics_contains(c.name, c.expected_substring, || {
|
|
215
|
+
crate::rbac::generate_role_check(c.args.clone(), input.clone(), false);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ============================================
|
|
221
|
+
// Error cases: invalid function signature inputs (table-driven)
|
|
222
|
+
// ============================================
|
|
223
|
+
|
|
224
|
+
#[test]
|
|
225
|
+
fn test_has_role_rejects_invalid_function_signature_table_driven() {
|
|
226
|
+
struct Case {
|
|
227
|
+
name: &'static str,
|
|
228
|
+
args: TokenStream,
|
|
229
|
+
input: TokenStream,
|
|
230
|
+
expected_substring: &'static str,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let args = quote! { caller, "minter" };
|
|
234
|
+
let cases = vec![
|
|
235
|
+
Case {
|
|
236
|
+
name: "no Env param",
|
|
237
|
+
args: args.clone(),
|
|
238
|
+
input: quote! { pub fn mint(caller: Address, amount: i128) {} },
|
|
239
|
+
expected_substring: "function must have an Env argument",
|
|
240
|
+
},
|
|
241
|
+
Case {
|
|
242
|
+
name: "param not in signature",
|
|
243
|
+
args: args.clone(),
|
|
244
|
+
input: quote! { pub fn mint(env: Env, account: Address, amount: i128) {} },
|
|
245
|
+
expected_substring: "not found in function signature",
|
|
246
|
+
},
|
|
247
|
+
Case {
|
|
248
|
+
name: "param not Address",
|
|
249
|
+
args: args.clone(),
|
|
250
|
+
input: quote! { pub fn mint(env: Env, caller: u32, amount: i128) {} },
|
|
251
|
+
expected_substring: "must be of type `Address` or `&Address`",
|
|
252
|
+
},
|
|
253
|
+
Case {
|
|
254
|
+
name: "wildcard Env pattern",
|
|
255
|
+
args: args.clone(),
|
|
256
|
+
input: quote! { pub fn mint(_: Env, caller: Address) {} },
|
|
257
|
+
expected_substring: "function must have an Env argument",
|
|
258
|
+
},
|
|
259
|
+
Case {
|
|
260
|
+
name: "tuple Env pattern",
|
|
261
|
+
args: args.clone(),
|
|
262
|
+
input: quote! { pub fn mint((env, _): (&Env, u32), caller: Address) { let _ = env; } },
|
|
263
|
+
expected_substring: "function must have an Env argument",
|
|
264
|
+
},
|
|
265
|
+
Case {
|
|
266
|
+
name: "struct Env pattern",
|
|
267
|
+
args: args.clone(),
|
|
268
|
+
input: quote! { pub fn mint(Env { .. }: Env, caller: Address) {} },
|
|
269
|
+
expected_substring: "function must have an Env argument",
|
|
270
|
+
},
|
|
271
|
+
Case {
|
|
272
|
+
name: "&&Address is invalid",
|
|
273
|
+
args: args.clone(),
|
|
274
|
+
input: quote! { pub fn mint(env: &Env, caller: &&Address) {} },
|
|
275
|
+
expected_substring: "must be of type `Address` or `&Address`",
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
for c in cases {
|
|
280
|
+
assert_panics_contains(c.name, c.expected_substring, || {
|
|
281
|
+
crate::rbac::generate_role_check(c.args.clone(), c.input.clone(), false);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================
|
|
287
|
+
// Error cases: non-function input
|
|
288
|
+
// ============================================
|
|
289
|
+
|
|
290
|
+
#[test]
|
|
291
|
+
fn test_has_role_rejects_non_function_inputs() {
|
|
292
|
+
let args = quote! { caller, "minter" };
|
|
293
|
+
for (case, input) in filter_item_inputs_excluding_labels(&["function"]) {
|
|
294
|
+
assert_panics_contains(case, "failed to parse function", || {
|
|
295
|
+
crate::rbac::generate_role_check(args.clone(), input.clone(), false);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============================================
|
|
301
|
+
// High-value coverage: Env + Address + role variants (table-driven)
|
|
302
|
+
// ============================================
|
|
303
|
+
|
|
304
|
+
#[test]
|
|
305
|
+
fn test_has_role_env_variants_generate_correct_env_ref_in_ensure_role() {
|
|
306
|
+
struct Case {
|
|
307
|
+
name: &'static str,
|
|
308
|
+
input: TokenStream,
|
|
309
|
+
expected_ensure_stmt: &'static str,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Role is a literal so we also validate Symbol::new uses the same env_ref form.
|
|
313
|
+
let args = quote! { caller, "minter" };
|
|
314
|
+
|
|
315
|
+
let cases = vec![
|
|
316
|
+
Case {
|
|
317
|
+
name: "owned Env",
|
|
318
|
+
input: quote! { pub fn mint(env: Env, caller: Address) {} },
|
|
319
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(&env,&soroban_sdk::Symbol::new(&env,\"minter\"),&caller);",
|
|
320
|
+
},
|
|
321
|
+
Case {
|
|
322
|
+
name: "ref Env",
|
|
323
|
+
input: quote! { pub fn mint(env: &Env, caller: Address) {} },
|
|
324
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,\"minter\"),&caller);",
|
|
325
|
+
},
|
|
326
|
+
Case {
|
|
327
|
+
name: "mut ref Env",
|
|
328
|
+
input: quote! { pub fn mint(env: &mut Env, caller: Address) {} },
|
|
329
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,\"minter\"),&caller);",
|
|
330
|
+
},
|
|
331
|
+
Case {
|
|
332
|
+
name: "qualified owned Env",
|
|
333
|
+
input: quote! { pub fn mint(env: soroban_sdk::Env, caller: Address) {} },
|
|
334
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(&env,&soroban_sdk::Symbol::new(&env,\"minter\"),&caller);",
|
|
335
|
+
},
|
|
336
|
+
Case {
|
|
337
|
+
name: "qualified ref Env",
|
|
338
|
+
input: quote! { pub fn mint(env: &soroban_sdk::Env, caller: Address) {} },
|
|
339
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,\"minter\"),&caller);",
|
|
340
|
+
},
|
|
341
|
+
Case {
|
|
342
|
+
name: "leading :: qualified owned Env",
|
|
343
|
+
input: quote! { pub fn mint(env: ::soroban_sdk::Env, caller: Address) {} },
|
|
344
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(&env,&soroban_sdk::Symbol::new(&env,\"minter\"),&caller);",
|
|
345
|
+
},
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
for c in cases {
|
|
349
|
+
assert_role_check_exact_stmts(args.clone(), c.input, false, c.expected_ensure_stmt, None, Some(1), c.name);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#[test]
|
|
354
|
+
fn test_has_role_address_variants_generate_correct_param_reference() {
|
|
355
|
+
struct Case {
|
|
356
|
+
name: &'static str,
|
|
357
|
+
input: TokenStream,
|
|
358
|
+
expected_ensure_stmt: &'static str,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let args = quote! { caller, "minter" };
|
|
362
|
+
|
|
363
|
+
let cases = vec![
|
|
364
|
+
Case {
|
|
365
|
+
name: "Address by value -> &caller",
|
|
366
|
+
input: quote! { pub fn mint(env: &Env, caller: Address) {} },
|
|
367
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,\"minter\"),&caller);",
|
|
368
|
+
},
|
|
369
|
+
Case {
|
|
370
|
+
name: "&Address -> caller",
|
|
371
|
+
input: quote! { pub fn mint(env: &Env, caller: &Address) {} },
|
|
372
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,\"minter\"),caller);",
|
|
373
|
+
},
|
|
374
|
+
Case {
|
|
375
|
+
name: "&mut Address -> caller",
|
|
376
|
+
input: quote! { pub fn mint(env: &Env, caller: &mut Address) {} },
|
|
377
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,\"minter\"),caller);",
|
|
378
|
+
},
|
|
379
|
+
Case {
|
|
380
|
+
name: "qualified Address by value",
|
|
381
|
+
input: quote! { pub fn mint(env: &Env, caller: soroban_sdk::Address) {} },
|
|
382
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,\"minter\"),&caller);",
|
|
383
|
+
},
|
|
384
|
+
Case {
|
|
385
|
+
name: "qualified &Address",
|
|
386
|
+
input: quote! { pub fn mint(env: &Env, caller: &soroban_sdk::Address) {} },
|
|
387
|
+
expected_ensure_stmt: "utils::rbac::ensure_role(env,&soroban_sdk::Symbol::new(env,\"minter\"),caller);",
|
|
388
|
+
},
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
for c in cases {
|
|
392
|
+
assert_role_check_exact_stmts(args.clone(), c.input, false, c.expected_ensure_stmt, None, Some(1), c.name);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[test]
|
|
397
|
+
fn test_only_role_inserts_expected_statements_and_preserves_original_body() {
|
|
398
|
+
let args = quote! { caller, "minter" };
|
|
399
|
+
let input = quote! {
|
|
400
|
+
pub fn mint(env: Env, caller: Address) {
|
|
401
|
+
let x = 1u32;
|
|
402
|
+
let _ = x + 1;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
let output_fn = assert_role_check_exact_stmts(
|
|
407
|
+
args,
|
|
408
|
+
input,
|
|
409
|
+
true,
|
|
410
|
+
"utils::rbac::ensure_role(&env,&soroban_sdk::Symbol::new(&env,\"minter\"),&caller);",
|
|
411
|
+
Some("caller.require_auth();"),
|
|
412
|
+
Some(4),
|
|
413
|
+
"only_role inserts ensure_role + require_auth",
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// Ensure original statements remain after the inserted checks.
|
|
417
|
+
let third_stmt = &output_fn.block.stmts[2];
|
|
418
|
+
let third_stmt_str = quote::quote!(#third_stmt).to_string().replace(" ", "");
|
|
419
|
+
assert!(third_stmt_str.starts_with("letx=1u32;"), "expected original 'let x = 1u32;' to be preserved");
|
|
420
|
+
}
|
|
@@ -8,8 +8,8 @@ pub struct MyContract;
|
|
|
8
8
|
use utils::{auth::Auth as _, multisig::MultiSig as _};
|
|
9
9
|
#[common_macros::contract_impl]
|
|
10
10
|
impl utils::auth::Auth for MyContract {
|
|
11
|
-
fn authorizer(env: &soroban_sdk::Env) -> soroban_sdk::Address {
|
|
12
|
-
env.current_contract_address()
|
|
11
|
+
fn authorizer(env: &soroban_sdk::Env) -> Option<soroban_sdk::Address> {
|
|
12
|
+
Some(env.current_contract_address())
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
#[common_macros::contract_impl(contracttrait)]
|
|
@@ -23,8 +23,8 @@ pub struct MyContract(pub u32);
|
|
|
23
23
|
use utils::{auth::Auth as _, multisig::MultiSig as _};
|
|
24
24
|
#[common_macros::contract_impl]
|
|
25
25
|
impl utils::auth::Auth for MyContract {
|
|
26
|
-
fn authorizer(env: &soroban_sdk::Env) -> soroban_sdk::Address {
|
|
27
|
-
env.current_contract_address()
|
|
26
|
+
fn authorizer(env: &soroban_sdk::Env) -> Option<soroban_sdk::Address> {
|
|
27
|
+
Some(env.current_contract_address())
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
#[common_macros::contract_impl(contracttrait)]
|