@layerzerolabs/protocol-stellar-v2 0.2.34 → 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 (68) hide show
  1. package/.turbo/turbo-build.log +250 -251
  2. package/.turbo/turbo-lint.log +226 -231
  3. package/.turbo/turbo-test.log +1994 -1731
  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/src/extensions/oft_fee.rs +5 -0
  31. package/contracts/oapps/oft/src/oft.rs +5 -4
  32. package/docs/layerzero-v2-on-stellar.md +46 -2
  33. package/package.json +3 -3
  34. package/sdk/.turbo/turbo-test.log +312 -316
  35. package/sdk/dist/generated/bml.d.ts +3 -3
  36. package/sdk/dist/generated/bml.js +3 -3
  37. package/sdk/dist/generated/counter.d.ts +32 -3
  38. package/sdk/dist/generated/counter.js +6 -3
  39. package/sdk/dist/generated/dvn.d.ts +3 -3
  40. package/sdk/dist/generated/dvn.js +3 -3
  41. package/sdk/dist/generated/dvn_fee_lib.d.ts +2 -2
  42. package/sdk/dist/generated/dvn_fee_lib.js +2 -2
  43. package/sdk/dist/generated/endpoint.d.ts +12 -13
  44. package/sdk/dist/generated/endpoint.js +7 -7
  45. package/sdk/dist/generated/executor.d.ts +3 -3
  46. package/sdk/dist/generated/executor.js +3 -3
  47. package/sdk/dist/generated/executor_fee_lib.d.ts +2 -2
  48. package/sdk/dist/generated/executor_fee_lib.js +2 -2
  49. package/sdk/dist/generated/executor_helper.d.ts +2 -2
  50. package/sdk/dist/generated/executor_helper.js +2 -2
  51. package/sdk/dist/generated/layerzero_view.d.ts +3 -3
  52. package/sdk/dist/generated/layerzero_view.js +3 -3
  53. package/sdk/dist/generated/oft.d.ts +32 -3
  54. package/sdk/dist/generated/oft.js +6 -3
  55. package/sdk/dist/generated/price_feed.d.ts +3 -3
  56. package/sdk/dist/generated/price_feed.js +3 -3
  57. package/sdk/dist/generated/sac_manager.d.ts +24 -3
  58. package/sdk/dist/generated/sac_manager.js +4 -3
  59. package/sdk/dist/generated/sml.d.ts +2 -2
  60. package/sdk/dist/generated/sml.js +2 -2
  61. package/sdk/dist/generated/treasury.d.ts +2 -2
  62. package/sdk/dist/generated/treasury.js +2 -2
  63. package/sdk/dist/generated/uln302.d.ts +3 -3
  64. package/sdk/dist/generated/uln302.js +3 -3
  65. package/sdk/dist/generated/upgrader.d.ts +2 -2
  66. package/sdk/dist/generated/upgrader.js +2 -2
  67. package/sdk/package.json +1 -1
  68. package/contracts/endpoint-v2/src/tests/messaging_channel/lazy_inbound_nonce.rs +0 -39
@@ -53,7 +53,7 @@ fn test_skip_requires_auth_even_when_caller_is_receiver() {
53
53
  endpoint_client.skip(&receiver, &receiver, &src_eid, &sender, &nonce);
54
54
  }
55
55
 
56
- // Successful skip updates inbound/lazy nonces and emits InboundNonceSkipped
56
+ // Successful skip updates inbound nonce and emits InboundNonceSkipped
57
57
  #[test]
58
58
  fn test_skip_success() {
59
59
  let context = setup();
@@ -65,13 +65,10 @@ fn test_skip_success() {
65
65
  let sender = BytesN::from_array(env, &[1u8; 32]);
66
66
  let nonce = 1;
67
67
 
68
- // Verify initial state: lazy inbound nonce should be 0.
69
- let initial_lazy_nonce = endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender);
70
- assert_eq!(initial_lazy_nonce, 0, "Initial lazy inbound nonce should be 0");
71
-
72
68
  // Initially, inbound nonce should be 0.
73
69
  let initial_nonce = endpoint_client.inbound_nonce(&receiver, &src_eid, &sender);
74
70
  assert_eq!(initial_nonce, 0);
71
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
75
72
 
76
73
  // Skip nonce 1 (expected nonce is initial_nonce + 1 = 1).
77
74
  skip_with_auth(&context, &receiver, &receiver, src_eid, &sender, nonce);
@@ -86,10 +83,7 @@ fn test_skip_success() {
86
83
  // Verify inbound nonce reflects the skip via public interface.
87
84
  let updated_nonce = endpoint_client.inbound_nonce(&receiver, &src_eid, &sender);
88
85
  assert_eq!(updated_nonce, nonce);
89
-
90
- // Verify lazy inbound nonce was updated.
91
- let lazy_nonce = endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender);
92
- assert_eq!(lazy_nonce, nonce);
86
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
93
87
  }
