@layerzerolabs/protocol-stellar-v2 0.2.60 → 0.2.61

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.
@@ -1,27 +1,15 @@
1
1
  use crate::{
2
2
  errors::DvnError,
3
3
  tests::setup::{TestSetup, VID},
4
- LzDVNClient, Sender, TransactionAuthData,
4
+ Call, LzDVNClient, Sender, TransactionAuthData,
5
5
  };
6
6
  use ed25519_dalek::{Signer, SigningKey};
7
7
  use rand::thread_rng;
8
- use soroban_sdk::{auth::Context, vec, xdr::ToXdr, Bytes, BytesN, Env, IntoVal, Vec};
9
- use utils::buffer_writer::BufferWriter;
10
-
11
- const AUTH_NONCE: i64 = 0;
12
- const AUTH_EXPIRY_LEDGER: u32 = 10000;
13
- const ENVELOPE_TYPE_SOROBAN_AUTHORIZATION: u32 = 9;
14
-
15
- /// Build the full preimage bytes from auth fields.
16
- fn build_preimage(env: &Env, auth_nonce: i64, auth_expiry_ledger: u32, invocation: &Bytes) -> Bytes {
17
- BufferWriter::new(env)
18
- .write_u32(ENVELOPE_TYPE_SOROBAN_AUTHORIZATION)
19
- .write_bytes_n(&env.ledger().network_id())
20
- .write_i64(auth_nonce)
21
- .write_u32(auth_expiry_ledger)
22
- .write_bytes(invocation)
23
- .to_bytes()
24
- }
8
+ use soroban_sdk::{
9
+ auth::{ContractContext, Context},
10
+ testutils::Address as _,
11
+ vec, Address, BytesN, Env, IntoVal, Symbol, Val, Vec,
12
+ };
25
13
 
26
14
  struct Ed25519KeyPair {
27
15
  signing_key: SigningKey,
@@ -46,18 +34,97 @@ impl Ed25519KeyPair {
46
34
  }
47
35
  }
48
36
 
