@lfdecentralizedtrust/zeto-contracts 0.2.1 → 0.5.0

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 (155) hide show
  1. package/README.md +70 -162
  2. package/contracts/factory.sol +30 -34
  3. package/contracts/factory_upgradeable.sol +160 -0
  4. package/contracts/lib/common/util.sol +40 -0
  5. package/contracts/lib/interfaces/ILockableCapability.sol +318 -0
  6. package/contracts/lib/interfaces/{izeto.sol → IZeto.sol} +13 -1
  7. package/contracts/lib/interfaces/{izeto_initializable.sol → IZetoInitializable.sol} +14 -12
  8. package/contracts/lib/interfaces/{izeto_kyc.sol → IZetoKyc.sol} +3 -3
  9. package/contracts/lib/interfaces/IZetoLockableCapability.sol +237 -0
  10. package/contracts/lib/interfaces/IZetoStorage.sol +106 -0
  11. package/contracts/lib/interfaces/{izeto_lockable.sol → IZetoVerifier.sol} +8 -21
  12. package/contracts/lib/registry.sol +77 -27
  13. package/contracts/lib/storage/base.sol +210 -0
  14. package/contracts/lib/storage/nullifier.sol +166 -0
  15. package/contracts/lib/zeto_common.sol +277 -28
  16. package/contracts/lib/zeto_fungible.sol +451 -33
  17. package/contracts/lib/zeto_fungible_base.sol +69 -0
  18. package/contracts/lib/zeto_fungible_burn.sol +83 -53
  19. package/contracts/lib/zeto_fungible_burn_nullifier.sol +115 -72
  20. package/contracts/lib/zeto_fungible_nullifier.sol +167 -0
  21. package/contracts/lib/zeto_lockable.sol +613 -0
  22. package/contracts/lib/zeto_non_fungible.sol +236 -0
  23. package/contracts/lib/zeto_non_fungible_base.sol +61 -0
  24. package/contracts/lib/zeto_non_fungible_nullifier.sol +61 -0
  25. package/contracts/test/escrow1.sol +90 -34
  26. package/contracts/test/escrow2.sol +64 -29
  27. package/contracts/test/qurrency.sol +10 -2
  28. package/contracts/test/smt.sol +8 -3
  29. package/contracts/test/tendecimals.sol +14 -12
  30. package/contracts/verifiers/impl/anon.sol +189 -0
  31. package/contracts/verifiers/impl/anon_batch.sol +301 -0
  32. package/contracts/verifiers/impl/anon_enc.sol +266 -0
  33. package/contracts/verifiers/impl/anon_enc_batch.sol +602 -0
  34. package/contracts/verifiers/impl/anon_enc_nullifier.sol +287 -0
  35. package/contracts/verifiers/impl/anon_enc_nullifier_batch.sol +679 -0
  36. package/contracts/verifiers/impl/anon_enc_nullifier_kyc.sol +294 -0
  37. package/contracts/verifiers/impl/anon_enc_nullifier_kyc_batch.sol +686 -0
  38. package/contracts/verifiers/impl/anon_enc_nullifier_non_repudiation.sol +413 -0
  39. package/contracts/verifiers/impl/anon_enc_nullifier_non_repudiation_batch.sol +1141 -0
  40. package/contracts/verifiers/impl/anon_nullifier_kyc_transfer.sol +217 -0
  41. package/contracts/verifiers/impl/anon_nullifier_kyc_transferLocked.sol +196 -0
  42. package/contracts/verifiers/impl/anon_nullifier_kyc_transferLocked_batch.sol +308 -0
  43. package/contracts/verifiers/impl/anon_nullifier_kyc_transfer_batch.sol +385 -0
  44. package/contracts/verifiers/impl/anon_nullifier_qurrency_transfer.sol +497 -0
  45. package/contracts/verifiers/impl/anon_nullifier_qurrency_transfer_batch.sol +1001 -0
  46. package/contracts/verifiers/{verifier_anon_nullifier_transferLocked.sol → impl/anon_nullifier_transfer.sol} +12 -19
  47. package/contracts/verifiers/{verifier_anon_nullifier_transferLocked_batch.sol → impl/anon_nullifier_transfer_batch.sol} +44 -51
  48. package/contracts/verifiers/impl/burn.sol +182 -0
  49. package/contracts/verifiers/impl/burn_batch.sol +238 -0
  50. package/contracts/verifiers/impl/burn_nullifier.sol +203 -0
  51. package/contracts/verifiers/impl/burn_nullifier_batch.sol +315 -0
  52. package/contracts/verifiers/impl/deposit.sol +182 -0
  53. package/contracts/verifiers/impl/deposit_kyc.sol +189 -0
  54. package/contracts/verifiers/impl/nf_anon.sol +175 -0
  55. package/contracts/verifiers/{verifier_nf_anon_nullifier_transferLocked.sol → impl/nf_anon_nullifier_transfer.sol} +6 -13
  56. package/contracts/verifiers/impl/withdraw.sol +189 -0
  57. package/contracts/verifiers/impl/withdraw_batch.sol +245 -0
  58. package/contracts/verifiers/impl/withdraw_nullifier.sol +210 -0
  59. package/contracts/verifiers/impl/withdraw_nullifier_batch.sol +322 -0
  60. package/contracts/verifiers/verifier_anon.sol +31 -186
  61. package/contracts/verifiers/verifier_anon_batch.sol +31 -298
  62. package/contracts/verifiers/verifier_anon_enc.sol +31 -263
  63. package/contracts/verifiers/verifier_anon_enc_batch.sol +31 -599
  64. package/contracts/verifiers/verifier_anon_enc_nullifier.sol +31 -284
  65. package/contracts/verifiers/verifier_anon_enc_nullifier_batch.sol +33 -676
  66. package/contracts/verifiers/verifier_anon_enc_nullifier_kyc.sol +31 -291
  67. package/contracts/verifiers/verifier_anon_enc_nullifier_kyc_batch.sol +33 -683
  68. package/contracts/verifiers/verifier_anon_enc_nullifier_non_repudiation.sol +33 -410
  69. package/contracts/verifiers/verifier_anon_enc_nullifier_non_repudiation_batch.sol +33 -1138
  70. package/contracts/verifiers/verifier_anon_nullifier_kyc_transfer.sol +33 -214
  71. package/contracts/verifiers/verifier_anon_nullifier_kyc_transferLocked.sol +33 -221
  72. package/contracts/verifiers/verifier_anon_nullifier_kyc_transferLocked_batch.sol +33 -389
  73. package/contracts/verifiers/verifier_anon_nullifier_kyc_transfer_batch.sol +33 -382
  74. package/contracts/verifiers/verifier_anon_nullifier_qurrency_transfer.sol +33 -221
  75. package/contracts/verifiers/verifier_anon_nullifier_qurrency_transfer_batch.sol +33 -389
  76. package/contracts/verifiers/verifier_anon_nullifier_transfer.sol +33 -207
  77. package/contracts/verifiers/verifier_anon_nullifier_transfer_batch.sol +33 -375
  78. package/contracts/verifiers/verifier_burn.sol +31 -179
  79. package/contracts/verifiers/verifier_burn_batch.sol +31 -235
  80. package/contracts/verifiers/verifier_burn_nullifier.sol +31 -200
  81. package/contracts/verifiers/verifier_burn_nullifier_batch.sol +31 -312
  82. package/contracts/verifiers/verifier_deposit.sol +31 -179
  83. package/contracts/verifiers/verifier_deposit_kyc.sol +34 -0
  84. package/contracts/verifiers/verifier_nf_anon.sol +31 -172
  85. package/contracts/verifiers/verifier_nf_anon_nullifier_transfer.sol +33 -179
  86. package/contracts/verifiers/verifier_withdraw.sol +31 -186
  87. package/contracts/verifiers/verifier_withdraw_batch.sol +31 -242
  88. package/contracts/verifiers/verifier_withdraw_nullifier.sol +31 -207
  89. package/contracts/verifiers/verifier_withdraw_nullifier_batch.sol +33 -319
  90. package/contracts/zeto_anon.sol +77 -231
  91. package/contracts/zeto_anon_burnable.sol +56 -12
  92. package/contracts/zeto_anon_enc.sol +93 -190
  93. package/contracts/zeto_anon_enc_nullifier.sol +249 -195
  94. package/contracts/zeto_anon_enc_nullifier_kyc.sol +51 -232
  95. package/contracts/zeto_anon_enc_nullifier_non_repudiation.sol +231 -238
  96. package/contracts/zeto_anon_nullifier.sol +164 -298
  97. package/contracts/zeto_anon_nullifier_burnable.sol +68 -18
  98. package/contracts/zeto_anon_nullifier_kyc.sol +40 -345
  99. package/contracts/zeto_anon_nullifier_qurrency.sol +218 -360
  100. package/contracts/zeto_nf_anon.sol +55 -130
  101. package/contracts/zeto_nf_anon_nullifier.sol +90 -152
  102. package/hardhat.config.ts +9 -3
  103. package/ignition/modules/lib/deps.ts +9 -6
  104. package/ignition/modules/zeto_anon_burnable.ts +8 -7
  105. package/ignition/modules/zeto_anon_enc_nullifier.ts +33 -1
  106. package/ignition/modules/zeto_anon_enc_nullifier_kyc.ts +2 -2
  107. package/ignition/modules/zeto_anon_enc_nullifier_non_repudiation.ts +2 -1
  108. package/ignition/modules/zeto_anon_nullifier.ts +30 -5
  109. package/ignition/modules/zeto_anon_nullifier_burnable.ts +52 -17
  110. package/ignition/modules/zeto_anon_nullifier_kyc.ts +27 -12
  111. package/ignition/modules/zeto_anon_nullifier_qurrency.ts +2 -1
  112. package/ignition/modules/zeto_nf_anon_nullifier.ts +16 -12
  113. package/package.json +4 -2
  114. package/scripts/deploy_cloneable.ts +19 -10
  115. package/scripts/deploy_upgradeable.ts +9 -5
  116. package/scripts/tokens/Zeto_Anon.ts +3 -3
  117. package/scripts/tokens/Zeto_AnonBurnable.ts +1 -1
  118. package/scripts/tokens/Zeto_AnonEnc.ts +12 -3
  119. package/scripts/tokens/Zeto_AnonEncNullifier.ts +7 -3
  120. package/scripts/tokens/Zeto_AnonEncNullifierKyc.ts +1 -1
  121. package/scripts/tokens/Zeto_AnonEncNullifierNonRepudiation.ts +3 -1
  122. package/scripts/tokens/Zeto_AnonNullifier.ts +3 -1
  123. package/scripts/tokens/Zeto_AnonNullifierBurnable.ts +3 -1
  124. package/scripts/tokens/Zeto_AnonNullifierKyc.ts +1 -1
  125. package/scripts/tokens/Zeto_AnonNullifierQurrency.ts +3 -1
  126. package/scripts/tokens/Zeto_NfAnon.ts +2 -2
  127. package/scripts/tokens/Zeto_NfAnonNullifier.ts +3 -2
  128. package/test/factory.ts +46 -73
  129. package/test/factory_upgradeable.ts +311 -0
  130. package/test/lib/anon_nullifier_helpers.ts +89 -0
  131. package/test/lib/anon_zeto_helpers.ts +76 -0
  132. package/test/lib/deploy.ts +3 -2
  133. package/test/lib/utils.ts +74 -35
  134. package/test/test/escrow1.ts +185 -58
  135. package/test/test/escrow2.ts +200 -107
  136. package/test/test/qurrency.ts +13 -33
  137. package/test/usdc-shielding.ts +39 -26
  138. package/test/utils.ts +144 -21
  139. package/test/zeto_anon.ts +956 -465
  140. package/test/zeto_anon_enc.ts +881 -142
  141. package/test/zeto_anon_enc_nullifier.ts +847 -38
  142. package/test/zeto_anon_enc_nullifier_kyc.ts +120 -181
  143. package/test/zeto_anon_enc_nullifier_non_repudiation.ts +77 -46
  144. package/test/zeto_anon_nullifier.ts +1212 -953
  145. package/test/zeto_anon_nullifier_kyc.ts +674 -655
  146. package/test/zeto_anon_nullifier_qurrency.ts +452 -306
  147. package/test/zeto_nf_anon.ts +737 -137
  148. package/test/zeto_nf_anon_nullifier.ts +876 -246
  149. package/contracts/lib/zeto_base.sol +0 -293
  150. package/contracts/lib/zeto_fungible_withdraw.sol +0 -132
  151. package/contracts/lib/zeto_fungible_withdraw_nullifier.sol +0 -144
  152. package/contracts/lib/zeto_nullifier.sol +0 -334
  153. package/contracts/zkDvP.sol_ +0 -273
  154. package/test/zkDvP.ts_ +0 -455
  155. /package/contracts/lib/{common.sol → common/common.sol} +0 -0
