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,298 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BSD-3-Clause-Clear
|
|
2
|
+
pragma solidity ^0.8.24;
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
FHE,
|
|
6
|
+
euint64,
|
|
7
|
+
euint8,
|
|
8
|
+
ebool,
|
|
9
|
+
externalEuint64
|
|
10
|
+
} from "@fhevm/solidity/lib/FHE.sol";
|
|
11
|
+
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @notice Encrypted Lottery with private ticket numbers - fair and verifiable!
|
|
15
|
+
*
|
|
16
|
+
* @dev Demonstrates FHE random selection and encrypted matching:
|
|
17
|
+
* - Players purchase tickets with encrypted numbers
|
|
18
|
+
* - Winning number generated using block randomness + FHE
|
|
19
|
+
* - Winners identified via FHE.eq without revealing ticket numbers
|
|
20
|
+
* - Only winner's ticket revealed at end
|
|
21
|
+
*
|
|
22
|
+
* Flow:
|
|
23
|
+
* 1. Lottery created with ticket price and duration
|
|
24
|
+
* 2. Players buy tickets with encrypted numbers
|
|
25
|
+
* 3. Owner draws winning number at end
|
|
26
|
+
* 4. Winners can claim prizes
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ IMPORTANT: Block randomness is predictable - use VRF in production!
|
|
29
|
+
*/
|
|
30
|
+
contract EncryptedLottery is ZamaEthereumConfig {
|
|
31
|
+
// ==================== TYPES ====================
|
|
32
|
+
|
|
33
|
+
enum LotteryState {
|
|
34
|
+
Open, // Accepting tickets
|
|
35
|
+
Drawing, // Drawing in progress
|
|
36
|
+
Completed // Winner revealed
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
struct Ticket {
|
|
40
|
+
address owner;
|
|
41
|
+
euint64 number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ==================== STATE ====================
|
|
45
|
+
|
|
46
|
+
/// Lottery owner
|
|
47
|
+
address public owner;
|
|
48
|
+
|
|
49
|
+
/// Current lottery state
|
|
50
|
+
LotteryState public state;
|
|
51
|
+
|
|
52
|
+
/// Ticket price in wei
|
|
53
|
+
uint256 public ticketPrice;
|
|
54
|
+
|
|
55
|
+
/// Lottery end time
|
|
56
|
+
uint256 public endTime;
|
|
57
|
+
|
|
58
|
+
/// All tickets
|
|
59
|
+
Ticket[] private _tickets;
|
|
60
|
+
|
|
61
|
+
/// Mapping from address to ticket indices
|
|
62
|
+
mapping(address => uint256[]) private _playerTickets;
|
|
63
|
+
|
|
64
|
+
/// Encrypted winning number
|
|
65
|
+
euint64 private _winningNumber;
|
|
66
|
+
|
|
67
|
+
/// Winner address (if found)
|
|
68
|
+
address public winner;
|
|
69
|
+
|
|
70
|
+
/// Prize pool
|
|
71
|
+
uint256 public prizePool;
|
|
72
|
+
|
|
73
|
+
/// Lottery round number
|
|
74
|
+
uint256 public roundNumber;
|
|
75
|
+
|
|
76
|
+
// ==================== EVENTS ====================
|
|
77
|
+
|
|
78
|
+
/// @notice Emitted when a ticket is purchased
|
|
79
|
+
/// @param buyer Address of ticket buyer
|
|
80
|
+
/// @param ticketIndex Index of the ticket
|
|
81
|
+
event TicketPurchased(address indexed buyer, uint256 indexed ticketIndex);
|
|
82
|
+
|
|
83
|
+
/// @notice Emitted when drawing starts
|
|
84
|
+
/// @param roundNumber Current round
|
|
85
|
+
event DrawingStarted(uint256 indexed roundNumber);
|
|
86
|
+
|
|
87
|
+
/// @notice Emitted when winner is found
|
|
88
|
+
/// @param winner Address of winner
|
|
89
|
+
/// @param prize Amount won
|
|
90
|
+
event WinnerFound(address indexed winner, uint256 prize);
|
|
91
|
+
|
|
92
|
+
/// @notice Emitted when no winner found
|
|
93
|
+
/// @param roundNumber Current round
|
|
94
|
+
/// @param rollover Amount rolled to next round
|
|
95
|
+
event NoWinner(uint256 indexed roundNumber, uint256 rollover);
|
|
96
|
+
|
|
97
|
+
// ==================== MODIFIERS ====================
|
|
98
|
+
|
|
99
|
+
modifier onlyOwner() {
|
|
100
|
+
require(msg.sender == owner, "Only owner");
|
|
101
|
+
_;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ==================== CONSTRUCTOR ====================
|
|
105
|
+
|
|
106
|
+
/// @notice Creates a new encrypted lottery
|
|
107
|
+
/// @param _ticketPrice Price per ticket in wei
|
|
108
|
+
/// @param _duration Duration in seconds
|
|
109
|
+
constructor(uint256 _ticketPrice, uint256 _duration) {
|
|
110
|
+
require(_ticketPrice > 0, "Ticket price must be > 0");
|
|
111
|
+
require(_duration > 0, "Duration must be > 0");
|
|
112
|
+
|
|
113
|
+
owner = msg.sender;
|
|
114
|
+
ticketPrice = _ticketPrice;
|
|
115
|
+
endTime = block.timestamp + _duration;
|
|
116
|
+
state = LotteryState.Open;
|
|
117
|
+
roundNumber = 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ==================== TICKET PURCHASE ====================
|
|
121
|
+
|
|
122
|
+
/// @notice Purchase a lottery ticket with an encrypted number
|
|
123
|
+
/// @param encryptedNumber Your encrypted ticket number
|
|
124
|
+
/// @param inputProof Proof validating the encrypted input
|
|
125
|
+
function buyTicket(
|
|
126
|
+
externalEuint64 encryptedNumber,
|
|
127
|
+
bytes calldata inputProof
|
|
128
|
+
) external payable {
|
|
129
|
+
require(state == LotteryState.Open, "Lottery not open");
|
|
130
|
+
require(block.timestamp < endTime, "Lottery ended");
|
|
131
|
+
require(msg.value >= ticketPrice, "Insufficient payment");
|
|
132
|
+
|
|
133
|
+
// 🔐 Convert external encrypted input
|
|
134
|
+
euint64 ticketNumber = FHE.fromExternal(encryptedNumber, inputProof);
|
|
135
|
+
|
|
136
|
+
// ✅ Grant contract permission
|
|
137
|
+
FHE.allowThis(ticketNumber);
|
|
138
|
+
|
|
139
|
+
// 📋 Store ticket
|
|
140
|
+
uint256 ticketIndex = _tickets.length;
|
|
141
|
+
_tickets.push(Ticket({owner: msg.sender, number: ticketNumber}));
|
|
142
|
+
|
|
143
|
+
_playerTickets[msg.sender].push(ticketIndex);
|
|
144
|
+
prizePool += msg.value;
|
|
145
|
+
|
|
146
|
+
emit TicketPurchased(msg.sender, ticketIndex);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ==================== DRAWING ====================
|
|
150
|
+
|
|
151
|
+
/// @notice Start the drawing process
|
|
152
|
+
/// @dev Only owner can call after lottery ends
|
|
153
|
+
function startDrawing() external onlyOwner {
|
|
154
|
+
require(state == LotteryState.Open, "Wrong state");
|
|
155
|
+
require(block.timestamp >= endTime, "Lottery not ended");
|
|
156
|
+
require(_tickets.length > 0, "No tickets sold");
|
|
157
|
+
|
|
158
|
+
// 🎲 Generate "random" winning number using block data
|
|
159
|
+
// ⚠️ WARNING: This is predictable! Use Chainlink VRF in production
|
|
160
|
+
uint64 randomSeed = uint64(
|
|
161
|
+
uint256(
|
|
162
|
+
keccak256(
|
|
163
|
+
abi.encodePacked(
|
|
164
|
+
block.prevrandao,
|
|
165
|
+
block.timestamp,
|
|
166
|
+
_tickets.length,
|
|
167
|
+
msg.sender
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// 🔐 Encrypt the winning number
|
|
174
|
+
_winningNumber = FHE.asEuint64(randomSeed);
|
|
175
|
+
FHE.allowThis(_winningNumber);
|
|
176
|
+
|
|
177
|
+
state = LotteryState.Drawing;
|
|
178
|
+
|
|
179
|
+
emit DrawingStarted(roundNumber);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// @notice Check if a ticket is a winner and claim prize
|
|
183
|
+
/// @param ticketIndex Index of the ticket to check
|
|
184
|
+
function checkAndClaim(uint256 ticketIndex) external {
|
|
185
|
+
require(state == LotteryState.Drawing, "Not in drawing phase");
|
|
186
|
+
require(ticketIndex < _tickets.length, "Invalid ticket");
|
|
187
|
+
require(_tickets[ticketIndex].owner == msg.sender, "Not your ticket");
|
|
188
|
+
|
|
189
|
+
// 🔍 Check if ticket number matches winning number
|
|
190
|
+
// This comparison happens in encrypted space!
|
|
191
|
+
ebool isWinner = FHE.eq(_tickets[ticketIndex].number, _winningNumber);
|
|
192
|
+
|
|
193
|
+
// 🔓 Make result publicly decryptable
|
|
194
|
+
FHE.allowThis(isWinner);
|
|
195
|
+
FHE.makePubliclyDecryptable(isWinner);
|
|
196
|
+
|
|
197
|
+
// Store for later reveal
|
|
198
|
+
// In production, use callback pattern
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// @notice Reveal winner with decryption proof
|
|
202
|
+
/// @param ticketIndex Ticket being checked
|
|
203
|
+
/// @param abiEncodedResult ABI-encoded bool result
|
|
204
|
+
/// @param decryptionProof KMS signature proving decryption
|
|
205
|
+
function revealWinner(
|
|
206
|
+
uint256 ticketIndex,
|
|
207
|
+
bytes memory abiEncodedResult,
|
|
208
|
+
bytes memory decryptionProof
|
|
209
|
+
) external {
|
|
210
|
+
require(state == LotteryState.Drawing, "Not in drawing phase");
|
|
211
|
+
require(ticketIndex < _tickets.length, "Invalid ticket");
|
|
212
|
+
|
|
213
|
+
// Rebuild the comparison for verification
|
|
214
|
+
ebool isWinner = FHE.eq(_tickets[ticketIndex].number, _winningNumber);
|
|
215
|
+
|
|
216
|
+
bytes32[] memory cts = new bytes32[](1);
|
|
217
|
+
cts[0] = FHE.toBytes32(isWinner);
|
|
218
|
+
|
|
219
|
+
// Verify decryption proof
|
|
220
|
+
FHE.checkSignatures(cts, abiEncodedResult, decryptionProof);
|
|
221
|
+
|
|
222
|
+
bool won = abi.decode(abiEncodedResult, (bool));
|
|
223
|
+
|
|
224
|
+
if (won) {
|
|
225
|
+
winner = _tickets[ticketIndex].owner;
|
|
226
|
+
state = LotteryState.Completed;
|
|
227
|
+
|
|
228
|
+
uint256 prize = prizePool;
|
|
229
|
+
prizePool = 0;
|
|
230
|
+
|
|
231
|
+
// Transfer prize
|
|
232
|
+
(bool sent, ) = winner.call{value: prize}("");
|
|
233
|
+
require(sent, "Prize transfer failed");
|
|
234
|
+
|
|
235
|
+
emit WinnerFound(winner, prize);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// @notice End drawing with no winner (rollover)
|
|
240
|
+
/// @dev Called if all tickets checked with no match
|
|
241
|
+
function endDrawingNoWinner() external onlyOwner {
|
|
242
|
+
require(state == LotteryState.Drawing, "Not in drawing phase");
|
|
243
|
+
|
|
244
|
+
state = LotteryState.Completed;
|
|
245
|
+
|
|
246
|
+
emit NoWinner(roundNumber, prizePool);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// @notice Start a new lottery round
|
|
250
|
+
/// @param _duration Duration for next round
|
|
251
|
+
function startNewRound(uint256 _duration) external onlyOwner {
|
|
252
|
+
require(state == LotteryState.Completed, "Current round not complete");
|
|
253
|
+
require(_duration > 0, "Duration must be > 0");
|
|
254
|
+
|
|
255
|
+
// Reset for new round
|
|
256
|
+
delete _tickets;
|
|
257
|
+
winner = address(0);
|
|
258
|
+
endTime = block.timestamp + _duration;
|
|
259
|
+
state = LotteryState.Open;
|
|
260
|
+
roundNumber++;
|
|
261
|
+
// prizePool carries over if no winner
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ==================== VIEW FUNCTIONS ====================
|
|
265
|
+
|
|
266
|
+
/// @notice Get total number of tickets sold
|
|
267
|
+
function getTicketCount() external view returns (uint256) {
|
|
268
|
+
return _tickets.length;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// @notice Get ticket indices for a player
|
|
272
|
+
function getPlayerTickets(
|
|
273
|
+
address player
|
|
274
|
+
) external view returns (uint256[] memory) {
|
|
275
|
+
return _playerTickets[player];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// @notice Check time remaining
|
|
279
|
+
function timeRemaining() external view returns (uint256) {
|
|
280
|
+
if (block.timestamp >= endTime) return 0;
|
|
281
|
+
return endTime - block.timestamp;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// @notice Get lottery info
|
|
285
|
+
function getLotteryInfo()
|
|
286
|
+
external
|
|
287
|
+
view
|
|
288
|
+
returns (
|
|
289
|
+
LotteryState currentState,
|
|
290
|
+
uint256 currentPrizePool,
|
|
291
|
+
uint256 currentEndTime,
|
|
292
|
+
uint256 currentRound,
|
|
293
|
+
uint256 totalTickets
|
|
294
|
+
)
|
|
295
|
+
{
|
|
296
|
+
return (state, prizePool, endTime, roundNumber, _tickets.length);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BSD-3-Clause-Clear
|
|
2
|
+
pragma solidity ^0.8.24;
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
FHE,
|
|
6
|
+
euint8,
|
|
7
|
+
euint16,
|
|
8
|
+
ebool,
|
|
9
|
+
externalEuint8
|
|
10
|
+
} from "@fhevm/solidity/lib/FHE.sol";
|
|
11
|
+
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @notice Encrypted Poker - Texas Hold'em with hidden hole cards!
|
|
15
|
+
*
|
|
16
|
+
* @dev Demonstrates multi-player FHE game mechanics:
|
|
17
|
+
* - Each player's hole cards remain encrypted until showdown
|
|
18
|
+
* - Hand strength computed via FHE without revealing cards
|
|
19
|
+
* - Winner determined by encrypted comparison
|
|
20
|
+
* - Only winning hand revealed, loser's cards stay private
|
|
21
|
+
*
|
|
22
|
+
* Simplified rules for demo:
|
|
23
|
+
* - 2 players (heads-up)
|
|
24
|
+
* - Cards encoded as 1-13 (Ace=1, King=13)
|
|
25
|
+
* - Hand strength = card1 + card2 (simplified for demo)
|
|
26
|
+
* - Higher total wins
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ IMPORTANT: Production poker would need proper hand rankings!
|
|
29
|
+
*/
|
|
30
|
+
contract EncryptedPoker is ZamaEthereumConfig {
|
|
31
|
+
// ==================== TYPES ====================
|
|
32
|
+
|
|
33
|
+
enum GameState {
|
|
34
|
+
WaitingForPlayers, // Waiting for 2 players
|
|
35
|
+
CardsDealt, // Both players have cards
|
|
36
|
+
BettingRound, // Players can bet
|
|
37
|
+
Showdown, // Comparing hands
|
|
38
|
+
Finished // Winner determined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
struct Player {
|
|
42
|
+
address addr;
|
|
43
|
+
euint8 card1;
|
|
44
|
+
euint8 card2;
|
|
45
|
+
euint16 handStrength; // card1 + card2 for simplified ranking
|
|
46
|
+
uint256 bet;
|
|
47
|
+
bool folded;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ==================== STATE ====================
|
|
51
|
+
|
|
52
|
+
/// Game state
|
|
53
|
+
GameState public state;
|
|
54
|
+
|
|
55
|
+
/// Players (0 and 1)
|
|
56
|
+
Player[2] private _players;
|
|
57
|
+
|
|
58
|
+
/// Current pot
|
|
59
|
+
uint256 public pot;
|
|
60
|
+
|
|
61
|
+
/// Minimum bet
|
|
62
|
+
uint256 public minBet;
|
|
63
|
+
|
|
64
|
+
/// Winner address (set after showdown)
|
|
65
|
+
address public winner;
|
|
66
|
+
|
|
67
|
+
/// Game ID
|
|
68
|
+
uint256 public gameId;
|
|
69
|
+
|
|
70
|
+
/// Encrypted comparison result (true if player 0 wins)
|
|
71
|
+
ebool private _player0Wins;
|
|
72
|
+
|
|
73
|
+
// ==================== EVENTS ====================
|
|
74
|
+
|
|
75
|
+
/// @notice Emitted when a player joins
|
|
76
|
+
/// @param player Address of joining player
|
|
77
|
+
/// @param seat Seat number (0 or 1)
|
|
78
|
+
event PlayerJoined(address indexed player, uint8 seat);
|
|
79
|
+
|
|
80
|
+
/// @notice Emitted when cards are dealt
|
|
81
|
+
event CardsDealt();
|
|
82
|
+
|
|
83
|
+
/// @notice Emitted when a bet is placed
|
|
84
|
+
/// @param player Address of betting player
|
|
85
|
+
/// @param amount Bet amount
|
|
86
|
+
event BetPlaced(address indexed player, uint256 amount);
|
|
87
|
+
|
|
88
|
+
/// @notice Emitted when a player folds
|
|
89
|
+
/// @param player Address of folding player
|
|
90
|
+
event PlayerFolded(address indexed player);
|
|
91
|
+
|
|
92
|
+
/// @notice Emitted when showdown begins
|
|
93
|
+
event ShowdownStarted();
|
|
94
|
+
|
|
95
|
+
/// @notice Emitted when winner is determined
|
|
96
|
+
/// @param winner Address of winner
|
|
97
|
+
/// @param pot Total pot won
|
|
98
|
+
event GameWon(address indexed winner, uint256 pot);
|
|
99
|
+
|
|
100
|
+
// ==================== CONSTRUCTOR ====================
|
|
101
|
+
|
|
102
|
+
/// @notice Creates a new poker game
|
|
103
|
+
/// @param _minBet Minimum bet amount in wei
|
|
104
|
+
constructor(uint256 _minBet) {
|
|
105
|
+
require(_minBet > 0, "Min bet must be > 0");
|
|
106
|
+
minBet = _minBet;
|
|
107
|
+
gameId = 1;
|
|
108
|
+
state = GameState.WaitingForPlayers;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ==================== JOIN GAME ====================
|
|
112
|
+
|
|
113
|
+
/// @notice Join the game with encrypted hole cards
|
|
114
|
+
/// @param encCard1 First encrypted card (1-13)
|
|
115
|
+
/// @param encCard2 Second encrypted card (1-13)
|
|
116
|
+
/// @param inputProof Proof validating encrypted inputs
|
|
117
|
+
function joinGame(
|
|
118
|
+
externalEuint8 encCard1,
|
|
119
|
+
externalEuint8 encCard2,
|
|
120
|
+
bytes calldata inputProof
|
|
121
|
+
) external payable {
|
|
122
|
+
require(
|
|
123
|
+
state == GameState.WaitingForPlayers,
|
|
124
|
+
"Game not accepting players"
|
|
125
|
+
);
|
|
126
|
+
require(msg.value >= minBet, "Must pay min bet to join");
|
|
127
|
+
require(
|
|
128
|
+
_players[0].addr != msg.sender && _players[1].addr != msg.sender,
|
|
129
|
+
"Already in game"
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// 🔐 Convert encrypted cards
|
|
133
|
+
euint8 card1 = FHE.fromExternal(encCard1, inputProof);
|
|
134
|
+
euint8 card2 = FHE.fromExternal(encCard2, inputProof);
|
|
135
|
+
|
|
136
|
+
// ✅ Grant permissions
|
|
137
|
+
FHE.allowThis(card1);
|
|
138
|
+
FHE.allowThis(card2);
|
|
139
|
+
FHE.allow(card1, msg.sender); // Player can see own cards
|
|
140
|
+
FHE.allow(card2, msg.sender);
|
|
141
|
+
|
|
142
|
+
// 🧮 Compute hand strength (simplified: sum of cards)
|
|
143
|
+
euint16 strength = FHE.add(FHE.asEuint16(card1), FHE.asEuint16(card2));
|
|
144
|
+
FHE.allowThis(strength);
|
|
145
|
+
|
|
146
|
+
uint8 seat;
|
|
147
|
+
if (_players[0].addr == address(0)) {
|
|
148
|
+
seat = 0;
|
|
149
|
+
_players[0] = Player({
|
|
150
|
+
addr: msg.sender,
|
|
151
|
+
card1: card1,
|
|
152
|
+
card2: card2,
|
|
153
|
+
handStrength: strength,
|
|
154
|
+
bet: msg.value,
|
|
155
|
+
folded: false
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
seat = 1;
|
|
159
|
+
_players[1] = Player({
|
|
160
|
+
addr: msg.sender,
|
|
161
|
+
card1: card1,
|
|
162
|
+
card2: card2,
|
|
163
|
+
handStrength: strength,
|
|
164
|
+
bet: msg.value,
|
|
165
|
+
folded: false
|
|
166
|
+
});
|
|
167
|
+
// Both players joined
|
|
168
|
+
state = GameState.CardsDealt;
|
|
169
|
+
emit CardsDealt();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
pot += msg.value;
|
|
173
|
+
emit PlayerJoined(msg.sender, seat);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ==================== BETTING ====================
|
|
177
|
+
|
|
178
|
+
/// @notice Place a bet
|
|
179
|
+
function bet() external payable {
|
|
180
|
+
require(
|
|
181
|
+
state == GameState.CardsDealt || state == GameState.BettingRound,
|
|
182
|
+
"Not betting phase"
|
|
183
|
+
);
|
|
184
|
+
require(msg.value > 0, "Must bet something");
|
|
185
|
+
|
|
186
|
+
uint8 seat = _getSeat(msg.sender);
|
|
187
|
+
require(!_players[seat].folded, "Already folded");
|
|
188
|
+
|
|
189
|
+
_players[seat].bet += msg.value;
|
|
190
|
+
pot += msg.value;
|
|
191
|
+
state = GameState.BettingRound;
|
|
192
|
+
|
|
193
|
+
emit BetPlaced(msg.sender, msg.value);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// @notice Fold your hand
|
|
197
|
+
function fold() external {
|
|
198
|
+
require(
|
|
199
|
+
state == GameState.CardsDealt || state == GameState.BettingRound,
|
|
200
|
+
"Not betting phase"
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
uint8 seat = _getSeat(msg.sender);
|
|
204
|
+
require(!_players[seat].folded, "Already folded");
|
|
205
|
+
|
|
206
|
+
_players[seat].folded = true;
|
|
207
|
+
|
|
208
|
+
// Other player wins by default
|
|
209
|
+
uint8 winnerSeat = seat == 0 ? 1 : 0;
|
|
210
|
+
winner = _players[winnerSeat].addr;
|
|
211
|
+
state = GameState.Finished;
|
|
212
|
+
|
|
213
|
+
// Transfer pot to winner
|
|
214
|
+
uint256 winnings = pot;
|
|
215
|
+
pot = 0;
|
|
216
|
+
(bool sent, ) = winner.call{value: winnings}("");
|
|
217
|
+
require(sent, "Transfer failed");
|
|
218
|
+
|
|
219
|
+
emit PlayerFolded(msg.sender);
|
|
220
|
+
emit GameWon(winner, winnings);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ==================== SHOWDOWN ====================
|
|
224
|
+
|
|
225
|
+
/// @notice Initiate showdown - compare hands using FHE
|
|
226
|
+
function showdown() external {
|
|
227
|
+
require(
|
|
228
|
+
state == GameState.CardsDealt || state == GameState.BettingRound,
|
|
229
|
+
"Not ready for showdown"
|
|
230
|
+
);
|
|
231
|
+
require(!_players[0].folded && !_players[1].folded, "Someone folded");
|
|
232
|
+
|
|
233
|
+
// 🎯 Compare hand strengths using FHE
|
|
234
|
+
_player0Wins = FHE.gt(
|
|
235
|
+
_players[0].handStrength,
|
|
236
|
+
_players[1].handStrength
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
FHE.allowThis(_player0Wins);
|
|
240
|
+
FHE.makePubliclyDecryptable(_player0Wins);
|
|
241
|
+
|
|
242
|
+
state = GameState.Showdown;
|
|
243
|
+
emit ShowdownStarted();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// @notice Reveal winner with decryption proof
|
|
247
|
+
/// @param abiEncodedResult ABI-encoded bool result
|
|
248
|
+
/// @param decryptionProof KMS signature proving decryption
|
|
249
|
+
function revealWinner(
|
|
250
|
+
bytes memory abiEncodedResult,
|
|
251
|
+
bytes memory decryptionProof
|
|
252
|
+
) external {
|
|
253
|
+
require(state == GameState.Showdown, "Not showdown phase");
|
|
254
|
+
|
|
255
|
+
// Verify decryption
|
|
256
|
+
bytes32[] memory cts = new bytes32[](1);
|
|
257
|
+
cts[0] = FHE.toBytes32(_player0Wins);
|
|
258
|
+
FHE.checkSignatures(cts, abiEncodedResult, decryptionProof);
|
|
259
|
+
|
|
260
|
+
bool player0Wins = abi.decode(abiEncodedResult, (bool));
|
|
261
|
+
|
|
262
|
+
if (player0Wins) {
|
|
263
|
+
winner = _players[0].addr;
|
|
264
|
+
} else {
|
|
265
|
+
winner = _players[1].addr;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
state = GameState.Finished;
|
|
269
|
+
|
|
270
|
+
// Transfer pot
|
|
271
|
+
uint256 winnings = pot;
|
|
272
|
+
pot = 0;
|
|
273
|
+
(bool sent, ) = winner.call{value: winnings}("");
|
|
274
|
+
require(sent, "Transfer failed");
|
|
275
|
+
|
|
276
|
+
emit GameWon(winner, winnings);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ==================== RESET ====================
|
|
280
|
+
|
|
281
|
+
/// @notice Reset for a new game
|
|
282
|
+
function resetGame() external {
|
|
283
|
+
require(state == GameState.Finished, "Game not finished");
|
|
284
|
+
|
|
285
|
+
delete _players[0];
|
|
286
|
+
delete _players[1];
|
|
287
|
+
pot = 0;
|
|
288
|
+
winner = address(0);
|
|
289
|
+
state = GameState.WaitingForPlayers;
|
|
290
|
+
gameId++;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ==================== VIEW FUNCTIONS ====================
|
|
294
|
+
|
|
295
|
+
/// @notice Get game info
|
|
296
|
+
function getGameInfo()
|
|
297
|
+
external
|
|
298
|
+
view
|
|
299
|
+
returns (
|
|
300
|
+
GameState currentState,
|
|
301
|
+
address player0,
|
|
302
|
+
address player1,
|
|
303
|
+
uint256 currentPot,
|
|
304
|
+
address currentWinner,
|
|
305
|
+
uint256 currentGameId
|
|
306
|
+
)
|
|
307
|
+
{
|
|
308
|
+
return (state, _players[0].addr, _players[1].addr, pot, winner, gameId);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// @notice Get player bet
|
|
312
|
+
function getPlayerBet(address player) external view returns (uint256) {
|
|
313
|
+
if (_players[0].addr == player) return _players[0].bet;
|
|
314
|
+
if (_players[1].addr == player) return _players[1].bet;
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/// @notice Check if address is in game
|
|
319
|
+
function isPlayer(address addr) external view returns (bool) {
|
|
320
|
+
return _players[0].addr == addr || _players[1].addr == addr;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// @notice Get own cards (only callable by the player themselves)
|
|
324
|
+
/// @dev Returns encrypted handles that only the caller can decrypt
|
|
325
|
+
function getMyCards() external view returns (euint8 card1, euint8 card2) {
|
|
326
|
+
uint8 seat = _getSeat(msg.sender);
|
|
327
|
+
return (_players[seat].card1, _players[seat].card2);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ==================== INTERNAL ====================
|
|
331
|
+
|
|
332
|
+
function _getSeat(address player) internal view returns (uint8) {
|
|
333
|
+
if (_players[0].addr == player) return 0;
|
|
334
|
+
if (_players[1].addr == player) return 1;
|
|
335
|
+
revert("Not a player");
|
|
336
|
+
}
|
|
337
|
+
}
|