49
- /// Compute signature_payload = SHA256(preimage) for the given preimage bytes.
50
- fn compute_signature_payload(env: &Env, preimage: &Bytes) -> BytesN<32> {
51
- env.crypto().sha256(preimage).into()
37
+ /// Creates a single self-call auth context and matching Call for testing.
38
+ fn make_self_call_context(
39
+ env: &Env,
40
+ contract_id: &soroban_sdk::Address,
41
+ ) -> (Vec<Context>, Vec<Call>) {
42
+ let fn_name = Symbol::new(env, "set_paused");
43
+ let args: Vec<Val> = vec![env, true.into_val(env)];
44
+
45
+ let context = Context::Contract(ContractContext {
46
+ contract: contract_id.clone(),
47
+ fn_name: fn_name.clone(),
48
+ args: args.clone(),
49
+ });
50
+ let auth_contexts = vec![env, context];
51
+
52
+ let calls = vec![env, Call { to: contract_id.clone(), func: fn_name, args }];
53
+
54
+ (auth_contexts, calls)
52
55
  }
53
56
 
54
- /// Compute call hash for multisig: keccak256(vid || expiration || invocation).
55
- fn compute_call_hash(env: &Env, vid: u32, expiration: u64, invocation: &Bytes) -> BytesN<32> {
56
- let mut writer = BufferWriter::new(env);
57
- let data = writer.write_u32(vid).write_u64(expiration).write_bytes(invocation).to_bytes();
58
- env.crypto().keccak256(&data).into()
57
+ /// Creates upgrade auth contexts (3 entries: upgrader call, upgrade, migrate).
58
+ fn make_upgrade_contexts(
59
+ env: &Env,
60
+ contract_id: &soroban_sdk::Address,
61
+ upgrader_id: &soroban_sdk::Address,
62
+ ) -> (Vec<Context>, Vec<Call>) {
63
+ let wasm_hash_val: Val = BytesN::from_array(env, &[0xABu8; 32]).into_val(env);
64
+ let migration_data_val: Val = soroban_sdk::Bytes::new(env).into_val(env);
65
+
66
+ // [0]: Upgrader call
67
+ let upgrader_ctx = Context::Contract(ContractContext {
68
+ contract: upgrader_id.clone(),
69
+ fn_name: Symbol::new(env, "upgrade_and_migrate"),
70
+ args: vec![
71
+ env,
72
+ contract_id.clone().into_val(env),
73
+ wasm_hash_val.clone(),
74
+ migration_data_val.clone(),
75
+ ],
76
+ });
77
+ // [1]: upgrade self-call
78
+ let upgrade_ctx = Context::Contract(ContractContext {
79
+ contract: contract_id.clone(),
80
+ fn_name: Symbol::new(env, "upgrade"),
81
+ args: vec![env, wasm_hash_val],
82
+ });
83
+ // [2]: migrate self-call
84
+ let migrate_ctx = Context::Contract(ContractContext {
85
+ contract: contract_id.clone(),
86
+ fn_name: Symbol::new(env, "migrate"),
87
+ args: vec![env, migration_data_val],
88
+ });
89
+
90
+ let auth_contexts = vec![env, upgrader_ctx.clone(), upgrade_ctx.clone(), migrate_ctx.clone()];
91
+
92
+ // Build matching Call vec
93
+ let calls = vec![
94
+ env,
95
+ Call {
96
+ to: upgrader_id.clone(),
97
+ func: Symbol::new(env, "upgrade_and_migrate"),
98
+ args: match &upgrader_ctx {
99
+ Context::Contract(c) => c.args.clone(),
100
+ _ => unreachable!(),
101
+ },
102
+ },
103
+ Call {
104
+ to: contract_id.clone(),
105
+ func: Symbol::new(env, "upgrade"),
106
+ args: match &upgrade_ctx {
107
+ Context::Contract(c) => c.args.clone(),
108
+ _ => unreachable!(),
109
+ },
110
+ },
111
+ Call {
112
+ to: contract_id.clone(),
113
+ func: Symbol::new(env, "migrate"),
114
+ args: match &migrate_ctx {
115
+ Context::Contract(c) => c.args.clone(),
116
+ _ => unreachable!(),
117
+ },
118
+ },
119
+ ];
120
+
121
+ (auth_contexts, calls)
59
122
  }
60
123
 
124
+ // ============================================================================
125
+ // Single Self-Call Tests
126
+ // ============================================================================
127
+
61
128
  #[test]
62
129
  fn test_check_auth_success() {
63
130
  extern crate std;
@@ -67,15 +134,14 @@ fn test_check_auth_success() {
67
134
  let setup = TestSetup::with_admin_bytes(1, std::vec![admin_bytes]);
68
135
  let env = setup.env.clone();
69
136
  let expiration = env.ledger().timestamp() + 1000;
70
- let auth_contexts: Vec<Context> = Vec::new(&env);
137
+ let (auth_contexts, calls) = make_self_call_context(&env, &setup.contract_id);
71
138
 
72
- let invocation = auth_contexts.clone().to_xdr(&env);
73
- let preimage = build_preimage(&env, AUTH_NONCE, AUTH_EXPIRY_LEDGER, &invocation);
74
- let payload = compute_signature_payload(&env, &preimage);
139
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
75
140
  let public_key = admin_kp.public_key(&env);
76
141
  let signature = admin_kp.sign(&env, &payload.to_array());
77
142
 
78
- let hash = compute_call_hash(&env, VID, expiration, &invocation);
143
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
144
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &calls);
79
145
  let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
80
146
 
81
147
  let tx_auth = TransactionAuthData {
@@ -83,7 +149,6 @@ fn test_check_auth_success() {
83
149
  expiration,
84
150
  signatures: vec![&env, sig],
85
151
  sender: Sender::Admin(public_key, signature),
86
- preimage,
87
152
  };
88
153
 
89
154
  let res = env.try_invoke_contract_check_auth::<DvnError>(
@@ -104,15 +169,14 @@ fn test_check_auth_not_admin() {
104
169
  let setup = TestSetup::new(1);
105
170
  let env = setup.env.clone();
106
171
  let expiration = env.ledger().timestamp() + 1000;
107
- let auth_contexts: Vec<Context> = Vec::new(&env);
172
+ let (auth_contexts, calls) = make_self_call_context(&env, &setup.contract_id);
108
173
 
109
- let invocation = auth_contexts.clone().to_xdr(&env);
110
- let preimage = build_preimage(&env, AUTH_NONCE, AUTH_EXPIRY_LEDGER, &invocation);
111
- let payload = compute_signature_payload(&env, &preimage);
174
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
112
175
  let public_key = non_admin_kp.public_key(&env);
113
176
  let signature = non_admin_kp.sign(&env, &payload.to_array());
114
177
 
115
- let hash = compute_call_hash(&env, VID, expiration, &invocation);
178
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
179
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &calls);
116
180
  let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
117
181
 
118
182
  let tx_auth = TransactionAuthData {
@@ -120,7 +184,6 @@ fn test_check_auth_not_admin() {
120
184
  expiration,
121
185
  signatures: vec![&env, sig],
122
186
  sender: Sender::Admin(public_key, signature),
123
- preimage,
124
187
  };
125
188
 
126
189
  let res = env.try_invoke_contract_check_auth::<DvnError>(
@@ -142,15 +205,14 @@ fn test_check_auth_wrong_signer_fails() {
142
205
  let setup = TestSetup::with_admin_bytes(1, std::vec![admin_bytes]);
143
206
  let env = setup.env.clone();
144
207
  let expiration = env.ledger().timestamp() + 1000;
145
- let auth_contexts: Vec<Context> = Vec::new(&env);
208
+ let (auth_contexts, calls) = make_self_call_context(&env, &setup.contract_id);
146
209
 
147
- let invocation = auth_contexts.clone().to_xdr(&env);
148
- let preimage = build_preimage(&env, AUTH_NONCE, AUTH_EXPIRY_LEDGER, &invocation);
149
- let payload = compute_signature_payload(&env, &preimage);
210
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
150
211
  let public_key = admin_kp.public_key(&env);
151
212
  let signature = admin_kp.sign(&env, &payload.to_array());
152
213
 
153
- let hash = compute_call_hash(&env, VID, expiration, &invocation);
214
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
215
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &calls);
154
216
  let wrong_sig = crate::tests::key_pair::KeyPair::generate().sign_bytes(&env, &hash);
155
217
 
156
218
  let tx_auth = TransactionAuthData {
@@ -158,9 +220,9 @@ fn test_check_auth_wrong_signer_fails() {
158
220
  expiration,
159
221
  signatures: vec![&env, wrong_sig],
160
222
  sender: Sender::Admin(public_key, signature),
161
- preimage,
162
223
  };
163
224
 
225
+ // verify_signatures panics with MultiSigError::SignerNotFound when signer is not found
164
226
  let res = env.try_invoke_contract_check_auth::<utils::errors::MultiSigError>(
165
227
  &setup.contract_id,
166
228
  &payload,
@@ -181,15 +243,14 @@ fn test_check_auth_invalid_vid_fails() {
181
243
  let env = setup.env.clone();
182
244
  let expiration = env.ledger().timestamp() + 1000;
183
245
  let wrong_vid = VID + 1;
184
- let auth_contexts: Vec<Context> = Vec::new(&env);
246
+ let (auth_contexts, calls) = make_self_call_context(&env, &setup.contract_id);
185
247
 
186
- let invocation = auth_contexts.clone().to_xdr(&env);
187
- let preimage = build_preimage(&env, AUTH_NONCE, AUTH_EXPIRY_LEDGER, &invocation);
188
- let payload = compute_signature_payload(&env, &preimage);
248
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
189
249
  let public_key = admin_kp.public_key(&env);
190
250
  let signature = admin_kp.sign(&env, &payload.to_array());
191
251
 
192
- let hash = compute_call_hash(&env, wrong_vid, expiration, &invocation);
252
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
253
+ let hash = dvn_client.hash_call_data(&wrong_vid, &expiration, &calls);
193
254
  let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
194
255
 
195
256
  let tx_auth = TransactionAuthData {
@@ -197,7 +258,6 @@ fn test_check_auth_invalid_vid_fails() {
197
258
  expiration,
198
259
  signatures: vec![&env, sig],
199
260
  sender: Sender::Admin(public_key, signature),
200
- preimage,
201
261
  };
202
262
 
203
263
  let res = env.try_invoke_contract_check_auth::<DvnError>(
@@ -219,15 +279,14 @@ fn test_check_auth_expired_fails() {
219
279
  let setup = TestSetup::with_admin_bytes(1, std::vec![admin_bytes]);
220
280
  let env = setup.env.clone();
221
281
  let expiration = env.ledger().timestamp();
222
- let auth_contexts: Vec<Context> = Vec::new(&env);
282
+ let (auth_contexts, calls) = make_self_call_context(&env, &setup.contract_id);
223
283
 
224
- let invocation = auth_contexts.clone().to_xdr(&env);
225
- let preimage = build_preimage(&env, AUTH_NONCE, AUTH_EXPIRY_LEDGER, &invocation);
226
- let payload = compute_signature_payload(&env, &preimage);
284
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
227
285
  let public_key = admin_kp.public_key(&env);
228
286
  let signature = admin_kp.sign(&env, &payload.to_array());
229
287
 
230
- let hash = compute_call_hash(&env, VID, expiration, &invocation);
288
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
289
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &calls);
231
290
  let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
232
291
 
233
292
  let tx_auth = TransactionAuthData {
@@ -235,7 +294,6 @@ fn test_check_auth_expired_fails() {
235
294
  expiration,
236
295
  signatures: vec![&env, sig],
237
296
  sender: Sender::Admin(public_key, signature),
238
- preimage,
239
297
  };
240
298
 
241
299
  let res = env.try_invoke_contract_check_auth::<DvnError>(
@@ -257,15 +315,14 @@ fn test_check_auth_hash_already_used_fails() {
257
315
  let setup = TestSetup::with_admin_bytes(1, std::vec![admin_bytes]);
258
316
  let env = setup.env.clone();
259
317
  let expiration = env.ledger().timestamp() + 1000;
260
- let auth_contexts: Vec<Context> = Vec::new(&env);
318
+ let (auth_contexts, calls) = make_self_call_context(&env, &setup.contract_id);
261
319
 
262
- let invocation = auth_contexts.clone().to_xdr(&env);
263
- let preimage = build_preimage(&env, AUTH_NONCE, AUTH_EXPIRY_LEDGER, &invocation);
264
- let payload = compute_signature_payload(&env, &preimage);
320
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
265
321
  let public_key = admin_kp.public_key(&env);
266
322
  let signature = admin_kp.sign(&env, &payload.to_array());
267
323
 
268
- let hash = compute_call_hash(&env, VID, expiration, &invocation);
324
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
325
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &calls);
269
326
  let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
270
327
 
271
328
  let tx_auth = TransactionAuthData {
@@ -273,7 +330,6 @@ fn test_check_auth_hash_already_used_fails() {
273
330
  expiration,
274
331
  signatures: vec![&env, sig.clone()],
275
332
  sender: Sender::Admin(public_key.clone(), signature.clone()),
276
- preimage: preimage.clone(),
277
333
  };
278
334
 
279
335
  let res = env.try_invoke_contract_check_auth::<DvnError>(
@@ -289,7 +345,6 @@ fn test_check_auth_hash_already_used_fails() {
289
345
  expiration,
290
346
  signatures: vec![&env, sig],
291
347
  sender: Sender::Admin(public_key, signature),
292
- preimage,
293
348
  };
294
349
 
295
350
  let res2 = env.try_invoke_contract_check_auth::<DvnError>(
@@ -305,21 +360,26 @@ fn test_check_auth_hash_already_used_fails() {
305
360
  #[test]
306
361
  fn test_check_auth_sender_none_fails_when_admin_required() {
307
362
  extern crate std;
363
+ use crate::Sender;
308
364
 
309
365
  let setup = TestSetup::new(1);
310
366
  let env = setup.env.clone();
311
367
  let expiration = env.ledger().timestamp() + 1000;
312
- let auth_contexts: Vec<Context> = Vec::new(&env);
368
+ let (auth_contexts, calls) = make_self_call_context(&env, &setup.contract_id);
313
369
 
314
- let invocation = auth_contexts.clone().to_xdr(&env);
315
- let preimage = build_preimage(&env, AUTH_NONCE, AUTH_EXPIRY_LEDGER, &invocation);
316
- let payload = compute_signature_payload(&env, &preimage);
370
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
317
371
 
318
- let hash = compute_call_hash(&env, VID, expiration, &invocation);
372
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
373
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &calls);
319
374
  let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
320
375
 
321
- let tx_auth =
322
- TransactionAuthData { vid: VID, expiration, signatures: vec![&env, sig], sender: Sender::None, preimage };
376
+ // Use Sender::None which should fail when admin is required
377
+ let tx_auth = TransactionAuthData {
378
+ vid: VID,
379
+ expiration,
380
+ signatures: vec![&env, sig],
381
+ sender: Sender::None,
382
+ };
323
383
 
324
384
  let res = env.try_invoke_contract_check_auth::<DvnError>(
325
385
  &setup.contract_id,
@@ -334,29 +394,42 @@ fn test_check_auth_sender_none_fails_when_admin_required() {
334
394
  #[test]
335
395
  fn test_check_auth_set_admin_bypasses_admin_verification() {
336
396
  extern crate std;
337
- use soroban_sdk::{auth::ContractContext, Symbol};
397
+ use crate::Sender;
338
398
 
339
399
  let setup = TestSetup::new(1);
340
400
  let env = setup.env.clone();
341
401
  let expiration = env.ledger().timestamp() + 1000;
342
402
 
403
+ // Create a context for set_admin call on the DVN contract
343
404
  let set_admin_context = Context::Contract(ContractContext {
344
405
  contract: setup.contract_id.clone(),
345
406
  fn_name: Symbol::new(&env, "set_admin"),
346
407
  args: Vec::new(&env),
347
408
  });
348
- let auth_contexts: Vec<Context> = vec![&env, set_admin_context.clone()];
409
+ let auth_contexts: Vec<Context> = vec![&env, set_admin_context];
410
+
411
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
349
412
 
350
- let invocation = auth_contexts.clone().to_xdr(&env);
351
- let preimage = build_preimage(&env, AUTH_NONCE, AUTH_EXPIRY_LEDGER, &invocation);
352
- let payload = compute_signature_payload(&env, &preimage);
413
+ let calls: Vec<Call> = vec![
414
+ &env,
415
+ Call {
416
+ to: setup.contract_id.clone(),
417
+ func: Symbol::new(&env, "set_admin"),
418
+ args: Vec::new(&env),
419
+ },
420
+ ];
353
421
 
354
422
  let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
355
- let hash = dvn_client.hash_call_data(&VID, &expiration, &invocation);
423
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &calls);
356
424
  let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
357
425
 
358
- let tx_auth =
359
- TransactionAuthData { vid: VID, expiration, signatures: vec![&env, sig], sender: Sender::None, preimage };
426
+ // Use Sender::None - should succeed for set_admin since it bypasses admin verification
427
+ let tx_auth = TransactionAuthData {
428
+ vid: VID,
429
+ expiration,
430
+ signatures: vec![&env, sig],
431
+ sender: Sender::None,
432
+ };
360
433
 
361
434
  let res = env.try_invoke_contract_check_auth::<DvnError>(
362
435
  &setup.contract_id,
@@ -365,11 +438,60 @@ fn test_check_auth_set_admin_bypasses_admin_verification() {
365
438
  &auth_contexts,
366
439
  );
367
440
 
441
+ // Should succeed because set_admin bypasses admin verification
368
442
  assert!(res.is_ok(), "Expected success for set_admin call, got {:?}", res);
369
443
  }
370
444
 
371
445
  #[test]
372
- fn test_check_auth_invalid_signature_payload_fails() {
446
+ fn test_check_auth_non_contract_context_fails() {
447
+ extern crate std;
448
+ use crate::Sender;
449
+ use soroban_sdk::auth::{ContractExecutable, CreateContractHostFnContext};
450
+
451
+ let admin_kp = Ed25519KeyPair::generate();
452
+ let admin_bytes = admin_kp.public_key_bytes();
453
+
454
+ let setup = TestSetup::with_admin_bytes(1, std::vec![admin_bytes]);
455
+ let env = setup.env.clone();
456
+ let expiration = env.ledger().timestamp() + 1000;
457
+
458
+ // Create a non-Contract context (CreateContractHostFn)
459
+ let non_contract_context = Context::CreateContractHostFn(CreateContractHostFnContext {
460
+ executable: ContractExecutable::Wasm(BytesN::from_array(&env, &[0; 32])),
461
+ salt: BytesN::from_array(&env, &[0; 32]),
462
+ });
463
+ let auth_contexts: Vec<Context> = vec![&env, non_contract_context];
464
+
465
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
466
+ let public_key = admin_kp.public_key(&env);
467
+ let signature = admin_kp.sign(&env, &payload.to_array());
468
+
469
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
470
+ let dummy_calls: Vec<Call> =
471
+ vec![&env, Call { to: setup.contract_id.clone(), func: Symbol::new(&env, "noop"), args: Vec::new(&env) }];
472
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &dummy_calls);
473
+ let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
474
+
475
+ let tx_auth = TransactionAuthData {
476
+ vid: VID,
477
+ expiration,
478
+ signatures: vec![&env, sig],
479
+ sender: Sender::Admin(public_key, signature),
480
+ };
481
+
482
+ let res = env.try_invoke_contract_check_auth::<DvnError>(
483
+ &setup.contract_id,
484
+ &payload,
485
+ tx_auth.into_val(&env),
486
+ &auth_contexts,
487
+ );
488
+
489
+ // Should fail with NonContractInvoke error
490
+ assert_eq!(res, Err(Ok(DvnError::NonContractInvoke)));
491
+ }
492
+
493
+ #[test]
494
+ fn test_check_auth_rejects_empty_contexts() {
373
495
  extern crate std;
374
496
  let admin_kp = Ed25519KeyPair::generate();
375
497
  let admin_bytes = admin_kp.public_key_bytes();
@@ -379,27 +501,68 @@ fn test_check_auth_invalid_signature_payload_fails() {
379
501
  let expiration = env.ledger().timestamp() + 1000;
380
502
  let auth_contexts: Vec<Context> = Vec::new(&env);
381
503
 
382
- let invocation = auth_contexts.clone().to_xdr(&env);
383
- // Compute the correct payload so admin signature is valid
384
- let preimage = build_preimage(&env, AUTH_NONCE, AUTH_EXPIRY_LEDGER, &invocation);
385
- let payload = compute_signature_payload(&env, &preimage);
504
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
386
505
  let public_key = admin_kp.public_key(&env);
387
506
  let signature = admin_kp.sign(&env, &payload.to_array());
388
507
 
389
- let hash = compute_call_hash(&env, VID, expiration, &invocation);
508
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
509
+ let dummy_calls: Vec<Call> =
510
+ vec![&env, Call { to: setup.contract_id.clone(), func: Symbol::new(&env, "noop"), args: Vec::new(&env) }];
511
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &dummy_calls);
390
512
  let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
391
513
 
392
- // Tamper with preimage so it no longer matches the signature_payload
393
- let mut tampered_invocation = Bytes::new(&env);
394
- tampered_invocation.extend_from_array(&[0u8; 32]);
395
- let tampered_preimage = build_preimage(&env, AUTH_NONCE, AUTH_EXPIRY_LEDGER, &tampered_invocation);
514
+ let tx_auth = TransactionAuthData {
515
+ vid: VID,
516
+ expiration,
517
+ signatures: vec![&env, sig],
518
+ sender: Sender::Admin(public_key, signature),
519
+ };
520
+
521
+ let res = env.try_invoke_contract_check_auth::<DvnError>(
522
+ &setup.contract_id,
523
+ &payload,
524
+ tx_auth.into_val(&env),
525
+ &auth_contexts,
526
+ );
527
+
528
+ assert_eq!(res, Err(Ok(DvnError::InvalidAuthContext)));
529
+ }
530
+
531
+ #[test]
532
+ fn test_check_auth_rejects_external_target() {
533
+ extern crate std;
534
+ let admin_kp = Ed25519KeyPair::generate();
535
+ let admin_bytes = admin_kp.public_key_bytes();
536
+
537
+ let setup = TestSetup::with_admin_bytes(1, std::vec![admin_bytes]);
538
+ let env = setup.env.clone();
539
+ let expiration = env.ledger().timestamp() + 1000;
540
+
541
+ // Create a context targeting a different contract
542
+ let other_setup = TestSetup::new(1);
543
+ let other_contract = other_setup.contract_id;
544
+ let external_context = Context::Contract(ContractContext {
545
+ contract: other_contract,
546
+ fn_name: Symbol::new(&env, "some_fn"),
547
+ args: Vec::new(&env),
548
+ });
549
+ let auth_contexts: Vec<Context> = vec![&env, external_context];
550
+
551
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
552
+ let public_key = admin_kp.public_key(&env);
553
+ let signature = admin_kp.sign(&env, &payload.to_array());
554
+
555
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
556
+ let dummy_calls: Vec<Call> =
557
+ vec![&env, Call { to: setup.contract_id.clone(), func: Symbol::new(&env, "noop"), args: Vec::new(&env) }];
558
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &dummy_calls);
559
+ let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
396
560
 
397
561
  let tx_auth = TransactionAuthData {
398
562
  vid: VID,
399
563
  expiration,
400
564
  signatures: vec![&env, sig],
401
565
  sender: Sender::Admin(public_key, signature),
402
- preimage: tampered_preimage,
403
566
  };
404
567
 
405
568
  let res = env.try_invoke_contract_check_auth::<DvnError>(
@@ -409,5 +572,291 @@ fn test_check_auth_invalid_signature_payload_fails() {
409
572
  &auth_contexts,
410
573
  );
411
574
 
412
- assert_eq!(res, Err(Ok(DvnError::InvalidPreimage)));
575
+ assert_eq!(res, Err(Ok(DvnError::InvalidAuthContext)));
576
+ }
577
+
578
+ // ============================================================================
579
+ // set_upgrader Tests
580
+ // ============================================================================
581
+
582
+ #[test]
583
+ fn test_set_upgrader_requires_admin() {
584
+ extern crate std;
585
+ use crate::Sender;
586
+
587
+ let setup = TestSetup::new(1);
588
+ let env = setup.env.clone();
589
+ let expiration = env.ledger().timestamp() + 1000;
590
+
591
+ let set_upgrader_context = Context::Contract(ContractContext {
592
+ contract: setup.contract_id.clone(),
593
+ fn_name: Symbol::new(&env, "set_upgrader"),
594
+ args: Vec::new(&env),
595
+ });
596
+ let auth_contexts: Vec<Context> = vec![&env, set_upgrader_context];
597
+
598
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
599
+
600
+ let calls: Vec<Call> = vec![
601
+ &env,
602
+ Call {
603
+ to: setup.contract_id.clone(),
604
+ func: Symbol::new(&env, "set_upgrader"),
605
+ args: Vec::new(&env),
606
+ },
607
+ ];
608
+
609
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
610
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &calls);
611
+ let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
612
+
613
+ let tx_auth = TransactionAuthData {
614
+ vid: VID,
615
+ expiration,
616
+ signatures: vec![&env, sig],
617
+ sender: Sender::None,
618
+ };
619
+
620
+ let res = env.try_invoke_contract_check_auth::<DvnError>(
621
+ &setup.contract_id,
622
+ &payload,
623
+ tx_auth.into_val(&env),
624
+ &auth_contexts,
625
+ );
626
+
627
+ assert_eq!(res, Err(Ok(DvnError::OnlyAdmin)));
628
+ }
629
+
630
+ // ============================================================================
631
+ // Upgrade Path Tests
632
+ // ============================================================================
633
+
634
+ #[test]
635
+ fn test_check_auth_upgrade_success() {
636
+ extern crate std;
637
+
638
+ let admin_kp = Ed25519KeyPair::generate();
639
+ let admin_bytes = admin_kp.public_key_bytes();
640
+
641
+ let setup = TestSetup::with_admin_bytes(1, std::vec![admin_bytes]);
642
+ let env = setup.env.clone();
643
+ let expiration = env.ledger().timestamp() + 1000;
644
+
645
+ let upgrader_id = Address::generate(&env);
646
+ env.as_contract(&setup.contract_id, || {
647
+ crate::storage::DvnStorage::set_upgrader(&env, &upgrader_id);
648
+ });
649
+
650
+ let (auth_contexts, calls) = make_upgrade_contexts(&env, &setup.contract_id, &upgrader_id);
651
+
652
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
653
+ let public_key = admin_kp.public_key(&env);
654
+ let signature = admin_kp.sign(&env, &payload.to_array());
655
+
656
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
657
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &calls);
658
+ let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
659
+
660
+ let tx_auth = TransactionAuthData {
661
+ vid: VID,
662
+ expiration,
663
+ signatures: vec![&env, sig],
664
+ sender: Sender::Admin(public_key, signature),
665
+ };
666
+
667
+ let res = env.try_invoke_contract_check_auth::<DvnError>(
668
+ &setup.contract_id,
669
+ &payload,
670
+ tx_auth.into_val(&env),
671
+ &auth_contexts,
672
+ );
673
+
674
+ assert!(res.is_ok(), "Expected success for upgrade, got {:?}", res);
675
+ }
676
+
677
+ #[test]
678
+ fn test_check_auth_upgrade_no_upgrader_set() {
679
+ extern crate std;
680
+
681
+ let setup = TestSetup::new(1);
682
+ let env = setup.env.clone();
683
+ let expiration = env.ledger().timestamp() + 1000;
684
+
685
+ // Don't register any upgrader
686
+ let fake_upgrader = TestSetup::new(1).contract_id;
687
+ let (auth_contexts, calls) = make_upgrade_contexts(&env, &setup.contract_id, &fake_upgrader);
688
+
689
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
690
+
691
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
692
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &calls);
693
+ let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
694
+
695
+ let tx_auth = TransactionAuthData {
696
+ vid: VID,
697
+ expiration,
698
+ signatures: vec![&env, sig],
699
+ sender: Sender::None,
700
+ };
701
+
702
+ let res = env.try_invoke_contract_check_auth::<DvnError>(
703
+ &setup.contract_id,
704
+ &payload,
705
+ tx_auth.into_val(&env),
706
+ &auth_contexts,
707
+ );
708
+
709
+ assert_eq!(res, Err(Ok(DvnError::UpgraderNotSet)));
710
+ }
711
+
712
+ #[test]
713
+ fn test_check_auth_upgrade_wrong_upgrader() {
714
+ extern crate std;
715
+
716
+ let setup = TestSetup::new(1);
717
+ let env = setup.env.clone();
718
+ let expiration = env.ledger().timestamp() + 1000;
719
+
720
+ // Register one upgrader
721
+ let registered_upgrader = Address::generate(&env);
722
+ env.as_contract(&setup.contract_id, || {
723
+ crate::storage::DvnStorage::set_upgrader(&env, &registered_upgrader);
724
+ });
725
+
726
+ // But use a different upgrader in the auth contexts
727
+ let wrong_upgrader = Address::generate(&env);
728
+ let (auth_contexts, calls) = make_upgrade_contexts(&env, &setup.contract_id, &wrong_upgrader);
729
+
730
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
731
+
732
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
733
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &calls);
734
+ let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
735
+
736
+ let tx_auth = TransactionAuthData {
737
+ vid: VID,
738
+ expiration,
739
+ signatures: vec![&env, sig],
740
+ sender: Sender::None,
741
+ };
742
+
743
+ let res = env.try_invoke_contract_check_auth::<DvnError>(
744
+ &setup.contract_id,
745
+ &payload,
746
+ tx_auth.into_val(&env),
747
+ &auth_contexts,
748
+ );
749
+
750
+ assert_eq!(res, Err(Ok(DvnError::InvalidUpgradeContext)));
751
+ }
752
+
753
+ #[test]
754
+ fn test_check_auth_upgrade_missing_migrate() {
755
+ extern crate std;
756
+
757
+ let setup = TestSetup::new(1);
758
+ let env = setup.env.clone();
759
+ let expiration = env.ledger().timestamp() + 1000;
760
+
761
+ let upgrader_id = TestSetup::new(1).contract_id;
762
+ env.as_contract(&setup.contract_id, || {
763
+ crate::storage::DvnStorage::set_upgrader(&env, &upgrader_id);
764
+ });
765
+
766
+ // Only 2 contexts: upgrader + upgrade (missing migrate) → InvalidAuthContext (len != 1 and != 3)
767
+ let upgrader_ctx = Context::Contract(ContractContext {
768
+ contract: upgrader_id.clone(),
769
+ fn_name: Symbol::new(&env, "upgrade_and_migrate"),
770
+ args: Vec::new(&env),
771
+ });
772
+ let upgrade_ctx = Context::Contract(ContractContext {
773
+ contract: setup.contract_id.clone(),
774
+ fn_name: Symbol::new(&env, "upgrade"),
775
+ args: Vec::new(&env),
776
+ });
777
+ let auth_contexts: Vec<Context> = vec![&env, upgrader_ctx, upgrade_ctx];
778
+
779
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
780
+ let dummy_calls: Vec<Call> =
781
+ vec![&env, Call { to: setup.contract_id.clone(), func: Symbol::new(&env, "noop"), args: Vec::new(&env) }];
782
+
783
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
784
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &dummy_calls);
785
+ let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
786
+
787
+ let tx_auth = TransactionAuthData {
788
+ vid: VID,
789
+ expiration,
790
+ signatures: vec![&env, sig],
791
+ sender: Sender::None,
792
+ };
793
+
794
+ let res = env.try_invoke_contract_check_auth::<DvnError>(
795
+ &setup.contract_id,
796
+ &payload,
797
+ tx_auth.into_val(&env),
798
+ &auth_contexts,
799
+ );
800
+
801
+ // 2 contexts is neither 1 (single-call) nor 3 (upgrade), so InvalidAuthContext
802
+ assert_eq!(res, Err(Ok(DvnError::InvalidAuthContext)));
803
+ }
804
+
805
+ #[test]
806
+ fn test_check_auth_upgrade_replay_protection() {
807
+ extern crate std;
808
+
809
+ let admin_kp = Ed25519KeyPair::generate();
810
+ let admin_bytes = admin_kp.public_key_bytes();
811
+
812
+ let setup = TestSetup::with_admin_bytes(1, std::vec![admin_bytes]);
813
+ let env = setup.env.clone();
814
+ let expiration = env.ledger().timestamp() + 1000;
815
+
816
+ let upgrader_id = Address::generate(&env);
817
+ env.as_contract(&setup.contract_id, || {
818
+ crate::storage::DvnStorage::set_upgrader(&env, &upgrader_id);
819
+ });
820
+
821
+ let (auth_contexts, calls) = make_upgrade_contexts(&env, &setup.contract_id, &upgrader_id);
822
+
823
+ let payload = BytesN::from_array(&env, &[0u8; 32]);
824
+ let public_key = admin_kp.public_key(&env);
825
+ let signature = admin_kp.sign(&env, &payload.to_array());
826
+
827
+ let dvn_client = LzDVNClient::new(&env, &setup.contract_id);
828
+ let hash = dvn_client.hash_call_data(&VID, &expiration, &calls);
829
+ let sig = setup.key_pairs[0].sign_bytes(&env, &hash);
830
+
831
+ let tx_auth = TransactionAuthData {
832
+ vid: VID,
833
+ expiration,
834
+ signatures: vec![&env, sig.clone()],
835
+ sender: Sender::Admin(public_key.clone(), signature.clone()),
836
+ };
837
+
838
+ let res = env.try_invoke_contract_check_auth::<DvnError>(
839
+ &setup.contract_id,
840
+ &payload,
841
+ tx_auth.into_val(&env),
842
+ &auth_contexts,
843
+ );
844
+ assert!(res.is_ok());
845
+
846
+ // Second attempt with same data should fail
847
+ let tx_auth2 = TransactionAuthData {
848
+ vid: VID,
849
+ expiration,
850
+ signatures: vec![&env, sig],
851
+ sender: Sender::Admin(public_key, signature),
852
+ };
853
+
854
+ let res2 = env.try_invoke_contract_check_auth::<DvnError>(
855
+ &setup.contract_id,
856
+ &payload,
857
+ tx_auth2.into_val(&env),
858
+ &auth_contexts,
859
+ );
860
+
861
+ assert_eq!(res2, Err(Ok(DvnError::HashAlreadyUsed)));
413
862
  }