@@ -15,7 +15,13 @@
15
15
  // limitations under the License.
16
16
 
17
17
  import { ethers, network } from "hardhat";
18
- import { ContractTransactionReceipt, Signer, BigNumberish } from "ethers";
18
+ import {
19
+ ContractTransactionReceipt,
20
+ Signer,
21
+ BigNumberish,
22
+ AbiCoder,
23
+ ZeroAddress,
24
+ } from "ethers";
19
25
  import { expect } from "chai";
20
26
  import {
21
27
  loadCircuit,
@@ -41,13 +47,22 @@ import {
41
47
  doMint,
42
48
  ZERO_UTXO,
43
49
  parseUTXOEvents,
50
+ logger,
44
51
  } from "./lib/utils";
45
52
  import {
46
53
  loadProvingKeys,
47
54
  prepareDepositProof,
48
- prepareNullifiersLockProof,
49
55
  prepareNullifierWithdrawProof,
56
+ encodeToBytesForDeposit,
57
+ encodeToBytesForWithdraw,
58
+ calculateSpendHash,
59
+ calculateCancelHash,
50
60
  } from "./utils";
61
+ process.env.SKIP_ANON_ENC_TESTS = "true";
62
+ import {
63
+ prepareProof as prepareProofForLockedEnc,
64
+ encodeToBytes as encodeToBytesForLockedEnc,
65
+ } from "./zeto_anon_enc";
51
66
  import { deployZeto } from "./lib/deploy";
52
67
  const poseidonHash = Poseidon.poseidon4;
53
68
 
@@ -65,6 +80,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
65
80
  let utxo4: UTXO;
66
81
  let utxo7: UTXO;
67
82
  let circuit: any, provingKey: any;
83
+ let circuitForLocked: any, provingKeyForLocked: any;
68
84
  let batchCircuit: any, batchProvingKey: any;
69
85
  let smtAlice: Merkletree;
70
86
  let smtBob: Merkletree;
@@ -94,6 +110,18 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
94
110
  ({ provingKeyFile: batchProvingKey } = loadProvingKeys(
95
111
  "anon_enc_nullifier_batch",
96
112
  ));
113
+ // For consuming locked UTXOs we use the non-nullifier `anon_enc`
114
+ // circuit. Rationale (mirrors {Zeto_AnonNullifier}): under the
115
+ // ILockableCapability storage, locked UTXOs live in a flat per-lock
116
+ // mapping with no SMT or nullifier history, so the locked-input proof
117
+ // has nothing to bind against on the nullifier side. The encryption
118
+ // witness is still required (receiver data availability), so we use
119
+ // `anon_enc` rather than the plain `anon` circuit. The contract's
120
+ // {constructPublicInputs(..., inputsLocked = true)} emits the
121
+ // `[ecdhPublicKey, encryptedValues, inputs, outputs, encryptionNonce]`
122
+ // layout that `Groth16Verifier_AnonEnc` expects.
123
+ circuitForLocked = await loadCircuit("anon_enc");
124
+ ({ provingKeyFile: provingKeyForLocked } = loadProvingKeys("anon_enc"));
97
125
  });
