@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,14 @@
15
15
  // limitations under the License.
16
16
 
17
17
  import { ethers, network } from "hardhat";
18
- import { Signer, BigNumberish, AddressLike } from "ethers";
18
+ import {
19
+ Signer,
20
+ BigNumberish,
21
+ AddressLike,
22
+ AbiCoder,
23
+ ZeroAddress,
24
+ ContractTransactionReceipt,
25
+ } from "ethers";
19
26
  import { expect } from "chai";
20
27
  import { loadCircuit, tokenUriHash, encodeProof } from "zeto-js";
21
28
  import { groth16 } from "snarkjs";
@@ -26,11 +33,14 @@ import {
26
33
  newUser,
27
34
  newAssetUTXO,
28
35
  doMint,
29
- ZERO_UTXO,
36
+ logger,
30
37
  } from "./lib/utils";
31
- import { loadProvingKeys, prepareAssetLockProof } from "./utils";
38
+ import {
39
+ loadProvingKeys,
40
+ calculateSpendHash,
41
+ calculateCancelHash,
42
+ } from "./utils";
32
43
  import { deployZeto } from "./lib/deploy";
33
-
34
44
  describe("Zeto based non-fungible token with anonymity without encryption or nullifiers", function () {
35
45
  let deployer: Signer;
36
46
  let Alice: User;
@@ -59,6 +69,13 @@ describe("Zeto based non-fungible token with anonymity without encryption or nul
59
69
  ({ provingKeyFile: provingKey } = loadProvingKeys("nf_anon"));
60
70
  });
61
71
 
72
+ it("non-owner should not be able to mint", async function () {
73
+ const utxo1 = newAssetUTXO(10, "http://ipfs.io/file-hash-1", Alice);
74
+ await expect(doMint(zeto, Alice.signer, [utxo1])).to.be.rejectedWith(
75
+ "OwnableUnauthorizedAccount",
76
+ );
77
+ });
78
+
62
79
  it("mint to Alice and transfer UTXOs honestly to Bob should succeed", async function () {
63
80
  const tokenId = 1001;
64
81
  const uri = "http://ipfs.io/file-hash-1";
@@ -129,161 +146,743 @@ describe("Zeto based non-fungible token with anonymity without encryption or nul
129
146
  });
130
147
  });
131
148
 
