@lfdecentralizedtrust/zeto-contracts 0.2.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +70 -162
  2. package/contracts/factory.sol +30 -34
  3. package/contracts/factory_upgradeable.sol +160 -0
  4. package/contracts/lib/common/util.sol +40 -0
  5. package/contracts/lib/interfaces/ILockableCapability.sol +318 -0
  6. package/contracts/lib/interfaces/{izeto.sol → IZeto.sol} +13 -1
  7. package/contracts/lib/interfaces/{izeto_initializable.sol → IZetoInitializable.sol} +14 -12
  8. package/contracts/lib/interfaces/{izeto_kyc.sol → IZetoKyc.sol} +3 -3
  9. package/contracts/lib/interfaces/IZetoLockableCapability.sol +237 -0
  10. package/contracts/lib/interfaces/IZetoStorage.sol +106 -0
  11. package/contracts/lib/interfaces/{izeto_lockable.sol → IZetoVerifier.sol} +8 -21
  12. package/contracts/lib/registry.sol +77 -27
  13. package/contracts/lib/storage/base.sol +210 -0
  14. package/contracts/lib/storage/nullifier.sol +166 -0
  15. package/contracts/lib/zeto_common.sol +277 -28
  16. package/contracts/lib/zeto_fungible.sol +451 -33
  17. package/contracts/lib/zeto_fungible_base.sol +69 -0
  18. package/contracts/lib/zeto_fungible_burn.sol +83 -53
  19. package/contracts/lib/zeto_fungible_burn_nullifier.sol +115 -72
  20. package/contracts/lib/zeto_fungible_nullifier.sol +167 -0
  21. package/contracts/lib/zeto_lockable.sol +613 -0
  22. package/contracts/lib/zeto_non_fungible.sol +236 -0
  23. package/contracts/lib/zeto_non_fungible_base.sol +61 -0
  24. package/contracts/lib/zeto_non_fungible_nullifier.sol +61 -0
  25. package/contracts/test/escrow1.sol +90 -34
  26. package/contracts/test/escrow2.sol +64 -29
  27. package/contracts/test/qurrency.sol +10 -2
  28. package/contracts/test/smt.sol +8 -3
  29. package/contracts/test/tendecimals.sol +14 -12
  30. package/contracts/verifiers/impl/anon.sol +189 -0
  31. package/contracts/verifiers/impl/anon_batch.sol +301 -0
  32. package/contracts/verifiers/impl/anon_enc.sol +266 -0
  33. package/contracts/verifiers/impl/anon_enc_batch.sol +602 -0
  34. package/contracts/verifiers/impl/anon_enc_nullifier.sol +287 -0
  35. package/contracts/verifiers/impl/anon_enc_nullifier_batch.sol +679 -0
  36. package/contracts/verifiers/impl/anon_enc_nullifier_kyc.sol +294 -0
  37. package/contracts/verifiers/impl/anon_enc_nullifier_kyc_batch.sol +686 -0
  38. package/contracts/verifiers/impl/anon_enc_nullifier_non_repudiation.sol +413 -0
  39. package/contracts/verifiers/impl/anon_enc_nullifier_non_repudiation_batch.sol +1141 -0
  40. package/contracts/verifiers/impl/anon_nullifier_kyc_transfer.sol +217 -0
  41. package/contracts/verifiers/impl/anon_nullifier_kyc_transferLocked.sol +196 -0
  42. package/contracts/verifiers/impl/anon_nullifier_kyc_transferLocked_batch.sol +308 -0
  43. package/contracts/verifiers/impl/anon_nullifier_kyc_transfer_batch.sol +385 -0
  44. package/contracts/verifiers/impl/anon_nullifier_qurrency_transfer.sol +497 -0
  45. package/contracts/verifiers/impl/anon_nullifier_qurrency_transfer_batch.sol +1001 -0
  46. package/contracts/verifiers/{verifier_anon_nullifier_transferLocked.sol → impl/anon_nullifier_transfer.sol} +12 -19
  47. package/contracts/verifiers/{verifier_anon_nullifier_transferLocked_batch.sol → impl/anon_nullifier_transfer_batch.sol} +44 -51
  48. package/contracts/verifiers/impl/burn.sol +182 -0
  49. package/contracts/verifiers/impl/burn_batch.sol +238 -0
  50. package/contracts/verifiers/impl/burn_nullifier.sol +203 -0
  51. package/contracts/verifiers/impl/burn_nullifier_batch.sol +315 -0
  52. package/contracts/verifiers/impl/deposit.sol +182 -0
  53. package/contracts/verifiers/impl/deposit_kyc.sol +189 -0
  54. package/contracts/verifiers/impl/nf_anon.sol +175 -0
  55. package/contracts/verifiers/{verifier_nf_anon_nullifier_transferLocked.sol → impl/nf_anon_nullifier_transfer.sol} +6 -13
  56. package/contracts/verifiers/impl/withdraw.sol +189 -0
  57. package/contracts/verifiers/impl/withdraw_batch.sol +245 -0
  58. package/contracts/verifiers/impl/withdraw_nullifier.sol +210 -0
  59. package/contracts/verifiers/impl/withdraw_nullifier_batch.sol +322 -0
  60. package/contracts/verifiers/verifier_anon.sol +31 -186
  61. package/contracts/verifiers/verifier_anon_batch.sol +31 -298
  62. package/contracts/verifiers/verifier_anon_enc.sol +31 -263
  63. package/contracts/verifiers/verifier_anon_enc_batch.sol +31 -599
  64. package/contracts/verifiers/verifier_anon_enc_nullifier.sol +31 -284
  65. package/contracts/verifiers/verifier_anon_enc_nullifier_batch.sol +33 -676
  66. package/contracts/verifiers/verifier_anon_enc_nullifier_kyc.sol +31 -291
  67. package/contracts/verifiers/verifier_anon_enc_nullifier_kyc_batch.sol +33 -683
  68. package/contracts/verifiers/verifier_anon_enc_nullifier_non_repudiation.sol +33 -410
  69. package/contracts/verifiers/verifier_anon_enc_nullifier_non_repudiation_batch.sol +33 -1138
  70. package/contracts/verifiers/verifier_anon_nullifier_kyc_transfer.sol +33 -214
  71. package/contracts/verifiers/verifier_anon_nullifier_kyc_transferLocked.sol +33 -221
  72. package/contracts/verifiers/verifier_anon_nullifier_kyc_transferLocked_batch.sol +33 -389
  73. package/contracts/verifiers/verifier_anon_nullifier_kyc_transfer_batch.sol +33 -382
  74. package/contracts/verifiers/verifier_anon_nullifier_qurrency_transfer.sol +33 -221
  75. package/contracts/verifiers/verifier_anon_nullifier_qurrency_transfer_batch.sol +33 -389
  76. package/contracts/verifiers/verifier_anon_nullifier_transfer.sol +33 -207
  77. package/contracts/verifiers/verifier_anon_nullifier_transfer_batch.sol +33 -375
  78. package/contracts/verifiers/verifier_burn.sol +31 -179
  79. package/contracts/verifiers/verifier_burn_batch.sol +31 -235
  80. package/contracts/verifiers/verifier_burn_nullifier.sol +31 -200
  81. package/contracts/verifiers/verifier_burn_nullifier_batch.sol +31 -312
  82. package/contracts/verifiers/verifier_deposit.sol +31 -179
  83. package/contracts/verifiers/verifier_deposit_kyc.sol +34 -0
  84. package/contracts/verifiers/verifier_nf_anon.sol +31 -172
  85. package/contracts/verifiers/verifier_nf_anon_nullifier_transfer.sol +33 -179
  86. package/contracts/verifiers/verifier_withdraw.sol +31 -186
  87. package/contracts/verifiers/verifier_withdraw_batch.sol +31 -242
  88. package/contracts/verifiers/verifier_withdraw_nullifier.sol +31 -207
  89. package/contracts/verifiers/verifier_withdraw_nullifier_batch.sol +33 -319
  90. package/contracts/zeto_anon.sol +77 -231
  91. package/contracts/zeto_anon_burnable.sol +56 -12
  92. package/contracts/zeto_anon_enc.sol +93 -190
  93. package/contracts/zeto_anon_enc_nullifier.sol +249 -195
  94. package/contracts/zeto_anon_enc_nullifier_kyc.sol +51 -232
  95. package/contracts/zeto_anon_enc_nullifier_non_repudiation.sol +231 -238
  96. package/contracts/zeto_anon_nullifier.sol +164 -298
  97. package/contracts/zeto_anon_nullifier_burnable.sol +68 -18
  98. package/contracts/zeto_anon_nullifier_kyc.sol +40 -345
  99. package/contracts/zeto_anon_nullifier_qurrency.sol +218 -360
  100. package/contracts/zeto_nf_anon.sol +55 -130
  101. package/contracts/zeto_nf_anon_nullifier.sol +90 -152
  102. package/hardhat.config.ts +9 -3
  103. package/ignition/modules/lib/deps.ts +9 -6
  104. package/ignition/modules/zeto_anon_burnable.ts +8 -7
  105. package/ignition/modules/zeto_anon_enc_nullifier.ts +33 -1
  106. package/ignition/modules/zeto_anon_enc_nullifier_kyc.ts +2 -2
  107. package/ignition/modules/zeto_anon_enc_nullifier_non_repudiation.ts +2 -1
  108. package/ignition/modules/zeto_anon_nullifier.ts +30 -5
  109. package/ignition/modules/zeto_anon_nullifier_burnable.ts +52 -17
  110. package/ignition/modules/zeto_anon_nullifier_kyc.ts +27 -12
  111. package/ignition/modules/zeto_anon_nullifier_qurrency.ts +2 -1
  112. package/ignition/modules/zeto_nf_anon_nullifier.ts +16 -12
  113. package/package.json +4 -2
  114. package/scripts/deploy_cloneable.ts +19 -10
  115. package/scripts/deploy_upgradeable.ts +9 -5
  116. package/scripts/tokens/Zeto_Anon.ts +3 -3
  117. package/scripts/tokens/Zeto_AnonBurnable.ts +1 -1
  118. package/scripts/tokens/Zeto_AnonEnc.ts +12 -3
  119. package/scripts/tokens/Zeto_AnonEncNullifier.ts +7 -3
  120. package/scripts/tokens/Zeto_AnonEncNullifierKyc.ts +1 -1
  121. package/scripts/tokens/Zeto_AnonEncNullifierNonRepudiation.ts +3 -1
  122. package/scripts/tokens/Zeto_AnonNullifier.ts +3 -1
  123. package/scripts/tokens/Zeto_AnonNullifierBurnable.ts +3 -1
  124. package/scripts/tokens/Zeto_AnonNullifierKyc.ts +1 -1
  125. package/scripts/tokens/Zeto_AnonNullifierQurrency.ts +3 -1
  126. package/scripts/tokens/Zeto_NfAnon.ts +2 -2
  127. package/scripts/tokens/Zeto_NfAnonNullifier.ts +3 -2
  128. package/test/factory.ts +46 -73
  129. package/test/factory_upgradeable.ts +311 -0
  130. package/test/lib/anon_nullifier_helpers.ts +89 -0
  131. package/test/lib/anon_zeto_helpers.ts +76 -0
  132. package/test/lib/deploy.ts +3 -2
  133. package/test/lib/utils.ts +74 -35
  134. package/test/test/escrow1.ts +185 -58
  135. package/test/test/escrow2.ts +200 -107
  136. package/test/test/qurrency.ts +13 -33
  137. package/test/usdc-shielding.ts +39 -26
  138. package/test/utils.ts +144 -21
  139. package/test/zeto_anon.ts +956 -465
  140. package/test/zeto_anon_enc.ts +881 -142
  141. package/test/zeto_anon_enc_nullifier.ts +847 -38
  142. package/test/zeto_anon_enc_nullifier_kyc.ts +120 -181
  143. package/test/zeto_anon_enc_nullifier_non_repudiation.ts +77 -46
  144. package/test/zeto_anon_nullifier.ts +1212 -953
  145. package/test/zeto_anon_nullifier_kyc.ts +674 -655
  146. package/test/zeto_anon_nullifier_qurrency.ts +452 -306
  147. package/test/zeto_nf_anon.ts +737 -137
  148. package/test/zeto_nf_anon_nullifier.ts +876 -246
  149. package/contracts/lib/zeto_base.sol +0 -293
  150. package/contracts/lib/zeto_fungible_withdraw.sol +0 -132
  151. package/contracts/lib/zeto_fungible_withdraw_nullifier.sol +0 -144
  152. package/contracts/lib/zeto_nullifier.sol +0 -334
  153. package/contracts/zkDvP.sol_ +0 -273
  154. package/test/zkDvP.ts_ +0 -455
  155. /package/contracts/lib/{common.sol → common/common.sol} +0 -0