94
88
 
95
89
  // Multiple sequential skips update to the latest nonce
@@ -111,13 +105,10 @@ fn test_skip_multiple_nonces() {
111
105
  let nonce2 = 2;
112
106
  skip_with_auth(&context, &receiver, &receiver, src_eid, &sender, nonce2);
113
107
 
114
- // Verify lazy inbound nonce was updated to nonce2.
115
- let lazy_nonce = endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender);
116
- assert_eq!(lazy_nonce, nonce2);
117
-
118
108
  // Verify inbound nonce reflects the latest skip.
119
109
  let updated_nonce = endpoint_client.inbound_nonce(&receiver, &src_eid, &sender);
120
110
  assert_eq!(updated_nonce, nonce2);
111
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
121
112
  }
122
113
 
123
114
  // Delegate authorization (delegate(receiver) is allowed)
@@ -146,9 +137,8 @@ fn test_skip_with_delegate() {
146
137
  InboundNonceSkipped { src_eid, sender: sender.clone(), receiver: receiver.clone(), nonce },
147
138
  );
148
139
 
149
- // Verify lazy inbound nonce was updated.
150
- let lazy_nonce = endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender);
151
- assert_eq!(lazy_nonce, nonce);
140
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), nonce);
141
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
152
142
  }
153
143
 
154
144
  // Path isolation (receiver/src_eid/sender are isolated)
@@ -176,11 +166,11 @@ fn test_skip_different_paths() {
176
166
  // Skip for different senders.
177
167
  skip_with_auth(&context, &receiver1, &receiver1, src_eid1, &sender2, nonce);
178
168
 
179
- // Verify all paths have independent lazy nonces.
180
- assert_eq!(endpoint_client.lazy_inbound_nonce(&receiver1, &src_eid1, &sender1), nonce);
181
- assert_eq!(endpoint_client.lazy_inbound_nonce(&receiver2, &src_eid1, &sender1), nonce);
182
- assert_eq!(endpoint_client.lazy_inbound_nonce(&receiver1, &src_eid2, &sender1), nonce);
183
- assert_eq!(endpoint_client.lazy_inbound_nonce(&receiver1, &src_eid1, &sender2), nonce);
169
+ // Verify all paths have independent inbound nonces.
170
+ assert_eq!(endpoint_client.inbound_nonce(&receiver1, &src_eid1, &sender1), nonce);
171
+ assert_eq!(endpoint_client.inbound_nonce(&receiver2, &src_eid1, &sender1), nonce);
172
+ assert_eq!(endpoint_client.inbound_nonce(&receiver1, &src_eid2, &sender1), nonce);
173
+ assert_eq!(endpoint_client.inbound_nonce(&receiver1, &src_eid1, &sender2), nonce);
184
174
  }
185
175
 
186
176
  // Invalid nonce rejection (must match expected nonce)
@@ -224,10 +214,10 @@ fn test_skip_next_nonce_accounts_for_verified_payload_hashes() {
224
214
  let result = endpoint_client.try_skip(&receiver, &receiver, &src_eid, &sender, &1);
225
215
  assert_eq!(result.err().unwrap().ok().unwrap(), EndpointError::InvalidNonce.into());
226
216
 
227
- // Skipping 2 should succeed and advance lazy inbound nonce to 2.
217
+ // Skipping 2 should succeed and advance inbound nonce to 2.
228
218
  skip_with_auth(&context, &receiver, &receiver, src_eid, &sender, 2);
229
- assert_eq!(endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender), 2);
230
219
  assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
220
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
231
221
 
232
222
  // skip() does not clear any existing payload hashes.
