@layerzerolabs/protocol-stellar-v2 0.2.39 → 0.2.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.turbo/turbo-build.log +226 -313
  2. package/.turbo/turbo-lint.log +98 -227
  3. package/.turbo/turbo-test.log +1803 -1954
  4. package/contracts/common-macros/src/lib.rs +38 -15
  5. package/contracts/common-macros/src/lz_contract.rs +12 -21
  6. package/contracts/common-macros/src/tests/lz_contract.rs +17 -8
  7. package/contracts/common-macros/src/tests/snapshots/common_macros__tests__lz_contract__snapshot_generated_lz_contract_code.snap +20 -0
  8. package/contracts/common-macros/src/upgradeable.rs +37 -30
  9. package/contracts/endpoint-v2/src/endpoint_v2.rs +4 -3
  10. package/contracts/endpoint-v2/src/errors.rs +2 -2
  11. package/contracts/endpoint-v2/src/messaging_channel.rs +11 -0
  12. package/contracts/endpoint-v2/src/messaging_composer.rs +1 -0
  13. package/contracts/endpoint-v2/src/tests/endpoint_v2/clear.rs +12 -25
  14. package/contracts/endpoint-v2/src/tests/endpoint_v2/initializable.rs +4 -4
  15. package/contracts/endpoint-v2/src/tests/endpoint_v2/verifiable.rs +50 -10
  16. package/contracts/endpoint-v2/src/tests/endpoint_v2/verify.rs +6 -35
  17. package/contracts/endpoint-v2/src/tests/messaging_channel/burn.rs +2 -2
  18. package/contracts/endpoint-v2/src/tests/messaging_channel/clear_payload.rs +50 -1
  19. package/contracts/endpoint-v2/src/tests/messaging_channel/inbound.rs +78 -0
  20. package/contracts/endpoint-v2/src/tests/messaging_channel/insert_and_drain_pending_nonces.rs +272 -0
  21. package/contracts/endpoint-v2/src/tests/messaging_channel/mod.rs +1 -0
  22. package/contracts/endpoint-v2/src/tests/messaging_channel/nilify.rs +10 -5
  23. package/contracts/endpoint-v2/src/tests/messaging_channel/skip.rs +30 -0
  24. package/contracts/macro-integration-tests/tests/runtime/oapp/mod.rs +22 -1
  25. package/contracts/macro-integration-tests/tests/runtime/oapp/oapp_core.rs +13 -11
  26. package/contracts/macro-integration-tests/tests/runtime/oapp/options_type3.rs +13 -10
  27. package/contracts/macro-integration-tests/tests/runtime/oapp/receiver.rs +15 -11
  28. package/contracts/macro-integration-tests/tests/runtime/oapp/sender.rs +3 -2
  29. package/contracts/macro-integration-tests/tests/runtime/ownable/two_step_transfer.rs +14 -12
  30. package/contracts/macro-integration-tests/tests/runtime/upgradeable/migrate_guard_and_state.rs +3 -9
  31. package/contracts/macro-integration-tests/tests/ui/lz_contract/fail/upgradeable_invalid_inner_option.stderr +24 -1
  32. package/contracts/macro-integration-tests/tests/ui/lz_contract/fail/upgradeable_missing_internal.stderr +3 -3
  33. package/contracts/macro-integration-tests/tests/ui/lz_contract/pass/upgradeable_rbac.rs +44 -0
  34. package/contracts/macro-integration-tests/tests/ui/oapp/pass/custom_all.rs +3 -0
  35. package/contracts/macro-integration-tests/tests/ui/oapp/pass/custom_single_trait.rs +3 -0
  36. package/contracts/macro-integration-tests/tests/ui/ownable/pass/basic.rs +1 -1
  37. package/contracts/macro-integration-tests/tests/ui/upgradeable/fail/attr_args.stderr +1 -1
  38. package/contracts/macro-integration-tests/tests/ui/upgradeable/fail/missing_auth_trait.stderr +2 -2
  39. package/contracts/macro-integration-tests/tests/ui/upgradeable/fail/missing_upgradeable_internal.stderr +2 -2
  40. package/contracts/macro-integration-tests/tests/ui/upgradeable/pass/rbac.rs +44 -0
  41. package/contracts/oapps/counter/integration_tests/utils.rs +5 -3
  42. package/contracts/oapps/counter/src/tests/mod.rs +16 -1
  43. package/contracts/oapps/counter/src/tests/test_counter.rs +5 -2
  44. package/contracts/oapps/oapp/src/oapp_core.rs +21 -7
  45. package/contracts/oapps/oapp/src/oapp_options_type3.rs +7 -5
  46. package/contracts/oapps/oapp/src/tests/mod.rs +21 -0
  47. package/contracts/oapps/oapp/src/tests/oapp_core.rs +12 -10
  48. package/contracts/oapps/oapp/src/tests/oapp_options_type3.rs +11 -7
  49. package/contracts/oapps/oapp/src/tests/oapp_receiver.rs +4 -2
  50. package/contracts/oapps/oapp/src/tests/oapp_sender.rs +3 -2
  51. package/contracts/oapps/oapp/src/tests/test_macros.rs +15 -0
  52. package/contracts/oapps/oapp-macros/src/generators.rs +6 -0
  53. package/contracts/oapps/oapp-macros/src/tests/snapshots/oapp_macros__tests__oapp__snapshot_generate_oapp.snap +15 -0
  54. package/contracts/oapps/oft/integration-tests/setup.rs +22 -4
  55. package/contracts/oapps/oft/integration-tests/utils.rs +94 -13
  56. package/contracts/oapps/oft/src/extensions/oft_fee.rs +23 -10
  57. package/contracts/oapps/oft/src/extensions/pausable.rs +31 -10
  58. package/contracts/oapps/oft/src/extensions/rate_limiter.rs +9 -4
  59. package/contracts/oapps/oft/src/oft.rs +1 -2
  60. package/contracts/oapps/oft/src/tests/extensions/oft_fee.rs +39 -27
  61. package/contracts/oapps/oft/src/tests/extensions/pausable.rs +38 -24
  62. package/contracts/oapps/oft/src/tests/extensions/rate_limiter.rs +87 -69
  63. package/contracts/oapps/oft-core/integration-tests/setup.rs +27 -3
  64. package/contracts/oapps/oft-core/src/oft_core.rs +10 -5
  65. package/contracts/oapps/oft-core/src/tests/test_msg_inspector.rs +20 -20
  66. package/contracts/oapps/oft-core/src/tests/test_utils.rs +31 -3
  67. package/contracts/upgrader/src/lib.rs +67 -30
  68. package/contracts/upgrader/src/tests/test_data/test_upgradeable_contract3.wasm +0 -0
  69. package/contracts/upgrader/src/tests/test_data/test_upgradeable_contract4.wasm +0 -0
  70. package/contracts/upgrader/src/tests/test_upgrader.rs +50 -4
  71. package/contracts/utils/src/ownable.rs +16 -5
  72. package/contracts/utils/src/tests/ownable.rs +39 -39
  73. package/contracts/utils/src/upgradeable.rs +60 -17
  74. package/docs/oapp-guide.md +4 -4
  75. package/package.json +3 -4
  76. package/sdk/.turbo/turbo-test.log +381 -366
  77. package/sdk/dist/generated/bml.d.ts +4 -4
  78. package/sdk/dist/generated/bml.js +6 -6
  79. package/sdk/dist/generated/counter.d.ts +158 -12
  80. package/sdk/dist/generated/counter.js +32 -12
  81. package/sdk/dist/generated/dvn.d.ts +4 -6
  82. package/sdk/dist/generated/dvn.js +8 -8
  83. package/sdk/dist/generated/dvn_fee_lib.d.ts +8 -10
  84. package/sdk/dist/generated/dvn_fee_lib.js +8 -8
  85. package/sdk/dist/generated/endpoint.d.ts +9 -9
  86. package/sdk/dist/generated/endpoint.js +9 -9
  87. package/sdk/dist/generated/executor.d.ts +9 -11
  88. package/sdk/dist/generated/executor.js +11 -11
  89. package/sdk/dist/generated/executor_fee_lib.d.ts +9 -11
  90. package/sdk/dist/generated/executor_fee_lib.js +11 -11
  91. package/sdk/dist/generated/executor_helper.d.ts +4 -4
  92. package/sdk/dist/generated/executor_helper.js +6 -6
  93. package/sdk/dist/generated/layerzero_view.d.ts +9 -11
  94. package/sdk/dist/generated/layerzero_view.js +11 -11
  95. package/sdk/dist/generated/oft.d.ts +194 -27
  96. package/sdk/dist/generated/oft.js +44 -22
  97. package/sdk/dist/generated/price_feed.d.ts +8 -10
  98. package/sdk/dist/generated/price_feed.js +8 -8
  99. package/sdk/dist/generated/sac_manager.d.ts +8 -8
  100. package/sdk/dist/generated/sac_manager.js +6 -6
  101. package/sdk/dist/generated/sml.d.ts +9 -9
  102. package/sdk/dist/generated/sml.js +9 -9
  103. package/sdk/dist/generated/treasury.d.ts +9 -9
  104. package/sdk/dist/generated/treasury.js +9 -9
  105. package/sdk/dist/generated/uln302.d.ts +9 -9
  106. package/sdk/dist/generated/uln302.js +9 -9
  107. package/sdk/dist/generated/upgrader.d.ts +25 -16
  108. package/sdk/dist/generated/upgrader.js +5 -5
  109. package/sdk/package.json +1 -1
  110. package/sdk/test/counter-sml.test.ts +20 -0
  111. package/sdk/test/counter-uln.test.ts +20 -0
  112. package/sdk/test/oft-sml.test.ts +22 -0
  113. package/sdk/test/upgrader.test.ts +1 -0
  114. package/turbo.json +1 -8
