create-fhevm-example 1.3.1 → 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 +63 -59
- 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 +13 -10
- 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/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,246 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { ethers, fhevm } from "hardhat";
|
|
3
|
+
import * as hre from "hardhat";
|
|
4
|
+
|
|
5
|
+
describe("BlindAuction", function () {
|
|
6
|
+
let auction: any;
|
|
7
|
+
let owner: any;
|
|
8
|
+
let bidder1: any;
|
|
9
|
+
let bidder2: any;
|
|
10
|
+
let bidder3: any;
|
|
11
|
+
|
|
12
|
+
const MINIMUM_BID = 100n;
|
|
13
|
+
|
|
14
|
+
async function deployAuction(durationSeconds: number = 60) {
|
|
15
|
+
// Get blockchain timestamp instead of Date.now()
|
|
16
|
+
const latestBlock = await ethers.provider.getBlock("latest");
|
|
17
|
+
const endTime =
|
|
18
|
+
(latestBlock?.timestamp || Math.floor(Date.now() / 1000)) +
|
|
19
|
+
durationSeconds;
|
|
20
|
+
const auctionContract = await ethers.deployContract("BlindAuction", [
|
|
21
|
+
endTime,
|
|
22
|
+
MINIMUM_BID,
|
|
23
|
+
]);
|
|
24
|
+
return { auction: auctionContract, endTime };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeEach(async function () {
|
|
28
|
+
[owner, bidder1, bidder2, bidder3] = await ethers.getSigners();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("Auction Creation", function () {
|
|
32
|
+
it("should create auction with correct parameters", async function () {
|
|
33
|
+
const { auction: auc } = await deployAuction(120);
|
|
34
|
+
|
|
35
|
+
expect(await auc.auctionState()).to.equal(0); // Open
|
|
36
|
+
expect(await auc.minimumBid()).to.equal(MINIMUM_BID);
|
|
37
|
+
expect(await auc.getBidderCount()).to.equal(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should revert if end time is in the past", async function () {
|
|
41
|
+
const pastTime = Math.floor(Date.now() / 1000) - 100;
|
|
42
|
+
await expect(
|
|
43
|
+
ethers.deployContract("BlindAuction", [pastTime, MINIMUM_BID])
|
|
44
|
+
).to.be.revertedWith("End time must be in future");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("Bidding", function () {
|
|
49
|
+
beforeEach(async function () {
|
|
50
|
+
const deployment = await deployAuction(3600); // 1 hour
|
|
51
|
+
auction = deployment.auction;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should accept encrypted bid", async function () {
|
|
55
|
+
const encryptedBid = await fhevm
|
|
56
|
+
.createEncryptedInput(await auction.getAddress(), bidder1.address)
|
|
57
|
+
.add64(500)
|
|
58
|
+
.encrypt();
|
|
59
|
+
|
|
60
|
+
await expect(
|
|
61
|
+
auction
|
|
62
|
+
.connect(bidder1)
|
|
63
|
+
.bid(encryptedBid.handles[0], encryptedBid.inputProof)
|
|
64
|
+
).to.not.be.reverted;
|
|
65
|
+
|
|
66
|
+
expect(await auction.hasBid(bidder1.address)).to.be.true;
|
|
67
|
+
expect(await auction.getBidderCount()).to.equal(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should reject second bid from same address", async function () {
|
|
71
|
+
const encryptedBid1 = await fhevm
|
|
72
|
+
.createEncryptedInput(await auction.getAddress(), bidder1.address)
|
|
73
|
+
.add64(500)
|
|
74
|
+
.encrypt();
|
|
75
|
+
|
|
76
|
+
await auction
|
|
77
|
+
.connect(bidder1)
|
|
78
|
+
.bid(encryptedBid1.handles[0], encryptedBid1.inputProof);
|
|
79
|
+
|
|
80
|
+
const encryptedBid2 = await fhevm
|
|
81
|
+
.createEncryptedInput(await auction.getAddress(), bidder1.address)
|
|
82
|
+
.add64(600)
|
|
83
|
+
.encrypt();
|
|
84
|
+
|
|
85
|
+
await expect(
|
|
86
|
+
auction
|
|
87
|
+
.connect(bidder1)
|
|
88
|
+
.bid(encryptedBid2.handles[0], encryptedBid2.inputProof)
|
|
89
|
+
).to.be.revertedWith("Already placed a bid");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should accept bids from multiple bidders", async function () {
|
|
93
|
+
// Bidder 1
|
|
94
|
+
const enc1 = await fhevm
|
|
95
|
+
.createEncryptedInput(await auction.getAddress(), bidder1.address)
|
|
96
|
+
.add64(200)
|
|
97
|
+
.encrypt();
|
|
98
|
+
await auction.connect(bidder1).bid(enc1.handles[0], enc1.inputProof);
|
|
99
|
+
|
|
100
|
+
// Bidder 2
|
|
101
|
+
const enc2 = await fhevm
|
|
102
|
+
.createEncryptedInput(await auction.getAddress(), bidder2.address)
|
|
103
|
+
.add64(300)
|
|
104
|
+
.encrypt();
|
|
105
|
+
await auction.connect(bidder2).bid(enc2.handles[0], enc2.inputProof);
|
|
106
|
+
|
|
107
|
+
// Bidder 3
|
|
108
|
+
const enc3 = await fhevm
|
|
109
|
+
.createEncryptedInput(await auction.getAddress(), bidder3.address)
|
|
110
|
+
.add64(150)
|
|
111
|
+
.encrypt();
|
|
112
|
+
await auction.connect(bidder3).bid(enc3.handles[0], enc3.inputProof);
|
|
113
|
+
|
|
114
|
+
expect(await auction.getBidderCount()).to.equal(3);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("End Auction", function () {
|
|
119
|
+
beforeEach(async function () {
|
|
120
|
+
// Deploy with 1 hour duration to avoid timing issues from evm_increaseTime in other tests
|
|
121
|
+
const deployment = await deployAuction(3600);
|
|
122
|
+
auction = deployment.auction;
|
|
123
|
+
|
|
124
|
+
// Place bids before it ends
|
|
125
|
+
const enc1 = await fhevm
|
|
126
|
+
.createEncryptedInput(await auction.getAddress(), bidder1.address)
|
|
127
|
+
.add64(200)
|
|
128
|
+
.encrypt();
|
|
129
|
+
await auction.connect(bidder1).bid(enc1.handles[0], enc1.inputProof);
|
|
130
|
+
|
|
131
|
+
const enc2 = await fhevm
|
|
132
|
+
.createEncryptedInput(await auction.getAddress(), bidder2.address)
|
|
133
|
+
.add64(500)
|
|
134
|
+
.encrypt();
|
|
135
|
+
await auction.connect(bidder2).bid(enc2.handles[0], enc2.inputProof);
|
|
136
|
+
|
|
137
|
+
const enc3 = await fhevm
|
|
138
|
+
.createEncryptedInput(await auction.getAddress(), bidder3.address)
|
|
139
|
+
.add64(300)
|
|
140
|
+
.encrypt();
|
|
141
|
+
await auction.connect(bidder3).bid(enc3.handles[0], enc3.inputProof);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should not allow ending before time", async function () {
|
|
145
|
+
await expect(auction.connect(owner).endAuction()).to.be.revertedWith(
|
|
146
|
+
"Auction not yet ended"
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should allow owner to end after time passes", async function () {
|
|
151
|
+
// Fast forward time past auction end
|
|
152
|
+
await hre.network.provider.send("evm_increaseTime", [3601]);
|
|
153
|
+
await hre.network.provider.send("evm_mine");
|
|
154
|
+
|
|
155
|
+
await expect(auction.connect(owner).endAuction()).to.not.be.reverted;
|
|
156
|
+
expect(await auction.auctionState()).to.equal(1); // Closed
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should not allow non-owner to end auction", async function () {
|
|
160
|
+
await hre.network.provider.send("evm_increaseTime", [3601]);
|
|
161
|
+
await hre.network.provider.send("evm_mine");
|
|
162
|
+
|
|
163
|
+
await expect(auction.connect(bidder1).endAuction()).to.be.revertedWith(
|
|
164
|
+
"Only owner can call"
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("Full Auction Flow", function () {
|
|
170
|
+
it("should correctly identify highest bidder", async function () {
|
|
171
|
+
// Check if running in mock mode
|
|
172
|
+
if (!hre.fhevm.isMock) {
|
|
173
|
+
console.log("Skipping: Test requires mock environment");
|
|
174
|
+
this.skip();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Deploy with short duration - use blockchain timestamp
|
|
178
|
+
const latestBlock = await ethers.provider.getBlock("latest");
|
|
179
|
+
const endTime = (latestBlock?.timestamp || 0) + 60; // 60 seconds from now
|
|
180
|
+
auction = await ethers.deployContract("BlindAuction", [
|
|
181
|
+
endTime,
|
|
182
|
+
MINIMUM_BID,
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
// Place bids: bidder2 has highest (500)
|
|
186
|
+
const enc1 = await fhevm
|
|
187
|
+
.createEncryptedInput(await auction.getAddress(), bidder1.address)
|
|
188
|
+
.add64(200)
|
|
189
|
+
.encrypt();
|
|
190
|
+
await auction.connect(bidder1).bid(enc1.handles[0], enc1.inputProof);
|
|
191
|
+
|
|
192
|
+
const enc2 = await fhevm
|
|
193
|
+
.createEncryptedInput(await auction.getAddress(), bidder2.address)
|
|
194
|
+
.add64(500) // 🏆 Highest bid
|
|
195
|
+
.encrypt();
|
|
196
|
+
await auction.connect(bidder2).bid(enc2.handles[0], enc2.inputProof);
|
|
197
|
+
|
|
198
|
+
const enc3 = await fhevm
|
|
199
|
+
.createEncryptedInput(await auction.getAddress(), bidder3.address)
|
|
200
|
+
.add64(300)
|
|
201
|
+
.encrypt();
|
|
202
|
+
await auction.connect(bidder3).bid(enc3.handles[0], enc3.inputProof);
|
|
203
|
+
|
|
204
|
+
// Fast forward time using evm commands
|
|
205
|
+
await hre.network.provider.send("evm_increaseTime", [65]); // 65 seconds
|
|
206
|
+
await hre.network.provider.send("evm_mine");
|
|
207
|
+
|
|
208
|
+
const endTx = await auction.connect(owner).endAuction();
|
|
209
|
+
const endReceipt = await endTx.wait();
|
|
210
|
+
|
|
211
|
+
// Parse AuctionEnded event to get encrypted handles
|
|
212
|
+
const auctionEndedEvent = endReceipt.logs.find((log: any) => {
|
|
213
|
+
try {
|
|
214
|
+
const parsed = auction.interface.parseLog(log);
|
|
215
|
+
return parsed?.name === "AuctionEnded";
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(auctionEndedEvent).to.not.be.undefined;
|
|
222
|
+
|
|
223
|
+
// Get encrypted handles from contract
|
|
224
|
+
const encWinningBid = await auction.getEncryptedWinningBid();
|
|
225
|
+
const encWinnerIndex = await auction.getEncryptedWinnerIndex();
|
|
226
|
+
|
|
227
|
+
// Request public decryption
|
|
228
|
+
const handles = [encWinningBid, encWinnerIndex];
|
|
229
|
+
const decryptResults = await fhevm.publicDecrypt(handles);
|
|
230
|
+
|
|
231
|
+
// Reveal winner
|
|
232
|
+
await auction.revealWinner(
|
|
233
|
+
decryptResults.abiEncodedClearValues,
|
|
234
|
+
decryptResults.decryptionProof
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Verify results
|
|
238
|
+
expect(await auction.auctionState()).to.equal(2); // Revealed
|
|
239
|
+
expect(await auction.winner()).to.equal(bidder2.address);
|
|
240
|
+
expect(await auction.winningAmount()).to.equal(500n);
|
|
241
|
+
|
|
242
|
+
console.log(`🏆 Winner: ${await auction.winner()}`);
|
|
243
|
+
console.log(`💰 Winning Amount: ${await auction.winningAmount()}`);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
|
|
2
|
+
import { ethers, fhevm } from "hardhat";
|
|
3
|
+
import { EncryptedEscrow, EncryptedEscrow__factory } from "../types";
|
|
4
|
+
import { expect } from "chai";
|
|
5
|
+
|
|
6
|
+
type Signers = {
|
|
7
|
+
buyer: HardhatEthersSigner;
|
|
8
|
+
seller: HardhatEthersSigner;
|
|
9
|
+
arbiter: HardhatEthersSigner;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const ARBITER_FEE = 1; // 1%
|
|
13
|
+
const DEADLINE_OFFSET = 86400; // 1 day
|
|
14
|
+
|
|
15
|
+
async function deployFixture() {
|
|
16
|
+
const factory = (await ethers.getContractFactory(
|
|
17
|
+
"EncryptedEscrow"
|
|
18
|
+
)) as EncryptedEscrow__factory;
|
|
19
|
+
const escrow = (await factory.deploy(ARBITER_FEE)) as EncryptedEscrow;
|
|
20
|
+
const escrowAddress = await escrow.getAddress();
|
|
21
|
+
|
|
22
|
+
return { escrow, escrowAddress };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Encrypted Escrow Tests
|
|
27
|
+
*
|
|
28
|
+
* Tests secure escrow with encrypted amounts and dispute resolution.
|
|
29
|
+
*/
|
|
30
|
+
describe("EncryptedEscrow", function () {
|
|
31
|
+
let signers: Signers;
|
|
32
|
+
let escrow: EncryptedEscrow;
|
|
33
|
+
let escrowAddress: string;
|
|
34
|
+
|
|
35
|
+
before(async function () {
|
|
36
|
+
const ethSigners: HardhatEthersSigner[] = await ethers.getSigners();
|
|
37
|
+
signers = {
|
|
38
|
+
buyer: ethSigners[0],
|
|
39
|
+
seller: ethSigners[1],
|
|
40
|
+
arbiter: ethSigners[2],
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
beforeEach(async function () {
|
|
45
|
+
if (!fhevm.isMock) {
|
|
46
|
+
console.warn("This test suite cannot run on Sepolia Testnet");
|
|
47
|
+
this.skip();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
({ escrow, escrowAddress } = await deployFixture());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("Initialization", function () {
|
|
54
|
+
it("should initialize with correct parameters", async function () {
|
|
55
|
+
expect(await escrow.owner()).to.equal(signers.buyer.address);
|
|
56
|
+
expect(await escrow.arbiterFeePercent()).to.equal(BigInt(ARBITER_FEE));
|
|
57
|
+
expect(await escrow.escrowCount()).to.equal(0n);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should reject fee percentage above 10%", async function () {
|
|
61
|
+
const factory = await ethers.getContractFactory("EncryptedEscrow");
|
|
62
|
+
await expect(factory.deploy(15)).to.be.revertedWith("Fee too high");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("Escrow Creation", function () {
|
|
67
|
+
it("should create escrow with encrypted amount", async function () {
|
|
68
|
+
const amount = ethers.parseEther("1");
|
|
69
|
+
const block = await ethers.provider.getBlock("latest");
|
|
70
|
+
const deadline = (block?.timestamp || 0) + DEADLINE_OFFSET;
|
|
71
|
+
|
|
72
|
+
const encryptedAmount = await fhevm
|
|
73
|
+
.createEncryptedInput(escrowAddress, signers.buyer.address)
|
|
74
|
+
.add64(amount)
|
|
75
|
+
.encrypt();
|
|
76
|
+
|
|
77
|
+
await expect(
|
|
78
|
+
escrow.createEscrow(
|
|
79
|
+
signers.seller.address,
|
|
80
|
+
signers.arbiter.address,
|
|
81
|
+
encryptedAmount.handles[0],
|
|
82
|
+
encryptedAmount.inputProof,
|
|
83
|
+
deadline
|
|
84
|
+
)
|
|
85
|
+
).to.emit(escrow, "EscrowCreated");
|
|
86
|
+
|
|
87
|
+
expect(await escrow.escrowCount()).to.equal(1n);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should reject invalid seller address", async function () {
|
|
91
|
+
const block = await ethers.provider.getBlock("latest");
|
|
92
|
+
const deadline = (block?.timestamp || 0) + DEADLINE_OFFSET;
|
|
93
|
+
|
|
94
|
+
const enc = await fhevm
|
|
95
|
+
.createEncryptedInput(escrowAddress, signers.buyer.address)
|
|
96
|
+
.add64(1000n)
|
|
97
|
+
.encrypt();
|
|
98
|
+
|
|
99
|
+
await expect(
|
|
100
|
+
escrow.createEscrow(
|
|
101
|
+
ethers.ZeroAddress,
|
|
102
|
+
signers.arbiter.address,
|
|
103
|
+
enc.handles[0],
|
|
104
|
+
enc.inputProof,
|
|
105
|
+
deadline
|
|
106
|
+
)
|
|
107
|
+
).to.be.revertedWith("Invalid seller");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should reject buyer as seller", async function () {
|
|
111
|
+
const block = await ethers.provider.getBlock("latest");
|
|
112
|
+
const deadline = (block?.timestamp || 0) + DEADLINE_OFFSET;
|
|
113
|
+
|
|
114
|
+
const enc = await fhevm
|
|
115
|
+
.createEncryptedInput(escrowAddress, signers.buyer.address)
|
|
116
|
+
.add64(1000n)
|
|
117
|
+
.encrypt();
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
escrow.createEscrow(
|
|
121
|
+
signers.buyer.address,
|
|
122
|
+
signers.arbiter.address,
|
|
123
|
+
enc.handles[0],
|
|
124
|
+
enc.inputProof,
|
|
125
|
+
deadline
|
|
126
|
+
)
|
|
127
|
+
).to.be.revertedWith("Buyer cannot be seller");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("Funding", function () {
|
|
132
|
+
let escrowId: bigint;
|
|
133
|
+
|
|
134
|
+
beforeEach(async function () {
|
|
135
|
+
const block = await ethers.provider.getBlock("latest");
|
|
136
|
+
const deadline = (block?.timestamp || 0) + DEADLINE_OFFSET;
|
|
137
|
+
|
|
138
|
+
const enc = await fhevm
|
|
139
|
+
.createEncryptedInput(escrowAddress, signers.buyer.address)
|
|
140
|
+
.add64(ethers.parseEther("1"))
|
|
141
|
+
.encrypt();
|
|
142
|
+
|
|
143
|
+
await escrow.createEscrow(
|
|
144
|
+
signers.seller.address,
|
|
145
|
+
signers.arbiter.address,
|
|
146
|
+
enc.handles[0],
|
|
147
|
+
enc.inputProof,
|
|
148
|
+
deadline
|
|
149
|
+
);
|
|
150
|
+
escrowId = 1n;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should allow buyer to fund escrow", async function () {
|
|
154
|
+
const amount = ethers.parseEther("1");
|
|
155
|
+
|
|
156
|
+
await expect(escrow.fundEscrow(escrowId, { value: amount }))
|
|
157
|
+
.to.emit(escrow, "EscrowFunded")
|
|
158
|
+
.withArgs(escrowId, amount);
|
|
159
|
+
|
|
160
|
+
const info = await escrow.getEscrow(escrowId);
|
|
161
|
+
expect(info.depositedAmount).to.equal(amount);
|
|
162
|
+
expect(info.state).to.equal(1); // Funded
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should prevent non-buyer from funding", async function () {
|
|
166
|
+
await expect(
|
|
167
|
+
escrow
|
|
168
|
+
.connect(signers.seller)
|
|
169
|
+
.fundEscrow(escrowId, { value: ethers.parseEther("1") })
|
|
170
|
+
).to.be.revertedWith("Only buyer can fund");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("Release and Refund", function () {
|
|
175
|
+
let escrowId: bigint;
|
|
176
|
+
const DEPOSIT = ethers.parseEther("1");
|
|
177
|
+
|
|
178
|
+
beforeEach(async function () {
|
|
179
|
+
const block = await ethers.provider.getBlock("latest");
|
|
180
|
+
const deadline = (block?.timestamp || 0) + DEADLINE_OFFSET;
|
|
181
|
+
|
|
182
|
+
const enc = await fhevm
|
|
183
|
+
.createEncryptedInput(escrowAddress, signers.buyer.address)
|
|
184
|
+
.add64(DEPOSIT)
|
|
185
|
+
.encrypt();
|
|
186
|
+
|
|
187
|
+
await escrow.createEscrow(
|
|
188
|
+
signers.seller.address,
|
|
189
|
+
signers.arbiter.address,
|
|
190
|
+
enc.handles[0],
|
|
191
|
+
enc.inputProof,
|
|
192
|
+
deadline
|
|
193
|
+
);
|
|
194
|
+
escrowId = 1n;
|
|
195
|
+
await escrow.fundEscrow(escrowId, { value: DEPOSIT });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should allow buyer to release funds", async function () {
|
|
199
|
+
const sellerBalanceBefore = await ethers.provider.getBalance(
|
|
200
|
+
signers.seller.address
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
await expect(escrow.release(escrowId))
|
|
204
|
+
.to.emit(escrow, "FundsReleased")
|
|
205
|
+
.withArgs(escrowId, signers.seller.address);
|
|
206
|
+
|
|
207
|
+
const sellerBalanceAfter = await ethers.provider.getBalance(
|
|
208
|
+
signers.seller.address
|
|
209
|
+
);
|
|
210
|
+
expect(sellerBalanceAfter - sellerBalanceBefore).to.equal(DEPOSIT);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should prevent non-buyer from releasing", async function () {
|
|
214
|
+
await expect(
|
|
215
|
+
escrow.connect(signers.seller).release(escrowId)
|
|
216
|
+
).to.be.revertedWith("Only buyer can release");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should prevent refund before deadline", async function () {
|
|
220
|
+
await expect(escrow.requestRefund(escrowId)).to.be.revertedWith(
|
|
221
|
+
"Deadline not passed"
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("Dispute Resolution", function () {
|
|
227
|
+
let escrowId: bigint;
|
|
228
|
+
const DEPOSIT = ethers.parseEther("1");
|
|
229
|
+
|
|
230
|
+
beforeEach(async function () {
|
|
231
|
+
const block = await ethers.provider.getBlock("latest");
|
|
232
|
+
const deadline = (block?.timestamp || 0) + DEADLINE_OFFSET;
|
|
233
|
+
|
|
234
|
+
const enc = await fhevm
|
|
235
|
+
.createEncryptedInput(escrowAddress, signers.buyer.address)
|
|
236
|
+
.add64(DEPOSIT)
|
|
237
|
+
.encrypt();
|
|
238
|
+
|
|
239
|
+
await escrow.createEscrow(
|
|
240
|
+
signers.seller.address,
|
|
241
|
+
signers.arbiter.address,
|
|
242
|
+
enc.handles[0],
|
|
243
|
+
enc.inputProof,
|
|
244
|
+
deadline
|
|
245
|
+
);
|
|
246
|
+
escrowId = 1n;
|
|
247
|
+
await escrow.fundEscrow(escrowId, { value: DEPOSIT });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should allow buyer to raise dispute", async function () {
|
|
251
|
+
await expect(escrow.raiseDispute(escrowId))
|
|
252
|
+
.to.emit(escrow, "DisputeRaised")
|
|
253
|
+
.withArgs(escrowId, signers.buyer.address);
|
|
254
|
+
|
|
255
|
+
const info = await escrow.getEscrow(escrowId);
|
|
256
|
+
expect(info.state).to.equal(4); // Disputed
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should allow arbiter to resolve in favor of buyer", async function () {
|
|
260
|
+
await escrow.raiseDispute(escrowId);
|
|
261
|
+
|
|
262
|
+
const buyerBalanceBefore = await ethers.provider.getBalance(
|
|
263
|
+
signers.buyer.address
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
await expect(
|
|
267
|
+
escrow.connect(signers.arbiter).resolveDispute(escrowId, true)
|
|
268
|
+
)
|
|
269
|
+
.to.emit(escrow, "DisputeResolved")
|
|
270
|
+
.withArgs(escrowId, signers.buyer.address);
|
|
271
|
+
|
|
272
|
+
const buyerBalanceAfter = await ethers.provider.getBalance(
|
|
273
|
+
signers.buyer.address
|
|
274
|
+
);
|
|
275
|
+
// Buyer receives 99% (1% arbiter fee)
|
|
276
|
+
const expected = DEPOSIT - DEPOSIT / 100n;
|
|
277
|
+
expect(buyerBalanceAfter - buyerBalanceBefore).to.equal(expected);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should prevent non-arbiter from resolving", async function () {
|
|
281
|
+
await escrow.raiseDispute(escrowId);
|
|
282
|
+
|
|
283
|
+
await expect(escrow.resolveDispute(escrowId, true)).to.be.revertedWith(
|
|
284
|
+
"Only arbiter"
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe("View Functions", function () {
|
|
290
|
+
it("should check deadline status", async function () {
|
|
291
|
+
// Non-existent escrow with deadline = 0 means timestamp > 0 is always true
|
|
292
|
+
expect(await escrow.isDeadlinePassed(999)).to.be.true;
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|