@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,12 @@
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
+ } from "ethers";
19
24
  import { expect } from "chai";
20
25
  import { loadCircuit, Poseidon, encodeProof, kycHash } from "zeto-js";
21
26
  import { groth16 } from "snarkjs";
@@ -30,16 +35,92 @@ import {
30
35
  ZERO_UTXO,
31
36
  parseUTXOEvents,
32
37
  parseRegistryEvents,
38
+ logger,
33
39
  } from "./lib/utils";
34
40
  import {
35
41
  loadProvingKeys,
36
- prepareDepositProof,
42
+ prepareDepositKycProof,
37
43
  prepareNullifierWithdrawProof,
44
+ encodeToBytesForDeposit,
45
+ encodeToBytesForWithdraw,
46
+ calculateSpendHash,
47
+ calculateCancelHash,
38
48
  } from "./utils";
39
49
  import { deployZeto } from "./lib/deploy";
40
50
  import smt from "../ignition/modules/test/smt";
41
51
 
42
- describe("Zeto based fungible token with anonymity, KYC, using nullifiers without encryption", function () {
52
+ if (!process.env.CIRCUITS_ROOT) {
53
+ process.env.CIRCUITS_ROOT = "/Users/jimzhang/Documents/zkp/circuits/";
54
+ }
55
+ if (!process.env.PROVING_KEYS_ROOT) {
56
+ process.env.PROVING_KEYS_ROOT = "/Users/jimzhang/Documents/zkp/proving-keys/";
57
+ }
58
+
59
+ // ABI fragments for the ZetoLockableCapability *Args payloads. These mirror
60
+ // the Solidity structs declared in IZetoLockableCapability.sol and let the
61
+ // tests construct the opaque `bytes` parameters that the lock lifecycle
62
+ // methods (createLock / updateLock / delegateLock / spendLock / cancelLock)
63
+ // pass through to the token-specific encoders.
64
+ const CREATE_ARGS_ABI =
65
+ "tuple(bytes32 txId, uint256[] inputs, uint256[] outputs, uint256[] lockedOutputs, bytes proof)";
66
+ const UPDATE_ARGS_ABI = "tuple(bytes32 txId)";
67
+ const DELEGATE_ARGS_ABI = "tuple(bytes32 txId)";
68
+ const SPEND_ARGS_ABI =
69
+ "tuple(bytes32 txId, uint256[] lockedOutputs, uint256[] outputs, bytes proof, bytes data)";
70
+
71
+ function encodeCreateArgs(args: {
72
+ txId: string;
73
+ inputs: BigNumberish[];
74
+ outputs: BigNumberish[];
75
+ lockedOutputs: BigNumberish[];
76
+ proof: string;
77
+ }) {
78
+ return new AbiCoder().encode([CREATE_ARGS_ABI], [args]);
79
+ }
80
+
81
+ function encodeUpdateArgs(txId: string) {
82
+ return new AbiCoder().encode([UPDATE_ARGS_ABI], [{ txId }]);
83
+ }
84
+
85
+ function encodeDelegateArgs(txId: string) {
86
+ return new AbiCoder().encode([DELEGATE_ARGS_ABI], [{ txId }]);
87
+ }
88
+
89
+ function encodeSpendArgs(args: {
90
+ txId: string;
91
+ lockedOutputs: BigNumberish[];
92
+ outputs: BigNumberish[];
93
+ proof: string;
94
+ data: string;
95
+ }) {
96
+ return new AbiCoder().encode([SPEND_ARGS_ABI], [args]);
97
+ }
98
+
99
+ function randomBytes32(): string {
100
+ return ethers.hexlify(ethers.randomBytes(32));
101
+ }
102
+
103
+ // The unlocked-input path encodes (root, ZkProof) so the contract can
104
+ // validate the SMT root before delegating to the verifier. The locked-input
105
+ // path elides the root: locked UTXOs are tracked in a flat per-lock mapping
106
+ // rather than an SMT, so there's no Merkle root to assert membership against.
107
+ function encodeUnlockedProof(root: BigNumberish, proof: any) {
108
+ return new AbiCoder().encode(
109
+ ["uint256 root", "tuple(uint256[2] pA, uint256[2][2] pB, uint256[2] pC)"],
110
+ [root, proof],
111
+ );
112
+ }
113
+
114
+ function encodeLockedProof(proof: any) {
115
+ return new AbiCoder().encode(
116
+ ["tuple(uint256[2] pA, uint256[2][2] pB, uint256[2] pC)"],
117
+ [proof],
118
+ );
119
+ }
120
+
121
+ describe(
122
+ "Zeto based fungible token with anonymity, KYC, using nullifiers without encryption",
123
+ function () {
43
124
  let deployer: Signer;
44
125
  let Alice: User;
45
126
  let Bob: User;
@@ -79,11 +160,17 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
79
160
 
80
161
  ({ deployer, zeto, erc20 } = await deployZeto("Zeto_AnonNullifierKyc"));
81
162
 
82
- const tx2 = await zeto.connect(deployer).register(Alice.babyJubPublicKey, '0x');
163
+ const tx2 = await zeto
164
+ .connect(deployer)
165
+ .register(Alice.babyJubPublicKey, "0x");
83
166
  const result1 = await tx2.wait();
84
- const tx3 = await zeto.connect(deployer).register(Bob.babyJubPublicKey, '0x');
167
+ const tx3 = await zeto
168
+ .connect(deployer)
169
+ .register(Bob.babyJubPublicKey, "0x");
85
170
  const result2 = await tx3.wait();
86
- const tx4 = await zeto.connect(deployer).register(Charlie.babyJubPublicKey, '0x');
171
+ const tx4 = await zeto
172
+ .connect(deployer)
173
+ .register(Charlie.babyJubPublicKey, "0x");
87
174
  const result3 = await tx4.wait();
88
175
 
89
176
  const storage1 = new InMemoryDB(str2Bytes("alice"));
@@ -106,7 +193,9 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
106
193
  await smtKyc.add(kycHash(publicKey3), kycHash(publicKey3));
107
194
 
108
195
  circuit = await loadCircuit("anon_nullifier_kyc_transfer");
109
- ({ provingKeyFile: provingKey } = loadProvingKeys("anon_nullifier_kyc_transfer"));
196
+ ({ provingKeyFile: provingKey } = loadProvingKeys(
197
+ "anon_nullifier_kyc_transfer",
198
+ ));
110
199
  batchCircuit = await loadCircuit("anon_nullifier_kyc_transfer_batch");
111
200
  ({ provingKeyFile: batchProvingKey } = loadProvingKeys(
112
201
  "anon_nullifier_kyc_transfer_batch",
@@ -237,11 +326,6 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
237
326
  await smtUnregistered.add(incomingUTXOs[i], incomingUTXOs[i]);
238
327
  }
239
328
 
240
- // check empty hashes are empty
241
- for (let i = outputUtxos.length; i < 10; i++) {
242
- expect(incomingUTXOs[i]).to.equal(0);
243
- }
244
-
245
329
  // mint sufficient balance in Zeto contract address for Alice to withdraw
246
330
  const mintTx = await erc20.connect(deployer).mint(zeto, 3);
247
331
  await mintTx.wait();
@@ -294,8 +378,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
294
378
  3,
295
379
  _withdrawNullifiers,
296
380
  withdrawCommitments[0],
297
- root.bigInt(),
298
- withdrawEncodedProof,
381
+ encodeToBytesForWithdraw(root.bigInt(), withdrawEncodedProof),
299
382
  "0x",
300
383
  );
301
384
  await tx.wait();
@@ -303,7 +386,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
303
386
  // Alice checks her ERC20 balance
304
387
  const endingBalance = await erc20.balanceOf(Alice.ethAddress);
305
388
  expect(endingBalance - startingBalance).to.be.equal(3);
306
- }).timeout(60000);
389
+ }).timeout(180000);
307
390
 
308
391
  it("mint ERC20 tokens to Alice to deposit to Zeto should succeed", async function () {
309
392
  const startingBalance = await erc20.balanceOf(Alice.ethAddress);
@@ -317,13 +400,29 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
317
400
 
318
401
  utxo100 = newUTXO(100, Alice);
319
402
  utxo0 = newUTXO(0, Alice);
320
- const { outputCommitments, encodedProof } = await prepareDepositProof(
403
+ const identitiesRoot = await smtKyc.root();
404
+ const proof3 = await smtKyc.generateCircomVerifierProof(
405
+ kycHash(Alice.babyJubPublicKey),
406
+ identitiesRoot,
407
+ );
408
+ const identitiesMerkleProofs = [
409
+ proof3.siblings.map((s) => s.bigInt()),
410
+ proof3.siblings.map((s) => s.bigInt()),
411
+ ];
412
+ const { outputCommitments, encodedProof } = await prepareDepositKycProof(
321
413
  Alice,
322
414
  [utxo100, utxo0],
415
+ identitiesRoot.bigInt(),
416
+ identitiesMerkleProofs,
323
417
  );
324
418
  const tx2 = await zeto
325
419
  .connect(Alice.signer)
326
- .deposit(100, outputCommitments, encodedProof, "0x");
420
+ .deposit(
421
+ 100,
422
+ outputCommitments,
423
+ encodeToBytesForDeposit(encodedProof),
424
+ "0x",
425
+ );
327
426
  await tx2.wait();
328
427
 
329
428
  await smtAlice.add(utxo100.hash, utxo100.hash);
@@ -541,8 +640,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
541
640
  80,
542
641
  nullifiers,
543
642
  outputCommitments[0],
544
- root.bigInt(),
545
- encodedProof,
643
+ encodeToBytesForWithdraw(root.bigInt(), encodedProof),
546
644
  "0x",
547
645
  );
548
646
  await tx.wait();
@@ -578,7 +676,17 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
578
676
  );
579
677
  });
