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,329 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BSD-3-Clause-Clear
|
|
2
|
+
pragma solidity ^0.8.24;
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
FHE,
|
|
6
|
+
euint32,
|
|
7
|
+
ebool,
|
|
8
|
+
externalEuint32
|
|
9
|
+
} from "@fhevm/solidity/lib/FHE.sol";
|
|
10
|
+
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @notice Common FHE mistakes and their correct alternatives. Covers: branching, permissions, require/revert, re-encryption, loops, noise, and deprecated APIs.
|
|
14
|
+
*
|
|
15
|
+
* @dev This contract demonstrates 9 critical anti-patterns in FHE development:
|
|
16
|
+
* 1. Branching on encrypted values (causes decryption!)
|
|
17
|
+
* 2. Returning encrypted values without permissions
|
|
18
|
+
* 3. Using require/revert with encrypted conditions
|
|
19
|
+
* 4. Encrypted computation without permission grants
|
|
20
|
+
* 5. Leaking information through gas/timing
|
|
21
|
+
* 6. Unauthenticated re-encryption (security critical!)
|
|
22
|
+
* 7. Encrypted loop iterations (gas/timing leak)
|
|
23
|
+
* 8. Too many chained operations (noise accumulation)
|
|
24
|
+
* 9. Using deprecated FHEVM APIs
|
|
25
|
+
*
|
|
26
|
+
* Each anti-pattern shows both ❌ WRONG and ✅ CORRECT implementations.
|
|
27
|
+
*/
|
|
28
|
+
contract FHEAntiPatterns is ZamaEthereumConfig {
|
|
29
|
+
euint32 private _secretBalance;
|
|
30
|
+
euint32 private _threshold;
|
|
31
|
+
|
|
32
|
+
// solhint-disable-next-line no-empty-blocks
|
|
33
|
+
constructor() {}
|
|
34
|
+
|
|
35
|
+
/// @notice Initialize the contract with encrypted balance and threshold values
|
|
36
|
+
/// @dev Both values are encrypted and permissions are granted to the caller
|
|
37
|
+
function initialize(
|
|
38
|
+
externalEuint32 balance,
|
|
39
|
+
externalEuint32 threshold,
|
|
40
|
+
bytes calldata inputProof
|
|
41
|
+
) external {
|
|
42
|
+
_secretBalance = FHE.fromExternal(balance, inputProof);
|
|
43
|
+
_threshold = FHE.fromExternal(threshold, inputProof);
|
|
44
|
+
|
|
45
|
+
FHE.allowThis(_secretBalance);
|
|
46
|
+
FHE.allowThis(_threshold);
|
|
47
|
+
FHE.allow(_secretBalance, msg.sender);
|
|
48
|
+
FHE.allow(_threshold, msg.sender);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ========================================================================
|
|
52
|
+
// ANTI-PATTERN 1: Branching on encrypted values
|
|
53
|
+
// ========================================================================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* ❌ WRONG: if/else on encrypted value causes decryption
|
|
57
|
+
* @dev This pattern LEAKS information by decrypting on-chain
|
|
58
|
+
*
|
|
59
|
+
* DO NOT USE THIS PATTERN - It defeats the purpose of encryption!
|
|
60
|
+
*/
|
|
61
|
+
// function wrongBranching() external {
|
|
62
|
+
// // ❌ This would compile but LEAK the comparison result!
|
|
63
|
+
// // if (_secretBalance > _threshold) { // WRONG: decrypts!
|
|
64
|
+
// // // do something
|
|
65
|
+
// // }
|
|
66
|
+
// }
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @notice ✅ CORRECT: Use FHE.select for conditional logic
|
|
70
|
+
* @dev All computation stays encrypted - demonstrates proper encrypted branching
|
|
71
|
+
*/
|
|
72
|
+
function correctConditional() external {
|
|
73
|
+
// Compare encrypted values - result is encrypted boolean
|
|
74
|
+
ebool isAboveThreshold = FHE.gt(_secretBalance, _threshold);
|
|
75
|
+
|
|
76
|
+
// Use select for encrypted branching
|
|
77
|
+
// If above threshold: result = balance - 10
|
|
78
|
+
// Else: result = balance
|
|
79
|
+
euint32 penaltyAmount = FHE.asEuint32(10);
|
|
80
|
+
euint32 balanceMinusPenalty = FHE.sub(_secretBalance, penaltyAmount);
|
|
81
|
+
|
|
82
|
+
// ✅ Encrypted conditional - no information leaked
|
|
83
|
+
_secretBalance = FHE.select(
|
|
84
|
+
isAboveThreshold,
|
|
85
|
+
balanceMinusPenalty,
|
|
86
|
+
_secretBalance
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
FHE.allowThis(_secretBalance);
|
|
90
|
+
FHE.allow(_secretBalance, msg.sender);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ========================================================================
|
|
94
|
+
// ANTI-PATTERN 2: View function returning encrypted without permissions
|
|
95
|
+
// ========================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @notice ❌ WRONG: Returns encrypted value but caller can't decrypt
|
|
99
|
+
* @dev Without prior allow(), the returned handle is useless
|
|
100
|
+
*/
|
|
101
|
+
function wrongGetBalance() external view returns (euint32) {
|
|
102
|
+
// ❌ Caller has no permission to decrypt this!
|
|
103
|
+
return _secretBalance;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @notice ✅ CORRECT: Ensure permissions were granted before returning
|
|
108
|
+
* @dev Caller must have been granted access in a previous transaction
|
|
109
|
+
*/
|
|
110
|
+
function correctGetBalance() external view returns (euint32) {
|
|
111
|
+
// ✅ Caller should have been granted access via allow()
|
|
112
|
+
// The initialize() function grants access to msg.sender
|
|
113
|
+
return _secretBalance;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ========================================================================
|
|
117
|
+
// ANTI-PATTERN 3: Using require/revert with encrypted conditions
|
|
118
|
+
// ========================================================================
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* ❌ WRONG: Cannot use require with encrypted values
|
|
122
|
+
* @dev This pattern doesn't even compile - encrypted bools can't be used in require
|
|
123
|
+
*/
|
|
124
|
+
// function wrongRequire() external {
|
|
125
|
+
// ebool hasEnough = FHE.ge(_secretBalance, FHE.asEuint32(100));
|
|
126
|
+
// // ❌ COMPILE ERROR: require expects bool, not ebool
|
|
127
|
+
// // require(hasEnough, "Insufficient balance");
|
|
128
|
+
// }
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @notice ✅ CORRECT: Use encrypted flags instead of require
|
|
132
|
+
* @dev Store result and let client check after decryption
|
|
133
|
+
*/
|
|
134
|
+
function correctValidation() external returns (ebool) {
|
|
135
|
+
// ✅ Return encrypted boolean for client to check
|
|
136
|
+
ebool hasEnough = FHE.ge(_secretBalance, FHE.asEuint32(100));
|
|
137
|
+
|
|
138
|
+
FHE.allowThis(hasEnough);
|
|
139
|
+
FHE.allow(hasEnough, msg.sender);
|
|
140
|
+
|
|
141
|
+
return hasEnough;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ========================================================================
|
|
145
|
+
// ANTI-PATTERN 4: Encrypted computation without permission grants
|
|
146
|
+
// ========================================================================
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @notice ❌ WRONG: Compute but forget to grant permissions
|
|
150
|
+
* @dev Result exists but no one can ever decrypt it
|
|
151
|
+
*/
|
|
152
|
+
function wrongCompute() external {
|
|
153
|
+
euint32 doubled = FHE.mul(_secretBalance, FHE.asEuint32(2));
|
|
154
|
+
_secretBalance = doubled;
|
|
155
|
+
// ❌ Missing FHE.allowThis and FHE.allow!
|
|
156
|
+
// This value is now locked forever
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// @notice ✅ CORRECT: Always grant permissions after computation
|
|
160
|
+
function correctCompute() external {
|
|
161
|
+
euint32 doubled = FHE.mul(_secretBalance, FHE.asEuint32(2));
|
|
162
|
+
_secretBalance = doubled;
|
|
163
|
+
|
|
164
|
+
// ✅ Grant permissions
|
|
165
|
+
FHE.allowThis(_secretBalance);
|
|
166
|
+
FHE.allow(_secretBalance, msg.sender);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ========================================================================
|
|
170
|
+
// ANTI-PATTERN 5: Leaking information through gas/timing
|
|
171
|
+
// ========================================================================
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @notice ⚠️ CAUTION: Be aware of side-channel attacks
|
|
175
|
+
* @dev Even with FHE.select, be careful about operations that might
|
|
176
|
+
* have different gas costs based on values
|
|
177
|
+
*
|
|
178
|
+
* BEST PRACTICES:
|
|
179
|
+
* - Use constant-time operations when possible
|
|
180
|
+
* - Avoid loops with encrypted iteration counts
|
|
181
|
+
* - Don't make external calls conditionally based on encrypted values
|
|
182
|
+
*/
|
|
183
|
+
function cautionSideChannels() external pure returns (string memory) {
|
|
184
|
+
return "Be aware of gas/timing side channels";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ========================================================================
|
|
188
|
+
// ANTI-PATTERN 6: Unauthenticated Re-encryption (SECURITY CRITICAL)
|
|
189
|
+
// ========================================================================
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @notice ❌ WRONG: Re-encrypt for any provided public key
|
|
193
|
+
* @dev This allows impersonation attacks - anyone can request re-encryption
|
|
194
|
+
* for any public key and pretend to be that user
|
|
195
|
+
*
|
|
196
|
+
* ATTACK SCENARIO:
|
|
197
|
+
* 1. Alice has encrypted balance
|
|
198
|
+
* 2. Eve calls wrongReencrypt(evePublicKey)
|
|
199
|
+
* 3. Eve gets Alice's balance re-encrypted for her key
|
|
200
|
+
* 4. Eve decrypts and learns Alice's secret balance!
|
|
201
|
+
*/
|
|
202
|
+
// function wrongReencrypt(bytes calldata userPublicKey) external view {
|
|
203
|
+
// // ❌ NO AUTHENTICATION! Anyone can provide any public key
|
|
204
|
+
// // Re-encrypt _secretBalance for userPublicKey
|
|
205
|
+
// // This leaks information to unauthorized users
|
|
206
|
+
// }
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @notice ✅ CORRECT: Require cryptographic proof of identity
|
|
210
|
+
* @dev Use EIP-712 signature to prove the requester owns the public key
|
|
211
|
+
*
|
|
212
|
+
* CLIENT-SIDE:
|
|
213
|
+
* 1. User signs a message: "I authorize re-encryption for contract X"
|
|
214
|
+
* 2. Signature is verified on-chain before re-encryption
|
|
215
|
+
*
|
|
216
|
+
* Note: In practice, this is handled by the FHEVM SDK's userDecrypt flow
|
|
217
|
+
*/
|
|
218
|
+
function correctReencryptPattern() external pure returns (string memory) {
|
|
219
|
+
return
|
|
220
|
+
"Always verify EIP-712 signature before re-encryption. "
|
|
221
|
+
"Use fhevm.js userDecrypt which handles this automatically.";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ========================================================================
|
|
225
|
+
// ANTI-PATTERN 7: Encrypted Loop Iterations (GAS/TIMING LEAK)
|
|
226
|
+
// ========================================================================
|
|
227
|
+
|
|
228
|
+
/// ❌ WRONG: Using encrypted value as loop count
|
|
229
|
+
/// @dev Loop count is visible through gas consumption and timing
|
|
230
|
+
///
|
|
231
|
+
/// PROBLEM: If we loop `encryptedCount` times, the gas cost reveals the count!
|
|
232
|
+
// function wrongEncryptedLoop(euint32 encryptedCount) external {
|
|
233
|
+
// // ❌ GAS LEAK: Number of iterations visible!
|
|
234
|
+
// // for (uint i = 0; i < decrypt(encryptedCount); i++) {
|
|
235
|
+
// // // Each iteration costs gas
|
|
236
|
+
// // }
|
|
237
|
+
// }
|
|
238
|
+
|
|
239
|
+
/// @notice ✅ CORRECT: Use fixed iteration count with select
|
|
240
|
+
/// @dev Always iterate the maximum possible times, use FHE.select to
|
|
241
|
+
/// conditionally apply operations
|
|
242
|
+
function correctFixedIterations() external {
|
|
243
|
+
// ✅ Fixed iteration count - no information leaked
|
|
244
|
+
uint256 MAX_ITERATIONS = 10;
|
|
245
|
+
|
|
246
|
+
euint32 accumulator = FHE.asEuint32(0);
|
|
247
|
+
euint32 counter = FHE.asEuint32(0);
|
|
248
|
+
|
|
249
|
+
for (uint256 i = 0; i < MAX_ITERATIONS; i++) {
|
|
250
|
+
// Check if we should still be iterating
|
|
251
|
+
ebool shouldContinue = FHE.lt(counter, _secretBalance);
|
|
252
|
+
|
|
253
|
+
// Conditionally add (add 1 if continuing, add 0 otherwise)
|
|
254
|
+
euint32 increment = FHE.select(
|
|
255
|
+
shouldContinue,
|
|
256
|
+
FHE.asEuint32(1),
|
|
257
|
+
FHE.asEuint32(0)
|
|
258
|
+
);
|
|
259
|
+
accumulator = FHE.add(accumulator, increment);
|
|
260
|
+
counter = FHE.add(counter, FHE.asEuint32(1));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
FHE.allowThis(accumulator);
|
|
264
|
+
FHE.allow(accumulator, msg.sender);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ========================================================================
|
|
268
|
+
// ANTI-PATTERN 8: Too Many Chained Operations (Noise Accumulation)
|
|
269
|
+
// ========================================================================
|
|
270
|
+
|
|
271
|
+
/// @notice ⚠️ CAUTION: FHE operations accumulate "noise"
|
|
272
|
+
/// @dev Each FHE operation adds noise to the ciphertext. After too many
|
|
273
|
+
/// operations, the ciphertext becomes corrupted and undecryptable.
|
|
274
|
+
///
|
|
275
|
+
/// FHEVM handles this via "bootstrapping" which is expensive.
|
|
276
|
+
/// Best practice: minimize operation chains where possible.
|
|
277
|
+
///
|
|
278
|
+
/// EXAMPLE OF NOISE ACCUMULATION:
|
|
279
|
+
/// - Each add/sub: +1 noise
|
|
280
|
+
/// - Each mul: +10 noise (roughly)
|
|
281
|
+
/// - Bootstrapping threshold: ~100 noise (varies by scheme)
|
|
282
|
+
function cautionNoiseAccumulation() external pure returns (string memory) {
|
|
283
|
+
return
|
|
284
|
+
"Keep FHE operation chains short. Multiplications add more noise than additions. "
|
|
285
|
+
"If you need many operations, consider batching or restructuring logic.";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ========================================================================
|
|
289
|
+
// ANTI-PATTERN 9: Using Deprecated FHEVM APIs
|
|
290
|
+
// ========================================================================
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @notice Caution about deprecated FHEVM APIs
|
|
294
|
+
* @dev OLD (v0.8 and earlier):
|
|
295
|
+
* - Decryption went through Zama Oracle
|
|
296
|
+
* - Used TFHE.decrypt() directly
|
|
297
|
+
*
|
|
298
|
+
* NEW (v0.9+):
|
|
299
|
+
* - Self-relaying public decryption
|
|
300
|
+
* - Use FHE.makePubliclyDecryptable() + off-chain relay
|
|
301
|
+
*/
|
|
302
|
+
function cautionDeprecatedAPIs() external pure returns (string memory) {
|
|
303
|
+
return
|
|
304
|
+
"Use FHEVM v0.9+ APIs. Old TFHE.decrypt() is deprecated. "
|
|
305
|
+
"Use FHE.makePubliclyDecryptable() for public decryption, "
|
|
306
|
+
"or userDecrypt pattern via fhevm.js for user decryption.";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ========================================================================
|
|
310
|
+
// SUMMARY: Key Rules (Always check before deploying!)
|
|
311
|
+
// ========================================================================
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* @notice Quick reference for FHE best practices
|
|
315
|
+
* @return rules Summary of all 9 key rules
|
|
316
|
+
*/
|
|
317
|
+
function getRules() external pure returns (string memory rules) {
|
|
318
|
+
return
|
|
319
|
+
"1. Never branch (if/else) on encrypted values - use FHE.select\n"
|
|
320
|
+
"2. Always call FHE.allowThis() AND FHE.allow(user) after computation\n"
|
|
321
|
+
"3. Cannot use require/revert with encrypted conditions\n"
|
|
322
|
+
"4. Return ebool for validations, let client decrypt and check\n"
|
|
323
|
+
"5. Be aware of gas/timing side channels\n"
|
|
324
|
+
"6. Always authenticate re-encryption requests (use EIP-712 signatures)\n"
|
|
325
|
+
"7. Never use encrypted values as loop iteration counts\n"
|
|
326
|
+
"8. Avoid chaining too many FHE operations (noise accumulation)\n"
|
|
327
|
+
"9. Use FHEVM v0.9+ APIs, avoid deprecated TFHE.decrypt()";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BSD-3-Clause-Clear
|
|
2
|
+
pragma solidity ^0.8.24;
|
|
3
|
+
|
|
4
|
+
import {FHE, euint32, externalEuint32} from "@fhevm/solidity/lib/FHE.sol";
|
|
5
|
+
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @notice Understanding FHE handles: creation, computation, immutability, and symbolic execution in mock mode.
|
|
9
|
+
*
|
|
10
|
+
* @dev Handle = uint256 pointer to encrypted data stored by FHE coprocessor.
|
|
11
|
+
* Types: euint8/16/32/64/128/256, ebool, eaddress, ebytes64/128/256
|
|
12
|
+
*/
|
|
13
|
+
contract FHEHandles is ZamaEthereumConfig {
|
|
14
|
+
// 🔐 Storage handles - persist across transactions
|
|
15
|
+
// These are just uint256 pointers, actual ciphertext is off-chain
|
|
16
|
+
euint32 private _storedValue;
|
|
17
|
+
euint32 private _computedValue;
|
|
18
|
+
|
|
19
|
+
event HandleCreated(string operation, uint256 gasUsed);
|
|
20
|
+
event HandleStored(string description);
|
|
21
|
+
|
|
22
|
+
constructor() {}
|
|
23
|
+
|
|
24
|
+
// ==================== HANDLE CREATION ====================
|
|
25
|
+
|
|
26
|
+
/// @notice Pattern 1: Create handle from user's encrypted input
|
|
27
|
+
function createFromExternal(
|
|
28
|
+
externalEuint32 input,
|
|
29
|
+
bytes calldata inputProof
|
|
30
|
+
) external {
|
|
31
|
+
uint256 gasBefore = gasleft();
|
|
32
|
+
|
|
33
|
+
// 📥 FHE.fromExternal: converts external handle to internal handle
|
|
34
|
+
// The proof is verified automatically
|
|
35
|
+
_storedValue = FHE.fromExternal(input, inputProof);
|
|
36
|
+
|
|
37
|
+
emit HandleCreated("fromExternal", gasBefore - gasleft());
|
|
38
|
+
|
|
39
|
+
FHE.allowThis(_storedValue);
|
|
40
|
+
FHE.allow(_storedValue, msg.sender);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// @notice Pattern 2: Create handle from plaintext constant
|
|
44
|
+
/// @dev ⚠️ The plaintext IS visible on-chain! But result is encrypted.
|
|
45
|
+
function createFromPlaintext(uint32 plaintextValue) external {
|
|
46
|
+
uint256 gasBefore = gasleft();
|
|
47
|
+
|
|
48
|
+
// 📥 FHE.asEuint32: encrypts a public constant
|
|
49
|
+
// Use for: thresholds, comparison values, zero-initialization
|
|
50
|
+
_storedValue = FHE.asEuint32(plaintextValue);
|
|
51
|
+
|
|
52
|
+
emit HandleCreated("asEuint32", gasBefore - gasleft());
|
|
53
|
+
|
|
54
|
+
FHE.allowThis(_storedValue);
|
|
55
|
+
FHE.allow(_storedValue, msg.sender);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ==================== HANDLE COMPUTATION ====================
|
|
59
|
+
|
|
60
|
+
/// @notice Key insight: FHE operations create NEW handles
|
|
61
|
+
/// @dev Original handles are IMMUTABLE - they never change
|
|
62
|
+
function computeNewHandle() external {
|
|
63
|
+
uint256 gasBefore = gasleft();
|
|
64
|
+
|
|
65
|
+
euint32 constant10 = FHE.asEuint32(10);
|
|
66
|
+
|
|
67
|
+
// 🔄 FHE.add creates a BRAND NEW handle
|
|
68
|
+
// _storedValue handle is UNCHANGED
|
|
69
|
+
// _computedValue gets the NEW handle
|
|
70
|
+
_computedValue = FHE.add(_storedValue, constant10);
|
|
71
|
+
|
|
72
|
+
emit HandleCreated("add (new handle)", gasBefore - gasleft());
|
|
73
|
+
|
|
74
|
+
// ⚠️ Must grant permissions for the NEW handle!
|
|
75
|
+
FHE.allowThis(_computedValue);
|
|
76
|
+
FHE.allow(_computedValue, msg.sender);
|
|
77
|
+
|
|
78
|
+
emit HandleStored("Computed value stored with new handle");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// @notice Chained operations = multiple intermediate handles
|
|
82
|
+
function chainedOperations() external {
|
|
83
|
+
// 📝 Each operation creates a new handle:
|
|
84
|
+
euint32 step1 = FHE.add(_storedValue, FHE.asEuint32(5)); // Handle #1
|
|
85
|
+
euint32 step2 = FHE.mul(step1, FHE.asEuint32(2)); // Handle #2
|
|
86
|
+
euint32 step3 = FHE.sub(step2, FHE.asEuint32(1)); // Handle #3
|
|
87
|
+
|
|
88
|
+
_computedValue = step3;
|
|
89
|
+
|
|
90
|
+
// Only final result needs permissions (if we're storing it)
|
|
91
|
+
// Intermediate handles (step1, step2) have ephemeral permission
|
|
92
|
+
// and are automatically cleaned up after transaction
|
|
93
|
+
FHE.allowThis(_computedValue);
|
|
94
|
+
FHE.allow(_computedValue, msg.sender);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ==================== HANDLE IMMUTABILITY ====================
|
|
98
|
+
|
|
99
|
+
/// @notice Demonstrates: updating a variable creates NEW handle
|
|
100
|
+
function demonstrateImmutability()
|
|
101
|
+
external
|
|
102
|
+
returns (euint32 original, euint32 updated)
|
|
103
|
+
{
|
|
104
|
+
// 📌 Save reference to current handle
|
|
105
|
+
euint32 originalHandle = _storedValue;
|
|
106
|
+
|
|
107
|
+
// 🔄 This creates NEW handle, assigns to _storedValue
|
|
108
|
+
// originalHandle still points to OLD value!
|
|
109
|
+
_storedValue = FHE.add(_storedValue, FHE.asEuint32(100));
|
|
110
|
+
|
|
111
|
+
FHE.allowThis(_storedValue);
|
|
112
|
+
FHE.allow(_storedValue, msg.sender);
|
|
113
|
+
|
|
114
|
+
// originalHandle → old value
|
|
115
|
+
// _storedValue → new value (old + 100)
|
|
116
|
+
return (originalHandle, _storedValue);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ==================== GETTERS ====================
|
|
120
|
+
|
|
121
|
+
function getStoredValue() external view returns (euint32) {
|
|
122
|
+
return _storedValue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getComputedValue() external view returns (euint32) {
|
|
126
|
+
return _computedValue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BSD-3-Clause-Clear
|
|
2
|
+
pragma solidity ^0.8.24;
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
FHE,
|
|
6
|
+
euint32,
|
|
7
|
+
euint64,
|
|
8
|
+
externalEuint32,
|
|
9
|
+
externalEuint64
|
|
10
|
+
} from "@fhevm/solidity/lib/FHE.sol";
|
|
11
|
+
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @notice Explains input proof validation in FHEVM: what proofs are, why they are needed, and how to use them correctly with single and batched inputs.
|
|
15
|
+
*
|
|
16
|
+
* @dev Why proofs? They ensure:
|
|
17
|
+
* 1. Ciphertext is valid (not garbage data)
|
|
18
|
+
* 2. Value is in valid range for the type
|
|
19
|
+
* 3. Sender knows the plaintext (proof of knowledge)
|
|
20
|
+
*/
|
|
21
|
+
contract FHEInputProof is ZamaEthereumConfig {
|
|
22
|
+
euint32 private _singleValue;
|
|
23
|
+
euint32 private _valueA;
|
|
24
|
+
euint64 private _valueB;
|
|
25
|
+
|
|
26
|
+
constructor() {}
|
|
27
|
+
|
|
28
|
+
// ==================== SINGLE INPUT ====================
|
|
29
|
+
|
|
30
|
+
/// @notice Receive single encrypted value with proof
|
|
31
|
+
function setSingleValue(
|
|
32
|
+
externalEuint32 encryptedInput,
|
|
33
|
+
bytes calldata inputProof
|
|
34
|
+
) external {
|
|
35
|
+
// 📥 FHE.fromExternal validates proof automatically
|
|
36
|
+
// If proof is invalid → transaction reverts
|
|
37
|
+
// Proof ensures:
|
|
38
|
+
// ✓ Valid euint32 ciphertext
|
|
39
|
+
// ✓ Encrypted for THIS contract
|
|
40
|
+
// ✓ Sender knows the plaintext
|
|
41
|
+
_singleValue = FHE.fromExternal(encryptedInput, inputProof);
|
|
42
|
+
|
|
43
|
+
FHE.allowThis(_singleValue);
|
|
44
|
+
FHE.allow(_singleValue, msg.sender);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ==================== MULTIPLE INPUTS (BATCHED) ====================
|
|
48
|
+
|
|
49
|
+
/// @notice Receive multiple encrypted values with SINGLE proof
|
|
50
|
+
/// @dev Client-side batching is more gas-efficient!
|
|
51
|
+
///
|
|
52
|
+
/// Client code:
|
|
53
|
+
/// const input = fhevm.createEncryptedInput(contractAddr, userAddr);
|
|
54
|
+
/// input.add32(valueA); // → handles[0]
|
|
55
|
+
/// input.add64(valueB); // → handles[1]
|
|
56
|
+
/// const enc = await input.encrypt();
|
|
57
|
+
/// // enc.inputProof covers BOTH values!
|
|
58
|
+
function setMultipleValues(
|
|
59
|
+
externalEuint32 inputA,
|
|
60
|
+
externalEuint64 inputB,
|
|
61
|
+
bytes calldata inputProof // ← Single proof covers both!
|
|
62
|
+
) external {
|
|
63
|
+
// Both validated by same proof
|
|
64
|
+
_valueA = FHE.fromExternal(inputA, inputProof);
|
|
65
|
+
_valueB = FHE.fromExternal(inputB, inputProof);
|
|
66
|
+
|
|
67
|
+
// ⚠️ Each value needs its own permission grants
|
|
68
|
+
FHE.allowThis(_valueA);
|
|
69
|
+
FHE.allowThis(_valueB);
|
|
70
|
+
FHE.allow(_valueA, msg.sender);
|
|
71
|
+
FHE.allow(_valueB, msg.sender);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ==================== COMPUTATION WITH NEW INPUT ====================
|
|
75
|
+
|
|
76
|
+
/// @notice Add new encrypted input to existing stored value
|
|
77
|
+
function addToValue(
|
|
78
|
+
externalEuint32 addend,
|
|
79
|
+
bytes calldata inputProof
|
|
80
|
+
) external {
|
|
81
|
+
// Validate the new input
|
|
82
|
+
euint32 validatedAddend = FHE.fromExternal(addend, inputProof);
|
|
83
|
+
|
|
84
|
+
// Combine with stored value
|
|
85
|
+
_singleValue = FHE.add(_singleValue, validatedAddend);
|
|
86
|
+
|
|
87
|
+
FHE.allowThis(_singleValue);
|
|
88
|
+
FHE.allow(_singleValue, msg.sender);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ==================== GETTERS ====================
|
|
92
|
+
|
|
93
|
+
function getSingleValue() external view returns (euint32) {
|
|
94
|
+
return _singleValue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getValueA() external view returns (euint32) {
|
|
98
|
+
return _valueA;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getValueB() external view returns (euint64) {
|
|
102
|
+
return _valueB;
|
|
103
|
+
}
|
|
104
|
+
}
|