@@ -15,7 +15,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,15 +35,89 @@ 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
 
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
+
42
121
  describe("Zeto based fungible token with anonymity, KYC, using nullifiers without encryption", function () {
43
122
  let deployer: Signer;
44
123
  let Alice: User;
@@ -79,11 +158,17 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
79
158
 
80
159
  ({ deployer, zeto, erc20 } = await deployZeto("Zeto_AnonNullifierKyc"));
81
160
 
82
- const tx2 = await zeto.connect(deployer).register(Alice.babyJubPublicKey, '0x');
161
+ const tx2 = await zeto
162
+ .connect(deployer)
163
+ .register(Alice.babyJubPublicKey, "0x");
83
164
  const result1 = await tx2.wait();
84
- const tx3 = await zeto.connect(deployer).register(Bob.babyJubPublicKey, '0x');
165
+ const tx3 = await zeto
166
+ .connect(deployer)
167
+ .register(Bob.babyJubPublicKey, "0x");
85
168
  const result2 = await tx3.wait();
86
- const tx4 = await zeto.connect(deployer).register(Charlie.babyJubPublicKey, '0x');
169
+ const tx4 = await zeto
170
+ .connect(deployer)
171
+ .register(Charlie.babyJubPublicKey, "0x");
87
172
  const result3 = await tx4.wait();
88
173
 
89
174
  const storage1 = new InMemoryDB(str2Bytes("alice"));
@@ -106,7 +191,9 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
106
191
  await smtKyc.add(kycHash(publicKey3), kycHash(publicKey3));
107
192
 
108
193
  circuit = await loadCircuit("anon_nullifier_kyc_transfer");
109
- ({ provingKeyFile: provingKey } = loadProvingKeys("anon_nullifier_kyc_transfer"));
194
+ ({ provingKeyFile: provingKey } = loadProvingKeys(
195
+ "anon_nullifier_kyc_transfer",
196
+ ));
110
197
  batchCircuit = await loadCircuit("anon_nullifier_kyc_transfer_batch");
111
198
  ({ provingKeyFile: batchProvingKey } = loadProvingKeys(
112
199
  "anon_nullifier_kyc_transfer_batch",
@@ -237,11 +324,6 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
237
324
  await smtUnregistered.add(incomingUTXOs[i], incomingUTXOs[i]);
238
325
  }
239
326
 
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
327
  // mint sufficient balance in Zeto contract address for Alice to withdraw
246
328
  const mintTx = await erc20.connect(deployer).mint(zeto, 3);
247
329
  await mintTx.wait();
@@ -294,8 +376,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
294
376
  3,
295
377
  _withdrawNullifiers,
296
378
  withdrawCommitments[0],
297
- root.bigInt(),
298
- withdrawEncodedProof,
379
+ encodeToBytesForWithdraw(root.bigInt(), withdrawEncodedProof),
299
380
  "0x",
300
381
  );
301
382
  await tx.wait();
@@ -303,7 +384,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
303
384
  // Alice checks her ERC20 balance
304
385
  const endingBalance = await erc20.balanceOf(Alice.ethAddress);
305
386
  expect(endingBalance - startingBalance).to.be.equal(3);
306
- }).timeout(60000);
387
+ }).timeout(180000);
307
388
 
