@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,
@@ -39,15 +45,17 @@ import {
39
45
  doMint,
40
46
  ZERO_UTXO,
41
47
  parseUTXOEvents,
48
+ logger,
42
49
  } from "./lib/utils";
43
50
  import {
44
51
  loadProvingKeys,
45
52
  prepareDepositProof,
46
- prepareLockProof,
47
53
  prepareWithdrawProof,
54
+ encodeToBytesForDeposit,
55
+ calculateSpendHash,
56
+ calculateCancelHash,
48
57
  } from "./utils";
49
58
  import { deployZeto } from "./lib/deploy";
50
-
51
59
  const poseidonHash = Poseidon.poseidon4;
52
60
 
53
61
  describe("Zeto based fungible token with anonymity and encryption", function () {
@@ -66,6 +74,12 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
66
74
  let batchCircuit: any, batchProvingKey: any;
67
75
 
68
76
  before(async function () {
77
+ // skip the tests if this file is being imported from another test
78
+ // module solely to access its exported `prepareProof` / `encodeToBytes`
79
+ // helpers (mirrors the SKIP_ANON_TESTS pattern in `zeto_anon.ts`).
80
+ if (process.env.SKIP_ANON_ENC_TESTS === "true") {
81
+ this.skip();
82
+ }
69
83
  if (network.name !== "hardhat") {
70
84
  // accommodate for longer block times on public networks
71
85
  this.timeout(120000);
@@ -84,6 +98,12 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
84
98
  ({ provingKeyFile: batchProvingKey } = loadProvingKeys("anon_enc_batch"));
85
99
  });
86
100
 
101
+ beforeEach(async function () {
102
+ if (process.env.SKIP_ANON_ENC_TESTS === "true") {
103
+ this.skip();
104
+ }
105
+ });
106
+
87
107
  it("(batch) mint to Alice and batch transfer 10 UTXOs honestly to Bob & Charlie then withdraw should succeed", async function () {
88
108
  // first mint the tokens for batch testing
89
109
  const inputUtxos = [];
@@ -120,9 +140,10 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
120
140
  );
121
141
 
122
142
  const events = parseUTXOEvents(zeto, result.txResult!);
123
- expect(events[0].inputs).to.deep.equal(inputUtxos.map((i) => i.hash));
124
- const incomingUTXOs: any = events[0].outputs;
125
- const ecdhPublicKey = events[0].ecdhPublicKey;
143
+ const event = events[0];
144
+ expect(event.inputs).to.deep.equal(inputUtxos.map((i) => i.hash));
145
+ const incomingUTXOs: any = event.outputs;
146
+ const ecdhPublicKey = event.ecdhPublicKey;
126
147
 
127
148
  // check the non-empty output hashes are correct
128
149
  for (let i = 0; i < outputUtxos.length; i++) {
@@ -132,9 +153,9 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
132
153
  ecdhPublicKey,
133
154
  );
134
155
  const plainText = poseidonDecrypt(
135
- events[0].encryptedValues.slice(4 * i, 4 * i + 4),
156
+ event.encryptedValues.slice(4 * i, 4 * i + 4),
136
157
  sharedKey,
137
- events[0].encryptionNonce,
158
+ event.encryptionNonce,
138
159
  2,
139
160
  );
140
161
  expect(plainText).to.deep.equal(
@@ -149,11 +170,6 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
149
170
  expect(incomingUTXOs[i]).to.equal(hash);
150
171
  }
151
172
 
152
- // check empty values, salt and hashes are empty
153
- for (let i = outputUtxos.length; i < 10; i++) {
154
- expect(incomingUTXOs[i]).to.equal(0);
155
- }
156
-
157
173
  // mint sufficient balance in Zeto contract address for Alice to withdraw
158
174
  const mintTx = await erc20.connect(deployer).mint(zeto, 3);
159
175
  await mintTx.wait();
@@ -173,7 +189,13 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
173
189
  // Alice withdraws her UTXOs to ERC20 tokens
174
190
  const tx = await zeto
175
191
  .connect(Alice.signer)
176
- .withdraw(3, inputCommitments, outputCommitments[0], encodedProof, "0x");
192
+ .withdraw(
193
+ 3,
194
+ inputCommitments,
195
+ outputCommitments[0],
196
+ encodeToBytesForWithdraw(encodedProof),
197
+ "0x",
198
+ );
177
199
  await tx.wait();
178
200
 
179
201
  // Alice checks her ERC20 balance
@@ -199,7 +221,12 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
199
221
  );
200
222
  const tx2 = await zeto
201
223
  .connect(Alice.signer)
202
- .deposit(100, outputCommitments, encodedProof, "0x");
224
+ .deposit(
225
+ 100,
226
+ outputCommitments,
227
+ encodeToBytesForDeposit(encodedProof),
228
+ "0x",
229
+ );
203
230
  await tx2.wait();
204
231
  }).timeout(60000);
205
232
 
@@ -233,18 +260,19 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
233
260
  // Bob uses the information in the event to recover the incoming UTXO
234
261
  // first obtain the UTXO from the transaction event
235
262
  const events = parseUTXOEvents(zeto, result.txResult!);
236
- expect(events[0].inputs).to.deep.equal([utxo1.hash, utxo2.hash]);
237
- expect(events[0].outputs).to.deep.equal([_utxo1.hash, utxo4.hash]);
238
- const incomingUTXOs: any = events[0].outputs;
263
+ const event = events[0];
264
+ expect(event.inputs).to.deep.equal([utxo1.hash, utxo2.hash]);
265
+ expect(event.outputs).to.deep.equal([_utxo1.hash, utxo4.hash]);
266
+ const incomingUTXOs: any = event.outputs;
239
267
 
240
- const ecdhPublicKey = events[0].ecdhPublicKey;
268
+ const ecdhPublicKey = event.ecdhPublicKey;
241
269
  // Bob reconstructs the shared key using his private key and ephemeral public key
242
270
 
243
271
  const sharedKey = genEcdhSharedKey(Bob.babyJubPrivateKey, ecdhPublicKey);
244
272
  const plainText = poseidonDecrypt(
245
- events[0].encryptedValues.slice(0, 4),
273
+ event.encryptedValues.slice(0, 4),
246
274
  sharedKey,
247
- events[0].encryptionNonce,
275
+ event.encryptionNonce,
248
276
  2,
249
277
  );
250
278
  expect(plainText).to.deep.equal(result.expectedPlainText.slice(0, 2));
@@ -284,7 +312,13 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
284
312
  // Alice withdraws her UTXOs to ERC20 tokens
285
313
  const tx = await zeto
286
314
  .connect(Alice.signer)
287
- .withdraw(80, inputCommitments, outputCommitments[0], encodedProof, "0x");
315
+ .withdraw(
316
+ 80,
317
+ inputCommitments,
318
+ outputCommitments[0],
319
+ encodeToBytesForWithdraw(encodedProof),
320
+ "0x",
321
+ );
288
322
  await tx.wait();
289
323
 
290
324
  // Alice checks her ERC20 balance
@@ -318,7 +352,7 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
318
352
  10,
319
353
  inputCommitments,
320
354
  outputCommitments[0],
321
- encodedProof,
355
+ encodeToBytesForWithdraw(encodedProof),
322
356
  "0x",
323
357
  ),
