@lfdecentralizedtrust/zeto-contracts 0.2.2 → 0.5.1

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