@layerzerolabs/oapp-stellar-contracts 0.2.122

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.
@@ -0,0 +1,570 @@
1
+ use crate::{self as oapp, errors::OAppError, oapp_receiver::LzReceiveInternal, oapp_sender::FeePayer};
2
+ use endpoint_v2::{MessagingFee, MessagingParams, MessagingReceipt, Origin};
3
+ use soroban_sdk::{
4
+ contract, contractimpl, symbol_short,
5
+ testutils::{Address as _, MockAuth, MockAuthInvoke},
6
+ token::{StellarAssetClient, TokenClient},
7
+ Address, Bytes, BytesN, Env, IntoVal,
8
+ };
9
+
10
+ // Mock Endpoint contract
11
+ #[contract]
12
+ pub struct MockEndpoint;
13
+
14
+ #[contractimpl]
15
+ impl MockEndpoint {
16
+ pub fn __constructor(env: Env, zro_token: &Address, native_token: &Address) {
17
+ // Store as Option<Address> so zro() can return None when unavailable.
18
+ env.storage().instance().set(&symbol_short!("zro"), &Some(zro_token.clone()));
19
+ env.storage().instance().set(&symbol_short!("ntk"), native_token);
20
+ }
21
+
22
+ pub fn set_delegate(_env: Env, _oapp: Address, _delegate: Option<Address>) {
23
+ // do nothing in mock
24
+ }
25
+
26
+ pub fn quote(_env: Env, _sender: Address, params: MessagingParams) -> MessagingFee {
27
+ MessagingFee { native_fee: 1000, zro_fee: if params.pay_in_zro { 500 } else { 0 } }
28
+ }
29
+
30
+ pub fn send(env: Env, _sender: Address, params: MessagingParams, _refund_address: Address) -> MessagingReceipt {
31
+ // Record last send call for assertions
32
+ env.storage().instance().set(&symbol_short!("snds"), &_sender);
33
+ env.storage().instance().set(&symbol_short!("sndp"), &params);
34
+ env.storage().instance().set(&symbol_short!("sndr"), &_refund_address);
35
+
36
+ // Return mock receipt
37
+ MessagingReceipt {
38
+ guid: BytesN::from_array(&env, &[1u8; 32]),
39
+ nonce: 1,
40
+ fee: MessagingFee { native_fee: 1000, zro_fee: if params.pay_in_zro { 500 } else { 0 } },
41
+ }
42
+ }
43
+
44
+ pub fn last_send(env: Env) -> (Address, MessagingParams, Address) {
45
+ (
46
+ env.storage().instance().get(&symbol_short!("snds")).unwrap(),
47
+ env.storage().instance().get(&symbol_short!("sndp")).unwrap(),
48
+ env.storage().instance().get(&symbol_short!("sndr")).unwrap(),
49
+ )
50
+ }
51
+
52
+ pub fn zro(env: Env) -> Option<Address> {
53
+ // Return a mock ZRO token address (or None if not set)
54
+ env.storage().instance().get(&symbol_short!("zro")).unwrap_or(None)
55
+ }
56
+
57
+ pub fn native_token(env: Env) -> Address {
58
+ env.storage().instance().get(&symbol_short!("ntk")).unwrap()
59
+ }
60
+
61
+ pub fn set_zro(env: Env, zro: &Option<Address>) {
62
+ env.storage().instance().set(&symbol_short!("zro"), zro);
63
+ }
64
+ }
65
+
66
+ #[oapp_macros::oapp]
67
+ #[common_macros::lz_contract]
68
+ pub struct DummyOAppSender;
69
+
70
+ impl LzReceiveInternal for DummyOAppSender {
71
+ fn __lz_receive(
72
+ _env: &Env,
73
+ _origin: &Origin,
74
+ _guid: &BytesN<32>,
75
+ _message: &Bytes,
76
+ _extra_data: &Bytes,
77
+ _executor: &Address,
78
+ _value: i128,
79
+ ) {
80
+ // Not used in sender tests
81
+ }
82
+ }
83
+
84
+ #[contractimpl]
85
+ impl DummyOAppSender {
86
+ pub fn __constructor(env: &Env, owner: &Address, endpoint: &Address) {
87
+ oapp::oapp_core::init_ownable_oapp::<Self>(env, owner, endpoint, owner);
88
+ }
89
+
90
+ pub fn quote(env: &Env, dst_eid: u32, message: &Bytes, options: &Bytes, pay_in_zro: bool) -> MessagingFee {
91
+ Self::__quote(env, dst_eid, message, options, pay_in_zro)
92
+ }
93
+
94
+ pub fn send(
95
+ env: &Env,
96
+ sender: &Address,
97
+ dst_eid: u32,
98
+ message: &Bytes,
99
+ options: &Bytes,
100
+ fee: &MessagingFee,
101
+ refund_address: &Address,
102
+ ) -> MessagingReceipt {
103
+ sender.require_auth();
104
+ Self::__lz_send(env, dst_eid, message, options, &FeePayer::Verified(sender.clone()), fee, refund_address)
105
+ }
106
+
107
+ pub fn pay_native_fee(env: &Env, fee_payer: &Address, native_fee: i128) {
108
+ fee_payer.require_auth();
109
+ Self::__pay_native(env, fee_payer, native_fee)
110
+ }
111
+
112
+ pub fn pay_zro_fee(env: &Env, fee_payer: &Address, zro_fee: i128) {
113
+ fee_payer.require_auth();
114
+ Self::__pay_zro(env, fee_payer, zro_fee)
115
+ }
116
+ }
117
+
118
+ const REMOTE_EID: u32 = 100;
119
+ const UNSET_EID: u32 = 999;
120
+
121
+ struct TestSetup<'a> {
122
+ env: Env,
123
+ token_admin: Address,
124
+ endpoint: Address,
125
+ owner: Address,
126
+ oapp_client: DummyOAppSenderClient<'a>,
127
+ native_token: Address,
128
+ zro_token: Address,
129
+ native_token_client: TokenClient<'a>,
130
+ native_token_admin_client: StellarAssetClient<'a>,
131
+ zro_token_admin_client: StellarAssetClient<'a>,
132
+ }
133
+
134
+ fn setup<'a>() -> TestSetup<'a> {
135
+ let env = Env::default();
136
+
137
+ let owner = Address::generate(&env);
138
+
139
+ let token_admin = Address::generate(&env);
140
+ // Deploy mock tokens
141
+ let native_token_sac = env.register_stellar_asset_contract_v2(token_admin.clone());
142
+ let native_token = native_token_sac.address();
143
+ let native_token_client = TokenClient::new(&env, &native_token);
144
+ let native_token_admin_client = StellarAssetClient::new(&env, &native_token);
145
+
146
+ let zro_token_sac = env.register_stellar_asset_contract_v2(token_admin.clone());
147
+ let zro_token = zro_token_sac.address();
148
+ let zro_token_admin_client = StellarAssetClient::new(&env, &zro_token);
149
+
150
+ // Deploy mock endpoint
151
+ let endpoint = env.register(MockEndpoint, (&zro_token, &native_token));
152
+
153
+ // Deploy OApp
154
+ let oapp = env.register(DummyOAppSender, (&owner, &endpoint));
155
+ let oapp_client = DummyOAppSenderClient::new(&env, &oapp);
156
+
157
+ TestSetup {
158
+ env,
159
+ endpoint,
160
+ token_admin,
161
+ native_token_client,
162
+ native_token_admin_client,
163
+ zro_token_admin_client,
164
+ native_token,
165
+ zro_token,
166
+ owner,
167
+ oapp_client,
168
+ }
169
+ }
170
+
171
+ fn set_peer_with_auth(
172
+ env: &Env,
173
+ owner: &Address,
174
+ oapp_client: &DummyOAppSenderClient<'_>,
175
+ eid: u32,
176
+ peer: &BytesN<32>,
177
+ ) {
178
+ let peer_option = Some(peer.clone());
179
+ env.mock_auths(&[MockAuth {
180
+ address: owner,
181
+ invoke: &MockAuthInvoke {
182
+ contract: &oapp_client.address,
183
+ fn_name: "set_peer",
184
+ args: (&eid, &peer_option, owner).into_val(env),
185
+ sub_invokes: &[],
186
+ },
187
+ }]);
188
+ oapp_client.set_peer(&eid, &peer_option, owner);
189
+ }
190
+
191
+ fn mint_to(
192
+ env: &Env,
193
+ token_admin: &Address,
194
+ token: &Address,
195
+ token_admin_client: &StellarAssetClient<'_>,
196
+ to: &Address,
197
+ amount: i128,
198
+ ) {
199
+ env.mock_auths(&[MockAuth {
200
+ address: token_admin,
201
+ invoke: &MockAuthInvoke {
202
+ contract: token,
203
+ fn_name: "mint",
204
+ args: (to, amount).into_val(env),
205
+ sub_invokes: &[],
206
+ },
207
+ }]);
208
+ token_admin_client.mint(to, &amount);
209
+ }
210
+
211
+ fn mock_send_auth(
212
+ env: &Env,
213
+ sender: &Address,
214
+ oapp_client: &DummyOAppSenderClient<'_>,
215
+ dst_eid: u32,
216
+ message: &Bytes,
217
+ options: &Bytes,
218
+ fee: &MessagingFee,
219
+ refund_address: &Address,
220
+ native_token: &Address,
221
+ endpoint: &Address,
222
+ zro_token: &Address,
223
+ ) {
224
+ let native_transfer = MockAuthInvoke {
225
+ contract: native_token,
226
+ fn_name: "transfer",
227
+ args: (sender, endpoint, &fee.native_fee).into_val(env),
228
+ sub_invokes: &[],
229
+ };
230
+
231
+ if fee.zro_fee > 0 {
232
+ let zro_transfer = MockAuthInvoke {
233
+ contract: zro_token,
234
+ fn_name: "transfer",
235
+ args: (sender, endpoint, &fee.zro_fee).into_val(env),
236
+ sub_invokes: &[],
237
+ };
238
+ let sub_invokes = [native_transfer, zro_transfer];
239
+ env.mock_auths(&[MockAuth {
240
+ address: sender,
241
+ invoke: &MockAuthInvoke {
242
+ contract: &oapp_client.address,
243
+ fn_name: "send",
244
+ args: (sender, &dst_eid, message, options, fee, refund_address).into_val(env),
245
+ sub_invokes: &sub_invokes,
246
+ },
247
+ }]);
248
+ } else {
249
+ let sub_invokes = [native_transfer];
250
+ env.mock_auths(&[MockAuth {
251
+ address: sender,
252
+ invoke: &MockAuthInvoke {
253
+ contract: &oapp_client.address,
254
+ fn_name: "send",
255
+ args: (sender, &dst_eid, message, options, fee, refund_address).into_val(env),
256
+ sub_invokes: &sub_invokes,
257
+ },
258
+ }]);
259
+ }
260
+ }
261
+
262
+ #[test]
263
+ fn test_quote_with_peer_set() {
264
+ let TestSetup { env, owner, oapp_client, .. } = setup();
265
+
266
+ // Set peer for destination
267
+ let peer = BytesN::from_array(&env, &[1; 32]);
268
+ set_peer_with_auth(&env, &owner, &oapp_client, REMOTE_EID, &peer);
269
+
270
+ let message = Bytes::from_array(&env, &[1, 2, 3, 4, 5]);
271
+ let options = Bytes::from_array(&env, &[6, 7, 8]);
272
+
273
+ // Test quote without ZRO
274
+ let fee = oapp_client.quote(&REMOTE_EID, &message, &options, &false);
275
+ assert_eq!(fee.native_fee, 1000);
276
+ assert_eq!(fee.zro_fee, 0);
277
+
278
+ // Test quote with ZRO
279
+ let fee_with_zro = oapp_client.quote(&REMOTE_EID, &message, &options, &true);
280
+ assert_eq!(fee_with_zro.native_fee, 1000);
281
+ assert_eq!(fee_with_zro.zro_fee, 500);
282
+ }
283
+
284
+ #[test]
285
+ fn test_quote_without_peer_panics() {
286
+ let TestSetup { env, oapp_client, .. } = setup();
287
+
288
+ let message = Bytes::from_array(&env, &[1, 2, 3]);
289
+ let options = Bytes::from_array(&env, &[]);
290
+
291
+ // This should panic because no peer is set for UNSET_EID
292
+ let result = oapp_client.try_quote(&UNSET_EID, &message, &options, &false);
293
+ assert_eq!(result.err().unwrap().ok().unwrap(), OAppError::NoPeer.into());
294
+ }
295
+
296
+ #[test]
297
+ fn test_lz_send_native_only() {
298
+ let TestSetup {
299
+ env,
300
+ oapp_client,
301
+ endpoint,
302
+ native_token,
303
+ zro_token,
304
+ native_token_client,
305
+ native_token_admin_client,
306
+ token_admin,
307
+ owner,
308
+ ..
309
+ } = setup();
310
+
311
+ // Setup peer
312
+ let peer = BytesN::from_array(&env, &[2; 32]);
313
+ set_peer_with_auth(&env, &owner, &oapp_client, REMOTE_EID, &peer);
314
+
315
+ let sender = Address::generate(&env);
316
+ mint_to(&env, &token_admin, &native_token, &native_token_admin_client, &sender, 1000i128);
317
+
318
+ let message = Bytes::from_array(&env, &[1, 2, 3]);
319
+ let options = Bytes::from_array(&env, &[]);
320
+ let refund_address = Address::generate(&env);
321
+ let fee = MessagingFee { native_fee: 1000, zro_fee: 0 };
322
+
323
+ mock_send_auth(
324
+ &env,
325
+ &sender,
326
+ &oapp_client,
327
+ REMOTE_EID,
328
+ &message,
329
+ &options,
330
+ &fee,
331
+ &refund_address,
332
+ &native_token,
333
+ &endpoint,
334
+ &zro_token,
335
+ );
336
+ let receipt = oapp_client.send(&sender, &REMOTE_EID, &message, &options, &fee, &refund_address);
337
+
338
+ assert_eq!(receipt.nonce, 1);
339
+ assert_eq!(receipt.fee.native_fee, 1000);
340
+ assert_eq!(receipt.fee.zro_fee, 0);
341
+
342
+ // Assert the endpoint received the correct send params
343
+ let endpoint_client = MockEndpointClient::new(&env, &endpoint);
344
+ let (endpoint_sender, params, refund) = endpoint_client.last_send();
345
+ assert_eq!(endpoint_sender, oapp_client.address);
346
+ assert_eq!(params.dst_eid, REMOTE_EID);
347
+ assert_eq!(params.receiver, peer);
348
+ assert_eq!(params.message, message);
349
+ assert_eq!(params.options, options);
350
+ assert_eq!(params.pay_in_zro, false);
351
+ assert_eq!(refund, refund_address);
352
+
353
+ assert_eq!(native_token_client.balance(&sender), 0);
354
+ assert_eq!(native_token_client.balance(&endpoint), 1000);
355
+ assert_eq!(TokenClient::new(&env, &zro_token).balance(&endpoint), 0);
356
+ }
357
+
358
+ #[test]
359
+ fn test_lz_send_with_zro() {
360
+ let TestSetup {
361
+ env,
362
+ oapp_client,
363
+ endpoint,
364
+ native_token,
365
+ zro_token,
366
+ native_token_client,
367
+ native_token_admin_client,
368
+ zro_token_admin_client,
369
+ token_admin,
370
+ owner,
371
+ ..
372
+ } = setup();
373
+
374
+ // Setup peer
375
+ let peer = BytesN::from_array(&env, &[3; 32]);
376
+ set_peer_with_auth(&env, &owner, &oapp_client, REMOTE_EID, &peer);
377
+
378
+ let sender = Address::generate(&env);
379
+ let fee = MessagingFee { native_fee: 1000, zro_fee: 500 };
380
+ mint_to(&env, &token_admin, &native_token, &native_token_admin_client, &sender, fee.native_fee);
381
+ mint_to(&env, &token_admin, &zro_token, &zro_token_admin_client, &sender, fee.zro_fee);
382
+
383
+ let message = Bytes::from_array(&env, &[4, 5, 6]);
384
+ let options = Bytes::from_array(&env, &[7]);
385
+ let refund_address = Address::generate(&env);
386
+
387
+ mock_send_auth(
388
+ &env,
389
+ &sender,
390
+ &oapp_client,
391
+ REMOTE_EID,
392
+ &message,
393
+ &options,
394
+ &fee,
395
+ &refund_address,
396
+ &native_token,
397
+ &endpoint,
398
+ &zro_token,
399
+ );
400
+ let receipt = oapp_client.send(&sender, &REMOTE_EID, &message, &options, &fee, &refund_address);
401
+
402
+ assert_eq!(receipt.nonce, 1);
403
+ assert_eq!(receipt.fee.native_fee, 1000);
404
+ assert_eq!(receipt.fee.zro_fee, 500);
405
+
406
+ // Assert the endpoint received the correct send params
407
+ let endpoint_client = MockEndpointClient::new(&env, &endpoint);
408
+ let (endpoint_sender, params, refund) = endpoint_client.last_send();
409
+ assert_eq!(endpoint_sender, oapp_client.address);
410
+ assert_eq!(params.dst_eid, REMOTE_EID);
411
+ assert_eq!(params.receiver, peer);
412
+ assert_eq!(params.message, message);
413
+ assert_eq!(params.options, options);
414
+ assert_eq!(params.pay_in_zro, true);
415
+ assert_eq!(refund, refund_address);
416
+
417
+ assert_eq!(native_token_client.balance(&sender), 0);
418
+ assert_eq!(native_token_client.balance(&endpoint), 1000);
419
+
420
+ let zro_token_client = TokenClient::new(&env, &zro_token);
421
+ assert_eq!(zro_token_client.balance(&sender), 0);
422
+ assert_eq!(zro_token_client.balance(&endpoint), 500);
423
+ }
424
+
425
+ #[test]
426
+ fn test_pay_native() {
427
+ let TestSetup { env, oapp_client, endpoint, native_token, native_token_admin_client, token_admin, .. } = setup();
428
+
429
+ let payer = Address::generate(&env);
430
+ let payment_amount = 2000i128;
431
+
432
+ // Fund the payer
433
+ mint_to(&env, &token_admin, &native_token, &native_token_admin_client, &payer, payment_amount);
434
+
435
+ // Make payment
436
+ env.mock_auths(&[MockAuth {
437
+ address: &payer,
438
+ invoke: &MockAuthInvoke {
439
+ contract: &oapp_client.address,
440
+ fn_name: "pay_native_fee",
441
+ args: (&payer, &payment_amount).into_val(&env),
442
+ sub_invokes: &[MockAuthInvoke {
443
+ contract: &native_token,
444
+ fn_name: "transfer",
445
+ args: (&payer, &endpoint, &payment_amount).into_val(&env),
446
+ sub_invokes: &[],
447
+ }],
448
+ },
449
+ }]);
450
+ oapp_client.pay_native_fee(&payer, &payment_amount);
451
+
452
+ // Verify balances
453
+ assert_eq!(native_token_admin_client.balance(&payer), 0);
454
+ assert_eq!(native_token_admin_client.balance(&endpoint), payment_amount);
455
+ }
456
+
457
+ #[test]
458
+ #[should_panic(expected = "balance is not sufficient to spend")]
459
+ fn test_pay_native_insufficient_balance() {
460
+ let TestSetup { env, oapp_client, endpoint, native_token, native_token_admin_client, token_admin, .. } = setup();
461
+
462
+ let balance = 500i128;
463
+ let payment_amount = 1000i128;
464
+
465
+ let payer = Address::generate(&env);
466
+
467
+ // Fund with less than required
468
+ mint_to(&env, &token_admin, &native_token, &native_token_admin_client, &payer, balance);
469
+
470
+ // This should panic due to insufficient balance
471
+ env.mock_auths(&[MockAuth {
472
+ address: &payer,
473
+ invoke: &MockAuthInvoke {
474
+ contract: &oapp_client.address,
475
+ fn_name: "pay_native_fee",
476
+ args: (&payer, &payment_amount).into_val(&env),
477
+ sub_invokes: &[MockAuthInvoke {
478
+ contract: &native_token,
479
+ fn_name: "transfer",
480
+ args: (&payer, &endpoint, &payment_amount).into_val(&env),
481
+ sub_invokes: &[],
482
+ }],
483
+ },
484
+ }]);
485
+ oapp_client.pay_native_fee(&payer, &payment_amount);
486
+ }
487
+
488
+ #[test]
489
+ fn test_pay_zro_success() {
490
+ let TestSetup { env, oapp_client, endpoint, zro_token, zro_token_admin_client, token_admin, .. } = setup();
491
+
492
+ let payer = Address::generate(&env);
493
+ let payment_amount = 500i128;
494
+ mint_to(&env, &token_admin, &zro_token, &zro_token_admin_client, &payer, payment_amount);
495
+
496
+ env.mock_auths(&[MockAuth {
497
+ address: &payer,
498
+ invoke: &MockAuthInvoke {
499
+ contract: &oapp_client.address,
500
+ fn_name: "pay_zro_fee",
501
+ args: (&payer, &payment_amount).into_val(&env),
502
+ sub_invokes: &[MockAuthInvoke {
503
+ contract: &zro_token,
504
+ fn_name: "transfer",
505
+ args: (&payer, &endpoint, &payment_amount).into_val(&env),
506
+ sub_invokes: &[],
507
+ }],
508
+ },
509
+ }]);
510
+ oapp_client.pay_zro_fee(&payer, &payment_amount);
511
+
512
+ assert_eq!(zro_token_admin_client.balance(&payer), 0);
513
+ assert_eq!(zro_token_admin_client.balance(&endpoint), payment_amount);
514
+ }
515
+
516
+ #[test]
517
+ #[should_panic(expected = "balance is not sufficient to spend")]
518
+ fn test_pay_zro_insufficient_balance() {
519
+ let TestSetup { env, oapp_client, endpoint, zro_token, zro_token_admin_client, token_admin, .. } = setup();
520
+
521
+ let payer = Address::generate(&env);
522
+ let balance = 100i128;
523
+ let payment_amount = 500i128;
524
+
525
+ // Fund with less than required
526
+ mint_to(&env, &token_admin, &zro_token, &zro_token_admin_client, &payer, balance);
527
+
528
+ env.mock_auths(&[MockAuth {
529
+ address: &payer,
530
+ invoke: &MockAuthInvoke {
531
+ contract: &oapp_client.address,
532
+ fn_name: "pay_zro_fee",
533
+ args: (&payer, &payment_amount).into_val(&env),
534
+ sub_invokes: &[MockAuthInvoke {
535
+ contract: &zro_token,
536
+ fn_name: "transfer",
537
+ args: (&payer, &endpoint, &payment_amount).into_val(&env),
538
+ sub_invokes: &[],
539
+ }],
540
+ },
541
+ }]);
542
+ oapp_client.pay_zro_fee(&payer, &payment_amount);
543
+ }
544
+
545
+ #[test]
546
+ fn test_pay_zro_unavailable_returns_error() {
547
+ let TestSetup { env, oapp_client, endpoint, zro_token, .. } = setup();
548
+
549
+ let endpoint_client = MockEndpointClient::new(&env, &endpoint);
550
+ endpoint_client.set_zro(&None);
551
+
552
+ let payer = Address::generate(&env);
553
+ let payment_amount = 500i128;
554
+
555
+ env.mock_auths(&[MockAuth {
556
+ address: &payer,
557
+ invoke: &MockAuthInvoke {
558
+ contract: &oapp_client.address,
559
+ fn_name: "pay_zro_fee",
560
+ args: (&payer, &payment_amount).into_val(&env),
561
+ sub_invokes: &[],
562
+ },
563
+ }]);
564
+ let result = oapp_client.try_pay_zro_fee(&payer, &payment_amount);
565
+ assert_eq!(result.err().unwrap().ok().unwrap(), OAppError::ZroTokenUnavailable.into());
566
+
567
+ // Ensure we did not transfer any ZRO accidentally
568
+ let token_client = TokenClient::new(&env, &zro_token);
569
+ assert_eq!(token_client.balance(&payer), 0);
570
+ }