324
358
  ).rejectedWith("UTXOAlreadySpent");
@@ -371,62 +405,705 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
371
405
  });
372
406
  });
373
407
 
374
- // describe("lockStates() tests", function () {
375
- // it("lockStates() should succeed when using unlocked states", async function () {
376
- // const { commitments, encodedProof } = await prepareLockProof(Alice, [utxo4, ZERO_UTXO]);
377
-
378
- // const tx = await zeto.connect(Alice.signer).lockStates(
379
- // commitments.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works
380
- // encodedProof,
381
- // Bob.ethAddress, // make Bob the delegate who can spend the state (if she has the right proof)
382
- // "0x",
383
- // );
384
- // const results = await tx.wait();
385
- // console.log(`Method transfer() complete. Gas used: ${results?.gasUsed}`);
386
- // });
387
-
388
- // it("lockStates() should fail when trying to lock as non-delegate", async function () {
389
- // if (network.name !== "hardhat") {
390
- // return;
391
- // }
392
-
393
- // // Bob is the owner of the UTXO, so he can generate the right proof
394
- // const { commitments, encodedProof } = await prepareLockProof(Alice, [utxo4, ZERO_UTXO]);
395
-
396
- // // but he's no longer the delegate (Alice is) to spend the state
397
- // await expect(zeto.connect(Alice.signer).lockStates(
398
- // commitments.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works
399
- // encodedProof,
400
- // Alice.ethAddress,
401
- // "0x",
402
- // )).rejectedWith(`UTXOAlreadyLocked(${utxo4.hash.toString()})`);
403
- // });
404
-
405
- // it("the original owner can NOT spend the locked state", async function () {
406
- // const utxo8 = newUTXO(5, Charlie);
407
- // const ephemeralKeypair = genKeypair();
408
- // await expect(doTransfer(Alice, [utxo4, ZERO_UTXO], [utxo8, ZERO_UTXO], [Charlie, Alice])).to.be.rejectedWith("UTXOAlreadyLocked");
409
- // });
410
-
411
- // it("the original owner can NOT withdraw the locked state", async function () {
412
- // const utxo8 = newUTXO(0, Alice);
413
- // const { inputCommitments, outputCommitments, encodedProof } =
414
- // await prepareWithdrawProof(Alice, [utxo4, ZERO_UTXO], utxo8);
415
-
416
- // await expect(zeto
417
- // .connect(Alice.signer)
418
- // .withdraw(5, inputCommitments, outputCommitments[0], encodedProof, "0x")).to.be.rejectedWith("UTXOAlreadyLocked");
419
- // });
420
-
421
- // it("the designated delegate can use the proper proof to spend the locked state", async function () {
422
- // const utxo8 = newUTXO(5, Charlie);
423
- // const ephemeralKeypair = genKeypair();
424
- // const { inputCommitments, outputCommitments, encodedProof, encryptedValues, encryptionNonce } = await prepareProof(Alice, [utxo4, ZERO_UTXO], [utxo8, ZERO_UTXO], [Charlie, Alice], ephemeralKeypair.privKey);
425
- // // Bob (in reality this is usually a contract that orchestrates a trade flow) can spend the locked state
426
- // // using the proof generated by the trade counterparty (Alice in this case)
427
- // await expect(sendTx(Bob, inputCommitments, outputCommitments, encryptedValues, encryptionNonce, encodedProof, ephemeralKeypair.pubKey)).to.be.fulfilled;
428
- // });
429
- // });
408
+ describe("ILockableCapability tests", function () {
409
+ // ABI fragments for the ZetoLockableCapability *Args payloads. These
410
+ // mirror the layout used in `zeto_anon.ts` / `zeto_anon_nullifier.ts`
411
+ // so that the cross-token contract surface stays uniform — the only
412
+ // thing that changes for the encryption-aware token is what goes
413
+ // inside the opaque `proof` blob (we wrap the Groth16 proof with the
414
+ // ECDH key, encryption nonce and encrypted values via
415
+ // `encodeToBytes` below).
416
+ const CREATE_ARGS_ABI =
417
+ "tuple(bytes32 txId, uint256[] inputs, uint256[] outputs, uint256[] lockedOutputs, bytes proof)";
418
+ const UPDATE_ARGS_ABI = "tuple(bytes32 txId)";
419
+ const DELEGATE_ARGS_ABI = "tuple(bytes32 txId)";
420
+ const SPEND_ARGS_ABI =
421
+ "tuple(bytes32 txId, uint256[] lockedOutputs, uint256[] outputs, bytes proof, bytes data)";
422
+
423
+ function encodeCreateArgs(args: {
424
+ txId: string;
425
+ inputs: BigNumberish[];
426
+ outputs: BigNumberish[];
427
+ lockedOutputs: BigNumberish[];
428
+ proof: string;
429
+ }) {
430
+ return new AbiCoder().encode([CREATE_ARGS_ABI], [args]);
431
+ }
432
+
433
+ function encodeUpdateArgs(txId: string) {
434
+ return new AbiCoder().encode([UPDATE_ARGS_ABI], [{ txId }]);
435
+ }
436
+
437
+ function encodeDelegateArgs(txId: string) {
438
+ return new AbiCoder().encode([DELEGATE_ARGS_ABI], [{ txId }]);
439
+ }
440
+
441
+ function encodeSpendArgs(args: {
442
+ txId: string;
443
+ lockedOutputs: BigNumberish[];
444
+ outputs: BigNumberish[];
445
+ proof: string;
446
+ data: string;
447
+ }) {
448
+ return new AbiCoder().encode([SPEND_ARGS_ABI], [args]);
449
+ }
450
+
451
+ function randomBytes32(): string {
452
+ return ethers.hexlify(ethers.randomBytes(32));
453
+ }
454
+
455
+ // prepareEncProofBytes wraps `prepareProof` to return ABI-encoded
456
+ // proof bytes ready to be slotted into createArgs / spendArgs.
457
+ // `Zeto_AnonEnc.constructPublicInputs` expects the proof tuple
458
+ // (uint256, uint256[2], uint256[], Commonlib.Proof) same layout
459
+ // as the regular `transfer` flow so the locked-input path reuses
460
+ // the same proof builder.
461
+ async function prepareEncProofBytes(
462
+ signer: User,
463
+ inputs: UTXO[],
464
+ outputs: UTXO[],
465
+ owners: User[],
466
+ ): Promise<string> {
467
+ const ephemeralKeypair = genKeypair();
468
+ const result = await prepareProof(
469
+ signer,
470
+ inputs,
471
+ outputs,
472
+ owners,
473
+ ephemeralKeypair.privKey,
474
+ );
475
+ return encodeToBytes(
476
+ result.encryptionNonce,
477
+ ephemeralKeypair.pubKey,
478
+ result.encryptedValues,
479
+ result.encodedProof,
480
+ );
481
+ }
482
+
483
+ describe("createLock -> updateLock -> delegateLock -> spendLock flow", function () {
484
+ let bobSourceUtxo: UTXO;
485
+ let lockedUtxo: UTXO;
486
+ let lockId: string;
487
+ let outUtxo1: UTXO;
488
+ let outUtxo2: UTXO;
489
+ let unlockHash: string;
490
+
491
+ before(async function () {
492
+ bobSourceUtxo = newUTXO(100, Bob);
493
+ await doMint(zeto, deployer, [bobSourceUtxo]);
494
+ });
495
+
496
+ it("createLock() with deterministic lockId computed from txId", async function () {
497
+ // The locked content is just a fresh UTXO of equal value held under Bob.
498
+ // The proof on the create path proves the standard transfer
499
+ // relationship: bobSourceUtxo -> lockedUtxo. The contract treats
500
+ // the locked output identically to a regular output for the purposes
501
+ // of circuit verification (they are merged into a single output set
502
+ // before constructPublicInputs), so the same anon_enc circuit is
503
+ // reused.
504
+ lockedUtxo = newUTXO(bobSourceUtxo.value!, Bob);
505
+ const proofBytes = await prepareEncProofBytes(
506
+ Bob,
507
+ [bobSourceUtxo, ZERO_UTXO],
508
+ [lockedUtxo, ZERO_UTXO],
509
+ [Bob, Bob],
510
+ );
511
+
512
+ const txId = randomBytes32();
513
+ const createArgs = encodeCreateArgs({
514
+ txId,
515
+ inputs: [bobSourceUtxo.hash],
516
+ outputs: [],
517
+ lockedOutputs: [lockedUtxo.hash],
518
+ proof: proofBytes,
519
+ });
520
+
521
+ const predicted = await zeto
522
+ .connect(Bob.signer)
523
+ .computeLockId(createArgs);
524
+ lockId = predicted;
525
+
526
+ const tx = await zeto
527
+ .connect(Bob.signer)
528
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x");
529
+ const result: ContractTransactionReceipt | null = await tx.wait();
530
+ logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
531
+
532
+ const created = result!.logs
533
+ .map((l) => {
534
+ try {
535
+ return zeto.interface.parseLog(l as any);
536
+ } catch (_e) {
537
+ return null;
538
+ }
539
+ })
540
+ .find((p) => p && p.name === "LockCreated");
541
+ expect(created, "LockCreated event not found").to.not.be.null;
542
+ expect(created!.args.lockId).to.equal(predicted);
543
+ expect(created!.args.owner).to.equal(Bob.ethAddress);
544
+ expect(created!.args.spender).to.equal(Bob.ethAddress);
545
+ });
546
+
547
+ it("isLockActive() and getLock() reflect the newly created lock", async function () {
548
+ expect(await zeto.isLockActive(lockId)).to.equal(true);
549
+ const info = await zeto.getLock(lockId);
550
+ expect(info.owner).to.equal(Bob.ethAddress);
551
+ expect(info.spender).to.equal(Bob.ethAddress);
552
+ expect(info.spendCommitment).to.equal(ethers.ZeroHash);
553
+ expect(info.cancelCommitment).to.equal(ethers.ZeroHash);
554
+ });
555
+
556
+ it("locked() returns true for locked UTXOs and false for unlocked or spent UTXOs", async function () {
557
+ // Just-created lock: spender == owner == Bob.
558
+ expect(await zeto.locked(lockedUtxo.hash)).to.deep.equal([
559
+ true,
560
+ Bob.ethAddress,
561
+ ]);
562
+ expect((await zeto.locked(bobSourceUtxo.hash))[0]).to.be.false;
563
+ });
564
+
565
+ it("updateLock() commits the spend hash while owner == spender", async function () {
566
+ outUtxo1 = newUTXO(10, Alice);
567
+ outUtxo2 = newUTXO(90, Bob);
568
+
569
+ unlockHash = calculateSpendHash(
570
+ [lockedUtxo],
571
+ [],
572
+ [outUtxo1, outUtxo2],
573
+ "0x",
574
+ );
575
+
576
+ const tx = await zeto
577
+ .connect(Bob.signer)
578
+ .updateLock(
579
+ lockId,
580
+ encodeUpdateArgs(randomBytes32()),
581
+ unlockHash,
582
+ ethers.ZeroHash,
583
+ "0x",
584
+ );
585
+ const result = await tx.wait();
586
+ logger.debug(`updateLock() complete. Gas used: ${result?.gasUsed}`);
587
+
588
+ const info = await zeto.getLock(lockId);
589
+ expect(info.spendCommitment).to.equal(unlockHash);
590
+ });
591
+
592
+ it("delegateLock() transfers spending authority to Alice", async function () {
593
+ const tx = await zeto
594
+ .connect(Bob.signer)
595
+ .delegateLock(
596
+ lockId,
597
+ encodeDelegateArgs(randomBytes32()),
598
+ Alice.ethAddress,
599
+ "0x",
600
+ );
601
+ const result = await tx.wait();
602
+ logger.debug(`delegateLock() complete. Gas used: ${result?.gasUsed}`);
603
+
604
+ const info = await zeto.getLock(lockId);
605
+ expect(info.spender).to.equal(Alice.ethAddress);
606
+ expect(await zeto.locked(lockedUtxo.hash)).to.deep.equal([
607
+ true,
608
+ Alice.ethAddress,
609
+ ]);
610
+ });
611
+
612
+ it("the new spender can spendLock() with the matching payload", async function () {
613
+ // Bob (still the original owner of the locked UTXO) generates the
614
+ // settlement proof; Alice (the new spender) submits it.
615
+ const settleProofBytes = await prepareEncProofBytes(
616
+ Bob,
617
+ [lockedUtxo, ZERO_UTXO],
618
+ [outUtxo1, outUtxo2],
619
+ [Alice, Bob],
620
+ );
621
+
622
+ const spendArgs = encodeSpendArgs({
623
+ txId: randomBytes32(),
624
+ lockedOutputs: [],
625
+ outputs: [outUtxo1.hash, outUtxo2.hash],
626
+ proof: settleProofBytes,
627
+ data: "0x",
628
+ });
629
+
630
+ const tx = await zeto
631
+ .connect(Alice.signer)
632
+ .spendLock(lockId, spendArgs, "0x");
633
+ const result = await tx.wait();
634
+
635
+ const parsed = result!.logs
636
+ .map((l) => {
637
+ try {
638
+ return zeto.interface.parseLog(l as any);
639
+ } catch (_e) {
640
+ return null;
641
+ }
642
+ })
643
+ .filter((p) => p !== null) as ReadonlyArray<{
644
+ name: string;
645
+ args: any;
646
+ }>;
647
+ const lockSpent = parsed.find((p) => p.name === "LockSpent");
648
+ const zetoLockSpent = parsed.find((p) => p.name === "ZetoLockSpent");
649
+ expect(lockSpent, "LockSpent event not emitted").to.not.be.undefined;
650
+ expect(zetoLockSpent, "ZetoLockSpent event not emitted").to.not.be
651
+ .undefined;
652
+ expect(lockSpent!.args.lockId).to.equal(lockId);
653
+ expect(lockSpent!.args.spender).to.equal(Alice.ethAddress);
654
+
655
+ // Lock is no longer active.
656
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
657
+
658
+ // Outputs are now ordinary unlocked UTXOs.
659
+ expect(await zeto.spent(outUtxo1.hash)).to.equal(1n); // UNSPENT
660
+ expect(await zeto.spent(outUtxo2.hash)).to.equal(1n); // UNSPENT
661
+
662
+ // Per-UTXO delegate projection is cleared post-consume.
663
+ expect(await zeto.locked(lockedUtxo.hash)).to.deep.equal([
664
+ false,
665
+ ZeroAddress,
666
+ ]);
667
+ });
668
+ });
669
+
670
+ describe("createLock -> cancelLock flow", function () {
671
+ let bobSourceUtxo: UTXO;
672
+ let lockedUtxo: UTXO;
673
+ let lockId: string;
674
+ let cancelHash: string;
675
+ let outUtxo1: UTXO;
676
+ let outUtxo2: UTXO;
677
+
678
+ before(async function () {
679
+ bobSourceUtxo = newUTXO(100, Bob);
680
+ await doMint(zeto, deployer, [bobSourceUtxo]);
681
+ });
682
+
683
+ it("Bob createLock() with a non-zero cancelCommitment", async function () {
684
+ lockedUtxo = newUTXO(bobSourceUtxo.value!, Bob);
685
+ const proofBytes = await prepareEncProofBytes(
686
+ Bob,
687
+ [bobSourceUtxo, ZERO_UTXO],
688
+ [lockedUtxo, ZERO_UTXO],
689
+ [Bob, Bob],
690
+ );
691
+
692
+ outUtxo1 = newUTXO(10, Alice);
693
+ outUtxo2 = newUTXO(90, Bob);
694
+ cancelHash = calculateCancelHash(
695
+ [lockedUtxo],
696
+ [],
697
+ [outUtxo1, outUtxo2],
698
+ "0x",
699
+ );
700
+
701
+ const createArgs = encodeCreateArgs({
702
+ txId: randomBytes32(),
703
+ inputs: [bobSourceUtxo.hash],
704
+ outputs: [],
705
+ lockedOutputs: [lockedUtxo.hash],
706
+ proof: proofBytes,
707
+ });
708
+ lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
709
+ const tx = await zeto
710
+ .connect(Bob.signer)
711
+ .createLock(createArgs, ethers.ZeroHash, cancelHash, "0x");
712
+ const result = await tx.wait();
713
+ logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
714
+ });
715
+
716
+ it("the owner can cancelLock() to reverse the lock without delegation", async function () {
717
+ const cancelProofBytes = await prepareEncProofBytes(
718
+ Bob,
719
+ [lockedUtxo, ZERO_UTXO],
720
+ [outUtxo1, outUtxo2],
721
+ [Alice, Bob],
722
+ );
723
+
724
+ const cancelArgs = encodeSpendArgs({
725
+ txId: randomBytes32(),
726
+ lockedOutputs: [],
727
+ outputs: [outUtxo1.hash, outUtxo2.hash],
728
+ proof: cancelProofBytes,
729
+ data: "0x",
730
+ });
731
+
732
+ const tx = await zeto
733
+ .connect(Bob.signer)
734
+ .cancelLock(lockId, cancelArgs, "0x");
735
+ const result = await tx.wait();
736
+
737
+ const parsed = result!.logs
738
+ .map((l) => {
739
+ try {
740
+ return zeto.interface.parseLog(l as any);
741
+ } catch (_e) {
742
+ return null;
743
+ }
744
+ })
745
+ .filter((p) => p !== null) as ReadonlyArray<{
746
+ name: string;
747
+ args: any;
748
+ }>;
749
+ const cancelled = parsed.find((p) => p.name === "LockCancelled");
750
+ const zetoCancelled = parsed.find(
751
+ (p) => p.name === "ZetoLockCancelled",
752
+ );
753
+ expect(cancelled, "LockCancelled event not emitted").to.not.be
754
+ .undefined;
755
+ expect(zetoCancelled, "ZetoLockCancelled event not emitted").to.not.be
756
+ .undefined;
757
+
758
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
759
+ });
760
+ });
761
+
762
+ describe("spendLock with a payload that does not match the spend commitment fails", function () {
763
+ let bobSourceUtxo: UTXO;
764
+ let lockedUtxo: UTXO;
765
+ let lockId: string;
766
+ let expectedHash: string;
767
+
768
+ before(async function () {
769
+ bobSourceUtxo = newUTXO(100, Bob);
770
+ await doMint(zeto, deployer, [bobSourceUtxo]);
771
+ });
772
+
773
+ it("Bob createLock() then updateLock() committing a specific spend hash", async function () {
774
+ lockedUtxo = newUTXO(bobSourceUtxo.value!, Bob);
775
+ const proofBytes = await prepareEncProofBytes(
776
+ Bob,
777
+ [bobSourceUtxo, ZERO_UTXO],
778
+ [lockedUtxo, ZERO_UTXO],
779
+ [Bob, Bob],
780
+ );
781
+ const createArgs = encodeCreateArgs({
782
+ txId: randomBytes32(),
783
+ inputs: [bobSourceUtxo.hash],
784
+ outputs: [],
785
+ lockedOutputs: [lockedUtxo.hash],
786
+ proof: proofBytes,
787
+ });
788
+ lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
789
+ await (
790
+ await zeto
791
+ .connect(Bob.signer)
792
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x")
793
+ ).wait();
794
+
795
+ const expectedOut1 = newUTXO(10, Alice);
796
+ const expectedOut2 = newUTXO(90, Bob);
797
+ expectedHash = calculateSpendHash(
798
+ [lockedUtxo],
799
+ [],
800
+ [expectedOut1, expectedOut2],
801
+ "0x",
802
+ );
803
+ await (
804
+ await zeto
805
+ .connect(Bob.signer)
806
+ .updateLock(
807
+ lockId,
808
+ encodeUpdateArgs(randomBytes32()),
809
+ expectedHash,
810
+ ethers.ZeroHash,
811
+ "0x",
812
+ )
813
+ ).wait();
814
+ });
815
+
816
+ it("spendLock() with a different payload reverts with InvalidUnlockHash", async function () {
817
+ if (network.name !== "hardhat") {
818
+ this.skip();
819
+ }
820
+ const wrongOut1 = newUTXO(20, Alice);
821
+ const wrongOut2 = newUTXO(80, Bob);
822
+
823
+ const settleProofBytes = await prepareEncProofBytes(
824
+ Bob,
825
+ [lockedUtxo, ZERO_UTXO],
826
+ [wrongOut1, wrongOut2],
827
+ [Alice, Bob],
828
+ );
829
+
830
+ const spendArgs = encodeSpendArgs({
831
+ txId: randomBytes32(),
832
+ lockedOutputs: [],
833
+ outputs: [wrongOut1.hash, wrongOut2.hash],
834
+ proof: settleProofBytes,
835
+ data: "0x",
836
+ });
837
+
838
+ const calculatedHash = calculateSpendHash(
839
+ [lockedUtxo],
840
+ [],
841
+ [wrongOut1, wrongOut2],
842
+ "0x",
843
+ );
844
+
845
+ await expect(
846
+ zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x"),
847
+ )
848
+ .to.be.revertedWithCustomError(zeto, "InvalidUnlockHash")
849
+ .withArgs(expectedHash, calculatedHash);
850
+ });
851
+ });
852
+
853
+ describe("negative cases for the lock lifecycle", function () {
854
+ // These tests rely on hardhat-style revert decoding.
855
+ if (network.name !== "hardhat") {
856
+ return;
857
+ }
858
+
859
+ // freshLock mints a UTXO for `owner`, locks it, and returns the
860
+ // resulting lock metadata so each negative case can branch off
861
+ // without polluting other test scopes.
862
+ async function freshLock(
863
+ owner: User,
864
+ spendCommitment: string = ethers.ZeroHash,
865
+ cancelCommitment: string = ethers.ZeroHash,
866
+ ): Promise<{
867
+ lockId: string;
868
+ sourceUtxo: UTXO;
869
+ lockedUtxo: UTXO;
870
+ createArgs: string;
871
+ }> {
872
+ const sourceUtxo = newUTXO(100, owner);
873
+ await doMint(zeto, deployer, [sourceUtxo]);
874
+
875
+ const lockedUtxo = newUTXO(sourceUtxo.value!, owner);
876
+ const proofBytes = await prepareEncProofBytes(
877
+ owner,
878
+ [sourceUtxo, ZERO_UTXO],
879
+ [lockedUtxo, ZERO_UTXO],
880
+ [owner, owner],
881
+ );
882
+
883
+ const createArgs = encodeCreateArgs({
884
+ txId: randomBytes32(),
885
+ inputs: [sourceUtxo.hash],
886
+ outputs: [],
887
+ lockedOutputs: [lockedUtxo.hash],
888
+ proof: proofBytes,
889
+ });
890
+ const lockId = await zeto
891
+ .connect(owner.signer)
892
+ .computeLockId(createArgs);
893
+ await (
894
+ await zeto
895
+ .connect(owner.signer)
896
+ .createLock(createArgs, spendCommitment, cancelCommitment, "0x")
897
+ ).wait();
898
+ return { lockId, sourceUtxo, lockedUtxo, createArgs };
899
+ }
900
+
901
+ // Re-encoding helper: a syntactically valid spend payload that does
902
+ // NOT need to verify a real ZK proof. Tests for authorization /
903
+ // immutability assertions short-circuit before the proof is touched.
904
+ function dummySpendArgs(): string {
905
+ return encodeSpendArgs({
906
+ txId: randomBytes32(),
907
+ lockedOutputs: [],
908
+ outputs: [],
909
+ proof: "0x",
910
+ data: "0x",
911
+ });
912
+ }
913
+
914
+ it("createLock() with a duplicate txId from the same caller reverts with DuplicateLock", async function () {
915
+ const { lockId, createArgs } = await freshLock(Bob);
916
+
917
+ // Same createArgs (same txId, same caller) => same lockId =>
918
+ // DuplicateLock fires before any input or proof validation.
919
+ await expect(
920
+ zeto
921
+ .connect(Bob.signer)
922
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x"),
923
+ )
924
+ .to.be.revertedWithCustomError(zeto, "DuplicateLock")
925
+ .withArgs(lockId);
926
+ });
927
+
928
+ it("updateLock() by a non-owner reverts with LockUnauthorized", async function () {
929
+ const { lockId } = await freshLock(Bob);
930
+
931
+ await expect(
932
+ zeto
933
+ .connect(Alice.signer)
934
+ .updateLock(
935
+ lockId,
936
+ encodeUpdateArgs(randomBytes32()),
937
+ ethers.ZeroHash,
938
+ ethers.ZeroHash,
939
+ "0x",
940
+ ),
941
+ )
942
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
943
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
944
+ });
945
+
946
+ it("updateLock() prefers LockUnauthorized over LockImmutable when both apply (M-5)", async function () {
947
+ // Lock is delegated (so spender != owner -> immutable) AND the
948
+ // caller is neither owner nor spender. The contract MUST report
949
+ // LockUnauthorized, not leak the immutability state to the
950
+ // unauthorized caller.
951
+ const { lockId } = await freshLock(Bob);
952
+ await (
953
+ await zeto
954
+ .connect(Bob.signer)
955
+ .delegateLock(
956
+ lockId,
957
+ encodeDelegateArgs(randomBytes32()),
958
+ Alice.ethAddress,
959
+ "0x",
960
+ )
961
+ ).wait();
962
+
963
+ await expect(
964
+ zeto
965
+ .connect(Charlie.signer)
966
+ .updateLock(
967
+ lockId,
968
+ encodeUpdateArgs(randomBytes32()),
969
+ ethers.ZeroHash,
970
+ ethers.ZeroHash,
971
+ "0x",
972
+ ),
973
+ )
974
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
975
+ // spender at this point is Alice (the new delegate).
976
+ .withArgs(lockId, Alice.ethAddress, Charlie.ethAddress);
977
+ });
978
+
979
+ it("updateLock() after delegateLock() reverts with LockImmutable", async function () {
980
+ const { lockId } = await freshLock(Bob);
981
+
982
+ // Bob delegates spending authority to Alice; spender (Alice) now
983
+ // differs from owner (Bob).
984
+ await (
985
+ await zeto
986
+ .connect(Bob.signer)
987
+ .delegateLock(
988
+ lockId,
989
+ encodeDelegateArgs(randomBytes32()),
990
+ Alice.ethAddress,
991
+ "0x",
992
+ )
993
+ ).wait();
994
+
995
+ // Even Bob (the owner) can no longer mutate the commitments —
996
+ // the lock is now externally controlled and must be considered
997
+ // immutable.
998
+ await expect(
999
+ zeto
1000
+ .connect(Bob.signer)
1001
+ .updateLock(
1002
+ lockId,
1003
+ encodeUpdateArgs(randomBytes32()),
1004
+ ethers.ZeroHash,
1005
+ ethers.ZeroHash,
1006
+ "0x",
1007
+ ),
1008
+ )
1009
+ .to.be.revertedWithCustomError(zeto, "LockImmutable")
1010
+ .withArgs(lockId);
1011
+ });
1012
+
1013
+ it("delegateLock() by a non-spender reverts with LockUnauthorized", async function () {
1014
+ const { lockId } = await freshLock(Bob);
1015
+
1016
+ await expect(
1017
+ zeto
1018
+ .connect(Alice.signer)
1019
+ .delegateLock(
1020
+ lockId,
1021
+ encodeDelegateArgs(randomBytes32()),
1022
+ Charlie.ethAddress,
1023
+ "0x",
1024
+ ),
1025
+ )
1026
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1027
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1028
+ });
1029
+
1030
+ it("spendLock() by a non-spender reverts with LockUnauthorized before touching the proof", async function () {
1031
+ const { lockId } = await freshLock(Bob);
1032
+
1033
+ // Garbage proof — the onlySpender modifier MUST short-circuit
1034
+ // before any proof verification is attempted.
1035
+ await expect(
1036
+ zeto.connect(Alice.signer).spendLock(lockId, dummySpendArgs(), "0x"),
1037
+ )
1038
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1039
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1040
+ });
1041
+
1042
+ it("cancelLock() by a non-spender reverts with LockUnauthorized", async function () {
1043
+ const { lockId } = await freshLock(Bob);
1044
+
1045
+ await expect(
1046
+ zeto.connect(Alice.signer).cancelLock(lockId, dummySpendArgs(), "0x"),
1047
+ )
1048
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1049
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1050
+ });
1051
+
1052
+ it("after a successful spendLock(), the lock is no longer active and getLock() reverts", async function () {
1053
+ const { lockId, lockedUtxo } = await freshLock(Bob);
1054
+
1055
+ const out1 = newUTXO(10, Alice);
1056
+ const out2 = newUTXO(90, Bob);
1057
+ const settleProofBytes = await prepareEncProofBytes(
1058
+ Bob,
1059
+ [lockedUtxo, ZERO_UTXO],
1060
+ [out1, out2],
1061
+ [Alice, Bob],
1062
+ );
1063
+ const spendArgs = encodeSpendArgs({
1064
+ txId: randomBytes32(),
1065
+ lockedOutputs: [],
1066
+ outputs: [out1.hash, out2.hash],
1067
+ proof: settleProofBytes,
1068
+ data: "0x",
1069
+ });
1070
+ await (
1071
+ await zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x")
1072
+ ).wait();
1073
+
1074
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
1075
+ await expect(zeto.getLock(lockId))
1076
+ .to.be.revertedWithCustomError(zeto, "LockNotActive")
1077
+ .withArgs(lockId);
1078
+ // Re-spending a consumed lock MUST also fail — lockActive is
1079
+ // the first modifier and emits LockNotActive before
1080
+ // onlySpender has a chance to fire.
1081
+ await expect(
1082
+ zeto.connect(Bob.signer).spendLock(lockId, dummySpendArgs(), "0x"),
1083
+ )
1084
+ .to.be.revertedWithCustomError(zeto, "LockNotActive")
1085
+ .withArgs(lockId);
1086
+ });
1087
+
1088
+ it("an unlocked input flow rejects a locked UTXO with AlreadyLocked", async function () {
1089
+ // Mint a UTXO, lock it, then try to spend it via the regular
1090
+ // transfer path. The base-storage validateInputs() guards this
1091
+ // and reverts with AlreadyLocked on the locked input.
1092
+ const { lockedUtxo } = await freshLock(Bob);
1093
+
1094
+ await expect(
1095
+ doTransfer(
1096
+ Bob,
1097
+ [lockedUtxo, ZERO_UTXO],
1098
+ [newUTXO(100, Alice), ZERO_UTXO],
1099
+ [Alice, Alice],
1100
+ ),
1101
+ )
1102
+ .to.be.revertedWithCustomError(zeto, "AlreadyLocked")
1103
+ .withArgs(lockedUtxo.hash);
1104
+ });
1105
+ });
1106
+ });
430
1107
 
