@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
@@ -18,8 +18,10 @@ use soroban_sdk::{
18
18
  contract, contractimpl, contracttype, log, symbol_short,
19
19
  testutils::{Address as _, MockAuth, MockAuthInvoke},
20
20
  token::{StellarAssetClient, TokenClient},
21
- Address, Bytes, BytesN, Env, IntoVal,
21
+ Address, Bytes, BytesN, Env, IntoVal, Symbol,
22
22
  };
23
+ use oapp::oapp_core::OAPP_ADMIN_ROLE;
24
+ use utils::rbac::grant_role_no_auth;
23
25
 
24
26
  // ============================================================================
25
27
  // Test OFT Contract
@@ -178,6 +180,11 @@ fn setup_chain<'a>(env: &Env) -> ChainSetup<'a> {
178
180
  let delegate = owner.clone();
179
181
  let shared_decimals: u32 = 6; // Default shared decimals
180
182
  let oft_address = env.register(TestOFT, (&oft_token, &owner, &endpoint_address, &delegate, &shared_decimals));
183
+
184
+ // Grant OAPP_ADMIN_ROLE to owner so they can call set_peer, set_delegate, set_msg_inspector, etc.
185
+ env.as_contract(&oft_address, || {
186
+ grant_role_no_auth(env, &owner, &Symbol::new(env, OAPP_ADMIN_ROLE), &owner);
187
+ });
181
188
  let composer_address = env.register(DummyComposer, (&endpoint_address,));
182
189
 
183
190
  let endpoint = EndpointV2Client::new(env, &endpoint_address);
@@ -255,17 +262,34 @@ pub fn wire_oft(env: &Env, chains: &[&ChainSetup<'_>]) {
255
262
  }
256
263
  }
257
264
 
265
+ fn grant_oapp_admin(env: &Env, contract: &Address, owner: &Address) {
266
+ let role = soroban_sdk::Symbol::new(env, oapp::oapp_core::OAPP_ADMIN_ROLE);
267
+ env.mock_auths(&[MockAuth {
268
+ address: owner,
269
+ invoke: &MockAuthInvoke {
270
+ contract,
271
+ fn_name: "grant_role",
272
+ args: (owner, &role, owner).into_val(env),
273
+ sub_invokes: &[],
274
+ },
275
+ }]);
276
+ utils::rbac::RoleBasedAccessControlClient::new(env, contract).grant_role(owner, &role, owner);
277
+ }
278
+
258
279
  pub fn set_peer(env: &Env, owner: &Address, oft: &OFTClient<'_>, dst_eid: u32, peer: &BytesN<32>) {
280
+ grant_oapp_admin(env, &oft.address, owner);
281
+
282
+ let peer_option = Some(peer.clone());
259
283
  env.mock_auths(&[MockAuth {
260
284
  address: owner,
261
285
  invoke: &MockAuthInvoke {
262
286
  contract: &oft.address,
263
287
  fn_name: "set_peer",
264
- args: (&dst_eid, &Some(peer.clone())).into_val(env),
288
+ args: (&dst_eid, &peer_option, owner).into_val(env),
265
289
  sub_invokes: &[],
266
290
  },
267
291
  }]);
268
- oapp::oapp_core::OAppCoreClient::new(env, &oft.address).set_peer(&dst_eid, &Some(peer.clone()));
292
+ oapp::oapp_core::OAppCoreClient::new(env, &oft.address).set_peer(&dst_eid, &peer_option, owner);
269
293
  }
270
294
 
271
295
  pub fn register_library(env: &Env, owner: &Address, endpoint: &EndpointV2Client<'_>, lib: &Address) {
@@ -55,10 +55,10 @@ use crate::{
55
55
  types::{OFTFeeDetail, OFTLimit, OFTReceipt, SendParam, SEND, SEND_AND_CALL},
56
56
  utils as oft_utils,
57
57
  };
58
- use common_macros::{contract_trait, only_auth};
58
+ use common_macros::{contract_trait, only_role};
59
59
  use endpoint_v2::{MessagingComposerClient, MessagingFee, MessagingReceipt};
60
60
  use oapp::{
61
- oapp_core::initialize_oapp,
61
+ oapp_core::{initialize_oapp, OAPP_ADMIN_ROLE},
62
62
  oapp_options_type3::OAppOptionsType3,
63
63
  oapp_receiver::OAppReceiver,
64
64
  oapp_sender::{FeePayer, OAppSenderInternal},
@@ -448,12 +448,17 @@ pub trait OFTCore: OFTInternal {
448
448
  /// Pass `None` to remove the inspector and disable outbound validation.
449
449
  ///
450
450
  /// # Authorization
451
- /// Requires owner authentication (`#[only_auth]`).
451
+ /// Requires the caller to have `OAPP_ADMIN_ROLE`.
452
452
  ///
453
453
  /// # Arguments
454
454
  /// * `inspector` - Address of the inspector contract, or `None` to remove it
455
- #[only_auth]
456
- fn set_msg_inspector(env: &soroban_sdk::Env, inspector: &Option<soroban_sdk::Address>) {
455
+ /// * `operator` - The address that must have OAPP_ADMIN_ROLE
456
+ #[only_role(operator, OAPP_ADMIN_ROLE)]
457
+ fn set_msg_inspector(
458
+ env: &soroban_sdk::Env,
459
+ inspector: &Option<soroban_sdk::Address>,
460
+ operator: &soroban_sdk::Address,
461
+ ) {
457
462
  Self::__set_msg_inspector(env, inspector);
458
463
  }
459
464
 
@@ -60,17 +60,17 @@ fn test_set_msg_inspector() {
60
60
  // Deploy a passing inspector
61
61
  let inspector_address = env.register(PassingInspector, ());
62
62
 
63
- // Owner sets the inspector
63
+ // Owner (with OAPP_ADMIN_ROLE) sets the inspector
64
64
  env.mock_auths(&[MockAuth {
65
65
  address: &setup.owner,
66
66
  invoke: &MockAuthInvoke {
67
67
  contract: &setup.oft.address,
68
68
  fn_name: "set_msg_inspector",
69
- args: (&Some(inspector_address.clone()),).into_val(&env),
69
+ args: (&Some(inspector_address.clone()), &setup.owner).into_val(&env),
70
70
  sub_invokes: &[],
71
71
  },
72
72
  }]);
73
- setup.oft.set_msg_inspector(&Some(inspector_address.clone()));
73
+ setup.oft.set_msg_inspector(&Some(inspector_address.clone()), &setup.owner);
74
74
 
75
75
  // Verify inspector is set
76
76
  let stored_inspector = setup.oft.msg_inspector();
@@ -90,11 +90,11 @@ fn test_remove_msg_inspector() {
90
90
  invoke: &MockAuthInvoke {
91
91
  contract: &setup.oft.address,
92
92
  fn_name: "set_msg_inspector",
93
- args: (&Some(inspector_address.clone()),).into_val(&env),
93
+ args: (&Some(inspector_address.clone()), &setup.owner).into_val(&env),
94
94
  sub_invokes: &[],
95
95
  },
96
96
  }]);
97
- setup.oft.set_msg_inspector(&Some(inspector_address));
97
+ setup.oft.set_msg_inspector(&Some(inspector_address), &setup.owner);
98
98
 
99
99
  // Verify it's set
100
100
  assert!(setup.oft.msg_inspector().is_some());
@@ -105,11 +105,11 @@ fn test_remove_msg_inspector() {
105
105
  invoke: &MockAuthInvoke {
106
106
  contract: &setup.oft.address,
107
107
  fn_name: "set_msg_inspector",
108
- args: (&None::<Address>,).into_val(&env),
108
+ args: (&None::<Address>, &setup.owner).into_val(&env),
109
109
  sub_invokes: &[],
110
110
  },
111
111
  }]);
112
- setup.oft.set_msg_inspector(&None);
112
+ setup.oft.set_msg_inspector(&None, &setup.owner);
113
113
 
114
114
  // Verify inspector is removed
115
115
  let stored_inspector = setup.oft.msg_inspector();
@@ -119,7 +119,7 @@ fn test_remove_msg_inspector() {
119
119
  // ==================== Access Control Tests ====================
120
120
 
121
121
  #[test]
122
- #[should_panic(expected = "HostError: Error(Auth, InvalidAction)")]
122
+ #[should_panic(expected = "Error(Contract, #1086)")] // RbacError::Unauthorized
123
123
  fn test_set_msg_inspector_requires_owner() {
124
124
  let env = Env::default();
125
125
  let setup = OFTTestSetup::new(&env);
@@ -127,20 +127,20 @@ fn test_set_msg_inspector_requires_owner() {
127
127
  // Deploy a passing inspector
128
128
  let inspector_address = env.register(PassingInspector, ());
129
129
 
130
- // Non-owner tries to set the inspector (no mock_auth for owner)
130
+ // Non-owner (without OAPP_ADMIN_ROLE) tries to set the inspector
131
131
  let non_owner = Address::generate(&env);
132
132
  env.mock_auths(&[MockAuth {
133
133
  address: &non_owner,
134
134
  invoke: &MockAuthInvoke {
135
135
  contract: &setup.oft.address,
136
136
  fn_name: "set_msg_inspector",
137
- args: (&Some(inspector_address.clone()),).into_val(&env),
137
+ args: (&Some(inspector_address.clone()), &non_owner).into_val(&env),
138
138
  sub_invokes: &[],
139
139
  },
140
140
  }]);
141
141
 
142
- // This should panic because non_owner is not the owner
143
- setup.oft.set_msg_inspector(&Some(inspector_address));
142
+ // This should panic because non_owner does not have OAPP_ADMIN_ROLE
143
+ setup.oft.set_msg_inspector(&Some(inspector_address), &non_owner);
144
144
  }
145
145
 
146
146
  // ==================== Integration Tests with Send ====================
@@ -184,11 +184,11 @@ fn test_send_with_passing_inspector() {
184
184
  invoke: &MockAuthInvoke {
185
185
  contract: &setup.oft.address,
186
186
  fn_name: "set_msg_inspector",
187
- args: (&Some(inspector_address.clone()),).into_val(&env),
187
+ args: (&Some(inspector_address.clone()), &setup.owner).into_val(&env),
188
188
  sub_invokes: &[],
189
189
  },
190
190
  }]);
191
- setup.oft.set_msg_inspector(&Some(inspector_address));
191
+ setup.oft.set_msg_inspector(&Some(inspector_address), &setup.owner);
192
192
 
193
193
  let sender = Address::generate(&env);
194
194
 
@@ -224,11 +224,11 @@ fn test_send_with_failing_inspector() {
224
224
  invoke: &MockAuthInvoke {
225
225
  contract: &setup.oft.address,
226
226
  fn_name: "set_msg_inspector",
227
- args: (&Some(inspector_address.clone()),).into_val(&env),
227
+ args: (&Some(inspector_address.clone()), &setup.owner).into_val(&env),
228
228
  sub_invokes: &[],
229
229
  },
230
230
  }]);
