AccessControlGrants
Why this example
Quick start
npm install
npm run test:mocked -- test/identity/AccessControlGrants.test.tsDependencies
Contract and test
// SPDX-License-Identifier: MIT
// solhint-disable not-rely-on-time
pragma solidity ^0.8.24;
import {FHE, euint8, ebool, externalEuint8} from "@fhevm/solidity/lib/FHE.sol";
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
/**
* @title AccessControlGrants
* @author Gustavo Valverde
* @notice Demonstrates user-controlled FHE.allow() permission patterns
* @dev Example for fhEVM Examples - Identity Category
*
* @custom:category identity
* @custom:chapter access-control
* @custom:concept User-controlled FHE.allow() permissions
* @custom:difficulty intermediate
*
* This contract shows how users can granularly control access to their
* encrypted data. Users can grant and revoke access to specific parties
* for specific data fields.
*
* Key patterns demonstrated:
* 1. FHE.allow() for granting read access
* 2. Tiered access control (view encrypted, decrypt, modify)
* 3. Time-limited access grants
* 4. Multi-party access coordination
*/
contract AccessControlGrants is ZamaEthereumConfig {
// ============ Data Structures ============
/// @notice Encrypted user credential score
mapping(address user => euint8 score) private credentialScores;
/// @notice Track which addresses have been granted access
mapping(address owner => mapping(address grantee => bool granted)) public hasAccess;
/// @notice Track time-limited access grants
mapping(address owner => mapping(address grantee => uint256 expiry)) public accessExpiry;
/// @notice Store list of grantees for each user (for enumeration)
mapping(address owner => address[] grantees) private granteeList;
/// @notice Store last comparison result for retrieval
/// @dev Key is keccak256(user1, user2) to support different comparisons
mapping(bytes32 key => ebool result) private comparisonResults;
// ============ Events ============
/**
* @notice Emitted when a user stores their credential score
* @param user The address of the user who stored their credential
*/
event CredentialStored(address indexed user);
/**
* @notice Emitted when access is granted to another address
* @param owner The data owner granting access
* @param grantee The address receiving access
*/
event AccessGranted(address indexed owner, address indexed grantee);
/**
* @notice Emitted when access is revoked from an address
* @param owner The data owner revoking access
* @param grantee The address losing access
*/
event AccessRevoked(address indexed owner, address indexed grantee);
/**
* @notice Emitted when time-limited access is granted
* @param owner The data owner granting access
* @param grantee The address receiving access
* @param expiry The timestamp when access expires
*/
event TimedAccessGranted(address indexed owner, address indexed grantee, uint256 indexed expiry);
// ============ Errors ============
error NoCredential();
error AlreadyGranted();
error NotGranted();
error AccessExpired();
// ============ Core Functions ============
/**
* @notice Store an encrypted credential score
* @dev Demonstrates: Basic encrypted storage with self-access
* @param encryptedScore Encrypted credential score (0-255)
* @param inputProof Proof for the encrypted input
*/
function storeCredential(
externalEuint8 encryptedScore,
bytes calldata inputProof
) external {
euint8 score = FHE.fromExternal(encryptedScore, inputProof);
credentialScores[msg.sender] = score;
// Grant contract permission (required for operations)
FHE.allowThis(score);
// Grant owner permission (can always access own data)
FHE.allow(score, msg.sender);
emit CredentialStored(msg.sender);
}
/**
* @notice Grant permanent access to another address
* @dev Demonstrates: FHE.allow() for access delegation
* @param grantee Address to grant access to
*
* After calling this, grantee can decrypt the credential score
*/
function grantAccess(address grantee) external {
if (!FHE.isInitialized(credentialScores[msg.sender])) {
revert NoCredential();
}
if (hasAccess[msg.sender][grantee]) {
revert AlreadyGranted();
}
// Grant access to the encrypted value
FHE.allow(credentialScores[msg.sender], grantee);
hasAccess[msg.sender][grantee] = true;
granteeList[msg.sender].push(grantee);
emit AccessGranted(msg.sender, grantee);
}
/**
* @notice Grant time-limited access
* @dev Demonstrates: Combining FHE access with expiry checks
* @param grantee Address to grant access to
* @param duration Duration in seconds
*
* Note: FHE.allow() is permanent, but we track expiry off-chain
* and check it before returning data
*/
function grantTimedAccess(address grantee, uint256 duration) external {
if (!FHE.isInitialized(credentialScores[msg.sender])) {
revert NoCredential();
}
// Grant FHE access
FHE.allow(credentialScores[msg.sender], grantee);
// Track expiry
accessExpiry[msg.sender][grantee] = block.timestamp + duration;
hasAccess[msg.sender][grantee] = true;
if (findGranteeIndex(msg.sender, grantee) == type(uint256).max) {
granteeList[msg.sender].push(grantee);
}
emit TimedAccessGranted(msg.sender, grantee, block.timestamp + duration);
}
/**
* @notice Revoke access from a grantee
* @dev Note: FHE access cannot be revoked on-chain, but we can
* prevent contract-level access and update a new encrypted value
* @param grantee Address to revoke access from
*/
function revokeAccess(address grantee) external {
if (!hasAccess[msg.sender][grantee]) {
revert NotGranted();
}
hasAccess[msg.sender][grantee] = false;
accessExpiry[msg.sender][grantee] = 0;
// Remove from grantee list
uint256 index = findGranteeIndex(msg.sender, grantee);
if (index != type(uint256).max) {
address[] storage list = granteeList[msg.sender];
list[index] = list[list.length - 1];
list.pop();
}
emit AccessRevoked(msg.sender, grantee);
}
/**
* @notice Get credential score (with access check)
* @dev Demonstrates: Access-controlled encrypted data retrieval
* @param owner Address whose credential to retrieve
* @return Encrypted credential score
*/
function getCredential(address owner) external view returns (euint8) {
if (!FHE.isInitialized(credentialScores[owner])) {
revert NoCredential();
}
// Check if caller has access (owner always has access)
if (msg.sender != owner) {
if (!hasAccess[owner][msg.sender]) {
revert NotGranted();
}
// Check if timed access has expired
uint256 expiry = accessExpiry[owner][msg.sender];
if (expiry != 0 && block.timestamp > expiry) {
revert AccessExpired();
}
}
return credentialScores[owner];
}
/**
* @notice Compare two users' credentials (both must grant access)
* @dev Demonstrates: Multi-party access for encrypted comparison
* @param user1 First user
* @param user2 Second user
* @return Encrypted boolean (true if user1 >= user2)
*
* Both users must have granted access to msg.sender for this to work
*/
function compareCredentials(
address user1,
address user2
) external returns (ebool) {
// Verify access to both
if (msg.sender != user1) {
if (!hasAccess[user1][msg.sender]) {
revert NotGranted();
}
uint256 expiry1 = accessExpiry[user1][msg.sender];
if (expiry1 != 0 && block.timestamp > expiry1) {
revert AccessExpired();
}
}
if (msg.sender != user2) {
if (!hasAccess[user2][msg.sender]) {
revert NotGranted();
}
uint256 expiry2 = accessExpiry[user2][msg.sender];
if (expiry2 != 0 && block.timestamp > expiry2) {
revert AccessExpired();
}
}
euint8 score1 = credentialScores[user1];
euint8 score2 = credentialScores[user2];
if (!FHE.isInitialized(score1) || !FHE.isInitialized(score2)) {
revert NoCredential();
}
ebool result = FHE.ge(score1, score2);
// Store result for later retrieval
bytes32 key = keccak256(abi.encodePacked(user1, user2));
comparisonResults[key] = result;
// Grant caller permission to decrypt the result
FHE.allowThis(result);
FHE.allow(result, msg.sender);
return result;
}
/**
* @notice Get the last comparison result between two users
* @dev Call compareCredentials first to compute and store the result
* @param user1 First user
* @param user2 Second user
* @return Encrypted boolean result
*/
function getComparisonResult(address user1, address user2) external view returns (ebool) {
bytes32 key = keccak256(abi.encodePacked(user1, user2));
return comparisonResults[key];
}
// ============ View Functions ============
/**
* @notice Check if a grantee has valid access
* @param owner Address of data owner
* @param grantee Address to check
* @return Whether grantee has valid (non-expired) access
*/
function hasValidAccess(
address owner,
address grantee
) external view returns (bool) {
if (!hasAccess[owner][grantee]) {
return false;
}
uint256 expiry = accessExpiry[owner][grantee];
if (expiry != 0 && block.timestamp > expiry) {
return false;
}
return true;
}
/**
* @notice Get all grantees for a user
* @param owner Address of data owner
* @return Array of grantee addresses
*/
function getGrantees(address owner) external view returns (address[] memory) {
return granteeList[owner];
}
/**
* @notice Check if user has a credential stored
* @param user Address to check
* @return Whether user has stored credential
*/
function hasCredential(address user) external view returns (bool) {
return FHE.isInitialized(credentialScores[user]);
}
// ============ Internal Functions ============
/**
* @notice Find the index of a grantee in the grantee list
* @param owner The data owner
* @param grantee The grantee to find
* @return index The index of the grantee, or type(uint256).max if not found
*/
function findGranteeIndex(
address owner,
address grantee
) internal view returns (uint256 index) {
address[] storage list = granteeList[owner];
for (uint256 i = 0; i < list.length; ++i) {
if (list[i] == grantee) {
return i;
}
}
return type(uint256).max;
}
}Pitfalls to avoid
API Reference
Overview
Developer Notes
hasAccess
accessExpiry
CredentialStored
Parameters
Name
Type
Description
AccessGranted
Parameters
Name
Type
Description
AccessRevoked
Parameters
Name
Type
Description
TimedAccessGranted
Parameters
Name
Type
Description
NoCredential
AlreadyGranted
NotGranted
AccessExpired
storeCredential
Parameters
Name
Type
Description
grantAccess
Parameters
Name
Type
Description
grantTimedAccess
Parameters
Name
Type
Description
revokeAccess
Parameters
Name
Type
Description
getCredential
Parameters
Name
Type
Description
Return Values
Name
Type
Description
compareCredentials
Parameters
Name
Type
Description
Return Values
Name
Type
Description
getComparisonResult
Parameters
Name
Type
Description
Return Values
Name
Type
Description
hasValidAccess
Parameters
Name
Type
Description
Return Values
Name
Type
Description
getGrantees
Parameters
Name
Type
Description
Return Values
Name
Type
Description
hasCredential
Parameters
Name
Type
Description
Return Values
Name
Type
Description
findGranteeIndex
Parameters
Name
Type
Description
Return Values
Name
Type
Description
Last updated