431
1108
  async function doTransfer(
432
1109
  signer: User,
@@ -480,71 +1157,21 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
480
1157
  owners: User[],
481
1158
  ephemeralPrivateKey: BigInt,
482
1159
  ) {
483
- const inputCommitments: BigNumberish[] = inputs.map(
484
- (input) => input.hash,
485
- ) as BigNumberish[];
486
- const inputValues = inputs.map((input) => BigInt(input.value || 0n));
487
- const inputSalts = inputs.map((input) => input.salt || 0n);
488
- const outputCommitments: BigNumberish[] = outputs.map(
489
- (output) => output.hash,
490
- ) as BigNumberish[];
491
- const outputValues = outputs.map((output) => BigInt(output.value || 0n));
492
- const outputOwnerPublicKeys: BigNumberish[][] = owners.map(
493
- (owner) => owner.babyJubPublicKey,
494
- ) as BigNumberish[][];
495
- const encryptionNonce: BigNumberish = newEncryptionNonce() as BigNumberish;
496
- const encryptInputs = stringifyBigInts({
497
- encryptionNonce,
498
- ecdhPrivateKey: formatPrivKeyForBabyJub(ephemeralPrivateKey),
499
- });
500
-
501
1160
  let circuitToUse = circuit;
502
1161
  let provingKeyToUse = provingKey;
503
- let isBatch = false;
504
- if (inputCommitments.length > 2 || outputCommitments.length > 2) {
505
- isBatch = true;
1162
+ if (inputs.length > 2 || outputs.length > 2) {
506
1163
  circuitToUse = batchCircuit;
507
1164
  provingKeyToUse = batchProvingKey;
508
1165
  }
509
- const startWitnessCalculation = Date.now();
510
- const witness = await circuitToUse.calculateWTNSBin(
511
- {
512
- inputCommitments,
513
- inputValues,
514
- inputSalts,
515
- inputOwnerPrivateKey: formatPrivKeyForBabyJub(signer.babyJubPrivateKey),
516
- outputCommitments,
517
- outputValues,
518
- outputSalts: outputs.map((output) => output.salt || 0n),
519
- outputOwnerPublicKeys,
520
- ...encryptInputs,
521
- },
522
- true,
523
- );
524
- const timeWitnessCalculation = Date.now() - startWitnessCalculation;
525
-
526
- const startProofGeneration = Date.now();
527
- const { proof, publicSignals } = (await groth16.prove(
1166
+ return prepareProofModule(
1167
+ circuitToUse,
528
1168
  provingKeyToUse,
529
- witness,
530
- )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] };
531
- const timeProofGeneration = Date.now() - startProofGeneration;
532
- console.log(
533
- `Witness calculation time: ${timeWitnessCalculation}ms, Proof generation time: ${timeProofGeneration}ms`,
1169
+ signer,
1170
+ inputs,
1171
+ outputs,
1172
+ owners,
1173
+ ephemeralPrivateKey,
534
1174
  );
535
-
536
- // console.log(publicSignals);
537
- const encodedProof = encodeProof(proof);
538
- const encryptedValues = isBatch
539
- ? publicSignals.slice(2, 42)
540
- : publicSignals.slice(2, 10);
541
- return {
542
- inputCommitments,
543
- outputCommitments,
544
- encryptedValues,
545
- encryptionNonce,
546
- encodedProof,
547
- };
548
1175
  }
549
1176
 
550
1177
  async function sendTx(
@@ -559,24 +1186,135 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
559
1186
  const tx = await zeto.connect(signer.signer).transfer(
560
1187
  inputCommitments.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works
561
1188
  outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works
562
- encryptionNonce,
563
- ecdhPublicKey,
564
- encryptedValues,
565
- encodedProof,
1189
+ encodeToBytes(
1190
+ encryptionNonce,
1191
+ ecdhPublicKey,
1192
+ encryptedValues,
1193
+ encodedProof,
1194
+ ),
566
1195
  "0x",
567
1196
  );
568
1197
  const results: ContractTransactionReceipt | null = await tx.wait();
569
1198
 
570
1199
  for (const input of inputCommitments) {
571
1200
  if (input === 0n) continue;
572
- expect(await zeto.spent(input)).to.equal(true);
1201
+ expect(await zeto.spent(input)).to.equal(2n);
573
1202
  }
574
1203
  for (const output of outputCommitments) {
575
1204
  if (output === 0n) continue;
576
- expect(await zeto.spent(output)).to.equal(false);
1205
+ expect(await zeto.spent(output)).to.equal(1n);
577
1206
  }
578
1207
  console.log(`Method transfer() complete. Gas used: ${results?.gasUsed}`);
579
1208
 
580
1209
  return results;
581
1210
  }
582
1211
  });
