@layerzerolabs/protocol-stellar-v2 0.2.33 → 0.2.35

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 (123) hide show
  1. package/.turbo/turbo-build.log +351 -367
  2. package/.turbo/turbo-lint.log +220 -223
  3. package/.turbo/turbo-test.log +1993 -1796
  4. package/Cargo.lock +10 -10
  5. package/Cargo.toml +1 -1
  6. package/contracts/common-macros/src/storage.rs +7 -5
  7. package/contracts/common-macros/src/tests/storage/snapshots/common_macros__tests__storage__generate_storage__snapshot_generated_storage_code.snap +3 -3
  8. package/contracts/endpoint-v2/src/endpoint_v2.rs +5 -4
  9. package/contracts/endpoint-v2/src/interfaces/messaging_channel.rs +7 -8
  10. package/contracts/endpoint-v2/src/messaging_channel.rs +78 -45
  11. package/contracts/endpoint-v2/src/storage.rs +8 -3
  12. package/contracts/endpoint-v2/src/tests/endpoint_setup.rs +2 -2
  13. package/contracts/endpoint-v2/src/tests/endpoint_v2/clear.rs +12 -15
  14. package/contracts/endpoint-v2/src/tests/endpoint_v2/verifiable.rs +46 -9
  15. package/contracts/endpoint-v2/src/tests/messaging_channel/burn.rs +7 -23
  16. package/contracts/endpoint-v2/src/tests/messaging_channel/clear_payload.rs +23 -20
  17. package/contracts/endpoint-v2/src/tests/messaging_channel/inbound.rs +94 -1
  18. package/contracts/endpoint-v2/src/tests/messaging_channel/inbound_nonce.rs +17 -15
  19. package/contracts/endpoint-v2/src/tests/messaging_channel/mod.rs +1 -1
  20. package/contracts/endpoint-v2/src/tests/messaging_channel/nilify.rs +48 -13
  21. package/contracts/endpoint-v2/src/tests/messaging_channel/pending_inbound_nonces.rs +111 -0
  22. package/contracts/endpoint-v2/src/tests/messaging_channel/skip.rs +15 -25
  23. package/contracts/layerzero-views/src/layerzero_view.rs +2 -2
  24. package/contracts/layerzero-views/src/tests/layerzero_view_tests.rs +3 -4
  25. package/contracts/layerzero-views/src/tests/setup.rs +0 -21
  26. package/contracts/message-libs/blocked-message-lib/src/lib.rs +4 -4
  27. package/contracts/message-libs/uln-302/src/send_uln.rs +5 -5
  28. package/contracts/oapps/counter/src/counter.rs +6 -0
  29. package/contracts/oapps/oapp/src/oapp_sender.rs +3 -2
  30. package/contracts/oapps/oft/integration-tests/extensions/test_oft_fee.rs +5 -11
  31. package/contracts/oapps/oft/integration-tests/extensions/test_pausable.rs +7 -14
  32. package/contracts/oapps/oft/integration-tests/extensions/test_rate_limiter.rs +11 -22
  33. package/contracts/oapps/oft/integration-tests/setup.rs +59 -7
  34. package/contracts/oapps/oft/integration-tests/utils.rs +28 -2
  35. package/contracts/oapps/oft/src/extensions/oft_fee.rs +5 -0
  36. package/contracts/oapps/oft/src/interfaces/mintable.rs +14 -0
  37. package/contracts/oapps/oft/src/interfaces/mod.rs +2 -2
  38. package/contracts/oapps/oft/src/oft.rs +8 -7
  39. package/contracts/oapps/oft/src/oft_types/mint_burn.rs +8 -8
  40. package/contracts/oapps/oft/src/oft_types/mod.rs +3 -4
  41. package/contracts/oapps/oft/src/tests/extensions/rate_limiter.rs +7 -5
  42. package/contracts/oapps/sac-manager/src/errors.rs +14 -0
  43. package/contracts/{sac-manager → oapps/sac-manager}/src/lib.rs +0 -4
  44. package/contracts/oapps/sac-manager/src/sac_manager.rs +115 -0
  45. package/contracts/oapps/sac-manager/src/storage.rs +20 -0
  46. package/contracts/{sac-manager → oapps/sac-manager}/src/tests/mod.rs +0 -4
  47. package/contracts/oapps/sac-manager/src/tests/sac_manager/clawback.rs +86 -0
  48. package/contracts/oapps/sac-manager/src/tests/sac_manager/mint.rs +58 -0
  49. package/contracts/{sac-manager → oapps/sac-manager}/src/tests/sac_manager/mod.rs +1 -3
  50. package/contracts/oapps/sac-manager/src/tests/sac_manager/set_minter.rs +69 -0
  51. package/contracts/oapps/sac-manager/src/tests/sac_manager/test_helper.rs +18 -0
  52. package/contracts/oapps/sac-manager/src/tests/sac_manager/view_functions.rs +28 -0
  53. package/contracts/{sac-manager → oapps/sac-manager}/src/tests/test_helper.rs +16 -59
  54. package/docs/layerzero-v2-on-stellar.md +46 -2
  55. package/package.json +8 -3
  56. package/sdk/.turbo/turbo-test.log +424 -429
  57. package/sdk/dist/generated/bml.d.ts +3 -3
  58. package/sdk/dist/generated/bml.js +3 -3
  59. package/sdk/dist/generated/counter.d.ts +32 -3
  60. package/sdk/dist/generated/counter.js +6 -3
  61. package/sdk/dist/generated/dvn.d.ts +3 -3
  62. package/sdk/dist/generated/dvn.js +3 -3
  63. package/sdk/dist/generated/dvn_fee_lib.d.ts +2 -2
  64. package/sdk/dist/generated/dvn_fee_lib.js +2 -2
  65. package/sdk/dist/generated/endpoint.d.ts +12 -13
  66. package/sdk/dist/generated/endpoint.js +7 -7
  67. package/sdk/dist/generated/executor.d.ts +3 -3
  68. package/sdk/dist/generated/executor.js +3 -3
  69. package/sdk/dist/generated/executor_fee_lib.d.ts +2 -2
  70. package/sdk/dist/generated/executor_fee_lib.js +2 -2
  71. package/sdk/dist/generated/executor_helper.d.ts +2 -2
  72. package/sdk/dist/generated/executor_helper.js +2 -2
  73. package/sdk/dist/generated/layerzero_view.d.ts +3 -3
  74. package/sdk/dist/generated/layerzero_view.js +3 -3
  75. package/sdk/dist/generated/oft.d.ts +32 -3
  76. package/sdk/dist/generated/oft.js +7 -4
  77. package/sdk/dist/generated/price_feed.d.ts +3 -3
  78. package/sdk/dist/generated/price_feed.js +3 -3
  79. package/sdk/dist/generated/sac_manager.d.ts +47 -318
  80. package/sdk/dist/generated/sac_manager.js +24 -129
  81. package/sdk/dist/generated/sml.d.ts +2 -2
  82. package/sdk/dist/generated/sml.js +2 -2
  83. package/sdk/dist/generated/treasury.d.ts +2 -2
  84. package/sdk/dist/generated/treasury.js +2 -2
  85. package/sdk/dist/generated/uln302.d.ts +3 -3
  86. package/sdk/dist/generated/uln302.js +3 -3
  87. package/sdk/dist/generated/upgrader.d.ts +2 -2
  88. package/sdk/dist/generated/upgrader.js +2 -2
  89. package/sdk/package.json +6 -1
  90. package/sdk/test/oft-sml.test.ts +72 -36
  91. package/sdk/test/sac-manager-redistribution.test.ts +38 -182
  92. package/contracts/endpoint-v2/src/tests/messaging_channel/lazy_inbound_nonce.rs +0 -39
  93. package/contracts/oapps/oft/src/interfaces/mint_burnable.rs +0 -18
  94. package/contracts/sac-manager/src/errors.rs +0 -18
  95. package/contracts/sac-manager/src/extensions/mod.rs +0 -6
  96. package/contracts/sac-manager/src/extensions/redistribution.rs +0 -109
  97. package/contracts/sac-manager/src/extensions/supply_control/mod.rs +0 -488
  98. package/contracts/sac-manager/src/extensions/supply_control/rate_limit.rs +0 -126
  99. package/contracts/sac-manager/src/interfaces/mod.rs +0 -3
  100. package/contracts/sac-manager/src/interfaces/sac_manager.rs +0 -52
  101. package/contracts/sac-manager/src/sac_manager.rs +0 -193
  102. package/contracts/sac-manager/src/storage.rs +0 -20
  103. package/contracts/sac-manager/src/tests/redistribution/mod.rs +0 -1
  104. package/contracts/sac-manager/src/tests/redistribution/redistribute_funds.rs +0 -82
  105. package/contracts/sac-manager/src/tests/sac_manager/admin_mint.rs +0 -206
  106. package/contracts/sac-manager/src/tests/sac_manager/burn.rs +0 -215
  107. package/contracts/sac-manager/src/tests/sac_manager/clawback.rs +0 -209
  108. package/contracts/sac-manager/src/tests/sac_manager/mint.rs +0 -252
  109. package/contracts/sac-manager/src/tests/sac_manager/set_oft_address.rs +0 -47
  110. package/contracts/sac-manager/src/tests/sac_manager/test_helper.rs +0 -75
  111. package/contracts/sac-manager/src/tests/sac_manager/view_functions.rs +0 -60
  112. package/contracts/sac-manager/src/tests/supply_control/enumerable_set.rs +0 -256
  113. package/contracts/sac-manager/src/tests/supply_control/mod.rs +0 -8
  114. package/contracts/sac-manager/src/tests/supply_control/refill.rs +0 -90
  115. package/contracts/sac-manager/src/tests/supply_control/set_mint_whitelist.rs +0 -245
  116. package/contracts/sac-manager/src/tests/supply_control/set_supply_controller.rs +0 -267
  117. package/contracts/sac-manager/src/tests/supply_control/set_supply_controller_manager.rs +0 -122
  118. package/contracts/sac-manager/src/tests/supply_control/test_helper.rs +0 -38
  119. package/contracts/sac-manager/src/tests/supply_control/update_allow_any_mint_burn.rs +0 -114
  120. package/contracts/sac-manager/src/tests/supply_control/update_limit_config.rs +0 -257
  121. /package/contracts/{sac-manager → oapps/sac-manager}/Cargo.toml +0 -0
  122. /package/contracts/{sac-manager → oapps/sac-manager}/src/tests/sac_manager/set_admin.rs +0 -0
  123. /package/contracts/{sac-manager → oapps/sac-manager}/src/tests/sac_manager/set_authorized.rs +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  use crate::extensions::rate_limiter::{Direction, Mode, RateLimitConfig};