@@ -457,23 +457,27 @@ pub fn contract_trait(attr: TokenStream, item: TokenStream) -> TokenStream {
457
457
  // Upgradeable Macro
458
458
  // ============================================================================
459
459
 
460
- /// Generates upgradeable implementation using the `Upgradeable` trait's default methods.
460
+ /// Generates upgradeable implementation using `Upgradeable` or `UpgradeableRbac` traits.
461
461
  ///
462
- /// This macro implements `Upgradeable` using the trait's default methods (which include auth).
462
+ /// `Upgradeable` uses Auth directly; `UpgradeableRbac` layers RoleBased
463
+ /// access control on top of Auth.
463
464
  ///
464
465
  /// # Requirements
465
- /// - The contract must implement the `Auth` trait (via `#[ownable]` or `#[multisig]`)
466
+ /// - `Upgradeable` (default): contract must implement `Auth` (via `#[ownable]` or `#[multisig]`)
467
+ /// - `UpgradeableRbac` (with `rbac`): contract must implement both `Auth` and `RoleBasedAccessControl` (e.g. from OApp)
466
468
  /// - By default, requires manual `UpgradeableInternal` implementation
467
469
  /// - With `no_migration` flag, auto-generates a no-op `UpgradeableInternal` impl
468
470
  ///
469
471
  /// # Options
470
- /// - `#[upgradeable]` - Requires manual `UpgradeableInternal` implementation (safety by default)
471
- /// - `#[upgradeable(no_migration)]` - Auto-generates no-op `UpgradeableInternal` (for initial deployment)
472
+ /// - `#[upgradeable]` - Implements Upgradeable, requires manual `UpgradeableInternal` (safety by default)
473
+ /// - `#[upgradeable(no_migration)]` - Implements Upgradeable, auto-generates no-op `UpgradeableInternal`
474
+ /// - `#[upgradeable(rbac)]` - Implements UpgradeableRbac, requires manual `UpgradeableInternal`
475
+ /// - `#[upgradeable(rbac, no_migration)]` - Implements UpgradeableRbac, auto-generates no-op `UpgradeableInternal`
472
476
  ///
473
477
  /// # Example
474
478
  /// ```ignore
475
- /// // Requires manual UpgradeableInternal implementation (default)
476
- /// #[ownable] // or #[multisig]
479
+ /// // Implements Upgradeable (default)
480
+ /// #[ownable]
477
481
  /// #[upgradeable]
478
482
  /// pub struct MyContract;
479
483
  ///
@@ -485,16 +489,32 @@ pub fn contract_trait(attr: TokenStream, item: TokenStream) -> TokenStream {
485
489
  /// }
486
490
  /// }
487
491
  ///
488
- /// // Auto-generates no-op UpgradeableInternal (for initial deployment)
489
- /// #[ownable] // or #[multisig]
492
+ /// // Implements Upgradeable (no migration)
493
+ /// #[ownable]
490
494
  /// #[upgradeable(no_migration)]
491
- /// pub struct MyContract;
492
- /// // No UpgradeableInternal impl needed!
495
+ /// pub struct SimpleContract;
496
+ ///
497
+ /// // Implements UpgradeableRbac (layered)
498
+ /// #[ownable]
499
+ /// #[upgradeable(rbac)]
500
+ /// pub struct RbacContract;
501
+ ///
502
+ /// impl utils::upgradeable::UpgradeableInternal for RbacContract {
503
+ /// type MigrationData = MyMigrationParams;
504
+ ///
505
+ /// fn __migrate(env: &Env, migration_data: &Self::MigrationData) {
506
+ /// // Custom migration logic here
507
+ /// }
508
+ /// }
509
+ ///
510
+ /// // Implements UpgradeableRbac (no migration)
511
+ /// #[ownable]
512
+ /// #[upgradeable(rbac, no_migration)]
513
+ /// pub struct SimpleRbacContract;
493
514
  /// ```
494
515
  ///
495
516
  /// Generated code includes:
496
- /// - `upgrade(env, new_wasm_hash)` - Upgrades the contract WASM (auth required)
497
- /// - `migrate(env, migration_data)` - Runs migration after upgrade (auth required, XDR-decodes `Bytes` to `MigrationData`)
517
+ /// - `upgrade` / `migrate` - Auth-based or Auth + RoleBased depending on options
498
518
  /// - `contractmeta!` with `binver` set to the Cargo package version (if not 0.0.0)
499
519
  #[proc_macro_attribute]
500
520
  pub fn upgradeable(attr: TokenStream, item: TokenStream) -> TokenStream {
@@ -518,8 +538,7 @@ pub fn upgradeable(attr: TokenStream, item: TokenStream) -> TokenStream {
518
538
  /// - `#[ownable]` - Single-owner access control
519
539
  ///
520
540
  /// # Options
521
- /// - `upgradeable` - Adds `#[upgradeable]`, requires manual `UpgradeableInternal` impl
522
- /// - `upgradeable(no_migration)` - Adds `#[upgradeable(no_migration)]`, auto-generates no-op impl
541
+ /// - `upgradeable(...)` - Adds `#[upgradeable(...)]`; content is passed verbatim to the upgradeable macro
523
542
  /// - `multisig` - Uses `#[multisig]` instead of `#[ownable]`
524
543
  ///
525
544
  /// # Examples
@@ -536,6 +555,10 @@ pub fn upgradeable(attr: TokenStream, item: TokenStream) -> TokenStream {
536
555
  /// #[lz_contract(upgradeable(no_migration))]
537
556
  /// pub struct DVNFeeLib;
538
557
  ///
558
+ /// // Contract with RBAC-based upgrade support
559
+ /// #[lz_contract(upgradeable(rbac))]
560
+ /// pub struct RbacOft;
561
+ ///
539
562
  /// // Contract with multisig auth and upgrade support (no migration)
540
563
  /// #[lz_contract(multisig, upgradeable(no_migration))]
541
564
  /// pub struct DVN;
@@ -16,21 +16,20 @@ use syn::{
16
16
  pub struct LzContractConfig {
17
17
  /// If true, adds `#[upgradeable]` for contract upgrade support.
18
18
  pub upgradeable: bool,
19
- /// If true, generates a default no-op `UpgradeableInternal` implementation.
20
- /// Only valid when `upgradeable` is also true.
21
- pub no_migration: bool,
19
+ /// Raw tokens inside `upgradeable(...)`, passed verbatim to the upgradeable macro.
20
+ /// Empty when `upgradeable` has no parentheses.
21
+ pub upgradeable_attr: TokenStream,
22
22
  /// If true, uses `#[multisig]` instead of `#[ownable]` for auth.
23
23
  pub multisig: bool,
24
24
  }
25
25
 
26
26
  impl Parse for LzContractConfig {
27
27
  fn parse(input: ParseStream) -> syn::Result<Self> {
28
+ let mut config = Self::default();
28
29
  if input.is_empty() {
29
- return Ok(Self::default());
30
+ return Ok(config);
30
31
  }
31
32
 
32
- let mut config = Self::default();
33
-
34
33
  // Parse comma-separated items, handling nested parentheses for upgradeable(no_migration)
35
34
  while !input.is_empty() {
36
35
  let ident: Ident = input.parse()?;
@@ -38,19 +37,11 @@ impl Parse for LzContractConfig {
38
37
  match ident.to_string().as_str() {
39
38
  "upgradeable" => {
40
39
  config.upgradeable = true;
41
- // Check for optional (no_migration) suffix
40
+ // Pass through optional (...) content verbatim to the upgradeable macro
42
41
  if input.peek(syn::token::Paren) {
43
42
  let content;
44
43
  parenthesized!(content in input);
45
- let inner_ident: Ident = content.parse()?;
46
- if inner_ident == "no_migration" {
47
- config.no_migration = true;
48
- } else {
49
- return Err(Error::new(inner_ident.span(), "expected `no_migration`"));
50
- }
51
- if !content.is_empty() {
52
- return Err(Error::new(content.span(), "unexpected tokens in `upgradeable(...)`"));
53
- }
44
+ config.upgradeable_attr = content.parse()?;
54
45
  }
55
46
  }
56
47
  "multisig" => config.multisig = true,
@@ -79,8 +70,7 @@ impl Parse for LzContractConfig {
79
70
  /// - `#[common_macros::ownable]` - Single-owner access control
80
71
  ///
81
72
  /// # Options
82
- /// - `upgradeable` - Adds `#[upgradeable]`, requires manual `UpgradeableInternal` impl
83
- /// - `upgradeable(no_migration)` - Adds `#[upgradeable(no_migration)]`, auto-generates no-op impl
73
+ /// - `upgradeable(...)` - Adds `#[upgradeable(...)]`; content is passed verbatim to the upgradeable macro
84
74
  /// - `multisig` - Uses `#[multisig]` instead of `#[ownable]`
85
75
  pub fn generate_lz_contract(attr: TokenStream, input: TokenStream) -> TokenStream {
86
76
  let config: LzContractConfig =
@@ -94,10 +84,11 @@ pub fn generate_lz_contract(attr: TokenStream, input: TokenStream) -> TokenStrea
94
84
  };
95
85
 
96
86
  let upgrade = if config.upgradeable {
97
- if config.no_migration {
98
- quote! { #[common_macros::upgradeable(no_migration)] }
99
- } else {
87
+ if config.upgradeable_attr.is_empty() {
100
88
  quote! { #[common_macros::upgradeable] }
89
+ } else {
90
+ let upgradeable_attr = &config.upgradeable_attr;
91
+ quote! { #[common_macros::upgradeable(#upgradeable_attr)] }
101
92
  }
102
93
  } else {
103
94
  quote! {}
@@ -30,9 +30,24 @@ fn snapshot_generated_lz_contract_code() {
30
30
  &syn::parse2::<syn::File>(upgradeable_no_migration_result).expect("failed to parse generated code"),
31
31
  );
32
32
 
33
+ let upgradeable_rbac_result =
34
+ crate::lz_contract::generate_lz_contract(quote! { upgradeable(rbac) }, quote! { pub struct MyContract; });
35
+ let upgradeable_rbac_formatted = prettyplease::unparse(
36
+ &syn::parse2::<syn::File>(upgradeable_rbac_result).expect("failed to parse generated code"),
37
+ );
38
+
39
+ // Pass-through: order and content preserved verbatim
40
+ let upgradeable_rbac_no_migration_result = crate::lz_contract::generate_lz_contract(
41
+ quote! { upgradeable(rbac, no_migration) },
42
+ quote! { pub struct MyContract; },
43
+ );
44
+ let upgradeable_rbac_no_migration_formatted = prettyplease::unparse(
45
+ &syn::parse2::<syn::File>(upgradeable_rbac_no_migration_result).expect("failed to parse generated code"),
46
+ );
47
+
33
48
  let combined = format!(
34
- "// === Default (ownable) ===\n\n{}\n\n// === MultiSig + Upgradeable ===\n\n{}\n\n// === Upgradeable (no_migration) ===\n\n{}",
35
- default_formatted, multisig_upgradeable_formatted, upgradeable_no_migration_formatted
49
+ "// === Default (ownable) ===\n\n{}\n\n// === MultiSig + Upgradeable ===\n\n{}\n\n// === Upgradeable (no_migration) ===\n\n{}\n\n// === Upgradeable (rbac) ===\n\n{}\n\n// === Upgradeable (rbac, no_migration) pass-through ===\n\n{}",
50
+ default_formatted, multisig_upgradeable_formatted, upgradeable_no_migration_formatted, upgradeable_rbac_formatted, upgradeable_rbac_no_migration_formatted
36
51
  );
37
52
 
38
53
  insta::assert_snapshot!(combined);
@@ -49,12 +64,6 @@ fn test_lz_contract_invalid_config_table_driven() {
49
64
  let cases: Vec<(&str, TokenStream, &str)> = vec![
50
65
  ("unknown option", quote! { not_a_real_option }, "expected one of `upgradeable`, `multisig`"),
51
66
  ("invalid attr syntax", quote! { 123 }, "failed to parse lz_contract config"),
52
- ("upgradeable(bad_inner)", quote! { upgradeable(not_migration) }, "expected `no_migration`"),
53
- (
54
- "upgradeable(extra_tokens)",
55
- quote! { upgradeable(no_migration, extra) },
56
- "unexpected tokens in `upgradeable(...)`",
57
- ),
58
67
  ];
59
68
 
60
69
  for (case, attr, expected_substring) in cases {
@@ -29,3 +29,23 @@ pub struct MyContract;
29
29
  #[common_macros::ownable]
30
30
  #[common_macros::upgradeable(no_migration)]
31
31
  pub struct MyContract;
32
+
33
+
34
+ // === Upgradeable (rbac) ===
35
+
36
+ #[soroban_sdk::contract]
37
+ #[common_macros::ttl_configurable]
38
+ #[common_macros::ttl_extendable]
39
+ #[common_macros::ownable]
40
+ #[common_macros::upgradeable(rbac)]
41
+ pub struct MyContract;
42
+
43
+
44
+ // === Upgradeable (rbac, no_migration) pass-through ===
45
+
46
+ #[soroban_sdk::contract]
47
+ #[common_macros::ttl_configurable]
48
+ #[common_macros::ttl_extendable]
49
+ #[common_macros::ownable]
50
+ #[common_macros::upgradeable(rbac, no_migration)]
51
+ pub struct MyContract;
@@ -4,7 +4,7 @@ use proc_macro2::TokenStream;
4
4
  use quote::quote;
5
5
  use syn::{
6
6
  parse::{Parse, ParseStream},
7
- Ident, ItemStruct,
7
+ Ident, ItemStruct, Token,
8
8
  };
9
9
 
10
10
  /// Configuration options for the `#[upgradeable]` macro.
@@ -13,51 +13,52 @@ pub struct UpgradeableConfig {
13
13
  /// If true, generates a default no-op `UpgradeableInternal` implementation.
14
14
  /// Use this for initial deployments when no migration logic is needed yet.
15
15
  pub no_migration: bool,
16
+ /// If true, uses `UpgradeableRbac` (Auth + RoleBased) instead of `Upgradeable` (Auth only).
17
+ pub rbac: bool,
16
18
  }
17
19
 
18
20
  impl Parse for UpgradeableConfig {
19
21
  fn parse(input: ParseStream) -> syn::Result<Self> {
22
+ let mut config = Self::default();
20
23
  if input.is_empty() {
21
- return Ok(Self::default());
24
+ return Ok(config);
22
25
  }
23
26
 
24
- let ident: Ident = input.parse()?;
25
- if ident == "no_migration" {
26
- Ok(Self { no_migration: true })
27
- } else {
28
- Err(syn::Error::new(ident.span(), "expected `no_migration`"))
27
+ while !input.is_empty() {
28
+ let ident: Ident = input.parse()?;
29
+ match ident.to_string().as_str() {
30
+ "no_migration" => config.no_migration = true,
31
+ "rbac" => config.rbac = true,
32
+ _ => return Err(syn::Error::new(ident.span(), "expected `no_migration` or `rbac`")),
33
+ }
34
+
35
+ // Consume optional trailing comma
36
+ if input.peek(Token![,]) {
37
+ input.parse::<Token![,]>()?;
38
+ }
29
39
  }
40
+ Ok(config)
30
41
  }
31
42
  }
32
43
 
33
44
  /// Generates the upgradeable implementation from the `#[upgradeable]` attribute macro.
34
45
  ///
35
- /// This function generates the implementation of the `Upgradeable` trait for a
36
- /// given contract type, enabling the contract to be upgraded by replacing its
37
- /// WASM bytecode with migration support.
46
+ /// Generates an impl of `Upgradeable` or `UpgradeableRbac` for a contract type,
47
+ /// enabling upgrades by replacing WASM bytecode with migration support.
38
48
  ///
39
49
  /// # Behavior
40
50
  ///
41
- /// - Implements the `Upgradeable` trait using its default methods (which include auth).
51
+ /// - By default implements `Upgradeable` (Auth-based, `#[only_auth]`). With `rbac`,
52
+ /// implements `UpgradeableRbac` (Auth + RoleBased, `UPGRADER_ROLE`).
42
53
  /// - Sets the contract crate version as `"binver"` metadata using
43
- /// `soroban_sdk::contractmeta!`. Gets the crate version via the env variable
44
- /// `CARGO_PKG_VERSION` which corresponds to the "version" attribute in
45
- /// Cargo.toml. If no such attribute or if it is "0.0.0", skips this step.
46
- /// - By default, requires the contract to implement `UpgradeableInternal` trait.
47
- /// - With `no_migration` flag, generates a default no-op `UpgradeableInternal` impl.
48
- ///
49
- /// # Example
50
- /// ```ignore
51
- /// // Requires manual UpgradeableInternal implementation (default, safety first)
52
- /// #[ownable]
53
- /// #[upgradeable]
54
- /// pub struct MyContract;
54
+ /// `soroban_sdk::contractmeta!`. Uses `CARGO_PKG_VERSION` (from Cargo.toml
55
+ /// `[package]` version). Skips if missing or `"0.0.0"`.
56
+ /// - By default, requires the contract to implement `UpgradeableInternal`.
57
+ /// - With `no_migration`, generates a no-op `UpgradeableInternal` impl.
58
+ /// - With `rbac`, uses `UpgradeableRbac` (requires `RoleBasedAccessControl`, which
59
+ /// extends `Auth`) instead of `Upgradeable` (requires `Auth`).
55
60
  ///
56
- /// // Auto-generates no-op UpgradeableInternal (for initial deployment)
57
- /// #[ownable]
58
- /// #[upgradeable(no_migration)]
59
- /// pub struct MyContract;
60
- /// ```
61
+ /// See the `#[upgradeable]` macro documentation for full examples.
61
62
  pub fn generate_upgradeable_impl(attr: TokenStream, input: TokenStream) -> TokenStream {
62
63
  let config: UpgradeableConfig =
63
64
  syn::parse2(attr).unwrap_or_else(|e| panic!("failed to parse upgradeable config: {}", e));
@@ -78,17 +79,23 @@ pub fn generate_upgradeable_impl(attr: TokenStream, input: TokenStream) -> Token
78
79
  quote! {}
79
80
  };
80
81
 
82
+ let trait_path = if config.rbac {
83
+ quote! { utils::upgradeable::UpgradeableRbac }
84
+ } else {
85
+ quote! { utils::upgradeable::Upgradeable }
86
+ };
87
+
81
88
  quote! {
82
89
  #item_struct
83
90
 
84
- use utils::upgradeable::Upgradeable as _;
91
+ use #trait_path as _;
85
92
 
86
93
  #binver
87
94
 
88
95
  #default_internal_impl
89
96
 
90
97
  #[common_macros::contract_impl(contracttrait)]
91
- impl utils::upgradeable::Upgradeable for #name {}
98
+ impl #trait_path for #name {}
92
99
  }
93
100
  }
94
101
 
@@ -52,7 +52,7 @@ impl ILayerZeroEndpointV2 for EndpointV2 {
52
52
  let packet = Self::build_outbound_packet(env, sender, *dst_eid, receiver, message, nonce);
53
53
 
54
54
  let fee = SendLibClient::new(env, &send_lib).quote(&packet, options, pay_in_zro);
55
- assert_with_error!(env, fee.native_fee >= 0 && fee.zro_fee >= 0, EndpointError::InvalidFeeAmount);
55
+ assert_with_error!(env, fee.native_fee >= 0 && fee.zro_fee >= 0, EndpointError::InvalidAmount);
56
56
 
57
57
  fee
58
58
  }
@@ -127,6 +127,7 @@ impl ILayerZeroEndpointV2 for EndpointV2 {
127
127
  reason: &Bytes,
128
128
  ) {
129
129
  executor.require_auth();
130
+ assert_with_error!(env, gas >= 0 && value >= 0, EndpointError::InvalidAmount);
130
131
  LzReceiveAlert {
131
132
  receiver: receiver.clone(),
132
133
  executor: executor.clone(),
@@ -257,7 +258,7 @@ impl EndpointV2 {
257
258
  // Fee amounts are modeled as non-negative values. The field type is i128 for
258
259
  // compatibility with token APIs, but negative fees are always invalid and rejected
259
260
  // here, while zero amounts are treated as a no-op (skipped by the check below).
260
- assert_with_error!(env, r.amount >= 0, EndpointError::InvalidFeeAmount);
261
+ assert_with_error!(env, r.amount >= 0, EndpointError::InvalidAmount);
261
262
  if r.amount > 0 {
262
263
  assert_with_error!(env, native_fee_supplied >= r.amount, EndpointError::InsufficientNativeFee);
263
264
  native_fee_supplied -= r.amount;
@@ -286,7 +287,7 @@ impl EndpointV2 {
286
287
  // Fee amounts are modeled as non-negative values. The field type is i128 for
287
288
  // compatibility with token APIs, but negative fees are always invalid and rejected
288
289
  // here, while zero amounts are treated as a no-op (skipped by the check below).
289
- assert_with_error!(env, r.amount >= 0, EndpointError::InvalidFeeAmount);
290
+ assert_with_error!(env, r.amount >= 0, EndpointError::InvalidAmount);
290
291
  if r.amount > 0 {
291
292
  assert_with_error!(env, zro_fee_supplied >= r.amount, EndpointError::InsufficientZroFee);
292
293
  zro_fee_supplied -= r.amount;
@@ -18,8 +18,8 @@ pub enum EndpointError {
18
18
  InsufficientZroFee,
19
19
  /// Timeout expiry is invalid (already expired)
20
20
  InvalidExpiry,
21
- /// Fee amount is invalid (negative)
22
- InvalidFeeAmount,
21
+ /// Amount is invalid (negative)
22
+ InvalidAmount,
23
23
  /// Compose index exceeds maximum allowed value
24
24
  InvalidIndex,
25
25
  /// Nonce is invalid for the requested operation
@@ -322,5 +322,16 @@ mod test {
322
322
  ) {
323
323
  Self::clear_payload(env, receiver, src_eid, sender, nonce, payload)
324
324
  }
325
+
326
+ /// Test-only wrapper for insert_and_drain_pending_nonces.
327
+ pub fn insert_and_drain_pending_nonces_for_test(
328
+ env: &Env,
329
+ receiver: &Address,
330
+ src_eid: u32,
331
+ sender: &BytesN<32>,
332
+ new_nonce: u64,
333
+ ) {
334
+ Self::insert_and_drain_pending_nonces(env, receiver, src_eid, sender, new_nonce)
335
+ }
325
336
  }
326
337
  }
@@ -73,6 +73,7 @@ impl IMessagingComposer for EndpointV2 {
73
73
  reason: &Bytes,
74
74
  ) {
75
75
  executor.require_auth();
76
+ assert_with_error!(env, gas >= 0 && value >= 0, EndpointError::InvalidAmount);
76
77
  assert_compose_index(env, index);
77
78
  LzComposeAlert {
78
79
  executor: executor.clone(),
@@ -92,33 +92,18 @@ fn test_clear_removes_inbound_payload_hash() {
92
92
  // Now clear the payload
93
93
  clear_packet_with_auth(&context, &receiver, &origin, &receiver, &guid, &message);
94
94
 
95
+ // Verify PacketDelivered event was emitted.
96
+ assert_eq_event(
97
+ env,
98
+ &endpoint_client.address,
99
+ PacketDelivered { origin: origin.clone(), receiver: receiver.clone() },
100
+ );
101
+
95
102
  // Verify payload hash was removed via public interface
96
103
  let stored_hash = endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce);
97
104
  assert_eq!(stored_hash, None);
98
105
  }
99
106
 
100
- // Event emission
101
- #[test]
102
- fn test_clear_emits_packet_delivered_event() {
103
- let context = setup();
104
- let env = &context.env;
105
- let endpoint_client = &context.endpoint_client;
106
-
107
- let src_eid = 2u32;
108
- let sender = BytesN::from_array(env, &[1u8; 32]);
109
- let receiver = env.register(MockReceiver, ());
110
- let nonce = 1u64;
111
-
112
- let message = Bytes::from_array(env, &[1, 2, 3, 4]);
113
- let guid = BytesN::from_array(env, &[5u8; 32]);
114
- let (_receive_lib, origin, _payload_hash) =
115
- arrange_verified_packet_with_auth(&context, src_eid, &sender, &receiver, nonce, &guid, &message);
116
-
117
- clear_packet_with_auth(&context, &receiver, &origin, &receiver, &guid, &message);
118
-
119
- assert_eq_event(env, &endpoint_client.address, PacketDelivered { origin: origin.clone(), receiver: receiver.clone() });
120
- }
121
-
122
107
  // Inbound nonce is advanced during verify, not during clear
123
108
  #[test]
124
109
  fn test_clear_does_not_change_inbound_nonce() {
@@ -147,7 +132,7 @@ fn test_clear_does_not_change_inbound_nonce() {
147
132
 
148
133
  // Sequential nonce behavior
149
134
  #[test]
150
- fn test_clear_success_sequential_nonces_update_lazy_nonce_to_latest() {
135
+ fn test_clear_success_sequential_nonces_keep_inbound_nonce_at_latest_verified() {
151
136
  let context = setup();
152
137
  let env = &context.env;
153
138
  let endpoint_client = &context.endpoint_client;
@@ -175,8 +160,10 @@ fn test_clear_success_sequential_nonces_update_lazy_nonce_to_latest() {
175
160
  let origin2 = Origin { src_eid, sender: sender.clone(), nonce: 2 };
176
161
 
177
162
  verify_packet_with_auth(&context, &receive_lib, &origin2, &receiver, &payload_hash2);
178
- clear_packet_with_auth(&context, &receiver, &origin2, &receiver, &guid2, &message2);
179
163
 
164
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
165
+
166
+ clear_packet_with_auth(&context, &receiver, &origin2, &receiver, &guid2, &message2);
180
167
  // Verify advanced inbound nonce.
181
168
  assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
182
169
  }
@@ -382,7 +369,7 @@ fn test_clear_failure_missing_intermediate_nonce() {
382
369
  }
383
370
 
384
371
  #[test]
385
- fn test_clear_does_not_advance_lazy_nonce_when_clearing_older_nonce() {
372
+ fn test_clear_does_not_advance_inbound_nonce_when_clearing_older_nonce() {
386
373
  let context = setup();
387
374
  let env = &context.env;
388
375
  let endpoint_client = &context.endpoint_client;
@@ -20,7 +20,7 @@ fn test_initializable_new_path_receiver_allows() {
20
20
  let sender = BytesN::from_array(&context.env, &[1u8; 32]);
21
21
  let origin = Origin { src_eid, sender, nonce: 1 };
22
22
 
23
- // For a new path (lazy nonce is 0), initializable depends on receiver contract.
23
+ // For a new path (inbound_nonce is 0), initializable depends on receiver contract.
24
24
  let result = endpoint_client.initializable(&origin, &receiver);
25
25
  assert!(result);
26
26
  }
@@ -36,12 +36,12 @@ fn test_initializable_new_path_receiver_rejects() {
36
36
  let sender = BytesN::from_array(&context.env, &[1u8; 32]);
37
37
  let origin = Origin { src_eid, sender, nonce: 1 };
38
38
 
39
- // For a new path (lazy nonce is 0), initializable depends on receiver contract.
39
+ // For a new path (inbound_nonce is 0), initializable depends on receiver contract.
40
40
  let result = endpoint_client.initializable(&origin, &receiver);
41
41
  assert!(!result);
42
42
  }
43
43
 
44
- // Established paths always return true (lazy nonce > 0)
44
+ // Established paths always return true (inbound_nonce > 0)
45
45
  #[test]
46
46
  fn test_initializable_established_path_always_true() {
47
47
  let context = setup();
@@ -58,7 +58,7 @@ fn test_initializable_established_path_always_true() {
58
58
  context.mock_auth(&receiver, "skip", (&receiver, &receiver, &src_eid, &sender, &nonce));
59
59
  context.endpoint_client.skip(&receiver, &receiver, &src_eid, &sender, &nonce);
60
60
 
61
- // Now the path is established (lazy_nonce > 0), it should return true regardless of receiver.
61
+ // Now the path is established (inbound_nonce > 0), it should return true regardless of receiver.
62
62
  let origin2 = Origin { src_eid, sender, nonce: 2 };
63
63
  let result = endpoint_client.initializable(&origin2, &receiver);
64
64
  assert!(result);
@@ -37,7 +37,7 @@ fn test_verifiable_new_path_nonce_1_true() {
37
37
 
38
38
  // Established path => verifiable when origin.nonce is in (inbound_nonce, inbound_nonce + 256]
39
39
  #[test]
40
- fn test_verifiable_after_skip_nonce_gt_lazy_true() {
40
+ fn test_verifiable_after_skip_nonce_gt_inbound_nonce_true() {
41
41
  let context = setup();
42
42
  let env = &context.env;
43
43
  let endpoint_client = &context.endpoint_client;
@@ -54,9 +54,9 @@ fn test_verifiable_after_skip_nonce_gt_lazy_true() {
54
54
  assert!(result);
55
55
  }
56
56
 
57
- // Nonce <= lazy is still verifiable when inbound payload hash exists
57
+ // Nonce <= inbound_nonce is still verifiable when an inbound payload hash exists
58
58
  #[test]
59
- fn test_verifiable_true_when_nonce_leq_lazy_but_payload_hash_exists() {
59
+ fn test_verifiable_true_when_nonce_leq_inbound_nonce_but_payload_hash_exists() {
60
60
  let context = setup();
61
61
  let env = &context.env;
62
62
  let endpoint_client = &context.endpoint_client;
@@ -68,28 +68,28 @@ fn test_verifiable_true_when_nonce_leq_lazy_but_payload_hash_exists() {
68
68
  // Setup receive library (needed to store a payload hash via verify).
69
69
  let receive_lib = context.setup_default_receive_lib(src_eid, 0);
70
70
 
71
- // Establish the path by skipping nonce 1 (lazy becomes 1).
71
+ // Establish the path by skipping nonce 1 (inbound_nonce becomes 1).
72
72
  skip_with_auth(&context, &receiver, src_eid, &sender, 1);
73
73
 
74
- // Verify nonce 2 (allowed because 2 > lazy 1) which stores inbound payload hash for nonce 2.
74
+ // Verify nonce 2 (allowed because 2 > inbound_nonce 1) which stores inbound payload hash for nonce 2.
75
75
  let origin2 = Origin { src_eid, sender: sender.clone(), nonce: 2u64 };
76
76
  let payload_hash2 = BytesN::from_array(env, &[0x33u8; 32]);
77
77
  verify_with_auth(&context, &receive_lib, &origin2, &receiver, &payload_hash2);
78
78
 
79
- // Advance lazy nonce to 3 while keeping payload hash for 2 (skip sets lazy nonce only).
79
+ // Advance inbound_nonce to 3 while keeping payload hash for 2 (skip doesn't clear payload hashes).
80
80
  skip_with_auth(&context, &receiver, src_eid, &sender, 3);
81
81
 
82
82
  // Sanity: inbound nonce was advanced, and the payload hash for nonce 2 still exists.
83
83
  assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 3);
84
84
  assert!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2u64).is_some());
85
85
 
86
- // Now nonce 2 <= lazy 3, but payload hash exists -> verifiable should be true.
86
+ // Now nonce 2 <= inbound_nonce 3, but payload hash exists -> verifiable should be true.
87
87
  assert!(endpoint_client.verifiable(&origin2, &receiver));
88
88
  }
89
89
 
90
- // Boundary case (nonce == lazy)
90
+ // Boundary case (nonce == inbound_nonce)
91
91
  #[test]
92
- fn test_verifiable_nonce_eq_lazy_false_without_payload_hash() {
92
+ fn test_verifiable_nonce_eq_inbound_nonce_false_without_payload_hash() {
93
93
  let context = setup();
94
94
  let env = &context.env;
95
95
  let endpoint_client = &context.endpoint_client;
@@ -103,7 +103,7 @@ fn test_verifiable_nonce_eq_lazy_false_without_payload_hash() {
103
103
  skip_with_auth(&context, &receiver, src_eid, &sender, 2);
104
104
  assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
105
105
 
106
- // nonce == lazy and payload hash missing -> verifiable should be false.
106
+ // nonce == inbound_nonce and payload hash missing -> verifiable should be false.
107
107
  let origin2 = Origin { src_eid, sender: sender.clone(), nonce: 2u64 };
108
108
  assert!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2u64).is_none());
109
109
  assert!(!endpoint_client.verifiable(&origin2, &receiver));
@@ -145,3 +145,43 @@ fn test_verifiable_upper_bound_is_enforced_when_inbound_nonce_nonzero() {
145
145
  assert!(endpoint_client.verifiable(&ok, &receiver));
146
146
  assert!(!endpoint_client.verifiable(&too_far, &receiver));
147
147
  }
148
+
149
+
150
+ #[test]
151
+ fn test_verifiable_true_when_payload_hash_exists_even_if_nonce_outside_window() {
152
+ let context = setup();
153
+ let env = &context.env;
154
+ let endpoint_client = &context.endpoint_client;
155
+
156
+ let src_eid = 2u32;
157
+ let sender = BytesN::from_array(env, &[1u8; 32]);
158
+ let receiver = soroban_sdk::Address::generate(env);
159
+
160
+ // NOTE: This state should be unreachable in normal execution flows.
161
+ //
162
+ // In production, payload hashes are written by `verify()` -> `inbound()`. For *new* nonces
163
+ // (`nonce > inbound_nonce`), `inbound()` calls `insert_and_drain_pending_nonces()` which enforces
164
+ // the pending window bound (`nonce <= inbound_nonce + 256`).
165
+ //
166
+ // Therefore, with `inbound_nonce == 0`, a payload hash at a "far" nonce (e.g. 999) cannot be
167
+ // created through valid contract calls (it would fail `verifiable()` and/or the pending window
168
+ // bound on insertion).
169
+ //
170
+ // We write storage directly here to simulate corrupted/invalid state and to ensure `verifiable()`
171
+ // preserves its OR semantics: if a payload hash exists for a nonce, `verifiable()` must return
172
+ // true even when the nonce is outside the pending window.
173
+
174
+ // Choose a nonce that is clearly outside the pending window when inbound_nonce == 0.
175
+ let far_nonce = 999u64;
176
+
177
+ // Write payload hash directly to storage to exercise the OR branch:
178
+ // `EndpointStorage::has_inbound_payload_hash(...) == true` should make verifiable() return true,
179
+ // even if the nonce is outside (inbound_nonce, inbound_nonce + 256].
180
+ let payload_hash = BytesN::from_array(env, &[0x42u8; 32]);
181
+ env.as_contract(&endpoint_client.address, || {
182
+ storage::EndpointStorage::set_inbound_payload_hash(env, &receiver, src_eid, &sender, far_nonce, &payload_hash)
183
+ });
184
+
185
+ let origin_far = Origin { src_eid, sender, nonce: far_nonce };
186
+ assert!(endpoint_client.verifiable(&origin_far, &receiver));
187
+ }