580
678
 
581
- it("deposit by an unregistered user should succeed", async function () {
679
+ it("deposit by an unregistered user should fail", async function () {
680
+ const identitiesRoot = await smtKycUnregistered.root();
681
+ const proof3 = await smtKycUnregistered.generateCircomVerifierProof(
682
+ kycHash(unregistered.babyJubPublicKey),
683
+ identitiesRoot,
684
+ );
685
+ const identitiesMerkleProofs = [
686
+ proof3.siblings.map((s) => s.bigInt()),
687
+ proof3.siblings.map((s) => s.bigInt()),
688
+ ];
689
+
582
690
  const tx = await erc20
583
691
  .connect(deployer)
584
692
  .mint(unregistered.ethAddress, 100);
@@ -590,160 +698,32 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
590
698
 
591
699
  unregisteredUtxo100 = newUTXO(100, unregistered);
592
700
  unregisteredUtxo0 = newUTXO(0, unregistered);
593
- const { outputCommitments, encodedProof } = await prepareDepositProof(
701
+ const { outputCommitments, encodedProof } = await prepareDepositKycProof(
594
702
  unregistered,
595
703
  [unregisteredUtxo100, unregisteredUtxo0],
704
+ identitiesRoot.bigInt(),
705
+ identitiesMerkleProofs,
596
706
  );
597
- const tx2 = await zeto
598
- .connect(unregistered.signer)
599
- .deposit(100, outputCommitments, encodedProof, "0x");
600
- await tx2.wait();
601
-
602
- // Alice tracks the UTXO inside the SMT
603
- await smtAlice.add(unregisteredUtxo100.hash, unregisteredUtxo100.hash);
604
- await smtAlice.add(unregisteredUtxo0.hash, unregisteredUtxo0.hash);
605
- // Bob also locally tracks the UTXOs inside the SMT
606
- await smtBob.add(unregisteredUtxo100.hash, unregisteredUtxo100.hash);
607
- await smtBob.add(unregisteredUtxo0.hash, unregisteredUtxo0.hash);
608
- });
609
-
610
- it("transfer from an unregistered user should fail", async function () {
611
- // catch up the local SMT for the unregistered user
612
- await smtUnregistered.add(utxo100.hash, utxo100.hash);
613
- await smtUnregistered.add(utxo0.hash, utxo0.hash);
614
- await smtUnregistered.add(utxo1.hash, utxo1.hash);
615
- await smtUnregistered.add(utxo2.hash, utxo2.hash);
616
- await smtUnregistered.add(_utxo3.hash, _utxo3.hash);
617
- await smtUnregistered.add(utxo4.hash, utxo4.hash);
618
- await smtUnregistered.add(utxo6.hash, utxo6.hash);
619
- await smtUnregistered.add(utxo7.hash, utxo7.hash);
620
- await smtUnregistered.add(
621
- withdrawChangeUTXO.hash,
622
- withdrawChangeUTXO.hash,
623
- );
624
- await smtUnregistered.add(
625
- unregisteredUtxo100.hash,
626
- unregisteredUtxo100.hash,
627
- );
628
- await smtUnregistered.add(unregisteredUtxo0.hash, unregisteredUtxo0.hash);
629
- const utxosRoot = await smtUnregistered.root();
630
-
631
- const nullifier = newNullifier(unregisteredUtxo100, unregistered);
632
- const output1 = newUTXO(100, Bob);
633
- const output2 = newUTXO(0, unregistered);
634
- const proof = await smtUnregistered.generateCircomVerifierProof(
635
- unregisteredUtxo100.hash,
636
- utxosRoot,
637
- );
638
- const merkleProofs = [
639
- proof.siblings.map((s) => s.bigInt()),
640
- proof.siblings.map((s) => s.bigInt()),
641
- ];
642
-
643
- const identitiesRoot = await smtKycUnregistered.root();
644
- const proof3 = await smtKycUnregistered.generateCircomVerifierProof(
645
- kycHash(unregistered.babyJubPublicKey),
646
- identitiesRoot,
647
- );
648
- const proof4 = await smtKycUnregistered.generateCircomVerifierProof(
649
- kycHash(Bob.babyJubPublicKey),
650
- identitiesRoot,
651
- );
652
- const identitiesMerkleProofs = [
653
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (unregistered)
654
- proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob)
655
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the 2nd owner of the output UTXO (unregistered)
656
- ];
657
707
  await expect(
658
- doTransfer(
659
- unregistered,
660
- [unregisteredUtxo100, ZERO_UTXO],
661
- [nullifier, ZERO_UTXO],
662
- [output1, output2],
663
- utxosRoot.bigInt(),
664
- merkleProofs,
665
- identitiesRoot.bigInt(),
666
- identitiesMerkleProofs,
667
- [Bob, unregistered],
668
- ),
669
- ).rejectedWith("Invalid proof");
670
- });
671
-
672
- it("the unregistered user can still withdraw their UTXOs to ERC20 tokens", async function () {
673
- const startingBalance = await erc20.balanceOf(unregistered.ethAddress);
674
- // unregistered user generates the nullifiers for the UTXOs to be spent
675
- const nullifier1 = newNullifier(unregisteredUtxo100, unregistered);
676
-
677
- // unregistered user generates inclusion proofs for the UTXOs to be spent
678
- let root = await smtUnregistered.root();
679
- const proof1 = await smtUnregistered.generateCircomVerifierProof(
680
- unregisteredUtxo100.hash,
681
- root,
682
- );
683
- const proof2 = await smtUnregistered.generateCircomVerifierProof(
684
- 0n,
685
- root,
686
- );
687
- const merkleProofs = [
688
- proof1.siblings.map((s) => s.bigInt()),
689
- proof2.siblings.map((s) => s.bigInt()),
690
- ];
691
-
692
- // unregistered user proposes the output ERC20 tokens
693
- const unregisteredWithdrawChangeUTXO = newUTXO(0, unregistered);
694
-
695
- const { nullifiers, outputCommitments, encodedProof } =
696
- await prepareNullifierWithdrawProof(
697
- unregistered,
698
- [unregisteredUtxo100, ZERO_UTXO],
699
- [nullifier1, ZERO_UTXO],
700
- unregisteredWithdrawChangeUTXO,
701
- root.bigInt(),
702
- merkleProofs,
703
- );
704
-
705
- // unregistered user withdraws her UTXOs to ERC20 tokens
706
- const tx = await zeto
707
- .connect(unregistered.signer)
708
- .withdraw(
709
- 100,
710
- nullifiers,
711
- outputCommitments[0],
712
- root.bigInt(),
713
- encodedProof,
714
- "0x",
715
- );
716
- await tx.wait();
717
-
718
- // Alice tracks the UTXO inside the SMT
719
- await smtAlice.add(
720
- unregisteredWithdrawChangeUTXO.hash,
721
- unregisteredWithdrawChangeUTXO.hash,
722
- );
723
- // Bob also locally tracks the UTXOs inside the SMT
724
- await smtBob.add(
725
- unregisteredWithdrawChangeUTXO.hash,
726
- unregisteredWithdrawChangeUTXO.hash,
727
- );
728
-
729
- // unregistered user checks her ERC20 balance
730
- const endingBalance = await erc20.balanceOf(unregistered.ethAddress);
731
- expect(endingBalance - startingBalance).to.be.equal(100);
708
+ zeto
709
+ .connect(unregistered.signer)
710
+ .deposit(
711
+ 100,
712
+ outputCommitments,
713
+ encodeToBytesForDeposit(encodedProof),
714
+ "0x",
715
+ ),
716
+ ).to.be.rejectedWith("VM Exception while processing transaction: reverted with custom error 'InvalidProof()'");
732
717
  });
733
718
  });
734
719
 
