@zentity/fhevm-contracts 0.1.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.
Files changed (83) hide show
  1. package/README.md +160 -0
  2. package/abi/ComplianceRules.json +352 -0
  3. package/abi/CompliantERC20.json +493 -0
  4. package/abi/IdentityRegistry.json +712 -0
  5. package/abi/index.d.ts +3 -0
  6. package/abi/index.js +9 -0
  7. package/contracts/ARCHITECTURE.md +66 -0
  8. package/contracts/ARCHITECTURE_EXPLAINER.md +77 -0
  9. package/contracts/compliance/ComplianceRules.sol +255 -0
  10. package/contracts/core/IdentityRegistry.sol +352 -0
  11. package/contracts/interfaces/IIdentityRegistry.sol +226 -0
  12. package/contracts/mocks/.gitkeep +0 -0
  13. package/contracts/tokens/CompliantERC20.sol +379 -0
  14. package/deployments/hardhat/addresses.json +20 -0
  15. package/deployments/localhost/.chainId +1 -0
  16. package/deployments/localhost/ComplianceRules.json +662 -0
  17. package/deployments/localhost/CompliantERC20.json +888 -0
  18. package/deployments/localhost/IdentityRegistry.json +1093 -0
  19. package/deployments/localhost/solcInputs/e36969353329df673b4fae03d39e01c4.json +60 -0
  20. package/deployments/sepolia/.chainId +1 -0
  21. package/deployments/sepolia/.gitkeep +0 -0
  22. package/deployments/sepolia/ComplianceRules.json +662 -0
  23. package/deployments/sepolia/CompliantERC20.json +888 -0
  24. package/deployments/sepolia/IdentityRegistry.json +1093 -0
  25. package/deployments/sepolia/solcInputs/93d280ff0d4e798a18947a9ed6015031.json +60 -0
  26. package/dist/index.d.ts +459 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +135 -0
  29. package/package.json +110 -0
  30. package/typechain-types/@fhevm/index.ts +5 -0
  31. package/typechain-types/@fhevm/solidity/config/ZamaConfig.sol/ZamaConfig.ts +69 -0
  32. package/typechain-types/@fhevm/solidity/config/ZamaConfig.sol/ZamaEthereumConfig.ts +90 -0
  33. package/typechain-types/@fhevm/solidity/config/ZamaConfig.sol/index.ts +5 -0
  34. package/typechain-types/@fhevm/solidity/config/index.ts +5 -0
  35. package/typechain-types/@fhevm/solidity/index.ts +7 -0
  36. package/typechain-types/@fhevm/solidity/lib/FHE.sol/FHE.ts +112 -0
  37. package/typechain-types/@fhevm/solidity/lib/FHE.sol/IKMSVerifier.ts +108 -0
  38. package/typechain-types/@fhevm/solidity/lib/FHE.sol/index.ts +5 -0
  39. package/typechain-types/@fhevm/solidity/lib/Impl.sol/IACL.ts +190 -0
  40. package/typechain-types/@fhevm/solidity/lib/Impl.sol/IFHEVMExecutor.ts +623 -0
  41. package/typechain-types/@fhevm/solidity/lib/Impl.sol/IInputVerifier.ts +90 -0
  42. package/typechain-types/@fhevm/solidity/lib/Impl.sol/index.ts +6 -0
  43. package/typechain-types/@fhevm/solidity/lib/index.ts +7 -0
  44. package/typechain-types/common.ts +131 -0
  45. package/typechain-types/contracts/compliance/ComplianceRules.ts +479 -0
  46. package/typechain-types/contracts/compliance/index.ts +4 -0
  47. package/typechain-types/contracts/core/IdentityRegistry.ts +874 -0
  48. package/typechain-types/contracts/core/index.ts +4 -0
  49. package/typechain-types/contracts/index.ts +11 -0
  50. package/typechain-types/contracts/interfaces/IIdentityRegistry.ts +798 -0
  51. package/typechain-types/contracts/interfaces/index.ts +4 -0
  52. package/typechain-types/contracts/tokens/CompliantERC20.sol/CompliantERC20.ts +572 -0
  53. package/typechain-types/contracts/tokens/CompliantERC20.sol/IComplianceChecker.ts +95 -0
  54. package/typechain-types/contracts/tokens/CompliantERC20.sol/index.ts +5 -0
  55. package/typechain-types/contracts/tokens/index.ts +5 -0
  56. package/typechain-types/factories/@fhevm/index.ts +4 -0
  57. package/typechain-types/factories/@fhevm/solidity/config/ZamaConfig.sol/ZamaConfig__factory.ts +69 -0
  58. package/typechain-types/factories/@fhevm/solidity/config/ZamaConfig.sol/ZamaEthereumConfig__factory.ts +43 -0
  59. package/typechain-types/factories/@fhevm/solidity/config/ZamaConfig.sol/index.ts +5 -0
  60. package/typechain-types/factories/@fhevm/solidity/config/index.ts +4 -0
  61. package/typechain-types/factories/@fhevm/solidity/index.ts +5 -0
  62. package/typechain-types/factories/@fhevm/solidity/lib/FHE.sol/FHE__factory.ts +88 -0
  63. package/typechain-types/factories/@fhevm/solidity/lib/FHE.sol/IKMSVerifier__factory.ts +54 -0
  64. package/typechain-types/factories/@fhevm/solidity/lib/FHE.sol/index.ts +5 -0
  65. package/typechain-types/factories/@fhevm/solidity/lib/Impl.sol/IACL__factory.ts +121 -0
  66. package/typechain-types/factories/@fhevm/solidity/lib/Impl.sol/IFHEVMExecutor__factory.ts +810 -0
  67. package/typechain-types/factories/@fhevm/solidity/lib/Impl.sol/IInputVerifier__factory.ts +32 -0
  68. package/typechain-types/factories/@fhevm/solidity/lib/Impl.sol/index.ts +6 -0
  69. package/typechain-types/factories/@fhevm/solidity/lib/index.ts +5 -0
  70. package/typechain-types/factories/contracts/compliance/ComplianceRules__factory.ts +437 -0
  71. package/typechain-types/factories/contracts/compliance/index.ts +4 -0
  72. package/typechain-types/factories/contracts/core/IdentityRegistry__factory.ts +777 -0
  73. package/typechain-types/factories/contracts/core/index.ts +4 -0
  74. package/typechain-types/factories/contracts/index.ts +7 -0
  75. package/typechain-types/factories/contracts/interfaces/IIdentityRegistry__factory.ts +640 -0
  76. package/typechain-types/factories/contracts/interfaces/index.ts +4 -0
  77. package/typechain-types/factories/contracts/tokens/CompliantERC20.sol/CompliantERC20__factory.ts +581 -0
  78. package/typechain-types/factories/contracts/tokens/CompliantERC20.sol/IComplianceChecker__factory.ts +44 -0
  79. package/typechain-types/factories/contracts/tokens/CompliantERC20.sol/index.ts +5 -0
  80. package/typechain-types/factories/contracts/tokens/index.ts +4 -0
  81. package/typechain-types/factories/index.ts +5 -0
  82. package/typechain-types/hardhat.d.ts +261 -0
  83. package/typechain-types/index.ts +32 -0
