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