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