98
126
 
99
127
  it("onchain SMT root should be equal to the offchain SMT root", async function () {
@@ -164,12 +192,13 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
164
192
 
165
193
  const signerAddress = await Alice.signer.getAddress();
166
194
  const events = parseUTXOEvents(zeto, result.txResult!);
167
- expect(events[0].submitter).to.equal(signerAddress);
168
- expect(events[0].inputs).to.deep.equal(nullifiers.map((n) => n.hash));
195
+ const event = events[0];
196
+ expect(event.submitter).to.equal(signerAddress);
197
+ expect(event.inputs).to.deep.equal(nullifiers.map((n) => n.hash));
169
198
 
170
- const incomingUTXOs: any = events[0].outputs;
199
+ const incomingUTXOs: any = event.outputs;
171
200
 
172
- const ecdhPublicKey = events[0].ecdhPublicKey;
201
+ const ecdhPublicKey = event.ecdhPublicKey;
173
202
 
174
203
  // check the non-empty output hashes are correct
175
204
  for (let i = 0; i < outputUtxos.length; i++) {
@@ -179,9 +208,9 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
179
208
  ecdhPublicKey,
180
209
  );
181
210
  const plainText = poseidonDecrypt(
182
- events[0].encryptedValues.slice(4 * i, 4 * i + 4),
211
+ event.encryptedValues.slice(4 * i, 4 * i + 4),
183
212
  sharedKey,
184
- events[0].encryptionNonce,
213
+ event.encryptionNonce,
185
214
  2,
186
215
  );
187
216
  expect(plainText).to.deep.equal(
@@ -198,11 +227,6 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
198
227
  await smtBob.add(incomingUTXOs[i], incomingUTXOs[i]);
199
228
  }
200
229
 
201
- // check empty values, salt and hashes are empty
202
- for (let i = outputUtxos.length; i < 10; i++) {
203
- expect(incomingUTXOs[i]).to.equal(0);
204
- }
205
-
206
230
  // mint sufficient balance in Zeto contract address for Alice to withdraw
207
231
  const mintTx = await erc20.connect(deployer).mint(zeto, 3);
208
232
  await mintTx.wait();
@@ -255,8 +279,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
255
279
  3,
256
280
  _withdrawNullifiers,
257
281
  withdrawCommitments[0],
258
- root.bigInt(),
259
- withdrawEncodedProof,
282
+ encodeToBytesForWithdraw(root.bigInt(), withdrawEncodedProof),
260
283
  "0x",
261
284
  );
262
285
  await tx.wait();
@@ -264,7 +287,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
264
287
  // Alice checks her ERC20 balance
265
288
  const endingBalance = await erc20.balanceOf(Alice.ethAddress);
266
289
  expect(endingBalance - startingBalance).to.be.equal(3);
267
- }).timeout(60000);
290
+ }).timeout(180000);
268
291
 
269
292
  it("mint ERC20 tokens to Alice to deposit to Zeto should succeed", async function () {
270
293
  const startingBalance = await erc20.balanceOf(Alice.ethAddress);
@@ -284,7 +307,12 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
284
307
  );
285
308
  const tx2 = await zeto
286
309
  .connect(Alice.signer)
287
- .deposit(100, outputCommitments, encodedProof, "0x");
310
+ .deposit(
311
+ 100,
312
+ outputCommitments,
313
+ encodeToBytesForDeposit(encodedProof),
314
+ "0x",
315
+ );
288
316
  await tx2.wait();
289
317
 
290
318
  await smtAlice.add(utxo100.hash, utxo100.hash);
@@ -359,20 +387,21 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
359
387
  // Bob parses the UTXOs from the onchain event
360
388
  const signerAddress = await Alice.signer.getAddress();
361
389
  const events = parseUTXOEvents(zeto, result2.txResult!);
362
- expect(events[0].submitter).to.equal(signerAddress);
363
- expect(events[0].inputs).to.deep.equal([nullifier1.hash, nullifier2.hash]);
364
- expect(events[0].outputs).to.deep.equal([_utxo3.hash, utxo4.hash]);
365
- await smtBob.add(events[0].outputs[0], events[0].outputs[0]);
366
- await smtBob.add(events[0].outputs[1], events[0].outputs[1]);
367
-
368
- const ecdhPublicKey = events[0].ecdhPublicKey;
390
+ const event = events[0];
391
+ expect(event.submitter).to.equal(signerAddress);
392
+ expect(event.inputs).to.deep.equal([nullifier1.hash, nullifier2.hash]);
393
+ expect(event.outputs).to.deep.equal([_utxo3.hash, utxo4.hash]);
394
+ await smtBob.add(event.outputs[0], event.outputs[0]);
395
+ await smtBob.add(event.outputs[1], event.outputs[1]);
396
+
397
+ const ecdhPublicKey = event.ecdhPublicKey;
369
398
  // Bob reconstructs the shared key using his private key and ephemeral public key
370
399
 
371
400
  const sharedKey = genEcdhSharedKey(Bob.babyJubPrivateKey, ecdhPublicKey);
372
401
  const plainText = poseidonDecrypt(
373
- events[0].encryptedValues.slice(0, 4),
402
+ event.encryptedValues.slice(0, 4),
374
403
  sharedKey,
375
- events[0].encryptionNonce,
404
+ event.encryptionNonce,
376
405
  2,
377
406
  );
378
407
  expect(plainText).to.deep.equal(result2.expectedPlainText.slice(0, 2));
@@ -415,8 +444,9 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
415
444
 
416
445
  // Alice gets the new UTXOs from the onchain event and keeps the local SMT in sync
417
446
  const events = parseUTXOEvents(zeto, result.txResult!);
418
- await smtAlice.add(events[0].outputs[0], events[0].outputs[0]);
419
- await smtAlice.add(events[0].outputs[1], events[0].outputs[1]);
447
+ const event = events[0];
448
+ await smtAlice.add(event.outputs[0], event.outputs[0]);
449
+ await smtAlice.add(event.outputs[1], event.outputs[1]);
420
450
  }).timeout(600000);
