@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_anon.ts
CHANGED
|
@@ -14,12 +14,17 @@
|
|
|
14
14
|
// See the License for the specific language governing permissions and
|
|
15
15
|
// limitations under the License.
|
|
16
16
|
|
|
17
|
-
import { ethers, network } from "hardhat";
|
|
18
|
-
import {
|
|
17
|
+
import { ethers, network, upgrades } from "hardhat";
|
|
18
|
+
import {
|
|
19
|
+
Signer,
|
|
20
|
+
BigNumberish,
|
|
21
|
+
AddressLike,
|
|
22
|
+
ZeroAddress,
|
|
23
|
+
AbiCoder,
|
|
24
|
+
ContractTransactionReceipt,
|
|
25
|
+
} from "ethers";
|
|
19
26
|
import { expect } from "chai";
|
|
20
|
-
import { loadCircuit,
|
|
21
|
-
import { groth16 } from "snarkjs";
|
|
22
|
-
import { formatPrivKeyForBabyJub, stringifyBigInts } from "maci-crypto";
|
|
27
|
+
import { loadCircuit, Poseidon } from "zeto-js";
|
|
23
28
|
import {
|
|
24
29
|
User,
|
|
25
30
|
UTXO,
|
|
@@ -28,15 +33,21 @@ import {
|
|
|
28
33
|
doMint,
|
|
29
34
|
parseUTXOEvents,
|
|
30
35
|
ZERO_UTXO,
|
|
36
|
+
logger,
|
|
31
37
|
} from "./lib/utils";
|
|
32
38
|
import {
|
|
33
39
|
loadProvingKeys,
|
|
34
40
|
prepareDepositProof,
|
|
35
41
|
prepareWithdrawProof,
|
|
36
42
|
prepareBurnProof,
|
|
43
|
+
inflateUtxos,
|
|
44
|
+
inflateOwners,
|
|
45
|
+
calculateSpendHash,
|
|
46
|
+
calculateCancelHash,
|
|
37
47
|
} from "./utils";
|
|
38
48
|
import { Zeto_Anon, Zeto_AnonBurnable } from "../typechain-types";
|
|
39
49
|
import { deployZeto } from "./lib/deploy";
|
|
50
|
+
import { prepareProof, encodeToBytes } from "./lib/anon_zeto_helpers";
|
|
40
51
|
|
|
41
52
|
const ZERO_PUBKEY = [0n, 0n];
|
|
42
53
|
const poseidonHash = Poseidon.poseidon4;
|
|
@@ -59,6 +70,9 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
59
70
|
let batchCircuit: any, batchProvingKey: any;
|
|
60
71
|
|
|
61
72
|
before(async function () {
|
|
73
|
+
if (process.env.SKIP_ANON_TESTS === "true") {
|
|
74
|
+
this.skip();
|
|
75
|
+
}
|
|
62
76
|
if (network.name !== "hardhat") {
|
|
63
77
|
// accommodate for longer block times on public networks
|
|
64
78
|
this.timeout(120000);
|
|
@@ -79,92 +93,220 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
79
93
|
({ provingKeyFile: batchProvingKey } = loadProvingKeys("anon_batch"));
|
|
80
94
|
});
|
|
81
95
|
|
|
96
|
+
beforeEach(async function () {
|
|
97
|
+
if (process.env.SKIP_ANON_TESTS === "true") {
|
|
98
|
+
this.skip();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
82
102
|
it("has 4 decimals", async function () {
|
|
83
103
|
const decimals = await zeto.decimals();
|
|
84
104
|
expect(decimals).to.equal(4, "Decimals should be 4");
|
|
85
105
|
});
|
|
86
106
|
|
|
87
|
-
it("
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await doMint(zeto, deployer, inputUtxos);
|
|
95
|
-
|
|
96
|
-
const aliceUTXOsToBeWithdrawn = [
|
|
97
|
-
newUTXO(1, Alice),
|
|
98
|
-
newUTXO(1, Alice),
|
|
99
|
-
newUTXO(1, Alice),
|
|
100
|
-
];
|
|
101
|
-
// Alice proposes the output UTXOs, 1 utxo to bob, 1 utxo to charlie and 3 utxos to alice
|
|
102
|
-
const _bOut1 = newUTXO(6, Bob);
|
|
103
|
-
const _bOut2 = newUTXO(1, Charlie);
|
|
104
|
-
|
|
105
|
-
const outputUtxos = [_bOut1, _bOut2, ...aliceUTXOsToBeWithdrawn];
|
|
106
|
-
const outputOwners = [Bob, Charlie, Alice, Alice, Alice];
|
|
107
|
-
const inflatedOutputUtxos = [...outputUtxos];
|
|
108
|
-
const inflatedOutputOwners = [...outputOwners];
|
|
109
|
-
for (let i = 0; i < 10 - outputUtxos.length; i++) {
|
|
110
|
-
inflatedOutputUtxos.push(ZERO_UTXO);
|
|
111
|
-
inflatedOutputOwners.push(Bob);
|
|
112
|
-
}
|
|
107
|
+
it("non-owner should not be able to mint", async function () {
|
|
108
|
+
const utxo1 = newUTXO(10, Alice);
|
|
109
|
+
const utxo2 = newUTXO(20, Alice);
|
|
110
|
+
await expect(doMint(zeto, Alice.signer, [utxo1, utxo2])).to.be.rejectedWith(
|
|
111
|
+
"OwnableUnauthorizedAccount",
|
|
112
|
+
);
|
|
113
|
+
});
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
// H-2: implementation contracts must be initialization-locked so an
|
|
116
|
+
// attacker cannot call initialize() directly on the impl, become its
|
|
117
|
+
// owner, and then upgradeTo(any) via _authorizeUpgrade (the OZ
|
|
118
|
+
// "implementation takeover" pattern, CVE-2022-35961 family). We test
|
|
119
|
+
// against the *actual deployed* implementation (read from the EIP-1967
|
|
120
|
+
// impl slot on the proxy) so this asserts the production deployment
|
|
121
|
+
// path produces a locked impl, not just that a redeployed contract
|
|
122
|
+
// would be locked.
|
|
123
|
+
it("initialize() reverts on the bare Zeto_Anon implementation contract", async function () {
|
|
124
|
+
const implAddress = await upgrades.erc1967.getImplementationAddress(
|
|
125
|
+
await zeto.getAddress(),
|
|
120
126
|
);
|
|
127
|
+
const impl = await ethers.getContractAt("Zeto_Anon", implAddress);
|
|
128
|
+
await expect(
|
|
129
|
+
impl.initialize("Z", "Z", await Alice.signer.getAddress(), {
|
|
130
|
+
verifier: ZeroAddress,
|
|
131
|
+
depositVerifier: ZeroAddress,
|
|
132
|
+
withdrawVerifier: ZeroAddress,
|
|
133
|
+
lockVerifier: ZeroAddress,
|
|
134
|
+
burnVerifier: ZeroAddress,
|
|
135
|
+
batchVerifier: ZeroAddress,
|
|
136
|
+
batchWithdrawVerifier: ZeroAddress,
|
|
137
|
+
batchLockVerifier: ZeroAddress,
|
|
138
|
+
batchBurnVerifier: ZeroAddress,
|
|
139
|
+
}),
|
|
140
|
+
).to.be.revertedWithCustomError(impl, "InvalidInitialization");
|
|
141
|
+
});
|
|
121
142
|
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
it("initialize() reverts on the bare Zeto_AnonBurnable implementation contract", async function () {
|
|
144
|
+
const implAddress = await upgrades.erc1967.getImplementationAddress(
|
|
145
|
+
await zetoBurnable.getAddress(),
|
|
146
|
+
);
|
|
147
|
+
const impl = await ethers.getContractAt("Zeto_AnonBurnable", implAddress);
|
|
148
|
+
await expect(
|
|
149
|
+
impl.initialize("Z", "Z", await Alice.signer.getAddress(), {
|
|
150
|
+
verifier: ZeroAddress,
|
|
151
|
+
depositVerifier: ZeroAddress,
|
|
152
|
+
withdrawVerifier: ZeroAddress,
|
|
153
|
+
lockVerifier: ZeroAddress,
|
|
154
|
+
burnVerifier: ZeroAddress,
|
|
155
|
+
batchVerifier: ZeroAddress,
|
|
156
|
+
batchWithdrawVerifier: ZeroAddress,
|
|
157
|
+
batchLockVerifier: ZeroAddress,
|
|
158
|
+
batchBurnVerifier: ZeroAddress,
|
|
159
|
+
}),
|
|
160
|
+
).to.be.revertedWithCustomError(impl, "InvalidInitialization");
|
|
161
|
+
});
|
|
137
162
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
describe("administrative invariants", function () {
|
|
164
|
+
it("setERC20() rejects the zero address", async function () {
|
|
165
|
+
// Use the burnable proxy as a stand-in: it has not had its ERC20
|
|
166
|
+
// bound during deployZeto (only the non-burnable Zeto_Anon does),
|
|
167
|
+
// so we are exercising the not-yet-set branch.
|
|
168
|
+
await expect(
|
|
169
|
+
zetoBurnable.connect(deployer).setERC20(ZeroAddress),
|
|
170
|
+
).to.be.revertedWithCustomError(zetoBurnable, "ZeroERC20Address");
|
|
171
|
+
});
|
|
142
172
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
it("setERC20() is one-shot: a second call reverts with ERC20AlreadySet", async function () {
|
|
174
|
+
// `zeto` already had setERC20(erc20) wired up in deployZeto, so the
|
|
175
|
+
// backing token is bound. Any subsequent call -- even by the owner,
|
|
176
|
+
// even to the same address -- must revert.
|
|
177
|
+
const current = await erc20.getAddress();
|
|
178
|
+
await expect(zeto.connect(deployer).setERC20(current))
|
|
179
|
+
.to.be.revertedWithCustomError(zeto, "ERC20AlreadySet")
|
|
180
|
+
.withArgs(current);
|
|
181
|
+
});
|
|
147
182
|
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
it("ownership transfer is two-step (Ownable2Step)", async function () {
|
|
184
|
+
// Stage Alice as the pending owner. The current owner (deployer)
|
|
185
|
+
// remains in control until Alice accepts.
|
|
186
|
+
const aliceAddr = await Alice.signer.getAddress();
|
|
187
|
+
const deployerAddr = await deployer.getAddress();
|
|
188
|
+
await (await zeto.connect(deployer).transferOwnership(aliceAddr)).wait();
|
|
189
|
+
expect(await zeto.owner()).to.equal(deployerAddr);
|
|
190
|
+
expect(await zeto.pendingOwner()).to.equal(aliceAddr);
|
|
150
191
|
|
|
151
|
-
|
|
192
|
+
// Anyone other than the pending owner accepting must revert.
|
|
193
|
+
await expect(
|
|
194
|
+
zeto.connect(Bob.signer).acceptOwnership(),
|
|
195
|
+
).to.be.revertedWithCustomError(zeto, "OwnableUnauthorizedAccount");
|
|
196
|
+
|
|
197
|
+
// Alice accepts and the swap completes atomically.
|
|
198
|
+
await (await zeto.connect(Alice.signer).acceptOwnership()).wait();
|
|
199
|
+
expect(await zeto.owner()).to.equal(aliceAddr);
|
|
200
|
+
|
|
201
|
+
// Restore the original owner so subsequent tests are unaffected.
|
|
202
|
+
await (
|
|
203
|
+
await zeto.connect(Alice.signer).transferOwnership(deployerAddr)
|
|
204
|
+
).wait();
|
|
205
|
+
await (await zeto.connect(deployer).acceptOwnership()).wait();
|
|
206
|
+
expect(await zeto.owner()).to.equal(deployerAddr);
|
|
207
|
+
});
|
|
152
208
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
209
|
+
it("computeSpendHash() and computeCancelHash() live in disjoint hash spaces", async function () {
|
|
210
|
+
// Defense-in-depth from M-7. Same payload must produce different
|
|
211
|
+
// commitments under the spend-vs-cancel domain so a spender cannot
|
|
212
|
+
// transpose a payload between spendLock and cancelLock.
|
|
213
|
+
const utxo = newUTXO(1, Alice);
|
|
214
|
+
const out = newUTXO(1, Bob);
|
|
215
|
+
const lockedInputs = [utxo.hash];
|
|
216
|
+
const lockedOutputs: bigint[] = [];
|
|
217
|
+
const outputs = [out.hash];
|
|
218
|
+
const data = "0x";
|
|
219
|
+
const spend = await zeto.computeSpendHash(
|
|
220
|
+
lockedInputs,
|
|
221
|
+
lockedOutputs,
|
|
222
|
+
outputs,
|
|
223
|
+
data,
|
|
224
|
+
);
|
|
225
|
+
const cancel = await zeto.computeCancelHash(
|
|
226
|
+
lockedInputs,
|
|
227
|
+
lockedOutputs,
|
|
228
|
+
outputs,
|
|
229
|
+
data,
|
|
230
|
+
);
|
|
231
|
+
expect(spend).to.not.equal(cancel);
|
|
232
|
+
// Mirror of off-chain helpers used by other tests.
|
|
233
|
+
expect(spend).to.equal(calculateSpendHash([utxo], [], [out], data));
|
|
234
|
+
expect(cancel).to.equal(calculateCancelHash([utxo], [], [out], data));
|
|
235
|
+
});
|
|
236
|
+
});
|
|
158
237
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
238
|
+
describe("batch transfers", () => {
|
|
239
|
+
let inputUtxos: UTXO[];
|
|
240
|
+
let outputUtxos: UTXO[];
|
|
241
|
+
let outputOwners: User[];
|
|
242
|
+
let aliceUTXOsToBeWithdrawn: UTXO[];
|
|
243
|
+
let txResult: ContractTransactionReceipt | null;
|
|
244
|
+
|
|
245
|
+
it("mint to Alice 10 UTXOs", async () => {
|
|
246
|
+
inputUtxos = [];
|
|
247
|
+
for (let i = 0; i < 10; i++) {
|
|
248
|
+
inputUtxos.push(newUTXO(1, Alice));
|
|
249
|
+
}
|
|
250
|
+
await doMint(zeto, deployer, inputUtxos);
|
|
251
|
+
});
|
|
164
252
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
253
|
+
it("transfer 10 UTXOs honestly to Bob & Charlie should succeed", async function () {
|
|
254
|
+
aliceUTXOsToBeWithdrawn = [
|
|
255
|
+
newUTXO(1, Alice),
|
|
256
|
+
newUTXO(1, Alice),
|
|
257
|
+
newUTXO(1, Alice),
|
|
258
|
+
];
|
|
259
|
+
const _bOut1 = newUTXO(6, Bob);
|
|
260
|
+
const _bOut2 = newUTXO(1, Charlie);
|
|
261
|
+
|
|
262
|
+
outputUtxos = [_bOut1, _bOut2, ...aliceUTXOsToBeWithdrawn];
|
|
263
|
+
outputOwners = [Bob, Charlie, Alice, Alice, Alice];
|
|
264
|
+
|
|
265
|
+
txResult = await doTransfer(Alice, inputUtxos, outputUtxos, outputOwners);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("check the non-empty output hashes are correct", async function () {
|
|
269
|
+
const events = parseUTXOEvents(zeto, txResult);
|
|
270
|
+
const incomingUTXOs: any = events[0].outputs;
|
|
271
|
+
for (let i = 0; i < outputUtxos.length; i++) {
|
|
272
|
+
const receivedValue = outputUtxos[i].value;
|
|
273
|
+
const receivedSalt = outputUtxos[i].salt;
|
|
274
|
+
const hash = poseidonHash([
|
|
275
|
+
BigInt(receivedValue),
|
|
276
|
+
receivedSalt,
|
|
277
|
+
outputOwners[i].babyJubPublicKey[0],
|
|
278
|
+
outputOwners[i].babyJubPublicKey[1],
|
|
279
|
+
]);
|
|
280
|
+
expect(incomingUTXOs[i]).to.equal(hash);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("withdraw 3 UTXOs to ERC20 tokens", async function () {
|
|
285
|
+
const mintTx = await erc20.connect(deployer).mint(zeto, 3);
|
|
286
|
+
await mintTx.wait();
|
|
287
|
+
const startingBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
288
|
+
|
|
289
|
+
const inflatedWithdrawInputs = [...aliceUTXOsToBeWithdrawn];
|
|
290
|
+
for (let i = aliceUTXOsToBeWithdrawn.length; i < 10; i++) {
|
|
291
|
+
inflatedWithdrawInputs.push(ZERO_UTXO);
|
|
292
|
+
}
|
|
293
|
+
const { inputCommitments, outputCommitments, encodedProof } =
|
|
294
|
+
await prepareWithdrawProof(Alice, inflatedWithdrawInputs, ZERO_UTXO);
|
|
295
|
+
|
|
296
|
+
const tx = await zeto
|
|
297
|
+
.connect(Alice.signer)
|
|
298
|
+
.withdraw(
|
|
299
|
+
3,
|
|
300
|
+
inputCommitments,
|
|
301
|
+
outputCommitments[0],
|
|
302
|
+
encodeToBytes(encodedProof),
|
|
303
|
+
"0x",
|
|
304
|
+
);
|
|
305
|
+
await tx.wait();
|
|
306
|
+
|
|
307
|
+
const endingBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
308
|
+
expect(endingBalance - startingBalance).to.be.equal(3);
|
|
309
|
+
});
|
|
168
310
|
});
|
|
169
311
|
|
|
170
312
|
it("mint ERC20 tokens to Alice to deposit to Zeto should succeed", async function () {
|
|
@@ -185,26 +327,24 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
185
327
|
);
|
|
186
328
|
const tx2 = await zeto
|
|
187
329
|
.connect(Alice.signer)
|
|
188
|
-
.deposit(100, outputCommitments, encodedProof, "0x");
|
|
189
|
-
await tx2.wait();
|
|
330
|
+
.deposit(100, outputCommitments, encodeToBytes(encodedProof), "0x");
|
|
331
|
+
const result = await tx2.wait();
|
|
332
|
+
logger.debug(`Method deposit() complete. Gas used: ${result?.gasUsed}`);
|
|
190
333
|
});
|
|
191
334
|
|
|
192
335
|
it("mint to Alice and transfer UTXOs honestly to Bob should succeed", async function () {
|
|
336
|
+
this.timeout(60000);
|
|
193
337
|
const startingBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
194
|
-
// first the authority mints UTXOs to Alice
|
|
195
338
|
utxo1 = newUTXO(10, Alice);
|
|
196
339
|
utxo2 = newUTXO(20, Alice);
|
|
197
340
|
await doMint(zeto, deployer, [utxo1, utxo2]);
|
|
198
341
|
|
|
199
|
-
// check the private mint activity is not exposed in the ERC20 contract
|
|
200
342
|
const afterMintBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
201
343
|
expect(afterMintBalance).to.equal(startingBalance);
|
|
202
344
|
|
|
203
|
-
// Alice proposes the output UTXOs
|
|
204
345
|
const _txo3 = newUTXO(25, Bob);
|
|
205
346
|
utxo4 = newUTXO(5, Alice, _txo3.salt);
|
|
206
347
|
|
|
207
|
-
// Alice transfers UTXOs to Bob
|
|
208
348
|
const result = await doTransfer(
|
|
209
349
|
Alice,
|
|
210
350
|
[utxo1, utxo2],
|
|
@@ -212,16 +352,11 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
212
352
|
[Bob, Alice],
|
|
213
353
|
);
|
|
214
354
|
|
|
215
|
-
// check the private transfer activity is not exposed in the ERC20 contract
|
|
216
355
|
const afterTransferBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
217
356
|
expect(afterTransferBalance).to.equal(startingBalance);
|
|
218
357
|
|
|
219
|
-
// Bob reconstructs the UTXO from off-chain secure message channels with Alice
|
|
220
|
-
// first obtain the UTXOs from the transaction event
|
|
221
358
|
const events = parseUTXOEvents(zeto, result);
|
|
222
359
|
const incomingUTXOs: any = events[0].outputs;
|
|
223
|
-
|
|
224
|
-
// Bob uses the information received from Alice to reconstruct the UTXO sent to him
|
|
225
360
|
const receivedValue = 25;
|
|
226
361
|
const receivedSalt = _txo3.salt;
|
|
227
362
|
const hash = poseidonHash([
|
|
@@ -232,28 +367,22 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
232
367
|
]);
|
|
233
368
|
expect(incomingUTXOs[0]).to.equal(hash);
|
|
234
369
|
|
|
235
|
-
// now Bob can reconstruct the UTXO using the information received from Alice
|
|
236
370
|
utxo3 = newUTXO(receivedValue, Bob, receivedSalt);
|
|
237
|
-
})
|
|
371
|
+
});
|
|
238
372
|
|
|
239
373
|
it("Bob transfers UTXOs, previously received from Alice, honestly to Charlie should succeed", async function () {
|
|
240
374
|
const startingBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
241
|
-
// give Bob another UTXO to be able to spend
|
|
242
375
|
const _utxo1 = newUTXO(20, Bob);
|
|
243
376
|
await doMint(zeto, deployer, [_utxo1]);
|
|
244
377
|
|
|
245
|
-
// check the private mint activity is not exposed in the ERC20 contract
|
|
246
378
|
const afterMintBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
247
379
|
expect(afterMintBalance).to.equal(startingBalance);
|
|
248
380
|
|
|
249
|
-
// propose the output UTXOs
|
|
250
381
|
const _utxo2 = newUTXO(30, Charlie);
|
|
251
382
|
utxo7 = newUTXO(15, Bob);
|
|
252
383
|
|
|
253
|
-
// Bob should be able to spend the UTXO that was reconstructed from the previous transaction
|
|
254
384
|
await doTransfer(Bob, [utxo3, _utxo1], [_utxo2, utxo7], [Charlie, Bob]);
|
|
255
385
|
|
|
256
|
-
// check the private transfer activity is not exposed in the ERC20 contract
|
|
257
386
|
const afterTransferBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
258
387
|
expect(afterTransferBalance).to.equal(startingBalance);
|
|
259
388
|
});
|
|
@@ -261,19 +390,23 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
261
390
|
it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () {
|
|
262
391
|
const startingBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
263
392
|
|
|
264
|
-
// Alice proposes the output ERC20 tokens
|
|
265
393
|
const outputCommitment = newUTXO(20, Alice);
|
|
266
394
|
|
|
267
395
|
const { inputCommitments, outputCommitments, encodedProof } =
|
|
268
396
|
await prepareWithdrawProof(Alice, [utxo100, ZERO_UTXO], outputCommitment);
|
|
269
397
|
|
|
270
|
-
// Alice withdraws her UTXOs to ERC20 tokens
|
|
271
398
|
const tx = await zeto
|
|
272
399
|
.connect(Alice.signer)
|
|
273
|
-
.withdraw(
|
|
274
|
-
|
|
400
|
+
.withdraw(
|
|
401
|
+
80,
|
|
402
|
+
inputCommitments,
|
|
403
|
+
outputCommitments[0],
|
|
404
|
+
encodeToBytes(encodedProof),
|
|
405
|
+
"0x",
|
|
406
|
+
);
|
|
407
|
+
const result = await tx.wait();
|
|
408
|
+
logger.debug(`Method withdraw() complete. Gas used: ${result?.gasUsed}`);
|
|
275
409
|
|
|
276
|
-
// Alice checks her ERC20 balance
|
|
277
410
|
const endingBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
278
411
|
expect(endingBalance - startingBalance).to.be.equal(80);
|
|
279
412
|
});
|
|
@@ -281,31 +414,24 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
281
414
|
it("Test support for large values, such as when using 18 decimals", async function () {
|
|
282
415
|
const EighteenDecimals = 10 ** 18;
|
|
283
416
|
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
await doMint(zeto, deployer, [utxo1, utxo2]);
|
|
417
|
+
const utxoA = newUTXO(10 * EighteenDecimals, Alice);
|
|
418
|
+
const utxoB = newUTXO(20 * EighteenDecimals, Alice);
|
|
419
|
+
await doMint(zeto, deployer, [utxoA, utxoB]);
|
|
288
420
|
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
const utxo4 = newUTXO(5 * EighteenDecimals, Alice);
|
|
421
|
+
const utxoC = newUTXO(25 * EighteenDecimals, Bob);
|
|
422
|
+
const utxoD = newUTXO(5 * EighteenDecimals, Alice);
|
|
292
423
|
|
|
293
|
-
// Alice transfers UTXOs to Bob
|
|
294
424
|
const result = await doTransfer(
|
|
295
425
|
Alice,
|
|
296
|
-
[
|
|
297
|
-
[
|
|
426
|
+
[utxoA, utxoB],
|
|
427
|
+
[utxoC, utxoD],
|
|
298
428
|
[Bob, Alice],
|
|
299
429
|
);
|
|
300
430
|
|
|
301
|
-
// Bob reconstructs the UTXO from off-chain secure message channels with Alice
|
|
302
|
-
// first obtain the UTXOs from the transaction event
|
|
303
431
|
const events = parseUTXOEvents(zeto, result);
|
|
304
432
|
const incomingUTXOs: any = events[0].outputs;
|
|
305
|
-
|
|
306
|
-
// Bob uses the information received from Alice to reconstruct the UTXO sent to him
|
|
307
433
|
const receivedValue = 25 * EighteenDecimals;
|
|
308
|
-
const receivedSalt =
|
|
434
|
+
const receivedSalt = utxoC.salt;
|
|
309
435
|
const hash = poseidonHash([
|
|
310
436
|
BigInt(receivedValue),
|
|
311
437
|
receivedSalt,
|
|
@@ -315,34 +441,6 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
315
441
|
expect(incomingUTXOs[0]).to.equal(hash);
|
|
316
442
|
});
|
|
317
443
|
|
|
318
|
-
it("(burnable) mint to Alice and transfer honestly to Bob then burn should succeed", async function () {
|
|
319
|
-
// first mint the tokens for testing
|
|
320
|
-
const inputUtxos = [];
|
|
321
|
-
for (let i = 0; i < 3; i++) {
|
|
322
|
-
inputUtxos.push(newUTXO(10, Alice));
|
|
323
|
-
}
|
|
324
|
-
await doMint(zetoBurnable, deployer, inputUtxos);
|
|
325
|
-
|
|
326
|
-
const remainder = newUTXO(5, Alice);
|
|
327
|
-
|
|
328
|
-
// Alice can burn the UTXOs she owns
|
|
329
|
-
const { inputCommitments, outputCommitment, encodedProof } =
|
|
330
|
-
await prepareBurnProof(Alice, inputUtxos.slice(0, 2), remainder);
|
|
331
|
-
const tx = await zetoBurnable
|
|
332
|
-
.connect(Alice.signer)
|
|
333
|
-
.burn(inputCommitments, outputCommitment, encodedProof, "0x");
|
|
334
|
-
await tx.wait();
|
|
335
|
-
|
|
336
|
-
// check that the burned UTXOs are spent
|
|
337
|
-
let spent = await zetoBurnable.spent(inputCommitments[0]);
|
|
338
|
-
expect(spent).to.be.true;
|
|
339
|
-
spent = await zetoBurnable.spent(inputCommitments[1]);
|
|
340
|
-
expect(spent).to.be.true;
|
|
341
|
-
// check that the remaining UTXO is not spent
|
|
342
|
-
spent = await zetoBurnable.spent(inputUtxos[2].hash as BigNumberish);
|
|
343
|
-
expect(spent).to.be.false;
|
|
344
|
-
});
|
|
345
|
-
|
|
346
444
|
describe("failure cases", function () {
|
|
347
445
|
// the following failure cases rely on the hardhat network
|
|
348
446
|
// to return the details of the errors. This is not possible
|
|
@@ -352,7 +450,6 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
352
450
|
}
|
|
353
451
|
|
|
354
452
|
it("Alice attempting to withdraw spent UTXOs should fail", async function () {
|
|
355
|
-
// Alice proposes the output ERC20 tokens
|
|
356
453
|
const outputCommitment = newUTXO(90, Alice);
|
|
357
454
|
|
|
358
455
|
const { inputCommitments, outputCommitments, encodedProof } =
|
|
@@ -369,7 +466,7 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
369
466
|
10,
|
|
370
467
|
inputCommitments,
|
|
371
468
|
outputCommitments[0],
|
|
372
|
-
encodedProof,
|
|
469
|
+
encodeToBytes(encodedProof),
|
|
373
470
|
"0x",
|
|
374
471
|
),
|
|
375
472
|
).rejectedWith("UTXOAlreadySpent");
|
|
@@ -401,7 +498,6 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
401
498
|
});
|
|
402
499
|
|
|
403
500
|
it("transfer spent UTXOs should fail (double spend protection)", async function () {
|
|
404
|
-
// create outputs
|
|
405
501
|
const utxo5 = newUTXO(25, Bob);
|
|
406
502
|
const utxo6 = newUTXO(5, Alice, utxo5.salt);
|
|
407
503
|
await expect(
|
|
@@ -418,287 +514,687 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
418
514
|
});
|
|
419
515
|
});
|
|
420
516
|
|
|
421
|
-
describe("
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
inflatedInputUtxos,
|
|
442
|
-
inflatedOutputUtxos,
|
|
443
|
-
inflatedOutputOwners,
|
|
444
|
-
);
|
|
445
|
-
await expect(
|
|
446
|
-
zeto.connect(Bob.signer).lock(
|
|
447
|
-
result.inputCommitments,
|
|
448
|
-
[result.outputCommitments[0]], // unlocked output
|
|
449
|
-
result.outputCommitments, // locked output
|
|
450
|
-
result.encodedProof,
|
|
451
|
-
Alice.ethAddress, // make Alice the delegate who can spend the state (if she has the right proof)
|
|
452
|
-
"0x",
|
|
453
|
-
),
|
|
454
|
-
).rejectedWith(`UTXODuplicate(${resusedUtxo.hash.toString()})`);
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
it("lock() should succeed when using unlocked states", async function () {
|
|
458
|
-
lockedUtxo1 = newUTXO(10, Bob);
|
|
459
|
-
lockedUtxo2 = newUTXO(5, Bob);
|
|
460
|
-
const result = await prepareProof(
|
|
461
|
-
circuit,
|
|
462
|
-
provingKey,
|
|
463
|
-
Bob,
|
|
464
|
-
[utxo7, ZERO_UTXO],
|
|
465
|
-
[lockedUtxo1, lockedUtxo2],
|
|
466
|
-
[Bob, Bob],
|
|
467
|
-
);
|
|
468
|
-
const tx = await zeto.connect(Bob.signer).lock(
|
|
469
|
-
result.inputCommitments,
|
|
470
|
-
[], // unlocked output
|
|
471
|
-
result.outputCommitments, // locked output
|
|
472
|
-
result.encodedProof,
|
|
473
|
-
Alice.ethAddress, // make Alice the delegate who can spend the state (if she has the right proof)
|
|
474
|
-
"0x",
|
|
475
|
-
);
|
|
476
|
-
const results = await tx.wait();
|
|
477
|
-
console.log(`Method transfer() complete. Gas used: ${results?.gasUsed}`);
|
|
478
|
-
});
|
|
517
|
+
describe("ILockableCapability tests", function () {
|
|
518
|
+
// ABI fragments for the ZetoLockableCapability *Args payloads. Mirrors
|
|
519
|
+
// the layout used in zeto_anon_nullifier.ts so that the cross-token
|
|
520
|
+
// contract surface stays uniform.
|
|
521
|
+
const CREATE_ARGS_ABI =
|
|
522
|
+
"tuple(bytes32 txId, uint256[] inputs, uint256[] outputs, uint256[] lockedOutputs, bytes proof)";
|
|
523
|
+
const UPDATE_ARGS_ABI = "tuple(bytes32 txId)";
|
|
524
|
+
const DELEGATE_ARGS_ABI = "tuple(bytes32 txId)";
|
|
525
|
+
const SPEND_ARGS_ABI =
|
|
526
|
+
"tuple(bytes32 txId, uint256[] lockedOutputs, uint256[] outputs, bytes proof, bytes data)";
|
|
527
|
+
|
|
528
|
+
function encodeCreateArgs(args: {
|
|
529
|
+
txId: string;
|
|
530
|
+
inputs: BigNumberish[];
|
|
531
|
+
outputs: BigNumberish[];
|
|
532
|
+
lockedOutputs: BigNumberish[];
|
|
533
|
+
proof: string;
|
|
534
|
+
}) {
|
|
535
|
+
return new AbiCoder().encode([CREATE_ARGS_ABI], [args]);
|
|
536
|
+
}
|
|
479
537
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
Alice.ethAddress,
|
|
484
|
-
]);
|
|
485
|
-
expect(await zeto.locked(lockedUtxo2.hash)).to.deep.equal([
|
|
486
|
-
true,
|
|
487
|
-
Alice.ethAddress,
|
|
488
|
-
]);
|
|
489
|
-
expect(await zeto.locked(utxo7.hash)).to.deep.equal([
|
|
490
|
-
false,
|
|
491
|
-
"0x0000000000000000000000000000000000000000",
|
|
492
|
-
]);
|
|
493
|
-
expect(await zeto.locked(utxo1.hash)).to.deep.equal([
|
|
494
|
-
false,
|
|
495
|
-
"0x0000000000000000000000000000000000000000",
|
|
496
|
-
]);
|
|
497
|
-
});
|
|
538
|
+
function encodeUpdateArgs(txId: string) {
|
|
539
|
+
return new AbiCoder().encode([UPDATE_ARGS_ABI], [{ txId }]);
|
|
540
|
+
}
|
|
498
541
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
542
|
+
function encodeDelegateArgs(txId: string) {
|
|
543
|
+
return new AbiCoder().encode([DELEGATE_ARGS_ABI], [{ txId }]);
|
|
544
|
+
}
|
|
503
545
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
);
|
|
546
|
+
function encodeSpendArgs(args: {
|
|
547
|
+
txId: string;
|
|
548
|
+
lockedOutputs: BigNumberish[];
|
|
549
|
+
outputs: BigNumberish[];
|
|
550
|
+
proof: string;
|
|
551
|
+
data: string;
|
|
552
|
+
}) {
|
|
553
|
+
return new AbiCoder().encode([SPEND_ARGS_ABI], [args]);
|
|
554
|
+
}
|
|
514
555
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
.connect(Bob.signer)
|
|
519
|
-
.lock(
|
|
520
|
-
result.inputCommitments,
|
|
521
|
-
[],
|
|
522
|
-
result.outputCommitments,
|
|
523
|
-
result.encodedProof,
|
|
524
|
-
Bob.ethAddress,
|
|
525
|
-
"0x",
|
|
526
|
-
),
|
|
527
|
-
).rejectedWith(`UTXOAlreadyLocked(${lockedUtxo1.hash.toString()})`);
|
|
528
|
-
});
|
|
556
|
+
function randomBytes32(): string {
|
|
557
|
+
return ethers.hexlify(ethers.randomBytes(32));
|
|
558
|
+
}
|
|
529
559
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
560
|
+
describe("createLock -> updateLock -> delegateLock -> spendLock flow", function () {
|
|
561
|
+
let bobSourceUtxo: UTXO;
|
|
562
|
+
let lockedUtxo: UTXO;
|
|
563
|
+
let lockId: string;
|
|
564
|
+
let outUtxo1: UTXO;
|
|
565
|
+
let outUtxo2: UTXO;
|
|
566
|
+
let unlockHash: string;
|
|
567
|
+
|
|
568
|
+
before(async function () {
|
|
569
|
+
bobSourceUtxo = newUTXO(100, Bob);
|
|
570
|
+
await doMint(zeto, deployer, [bobSourceUtxo]);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("createLock() with deterministic lockId computed from txId", async function () {
|
|
574
|
+
// The locked content is just a fresh UTXO of equal value held under Bob.
|
|
575
|
+
// The proof on the create path proves the standard transfer
|
|
576
|
+
// relationship: bobSourceUtxo -> lockedUtxo. The contract treats
|
|
577
|
+
// the locked output identically to a regular output for the purposes
|
|
578
|
+
// of circuit verification (they are merged into a single output set
|
|
579
|
+
// before constructPublicInputs), so the same anon circuit is reused.
|
|
580
|
+
lockedUtxo = newUTXO(bobSourceUtxo.value!, Bob);
|
|
581
|
+
const encodedZkProof = await prepareProof(
|
|
582
|
+
circuit,
|
|
583
|
+
provingKey,
|
|
534
584
|
Bob,
|
|
535
|
-
[
|
|
536
|
-
[
|
|
537
|
-
[
|
|
538
|
-
)
|
|
539
|
-
).to.be.rejectedWith("UTXOAlreadyLocked");
|
|
540
|
-
});
|
|
585
|
+
[bobSourceUtxo, ZERO_UTXO],
|
|
586
|
+
[lockedUtxo, ZERO_UTXO],
|
|
587
|
+
[Bob, Bob],
|
|
588
|
+
);
|
|
541
589
|
|
|
542
|
-
|
|
543
|
-
|
|
590
|
+
const txId = randomBytes32();
|
|
591
|
+
const createArgs = encodeCreateArgs({
|
|
592
|
+
txId,
|
|
593
|
+
inputs: [bobSourceUtxo.hash],
|
|
594
|
+
outputs: [],
|
|
595
|
+
lockedOutputs: [lockedUtxo.hash],
|
|
596
|
+
proof: encodeToBytes(encodedZkProof),
|
|
597
|
+
});
|
|
544
598
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
599
|
+
const predicted = await zeto
|
|
600
|
+
.connect(Bob.signer)
|
|
601
|
+
.computeLockId(createArgs);
|
|
602
|
+
lockId = predicted;
|
|
603
|
+
|
|
604
|
+
const tx = await zeto
|
|
605
|
+
.connect(Bob.signer)
|
|
606
|
+
.createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x");
|
|
607
|
+
const result: ContractTransactionReceipt | null = await tx.wait();
|
|
608
|
+
logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
|
|
609
|
+
|
|
610
|
+
const created = result!.logs
|
|
611
|
+
.map((l) => {
|
|
612
|
+
try {
|
|
613
|
+
return zeto.interface.parseLog(l as any);
|
|
614
|
+
} catch (_e) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
})
|
|
618
|
+
.find((p) => p && p.name === "LockCreated");
|
|
619
|
+
expect(created, "LockCreated event not found").to.not.be.null;
|
|
620
|
+
expect(created!.args.lockId).to.equal(predicted);
|
|
621
|
+
expect(created!.args.owner).to.equal(Bob.ethAddress);
|
|
622
|
+
expect(created!.args.spender).to.equal(Bob.ethAddress);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("isLockActive() and getLock() reflect the newly created lock", async function () {
|
|
626
|
+
expect(await zeto.isLockActive(lockId)).to.equal(true);
|
|
627
|
+
const info = await zeto.getLock(lockId);
|
|
628
|
+
expect(info.owner).to.equal(Bob.ethAddress);
|
|
629
|
+
expect(info.spender).to.equal(Bob.ethAddress);
|
|
630
|
+
expect(info.spendCommitment).to.equal(ethers.ZeroHash);
|
|
631
|
+
expect(info.cancelCommitment).to.equal(ethers.ZeroHash);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("locked() returns true for locked UTXOs and false for unlocked or spent UTXOs", async function () {
|
|
635
|
+
// Just-created lock: spender == owner == Bob.
|
|
636
|
+
expect(await zeto.locked(lockedUtxo.hash)).to.deep.equal([
|
|
637
|
+
true,
|
|
638
|
+
Bob.ethAddress,
|
|
639
|
+
]);
|
|
640
|
+
expect((await zeto.locked(bobSourceUtxo.hash))[0]).to.be.false;
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("updateLock() commits the spend hash while owner == spender", async function () {
|
|
644
|
+
outUtxo1 = newUTXO(10, Alice);
|
|
645
|
+
outUtxo2 = newUTXO(90, Bob);
|
|
646
|
+
|
|
647
|
+
unlockHash = calculateSpendHash(
|
|
648
|
+
[lockedUtxo],
|
|
649
|
+
[],
|
|
650
|
+
[outUtxo1, outUtxo2],
|
|
651
|
+
"0x",
|
|
550
652
|
);
|
|
551
653
|
|
|
552
|
-
|
|
553
|
-
await expect(
|
|
554
|
-
zeto
|
|
654
|
+
const tx = await zeto
|
|
555
655
|
.connect(Bob.signer)
|
|
556
|
-
.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
656
|
+
.updateLock(
|
|
657
|
+
lockId,
|
|
658
|
+
encodeUpdateArgs(randomBytes32()),
|
|
659
|
+
unlockHash,
|
|
660
|
+
ethers.ZeroHash,
|
|
561
661
|
"0x",
|
|
562
|
-
)
|
|
563
|
-
|
|
564
|
-
|
|
662
|
+
);
|
|
663
|
+
const result = await tx.wait();
|
|
664
|
+
logger.debug(`updateLock() complete. Gas used: ${result?.gasUsed}`);
|
|
565
665
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
666
|
+
const info = await zeto.getLock(lockId);
|
|
667
|
+
expect(info.spendCommitment).to.equal(unlockHash);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it("delegateLock() transfers spending authority to Alice", async function () {
|
|
671
|
+
const tx = await zeto
|
|
569
672
|
.connect(Bob.signer)
|
|
570
673
|
.delegateLock(
|
|
571
|
-
|
|
572
|
-
|
|
674
|
+
lockId,
|
|
675
|
+
encodeDelegateArgs(randomBytes32()),
|
|
676
|
+
Alice.ethAddress,
|
|
573
677
|
"0x",
|
|
574
|
-
)
|
|
575
|
-
|
|
576
|
-
|
|
678
|
+
);
|
|
679
|
+
const result = await tx.wait();
|
|
680
|
+
logger.debug(`delegateLock() complete. Gas used: ${result?.gasUsed}`);
|
|
681
|
+
|
|
682
|
+
const info = await zeto.getLock(lockId);
|
|
683
|
+
expect(info.spender).to.equal(Alice.ethAddress);
|
|
684
|
+
// Per-UTXO delegate projection in ZetoFungible must reflect the
|
|
685
|
+
// new spender. This is the layer that BaseStorage no longer tracks
|
|
686
|
+
// after the "pull delegates out of BaseStorage" refactor.
|
|
687
|
+
expect(await zeto.locked(lockedUtxo.hash)).to.deep.equal([
|
|
688
|
+
true,
|
|
689
|
+
Alice.ethAddress,
|
|
690
|
+
]);
|
|
691
|
+
});
|
|
577
692
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
693
|
+
it("the new spender can spendLock() with the matching payload", async function () {
|
|
694
|
+
const encodedZkProofForSettle = await prepareProof(
|
|
695
|
+
circuit,
|
|
696
|
+
provingKey,
|
|
697
|
+
Bob,
|
|
698
|
+
[lockedUtxo, ZERO_UTXO],
|
|
699
|
+
[outUtxo1, outUtxo2],
|
|
700
|
+
[Alice, Bob],
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
const spendArgs = encodeSpendArgs({
|
|
704
|
+
txId: randomBytes32(),
|
|
705
|
+
lockedOutputs: [],
|
|
706
|
+
outputs: [outUtxo1.hash, outUtxo2.hash],
|
|
707
|
+
proof: encodeToBytes(encodedZkProofForSettle),
|
|
708
|
+
data: "0x",
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const tx = await zeto
|
|
581
712
|
.connect(Alice.signer)
|
|
582
|
-
.
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
)
|
|
587
|
-
|
|
713
|
+
.spendLock(lockId, spendArgs, "0x");
|
|
714
|
+
const result = await tx.wait();
|
|
715
|
+
|
|
716
|
+
const parsed = result!.logs
|
|
717
|
+
.map((l) => {
|
|
718
|
+
try {
|
|
719
|
+
return zeto.interface.parseLog(l as any);
|
|
720
|
+
} catch (_e) {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
})
|
|
724
|
+
.filter((p) => p !== null) as ReadonlyArray<{
|
|
725
|
+
name: string;
|
|
726
|
+
args: any;
|
|
727
|
+
}>;
|
|
728
|
+
const lockSpent = parsed.find((p) => p.name === "LockSpent");
|
|
729
|
+
const zetoLockSpent = parsed.find((p) => p.name === "ZetoLockSpent");
|
|
730
|
+
expect(lockSpent, "LockSpent event not emitted").to.not.be.undefined;
|
|
731
|
+
expect(zetoLockSpent, "ZetoLockSpent event not emitted").to.not.be
|
|
732
|
+
.undefined;
|
|
733
|
+
expect(lockSpent!.args.lockId).to.equal(lockId);
|
|
734
|
+
expect(lockSpent!.args.spender).to.equal(Alice.ethAddress);
|
|
735
|
+
|
|
736
|
+
// Lock is no longer active.
|
|
737
|
+
expect(await zeto.isLockActive(lockId)).to.equal(false);
|
|
738
|
+
|
|
739
|
+
// Outputs are now ordinary unlocked UTXOs.
|
|
740
|
+
expect(await zeto.spent(outUtxo1.hash)).to.equal(1n); // UNSPENT
|
|
741
|
+
expect(await zeto.spent(outUtxo2.hash)).to.equal(1n); // UNSPENT
|
|
742
|
+
|
|
743
|
+
// Per-UTXO delegate projection is cleared post-consume.
|
|
744
|
+
expect(await zeto.locked(lockedUtxo.hash)).to.deep.equal([
|
|
745
|
+
false,
|
|
746
|
+
ZeroAddress,
|
|
747
|
+
]);
|
|
748
|
+
});
|
|
588
749
|
});
|
|
589
750
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
751
|
+
describe("createLock -> cancelLock flow", function () {
|
|
752
|
+
let bobSourceUtxo: UTXO;
|
|
753
|
+
let lockedUtxo: UTXO;
|
|
754
|
+
let lockId: string;
|
|
755
|
+
let cancelHash: string;
|
|
756
|
+
let outUtxo1: UTXO;
|
|
757
|
+
let outUtxo2: UTXO;
|
|
758
|
+
|
|
759
|
+
before(async function () {
|
|
760
|
+
bobSourceUtxo = newUTXO(100, Bob);
|
|
761
|
+
await doMint(zeto, deployer, [bobSourceUtxo]);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("Bob createLock() with a non-zero cancelCommitment", async function () {
|
|
765
|
+
lockedUtxo = newUTXO(bobSourceUtxo.value!, Bob);
|
|
766
|
+
const encodedZkProof = await prepareProof(
|
|
594
767
|
circuit,
|
|
595
768
|
provingKey,
|
|
596
769
|
Bob,
|
|
597
|
-
[
|
|
598
|
-
[
|
|
599
|
-
[
|
|
770
|
+
[bobSourceUtxo, ZERO_UTXO],
|
|
771
|
+
[lockedUtxo, ZERO_UTXO],
|
|
772
|
+
[Bob, Bob],
|
|
600
773
|
);
|
|
601
|
-
// Charlie is the new delegate (in reality this is usually a contract that orchestrates a trade flow) to spend the locked state
|
|
602
|
-
// using the proof generated by the trade counterparty (Bob in this case)
|
|
603
|
-
await expect(
|
|
604
|
-
sendTx(
|
|
605
|
-
Charlie,
|
|
606
|
-
inputCommitments,
|
|
607
|
-
outputCommitments,
|
|
608
|
-
encodedProof,
|
|
609
|
-
true,
|
|
610
|
-
),
|
|
611
|
-
).to.be.rejectedWith(`UTXOAlreadyOwned(${lockedUtxo2.hash.toString()})`);
|
|
612
|
-
});
|
|
613
774
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
775
|
+
outUtxo1 = newUTXO(10, Alice);
|
|
776
|
+
outUtxo2 = newUTXO(90, Bob);
|
|
777
|
+
cancelHash = calculateCancelHash(
|
|
778
|
+
[lockedUtxo],
|
|
779
|
+
[],
|
|
780
|
+
[outUtxo1, outUtxo2],
|
|
781
|
+
"0x",
|
|
782
|
+
);
|
|
621
783
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
784
|
+
const createArgs = encodeCreateArgs({
|
|
785
|
+
txId: randomBytes32(),
|
|
786
|
+
inputs: [bobSourceUtxo.hash],
|
|
787
|
+
outputs: [],
|
|
788
|
+
lockedOutputs: [lockedUtxo.hash],
|
|
789
|
+
proof: encodeToBytes(encodedZkProof),
|
|
790
|
+
});
|
|
791
|
+
lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
|
|
792
|
+
const tx = await zeto
|
|
793
|
+
.connect(Bob.signer)
|
|
794
|
+
.createLock(createArgs, ethers.ZeroHash, cancelHash, "0x");
|
|
795
|
+
const result = await tx.wait();
|
|
796
|
+
logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it("the owner can cancelLock() to reverse the lock without delegation", async function () {
|
|
800
|
+
const encodedZkProofForCancel = await prepareProof(
|
|
626
801
|
circuit,
|
|
627
802
|
provingKey,
|
|
628
803
|
Bob,
|
|
629
|
-
[
|
|
630
|
-
[
|
|
631
|
-
[Alice,
|
|
804
|
+
[lockedUtxo, ZERO_UTXO],
|
|
805
|
+
[outUtxo1, outUtxo2],
|
|
806
|
+
[Alice, Bob],
|
|
632
807
|
);
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
808
|
+
|
|
809
|
+
const cancelArgs = encodeSpendArgs({
|
|
810
|
+
txId: randomBytes32(),
|
|
811
|
+
lockedOutputs: [],
|
|
812
|
+
outputs: [outUtxo1.hash, outUtxo2.hash],
|
|
813
|
+
proof: encodeToBytes(encodedZkProofForCancel),
|
|
814
|
+
data: "0x",
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
const tx = await zeto
|
|
818
|
+
.connect(Bob.signer)
|
|
819
|
+
.cancelLock(lockId, cancelArgs, "0x");
|
|
820
|
+
const result = await tx.wait();
|
|
821
|
+
|
|
822
|
+
const parsed = result!.logs
|
|
823
|
+
.map((l) => {
|
|
824
|
+
try {
|
|
825
|
+
return zeto.interface.parseLog(l as any);
|
|
826
|
+
} catch (_e) {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
})
|
|
830
|
+
.filter((p) => p !== null) as ReadonlyArray<{
|
|
831
|
+
name: string;
|
|
832
|
+
args: any;
|
|
833
|
+
}>;
|
|
834
|
+
const cancelled = parsed.find((p) => p.name === "LockCancelled");
|
|
835
|
+
const zetoCancelled = parsed.find(
|
|
836
|
+
(p) => p.name === "ZetoLockCancelled",
|
|
837
|
+
);
|
|
838
|
+
expect(cancelled, "LockCancelled event not emitted").to.not.be
|
|
839
|
+
.undefined;
|
|
840
|
+
expect(zetoCancelled, "ZetoLockCancelled event not emitted").to.not.be
|
|
841
|
+
.undefined;
|
|
842
|
+
|
|
843
|
+
expect(await zeto.isLockActive(lockId)).to.equal(false);
|
|
844
|
+
});
|
|
644
845
|
});
|
|
645
846
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
847
|
+
describe("spendLock with a payload that does not match the spend commitment fails", function () {
|
|
848
|
+
let bobSourceUtxo: UTXO;
|
|
849
|
+
let lockedUtxo: UTXO;
|
|
850
|
+
let lockId: string;
|
|
851
|
+
let expectedHash: string;
|
|
852
|
+
|
|
853
|
+
before(async function () {
|
|
854
|
+
bobSourceUtxo = newUTXO(100, Bob);
|
|
855
|
+
await doMint(zeto, deployer, [bobSourceUtxo]);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it("Bob createLock() then updateLock() committing a specific spend hash", async function () {
|
|
859
|
+
lockedUtxo = newUTXO(bobSourceUtxo.value!, Bob);
|
|
860
|
+
const encodedZkProof = await prepareProof(
|
|
650
861
|
circuit,
|
|
651
862
|
provingKey,
|
|
652
863
|
Bob,
|
|
653
|
-
[
|
|
654
|
-
[
|
|
655
|
-
[
|
|
864
|
+
[bobSourceUtxo, ZERO_UTXO],
|
|
865
|
+
[lockedUtxo, ZERO_UTXO],
|
|
866
|
+
[Bob, Bob],
|
|
656
867
|
);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
868
|
+
const createArgs = encodeCreateArgs({
|
|
869
|
+
txId: randomBytes32(),
|
|
870
|
+
inputs: [bobSourceUtxo.hash],
|
|
871
|
+
outputs: [],
|
|
872
|
+
lockedOutputs: [lockedUtxo.hash],
|
|
873
|
+
proof: encodeToBytes(encodedZkProof),
|
|
874
|
+
});
|
|
875
|
+
lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
|
|
876
|
+
await (
|
|
877
|
+
await zeto
|
|
878
|
+
.connect(Bob.signer)
|
|
879
|
+
.createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x")
|
|
880
|
+
).wait();
|
|
881
|
+
|
|
882
|
+
const expectedOut1 = newUTXO(10, Alice);
|
|
883
|
+
const expectedOut2 = newUTXO(90, Bob);
|
|
884
|
+
expectedHash = calculateSpendHash(
|
|
885
|
+
[lockedUtxo],
|
|
886
|
+
[],
|
|
887
|
+
[expectedOut1, expectedOut2],
|
|
888
|
+
"0x",
|
|
889
|
+
);
|
|
890
|
+
await (
|
|
891
|
+
await zeto
|
|
892
|
+
.connect(Bob.signer)
|
|
893
|
+
.updateLock(
|
|
894
|
+
lockId,
|
|
895
|
+
encodeUpdateArgs(randomBytes32()),
|
|
896
|
+
expectedHash,
|
|
897
|
+
ethers.ZeroHash,
|
|
898
|
+
"0x",
|
|
899
|
+
)
|
|
900
|
+
).wait();
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it("spendLock() with a different payload reverts with InvalidUnlockHash", async function () {
|
|
904
|
+
if (network.name !== "hardhat") {
|
|
905
|
+
this.skip();
|
|
906
|
+
}
|
|
907
|
+
const wrongOut1 = newUTXO(20, Alice);
|
|
908
|
+
const wrongOut2 = newUTXO(80, Bob);
|
|
909
|
+
|
|
910
|
+
const encodedZkProofForSettle = await prepareProof(
|
|
911
|
+
circuit,
|
|
912
|
+
provingKey,
|
|
913
|
+
Bob,
|
|
914
|
+
[lockedUtxo, ZERO_UTXO],
|
|
915
|
+
[wrongOut1, wrongOut2],
|
|
916
|
+
[Alice, Bob],
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
const spendArgs = encodeSpendArgs({
|
|
920
|
+
txId: randomBytes32(),
|
|
921
|
+
lockedOutputs: [],
|
|
922
|
+
outputs: [wrongOut1.hash, wrongOut2.hash],
|
|
923
|
+
proof: encodeToBytes(encodedZkProofForSettle),
|
|
924
|
+
data: "0x",
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
const calculatedHash = calculateSpendHash(
|
|
928
|
+
[lockedUtxo],
|
|
929
|
+
[],
|
|
930
|
+
[wrongOut1, wrongOut2],
|
|
931
|
+
"0x",
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
await expect(
|
|
935
|
+
zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x"),
|
|
936
|
+
)
|
|
937
|
+
.to.be.revertedWithCustomError(zeto, "InvalidUnlockHash")
|
|
938
|
+
.withArgs(expectedHash, calculatedHash);
|
|
939
|
+
});
|
|
668
940
|
});
|
|
669
941
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
[lockedUtxo2, ZERO_UTXO],
|
|
676
|
-
[utxo1, ZERO_UTXO],
|
|
677
|
-
[Charlie, Charlie],
|
|
678
|
-
),
|
|
679
|
-
).to.be.rejectedWith("UTXOAlreadyLocked");
|
|
942
|
+
describe("negative cases for the lock lifecycle", function () {
|
|
943
|
+
// These tests rely on hardhat-style revert decoding.
|
|
944
|
+
if (network.name !== "hardhat") {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
680
947
|
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
948
|
+
// freshLock mints a UTXO for `owner`, locks it, and returns the
|
|
949
|
+
// resulting lock metadata so each negative case can branch off
|
|
950
|
+
// without polluting other test scopes.
|
|
951
|
+
async function freshLock(
|
|
952
|
+
owner: User,
|
|
953
|
+
spendCommitment: string = ethers.ZeroHash,
|
|
954
|
+
cancelCommitment: string = ethers.ZeroHash,
|
|
955
|
+
): Promise<{
|
|
956
|
+
lockId: string;
|
|
957
|
+
sourceUtxo: UTXO;
|
|
958
|
+
lockedUtxo: UTXO;
|
|
959
|
+
createArgs: string;
|
|
960
|
+
}> {
|
|
961
|
+
const sourceUtxo = newUTXO(100, owner);
|
|
962
|
+
await doMint(zeto, deployer, [sourceUtxo]);
|
|
963
|
+
|
|
964
|
+
const lockedUtxo = newUTXO(sourceUtxo.value!, owner);
|
|
965
|
+
const encodedZkProof = await prepareProof(
|
|
684
966
|
circuit,
|
|
685
967
|
provingKey,
|
|
686
|
-
|
|
687
|
-
[
|
|
688
|
-
[
|
|
689
|
-
[
|
|
968
|
+
owner,
|
|
969
|
+
[sourceUtxo, ZERO_UTXO],
|
|
970
|
+
[lockedUtxo, ZERO_UTXO],
|
|
971
|
+
[owner, owner],
|
|
690
972
|
);
|
|
691
|
-
await expect(
|
|
692
|
-
zeto
|
|
693
|
-
.connect(Alice.signer)
|
|
694
|
-
.unlock(inputCommitments, outputCommitments, encodedProof, "0x"),
|
|
695
|
-
).to.be.fulfilled;
|
|
696
973
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
974
|
+
const createArgs = encodeCreateArgs({
|
|
975
|
+
txId: randomBytes32(),
|
|
976
|
+
inputs: [sourceUtxo.hash],
|
|
977
|
+
outputs: [],
|
|
978
|
+
lockedOutputs: [lockedUtxo.hash],
|
|
979
|
+
proof: encodeToBytes(encodedZkProof),
|
|
980
|
+
});
|
|
981
|
+
const lockId = await zeto
|
|
982
|
+
.connect(owner.signer)
|
|
983
|
+
.computeLockId(createArgs);
|
|
984
|
+
await (
|
|
985
|
+
await zeto
|
|
986
|
+
.connect(owner.signer)
|
|
987
|
+
.createLock(createArgs, spendCommitment, cancelCommitment, "0x")
|
|
988
|
+
).wait();
|
|
989
|
+
return { lockId, sourceUtxo, lockedUtxo, createArgs };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Re-encoding helper: a syntactically valid spend payload that does
|
|
993
|
+
// NOT need to verify a real ZK proof. Tests for authorization /
|
|
994
|
+
// immutability assertions short-circuit before the proof is touched.
|
|
995
|
+
function dummySpendArgs(): string {
|
|
996
|
+
return encodeSpendArgs({
|
|
997
|
+
txId: randomBytes32(),
|
|
998
|
+
lockedOutputs: [],
|
|
999
|
+
outputs: [],
|
|
1000
|
+
proof: "0x",
|
|
1001
|
+
data: "0x",
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
it("createLock() with a duplicate txId from the same caller reverts with DuplicateLock", async function () {
|
|
1006
|
+
const { lockId, createArgs } = await freshLock(Bob);
|
|
1007
|
+
|
|
1008
|
+
// Same createArgs (same txId, same caller) => same lockId =>
|
|
1009
|
+
// DuplicateLock fires before any input or proof validation.
|
|
1010
|
+
await expect(
|
|
1011
|
+
zeto
|
|
1012
|
+
.connect(Bob.signer)
|
|
1013
|
+
.createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x"),
|
|
1014
|
+
)
|
|
1015
|
+
.to.be.revertedWithCustomError(zeto, "DuplicateLock")
|
|
1016
|
+
.withArgs(lockId);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
it("updateLock() by a non-owner reverts with LockUnauthorized", async function () {
|
|
1020
|
+
const { lockId } = await freshLock(Bob);
|
|
1021
|
+
|
|
1022
|
+
await expect(
|
|
1023
|
+
zeto
|
|
1024
|
+
.connect(Alice.signer)
|
|
1025
|
+
.updateLock(
|
|
1026
|
+
lockId,
|
|
1027
|
+
encodeUpdateArgs(randomBytes32()),
|
|
1028
|
+
ethers.ZeroHash,
|
|
1029
|
+
ethers.ZeroHash,
|
|
1030
|
+
"0x",
|
|
1031
|
+
),
|
|
1032
|
+
)
|
|
1033
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1034
|
+
.withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
it("updateLock() prefers LockUnauthorized over LockImmutable when both apply (M-5)", async function () {
|
|
1038
|
+
// Lock is delegated (so spender != owner -> immutable) AND the
|
|
1039
|
+
// caller is neither owner nor spender. The contract MUST report
|
|
1040
|
+
// LockUnauthorized, not leak the immutability state to the
|
|
1041
|
+
// unauthorized caller.
|
|
1042
|
+
const { lockId } = await freshLock(Bob);
|
|
1043
|
+
await (
|
|
1044
|
+
await zeto
|
|
1045
|
+
.connect(Bob.signer)
|
|
1046
|
+
.delegateLock(
|
|
1047
|
+
lockId,
|
|
1048
|
+
encodeDelegateArgs(randomBytes32()),
|
|
1049
|
+
Alice.ethAddress,
|
|
1050
|
+
"0x",
|
|
1051
|
+
)
|
|
1052
|
+
).wait();
|
|
1053
|
+
|
|
1054
|
+
await expect(
|
|
1055
|
+
zeto
|
|
1056
|
+
.connect(Charlie.signer)
|
|
1057
|
+
.updateLock(
|
|
1058
|
+
lockId,
|
|
1059
|
+
encodeUpdateArgs(randomBytes32()),
|
|
1060
|
+
ethers.ZeroHash,
|
|
1061
|
+
ethers.ZeroHash,
|
|
1062
|
+
"0x",
|
|
1063
|
+
),
|
|
1064
|
+
)
|
|
1065
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1066
|
+
// spender at this point is Alice (the new delegate).
|
|
1067
|
+
.withArgs(lockId, Bob.ethAddress, Charlie.ethAddress);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
it("updateLock() after delegateLock() reverts with LockImmutable", async function () {
|
|
1071
|
+
const { lockId } = await freshLock(Bob);
|
|
1072
|
+
|
|
1073
|
+
// Bob delegates spending authority to Alice; spender (Alice) now
|
|
1074
|
+
// differs from owner (Bob).
|
|
1075
|
+
await (
|
|
1076
|
+
await zeto
|
|
1077
|
+
.connect(Bob.signer)
|
|
1078
|
+
.delegateLock(
|
|
1079
|
+
lockId,
|
|
1080
|
+
encodeDelegateArgs(randomBytes32()),
|
|
1081
|
+
Alice.ethAddress,
|
|
1082
|
+
"0x",
|
|
1083
|
+
)
|
|
1084
|
+
).wait();
|
|
1085
|
+
|
|
1086
|
+
// Even Bob (the owner) can no longer mutate the commitments —
|
|
1087
|
+
// the lock is now externally controlled and must be considered
|
|
1088
|
+
// immutable.
|
|
1089
|
+
await expect(
|
|
1090
|
+
zeto
|
|
1091
|
+
.connect(Bob.signer)
|
|
1092
|
+
.updateLock(
|
|
1093
|
+
lockId,
|
|
1094
|
+
encodeUpdateArgs(randomBytes32()),
|
|
1095
|
+
ethers.ZeroHash,
|
|
1096
|
+
ethers.ZeroHash,
|
|
1097
|
+
"0x",
|
|
1098
|
+
),
|
|
1099
|
+
)
|
|
1100
|
+
.to.be.revertedWithCustomError(zeto, "LockImmutable")
|
|
1101
|
+
.withArgs(lockId);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("delegateLock() by a non-spender reverts with LockUnauthorized", async function () {
|
|
1105
|
+
const { lockId } = await freshLock(Bob);
|
|
1106
|
+
|
|
1107
|
+
await expect(
|
|
1108
|
+
zeto
|
|
1109
|
+
.connect(Alice.signer)
|
|
1110
|
+
.delegateLock(
|
|
1111
|
+
lockId,
|
|
1112
|
+
encodeDelegateArgs(randomBytes32()),
|
|
1113
|
+
Charlie.ethAddress,
|
|
1114
|
+
"0x",
|
|
1115
|
+
),
|
|
1116
|
+
)
|
|
1117
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1118
|
+
.withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it("spendLock() by a non-spender reverts with LockUnauthorized before touching the proof", async function () {
|
|
1122
|
+
const { lockId } = await freshLock(Bob);
|
|
1123
|
+
|
|
1124
|
+
// Garbage proof — the onlySpender modifier MUST short-circuit
|
|
1125
|
+
// before any proof verification is attempted.
|
|
1126
|
+
await expect(
|
|
1127
|
+
zeto.connect(Alice.signer).spendLock(lockId, dummySpendArgs(), "0x"),
|
|
1128
|
+
)
|
|
1129
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1130
|
+
.withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
it("cancelLock() by a non-spender reverts with LockUnauthorized", async function () {
|
|
1134
|
+
const { lockId } = await freshLock(Bob);
|
|
1135
|
+
|
|
1136
|
+
await expect(
|
|
1137
|
+
zeto.connect(Alice.signer).cancelLock(lockId, dummySpendArgs(), "0x"),
|
|
1138
|
+
)
|
|
1139
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1140
|
+
.withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
it("after a successful spendLock(), the lock is no longer active and getLock() reverts", async function () {
|
|
1144
|
+
const { lockId, lockedUtxo } = await freshLock(Bob);
|
|
1145
|
+
|
|
1146
|
+
const out1 = newUTXO(10, Alice);
|
|
1147
|
+
const out2 = newUTXO(90, Bob);
|
|
1148
|
+
const zkProof = await prepareProof(
|
|
1149
|
+
circuit,
|
|
1150
|
+
provingKey,
|
|
1151
|
+
Bob,
|
|
1152
|
+
[lockedUtxo, ZERO_UTXO],
|
|
1153
|
+
[out1, out2],
|
|
1154
|
+
[Alice, Bob],
|
|
1155
|
+
);
|
|
1156
|
+
const spendArgs = encodeSpendArgs({
|
|
1157
|
+
txId: randomBytes32(),
|
|
1158
|
+
lockedOutputs: [],
|
|
1159
|
+
outputs: [out1.hash, out2.hash],
|
|
1160
|
+
proof: encodeToBytes(zkProof),
|
|
1161
|
+
data: "0x",
|
|
1162
|
+
});
|
|
1163
|
+
await (
|
|
1164
|
+
await zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x")
|
|
1165
|
+
).wait();
|
|
1166
|
+
|
|
1167
|
+
expect(await zeto.isLockActive(lockId)).to.equal(false);
|
|
1168
|
+
await expect(zeto.getLock(lockId))
|
|
1169
|
+
.to.be.revertedWithCustomError(zeto, "LockNotActive")
|
|
1170
|
+
.withArgs(lockId);
|
|
1171
|
+
// Re-spending a consumed lock MUST also fail — lockActive is
|
|
1172
|
+
// the first modifier and emits LockNotActive before
|
|
1173
|
+
// onlySpender has a chance to fire.
|
|
1174
|
+
await expect(
|
|
1175
|
+
zeto.connect(Bob.signer).spendLock(lockId, dummySpendArgs(), "0x"),
|
|
1176
|
+
)
|
|
1177
|
+
.to.be.revertedWithCustomError(zeto, "LockNotActive")
|
|
1178
|
+
.withArgs(lockId);
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
it("an unlocked input flow rejects a locked UTXO with AlreadyLocked", async function () {
|
|
1182
|
+
// Mint a UTXO, lock it, then try to spend it via the regular
|
|
1183
|
+
// transfer path. The base-storage validateInputs() guards this
|
|
1184
|
+
// and reverts with AlreadyLocked on the locked input.
|
|
1185
|
+
const { lockedUtxo } = await freshLock(Bob);
|
|
1186
|
+
|
|
1187
|
+
await expect(
|
|
1188
|
+
doTransfer(
|
|
1189
|
+
Bob,
|
|
1190
|
+
[lockedUtxo, ZERO_UTXO],
|
|
1191
|
+
[newUTXO(100, Alice), ZERO_UTXO],
|
|
1192
|
+
[Alice, Alice],
|
|
1193
|
+
),
|
|
1194
|
+
)
|
|
1195
|
+
.to.be.revertedWithCustomError(zeto, "AlreadyLocked")
|
|
1196
|
+
.withArgs(lockedUtxo.hash);
|
|
1197
|
+
});
|
|
702
1198
|
});
|
|
703
1199
|
});
|
|
704
1200
|
|
|
@@ -718,20 +1214,23 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
718
1214
|
circuitToUse = batchCircuit;
|
|
719
1215
|
provingKeyToUse = batchProvingKey;
|
|
720
1216
|
}
|
|
721
|
-
const
|
|
1217
|
+
const inflatedInputUtxos = inflateUtxos(inputs);
|
|
1218
|
+
const inflatedOutputUtxos = inflateUtxos(outputs);
|
|
1219
|
+
const inflatedOwners = inflateOwners(owners);
|
|
1220
|
+
|
|
1221
|
+
encodedProof = await prepareProof(
|
|
722
1222
|
circuitToUse,
|
|
723
1223
|
provingKeyToUse,
|
|
724
1224
|
signer,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1225
|
+
inflatedInputUtxos,
|
|
1226
|
+
inflatedOutputUtxos,
|
|
1227
|
+
inflatedOwners,
|
|
728
1228
|
);
|
|
729
|
-
inputCommitments =
|
|
730
|
-
outputCommitments =
|
|
1229
|
+
inputCommitments = inputs.map((input) => input.hash);
|
|
1230
|
+
outputCommitments = outputs.map((output) => output.hash);
|
|
731
1231
|
outputOwnerAddresses = owners.map(
|
|
732
1232
|
(owner) => owner.ethAddress || ZeroAddress,
|
|
733
1233
|
) as [AddressLike, AddressLike];
|
|
734
|
-
encodedProof = result.encodedProof;
|
|
735
1234
|
|
|
736
1235
|
return await sendTx(
|
|
737
1236
|
signer,
|
|
@@ -746,100 +1245,92 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
|
|
|
746
1245
|
inputCommitments: BigNumberish[],
|
|
747
1246
|
outputCommitments: BigNumberish[],
|
|
748
1247
|
encodedProof: any,
|
|
749
|
-
isLocked = false,
|
|
750
1248
|
) {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works
|
|
756
|
-
encodedProof,
|
|
757
|
-
"0x",
|
|
758
|
-
);
|
|
759
|
-
} else {
|
|
760
|
-
tx = await zeto
|
|
761
|
-
.connect(signer.signer)
|
|
762
|
-
.transfer(inputCommitments, outputCommitments, encodedProof, "0x");
|
|
763
|
-
}
|
|
1249
|
+
const proof = encodeToBytes(encodedProof);
|
|
1250
|
+
const tx = await zeto
|
|
1251
|
+
.connect(signer.signer)
|
|
1252
|
+
.transfer(inputCommitments, outputCommitments, proof, "0x");
|
|
764
1253
|
const results = await tx.wait();
|
|
765
|
-
|
|
1254
|
+
logger.debug(`Method transfer() complete. Gas used: ${results?.gasUsed}`);
|
|
766
1255
|
|
|
767
1256
|
for (const input of inputCommitments) {
|
|
768
1257
|
if (input === 0n) {
|
|
769
1258
|
continue;
|
|
770
1259
|
}
|
|
771
|
-
const
|
|
772
|
-
expect(
|
|
1260
|
+
const status = await zeto.spent(input);
|
|
1261
|
+
expect(status).to.equal(2n); // UTXOStatus.SPENT
|
|
773
1262
|
}
|
|
774
1263
|
for (const output of outputCommitments) {
|
|
775
1264
|
if (output === 0n) {
|
|
776
1265
|
continue;
|
|
777
1266
|
}
|
|
778
|
-
|
|
1267
|
+
const status = await zeto.spent(output);
|
|
1268
|
+
expect(status).to.equal(1n); // UTXOStatus.UNSPENT
|
|
779
1269
|
}
|
|
780
1270
|
|
|
781
1271
|
return results;
|
|
782
1272
|
}
|
|
783
|
-
});
|
|
784
1273
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1274
|
+
describe("Zeto_AnonBurnable", function () {
|
|
1275
|
+
it("(burnable) mint to Alice and burn a subset should succeed", async function () {
|
|
1276
|
+
const inputUtxos = [];
|
|
1277
|
+
for (let i = 0; i < 3; i++) {
|
|
1278
|
+
inputUtxos.push(newUTXO(10, Alice));
|
|
1279
|
+
}
|
|
1280
|
+
await doMint(zetoBurnable, deployer, inputUtxos);
|
|
1281
|
+
|
|
1282
|
+
const remainder = newUTXO(5, Alice);
|
|
1283
|
+
|
|
1284
|
+
const { inputCommitments, outputCommitment, encodedProof } =
|
|
1285
|
+
await prepareBurnProof(Alice, inputUtxos.slice(0, 2), remainder);
|
|
1286
|
+
const tx = await zetoBurnable
|
|
1287
|
+
.connect(Alice.signer)
|
|
1288
|
+
.burn(inputCommitments, outputCommitment, encodedProof, "0x");
|
|
1289
|
+
const result = await tx.wait();
|
|
1290
|
+
logger.debug(`Method burn() complete. Gas used: ${result?.gasUsed}`);
|
|
1291
|
+
|
|
1292
|
+
let spent = await zetoBurnable.spent(inputCommitments[0]);
|
|
1293
|
+
expect(spent).to.equal(2n); // UTXOStatus.SPENT
|
|
1294
|
+
spent = await zetoBurnable.spent(inputCommitments[1]);
|
|
1295
|
+
expect(spent).to.equal(2n); // UTXOStatus.SPENT
|
|
1296
|
+
spent = await zetoBurnable.spent(inputUtxos[2].hash as BigNumberish);
|
|
1297
|
+
expect(spent).to.equal(1n); // UTXOStatus.UNSPENT
|
|
1298
|
+
spent = await zetoBurnable.spent(outputCommitment);
|
|
1299
|
+
expect(spent).to.equal(1n); // UTXOStatus.UNSPENT (the burn remainder)
|
|
1300
|
+
});
|
|
809
1301
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
};
|
|
841
|
-
}
|
|
1302
|
+
it("burn rejects already-spent inputs", async function () {
|
|
1303
|
+
const inputUtxos = [newUTXO(10, Alice), newUTXO(10, Alice)];
|
|
1304
|
+
await doMint(zetoBurnable, deployer, inputUtxos);
|
|
1305
|
+
|
|
1306
|
+
const remainder1 = newUTXO(5, Alice);
|
|
1307
|
+
const proof1 = await prepareBurnProof(Alice, inputUtxos, remainder1);
|
|
1308
|
+
await zetoBurnable
|
|
1309
|
+
.connect(Alice.signer)
|
|
1310
|
+
.burn(
|
|
1311
|
+
proof1.inputCommitments,
|
|
1312
|
+
proof1.outputCommitment,
|
|
1313
|
+
proof1.encodedProof,
|
|
1314
|
+
"0x",
|
|
1315
|
+
);
|
|
1316
|
+
|
|
1317
|
+
const remainder2 = newUTXO(5, Alice);
|
|
1318
|
+
const proof2 = await prepareBurnProof(Alice, inputUtxos, remainder2);
|
|
1319
|
+
await expect(
|
|
1320
|
+
zetoBurnable
|
|
1321
|
+
.connect(Alice.signer)
|
|
1322
|
+
.burn(
|
|
1323
|
+
proof2.inputCommitments,
|
|
1324
|
+
proof2.outputCommitment,
|
|
1325
|
+
proof2.encodedProof,
|
|
1326
|
+
"0x",
|
|
1327
|
+
),
|
|
1328
|
+
).to.be.rejectedWith("UTXOAlreadySpent");
|
|
1329
|
+
});
|
|
1330
|
+
});
|
|
1331
|
+
});
|
|
842
1332
|
|
|
843
1333
|
module.exports = {
|
|
844
1334
|
prepareProof,
|
|
1335
|
+
encodeToBytes,
|
|
845
1336
|
};
|