308
389
  it("mint ERC20 tokens to Alice to deposit to Zeto should succeed", async function () {
309
390
  const startingBalance = await erc20.balanceOf(Alice.ethAddress);
@@ -317,13 +398,29 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
317
398
 
318
399
  utxo100 = newUTXO(100, Alice);
319
400
  utxo0 = newUTXO(0, Alice);
320
- const { outputCommitments, encodedProof } = await prepareDepositProof(
401
+ const identitiesRoot = await smtKyc.root();
402
+ const proof3 = await smtKyc.generateCircomVerifierProof(
403
+ kycHash(Alice.babyJubPublicKey),
404
+ identitiesRoot,
405
+ );
406
+ const identitiesMerkleProofs = [
407
+ proof3.siblings.map((s) => s.bigInt()),
408
+ proof3.siblings.map((s) => s.bigInt()),
409
+ ];
410
+ const { outputCommitments, encodedProof } = await prepareDepositKycProof(
321
411
  Alice,
322
412
  [utxo100, utxo0],
413
+ identitiesRoot.bigInt(),
414
+ identitiesMerkleProofs,
323
415
  );
324
416
  const tx2 = await zeto
325
417
  .connect(Alice.signer)
326
- .deposit(100, outputCommitments, encodedProof, "0x");
418
+ .deposit(
419
+ 100,
420
+ outputCommitments,
421
+ encodeToBytesForDeposit(encodedProof),
422
+ "0x",
423
+ );
327
424
  await tx2.wait();
328
425
 
329
426
  await smtAlice.add(utxo100.hash, utxo100.hash);
@@ -541,8 +638,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
541
638
  80,
542
639
  nullifiers,
543
640
  outputCommitments[0],
544
- root.bigInt(),
545
- encodedProof,
641
+ encodeToBytesForWithdraw(root.bigInt(), encodedProof),
546
642
  "0x",
547
643
  );
548
644
  await tx.wait();
@@ -578,7 +674,17 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
578
674
  );
579
675
  });
580
676
 
581
- it("deposit by an unregistered user should succeed", async function () {
677
+ it("deposit by an unregistered user should fail", async function () {
678
+ const identitiesRoot = await smtKycUnregistered.root();
679
+ const proof3 = await smtKycUnregistered.generateCircomVerifierProof(
680
+ kycHash(unregistered.babyJubPublicKey),
681
+ identitiesRoot,
682
+ );
683
+ const identitiesMerkleProofs = [
684
+ proof3.siblings.map((s) => s.bigInt()),
685
+ proof3.siblings.map((s) => s.bigInt()),
686
+ ];
687
+
582
688
  const tx = await erc20
583
689
  .connect(deployer)
584
690
  .mint(unregistered.ethAddress, 100);
@@ -590,160 +696,32 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
590
696
 
591
697
  unregisteredUtxo100 = newUTXO(100, unregistered);
592
698
  unregisteredUtxo0 = newUTXO(0, unregistered);
593
- const { outputCommitments, encodedProof } = await prepareDepositProof(
699
+ const { outputCommitments, encodedProof } = await prepareDepositKycProof(
594
700
  unregistered,
595
701
  [unregisteredUtxo100, unregisteredUtxo0],
702
+ identitiesRoot.bigInt(),
703
+ identitiesMerkleProofs,
596
704
  );
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
705
  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);
706
+ zeto
707
+ .connect(unregistered.signer)
708
+ .deposit(
709
+ 100,
710
+ outputCommitments,
711
+ encodeToBytesForDeposit(encodedProof),
712
+ "0x",
713
+ ),
714
+ ).to.be.rejectedWith("VM Exception while processing transaction: reverted with custom error 'InvalidProof()'");
732
715
  });
733
716
  });
734
717
 