233
223
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &1), Some(payload_hash_1));
@@ -251,8 +241,8 @@ fn test_skip_closes_gap_and_advances_inbound_nonce() {
251
241
 
252
242
  // Skip nonce 1 to close the gap. This should allow inbound_nonce to advance to 2.
253
243
  skip_with_auth(&context, &receiver, &receiver, src_eid, &sender, 1);
254
- assert_eq!(endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender), 1);
255
244
  assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
245
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
256
246
 
257
247
  // Payload hash at nonce 2 remains intact.
258
248
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2), Some(payload_hash_2));
@@ -271,7 +261,7 @@ fn test_skip_rejects_repeated_same_nonce() {
271
261
 
272
262
  // Skip nonce 1 successfully.
273
263
  skip_with_auth(&context, &receiver, &receiver, src_eid, &sender, 1);
274
- assert_eq!(endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender), 1);
264
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
275
265
 
276
266
  // Skipping nonce 1 again should fail since the next expected nonce is now 2.
277
267
  context.mock_auth(&receiver, "skip", (&receiver, &receiver, &src_eid, &sender, &1u64));
@@ -104,9 +104,9 @@ impl LayerZeroView {
104
104
  let empty_hash = empty_payload_hash(env);
105
105
  let nil_hash = nil_payload_hash(env);
106
106
 
107
- // Executed: payload hash has been cleared (None) and nonce <= lazy_inbound_nonce
107
+ // Executed: payload hash has been cleared (None) and nonce <= inbound_nonce
108
108
  if payload_hash.is_none()
109
- && origin.nonce <= messaging_channel.lazy_inbound_nonce(receiver, &origin.src_eid, &origin.sender)
109
+ && origin.nonce <= messaging_channel.inbound_nonce(receiver, &origin.src_eid, &origin.sender)
110
110
  {
111
111
  return ExecutionState::Executed;
112
112
  }
@@ -206,9 +206,9 @@ fn test_executable_state_executed() {
206
206
  let receiver = test_setup.register_oapp();
207
207
  let sender = soroban_sdk::Address::generate(&test_setup.env);
208
208
 
209
- // Clear payload hash (None) and set lazy_inbound_nonce >= nonce = Executed
209
+ // Clear payload hash (None) and set inbound_nonce >= nonce = Executed
210
210
  test_setup.set_payload_hash(&receiver, REMOTE_EID, &sender, 1, &None);
211
- test_setup.set_lazy_inbound_nonce(&receiver, REMOTE_EID, &sender, 1);
211
+ test_setup.set_inbound_nonce(&receiver, REMOTE_EID, &sender, 1);
212
212
 
213
213
  let origin = Origin { src_eid: REMOTE_EID, sender: address_to_bytes32(&sender), nonce: 1 };
214
214
 
@@ -262,9 +262,8 @@ fn test_executable_multiple_nonces_in_sequence() {
262
262
  assert_eq!(test_setup.view_client.executable(&origin_2, &receiver), ExecutionState::VerifiedButNotExecutable);
263
263
  assert_eq!(test_setup.view_client.executable(&origin_3, &receiver), ExecutionState::VerifiedButNotExecutable);
264
264
 
265
- // Now execute nonce 1 (set lazy_inbound_nonce)
265
+ // Now execute nonce 1 (clear payload hash, advance inbound_nonce)
266
266
  test_setup.set_payload_hash(&receiver, REMOTE_EID, &sender, 1, &None);
267
- test_setup.set_lazy_inbound_nonce(&receiver, REMOTE_EID, &sender, 1);
268
267
  test_setup.set_inbound_nonce(&receiver, REMOTE_EID, &sender, 2);
269
268
 
270
269
  assert_eq!(test_setup.view_client.executable(&origin_1, &receiver), ExecutionState::Executed);
@@ -30,7 +30,6 @@ mod endpoint_storage {
30
30
  Initializable(Address, u32, BytesN<32>),
31
31
  Verifiable(Address, u32, BytesN<32>),
32
32
  // State for executable tests
33
- LazyInboundNonce(Address, u32, BytesN<32>),
34
33
  InboundNonce(Address, u32, BytesN<32>),
35
34
  InboundPayloadHash(Address, u32, BytesN<32>, u64),
36
35
  ReceiveLibrary(Address, u32),
@@ -91,13 +90,6 @@ impl MockEndpoint {
91
90
  .set(&endpoint_storage::MockEndpointStorage::Verifiable(receiver.clone(), *src_eid, sender.clone()), value);
92
91
  }
93
92
 
94
- pub fn set_lazy_inbound_nonce(env: &Env, receiver: &Address, src_eid: &u32, sender: &BytesN<32>, nonce: &u64) {
95
- env.storage().persistent().set(
96
- &endpoint_storage::MockEndpointStorage::LazyInboundNonce(receiver.clone(), *src_eid, sender.clone()),
97
- nonce,
98
- );
99
- }
100
-
101
93
  pub fn set_inbound_nonce(env: &Env, receiver: &Address, src_eid: &u32, sender: &BytesN<32>, nonce: &u64) {
102
94
  env.storage().persistent().set(
103
95
  &endpoint_storage::MockEndpointStorage::InboundNonce(receiver.clone(), *src_eid, sender.clone()),
@@ -135,13 +127,6 @@ impl MockEndpoint {
135
127
  // Getters required by LayerZeroView
136
128
  // =========================================================================
137
129
 
138
- pub fn lazy_inbound_nonce(env: &Env, receiver: &Address, src_eid: &u32, sender: &BytesN<32>) -> u64 {
139
- env.storage()
140
- .persistent()
141
- .get(&endpoint_storage::MockEndpointStorage::LazyInboundNonce(receiver.clone(), *src_eid, sender.clone()))
142
- .unwrap_or(0)
143
- }
144
-
145
130
  pub fn inbound_nonce(env: &Env, receiver: &Address, src_eid: &u32, sender: &BytesN<32>) -> u64 {
146
131
  env.storage()
147
132
  .persistent()
@@ -282,12 +267,6 @@ impl<'a> TestSetup<'a> {
282
267
  self.endpoint_client.set_verifiable(receiver, &src_eid, &sender_bytes32, &value);
283
268
  }
284
269
 
285
- /// Set lazy inbound nonce (marks messages up to this nonce as processed).
286
- pub fn set_lazy_inbound_nonce(&self, receiver: &Address, src_eid: u32, sender: &Address, nonce: u64) {
287
- let sender_bytes32 = address_to_bytes32(sender);
288
- self.endpoint_client.set_lazy_inbound_nonce(receiver, &src_eid, &sender_bytes32, &nonce);
289
- }
290
-
291
270
  /// Set inbound nonce (marks messages up to this nonce as verified and executable).
292
271
  pub fn set_inbound_nonce(&self, receiver: &Address, src_eid: u32, sender: &Address, nonce: u64) {
293
272
  let sender_bytes32 = address_to_bytes32(sender);
@@ -15,12 +15,12 @@
15
15
  #[cfg(test)]
16
16
  mod tests;
17
17
 
18
- use common_macros::{contract_error, contract_impl};
18
+ use common_macros::contract_error;
19
19
  use endpoint_v2::{
20
20
  FeesAndPacket, IMessageLib, ISendLib, MessageLibType, MessageLibVersion, MessagingFee, OutboundPacket,
21
21
  SetConfigParam,
22
22
  };
23
- use soroban_sdk::{contract, panic_with_error, Address, Bytes, Env, Vec};
23
+ use soroban_sdk::{contract, contractimpl, panic_with_error, Address, Bytes, Env, Vec};
24
24
 
25
25
  #[contract_error]
26
26
  pub enum BlockedMessageLibError {
@@ -31,7 +31,7 @@ pub enum BlockedMessageLibError {
31
31
  #[contract]
32
32
  pub struct BlockedMessageLib;
33
33
 
34
- #[contract_impl]
34
+ #[contractimpl]
35
35
  impl IMessageLib for BlockedMessageLib {
36
36
  /// Always panics - config modification is not supported.
37
37
  fn set_config(env: &Env, _oapp: &Address, _param: &Vec<SetConfigParam>) {
@@ -59,7 +59,7 @@ impl IMessageLib for BlockedMessageLib {
59
59
  }
60
60
  }
61
61
 
62
- #[contract_impl]
62
+ #[contractimpl]
63
63
  impl ISendLib for BlockedMessageLib {
64
64
  /// Always panics - quoting is blocked.
65
65
  fn quote(env: &Env, _packet: &OutboundPacket, _options: &Bytes, _pay_in_zro: bool) -> MessagingFee {
@@ -45,7 +45,7 @@ impl ISendLib for Uln302 {
45
45
 
46
46
  // Treasury fee
47
47
  let workers_fee = executor_fee + dvns_fee;
48
- let (_, treasury_fee) = Self::quote_treasury(env, &packet.sender, packet.dst_eid, &workers_fee, &pay_in_zro);
48
+ let (_, treasury_fee) = Self::quote_treasury(env, &packet.sender, packet.dst_eid, workers_fee, pay_in_zro);
49
49
 
50
50
  if pay_in_zro {
51
51
  MessagingFee { native_fee: workers_fee, zro_fee: treasury_fee }
@@ -91,7 +91,7 @@ impl ISendLib for Uln302 {
91
91
  // Treasury fee
92
92
  let total_worker_fee = native_fee_recipients.iter().map(|fee| fee.amount).sum();
93
93
  let (treasury_addr, treasury_fee) =
94
- Self::quote_treasury(env, &packet.sender, packet.dst_eid, &total_worker_fee, &pay_in_zro);
94
+ Self::quote_treasury(env, &packet.sender, packet.dst_eid, total_worker_fee, pay_in_zro);
95
95
 
96
96
  // Handle ZRO fee recipients
97
97
  let mut zro_fee_recipients = vec![env];
@@ -280,12 +280,12 @@ impl Uln302 {
280
280
  env: &Env,
281
281
  sender: &Address,
282
282
  dst_eid: u32,
283
- workers_fee: &i128,
284
- pay_in_zro: &bool,
283
+ workers_fee: i128,
284
+ pay_in_zro: bool,
285
285
  ) -> (Address, i128) {
286
286
  let treasury_addr = Self::treasury(env);
287
287
  let treasury_fee =
288
- LayerZeroTreasuryClient::new(env, &treasury_addr).get_fee(sender, &dst_eid, workers_fee, pay_in_zro);
288
+ LayerZeroTreasuryClient::new(env, &treasury_addr).get_fee(sender, &dst_eid, &workers_fee, &pay_in_zro);
289
289
  assert_with_error!(env, treasury_fee >= 0, Uln302Error::InvalidFee);
290
290
  (treasury_addr, treasury_fee)
291
291
  }
@@ -67,6 +67,12 @@ impl Counter {
67
67
  }
68
68
  }
69
69
 
70
+ #[only_auth]
71
+ pub fn withdraw(env: &Env, to: &Address, amount: i128) {
72
+ let native_token = LayerZeroEndpointV2Client::new(env, &Self::endpoint(env)).native_token();
73
+ TokenClient::new(env, &native_token).transfer(&env.current_contract_address(), to, &amount);
74
+ }
75
+
70
76
  // ============================================================================================
71
77
  // View functions
72
78
  // ============================================================================================
@@ -3,7 +3,7 @@ use crate::{
3
3
  oapp_core::{endpoint_client, get_peer_or_panic, OAppCore},
4
4
  };
5
5
  use endpoint_v2::{MessagingFee, MessagingParams, MessagingReceipt};
6
- use soroban_sdk::{token::TokenClient, Address, Bytes, Env};
6
+ use soroban_sdk::{contracttype, token::TokenClient, Address, Bytes, Env};
7
7
  use utils::option_ext::OptionExt;
8
8
 
9
9
  /// The version of the OAppSender implementation.
@@ -22,7 +22,8 @@ pub const SENDER_VERSION: u64 = 1;
22
22
  /// - `Verified` — Caller asserts that `require_auth()` has already been called.
23
23
  /// Use this to avoid a duplicate `require_auth()` node in the Soroban auth tree
24
24
  /// (e.g., when the same address was already authorized as the message sender).
25
- #[derive(Clone)]
25
+ #[contracttype]
26
+ #[derive(Clone, Debug, Eq, PartialEq)]
26
27
  pub enum FeePayer {
27
28
  /// The fee payer has **not** been authorized yet.
28
29
  /// `__lz_send` will call `fee_payer.require_auth()` before transferring fees.
@@ -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)
@@ -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
  };
@@ -221,12 +221,16 @@ sequenceDiagram
221
221
 
222
222
  ## Stellar-specific considerations
223
223
 
224
- Two core differences between Soroban and EVM require protocol-level adaptations:
224
+ Several differences between Soroban and EVM require protocol-level adaptations:
225
225
 
226
226
  1. Stellar's variable-length address model differs from LayerZero's fixed
227
227
  bytes32 address abstraction
228
228
  2. Soroban's Time-To-Live (TTL)-based storage model requires active state
229
229
  maintenance, unlike EVM's persistent storage
230
+ 3. Soroban's 200-read-per-transaction storage limit makes the lazy inbound
231
+ nonce model susceptible to denial-of-service for certain OApps
232
+ 4. Soroban prohibits reentrancy, requiring alternative patterns for cross-contract
233
+ call flows
230
234
 
231
235
  ### Constraint 1: bytes32 address format mismatch
232
236
 
@@ -296,7 +300,47 @@ constraints may evolve.
296
300
  - Hard upper cap on extension targets (e.g., 1 year) to prevent excessive fees
297
301
  - TTL parameters can be permanently frozen once the ecosystem stabilizes
298
302
 
299
- ### Constraint 3: Reentrancy prohibition
303
+ ### Constraint 3: Storage read limits
304
+
305
+ Soroban enforces a hard limit of 200 persistent/temporary storage reads per
306
+ transaction. Under the lazy inbound nonce model used in EVM, the `inbound_nonce`
307
+ is not stored directly — it is computed on the fly by iterating forward from the
308
+ last checkpoint (`lazy_inbound_nonce`) and probing storage for each consecutive
309
+ payload hash. The same iterative check occurs during `clear`, which must verify
310
+ that all nonces between the checkpoint and the target nonce have been verified.
311
+
312
+ For certain OApps, failed `lz_receive` executions can create nonce gaps that
313
+ grow over time. Under the lazy model, clearing subsequent messages requires
314
+ iterating across these gaps, and the accumulated storage reads can exceed the
315
+ 200-read limit, making the messaging path susceptible to denial-of-service.
316
+
317
+ **Solution: Eager inbound nonce with pending nonce list (Solana model)**
318
+
319
+ Stellar adopts the same inbound nonce model used by LayerZero V2 on Solana.
320
+ Instead of lazily computing the inbound nonce via storage probing, the
321
+ `inbound_nonce` is stored directly and updated eagerly during verification:
322
+
323
+ 1. **`PendingInboundNonces`**: A sorted list of out-of-order verified nonces is
324
+ maintained in a single storage entry per path. When a message is verified
325
+ (or skipped/nilified), its nonce is inserted into this list.
326
+
327
+ 2. **Drain on insert**: After each insertion, consecutive nonces at the front of
328
+ the list are drained to advance the `inbound_nonce`. For example, if
329
+ `inbound_nonce = 3` and the pending list becomes `[4, 5, 7]`, nonces 4 and 5
330
+ are drained, advancing `inbound_nonce` to 5.
331
+
332
+ 3. **O(1) clear**: The `clear_payload` operation becomes a simple comparison
333
+ (`nonce <= inbound_nonce`) with no iteration or storage probing.
334
+
335
+ 4. **Bounded list size**: The pending list is capped at 256 entries
336
+ (`PENDING_INBOUND_NONCE_MAX_LEN`). Nonces beyond `inbound_nonce + 256`
337
+ cannot be verified, preventing unbounded memory growth and limiting the
338
+ maximum storage reads per verify operation.
339
+
340
+ This eliminates the iterative storage reads that could cause DoS under the lazy
341
+ model, keeping all operations within Soroban's transaction resource limits.
342
+
343
+ ### Constraint 4: Reentrancy prohibition
300
344
 
301
345
  Soroban prohibits reentrancy—a contract cannot call itself, directly or
302
346
  indirectly, within the same transaction. This fundamental difference from EVM
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@layerzerolabs/protocol-stellar-v2",
3
- "version": "0.2.34",
3
+ "version": "0.2.35",
4
4
  "private": false,
5
5
  "devDependencies": {
6
6
  "@types/node": "^22.18.6",
7
7
  "tsx": "^4.19.3",
8
8
  "typescript": "^5.8.2",
9
- "@layerzerolabs/common-node-utils": "0.2.34",
10
- "@layerzerolabs/vm-tooling-stellar": "0.2.34"
9
+ "@layerzerolabs/common-node-utils": "0.2.35",
10
+ "@layerzerolabs/vm-tooling-stellar": "0.2.35"
11
11
  },
12
12
  "publishConfig": {
13
13
  "access": "restricted",