@lfdecentralizedtrust/zeto-contracts 0.2.2 → 0.5.1

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