735
- describe("lock() tests", function () {
736
- let lockedUtxo1: UTXO;
737
- let smtBobForLocked: Merkletree;
718
+ describe("ILockableCapability tests", function () {
738
719
  let utxo9: UTXO;
739
720
  let utxo10: UTXO;
740
721
  let utxo11: UTXO;
741
722
  let utxo12: UTXO;
742
723
 
743
724
  before(async function () {
744
- const storage1 = new InMemoryDB(str2Bytes(""));
745
- smtBobForLocked = new Merkletree(storage1, true, 64);
746
-
747
725
  // mint UTXOs for Alice and spend one (to use it in a failure case test)
748
726
  utxo9 = newUTXO(10, Alice);
749
727
  utxo10 = newUTXO(15, Alice);
@@ -775,7 +753,10 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
775
753
  utxo12 = newUTXO(0, Alice);
776
754
 
777
755
  let root = await smtAlice.root();
778
- const proof1 = await smtAlice.generateCircomVerifierProof(utxo10.hash, root);
756
+ const proof1 = await smtAlice.generateCircomVerifierProof(
757
+ utxo10.hash,
758
+ root,
759
+ );
779
760
  const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
780
761
  const merkleProofs = [
781
762
  proof1.siblings.map((s) => s.bigInt()),
@@ -819,34 +800,51 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
819
800
  await smtBob.add(utxo12.hash, utxo12.hash);
820
801
  });
821
802
 
822
- describe("lock -> transfer flow", function () {
823
- it("lock() should succeed when using unlocked states", async function () {
803
+ // createLock -> updateLock -> delegateLock -> spendLock happy path. Note
804
+ // that for KYC the create-lock proof is the standard transfer proof
805
+ // (validates the SMT root and identities tree); the contract transitions
806
+ // the locked output into a flat per-lock mapping. The spendLock proof
807
+ // uses the dedicated transferLocked circuit / verifier wired through the
808
+ // ignition module's lockVerifier slot.
809
+ describe("createLock -> updateLock -> delegateLock -> spendLock flow", function () {
810
+ let lockedUtxo1: UTXO;
811
+ let lockId: string;
812
+ let outputUtxo1: UTXO;
813
+ let outputUtxo2: UTXO;
814
+ let unlockHash: string;
815
+ // Local SMT mirroring on-chain locked-state bookkeeping used by
816
+ // delegateLock tests (`locked()` / storage mirrors).
817
+ let smtForLocked: Merkletree;
818
+
819
+ before(async function () {
820
+ const storage = new InMemoryDB(str2Bytes("kyc-locked-1"));
821
+ smtForLocked = new Merkletree(storage, true, 64);
822
+ });
823
+
824
+ it("createLock() with deterministic lockId computed from txId", async function () {
824
825
  const nullifier1 = newNullifier(utxo11, Bob);
825
826
  lockedUtxo1 = newUTXO(utxo11.value!, Bob);
826
827
  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);
828
+ const p1 = await smtBob.generateCircomVerifierProof(utxo11.hash, root);
829
+ const p2 = await smtBob.generateCircomVerifierProof(0n, root);
832
830
  const merkleProofs = [
833
- proof1.siblings.map((s) => s.bigInt()),
834
- proof2.siblings.map((s) => s.bigInt()),
831
+ p1.siblings.map((s) => s.bigInt()),
832
+ p2.siblings.map((s) => s.bigInt()),
835
833
  ];
836
834
 
837
835
  const identitiesRoot = await smtKyc.root();
838
- const proof3 = await smtKyc.generateCircomVerifierProof(
836
+ const idProofBob = await smtKyc.generateCircomVerifierProof(
839
837
  kycHash(Bob.babyJubPublicKey),
840
838
  identitiesRoot,
841
839
  );
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
840
+ const identityMerkleProofs = [
841
+ idProofBob.siblings.map((s) => s.bigInt()), // sender (Bob)
842
+ idProofBob.siblings.map((s) => s.bigInt()), // 1st output owner (Bob)
843
+ idProofBob.siblings.map((s) => s.bigInt()), // 2nd output owner (dummy)
846
844
  ];
847
845
 
848
- const { outputCommitments, encodedProof } = await prepareProof(
849
- circuit, // use the regular circuit for the lock() call
846
+ const { encodedProof } = await prepareProof(
847
+ circuit,
850
848
  provingKey,
851
849
  Bob,
852
850
  [utxo11, ZERO_UTXO],
@@ -855,547 +853,565 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
855
853
  root.bigInt(),
856
854
  merkleProofs,
857
855
  identitiesRoot.bigInt(),
858
- identitiesMerkleProofs,
856
+ identityMerkleProofs,
859
857
  [Bob, Bob],
860
858
  );
861
859
 
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();
860
+ const txId = randomBytes32();
861
+ const createArgs = encodeCreateArgs({
862
+ txId,
863
+ inputs: [nullifier1.hash],
864
+ outputs: [],
865
+ lockedOutputs: [lockedUtxo1.hash],
866
+ proof: encodeUnlockedProof(root.bigInt(), encodedProof),
867
+ });
868
+
869
+ // Pre-compute lockId off-chain and confirm against the contract.
870
+ const predicted = await zeto
871
+ .connect(Bob.signer)
872
+ .computeLockId(createArgs);
873
+ lockId = predicted;
872
874
 
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),
875
+ const tx = await zeto
876
+ .connect(Bob.signer)
877
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x");
878
+ const result: ContractTransactionReceipt | null = await tx.wait();
879
+ logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
880
+
881
+ const created = result!.logs
882
+ .map((l) => {
883
+ try {
884
+ return zeto.interface.parseLog(l as any);
885
+ } catch (_e) {
886
+ return null;
887
+ }
888
+ })
889
+ .find((p) => p && p.name === "LockCreated");
890
+ expect(created, "LockCreated event not found").to.not.be.null;
891
+ expect(created!.args.lockId).to.equal(predicted);
892
+ expect(created!.args.owner).to.equal(Bob.ethAddress);
893
+ expect(created!.args.spender).to.equal(Bob.ethAddress);
894
+
895
+ // Mirror the locked UTXO into the local SMT so the locked-circuit
896
+ // witness has a valid inclusion proof to consume.
897
+ await smtForLocked.add(
898
+ lockedUtxo1.hash,
899
+ ethers.toBigInt(Bob.ethAddress),
879
900
  );
880
901
  });
881
902
 
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());
903
+ it("isLockActive() and getLock() reflect the newly created lock", async function () {
904
+ expect(await zeto.isLockActive(lockId)).to.equal(true);
905
+ const info = await zeto.getLock(lockId);
906
+ expect(info.owner).to.equal(Bob.ethAddress);
907
+ expect(info.spender).to.equal(Bob.ethAddress);
908
+ expect(info.spendCommitment).to.equal(ethers.ZeroHash);
909
+ expect(info.cancelCommitment).to.equal(ethers.ZeroHash);
886
910
  });
887
911
 
888
- it("lock() should fail when trying to lock again", async function () {
889
- if (network.name !== "hardhat") {
890
- return;
891
- }
912
+ it("locked() returns true for locked UTXOs and false for unlocked or spent UTXOs", async function () {
913
+ expect(await zeto.locked(lockedUtxo1.hash)).to.deep.equal([
914
+ true,
915
+ Bob.ethAddress,
916
+ ]);
917
+ // utxo12 is an unlocked UTXO that still belongs to Alice.
918
+ expect((await zeto.locked(utxo12.hash))[0]).to.be.false;
919
+ });
892
920
 
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
- ];
921
+ it("updateLock() commits the spend hash while owner == spender", async function () {
922
+ outputUtxo1 = newUTXO(10, Alice);
923
+ outputUtxo2 = newUTXO(5, Bob);
910
924
 
911
- const identitiesRoot = await smtKyc.root();
912
- const proof3 = await smtKyc.generateCircomVerifierProof(
913
- kycHash(Bob.babyJubPublicKey),
914
- identitiesRoot,
925
+ unlockHash = calculateSpendHash(
926
+ [lockedUtxo1],
927
+ [],
928
+ [outputUtxo1, outputUtxo2],
929
+ "0x",
915
930
  );
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
931
 
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
- );
932
+ const tx = await zeto
933
+ .connect(Bob.signer)
934
+ .updateLock(
935
+ lockId,
936
+ encodeUpdateArgs(randomBytes32()),
937
+ unlockHash,
938
+ ethers.ZeroHash,
939
+ "0x",
940
+ );
941
+ const result: ContractTransactionReceipt | null = await tx.wait();
942
+ logger.debug(`updateLock() complete. Gas used: ${result?.gasUsed}`);
936
943
 
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");
944
+ const info = await zeto.getLock(lockId);
945
+ expect(info.spendCommitment).to.equal(unlockHash);
950
946
  });
951
947
 
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
- });
948
+ it("delegateLock() transfers spending authority to Alice", async function () {
949
+ const tx = await zeto
950
+ .connect(Bob.signer)
951
+ .delegateLock(
952
+ lockId,
953
+ encodeDelegateArgs(randomBytes32()),
954
+ Alice.ethAddress,
955
+ "0x",
956
+ );
957
+ const result: ContractTransactionReceipt | null = await tx.wait();
958
+ logger.debug(`delegateLock() complete. Gas used: ${result?.gasUsed}`);
1002
959
 
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(
960
+ const info = await zeto.getLock(lockId);
961
+ expect(info.spender).to.equal(Alice.ethAddress);
962
+ // Storage-layer delegate must follow the spender.
963
+ expect(await zeto.locked(lockedUtxo1.hash)).to.deep.equal([
964
+ true,
965
+ Alice.ethAddress,
966
+ ]);
967
+ // Mirror the new delegate into the local locked SMT so the spendLock
968
+ // witness's SMT inclusion proof matches the (utxoHash, delegate) leaf.
969
+ await smtForLocked.update(
1008
970
  lockedUtxo1.hash,
1009
- root,
1010
- );
1011
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
1012
- 0n,
1013
- root,
971
+ ethers.toBigInt(Alice.ethAddress),
1014
972
  );
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
973
  });
1033
974
 
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
975
+ it("the new spender can spendLock() with the matching payload", async function () {
976
+ // The locked-input proof is generated against the dedicated
977
+ // transferLocked circuit; identity-tree membership for both the
978
+ // (still-original) sender Bob and each output owner is asserted as
979
+ // part of the snark, mirroring the unlocked transfer flow.
1036
980
  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
981
  const identitiesRoot = await smtKyc.root();
1055
- const proof3 = await smtKyc.generateCircomVerifierProof(
982
+ const idProofBob = await smtKyc.generateCircomVerifierProof(
1056
983
  kycHash(Bob.babyJubPublicKey),
1057
984
  identitiesRoot,
1058
985
  );
1059
- const proof4 = await smtKyc.generateCircomVerifierProof(
986
+ const idProofAlice = await smtKyc.generateCircomVerifierProof(
1060
987
  kycHash(Alice.babyJubPublicKey),
1061
988
  identitiesRoot,
1062
989
  );
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)
990
+ const identityMerkleProofs = [
991
+ idProofBob.siblings.map((s) => s.bigInt()), // sender (Bob)
992
+ idProofAlice.siblings.map((s) => s.bigInt()), // 1st output owner (Alice)
993
+ idProofBob.siblings.map((s) => s.bigInt()), // 2nd output owner (Bob)
1067
994
  ];
1068
995
 
1069
- const result = await prepareProof(
996
+ const { encodedProof } = await prepareProof(
1070
997
  circuitForLocked,
1071
998
  provingKeyForLocked,
1072
999
  Bob,
1073
1000
  [lockedUtxo1, ZERO_UTXO],
1074
1001
  [nullifier1, ZERO_UTXO],
1075
- [utxo9, utxo1],
1076
- root.bigInt(),
1077
- merkleProofs,
1002
+ [outputUtxo1, outputUtxo2],
1003
+ 0n,
1004
+ [],
1078
1005
  identitiesRoot.bigInt(),
1079
- identitiesMerkleProofs,
1080
- [Alice, Alice],
1081
- Alice.ethAddress, // current lock delegate
1006
+ identityMerkleProofs,
1007
+ [Alice, Bob],
1008
+ undefined,
1009
+ { transferLocked: true },
1082
1010
  );
1083
- const nullifiers = [nullifier1.hash];
1084
1011
 
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()})`);
1012
+ const spendArgs = encodeSpendArgs({
1013
+ txId: randomBytes32(),
1014
+ lockedOutputs: [],
1015
+ outputs: [outputUtxo1.hash, outputUtxo2.hash],
1016
+ proof: encodeLockedProof(encodedProof),
1017
+ data: "0x",
1018
+ });
1019
+
1020
+ const tx = await zeto
1021
+ .connect(Alice.signer)
1022
+ .spendLock(lockId, spendArgs, "0x");
1023
+ const result = await tx.wait();
1024
+
1025
+ const parsed = result!.logs
1026
+ .map((l) => {
1027
+ try {
1028
+ return zeto.interface.parseLog(l as any);
1029
+ } catch (_e) {
1030
+ return null;
1031
+ }
1032
+ })
1033
+ .filter((p) => p !== null) as ReadonlyArray<{
1034
+ name: string;
1035
+ args: any;
1036
+ }>;
1037
+ const lockSpent = parsed.find((p) => p.name === "LockSpent");
1038
+ const zetoLockSpent = parsed.find((p) => p.name === "ZetoLockSpent");
1039
+ expect(lockSpent, "LockSpent event not emitted").to.not.be.undefined;
1040
+ expect(zetoLockSpent, "ZetoLockSpent event not emitted").to.not.be
1041
+ .undefined;
1042
+ expect(lockSpent!.args.lockId).to.equal(lockId);
1043
+ expect(lockSpent!.args.spender).to.equal(Alice.ethAddress);
1044
+
1045
+ const outputs = zetoLockSpent!.args.outputs;
1046
+ await smtAlice.add(outputs[0], outputs[0]);
1047
+ await smtAlice.add(outputs[1], outputs[1]);
1048
+ await smtBob.add(outputs[0], outputs[0]);
1049
+ await smtBob.add(outputs[1], outputs[1]);
1050
+
1051
+ // Lock is no longer active after spendLock().
1052
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
1097
1053
  });
1098
1054
 
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,
1055
+ it("onchain SMT root for the unlocked UTXOs equals the offchain SMT root", async function () {
1056
+ const bobRoot = await smtBob.root();
1057
+ const aliceRoot = await smtAlice.root();
1058
+ const onchainRoot = await zeto.getRoot();
1059
+ expect(bobRoot.string()).to.equal(onchainRoot.toString());
1060
+ expect(aliceRoot.string()).to.equal(onchainRoot.toString());
1061
+ });
1062
+ });
1063
+
1064
+ // createLock with a non-zero cancelCommitment, then cancelLock by the
1065
+ // owner without going through delegateLock. Cancelling a lock follows
1066
+ // the same proof-encoding rules as spendLock (locked-input verifier,
1067
+ // bare Commonlib.Proof) but is gated by the cancelCommitment hash.
1068
+ describe("createLock -> cancelLock flow", function () {
1069
+ let lockedUtxo: UTXO;
1070
+ let lockId: string;
1071
+ let cancelHash: string;
1072
+ let outUtxo1: UTXO;
1073
+ let outUtxo2: UTXO;
1074
+ let aliceUtxoForLock: UTXO;
1075
+ let smtForLocked: Merkletree;
1076
+
1077
+ before(async function () {
1078
+ // Mint a fresh source UTXO for Alice so this scope can build its own
1079
+ // lock without colliding with the prior describe block's spend flow.
1080
+ aliceUtxoForLock = newUTXO(50, Alice);
1081
+ await doMint(zeto, deployer, [aliceUtxoForLock]);
1082
+ await smtAlice.add(aliceUtxoForLock.hash, aliceUtxoForLock.hash);
1083
+ await smtBob.add(aliceUtxoForLock.hash, aliceUtxoForLock.hash);
1084
+ const storage = new InMemoryDB(str2Bytes("kyc-locked-2"));
1085
+ smtForLocked = new Merkletree(storage, true, 64);
1086
+ });
1087
+
1088
+ it("Alice createLock() with a cancelCommitment", async function () {
1089
+ const nullifier1 = newNullifier(aliceUtxoForLock, Alice);
1090
+ lockedUtxo = newUTXO(aliceUtxoForLock.value!, Alice);
1091
+ const root = await smtAlice.root();
1092
+ const p1 = await smtAlice.generateCircomVerifierProof(
1093
+ aliceUtxoForLock.hash,
1109
1094
  root,
1110
1095
  );
1096
+ const p2 = await smtAlice.generateCircomVerifierProof(0n, root);
1111
1097
  const merkleProofs = [
1112
- proof1.siblings.map((s) => s.bigInt()),
1113
- proof2.siblings.map((s) => s.bigInt()),
1098
+ p1.siblings.map((s) => s.bigInt()),
1099
+ p2.siblings.map((s) => s.bigInt()),
1114
1100
  ];
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
1101
 
1119
1102
  const identitiesRoot = await smtKyc.root();
1120
- const proof3 = await smtKyc.generateCircomVerifierProof(
1121
- kycHash(Bob.babyJubPublicKey),
1122
- identitiesRoot,
1123
- );
1124
- const proof4 = await smtKyc.generateCircomVerifierProof(
1103
+ const idProofAlice = await smtKyc.generateCircomVerifierProof(
1125
1104
  kycHash(Alice.babyJubPublicKey),
1126
1105
  identitiesRoot,
1127
1106
  );
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)
1107
+ const identityMerkleProofs = [
1108
+ idProofAlice.siblings.map((s) => s.bigInt()),
1109
+ idProofAlice.siblings.map((s) => s.bigInt()),
1110
+ idProofAlice.siblings.map((s) => s.bigInt()),
1132
1111
  ];
1133
1112
 
1134
- const result = await prepareProof(
1135
- circuitForLocked,
1136
- provingKeyForLocked,
1137
- Bob,
1138
- [lockedUtxo1, ZERO_UTXO],
1113
+ const { encodedProof } = await prepareProof(
1114
+ circuit,
1115
+ provingKey,
1116
+ Alice,
1117
+ [aliceUtxoForLock, ZERO_UTXO],
1139
1118
  [nullifier1, ZERO_UTXO],
1140
- [utxo10, utxo1],
1119
+ [lockedUtxo, ZERO_UTXO],
1141
1120
  root.bigInt(),
1142
1121
  merkleProofs,
1143
1122
  identitiesRoot.bigInt(),
1144
- identitiesMerkleProofs,
1123
+ identityMerkleProofs,
1145
1124
  [Alice, Alice],
1146
- Alice.ethAddress, // current lock delegate
1147
1125
  );
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
1126
 
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,
1127
+ outUtxo1 = newUTXO(20, Bob);
1128
+ outUtxo2 = newUTXO(30, Alice);
1129
+ cancelHash = calculateCancelHash(
1130
+ [lockedUtxo],
1131
+ [],
1132
+ [outUtxo1, outUtxo2],
1133
+ "0x",
1171
1134
  );
1172
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
1173
- 0n,
1174
- root,
1135
+
1136
+ const createArgs = encodeCreateArgs({
1137
+ txId: randomBytes32(),
1138
+ inputs: [nullifier1.hash],
1139
+ outputs: [],
1140
+ lockedOutputs: [lockedUtxo.hash],
1141
+ proof: encodeUnlockedProof(root.bigInt(), encodedProof),
1142
+ });
1143
+ lockId = await zeto.connect(Alice.signer).computeLockId(createArgs);
1144
+
1145
+ const tx = await zeto
1146
+ .connect(Alice.signer)
1147
+ .createLock(createArgs, ethers.ZeroHash, cancelHash, "0x");
1148
+ const result: ContractTransactionReceipt | null = await tx.wait();
1149
+ logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
1150
+
1151
+ // Mirror the locked UTXO so the cancel-side witness can prove inclusion.
1152
+ await smtForLocked.add(
1153
+ lockedUtxo.hash,
1154
+ ethers.toBigInt(Alice.ethAddress),
1175
1155
  );
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);
1156
+ });
1183
1157
 
1158
+ it("the owner can cancelLock() to reverse the lock without delegation", async function () {
1159
+ const nullifier1 = newNullifier(lockedUtxo, Alice);
1184
1160
  const identitiesRoot = await smtKyc.root();
1185
- const proof3 = await smtKyc.generateCircomVerifierProof(
1186
- kycHash(Bob.babyJubPublicKey),
1161
+ const idProofAlice = await smtKyc.generateCircomVerifierProof(
1162
+ kycHash(Alice.babyJubPublicKey),
1187
1163
  identitiesRoot,
1188
1164
  );
1189
- const proof4 = await smtKyc.generateCircomVerifierProof(
1190
- kycHash(Alice.babyJubPublicKey),
1165
+ const idProofBob = await smtKyc.generateCircomVerifierProof(
1166
+ kycHash(Bob.babyJubPublicKey),
1191
1167
  identitiesRoot,
1192
1168
  );
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)
1169
+ const identityMerkleProofs = [
1170
+ idProofAlice.siblings.map((s) => s.bigInt()), // sender (Alice)
1171
+ idProofBob.siblings.map((s) => s.bigInt()), // 1st output owner (Bob)
1172
+ idProofAlice.siblings.map((s) => s.bigInt()), // 2nd output owner (Alice)
1197
1173
  ];
1198
1174
 
1199
- const result = await prepareProof(
1175
+ const { encodedProof } = await prepareProof(
1200
1176
  circuitForLocked,
1201
1177
  provingKeyForLocked,
1202
- Bob,
1203
- [lockedUtxo1, ZERO_UTXO],
1178
+ Alice,
1179
+ [lockedUtxo, ZERO_UTXO],
1204
1180
  [nullifier1, ZERO_UTXO],
1205
- [utxo1, utxo2],
1206
- root.bigInt(),
1207
- merkleProofs,
1181
+ [outUtxo1, outUtxo2],
1182
+ 0n,
1183
+ [],
1208
1184
  identitiesRoot.bigInt(),
1209
- identitiesMerkleProofs,
1210
- [Alice, Bob],
1211
- Alice.ethAddress, // current lock delegate
1185
+ identityMerkleProofs,
1186
+ [Bob, Alice],
1187
+ undefined,
1188
+ { transferLocked: true },
1212
1189
  );
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
1190
 
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
- });
1191
+ const cancelArgs = encodeSpendArgs({
1192
+ txId: randomBytes32(),
1193
+ lockedOutputs: [],
1194
+ outputs: [outUtxo1.hash, outUtxo2.hash],
1195
+ proof: encodeLockedProof(encodedProof),
1196
+ data: "0x",
1197
+ });
1234
1198
 
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
- });
1199
+ const tx = await zeto
1200
+ .connect(Alice.signer)
1201
+ .cancelLock(lockId, cancelArgs, "0x");
1202
+ const result = await tx.wait();
1240
1203
 
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());
1204
+ const parsed = result!.logs
1205
+ .map((l) => {
1206
+ try {
1207
+ return zeto.interface.parseLog(l as any);
1208
+ } catch (_e) {
1209
+ return null;
1210
+ }
1211
+ })
1212
+ .filter((p) => p !== null) as ReadonlyArray<{
1213
+ name: string;
1214
+ args: any;
1215
+ }>;
1216
+ const cancelled = parsed.find((p) => p.name === "LockCancelled");
1217
+ const zetoCancelled = parsed.find((p) => p.name === "ZetoLockCancelled");
1218
+ expect(cancelled, "LockCancelled event not emitted").to.not.be.undefined;
1219
+ expect(zetoCancelled, "ZetoLockCancelled event not emitted").to.not.be
1220
+ .undefined;
1221
+
1222
+ const outputs = zetoCancelled!.args.outputs;
1223
+ await smtAlice.add(outputs[0], outputs[0]);
1224
+ await smtAlice.add(outputs[1], outputs[1]);
1225
+ await smtBob.add(outputs[0], outputs[0]);
1226
+ await smtBob.add(outputs[1], outputs[1]);
1227
+
1228
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
1245
1229
  });
1246
1230
  });
1247
1231
 
1248
- describe("lock -> delegate -> transfer flow", function () {
1249
- let lockedUtxo2: UTXO;
1232
+ // Negative cases for the lock lifecycle. These rely on hardhat's revert
1233
+ // decoding for custom errors and so are gated by the network name.
1234
+ describe("negative cases for the lock lifecycle", function () {
1235
+ if (network.name !== "hardhat") {
1236
+ return;
1237
+ }
1250
1238
 
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,
1239
+ // freshLock mints a fresh UTXO for `owner`, locks it, and returns the
1240
+ // metadata so each negative scope can branch off without polluting
1241
+ // siblings. Each call advances both local SMTs.
1242
+ async function freshLock(
1243
+ owner: User,
1244
+ spendCommitment: string = ethers.ZeroHash,
1245
+ cancelCommitment: string = ethers.ZeroHash,
1246
+ ): Promise<{
1247
+ lockId: string;
1248
+ sourceUtxo: UTXO;
1249
+ lockedUtxo: UTXO;
1250
+ createArgs: string;
1251
+ }> {
1252
+ const sourceUtxo = newUTXO(7, owner);
1253
+ await doMint(zeto, deployer, [sourceUtxo]);
1254
+ await smtAlice.add(sourceUtxo.hash, sourceUtxo.hash);
1255
+ await smtBob.add(sourceUtxo.hash, sourceUtxo.hash);
1256
+
1257
+ const nullifier = newNullifier(sourceUtxo, owner);
1258
+ const lockedUtxo = newUTXO(sourceUtxo.value!, owner);
1259
+ const root = await smtBob.root();
1260
+ const p1 = await smtBob.generateCircomVerifierProof(
1261
+ sourceUtxo.hash,
1257
1262
  root,
1258
1263
  );
1259
- const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
1264
+ const p2 = await smtBob.generateCircomVerifierProof(0n, root);
1260
1265
  const merkleProofs = [
1261
- proof1.siblings.map((s) => s.bigInt()),
1262
- proof2.siblings.map((s) => s.bigInt()),
1266
+ p1.siblings.map((s) => s.bigInt()),
1267
+ p2.siblings.map((s) => s.bigInt()),
1263
1268
  ];
1264
1269
 
1265
1270
  const identitiesRoot = await smtKyc.root();
1266
- const proof3 = await smtKyc.generateCircomVerifierProof(
1267
- kycHash(Alice.babyJubPublicKey),
1271
+ const idProofOwner = await smtKyc.generateCircomVerifierProof(
1272
+ kycHash(owner.babyJubPublicKey),
1268
1273
  identitiesRoot,
1269
1274
  );
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
1275
+ const identityMerkleProofs = [
1276
+ idProofOwner.siblings.map((s) => s.bigInt()),
1277
+ idProofOwner.siblings.map((s) => s.bigInt()),
1278
+ idProofOwner.siblings.map((s) => s.bigInt()),
1274
1279
  ];
1275
1280
 
1276
- const { outputCommitments, encodedProof } = await prepareProof(
1281
+ const { encodedProof } = await prepareProof(
1277
1282
  circuit,
1278
1283
  provingKey,
1279
- Alice,
1280
- [utxo9, ZERO_UTXO],
1281
- [nullifier1, ZERO_UTXO],
1282
- [lockedUtxo2, ZERO_UTXO],
1284
+ owner,
1285
+ [sourceUtxo, ZERO_UTXO],
1286
+ [nullifier, ZERO_UTXO],
1287
+ [lockedUtxo, ZERO_UTXO],
1283
1288
  root.bigInt(),
1284
1289
  merkleProofs,
1285
1290
  identitiesRoot.bigInt(),
1286
- identitiesMerkleProofs,
1287
- [Alice, Alice],
1291
+ identityMerkleProofs,
1292
+ [owner, owner],
1288
1293
  );
1289
1294
 
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();
1295
+ const createArgs = encodeCreateArgs({
1296
+ txId: randomBytes32(),
1297
+ inputs: [nullifier.hash],
1298
+ outputs: [],
1299
+ lockedOutputs: [lockedUtxo.hash],
1300
+ proof: encodeUnlockedProof(root.bigInt(), encodedProof),
1301
+ });
1302
+ const lockId = await zeto
1303
+ .connect(owner.signer)
1304
+ .computeLockId(createArgs);
1305
+ await (
1306
+ await zeto
1307
+ .connect(owner.signer)
1308
+ .createLock(createArgs, spendCommitment, cancelCommitment, "0x")
1309
+ ).wait();
1310
+ return { lockId, sourceUtxo, lockedUtxo, createArgs };
1311
+ }
1300
1312
 
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
- );
1313
+ // Authorization / immutability assertions short-circuit before the
1314
+ // proof is ever touched, so we can pass a syntactically valid but
1315
+ // ZK-vacuous spend payload.
1316
+ function dummySpendArgs(): string {
1317
+ return encodeSpendArgs({
1318
+ txId: randomBytes32(),
1319
+ lockedOutputs: [],
1320
+ outputs: [],
1321
+ proof: "0x",
1322
+ data: "0x",
1323
+ });
1324
+ }
1325
+
1326
+ it("createLock() with a duplicate txId from the same caller reverts with DuplicateLock", async function () {
1327
+ const { lockId, createArgs } = await freshLock(Bob);
1328
+
1329
+ await expect(
1330
+ zeto
1331
+ .connect(Bob.signer)
1332
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x"),
1333
+ ).rejectedWith(`DuplicateLock("${lockId}")`);
1308
1334
  });
1309
1335
 
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
- );
1336
+ it("updateLock() by a non-owner reverts with LockUnauthorized", async function () {
1337
+ const { lockId } = await freshLock(Bob);
1338
+
1339
+ await expect(
1340
+ zeto
1341
+ .connect(Alice.signer)
1342
+ .updateLock(
1343
+ lockId,
1344
+ encodeUpdateArgs(randomBytes32()),
1345
+ ethers.ZeroHash,
1346
+ ethers.ZeroHash,
1347
+ "0x",
1348
+ ),
1349
+ ).to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1350
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1321
1351
  });
1322
1352
 
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());
1353
+ it("updateLock() after delegateLock() reverts with LockImmutable", async function () {
1354
+ const { lockId } = await freshLock(Bob);
1355
+
1356
+ await (
1357
+ await zeto
1358
+ .connect(Bob.signer)
1359
+ .delegateLock(
1360
+ lockId,
1361
+ encodeDelegateArgs(randomBytes32()),
1362
+ Alice.ethAddress,
1363
+ "0x",
1364
+ )
1365
+ ).wait();
1366
+
1367
+ await expect(
1368
+ zeto
1369
+ .connect(Bob.signer)
1370
+ .updateLock(
1371
+ lockId,
1372
+ encodeUpdateArgs(randomBytes32()),
1373
+ ethers.ZeroHash,
1374
+ ethers.ZeroHash,
1375
+ "0x",
1376
+ ),
1377
+ ).rejectedWith(`LockImmutable("${lockId}")`);
1327
1378
  });
1328
1379
 
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);
1380
+ it("delegateLock() by a non-spender reverts with LockUnauthorized", async function () {
1381
+ const { lockId } = await freshLock(Bob);
1349
1382
 
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
- ];
1383
+ await expect(
1384
+ zeto
1385
+ .connect(Alice.signer)
1386
+ .delegateLock(
1387
+ lockId,
1388
+ encodeDelegateArgs(randomBytes32()),
1389
+ Charlie.ethAddress,
1390
+ "0x",
1391
+ ),
1392
+ )
1393
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1394
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1395
+ });
1364
1396
 
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];
1397
+ it("spendLock() by a non-spender reverts with LockUnauthorized before touching the proof", async function () {
1398
+ const { lockId } = await freshLock(Bob);
1380
1399
 
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
1400
  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;
1401
+ zeto.connect(Alice.signer).spendLock(lockId, dummySpendArgs(), "0x"),
1402
+ )
1403
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1404
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1405
+ });
1406
+
1407
+ it("cancelLock() by a non-spender reverts with LockUnauthorized", async function () {
1408
+ const { lockId } = await freshLock(Bob);
1393
1409
 
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);
1410
+ await expect(
1411
+ zeto.connect(Alice.signer).cancelLock(lockId, dummySpendArgs(), "0x"),
1412
+ )
1413
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1414
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1399
1415
  });
1400
1416
  });
1401
1417
  });
@@ -1443,8 +1459,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1443
1459
  10,
1444
1460
  nullifiers,
1445
1461
  outputCommitments[0],
1446
- root.bigInt(),
1447
- encodedProof,
1462
+ encodeToBytesForWithdraw(root.bigInt(), encodedProof),
1448
1463
  "0x",
1449
1464
  ),
1450
1465
  ).rejectedWith("UTXOAlreadySpent");
@@ -1692,7 +1707,6 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1692
1707
  identitiesRoot: BigInt,
1693
1708
  identitiesMerkleProof: BigInt[][],
1694
1709
  owners: User[],
1695
- lockDelegate?: User,
1696
1710
  ) {
1697
1711
  let nullifiers: BigNumberish[];
1698
1712
  let outputCommitments: BigNumberish[];
@@ -1704,11 +1718,6 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1704
1718
  circuitToUse = batchCircuit;
1705
1719
  provingKeyToUse = batchProvingKey;
1706
1720
  }
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
1721
  const result = await prepareProof(
1713
1722
  circuitToUse,
1714
1723
  provingKeyToUse,
@@ -1721,7 +1730,6 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1721
1730
  identitiesRoot,
1722
1731
  identitiesMerkleProof,
1723
1732
  owners,
1724
- lockDelegate?.ethAddress,
1725
1733
  );
1726
1734
  nullifiers = _nullifiers.map((nullifier) => nullifier.hash) as [
1727
1735
  BigNumberish,
@@ -1736,7 +1744,6 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1736
1744
  outputCommitments,
1737
1745
  utxosRoot,
1738
1746
  encodedProof,
1739
- lockDelegate !== undefined,
1740
1747
  );
1741
1748
  return {
1742
1749
  txResult,
@@ -1756,6 +1763,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1756
1763
  identitiesMerkleProof: BigInt[][],
1757
1764
  owners: User[],
1758
1765
  lockDelegate?: string,
1766
+ opts?: { transferLocked?: boolean },
1759
1767
  ) {
1760
1768
  const nullifiers = _nullifiers.map((nullifier) => nullifier.hash) as [
1761
1769
  BigNumberish,
@@ -1774,24 +1782,41 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1774
1782
  (owner) => owner.babyJubPublicKey,
1775
1783
  ) as BigNumberish[][];
1776
1784
  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);
1785
+
1786
+ let inputObj: any;
1787
+ if (opts?.transferLocked) {
1788
+ inputObj = {
1789
+ inputCommitments,
1790
+ inputValues,
1791
+ inputSalts,
1792
+ inputOwnerPrivateKey: signer.formattedPrivateKey,
1793
+ identitiesRoot,
1794
+ identitiesMerkleProof,
1795
+ outputCommitments,
1796
+ outputValues,
1797
+ outputSalts: outputs.map((output) => output.salt || 0n),
1798
+ outputOwnerPublicKeys,
1799
+ };
1800
+ } else {
1801
+ inputObj = {
1802
+ nullifiers,
1803
+ inputCommitments,
1804
+ inputValues,
1805
+ inputSalts,
1806
+ inputOwnerPrivateKey: signer.formattedPrivateKey,
1807
+ utxosRoot,
1808
+ enabled: nullifiers.map((n) => (n !== 0n ? 1 : 0)),
1809
+ utxosMerkleProof,
1810
+ identitiesRoot,
1811
+ identitiesMerkleProof,
1812
+ outputCommitments,
1813
+ outputValues,
1814
+ outputSalts: outputs.map((output) => output.salt || 0n),
1815
+ outputOwnerPublicKeys,
1816
+ };
1817
+ if (lockDelegate) {
1818
+ inputObj["lockDelegate"] = ethers.toBigInt(lockDelegate);
1819
+ }
1795
1820
  }
1796
1821
 
1797
1822
  const witness = await circuit.calculateWTNSBin(inputObj, true);
@@ -1822,27 +1847,14 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1822
1847
  outputCommitments: BigNumberish[],
1823
1848
  root: BigNumberish,
1824
1849
  encodedProof: any,
1825
- isLocked: boolean = false,
1826
1850
  ) {
1827
1851
  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
- }
1852
+ const tx = await zeto.connect(signer.signer).transfer(
1853
+ nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works
1854
+ outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works
1855
+ encodeToBytes(root, encodedProof),
1856
+ "0x",
1857
+ );
1846
1858
  const results: ContractTransactionReceipt | null = await tx.wait();
1847
1859
  console.log(
1848
1860
  `Time to execute transaction: ${Date.now() - startTx}ms. Gas used: ${results?.gasUsed
@@ -1851,3 +1863,10 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou
1851
1863
  return results;
1852
1864
  }
1853
1865
  });
1866
+
1867
+ function encodeToBytes(root: any, proof: any) {
1868
+ return new AbiCoder().encode(
1869
+ ["uint256 root", "tuple(uint256[2] pA, uint256[2][2] pB, uint256[2] pC)"],
1870
+ [root, proof],
1871
+ );
1872
+ }