231
- setup.oft.set_msg_inspector(&Some(inspector_address));
231
+ setup.oft.set_msg_inspector(&Some(inspector_address), &setup.owner);
232
232
 
233
233
  let sender = Address::generate(&env);
234
234
 
@@ -266,11 +266,11 @@ fn test_quote_send_with_passing_inspector() {
266
266
  invoke: &MockAuthInvoke {
267
267
  contract: &setup.oft.address,
268
268
  fn_name: "set_msg_inspector",
269
- args: (&Some(inspector_address.clone()),).into_val(&env),
269
+ args: (&Some(inspector_address.clone()), &setup.owner).into_val(&env),
270
270
  sub_invokes: &[],
271
271
  },
272
272
  }]);
273
- setup.oft.set_msg_inspector(&Some(inspector_address));
273
+ setup.oft.set_msg_inspector(&Some(inspector_address), &setup.owner);
274
274
 
275
275
  let sender = Address::generate(&env);
276
276
 
@@ -300,11 +300,11 @@ fn test_quote_send_with_failing_inspector() {
300
300
  invoke: &MockAuthInvoke {
301
301
  contract: &setup.oft.address,
302
302
  fn_name: "set_msg_inspector",
303
- args: (&Some(inspector_address.clone()),).into_val(&env),
303
+ args: (&Some(inspector_address.clone()), &setup.owner).into_val(&env),
304
304
  sub_invokes: &[],
305
305
  },
306
306
  }]);