1212
+
1213
+ function encodeToBytes(
1214
+ encryptionNonce: any,
1215
+ ecdhPublicKey: any,
1216
+ encryptedValues: any,
1217
+ proof: any,
1218
+ ) {
1219
+ const bytes = new AbiCoder().encode(
1220
+ [
1221
+ "uint256 encryptionNonce",
1222
+ "uint256[2] ecdhPublicKey",
1223
+ "uint256[] encryptedValues",
1224
+ "tuple(uint256[2] pA, uint256[2][2] pB, uint256[2] pC)",
1225
+ ],
1226
+ [encryptionNonce, ecdhPublicKey, encryptedValues, proof],
1227
+ );
1228
+ // console.log("Encryption nonce:", encryptionNonce);
1229
+ // console.log("ECDH public key:", ecdhPublicKey);
1230
+ // console.log("Encrypted values:", encryptedValues);
1231
+ // console.log("Proof:", proof);
1232
+ // console.log("Encoded proof:", bytes);
1233
+ return bytes;
1234
+ }
1235
+
1236
+ function encodeToBytesForWithdraw(proof: any) {
1237
+ return new AbiCoder().encode(
1238
+ ["tuple(uint256[2] pA, uint256[2][2] pB, uint256[2] pC)"],
1239
+ [proof],
1240
+ );
1241
+ }
1242
+
1243
+ // Module-scope twin of the in-describe `prepareProof`. Lifted so that other
1244
+ // test suites (notably `zeto_anon_enc_nullifier.ts`) can reuse it for
1245
+ // locked-input proofs that need to verify against the `anon_enc` circuit
1246
+ // rather than a nullifier-aware encryption circuit. Mirrors the
1247
+ // {prepareProof,encodeToBytes} export pattern at the bottom of
1248
+ // `zeto_anon.ts`.
1249
+ async function prepareProofModule(
1250
+ circuit: any,
1251
+ provingKey: any,
1252
+ signer: User,
1253
+ inputs: UTXO[],
1254
+ outputs: UTXO[],
1255
+ owners: User[],
1256
+ ephemeralPrivateKey: BigInt,
1257
+ ) {
1258
+ const inputCommitments: BigNumberish[] = inputs.map(
1259
+ (input) => input.hash,
1260
+ ) as BigNumberish[];
1261
+ const inputValues = inputs.map((input) => BigInt(input.value || 0n));
1262
+ const inputSalts = inputs.map((input) => input.salt || 0n);
1263
+ const outputCommitments: BigNumberish[] = outputs.map(
1264
+ (output) => output.hash,
1265
+ ) as BigNumberish[];
1266
+ const outputValues = outputs.map((output) => BigInt(output.value || 0n));
1267
+ const outputOwnerPublicKeys: BigNumberish[][] = owners.map(
1268
+ (owner) => owner.babyJubPublicKey,
1269
+ ) as BigNumberish[][];
1270
+ const encryptionNonce: BigNumberish = newEncryptionNonce() as BigNumberish;
1271
+ const encryptInputs = stringifyBigInts({
1272
+ encryptionNonce,
1273
+ ecdhPrivateKey: formatPrivKeyForBabyJub(ephemeralPrivateKey),
1274
+ });
1275
+
1276
+ const startWitnessCalculation = Date.now();
1277
+ const witness = await circuit.calculateWTNSBin(
1278
+ {
1279
+ inputCommitments,
1280
+ inputValues,
1281
+ inputSalts,
1282
+ inputOwnerPrivateKey: formatPrivKeyForBabyJub(signer.babyJubPrivateKey),
1283
+ outputCommitments,
1284
+ outputValues,
1285
+ outputSalts: outputs.map((output) => output.salt || 0n),
1286
+ outputOwnerPublicKeys,
1287
+ ...encryptInputs,
1288
+ },
1289
+ true,
1290
+ );
1291
+ const timeWitnessCalculation = Date.now() - startWitnessCalculation;
1292
+
1293
+ const startProofGeneration = Date.now();
1294
+ const { proof, publicSignals } = (await groth16.prove(
1295
+ provingKey,
1296
+ witness,
1297
+ )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] };
1298
+ const timeProofGeneration = Date.now() - startProofGeneration;
1299
+ console.log(
1300
+ `Witness calculation time: ${timeWitnessCalculation}ms, Proof generation time: ${timeProofGeneration}ms`,
1301
+ );
1302
+
1303
+ // The encryption circuit emits 4 encrypted-values per output commitment
1304
+ // (poseidon-cipher chunked into 4 field elements). publicSignals[0..2]
1305
+ // are the ECDH public key, so the encrypted blob starts at index 2.
1306
+ const encryptedValues = publicSignals.slice(2, 2 + 4 * outputs.length);
1307
+ const encodedProof = encodeProof(proof);
1308
+ return {
1309
+ inputCommitments,
1310
+ outputCommitments,
1311
+ encryptedValues,
1312
+ encryptionNonce,
1313
+ encodedProof,
1314
+ };
1315
+ }
1316
+
1317
+ module.exports = {
1318
+ prepareProof: prepareProofModule,
1319
+ encodeToBytes,
1320
+ };