AccessControlGrants

Category: Identity | Difficulty: Intermediate | Chapters: Access Control | Concept: User-controlled FHE.allow() permissions

Demonstrates user-controlled FHE.allow() permission patterns

Why this example

This example focuses on User-controlled FHE.allow() permissions. It is designed to be self-contained and easy to run locally.

Quick start

npm install
npm run test:mocked -- test/identity/AccessControlGrants.test.ts

Dependencies

None

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

  • should still allow decrypting previously obtained ciphertext after revocation

API Reference

Overview

Demonstrates user-controlled FHE.allow() permission patterns

Developer Notes

Example for fhEVM Examples - Identity Category

hasAccess

Track which addresses have been granted access

accessExpiry

Track time-limited access grants

CredentialStored

Emitted when a user stores their credential score

Parameters

Name
Type
Description

user

address

The address of the user who stored their credential

AccessGranted

Emitted when access is granted to another address

Parameters

Name
Type
Description

owner

address

The data owner granting access

grantee

address

The address receiving access

AccessRevoked

Emitted when access is revoked from an address

Parameters

Name
Type
Description

owner

address

The data owner revoking access

grantee

address

The address losing access

TimedAccessGranted

Emitted when time-limited access is granted

Parameters

Name
Type
Description

owner

address

The data owner granting access

grantee

address

The address receiving access

expiry

uint256

The timestamp when access expires

NoCredential

AlreadyGranted

NotGranted

AccessExpired

storeCredential

Store an encrypted credential score

Demonstrates: Basic encrypted storage with self-access

Parameters

Name
Type
Description

encryptedScore

externalEuint8

Encrypted credential score (0-255)

inputProof

bytes

Proof for the encrypted input

grantAccess

Grant permanent access to another address

Demonstrates: FHE.allow() for access delegation

Parameters

Name
Type
Description

grantee

address

Address to grant access to After calling this, grantee can decrypt the credential score

grantTimedAccess

Grant time-limited access

Demonstrates: Combining FHE access with expiry checks

Parameters

Name
Type
Description

grantee

address

Address to grant access to

duration

uint256

Duration in seconds Note: FHE.allow() is permanent, but we track expiry off-chain and check it before returning data

revokeAccess

Revoke access from a grantee

Note: FHE access cannot be revoked on-chain, but we can prevent contract-level access and update a new encrypted value

Parameters

Name
Type
Description

grantee

address

Address to revoke access from

getCredential

Get credential score (with access check)

Demonstrates: Access-controlled encrypted data retrieval

Parameters

Name
Type
Description

owner

address

Address whose credential to retrieve

Return Values

Name
Type
Description

[0]

euint8

Encrypted credential score

compareCredentials

Compare two users' credentials (both must grant access)

Demonstrates: Multi-party access for encrypted comparison

Parameters

Name
Type
Description

user1

address

First user

user2

address

Second user

Return Values

Name
Type
Description

[0]

ebool

Encrypted boolean (true if user1 >= user2) Both users must have granted access to msg.sender for this to work

getComparisonResult

Get the last comparison result between two users

Call compareCredentials first to compute and store the result

Parameters

Name
Type
Description

user1

address

First user

user2

address

Second user

Return Values

Name
Type
Description

[0]

ebool

Encrypted boolean result

hasValidAccess

Check if a grantee has valid access

Parameters

Name
Type
Description

owner

address

Address of data owner

grantee

address

Address to check

Return Values

Name
Type
Description

[0]

bool

Whether grantee has valid (non-expired) access

getGrantees

Get all grantees for a user

Parameters

Name
Type
Description

owner

address

Address of data owner

Return Values

Name
Type
Description

[0]

address[]

Array of grantee addresses

hasCredential

Check if user has a credential stored

Parameters

Name
Type
Description

user

address

Address to check

Return Values

Name
Type
Description

[0]

bool

Whether user has stored credential

findGranteeIndex

Find the index of a grantee in the grantee list

Parameters

Name
Type
Description

owner

address

The data owner

grantee

address

The grantee to find

Return Values

Name
Type
Description

index

uint256

The index of the grantee, or type(uint256).max if not found

Last updated