421
451
 
422
452
  it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () {
@@ -457,8 +487,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
457
487
  80,
458
488
  nullifiers,
459
489
  outputCommitments[0],
460
- root.bigInt(),
461
- encodedProof,
490
+ encodeToBytesForWithdraw(root.bigInt(), encodedProof),
462
491
  "0x",
463
492
  );
464
493
  await tx.wait();
@@ -473,7 +502,767 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
473
502
  expect(endingBalance - startingBalance).to.be.equal(80);
474
503
  });
475
504
 
476
- // describe("lockStates() tests", function () {
505
+ describe("ILockableCapability tests", function () {
506
+ // ABI fragments for the ZetoLockableCapability *Args payloads.
507
+ // Identical to the layout used by every other Zeto fungible token —
508
+ // only the contents of the opaque `proof` blob differ between tokens.
509
+ const CREATE_ARGS_ABI =
510
+ "tuple(bytes32 txId, uint256[] inputs, uint256[] outputs, uint256[] lockedOutputs, bytes proof)";
511
+ const UPDATE_ARGS_ABI = "tuple(bytes32 txId)";
512
+ const DELEGATE_ARGS_ABI = "tuple(bytes32 txId)";
513
+ const SPEND_ARGS_ABI =
514
+ "tuple(bytes32 txId, uint256[] lockedOutputs, uint256[] outputs, bytes proof, bytes data)";
515
+
516
+ function encodeCreateArgs(args: {
517
+ txId: string;
518
+ inputs: BigNumberish[];
519
+ outputs: BigNumberish[];
520
+ lockedOutputs: BigNumberish[];
521
+ proof: string;
522
+ }) {
523
+ return new AbiCoder().encode([CREATE_ARGS_ABI], [args]);
524
+ }
525
+
526
+ function encodeUpdateArgs(txId: string) {
527
+ return new AbiCoder().encode([UPDATE_ARGS_ABI], [{ txId }]);
528
+ }
529
+
530
+ function encodeDelegateArgs(txId: string) {
531
+ return new AbiCoder().encode([DELEGATE_ARGS_ABI], [{ txId }]);
532
+ }
533
+
534
+ function encodeSpendArgs(args: {
535
+ txId: string;
536
+ lockedOutputs: BigNumberish[];
537
+ outputs: BigNumberish[];
538
+ proof: string;
539
+ data: string;
540
+ }) {
541
+ return new AbiCoder().encode([SPEND_ARGS_ABI], [args]);
542
+ }
543
+
544
+ function randomBytes32(): string {
545
+ return ethers.hexlify(ethers.randomBytes(32));
546
+ }
547
+
548
+ // prepareCreateLockProofBytes generates an `anon_enc_nullifier` proof
549
+ // for the createLock transition `[sourceUtxo, ZERO] -> [lockedUtxo,
550
+ // ZERO]`. The createLock path goes through
551
+ // `constructPublicInputs(..., inputsLocked = false)` so we use the
552
+ // standard nullifier-aware encryption circuit (same as a regular
553
+ // unlocked transfer).
554
+ async function prepareCreateLockProofBytes(
555
+ signer: User,
556
+ inputs: UTXO[],
557
+ nullifiers: UTXO[],
558
+ lockedOutputs: UTXO[],
559
+ root: bigint,
560
+ merkleProofs: BigInt[][],
561
+ owners: User[],
562
+ ): Promise<string> {
563
+ const ephemeralKeypair = genKeypair();
564
+ const result = await prepareProof(
565
+ signer,
566
+ inputs,
567
+ nullifiers,
568
+ lockedOutputs,
569
+ root as unknown as BigInt,
570
+ merkleProofs,
571
+ owners,
572
+ ephemeralKeypair.privKey,
573
+ );
574
+ return encodeToBytes(
575
+ root,
576
+ result.encryptionNonce,
577
+ ephemeralKeypair.pubKey,
578
+ result.encryptedValues,
579
+ result.encodedProof,
580
+ );
581
+ }
582
+
583
+ // prepareLockedSpendProofBytes generates a non-nullifier `anon_enc`
584
+ // proof for a spendLock / cancelLock settlement. The contract's
585
+ // locked-input verifier slot is wired to `Groth16Verifier_AnonEnc`,
586
+ // so we go through the same code path that {Zeto_AnonEnc} uses for
587
+ // its regular transfers — but wrap the result in the 5-tuple
588
+ // `(root, encryptionNonce, ecdhPublicKey, encryptedValues, proof)`
589
+ // that {Zeto_AnonEncNullifier.decodeProof_EncNullifier} expects. The
590
+ // `root` field is decoded but ignored when `inputsLocked = true`, so
591
+ // we pass 0n.
592
+ async function prepareLockedSpendProofBytes(
593
+ signer: User,
594
+ lockedInputs: UTXO[],
595
+ outputs: UTXO[],
596
+ owners: User[],
597
+ ): Promise<string> {
598
+ const ephemeralKeypair = genKeypair();
599
+ const result = await prepareProofForLockedEnc(
600
+ circuitForLocked,
601
+ provingKeyForLocked,
602
+ signer,
603
+ lockedInputs,
604
+ outputs,
605
+ owners,
606
+ ephemeralKeypair.privKey,
607
+ );
608
+ return encodeToBytes(
609
+ 0n,
610
+ result.encryptionNonce,
611
+ ephemeralKeypair.pubKey,
612
+ result.encryptedValues,
613
+ result.encodedProof,
614
+ );
615
+ }
616
+
617
+ describe("createLock -> updateLock -> delegateLock -> spendLock flow", function () {
618
+ let bobUtxo1: UTXO;
619
+ let aliceUtxo1: UTXO;
620
+ let lockedUtxo1: UTXO;
621
+ let lockId: string;
622
+ let outputUtxo1: UTXO;
623
+ let outputUtxo2: UTXO;
624
+ let unlockHash: string;
625
+
626
+ before(async function () {
627
+ bobUtxo1 = newUTXO(100, Bob);
628
+ await doMint(zeto, deployer, [bobUtxo1]);
629
+ await smtAlice.add(bobUtxo1.hash, bobUtxo1.hash);
630
+ await smtBob.add(bobUtxo1.hash, bobUtxo1.hash);
631
+
632
+ aliceUtxo1 = newUTXO(100, Alice);
633
+ await doMint(zeto, deployer, [aliceUtxo1]);
634
+ await smtAlice.add(aliceUtxo1.hash, aliceUtxo1.hash);
635
+ await smtBob.add(aliceUtxo1.hash, aliceUtxo1.hash);
636
+ });
637
+
638
+ it("createLock() with deterministic lockId computed from txId", async function () {
639
+ const nullifier1 = newNullifier(bobUtxo1, Bob);
640
+ lockedUtxo1 = newUTXO(bobUtxo1.value!, Bob);
641
+ const root = await smtBob.root();
642
+ const p1 = await smtBob.generateCircomVerifierProof(
643
+ bobUtxo1.hash,
644
+ root,
645
+ );
646
+ const p2 = await smtBob.generateCircomVerifierProof(0n, root);
647
+ const merkleProofs = [
648
+ p1.siblings.map((s) => s.bigInt()),
649
+ p2.siblings.map((s) => s.bigInt()),
650
+ ];
651
+ const proofBytes = await prepareCreateLockProofBytes(
652
+ Bob,
653
+ [bobUtxo1, ZERO_UTXO],
654
+ [nullifier1, ZERO_UTXO],
655
+ [lockedUtxo1, ZERO_UTXO],
656
+ root.bigInt(),
657
+ merkleProofs,
658
+ [Bob, Bob],
659
+ );
660
+
661
+ const txId = randomBytes32();
662
+ const createArgs = encodeCreateArgs({
663
+ txId,
664
+ inputs: [nullifier1.hash],
665
+ outputs: [],
666
+ lockedOutputs: [lockedUtxo1.hash],
667
+ proof: proofBytes,
668
+ });
669
+
670
+ const predicted = await zeto
671
+ .connect(Bob.signer)
672
+ .computeLockId(createArgs);
673
+ lockId = predicted;
674
+
675
+ const tx = await zeto
676
+ .connect(Bob.signer)
677
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x");
678
+ const result: ContractTransactionReceipt | null = await tx.wait();
679
+ logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
680
+
681
+ const created = result!.logs
682
+ .map((l) => {
683
+ try {
684
+ return zeto.interface.parseLog(l as any);
685
+ } catch (_e) {
686
+ return null;
687
+ }
688
+ })
689
+ .find((p) => p && p.name === "LockCreated");
690
+ expect(created, "LockCreated event not found").to.not.be.null;
691
+ expect(created!.args.lockId).to.equal(predicted);
692
+ expect(created!.args.owner).to.equal(Bob.ethAddress);
693
+ expect(created!.args.spender).to.equal(Bob.ethAddress);
694
+ });
695
+
696
+ it("isLockActive() and getLock() reflect the newly created lock", async function () {
697
+ expect(await zeto.isLockActive(lockId)).to.equal(true);
698
+ const info = await zeto.getLock(lockId);
699
+ expect(info.owner).to.equal(Bob.ethAddress);
700
+ expect(info.spender).to.equal(Bob.ethAddress);
701
+ expect(info.spendCommitment).to.equal(ethers.ZeroHash);
702
+ expect(info.cancelCommitment).to.equal(ethers.ZeroHash);
703
+ });
704
+
705
+ it("locked() returns true for locked UTXOs and false for unlocked or spent UTXOs", async function () {
706
+ expect(await zeto.locked(lockedUtxo1.hash)).to.deep.equal([
707
+ true,
708
+ Bob.ethAddress,
709
+ ]);
710
+ expect((await zeto.locked(aliceUtxo1.hash))[0]).to.be.false;
711
+ expect((await zeto.locked(bobUtxo1.hash))[0]).to.be.false;
712
+ });
713
+
714
+ it("updateLock() commits the spend hash while owner == spender", async function () {
715
+ outputUtxo1 = newUTXO(10, Alice);
716
+ outputUtxo2 = newUTXO(90, Bob);
717
+
718
+ unlockHash = calculateSpendHash(
719
+ [lockedUtxo1],
720
+ [],
721
+ [outputUtxo1, outputUtxo2],
722
+ "0x",
723
+ );
724
+
725
+ const tx = await zeto
726
+ .connect(Bob.signer)
727
+ .updateLock(
728
+ lockId,
729
+ encodeUpdateArgs(randomBytes32()),
730
+ unlockHash,
731
+ ethers.ZeroHash,
732
+ "0x",
733
+ );
734
+ const result: ContractTransactionReceipt | null = await tx.wait();
735
+ logger.debug(`updateLock() complete. Gas used: ${result?.gasUsed}`);
736
+
737
+ const info = await zeto.getLock(lockId);
738
+ expect(info.spendCommitment).to.equal(unlockHash);
739
+ });
740
+
741
+ it("delegateLock() transfers spending authority to Alice", async function () {
742
+ const tx = await zeto
743
+ .connect(Bob.signer)
744
+ .delegateLock(
745
+ lockId,
746
+ encodeDelegateArgs(randomBytes32()),
747
+ Alice.ethAddress,
748
+ "0x",
749
+ );
750
+ const result: ContractTransactionReceipt | null = await tx.wait();
751
+ logger.debug(`delegateLock() complete. Gas used: ${result?.gasUsed}`);
752
+
753
+ const info = await zeto.getLock(lockId);
754
+ expect(info.spender).to.equal(Alice.ethAddress);
755
+ expect(await zeto.locked(lockedUtxo1.hash)).to.deep.equal([
756
+ true,
757
+ Alice.ethAddress,
758
+ ]);
759
+ });
760
+
761
+ it("the new spender can spendLock() with the matching payload", async function () {
762
+ const settleProofBytes = await prepareLockedSpendProofBytes(
763
+ Bob,
764
+ [lockedUtxo1, ZERO_UTXO],
765
+ [outputUtxo1, outputUtxo2],
766
+ [Alice, Bob],
767
+ );
768
+
769
+ const spendArgs = encodeSpendArgs({
770
+ txId: randomBytes32(),
771
+ lockedOutputs: [],
772
+ outputs: [outputUtxo1.hash, outputUtxo2.hash],
773
+ proof: settleProofBytes,
774
+ data: "0x",
775
+ });
776
+
777
+ const tx = await zeto
778
+ .connect(Alice.signer)
779
+ .spendLock(lockId, spendArgs, "0x");
780
+ const result = await tx.wait();
781
+
782
+ const parsed = result!.logs
783
+ .map((l) => {
784
+ try {
785
+ return zeto.interface.parseLog(l as any);
786
+ } catch (_e) {
787
+ return null;
788
+ }
789
+ })
790
+ .filter((p) => p !== null) as ReadonlyArray<{
791
+ name: string;
792
+ args: any;
793
+ }>;
794
+ const lockSpent = parsed.find((p) => p.name === "LockSpent");
795
+ const zetoLockSpent = parsed.find((p) => p.name === "ZetoLockSpent");
796
+ expect(lockSpent, "LockSpent event not emitted").to.not.be.undefined;
797
+ expect(zetoLockSpent, "ZetoLockSpent event not emitted").to.not.be
798
+ .undefined;
799
+ expect(lockSpent!.args.lockId).to.equal(lockId);
800
+ expect(lockSpent!.args.spender).to.equal(Alice.ethAddress);
801
+
802
+ const outputs = zetoLockSpent!.args.outputs;
803
+ await smtAlice.add(outputs[0], outputs[0]);
804
+ await smtAlice.add(outputs[1], outputs[1]);
805
+ await smtBob.add(outputs[0], outputs[0]);
806
+ await smtBob.add(outputs[1], outputs[1]);
807
+
808
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
809
+ });
810
+
811
+ it("onchain SMT root for the unlocked UTXOs equals the offchain SMT root", async function () {
812
+ const bobRoot = await smtBob.root();
813
+ const aliceRoot = await smtAlice.root();
814
+ const onchainRoot = await zeto.getRoot();
815
+ expect(bobRoot.string()).to.equal(onchainRoot.toString());
816
+ expect(aliceRoot.string()).to.equal(onchainRoot.toString());
817
+ });
818
+ });
819
+
820
+ describe("createLock -> cancelLock flow", function () {
821
+ let bobUtxo1: UTXO;
822
+ let lockedUtxo1: UTXO;
823
+ let lockId: string;
824
+ let cancelHash: string;
825
+ let outUtxo1: UTXO;
826
+ let outUtxo2: UTXO;
827
+
828
+ before(async function () {
829
+ bobUtxo1 = newUTXO(100, Bob);
830
+ await doMint(zeto, deployer, [bobUtxo1]);
831
+ await smtAlice.add(bobUtxo1.hash, bobUtxo1.hash);
832
+ await smtBob.add(bobUtxo1.hash, bobUtxo1.hash);
833
+ });
834
+
835
+ it("Bob createLock() with a non-zero cancelCommitment", async function () {
836
+ const nullifier1 = newNullifier(bobUtxo1, Bob);
837
+ lockedUtxo1 = newUTXO(bobUtxo1.value!, Bob);
838
+ const root = await smtBob.root();
839
+ const p1 = await smtBob.generateCircomVerifierProof(
840
+ bobUtxo1.hash,
841
+ root,
842
+ );
843
+ const p2 = await smtBob.generateCircomVerifierProof(0n, root);
844
+ const merkleProofs = [
845
+ p1.siblings.map((s) => s.bigInt()),
846
+ p2.siblings.map((s) => s.bigInt()),
847
+ ];
848
+ const proofBytes = await prepareCreateLockProofBytes(
849
+ Bob,
850
+ [bobUtxo1, ZERO_UTXO],
851
+ [nullifier1, ZERO_UTXO],
852
+ [lockedUtxo1, ZERO_UTXO],
853
+ root.bigInt(),
854
+ merkleProofs,
855
+ [Bob, Bob],
856
+ );
857
+
858
+ outUtxo1 = newUTXO(10, Alice);
859
+ outUtxo2 = newUTXO(90, Bob);
860
+ cancelHash = calculateCancelHash(
861
+ [lockedUtxo1],
862
+ [],
863
+ [outUtxo1, outUtxo2],
864
+ "0x",
865
+ );
866
+
867
+ const createArgs = encodeCreateArgs({
868
+ txId: randomBytes32(),
869
+ inputs: [nullifier1.hash],
870
+ outputs: [],
871
+ lockedOutputs: [lockedUtxo1.hash],
872
+ proof: proofBytes,
873
+ });
874
+ lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
875
+ const tx = await zeto
876
+ .connect(Bob.signer)
877
+ .createLock(createArgs, ethers.ZeroHash, cancelHash, "0x");
878
+ const result: ContractTransactionReceipt | null = await tx.wait();
879
+ logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
880
+ });
881
+
882
+ it("the owner can cancelLock() to reverse the lock without delegation", async function () {
883
+ const cancelProofBytes = await prepareLockedSpendProofBytes(
884
+ Bob,
885
+ [lockedUtxo1, ZERO_UTXO],
886
+ [outUtxo1, outUtxo2],
887
+ [Alice, Bob],
888
+ );
889
+
890
+ const cancelArgs = encodeSpendArgs({
891
+ txId: randomBytes32(),
892
+ lockedOutputs: [],
893
+ outputs: [outUtxo1.hash, outUtxo2.hash],
894
+ proof: cancelProofBytes,
895
+ data: "0x",
896
+ });
897
+
898
+ const tx = await zeto
899
+ .connect(Bob.signer)
900
+ .cancelLock(lockId, cancelArgs, "0x");
901
+ const result = await tx.wait();
902
+
903
+ const parsed = result!.logs
904
+ .map((l) => {
905
+ try {
906
+ return zeto.interface.parseLog(l as any);
907
+ } catch (_e) {
908
+ return null;
909
+ }
910
+ })
911
+ .filter((p) => p !== null) as ReadonlyArray<{
912
+ name: string;
913
+ args: any;
914
+ }>;
915
+ const cancelled = parsed.find((p) => p.name === "LockCancelled");
916
+ const zetoCancelled = parsed.find(
917
+ (p) => p.name === "ZetoLockCancelled",
918
+ );
919
+ expect(cancelled, "LockCancelled event not emitted").to.not.be
920
+ .undefined;
921
+ expect(zetoCancelled, "ZetoLockCancelled event not emitted").to.not.be
922
+ .undefined;
923
+
924
+ const outputs = zetoCancelled!.args.outputs;
925
+ await smtAlice.add(outputs[0], outputs[0]);
926
+ await smtAlice.add(outputs[1], outputs[1]);
927
+ await smtBob.add(outputs[0], outputs[0]);
928
+ await smtBob.add(outputs[1], outputs[1]);
929
+
930
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
931
+ });
932
+
933
+ it("onchain SMT root for the unlocked UTXOs equals the offchain SMT root", async function () {
934
+ const bobRoot = await smtBob.root();
935
+ const aliceRoot = await smtAlice.root();
936
+ const onchainRoot = await zeto.getRoot();
937
+ expect(bobRoot.string()).to.equal(onchainRoot.toString());
938
+ expect(aliceRoot.string()).to.equal(onchainRoot.toString());
939
+ });
940
+ });
941
+
942
+ describe("spendLock with a payload that does not match the spend commitment fails", function () {
943
+ let bobUtxo1: UTXO;
944
+ let lockedUtxo1: UTXO;
945
+ let lockId: string;
946
+ let expectedHash: string;
947
+
948
+ before(async function () {
949
+ bobUtxo1 = newUTXO(100, Bob);
950
+ await doMint(zeto, deployer, [bobUtxo1]);
951
+ await smtAlice.add(bobUtxo1.hash, bobUtxo1.hash);
952
+ await smtBob.add(bobUtxo1.hash, bobUtxo1.hash);
953
+ });
954
+
955
+ it("Bob createLock() then updateLock() committing a specific spend hash", async function () {
956
+ const nullifier1 = newNullifier(bobUtxo1, Bob);
957
+ lockedUtxo1 = newUTXO(bobUtxo1.value!, Bob);
958
+ const root = await smtBob.root();
959
+ const p1 = await smtBob.generateCircomVerifierProof(
960
+ bobUtxo1.hash,
961
+ root,
962
+ );
963
+ const p2 = await smtBob.generateCircomVerifierProof(0n, root);
964
+ const merkleProofs = [
965
+ p1.siblings.map((s) => s.bigInt()),
966
+ p2.siblings.map((s) => s.bigInt()),
967
+ ];
968
+ const proofBytes = await prepareCreateLockProofBytes(
969
+ Bob,
970
+ [bobUtxo1, ZERO_UTXO],
971
+ [nullifier1, ZERO_UTXO],
972
+ [lockedUtxo1, ZERO_UTXO],
973
+ root.bigInt(),
974
+ merkleProofs,
975
+ [Bob, Bob],
976
+ );
977
+
978
+ const createArgs = encodeCreateArgs({
979
+ txId: randomBytes32(),
980
+ inputs: [nullifier1.hash],
981
+ outputs: [],
982
+ lockedOutputs: [lockedUtxo1.hash],
983
+ proof: proofBytes,
984
+ });
985
+ lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
986
+ await (
987
+ await zeto
988
+ .connect(Bob.signer)
989
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x")
990
+ ).wait();
991
+
992
+ const expectedOut1 = newUTXO(10, Alice);
993
+ const expectedOut2 = newUTXO(90, Bob);
994
+ expectedHash = calculateSpendHash(
995
+ [lockedUtxo1],
996
+ [],
997
+ [expectedOut1, expectedOut2],
998
+ "0x",
999
+ );
1000
+ await (
1001
+ await zeto
1002
+ .connect(Bob.signer)
1003
+ .updateLock(
1004
+ lockId,
1005
+ encodeUpdateArgs(randomBytes32()),
1006
+ expectedHash,
1007
+ ethers.ZeroHash,
1008
+ "0x",
1009
+ )
1010
+ ).wait();
1011
+ });
1012
+
1013
+ it("spendLock() with a different payload reverts with InvalidUnlockHash", async function () {
1014
+ if (network.name !== "hardhat") {
1015
+ this.skip();
1016
+ }
1017
+ const wrongOut1 = newUTXO(20, Alice);
1018
+ const wrongOut2 = newUTXO(80, Bob);
1019
+
1020
+ const settleProofBytes = await prepareLockedSpendProofBytes(
1021
+ Bob,
1022
+ [lockedUtxo1, ZERO_UTXO],
1023
+ [wrongOut1, wrongOut2],
1024
+ [Alice, Bob],
1025
+ );
1026
+
1027
+ const spendArgs = encodeSpendArgs({
1028
+ txId: randomBytes32(),
1029
+ lockedOutputs: [],
1030
+ outputs: [wrongOut1.hash, wrongOut2.hash],
1031
+ proof: settleProofBytes,
1032
+ data: "0x",
1033
+ });
1034
+
1035
+ const calculatedHash = calculateSpendHash(
1036
+ [lockedUtxo1],
1037
+ [],
1038
+ [wrongOut1, wrongOut2],
1039
+ "0x",
1040
+ );
1041
+
1042
+ await expect(
1043
+ zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x"),
1044
+ )
1045
+ .to.be.revertedWithCustomError(zeto, "InvalidUnlockHash")
1046
+ .withArgs(expectedHash, calculatedHash);
1047
+ });
1048
+ });
1049
+
1050
+ describe("negative cases for the lock lifecycle", function () {
1051
+ // These tests rely on hardhat-style revert decoding.
1052
+ if (network.name !== "hardhat") {
1053
+ return;
1054
+ }
1055
+
1056
+ // freshLock mints a UTXO for `owner`, locks it, and returns the
1057
+ // resulting lock metadata so each negative case can branch off
1058
+ // without polluting other test scopes. Each call advances both
1059
+ // local SMTs to keep them in sync with the on-chain unlocked-UTXO
1060
+ // tree (the createLock proof needs a valid SMT inclusion proof).
1061
+ async function freshLock(
1062
+ owner: User,
1063
+ spendCommitment: string = ethers.ZeroHash,
1064
+ cancelCommitment: string = ethers.ZeroHash,
1065
+ ): Promise<{
1066
+ lockId: string;
1067
+ sourceUtxo: UTXO;
1068
+ lockedUtxo: UTXO;
1069
+ createArgs: string;
1070
+ }> {
1071
+ const sourceUtxo = newUTXO(100, owner);
1072
+ await doMint(zeto, deployer, [sourceUtxo]);
1073
+ await smtAlice.add(sourceUtxo.hash, sourceUtxo.hash);
1074
+ await smtBob.add(sourceUtxo.hash, sourceUtxo.hash);
1075
+
1076
+ const nullifier = newNullifier(sourceUtxo, owner);
1077
+ const lockedUtxo = newUTXO(sourceUtxo.value!, owner);
1078
+ const root = await smtBob.root();
1079
+ const p1 = await smtBob.generateCircomVerifierProof(
1080
+ sourceUtxo.hash,
1081
+ root,
1082
+ );
1083
+ const p2 = await smtBob.generateCircomVerifierProof(0n, root);
1084
+ const merkleProofs = [
1085
+ p1.siblings.map((s) => s.bigInt()),
1086
+ p2.siblings.map((s) => s.bigInt()),
1087
+ ];
1088
+ const proofBytes = await prepareCreateLockProofBytes(
1089
+ owner,
1090
+ [sourceUtxo, ZERO_UTXO],
1091
+ [nullifier, ZERO_UTXO],
1092
+ [lockedUtxo, ZERO_UTXO],
1093
+ root.bigInt(),
1094
+ merkleProofs,
1095
+ [owner, owner],
1096
+ );
1097
+
1098
+ const createArgs = encodeCreateArgs({
1099
+ txId: randomBytes32(),
1100
+ inputs: [nullifier.hash],
1101
+ outputs: [],
1102
+ lockedOutputs: [lockedUtxo.hash],
1103
+ proof: proofBytes,
1104
+ });
1105
+ const lockId = await zeto
1106
+ .connect(owner.signer)
1107
+ .computeLockId(createArgs);
1108
+ await (
1109
+ await zeto
1110
+ .connect(owner.signer)
1111
+ .createLock(createArgs, spendCommitment, cancelCommitment, "0x")
1112
+ ).wait();
1113
+ return { lockId, sourceUtxo, lockedUtxo, createArgs };
1114
+ }
1115
+
1116
+ function dummySpendArgs(): string {
1117
+ return encodeSpendArgs({
1118
+ txId: randomBytes32(),
1119
+ lockedOutputs: [],
1120
+ outputs: [],
1121
+ proof: "0x",
1122
+ data: "0x",
1123
+ });
1124
+ }
1125
+
1126
+ it("createLock() with a duplicate txId from the same caller reverts with DuplicateLock", async function () {
1127
+ const { lockId, createArgs } = await freshLock(Bob);
1128
+
1129
+ await expect(
1130
+ zeto
1131
+ .connect(Bob.signer)
1132
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x"),
1133
+ )
1134
+ .to.be.revertedWithCustomError(zeto, "DuplicateLock")
1135
+ .withArgs(lockId);
1136
+ });
1137
+
1138
+ it("updateLock() by a non-owner reverts with LockUnauthorized", async function () {
1139
+ const { lockId } = await freshLock(Bob);
1140
+
1141
+ await expect(
1142
+ zeto
1143
+ .connect(Alice.signer)
1144
+ .updateLock(
1145
+ lockId,
1146
+ encodeUpdateArgs(randomBytes32()),
1147
+ ethers.ZeroHash,
1148
+ ethers.ZeroHash,
1149
+ "0x",
1150
+ ),
1151
+ )
1152
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1153
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1154
+ });
1155
+
1156
+ it("updateLock() after delegateLock() reverts with LockImmutable", async function () {
1157
+ const { lockId } = await freshLock(Bob);
1158
+
1159
+ await (
1160
+ await zeto
1161
+ .connect(Bob.signer)
1162
+ .delegateLock(
1163
+ lockId,
1164
+ encodeDelegateArgs(randomBytes32()),
1165
+ Alice.ethAddress,
1166
+ "0x",
1167
+ )
1168
+ ).wait();
1169
+
1170
+ await expect(
1171
+ zeto
1172
+ .connect(Bob.signer)
1173
+ .updateLock(
1174
+ lockId,
1175
+ encodeUpdateArgs(randomBytes32()),
1176
+ ethers.ZeroHash,
1177
+ ethers.ZeroHash,
1178
+ "0x",
1179
+ ),
1180
+ )
1181
+ .to.be.revertedWithCustomError(zeto, "LockImmutable")
1182
+ .withArgs(lockId);
1183
+ });
1184
+
1185
+ it("delegateLock() by a non-spender reverts with LockUnauthorized", async function () {
1186
+ const { lockId } = await freshLock(Bob);
1187
+
1188
+ await expect(
1189
+ zeto
1190
+ .connect(Alice.signer)
1191
+ .delegateLock(
1192
+ lockId,
1193
+ encodeDelegateArgs(randomBytes32()),
1194
+ Charlie.ethAddress,
1195
+ "0x",
1196
+ ),
1197
+ )
1198
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1199
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1200
+ });
1201
+
1202
+ it("spendLock() by a non-spender reverts with LockUnauthorized before touching the proof", async function () {
1203
+ const { lockId } = await freshLock(Bob);
1204
+
1205
+ await expect(
1206
+ zeto.connect(Alice.signer).spendLock(lockId, dummySpendArgs(), "0x"),
1207
+ )
1208
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1209
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1210
+ });
1211
+
1212
+ it("cancelLock() by a non-spender reverts with LockUnauthorized", async function () {
1213
+ const { lockId } = await freshLock(Bob);
1214
+
1215
+ await expect(
1216
+ zeto.connect(Alice.signer).cancelLock(lockId, dummySpendArgs(), "0x"),
1217
+ )
1218
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1219
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1220
+ });
1221
+
1222
+ it("after a successful spendLock(), the lock is no longer active and getLock() reverts", async function () {
1223
+ const { lockId, lockedUtxo } = await freshLock(Bob);
1224
+
1225
+ const out1 = newUTXO(10, Alice);
1226
+ const out2 = newUTXO(90, Bob);
1227
+ const settleProofBytes = await prepareLockedSpendProofBytes(
1228
+ Bob,
1229
+ [lockedUtxo, ZERO_UTXO],
1230
+ [out1, out2],
1231
+ [Alice, Bob],
1232
+ );
1233
+ const spendArgs = encodeSpendArgs({
1234
+ txId: randomBytes32(),
1235
+ lockedOutputs: [],
1236
+ outputs: [out1.hash, out2.hash],
1237
+ proof: settleProofBytes,
1238
+ data: "0x",
1239
+ });
1240
+ await (
1241
+ await zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x")
1242
+ ).wait();
1243
+ // Keep both SMTs in sync with the new unlocked outputs so
1244
+ // unrelated tests in later blocks can still reason about roots.
1245
+ await smtAlice.add(out1.hash, out1.hash);
1246
+ await smtAlice.add(out2.hash, out2.hash);
1247
+ await smtBob.add(out1.hash, out1.hash);
1248
+ await smtBob.add(out2.hash, out2.hash);
1249
+
1250
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
1251
+ await expect(zeto.getLock(lockId))
1252
+ .to.be.revertedWithCustomError(zeto, "LockNotActive")
1253
+ .withArgs(lockId);
1254
+ // Re-spending a consumed lock MUST also fail — lockActive is the
1255
+ // first modifier and emits LockNotActive before onlySpender.
1256
+ await expect(
1257
+ zeto.connect(Bob.signer).spendLock(lockId, dummySpendArgs(), "0x"),
1258
+ )
1259
+ .to.be.revertedWithCustomError(zeto, "LockNotActive")
1260
+ .withArgs(lockId);
1261
+ });
1262
+ });
1263
+ });
1264
+
1265
+ // describe("legacy lockStates() tests, kept for reference", function () {
477
1266
  // let nullifier1: any;