735
- describe("lock() tests", function () {
736
- let lockedUtxo1: UTXO;
737
- let smtBobForLocked: Merkletree;
720
+ describe("ILockableCapability tests", function () {
738
721
  let utxo9: UTXO;
739
722
  let utxo10: UTXO;
740
723
  let utxo11: UTXO;
741
724
  let utxo12: UTXO;
742
725
 
743
726
  before(async function () {
744
- const storage1 = new InMemoryDB(str2Bytes(""));
745
- smtBobForLocked = new Merkletree(storage1, true, 64);
746
-
747
727
  // mint UTXOs for Alice and spend one (to use it in a failure case test)
748
728
  utxo9 = newUTXO(10, Alice);
749
729
  utxo10 = newUTXO(15, Alice);
@@ -775,7 +755,10 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
775
755
  utxo12 = newUTXO(0, Alice);
776
756
 
777
757
  let root = await smtAlice.root();
778
- const proof1 = await smtAlice.generateCircomVerifierProof(utxo10.hash, root);
758
+ const proof1 = await smtAlice.generateCircomVerifierProof(
759
+ utxo10.hash,
760
+ root,
761
+ );
779
762
  const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
780
763
  const merkleProofs = [
781
764
  proof1.siblings.map((s) => s.bigInt()),
@@ -819,34 +802,51 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
819
802
  await smtBob.add(utxo12.hash, utxo12.hash);
820
803
  });
821
804
 
822
- describe("lock -> transfer flow", function () {
823
- it("lock() should succeed when using unlocked states", async function () {
805
+ // createLock -> updateLock -> delegateLock -> spendLock happy path. Note
806
+ // that for KYC the create-lock proof is the standard transfer proof
807
+ // (validates the SMT root and identities tree); the contract transitions
808
+ // the locked output into a flat per-lock mapping. The spendLock proof
809
+ // uses the dedicated transferLocked circuit / verifier wired through the
810
+ // ignition module's lockVerifier slot.
811
+ describe("createLock -> updateLock -> delegateLock -> spendLock flow", function () {
812
+ let lockedUtxo1: UTXO;
813
+ let lockId: string;
814
+ let outputUtxo1: UTXO;
815
+ let outputUtxo2: UTXO;
816
+ let unlockHash: string;
817
+ // Local SMT mirroring on-chain locked-state bookkeeping used by
818
+ // delegateLock tests (`locked()` / storage mirrors).
819
+ let smtForLocked: Merkletree;
820
+
821
+ before(async function () {
822
+ const storage = new InMemoryDB(str2Bytes("kyc-locked-1"));
823
+ smtForLocked = new Merkletree(storage, true, 64);
824
+ });
825
+
826
+ it("createLock() with deterministic lockId computed from txId", async function () {
824
827
  const nullifier1 = newNullifier(utxo11, Bob);
825
828
  lockedUtxo1 = newUTXO(utxo11.value!, Bob);
826
829
  const root = await smtBob.root();
827
- const proof1 = await smtBob.generateCircomVerifierProof(
828
- utxo11.hash,
829
- root,
830
- );
831
- const proof2 = await smtBob.generateCircomVerifierProof(0n, root);
830
+ const p1 = await smtBob.generateCircomVerifierProof(utxo11.hash, root);
831
+ const p2 = await smtBob.generateCircomVerifierProof(0n, root);
832
832
  const merkleProofs = [
833
- proof1.siblings.map((s) => s.bigInt()),
834
- proof2.siblings.map((s) => s.bigInt()),
833
+ p1.siblings.map((s) => s.bigInt()),
834
+ p2.siblings.map((s) => s.bigInt()),
835
835
  ];
836
836
 
837
837
  const identitiesRoot = await smtKyc.root();
838
- const proof3 = await smtKyc.generateCircomVerifierProof(
838
+ const idProofBob = await smtKyc.generateCircomVerifierProof(
839
839
  kycHash(Bob.babyJubPublicKey),
840
840
  identitiesRoot,
841
841
  );
842
- const identitiesMerkleProofs = [
843
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob)
844
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob)
845
- proof3.siblings.map((s) => s.bigInt()), // dummy identity proof for the 2nd owner of the output UTXO
842
+ const identityMerkleProofs = [
843
+ idProofBob.siblings.map((s) => s.bigInt()), // sender (Bob)
844
+ idProofBob.siblings.map((s) => s.bigInt()), // 1st output owner (Bob)
845
+ idProofBob.siblings.map((s) => s.bigInt()), // 2nd output owner (dummy)
846
846
  ];
847
847
 
848
- const { outputCommitments, encodedProof } = await prepareProof(
849
- circuit, // use the regular circuit for the lock() call
848
+ const { encodedProof } = await prepareProof(
849
+ circuit,
850
850
  provingKey,
851
851
  Bob,
852
852
  [utxo11, ZERO_UTXO],
@@ -855,547 +855,565 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
855
855
  root.bigInt(),
856
856
  merkleProofs,
857
857
  identitiesRoot.bigInt(),
858
- identitiesMerkleProofs,
858
+ identityMerkleProofs,
859
859
  [Bob, Bob],
860
860
  );
861
861
 
862
- const tx = await zeto.connect(Bob.signer).lock(
863
- [nullifier1.hash],
864
- [],
865
- outputCommitments,
866
- root.bigInt(),
867
- encodedProof,
868
- Alice.ethAddress, // make Alice the delegate who can spend the state (if she has the right proof)
869
- "0x",
870
- );
871
- const result: ContractTransactionReceipt | null = await tx.wait();
862
+ const txId = randomBytes32();
863
+ const createArgs = encodeCreateArgs({
864
+ txId,
865
+ inputs: [nullifier1.hash],
866
+ outputs: [],
867
+ lockedOutputs: [lockedUtxo1.hash],
868
+ proof: encodeUnlockedProof(root.bigInt(), encodedProof),
869
+ });
870
+
871
+ // Pre-compute lockId off-chain and confirm against the contract.
872
+ const predicted = await zeto
873
+ .connect(Bob.signer)
874
+ .computeLockId(createArgs);
875
+ lockId = predicted;
872
876
 
873
- // Note that the locked UTXO should NOT be added to the local SMT for UTXOs because it's tracked in a separate SMT onchain
874
- // we add it to the local SMT for locked UTXOs
875
- const events = parseUTXOEvents(zeto, result!);
876
- await smtBobForLocked.add(
877
- events[0].lockedOutputs[0],
878
- ethers.toBigInt(events[0].delegate),
877
+ const tx = await zeto
878
+ .connect(Bob.signer)
879
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x");
880
+ const result: ContractTransactionReceipt | null = await tx.wait();
881
+ logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
882
+
883
+ const created = result!.logs
884
+ .map((l) => {
885
+ try {
886
+ return zeto.interface.parseLog(l as any);
887
+ } catch (_e) {
888
+ return null;
889
+ }
890
+ })
891
+ .find((p) => p && p.name === "LockCreated");
892
+ expect(created, "LockCreated event not found").to.not.be.null;
893
+ expect(created!.args.lockId).to.equal(predicted);
894
+ expect(created!.args.owner).to.equal(Bob.ethAddress);
895
+ expect(created!.args.spender).to.equal(Bob.ethAddress);
896
+
897
+ // Mirror the locked UTXO into the local SMT so the locked-circuit
898
+ // witness has a valid inclusion proof to consume.
899
+ await smtForLocked.add(
900
+ lockedUtxo1.hash,
901
+ ethers.toBigInt(Bob.ethAddress),
879
902
  );
880
903
  });
881
904
 
882
- it("onchain SMT root for the locked UTXOs should be equal to the offchain SMT root", async function () {
883
- const root = await smtBobForLocked.root();
884
- const onchainRoot = await zeto.getRootForLocked();
885
- expect(root.string()).to.equal(onchainRoot.toString());
905
+ it("isLockActive() and getLock() reflect the newly created lock", async function () {
906
+ expect(await zeto.isLockActive(lockId)).to.equal(true);
907
+ const info = await zeto.getLock(lockId);
908
+ expect(info.owner).to.equal(Bob.ethAddress);
909
+ expect(info.spender).to.equal(Bob.ethAddress);
910
+ expect(info.spendCommitment).to.equal(ethers.ZeroHash);
911
+ expect(info.cancelCommitment).to.equal(ethers.ZeroHash);
886
912
  });
887
913
 
888
- it("lock() should fail when trying to lock again", async function () {
889
- if (network.name !== "hardhat") {
890
- return;
891
- }
914
+ it("locked() returns true for locked UTXOs and false for unlocked or spent UTXOs", async function () {
915
+ expect(await zeto.locked(lockedUtxo1.hash)).to.deep.equal([
916
+ true,
917
+ Bob.ethAddress,
918
+ ]);
919
+ // utxo12 is an unlocked UTXO that still belongs to Alice.
920
+ expect((await zeto.locked(utxo12.hash))[0]).to.be.false;
921
+ });
892
922
 
893
- // the owner of the locked UTXO can generate the proper proof to spend the locked state,
894
- // but not able to lock again because it's not the current delegate
895
- const nullifier1 = newNullifier(lockedUtxo1, Bob);
896
- const utxo1 = newUTXO(lockedUtxo1.value!, Bob);
897
- const root = await smtBobForLocked.root();
898
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
899
- lockedUtxo1.hash,
900
- root,
901
- );
902
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
903
- 0n,
904
- root,
905
- );
906
- const merkleProofs = [
907
- proof1.siblings.map((s) => s.bigInt()),
908
- proof2.siblings.map((s) => s.bigInt()),
909
- ];
923
+ it("updateLock() commits the spend hash while owner == spender", async function () {
924
+ outputUtxo1 = newUTXO(10, Alice);
925
+ outputUtxo2 = newUTXO(5, Bob);
910
926
 
911
- const identitiesRoot = await smtKyc.root();
912
- const proof3 = await smtKyc.generateCircomVerifierProof(
913
- kycHash(Bob.babyJubPublicKey),
914
- identitiesRoot,
927
+ unlockHash = calculateSpendHash(
928
+ [lockedUtxo1],
929
+ [],
930
+ [outputUtxo1, outputUtxo2],
931
+ "0x",
915
932
  );
916
- const identitiesMerkleProofs = [
917
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob)
918
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob)
919
- proof3.siblings.map((s) => s.bigInt()), // dummy identity proof for the 2nd owner of the output UTXO
920
- ];
921
933
 
922
- const { outputCommitments, encodedProof } = await prepareProof(
923
- circuitForLocked,
924
- provingKeyForLocked,
925
- Bob,
926
- [lockedUtxo1, ZERO_UTXO],
927
- [nullifier1, ZERO_UTXO],
928
- [utxo1, ZERO_UTXO],
929
- root.bigInt(),
930
- merkleProofs,
931
- identitiesRoot.bigInt(),
932
- identitiesMerkleProofs,
933
- [Bob, Bob],
934
- Alice.ethAddress,
935
- );
934
+ const tx = await zeto
935
+ .connect(Bob.signer)
936
+ .updateLock(
937
+ lockId,
938
+ encodeUpdateArgs(randomBytes32()),
939
+ unlockHash,
940
+ ethers.ZeroHash,
941
+ "0x",
942
+ );
943
+ const result: ContractTransactionReceipt | null = await tx.wait();
944
+ logger.debug(`updateLock() complete. Gas used: ${result?.gasUsed}`);
936
945
 
937
- await expect(
938
- zeto
939
- .connect(Bob.signer)
940
- .lock(
941
- [nullifier1.hash],
942
- [],
943
- outputCommitments,
944
- root.bigInt(),
945
- encodedProof,
946
- Charlie.ethAddress,
947
- "0x",
948
- ),
949
- ).to.be.rejectedWith("UTXORootNotFound");
946
+ const info = await zeto.getLock(lockId);
947
+ expect(info.spendCommitment).to.equal(unlockHash);
950
948
  });
951
949
 
952
- it("the original owner can NOT spend the locked state", async function () {
953
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
954
- const nullifier1 = newNullifier(lockedUtxo1, Bob);
955
- const root = await smtBobForLocked.root();
956
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
957
- lockedUtxo1.hash,
958
- root,
959
- );
960
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
961
- 0n,
962
- root,
963
- );
964
- const merkleProofs = [
965
- proof1.siblings.map((s) => s.bigInt()),
966
- proof2.siblings.map((s) => s.bigInt()),
967
- ];
968
- // Bob proposes the output UTXOs, attempting to transfer the locked UTXO to Charlie
969
- const _utxo1 = newUTXO(lockedUtxo1.value!, Charlie);
970
-
971
- const identitiesRoot = await smtKyc.root();
972
- const proof3 = await smtKyc.generateCircomVerifierProof(
973
- kycHash(Bob.babyJubPublicKey),
974
- identitiesRoot,
975
- );
976
- const proof4 = await smtKyc.generateCircomVerifierProof(
977
- kycHash(Charlie.babyJubPublicKey),
978
- identitiesRoot,
979
- );
980
- const identitiesMerkleProofs = [
981
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob)
982
- proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Charlie)
983
- proof3.siblings.map((s) => s.bigInt()), // dummy identity proof for the 2nd owner of the output UTXO
984
- ];
985
-
986
- // Bob should NOT be able to spend the UTXO which has been locked and delegated to Alice
987
- await expect(
988
- doTransfer(
989
- Bob,
990
- [lockedUtxo1, ZERO_UTXO],
991
- [nullifier1, ZERO_UTXO],
992
- [_utxo1, ZERO_UTXO],
993
- root.bigInt(),
994
- merkleProofs,
995
- identitiesRoot.bigInt(),
996
- identitiesMerkleProofs,
997
- [Charlie, Bob],
998
- Alice,
999
- ),
1000
- ).to.be.rejectedWith("Invalid proof"); // due to the lockDelegate part of the public input not matching the current delegate
1001
- });
950
+ it("delegateLock() transfers spending authority to Alice", async function () {
951
+ const tx = await zeto
952
+ .connect(Bob.signer)
953
+ .delegateLock(
954
+ lockId,
955
+ encodeDelegateArgs(randomBytes32()),
956
+ Alice.ethAddress,
957
+ "0x",
958
+ );
959
+ const result: ContractTransactionReceipt | null = await tx.wait();
960
+ logger.debug(`delegateLock() complete. Gas used: ${result?.gasUsed}`);
1002
961
 
1003
- it("the original owner can NOT withdraw the locked state", async function () {
1004
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
1005
- const nullifier1 = newNullifier(lockedUtxo1, Bob);
1006
- const root = await smtBobForLocked.root();
1007
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
962
+ const info = await zeto.getLock(lockId);
963
+ expect(info.spender).to.equal(Alice.ethAddress);
964
+ // Storage-layer delegate must follow the spender.
965
+ expect(await zeto.locked(lockedUtxo1.hash)).to.deep.equal([
966
+ true,
967
+ Alice.ethAddress,
968
+ ]);
969
+ // Mirror the new delegate into the local locked SMT so the spendLock
970
+ // witness's SMT inclusion proof matches the (utxoHash, delegate) leaf.
971
+ await smtForLocked.update(
1008
972
  lockedUtxo1.hash,
1009
- root,
1010
- );
1011
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
1012
- 0n,
1013
- root,
973
+ ethers.toBigInt(Alice.ethAddress),
1014
974
  );
1015
- const merkleProofs = [
1016
- proof1.siblings.map((s) => s.bigInt()),
1017
- proof2.siblings.map((s) => s.bigInt()),
1018
- ];
1019
-
1020
- const utxo1 = newUTXO(5, Bob);
1021
-
1022
- await expect(
1023
- prepareNullifierWithdrawProof(
1024
- Bob,
1025
- [lockedUtxo1, ZERO_UTXO],
1026
- [nullifier1, ZERO_UTXO],
1027
- utxo1,
1028
- root.bigInt(),
1029
- merkleProofs,
1030
- ),
1031
- ).to.be.rejectedWith("SMTVerifier_249 line: 134");
1032
975
  });
1033
976
 
1034
- it("attempting to use an existing UTXO as output should fail", async function () {
1035
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
977
+ it("the new spender can spendLock() with the matching payload", async function () {
978
+ // The locked-input proof is generated against the dedicated
979
+ // transferLocked circuit; identity-tree membership for both the
980
+ // (still-original) sender Bob and each output owner is asserted as
981
+ // part of the snark, mirroring the unlocked transfer flow.
1036
982
  const nullifier1 = newNullifier(lockedUtxo1, Bob);
1037
- const root = await smtBobForLocked.root();
1038
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
1039
- lockedUtxo1.hash,
1040
- root,
1041
- );
1042
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
1043
- 0n,
1044
- root,
1045
- );
1046
- const merkleProofs = [
1047
- proof1.siblings.map((s) => s.bigInt()),
1048
- proof2.siblings.map((s) => s.bigInt()),
1049
- ];
1050
- // Bob proposes the output UTXOs, attempting to transfer the locked UTXO to Alice
1051
- // and reusing an existing UTXO (utxo4)
1052
- const utxo1 = newUTXO(5, Alice);
1053
-
1054
983
  const identitiesRoot = await smtKyc.root();
1055
- const proof3 = await smtKyc.generateCircomVerifierProof(
984
+ const idProofBob = await smtKyc.generateCircomVerifierProof(
1056
985
  kycHash(Bob.babyJubPublicKey),
1057
986
  identitiesRoot,
1058
987
  );
1059
- const proof4 = await smtKyc.generateCircomVerifierProof(
988
+ const idProofAlice = await smtKyc.generateCircomVerifierProof(
1060
989
  kycHash(Alice.babyJubPublicKey),
1061
990
  identitiesRoot,
1062
991
  );
1063
- const identitiesMerkleProofs = [
1064
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob)
1065
- proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice)
1066
- proof4.siblings.map((s) => s.bigInt()), // identity proof for the 2nd owner of the output UTXO (Alice)
992
+ const identityMerkleProofs = [
993
+ idProofBob.siblings.map((s) => s.bigInt()), // sender (Bob)
994
+ idProofAlice.siblings.map((s) => s.bigInt()), // 1st output owner (Alice)
995
+ idProofBob.siblings.map((s) => s.bigInt()), // 2nd output owner (Bob)
1067
996
  ];
1068
997
 
1069
- const result = await prepareProof(
998
+ const { encodedProof } = await prepareProof(
1070
999
  circuitForLocked,
1071
1000
  provingKeyForLocked,
1072
1001
  Bob,
1073
1002
  [lockedUtxo1, ZERO_UTXO],
1074
1003
  [nullifier1, ZERO_UTXO],
1075
- [utxo9, utxo1],
1076
- root.bigInt(),
1077
- merkleProofs,
1004
+ [outputUtxo1, outputUtxo2],
1005
+ 0n,
1006
+ [],
1078
1007
  identitiesRoot.bigInt(),
1079
- identitiesMerkleProofs,
1080
- [Alice, Alice],
1081
- Alice.ethAddress, // current lock delegate
1008
+ identityMerkleProofs,
1009
+ [Alice, Bob],
1010
+ undefined,
1011
+ { transferLocked: true },
1082
1012
  );
1083
- const nullifiers = [nullifier1.hash];
1084
1013
 
1085
- // Alice (in reality this is usually a contract that orchestrates a trade flow) can spend the locked state
1086
- // using the proof generated by the trade counterparty (Bob in this case)
1087
- await expect(
1088
- sendTx(
1089
- Alice,
1090
- nullifiers,
1091
- result.outputCommitments,
1092
- root.bigInt(),
1093
- result.encodedProof,
1094
- true,
1095
- ),
1096
- ).to.be.rejectedWith(`UTXOAlreadyOwned(${utxo9.hash.toString()})`);
1014
+ const spendArgs = encodeSpendArgs({
1015
+ txId: randomBytes32(),
1016
+ lockedOutputs: [],
1017
+ outputs: [outputUtxo1.hash, outputUtxo2.hash],
1018
+ proof: encodeLockedProof(encodedProof),
1019
+ data: "0x",
1020
+ });
1021
+
1022
+ const tx = await zeto
1023
+ .connect(Alice.signer)
1024
+ .spendLock(lockId, spendArgs, "0x");
1025
+ const result = await tx.wait();
1026
+
1027
+ const parsed = result!.logs
1028
+ .map((l) => {
1029
+ try {
1030
+ return zeto.interface.parseLog(l as any);
1031
+ } catch (_e) {
1032
+ return null;
1033
+ }
1034
+ })
1035
+ .filter((p) => p !== null) as ReadonlyArray<{
1036
+ name: string;
1037
+ args: any;
1038
+ }>;
1039
+ const lockSpent = parsed.find((p) => p.name === "LockSpent");
1040
+ const zetoLockSpent = parsed.find((p) => p.name === "ZetoLockSpent");
1041
+ expect(lockSpent, "LockSpent event not emitted").to.not.be.undefined;
1042
+ expect(zetoLockSpent, "ZetoLockSpent event not emitted").to.not.be
1043
+ .undefined;
1044
+ expect(lockSpent!.args.lockId).to.equal(lockId);
1045
+ expect(lockSpent!.args.spender).to.equal(Alice.ethAddress);
1046
+
1047
+ const outputs = zetoLockSpent!.args.outputs;
1048
+ await smtAlice.add(outputs[0], outputs[0]);
1049
+ await smtAlice.add(outputs[1], outputs[1]);
1050
+ await smtBob.add(outputs[0], outputs[0]);
1051
+ await smtBob.add(outputs[1], outputs[1]);
1052
+
1053
+ // Lock is no longer active after spendLock().
1054
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
1097
1055
  });
1098
1056
 
1099
- it("attempting to use a spent UTXO as output should fail", async function () {
1100
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
1101
- const nullifier1 = newNullifier(lockedUtxo1, Bob);
1102
- const root = await smtBobForLocked.root();
1103
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
1104
- lockedUtxo1.hash,
1105
- root,
1106
- );
1107
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
1108
- 0n,
1057
+ it("onchain SMT root for the unlocked UTXOs equals the offchain SMT root", async function () {
1058
+ const bobRoot = await smtBob.root();
1059
+ const aliceRoot = await smtAlice.root();
1060
+ const onchainRoot = await zeto.getRoot();
1061
+ expect(bobRoot.string()).to.equal(onchainRoot.toString());
1062
+ expect(aliceRoot.string()).to.equal(onchainRoot.toString());
1063
+ });
1064
+ });
1065
+
1066
+ // createLock with a non-zero cancelCommitment, then cancelLock by the
1067
+ // owner without going through delegateLock. Cancelling a lock follows
1068
+ // the same proof-encoding rules as spendLock (locked-input verifier,
1069
+ // bare Commonlib.Proof) but is gated by the cancelCommitment hash.
1070
+ describe("createLock -> cancelLock flow", function () {
1071
+ let lockedUtxo: UTXO;
1072
+ let lockId: string;
1073
+ let cancelHash: string;
1074
+ let outUtxo1: UTXO;
1075
+ let outUtxo2: UTXO;
1076
+ let aliceUtxoForLock: UTXO;
1077
+ let smtForLocked: Merkletree;
1078
+
1079
+ before(async function () {
1080
+ // Mint a fresh source UTXO for Alice so this scope can build its own
1081
+ // lock without colliding with the prior describe block's spend flow.
1082
+ aliceUtxoForLock = newUTXO(50, Alice);
1083
+ await doMint(zeto, deployer, [aliceUtxoForLock]);
1084
+ await smtAlice.add(aliceUtxoForLock.hash, aliceUtxoForLock.hash);
1085
+ await smtBob.add(aliceUtxoForLock.hash, aliceUtxoForLock.hash);
1086
+ const storage = new InMemoryDB(str2Bytes("kyc-locked-2"));
1087
+ smtForLocked = new Merkletree(storage, true, 64);
1088
+ });
1089
+
1090
+ it("Alice createLock() with a cancelCommitment", async function () {
1091
+ const nullifier1 = newNullifier(aliceUtxoForLock, Alice);
1092
+ lockedUtxo = newUTXO(aliceUtxoForLock.value!, Alice);
1093
+ const root = await smtAlice.root();
1094
+ const p1 = await smtAlice.generateCircomVerifierProof(
1095
+ aliceUtxoForLock.hash,
1109
1096
  root,
1110
1097
  );
1098
+ const p2 = await smtAlice.generateCircomVerifierProof(0n, root);
1111
1099
  const merkleProofs = [
1112
- proof1.siblings.map((s) => s.bigInt()),
1113
- proof2.siblings.map((s) => s.bigInt()),
1100
+ p1.siblings.map((s) => s.bigInt()),
1101
+ p2.siblings.map((s) => s.bigInt()),
1114
1102
  ];
1115
- // Bob proposes the output UTXOs, attempting to transfer the locked UTXO to Alice
1116
- // and reusing an unlocked spent UTXO (utxo4)
1117
- const utxo1 = newUTXO(0, Alice);
1118
1103
 
1119
1104
  const identitiesRoot = await smtKyc.root();
1120
- const proof3 = await smtKyc.generateCircomVerifierProof(
1121
- kycHash(Bob.babyJubPublicKey),
1122
- identitiesRoot,
1123
- );
1124
- const proof4 = await smtKyc.generateCircomVerifierProof(
1105
+ const idProofAlice = await smtKyc.generateCircomVerifierProof(
1125
1106
  kycHash(Alice.babyJubPublicKey),
1126
1107
  identitiesRoot,
1127
1108
  );
1128
- const identitiesMerkleProofs = [
1129
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob)
1130
- proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice)
1131
- proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice)
1109
+ const identityMerkleProofs = [
1110
+ idProofAlice.siblings.map((s) => s.bigInt()),
1111
+ idProofAlice.siblings.map((s) => s.bigInt()),
1112
+ idProofAlice.siblings.map((s) => s.bigInt()),
1132
1113
  ];
1133
1114
 
1134
- const result = await prepareProof(
1135
- circuitForLocked,
1136
- provingKeyForLocked,
1137
- Bob,
1138
- [lockedUtxo1, ZERO_UTXO],
1115
+ const { encodedProof } = await prepareProof(
1116
+ circuit,
1117
+ provingKey,
1118
+ Alice,
1119
+ [aliceUtxoForLock, ZERO_UTXO],
1139
1120
  [nullifier1, ZERO_UTXO],
1140
- [utxo10, utxo1],
1121
+ [lockedUtxo, ZERO_UTXO],
1141
1122
  root.bigInt(),
1142
1123
  merkleProofs,
1143
1124
  identitiesRoot.bigInt(),
1144
- identitiesMerkleProofs,
1125
+ identityMerkleProofs,
1145
1126
  [Alice, Alice],
1146
- Alice.ethAddress, // current lock delegate
1147
1127
  );
1148
- const nullifiers = [nullifier1.hash];
1149
-
1150
- // Alice (in reality this is usually a contract that orchestrates a trade flow) can spend the locked state
1151
- // using the proof generated by the trade counterparty (Bob in this case)
1152
- await expect(
1153
- sendTx(
1154
- Alice,
1155
- nullifiers,
1156
- result.outputCommitments,
1157
- root.bigInt(),
1158
- result.encodedProof,
1159
- true,
1160
- ),
1161
- ).to.be.rejectedWith(`UTXOAlreadyOwned(${utxo10.hash.toString()})`);
1162
- });
1163
1128
 
1164
- it("the designated delegate can use the proper proof to spend the locked state", async function () {
1165
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
1166
- const nullifier1 = newNullifier(lockedUtxo1, Bob);
1167
- const root = await smtBobForLocked.root();
1168
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
1169
- lockedUtxo1.hash,
1170
- root,
1129
+ outUtxo1 = newUTXO(20, Bob);
1130
+ outUtxo2 = newUTXO(30, Alice);
1131
+ cancelHash = calculateCancelHash(
1132
+ [lockedUtxo],
1133
+ [],
1134
+ [outUtxo1, outUtxo2],
1135
+ "0x",
1171
1136
  );
1172
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
1173
- 0n,
1174
- root,
1137
+
1138
+ const createArgs = encodeCreateArgs({
1139
+ txId: randomBytes32(),
1140
+ inputs: [nullifier1.hash],
1141
+ outputs: [],
1142
+ lockedOutputs: [lockedUtxo.hash],
1143
+ proof: encodeUnlockedProof(root.bigInt(), encodedProof),
1144
+ });
1145
+ lockId = await zeto.connect(Alice.signer).computeLockId(createArgs);
1146
+
1147
+ const tx = await zeto
1148
+ .connect(Alice.signer)
1149
+ .createLock(createArgs, ethers.ZeroHash, cancelHash, "0x");
1150
+ const result: ContractTransactionReceipt | null = await tx.wait();
1151
+ logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
1152
+
1153
+ // Mirror the locked UTXO so the cancel-side witness can prove inclusion.
1154
+ await smtForLocked.add(
1155
+ lockedUtxo.hash,
1156
+ ethers.toBigInt(Alice.ethAddress),
1175
1157
  );
1176
- const merkleProofs = [
1177
- proof1.siblings.map((s) => s.bigInt()),
1178
- proof2.siblings.map((s) => s.bigInt()),
1179
- ];
1180
- // Bob proposes the output UTXOs, attempting to transfer the locked UTXO to Alice
1181
- const utxo1 = newUTXO(10, Alice);
1182
- const utxo2 = newUTXO(5, Bob);
1158
+ });
1183
1159
 
1160
+ it("the owner can cancelLock() to reverse the lock without delegation", async function () {
1161
+ const nullifier1 = newNullifier(lockedUtxo, Alice);
1184
1162
  const identitiesRoot = await smtKyc.root();
1185
- const proof3 = await smtKyc.generateCircomVerifierProof(
1186
- kycHash(Bob.babyJubPublicKey),
1163
+ const idProofAlice = await smtKyc.generateCircomVerifierProof(
1164
+ kycHash(Alice.babyJubPublicKey),
1187
1165
  identitiesRoot,
1188
1166
  );
1189
- const proof4 = await smtKyc.generateCircomVerifierProof(
1190
- kycHash(Alice.babyJubPublicKey),
1167
+ const idProofBob = await smtKyc.generateCircomVerifierProof(
1168
+ kycHash(Bob.babyJubPublicKey),
1191
1169
  identitiesRoot,
1192
1170
  );
1193
- const identitiesMerkleProofs = [
1194
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob)
1195
- proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice)
1196
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice)
1171
+ const identityMerkleProofs = [
1172
+ idProofAlice.siblings.map((s) => s.bigInt()), // sender (Alice)
1173
+ idProofBob.siblings.map((s) => s.bigInt()), // 1st output owner (Bob)
1174
+ idProofAlice.siblings.map((s) => s.bigInt()), // 2nd output owner (Alice)
1197
1175
  ];
1198
1176
 
1199
- const result = await prepareProof(
1177
+ const { encodedProof } = await prepareProof(
1200
1178
  circuitForLocked,
1201
1179
  provingKeyForLocked,
1202
- Bob,
1203
- [lockedUtxo1, ZERO_UTXO],
1180
+ Alice,
1181
+ [lockedUtxo, ZERO_UTXO],
1204
1182
  [nullifier1, ZERO_UTXO],
1205
- [utxo1, utxo2],
1206
- root.bigInt(),
1207
- merkleProofs,
1183
+ [outUtxo1, outUtxo2],
1184
+ 0n,
1185
+ [],
1208
1186
  identitiesRoot.bigInt(),
1209
- identitiesMerkleProofs,
1210
- [Alice, Bob],
1211
- Alice.ethAddress, // current lock delegate
1187
+ identityMerkleProofs,
1188
+ [Bob, Alice],
1189
+ undefined,
1190
+ { transferLocked: true },
1212
1191
  );
1213
- const nullifiers = [nullifier1.hash];
1214
-
1215
- // Alice (in reality this is usually a contract that orchestrates a trade flow) can spend the locked state
1216
- // using the proof generated by the trade counterparty (Bob in this case)
1217
- await expect(
1218
- sendTx(
1219
- Alice,
1220
- nullifiers,
1221
- result.outputCommitments,
1222
- root.bigInt(),
1223
- result.encodedProof,
1224
- true,
1225
- ),
1226
- ).to.be.fulfilled;
1227
1192
 
1228
- // Alice and Bob keep the local SMT in sync
1229
- await smtAlice.add(utxo1.hash, utxo1.hash);
1230
- await smtAlice.add(utxo2.hash, utxo2.hash);
1231
- await smtBob.add(utxo1.hash, utxo1.hash);
1232
- await smtBob.add(utxo2.hash, utxo2.hash);
1233
- });
1193
+ const cancelArgs = encodeSpendArgs({
1194
+ txId: randomBytes32(),
1195
+ lockedOutputs: [],
1196
+ outputs: [outUtxo1.hash, outUtxo2.hash],
1197
+ proof: encodeLockedProof(encodedProof),
1198
+ data: "0x",
1199
+ });
1234
1200
 
1235
- it("onchain SMT root for the locked UTXOs should be equal to the offchain SMT root", async function () {
1236
- const root = await smtBobForLocked.root();
1237
- const onchainRoot = await zeto.getRootForLocked();
1238
- expect(root.string()).to.equal(onchainRoot.toString());
1239
- });
1201
+ const tx = await zeto
1202
+ .connect(Alice.signer)
1203
+ .cancelLock(lockId, cancelArgs, "0x");
1204
+ const result = await tx.wait();
1240
1205
 
1241
- it("onchain SMT root for the unlocked UTXOs should be equal to the offchain SMT root", async function () {
1242
- const root = await smtBob.root();
1243
- const onchainRoot = await zeto.getRoot();
1244
- expect(root.string()).to.equal(onchainRoot.toString());
1206
+ const parsed = result!.logs
1207
+ .map((l) => {
1208
+ try {
1209
+ return zeto.interface.parseLog(l as any);
1210
+ } catch (_e) {
1211
+ return null;
1212
+ }
1213
+ })
1214
+ .filter((p) => p !== null) as ReadonlyArray<{
1215
+ name: string;
1216
+ args: any;
1217
+ }>;
1218
+ const cancelled = parsed.find((p) => p.name === "LockCancelled");
1219
+ const zetoCancelled = parsed.find((p) => p.name === "ZetoLockCancelled");
1220
+ expect(cancelled, "LockCancelled event not emitted").to.not.be.undefined;
1221
+ expect(zetoCancelled, "ZetoLockCancelled event not emitted").to.not.be
1222
+ .undefined;
1223
+
1224
+ const outputs = zetoCancelled!.args.outputs;
1225
+ await smtAlice.add(outputs[0], outputs[0]);
1226
+ await smtAlice.add(outputs[1], outputs[1]);
1227
+ await smtBob.add(outputs[0], outputs[0]);
1228
+ await smtBob.add(outputs[1], outputs[1]);
1229
+
1230
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
1245
1231
  });
1246
1232
  });
1247
1233
 
1248
- describe("lock -> delegate -> transfer flow", function () {
1249
- let lockedUtxo2: UTXO;
1234
+ // Negative cases for the lock lifecycle. These rely on hardhat's revert
1235
+ // decoding for custom errors and so are gated by the network name.
1236
+ describe("negative cases for the lock lifecycle", function () {
1237
+ if (network.name !== "hardhat") {
1238
+ return;
1239
+ }
1250
1240
 
1251
- it("Alice locks a UTXO and makes Bob the delegate ", async function () {
1252
- const nullifier1 = newNullifier(utxo9, Alice);
1253
- lockedUtxo2 = newUTXO(utxo9.value!, Alice);
1254
- const root = await smtAlice.root();
1255
- const proof1 = await smtAlice.generateCircomVerifierProof(
1256
- utxo9.hash,
1241
+ // freshLock mints a fresh UTXO for `owner`, locks it, and returns the
1242
+ // metadata so each negative scope can branch off without polluting
1243
+ // siblings. Each call advances both local SMTs.
1244
+ async function freshLock(
1245
+ owner: User,
1246
+ spendCommitment: string = ethers.ZeroHash,
1247
+ cancelCommitment: string = ethers.ZeroHash,
1248
+ ): Promise<{
1249
+ lockId: string;
1250
+ sourceUtxo: UTXO;
1251
+ lockedUtxo: UTXO;
1252
+ createArgs: string;
1253
+ }> {
1254
+ const sourceUtxo = newUTXO(7, owner);
1255
+ await doMint(zeto, deployer, [sourceUtxo]);
1256
+ await smtAlice.add(sourceUtxo.hash, sourceUtxo.hash);
1257
+ await smtBob.add(sourceUtxo.hash, sourceUtxo.hash);
1258
+
1259
+ const nullifier = newNullifier(sourceUtxo, owner);
1260
+ const lockedUtxo = newUTXO(sourceUtxo.value!, owner);
1261
+ const root = await smtBob.root();
1262
+ const p1 = await smtBob.generateCircomVerifierProof(
1263
+ sourceUtxo.hash,
1257
1264
  root,
1258
1265
  );
1259
- const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
1266
+ const p2 = await smtBob.generateCircomVerifierProof(0n, root);
1260
1267
  const merkleProofs = [
1261
- proof1.siblings.map((s) => s.bigInt()),
1262
- proof2.siblings.map((s) => s.bigInt()),
1268
+ p1.siblings.map((s) => s.bigInt()),
1269
+ p2.siblings.map((s) => s.bigInt()),
1263
1270
  ];
1264
1271
 
1265
1272
  const identitiesRoot = await smtKyc.root();
1266
- const proof3 = await smtKyc.generateCircomVerifierProof(
1267
- kycHash(Alice.babyJubPublicKey),
1273
+ const idProofOwner = await smtKyc.generateCircomVerifierProof(
1274
+ kycHash(owner.babyJubPublicKey),
1268
1275
  identitiesRoot,
1269
1276
  );
1270
- const identitiesMerkleProofs = [
1271
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob)
1272
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice)
1273
- proof3.siblings.map((s) => s.bigInt()), // dummy identity proof for the 2nd owner of the output UTXO
1277
+ const identityMerkleProofs = [
1278
+ idProofOwner.siblings.map((s) => s.bigInt()),
1279
+ idProofOwner.siblings.map((s) => s.bigInt()),
1280
+ idProofOwner.siblings.map((s) => s.bigInt()),
1274
1281
  ];
1275
1282
 
1276
- const { outputCommitments, encodedProof } = await prepareProof(
1283
+ const { encodedProof } = await prepareProof(
1277
1284
  circuit,
1278
1285
  provingKey,
1279
- Alice,
1280
- [utxo9, ZERO_UTXO],
1281
- [nullifier1, ZERO_UTXO],
1282
- [lockedUtxo2, ZERO_UTXO],
1286
+ owner,
1287
+ [sourceUtxo, ZERO_UTXO],
1288
+ [nullifier, ZERO_UTXO],
1289
+ [lockedUtxo, ZERO_UTXO],
1283
1290
  root.bigInt(),
1284
1291
  merkleProofs,
1285
1292
  identitiesRoot.bigInt(),
1286
- identitiesMerkleProofs,
1287
- [Alice, Alice],
1293
+ identityMerkleProofs,
1294
+ [owner, owner],
1288
1295
  );
1289
1296
 
1290
- const tx = await zeto.connect(Bob.signer).lock(
1291
- [nullifier1.hash],
1292
- [],
1293
- outputCommitments,
1294
- root.bigInt(),
1295
- encodedProof,
1296
- Bob.ethAddress, // make Bob the delegate who can spend the state (if he has the right proof)
1297
- "0x",
1298
- );
1299
- const result: ContractTransactionReceipt | null = await tx.wait();
1297
+ const createArgs = encodeCreateArgs({
1298
+ txId: randomBytes32(),
1299
+ inputs: [nullifier.hash],
1300
+ outputs: [],
1301
+ lockedOutputs: [lockedUtxo.hash],
1302
+ proof: encodeUnlockedProof(root.bigInt(), encodedProof),
1303
+ });
1304
+ const lockId = await zeto
1305
+ .connect(owner.signer)
1306
+ .computeLockId(createArgs);
1307
+ await (
1308
+ await zeto
1309
+ .connect(owner.signer)
1310
+ .createLock(createArgs, spendCommitment, cancelCommitment, "0x")
1311
+ ).wait();
1312
+ return { lockId, sourceUtxo, lockedUtxo, createArgs };
1313
+ }
1300
1314
 
1301
- // Note that the locked UTXO should NOT be added to the local SMT for UTXOs because it's tracked in a separate SMT onchain
1302
- // we add it to the local SMT for locked UTXOs
1303
- const events = parseUTXOEvents(zeto, result!);
1304
- await smtBobForLocked.add(
1305
- events[0].lockedOutputs[0],
1306
- ethers.toBigInt(events[0].delegate),
1307
- );
1315
+ // Authorization / immutability assertions short-circuit before the
1316
+ // proof is ever touched, so we can pass a syntactically valid but
1317
+ // ZK-vacuous spend payload.
1318
+ function dummySpendArgs(): string {
1319
+ return encodeSpendArgs({
1320
+ txId: randomBytes32(),
1321
+ lockedOutputs: [],
1322
+ outputs: [],
1323
+ proof: "0x",
1324
+ data: "0x",
1325
+ });
1326
+ }
1327
+
1328
+ it("createLock() with a duplicate txId from the same caller reverts with DuplicateLock", async function () {
1329
+ const { lockId, createArgs } = await freshLock(Bob);
1330
+
1331
+ await expect(
1332
+ zeto
1333
+ .connect(Bob.signer)
1334
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x"),
1335
+ ).rejectedWith(`DuplicateLock("${lockId}")`);
1308
1336
  });
1309
1337
 
1310
- it("Bob delegates the lock to Charlie", async function () {
1311
- const tx = await zeto
1312
- .connect(Bob.signer)
1313
- .delegateLock([lockedUtxo2.hash], Charlie.ethAddress, "0x");
1314
- const result = await tx.wait();
1315
- const events = parseUTXOEvents(zeto, result);
1316
- // this should update the existing leaf node value from address of Alice to Charlie
1317
- await smtBobForLocked.update(
1318
- events[0].lockedOutputs[0],
1319
- ethers.toBigInt(events[0].newDelegate),
1320
- );
1338
+ it("updateLock() by a non-owner reverts with LockUnauthorized", async function () {
1339
+ const { lockId } = await freshLock(Bob);
1340
+
1341
+ await expect(
1342
+ zeto
1343
+ .connect(Alice.signer)
1344
+ .updateLock(
1345
+ lockId,
1346
+ encodeUpdateArgs(randomBytes32()),
1347
+ ethers.ZeroHash,
1348
+ ethers.ZeroHash,
1349
+ "0x",
1350
+ ),
1351
+ ).to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1352
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1321
1353
  });
1322
1354
 
1323
- it("onchain SMT root for the locked UTXOs should be equal to the offchain SMT root", async function () {
1324
- const root = await smtBobForLocked.root();
1325
- const onchainRoot = await zeto.getRootForLocked();
1326
- expect(root.string()).to.equal(onchainRoot.toString());
1355
+ it("updateLock() after delegateLock() reverts with LockImmutable", async function () {
1356
+ const { lockId } = await freshLock(Bob);
1357
+
1358
+ await (
1359
+ await zeto
1360
+ .connect(Bob.signer)
1361
+ .delegateLock(
1362
+ lockId,
1363
+ encodeDelegateArgs(randomBytes32()),
1364
+ Alice.ethAddress,
1365
+ "0x",
1366
+ )
1367
+ ).wait();
1368
+
1369
+ await expect(
1370
+ zeto
1371
+ .connect(Bob.signer)
1372
+ .updateLock(
1373
+ lockId,
1374
+ encodeUpdateArgs(randomBytes32()),
1375
+ ethers.ZeroHash,
1376
+ ethers.ZeroHash,
1377
+ "0x",
1378
+ ),
1379
+ ).rejectedWith(`LockImmutable("${lockId}")`);
1327
1380
  });
1328
1381
 
1329
- it("Charlie can use the proper proof to spend the locked state", async function () {
1330
- // Alice generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
1331
- const nullifier1 = newNullifier(lockedUtxo2, Alice);
1332
- // borrowing the SMT from Bob to save test code duplication
1333
- const root = await smtBobForLocked.root();
1334
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
1335
- lockedUtxo2.hash,
1336
- root,
1337
- );
1338
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
1339
- 0n,
1340
- root,
1341
- );
1342
- const merkleProofs = [
1343
- proof1.siblings.map((s) => s.bigInt()),
1344
- proof2.siblings.map((s) => s.bigInt()),
1345
- ];
1346
- // Bob proposes the output UTXOs, attempting to transfer the locked UTXO to Alice
1347
- const utxo1 = newUTXO(1, Bob);
1348
- const utxo2 = newUTXO(9, Alice);
1382
+ it("delegateLock() by a non-spender reverts with LockUnauthorized", async function () {
1383
+ const { lockId } = await freshLock(Bob);
1349
1384
 
1350
- const identitiesRoot = await smtKyc.root();
1351
- const proof3 = await smtKyc.generateCircomVerifierProof(
1352
- kycHash(Alice.babyJubPublicKey),
1353
- identitiesRoot,
1354
- );
1355
- const proof4 = await smtKyc.generateCircomVerifierProof(
1356
- kycHash(Bob.babyJubPublicKey),
1357
- identitiesRoot,
1358
- );
1359
- const identitiesMerkleProofs = [
1360
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Alice)
1361
- proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob)
1362
- proof3.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice)
1363
- ];
1385
+ await expect(
1386
+ zeto
1387
+ .connect(Alice.signer)
1388
+ .delegateLock(
1389
+ lockId,
1390
+ encodeDelegateArgs(randomBytes32()),
1391
+ Charlie.ethAddress,
1392
+ "0x",
1393
+ ),
1394
+ )
1395
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1396
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1397
+ });
1364
1398
 
1365
- const result = await prepareProof(
1366
- circuitForLocked,
1367
- provingKeyForLocked,
1368
- Alice,
1369
- [lockedUtxo2, ZERO_UTXO],
1370
- [nullifier1, ZERO_UTXO],
1371
- [utxo1, utxo2],
1372
- root.bigInt(),
1373
- merkleProofs,
1374
- identitiesRoot.bigInt(),
1375
- identitiesMerkleProofs,
1376
- [Bob, Alice],
1377
- Charlie.ethAddress, // current lock delegate
1378
- );
1379
- const nullifiers = [nullifier1.hash];
1399
+ it("spendLock() by a non-spender reverts with LockUnauthorized before touching the proof", async function () {
1400
+ const { lockId } = await freshLock(Bob);
1380
1401
 
1381
- // Charlie (in reality this is usually a contract that orchestrates a trade flow) can spend the locked state
1382
- // using the proof generated by the trade counterparty (Bob in this case)
1383
1402
  await expect(
1384
- sendTx(
1385
- Charlie, // Charlie must sign this transaction as he is the delegate now
1386
- nullifiers,
1387
- result.outputCommitments,
1388
- root.bigInt(),
1389
- result.encodedProof,
1390
- true,
1391
- ),
1392
- ).to.be.fulfilled;
1403
+ zeto.connect(Alice.signer).spendLock(lockId, dummySpendArgs(), "0x"),
1404
+ )
1405
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1406
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1407
+ });
1408
+
1409
+ it("cancelLock() by a non-spender reverts with LockUnauthorized", async function () {
1410
+ const { lockId } = await freshLock(Bob);
1393
1411
 
1394
- // Alice and Bob keep the local SMT in sync
1395
- await smtAlice.add(utxo1.hash, utxo1.hash);
1396
- await smtAlice.add(utxo2.hash, utxo2.hash);
1397
- await smtBob.add(utxo1.hash, utxo1.hash);
1398
- await smtBob.add(utxo2.hash, utxo2.hash);
1412
+ await expect(
1413
+ zeto.connect(Alice.signer).cancelLock(lockId, dummySpendArgs(), "0x"),
1414
+ )
1415
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1416
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1399
1417
  });
1400
1418
  });
1401
1419
  });
@@ -1443,8 +1461,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1443
1461
  10,
1444
1462
  nullifiers,
1445
1463
  outputCommitments[0],
1446
- root.bigInt(),
1447
- encodedProof,
1464
+ encodeToBytesForWithdraw(root.bigInt(), encodedProof),
1448
1465
  "0x",
1449
1466
  ),
1450
1467
  ).rejectedWith("UTXOAlreadySpent");
@@ -1692,7 +1709,6 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1692
1709
  identitiesRoot: BigInt,
1693
1710
  identitiesMerkleProof: BigInt[][],
1694
1711
  owners: User[],
1695
- lockDelegate?: User,
1696
1712
  ) {
1697
1713
  let nullifiers: BigNumberish[];
1698
1714
  let outputCommitments: BigNumberish[];
@@ -1704,11 +1720,6 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1704
1720
  circuitToUse = batchCircuit;
1705
1721
  provingKeyToUse = batchProvingKey;
1706
1722
  }
1707
- if (lockDelegate) {
1708
- // if there is a lock delegate, we use the circuit for locked UTXOs
1709
- circuitToUse = circuitForLocked;
1710
- provingKeyToUse = provingKeyForLocked;
1711
- }
1712
1723
  const result = await prepareProof(
1713
1724
  circuitToUse,
1714
1725
  provingKeyToUse,
@@ -1721,7 +1732,6 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1721
1732
  identitiesRoot,
1722
1733
  identitiesMerkleProof,
1723
1734
  owners,
1724
- lockDelegate?.ethAddress,
1725
1735
  );
1726
1736
  nullifiers = _nullifiers.map((nullifier) => nullifier.hash) as [
1727
1737
  BigNumberish,
@@ -1736,7 +1746,6 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1736
1746
  outputCommitments,
1737
1747
  utxosRoot,
1738
1748
  encodedProof,
1739
- lockDelegate !== undefined,
1740
1749
  );
1741
1750
  return {
1742
1751
  txResult,
@@ -1756,6 +1765,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1756
1765
  identitiesMerkleProof: BigInt[][],
1757
1766
  owners: User[],
1758
1767
  lockDelegate?: string,
1768
+ opts?: { transferLocked?: boolean },
1759
1769
  ) {
1760
1770
  const nullifiers = _nullifiers.map((nullifier) => nullifier.hash) as [
1761
1771
  BigNumberish,
@@ -1774,24 +1784,41 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1774
1784
  (owner) => owner.babyJubPublicKey,
1775
1785
  ) as BigNumberish[][];
1776
1786
  const startWitnessCalculation = Date.now();
1777
- const inputObj: any = {
1778
- nullifiers,
1779
- inputCommitments,
1780
- inputValues,
1781
- inputSalts,
1782
- inputOwnerPrivateKey: signer.formattedPrivateKey,
1783
- utxosRoot,
1784
- enabled: nullifiers.map((n) => (n !== 0n ? 1 : 0)),
1785
- utxosMerkleProof,
1786
- identitiesRoot,
1787
- identitiesMerkleProof,
1788
- outputCommitments,
1789
- outputValues,
1790
- outputSalts: outputs.map((output) => output.salt || 0n),
1791
- outputOwnerPublicKeys,
1792
- };
1793
- if (lockDelegate) {
1794
- inputObj["lockDelegate"] = ethers.toBigInt(lockDelegate);
1787
+
1788
+ let inputObj: any;
1789
+ if (opts?.transferLocked) {
1790
+ inputObj = {
1791
+ inputCommitments,
1792
+ inputValues,
1793
+ inputSalts,
1794
+ inputOwnerPrivateKey: signer.formattedPrivateKey,
1795
+ identitiesRoot,
1796
+ identitiesMerkleProof,
1797
+ outputCommitments,
1798
+ outputValues,
1799
+ outputSalts: outputs.map((output) => output.salt || 0n),
1800
+ outputOwnerPublicKeys,
1801
+ };
1802
+ } else {
1803
+ inputObj = {
1804
+ nullifiers,
1805
+ inputCommitments,
1806
+ inputValues,
1807
+ inputSalts,
1808
+ inputOwnerPrivateKey: signer.formattedPrivateKey,
1809
+ utxosRoot,
1810
+ enabled: nullifiers.map((n) => (n !== 0n ? 1 : 0)),
1811
+ utxosMerkleProof,
1812
+ identitiesRoot,
1813
+ identitiesMerkleProof,
1814
+ outputCommitments,
1815
+ outputValues,
1816
+ outputSalts: outputs.map((output) => output.salt || 0n),
1817
+ outputOwnerPublicKeys,
1818
+ };
1819
+ if (lockDelegate) {
1820
+ inputObj["lockDelegate"] = ethers.toBigInt(lockDelegate);
1821
+ }
1795
1822
  }
1796
1823
 
1797
1824
  const witness = await circuit.calculateWTNSBin(inputObj, true);
@@ -1822,27 +1849,14 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1822
1849
  outputCommitments: BigNumberish[],
1823
1850
  root: BigNumberish,
1824
1851
  encodedProof: any,
1825
- isLocked: boolean = false,
1826
1852
  ) {
1827
1853
  const startTx = Date.now();
1828
- let tx: any;
1829
- if (!isLocked) {
1830
- tx = await zeto.connect(signer.signer).transfer(
1831
- nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works
1832
- outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works
1833
- root,
1834
- encodedProof,
1835
- "0x",
1836
- );
1837
- } else {
1838
- tx = await zeto.connect(signer.signer).transferLocked(
1839
- nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works
1840
- outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works
1841
- root,
1842
- encodedProof,
1843
- "0x",
1844
- );
1845
- }
1854
+ const tx = await zeto.connect(signer.signer).transfer(
1855
+ nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works
1856
+ outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works
1857
+ encodeToBytes(root, encodedProof),
1858
+ "0x",
1859
+ );
1846
1860
  const results: ContractTransactionReceipt | null = await tx.wait();
1847
1861
  console.log(
1848
1862
  `Time to execute transaction: ${Date.now() - startTx}ms. Gas used: ${results?.gasUsed
@@ -1851,3 +1865,10 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1851
1865
  return results;
1852
1866
  }
1853
1867
  });
1868
+
1869
+ function encodeToBytes(root: any, proof: any) {
1870
+ return new AbiCoder().encode(
1871
+ ["uint256 root", "tuple(uint256[2] pA, uint256[2][2] pB, uint256[2] pC)"],
1872
+ [root, proof],
1873
+ );
1874
+ }