132
- describe("lock() tests", function () {
133
- describe("lock -> delegate -> spend", function () {
134
- let lockedUtxo1: UTXO;
135
-
136
- it("lock() should succeed when using unlocked states", async function () {
137
- lockedUtxo1 = newAssetUTXO(utxo3.tokenId!, utxo3.uri!, Charlie);
138
- const { inputCommitment, outputCommitment, encodedProof } =
139
- await prepareProof(
140
- circuit,
141
- provingKey,
142
- Charlie,
143
- utxo3,
144
- lockedUtxo1,
145
- Charlie,
149
+ describe("ILockableCapability tests", function () {
150
+ // ABI fragments for the ZetoLockableCapability *Args payloads.
151
+ // Mirrors the layout used in zeto_anon.ts so that the cross-token
152
+ // contract surface stays uniform.
153
+ const CREATE_ARGS_ABI =
154
+ "tuple(bytes32 txId, uint256[] inputs, uint256[] outputs, uint256[] lockedOutputs, bytes proof)";
155
+ const UPDATE_ARGS_ABI = "tuple(bytes32 txId)";
156
+ const DELEGATE_ARGS_ABI = "tuple(bytes32 txId)";
157
+ const SPEND_ARGS_ABI =
158
+ "tuple(bytes32 txId, uint256[] lockedOutputs, uint256[] outputs, bytes proof, bytes data)";
159
+
160
+ function encodeCreateArgs(args: {
161
+ txId: string;
162
+ inputs: BigNumberish[];
163
+ outputs: BigNumberish[];
164
+ lockedOutputs: BigNumberish[];
165
+ proof: string;
166
+ }) {
167
+ return new AbiCoder().encode([CREATE_ARGS_ABI], [args]);
168
+ }
169
+
170
+ function encodeUpdateArgs(txId: string) {
171
+ return new AbiCoder().encode([UPDATE_ARGS_ABI], [{ txId }]);
172
+ }
173
+
174
+ function encodeDelegateArgs(txId: string) {
175
+ return new AbiCoder().encode([DELEGATE_ARGS_ABI], [{ txId }]);
176
+ }
177
+
178
+ function encodeSpendArgs(args: {
179
+ txId: string;
180
+ lockedOutputs: BigNumberish[];
181
+ outputs: BigNumberish[];
182
+ proof: string;
183
+ data: string;
184
+ }) {
185
+ return new AbiCoder().encode([SPEND_ARGS_ABI], [args]);
186
+ }
187
+
188
+ function randomBytes32(): string {
189
+ return ethers.hexlify(ethers.randomBytes(32));
190
+ }
191
+
192
+ // mintAndLock mints a fresh UTXO under `owner`, then runs createLock
193
+ // to convert it into a single locked UTXO of identical tokenId/uri.
194
+ // Returns everything a downstream test needs to drive update /
195
+ // delegate / spend / cancel without re-deriving any of it.
196
+ async function mintAndLock(
197
+ owner: User,
198
+ tokenId: number,
199
+ uri: string,
200
+ spendCommitment: string = ethers.ZeroHash,
201
+ cancelCommitment: string = ethers.ZeroHash,
202
+ ): Promise<{
203
+ lockId: string;
204
+ sourceUtxo: UTXO;
205
+ lockedUtxo: UTXO;
206
+ createArgs: string;
207
+ }> {
208
+ const sourceUtxo = newAssetUTXO(tokenId, uri, owner);
209
+ await doMint(zeto, deployer, [sourceUtxo]);
210
+
211
+ const lockedUtxo = newAssetUTXO(tokenId, uri, owner);
212
+ const { encodedProof } = await prepareProof(
213
+ circuit,
214
+ provingKey,
215
+ owner,
216
+ sourceUtxo,
217
+ lockedUtxo,
218
+ owner,
219
+ );
220
+
221
+ const createArgs = encodeCreateArgs({
222
+ txId: randomBytes32(),
223
+ inputs: [sourceUtxo.hash],
224
+ outputs: [],
225
+ lockedOutputs: [lockedUtxo.hash],
226
+ proof: encodeToBytes(encodedProof),
227
+ });
228
+
229
+ const lockId = await zeto
230
+ .connect(owner.signer)
231
+ .computeLockId(createArgs);
232
+ await (
233
+ await zeto
234
+ .connect(owner.signer)
235
+ .createLock(createArgs, spendCommitment, cancelCommitment, "0x")
236
+ ).wait();
237
+
238
+ return { lockId, sourceUtxo, lockedUtxo, createArgs };
239
+ }
240
+
241
+ // dummySpendArgs returns a syntactically valid spend payload that
242
+ // does NOT need to verify a real ZK proof. Tests for authorization
243
+ // checks that short-circuit before the proof is touched can use this
244
+ // to avoid the cost of generating a real proof.
245
+ function dummySpendArgs(): string {
246
+ return encodeSpendArgs({
247
+ txId: randomBytes32(),
248
+ lockedOutputs: [],
249
+ outputs: [],
250
+ proof: "0x",
251
+ data: "0x",
252
+ });
253
+ }
254
+
255
+ describe("createLock -> updateLock -> delegateLock -> spendLock flow", function () {
256
+ const tokenId = 2001;
257
+ const uri = "http://ipfs.io/lock-flow-1";
258
+ let bobSourceUtxo: UTXO;
259
+ let lockedUtxo: UTXO;
260
+ let lockId: string;
261
+ let outUtxo: UTXO;
262
+ let unlockHash: string;
263
+
264
+ it("createLock() with deterministic lockId computed from txId", async function () {
265
+ bobSourceUtxo = newAssetUTXO(tokenId, uri, Bob);
266
+ await doMint(zeto, deployer, [bobSourceUtxo]);
267
+
268
+ // Locked content is a fresh UTXO of identical tokenId/uri held
269
+ // under Bob. The ZK proof on the create path is the standard
270
+ // 1-in/1-out NF transfer relationship — the locked output is
271
+ // treated identically to a regular output for circuit purposes.
272
+ lockedUtxo = newAssetUTXO(tokenId, uri, Bob);
273
+ const { encodedProof } = await prepareProof(
274
+ circuit,
275
+ provingKey,
276
+ Bob,
277
+ bobSourceUtxo,
278
+ lockedUtxo,
279
+ Bob,
280
+ );
281
+
282
+ const createArgs = encodeCreateArgs({
283
+ txId: randomBytes32(),
284
+ inputs: [bobSourceUtxo.hash],
285
+ outputs: [],
286
+ lockedOutputs: [lockedUtxo.hash],
287
+ proof: encodeToBytes(encodedProof),
288
+ });
289
+
290
+ const predicted = await zeto
291
+ .connect(Bob.signer)
292
+ .computeLockId(createArgs);
293
+ lockId = predicted;
294
+
295
+ const tx = await zeto
296
+ .connect(Bob.signer)
297
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x");
298
+ const result: ContractTransactionReceipt | null = await tx.wait();
299
+ logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
300
+
301
+ const created = result!.logs
302
+ .map((l) => {
303
+ try {
304
+ return zeto.interface.parseLog(l as any);
305
+ } catch (_e) {
306
+ return null;
307
+ }
308
+ })
309
+ .find((p: any) => p && p.name === "LockCreated");
310
+ expect(created, "LockCreated event not found").to.not.be.null;
311
+ expect(created!.args.lockId).to.equal(predicted);
312
+ expect(created!.args.owner).to.equal(Bob.ethAddress);
313
+ expect(created!.args.spender).to.equal(Bob.ethAddress);
314
+ });
315
+
316
+ it("isLockActive() and getLock() reflect the newly created lock", async function () {
317
+ expect(await zeto.isLockActive(lockId)).to.equal(true);
318
+ const info = await zeto.getLock(lockId);
319
+ expect(info.owner).to.equal(Bob.ethAddress);
320
+ expect(info.spender).to.equal(Bob.ethAddress);
321
+ expect(info.spendCommitment).to.equal(ethers.ZeroHash);
322
+ expect(info.cancelCommitment).to.equal(ethers.ZeroHash);
323
+ });
324
+
325
+ it("locked() returns (true, spender) for locked UTXOs and (false, 0) otherwise", async function () {
326
+ // Just-created lock: spender == owner == Bob.
327
+ expect(await zeto.locked(lockedUtxo.hash)).to.deep.equal([
328
+ true,
329
+ Bob.ethAddress,
330
+ ]);
331
+ // Source UTXO is now spent (consumed by createLock), so it is
332
+ // not locked.
333
+ expect((await zeto.locked(bobSourceUtxo.hash))[0]).to.be.false;
334
+ });
335
+
336
+ it("updateLock() commits the spend hash while owner == spender", async function () {
337
+ outUtxo = newAssetUTXO(tokenId, uri, Charlie);
338
+
339
+ // For NF, the spend payload is a single unlocked output —
340
+ // there's no second-output split to consider.
341
+ unlockHash = calculateSpendHash([lockedUtxo], [], [outUtxo], "0x");
342
+
343
+ const tx = await zeto
344
+ .connect(Bob.signer)
345
+ .updateLock(
346
+ lockId,
347
+ encodeUpdateArgs(randomBytes32()),
348
+ unlockHash,
349
+ ethers.ZeroHash,
350
+ "0x",
146
351
  );
352
+ const result = await tx.wait();
353
+ logger.debug(`updateLock() complete. Gas used: ${result?.gasUsed}`);
147
354
 
148
- await expect(
149
- zeto.connect(Charlie.signer).lock(
150
- inputCommitment,
151
- outputCommitment,
152
- encodedProof,
153
- Alice.ethAddress, // make Alice the delegate who can spend the state (if she has the right proof)
355
+ const info = await zeto.getLock(lockId);
356
+ expect(info.spendCommitment).to.equal(unlockHash);
357
+ });
358
+
359
+ it("delegateLock() transfers spending authority to Alice", async function () {
360
+ const tx = await zeto
361
+ .connect(Bob.signer)
362
+ .delegateLock(
363
+ lockId,
364
+ encodeDelegateArgs(randomBytes32()),
365
+ Alice.ethAddress,
154
366
  "0x",
155
- ),
156
- ).to.be.fulfilled;
367
+ );
368
+ const result = await tx.wait();
369
+ logger.debug(`delegateLock() complete. Gas used: ${result?.gasUsed}`);
370
+
371
+ const info = await zeto.getLock(lockId);
372
+ expect(info.spender).to.equal(Alice.ethAddress);
373
+ // Per-UTXO delegate projection in ZetoLockable must reflect the
374
+ // new spender.
375
+ expect(await zeto.locked(lockedUtxo.hash)).to.deep.equal([
376
+ true,
377
+ Alice.ethAddress,
378
+ ]);
157
379
  });
158
380
 
159
- it("lock() should fail when trying to lock again", async function () {
381
+ it("the new spender can spendLock() with the matching payload", async function () {
382
+ const { encodedProof } = await prepareProof(
383
+ circuit,
384
+ provingKey,
385
+ Bob,
386
+ lockedUtxo,
387
+ outUtxo,
388
+ Charlie,
389
+ );
390
+
391
+ const spendArgs = encodeSpendArgs({
392
+ txId: randomBytes32(),
393
+ lockedOutputs: [],
394
+ outputs: [outUtxo.hash],
395
+ proof: encodeToBytes(encodedProof),
396
+ data: "0x",
397
+ });
398
+
399
+ const tx = await zeto
400
+ .connect(Alice.signer)
401
+ .spendLock(lockId, spendArgs, "0x");
402
+ const result = await tx.wait();
403
+
404
+ const parsed = result!.logs
405
+ .map((l) => {
406
+ try {
407
+ return zeto.interface.parseLog(l as any);
408
+ } catch (_e) {
409
+ return null;
410
+ }
411
+ })
412
+ .filter((p: any) => p !== null) as ReadonlyArray<{
413
+ name: string;
414
+ args: any;
415
+ }>;
416
+ const lockSpent = parsed.find((p) => p.name === "LockSpent");
417
+ const zetoLockSpent = parsed.find((p) => p.name === "ZetoLockSpent");
418
+ expect(lockSpent, "LockSpent event not emitted").to.not.be.undefined;
419
+ expect(zetoLockSpent, "ZetoLockSpent event not emitted").to.not.be
420
+ .undefined;
421
+ expect(lockSpent!.args.lockId).to.equal(lockId);
422
+ expect(lockSpent!.args.spender).to.equal(Alice.ethAddress);
423
+
424
+ // Lock is no longer active.
425
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
426
+
427
+ // Output is now an ordinary unlocked UTXO.
428
+ expect(await zeto.spent(outUtxo.hash)).to.equal(1n); // UNSPENT
429
+
430
+ // Per-UTXO delegate projection is cleared post-consume.
431
+ expect(await zeto.locked(lockedUtxo.hash)).to.deep.equal([
432
+ false,
433
+ ZeroAddress,
434
+ ]);
435
+ });
436
+ });
437
+
438
+ describe("createLock -> cancelLock flow", function () {
439
+ const tokenId = 2002;
440
+ const uri = "http://ipfs.io/lock-flow-2";
441
+ let bobSourceUtxo: UTXO;
442
+ let lockedUtxo: UTXO;
443
+ let lockId: string;
444
+ let cancelHash: string;
445
+ let outUtxo: UTXO;
446
+
447
+ it("Bob createLock() with a non-zero cancelCommitment", async function () {
448
+ bobSourceUtxo = newAssetUTXO(tokenId, uri, Bob);
449
+ await doMint(zeto, deployer, [bobSourceUtxo]);
450
+
451
+ lockedUtxo = newAssetUTXO(tokenId, uri, Bob);
452
+ const { encodedProof } = await prepareProof(
453
+ circuit,
454
+ provingKey,
455
+ Bob,
456
+ bobSourceUtxo,
457
+ lockedUtxo,
458
+ Bob,
459
+ );
460
+
461
+ outUtxo = newAssetUTXO(tokenId, uri, Bob);
462
+ cancelHash = calculateCancelHash([lockedUtxo], [], [outUtxo], "0x");
463
+
464
+ const createArgs = encodeCreateArgs({
465
+ txId: randomBytes32(),
466
+ inputs: [bobSourceUtxo.hash],
467
+ outputs: [],
468
+ lockedOutputs: [lockedUtxo.hash],
469
+ proof: encodeToBytes(encodedProof),
470
+ });
471
+ lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
472
+ await (
473
+ await zeto
474
+ .connect(Bob.signer)
475
+ .createLock(createArgs, ethers.ZeroHash, cancelHash, "0x")
476
+ ).wait();
477
+ });
478
+
479
+ it("the owner can cancelLock() to reverse the lock without delegation", async function () {
480
+ const { encodedProof } = await prepareProof(
481
+ circuit,
482
+ provingKey,
483
+ Bob,
484
+ lockedUtxo,
485
+ outUtxo,
486
+ Bob,
487
+ );
488
+
489
+ const cancelArgs = encodeSpendArgs({
490
+ txId: randomBytes32(),
491
+ lockedOutputs: [],
492
+ outputs: [outUtxo.hash],
493
+ proof: encodeToBytes(encodedProof),
494
+ data: "0x",
495
+ });
496
+
497
+ const tx = await zeto
498
+ .connect(Bob.signer)
499
+ .cancelLock(lockId, cancelArgs, "0x");
500
+ const result = await tx.wait();
501
+
502
+ const parsed = result!.logs
503
+ .map((l) => {
504
+ try {
505
+ return zeto.interface.parseLog(l as any);
506
+ } catch (_e) {
507
+ return null;
508
+ }
509
+ })
510
+ .filter((p: any) => p !== null) as ReadonlyArray<{
511
+ name: string;
512
+ args: any;
513
+ }>;
514
+ const cancelled = parsed.find((p) => p.name === "LockCancelled");
515
+ const zetoCancelled = parsed.find((p) => p.name === "ZetoLockCancelled");
516
+ expect(cancelled, "LockCancelled event not emitted").to.not.be
517
+ .undefined;
518
+ expect(zetoCancelled, "ZetoLockCancelled event not emitted").to.not.be
519
+ .undefined;
520
+
521
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
522
+ expect(await zeto.spent(outUtxo.hash)).to.equal(1n); // UNSPENT
523
+ });
524
+ });
525
+
526
+ describe("spendLock with a payload that does not match the spend commitment fails", function () {
527
+ const tokenId = 2003;
528
+ const uri = "http://ipfs.io/lock-flow-3";
529
+ let lockedUtxo: UTXO;
530
+ let lockId: string;
531
+ let expectedHash: string;
532
+
533
+ before(async function () {
534
+ // Mint, lock, then commit to a specific spend payload.
535
+ const bobSourceUtxo = newAssetUTXO(tokenId, uri, Bob);
536
+ await doMint(zeto, deployer, [bobSourceUtxo]);
537
+
538
+ lockedUtxo = newAssetUTXO(tokenId, uri, Bob);
539
+ const { encodedProof } = await prepareProof(
540
+ circuit,
541
+ provingKey,
542
+ Bob,
543
+ bobSourceUtxo,
544
+ lockedUtxo,
545
+ Bob,
546
+ );
547
+
548
+ const createArgs = encodeCreateArgs({
549
+ txId: randomBytes32(),
550
+ inputs: [bobSourceUtxo.hash],
551
+ outputs: [],
552
+ lockedOutputs: [lockedUtxo.hash],
553
+ proof: encodeToBytes(encodedProof),
554
+ });
555
+ lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
556
+ await (
557
+ await zeto
558
+ .connect(Bob.signer)
559
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x")
560
+ ).wait();
561
+
562
+ const expectedOut = newAssetUTXO(tokenId, uri, Charlie);
563
+ expectedHash = calculateSpendHash(
564
+ [lockedUtxo],
565
+ [],
566
+ [expectedOut],
567
+ "0x",
568
+ );
569
+ await (
570
+ await zeto
571
+ .connect(Bob.signer)
572
+ .updateLock(
573
+ lockId,
574
+ encodeUpdateArgs(randomBytes32()),
575
+ expectedHash,
576
+ ethers.ZeroHash,
577
+ "0x",
578
+ )
579
+ ).wait();
580
+ });
581
+
582
+ it("spendLock() with a different payload reverts with InvalidUnlockHash", async function () {
160
583
  if (network.name !== "hardhat") {
161
- return;
584
+ this.skip();
162
585
  }
586
+ const wrongOut = newAssetUTXO(tokenId, uri, Alice);
587
+
588
+ const { encodedProof } = await prepareProof(
589
+ circuit,
590
+ provingKey,
591
+ Bob,
592
+ lockedUtxo,
593
+ wrongOut,
594
+ Alice,
595
+ );
163
596
 
164
- // Charlie is the owner of the UTXO, so he can generate the right proof
165
- const utxo1 = newAssetUTXO(
166
- lockedUtxo1.tokenId!,
167
- lockedUtxo1.uri!,
168
- Charlie,
597
+ const spendArgs = encodeSpendArgs({
598
+ txId: randomBytes32(),
599
+ lockedOutputs: [],
600
+ outputs: [wrongOut.hash],
601
+ proof: encodeToBytes(encodedProof),
602
+ data: "0x",
603
+ });
604
+
605
+ const calculatedHash = calculateSpendHash(
606
+ [lockedUtxo],
607
+ [],
608
+ [wrongOut],
609
+ "0x",
169
610
  );
170
- const { inputCommitment, outputCommitment, encodedProof } =
171
- await prepareProof(
172
- circuit,
173
- provingKey,
174
- Charlie,
175
- lockedUtxo1,
176
- utxo1,
177
- Charlie,
178
- );
611
+
179
612
  await expect(
180
- zeto
181
- .connect(Charlie.signer)
182
- .lock(
183
- inputCommitment,
184
- outputCommitment,
185
- encodedProof,
186
- Bob.ethAddress,
187
- "0x",
188
- ),
189
- ).rejectedWith(`UTXOAlreadyLocked(${lockedUtxo1.hash.toString()})`);
613
+ zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x"),
614
+ )
615
+ .to.be.revertedWithCustomError(zeto, "InvalidUnlockHash")
616
+ .withArgs(expectedHash, calculatedHash);
190
617
  });
618
+ });
191
619
 
192
- it("the original owner can NOT use the proper proof to spend the locked state", async function () {
193
- const utxo1 = newAssetUTXO(
194
- lockedUtxo1.tokenId!,
195
- lockedUtxo1.uri!,
196
- Alice,
620
+ describe("locked inputs cannot be re-spent via plain transfer", function () {
621
+ // A locked UTXO must not be consumable by the public transfer()
622
+ // entry point — only spendLock / cancelLock can move it. The
623
+ // storage layer enforces this with `AlreadyLocked`.
624
+ if (network.name !== "hardhat") {
625
+ return;
626
+ }
627
+
628
+ it("transfer() of a locked UTXO reverts with AlreadyLocked", async function () {
629
+ const tokenId = 2010;
630
+ const uri = "http://ipfs.io/locked-no-transfer";
631
+ const { lockedUtxo } = await mintAndLock(Bob, tokenId, uri);
632
+
633
+ // Output must preserve tokenId/uri to satisfy the NF circuit's
634
+ // mass-conservation invariant; otherwise the proof would not
635
+ // even be computable and we'd never reach the storage layer's
636
+ // AlreadyLocked check.
637
+ const otherUtxo = newAssetUTXO(tokenId, uri, Bob);
638
+ await expect(doTransfer(Bob, lockedUtxo, otherUtxo, Bob)).rejectedWith(
639
+ "AlreadyLocked",
197
640
  );
641
+ });
642
+ });
643
+
644
+ describe("negative cases for the lock lifecycle", function () {
645
+ // These tests rely on hardhat-style revert decoding.
646
+ if (network.name !== "hardhat") {
647
+ return;
648
+ }
649
+
650
+ it("createLock() with a duplicate txId from the same caller reverts with DuplicateLock", async function () {
651
+ const { lockId, createArgs } = await mintAndLock(
652
+ Bob,
653
+ 3001,
654
+ "http://ipfs.io/dup-lock",
655
+ );
656
+
657
+ // Same createArgs (same txId, same caller) => same lockId =>
658
+ // DuplicateLock fires before any input or proof validation.
198
659
  await expect(
199
- doTransfer(Charlie, lockedUtxo1, utxo1, Alice),
200
- ).to.be.rejectedWith("UTXOAlreadyLocked");
660
+ zeto
661
+ .connect(Bob.signer)
662
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x"),
663
+ )
664
+ .to.be.revertedWithCustomError(zeto, "DuplicateLock")
665
+ .withArgs(lockId);
201
666
  });
202
667
 
203
- it("the current delegate can move the lock to a new delegate", async function () {
668
+ it("updateLock() by a non-owner reverts with LockUnauthorized", async function () {
669
+ const { lockId } = await mintAndLock(
670
+ Bob,
671
+ 3002,
672
+ "http://ipfs.io/non-owner-update",
673
+ );
674
+
204
675
  await expect(
205
676
  zeto
206
677
  .connect(Alice.signer)
207
- .delegateLock([lockedUtxo1.hash], Bob.ethAddress, "0x"),
208
- ).to.be.fulfilled;
678
+ .updateLock(
679
+ lockId,
680
+ encodeUpdateArgs(randomBytes32()),
681
+ ethers.ZeroHash,
682
+ ethers.ZeroHash,
683
+ "0x",
684
+ ),
685
+ )
686
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
687
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
209
688
  });
210
689
 
211
- it("the designated delegate can use the proper proof to spend the locked state", async function () {
212
- const utxo1 = newAssetUTXO(lockedUtxo1.tokenId!, lockedUtxo1.uri!, Bob);
213
- const { inputCommitment, outputCommitment, encodedProof } =
214
- await prepareProof(
215
- circuit,
216
- provingKey,
217
- Charlie,
218
- lockedUtxo1,
219
- utxo1,
220
- Bob,
221
- );
222
- // Alice (in reality this is usually a contract that orchestrates a trade flow) can spend the locked state
223
- // using the proof generated by the trade counterparty (Charlie in this case)
690
+ it("updateLock() prefers LockUnauthorized over LockImmutable when both apply", async function () {
691
+ // Lock is delegated (so spender != owner -> immutable) AND the
692
+ // caller is neither owner nor spender. The contract MUST report
693
+ // LockUnauthorized, not leak the immutability state to the
694
+ // unauthorized caller.
695
+ const { lockId } = await mintAndLock(
696
+ Bob,
697
+ 3003,
698
+ "http://ipfs.io/auth-over-immut",
699
+ );
700
+ await (
701
+ await zeto
702
+ .connect(Bob.signer)
703
+ .delegateLock(
704
+ lockId,
705
+ encodeDelegateArgs(randomBytes32()),
706
+ Alice.ethAddress,
707
+ "0x",
708
+ )
709
+ ).wait();
710
+
224
711
  await expect(
225
- sendTx(Bob, inputCommitment, outputCommitment, encodedProof, true),
226
- ).to.be.fulfilled;
712
+ zeto
713
+ .connect(Charlie.signer)
714
+ .updateLock(
715
+ lockId,
716
+ encodeUpdateArgs(randomBytes32()),
717
+ ethers.ZeroHash,
718
+ ethers.ZeroHash,
719
+ "0x",
720
+ ),
721
+ )
722
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
723
+ // spender at this point is Alice (the new delegate).
724
+ .withArgs(lockId, Bob.ethAddress, Charlie.ethAddress);
227
725
  });
228
- });
229
726
 
230
- describe("lock -> unlock -> spend", function () {
231
- let lockedUtxo1: UTXO;
232
- let utxo1: UTXO;
233
-
234
- it("lock() should succeed when using unlocked states", async function () {
235
- const tokenId = 1001;
236
- const uri = "http://ipfs.io/file-hash-1";
237
- const utxo1 = newAssetUTXO(tokenId, uri, Alice);
238
- await doMint(zeto, deployer, [utxo1]);
239
-
240
- lockedUtxo1 = newAssetUTXO(utxo1.tokenId!, utxo1.uri!, Alice);
241
- const { inputCommitment, outputCommitment, encodedProof } =
242
- await prepareProof(
243
- circuit,
244
- provingKey,
245
- Alice,
246
- utxo1,
247
- lockedUtxo1,
248
- Alice,
249
- );
727
+ it("updateLock() after delegateLock() reverts with LockImmutable", async function () {
728
+ const { lockId } = await mintAndLock(
729
+ Bob,
730
+ 3004,
731
+ "http://ipfs.io/immut-after-delegate",
732
+ );
733
+
734
+ // Bob delegates spending authority to Alice; spender (Alice)
735
+ // now differs from owner (Bob).
736
+ await (
737
+ await zeto
738
+ .connect(Bob.signer)
739
+ .delegateLock(
740
+ lockId,
741
+ encodeDelegateArgs(randomBytes32()),
742
+ Alice.ethAddress,
743
+ "0x",
744
+ )
745
+ ).wait();
250
746
 
747
+ // Even Bob (the owner) can no longer mutate the commitments —
748
+ // the lock is now externally controlled and must be considered
749
+ // immutable.
251
750
  await expect(
252
751
  zeto
253
- .connect(Alice.signer)
254
- .lock(
255
- inputCommitment,
256
- outputCommitment,
257
- encodedProof,
258
- Bob.ethAddress,
752
+ .connect(Bob.signer)
753
+ .updateLock(
754
+ lockId,
755
+ encodeUpdateArgs(randomBytes32()),
756
+ ethers.ZeroHash,
757
+ ethers.ZeroHash,
259
758
  "0x",
260
759
  ),
261
- ).to.be.fulfilled;
760
+ )
761
+ .to.be.revertedWithCustomError(zeto, "LockImmutable")
762
+ .withArgs(lockId);
262
763
  });
263
764
 
264
- it("unlock() should succeed when using locked states", async function () {
265
- utxo1 = newAssetUTXO(lockedUtxo1.tokenId!, lockedUtxo1.uri!, Alice);
266
- const { inputCommitment, outputCommitment, encodedProof } =
267
- await prepareProof(
268
- circuit,
269
- provingKey,
270
- Alice,
271
- lockedUtxo1,
272
- utxo1,
273
- Alice,
274
- );
765
+ it("delegateLock() by a non-spender reverts with LockUnauthorized", async function () {
766
+ const { lockId } = await mintAndLock(
767
+ Bob,
768
+ 3005,
769
+ "http://ipfs.io/non-spender-delegate",
770
+ );
275
771
 
276
- // Bob as the current delegate can unlock the state
277
772
  await expect(
278
773
  zeto
279
- .connect(Bob.signer)
280
- .unlock(inputCommitment, outputCommitment, encodedProof, "0x"),
281
- ).to.be.fulfilled;
774
+ .connect(Alice.signer)
775
+ .delegateLock(
776
+ lockId,
777
+ encodeDelegateArgs(randomBytes32()),
778
+ Charlie.ethAddress,
779
+ "0x",
780
+ ),
781
+ )
782
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
783
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
282
784
  });
283
785
 
284
- it("current owner can spend an unlocked UTXO as usual", async function () {
285
- const utxo2 = newAssetUTXO(utxo1.tokenId!, utxo1.uri!, Bob);
286
- await expect(doTransfer(Alice, utxo1, utxo2, Bob)).to.be.fulfilled;
786
+ it("spendLock() by a non-spender reverts with LockUnauthorized before touching the proof", async function () {
787
+ const { lockId } = await mintAndLock(
788
+ Bob,
789
+ 3006,
790
+ "http://ipfs.io/non-spender-spend",
791
+ );
792
+
793
+ // Garbage proof — the onlySpender modifier MUST short-circuit
794
+ // before any proof verification is attempted.
795
+ await expect(
796
+ zeto.connect(Alice.signer).spendLock(lockId, dummySpendArgs(), "0x"),
797
+ )
798
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
799
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
800
+ });
801
+
802
+ it("cancelLock() by a non-spender reverts with LockUnauthorized", async function () {
803
+ const { lockId } = await mintAndLock(
804
+ Bob,
805
+ 3007,
806
+ "http://ipfs.io/non-spender-cancel",
807
+ );
808
+
809
+ await expect(
810
+ zeto.connect(Alice.signer).cancelLock(lockId, dummySpendArgs(), "0x"),
811
+ )
812
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
813
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
814
+ });
815
+
816
+ it("after a successful spendLock(), getLock() reverts with LockNotActive", async function () {
817
+ const tokenId = 3008;
818
+ const uri = "http://ipfs.io/post-spend-getlock";
819
+ const { lockId, lockedUtxo } = await mintAndLock(Bob, tokenId, uri);
820
+
821
+ const outUtxo = newAssetUTXO(tokenId, uri, Bob);
822
+ const { encodedProof } = await prepareProof(
823
+ circuit,
824
+ provingKey,
825
+ Bob,
826
+ lockedUtxo,
827
+ outUtxo,
828
+ Bob,
829
+ );
830
+ const spendArgs = encodeSpendArgs({
831
+ txId: randomBytes32(),
832
+ lockedOutputs: [],
833
+ outputs: [outUtxo.hash],
834
+ proof: encodeToBytes(encodedProof),
835
+ data: "0x",
836
+ });
837
+ await (
838
+ await zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x")
839
+ ).wait();
840
+
841
+ await expect(zeto.getLock(lockId))
842
+ .to.be.revertedWithCustomError(zeto, "LockNotActive")
843
+ .withArgs(lockId);
844
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
845
+ });
846
+
847
+ it("InvalidNonFungibleArity fires when createLock supplies more than one input", async function () {
848
+ const tokenId = 3009;
849
+ const uri = "http://ipfs.io/arity-create";
850
+
851
+ const u1 = newAssetUTXO(tokenId, uri, Bob);
852
+ const u2 = newAssetUTXO(tokenId + 1, uri + "2", Bob);
853
+ await doMint(zeto, deployer, [u1, u2]);
854
+
855
+ const lockedUtxo = newAssetUTXO(tokenId, uri, Bob);
856
+ // Borrow a real proof for the (u1 -> lockedUtxo) transition;
857
+ // the InvalidNonFungibleArity check fires before the verifier is
858
+ // touched, so a real proof is not strictly required, but the
859
+ // payload must still be well-formed bytes.
860
+ const { encodedProof } = await prepareProof(
861
+ circuit,
862
+ provingKey,
863
+ Bob,
864
+ u1,
865
+ lockedUtxo,
866
+ Bob,
867
+ );
868
+
869
+ const createArgs = encodeCreateArgs({
870
+ txId: randomBytes32(),
871
+ // Two inputs is invalid for the NF lock circuit: a single
872
+ // token cannot be split.
873
+ inputs: [u1.hash, u2.hash],
874
+ outputs: [],
875
+ lockedOutputs: [lockedUtxo.hash],
876
+ proof: encodeToBytes(encodedProof),
877
+ });
878
+
879
+ await expect(
880
+ zeto
881
+ .connect(Bob.signer)
882
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x"),
883
+ )
884
+ .to.be.revertedWithCustomError(zeto, "InvalidNonFungibleArity")
885
+ .withArgs(2, 0, 1);
287
886
  });
288
887
  });
289
888
  });
@@ -314,23 +913,16 @@ describe("Zeto based non-fungible token with anonymity without encryption or nul
314
913
  inputCommitment: BigNumberish,
315
914
  outputCommitment: BigNumberish,
316
915
  encodedProof: any,
317
- isLocked = false,
318
916
  ) {
319
- let tx;
320
- if (isLocked) {
321
- tx = await zeto
322
- .connect(signer.signer)
323
- .transferLocked(inputCommitment, outputCommitment, encodedProof, "0x");
324
- } else {
325
- tx = await zeto
326
- .connect(signer.signer)
327
- .transfer(inputCommitment, outputCommitment, encodedProof, "0x");
328
- }
917
+ const proof = encodeToBytes(encodedProof);
918
+ const tx = await zeto
919
+ .connect(signer.signer)
920
+ .transfer(inputCommitment, outputCommitment, proof, "0x");
329
921
  const results = await tx.wait();
330
- console.log(`Method transfer() complete. Gas used: ${results?.gasUsed}`);
922
+ logger.debug(`Method transfer() complete. Gas used: ${results?.gasUsed}`);
331
923
 
332
- expect(await zeto.spent(inputCommitment)).to.equal(true);
333
- expect(await zeto.spent(outputCommitment)).to.equal(false);
924
+ expect(await zeto.spent(inputCommitment)).to.equal(2n); // UTXOStatus.SPENT
925
+ expect(await zeto.spent(outputCommitment)).to.equal(1n); // UTXOStatus.UNSPENT
334
926
  }
335
927
  });
336
928
 
@@ -374,7 +966,7 @@ async function prepareProof(
374
966
  witness,
375
967
  )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] };
376
968
  const timeProofGeneration = Date.now() - startProofGeneration;
377
- console.log(
969
+ logger.debug(
378
970
  `Witness calculation time: ${timeWitnessCalculation}ms, Proof generation time: ${timeProofGeneration}ms`,
379
971
  );
380
972
  const encodedProof = encodeProof(proof);
@@ -385,6 +977,13 @@ async function prepareProof(
385
977
  };
386
978
  }
387
979
 
980
+ function encodeToBytes(proof: any) {
981
+ return new AbiCoder().encode(
982
+ ["tuple(uint256[2] pA, uint256[2][2] pB, uint256[2] pC)"],
983
+ [proof],
984
+ );
985
+ }
986
+
388
987
  module.exports = {
389
988
  prepareProof,
390
989
  };