307
- setup.oft.set_msg_inspector(&Some(inspector_address));
307
+ setup.oft.set_msg_inspector(&Some(inspector_address), &setup.owner);
308
308
 
309
309
  let sender = Address::generate(&env);
310
310
 
@@ -8,7 +8,7 @@ use crate::{
8
8
  types::{OFTReceipt, SendParam},
9
9
  };
10
10
  use endpoint_v2::{LayerZeroReceiverClient, MessagingFee, MessagingParams, MessagingReceipt, Origin};
11
- use oapp::oapp_core::OAppCoreClient;
11
+ use oapp::oapp_core::{OAppCoreClient, OAPP_ADMIN_ROLE};
12
12
  use soroban_sdk::{
13
13
  address_payload::AddressPayload,
14
14
  bytes, contract, contractimpl, log, symbol_short,
@@ -16,6 +16,7 @@ use soroban_sdk::{
16
16
  token::{StellarAssetClient, TokenClient},
17
17
  Address, Bytes, BytesN, Env, IntoVal, String, Symbol,
18
18
  };
19
+ use utils::rbac::grant_role_no_auth;
19
20
 
20
21
  // ==================== Constants ====================
21
22
 
@@ -99,6 +100,20 @@ pub fn create_origin(src_eid: u32, sender: &BytesN<32>, nonce: u64) -> Origin {
99
100
  Origin { src_eid, sender: sender.clone(), nonce }
100
101
  }
101
102
 
103
+ fn grant_oapp_admin(env: &Env, contract: &Address, owner: &Address) {
104
+ let role = Symbol::new(env, oapp::oapp_core::OAPP_ADMIN_ROLE);
105
+ env.mock_auths(&[MockAuth {
106
+ address: owner,
107
+ invoke: &MockAuthInvoke {
108
+ contract,
109
+ fn_name: "grant_role",
110
+ args: (owner, &role, owner).into_val(env),
111
+ sub_invokes: &[],
112
+ },
113
+ }]);
114
+ utils::rbac::RoleBasedAccessControlClient::new(env, contract).grant_role(owner, &role, owner);
115
+ }
116
+
102
117
  // ==================== Test OFT Contracts ====================
103
118
 
104
119
  mod test_mint_burn_oft {
@@ -596,6 +611,16 @@ impl<'a> OFTTestSetupBuilder<'a> {
596
611
  OFTTestSetup::mint_to(env, &owner, &native_token, &owner, INITIAL_MINT_AMOUNT);
597
612
  OFTTestSetup::mint_to(env, &owner, &zro_token, &owner, INITIAL_MINT_AMOUNT);
598
613
 
614
+ // Grant OAPP_ADMIN_ROLE to owner so they can call set_peer, set_delegate, set_msg_inspector, etc.
615
+ env.as_contract(&oft_address, || {
616
+ grant_role_no_auth(
617
+ env,
618
+ &owner,
619
+ &Symbol::new(env, OAPP_ADMIN_ROLE),
620
+ &owner,
621
+ );
622
+ });
623
+
599
624
  // Setup based on OFT type
600
625
  match oft_type {
601
626
  OFTType::MintBurn => {
@@ -649,16 +674,19 @@ impl<'a> OFTTestSetup<'a> {
649
674
  }
650
675
 
651
676
  pub fn set_peer(&self, eid: u32, peer: &BytesN<32>) {
677
+ grant_oapp_admin(self.env, &self.oft.address, &self.owner);
678
+
679
+ let peer_option = Some(peer.clone());
652
680
  self.env.mock_auths(&[MockAuth {
653
681
  address: &self.owner,
654
682
  invoke: &MockAuthInvoke {
655
683
  contract: &self.oft.address,
656
684
  fn_name: "set_peer",
657
- args: (&eid, peer).into_val(self.env),
685
+ args: (&eid, &peer_option, &self.owner).into_val(self.env),
658
686
  sub_invokes: &[],
659
687
  },
660
688
  }]);
661
- OAppCoreClient::new(self.env, &self.oft.address).set_peer(&eid, &Some(peer.clone()));
689
+ OAppCoreClient::new(self.env, &self.oft.address).set_peer(&eid, &peer_option, &self.owner);
662
690
  }
663
691
 
664
692
  pub fn mint_to(env: &Env, owner: &Address, token: &Address, to: &Address, amount: i128) {
@@ -2,30 +2,47 @@
2
2
 
3
3
  //! # Upgrader Contract
4
4
  //!
5
- //! A stateless utility contract for performing atomic upgrade and migrate operations
6
- //! on contracts implementing the [`Upgradeable`](utils::upgradeable::Upgradeable) trait.
5
+ //! A stateless utility contract for performing atomic upgrade-and-migrate operations
6
+ //! on contracts that implement [`Upgradeable`](utils::upgradeable::Upgradeable) (Auth-based)
7
+ //! or [`UpgradeableRbac`](utils::upgradeable::UpgradeableRbac) (RBAC-based).
7
8
  //!
8
- //! ## Security Model
9
+ //! ## Security model
9
10
  //!
10
- //! The Upgrader is permissionless - anyone can call it, but security is enforced by the
11
- //! target contract's authorization checks. The target contract's `#[only_auth]` guard
12
- //! ensures only its authorizer can successfully upgrade it.
11
+ //! The Upgrader is permissionless: anyone may call it. Security is enforced by the target
12
+ //! contracts authorization:
13
+ //! - **Auth-based**: the target’s `#[only_auth]` ensures only its authorizer can upgrade/migrate.
14
+ //! - **RBAC-based**: the target’s `#[only_role(operator, UPGRADER_ROLE)]` ensures only an
15
+ //! address with `UPGRADER_ROLE` can upgrade/migrate; that address must be passed as `operator`
16
+ //! and must have signed the transaction.
13
17
  //!
14
18
  //! ## Usage
15
19
  //!
20
+ //! - For **Auth-based** targets, pass `operator: &None`. The transaction must be authorized
21
+ //! by the target contract’s authorizer.
22
+ //! - For **RBAC-based** targets, pass `operator: &Some(upgrader_address)`. The transaction
23
+ //! must be signed by that address, which must hold `UPGRADER_ROLE` on the target.
24
+ //!
16
25
  //! ```ignore
17
26
  //! let upgrader = UpgraderClient::new(&env, &upgrader_id);
18
27
  //! let migration_data = my_data.to_xdr(&env);
19
- //! upgrader.upgrade_and_migrate(&target_contract, &new_wasm_hash, &migration_data);
28
+ //! // Auth-based target:
29
+ //! upgrader.upgrade_and_migrate(&target_contract, &new_wasm_hash, &migration_data, &None);
30
+ //! // RBAC-based target:
31
+ //! upgrader.upgrade_and_migrate(&target_contract, &new_wasm_hash, &migration_data, &Some(operator));
20
32
  //! ```
21
33
 
22
34
  use soroban_sdk::{contract, contractimpl, xdr::ToXdr, Address, Bytes, BytesN, Env};
23
- use utils::{auth::AuthClient, errors::AuthError, option_ext::OptionExt, upgradeable::UpgradeableClient};
35
+ use utils::{
36
+ auth::AuthClient,
37
+ errors::AuthError,
38
+ option_ext::OptionExt,
39
+ upgradeable::{UpgradeableClient, UpgradeableRbacClient},
40
+ };
24
41
 
25
42
  /// Upgrader contract for managing upgrades of other contracts.
26
43
  ///
27
- /// This is a stateless utility contract that anyone can use.
28
- /// Security is enforced by the target contract's ownership checks.
44
+ /// Stateless utility: anyone may call it. Authorization is enforced by the target
45
+ /// contract (Auth or RBAC).
29
46
  #[contract]
30
47
  pub struct Upgrader;
31
48
 
@@ -33,39 +50,59 @@ pub struct Upgrader;
33
50
  impl Upgrader {
34
51
  /// Upgrades a target contract without custom migration data.
35
52
  ///
36
- /// This is a convenience wrapper that calls `upgrade_and_migrate` with empty migration data.
53
+ /// Convenience wrapper around [`upgrade_and_migrate`](Self::upgrade_and_migrate) that
54
+ /// passes empty migration data (XDR encoding of `()`). Use only when the target’s
55
+ /// `MigrationData` is `()` or it supports empty migration.
37
56
  ///
38
57
  /// # Arguments
39
- /// * `contract_address` - The address of the contract to upgrade
40
- /// * `wasm_hash` - The hash of the new WASM bytecode
41
- pub fn upgrade(env: &Env, contract_address: &Address, wasm_hash: &BytesN<32>) {
42
- Self::upgrade_and_migrate(env, contract_address, wasm_hash, &().to_xdr(env));
58
+ /// * `contract_address` - Address of the contract to upgrade.
59
+ /// * `wasm_hash` - Hash of the new WASM bytecode.
60
+ /// * `operator` - `None` for Auth-based targets; `Some(addr)` for RBAC-based targets
61
+ pub fn upgrade(env: &Env, contract_address: &Address, wasm_hash: &BytesN<32>, operator: &Option<Address>) {
62
+ Self::upgrade_and_migrate(env, contract_address, wasm_hash, &().to_xdr(env), operator);
43
63
  }
44
64
 
45
65
  /// Upgrades a target contract and runs its migration in a single transaction.
46
66
  ///
47
- /// The caller must be authorized as the authorizer of the target contract.
48
- /// This is enforced by the target contract's `#[only_auth]` check.
67
+ /// Chooses Auth-based or RBAC-based flow from `operator`:
68
+ /// - **`Some(operator)`**: RBAC flow. `operator` must sign the transaction and must have
69
+ /// `UPGRADER_ROLE` on the target. The target must implement [`UpgradeableRbac`](utils::upgradeable::UpgradeableRbac).
70
+ /// - **`None`**: Auth flow. The target’s authorizer must sign the transaction. The target
71
+ /// must implement [`Upgradeable`](utils::upgradeable::Upgradeable).
49
72
  ///
50
73
  /// # Arguments
51
- /// * `contract_address` - The address of the contract to upgrade
52
- /// * `wasm_hash` - The hash of the new WASM bytecode
53
- /// * `migration_data` - XDR-encoded bytes to pass to the migrate function.
54
- /// Use `value.to_xdr(&env)` to encode the target contract's MigrationData type.
74
+ /// * `contract_address` - Address of the contract to upgrade.
75
+ /// * `wasm_hash` - Hash of the new WASM bytecode.
76
+ /// * `migration_data` - XDR-encoded migration payload. Use `value.to_xdr(&env)` for the
77
+ /// target contract’s `MigrationData` type; use `().to_xdr(&env)` for no custom data.
78
+ /// * `operator` - `None` for Auth-based target; `Some(operator)` for RBAC-based target.
55
79
  ///
56
80
  /// # Example
57
81
  /// ```ignore
58
82
  /// let migration_data = my_data.to_xdr(&env);
59
- /// upgrader.upgrade_and_migrate(&contract_addr, &wasm_hash, &migration_data);
83
+ /// upgrader.upgrade_and_migrate(&contract_addr, &wasm_hash, &migration_data, &None);
60
84
  /// ```
61
- pub fn upgrade_and_migrate(env: &Env, contract_address: &Address, wasm_hash: &BytesN<32>, migration_data: &Bytes) {
62
- AuthClient::new(env, contract_address)
63
- .authorizer()
64
- .unwrap_or_panic(env, AuthError::AuthorizerNotFound)
65
- .require_auth();
66
- let client = UpgradeableClient::new(env, contract_address);
67
- client.upgrade(wasm_hash);
68
- client.migrate(migration_data);
85
+ pub fn upgrade_and_migrate(
86
+ env: &Env,
87
+ contract_address: &Address,
88
+ wasm_hash: &BytesN<32>,
89
+ migration_data: &Bytes,
90
+ operator: &Option<Address>,
91
+ ) {
92
+ if let Some(operator) = operator {
93
+ operator.require_auth();
94
+ let client = UpgradeableRbacClient::new(env, contract_address);
95
+ client.upgrade(wasm_hash, operator);
96
+ client.migrate(migration_data, operator);
97
+ } else {
98
+ AuthClient::new(env, contract_address)
99
+ .authorizer()
100
+ .unwrap_or_panic(env, AuthError::AuthorizerNotFound)
101
+ .require_auth();
102
+ let client = UpgradeableClient::new(env, contract_address);
103
+ client.upgrade(wasm_hash);
104
+ client.migrate(migration_data);
105
+ }
69
106
  }
70
107
  }
71
108
 
@@ -16,6 +16,19 @@ trait TestUpgradeableContract2 {
16
16
  fn counter2(env: &Env) -> u32;
17
17
  }
18
18
 
19
+ #[allow(dead_code)]
20
+ #[contractclient(name = "TestRbacUpgradeableContractClient")]
21
+ trait TestRbacUpgradeableContract {
22
+ fn counter(env: &Env) -> u32;
23
+ }
24
+
25
+ #[allow(dead_code)]
26
+ #[contractclient(name = "TestRbacUpgradeableContractClient2")]
27
+ trait TestRbacUpgradeableContract2 {
28
+ fn counter(env: &Env) -> u32;
29
+ fn counter2(env: &Env) -> u32;
30
+ }
31
+
19
32
  mod contract_v1 {
20
33
  //#![no_std]
21
34
 
@@ -100,6 +113,13 @@ mod contract_v2 {
100
113
  soroban_sdk::contractimport!(file = "./src/tests/test_data/test_upgradeable_contract2.wasm");
101
114
  }
102
115
 
116
+ mod contract_v3 {
117
+ soroban_sdk::contractimport!(file = "./src/tests/test_data/test_upgradeable_contract3.wasm");
118
+ }
119
+ mod contract_v4 {
120
+ soroban_sdk::contractimport!(file = "./src/tests/test_data/test_upgradeable_contract4.wasm");
121
+ }
122
+
103
123
  fn install_new_wasm(e: &Env) -> BytesN<32> {
104
124
  e.deployer().upload_contract_wasm(contract_v2::WASM)
105
125
  }
@@ -107,7 +127,7 @@ fn install_new_wasm(e: &Env) -> BytesN<32> {
107
127
  #[test]
108
128
  fn test_upgrade_with_upgrader() {
109
129
  let e = Env::default();
110
- e.mock_all_auths_allowing_non_root_auth();
130
+ e.mock_all_auths();
111
131
 
112
132
  let owner = Address::generate(&e);
113
133
  let contract_id = e.register(contract_v1::WASM, (&owner,));
@@ -121,7 +141,7 @@ fn test_upgrade_with_upgrader() {
121
141
  let counter_value = 2_u32;
122
142
  // Encode migration data as XDR bytes
123
143
  let migration_data = counter_value.to_xdr(&e);
124
- upgrader_client.upgrade_and_migrate(&contract_id, &new_wasm_hash, &migration_data);
144
+ upgrader_client.upgrade_and_migrate(&contract_id, &new_wasm_hash, &migration_data, &None);
125
145
 
126
146
  let client_v2 = TestUpgradeableContractClient2::new(&e, &contract_id);
127
147
 
@@ -131,7 +151,7 @@ fn test_upgrade_with_upgrader() {
131
151
  #[test]
132
152
  fn test_upgrade_without_migration_data_returns_error_for_non_unit_migration() {
133
153
  let e = Env::default();
134
- e.mock_all_auths_allowing_non_root_auth();
154
+ e.mock_all_auths();
135
155
 
136
156
  let owner = Address::generate(&e);
137
157
  let contract_id = e.register(contract_v1::WASM, (&owner,));
@@ -142,6 +162,32 @@ fn test_upgrade_without_migration_data_returns_error_for_non_unit_migration() {
142
162
  let new_wasm_hash = install_new_wasm(&e);
143
163
  // The upgradeable WASM fixture requires non-unit migration data (u32).
144
164
  // `Upgrader::upgrade` always passes empty `()` migration bytes, so this must fail.
145
- let res = upgrader_client.try_upgrade(&contract_id, &new_wasm_hash);
165
+ let res = upgrader_client.try_upgrade(&contract_id, &new_wasm_hash, &None);
146
166
  assert_eq!(res.err().unwrap().unwrap(), utils::errors::UpgradeableError::InvalidMigrationData.into());
147
167
  }
168
+
169
+ #[test]
170
+ fn test_upgrade_with_upgrader_rbac() {
171
+ let e = Env::default();
172
+ e.mock_all_auths();
173
+
174
+ let owner = Address::generate(&e);
175
+ let operator = Address::generate(&e);
176
+ // RBAC fixture constructor: (owner, upgrader_operator)
177
+ let contract_id = e.register(contract_v3::WASM, (&owner, &operator));
178
+ let client_v3 = TestRbacUpgradeableContractClient::new(&e, &contract_id);
179
+ assert_eq!(client_v3.counter(), 1);
180
+
181
+ let upgrader = e.register(Upgrader, ());
182
+ let upgrader_client = UpgraderClient::new(&e, &upgrader);
183
+
184
+ let new_wasm_hash = e.deployer().upload_contract_wasm(contract_v4::WASM);
185
+ let counter_value = 42_u32;
186
+ let migration_data = counter_value.to_xdr(&e);
187
+ // Use RBAC path: pass Some(operator) so the upgrader uses UpgradeableRbac and operator must have signed
188
+ upgrader_client.upgrade_and_migrate(&contract_id, &new_wasm_hash, &migration_data, &Some(operator));
189
+
190
+ let client_v4 = TestRbacUpgradeableContractClient2::new(&e, &contract_id);
191
+ assert_eq!(client_v4.counter(), 1);
192
+ assert_eq!(client_v4.counter2(), counter_value);
193
+ }
@@ -64,7 +64,7 @@ pub enum OwnableStorage {
64
64
  ///
65
65
  /// Supports both single-step and two-step ownership transfer:
66
66
  /// - Single-step: `transfer_ownership` - Immediate transfer (use with caution)
67
- /// - Two-step: `propose_ownership_transfer` + `accept_ownership` - Safer, requires new owner to accept
67
+ /// - Two-step: `begin_ownership_transfer` + `accept_ownership` - Safer, requires new owner to accept
68
68
  #[contract_trait]
69
69
  pub trait Ownable: Auth {
70
70
  // ===========================================================================
@@ -88,7 +88,7 @@ pub trait Ownable: Auth {
88
88
  /// Transfers ownership immediately to a new address.
89
89
  ///
90
90
  /// Use with caution - if you transfer to a wrong address, ownership is lost forever.
91
- /// Consider using `propose_ownership_transfer` instead.
91
+ /// Consider using `begin_ownership_transfer` instead.
92
92
  ///
93
93
  /// # Panics
94
94
  /// - `OwnerNotSet` if no owner is currently set
@@ -105,7 +105,7 @@ pub trait Ownable: Auth {
105
105
  // Two-step transfer (safer)
106
106
  // ===========================================================================
107
107
 
108
- /// Proposes an ownership transfer to a new address.
108
+ /// Begins an ownership transfer to a new address.
109
109
  ///
110
110
  /// The new owner must call `accept_ownership()` within `ttl` ledgers
111
111
  /// to complete the transfer. The pending transfer will automatically expire after.
@@ -120,7 +120,7 @@ pub trait Ownable: Auth {
120
120
  /// - `NoPendingTransfer` when cancelling and no pending transfer exists
121
121
  /// - `InvalidTtl` if ttl exceeds max TTL
122
122
  /// - `InvalidPendingOwner` when cancelling with wrong new_owner address
123
- fn propose_ownership_transfer(env: &soroban_sdk::Env, new_owner: &soroban_sdk::Address, ttl: u32) {
123
+ fn begin_ownership_transfer(env: &soroban_sdk::Env, new_owner: &soroban_sdk::Address, ttl: u32) {
124
124
  let old_owner = enforce_owner_auth::<Self>(env);
125
125
 
126
126
  // Cancel case: ttl == 0
@@ -158,7 +158,7 @@ pub trait Ownable: Auth {
158
158
  new_owner.require_auth();
159
159
 
160
160
  // Safe to unwrap: owner must exist if pending_owner exists because:
161
- // 1. pending_owner can only be set via propose_ownership_transfer, which requires owner auth
161
+ // 1. pending_owner can only be set via begin_ownership_transfer, which requires owner auth
162
162
  // 2. renounce_ownership is blocked while a 2-step transfer is in progress
163
163
  let old_owner = OwnableStorage::owner(env).unwrap();
164
164
 
@@ -189,6 +189,17 @@ pub trait Ownable: Auth {
189
189
 
190
190
  /// Trait for initializing the owner of the contract.
191
191
  pub trait OwnableInitializer {
192
+ /// Initializes the owner of the contract.
193
+ ///
194
+ /// # Critical: constructor-only, never expose as a public entrypoint
195
+ ///
196
+ /// `init_owner` must **ONLY** be called from the contract constructor. Do not expose it
197
+ /// as a public function under the assumption that it will "simply fail" after initialization.
198
+ ///
199
+ /// After `renounce_ownership`, the owner is removed and `has_owner` returns false. If
200
+ /// `init_owner` were exposed publicly, anyone could call it post-renounce and become the
201
+ /// new owner, effectively undoing the renunciation. Always keep this logic internal to
202
+ /// the constructor.
192
203
  fn init_owner(env: &Env, owner: &Address) {
193
204
  assert_with_error!(env, !OwnableStorage::has_owner(env), OwnableError::OwnerAlreadySet);
194
205
  OwnableStorage::set_owner(env, owner);