@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.
- package/README.md +76 -162
- package/config/eip170.ts +30 -0
- package/contracts/factory.sol +30 -34
- package/contracts/factory_upgradeable.sol +11 -7
- package/contracts/lib/common/util.sol +40 -0
- package/contracts/lib/interfaces/ILockableCapability.sol +318 -0
- package/contracts/lib/interfaces/{izeto.sol → IZeto.sol} +13 -1
- package/contracts/lib/interfaces/{izeto_initializable.sol → IZetoInitializable.sol} +14 -12
- package/contracts/lib/interfaces/{izeto_kyc.sol → IZetoKyc.sol} +3 -3
- package/contracts/lib/interfaces/IZetoLockHooks.sol +42 -0
- package/contracts/lib/interfaces/IZetoLockableCapability.sol +237 -0
- package/contracts/lib/interfaces/IZetoStorage.sol +106 -0
- package/contracts/lib/interfaces/{izeto_lockable.sol → IZetoVerifier.sol} +8 -21
- package/contracts/lib/registry.sol +74 -26
- package/contracts/lib/storage/base.sol +210 -0
- package/contracts/lib/storage/nullifier.sol +166 -0
- package/contracts/lib/zeto_common.sol +277 -28
- package/contracts/lib/zeto_fungible.sol +443 -33
- package/contracts/lib/zeto_fungible_base.sol +69 -0
- package/contracts/lib/zeto_fungible_burn.sol +83 -53
- package/contracts/lib/zeto_fungible_burn_nullifier.sol +115 -72
- package/contracts/lib/zeto_fungible_nullifier.sol +167 -0
- package/contracts/lib/zeto_lockable.sol +219 -0
- package/contracts/lib/zeto_lockable_lib.sol +460 -0
- package/contracts/lib/zeto_lockable_storage.sol +43 -0
- package/contracts/lib/zeto_non_fungible.sol +228 -0
- package/contracts/lib/zeto_non_fungible_base.sol +61 -0
- package/contracts/lib/zeto_non_fungible_nullifier.sol +61 -0
- package/contracts/test/escrow1.sol +90 -34
- package/contracts/test/escrow2.sol +64 -29
- package/contracts/test/qurrency.sol +10 -2
- package/contracts/test/smt.sol +6 -1
- package/contracts/test/tendecimals.sol +14 -12
- package/contracts/verifiers/impl/anon.sol +189 -0
- package/contracts/verifiers/impl/anon_batch.sol +301 -0
- package/contracts/verifiers/impl/anon_enc.sol +266 -0
- package/contracts/verifiers/impl/anon_enc_batch.sol +602 -0
- package/contracts/verifiers/impl/anon_enc_nullifier.sol +287 -0
- package/contracts/verifiers/impl/anon_enc_nullifier_batch.sol +679 -0
- package/contracts/verifiers/impl/anon_enc_nullifier_kyc.sol +294 -0
- package/contracts/verifiers/impl/anon_enc_nullifier_kyc_batch.sol +686 -0
- package/contracts/verifiers/impl/anon_enc_nullifier_non_repudiation.sol +413 -0
- package/contracts/verifiers/impl/anon_enc_nullifier_non_repudiation_batch.sol +1141 -0
- package/contracts/verifiers/impl/anon_nullifier_kyc_transfer.sol +217 -0
- package/contracts/verifiers/impl/anon_nullifier_kyc_transferLocked.sol +196 -0
- package/contracts/verifiers/impl/anon_nullifier_kyc_transferLocked_batch.sol +308 -0
- package/contracts/verifiers/impl/anon_nullifier_kyc_transfer_batch.sol +385 -0
- package/contracts/verifiers/impl/anon_nullifier_qurrency_transfer.sol +497 -0
- package/contracts/verifiers/impl/anon_nullifier_qurrency_transfer_batch.sol +1001 -0
- package/contracts/verifiers/{verifier_anon_nullifier_transferLocked.sol → impl/anon_nullifier_transfer.sol} +12 -19
- package/contracts/verifiers/{verifier_anon_nullifier_transferLocked_batch.sol → impl/anon_nullifier_transfer_batch.sol} +44 -51
- package/contracts/verifiers/impl/burn.sol +182 -0
- package/contracts/verifiers/impl/burn_batch.sol +238 -0
- package/contracts/verifiers/impl/burn_nullifier.sol +203 -0
- package/contracts/verifiers/impl/burn_nullifier_batch.sol +315 -0
- package/contracts/verifiers/impl/deposit.sol +182 -0
- package/contracts/verifiers/impl/deposit_kyc.sol +189 -0
- package/contracts/verifiers/impl/nf_anon.sol +175 -0
- package/contracts/verifiers/{verifier_nf_anon_nullifier_transferLocked.sol → impl/nf_anon_nullifier_transfer.sol} +6 -13
- package/contracts/verifiers/impl/withdraw.sol +189 -0
- package/contracts/verifiers/impl/withdraw_batch.sol +245 -0
- package/contracts/verifiers/impl/withdraw_nullifier.sol +210 -0
- package/contracts/verifiers/impl/withdraw_nullifier_batch.sol +322 -0
- package/contracts/verifiers/verifier_anon.sol +31 -186
- package/contracts/verifiers/verifier_anon_batch.sol +31 -298
- package/contracts/verifiers/verifier_anon_enc.sol +31 -263
- package/contracts/verifiers/verifier_anon_enc_batch.sol +31 -599
- package/contracts/verifiers/verifier_anon_enc_nullifier.sol +31 -284
- package/contracts/verifiers/verifier_anon_enc_nullifier_batch.sol +33 -676
- package/contracts/verifiers/verifier_anon_enc_nullifier_kyc.sol +31 -291
- package/contracts/verifiers/verifier_anon_enc_nullifier_kyc_batch.sol +33 -683
- package/contracts/verifiers/verifier_anon_enc_nullifier_non_repudiation.sol +33 -410
- package/contracts/verifiers/verifier_anon_enc_nullifier_non_repudiation_batch.sol +33 -1138
- package/contracts/verifiers/verifier_anon_nullifier_kyc_transfer.sol +33 -214
- package/contracts/verifiers/verifier_anon_nullifier_kyc_transferLocked.sol +33 -221
- package/contracts/verifiers/verifier_anon_nullifier_kyc_transferLocked_batch.sol +33 -389
- package/contracts/verifiers/verifier_anon_nullifier_kyc_transfer_batch.sol +33 -382
- package/contracts/verifiers/verifier_anon_nullifier_qurrency_transfer.sol +33 -221
- package/contracts/verifiers/verifier_anon_nullifier_qurrency_transfer_batch.sol +33 -389
- package/contracts/verifiers/verifier_anon_nullifier_transfer.sol +33 -207
- package/contracts/verifiers/verifier_anon_nullifier_transfer_batch.sol +33 -375
- package/contracts/verifiers/verifier_burn.sol +31 -179
- package/contracts/verifiers/verifier_burn_batch.sol +31 -235
- package/contracts/verifiers/verifier_burn_nullifier.sol +31 -200
- package/contracts/verifiers/verifier_burn_nullifier_batch.sol +31 -312
- package/contracts/verifiers/verifier_deposit.sol +31 -179
- package/contracts/verifiers/verifier_deposit_kyc.sol +34 -0
- package/contracts/verifiers/verifier_nf_anon.sol +31 -172
- package/contracts/verifiers/verifier_nf_anon_nullifier_transfer.sol +33 -179
- package/contracts/verifiers/verifier_withdraw.sol +31 -186
- package/contracts/verifiers/verifier_withdraw_batch.sol +31 -242
- package/contracts/verifiers/verifier_withdraw_nullifier.sol +31 -207
- package/contracts/verifiers/verifier_withdraw_nullifier_batch.sol +33 -319
- package/contracts/zeto_anon.sol +77 -231
- package/contracts/zeto_anon_burnable.sol +56 -12
- package/contracts/zeto_anon_enc.sol +93 -190
- package/contracts/zeto_anon_enc_nullifier.sol +249 -195
- package/contracts/zeto_anon_enc_nullifier_kyc.sol +51 -232
- package/contracts/zeto_anon_enc_nullifier_non_repudiation.sol +231 -238
- package/contracts/zeto_anon_nullifier.sol +164 -298
- package/contracts/zeto_anon_nullifier_burnable.sol +68 -18
- package/contracts/zeto_anon_nullifier_kyc.sol +40 -345
- package/contracts/zeto_anon_nullifier_qurrency.sol +217 -361
- package/contracts/zeto_nf_anon.sol +55 -130
- package/contracts/zeto_nf_anon_nullifier.sol +90 -152
- package/hardhat.config.ts +18 -3
- package/ignition/modules/lib/deps.ts +13 -0
- package/ignition/modules/test/tendecimals.ts +7 -1
- package/ignition/modules/zeto_anon.ts +3 -0
- package/ignition/modules/zeto_anon_burnable.ts +11 -7
- package/ignition/modules/zeto_anon_enc.ts +3 -0
- package/ignition/modules/zeto_anon_enc_nullifier.ts +34 -0
- package/ignition/modules/zeto_anon_enc_nullifier_kyc.ts +5 -2
- package/ignition/modules/zeto_anon_enc_nullifier_non_repudiation.ts +3 -0
- package/ignition/modules/zeto_anon_nullifier.ts +31 -4
- package/ignition/modules/zeto_anon_nullifier_burnable.ts +53 -16
- package/ignition/modules/zeto_anon_nullifier_kyc.ts +30 -12
- package/ignition/modules/zeto_anon_nullifier_qurrency.ts +3 -0
- package/ignition/modules/zeto_nf_anon.ts +3 -1
- package/ignition/modules/zeto_nf_anon_nullifier.ts +17 -12
- package/package.json +7 -3
- package/scripts/deploy_cloneable.ts +19 -10
- package/scripts/deploy_upgradeable.ts +9 -5
- package/scripts/lib/zeto_libraries.ts +47 -0
- package/scripts/tokens/Zeto_Anon.ts +6 -3
- package/scripts/tokens/Zeto_AnonBurnable.ts +4 -1
- package/scripts/tokens/Zeto_AnonEnc.ts +15 -3
- package/scripts/tokens/Zeto_AnonEncNullifier.ts +11 -8
- package/scripts/tokens/Zeto_AnonEncNullifierKyc.ts +7 -6
- package/scripts/tokens/Zeto_AnonEncNullifierNonRepudiation.ts +7 -6
- package/scripts/tokens/Zeto_AnonNullifier.ts +7 -6
- package/scripts/tokens/Zeto_AnonNullifierBurnable.ts +7 -6
- package/scripts/tokens/Zeto_AnonNullifierKyc.ts +7 -6
- package/scripts/tokens/Zeto_AnonNullifierQurrency.ts +7 -8
- package/scripts/tokens/Zeto_NfAnon.ts +5 -3
- package/scripts/tokens/Zeto_NfAnonNullifier.ts +7 -7
- package/test/factory.ts +55 -73
- package/test/lib/anon_nullifier_helpers.ts +89 -0
- package/test/lib/anon_zeto_helpers.ts +76 -0
- package/test/lib/deploy.ts +5 -2
- package/test/lib/eip170.ts +23 -0
- package/test/lib/utils.ts +74 -35
- package/test/test/escrow1.ts +185 -58
- package/test/test/escrow2.ts +200 -107
- package/test/test/qurrency.ts +13 -33
- package/test/usdc-shielding.ts +39 -27
- package/test/utils.ts +144 -21
- package/test/zeto_anon.ts +956 -465
- package/test/zeto_anon_enc.ts +881 -143
- package/test/zeto_anon_enc_nullifier.ts +850 -39
- package/test/zeto_anon_enc_nullifier_kyc.ts +123 -182
- package/test/zeto_anon_enc_nullifier_non_repudiation.ts +80 -47
- package/test/zeto_anon_nullifier.ts +1212 -954
- package/test/zeto_anon_nullifier_kyc.ts +677 -656
- package/test/zeto_anon_nullifier_qurrency.ts +455 -307
- package/test/zeto_nf_anon.ts +737 -138
- package/test/zeto_nf_anon_nullifier.ts +876 -247
- package/contracts/lib/zeto_base.sol +0 -293
- package/contracts/lib/zeto_fungible_withdraw.sol +0 -132
- package/contracts/lib/zeto_fungible_withdraw_nullifier.sol +0 -144
- package/contracts/lib/zeto_nullifier.sol +0 -340
- package/contracts/zkDvP.sol_ +0 -273
- package/test/zkDvP.ts_ +0 -455
- /package/contracts/lib/{common.sol → common/common.sol} +0 -0
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
// limitations under the License.
|
|
16
16
|
|
|
17
17
|
import { ethers, network } from "hardhat";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
ContractTransactionReceipt,
|
|
20
|
+
Signer,
|
|
21
|
+
BigNumberish,
|
|
22
|
+
AbiCoder,
|
|
23
|
+
ZeroAddress,
|
|
24
|
+
} from "ethers";
|
|
19
25
|
import { expect } from "chai";
|
|
20
26
|
import {
|
|
21
27
|
loadCircuit,
|
|
@@ -41,17 +47,28 @@ import {
|
|
|
41
47
|
doMint,
|
|
42
48
|
ZERO_UTXO,
|
|
43
49
|
parseUTXOEvents,
|
|
50
|
+
logger,
|
|
44
51
|
} from "./lib/utils";
|
|
45
52
|
import {
|
|
46
53
|
loadProvingKeys,
|
|
47
54
|
prepareDepositProof,
|
|
48
|
-
prepareNullifiersLockProof,
|
|
49
55
|
prepareNullifierWithdrawProof,
|
|
56
|
+
encodeToBytesForDeposit,
|
|
57
|
+
encodeToBytesForWithdraw,
|
|
58
|
+
calculateSpendHash,
|
|
59
|
+
calculateCancelHash,
|
|
50
60
|
} from "./utils";
|
|
61
|
+
process.env.SKIP_ANON_ENC_TESTS = "true";
|
|
62
|
+
import {
|
|
63
|
+
prepareProof as prepareProofForLockedEnc,
|
|
64
|
+
encodeToBytes as encodeToBytesForLockedEnc,
|
|
65
|
+
} from "./zeto_anon_enc";
|
|
51
66
|
import { deployZeto } from "./lib/deploy";
|
|
52
67
|
const poseidonHash = Poseidon.poseidon4;
|
|
53
68
|
|
|
54
|
-
describe(
|
|
69
|
+
describe(
|
|
70
|
+
"Zeto based fungible token with anonymity using nullifiers and encryption",
|
|
71
|
+
function () {
|
|
55
72
|
let deployer: Signer;
|
|
56
73
|
let Alice: User;
|
|
57
74
|
let Bob: User;
|
|
@@ -65,6 +82,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
65
82
|
let utxo4: UTXO;
|
|
66
83
|
let utxo7: UTXO;
|
|
67
84
|
let circuit: any, provingKey: any;
|
|
85
|
+
let circuitForLocked: any, provingKeyForLocked: any;
|
|
68
86
|
let batchCircuit: any, batchProvingKey: any;
|
|
69
87
|
let smtAlice: Merkletree;
|
|
70
88
|
let smtBob: Merkletree;
|
|
@@ -94,6 +112,18 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
94
112
|
({ provingKeyFile: batchProvingKey } = loadProvingKeys(
|
|
95
113
|
"anon_enc_nullifier_batch",
|
|
96
114
|
));
|
|
115
|
+
// For consuming locked UTXOs we use the non-nullifier `anon_enc`
|
|
116
|
+
// circuit. Rationale (mirrors {Zeto_AnonNullifier}): under the
|
|
117
|
+
// ILockableCapability storage, locked UTXOs live in a flat per-lock
|
|
118
|
+
// mapping with no SMT or nullifier history, so the locked-input proof
|
|
119
|
+
// has nothing to bind against on the nullifier side. The encryption
|
|
120
|
+
// witness is still required (receiver data availability), so we use
|
|
121
|
+
// `anon_enc` rather than the plain `anon` circuit. The contract's
|
|
122
|
+
// {constructPublicInputs(..., inputsLocked = true)} emits the
|
|
123
|
+
// `[ecdhPublicKey, encryptedValues, inputs, outputs, encryptionNonce]`
|
|
124
|
+
// layout that `Groth16Verifier_AnonEnc` expects.
|
|
125
|
+
circuitForLocked = await loadCircuit("anon_enc");
|
|
126
|
+
({ provingKeyFile: provingKeyForLocked } = loadProvingKeys("anon_enc"));
|
|
97
127
|
});
|
|
98
128
|
|
|
99
129
|
it("onchain SMT root should be equal to the offchain SMT root", async function () {
|
|
@@ -164,12 +194,13 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
164
194
|
|
|
165
195
|
const signerAddress = await Alice.signer.getAddress();
|
|
166
196
|
const events = parseUTXOEvents(zeto, result.txResult!);
|
|
167
|
-
|
|
168
|
-
expect(
|
|
197
|
+
const event = events[0];
|
|
198
|
+
expect(event.submitter).to.equal(signerAddress);
|
|
199
|
+
expect(event.inputs).to.deep.equal(nullifiers.map((n) => n.hash));
|
|
169
200
|
|
|
170
|
-
const incomingUTXOs: any =
|
|
201
|
+
const incomingUTXOs: any = event.outputs;
|
|
171
202
|
|
|
172
|
-
const ecdhPublicKey =
|
|
203
|
+
const ecdhPublicKey = event.ecdhPublicKey;
|
|
173
204
|
|
|
174
205
|
// check the non-empty output hashes are correct
|
|
175
206
|
for (let i = 0; i < outputUtxos.length; i++) {
|
|
@@ -179,9 +210,9 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
179
210
|
ecdhPublicKey,
|
|
180
211
|
);
|
|
181
212
|
const plainText = poseidonDecrypt(
|
|
182
|
-
|
|
213
|
+
event.encryptedValues.slice(4 * i, 4 * i + 4),
|
|
183
214
|
sharedKey,
|
|
184
|
-
|
|
215
|
+
event.encryptionNonce,
|
|
185
216
|
2,
|
|
186
217
|
);
|
|
187
218
|
expect(plainText).to.deep.equal(
|
|
@@ -198,11 +229,6 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
198
229
|
await smtBob.add(incomingUTXOs[i], incomingUTXOs[i]);
|
|
199
230
|
}
|
|
200
231
|
|
|
201
|
-
// check empty values, salt and hashes are empty
|
|
202
|
-
for (let i = outputUtxos.length; i < 10; i++) {
|
|
203
|
-
expect(incomingUTXOs[i]).to.equal(0);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
232
|
// mint sufficient balance in Zeto contract address for Alice to withdraw
|
|
207
233
|
const mintTx = await erc20.connect(deployer).mint(zeto, 3);
|
|
208
234
|
await mintTx.wait();
|
|
@@ -255,8 +281,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
255
281
|
3,
|
|
256
282
|
_withdrawNullifiers,
|
|
257
283
|
withdrawCommitments[0],
|
|
258
|
-
root.bigInt(),
|
|
259
|
-
withdrawEncodedProof,
|
|
284
|
+
encodeToBytesForWithdraw(root.bigInt(), withdrawEncodedProof),
|
|
260
285
|
"0x",
|
|
261
286
|
);
|
|
262
287
|
await tx.wait();
|
|
@@ -264,7 +289,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
264
289
|
// Alice checks her ERC20 balance
|
|
265
290
|
const endingBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
266
291
|
expect(endingBalance - startingBalance).to.be.equal(3);
|
|
267
|
-
}).timeout(
|
|
292
|
+
}).timeout(180000);
|
|
268
293
|
|
|
269
294
|
it("mint ERC20 tokens to Alice to deposit to Zeto should succeed", async function () {
|
|
270
295
|
const startingBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
@@ -284,7 +309,12 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
284
309
|
);
|
|
285
310
|
const tx2 = await zeto
|
|
286
311
|
.connect(Alice.signer)
|
|
287
|
-
.deposit(
|
|
312
|
+
.deposit(
|
|
313
|
+
100,
|
|
314
|
+
outputCommitments,
|
|
315
|
+
encodeToBytesForDeposit(encodedProof),
|
|
316
|
+
"0x",
|
|
317
|
+
);
|
|
288
318
|
await tx2.wait();
|
|
289
319
|
|
|
290
320
|
await smtAlice.add(utxo100.hash, utxo100.hash);
|
|
@@ -359,20 +389,21 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
359
389
|
// Bob parses the UTXOs from the onchain event
|
|
360
390
|
const signerAddress = await Alice.signer.getAddress();
|
|
361
391
|
const events = parseUTXOEvents(zeto, result2.txResult!);
|
|
362
|
-
|
|
363
|
-
expect(
|
|
364
|
-
expect(
|
|
365
|
-
|
|
366
|
-
await smtBob.add(
|
|
367
|
-
|
|
368
|
-
|
|
392
|
+
const event = events[0];
|
|
393
|
+
expect(event.submitter).to.equal(signerAddress);
|
|
394
|
+
expect(event.inputs).to.deep.equal([nullifier1.hash, nullifier2.hash]);
|
|
395
|
+
expect(event.outputs).to.deep.equal([_utxo3.hash, utxo4.hash]);
|
|
396
|
+
await smtBob.add(event.outputs[0], event.outputs[0]);
|
|
397
|
+
await smtBob.add(event.outputs[1], event.outputs[1]);
|
|
398
|
+
|
|
399
|
+
const ecdhPublicKey = event.ecdhPublicKey;
|
|
369
400
|
// Bob reconstructs the shared key using his private key and ephemeral public key
|
|
370
401
|
|
|
371
402
|
const sharedKey = genEcdhSharedKey(Bob.babyJubPrivateKey, ecdhPublicKey);
|
|
372
403
|
const plainText = poseidonDecrypt(
|
|
373
|
-
|
|
404
|
+
event.encryptedValues.slice(0, 4),
|
|
374
405
|
sharedKey,
|
|
375
|
-
|
|
406
|
+
event.encryptionNonce,
|
|
376
407
|
2,
|
|
377
408
|
);
|
|
378
409
|
expect(plainText).to.deep.equal(result2.expectedPlainText.slice(0, 2));
|
|
@@ -415,8 +446,9 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
415
446
|
|
|
416
447
|
// Alice gets the new UTXOs from the onchain event and keeps the local SMT in sync
|
|
417
448
|
const events = parseUTXOEvents(zeto, result.txResult!);
|
|
418
|
-
|
|
419
|
-
await smtAlice.add(
|
|
449
|
+
const event = events[0];
|
|
450
|
+
await smtAlice.add(event.outputs[0], event.outputs[0]);
|
|
451
|
+
await smtAlice.add(event.outputs[1], event.outputs[1]);
|
|
420
452
|
}).timeout(600000);
|
|
421
453
|
|
|
422
454
|
it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () {
|
|
@@ -457,8 +489,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
457
489
|
80,
|
|
458
490
|
nullifiers,
|
|
459
491
|
outputCommitments[0],
|
|
460
|
-
root.bigInt(),
|
|
461
|
-
encodedProof,
|
|
492
|
+
encodeToBytesForWithdraw(root.bigInt(), encodedProof),
|
|
462
493
|
"0x",
|
|
463
494
|
);
|
|
464
495
|
await tx.wait();
|
|
@@ -473,7 +504,767 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
473
504
|
expect(endingBalance - startingBalance).to.be.equal(80);
|
|
474
505
|
});
|
|
475
506
|
|
|
476
|
-
|
|
507
|
+
describe("ILockableCapability tests", function () {
|
|
508
|
+
// ABI fragments for the ZetoLockableCapability *Args payloads.
|
|
509
|
+
// Identical to the layout used by every other Zeto fungible token —
|
|
510
|
+
// only the contents of the opaque `proof` blob differ between tokens.
|
|
511
|
+
const CREATE_ARGS_ABI =
|
|
512
|
+
"tuple(bytes32 txId, uint256[] inputs, uint256[] outputs, uint256[] lockedOutputs, bytes proof)";
|
|
513
|
+
const UPDATE_ARGS_ABI = "tuple(bytes32 txId)";
|
|
514
|
+
const DELEGATE_ARGS_ABI = "tuple(bytes32 txId)";
|
|
515
|
+
const SPEND_ARGS_ABI =
|
|
516
|
+
"tuple(bytes32 txId, uint256[] lockedOutputs, uint256[] outputs, bytes proof, bytes data)";
|
|
517
|
+
|
|
518
|
+
function encodeCreateArgs(args: {
|
|
519
|
+
txId: string;
|
|
520
|
+
inputs: BigNumberish[];
|
|
521
|
+
outputs: BigNumberish[];
|
|
522
|
+
lockedOutputs: BigNumberish[];
|
|
523
|
+
proof: string;
|
|
524
|
+
}) {
|
|
525
|
+
return new AbiCoder().encode([CREATE_ARGS_ABI], [args]);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function encodeUpdateArgs(txId: string) {
|
|
529
|
+
return new AbiCoder().encode([UPDATE_ARGS_ABI], [{ txId }]);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function encodeDelegateArgs(txId: string) {
|
|
533
|
+
return new AbiCoder().encode([DELEGATE_ARGS_ABI], [{ txId }]);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function encodeSpendArgs(args: {
|
|
537
|
+
txId: string;
|
|
538
|
+
lockedOutputs: BigNumberish[];
|
|
539
|
+
outputs: BigNumberish[];
|
|
540
|
+
proof: string;
|
|
541
|
+
data: string;
|
|
542
|
+
}) {
|
|
543
|
+
return new AbiCoder().encode([SPEND_ARGS_ABI], [args]);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function randomBytes32(): string {
|
|
547
|
+
return ethers.hexlify(ethers.randomBytes(32));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// prepareCreateLockProofBytes generates an `anon_enc_nullifier` proof
|
|
551
|
+
// for the createLock transition `[sourceUtxo, ZERO] -> [lockedUtxo,
|
|
552
|
+
// ZERO]`. The createLock path goes through
|
|
553
|
+
// `constructPublicInputs(..., inputsLocked = false)` so we use the
|
|
554
|
+
// standard nullifier-aware encryption circuit (same as a regular
|
|
555
|
+
// unlocked transfer).
|
|
556
|
+
async function prepareCreateLockProofBytes(
|
|
557
|
+
signer: User,
|
|
558
|
+
inputs: UTXO[],
|
|
559
|
+
nullifiers: UTXO[],
|
|
560
|
+
lockedOutputs: UTXO[],
|
|
561
|
+
root: bigint,
|
|
562
|
+
merkleProofs: BigInt[][],
|
|
563
|
+
owners: User[],
|
|
564
|
+
): Promise<string> {
|
|
565
|
+
const ephemeralKeypair = genKeypair();
|
|
566
|
+
const result = await prepareProof(
|
|
567
|
+
signer,
|
|
568
|
+
inputs,
|
|
569
|
+
nullifiers,
|
|
570
|
+
lockedOutputs,
|
|
571
|
+
root as unknown as BigInt,
|
|
572
|
+
merkleProofs,
|
|
573
|
+
owners,
|
|
574
|
+
ephemeralKeypair.privKey,
|
|
575
|
+
);
|
|
576
|
+
return encodeToBytes(
|
|
577
|
+
root,
|
|
578
|
+
result.encryptionNonce,
|
|
579
|
+
ephemeralKeypair.pubKey,
|
|
580
|
+
result.encryptedValues,
|
|
581
|
+
result.encodedProof,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// prepareLockedSpendProofBytes generates a non-nullifier `anon_enc`
|
|
586
|
+
// proof for a spendLock / cancelLock settlement. The contract's
|
|
587
|
+
// locked-input verifier slot is wired to `Groth16Verifier_AnonEnc`,
|
|
588
|
+
// so we go through the same code path that {Zeto_AnonEnc} uses for
|
|
589
|
+
// its regular transfers — but wrap the result in the 5-tuple
|
|
590
|
+
// `(root, encryptionNonce, ecdhPublicKey, encryptedValues, proof)`
|
|
591
|
+
// that {Zeto_AnonEncNullifier.decodeProof_EncNullifier} expects. The
|
|
592
|
+
// `root` field is decoded but ignored when `inputsLocked = true`, so
|
|
593
|
+
// we pass 0n.
|
|
594
|
+
async function prepareLockedSpendProofBytes(
|
|
595
|
+
signer: User,
|
|
596
|
+
lockedInputs: UTXO[],
|
|
597
|
+
outputs: UTXO[],
|
|
598
|
+
owners: User[],
|
|
599
|
+
): Promise<string> {
|
|
600
|
+
const ephemeralKeypair = genKeypair();
|
|
601
|
+
const result = await prepareProofForLockedEnc(
|
|
602
|
+
circuitForLocked,
|
|
603
|
+
provingKeyForLocked,
|
|
604
|
+
signer,
|
|
605
|
+
lockedInputs,
|
|
606
|
+
outputs,
|
|
607
|
+
owners,
|
|
608
|
+
ephemeralKeypair.privKey,
|
|
609
|
+
);
|
|
610
|
+
return encodeToBytes(
|
|
611
|
+
0n,
|
|
612
|
+
result.encryptionNonce,
|
|
613
|
+
ephemeralKeypair.pubKey,
|
|
614
|
+
result.encryptedValues,
|
|
615
|
+
result.encodedProof,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
describe("createLock -> updateLock -> delegateLock -> spendLock flow", function () {
|
|
620
|
+
let bobUtxo1: UTXO;
|
|
621
|
+
let aliceUtxo1: UTXO;
|
|
622
|
+
let lockedUtxo1: UTXO;
|
|
623
|
+
let lockId: string;
|
|
624
|
+
let outputUtxo1: UTXO;
|
|
625
|
+
let outputUtxo2: UTXO;
|
|
626
|
+
let unlockHash: string;
|
|
627
|
+
|
|
628
|
+
before(async function () {
|
|
629
|
+
bobUtxo1 = newUTXO(100, Bob);
|
|
630
|
+
await doMint(zeto, deployer, [bobUtxo1]);
|
|
631
|
+
await smtAlice.add(bobUtxo1.hash, bobUtxo1.hash);
|
|
632
|
+
await smtBob.add(bobUtxo1.hash, bobUtxo1.hash);
|
|
633
|
+
|
|
634
|
+
aliceUtxo1 = newUTXO(100, Alice);
|
|
635
|
+
await doMint(zeto, deployer, [aliceUtxo1]);
|
|
636
|
+
await smtAlice.add(aliceUtxo1.hash, aliceUtxo1.hash);
|
|
637
|
+
await smtBob.add(aliceUtxo1.hash, aliceUtxo1.hash);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("createLock() with deterministic lockId computed from txId", async function () {
|
|
641
|
+
const nullifier1 = newNullifier(bobUtxo1, Bob);
|
|
642
|
+
lockedUtxo1 = newUTXO(bobUtxo1.value!, Bob);
|
|
643
|
+
const root = await smtBob.root();
|
|
644
|
+
const p1 = await smtBob.generateCircomVerifierProof(
|
|
645
|
+
bobUtxo1.hash,
|
|
646
|
+
root,
|
|
647
|
+
);
|
|
648
|
+
const p2 = await smtBob.generateCircomVerifierProof(0n, root);
|
|
649
|
+
const merkleProofs = [
|
|
650
|
+
p1.siblings.map((s) => s.bigInt()),
|
|
651
|
+
p2.siblings.map((s) => s.bigInt()),
|
|
652
|
+
];
|
|
653
|
+
const proofBytes = await prepareCreateLockProofBytes(
|
|
654
|
+
Bob,
|
|
655
|
+
[bobUtxo1, ZERO_UTXO],
|
|
656
|
+
[nullifier1, ZERO_UTXO],
|
|
657
|
+
[lockedUtxo1, ZERO_UTXO],
|
|
658
|
+
root.bigInt(),
|
|
659
|
+
merkleProofs,
|
|
660
|
+
[Bob, Bob],
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
const txId = randomBytes32();
|
|
664
|
+
const createArgs = encodeCreateArgs({
|
|
665
|
+
txId,
|
|
666
|
+
inputs: [nullifier1.hash],
|
|
667
|
+
outputs: [],
|
|
668
|
+
lockedOutputs: [lockedUtxo1.hash],
|
|
669
|
+
proof: proofBytes,
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const predicted = await zeto
|
|
673
|
+
.connect(Bob.signer)
|
|
674
|
+
.computeLockId(createArgs);
|
|
675
|
+
lockId = predicted;
|
|
676
|
+
|
|
677
|
+
const tx = await zeto
|
|
678
|
+
.connect(Bob.signer)
|
|
679
|
+
.createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x");
|
|
680
|
+
const result: ContractTransactionReceipt | null = await tx.wait();
|
|
681
|
+
logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
|
|
682
|
+
|
|
683
|
+
const created = result!.logs
|
|
684
|
+
.map((l) => {
|
|
685
|
+
try {
|
|
686
|
+
return zeto.interface.parseLog(l as any);
|
|
687
|
+
} catch (_e) {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
})
|
|
691
|
+
.find((p) => p && p.name === "LockCreated");
|
|
692
|
+
expect(created, "LockCreated event not found").to.not.be.null;
|
|
693
|
+
expect(created!.args.lockId).to.equal(predicted);
|
|
694
|
+
expect(created!.args.owner).to.equal(Bob.ethAddress);
|
|
695
|
+
expect(created!.args.spender).to.equal(Bob.ethAddress);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("isLockActive() and getLock() reflect the newly created lock", async function () {
|
|
699
|
+
expect(await zeto.isLockActive(lockId)).to.equal(true);
|
|
700
|
+
const info = await zeto.getLock(lockId);
|
|
701
|
+
expect(info.owner).to.equal(Bob.ethAddress);
|
|
702
|
+
expect(info.spender).to.equal(Bob.ethAddress);
|
|
703
|
+
expect(info.spendCommitment).to.equal(ethers.ZeroHash);
|
|
704
|
+
expect(info.cancelCommitment).to.equal(ethers.ZeroHash);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("locked() returns true for locked UTXOs and false for unlocked or spent UTXOs", async function () {
|
|
708
|
+
expect(await zeto.locked(lockedUtxo1.hash)).to.deep.equal([
|
|
709
|
+
true,
|
|
710
|
+
Bob.ethAddress,
|
|
711
|
+
]);
|
|
712
|
+
expect((await zeto.locked(aliceUtxo1.hash))[0]).to.be.false;
|
|
713
|
+
expect((await zeto.locked(bobUtxo1.hash))[0]).to.be.false;
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it("updateLock() commits the spend hash while owner == spender", async function () {
|
|
717
|
+
outputUtxo1 = newUTXO(10, Alice);
|
|
718
|
+
outputUtxo2 = newUTXO(90, Bob);
|
|
719
|
+
|
|
720
|
+
unlockHash = calculateSpendHash(
|
|
721
|
+
[lockedUtxo1],
|
|
722
|
+
[],
|
|
723
|
+
[outputUtxo1, outputUtxo2],
|
|
724
|
+
"0x",
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
const tx = await zeto
|
|
728
|
+
.connect(Bob.signer)
|
|
729
|
+
.updateLock(
|
|
730
|
+
lockId,
|
|
731
|
+
encodeUpdateArgs(randomBytes32()),
|
|
732
|
+
unlockHash,
|
|
733
|
+
ethers.ZeroHash,
|
|
734
|
+
"0x",
|
|
735
|
+
);
|
|
736
|
+
const result: ContractTransactionReceipt | null = await tx.wait();
|
|
737
|
+
logger.debug(`updateLock() complete. Gas used: ${result?.gasUsed}`);
|
|
738
|
+
|
|
739
|
+
const info = await zeto.getLock(lockId);
|
|
740
|
+
expect(info.spendCommitment).to.equal(unlockHash);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it("delegateLock() transfers spending authority to Alice", async function () {
|
|
744
|
+
const tx = await zeto
|
|
745
|
+
.connect(Bob.signer)
|
|
746
|
+
.delegateLock(
|
|
747
|
+
lockId,
|
|
748
|
+
encodeDelegateArgs(randomBytes32()),
|
|
749
|
+
Alice.ethAddress,
|
|
750
|
+
"0x",
|
|
751
|
+
);
|
|
752
|
+
const result: ContractTransactionReceipt | null = await tx.wait();
|
|
753
|
+
logger.debug(`delegateLock() complete. Gas used: ${result?.gasUsed}`);
|
|
754
|
+
|
|
755
|
+
const info = await zeto.getLock(lockId);
|
|
756
|
+
expect(info.spender).to.equal(Alice.ethAddress);
|
|
757
|
+
expect(await zeto.locked(lockedUtxo1.hash)).to.deep.equal([
|
|
758
|
+
true,
|
|
759
|
+
Alice.ethAddress,
|
|
760
|
+
]);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it("the new spender can spendLock() with the matching payload", async function () {
|
|
764
|
+
const settleProofBytes = await prepareLockedSpendProofBytes(
|
|
765
|
+
Bob,
|
|
766
|
+
[lockedUtxo1, ZERO_UTXO],
|
|
767
|
+
[outputUtxo1, outputUtxo2],
|
|
768
|
+
[Alice, Bob],
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
const spendArgs = encodeSpendArgs({
|
|
772
|
+
txId: randomBytes32(),
|
|
773
|
+
lockedOutputs: [],
|
|
774
|
+
outputs: [outputUtxo1.hash, outputUtxo2.hash],
|
|
775
|
+
proof: settleProofBytes,
|
|
776
|
+
data: "0x",
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
const tx = await zeto
|
|
780
|
+
.connect(Alice.signer)
|
|
781
|
+
.spendLock(lockId, spendArgs, "0x");
|
|
782
|
+
const result = await tx.wait();
|
|
783
|
+
|
|
784
|
+
const parsed = result!.logs
|
|
785
|
+
.map((l) => {
|
|
786
|
+
try {
|
|
787
|
+
return zeto.interface.parseLog(l as any);
|
|
788
|
+
} catch (_e) {
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
})
|
|
792
|
+
.filter((p) => p !== null) as ReadonlyArray<{
|
|
793
|
+
name: string;
|
|
794
|
+
args: any;
|
|
795
|
+
}>;
|
|
796
|
+
const lockSpent = parsed.find((p) => p.name === "LockSpent");
|
|
797
|
+
const zetoLockSpent = parsed.find((p) => p.name === "ZetoLockSpent");
|
|
798
|
+
expect(lockSpent, "LockSpent event not emitted").to.not.be.undefined;
|
|
799
|
+
expect(zetoLockSpent, "ZetoLockSpent event not emitted").to.not.be
|
|
800
|
+
.undefined;
|
|
801
|
+
expect(lockSpent!.args.lockId).to.equal(lockId);
|
|
802
|
+
expect(lockSpent!.args.spender).to.equal(Alice.ethAddress);
|
|
803
|
+
|
|
804
|
+
const outputs = zetoLockSpent!.args.outputs;
|
|
805
|
+
await smtAlice.add(outputs[0], outputs[0]);
|
|
806
|
+
await smtAlice.add(outputs[1], outputs[1]);
|
|
807
|
+
await smtBob.add(outputs[0], outputs[0]);
|
|
808
|
+
await smtBob.add(outputs[1], outputs[1]);
|
|
809
|
+
|
|
810
|
+
expect(await zeto.isLockActive(lockId)).to.equal(false);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it("onchain SMT root for the unlocked UTXOs equals the offchain SMT root", async function () {
|
|
814
|
+
const bobRoot = await smtBob.root();
|
|
815
|
+
const aliceRoot = await smtAlice.root();
|
|
816
|
+
const onchainRoot = await zeto.getRoot();
|
|
817
|
+
expect(bobRoot.string()).to.equal(onchainRoot.toString());
|
|
818
|
+
expect(aliceRoot.string()).to.equal(onchainRoot.toString());
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
describe("createLock -> cancelLock flow", function () {
|
|
823
|
+
let bobUtxo1: UTXO;
|
|
824
|
+
let lockedUtxo1: UTXO;
|
|
825
|
+
let lockId: string;
|
|
826
|
+
let cancelHash: string;
|
|
827
|
+
let outUtxo1: UTXO;
|
|
828
|
+
let outUtxo2: UTXO;
|
|
829
|
+
|
|
830
|
+
before(async function () {
|
|
831
|
+
bobUtxo1 = newUTXO(100, Bob);
|
|
832
|
+
await doMint(zeto, deployer, [bobUtxo1]);
|
|
833
|
+
await smtAlice.add(bobUtxo1.hash, bobUtxo1.hash);
|
|
834
|
+
await smtBob.add(bobUtxo1.hash, bobUtxo1.hash);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it("Bob createLock() with a non-zero cancelCommitment", async function () {
|
|
838
|
+
const nullifier1 = newNullifier(bobUtxo1, Bob);
|
|
839
|
+
lockedUtxo1 = newUTXO(bobUtxo1.value!, Bob);
|
|
840
|
+
const root = await smtBob.root();
|
|
841
|
+
const p1 = await smtBob.generateCircomVerifierProof(
|
|
842
|
+
bobUtxo1.hash,
|
|
843
|
+
root,
|
|
844
|
+
);
|
|
845
|
+
const p2 = await smtBob.generateCircomVerifierProof(0n, root);
|
|
846
|
+
const merkleProofs = [
|
|
847
|
+
p1.siblings.map((s) => s.bigInt()),
|
|
848
|
+
p2.siblings.map((s) => s.bigInt()),
|
|
849
|
+
];
|
|
850
|
+
const proofBytes = await prepareCreateLockProofBytes(
|
|
851
|
+
Bob,
|
|
852
|
+
[bobUtxo1, ZERO_UTXO],
|
|
853
|
+
[nullifier1, ZERO_UTXO],
|
|
854
|
+
[lockedUtxo1, ZERO_UTXO],
|
|
855
|
+
root.bigInt(),
|
|
856
|
+
merkleProofs,
|
|
857
|
+
[Bob, Bob],
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
outUtxo1 = newUTXO(10, Alice);
|
|
861
|
+
outUtxo2 = newUTXO(90, Bob);
|
|
862
|
+
cancelHash = calculateCancelHash(
|
|
863
|
+
[lockedUtxo1],
|
|
864
|
+
[],
|
|
865
|
+
[outUtxo1, outUtxo2],
|
|
866
|
+
"0x",
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
const createArgs = encodeCreateArgs({
|
|
870
|
+
txId: randomBytes32(),
|
|
871
|
+
inputs: [nullifier1.hash],
|
|
872
|
+
outputs: [],
|
|
873
|
+
lockedOutputs: [lockedUtxo1.hash],
|
|
874
|
+
proof: proofBytes,
|
|
875
|
+
});
|
|
876
|
+
lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
|
|
877
|
+
const tx = await zeto
|
|
878
|
+
.connect(Bob.signer)
|
|
879
|
+
.createLock(createArgs, ethers.ZeroHash, cancelHash, "0x");
|
|
880
|
+
const result: ContractTransactionReceipt | null = await tx.wait();
|
|
881
|
+
logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it("the owner can cancelLock() to reverse the lock without delegation", async function () {
|
|
885
|
+
const cancelProofBytes = await prepareLockedSpendProofBytes(
|
|
886
|
+
Bob,
|
|
887
|
+
[lockedUtxo1, ZERO_UTXO],
|
|
888
|
+
[outUtxo1, outUtxo2],
|
|
889
|
+
[Alice, Bob],
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
const cancelArgs = encodeSpendArgs({
|
|
893
|
+
txId: randomBytes32(),
|
|
894
|
+
lockedOutputs: [],
|
|
895
|
+
outputs: [outUtxo1.hash, outUtxo2.hash],
|
|
896
|
+
proof: cancelProofBytes,
|
|
897
|
+
data: "0x",
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
const tx = await zeto
|
|
901
|
+
.connect(Bob.signer)
|
|
902
|
+
.cancelLock(lockId, cancelArgs, "0x");
|
|
903
|
+
const result = await tx.wait();
|
|
904
|
+
|
|
905
|
+
const parsed = result!.logs
|
|
906
|
+
.map((l) => {
|
|
907
|
+
try {
|
|
908
|
+
return zeto.interface.parseLog(l as any);
|
|
909
|
+
} catch (_e) {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
})
|
|
913
|
+
.filter((p) => p !== null) as ReadonlyArray<{
|
|
914
|
+
name: string;
|
|
915
|
+
args: any;
|
|
916
|
+
}>;
|
|
917
|
+
const cancelled = parsed.find((p) => p.name === "LockCancelled");
|
|
918
|
+
const zetoCancelled = parsed.find(
|
|
919
|
+
(p) => p.name === "ZetoLockCancelled",
|
|
920
|
+
);
|
|
921
|
+
expect(cancelled, "LockCancelled event not emitted").to.not.be
|
|
922
|
+
.undefined;
|
|
923
|
+
expect(zetoCancelled, "ZetoLockCancelled event not emitted").to.not.be
|
|
924
|
+
.undefined;
|
|
925
|
+
|
|
926
|
+
const outputs = zetoCancelled!.args.outputs;
|
|
927
|
+
await smtAlice.add(outputs[0], outputs[0]);
|
|
928
|
+
await smtAlice.add(outputs[1], outputs[1]);
|
|
929
|
+
await smtBob.add(outputs[0], outputs[0]);
|
|
930
|
+
await smtBob.add(outputs[1], outputs[1]);
|
|
931
|
+
|
|
932
|
+
expect(await zeto.isLockActive(lockId)).to.equal(false);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it("onchain SMT root for the unlocked UTXOs equals the offchain SMT root", async function () {
|
|
936
|
+
const bobRoot = await smtBob.root();
|
|
937
|
+
const aliceRoot = await smtAlice.root();
|
|
938
|
+
const onchainRoot = await zeto.getRoot();
|
|
939
|
+
expect(bobRoot.string()).to.equal(onchainRoot.toString());
|
|
940
|
+
expect(aliceRoot.string()).to.equal(onchainRoot.toString());
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
describe("spendLock with a payload that does not match the spend commitment fails", function () {
|
|
945
|
+
let bobUtxo1: UTXO;
|
|
946
|
+
let lockedUtxo1: UTXO;
|
|
947
|
+
let lockId: string;
|
|
948
|
+
let expectedHash: string;
|
|
949
|
+
|
|
950
|
+
before(async function () {
|
|
951
|
+
bobUtxo1 = newUTXO(100, Bob);
|
|
952
|
+
await doMint(zeto, deployer, [bobUtxo1]);
|
|
953
|
+
await smtAlice.add(bobUtxo1.hash, bobUtxo1.hash);
|
|
954
|
+
await smtBob.add(bobUtxo1.hash, bobUtxo1.hash);
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it("Bob createLock() then updateLock() committing a specific spend hash", async function () {
|
|
958
|
+
const nullifier1 = newNullifier(bobUtxo1, Bob);
|
|
959
|
+
lockedUtxo1 = newUTXO(bobUtxo1.value!, Bob);
|
|
960
|
+
const root = await smtBob.root();
|
|
961
|
+
const p1 = await smtBob.generateCircomVerifierProof(
|
|
962
|
+
bobUtxo1.hash,
|
|
963
|
+
root,
|
|
964
|
+
);
|
|
965
|
+
const p2 = await smtBob.generateCircomVerifierProof(0n, root);
|
|
966
|
+
const merkleProofs = [
|
|
967
|
+
p1.siblings.map((s) => s.bigInt()),
|
|
968
|
+
p2.siblings.map((s) => s.bigInt()),
|
|
969
|
+
];
|
|
970
|
+
const proofBytes = await prepareCreateLockProofBytes(
|
|
971
|
+
Bob,
|
|
972
|
+
[bobUtxo1, ZERO_UTXO],
|
|
973
|
+
[nullifier1, ZERO_UTXO],
|
|
974
|
+
[lockedUtxo1, ZERO_UTXO],
|
|
975
|
+
root.bigInt(),
|
|
976
|
+
merkleProofs,
|
|
977
|
+
[Bob, Bob],
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
const createArgs = encodeCreateArgs({
|
|
981
|
+
txId: randomBytes32(),
|
|
982
|
+
inputs: [nullifier1.hash],
|
|
983
|
+
outputs: [],
|
|
984
|
+
lockedOutputs: [lockedUtxo1.hash],
|
|
985
|
+
proof: proofBytes,
|
|
986
|
+
});
|
|
987
|
+
lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
|
|
988
|
+
await (
|
|
989
|
+
await zeto
|
|
990
|
+
.connect(Bob.signer)
|
|
991
|
+
.createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x")
|
|
992
|
+
).wait();
|
|
993
|
+
|
|
994
|
+
const expectedOut1 = newUTXO(10, Alice);
|
|
995
|
+
const expectedOut2 = newUTXO(90, Bob);
|
|
996
|
+
expectedHash = calculateSpendHash(
|
|
997
|
+
[lockedUtxo1],
|
|
998
|
+
[],
|
|
999
|
+
[expectedOut1, expectedOut2],
|
|
1000
|
+
"0x",
|
|
1001
|
+
);
|
|
1002
|
+
await (
|
|
1003
|
+
await zeto
|
|
1004
|
+
.connect(Bob.signer)
|
|
1005
|
+
.updateLock(
|
|
1006
|
+
lockId,
|
|
1007
|
+
encodeUpdateArgs(randomBytes32()),
|
|
1008
|
+
expectedHash,
|
|
1009
|
+
ethers.ZeroHash,
|
|
1010
|
+
"0x",
|
|
1011
|
+
)
|
|
1012
|
+
).wait();
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
it("spendLock() with a different payload reverts with InvalidUnlockHash", async function () {
|
|
1016
|
+
if (network.name !== "hardhat") {
|
|
1017
|
+
this.skip();
|
|
1018
|
+
}
|
|
1019
|
+
const wrongOut1 = newUTXO(20, Alice);
|
|
1020
|
+
const wrongOut2 = newUTXO(80, Bob);
|
|
1021
|
+
|
|
1022
|
+
const settleProofBytes = await prepareLockedSpendProofBytes(
|
|
1023
|
+
Bob,
|
|
1024
|
+
[lockedUtxo1, ZERO_UTXO],
|
|
1025
|
+
[wrongOut1, wrongOut2],
|
|
1026
|
+
[Alice, Bob],
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
const spendArgs = encodeSpendArgs({
|
|
1030
|
+
txId: randomBytes32(),
|
|
1031
|
+
lockedOutputs: [],
|
|
1032
|
+
outputs: [wrongOut1.hash, wrongOut2.hash],
|
|
1033
|
+
proof: settleProofBytes,
|
|
1034
|
+
data: "0x",
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
const calculatedHash = calculateSpendHash(
|
|
1038
|
+
[lockedUtxo1],
|
|
1039
|
+
[],
|
|
1040
|
+
[wrongOut1, wrongOut2],
|
|
1041
|
+
"0x",
|
|
1042
|
+
);
|
|
1043
|
+
|
|
1044
|
+
await expect(
|
|
1045
|
+
zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x"),
|
|
1046
|
+
)
|
|
1047
|
+
.to.be.revertedWithCustomError(zeto, "InvalidUnlockHash")
|
|
1048
|
+
.withArgs(expectedHash, calculatedHash);
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
describe("negative cases for the lock lifecycle", function () {
|
|
1053
|
+
// These tests rely on hardhat-style revert decoding.
|
|
1054
|
+
if (network.name !== "hardhat") {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// freshLock mints a UTXO for `owner`, locks it, and returns the
|
|
1059
|
+
// resulting lock metadata so each negative case can branch off
|
|
1060
|
+
// without polluting other test scopes. Each call advances both
|
|
1061
|
+
// local SMTs to keep them in sync with the on-chain unlocked-UTXO
|
|
1062
|
+
// tree (the createLock proof needs a valid SMT inclusion proof).
|
|
1063
|
+
async function freshLock(
|
|
1064
|
+
owner: User,
|
|
1065
|
+
spendCommitment: string = ethers.ZeroHash,
|
|
1066
|
+
cancelCommitment: string = ethers.ZeroHash,
|
|
1067
|
+
): Promise<{
|
|
1068
|
+
lockId: string;
|
|
1069
|
+
sourceUtxo: UTXO;
|
|
1070
|
+
lockedUtxo: UTXO;
|
|
1071
|
+
createArgs: string;
|
|
1072
|
+
}> {
|
|
1073
|
+
const sourceUtxo = newUTXO(100, owner);
|
|
1074
|
+
await doMint(zeto, deployer, [sourceUtxo]);
|
|
1075
|
+
await smtAlice.add(sourceUtxo.hash, sourceUtxo.hash);
|
|
1076
|
+
await smtBob.add(sourceUtxo.hash, sourceUtxo.hash);
|
|
1077
|
+
|
|
1078
|
+
const nullifier = newNullifier(sourceUtxo, owner);
|
|
1079
|
+
const lockedUtxo = newUTXO(sourceUtxo.value!, owner);
|
|
1080
|
+
const root = await smtBob.root();
|
|
1081
|
+
const p1 = await smtBob.generateCircomVerifierProof(
|
|
1082
|
+
sourceUtxo.hash,
|
|
1083
|
+
root,
|
|
1084
|
+
);
|
|
1085
|
+
const p2 = await smtBob.generateCircomVerifierProof(0n, root);
|
|
1086
|
+
const merkleProofs = [
|
|
1087
|
+
p1.siblings.map((s) => s.bigInt()),
|
|
1088
|
+
p2.siblings.map((s) => s.bigInt()),
|
|
1089
|
+
];
|
|
1090
|
+
const proofBytes = await prepareCreateLockProofBytes(
|
|
1091
|
+
owner,
|
|
1092
|
+
[sourceUtxo, ZERO_UTXO],
|
|
1093
|
+
[nullifier, ZERO_UTXO],
|
|
1094
|
+
[lockedUtxo, ZERO_UTXO],
|
|
1095
|
+
root.bigInt(),
|
|
1096
|
+
merkleProofs,
|
|
1097
|
+
[owner, owner],
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
const createArgs = encodeCreateArgs({
|
|
1101
|
+
txId: randomBytes32(),
|
|
1102
|
+
inputs: [nullifier.hash],
|
|
1103
|
+
outputs: [],
|
|
1104
|
+
lockedOutputs: [lockedUtxo.hash],
|
|
1105
|
+
proof: proofBytes,
|
|
1106
|
+
});
|
|
1107
|
+
const lockId = await zeto
|
|
1108
|
+
.connect(owner.signer)
|
|
1109
|
+
.computeLockId(createArgs);
|
|
1110
|
+
await (
|
|
1111
|
+
await zeto
|
|
1112
|
+
.connect(owner.signer)
|
|
1113
|
+
.createLock(createArgs, spendCommitment, cancelCommitment, "0x")
|
|
1114
|
+
).wait();
|
|
1115
|
+
return { lockId, sourceUtxo, lockedUtxo, createArgs };
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function dummySpendArgs(): string {
|
|
1119
|
+
return encodeSpendArgs({
|
|
1120
|
+
txId: randomBytes32(),
|
|
1121
|
+
lockedOutputs: [],
|
|
1122
|
+
outputs: [],
|
|
1123
|
+
proof: "0x",
|
|
1124
|
+
data: "0x",
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
it("createLock() with a duplicate txId from the same caller reverts with DuplicateLock", async function () {
|
|
1129
|
+
const { lockId, createArgs } = await freshLock(Bob);
|
|
1130
|
+
|
|
1131
|
+
await expect(
|
|
1132
|
+
zeto
|
|
1133
|
+
.connect(Bob.signer)
|
|
1134
|
+
.createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x"),
|
|
1135
|
+
)
|
|
1136
|
+
.to.be.revertedWithCustomError(zeto, "DuplicateLock")
|
|
1137
|
+
.withArgs(lockId);
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it("updateLock() by a non-owner reverts with LockUnauthorized", async function () {
|
|
1141
|
+
const { lockId } = await freshLock(Bob);
|
|
1142
|
+
|
|
1143
|
+
await expect(
|
|
1144
|
+
zeto
|
|
1145
|
+
.connect(Alice.signer)
|
|
1146
|
+
.updateLock(
|
|
1147
|
+
lockId,
|
|
1148
|
+
encodeUpdateArgs(randomBytes32()),
|
|
1149
|
+
ethers.ZeroHash,
|
|
1150
|
+
ethers.ZeroHash,
|
|
1151
|
+
"0x",
|
|
1152
|
+
),
|
|
1153
|
+
)
|
|
1154
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1155
|
+
.withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
it("updateLock() after delegateLock() reverts with LockImmutable", async function () {
|
|
1159
|
+
const { lockId } = await freshLock(Bob);
|
|
1160
|
+
|
|
1161
|
+
await (
|
|
1162
|
+
await zeto
|
|
1163
|
+
.connect(Bob.signer)
|
|
1164
|
+
.delegateLock(
|
|
1165
|
+
lockId,
|
|
1166
|
+
encodeDelegateArgs(randomBytes32()),
|
|
1167
|
+
Alice.ethAddress,
|
|
1168
|
+
"0x",
|
|
1169
|
+
)
|
|
1170
|
+
).wait();
|
|
1171
|
+
|
|
1172
|
+
await expect(
|
|
1173
|
+
zeto
|
|
1174
|
+
.connect(Bob.signer)
|
|
1175
|
+
.updateLock(
|
|
1176
|
+
lockId,
|
|
1177
|
+
encodeUpdateArgs(randomBytes32()),
|
|
1178
|
+
ethers.ZeroHash,
|
|
1179
|
+
ethers.ZeroHash,
|
|
1180
|
+
"0x",
|
|
1181
|
+
),
|
|
1182
|
+
)
|
|
1183
|
+
.to.be.revertedWithCustomError(zeto, "LockImmutable")
|
|
1184
|
+
.withArgs(lockId);
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
it("delegateLock() by a non-spender reverts with LockUnauthorized", async function () {
|
|
1188
|
+
const { lockId } = await freshLock(Bob);
|
|
1189
|
+
|
|
1190
|
+
await expect(
|
|
1191
|
+
zeto
|
|
1192
|
+
.connect(Alice.signer)
|
|
1193
|
+
.delegateLock(
|
|
1194
|
+
lockId,
|
|
1195
|
+
encodeDelegateArgs(randomBytes32()),
|
|
1196
|
+
Charlie.ethAddress,
|
|
1197
|
+
"0x",
|
|
1198
|
+
),
|
|
1199
|
+
)
|
|
1200
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1201
|
+
.withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
it("spendLock() by a non-spender reverts with LockUnauthorized before touching the proof", async function () {
|
|
1205
|
+
const { lockId } = await freshLock(Bob);
|
|
1206
|
+
|
|
1207
|
+
await expect(
|
|
1208
|
+
zeto.connect(Alice.signer).spendLock(lockId, dummySpendArgs(), "0x"),
|
|
1209
|
+
)
|
|
1210
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1211
|
+
.withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
it("cancelLock() by a non-spender reverts with LockUnauthorized", async function () {
|
|
1215
|
+
const { lockId } = await freshLock(Bob);
|
|
1216
|
+
|
|
1217
|
+
await expect(
|
|
1218
|
+
zeto.connect(Alice.signer).cancelLock(lockId, dummySpendArgs(), "0x"),
|
|
1219
|
+
)
|
|
1220
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1221
|
+
.withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it("after a successful spendLock(), the lock is no longer active and getLock() reverts", async function () {
|
|
1225
|
+
const { lockId, lockedUtxo } = await freshLock(Bob);
|
|
1226
|
+
|
|
1227
|
+
const out1 = newUTXO(10, Alice);
|
|
1228
|
+
const out2 = newUTXO(90, Bob);
|
|
1229
|
+
const settleProofBytes = await prepareLockedSpendProofBytes(
|
|
1230
|
+
Bob,
|
|
1231
|
+
[lockedUtxo, ZERO_UTXO],
|
|
1232
|
+
[out1, out2],
|
|
1233
|
+
[Alice, Bob],
|
|
1234
|
+
);
|
|
1235
|
+
const spendArgs = encodeSpendArgs({
|
|
1236
|
+
txId: randomBytes32(),
|
|
1237
|
+
lockedOutputs: [],
|
|
1238
|
+
outputs: [out1.hash, out2.hash],
|
|
1239
|
+
proof: settleProofBytes,
|
|
1240
|
+
data: "0x",
|
|
1241
|
+
});
|
|
1242
|
+
await (
|
|
1243
|
+
await zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x")
|
|
1244
|
+
).wait();
|
|
1245
|
+
// Keep both SMTs in sync with the new unlocked outputs so
|
|
1246
|
+
// unrelated tests in later blocks can still reason about roots.
|
|
1247
|
+
await smtAlice.add(out1.hash, out1.hash);
|
|
1248
|
+
await smtAlice.add(out2.hash, out2.hash);
|
|
1249
|
+
await smtBob.add(out1.hash, out1.hash);
|
|
1250
|
+
await smtBob.add(out2.hash, out2.hash);
|
|
1251
|
+
|
|
1252
|
+
expect(await zeto.isLockActive(lockId)).to.equal(false);
|
|
1253
|
+
await expect(zeto.getLock(lockId))
|
|
1254
|
+
.to.be.revertedWithCustomError(zeto, "LockNotActive")
|
|
1255
|
+
.withArgs(lockId);
|
|
1256
|
+
// Re-spending a consumed lock MUST also fail — lockActive is the
|
|
1257
|
+
// first modifier and emits LockNotActive before onlySpender.
|
|
1258
|
+
await expect(
|
|
1259
|
+
zeto.connect(Bob.signer).spendLock(lockId, dummySpendArgs(), "0x"),
|
|
1260
|
+
)
|
|
1261
|
+
.to.be.revertedWithCustomError(zeto, "LockNotActive")
|
|
1262
|
+
.withArgs(lockId);
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
// describe("legacy lockStates() tests, kept for reference", function () {
|
|
477
1268
|
// let nullifier1: any;
|
|
478
1269
|
// it("lockStates() should succeed when using unlocked states", async function () {
|
|
479
1270
|
// nullifier1 = newNullifier(utxo4, Alice);
|
|
@@ -651,8 +1442,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
651
1442
|
10,
|
|
652
1443
|
nullifiers,
|
|
653
1444
|
outputCommitments[0],
|
|
654
|
-
root.bigInt(),
|
|
655
|
-
encodedProof,
|
|
1445
|
+
encodeToBytesForWithdraw(root.bigInt(), encodedProof),
|
|
656
1446
|
"0x",
|
|
657
1447
|
),
|
|
658
1448
|
).rejectedWith("UTXOAlreadySpent");
|
|
@@ -970,11 +1760,13 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
970
1760
|
const tx = await zeto.connect(signer.signer).transfer(
|
|
971
1761
|
nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works
|
|
972
1762
|
outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1763
|
+
encodeToBytes(
|
|
1764
|
+
root,
|
|
1765
|
+
encryptionNonce,
|
|
1766
|
+
ecdhPublicKey,
|
|
1767
|
+
encryptedValues,
|
|
1768
|
+
encodedProof,
|
|
1769
|
+
),
|
|
978
1770
|
"0x",
|
|
979
1771
|
);
|
|
980
1772
|
const results: ContractTransactionReceipt | null = await tx.wait();
|
|
@@ -984,3 +1776,22 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
984
1776
|
return results;
|
|
985
1777
|
}
|
|
986
1778
|
});
|
|
1779
|
+
|
|
1780
|
+
function encodeToBytes(
|
|
1781
|
+
root: any,
|
|
1782
|
+
encryptionNonce: any,
|
|
1783
|
+
ecdhPublicKey: any,
|
|
1784
|
+
encryptedValues: any,
|
|
1785
|
+
proof: any,
|
|
1786
|
+
) {
|
|
1787
|
+
return new AbiCoder().encode(
|
|
1788
|
+
[
|
|
1789
|
+
"uint256 root",
|
|
1790
|
+
"uint256 encryptionNonce",
|
|
1791
|
+
"uint256[2] ecdhPublicKey",
|
|
1792
|
+
"uint256[] encryptedValues",
|
|
1793
|
+
"tuple(uint256[2] pA, uint256[2][2] pB, uint256[2] pC)",
|
|
1794
|
+
],
|
|
1795
|
+
[root, encryptionNonce, ecdhPublicKey, encryptedValues, proof],
|
|
1796
|
+
);
|
|
1797
|
+
}
|