4
4
  use crate::integration_tests::setup::{decode_packet, ChainSetup};
5
+ use crate::MintableClient;
5
6
  use endpoint_v2::{MessagingFee, Origin, OutboundPacket};
6
7
  use message_lib_common::packet_codec_v1;
7
8
  use oft_core::{OFTFeeDetail, OFTLimit, OFTReceipt, SendParam};
@@ -38,7 +39,11 @@ pub fn create_recipient_address(env: &Env) -> Address {
38
39
  // OFT Core Operations
39
40
  // ============================================================================
40
41
 
41
- pub fn quote_oft(chain: &ChainSetup<'_>, from: &Address, send_param: &SendParam) -> (OFTLimit, Vec<OFTFeeDetail>, OFTReceipt) {
42
+ pub fn quote_oft(
43
+ chain: &ChainSetup<'_>,
44
+ from: &Address,
45
+ send_param: &SendParam,
46
+ ) -> (OFTLimit, Vec<OFTFeeDetail>, OFTReceipt) {
42
47
  chain.oft.quote_oft(from, send_param)
43
48
  }
44
49
 
@@ -61,7 +66,8 @@ pub fn quote_send(
61
66
  chain.oft.quote_send(sender, send_param, &pay_in_zro)
62
67
  }
63
68
 
64
- /// Send without fee (standard OFT send)
69
+ /// Send without fee (standard OFT send).
70
+ /// Sender authorizes OFT send (OFT debits by calling token burn directly) and SAC burn.
65
71
  pub fn send(
66
72
  env: &Env,
67
73
  chain: &ChainSetup<'_>,
@@ -308,6 +314,26 @@ pub fn mint_to(env: &Env, owner: &Address, token: &Address, to: &Address, amount
308
314
  sac.mint(to, &amount);
309
315
  }
310
316
 
317
+ /// Mints the OFT token (via the Mintable wrapper) to the given address.
318
+ /// Use when OFT is MintBurn; the wrapper calls the underlying SAC mint.
319
+ pub fn mint_oft_token_to(env: &Env, chain: &ChainSetup<'_>, to: &Address, amount: i128) {
320
+ env.mock_auths(&[MockAuth {
321
+ address: &chain.owner,
322
+ invoke: &MockAuthInvoke {
323
+ contract: &chain.sac_wrapper,
324
+ fn_name: "mint",
325
+ args: (to, &amount, &chain.owner).into_val(env),
326
+ sub_invokes: &[MockAuthInvoke {
327
+ contract: &chain.oft_token,
328
+ fn_name: "mint",
329
+ args: (to, &amount).into_val(env),
330
+ sub_invokes: &[],
331
+ }],
332
+ },
333
+ }]);
334
+ MintableClient::new(env, &chain.sac_wrapper).mint(to, &amount, &chain.owner);
335
+ }
336
+
311
337
  pub fn transfer_sac_admin(env: &Env, owner: &Address, token: &Address, new_admin: &Address) {
312
338
  env.mock_auths(&[MockAuth {
313
339
  address: owner,
@@ -122,6 +122,11 @@ pub trait OFTFee: OFTFeeInternal + Auth {
122
122
  Self::__effective_fee_bps(env, dst_eid)
123
123
  }
124
124
 
125
+ /// Returns true if the OFT has a fee rate greater than 0 for the specified destination
126
+ fn has_oft_fee(env: &soroban_sdk::Env, dst_eid: u32) -> bool {
127
+ Self::__effective_fee_bps(env, dst_eid) > 0
128
+ }
129
+
125
130
  /// Returns the fee deposit address.
126
131
  fn fee_deposit_address(env: &soroban_sdk::Env) -> Option<soroban_sdk::Address> {
127
132
  Self::__fee_deposit_address(env)
@@ -0,0 +1,14 @@
1
+ //! Mintable trait - the interface the OFT uses to mint tokens on credit (receive).
2
+
3
+ use soroban_sdk::{contractclient, Address, Env};
4
+
5
+ /// The mint interface for OFT MintBurn operations.
6
+ ///
7
+ /// A contract that implements `mint` (e.g. SAC Manager or a token wrapper) is used
8
+ /// for crediting; the OFT calls the token (SAC) directly for burn on debit.
9
+ #[contractclient(name = "MintableClient")]
10
+ pub trait Mintable {
11
+ /// Mints `amount` tokens to `to`. The `operation` address is the caller (e.g. OFT)
12
+ /// requesting the mint, for use by SAC wrappers that enforce authorization.
13
+ fn mint(env: &Env, to: &Address, amount: i128, operation: &Address);
14
+ }
@@ -1,3 +1,3 @@
1
- mod mint_burnable;
1
+ mod mintable;
2
2
 
3
- pub use mint_burnable::*;
3
+ pub use mintable::*;
@@ -80,21 +80,22 @@ impl OFTInternal for OFT {
80
80
  /// Overrides default to add pausable check and fee calculation.
81
81
  ///
82
82
  /// Dust handling (consistent with EVM):
83
- /// - fee = 0: dust stays with sender (amount_sent_ld has dust removed)
84
- /// - fee > 0: dust is absorbed into the charged fee (amount_sent_ld is the full amount)
83
+ /// - no fee: dust stays with sender (amount_sent_ld has dust removed)
84
+ /// - has fee: dust is absorbed into the charged fee (amount_sent_ld is the full amount)
85
85
  fn __debit_view(env: &Env, amount_ld: i128, min_amount_ld: i128, dst_eid: u32) -> (i128, i128) {
86
86
  Self::__assert_not_paused(env);
87
87
 
88
88
  let conversion_rate = Self::__decimal_conversion_rate(env);
89
- let fee = Self::__fee_view(env, dst_eid, amount_ld);
89
+ let has_fee = Self::has_oft_fee(env, dst_eid);
90
90
 
91
- let (amount_sent_ld, amount_received_ld) = if fee == 0 {
91
+ let (amount_sent_ld, amount_received_ld) = if !has_fee {
92
92
  // No fee: dust stays with sender (default OFT behavior)
93
93
  let amount_sent_ld = oft_utils::remove_dust(amount_ld, conversion_rate);
94
94
  (amount_sent_ld, amount_sent_ld)
95
95
  } else {
96
96
  // With fee: match EVM OFTFee behavior
97
97
  // - sender pays full amount_ld (no dust removed), dust is absorbed into the charged fee
98
+ let fee = Self::__fee_view(env, dst_eid, amount_ld);
98
99
  let amount_received_ld = oft_utils::remove_dust(amount_ld - fee, conversion_rate);
99
100
  (amount_ld, amount_received_ld)
100
101
  };
@@ -110,8 +111,8 @@ impl OFTInternal for OFT {
110
111
  OftType::LockUnlock => {
111
112
  lock_unlock::debit::<Self>(env, &Self::token(env), sender, amount_ld, min_amount_ld, dst_eid)
112
113
  }
113
- OftType::MintBurn(mint_burnable) => {
114
- mint_burn::debit::<Self>(env, &mint_burnable, sender, amount_ld, min_amount_ld, dst_eid)
114
+ OftType::MintBurn(_mintable) => {
115
+ mint_burn::debit::<Self>(env, &Self::token(env), sender, amount_ld, min_amount_ld, dst_eid)
115
116
  }
116
117
  };
117
118
 
@@ -133,7 +134,7 @@ impl OFTInternal for OFT {
133
134
  // Core credit logic (based on mode)
134
135
  let amount_credited = match Self::oft_type(env) {
135
136
  OftType::LockUnlock => lock_unlock::credit::<Self>(env, &Self::token(env), to, amount_ld, src_eid),
136
- OftType::MintBurn(mint_burnable) => mint_burn::credit::<Self>(env, &mint_burnable, to, amount_ld, src_eid),
137
+ OftType::MintBurn(mintable) => mint_burn::credit::<Self>(env, &mintable, to, amount_ld, src_eid),
137
138
  };
138
139
 
139
140
  // Rate limit checks
@@ -1,17 +1,17 @@
1
1
  //! MintBurn type implementation for OFT.
2
2
  //!
3
3
  //! This OFT type burns tokens on debit (send) and mints tokens on credit (receive).
4
- //! Used when the OFT contract has mint/burn authority over the token.
4
+ //! Used when the OFT contract has mint authority over the token.
5
5
 
6
- use crate::interfaces::MintBurnableClient;
6
+ use crate::interfaces::MintableClient;
7
7
  use oft_core::OFTCore;
8
- use soroban_sdk::{Address, Env};
8
+ use soroban_sdk::{token::TokenClient, Address, Env};
9
9
 
10
10
  /// Debit tokens using MintBurn OFT type (burns tokens from sender).
11
11
  ///
12
12
  /// # Parameters
13
13
  /// * `env` - The Soroban environment
14
- /// * `burnable` - Address of the contract responsible for burning tokens
14
+ /// * `token` - Address of the token (SAC) to burn from
15
15
  /// * `sender` - Address of the token sender
16
16
  /// * `amount_ld` - Amount to debit in local decimals
17
17
  /// * `min_amount_ld` - Minimum amount that must be received (for slippage protection)
@@ -22,14 +22,14 @@ use soroban_sdk::{Address, Env};
22
22
  /// * `amount_received_ld` - The amount received in local decimals on the remote
23
23
  pub fn debit<T: OFTCore>(
24
24
  env: &Env,
25
- burnable: &Address,
25
+ token: &Address,
26
26
  sender: &Address,
27
27
  amount_ld: i128,
28
28
  min_amount_ld: i128,
29
29
  dst_eid: u32,
30
30
  ) -> (i128, i128) {
31
31
  let (amount_sent_ld, amount_received_ld) = T::__debit_view(env, amount_ld, min_amount_ld, dst_eid);
32
- MintBurnableClient::new(env, burnable).burn(sender, &amount_received_ld);
32
+ TokenClient::new(env, token).burn(sender, &amount_received_ld);
33
33
  (amount_sent_ld, amount_received_ld)
34
34
  }
35
35
 
@@ -37,7 +37,7 @@ pub fn debit<T: OFTCore>(
37
37
  ///
38
38
  /// # Parameters
39
39
  /// * `env` - The Soroban environment
40
- /// * `mintable` - Address of the contract responsible for minting tokens
40
+ /// * `mintable` - Address of the contract responsible for minting tokens (e.g. SAC wrapper)
41
41
  /// * `to` - Address of the token recipient
42
42
  /// * `amount_ld` - Amount to credit in local decimals
43
43
  /// * `_src_eid` - Source endpoint ID (unused)
@@ -45,6 +45,6 @@ pub fn debit<T: OFTCore>(
45
45
  /// # Returns
46
46
  /// The amount credited
47
47
  pub fn credit<T: OFTCore>(env: &Env, mintable: &Address, to: &Address, amount_ld: i128, _src_eid: u32) -> i128 {
48
- MintBurnableClient::new(env, mintable).mint(to, &amount_ld);
48
+ MintableClient::new(env, mintable).mint(to, &amount_ld, &env.current_contract_address());
49
49
  amount_ld
50
50
  }
@@ -4,9 +4,8 @@
4
4
  //!
5
5
  //! - **LockUnlock**: Locks tokens on send, unlocks on receive. Operates directly on the
6
6
  //! token via standard SEP-41 `transfer`.
7
- //! - **MintBurn**: Burns tokens on send, mints on receive. Operates on any contract
8
- //! that implements [`MintBurnable`](crate::interfaces::MintBurnable)
9
- //! whether it's a raw SAC or a token wrapper (e.g. SAC Manager).
7
+ //! - **MintBurn**: Burns tokens on send (via TokenClient on the token), mints on receive
8
+ //! via a contract that implements [`Mintable`](crate::interfaces::Mintable).
10
9
 
11
10
  use soroban_sdk::{contracttype, Address};
12
11
 
@@ -20,6 +19,6 @@ pub enum OftType {
20
19
  /// Lock tokens on send, unlock on receive.
21
20
  LockUnlock,
22
21
  /// Burn tokens on send, mint on receive.
23
- /// The address is the contract responsible for minting and burning tokens.
22
+ /// The address is the Mintable contract used for minting on credit
24
23
  MintBurn(Address),
25
24
  }
@@ -1,7 +1,9 @@
1
1
  extern crate std;
2
2
 
3
3
  use crate as oft;
4
- use crate::extensions::rate_limiter::{Direction, Mode, RateLimitConfig, RateLimitError, RateLimiter, RateLimiterInternal};
4
+ use crate::extensions::rate_limiter::{
5
+ Direction, Mode, RateLimitConfig, RateLimitError, RateLimiter, RateLimiterInternal,
6
+ };
5
7
  use soroban_sdk::{contract, contractimpl, testutils::Ledger as _, Address, Env};
6
8
  use utils::auth::Auth;
7
9
 
@@ -451,10 +453,10 @@ fn test_switching_from_net_to_gross_mode() {
451
453
 
452
454
  // Switch to Gross mode
453
455
  client.set_rate_limit(&direction, &eid, &Some(config_with_mode(100, 100, Mode::Gross)));
454
-
456
+
455
457
  // in-flight should be checkpointed
456
458
  assert_eq!(client.rate_limit_in_flight(&direction, &eid), 40);
457
-
459
+
458
460
  // Now releases should be no-op
459
461
  client.release(&direction, &eid, &20i128);
460
462
  assert_eq!(client.rate_limit_in_flight(&direction, &eid), 40, "Gross mode should not release");
@@ -474,10 +476,10 @@ fn test_switching_from_gross_to_net_mode() {
474
476
 
475
477
  // Switch to Net mode
476
478
  client.set_rate_limit(&direction, &eid, &Some(config_with_mode(100, 100, Mode::Net)));
477
-
479
+
478
480
  // in-flight should be checkpointed
479
481
  assert_eq!(client.rate_limit_in_flight(&direction, &eid), 60);
480
-
482
+
481
483
  // Now releases should work
482
484
  client.release(&direction, &eid, &20i128);
483
485
  assert_eq!(client.rate_limit_in_flight(&direction, &eid), 40, "Net mode should release");
@@ -0,0 +1,14 @@
1
+ //! General errors for the SAC Manager contract.
2
+
3
+ use common_macros::contract_error;
4
+
5
+ /// General SacManagerError: 3220-3249
6
+ #[contract_error]
7
+ pub enum SacManagerError {
8
+ /// The address is already a minter
9
+ AlreadyMinter,
10
+ /// Minter address not in the minters list (e.g. when deactivating)
11
+ MinterNotFound,
12
+ /// Caller is not authorized for this operation
13
+ Unauthorized,
14
+ }
@@ -1,12 +1,8 @@
1
1
  #![no_std]
2
2
 
3
- pub mod extensions;
4
-
5
3
  mod errors;
6
- mod interfaces;
7
4
 
8
5
  pub use errors::*;
9
- pub use interfaces::*;
10
6
 
11
7
  cfg_if::cfg_if! {
12
8
  // Include implementation when NOT in library mode, OR when testutils is enabled (for tests)
@@ -0,0 +1,115 @@
1
+ //! SAC Manager Contract - Manages a Stellar Asset Contract (SAC) as its admin.
2
+ //!
3
+ //! This contract becomes the admin of a SAC and provides:
4
+ //! - Mintable interface for OFT (mint on credit only; OFT burns on the token directly)
5
+ //! - Admin operations (clawback, set_admin, set_authorized)
6
+ //! - Upgradeable (owner-authorized)
7
+ //!
8
+ //! ## Authorization Model
9
+ //!
10
+ //! - **Owner** (via Ownable): The admin address. Can do everything (upgrades, TTL, admin ops, set_minter).
11
+ //! - **Minters**: Stored list of addresses. Only an address in Minters can call mint via Mintable with `operation: minter_address`.
12
+
13
+ use crate::{errors::SacManagerError, storage::SacManagerStorage};
14
+ use common_macros::{contract_impl, lz_contract, only_auth};
15
+ use oft::Mintable;
16
+ use soroban_sdk::{assert_with_error, token::StellarAssetClient, Address, Env, Vec};
17
+ use utils::ownable::OwnableInitializer;
18
+
19
+ // =========================================================================
20
+ // SAC Manager Contract
21
+ // =========================================================================
22
+
23
+ /// SAC Manager Contract
24
+ ///
25
+ /// Manages a SAC as its admin, forwarding token actions to
26
+ /// the underlying SAC while enforcing access control.
27
+ #[lz_contract(upgradeable(no_migration))]
28
+ pub struct SacManager;
29
+
30
+ #[contract_impl]
31
+ impl SacManager {
32
+ /// Constructs the SAC manager contract.
33
+ ///
34
+ /// # Arguments
35
+ /// * `sac_token` - The underlying Stellar Asset Contract address
36
+ /// * `owner` - The initial owner address (for upgrades, owner ops)
37
+ ///
38
+ /// Minters can be set separately via `set_minter` after construction.
39
+ pub fn __constructor(env: &Env, sac_token: &Address, owner: &Address) {
40
+ Self::init_owner(env, owner);
41
+ SacManagerStorage::set_sac_token(env, sac_token);
42
+ }
43
+
44
+ // =========================================================================
45
+ // SAC Admin Operations
46
+ // =========================================================================
47
+
48
+ /// Delegates to the underlying SAC to clawback `amount` from `from` account.
49
+ #[only_auth]
50
+ pub fn clawback(env: &Env, from: &Address, amount: i128) {
51
+ sac_client(env).clawback(from, &amount);
52
+ }
53
+
54
+ /// Delegates to the underlying SAC `set_admin` to release the admin role from the current contract to `to`.
55
+ #[only_auth]
56
+ pub fn release_sac_admin(env: &Env, to: &Address) {
57
+ sac_client(env).set_admin(to);
58
+ }
59
+
60
+ /// Delegates to the underlying SAC `set_authorized` to set whether `id` is authorized to use its balance.
61
+ #[only_auth]
62
+ pub fn set_authorized(env: &Env, id: &Address, authorize: bool) {
63
+ sac_client(env).set_authorized(id, &authorize);
64
+ }
65
+
66
+ /// Adds (`active = true`) or removes (`active = false`) a minter.
67
+ #[only_auth]
68
+ pub fn set_minter(env: &Env, minter: &Address, active: bool) {
69
+ let mut minters = SacManagerStorage::minters(env);
70
+ let minter_index = minters.first_index_of(minter);
71
+ if active {
72
+ assert_with_error!(env, minter_index.is_none(), SacManagerError::AlreadyMinter);
73
+ minters.push_back(minter.clone());
74
+ } else {
75
+ minters.remove(minter_index.unwrap_or_panic(env, SacManagerError::MinterNotFound));
76
+ }
77
+ SacManagerStorage::set_minters(env, &minters);
78
+ }
79
+
80
+ // =========================================================================
81
+ // View Functions
82
+ // =========================================================================
83
+
84
+ /// Returns the underlying SAC (Stellar Asset Contract) address.
85
+ pub fn underlying_sac(env: &Env) -> Address {
86
+ SacManagerStorage::sac_token(env).unwrap()
87
+ }
88
+
89
+ /// Returns the list of addresses authorized to call mint.
90
+ pub fn minters(env: &Env) -> Vec<Address> {
91
+ SacManagerStorage::minters(env)
92
+ }
93
+ }
94
+
95
+ // =========================================================================
96
+ // Mintable Implementation (OFT mint on credit)
97
+ // =========================================================================
98
+
99
+ #[contract_impl]
100
+ impl Mintable for SacManager {
101
+ fn mint(env: &Env, to: &Address, amount: i128, operation: &Address) {
102
+ assert_with_error!(env, Self::minters(env).contains(operation), SacManagerError::Unauthorized);
103
+ operation.require_auth();
104
+
105
+ sac_client(env).mint(to, &amount);
106
+ }
107
+ }
108
+
109
+ // =========================================================================
110
+ // Helper Functions
111
+ // =========================================================================
112
+
113
+ fn sac_client(env: &Env) -> StellarAssetClient<'_> {
114
+ StellarAssetClient::new(env, &SacManager::underlying_sac(env))
115
+ }
@@ -0,0 +1,20 @@
1
+ //! Storage definitions for the SAC manager contract.
2
+ //!
3
+ //! This file contains the core storage.
4
+
5
+ use common_macros::storage;
6
+ use soroban_sdk::{Address, Vec};
7
+
8
+ /// Core storage for the SacManager contract.
9
+ /// Contains essential state.
10
+ #[storage]
11
+ pub enum SacManagerStorage {
12
+ /// The underlying SAC (Stellar Asset Contract) address
13
+ #[instance(Address)]
14
+ SacToken,
15
+
16
+ /// Addresses authorized to call mint (e.g. OFT or other minters).
17
+ #[persistent(Vec<Address>)]
18
+ #[default(Vec::new(env))]
19
+ Minters,
20
+ }
@@ -1,14 +1,10 @@
1
1
  //! Token Wrapper Tests
2
2
  //!
3
3
  //! Test modules organized by source module:
4
- //! - `supply_control`: Tests for supply control extension (controllers, mint, burn, rate limits)
5
- //! - `redistribution`: Tests for redistribution extension (feature flag settings)
6
4
  //! - `sac_manager`: Tests for the main SAC manager contract (admin, authorized, clawback, mint/burn, views)
7
5
 
8
6
  #![cfg(test)]
9
7
 
10
8
  mod test_helper;
11
9
 
12
- mod redistribution;
13
10
  mod sac_manager;
14
- mod supply_control;
@@ -0,0 +1,86 @@
1
+ //! Clawback Integration Tests
2
+ //!
3
+ //! Tests for admin-initiated clawback functionality via `clawback`.
4
+ //! The clawback operation uses SAC clawback, which requires AUTH_CLAWBACK_ENABLED
5
+ //! flag on the SAC issuer.
6
+
7
+ use super::test_helper::mock_clawback_auth;
8
+ use crate::tests::test_helper::{mock_oft_mint_auth, TestSetup};
9
+ use soroban_sdk::testutils::IssuerFlags;
10
+
11
+ // =========================================================================
12
+ // clawback Tests (owner only)
13
+ // =========================================================================
14
+
15
+ #[test]
16
+ fn test_clawback_by_owner() {
17
+ let setup = TestSetup::new().with_manager_as_sac_admin().build();
18
+ let user = setup.generate_address();
19
+
20
+ setup.sac_contract.issuer().set_flag(IssuerFlags::RevocableFlag);
21
+ setup.sac_contract.issuer().set_flag(IssuerFlags::ClawbackEnabledFlag);
22
+
23
+ mock_oft_mint_auth(&setup, &user, 1000_i128);
24
+ setup.sac_manager_client.mint(&user, &1000, &setup.minter);
25
+ assert_eq!(setup.sac_client.balance(&user), 1000);
26
+
27
+ mock_clawback_auth(&setup, &setup.owner, &user, 500_i128);
28
+ setup.sac_manager_client.clawback(&user, &500);
29
+ assert_eq!(setup.sac_client.balance(&user), 500);
30
+ }
31
+
32
+ #[test]
33
+ fn test_clawback_full_balance() {
34
+ let setup = TestSetup::new().with_manager_as_sac_admin().build();
35
+ let user = setup.generate_address();
36
+
37
+ setup.sac_contract.issuer().set_flag(IssuerFlags::RevocableFlag);
38
+ setup.sac_contract.issuer().set_flag(IssuerFlags::ClawbackEnabledFlag);
39
+
40
+ mock_oft_mint_auth(&setup, &user, 1000_i128);
41
+ setup.sac_manager_client.mint(&user, &1000, &setup.minter);
42
+
43
+ mock_clawback_auth(&setup, &setup.owner, &user, 1000_i128);
44
+ setup.sac_manager_client.clawback(&user, &1000);
45
+ assert_eq!(setup.sac_client.balance(&user), 0);
46
+ }
47
+
48
+ #[test]
49
+ fn test_clawback_fails_when_amount_exceeds_balance() {
50
+ let setup = TestSetup::new().with_manager_as_sac_admin().build();
51
+ let user = setup.generate_address();
52
+
53
+ setup.sac_contract.issuer().set_flag(IssuerFlags::RevocableFlag);
54
+ setup.sac_contract.issuer().set_flag(IssuerFlags::ClawbackEnabledFlag);
55
+
56
+ mock_oft_mint_auth(&setup, &user, 100_i128);
57
+ setup.sac_manager_client.mint(&user, &100, &setup.minter);
58
+
59
+ mock_clawback_auth(&setup, &setup.owner, &user, 200_i128);
60
+ let result = setup.sac_manager_client.try_clawback(&user, &200);
61
+ assert!(result.is_err());
62
+ }
63
+
64
+ #[test]
65
+ fn test_clawback_by_non_owner_fails() {
66
+ let setup = TestSetup::new().with_manager_as_sac_admin().build();
67
+ let user = setup.generate_address();
68
+ let random = setup.generate_address();
69
+
70
+ mock_clawback_auth(&setup, &random, &user, 500_i128);
71
+ let result = setup.sac_manager_client.try_clawback(&user, &500);
72
+ assert!(result.is_err());
73
+ }
74
+
75
+ // =========================================================================
76
+ // clawback Auth Tests
77
+ // =========================================================================
78
+
79
+ #[test]
80
+ #[should_panic(expected = "Error(Auth, InvalidAction)")]
81
+ fn test_clawback_fails_without_auth() {
82
+ let setup = TestSetup::new().build();
83
+ let user = setup.generate_address();
84
+
85
+ setup.sac_manager_client.clawback(&user, &300);
86
+ }
@@ -0,0 +1,58 @@
1
+ //! Mint (Mintable) Integration Tests
2
+ //!
3
+ //! Tests minter-originated mint via Mintable::mint(..., operation: minter),
4
+ //! authorization (operation must be in minters, operation must auth), and error cases.
5
+
6
+ use crate::errors::SacManagerError;
7
+ use crate::tests::test_helper::{mock_auth, mock_oft_mint_auth, TestSetup};
8
+
9
+ // =========================================================================
10
+ // Mint success — minter calls mint
11
+ // =========================================================================
12
+
13
+ #[test]
14
+ fn test_mint_by_minter() {
15
+ let setup = TestSetup::new().with_manager_as_sac_admin().build();
16
+ let recipient = setup.generate_address();
17
+
18
+ mock_oft_mint_auth(&setup, &recipient, 1000_i128);
19
+ setup.sac_manager_client.mint(&recipient, &1000, &setup.minter);
20
+ assert_eq!(setup.sac_client.balance(&recipient), 1000);
21
+ }
22
+
23
+ // =========================================================================
24
+ // Mint error cases — operation not in minters / non-minter
25
+ // =========================================================================
26
+
27
+ #[test]
28
+ fn test_mint_fails_when_operation_not_in_minters() {
29
+ let setup = TestSetup::new().with_empty_minters().build();
30
+ let recipient = setup.generate_address();
31
+
32
+ let result = setup.sac_manager_client.try_mint(&recipient, &1000, &setup.minter);
33
+ assert_eq!(result.err().unwrap().unwrap(), SacManagerError::Unauthorized.into());
34
+ }
35
+
36
+ #[test]
37
+ fn test_mint_by_non_minter_fails() {
38
+ let setup = TestSetup::new().with_manager_as_sac_admin().build();
39
+ let recipient = setup.generate_address();
40
+ let random = setup.generate_address();
41
+
42
+ mock_auth(&setup.env, &setup.sac_manager, &random, "mint", (&recipient, 1000_i128, &random));
43
+ let result = setup.sac_manager_client.try_mint(&recipient, &1000, &random);
44
+ assert_eq!(result.err().unwrap().unwrap(), SacManagerError::Unauthorized.into());
45
+ }
46
+
47
+ // =========================================================================
48
+ // Mint auth tests — operation must authorize
49
+ // =========================================================================
50
+
51
+ #[test]
52
+ #[should_panic(expected = "Error(Auth, InvalidAction)")]
53
+ fn test_mint_fails_without_minter_auth() {
54
+ let setup = TestSetup::new().with_manager_as_sac_admin().build();
55
+ let recipient = setup.generate_address();
56
+
57
+ setup.sac_manager_client.mint(&recipient, &1000, &setup.minter);
58
+ }
@@ -1,9 +1,7 @@
1
1
  mod clawback;
2
- mod admin_mint;
3
- mod burn;
4
2
  mod mint;
5
3
  mod set_admin;
6
4
  mod set_authorized;
7
- mod set_oft_address;
5
+ mod set_minter;
8
6
  mod test_helper;
9
7
  mod view_functions;
@@ -0,0 +1,69 @@
1
+ //! set_minter Integration Tests
2
+
3
+ use super::test_helper::mock_set_minter_auth;
4
+ use crate::errors::SacManagerError;
5
+ use crate::tests::test_helper::TestSetup;
6
+
7
+ // =========================================================================
8
+ // set_minter Tests
9
+ // =========================================================================
10
+
11
+ #[test]
12
+ fn test_set_minter_add_when_empty() {
13
+ let setup = TestSetup::new().with_empty_minters().build();
14
+ let new_minter = setup.generate_address();
15
+
16
+ mock_set_minter_auth(&setup, &new_minter, true);
17
+ setup.sac_manager_client.set_minter(&new_minter, &true);
18
+ let stored = setup.sac_manager_client.minters();
19
+ assert_eq!(stored.len(), 1);
20
+ assert_eq!(stored.get(0), Some(new_minter));
21
+ }
22
+
23
+ #[test]
24
+ fn test_set_minter_add_then_remove() {
25
+ let setup = TestSetup::new().build();
26
+ let new_minter = setup.generate_address();
27
+
28
+ mock_set_minter_auth(&setup, &new_minter, true);
29
+ setup.sac_manager_client.set_minter(&new_minter, &true);
30
+ assert_eq!(setup.sac_manager_client.minters().len(), 2);
31
+
32
+ mock_set_minter_auth(&setup, &new_minter, false);
33
+ setup.sac_manager_client.set_minter(&new_minter, &false);
34
+ let stored = setup.sac_manager_client.minters();
35
+ assert_eq!(stored.len(), 1);
36
+ assert_eq!(stored.get(0), Some(setup.minter.clone()));
37
+ }
38
+
39
+ #[test]
40
+ fn test_set_minter_add_fails_if_already_active() {
41
+ let setup = TestSetup::new().build();
42
+
43
+ mock_set_minter_auth(&setup, &setup.minter, true);
44
+ let result = setup.sac_manager_client.try_set_minter(&setup.minter, &true);
45
+ assert_eq!(result.err().unwrap().unwrap(), SacManagerError::AlreadyMinter.into());
46
+ }
47
+
48
+ #[test]
49
+ fn test_set_minter_remove_fails_if_not_minter() {
50
+ let setup = TestSetup::new().build();
51
+ let random = setup.generate_address();
52
+
53
+ mock_set_minter_auth(&setup, &random, false);
54
+ let result = setup.sac_manager_client.try_set_minter(&random, &false);
55
+ assert_eq!(result.err().unwrap().unwrap(), SacManagerError::MinterNotFound.into());
56
+ }
57
+
58
+ // =========================================================================
59
+ // set_minter Auth Tests
60
+ // =========================================================================
61
+
62
+ #[test]
63
+ #[should_panic(expected = "Error(Auth, InvalidAction)")]
64
+ fn test_set_minter_without_auth() {
65
+ let setup = TestSetup::new().with_empty_minters().build();
66
+ let new_minter = setup.generate_address();
67
+
68
+ setup.sac_manager_client.set_minter(&new_minter, &true);
69
+ }