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,255 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BSD-3-Clause-Clear
|
|
2
|
+
pragma solidity ^0.8.24;
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
FHE,
|
|
6
|
+
euint64,
|
|
7
|
+
ebool,
|
|
8
|
+
externalEuint64
|
|
9
|
+
} from "@fhevm/solidity/lib/FHE.sol";
|
|
10
|
+
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @notice Blind Auction with encrypted bids - only the winning price is revealed
|
|
14
|
+
*
|
|
15
|
+
* @dev Demonstrates advanced FHE patterns:
|
|
16
|
+
* - Encrypted bid storage and comparison
|
|
17
|
+
* - FHE.gt() and FHE.select() for finding maximum
|
|
18
|
+
* - FHE.makePubliclyDecryptable() for revealing results
|
|
19
|
+
* - FHE.checkSignatures() for proof verification
|
|
20
|
+
*
|
|
21
|
+
* Flow:
|
|
22
|
+
* 1. Owner creates auction with end time and minimum bid
|
|
23
|
+
* 2. Bidders submit encrypted bids (one per address)
|
|
24
|
+
* 3. Owner ends auction → winner computed via FHE.gt/select
|
|
25
|
+
* 4. Anyone can reveal winner after decryption proof is ready
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ IMPORTANT: Losing bids remain encrypted forever!
|
|
28
|
+
*/
|
|
29
|
+
contract BlindAuction is ZamaEthereumConfig {
|
|
30
|
+
// ==================== TYPES ====================
|
|
31
|
+
|
|
32
|
+
enum AuctionState {
|
|
33
|
+
Open, // Accepting bids
|
|
34
|
+
Closed, // Bidding ended, pending reveal
|
|
35
|
+
Revealed // Winner revealed on-chain
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ==================== STATE ====================
|
|
39
|
+
|
|
40
|
+
/// Auction owner (deployer)
|
|
41
|
+
address public owner;
|
|
42
|
+
|
|
43
|
+
/// Current auction state
|
|
44
|
+
AuctionState public auctionState;
|
|
45
|
+
|
|
46
|
+
/// Minimum bid in plaintext (for gas efficiency)
|
|
47
|
+
uint64 public minimumBid;
|
|
48
|
+
|
|
49
|
+
/// Auction end timestamp
|
|
50
|
+
uint256 public endTime;
|
|
51
|
+
|
|
52
|
+
/// All bidder addresses (for iteration)
|
|
53
|
+
address[] public bidders;
|
|
54
|
+
|
|
55
|
+
/// Mapping from bidder address to their encrypted bid
|
|
56
|
+
mapping(address => euint64) private _bids;
|
|
57
|
+
|
|
58
|
+
/// Whether an address has bid
|
|
59
|
+
mapping(address => bool) public hasBid;
|
|
60
|
+
|
|
61
|
+
/// Encrypted winning bid amount (set after auction ends)
|
|
62
|
+
euint64 private _winningBid;
|
|
63
|
+
|
|
64
|
+
/// Encrypted winner index in bidders array
|
|
65
|
+
euint64 private _winnerIndex;
|
|
66
|
+
|
|
67
|
+
/// Address of the winner (set after reveal)
|
|
68
|
+
address public winner;
|
|
69
|
+
|
|
70
|
+
/// Revealed winning amount (set after reveal)
|
|
71
|
+
uint64 public winningAmount;
|
|
72
|
+
|
|
73
|
+
// ==================== EVENTS ====================
|
|
74
|
+
|
|
75
|
+
/// @notice Emitted when a new bid is placed
|
|
76
|
+
/// @param bidder Address of the bidder
|
|
77
|
+
event BidPlaced(address indexed bidder);
|
|
78
|
+
|
|
79
|
+
/// @notice Emitted when auction is closed
|
|
80
|
+
/// @param encryptedWinningBid Handle for encrypted winning bid
|
|
81
|
+
/// @param encryptedWinnerIndex Handle for encrypted winner index
|
|
82
|
+
event AuctionEnded(
|
|
83
|
+
euint64 encryptedWinningBid,
|
|
84
|
+
euint64 encryptedWinnerIndex
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
/// @notice Emitted when winner is revealed
|
|
88
|
+
/// @param winner Address of the winner
|
|
89
|
+
/// @param amount Winning bid amount
|
|
90
|
+
event WinnerRevealed(address indexed winner, uint64 amount);
|
|
91
|
+
|
|
92
|
+
// ==================== MODIFIERS ====================
|
|
93
|
+
|
|
94
|
+
/// @dev Restricts function to owner only
|
|
95
|
+
modifier onlyOwner() {
|
|
96
|
+
require(msg.sender == owner, "Only owner can call");
|
|
97
|
+
_;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ==================== CONSTRUCTOR ====================
|
|
101
|
+
|
|
102
|
+
/// @notice Creates a new blind auction
|
|
103
|
+
/// @param _endTime Unix timestamp when bidding ends
|
|
104
|
+
/// @param _minimumBid Minimum bid amount (plaintext)
|
|
105
|
+
constructor(uint256 _endTime, uint64 _minimumBid) {
|
|
106
|
+
require(_endTime > block.timestamp, "End time must be in future");
|
|
107
|
+
owner = msg.sender;
|
|
108
|
+
endTime = _endTime;
|
|
109
|
+
minimumBid = _minimumBid;
|
|
110
|
+
auctionState = AuctionState.Open;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ==================== BIDDING ====================
|
|
114
|
+
|
|
115
|
+
/// @notice Submit an encrypted bid to the auction
|
|
116
|
+
/// @dev Each address can only bid once
|
|
117
|
+
/// @param encryptedBid The encrypted bid amount
|
|
118
|
+
/// @param inputProof Proof validating the encrypted input
|
|
119
|
+
function bid(
|
|
120
|
+
externalEuint64 encryptedBid,
|
|
121
|
+
bytes calldata inputProof
|
|
122
|
+
) external {
|
|
123
|
+
require(auctionState == AuctionState.Open, "Auction not open");
|
|
124
|
+
require(block.timestamp < endTime, "Auction has ended");
|
|
125
|
+
require(!hasBid[msg.sender], "Already placed a bid");
|
|
126
|
+
|
|
127
|
+
// 🔐 Convert external encrypted input to internal euint64
|
|
128
|
+
euint64 bidAmount = FHE.fromExternal(encryptedBid, inputProof);
|
|
129
|
+
|
|
130
|
+
// ✅ Grant contract permission to operate on this value
|
|
131
|
+
FHE.allowThis(bidAmount);
|
|
132
|
+
|
|
133
|
+
// 📋 Store the bid
|
|
134
|
+
_bids[msg.sender] = bidAmount;
|
|
135
|
+
hasBid[msg.sender] = true;
|
|
136
|
+
bidders.push(msg.sender);
|
|
137
|
+
|
|
138
|
+
emit BidPlaced(msg.sender);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ==================== END AUCTION ====================
|
|
142
|
+
|
|
143
|
+
/// @notice End the auction and compute the winner
|
|
144
|
+
/// @dev Only owner can call after end time
|
|
145
|
+
function endAuction() external onlyOwner {
|
|
146
|
+
require(auctionState == AuctionState.Open, "Auction not open");
|
|
147
|
+
require(block.timestamp >= endTime, "Auction not yet ended");
|
|
148
|
+
require(bidders.length > 0, "No bids placed");
|
|
149
|
+
|
|
150
|
+
// 🏆 Find the winning bid using encrypted comparisons
|
|
151
|
+
|
|
152
|
+
// Initialize with first bidder
|
|
153
|
+
euint64 currentMax = _bids[bidders[0]];
|
|
154
|
+
euint64 currentWinnerIdx = FHE.asEuint64(0);
|
|
155
|
+
|
|
156
|
+
// 🔄 Iterate through remaining bidders
|
|
157
|
+
for (uint256 i = 1; i < bidders.length; i++) {
|
|
158
|
+
euint64 candidateBid = _bids[bidders[i]];
|
|
159
|
+
|
|
160
|
+
// 🔍 Compare: is candidate > currentMax?
|
|
161
|
+
// Note: If equal, first bidder wins (no update)
|
|
162
|
+
ebool isGreater = FHE.gt(candidateBid, currentMax);
|
|
163
|
+
|
|
164
|
+
// 🔀 Select the higher bid and its index
|
|
165
|
+
currentMax = FHE.select(isGreater, candidateBid, currentMax);
|
|
166
|
+
currentWinnerIdx = FHE.select(
|
|
167
|
+
isGreater,
|
|
168
|
+
FHE.asEuint64(uint64(i)),
|
|
169
|
+
currentWinnerIdx
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 📊 Check minimum bid threshold
|
|
174
|
+
ebool meetsMinimum = FHE.ge(currentMax, FHE.asEuint64(minimumBid));
|
|
175
|
+
|
|
176
|
+
// If no one meets minimum, winner stays at index 0 but amount becomes 0
|
|
177
|
+
_winningBid = FHE.select(meetsMinimum, currentMax, FHE.asEuint64(0));
|
|
178
|
+
_winnerIndex = currentWinnerIdx;
|
|
179
|
+
|
|
180
|
+
// 🔓 Make results publicly decryptable
|
|
181
|
+
FHE.allowThis(_winningBid);
|
|
182
|
+
FHE.allowThis(_winnerIndex);
|
|
183
|
+
FHE.makePubliclyDecryptable(_winningBid);
|
|
184
|
+
FHE.makePubliclyDecryptable(_winnerIndex);
|
|
185
|
+
|
|
186
|
+
auctionState = AuctionState.Closed;
|
|
187
|
+
|
|
188
|
+
emit AuctionEnded(_winningBid, _winnerIndex);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// @notice Reveal the winner with KMS decryption proof
|
|
192
|
+
/// @dev Anyone can call with valid decryption proof
|
|
193
|
+
/// @param abiEncodedResults ABI-encoded (uint64 amount, uint64 index)
|
|
194
|
+
/// @param decryptionProof KMS signature proving decryption
|
|
195
|
+
function revealWinner(
|
|
196
|
+
bytes memory abiEncodedResults,
|
|
197
|
+
bytes memory decryptionProof
|
|
198
|
+
) external {
|
|
199
|
+
require(auctionState == AuctionState.Closed, "Auction not closed");
|
|
200
|
+
|
|
201
|
+
// 🔐 Build ciphertext list for verification
|
|
202
|
+
// Order MUST match abiEncodedResults encoding order!
|
|
203
|
+
bytes32[] memory cts = new bytes32[](2);
|
|
204
|
+
cts[0] = FHE.toBytes32(_winningBid);
|
|
205
|
+
cts[1] = FHE.toBytes32(_winnerIndex);
|
|
206
|
+
|
|
207
|
+
// 🔍 Verify the decryption proof (reverts if invalid)
|
|
208
|
+
FHE.checkSignatures(cts, abiEncodedResults, decryptionProof);
|
|
209
|
+
|
|
210
|
+
// 📤 Decode the verified results
|
|
211
|
+
(uint64 revealedAmount, uint64 winnerIdx) = abi.decode(
|
|
212
|
+
abiEncodedResults,
|
|
213
|
+
(uint64, uint64)
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// 🏆 Look up winner address
|
|
217
|
+
if (revealedAmount >= minimumBid && winnerIdx < bidders.length) {
|
|
218
|
+
winner = bidders[winnerIdx];
|
|
219
|
+
winningAmount = revealedAmount;
|
|
220
|
+
} else {
|
|
221
|
+
// No valid winner (all bids below minimum)
|
|
222
|
+
winner = address(0);
|
|
223
|
+
winningAmount = 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
auctionState = AuctionState.Revealed;
|
|
227
|
+
|
|
228
|
+
emit WinnerRevealed(winner, winningAmount);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ==================== VIEW FUNCTIONS ====================
|
|
232
|
+
|
|
233
|
+
/// @notice Get the number of bidders
|
|
234
|
+
function getBidderCount() external view returns (uint256) {
|
|
235
|
+
return bidders.length;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// @notice Get bidder address by index
|
|
239
|
+
function getBidder(uint256 index) external view returns (address) {
|
|
240
|
+
require(index < bidders.length, "Index out of bounds");
|
|
241
|
+
return bidders[index];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// @notice Get encrypted winning bid handle (after auction ends)
|
|
245
|
+
function getEncryptedWinningBid() external view returns (euint64) {
|
|
246
|
+
require(auctionState != AuctionState.Open, "Auction still open");
|
|
247
|
+
return _winningBid;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// @notice Get encrypted winner index handle (after auction ends)
|
|
251
|
+
function getEncryptedWinnerIndex() external view returns (euint64) {
|
|
252
|
+
require(auctionState != AuctionState.Open, "Auction still open");
|
|
253
|
+
return _winnerIndex;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BSD-3-Clause-Clear
|
|
2
|
+
pragma solidity ^0.8.24;
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
FHE,
|
|
6
|
+
euint64,
|
|
7
|
+
ebool,
|
|
8
|
+
externalEuint64
|
|
9
|
+
} from "@fhevm/solidity/lib/FHE.sol";
|
|
10
|
+
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @notice Encrypted Escrow service - amounts hidden until release!
|
|
14
|
+
*
|
|
15
|
+
* @dev Demonstrates secure escrow with FHE:
|
|
16
|
+
* - Escrow amounts remain encrypted
|
|
17
|
+
* - Conditions verified without revealing values
|
|
18
|
+
* - Multi-party agreement pattern
|
|
19
|
+
* - Dispute resolution with arbiter
|
|
20
|
+
*
|
|
21
|
+
* Flow:
|
|
22
|
+
* 1. Buyer creates escrow with encrypted amount
|
|
23
|
+
* 2. Buyer deposits funds
|
|
24
|
+
* 3. Seller delivers goods/services
|
|
25
|
+
* 4. Buyer releases funds OR disputes
|
|
26
|
+
* 5. Arbiter resolves disputes if needed
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ IMPORTANT: Amount revealed only on release/refund
|
|
29
|
+
*/
|
|
30
|
+
contract EncryptedEscrow is ZamaEthereumConfig {
|
|
31
|
+
// ==================== TYPES ====================
|
|
32
|
+
|
|
33
|
+
enum EscrowState {
|
|
34
|
+
Created, // Escrow created, awaiting deposit
|
|
35
|
+
Funded, // Funds deposited
|
|
36
|
+
Released, // Funds released to seller
|
|
37
|
+
Refunded, // Funds returned to buyer
|
|
38
|
+
Disputed // Under dispute resolution
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
struct Escrow {
|
|
42
|
+
address buyer;
|
|
43
|
+
address seller;
|
|
44
|
+
address arbiter;
|
|
45
|
+
euint64 encryptedAmount;
|
|
46
|
+
uint256 depositedAmount;
|
|
47
|
+
EscrowState state;
|
|
48
|
+
uint256 createdAt;
|
|
49
|
+
uint256 deadline;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ==================== STATE ====================
|
|
53
|
+
|
|
54
|
+
/// Contract owner
|
|
55
|
+
address public owner;
|
|
56
|
+
|
|
57
|
+
/// Escrow ID counter
|
|
58
|
+
uint256 public escrowCount;
|
|
59
|
+
|
|
60
|
+
/// Mapping from escrow ID to Escrow data
|
|
61
|
+
mapping(uint256 => Escrow) private _escrows;
|
|
62
|
+
|
|
63
|
+
/// Arbiter fee percentage (default 1%)
|
|
64
|
+
uint256 public arbiterFeePercent;
|
|
65
|
+
|
|
66
|
+
// ==================== EVENTS ====================
|
|
67
|
+
|
|
68
|
+
/// @notice Emitted when escrow is created
|
|
69
|
+
/// @param escrowId ID of the escrow
|
|
70
|
+
/// @param buyer Address of buyer
|
|
71
|
+
/// @param seller Address of seller
|
|
72
|
+
event EscrowCreated(
|
|
73
|
+
uint256 indexed escrowId,
|
|
74
|
+
address indexed buyer,
|
|
75
|
+
address indexed seller
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
/// @notice Emitted when escrow is funded
|
|
79
|
+
/// @param escrowId ID of the escrow
|
|
80
|
+
/// @param amount Amount deposited
|
|
81
|
+
event EscrowFunded(uint256 indexed escrowId, uint256 amount);
|
|
82
|
+
|
|
83
|
+
/// @notice Emitted when funds are released
|
|
84
|
+
/// @param escrowId ID of the escrow
|
|
85
|
+
/// @param recipient Address receiving funds
|
|
86
|
+
event FundsReleased(uint256 indexed escrowId, address indexed recipient);
|
|
87
|
+
|
|
88
|
+
/// @notice Emitted when funds are refunded
|
|
89
|
+
/// @param escrowId ID of the escrow
|
|
90
|
+
/// @param recipient Address receiving refund
|
|
91
|
+
event FundsRefunded(uint256 indexed escrowId, address indexed recipient);
|
|
92
|
+
|
|
93
|
+
/// @notice Emitted when dispute is raised
|
|
94
|
+
/// @param escrowId ID of the escrow
|
|
95
|
+
/// @param raisedBy Address raising dispute
|
|
96
|
+
event DisputeRaised(uint256 indexed escrowId, address indexed raisedBy);
|
|
97
|
+
|
|
98
|
+
/// @notice Emitted when dispute is resolved
|
|
99
|
+
/// @param escrowId ID of the escrow
|
|
100
|
+
/// @param winner Address favored in resolution
|
|
101
|
+
event DisputeResolved(uint256 indexed escrowId, address indexed winner);
|
|
102
|
+
|
|
103
|
+
// ==================== CONSTRUCTOR ====================
|
|
104
|
+
|
|
105
|
+
/// @notice Creates the escrow contract
|
|
106
|
+
/// @param _arbiterFeePercent Fee percentage for arbiter (0-10)
|
|
107
|
+
constructor(uint256 _arbiterFeePercent) {
|
|
108
|
+
require(_arbiterFeePercent <= 10, "Fee too high");
|
|
109
|
+
owner = msg.sender;
|
|
110
|
+
arbiterFeePercent = _arbiterFeePercent;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ==================== ESCROW CREATION ====================
|
|
114
|
+
|
|
115
|
+
/// @notice Create a new escrow with encrypted amount
|
|
116
|
+
/// @param seller Address of the seller
|
|
117
|
+
/// @param arbiter Address of dispute arbiter
|
|
118
|
+
/// @param encryptedAmount Encrypted escrow amount
|
|
119
|
+
/// @param inputProof Proof validating the encrypted input
|
|
120
|
+
/// @param deadline Deadline timestamp for delivery
|
|
121
|
+
/// @return escrowId The ID of created escrow
|
|
122
|
+
function createEscrow(
|
|
123
|
+
address seller,
|
|
124
|
+
address arbiter,
|
|
125
|
+
externalEuint64 encryptedAmount,
|
|
126
|
+
bytes calldata inputProof,
|
|
127
|
+
uint256 deadline
|
|
128
|
+
) external returns (uint256 escrowId) {
|
|
129
|
+
require(seller != address(0), "Invalid seller");
|
|
130
|
+
require(seller != msg.sender, "Buyer cannot be seller");
|
|
131
|
+
require(arbiter != address(0), "Invalid arbiter");
|
|
132
|
+
require(arbiter != msg.sender && arbiter != seller, "Invalid arbiter");
|
|
133
|
+
require(deadline > block.timestamp, "Deadline must be future");
|
|
134
|
+
|
|
135
|
+
escrowId = ++escrowCount;
|
|
136
|
+
|
|
137
|
+
// 🔐 Convert external encrypted input
|
|
138
|
+
euint64 amount = FHE.fromExternal(encryptedAmount, inputProof);
|
|
139
|
+
|
|
140
|
+
// ✅ Grant permissions
|
|
141
|
+
FHE.allowThis(amount);
|
|
142
|
+
FHE.allow(amount, msg.sender); // Buyer can view
|
|
143
|
+
FHE.allow(amount, seller); // Seller can view
|
|
144
|
+
FHE.allow(amount, arbiter); // Arbiter can view
|
|
145
|
+
|
|
146
|
+
_escrows[escrowId] = Escrow({
|
|
147
|
+
buyer: msg.sender,
|
|
148
|
+
seller: seller,
|
|
149
|
+
arbiter: arbiter,
|
|
150
|
+
encryptedAmount: amount,
|
|
151
|
+
depositedAmount: 0,
|
|
152
|
+
state: EscrowState.Created,
|
|
153
|
+
createdAt: block.timestamp,
|
|
154
|
+
deadline: deadline
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
emit EscrowCreated(escrowId, msg.sender, seller);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// @notice Fund the escrow
|
|
161
|
+
/// @dev Amount verified against encrypted value on release
|
|
162
|
+
/// @param escrowId ID of escrow to fund
|
|
163
|
+
function fundEscrow(uint256 escrowId) external payable {
|
|
164
|
+
Escrow storage escrow = _escrows[escrowId];
|
|
165
|
+
require(escrow.buyer == msg.sender, "Only buyer can fund");
|
|
166
|
+
require(escrow.state == EscrowState.Created, "Invalid state");
|
|
167
|
+
require(msg.value > 0, "Must send funds");
|
|
168
|
+
|
|
169
|
+
escrow.depositedAmount = msg.value;
|
|
170
|
+
escrow.state = EscrowState.Funded;
|
|
171
|
+
|
|
172
|
+
emit EscrowFunded(escrowId, msg.value);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ==================== RELEASE / REFUND ====================
|
|
176
|
+
|
|
177
|
+
/// @notice Release funds to seller
|
|
178
|
+
/// @dev Only buyer can release after delivery
|
|
179
|
+
/// @param escrowId ID of escrow to release
|
|
180
|
+
function release(uint256 escrowId) external {
|
|
181
|
+
Escrow storage escrow = _escrows[escrowId];
|
|
182
|
+
require(escrow.buyer == msg.sender, "Only buyer can release");
|
|
183
|
+
require(escrow.state == EscrowState.Funded, "Not funded");
|
|
184
|
+
|
|
185
|
+
uint256 amount = escrow.depositedAmount;
|
|
186
|
+
escrow.depositedAmount = 0;
|
|
187
|
+
escrow.state = EscrowState.Released;
|
|
188
|
+
|
|
189
|
+
// Transfer to seller
|
|
190
|
+
(bool sent, ) = escrow.seller.call{value: amount}("");
|
|
191
|
+
require(sent, "Transfer failed");
|
|
192
|
+
|
|
193
|
+
emit FundsReleased(escrowId, escrow.seller);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// @notice Request refund (before deadline or after timeout)
|
|
197
|
+
/// @param escrowId ID of escrow
|
|
198
|
+
function requestRefund(uint256 escrowId) external {
|
|
199
|
+
Escrow storage escrow = _escrows[escrowId];
|
|
200
|
+
require(escrow.buyer == msg.sender, "Only buyer");
|
|
201
|
+
require(escrow.state == EscrowState.Funded, "Not funded");
|
|
202
|
+
require(block.timestamp > escrow.deadline, "Deadline not passed");
|
|
203
|
+
|
|
204
|
+
uint256 amount = escrow.depositedAmount;
|
|
205
|
+
escrow.depositedAmount = 0;
|
|
206
|
+
escrow.state = EscrowState.Refunded;
|
|
207
|
+
|
|
208
|
+
(bool sent, ) = escrow.buyer.call{value: amount}("");
|
|
209
|
+
require(sent, "Transfer failed");
|
|
210
|
+
|
|
211
|
+
emit FundsRefunded(escrowId, escrow.buyer);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ==================== DISPUTE RESOLUTION ====================
|
|
215
|
+
|
|
216
|
+
/// @notice Raise a dispute
|
|
217
|
+
/// @param escrowId ID of escrow
|
|
218
|
+
function raiseDispute(uint256 escrowId) external {
|
|
219
|
+
Escrow storage escrow = _escrows[escrowId];
|
|
220
|
+
require(
|
|
221
|
+
escrow.buyer == msg.sender || escrow.seller == msg.sender,
|
|
222
|
+
"Not a party"
|
|
223
|
+
);
|
|
224
|
+
require(escrow.state == EscrowState.Funded, "Not funded");
|
|
225
|
+
|
|
226
|
+
escrow.state = EscrowState.Disputed;
|
|
227
|
+
|
|
228
|
+
emit DisputeRaised(escrowId, msg.sender);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// @notice Resolve dispute - arbiter decides winner
|
|
232
|
+
/// @param escrowId ID of escrow
|
|
233
|
+
/// @param favorBuyer True to refund buyer, false to release to seller
|
|
234
|
+
function resolveDispute(uint256 escrowId, bool favorBuyer) external {
|
|
235
|
+
Escrow storage escrow = _escrows[escrowId];
|
|
236
|
+
require(escrow.arbiter == msg.sender, "Only arbiter");
|
|
237
|
+
require(escrow.state == EscrowState.Disputed, "Not disputed");
|
|
238
|
+
|
|
239
|
+
uint256 amount = escrow.depositedAmount;
|
|
240
|
+
uint256 arbiterFee = (amount * arbiterFeePercent) / 100;
|
|
241
|
+
uint256 payout = amount - arbiterFee;
|
|
242
|
+
|
|
243
|
+
escrow.depositedAmount = 0;
|
|
244
|
+
|
|
245
|
+
address winner;
|
|
246
|
+
if (favorBuyer) {
|
|
247
|
+
escrow.state = EscrowState.Refunded;
|
|
248
|
+
winner = escrow.buyer;
|
|
249
|
+
} else {
|
|
250
|
+
escrow.state = EscrowState.Released;
|
|
251
|
+
winner = escrow.seller;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Pay arbiter fee
|
|
255
|
+
if (arbiterFee > 0) {
|
|
256
|
+
(bool feeSent, ) = escrow.arbiter.call{value: arbiterFee}("");
|
|
257
|
+
require(feeSent, "Arbiter fee failed");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Pay winner
|
|
261
|
+
(bool sent, ) = winner.call{value: payout}("");
|
|
262
|
+
require(sent, "Payout failed");
|
|
263
|
+
|
|
264
|
+
emit DisputeResolved(escrowId, winner);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ==================== VIEW FUNCTIONS ====================
|
|
268
|
+
|
|
269
|
+
/// @notice Get escrow details
|
|
270
|
+
function getEscrow(
|
|
271
|
+
uint256 escrowId
|
|
272
|
+
)
|
|
273
|
+
external
|
|
274
|
+
view
|
|
275
|
+
returns (
|
|
276
|
+
address buyer,
|
|
277
|
+
address seller,
|
|
278
|
+
address arbiter,
|
|
279
|
+
uint256 depositedAmount,
|
|
280
|
+
EscrowState state,
|
|
281
|
+
uint256 createdAt,
|
|
282
|
+
uint256 deadline
|
|
283
|
+
)
|
|
284
|
+
{
|
|
285
|
+
Escrow storage escrow = _escrows[escrowId];
|
|
286
|
+
return (
|
|
287
|
+
escrow.buyer,
|
|
288
|
+
escrow.seller,
|
|
289
|
+
escrow.arbiter,
|
|
290
|
+
escrow.depositedAmount,
|
|
291
|
+
escrow.state,
|
|
292
|
+
escrow.createdAt,
|
|
293
|
+
escrow.deadline
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// @notice Get encrypted amount handle (for permitted parties)
|
|
298
|
+
function getEncryptedAmount(
|
|
299
|
+
uint256 escrowId
|
|
300
|
+
) external view returns (euint64) {
|
|
301
|
+
Escrow storage escrow = _escrows[escrowId];
|
|
302
|
+
require(
|
|
303
|
+
msg.sender == escrow.buyer ||
|
|
304
|
+
msg.sender == escrow.seller ||
|
|
305
|
+
msg.sender == escrow.arbiter,
|
|
306
|
+
"Not authorized"
|
|
307
|
+
);
|
|
308
|
+
return escrow.encryptedAmount;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// @notice Check if deadline has passed
|
|
312
|
+
function isDeadlinePassed(uint256 escrowId) external view returns (bool) {
|
|
313
|
+
return block.timestamp > _escrows[escrowId].deadline;
|
|
314
|
+
}
|
|
315
|
+
}
|