@@ -0,0 +1,66 @@
1
+ # Zentity FHEVM Architecture (Web3 Data Flow)
2
+
3
+ This document focuses on the Web3 data flow and where encryption happens in the
4
+ fhEVM flow. It is meant to be a high-level map for developers and reviewers.
5
+
6
+ ## Web3 Data Flow (Encrypted State + Compliance)
7
+
8
+ ```mermaid
9
+ flowchart TD
10
+ User["User Wallet + Zama SDK"] -- "1) Encrypt input: externalEuint" --> EncIn["Ciphertext handle + proof"]
11
+ EncIn -- "2) tx call" --> IR["IdentityRegistry"]
12
+ EncIn -- "2) tx call" --> ERC["CompliantERC20"]
13
+
14
+ subgraph OnChain["On-chain fhEVM"]
15
+ IR -- "3) Stores encrypted attrs" --> IRState["Encrypted attributes: birthYear/country/kyc/blacklist"]
16
+ IR -- "4) ACL grants" --> ACL["ACL per ciphertext handle"]
17
+ ERC -- "5) calls" --> CR["ComplianceRules"]
18
+ CR -- "6) queries" --> IR
19
+ CR -- "7) returns ebool" --> ERC
20
+ ERC -- "8) select transfer amount" --> ERCState["Encrypted balances"]
21
+ end
22
+
23
+ subgraph OffChain["Off-chain FHE coprocessor"]
24
+ FHE["FHE Coprocessor"] <--> OnChain
25
+ end
26
+
27
+ User -- "9) grantAccessTo ComplianceRules" --> IR
28
+ IR -- "10) AccessGranted event" --> UI["UI / dApp"]
29
+
30
+ note1["Encryption happens client-side. Values are stored on-chain as ciphertext handles with ACL permissions."]
31
+ note2["ComplianceRules evaluates only for allowed callers; results are encrypted and ACL-protected."]
32
+
33
+ note1 -.-> User
34
+ note2 -.-> CR
35
+ ```
36
+
37
+ ### Key points
38
+ - **Encryption happens client-side** using the Zama SDK. Inputs are submitted as
39
+ `externalEuint*` handles with a proof.
40
+ - **On-chain storage is always encrypted** (ciphertext handles), guarded by the
41
+ ACL. No plaintext identity data is stored on-chain.
42
+ - **Ciphertexts remain polymorphically encrypted** under FHE operations, so
43
+ computation happens without decryption and without changing confidentiality.
44
+ - **Compliance is evaluated under encryption** in `ComplianceRules`, and the
45
+ encrypted boolean is consumed by `CompliantERC20` using `FHE.select`.
46
+ - **Access grants are explicit**: users must call `IdentityRegistry.grantAccessTo`
47
+ for any contract to read or compute on their identity ciphertexts.
48
+ - **Silent failure**: if compliance or balance checks fail, the transfer silently
49
+ becomes a zero transfer to avoid leaking sensitive conditions.
50
+
51
+ ## How encryption is saved on-chain
52
+ - Encrypted values are stored as **ciphertext handles** in contract state.
53
+ - The ACL contract enforces who may use each handle for computation or decryption.
54
+ - `grantAccessTo` updates ACL rights for the user's ciphertext handles.
55
+
56
+ ## Contracts involved
57
+ - `IdentityRegistry`: encrypted identity attributes + ACL grants.
58
+ - `ComplianceRules`: encrypted compliance checks (authorized or self-only).
59
+ - `CompliantERC20`: encrypted balances, compliance-gated transfers.
60
+
61
+ ## Ownership and admin safety
62
+ - These contracts use **two-step ownership transfer**:
63
+ 1. `transferOwnership(newOwner)` by current owner
64
+ 2. `acceptOwnership()` by pending owner
65
+ - This reduces the risk of accidental loss of admin control.
66
+ - For production, transfer ownership to a multisig once deployments are verified.
@@ -0,0 +1,77 @@
1
+ # Zentity Architecture Explainer (Web2 -> Web3 Flow)
2
+
3
+ This document explains the end-to-end flow from Web2 verification to Web3
4
+ attestation and encrypted compliance checks.
5
+
6
+ ## Web2 -> Web3 Data Flow
7
+
8
+ ```mermaid
9
+ sequenceDiagram
10
+ autonumber
11
+ actor User
12
+ participant UI as Zentity Web App (Web2)
13
+ participant BE as Zentity Backend (Web2)
14
+ participant KYC as KYC/Liveness Services (Web2)
15
+ participant Registrar as Registrar Wallet (Web3)
16
+ participant IR as IdentityRegistry (fhEVM)
17
+ participant CR as ComplianceRules (fhEVM)
18
+ participant ERC as CompliantERC20 (fhEVM)
19
+
20
+ User->>UI: Complete verification flow (docs + liveness)
21
+ UI->>BE: Submit verification data
22
+ BE->>KYC: Verify identity and liveness
23
+ KYC-->>BE: KYC result + attributes
24
+
25
+ BE->>UI: Verification complete (Web2)
26
+ UI->>Registrar: Request on-chain attestation
27
+
28
+ Note over UI,Registrar: Identity attributes are encrypted client-side<br/>and sent as externalEuint handles + proof.
29
+
30
+ Registrar->>IR: attestIdentity(user, encAttrs, proof)
31
+ IR-->>Registrar: Encrypted state stored + ACL for user
32
+
33
+ User->>IR: grantAccessTo(ComplianceRules)
34
+ IR-->>User: AccessGranted event
35
+
36
+ User->>ERC: transfer(to, encAmount, proof)
37
+ ERC->>CR: checkCompliance(user)
38
+ CR->>IR: read encrypted attributes (ACL-protected)
39
+ CR-->>ERC: encrypted compliance result
40
+ ERC-->>User: transaction confirmed (silent failure if non-compliant)
41
+ ```
42
+
43
+ ## What stays in Web2 vs Web3
44
+
45
+ ### Web2 (Zentity Backend)
46
+ - **Collects and verifies identity data** (documents, liveness, etc.).
47
+ - **Produces the final KYC result** and attribute set.
48
+ - **Never stores plaintext on-chain**. Only encrypted attributes are sent to
49
+ Web3 via the registrar flow.
50
+
51
+ ### Web3 (fhEVM + Zama)
52
+ - **Stores encrypted identity attributes** in `IdentityRegistry` as ciphertext
53
+ handles.
54
+ - **Enforces access via ACL**. Only explicitly granted contracts/users can use
55
+ a given handle.
56
+ - **Computes compliance under encryption** in `ComplianceRules`.
57
+ - **Transfers are guarded** by encrypted compliance checks in `CompliantERC20`.
58
+
59
+ ## Why the flow is secure (and complex)
60
+ - **Confidentiality** is preserved because the EVM never sees plaintext values.
61
+ - **Integrity** is enforced through ACL permissions and explicit caller checks.
62
+ - **Non-reverting patterns** (silent failure) prevent information leakage.
63
+
64
+ ## Operational responsibilities
65
+ - **Users** must grant access to `ComplianceRules` once per network.
66
+ - **Deployments** must authorize `CompliantERC20` as an allowed caller in
67
+ `ComplianceRules`.
68
+ - **UI** should surface access requirements and avoid caching access state
69
+ locally (rely on on-chain events).
70
+ - **Owners** should transfer ownership to a multisig using the two-step flow
71
+ (`transferOwnership` → `acceptOwnership`) after deployments are verified.
72
+
73
+ ## Practical checklist for integrators
74
+ - Ensure `grantAccessTo(ComplianceRules)` is called before any transfer attempts.
75
+ - Ensure `ComplianceRules.setAuthorizedCaller(tokenAddress, true)` is done at deploy.
76
+ - Handle silent failures in the UI (show effective transfer or warning).
77
+ - Avoid exposing arbitrary external calls from privileged contracts.
@@ -0,0 +1,255 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.27;
3
+
4
+ import {FHE, ebool} from "@fhevm/solidity/lib/FHE.sol";
5
+ import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
6
+ import {IIdentityRegistry} from "../interfaces/IIdentityRegistry.sol";
7
+
8
+ /**
9
+ * @title ComplianceRules
10
+ * @author Gustavo Valverde
11
+ * @notice Combines multiple compliance checks using FHE operations
12
+ * @dev Part of zentity-fhevm-contracts - Builder Track
13
+ *
14
+ * @custom:category compliance
15
+ * @custom:concept Combining encrypted compliance checks with FHE.and()
16
+ * @custom:difficulty intermediate
17
+ *
18
+ * This contract aggregates compliance checks from IdentityRegistry and returns
19
+ * encrypted boolean results. Consumer contracts (like CompliantERC20) can use
20
+ * these results with FHE.select() for branch-free logic.
21
+ *
22
+ * Key patterns demonstrated:
23
+ * 1. FHE.and() for combining multiple encrypted conditions
24
+ * 2. Integration with IdentityRegistry
25
+ * 3. Configurable compliance parameters
26
+ * 4. Encrypted result caching
27
+ */
28
+ contract ComplianceRules is ZamaEthereumConfig {
29
+ // ============ State ============
30
+
31
+ /// @notice Reference to the identity registry
32
+ IIdentityRegistry public immutable identityRegistry;
33
+
34
+ /// @notice Owner/admin
35
+ address public owner;
36
+ /// @notice Pending owner for two-step ownership transfer
37
+ address public pendingOwner;
38
+
39
+ /// @notice Minimum KYC level required for compliance
40
+ uint8 public minKycLevel;
41
+
42
+ /// @notice Store last compliance check result for each user
43
+ mapping(address user => ebool result) private complianceResults;
44
+
45
+ /// @notice Authorized callers that can request compliance checks for others
46
+ mapping(address caller => bool authorized) public authorizedCallers;
47
+
48
+ // ============ Events ============
49
+
50
+ /// @notice Emitted when the minimum KYC level requirement is updated
51
+ /// @param newLevel The new minimum KYC level required for compliance
52
+ event MinKycLevelUpdated(uint8 indexed newLevel);
53
+
54
+ /// @notice Emitted when a compliance check is performed for a user
55
+ /// @param user Address of the user whose compliance was checked
56
+ event ComplianceChecked(address indexed user);
57
+
58
+ /// @notice Emitted when a caller's authorization is updated
59
+ /// @param caller Address being authorized or revoked
60
+ /// @param allowed Whether the caller is allowed
61
+ event AuthorizedCallerUpdated(address indexed caller, bool allowed);
62
+
63
+ /// @notice Emitted when ownership transfer is initiated
64
+ /// @param currentOwner Current owner address
65
+ /// @param pendingOwner Address that can accept ownership
66
+ event OwnershipTransferStarted(address indexed currentOwner, address indexed pendingOwner);
67
+
68
+ /// @notice Emitted when ownership transfer is completed
69
+ /// @param previousOwner Previous owner address
70
+ /// @param newOwner New owner address
71
+ event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
72
+
73
+ // ============ Errors ============
74
+
75
+ /// @notice Thrown when caller is not the contract owner
76
+ error OnlyOwner();
77
+ /// @notice Thrown when caller is not the pending owner
78
+ error OnlyPendingOwner();
79
+ /// @notice Thrown when new owner is the zero address
80
+ error InvalidOwner();
81
+
82
+ /// @notice Thrown when registry address is zero
83
+ error RegistryNotSet();
84
+
85
+ /// @notice Thrown when caller is not authorized to check another user
86
+ error CallerNotAuthorized();
87
+
88
+ /// @notice Thrown when caller lacks permission for encrypted result
89
+ error AccessProhibited();
90
+
91
+ // ============ Modifiers ============
92
+
93
+ modifier onlyOwner() {
94
+ if (msg.sender != owner) revert OnlyOwner();
95
+ _;
96
+ }
97
+
98
+ modifier onlyAuthorizedOrSelf(address user) {
99
+ if (msg.sender != user && !authorizedCallers[msg.sender]) {
100
+ revert CallerNotAuthorized();
101
+ }
102
+ _;
103
+ }
104
+
105
+ // ============ Constructor ============
106
+
107
+ /**
108
+ * @notice Initialize with identity registry reference
109
+ * @param registry Address of the IdentityRegistry contract
110
+ * @param initialMinKycLevel Initial minimum KYC level (default: 1)
111
+ */
112
+ constructor(address registry, uint8 initialMinKycLevel) {
113
+ if (registry == address(0)) revert RegistryNotSet();
114
+ identityRegistry = IIdentityRegistry(registry);
115
+ owner = msg.sender;
116
+ minKycLevel = initialMinKycLevel;
117
+ }
118
+
119
+ // ============ Admin Functions ============
120
+
121
+ /**
122
+ * @notice Update minimum KYC level
123
+ * @param newLevel New minimum level
124
+ */
125
+ function setMinKycLevel(uint8 newLevel) external onlyOwner {
126
+ minKycLevel = newLevel;
127
+ emit MinKycLevelUpdated(newLevel);
128
+ }
129
+
130
+ /**
131
+ * @notice Allow or revoke a caller to check compliance for other users
132
+ * @param caller Address to update
133
+ * @param allowed Whether the caller is allowed
134
+ */
135
+ function setAuthorizedCaller(address caller, bool allowed) external onlyOwner {
136
+ authorizedCallers[caller] = allowed;
137
+ emit AuthorizedCallerUpdated(caller, allowed);
138
+ }
139
+
140
+ /**
141
+ * @notice Initiate transfer of contract ownership
142
+ * @param newOwner Address that can accept ownership
143
+ */
144
+ function transferOwnership(address newOwner) external onlyOwner {
145
+ if (newOwner == address(0)) revert InvalidOwner();
146
+ pendingOwner = newOwner;
147
+ emit OwnershipTransferStarted(owner, newOwner);
148
+ }
149
+
150
+ /**
151
+ * @notice Accept ownership transfer
152
+ */
153
+ function acceptOwnership() external {
154
+ if (msg.sender != pendingOwner) revert OnlyPendingOwner();
155
+ address previousOwner = owner;
156
+ owner = pendingOwner;
157
+ pendingOwner = address(0);
158
+ emit OwnershipTransferred(previousOwner, owner);
159
+ }
160
+
161
+ // ============ Compliance Checks ============
162
+
163
+ /**
164
+ * @notice Check if user passes all compliance requirements
165
+ * @dev Combines: hasMinKycLevel AND isNotBlacklisted
166
+ * @param user Address to check
167
+ * @return Encrypted boolean indicating compliance status
168
+ *
169
+ * Note: This function makes external calls to IdentityRegistry which
170
+ * computes and stores verification results. The combined result is
171
+ * stored locally for later retrieval.
172
+ */
173
+ function checkCompliance(address user) external onlyAuthorizedOrSelf(user) returns (ebool) {
174
+ // Check if user is attested
175
+ if (!identityRegistry.isAttested(user)) {
176
+ ebool notAttestedResult = FHE.asEbool(false);
177
+ FHE.allowThis(notAttestedResult);
178
+ FHE.allow(notAttestedResult, msg.sender);
179
+ complianceResults[user] = notAttestedResult;
180
+ return notAttestedResult;
181
+ }
182
+
183
+ // Get individual compliance checks
184
+ ebool hasKyc = identityRegistry.hasMinKycLevel(user, minKycLevel);
185
+ ebool notBlacklisted = identityRegistry.isNotBlacklisted(user);
186
+
187
+ // Combine all conditions
188
+ ebool result = FHE.and(hasKyc, notBlacklisted);
189
+
190
+ // Store and grant permissions
191
+ complianceResults[user] = result;
192
+ FHE.allowThis(result);
193
+ FHE.allow(result, msg.sender);
194
+
195
+ emit ComplianceChecked(user);
196
+
197
+ return result;
198
+ }
199
+
200
+ /**
201
+ * @notice Check compliance with additional country restriction
202
+ * @param user Address to check
203
+ * @param allowedCountry Country code that is allowed
204
+ * @return Encrypted boolean indicating compliance status
205
+ */
206
+ function checkComplianceWithCountry(
207
+ address user,
208
+ uint16 allowedCountry
209
+ ) external onlyAuthorizedOrSelf(user) returns (ebool) {
210
+ // Check if user is attested
211
+ if (!identityRegistry.isAttested(user)) {
212
+ ebool notAttestedResult = FHE.asEbool(false);
213
+ FHE.allowThis(notAttestedResult);
214
+ FHE.allow(notAttestedResult, msg.sender);
215
+ return notAttestedResult;
216
+ }
217
+
218
+ // Get individual compliance checks
219
+ ebool hasKyc = identityRegistry.hasMinKycLevel(user, minKycLevel);
220
+ ebool notBlacklisted = identityRegistry.isNotBlacklisted(user);
221
+ ebool isFromAllowedCountry = identityRegistry.isFromCountry(user, allowedCountry);
222
+
223
+ // Combine all conditions
224
+ ebool result = FHE.and(FHE.and(hasKyc, notBlacklisted), isFromAllowedCountry);
225
+
226
+ // Grant permissions
227
+ FHE.allowThis(result);
228
+ FHE.allow(result, msg.sender);
229
+
230
+ emit ComplianceChecked(user);
231
+
232
+ return result;
233
+ }
234
+
235
+ /**
236
+ * @notice Get the last compliance check result for a user
237
+ * @dev Call checkCompliance first to compute and store the result
238
+ * @param user Address to get result for
239
+ * @return Encrypted boolean result
240
+ */
241
+ function getComplianceResult(address user) external view returns (ebool) {
242
+ ebool result = complianceResults[user];
243
+ if (!FHE.isSenderAllowed(result)) revert AccessProhibited();
244
+ return result;
245
+ }
246
+
247
+ /**
248
+ * @notice Check if compliance result exists for user
249
+ * @param user Address to check
250
+ * @return Whether a cached result exists
251
+ */
252
+ function hasComplianceResult(address user) external view returns (bool) {
253
+ return FHE.isInitialized(complianceResults[user]);
254
+ }
255
+ }