create-fhevm-example 1.3.2 → 1.4.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/contracts/advanced/BlindAuction.sol +255 -0
- package/contracts/advanced/EncryptedEscrow.sol +315 -0
- package/contracts/advanced/HiddenVoting.sol +231 -0
- package/contracts/advanced/PrivateKYC.sol +309 -0
- package/contracts/advanced/PrivatePayroll.sol +285 -0
- package/contracts/basic/decryption/PublicDecryptMultipleValues.sol +160 -0
- package/contracts/basic/decryption/PublicDecryptSingleValue.sol +142 -0
- package/contracts/basic/decryption/UserDecryptMultipleValues.sol +61 -0
- package/contracts/basic/decryption/UserDecryptSingleValue.sol +59 -0
- package/contracts/basic/encryption/EncryptMultipleValues.sol +72 -0
- package/contracts/basic/encryption/EncryptSingleValue.sol +44 -0
- package/contracts/basic/encryption/FHECounter.sol +54 -0
- package/contracts/basic/fhe-operations/FHEAdd.sol +51 -0
- package/contracts/basic/fhe-operations/FHEArithmetic.sol +99 -0
- package/contracts/basic/fhe-operations/FHEComparison.sol +116 -0
- package/contracts/basic/fhe-operations/FHEIfThenElse.sol +53 -0
- package/contracts/concepts/FHEAccessControl.sol +94 -0
- package/contracts/concepts/FHEAntiPatterns.sol +329 -0
- package/contracts/concepts/FHEHandles.sol +128 -0
- package/contracts/concepts/FHEInputProof.sol +104 -0
- package/contracts/gaming/EncryptedLottery.sol +298 -0
- package/contracts/gaming/EncryptedPoker.sol +337 -0
- package/contracts/gaming/RockPaperScissors.sol +213 -0
- package/contracts/openzeppelin/ERC7984.sol +85 -0
- package/contracts/openzeppelin/ERC7984ERC20Wrapper.sol +43 -0
- package/contracts/openzeppelin/SwapERC7984ToERC20.sol +110 -0
- package/contracts/openzeppelin/SwapERC7984ToERC7984.sol +48 -0
- package/contracts/openzeppelin/VestingWallet.sol +147 -0
- package/contracts/openzeppelin/mocks/ERC20Mock.sol +31 -0
- package/dist/scripts/commands/add-mode.d.ts.map +1 -0
- package/dist/scripts/{add-mode.js → commands/add-mode.js} +27 -61
- package/dist/scripts/commands/doctor.d.ts.map +1 -0
- package/dist/scripts/{doctor.js → commands/doctor.js} +2 -2
- package/dist/scripts/commands/generate-config.d.ts.map +1 -0
- package/dist/scripts/{generate-config.js → commands/generate-config.js} +3 -10
- package/dist/scripts/commands/generate-docs.d.ts.map +1 -0
- package/dist/scripts/{generate-docs.js → commands/generate-docs.js} +4 -3
- package/dist/scripts/commands/maintenance.d.ts.map +1 -0
- package/dist/scripts/{maintenance.js → commands/maintenance.js} +11 -10
- package/dist/scripts/index.js +14 -33
- package/dist/scripts/{builders.d.ts → shared/builders.d.ts} +2 -2
- package/dist/scripts/shared/builders.d.ts.map +1 -0
- package/dist/scripts/{builders.js → shared/builders.js} +49 -30
- package/dist/scripts/{config.d.ts → shared/config.d.ts} +0 -2
- package/dist/scripts/shared/config.d.ts.map +1 -0
- package/dist/scripts/{config.js → shared/config.js} +48 -59
- package/dist/scripts/shared/generators.d.ts +42 -0
- package/dist/scripts/shared/generators.d.ts.map +1 -0
- package/dist/scripts/{utils.js → shared/generators.js} +34 -271
- package/dist/scripts/shared/ui.d.ts.map +1 -0
- package/dist/scripts/{ui.js → shared/ui.js} +3 -2
- package/dist/scripts/{utils.d.ts → shared/utils.d.ts} +4 -27
- package/dist/scripts/shared/utils.d.ts.map +1 -0
- package/dist/scripts/shared/utils.js +228 -0
- package/fhevm-hardhat-template/.eslintignore +26 -0
- package/fhevm-hardhat-template/.eslintrc.yml +21 -0
- package/fhevm-hardhat-template/.github/workflows/main.yml +47 -0
- package/fhevm-hardhat-template/.github/workflows/manual-windows.yml +28 -0
- package/fhevm-hardhat-template/.github/workflows/manual.yml +28 -0
- package/fhevm-hardhat-template/.prettierignore +25 -0
- package/fhevm-hardhat-template/.prettierrc.yml +15 -0
- package/fhevm-hardhat-template/.solcover.js +4 -0
- package/fhevm-hardhat-template/.solhint.json +12 -0
- package/fhevm-hardhat-template/.solhintignore +3 -0
- package/fhevm-hardhat-template/.vscode/extensions.json +3 -0
- package/fhevm-hardhat-template/.vscode/settings.json +9 -0
- package/fhevm-hardhat-template/LICENSE +33 -0
- package/fhevm-hardhat-template/README.md +110 -0
- package/fhevm-hardhat-template/contracts/FHECounter.sol +46 -0
- package/fhevm-hardhat-template/deploy/deploy.ts +17 -0
- package/fhevm-hardhat-template/hardhat.config.ts +90 -0
- package/fhevm-hardhat-template/package-lock.json +10405 -0
- package/fhevm-hardhat-template/package.json +104 -0
- package/fhevm-hardhat-template/tasks/FHECounter.ts +184 -0
- package/fhevm-hardhat-template/tasks/accounts.ts +9 -0
- package/fhevm-hardhat-template/test/FHECounter.ts +104 -0
- package/fhevm-hardhat-template/test/FHECounterSepolia.ts +104 -0
- package/fhevm-hardhat-template/tsconfig.json +23 -0
- package/package.json +11 -8
- package/test/advanced/BlindAuction.ts +246 -0
- package/test/advanced/EncryptedEscrow.ts +295 -0
- package/test/advanced/HiddenVoting.ts +268 -0
- package/test/advanced/PrivateKYC.ts +382 -0
- package/test/advanced/PrivatePayroll.ts +253 -0
- package/test/basic/decryption/PublicDecryptMultipleValues.ts +254 -0
- package/test/basic/decryption/PublicDecryptSingleValue.ts +264 -0
- package/test/basic/decryption/UserDecryptMultipleValues.ts +107 -0
- package/test/basic/decryption/UserDecryptSingleValue.ts +97 -0
- package/test/basic/encryption/EncryptMultipleValues.ts +110 -0
- package/test/basic/encryption/EncryptSingleValue.ts +124 -0
- package/test/basic/encryption/FHECounter.ts +112 -0
- package/test/basic/fhe-operations/FHEAdd.ts +97 -0
- package/test/basic/fhe-operations/FHEArithmetic.ts +161 -0
- package/test/basic/fhe-operations/FHEComparison.ts +167 -0
- package/test/basic/fhe-operations/FHEIfThenElse.ts +97 -0
- package/test/concepts/FHEAccessControl.ts +154 -0
- package/test/concepts/FHEAntiPatterns.ts +111 -0
- package/test/concepts/FHEHandles.ts +156 -0
- package/test/concepts/FHEInputProof.ts +151 -0
- package/test/gaming/EncryptedLottery.ts +214 -0
- package/test/gaming/EncryptedPoker.ts +349 -0
- package/test/gaming/RockPaperScissors.ts +205 -0
- package/test/openzeppelin/ERC7984.ts +142 -0
- package/test/openzeppelin/ERC7984ERC20Wrapper.ts +71 -0
- package/test/openzeppelin/SwapERC7984ToERC20.ts +76 -0
- package/test/openzeppelin/SwapERC7984ToERC7984.ts +113 -0
- package/test/openzeppelin/VestingWallet.ts +89 -0
- package/dist/scripts/add-mode.d.ts.map +0 -1
- package/dist/scripts/builders.d.ts.map +0 -1
- package/dist/scripts/config.d.ts.map +0 -1
- package/dist/scripts/doctor.d.ts.map +0 -1
- package/dist/scripts/generate-config.d.ts.map +0 -1
- package/dist/scripts/generate-docs.d.ts.map +0 -1
- package/dist/scripts/help.d.ts +0 -9
- package/dist/scripts/help.d.ts.map +0 -1
- package/dist/scripts/help.js +0 -73
- package/dist/scripts/maintenance.d.ts.map +0 -1
- package/dist/scripts/ui.d.ts.map +0 -1
- package/dist/scripts/utils.d.ts.map +0 -1
- /package/dist/scripts/{add-mode.d.ts → commands/add-mode.d.ts} +0 -0
- /package/dist/scripts/{doctor.d.ts → commands/doctor.d.ts} +0 -0
- /package/dist/scripts/{generate-config.d.ts → commands/generate-config.d.ts} +0 -0
- /package/dist/scripts/{generate-docs.d.ts → commands/generate-docs.d.ts} +0 -0
- /package/dist/scripts/{maintenance.d.ts → commands/maintenance.d.ts} +0 -0
- /package/dist/scripts/{ui.d.ts → shared/ui.d.ts} +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
|
|
2
|
+
import { ethers, fhevm } from "hardhat";
|
|
3
|
+
import { PrivatePayroll, PrivatePayroll__factory } from "../types";
|
|
4
|
+
import { expect } from "chai";
|
|
5
|
+
|
|
6
|
+
type Signers = {
|
|
7
|
+
employer: HardhatEthersSigner;
|
|
8
|
+
employee1: HardhatEthersSigner;
|
|
9
|
+
employee2: HardhatEthersSigner;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const PAYMENT_PERIOD = 86400; // 1 day for testing
|
|
13
|
+
|
|
14
|
+
async function deployFixture() {
|
|
15
|
+
const factory = (await ethers.getContractFactory(
|
|
16
|
+
"PrivatePayroll"
|
|
17
|
+
)) as PrivatePayroll__factory;
|
|
18
|
+
const payroll = (await factory.deploy(PAYMENT_PERIOD)) as PrivatePayroll;
|
|
19
|
+
const payrollAddress = await payroll.getAddress();
|
|
20
|
+
|
|
21
|
+
return { payroll, payrollAddress };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Private Payroll Tests
|
|
26
|
+
*
|
|
27
|
+
* Tests confidential salary management and FHE-based payment processing.
|
|
28
|
+
*/
|
|
29
|
+
describe("PrivatePayroll", function () {
|
|
30
|
+
let signers: Signers;
|
|
31
|
+
let payroll: PrivatePayroll;
|
|
32
|
+
let payrollAddress: string;
|
|
33
|
+
|
|
34
|
+
before(async function () {
|
|
35
|
+
const ethSigners: HardhatEthersSigner[] = await ethers.getSigners();
|
|
36
|
+
signers = {
|
|
37
|
+
employer: ethSigners[0],
|
|
38
|
+
employee1: ethSigners[1],
|
|
39
|
+
employee2: ethSigners[2],
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
beforeEach(async function () {
|
|
44
|
+
if (!fhevm.isMock) {
|
|
45
|
+
console.warn("This test suite cannot run on Sepolia Testnet");
|
|
46
|
+
this.skip();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
({ payroll, payrollAddress } = await deployFixture());
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("Initialization", function () {
|
|
53
|
+
it("should initialize with correct parameters", async function () {
|
|
54
|
+
expect(await payroll.employer()).to.equal(signers.employer.address);
|
|
55
|
+
expect(await payroll.paymentPeriod()).to.equal(BigInt(PAYMENT_PERIOD));
|
|
56
|
+
expect(await payroll.getEmployeeCount()).to.equal(0n);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should default to 30 days if zero period provided", async function () {
|
|
60
|
+
const factory = await ethers.getContractFactory("PrivatePayroll");
|
|
61
|
+
const payroll30 = await factory.deploy(0);
|
|
62
|
+
expect(await payroll30.paymentPeriod()).to.equal(30n * 24n * 60n * 60n);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("Employee Management", function () {
|
|
67
|
+
it("should add employee with encrypted salary", async function () {
|
|
68
|
+
const salary = 5000n;
|
|
69
|
+
|
|
70
|
+
const encryptedSalary = await fhevm
|
|
71
|
+
.createEncryptedInput(payrollAddress, signers.employer.address)
|
|
72
|
+
.add64(salary)
|
|
73
|
+
.encrypt();
|
|
74
|
+
|
|
75
|
+
await expect(
|
|
76
|
+
payroll.addEmployee(
|
|
77
|
+
signers.employee1.address,
|
|
78
|
+
encryptedSalary.handles[0],
|
|
79
|
+
encryptedSalary.inputProof
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
.to.emit(payroll, "EmployeeAdded")
|
|
83
|
+
.withArgs(signers.employee1.address);
|
|
84
|
+
|
|
85
|
+
expect(await payroll.isEmployee(signers.employee1.address)).to.be.true;
|
|
86
|
+
expect(await payroll.getEmployeeCount()).to.equal(1n);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should prevent adding duplicate employee", async function () {
|
|
90
|
+
const enc1 = await fhevm
|
|
91
|
+
.createEncryptedInput(payrollAddress, signers.employer.address)
|
|
92
|
+
.add64(5000n)
|
|
93
|
+
.encrypt();
|
|
94
|
+
|
|
95
|
+
await payroll.addEmployee(
|
|
96
|
+
signers.employee1.address,
|
|
97
|
+
enc1.handles[0],
|
|
98
|
+
enc1.inputProof
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const enc2 = await fhevm
|
|
102
|
+
.createEncryptedInput(payrollAddress, signers.employer.address)
|
|
103
|
+
.add64(6000n)
|
|
104
|
+
.encrypt();
|
|
105
|
+
|
|
106
|
+
await expect(
|
|
107
|
+
payroll.addEmployee(
|
|
108
|
+
signers.employee1.address,
|
|
109
|
+
enc2.handles[0],
|
|
110
|
+
enc2.inputProof
|
|
111
|
+
)
|
|
112
|
+
).to.be.revertedWith("Already an employee");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should remove employee correctly", async function () {
|
|
116
|
+
const enc = await fhevm
|
|
117
|
+
.createEncryptedInput(payrollAddress, signers.employer.address)
|
|
118
|
+
.add64(5000n)
|
|
119
|
+
.encrypt();
|
|
120
|
+
|
|
121
|
+
await payroll.addEmployee(
|
|
122
|
+
signers.employee1.address,
|
|
123
|
+
enc.handles[0],
|
|
124
|
+
enc.inputProof
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
await expect(payroll.removeEmployee(signers.employee1.address))
|
|
128
|
+
.to.emit(payroll, "EmployeeRemoved")
|
|
129
|
+
.withArgs(signers.employee1.address);
|
|
130
|
+
|
|
131
|
+
expect(await payroll.isEmployee(signers.employee1.address)).to.be.false;
|
|
132
|
+
expect(await payroll.getEmployeeCount()).to.equal(0n);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should update employee salary", async function () {
|
|
136
|
+
// Add employee
|
|
137
|
+
const enc1 = await fhevm
|
|
138
|
+
.createEncryptedInput(payrollAddress, signers.employer.address)
|
|
139
|
+
.add64(5000n)
|
|
140
|
+
.encrypt();
|
|
141
|
+
|
|
142
|
+
await payroll.addEmployee(
|
|
143
|
+
signers.employee1.address,
|
|
144
|
+
enc1.handles[0],
|
|
145
|
+
enc1.inputProof
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Update salary
|
|
149
|
+
const enc2 = await fhevm
|
|
150
|
+
.createEncryptedInput(payrollAddress, signers.employer.address)
|
|
151
|
+
.add64(6000n)
|
|
152
|
+
.encrypt();
|
|
153
|
+
|
|
154
|
+
await expect(
|
|
155
|
+
payroll.updateSalary(
|
|
156
|
+
signers.employee1.address,
|
|
157
|
+
enc2.handles[0],
|
|
158
|
+
enc2.inputProof
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
.to.emit(payroll, "SalaryUpdated")
|
|
162
|
+
.withArgs(signers.employee1.address);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("Funding", function () {
|
|
167
|
+
it("should accept funds from employer", async function () {
|
|
168
|
+
const amount = ethers.parseEther("10");
|
|
169
|
+
|
|
170
|
+
await expect(payroll.fund({ value: amount }))
|
|
171
|
+
.to.emit(payroll, "ContractFunded")
|
|
172
|
+
.withArgs(amount);
|
|
173
|
+
|
|
174
|
+
expect(await payroll.getBalance()).to.equal(amount);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should accept direct ETH transfers", async function () {
|
|
178
|
+
const amount = ethers.parseEther("5");
|
|
179
|
+
|
|
180
|
+
await signers.employer.sendTransaction({
|
|
181
|
+
to: payrollAddress,
|
|
182
|
+
value: amount,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(await payroll.getBalance()).to.equal(amount);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should reject zero funding via fund()", async function () {
|
|
189
|
+
await expect(payroll.fund({ value: 0 })).to.be.revertedWith(
|
|
190
|
+
"Must send funds"
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("Access Control", function () {
|
|
196
|
+
it("should prevent non-employer from adding employees", async function () {
|
|
197
|
+
const enc = await fhevm
|
|
198
|
+
.createEncryptedInput(payrollAddress, signers.employee1.address)
|
|
199
|
+
.add64(5000n)
|
|
200
|
+
.encrypt();
|
|
201
|
+
|
|
202
|
+
await expect(
|
|
203
|
+
payroll
|
|
204
|
+
.connect(signers.employee1)
|
|
205
|
+
.addEmployee(
|
|
206
|
+
signers.employee2.address,
|
|
207
|
+
enc.handles[0],
|
|
208
|
+
enc.inputProof
|
|
209
|
+
)
|
|
210
|
+
).to.be.revertedWith("Only employer");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should allow employee to get their salary handle", async function () {
|
|
214
|
+
const enc = await fhevm
|
|
215
|
+
.createEncryptedInput(payrollAddress, signers.employer.address)
|
|
216
|
+
.add64(5000n)
|
|
217
|
+
.encrypt();
|
|
218
|
+
|
|
219
|
+
await payroll.addEmployee(
|
|
220
|
+
signers.employee1.address,
|
|
221
|
+
enc.handles[0],
|
|
222
|
+
enc.inputProof
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Employee should be able to call getMySalary
|
|
226
|
+
const salaryHandle = await payroll
|
|
227
|
+
.connect(signers.employee1)
|
|
228
|
+
.getMySalary();
|
|
229
|
+
expect(salaryHandle).to.not.equal(0n);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should prevent non-employee from getting salary", async function () {
|
|
233
|
+
await expect(
|
|
234
|
+
payroll.connect(signers.employee1).getMySalary()
|
|
235
|
+
).to.be.revertedWith("Not an employee");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("View Functions", function () {
|
|
240
|
+
it("should return correct payroll info", async function () {
|
|
241
|
+
await payroll.fund({ value: ethers.parseEther("1") });
|
|
242
|
+
|
|
243
|
+
const info = await payroll.getPayrollInfo();
|
|
244
|
+
expect(info.employeeCount).to.equal(0n);
|
|
245
|
+
expect(info.balance).to.equal(ethers.parseEther("1"));
|
|
246
|
+
expect(info.period).to.equal(BigInt(PAYMENT_PERIOD));
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should check payment due status", async function () {
|
|
250
|
+
expect(await payroll.isPaymentDue(signers.employee1.address)).to.be.false;
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
|
|
2
|
+
import type { ClearValueType } from "@zama-fhe/relayer-sdk/node";
|
|
3
|
+
import { expect } from "chai";
|
|
4
|
+
import { ethers as EthersT } from "ethers";
|
|
5
|
+
import { ethers, fhevm } from "hardhat";
|
|
6
|
+
import * as hre from "hardhat";
|
|
7
|
+
|
|
8
|
+
import { HighestDieRoll, HighestDieRoll__factory } from "../types";
|
|
9
|
+
import { Signers } from "./types";
|
|
10
|
+
|
|
11
|
+
async function deployFixture() {
|
|
12
|
+
// Contracts are deployed using the first signer/account by default
|
|
13
|
+
const factory = (await ethers.getContractFactory(
|
|
14
|
+
"HighestDieRoll"
|
|
15
|
+
)) as HighestDieRoll__factory;
|
|
16
|
+
const highestDiceRoll = (await factory.deploy()) as HighestDieRoll;
|
|
17
|
+
const highestDiceRoll_address = await highestDiceRoll.getAddress();
|
|
18
|
+
|
|
19
|
+
return { highestDiceRoll, highestDiceRoll_address };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("HighestDieRoll", function () {
|
|
23
|
+
let contract: HighestDieRoll;
|
|
24
|
+
let contractAddress: string;
|
|
25
|
+
let signers: Signers;
|
|
26
|
+
let playerA: HardhatEthersSigner;
|
|
27
|
+
let playerB: HardhatEthersSigner;
|
|
28
|
+
|
|
29
|
+
before(async function () {
|
|
30
|
+
// Check whether the tests are running against an FHEVM mock environment
|
|
31
|
+
if (!hre.fhevm.isMock) {
|
|
32
|
+
throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ethSigners: HardhatEthersSigner[] = await ethers.getSigners();
|
|
36
|
+
signers = {
|
|
37
|
+
owner: ethSigners[0],
|
|
38
|
+
alice: ethSigners[1],
|
|
39
|
+
bob: ethSigners[2],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
playerA = signers.alice;
|
|
43
|
+
playerB = signers.bob;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
beforeEach(async function () {
|
|
47
|
+
// Deploy a new contract each time we run a new test
|
|
48
|
+
const deployment = await deployFixture();
|
|
49
|
+
contractAddress = deployment.highestDiceRoll_address;
|
|
50
|
+
contract = deployment.highestDiceRoll;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Helper: Parses the GameCreated event from a transaction receipt.
|
|
55
|
+
* WARNING: This function is for illustrative purposes only and is not production-ready
|
|
56
|
+
* (it does not handle several events in same tx).
|
|
57
|
+
*/
|
|
58
|
+
function parseGameCreatedEvent(
|
|
59
|
+
txReceipt: EthersT.ContractTransactionReceipt | null
|
|
60
|
+
): {
|
|
61
|
+
txHash: `0x${string}`;
|
|
62
|
+
gameId: number;
|
|
63
|
+
playerA: `0x${string}`;
|
|
64
|
+
playerB: `0x${string}`;
|
|
65
|
+
playerAEncryptedDiceRoll: `0x${string}`;
|
|
66
|
+
playerBEncryptedDiceRoll: `0x${string}`;
|
|
67
|
+
} {
|
|
68
|
+
const gameCreatedEvents: Array<{
|
|
69
|
+
txHash: `0x${string}`;
|
|
70
|
+
gameId: number;
|
|
71
|
+
playerA: `0x${string}`;
|
|
72
|
+
playerB: `0x${string}`;
|
|
73
|
+
playerAEncryptedDiceRoll: `0x${string}`;
|
|
74
|
+
playerBEncryptedDiceRoll: `0x${string}`;
|
|
75
|
+
}> = [];
|
|
76
|
+
|
|
77
|
+
if (txReceipt) {
|
|
78
|
+
const logs = Array.isArray(txReceipt.logs)
|
|
79
|
+
? txReceipt.logs
|
|
80
|
+
: [txReceipt.logs];
|
|
81
|
+
for (let i = 0; i < logs.length; ++i) {
|
|
82
|
+
const parsedLog = contract.interface.parseLog(logs[i]);
|
|
83
|
+
if (!parsedLog || parsedLog.name !== "GameCreated") {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const ge = {
|
|
87
|
+
txHash: txReceipt.hash as `0x${string}`,
|
|
88
|
+
gameId: Number(parsedLog.args[0]),
|
|
89
|
+
playerA: parsedLog.args[1],
|
|
90
|
+
playerB: parsedLog.args[2],
|
|
91
|
+
playerAEncryptedDiceRoll: parsedLog.args[3],
|
|
92
|
+
playerBEncryptedDiceRoll: parsedLog.args[4],
|
|
93
|
+
};
|
|
94
|
+
gameCreatedEvents.push(ge);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// In this example, we expect on one single GameCreated event
|
|
99
|
+
expect(gameCreatedEvents.length).to.eq(1);
|
|
100
|
+
|
|
101
|
+
return gameCreatedEvents[0];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ✅ Test should succeed
|
|
105
|
+
it("decryption should succeed", async function () {
|
|
106
|
+
console.log(``);
|
|
107
|
+
console.log(`🎲 HighestDieRoll Game contract address: ${contractAddress}`);
|
|
108
|
+
console.log(` 🤖 playerA.address: ${playerA.address}`);
|
|
109
|
+
console.log(` 🎃 playerB.address: ${playerB.address}`);
|
|
110
|
+
console.log(``);
|
|
111
|
+
|
|
112
|
+
// Starts a new Heads or Tails game. This will emit a `GameCreated` event
|
|
113
|
+
const tx = await contract
|
|
114
|
+
.connect(signers.owner)
|
|
115
|
+
.highestDieRoll(playerA, playerB);
|
|
116
|
+
|
|
117
|
+
// Parse the `GameCreated` event
|
|
118
|
+
const gameCreatedEvent = parseGameCreatedEvent(await tx.wait())!;
|
|
119
|
+
|
|
120
|
+
// GameId is 1 since we are playing the first game
|
|
121
|
+
expect(gameCreatedEvent.gameId).to.eq(1);
|
|
122
|
+
expect(gameCreatedEvent.playerA).to.eq(playerA.address);
|
|
123
|
+
expect(gameCreatedEvent.playerB).to.eq(playerB.address);
|
|
124
|
+
expect(await contract.getGamesCount()).to.eq(1);
|
|
125
|
+
|
|
126
|
+
console.log(`✅ New game #${gameCreatedEvent.gameId} created!`);
|
|
127
|
+
console.log(JSON.stringify(gameCreatedEvent, null, 2));
|
|
128
|
+
|
|
129
|
+
const gameId = gameCreatedEvent.gameId;
|
|
130
|
+
const playerADiceRoll = gameCreatedEvent.playerAEncryptedDiceRoll;
|
|
131
|
+
const playerBDiceRoll = gameCreatedEvent.playerBEncryptedDiceRoll;
|
|
132
|
+
|
|
133
|
+
// Call the Zama Relayer to compute the decryption
|
|
134
|
+
const publicDecryptResults = await fhevm.publicDecrypt([
|
|
135
|
+
playerADiceRoll,
|
|
136
|
+
playerBDiceRoll,
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
// The Relayer returns a `PublicDecryptResults` object containing:
|
|
140
|
+
// - the ORDERED clear values (here we have only one single value)
|
|
141
|
+
// - the ORDERED clear values in ABI-encoded form
|
|
142
|
+
// - the KMS decryption proof associated with the ORDERED clear values in ABI-encoded form
|
|
143
|
+
const abiEncodedClearGameResult =
|
|
144
|
+
publicDecryptResults.abiEncodedClearValues;
|
|
145
|
+
const decryptionProof = publicDecryptResults.decryptionProof;
|
|
146
|
+
|
|
147
|
+
const clearValueA: ClearValueType =
|
|
148
|
+
publicDecryptResults.clearValues[playerADiceRoll];
|
|
149
|
+
const clearValueB: ClearValueType =
|
|
150
|
+
publicDecryptResults.clearValues[playerBDiceRoll];
|
|
151
|
+
|
|
152
|
+
expect(typeof clearValueA).to.eq("bigint");
|
|
153
|
+
expect(typeof clearValueB).to.eq("bigint");
|
|
154
|
+
|
|
155
|
+
// playerA's 8-sided die roll result (between 1 and 8)
|
|
156
|
+
const a = (Number(clearValueA) % 8) + 1;
|
|
157
|
+
// playerB's 8-sided die roll result (between 1 and 8)
|
|
158
|
+
const b = (Number(clearValueB) % 8) + 1;
|
|
159
|
+
|
|
160
|
+
const isDraw = a === b;
|
|
161
|
+
const playerAWon = a > b;
|
|
162
|
+
const playerBWon = a < b;
|
|
163
|
+
|
|
164
|
+
console.log(``);
|
|
165
|
+
console.log(`🎲 playerA's 8-sided die roll is ${a}`);
|
|
166
|
+
console.log(`🎲 playerB's 8-sided die roll is ${b}`);
|
|
167
|
+
|
|
168
|
+
// Let's forward the `PublicDecryptResults` content to the on-chain contract whose job
|
|
169
|
+
// will simply be to verify the proof and store the final winner of the game
|
|
170
|
+
await contract.recordAndVerifyWinner(
|
|
171
|
+
gameId,
|
|
172
|
+
abiEncodedClearGameResult,
|
|
173
|
+
decryptionProof
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const isRevealed = await contract.isGameRevealed(gameId);
|
|
177
|
+
const winner = await contract.getWinner(gameId);
|
|
178
|
+
|
|
179
|
+
expect(isRevealed).to.eq(true);
|
|
180
|
+
expect(
|
|
181
|
+
winner === playerA.address ||
|
|
182
|
+
winner === playerB.address ||
|
|
183
|
+
winner === EthersT.ZeroAddress
|
|
184
|
+
).to.eq(true);
|
|
185
|
+
|
|
186
|
+
expect(isDraw).to.eq(winner === EthersT.ZeroAddress);
|
|
187
|
+
expect(playerAWon).to.eq(winner === playerA.address);
|
|
188
|
+
expect(playerBWon).to.eq(winner === playerB.address);
|
|
189
|
+
|
|
190
|
+
console.log(``);
|
|
191
|
+
if (winner === playerA.address) {
|
|
192
|
+
console.log(`🤖 playerA is the winner 🥇🥇`);
|
|
193
|
+
} else if (winner === playerB.address) {
|
|
194
|
+
console.log(`🎃 playerB is the winner 🥇🥇`);
|
|
195
|
+
} else if (winner === EthersT.ZeroAddress) {
|
|
196
|
+
console.log(`Game is a draw!`);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ❌ Test should fail because clear values are ABI-encoded in the wrong order.
|
|
201
|
+
it("decryption should fail when ABI-encoding is wrongly ordered", async function () {
|
|
202
|
+
// Test Case: Verify strict ordering is enforced for cryptographic proof generation.
|
|
203
|
+
// The `decryptionProof` is generated based on the expected order (A, B). By ABI-encoding
|
|
204
|
+
// the clear values in the **reverse order** (B, A), we create a mismatch when the contract
|
|
205
|
+
// internally verifies the proof (e.g., checks a signature against a newly computed hash).
|
|
206
|
+
// This intentional failure is expected to revert with the `KMSInvalidSigner` error,
|
|
207
|
+
// confirming the proof's order dependency.
|
|
208
|
+
const tx = await contract
|
|
209
|
+
.connect(signers.owner)
|
|
210
|
+
.highestDieRoll(playerA, playerB);
|
|
211
|
+
const gameCreatedEvent = parseGameCreatedEvent(await tx.wait())!;
|
|
212
|
+
const gameId = gameCreatedEvent.gameId;
|
|
213
|
+
const playerADiceRoll = gameCreatedEvent.playerAEncryptedDiceRoll;
|
|
214
|
+
const playerBDiceRoll = gameCreatedEvent.playerBEncryptedDiceRoll;
|
|
215
|
+
// Call `fhevm.publicDecrypt` using order (A, B)
|
|
216
|
+
const publicDecryptResults = await fhevm.publicDecrypt([
|
|
217
|
+
playerADiceRoll,
|
|
218
|
+
playerBDiceRoll,
|
|
219
|
+
]);
|
|
220
|
+
const clearValueA: ClearValueType =
|
|
221
|
+
publicDecryptResults.clearValues[playerADiceRoll];
|
|
222
|
+
const clearValueB: ClearValueType =
|
|
223
|
+
publicDecryptResults.clearValues[playerBDiceRoll];
|
|
224
|
+
const decryptionProof = publicDecryptResults.decryptionProof;
|
|
225
|
+
expect(typeof clearValueA).to.eq("bigint");
|
|
226
|
+
expect(typeof clearValueB).to.eq("bigint");
|
|
227
|
+
expect(
|
|
228
|
+
ethers.AbiCoder.defaultAbiCoder().encode(
|
|
229
|
+
["uint256", "uint256"],
|
|
230
|
+
[clearValueA, clearValueB]
|
|
231
|
+
)
|
|
232
|
+
).to.eq(publicDecryptResults.abiEncodedClearValues);
|
|
233
|
+
const wrongOrderBAInsteadOfABAbiEncodedValues =
|
|
234
|
+
ethers.AbiCoder.defaultAbiCoder().encode(
|
|
235
|
+
["uint256", "uint256"],
|
|
236
|
+
[clearValueB, clearValueA]
|
|
237
|
+
);
|
|
238
|
+
// ❌ Call `contract.recordAndVerifyWinner` using order (B, A)
|
|
239
|
+
await expect(
|
|
240
|
+
contract.recordAndVerifyWinner(
|
|
241
|
+
gameId,
|
|
242
|
+
wrongOrderBAInsteadOfABAbiEncodedValues,
|
|
243
|
+
decryptionProof
|
|
244
|
+
)
|
|
245
|
+
).to.be.revertedWithCustomError(
|
|
246
|
+
{
|
|
247
|
+
interface: new EthersT.Interface([
|
|
248
|
+
"error KMSInvalidSigner(address invalidSigner)",
|
|
249
|
+
]),
|
|
250
|
+
},
|
|
251
|
+
"KMSInvalidSigner"
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
});
|