@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
@@ -14,11 +14,18 @@
14
14
  // See the License for the specific language governing permissions and
15
15
  // limitations under the License.
16
16
 
17
- import { ethers, network } from "hardhat";
18
- import { ContractTransactionReceipt, Signer, BigNumberish, lock } from "ethers";
17
+ import { ethers, network, upgrades } from "hardhat";
18
+ import {
19
+ ContractTransactionReceipt,
20
+ Signer,
21
+ BigNumberish,
22
+ AbiCoder,
23
+ ZeroAddress,
24
+ } from "ethers";
19
25
  import { expect } from "chai";
20
- import { loadCircuit, Poseidon, encodeProof } from "zeto-js";
21
- import { groth16 } from "snarkjs";
26
+ import * as chai from "chai";
27
+ chai.config.truncateThreshold = 0; // disable truncating
28
+ import { loadCircuit, Poseidon } from "zeto-js";
22
29
  import { Merkletree, InMemoryDB, str2Bytes } from "@iden3/js-merkletree";
23
30
  import {
24
31
  UTXO,
@@ -29,15 +36,30 @@ import {
29
36
  doMint,
30
37
  ZERO_UTXO,
31
38
  parseUTXOEvents,
39
+ logger,
32
40
  } from "./lib/utils";
33
41
  import {
34
42
  loadProvingKeys,
35
43
  prepareDepositProof,
36
44
  prepareNullifierWithdrawProof,
37
- prepareNullifierBurnProof,
45
+ encodeToBytesForDeposit,
46
+ encodeToBytesForWithdraw,
47
+ inflateUtxos,
48
+ inflateOwners,
49
+ calculateSpendHash,
50
+ calculateCancelHash,
38
51
  } from "./utils";
52
+ import {
53
+ prepareProof as prepareProofForLocked,
54
+ encodeToBytes as encodeToBytesForLocked,
55
+ } from "./lib/anon_zeto_helpers";
56
+ import {
57
+ prepareProof,
58
+ encodeToBytes,
59
+ } from "./lib/anon_nullifier_helpers";
39
60
  import { deployZeto } from "./lib/deploy";
40
- import { Zeto_AnonNullifier, Zeto_AnonNullifierBurnable } from "../typechain-types";
61
+ import { Zeto_AnonNullifier } from "../typechain-types";
62
+ import smt from "../ignition/modules/test/smt";
41
63
 
42
64
  describe("Zeto based fungible token with anonymity using nullifiers without encryption", function () {
43
65
  let deployer: Signer;
@@ -46,14 +68,6 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
46
68
  let Charlie: User;
47
69
  let erc20: any;
48
70
  let zeto: Zeto_AnonNullifier;
49
- let zetoBurnable: Zeto_AnonNullifierBurnable;
50
- let utxo100: UTXO;
51
- let utxo1: UTXO;
52
- let utxo2: UTXO;
53
- let utxo3: UTXO;
54
- let utxo4: UTXO;
55
- let utxo7: UTXO;
56
- let utxo9: UTXO;
57
71
  let circuit: any, provingKey: any;
58
72
  let circuitForLocked: any, provingKeyForLocked: any;
59
73
  let batchCircuit: any, batchProvingKey: any;
@@ -61,19 +75,22 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
61
75
  let smtBob: Merkletree;
62
76
 
63
77
  before(async function () {
78
+ // skip the tests if this is called by other test modules to use the exported test functions
79
+ if (process.env.SKIP_ANON_NULLIFIER_TESTS === "true") {
80
+ this.skip();
81
+ }
64
82
  if (network.name !== "hardhat") {
65
83
  // accommodate for longer block times on public networks
66
84
  this.timeout(120000);
67
85
  }
68
86
 
69
- let [d, a, b, c] = await ethers.getSigners();
87
+ let [d, a, b, c, e] = await ethers.getSigners();
70
88
  deployer = d;
71
89
  Alice = await newUser(a);
72
90
  Bob = await newUser(b);
73
91
  Charlie = await newUser(c);
74
92
 
75
93
  ({ deployer, zeto, erc20 } = await deployZeto("Zeto_AnonNullifier"));
76
- ({ zeto: zetoBurnable } = await deployZeto("Zeto_AnonNullifierBurnable"));
77
94
 
78
95
  const storage1 = new InMemoryDB(str2Bytes(""));
79
96
  smtAlice = new Merkletree(storage1, true, 64);
@@ -85,14 +102,24 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
85
102
  ({ provingKeyFile: provingKey } = loadProvingKeys(
86
103
  "anon_nullifier_transfer",
87
104
  ));
88
- circuitForLocked = await loadCircuit("anon_nullifier_transferLocked");
89
- ({ provingKeyFile: provingKeyForLocked } = loadProvingKeys(
90
- "anon_nullifier_transferLocked",
91
- ));
92
105
  batchCircuit = await loadCircuit("anon_nullifier_transfer_batch");
93
106
  ({ provingKeyFile: batchProvingKey } = loadProvingKeys(
94
107
  "anon_nullifier_transfer_batch",
95
108
  ));
109
+ // for consuming locked UTXOs, we always use the "regular" circuit,
110
+ // because the locked UTXOs are always tracked in their own base storage,
111
+ // regardless of which token type is being used. This means for the nullifier-based token,
112
+ // where the unlocked UTXOs are tracked in SMTs, processing unlocked UTXOs require the
113
+ // circuits based on SMT proofs, while the locked UTXOs are processed using the "base" circuit.
114
+ circuitForLocked = await loadCircuit("anon");
115
+ ({ provingKeyFile: provingKeyForLocked } = loadProvingKeys("anon"));
116
+ });
117
+
118
+ beforeEach(async function () {
119
+ // skip the tests if this is called by other test modules to use the exported test functions
120
+ if (process.env.SKIP_ANON_NULLIFIER_TESTS === "true") {
121
+ this.skip();
122
+ }
96
123
  });
97
124
 
98
125
  it("onchain SMT root should be equal to the offchain SMT root", async function () {
@@ -102,917 +129,1199 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
102
129
  expect(root.string()).to.equal(onchainRoot.toString());
103
130
  });
104
131
 
105
- it("(batch) mint to Alice and batch transfer 10 UTXOs honestly to Bob & Charlie then withdraw should succeed", async function () {
106
- // first mint the tokens for batch testing
107
- const inputUtxos = [];
108
- const nullifiers = [];
109
- for (let i = 0; i < 10; i++) {
110
- // mint 10 utxos
111
- const _utxo = newUTXO(1, Alice);
112
- nullifiers.push(newNullifier(_utxo, Alice));
113
- inputUtxos.push(_utxo);
114
- }
115
- const mintResult = await doMint(zeto, deployer, inputUtxos);
116
-
117
- const mintEvents = parseUTXOEvents(zeto, mintResult);
118
- const mintedHashes = mintEvents[0].outputs;
119
- for (let i = 0; i < mintedHashes.length; i++) {
120
- if (mintedHashes[i] !== 0) {
121
- await smtAlice.add(mintedHashes[i], mintedHashes[i]);
122
- await smtBob.add(mintedHashes[i], mintedHashes[i]);
123
- }
124
- }
125
- // Alice generates inclusion proofs for the UTXOs to be spent
126
- let root = await smtAlice.root();
127
- const mtps = [];
128
- for (let i = 0; i < inputUtxos.length; i++) {
129
- const p = await smtAlice.generateCircomVerifierProof(
130
- inputUtxos[i].hash,
131
- root,
132
- );
133
- mtps.push(p.siblings.map((s) => s.bigInt()));
134
- }
135
- const aliceUTXOsToBeWithdrawn = [
136
- newUTXO(1, Alice),
137
- newUTXO(1, Alice),
138
- newUTXO(1, Alice),
139
- ];
140
- // Alice proposes the output UTXOs, 1 utxo to bob, 1 utxo to charlie and 3 utxos to alice
141
- const _bOut1 = newUTXO(6, Bob);
142
- const _bOut2 = newUTXO(1, Charlie);
143
-
144
- const outputUtxos = [_bOut1, _bOut2, ...aliceUTXOsToBeWithdrawn];
145
- const outputOwners = [Bob, Charlie, Alice, Alice, Alice];
146
- const inflatedOutputUtxos = [...outputUtxos];
147
- const inflatedOutputOwners = [...outputOwners];
148
- for (let i = 0; i < 10 - outputUtxos.length; i++) {
149
- inflatedOutputUtxos.push(ZERO_UTXO);
150
- inflatedOutputOwners.push(Bob);
151
- }
152
- // Alice transfers her UTXOs to Bob
153
- const result = await doTransfer(
154
- Alice,
155
- inputUtxos,
156
- nullifiers,
157
- inflatedOutputUtxos,
158
- root.bigInt(),
159
- mtps,
160
- inflatedOutputOwners,
161
- );
162
-
163
- const signerAddress = await Alice.signer.getAddress();
164
- const events = parseUTXOEvents(zeto, result.txResult!);
165
- expect(events[0].submitter).to.equal(signerAddress);
166
- expect(events[0].inputs).to.deep.equal(nullifiers.map((n) => n.hash));
167
-
168
- const incomingUTXOs: any = events[0].outputs;
169
- // check the non-empty output hashes are correct
170
- for (let i = 0; i < outputUtxos.length; i++) {
171
- // Bob uses the information received from Alice to reconstruct the UTXO sent to him
172
- const receivedValue = outputUtxos[i].value;
173
- const receivedSalt = outputUtxos[i].salt;
174
- const hash = Poseidon.poseidon4([
175
- BigInt(receivedValue),
176
- receivedSalt,
177
- outputOwners[i].babyJubPublicKey[0],
178
- outputOwners[i].babyJubPublicKey[1],
179
- ]);
180
- expect(incomingUTXOs[i]).to.equal(hash);
181
- await smtAlice.add(incomingUTXOs[i], incomingUTXOs[i]);
182
- await smtBob.add(incomingUTXOs[i], incomingUTXOs[i]);
183
- }
184
-
185
- // check empty hashes are empty
186
- for (let i = outputUtxos.length; i < 10; i++) {
187
- expect(incomingUTXOs[i]).to.equal(0);
188
- }
189
-
190
- // mint sufficient balance in Zeto contract address for Alice to withdraw
191
- const mintTx = await erc20.connect(deployer).mint(zeto, 3);
192
- await mintTx.wait();
193
- const startingBalance = await erc20.balanceOf(Alice.ethAddress);
194
-
195
- // Alice generates the nullifiers for the UTXOs to be spent
196
- root = await smtAlice.root();
197
- const inflatedWithdrawNullifiers = [];
198
- const inflatedWithdrawInputs = [];
199
- const inflatedWithdrawMTPs = [];
200
- for (let i = 0; i < aliceUTXOsToBeWithdrawn.length; i++) {
201
- inflatedWithdrawInputs.push(aliceUTXOsToBeWithdrawn[i]);
202
- inflatedWithdrawNullifiers.push(
203
- newNullifier(aliceUTXOsToBeWithdrawn[i], Alice),
204
- );
205
- const _withdrawUTXOProof = await smtAlice.generateCircomVerifierProof(
206
- aliceUTXOsToBeWithdrawn[i].hash,
207
- root,
208
- );
209
- inflatedWithdrawMTPs.push(
210
- _withdrawUTXOProof.siblings.map((s) => s.bigInt()),
211
- );
212
- }
213
- // Alice generates inclusion proofs for the UTXOs to be spent
214
-
215
- for (let i = aliceUTXOsToBeWithdrawn.length; i < 10; i++) {
216
- inflatedWithdrawInputs.push(ZERO_UTXO);
217
- inflatedWithdrawNullifiers.push(ZERO_UTXO);
218
- const _zeroProof = await smtAlice.generateCircomVerifierProof(0n, root);
219
- inflatedWithdrawMTPs.push(_zeroProof.siblings.map((s) => s.bigInt()));
220
- }
221
-
222
- const {
223
- nullifiers: _withdrawNullifiers,
224
- outputCommitments: withdrawCommitments,
225
- encodedProof: withdrawEncodedProof,
226
- } = await prepareNullifierWithdrawProof(
227
- Alice,
228
- inflatedWithdrawInputs,
229
- inflatedWithdrawNullifiers,
230
- ZERO_UTXO,
231
- root.bigInt(),
232
- inflatedWithdrawMTPs,
233
- );
234
-
235
- // Alice withdraws her UTXOs to ERC20 tokens
236
- const tx = await zeto
237
- .connect(Alice.signer)
238
- .withdraw(
239
- 3,
240
- _withdrawNullifiers,
241
- withdrawCommitments[0],
242
- root.bigInt(),
243
- withdrawEncodedProof,
244
- "0x",
245
- );
246
- await tx.wait();
247
-
248
- // Alice checks her ERC20 balance
249
- const endingBalance = await erc20.balanceOf(Alice.ethAddress);
250
- expect(endingBalance - startingBalance).to.be.equal(3);
251
- }).timeout(60000);
252
-
253
- it("mint ERC20 tokens to Alice to deposit to Zeto should succeed", async function () {
254
- const startingBalance = await erc20.balanceOf(Alice.ethAddress);
255
- const tx = await erc20.connect(deployer).mint(Alice.ethAddress, 100);
256
- await tx.wait();
257
- const endingBalance = await erc20.balanceOf(Alice.ethAddress);
258
- expect(endingBalance - startingBalance).to.be.equal(100);
259
-
260
- const tx1 = await erc20.connect(Alice.signer).approve(zeto.target, 100);
261
- await tx1.wait();
262
-
263
- utxo100 = newUTXO(100, Alice);
264
- const utxo0 = newUTXO(0, Alice);
265
- const { outputCommitments, encodedProof } = await prepareDepositProof(
266
- Alice,
267
- [utxo0, utxo100],
132
+ // H-2: implementation contracts must be initialization-locked so an
133
+ // attacker cannot call initialize() directly on the impl, become its
134
+ // owner, and then upgradeTo(any) via _authorizeUpgrade (the OZ
135
+ // "implementation takeover" pattern, CVE-2022-35961 family). We test
136
+ // against the *actual deployed* implementation (read from the EIP-1967
137
+ // impl slot on the proxy) so this asserts the production deployment
138
+ // path produces a locked impl, not just that a redeployed contract
139
+ // would be locked.
140
+ it("initialize() reverts on the bare Zeto_AnonNullifier implementation contract", async function () {
141
+ const implAddress = await upgrades.erc1967.getImplementationAddress(
142
+ await zeto.getAddress(),
268
143
  );
269
- const tx2 = await zeto
270
- .connect(Alice.signer)
271
- .deposit(100, outputCommitments, encodedProof, "0x");
272
- await tx2.wait();
273
-
274
- await smtAlice.add(utxo100.hash, utxo100.hash);
275
- await smtAlice.add(utxo0.hash, utxo0.hash);
276
- await smtBob.add(utxo100.hash, utxo100.hash);
277
- await smtBob.add(utxo0.hash, utxo0.hash);
144
+ const impl = await ethers.getContractAt("Zeto_AnonNullifier", implAddress);
145
+ await expect(
146
+ impl.initialize("Z", "Z", await Alice.signer.getAddress(), {
147
+ verifier: ZeroAddress,
148
+ depositVerifier: ZeroAddress,
149
+ withdrawVerifier: ZeroAddress,
150
+ lockVerifier: ZeroAddress,
151
+ burnVerifier: ZeroAddress,
152
+ batchVerifier: ZeroAddress,
153
+ batchWithdrawVerifier: ZeroAddress,
154
+ batchLockVerifier: ZeroAddress,
155
+ batchBurnVerifier: ZeroAddress,
156
+ }),
157
+ ).to.be.revertedWithCustomError(impl, "InvalidInitialization");
278
158
  });
279
159
 
280
- it("mint to Alice and transfer UTXOs honestly to Bob should succeed", async function () {
281
- const startingBalance = await erc20.balanceOf(Alice.ethAddress);
282
- // The authority mints a new UTXO and assigns it to Alice
283
- utxo1 = newUTXO(10, Alice);
284
- utxo2 = newUTXO(20, Alice);
285
- const result1 = await doMint(zeto, deployer, [utxo1, utxo2]);
286
-
287
- // check the private mint activity is not exposed in the ERC20 contract
288
- const afterMintBalance = await erc20.balanceOf(Alice.ethAddress);
289
- expect(afterMintBalance).to.equal(startingBalance);
290
-
291
- // Alice locally tracks the UTXOs inside the Sparse Merkle Tree
292
- // hardhat doesn't have a good way to subscribe to events so we have to parse the Tx result object
293
- const mintEvents = parseUTXOEvents(zeto, result1);
294
- const [_utxo1, _utxo2] = mintEvents[0].outputs;
295
- await smtAlice.add(_utxo1, _utxo1);
296
- await smtAlice.add(_utxo2, _utxo2);
297
- let root = await smtAlice.root();
298
- let onchainRoot = await zeto.getRoot();
299
- expect(root.string()).to.equal(onchainRoot.toString());
300
- // Bob also locally tracks the UTXOs inside the Sparse Merkle Tree
301
- await smtBob.add(_utxo1, _utxo1);
302
- await smtBob.add(_utxo2, _utxo2);
303
-
304
- // Alice proposes the output UTXOs for the transfer to Bob
305
- const _utxo3 = newUTXO(25, Bob);
306
- utxo4 = newUTXO(5, Alice);
307
-
308
- // Alice generates the nullifiers for the UTXOs to be spent
309
- const nullifier1 = newNullifier(utxo1, Alice);
310
- const nullifier2 = newNullifier(utxo2, Alice);
311
-
312
- // Alice generates inclusion proofs for the UTXOs to be spent
313
- const proof1 = await smtAlice.generateCircomVerifierProof(utxo1.hash, root);
314
- const proof2 = await smtAlice.generateCircomVerifierProof(utxo2.hash, root);
315
- const merkleProofs = [
316
- proof1.siblings.map((s) => s.bigInt()),
317
- proof2.siblings.map((s) => s.bigInt()),
318
- ];
319
-
320
- // Alice transfers her UTXOs to Bob
321
- const result2 = await doTransfer(
322
- Alice,
323
- [utxo1, utxo2],
324
- [nullifier1, nullifier2],
325
- [_utxo3, utxo4],
326
- root.bigInt(),
327
- merkleProofs,
328
- [Bob, Alice],
329
- );
330
-
331
- // check the private transfer activity is not exposed in the ERC20 contract
332
- const afterTransferBalance = await erc20.balanceOf(Alice.ethAddress);
333
- expect(afterTransferBalance).to.equal(startingBalance);
334
-
335
- // Alice locally tracks the UTXOs inside the Sparse Merkle Tree
336
- await smtAlice.add(_utxo3.hash, _utxo3.hash);
337
- await smtAlice.add(utxo4.hash, utxo4.hash);
338
- root = await smtAlice.root();
339
- onchainRoot = await zeto.getRoot();
340
- expect(root.string()).to.equal(onchainRoot.toString());
341
-
342
- // Bob locally tracks the UTXOs inside the Sparse Merkle Tree
343
- // Bob parses the UTXOs from the onchain event
344
- const signerAddress = await Alice.signer.getAddress();
345
- const events = parseUTXOEvents(zeto, result2.txResult!);
346
- expect(events[0].submitter).to.equal(signerAddress);
347
- expect(events[0].inputs).to.deep.equal([nullifier1.hash, nullifier2.hash]);
348
- expect(events[0].outputs).to.deep.equal([_utxo3.hash, utxo4.hash]);
349
- await smtBob.add(events[0].outputs[0], events[0].outputs[0]);
350
- await smtBob.add(events[0].outputs[1], events[0].outputs[1]);
351
-
352
- // Bob uses the information received from Alice to reconstruct the UTXO sent to him
353
- const receivedValue = _utxo3.value!;
354
- const receivedSalt = _utxo3.salt;
355
- const incomingUTXOs: any = events[0].outputs;
356
- const hash = Poseidon.poseidon4([
357
- BigInt(receivedValue),
358
- receivedSalt,
359
- Bob.babyJubPublicKey[0],
360
- Bob.babyJubPublicKey[1],
361
- ]);
362
- expect(incomingUTXOs[0]).to.equal(hash);
363
-
364
- // Bob uses the received values to construct the UTXO received from the transaction
365
- utxo3 = newUTXO(receivedValue, Bob, receivedSalt);
366
- }).timeout(600000);
367
-
368
- it("Bob transfers UTXOs, previously received from Alice, honestly to Charlie should succeed", async function () {
369
- // Bob generates the nullifiers for the UTXO to be spent
370
- const nullifier1 = newNullifier(utxo3, Bob);
371
-
372
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
373
- const root = await smtBob.root();
374
- const proof1 = await smtBob.generateCircomVerifierProof(utxo3.hash, root);
375
- const proof2 = await smtBob.generateCircomVerifierProof(0n, root);
376
- const merkleProofs = [
377
- proof1.siblings.map((s) => s.bigInt()),
378
- proof2.siblings.map((s) => s.bigInt()),
379
- ];
380
-
381
- // Bob proposes the output UTXOs
382
- const utxo6 = newUTXO(10, Charlie);
383
- utxo7 = newUTXO(15, Bob);
384
-
385
- // Bob should be able to spend the UTXO that was reconstructed from the previous transaction
386
- const result = await doTransfer(
387
- Bob,
388
- [utxo3, ZERO_UTXO],
389
- [nullifier1, ZERO_UTXO],
390
- [utxo6, utxo7],
391
- root.bigInt(),
392
- merkleProofs,
393
- [Charlie, Bob],
394
- );
395
-
396
- // Bob keeps the local SMT in sync
397
- await smtBob.add(utxo6.hash, utxo6.hash);
398
- await smtBob.add(utxo7.hash, utxo7.hash);
399
-
400
- // Alice gets the new UTXOs from the onchain event and keeps the local SMT in sync
401
- const events = parseUTXOEvents(zeto, result.txResult!);
402
- await smtAlice.add(events[0].outputs[0], events[0].outputs[0]);
403
- await smtAlice.add(events[0].outputs[1], events[0].outputs[1]);
404
- }).timeout(600000);
160
+ describe("batch transfers", () => {
161
+ let inputUtxos: UTXO[];
162
+ let nullifiers: UTXO[];
163
+ let outputUtxos: UTXO[];
164
+ let outputOwners: User[];
165
+ let aliceUTXOsToBeWithdrawn: UTXO[];
166
+ let txResult: any;
405
167
 
406
- it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () {
407
- const startingBalance = await erc20.balanceOf(Alice.ethAddress);
408
-
409
- // Alice generates the nullifiers for the UTXOs to be spent
410
- const nullifier1 = newNullifier(utxo100, Alice);
411
-
412
- // Alice generates inclusion proofs for the UTXOs to be spent
413
- let root = await smtAlice.root();
414
- const proof1 = await smtAlice.generateCircomVerifierProof(
415
- utxo100.hash,
416
- root,
417
- );
418
- const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
419
- const merkleProofs = [
420
- proof1.siblings.map((s) => s.bigInt()),
421
- proof2.siblings.map((s) => s.bigInt()),
422
- ];
168
+ beforeEach(async function () {
169
+ // this.skip();
170
+ });
423
171
 
424
- // Alice proposes the output ERC20 tokens
425
- const withdrawChangesUTXO = newUTXO(20, Alice);
172
+ it("mint 10 UTXOs to Alice", async function () {
173
+ // first mint the tokens for batch testing
174
+ inputUtxos = [];
175
+ nullifiers = [];
176
+ for (let i = 0; i < 10; i++) {
177
+ // mint 10 utxos
178
+ const _utxo = newUTXO(1, Alice);
179
+ nullifiers.push(newNullifier(_utxo, Alice));
180
+ inputUtxos.push(_utxo);
181
+ }
182
+ const mintResult = await doMint(zeto, deployer, inputUtxos);
183
+
184
+ const mintEvents = parseUTXOEvents(zeto, mintResult);
185
+ const mintedHashes = mintEvents[0].outputs;
186
+ for (let i = 0; i < mintedHashes.length; i++) {
187
+ if (mintedHashes[i] !== 0) {
188
+ await smtAlice.add(mintedHashes[i], mintedHashes[i]);
189
+ await smtBob.add(mintedHashes[i], mintedHashes[i]);
190
+ }
191
+ }
192
+ });
426
193
 
427
- const { nullifiers, outputCommitments, encodedProof } =
428
- await prepareNullifierWithdrawProof(
194
+ it("Alice transfers some UTXOs to Bob and Charlie", async function () {
195
+ // Batched proof generation routinely runs ~38–40s on a warm machine;
196
+ // the mocha default of 40s flakes on cold starts. Mirror the explicit
197
+ // timeout already used for the batch-withdraw it() below.
198
+ this.timeout(60000);
199
+ // Alice generates inclusion proofs for the UTXOs to be spent
200
+ let root = await smtAlice.root();
201
+ const mtps = [];
202
+ for (let i = 0; i < inputUtxos.length; i++) {
203
+ const p = await smtAlice.generateCircomVerifierProof(
204
+ inputUtxos[i].hash,
205
+ root,
206
+ );
207
+ mtps.push(p.siblings.map((s) => s.bigInt()));
208
+ }
209
+ aliceUTXOsToBeWithdrawn = [
210
+ newUTXO(1, Alice),
211
+ newUTXO(1, Alice),
212
+ newUTXO(1, Alice),
213
+ ];
214
+ // Alice proposes the output UTXOs, 1 utxo to bob, 1 utxo to charlie and 3 utxos to alice
215
+ const _bOut1 = newUTXO(6, Bob);
216
+ const _bOut2 = newUTXO(1, Charlie);
217
+
218
+ outputUtxos = [_bOut1, _bOut2, ...aliceUTXOsToBeWithdrawn];
219
+ outputOwners = [Bob, Charlie, Alice, Alice, Alice];
220
+ // Alice transfers her UTXOs to Bob
221
+ txResult = await doTransfer(
429
222
  Alice,
430
- [utxo100, ZERO_UTXO],
431
- [nullifier1, ZERO_UTXO],
432
- withdrawChangesUTXO,
433
- root.bigInt(),
434
- merkleProofs,
435
- );
436
-
437
- // Alice withdraws her UTXOs to ERC20 tokens
438
- const tx = await zeto
439
- .connect(Alice.signer)
440
- .withdraw(
441
- 80,
223
+ inputUtxos,
442
224
  nullifiers,
443
- outputCommitments[0],
225
+ outputUtxos,
444
226
  root.bigInt(),
445
- encodedProof,
446
- "0x",
227
+ mtps,
228
+ outputOwners,
447
229
  );
448
- await tx.wait();
449
-
450
- // Alice tracks the UTXO inside the SMT
451
- await smtAlice.add(withdrawChangesUTXO.hash, withdrawChangesUTXO.hash);
452
- // Bob also locally tracks the UTXOs inside the SMT
453
- await smtBob.add(withdrawChangesUTXO.hash, withdrawChangesUTXO.hash);
454
-
455
- // Alice checks her ERC20 balance
456
- const endingBalance = await erc20.balanceOf(Alice.ethAddress);
457
- expect(endingBalance - startingBalance).to.be.equal(80);
458
- });
230
+ });
459
231
 
460
- it("(burnable) mint to Alice and burn should succeed", async function () {
461
- // first mint the tokens
462
- const inputUtxos = [];
463
- const nullifiers = [];
464
- for (let i = 0; i < 2; i++) {
465
- const _utxo = newUTXO(1, Alice);
466
- nullifiers.push(newNullifier(_utxo, Alice));
467
- inputUtxos.push(_utxo);
468
- }
469
- const mintResult = await doMint(zetoBurnable, deployer, inputUtxos);
470
- const outputUtxo = newUTXO(0, Alice);
232
+ it("check the transfer is successful", async function () {
233
+ const signerAddress = await Alice.signer.getAddress();
234
+ const events = parseUTXOEvents(zeto, txResult);
235
+ expect(events[0].submitter).to.equal(signerAddress);
236
+ expect(events[0].inputs).to.deep.equal(nullifiers.map((n) => n.hash));
237
+
238
+ const incomingUTXOs: any = events[0].outputs;
239
+ // check the non-empty output hashes are correct
240
+ for (let i = 0; i < outputUtxos.length; i++) {
241
+ // Bob uses the information received from Alice to reconstruct the UTXO sent to him
242
+ const receivedValue = outputUtxos[i].value;
243
+ const receivedSalt = outputUtxos[i].salt;
244
+ const hash = Poseidon.poseidon4([
245
+ BigInt(receivedValue),
246
+ receivedSalt,
247
+ outputOwners[i].babyJubPublicKey[0],
248
+ outputOwners[i].babyJubPublicKey[1],
249
+ ]);
250
+ expect(incomingUTXOs[i]).to.equal(hash);
251
+ await smtAlice.add(incomingUTXOs[i], incomingUTXOs[i]);
252
+ await smtBob.add(incomingUTXOs[i], incomingUTXOs[i]);
253
+ }
254
+ });
471
255
 
472
- const storage1 = new InMemoryDB(str2Bytes(""));
473
- const smtAliceBurnable = new Merkletree(storage1, true, 64);
256
+ it("Alice withdraws her UTXOs to ERC20 tokens", async function () {
257
+ // mint sufficient balance in Zeto contract address for Alice to withdraw
258
+ const mintTx = await erc20.connect(deployer).mint(zeto, 3);
259
+ await mintTx.wait();
260
+ const startingBalance = await erc20.balanceOf(Alice.ethAddress);
474
261
 
475
- const mintEvents = parseUTXOEvents(zetoBurnable, mintResult);
476
- const mintedHashes = mintEvents[0].outputs;
477
- for (let i = 0; i < mintedHashes.length; i++) {
478
- if (mintedHashes[i] !== 0) {
479
- await smtAliceBurnable.add(mintedHashes[i], mintedHashes[i]);
262
+ // Alice generates the nullifiers for the UTXOs to be spent
263
+ const root = await smtAlice.root();
264
+ const inflatedWithdrawNullifiers = [];
265
+ const inflatedWithdrawInputs = [];
266
+ const inflatedWithdrawMTPs = [];
267
+ for (let i = 0; i < aliceUTXOsToBeWithdrawn.length; i++) {
268
+ inflatedWithdrawInputs.push(aliceUTXOsToBeWithdrawn[i]);
269
+ inflatedWithdrawNullifiers.push(
270
+ newNullifier(aliceUTXOsToBeWithdrawn[i], Alice),
271
+ );
272
+ const _withdrawUTXOProof = await smtAlice.generateCircomVerifierProof(
273
+ aliceUTXOsToBeWithdrawn[i].hash,
274
+ root,
275
+ );
276
+ inflatedWithdrawMTPs.push(
277
+ _withdrawUTXOProof.siblings.map((s) => s.bigInt()),
278
+ );
480
279
  }
481
- }
482
-
483
- // Alice generates inclusion proofs for the UTXOs to be spent
484
- let root = await smtAliceBurnable.root();
485
- const mtps = [];
486
- for (let i = 0; i < inputUtxos.length; i++) {
487
- const p = await smtAliceBurnable.generateCircomVerifierProof(
488
- inputUtxos[i].hash,
489
- root,
490
- );
491
- mtps.push(p.siblings.map((s) => s.bigInt()));
492
- }
280
+ // Alice generates inclusion proofs for the UTXOs to be spent
493
281
 
494
- // Alice generates the nullifiers for the UTXOs to be burnt
495
- const {
496
- nullifiers: _burnNullifiers,
497
- outputCommitment,
498
- encodedProof,
499
- } = await prepareNullifierBurnProof(
500
- Alice,
501
- inputUtxos,
502
- outputUtxo,
503
- nullifiers,
504
- root.bigInt(),
505
- mtps,
506
- );
282
+ for (let i = aliceUTXOsToBeWithdrawn.length; i < 10; i++) {
283
+ inflatedWithdrawInputs.push(ZERO_UTXO);
284
+ inflatedWithdrawNullifiers.push(ZERO_UTXO);
285
+ const _zeroProof = await smtAlice.generateCircomVerifierProof(0n, root);
286
+ inflatedWithdrawMTPs.push(_zeroProof.siblings.map((s) => s.bigInt()));
287
+ }
507
288
 
508
- // Alice withdraws her UTXOs to ERC20 tokens
509
- const tx = await zetoBurnable
510
- .connect(Alice.signer)
511
- .burn(
512
- _burnNullifiers,
513
- outputCommitment,
289
+ const {
290
+ nullifiers: _withdrawNullifiers,
291
+ outputCommitments: withdrawCommitments,
292
+ encodedProof: withdrawEncodedProof,
293
+ } = await prepareNullifierWithdrawProof(
294
+ Alice,
295
+ inflatedWithdrawInputs,
296
+ inflatedWithdrawNullifiers,
297
+ ZERO_UTXO,
514
298
  root.bigInt(),
515
- encodedProof,
516
- "0x",
299
+ inflatedWithdrawMTPs,
517
300
  );
518
- await tx.wait();
519
- }).timeout(60000);
520
-
521
- describe("lock() tests", function () {
522
- let lockedUtxo1: UTXO;
523
- let smtBobForLocked: Merkletree;
524
- let utxo10: UTXO;
525
- let utxo11: UTXO;
526
- let utxo12: UTXO;
527
301
 
528
- before(async function () {
529
- const storage1 = new InMemoryDB(str2Bytes(""));
530
- smtBobForLocked = new Merkletree(storage1, true, 64);
302
+ // Alice withdraws her UTXOs to ERC20 tokens
303
+ const tx = await zeto
304
+ .connect(Alice.signer)
305
+ .withdraw(
306
+ 3,
307
+ _withdrawNullifiers,
308
+ withdrawCommitments[0],
309
+ encodeToBytesForWithdraw(root.bigInt(), withdrawEncodedProof),
310
+ "0x",
311
+ );
312
+ const result1 = await tx.wait();
313
+ logger.debug(`Method withdraw() complete. Gas used: ${result1?.gasUsed}`);
531
314
 
532
- // mint a UTXO for Bob and spend it (to use it in a failure case test)
533
- utxo12 = newUTXO(1, Alice);
534
- await doMint(zeto, deployer, [utxo12]);
535
- await smtAlice.add(utxo12.hash, utxo12.hash);
536
- await smtBob.add(utxo12.hash, utxo12.hash);
315
+ // Alice checks her ERC20 balance
316
+ const endingBalance = await erc20.balanceOf(Alice.ethAddress);
317
+ expect(endingBalance - startingBalance).to.be.equal(3);
318
+ }).timeout(60000);
319
+ });
537
320
 
538
- const _utxo1 = newUTXO(1, Bob);
539
- const nullifier1 = newNullifier(utxo12, Alice);
540
- let root = await smtAlice.root();
541
- const proof1 = await smtAlice.generateCircomVerifierProof(
542
- utxo12.hash,
543
- root,
544
- );
545
- const proof2 = await smtAlice.generateCircomVerifierProof(
546
- utxo12.hash,
547
- root,
548
- );
549
- const merkleProofs = [
550
- proof1.siblings.map((s) => s.bigInt()),
551
- proof2.siblings.map((s) => s.bigInt()),
552
- ];
321
+ describe("mint, deposit, transfer, withdraw flows", () => {
322
+ let aliceUtxo30: UTXO;
323
+ let aliceUtxo70: UTXO;
553
324
 
554
- await doTransfer(
555
- Alice,
556
- [utxo12, ZERO_UTXO],
557
- [nullifier1, ZERO_UTXO],
558
- [_utxo1, ZERO_UTXO],
559
- root.bigInt(),
560
- merkleProofs,
561
- [Bob, Alice],
562
- );
563
- await smtAlice.add(_utxo1.hash, _utxo1.hash);
564
- await smtBob.add(_utxo1.hash, _utxo1.hash);
325
+ beforeEach(async function () {
326
+ // this.skip();
565
327
  });
566
328
 
567
- describe("lock -> transfer flow", function () {
568
- it("lock() should succeed when using unlocked states", async function () {
569
- const nullifier1 = newNullifier(utxo7, Bob);
570
- lockedUtxo1 = newUTXO(utxo7.value!, Bob);
571
- const root = await smtBob.root();
572
- const proof1 = await smtBob.generateCircomVerifierProof(
573
- utxo7.hash,
574
- root,
575
- );
576
- const proof2 = await smtBob.generateCircomVerifierProof(0n, root);
577
- const merkleProofs = [
578
- proof1.siblings.map((s) => s.bigInt()),
579
- proof2.siblings.map((s) => s.bigInt()),
580
- ];
581
- const { outputCommitments, encodedProof } = await prepareProof(
582
- circuit,
583
- provingKey,
584
- Bob,
585
- [utxo7, ZERO_UTXO],
586
- [nullifier1, ZERO_UTXO],
587
- [lockedUtxo1, ZERO_UTXO],
588
- root.bigInt(),
589
- merkleProofs,
590
- [Bob, Bob],
591
- );
329
+ describe("Shielding ERC20 tokens to Zeto privacy tokens", async function () {
330
+ it("mint ERC20 tokens to Alice", async function () {
331
+ const startingBalance = await erc20.balanceOf(Alice.ethAddress);
332
+ const tx = await erc20.connect(deployer).mint(Alice.ethAddress, 100);
333
+ await tx.wait();
334
+ const endingBalance = await erc20.balanceOf(Alice.ethAddress);
335
+ expect(endingBalance - startingBalance).to.be.equal(100);
336
+ });
592
337
 
593
- const tx = await zeto.connect(Bob.signer).lock(
594
- [nullifier1.hash],
595
- [],
596
- outputCommitments,
597
- root.bigInt(),
598
- encodedProof,
599
- Alice.ethAddress, // make Alice the delegate who can spend the state (if she has the right proof)
600
- "0x",
601
- );
602
- const result: ContractTransactionReceipt | null = await tx.wait();
338
+ it("Alice approves the Zeto contract to spend her ERC20 tokens, to prepare for the deposit", async function () {
339
+ const tx1 = await erc20.connect(Alice.signer).approve(zeto.target, 100);
340
+ await tx1.wait();
341
+ });
603
342
 
604
- // Note that the locked UTXO should NOT be added to the local SMT for UTXOs because it's tracked in a separate SMT onchain
605
- // we add it to the local SMT for locked UTXOs
606
- const events = parseUTXOEvents(zeto, result!);
607
- await smtBobForLocked.add(
608
- events[0].lockedOutputs[0],
609
- ethers.toBigInt(events[0].delegate),
343
+ it("Alice deposits her ERC20 tokens to Zeto, and get shielded UTXOs in return", async function () {
344
+ aliceUtxo30 = newUTXO(30, Alice);
345
+ aliceUtxo70 = newUTXO(70, Alice);
346
+ const { outputCommitments, encodedProof } = await prepareDepositProof(
347
+ Alice,
348
+ [aliceUtxo30, aliceUtxo70],
610
349
  );
350
+ const tx2 = await zeto
351
+ .connect(Alice.signer)
352
+ .deposit(
353
+ 100,
354
+ outputCommitments,
355
+ encodeToBytesForDeposit(encodedProof),
356
+ "0x",
357
+ );
358
+ const result = await tx2.wait();
359
+ logger.debug(`Method deposit() complete. Gas used: ${result?.gasUsed}`);
360
+
361
+ await smtAlice.add(aliceUtxo30.hash, aliceUtxo30.hash);
362
+ await smtAlice.add(aliceUtxo70.hash, aliceUtxo70.hash);
363
+ await smtBob.add(aliceUtxo30.hash, aliceUtxo30.hash);
364
+ await smtBob.add(aliceUtxo70.hash, aliceUtxo70.hash);
611
365
  });
366
+ });
367
+
368
+ describe("Transferring privacy tokens", async function () {
369
+ let value1: any;
370
+ let salt1: bigint;
371
+ let transferEvent: any;
612
372
 
613
- it("onchain SMT root for the locked UTXOs should be equal to the offchain SMT root", async function () {
614
- const root = await smtBobForLocked.root();
615
- const onchainRoot = await zeto.getRootForLocked();
616
- expect(root.string()).to.equal(onchainRoot.toString());
373
+ it("Check the onchain SMT root for the unlocked UTXOs should be equal to the offchain SMT root", async function () {
374
+ const onchainRoot = await zeto.getRoot();
375
+ const aliceRoot = await smtAlice.root();
376
+ expect(aliceRoot.string()).to.equal(onchainRoot.toString());
377
+ const bobRoot = await smtBob.root();
378
+ expect(bobRoot.string()).to.equal(onchainRoot.toString());
617
379
  });
618
380
 
619
- it("lock() should fail when trying to lock again by not as the current delegate", async function () {
620
- if (network.name !== "hardhat") {
621
- return;
622
- }
381
+ it("Alice transfers her privacy tokens to Bob", async function () {
382
+ // Alice proposes the output UTXOs for the transfer to Bob
383
+ // 25 to Bob
384
+ const _utxo1 = newUTXO(25, Bob);
385
+ // 5 back to Alice as change
386
+ const _utxo2 = newUTXO(5, Alice);
623
387
 
624
- // the owner of the locked UTXO can generate the proper proof to spend the locked state,
625
- // but not able to lock again because it's not the current delegate
626
- const nullifier1 = newNullifier(lockedUtxo1, Bob);
627
- const utxo1 = newUTXO(lockedUtxo1.value!, Bob);
628
- const root = await smtBobForLocked.root();
629
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
630
- lockedUtxo1.hash,
631
- root,
632
- );
633
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
634
- 0n,
388
+ // Alice will share these secrets with Bob when she performs the transfer
389
+ value1 = _utxo1.value!;
390
+ salt1 = _utxo1.salt!;
391
+
392
+ // Alice generates the nullifiers for the UTXOs to be spent
393
+ const nullifier1 = newNullifier(aliceUtxo30, Alice);
394
+
395
+ // Alice generates inclusion proofs for the UTXOs to be spent
396
+ const root = await smtAlice.root();
397
+ const proof1 = await smtAlice.generateCircomVerifierProof(
398
+ aliceUtxo30.hash,
635
399
  root,
636
400
  );
401
+ const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
637
402
  const merkleProofs = [
638
403
  proof1.siblings.map((s) => s.bigInt()),
639
404
  proof2.siblings.map((s) => s.bigInt()),
640
405
  ];
641
- const { outputCommitments, encodedProof } = await prepareProof(
642
- circuitForLocked,
643
- provingKeyForLocked,
644
- Bob,
645
- [lockedUtxo1, ZERO_UTXO],
406
+
407
+ // Alice transfers her UTXOs to Bob
408
+ const result2 = await doTransfer(
409
+ Alice,
410
+ [aliceUtxo30, ZERO_UTXO],
646
411
  [nullifier1, ZERO_UTXO],
647
- [utxo1, ZERO_UTXO],
412
+ [_utxo1, _utxo2],
648
413
  root.bigInt(),
649
414
  merkleProofs,
650
- [Bob, Bob],
651
- Alice.ethAddress,
415
+ [Bob, Alice],
652
416
  );
653
417
 
654
- await expect(
655
- zeto
656
- .connect(Bob.signer)
657
- .lock(
658
- [nullifier1.hash],
659
- [],
660
- outputCommitments,
661
- root.bigInt(),
662
- encodedProof,
663
- Charlie.ethAddress,
664
- "0x",
665
- ),
666
- ).to.be.rejectedWith("UTXORootNotFound");
667
- });
418
+ // Alice locally tracks the UTXOs inside the Sparse Merkle Tree
419
+ await smtAlice.add(_utxo1.hash, _utxo1.hash);
420
+ await smtAlice.add(_utxo2.hash, _utxo2.hash);
668
421
 
669
- it("the original owner can NOT spend the locked state", async function () {
670
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
671
- const nullifier1 = newNullifier(lockedUtxo1, Bob);
672
- const root = await smtBobForLocked.root();
673
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
674
- lockedUtxo1.hash,
675
- root,
676
- );
677
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
678
- 0n,
679
- root,
680
- );
681
- const merkleProofs = [
682
- proof1.siblings.map((s) => s.bigInt()),
683
- proof2.siblings.map((s) => s.bigInt()),
684
- ];
685
- // Bob proposes the output UTXOs, attempting to transfer the locked UTXO to Charlie
686
- utxo9 = newUTXO(15, Charlie);
422
+ const events = parseUTXOEvents(zeto, result2);
423
+ transferEvent = events[0];
424
+ const signerAddress = await Alice.signer.getAddress();
425
+ expect(transferEvent.submitter).to.equal(signerAddress);
426
+ });
687
427
 
688
- // Bob should NOT be able to spend the UTXO which has been locked and delegated to Alice
689
- await expect(
690
- doTransfer(
428
+ describe("Bob can spend the tokens received from Alice", async function () {
429
+ let bobUtxo25: UTXO;
430
+ let transferEventToCharlie: any;
431
+
432
+ it("Bob locally tracks the new UTXOs inside the Sparse Merkle Tree", async function () {
433
+ // Bob parses the UTXOs from the onchain event
434
+ // and add them to the local SMT
435
+ await smtBob.add(transferEvent.outputs[0], transferEvent.outputs[0]);
436
+ await smtBob.add(transferEvent.outputs[1], transferEvent.outputs[1]);
437
+
438
+ // Bob uses the received values to construct the UTXO received from the transaction
439
+ bobUtxo25 = newUTXO(value1, Bob, salt1);
440
+ // Bob verifies the UTXO is valid onchain
441
+ expect(bobUtxo25.hash).to.equal(transferEvent.outputs[0]);
442
+ });
443
+
444
+ it("Bob transfers UTXOs, previously received from Alice, honestly to Charlie should succeed", async function () {
445
+ // Bob generates the nullifiers for the UTXO to be spent
446
+ const nullifier1 = newNullifier(bobUtxo25, Bob);
447
+
448
+ // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
449
+ const root = await smtBob.root();
450
+ const proof1 = await smtBob.generateCircomVerifierProof(
451
+ bobUtxo25.hash,
452
+ root,
453
+ );
454
+ const proof2 = await smtBob.generateCircomVerifierProof(0n, root);
455
+ const merkleProofs = [
456
+ proof1.siblings.map((s) => s.bigInt()),
457
+ proof2.siblings.map((s) => s.bigInt()),
458
+ ];
459
+
460
+ // Bob proposes the output UTXOs
461
+ const _utxo1 = newUTXO(10, Charlie);
462
+ const _utxo2 = newUTXO(15, Bob);
463
+
464
+ // Bob should be able to spend the UTXO that was reconstructed from the previous transaction
465
+ const result = await doTransfer(
691
466
  Bob,
692
- [lockedUtxo1, ZERO_UTXO],
467
+ [bobUtxo25, ZERO_UTXO],
693
468
  [nullifier1, ZERO_UTXO],
694
- [utxo9, ZERO_UTXO],
469
+ [_utxo1, _utxo2],
695
470
  root.bigInt(),
696
471
  merkleProofs,
697
472
  [Charlie, Bob],
698
- Alice,
699
- ),
700
- ).to.be.rejectedWith("Invalid proof"); // due to the lockDelegate part of the public input not matching the current delegate
473
+ );
474
+
475
+ // Bob keeps the local SMT in sync
476
+ await smtBob.add(_utxo1.hash, _utxo1.hash);
477
+ await smtBob.add(_utxo2.hash, _utxo2.hash);
478
+
479
+ const events = parseUTXOEvents(zeto, result);
480
+ transferEventToCharlie = events[0];
481
+ });
482
+
483
+ it("Alice gets the new UTXOs from the onchain event and keeps the local SMT in sync", async function () {
484
+ await smtAlice.add(
485
+ transferEventToCharlie.outputs[0],
486
+ transferEventToCharlie.outputs[0],
487
+ );
488
+ await smtAlice.add(
489
+ transferEventToCharlie.outputs[1],
490
+ transferEventToCharlie.outputs[1],
491
+ );
492
+ });
701
493
  });
494
+ });
702
495
 
703
- it("the original owner can NOT withdraw the locked state", async function () {
704
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
705
- const nullifier1 = newNullifier(lockedUtxo1, Bob);
706
- const root = await smtBobForLocked.root();
707
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
708
- lockedUtxo1.hash,
709
- root,
710
- );
711
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
712
- 0n,
496
+ describe("Alice withdraws her UTXOs back to ERC20 tokens", async function () {
497
+ let startingBalance: any;
498
+ let withdrawEvent: any;
499
+
500
+ it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () {
501
+ startingBalance = await erc20.balanceOf(Alice.ethAddress);
502
+
503
+ // Alice generates the nullifiers for the UTXOs to be spent
504
+ const nullifier1 = newNullifier(aliceUtxo70, Alice);
505
+
506
+ // Alice generates inclusion proofs for the UTXOs to be spent
507
+ let root = await smtAlice.root();
508
+ const proof1 = await smtAlice.generateCircomVerifierProof(
509
+ aliceUtxo70.hash,
713
510
  root,
714
511
  );
512
+ const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
715
513
  const merkleProofs = [
716
514
  proof1.siblings.map((s) => s.bigInt()),
717
515
  proof2.siblings.map((s) => s.bigInt()),
718
516
  ];
719
517
 
720
- const utxo1 = newUTXO(5, Bob);
518
+ // Alice proposes the output ERC20 tokens
519
+ const withdrawChangesUTXO = newUTXO(20, Alice);
721
520
 
722
- await expect(
723
- prepareNullifierWithdrawProof(
724
- Bob,
725
- [lockedUtxo1, ZERO_UTXO],
521
+ const { nullifiers, outputCommitments, encodedProof } =
522
+ await prepareNullifierWithdrawProof(
523
+ Alice,
524
+ [aliceUtxo70, ZERO_UTXO],
726
525
  [nullifier1, ZERO_UTXO],
727
- utxo1,
526
+ withdrawChangesUTXO,
728
527
  root.bigInt(),
729
528
  merkleProofs,
730
- ),
731
- ).to.be.rejectedWith("SMTVerifier_249 line: 134");
732
- });
529
+ );
733
530
 
734
- it("attempting to use an existing UTXO as output should fail", async function () {
735
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
736
- const nullifier1 = newNullifier(lockedUtxo1, Bob);
737
- const root = await smtBobForLocked.root();
738
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
739
- lockedUtxo1.hash,
740
- root,
531
+ // Alice withdraws her UTXOs to ERC20 tokens
532
+ const tx = await zeto
533
+ .connect(Alice.signer)
534
+ .withdraw(
535
+ 50,
536
+ nullifiers,
537
+ outputCommitments[0],
538
+ encodeToBytesForWithdraw(root.bigInt(), encodedProof),
539
+ "0x",
540
+ );
541
+ const result = await tx.wait();
542
+ logger.debug(
543
+ `Method withdraw() complete. Gas used: ${result?.gasUsed}`,
741
544
  );
742
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
743
- 0n,
545
+
546
+ // Alice tracks the UTXO inside the SMT
547
+ await smtAlice.add(withdrawChangesUTXO.hash, withdrawChangesUTXO.hash);
548
+ const events = parseUTXOEvents(zeto, result);
549
+ withdrawEvent = events[1];
550
+ });
551
+
552
+ it("Bob also locally tracks the new UTXOs from the withdraw event inside the SMT", async function () {
553
+ await smtBob.add(withdrawEvent.output, withdrawEvent.output);
554
+ });
555
+
556
+ it("Alice checks her ERC20 balance", async function () {
557
+ const endingBalance = await erc20.balanceOf(Alice.ethAddress);
558
+ expect(endingBalance - startingBalance).to.be.equal(50);
559
+ });
560
+ });
561
+ });
562
+
563
+ describe("ILockableCapability tests", function () {
564
+ // ABI fragments for the ZetoLockableCapability *Args payloads.
565
+ const CREATE_ARGS_ABI =
566
+ "tuple(bytes32 txId, uint256[] inputs, uint256[] outputs, uint256[] lockedOutputs, bytes proof)";
567
+ const UPDATE_ARGS_ABI = "tuple(bytes32 txId)";
568
+ const DELEGATE_ARGS_ABI = "tuple(bytes32 txId)";
569
+ const SPEND_ARGS_ABI =
570
+ "tuple(bytes32 txId, uint256[] lockedOutputs, uint256[] outputs, bytes proof, bytes data)";
571
+
572
+ function encodeCreateArgs(args: {
573
+ txId: string;
574
+ inputs: BigNumberish[];
575
+ outputs: BigNumberish[];
576
+ lockedOutputs: BigNumberish[];
577
+ proof: string;
578
+ }) {
579
+ return new AbiCoder().encode([CREATE_ARGS_ABI], [args]);
580
+ }
581
+
582
+ function encodeUpdateArgs(txId: string) {
583
+ return new AbiCoder().encode([UPDATE_ARGS_ABI], [{ txId }]);
584
+ }
585
+
586
+ function encodeDelegateArgs(txId: string) {
587
+ return new AbiCoder().encode([DELEGATE_ARGS_ABI], [{ txId }]);
588
+ }
589
+
590
+ function encodeSpendArgs(args: {
591
+ txId: string;
592
+ lockedOutputs: BigNumberish[];
593
+ outputs: BigNumberish[];
594
+ proof: string;
595
+ data: string;
596
+ }) {
597
+ return new AbiCoder().encode([SPEND_ARGS_ABI], [args]);
598
+ }
599
+
600
+ function randomBytes32(): string {
601
+ return ethers.hexlify(ethers.randomBytes(32));
602
+ }
603
+
604
+ beforeEach(async function () {
605
+ // this.skip();
606
+ });
607
+
608
+ describe("createLock -> updateLock -> delegateLock -> spendLock flow", function () {
609
+ let bobUtxo1: UTXO;
610
+ let aliceUtxo1: UTXO;
611
+ let lockedUtxo1: UTXO;
612
+ let lockId: string;
613
+ let outputUtxo1: UTXO;
614
+ let outputUtxo2: UTXO;
615
+ let unlockHash: string;
616
+
617
+ before(async function () {
618
+ bobUtxo1 = newUTXO(100, Bob);
619
+ await doMint(zeto, deployer, [bobUtxo1]);
620
+ await smtAlice.add(bobUtxo1.hash, bobUtxo1.hash);
621
+ await smtBob.add(bobUtxo1.hash, bobUtxo1.hash);
622
+
623
+ aliceUtxo1 = newUTXO(100, Alice);
624
+ await doMint(zeto, deployer, [aliceUtxo1]);
625
+ await smtAlice.add(aliceUtxo1.hash, aliceUtxo1.hash);
626
+ await smtBob.add(aliceUtxo1.hash, aliceUtxo1.hash);
627
+ });
628
+
629
+ it("createLock() with deterministic lockId computed from txId", async function () {
630
+ const nullifier1 = newNullifier(bobUtxo1, Bob);
631
+ lockedUtxo1 = newUTXO(bobUtxo1.value!, Bob);
632
+ const root = await smtBob.root();
633
+ const p1 = await smtBob.generateCircomVerifierProof(
634
+ bobUtxo1.hash,
744
635
  root,
745
636
  );
637
+ const p2 = await smtBob.generateCircomVerifierProof(0n, root);
746
638
  const merkleProofs = [
747
- proof1.siblings.map((s) => s.bigInt()),
748
- proof2.siblings.map((s) => s.bigInt()),
639
+ p1.siblings.map((s) => s.bigInt()),
640
+ p2.siblings.map((s) => s.bigInt()),
749
641
  ];
750
- // Bob proposes the output UTXOs, attempting to transfer the locked UTXO to Alice
751
- // and reusing an existing UTXO (utxo4)
752
- const utxo1 = newUTXO(10, Alice);
753
-
754
- const result = await prepareProof(
755
- circuitForLocked,
756
- provingKeyForLocked,
642
+ const encodedZkProof = await prepareProof(
643
+ circuit,
644
+ provingKey,
757
645
  Bob,
758
- [lockedUtxo1, ZERO_UTXO],
646
+ [bobUtxo1, ZERO_UTXO],
759
647
  [nullifier1, ZERO_UTXO],
760
- [utxo4, utxo1],
648
+ [lockedUtxo1, ZERO_UTXO],
761
649
  root.bigInt(),
762
650
  merkleProofs,
763
- [Alice, Alice],
764
- Alice.ethAddress, // current lock delegate
651
+ [Bob, Bob],
765
652
  );
766
- const nullifiers = [nullifier1.hash];
767
653
 
768
- // Alice (in reality this is usually a contract that orchestrates a trade flow) can spend the locked state
769
- // using the proof generated by the trade counterparty (Bob in this case)
770
- await expect(
771
- sendTx(
772
- Alice,
773
- nullifiers,
774
- result.outputCommitments,
775
- root.bigInt(),
776
- result.encodedProof,
777
- true,
778
- ),
779
- ).to.be.rejectedWith(`UTXOAlreadyOwned(${utxo4.hash.toString()})`);
654
+ const txId = randomBytes32();
655
+ const createArgs = encodeCreateArgs({
656
+ txId,
657
+ inputs: [nullifier1.hash],
658
+ outputs: [],
659
+ lockedOutputs: [lockedUtxo1.hash],
660
+ proof: encodeToBytes(root.bigInt(), encodedZkProof),
661
+ });
662
+
663
+ // Pre-compute lockId off-chain and compare against contract.
664
+ const predicted = await zeto
665
+ .connect(Bob.signer)
666
+ .computeLockId(createArgs);
667
+ lockId = predicted;
668
+
669
+ const tx = await zeto
670
+ .connect(Bob.signer)
671
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x");
672
+ const result: ContractTransactionReceipt | null = await tx.wait();
673
+ logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
674
+
675
+ // Expect a LockCreated event with the predicted lockId.
676
+ const created = result!.logs
677
+ .map((l) => {
678
+ try {
679
+ return zeto.interface.parseLog(l as any);
680
+ } catch (_e) {
681
+ return null;
682
+ }
683
+ })
684
+ .find((p) => p && p.name === "LockCreated");
685
+ expect(created, "LockCreated event not found").to.not.be.null;
686
+ expect(created!.args.lockId).to.equal(predicted);
687
+ expect(created!.args.owner).to.equal(Bob.ethAddress);
688
+ expect(created!.args.spender).to.equal(Bob.ethAddress);
780
689
  });
781
690
 
782
- it("attempting to use a spent UTXO as output should fail", async function () {
783
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
784
- const nullifier1 = newNullifier(lockedUtxo1, Bob);
785
- const root = await smtBobForLocked.root();
786
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
787
- lockedUtxo1.hash,
788
- root,
789
- );
790
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
791
- 0n,
792
- root,
691
+ it("isLockActive() and getLock() reflect the newly created lock", async function () {
692
+ expect(await zeto.isLockActive(lockId)).to.equal(true);
693
+ const info = await zeto.getLock(lockId);
694
+ expect(info.owner).to.equal(Bob.ethAddress);
695
+ expect(info.spender).to.equal(Bob.ethAddress);
696
+ expect(info.spendCommitment).to.equal(ethers.ZeroHash);
697
+ expect(info.cancelCommitment).to.equal(ethers.ZeroHash);
698
+ });
699
+
700
+ it("locked() returns true for locked UTXOs and false for unlocked or spent UTXOs", async function () {
701
+ expect(await zeto.locked(lockedUtxo1.hash)).to.deep.equal([
702
+ true,
703
+ Bob.ethAddress,
704
+ ]);
705
+ expect((await zeto.locked(aliceUtxo1.hash))[0]).to.be.false;
706
+ expect((await zeto.locked(bobUtxo1.hash))[0]).to.be.false;
707
+ });
708
+
709
+ it("updateLock() commits the spend hash while owner == spender", async function () {
710
+ outputUtxo1 = newUTXO(10, Alice);
711
+ outputUtxo2 = newUTXO(90, Bob);
712
+
713
+ unlockHash = calculateSpendHash(
714
+ [lockedUtxo1],
715
+ [],
716
+ [outputUtxo1, outputUtxo2],
717
+ "0x",
793
718
  );
794
- const merkleProofs = [
795
- proof1.siblings.map((s) => s.bigInt()),
796
- proof2.siblings.map((s) => s.bigInt()),
797
- ];
798
- // Bob proposes the output UTXOs, attempting to transfer the locked UTXO to Alice
799
- // and reusing an unlocked spent UTXO (utxo12)
800
- const utxo1 = newUTXO(14, Alice);
801
719
 
802
- const result = await prepareProof(
720
+ const tx = await zeto
721
+ .connect(Bob.signer)
722
+ .updateLock(
723
+ lockId,
724
+ encodeUpdateArgs(randomBytes32()),
725
+ unlockHash,
726
+ ethers.ZeroHash,
727
+ "0x",
728
+ );
729
+ const result: ContractTransactionReceipt | null = await tx.wait();
730
+ logger.debug(`updateLock() complete. Gas used: ${result?.gasUsed}`);
731
+
732
+ const info = await zeto.getLock(lockId);
733
+ expect(info.spendCommitment).to.equal(unlockHash);
734
+ });
735
+
736
+ it("delegateLock() transfers spending authority to Alice", async function () {
737
+ const tx = await zeto
738
+ .connect(Bob.signer)
739
+ .delegateLock(
740
+ lockId,
741
+ encodeDelegateArgs(randomBytes32()),
742
+ Alice.ethAddress,
743
+ "0x",
744
+ );
745
+ const result: ContractTransactionReceipt | null = await tx.wait();
746
+ logger.debug(`delegateLock() complete. Gas used: ${result?.gasUsed}`);
747
+
748
+ const info = await zeto.getLock(lockId);
749
+ expect(info.spender).to.equal(Alice.ethAddress);
750
+ // Storage-layer delegate must also be moved.
751
+ expect(await zeto.locked(lockedUtxo1.hash)).to.deep.equal([
752
+ true,
753
+ Alice.ethAddress,
754
+ ]);
755
+ });
756
+
757
+ it("the new spender can spendLock() with the matching payload", async function () {
758
+ const encodedZkProofForSettle = await prepareProofForLocked(
803
759
  circuitForLocked,
804
760
  provingKeyForLocked,
805
761
  Bob,
806
762
  [lockedUtxo1, ZERO_UTXO],
807
- [nullifier1, ZERO_UTXO],
808
- [utxo12, utxo1],
809
- root.bigInt(),
810
- merkleProofs,
811
- [Alice, Alice],
812
- Alice.ethAddress, // current lock delegate
763
+ [outputUtxo1, outputUtxo2],
764
+ [Alice, Bob],
813
765
  );
814
- const nullifiers = [nullifier1.hash];
815
766
 
816
- // Alice (in reality this is usually a contract that orchestrates a trade flow) can spend the locked state
817
- // using the proof generated by the trade counterparty (Bob in this case)
818
- await expect(
819
- sendTx(
820
- Alice,
821
- nullifiers,
822
- result.outputCommitments,
823
- root.bigInt(),
824
- result.encodedProof,
825
- true,
826
- ),
827
- ).to.be.rejectedWith(`UTXOAlreadyOwned(${utxo12.hash.toString()})`);
767
+ const spendArgs = encodeSpendArgs({
768
+ txId: randomBytes32(),
769
+ lockedOutputs: [],
770
+ outputs: [outputUtxo1.hash, outputUtxo2.hash],
771
+ proof: encodeToBytesForLocked(encodedZkProofForSettle),
772
+ data: "0x",
773
+ });
774
+
775
+ const tx = await zeto
776
+ .connect(Alice.signer)
777
+ .spendLock(lockId, spendArgs, "0x");
778
+ const result = await tx.wait();
779
+
780
+ const parsed = result!.logs
781
+ .map((l) => {
782
+ try {
783
+ return zeto.interface.parseLog(l as any);
784
+ } catch (_e) {
785
+ return null;
786
+ }
787
+ })
788
+ .filter((p) => p !== null) as ReadonlyArray<{
789
+ name: string;
790
+ args: any;
791
+ }>;
792
+ const lockSpent = parsed.find((p) => p.name === "LockSpent");
793
+ const zetoLockSpent = parsed.find((p) => p.name === "ZetoLockSpent");
794
+ expect(lockSpent, "LockSpent event not emitted").to.not.be.undefined;
795
+ expect(zetoLockSpent, "ZetoLockSpent event not emitted").to.not.be
796
+ .undefined;
797
+ expect(lockSpent!.args.lockId).to.equal(lockId);
798
+ expect(lockSpent!.args.spender).to.equal(Alice.ethAddress);
799
+
800
+ const outputs = zetoLockSpent!.args.outputs;
801
+ await smtAlice.add(outputs[0], outputs[0]);
802
+ await smtAlice.add(outputs[1], outputs[1]);
803
+ await smtBob.add(outputs[0], outputs[0]);
804
+ await smtBob.add(outputs[1], outputs[1]);
805
+
806
+ // Lock is no longer active.
807
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
828
808
  });
829
809
 
830
- it("the designated delegate can use the proper proof to spend the locked state", async function () {
831
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
832
- const nullifier1 = newNullifier(lockedUtxo1, Bob);
833
- const root = await smtBobForLocked.root();
834
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
835
- lockedUtxo1.hash,
836
- root,
837
- );
838
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
839
- 0n,
810
+ it("onchain SMT root for the unlocked UTXOs equals the offchain SMT root", async function () {
811
+ const bobRoot = await smtBob.root();
812
+ const aliceRoot = await smtAlice.root();
813
+ const onchainRoot = await zeto.getRoot();
814
+ expect(bobRoot.string()).to.equal(onchainRoot.toString());
815
+ expect(aliceRoot.string()).to.equal(onchainRoot.toString());
816
+ });
817
+ });
818
+
819
+ describe("createLock -> cancelLock flow", function () {
820
+ let bobUtxo1: UTXO;
821
+ let lockedUtxo1: UTXO;
822
+ let lockId: string;
823
+ let cancelHash: string;
824
+ let outUtxo1: UTXO;
825
+ let outUtxo2: UTXO;
826
+
827
+ before(async function () {
828
+ bobUtxo1 = newUTXO(100, Bob);
829
+ await doMint(zeto, deployer, [bobUtxo1]);
830
+ await smtAlice.add(bobUtxo1.hash, bobUtxo1.hash);
831
+ await smtBob.add(bobUtxo1.hash, bobUtxo1.hash);
832
+ });
833
+
834
+ it("Bob createLock()", async function () {
835
+ const nullifier1 = newNullifier(bobUtxo1, Bob);
836
+ lockedUtxo1 = newUTXO(bobUtxo1.value!, Bob);
837
+ const root = await smtBob.root();
838
+ const p1 = await smtBob.generateCircomVerifierProof(
839
+ bobUtxo1.hash,
840
840
  root,
841
841
  );
842
+ const p2 = await smtBob.generateCircomVerifierProof(0n, root);
842
843
  const merkleProofs = [
843
- proof1.siblings.map((s) => s.bigInt()),
844
- proof2.siblings.map((s) => s.bigInt()),
844
+ p1.siblings.map((s) => s.bigInt()),
845
+ p2.siblings.map((s) => s.bigInt()),
845
846
  ];
846
- // Bob proposes the output UTXOs, attempting to transfer the locked UTXO to Alice
847
- utxo9 = newUTXO(10, Alice);
848
- utxo10 = newUTXO(5, Bob);
847
+ const encodedZkProof = await prepareProof(
848
+ circuit,
849
+ provingKey,
850
+ Bob,
851
+ [bobUtxo1, ZERO_UTXO],
852
+ [nullifier1, ZERO_UTXO],
853
+ [lockedUtxo1, ZERO_UTXO],
854
+ root.bigInt(),
855
+ merkleProofs,
856
+ [Bob, Bob],
857
+ );
858
+
859
+ // Commit the cancel hash up-front to exercise the cancelCommitment branch.
860
+ outUtxo1 = newUTXO(10, Alice);
861
+ outUtxo2 = newUTXO(90, Bob);
862
+ cancelHash = calculateCancelHash(
863
+ [lockedUtxo1],
864
+ [],
865
+ [outUtxo1, outUtxo2],
866
+ "0x",
867
+ );
868
+
869
+ const createArgs = encodeCreateArgs({
870
+ txId: randomBytes32(),
871
+ inputs: [nullifier1.hash],
872
+ outputs: [],
873
+ lockedOutputs: [lockedUtxo1.hash],
874
+ proof: encodeToBytes(root.bigInt(), encodedZkProof),
875
+ });
876
+ lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
877
+ const tx = await zeto
878
+ .connect(Bob.signer)
879
+ .createLock(createArgs, ethers.ZeroHash, cancelHash, "0x");
880
+ const result: ContractTransactionReceipt | null = await tx.wait();
881
+ logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
882
+ });
849
883
 
850
- const result = await prepareProof(
884
+ it("the owner can cancelLock() to reverse the lock without delegation", async function () {
885
+ const encodedZkProofForCancel = await prepareProofForLocked(
851
886
  circuitForLocked,
852
887
  provingKeyForLocked,
853
888
  Bob,
854
889
  [lockedUtxo1, ZERO_UTXO],
855
- [nullifier1, ZERO_UTXO],
856
- [utxo9, utxo10],
857
- root.bigInt(),
858
- merkleProofs,
890
+ [outUtxo1, outUtxo2],
859
891
  [Alice, Bob],
860
- Alice.ethAddress, // current lock delegate
861
892
  );
862
- const nullifiers = [nullifier1.hash];
863
893
 
864
- // Alice (in reality this is usually a contract that orchestrates a trade flow) can spend the locked state
865
- // using the proof generated by the trade counterparty (Bob in this case)
866
- await expect(
867
- sendTx(
868
- Alice,
869
- nullifiers,
870
- result.outputCommitments,
871
- root.bigInt(),
872
- result.encodedProof,
873
- true,
874
- ),
875
- ).to.be.fulfilled;
894
+ const cancelArgs = encodeSpendArgs({
895
+ txId: randomBytes32(),
896
+ lockedOutputs: [],
897
+ outputs: [outUtxo1.hash, outUtxo2.hash],
898
+ proof: encodeToBytesForLocked(encodedZkProofForCancel),
899
+ data: "0x",
900
+ });
876
901
 
877
- // Alice and Bob keep the local SMT in sync
878
- await smtAlice.add(utxo9.hash, utxo9.hash);
879
- await smtAlice.add(utxo10.hash, utxo10.hash);
880
- await smtBob.add(utxo9.hash, utxo9.hash);
881
- await smtBob.add(utxo10.hash, utxo10.hash);
882
- });
902
+ const tx = await zeto
903
+ .connect(Bob.signer)
904
+ .cancelLock(lockId, cancelArgs, "0x");
905
+ const result = await tx.wait();
883
906
 
884
- it("onchain SMT root for the locked UTXOs should be equal to the offchain SMT root", async function () {
885
- const root = await smtBobForLocked.root();
886
- const onchainRoot = await zeto.getRootForLocked();
887
- expect(root.string()).to.equal(onchainRoot.toString());
907
+ const parsed = result!.logs
908
+ .map((l) => {
909
+ try {
910
+ return zeto.interface.parseLog(l as any);
911
+ } catch (_e) {
912
+ return null;
913
+ }
914
+ })
915
+ .filter((p) => p !== null) as ReadonlyArray<{
916
+ name: string;
917
+ args: any;
918
+ }>;
919
+ const cancelled = parsed.find((p) => p.name === "LockCancelled");
920
+ const zetoCancelled = parsed.find(
921
+ (p) => p.name === "ZetoLockCancelled",
922
+ );
923
+ expect(cancelled, "LockCancelled event not emitted").to.not.be
924
+ .undefined;
925
+ expect(zetoCancelled, "ZetoLockCancelled event not emitted").to.not.be
926
+ .undefined;
927
+
928
+ const outputs = zetoCancelled!.args.outputs;
929
+ await smtAlice.add(outputs[0], outputs[0]);
930
+ await smtAlice.add(outputs[1], outputs[1]);
931
+ await smtBob.add(outputs[0], outputs[0]);
932
+ await smtBob.add(outputs[1], outputs[1]);
933
+
934
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
888
935
  });
889
936
 
890
- it("onchain SMT root for the unlocked UTXOs should be equal to the offchain SMT root", async function () {
891
- const root = await smtBob.root();
937
+ it("onchain SMT root for the unlocked UTXOs equals the offchain SMT root", async function () {
938
+ const bobRoot = await smtBob.root();
939
+ const aliceRoot = await smtAlice.root();
892
940
  const onchainRoot = await zeto.getRoot();
893
- expect(root.string()).to.equal(onchainRoot.toString());
941
+ expect(bobRoot.string()).to.equal(onchainRoot.toString());
942
+ expect(aliceRoot.string()).to.equal(onchainRoot.toString());
894
943
  });
895
944
  });
896
945
 
897
- describe("lock -> delegate -> transfer flow", function () {
898
- let lockedUtxo2: UTXO;
946
+ describe("spendLock with a payload that does not match the spend commitment fails", function () {
947
+ let bobUtxo1: UTXO;
948
+ let lockedUtxo1: UTXO;
949
+ let lockId: string;
950
+ let expectedHash: string;
951
+
952
+ before(async function () {
953
+ bobUtxo1 = newUTXO(100, Bob);
954
+ await doMint(zeto, deployer, [bobUtxo1]);
955
+ await smtAlice.add(bobUtxo1.hash, bobUtxo1.hash);
956
+ await smtBob.add(bobUtxo1.hash, bobUtxo1.hash);
957
+ });
899
958
 
900
- it("Bob locks a UTXO and makes Alice as the delegate ", async function () {
901
- const nullifier1 = newNullifier(utxo10, Bob);
902
- lockedUtxo2 = newUTXO(utxo10.value!, Bob);
959
+ it("Bob createLock()", async function () {
960
+ const nullifier1 = newNullifier(bobUtxo1, Bob);
961
+ lockedUtxo1 = newUTXO(bobUtxo1.value!, Bob);
903
962
  const root = await smtBob.root();
904
- const proof1 = await smtBob.generateCircomVerifierProof(
905
- utxo10.hash,
963
+ const p1 = await smtBob.generateCircomVerifierProof(
964
+ bobUtxo1.hash,
906
965
  root,
907
966
  );
908
- const proof2 = await smtBob.generateCircomVerifierProof(0n, root);
967
+ const p2 = await smtBob.generateCircomVerifierProof(0n, root);
909
968
  const merkleProofs = [
910
- proof1.siblings.map((s) => s.bigInt()),
911
- proof2.siblings.map((s) => s.bigInt()),
969
+ p1.siblings.map((s) => s.bigInt()),
970
+ p2.siblings.map((s) => s.bigInt()),
912
971
  ];
913
- const { outputCommitments, encodedProof } = await prepareProof(
972
+ const encodedZkProof = await prepareProof(
914
973
  circuit,
915
974
  provingKey,
916
975
  Bob,
917
- [utxo10, ZERO_UTXO],
976
+ [bobUtxo1, ZERO_UTXO],
918
977
  [nullifier1, ZERO_UTXO],
919
- [lockedUtxo2, ZERO_UTXO],
978
+ [lockedUtxo1, ZERO_UTXO],
920
979
  root.bigInt(),
921
980
  merkleProofs,
922
981
  [Bob, Bob],
923
982
  );
924
983
 
925
- const tx = await zeto.connect(Bob.signer).lock(
926
- [nullifier1.hash],
984
+ const createArgs = encodeCreateArgs({
985
+ txId: randomBytes32(),
986
+ inputs: [nullifier1.hash],
987
+ outputs: [],
988
+ lockedOutputs: [lockedUtxo1.hash],
989
+ proof: encodeToBytes(root.bigInt(), encodedZkProof),
990
+ });
991
+ lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
992
+ await (
993
+ await zeto
994
+ .connect(Bob.signer)
995
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x")
996
+ ).wait();
997
+ });
998
+
999
+ it("Bob updateLock() committing a specific spend hash", async function () {
1000
+ const expectedOut1 = newUTXO(10, Alice);
1001
+ const expectedOut2 = newUTXO(90, Bob);
1002
+ expectedHash = calculateSpendHash(
1003
+ [lockedUtxo1],
927
1004
  [],
928
- outputCommitments,
929
- root.bigInt(),
930
- encodedProof,
931
- Alice.ethAddress, // make Alice the delegate who can spend the state (if she has the right proof)
1005
+ [expectedOut1, expectedOut2],
932
1006
  "0x",
933
1007
  );
934
- const result: ContractTransactionReceipt | null = await tx.wait();
935
1008
 
936
- // Note that the locked UTXO should NOT be added to the local SMT for UTXOs because it's tracked in a separate SMT onchain
937
- // we add it to the local SMT for locked UTXOs
938
- const events = parseUTXOEvents(zeto, result!);
939
- await smtBobForLocked.add(
940
- events[0].lockedOutputs[0],
941
- ethers.toBigInt(events[0].delegate),
942
- );
1009
+ await (
1010
+ await zeto
1011
+ .connect(Bob.signer)
1012
+ .updateLock(
1013
+ lockId,
1014
+ encodeUpdateArgs(randomBytes32()),
1015
+ expectedHash,
1016
+ ethers.ZeroHash,
1017
+ "0x",
1018
+ )
1019
+ ).wait();
943
1020
  });
944
1021
 
945
- it("Alice delegates the lock to Charlie", async function () {
946
- const tx = await zeto
947
- .connect(Alice.signer)
948
- .delegateLock([lockedUtxo2.hash], Charlie.ethAddress, "0x");
949
- const result = await tx.wait();
950
- const events = parseUTXOEvents(zeto, result);
951
- // this should update the existing leaf node value from address of Alice to Charlie
952
- await smtBobForLocked.update(
953
- events[0].lockedOutputs[0],
954
- ethers.toBigInt(events[0].newDelegate),
1022
+ it("spendLock() with a different payload reverts with InvalidUnlockHash", async function () {
1023
+ const wrongOut1 = newUTXO(20, Alice);
1024
+ const wrongOut2 = newUTXO(80, Bob);
1025
+
1026
+ const encodedZkProofForSettle = await prepareProofForLocked(
1027
+ circuitForLocked,
1028
+ provingKeyForLocked,
1029
+ Bob,
1030
+ [lockedUtxo1, ZERO_UTXO],
1031
+ [wrongOut1, wrongOut2],
1032
+ [Alice, Bob],
955
1033
  );
956
- });
957
1034
 
958
- it("onchain SMT root for the locked UTXOs should be equal to the offchain SMT root", async function () {
959
- const root = await smtBobForLocked.root();
960
- const onchainRoot = await zeto.getRootForLocked();
961
- expect(root.string()).to.equal(onchainRoot.toString());
962
- });
1035
+ const spendArgs = encodeSpendArgs({
1036
+ txId: randomBytes32(),
1037
+ lockedOutputs: [],
1038
+ outputs: [wrongOut1.hash, wrongOut2.hash],
1039
+ proof: encodeToBytesForLocked(encodedZkProofForSettle),
1040
+ data: "0x",
1041
+ });
963
1042
 
964
- it("Charlie can use the proper proof to spend the locked state", async function () {
965
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
966
- const nullifier1 = newNullifier(lockedUtxo2, Bob);
967
- const root = await smtBobForLocked.root();
968
- const proof1 = await smtBobForLocked.generateCircomVerifierProof(
969
- lockedUtxo2.hash,
970
- root,
1043
+ const calculatedHash = calculateSpendHash(
1044
+ [lockedUtxo1],
1045
+ [],
1046
+ [wrongOut1, wrongOut2],
1047
+ "0x",
1048
+ );
1049
+
1050
+ await expect(
1051
+ zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x"),
1052
+ ).rejectedWith(
1053
+ `InvalidUnlockHash("${expectedHash}", "${calculatedHash}")`,
971
1054
  );
972
- const proof2 = await smtBobForLocked.generateCircomVerifierProof(
973
- 0n,
1055
+ });
1056
+ });
1057
+
1058
+ describe("negative cases for the lock lifecycle", function () {
1059
+ // These tests rely on hardhat-style revert decoding. Skip on real chains.
1060
+ if (network.name !== "hardhat") {
1061
+ return;
1062
+ }
1063
+
1064
+ // freshLock mints a UTXO for `owner`, locks it, and returns the
1065
+ // resulting lock metadata so each negative case can branch off without
1066
+ // polluting other test scopes. Each call advances the SMT.
1067
+ async function freshLock(
1068
+ owner: User,
1069
+ spendCommitment: string = ethers.ZeroHash,
1070
+ cancelCommitment: string = ethers.ZeroHash,
1071
+ ): Promise<{
1072
+ lockId: string;
1073
+ sourceUtxo: UTXO;
1074
+ lockedUtxo: UTXO;
1075
+ createArgs: string;
1076
+ }> {
1077
+ const sourceUtxo = newUTXO(100, owner);
1078
+ await doMint(zeto, deployer, [sourceUtxo]);
1079
+ await smtAlice.add(sourceUtxo.hash, sourceUtxo.hash);
1080
+ await smtBob.add(sourceUtxo.hash, sourceUtxo.hash);
1081
+
1082
+ const nullifier = newNullifier(sourceUtxo, owner);
1083
+ const lockedUtxo = newUTXO(sourceUtxo.value!, owner);
1084
+ const root = await smtBob.root();
1085
+ const p1 = await smtBob.generateCircomVerifierProof(
1086
+ sourceUtxo.hash,
974
1087
  root,
975
1088
  );
1089
+ const p2 = await smtBob.generateCircomVerifierProof(0n, root);
976
1090
  const merkleProofs = [
977
- proof1.siblings.map((s) => s.bigInt()),
978
- proof2.siblings.map((s) => s.bigInt()),
1091
+ p1.siblings.map((s) => s.bigInt()),
1092
+ p2.siblings.map((s) => s.bigInt()),
979
1093
  ];
980
- // Bob proposes the output UTXOs, attempting to transfer the locked UTXO to Alice
981
- const utxo1 = newUTXO(1, Alice);
982
- utxo11 = newUTXO(4, Bob);
1094
+ const encodedZkProof = await prepareProof(
1095
+ circuit,
1096
+ provingKey,
1097
+ owner,
1098
+ [sourceUtxo, ZERO_UTXO],
1099
+ [nullifier, ZERO_UTXO],
1100
+ [lockedUtxo, ZERO_UTXO],
1101
+ root.bigInt(),
1102
+ merkleProofs,
1103
+ [owner, owner],
1104
+ );
1105
+
1106
+ const createArgs = encodeCreateArgs({
1107
+ txId: randomBytes32(),
1108
+ inputs: [nullifier.hash],
1109
+ outputs: [],
1110
+ lockedOutputs: [lockedUtxo.hash],
1111
+ proof: encodeToBytes(root.bigInt(), encodedZkProof),
1112
+ });
1113
+ const lockId = await zeto
1114
+ .connect(owner.signer)
1115
+ .computeLockId(createArgs);
1116
+ await (
1117
+ await zeto
1118
+ .connect(owner.signer)
1119
+ .createLock(createArgs, spendCommitment, cancelCommitment, "0x")
1120
+ ).wait();
1121
+ return { lockId, sourceUtxo, lockedUtxo, createArgs };
1122
+ }
1123
+
1124
+ // Re-encoding helper: produce a syntactically valid spend payload that
1125
+ // does NOT need to verify a real ZK proof. Tests for authorization /
1126
+ // immutability assertions short-circuit before the proof is touched.
1127
+ function dummySpendArgs(): string {
1128
+ return encodeSpendArgs({
1129
+ txId: randomBytes32(),
1130
+ lockedOutputs: [],
1131
+ outputs: [],
1132
+ proof: "0x",
1133
+ data: "0x",
1134
+ });
1135
+ }
1136
+
1137
+ it("createLock() with a duplicate txId from the same caller reverts with DuplicateLock", async function () {
1138
+ const { lockId, createArgs } = await freshLock(Bob);
1139
+
1140
+ // Same createArgs (same txId, same caller) → same lockId → DuplicateLock
1141
+ // fires before any input or proof validation.
1142
+ await expect(
1143
+ zeto
1144
+ .connect(Bob.signer)
1145
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x"),
1146
+ ).rejectedWith(`DuplicateLock("${lockId}")`);
1147
+ });
1148
+
1149
+ it("updateLock() by a non-owner reverts with LockUnauthorized", async function () {
1150
+ const { lockId } = await freshLock(Bob);
1151
+
1152
+ await expect(
1153
+ zeto
1154
+ .connect(Alice.signer)
1155
+ .updateLock(
1156
+ lockId,
1157
+ encodeUpdateArgs(randomBytes32()),
1158
+ ethers.ZeroHash,
1159
+ ethers.ZeroHash,
1160
+ "0x",
1161
+ ),
1162
+ ).to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1163
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1164
+ });
1165
+
1166
+ it("updateLock() after delegateLock() reverts with LockImmutable", async function () {
1167
+ const { lockId } = await freshLock(Bob);
1168
+
1169
+ // Bob delegates spending authority to Alice; spender (Alice) now
1170
+ // differs from owner (Bob).
1171
+ await (
1172
+ await zeto
1173
+ .connect(Bob.signer)
1174
+ .delegateLock(
1175
+ lockId,
1176
+ encodeDelegateArgs(randomBytes32()),
1177
+ Alice.ethAddress,
1178
+ "0x",
1179
+ )
1180
+ ).wait();
1181
+
1182
+ // Even Bob (the owner) can no longer mutate the commitments — the
1183
+ // lock is now externally controlled and must be considered immutable.
1184
+ await expect(
1185
+ zeto
1186
+ .connect(Bob.signer)
1187
+ .updateLock(
1188
+ lockId,
1189
+ encodeUpdateArgs(randomBytes32()),
1190
+ ethers.ZeroHash,
1191
+ ethers.ZeroHash,
1192
+ "0x",
1193
+ ),
1194
+ ).rejectedWith(`LockImmutable("${lockId}")`);
1195
+ });
1196
+
1197
+ it("delegateLock() by a non-spender reverts with LockUnauthorized", async function () {
1198
+ const { lockId } = await freshLock(Bob);
1199
+
1200
+ await expect(
1201
+ zeto
1202
+ .connect(Alice.signer)
1203
+ .delegateLock(
1204
+ lockId,
1205
+ encodeDelegateArgs(randomBytes32()),
1206
+ Charlie.ethAddress,
1207
+ "0x",
1208
+ ),
1209
+ )
1210
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1211
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1212
+ });
1213
+
1214
+ it("spendLock() by a non-spender reverts with LockUnauthorized before touching the proof", async function () {
1215
+ const { lockId } = await freshLock(Bob);
983
1216
 
984
- const result = await prepareProof(
1217
+ // Garbage proof the onlySpender modifier MUST short-circuit before
1218
+ // any proof verification is attempted. This is also what protects the
1219
+ // contract from accepting a spend whose payload does not match the
1220
+ // committed unlockHash for an unauthorized caller.
1221
+ await expect(
1222
+ zeto.connect(Alice.signer).spendLock(lockId, dummySpendArgs(), "0x"),
1223
+ )
1224
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1225
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1226
+ });
1227
+
1228
+ it("cancelLock() by a non-spender reverts with LockUnauthorized", async function () {
1229
+ const { lockId } = await freshLock(Bob);
1230
+
1231
+ await expect(
1232
+ zeto.connect(Alice.signer).cancelLock(lockId, dummySpendArgs(), "0x"),
1233
+ )
1234
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
1235
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
1236
+ });
1237
+
1238
+ it("cancelLock() with a payload that does not match cancelCommitment reverts with InvalidUnlockHash", async function () {
1239
+ // Pre-commit a non-zero cancelCommitment so the hash check is armed.
1240
+ const expectedOut1 = newUTXO(10, Alice);
1241
+ const expectedOut2 = newUTXO(90, Bob);
1242
+ // We don't yet know the lockedUtxo hash (it's randomized inside
1243
+ // freshLock), so commit to a fixed value that we know cannot match
1244
+ // any honest cancellation. The hash-mismatch check fires regardless
1245
+ // of the locked-input contents.
1246
+ const sentinelHash = ethers.keccak256(
1247
+ ethers.toUtf8Bytes("zeto:test:cancelCommitment"),
1248
+ );
1249
+ const { lockId, lockedUtxo } = await freshLock(
1250
+ Bob,
1251
+ ethers.ZeroHash,
1252
+ sentinelHash,
1253
+ );
1254
+
1255
+ const wrongProof = await prepareProofForLocked(
985
1256
  circuitForLocked,
986
1257
  provingKeyForLocked,
987
1258
  Bob,
988
- [lockedUtxo2, ZERO_UTXO],
989
- [nullifier1, ZERO_UTXO],
990
- [utxo1, utxo11],
991
- root.bigInt(),
992
- merkleProofs,
1259
+ [lockedUtxo, ZERO_UTXO],
1260
+ [expectedOut1, expectedOut2],
993
1261
  [Alice, Bob],
994
- Charlie.ethAddress, // current lock delegate
995
1262
  );
996
- const nullifiers = [nullifier1.hash];
1263
+ const cancelArgs = encodeSpendArgs({
1264
+ txId: randomBytes32(),
1265
+ lockedOutputs: [],
1266
+ outputs: [expectedOut1.hash, expectedOut2.hash],
1267
+ proof: encodeToBytesForLocked(wrongProof),
1268
+ data: "0x",
1269
+ });
1270
+ const calculatedHash = calculateCancelHash(
1271
+ [lockedUtxo],
1272
+ [],
1273
+ [expectedOut1, expectedOut2],
1274
+ "0x",
1275
+ );
997
1276
 
998
- // Charlie (in reality this is usually a contract that orchestrates a trade flow) can spend the locked state
999
- // using the proof generated by the trade counterparty (Bob in this case)
1000
1277
  await expect(
1001
- sendTx(
1002
- Charlie,
1003
- nullifiers,
1004
- result.outputCommitments,
1005
- root.bigInt(),
1006
- result.encodedProof,
1007
- true,
1008
- ),
1009
- ).to.be.fulfilled;
1278
+ zeto.connect(Bob.signer).cancelLock(lockId, cancelArgs, "0x"),
1279
+ ).rejectedWith(
1280
+ `InvalidUnlockHash("${sentinelHash}", "${calculatedHash}")`,
1281
+ );
1282
+ });
1010
1283
 
1011
- // Alice and Bob keep the local SMT in sync
1012
- await smtAlice.add(utxo1.hash, utxo1.hash);
1013
- await smtAlice.add(utxo11.hash, utxo11.hash);
1014
- await smtBob.add(utxo1.hash, utxo1.hash);
1015
- await smtBob.add(utxo11.hash, utxo11.hash);
1284
+ it("after a successful spendLock(), the lock is no longer active and getLock() reverts", async function () {
1285
+ const { lockId, lockedUtxo } = await freshLock(Bob);
1286
+
1287
+ const out1 = newUTXO(10, Alice);
1288
+ const out2 = newUTXO(90, Bob);
1289
+ const zkProof = await prepareProofForLocked(
1290
+ circuitForLocked,
1291
+ provingKeyForLocked,
1292
+ Bob,
1293
+ [lockedUtxo, ZERO_UTXO],
1294
+ [out1, out2],
1295
+ [Alice, Bob],
1296
+ );
1297
+ const spendArgs = encodeSpendArgs({
1298
+ txId: randomBytes32(),
1299
+ lockedOutputs: [],
1300
+ outputs: [out1.hash, out2.hash],
1301
+ proof: encodeToBytesForLocked(zkProof),
1302
+ data: "0x",
1303
+ });
1304
+ await (
1305
+ await zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x")
1306
+ ).wait();
1307
+ // Keep both SMTs in sync with the new unlocked outputs so unrelated
1308
+ // tests in later blocks can still reason about roots.
1309
+ await smtAlice.add(out1.hash, out1.hash);
1310
+ await smtAlice.add(out2.hash, out2.hash);
1311
+ await smtBob.add(out1.hash, out1.hash);
1312
+ await smtBob.add(out2.hash, out2.hash);
1313
+
1314
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
1315
+ await expect(zeto.getLock(lockId))
1316
+ .to.be.revertedWithCustomError(zeto, "LockNotActive")
1317
+ .withArgs(lockId);
1318
+ // Re-spending a consumed lock MUST also fail — lockActive is the
1319
+ // first modifier and emits LockNotActive before onlySpender.
1320
+ await expect(
1321
+ zeto.connect(Bob.signer).spendLock(lockId, dummySpendArgs(), "0x"),
1322
+ )
1323
+ .to.be.revertedWithCustomError(zeto, "LockNotActive")
1324
+ .withArgs(lockId);
1016
1325
  });
1017
1326
  });
1018
1327
  });
@@ -1025,14 +1334,60 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
1025
1334
  return;
1026
1335
  }
1027
1336
 
1337
+ let aliceUtxo1: UTXO;
1338
+ let aliceUtxoSpent: UTXO;
1339
+
1340
+ before(async function () {
1341
+ // mint a UTXO for Alice
1342
+ aliceUtxo1 = newUTXO(100, Alice);
1343
+ aliceUtxoSpent = newUTXO(10, Alice);
1344
+ await doMint(zeto, deployer, [aliceUtxo1, aliceUtxoSpent]);
1345
+ await smtAlice.add(aliceUtxo1.hash, aliceUtxo1.hash);
1346
+ await smtAlice.add(aliceUtxoSpent.hash, aliceUtxoSpent.hash);
1347
+
1348
+ // spent one of the UTXOs
1349
+ const _utxo1 = newUTXO(5, Bob);
1350
+ const _utxo2 = newUTXO(5, Alice);
1351
+ const nullifier1 = newNullifier(aliceUtxoSpent, Alice);
1352
+ const root = await smtAlice.root();
1353
+ const proof1 = await smtAlice.generateCircomVerifierProof(
1354
+ aliceUtxoSpent.hash,
1355
+ root,
1356
+ );
1357
+ const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
1358
+ const merkleProofs = [
1359
+ proof1.siblings.map((s) => s.bigInt()),
1360
+ proof2.siblings.map((s) => s.bigInt()),
1361
+ ];
1362
+ await expect(
1363
+ doTransfer(
1364
+ Alice,
1365
+ [aliceUtxoSpent, ZERO_UTXO],
1366
+ [nullifier1, ZERO_UTXO],
1367
+ [_utxo1, _utxo2],
1368
+ root.bigInt(),
1369
+ merkleProofs,
1370
+ [Bob, Alice],
1371
+ ),
1372
+ ).to.be.fulfilled;
1373
+
1374
+ // Alice locally tracks the UTXOs inside the Sparse Merkle Tree
1375
+ await smtAlice.add(_utxo1.hash, _utxo1.hash);
1376
+ await smtAlice.add(_utxo2.hash, _utxo2.hash);
1377
+ });
1378
+
1379
+ beforeEach(async function () {
1380
+ // this.skip();
1381
+ });
1382
+
1028
1383
  it("Alice attempting to withdraw spent UTXOs should fail", async function () {
1029
1384
  // Alice generates the nullifiers for the UTXOs to be spent
1030
- const nullifier1 = newNullifier(utxo100, Alice);
1385
+ const nullifier1 = newNullifier(aliceUtxoSpent, Alice);
1031
1386
 
1032
1387
  // Alice generates inclusion proofs for the UTXOs to be spent
1033
1388
  let root = await smtAlice.root();
1034
1389
  const proof1 = await smtAlice.generateCircomVerifierProof(
1035
- utxo100.hash,
1390
+ aliceUtxoSpent.hash,
1036
1391
  root,
1037
1392
  );
1038
1393
  const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
@@ -1041,64 +1396,58 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
1041
1396
  proof2.siblings.map((s) => s.bigInt()),
1042
1397
  ];
1043
1398
 
1044
- // Alice proposes the output ERC20 tokens
1045
- const outputCommitment = newUTXO(90, Alice);
1399
+ // Alice proposes the output UTXO
1400
+ const outputCommitment = newUTXO(9, Alice);
1046
1401
 
1047
- const { nullifiers, outputCommitments, encodedProof } =
1048
- await prepareNullifierWithdrawProof(
1049
- Alice,
1050
- [utxo100, ZERO_UTXO],
1051
- [nullifier1, ZERO_UTXO],
1052
- outputCommitment,
1053
- root.bigInt(),
1054
- merkleProofs,
1055
- );
1402
+ const { encodedProof } = await prepareNullifierWithdrawProof(
1403
+ Alice,
1404
+ [aliceUtxoSpent, ZERO_UTXO],
1405
+ [nullifier1, ZERO_UTXO],
1406
+ outputCommitment,
1407
+ root.bigInt(),
1408
+ merkleProofs,
1409
+ );
1056
1410
 
1057
1411
  await expect(
1058
1412
  zeto
1059
1413
  .connect(Alice.signer)
1060
1414
  .withdraw(
1061
- 10,
1062
- nullifiers,
1063
- outputCommitments[0],
1064
- root.bigInt(),
1065
- encodedProof,
1415
+ 1,
1416
+ [nullifier1.hash],
1417
+ outputCommitment.hash,
1418
+ encodeToBytesForWithdraw(root.bigInt(), encodedProof),
1066
1419
  "0x",
1067
1420
  ),
1068
1421
  ).rejectedWith("UTXOAlreadySpent");
1069
1422
  });
1070
1423
 
1071
1424
  it("mint existing unspent UTXOs should fail", async function () {
1072
- await expect(doMint(zeto, deployer, [utxo4])).rejectedWith(
1425
+ await expect(doMint(zeto, deployer, [aliceUtxo1])).rejectedWith(
1073
1426
  "UTXOAlreadyOwned",
1074
1427
  );
1075
1428
  });
1076
1429
 
1077
1430
  it("mint existing spent UTXOs should fail", async function () {
1078
- await expect(doMint(zeto, deployer, [utxo1])).rejectedWith(
1431
+ await expect(doMint(zeto, deployer, [aliceUtxoSpent])).rejectedWith(
1079
1432
  "UTXOAlreadyOwned",
1080
1433
  );
1081
1434
  });
1082
1435
 
1083
1436
  it("transfer spent UTXOs should fail (double spend protection)", async function () {
1084
1437
  // create outputs
1085
- const _utxo1 = newUTXO(25, Bob);
1438
+ const _utxo1 = newUTXO(5, Bob);
1086
1439
  const _utxo2 = newUTXO(5, Alice);
1087
1440
 
1088
1441
  // generate the nullifiers for the UTXOs to be spent
1089
- const nullifier1 = newNullifier(utxo1, Alice);
1090
- const nullifier2 = newNullifier(utxo2, Alice);
1442
+ const nullifier1 = newNullifier(aliceUtxoSpent, Alice);
1091
1443
 
1092
1444
  // generate inclusion proofs for the UTXOs to be spent
1093
1445
  let root = await smtAlice.root();
1094
1446
  const proof1 = await smtAlice.generateCircomVerifierProof(
1095
- utxo1.hash,
1096
- root,
1097
- );
1098
- const proof2 = await smtAlice.generateCircomVerifierProof(
1099
- utxo2.hash,
1447
+ aliceUtxoSpent.hash,
1100
1448
  root,
1101
1449
  );
1450
+ const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
1102
1451
  const merkleProofs = [
1103
1452
  proof1.siblings.map((s) => s.bigInt()),
1104
1453
  proof2.siblings.map((s) => s.bigInt()),
@@ -1107,21 +1456,22 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
1107
1456
  await expect(
1108
1457
  doTransfer(
1109
1458
  Alice,
1110
- [utxo1, utxo2],
1111
- [nullifier1, nullifier2],
1459
+ [aliceUtxoSpent, ZERO_UTXO],
1460
+ [nullifier1, ZERO_UTXO],
1112
1461
  [_utxo1, _utxo2],
1113
1462
  root.bigInt(),
1114
1463
  merkleProofs,
1115
1464
  [Bob, Alice],
1116
1465
  ),
1117
1466
  ).rejectedWith("UTXOAlreadySpent");
1118
- }).timeout(600000);
1467
+ });
1119
1468
 
1120
1469
  it("transfer with existing UTXOs in the output should fail (mass conservation protection)", async function () {
1121
- const nullifier1 = newNullifier(utxo9, Alice);
1470
+ const nullifier1 = newNullifier(aliceUtxo1, Alice);
1471
+ const _utxo1 = newUTXO(90, Alice);
1122
1472
  let root = await smtAlice.root();
1123
1473
  const proof1 = await smtAlice.generateCircomVerifierProof(
1124
- utxo9.hash,
1474
+ aliceUtxo1.hash,
1125
1475
  root,
1126
1476
  );
1127
1477
  const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
@@ -1133,29 +1483,29 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
1133
1483
  await expect(
1134
1484
  doTransfer(
1135
1485
  Alice,
1136
- [utxo9, ZERO_UTXO],
1486
+ [aliceUtxo1, ZERO_UTXO],
1137
1487
  [nullifier1, ZERO_UTXO],
1138
- [utxo1, ZERO_UTXO],
1488
+ [aliceUtxoSpent, _utxo1],
1139
1489
  root.bigInt(),
1140
1490
  merkleProofs,
1141
1491
  [Alice, Alice],
1142
1492
  ),
1143
1493
  ).rejectedWith("UTXOAlreadyOwned");
1144
- }).timeout(600000);
1494
+ });
1145
1495
 
1146
1496
  it("spend by using the same UTXO as both inputs should fail", async function () {
1147
- const _utxo1 = newUTXO(15, Alice);
1148
- const _utxo2 = newUTXO(5, Bob);
1149
- const nullifier1 = newNullifier(utxo9, Alice);
1150
- const nullifier2 = newNullifier(utxo9, Alice);
1497
+ const _utxo1 = newUTXO(150, Alice);
1498
+ const _utxo2 = newUTXO(50, Bob);
1499
+ const nullifier1 = newNullifier(aliceUtxo1, Alice);
1500
+ const nullifier2 = newNullifier(aliceUtxo1, Alice);
1151
1501
  // generate inclusion proofs for the UTXOs to be spent
1152
1502
  let root = await smtAlice.root();
1153
1503
  const proof1 = await smtAlice.generateCircomVerifierProof(
1154
- utxo9.hash,
1504
+ aliceUtxo1.hash,
1155
1505
  root,
1156
1506
  );
1157
1507
  const proof2 = await smtAlice.generateCircomVerifierProof(
1158
- utxo9.hash,
1508
+ aliceUtxo1.hash,
1159
1509
  root,
1160
1510
  );
1161
1511
  const merkleProofs = [
@@ -1166,7 +1516,7 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
1166
1516
  await expect(
1167
1517
  doTransfer(
1168
1518
  Alice,
1169
- [utxo9, utxo9],
1519
+ [aliceUtxo1, aliceUtxo1],
1170
1520
  [nullifier1, nullifier2],
1171
1521
  [_utxo1, _utxo2],
1172
1522
  root.bigInt(),
@@ -1174,7 +1524,7 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
1174
1524
  [Alice, Bob],
1175
1525
  ),
1176
1526
  ).rejectedWith(`UTXODuplicate`);
1177
- }).timeout(600000);
1527
+ });
1178
1528
 
1179
1529
  it("transfer non-existing UTXOs should fail", async function () {
1180
1530
  const nonExisting1 = newUTXO(25, Alice);
@@ -1205,20 +1555,20 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
1205
1555
 
1206
1556
  // propose the output UTXOs
1207
1557
  const _utxo1 = newUTXO(30, Charlie);
1208
- utxo7 = newUTXO(15, Bob);
1558
+ const _utxo2 = newUTXO(15, Bob);
1209
1559
 
1210
1560
  await expect(
1211
1561
  doTransfer(
1212
1562
  Alice,
1213
1563
  [nonExisting1, nonExisting2],
1214
1564
  [nullifier1, nullifier2],
1215
- [utxo7, _utxo1],
1565
+ [_utxo1, _utxo2],
1216
1566
  root.bigInt(),
1217
1567
  merkleProofs,
1218
- [Bob, Charlie],
1568
+ [Charlie, Bob],
1219
1569
  ),
1220
1570
  ).rejectedWith("UTXORootNotFound");
1221
- }).timeout(600000);
1571
+ });
1222
1572
 
1223
1573
  it("repeated mint calls with single UTXO should not fail", async function () {
1224
1574
  const utxo5 = newUTXO(10, Alice);
@@ -1236,157 +1586,66 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
1236
1586
  root: BigInt,
1237
1587
  merkleProofs: BigInt[][],
1238
1588
  owners: User[],
1239
- lockDelegate?: User,
1240
1589
  ) {
1241
- let nullifiers: BigNumberish[];
1242
- let outputCommitments: BigNumberish[];
1243
- let encodedProof: any;
1244
- const circuitToUse = lockDelegate
1245
- ? circuitForLocked
1246
- : inputs.length > 2
1247
- ? batchCircuit
1248
- : circuit;
1249
- const provingKeyToUse = lockDelegate
1250
- ? provingKeyForLocked
1251
- : inputs.length > 2
1252
- ? batchProvingKey
1253
- : provingKey;
1254
- const result = await prepareProof(
1590
+ // NOTE: this helper is only for unlocked-input transfers. Locked-input
1591
+ // transitions go through {createLock,updateLock,delegateLock,spendLock,
1592
+ // cancelLock} — exercised by the "ILockableCapability tests" block.
1593
+ const circuitToUse = inputs.length > 2 ? batchCircuit : circuit;
1594
+ const provingKeyToUse = inputs.length > 2 ? batchProvingKey : provingKey;
1595
+
1596
+ const inflatedInputUtxos = inflateUtxos(inputs);
1597
+ const inflatedNullifiers = inflateUtxos(_nullifiers);
1598
+ const inflatedOutputUtxos = inflateUtxos(outputs);
1599
+ const inflatedOwners = inflateOwners(owners);
1600
+
1601
+ const encodedProof = await prepareProof(
1255
1602
  circuitToUse,
1256
1603
  provingKeyToUse,
1257
1604
  signer,
1258
- inputs,
1259
- _nullifiers,
1260
- outputs,
1605
+ inflatedInputUtxos,
1606
+ inflatedNullifiers,
1607
+ inflatedOutputUtxos,
1261
1608
  root,
1262
1609
  merkleProofs,
1263
- owners,
1264
- lockDelegate?.ethAddress,
1610
+ inflatedOwners,
1265
1611
  );
1266
- nullifiers = _nullifiers.map(
1612
+ const nullifiers = _nullifiers.map(
1267
1613
  (nullifier) => nullifier.hash,
1268
1614
  ) as BigNumberish[];
1269
- outputCommitments = result.outputCommitments;
1270
- encodedProof = result.encodedProof;
1615
+ const outputCommitments = outputs.map((output) => output.hash);
1271
1616
 
1272
- const txResult = await sendTx(
1617
+ return await sendTransfer(
1273
1618
  signer,
1274
1619
  nullifiers,
1275
1620
  outputCommitments,
1276
1621
  root,
1277
1622
  encodedProof,
1278
- lockDelegate !== undefined,
1279
1623
  );
1280
- return {
1281
- txResult,
1282
- };
1283
1624
  }
1284
1625
 
1285
- async function sendTx(
1626
+ async function sendTransfer(
1286
1627
  signer: User,
1287
1628
  nullifiers: BigNumberish[],
1288
1629
  outputCommitments: BigNumberish[],
1289
1630
  root: BigNumberish,
1290
1631
  encodedProof: any,
1291
- isLocked: boolean = false,
1292
1632
  ) {
1293
1633
  const startTx = Date.now();
1294
- let tx: any;
1295
- if (!isLocked) {
1296
- tx = await zeto.connect(signer.signer).transfer(
1297
- nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works
1298
- outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works
1299
- root,
1300
- encodedProof,
1301
- "0x",
1302
- );
1303
- } else {
1304
- tx = await zeto.connect(signer.signer).transferLocked(
1305
- nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works
1306
- outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works
1307
- root,
1308
- encodedProof,
1309
- "0x",
1310
- );
1311
- }
1634
+ const tx = await zeto.connect(signer.signer).transfer(
1635
+ nullifiers.filter((ic) => ic !== 0n), // trim padding zeros so we exercise the contract padding logic
1636
+ outputCommitments.filter((oc) => oc !== 0n),
1637
+ encodeToBytes(root, encodedProof),
1638
+ "0x",
1639
+ );
1312
1640
  const results: ContractTransactionReceipt | null = await tx.wait();
1313
- console.log(
1641
+ logger.debug(
1314
1642
  `Time to execute transaction: ${Date.now() - startTx}ms. Gas used: ${results?.gasUsed}`,
1315
1643
  );
1316
1644
  return results;
1317
1645
  }
1318
1646
  });
1319
1647
 
1320
- async function prepareProof(
1321
- circuit: any,
1322
- provingKey: any,
1323
- signer: User,
1324
- inputs: UTXO[],
1325
- _nullifiers: UTXO[],
1326
- outputs: UTXO[],
1327
- root: BigInt,
1328
- merkleProof: BigInt[][],
1329
- owners: User[],
1330
- lockDelegate?: string,
1331
- ) {
1332
- const nullifiers = _nullifiers.map((nullifier) => nullifier.hash) as [
1333
- BigNumberish,
1334
- BigNumberish,
1335
- ];
1336
- const inputCommitments: BigNumberish[] = inputs.map(
1337
- (input) => input.hash,
1338
- ) as BigNumberish[];
1339
- const inputValues = inputs.map((input) => BigInt(input.value || 0n));
1340
- const inputSalts = inputs.map((input) => input.salt || 0n);
1341
- const outputCommitments: BigNumberish[] = outputs.map(
1342
- (output) => output.hash,
1343
- ) as BigNumberish[];
1344
- const outputValues = outputs.map((output) => BigInt(output.value || 0n));
1345
- const outputOwnerPublicKeys: BigNumberish[][] = owners.map(
1346
- (owner) => owner.babyJubPublicKey,
1347
- ) as BigNumberish[][];
1348
-
1349
- const startWitnessCalculation = Date.now();
1350
- const inputObj: any = {
1351
- nullifiers,
1352
- inputCommitments,
1353
- inputValues,
1354
- inputSalts,
1355
- inputOwnerPrivateKey: signer.formattedPrivateKey,
1356
- root,
1357
- enabled: nullifiers.map((n) => (n !== 0n ? 1 : 0)),
1358
- merkleProof,
1359
- outputCommitments,
1360
- outputValues,
1361
- outputSalts: outputs.map((output) => output.salt || 0n),
1362
- outputOwnerPublicKeys,
1363
- };
1364
- if (lockDelegate) {
1365
- inputObj["lockDelegate"] = ethers.toBigInt(lockDelegate);
1366
- }
1367
-
1368
- const witness = await circuit.calculateWTNSBin(inputObj, true);
1369
- const timeWithnessCalculation = Date.now() - startWitnessCalculation;
1370
-
1371
- const startProofGeneration = Date.now();
1372
- const { proof, publicSignals } = (await groth16.prove(
1373
- provingKey,
1374
- witness,
1375
- )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] };
1376
- const timeProofGeneration = Date.now() - startProofGeneration;
1377
-
1378
- console.log(
1379
- `Witness calculation time: ${timeWithnessCalculation}ms. Proof generation time: ${timeProofGeneration}ms.`,
1380
- );
1381
-
1382
- const encodedProof = encodeProof(proof);
1383
- return {
1384
- inputCommitments,
1385
- outputCommitments,
1386
- encodedProof,
1387
- };
1388
- }
1389
-
1390
1648
  module.exports = {
1391
1649
  prepareProof,
1650
+ encodeToBytes,
1392
1651
  };