478
1267
  // it("lockStates() should succeed when using unlocked states", async function () {
479
1268
  // nullifier1 = newNullifier(utxo4, Alice);
@@ -651,8 +1440,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
651
1440
  10,
652
1441
  nullifiers,
653
1442
  outputCommitments[0],
654
- root.bigInt(),
655
- encodedProof,
1443
+ encodeToBytesForWithdraw(root.bigInt(), encodedProof),
656
1444
  "0x",
657
1445
  ),
658
1446
  ).rejectedWith("UTXOAlreadySpent");
@@ -970,11 +1758,13 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
970
1758
  const tx = await zeto.connect(signer.signer).transfer(
971
1759
  nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works
972
1760
  outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works
973
- root,
974
- encryptionNonce,
975
- ecdhPublicKey,
976
- encryptedValues,
977
- encodedProof,
1761
+ encodeToBytes(
1762
+ root,
1763
+ encryptionNonce,
1764
+ ecdhPublicKey,
1765
+ encryptedValues,
1766
+ encodedProof,
1767
+ ),
978
1768
  "0x",
979
1769
  );
980
1770
  const results: ContractTransactionReceipt | null = await tx.wait();
@@ -984,3 +1774,22 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
984
1774
  return results;
985
1775
  }
986
1776
  });
1777
+
1778
+ function encodeToBytes(
1779
+ root: any,
1780
+ encryptionNonce: any,
1781
+ ecdhPublicKey: any,
1782
+ encryptedValues: any,
1783
+ proof: any,
1784
+ ) {
1785
+ return new AbiCoder().encode(
1786
+ [
1787
+ "uint256 root",
1788
+ "uint256 encryptionNonce",
1789
+ "uint256[2] ecdhPublicKey",
1790
+ "uint256[] encryptedValues",
1791
+ "tuple(uint256[2] pA, uint256[2][2] pB, uint256[2] pC)",
1792
+ ],
1793
+ [root, encryptionNonce, ecdhPublicKey, encryptedValues, proof],
1794
+ );
1795
+ }