IdentityRegistry

Category: Identity | Difficulty: Intermediate | Chapters: Identity, Access Control | Concept: Storing encrypted identity attributes (euint8, euint16, ebool)

On-chain encrypted identity registry for KYC or compliance platforms

Why this example

This example focuses on Storing encrypted identity attributes (euint8, euint16, ebool). It is designed to be self-contained and easy to run locally.

Quick start

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

Dependencies

None

Contract and test

// SPDX-License-Identifier: MIT
// solhint-disable not-rely-on-time
pragma solidity ^0.8.27;

import {FHE, euint8, euint16, ebool, externalEuint8, externalEuint16, externalEbool} from "@fhevm/solidity/lib/FHE.sol";
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {IIdentityRegistry} from "./IIdentityRegistry.sol";

/**
 * @title IdentityRegistry
 * @author Gustavo Valverde
 * @notice On-chain encrypted identity registry for KYC or compliance platforms
 * @dev Example for fhEVM Examples - Identity Category
 *
 * @custom:category identity
 * @custom:chapter identity,access-control
 * @custom:concept Storing encrypted identity attributes (euint8, euint16, ebool)
 * @custom:difficulty intermediate
 *
 * This contract maintains an encrypted identity registry where authorized registrars
 * (typically a backend service) can attest to user identity attributes. All sensitive data
 * remains encrypted on-chain.
 *
 * Key patterns demonstrated:
 * 1. Multiple encrypted types (euint8, euint16, ebool)
 * 2. Role-based access control (registrars)
 * 3. Struct-like data storage with mappings
 * 4. FHE permission management (allowThis, allow)
 */
contract IdentityRegistry is IIdentityRegistry, ZamaEthereumConfig {
    // ============ Encrypted Identity Attributes ============

    /// @notice Encrypted birth year offset from 1900
    mapping(address user => euint8 birthYearOffset) private birthYearOffsets;

    /// @notice Encrypted country code (ISO 3166-1 numeric)
    mapping(address user => euint16 countryCode) private countryCodes;

    /// @notice Encrypted KYC verification level (0-5)
    mapping(address user => euint8 kycLevel) private kycLevels;

    /// @notice Encrypted blacklist status
    mapping(address user => ebool blacklisted) private isBlacklisted;

    /// @notice Timestamp of last attestation
    mapping(address user => uint256 timestamp) public attestationTimestamp;

    /// @notice Store verification results for external queries
    mapping(bytes32 key => ebool result) private verificationResults;

    // ============ Access Control ============

    /// @notice Owner of the registry
    address public owner;
    /// @notice Pending owner for two-step ownership transfer
    address public pendingOwner;

    /// @notice Authorized registrars who can attest identities
    mapping(address registrar => bool authorized) public registrars;

    /// @notice Thrown when caller lacks permission for encrypted data
    error AccessProhibited();

    // ============ Modifiers ============

    modifier onlyOwner() {
        if (msg.sender != owner) revert OnlyOwner();
        _;
    }

    modifier onlyRegistrar() {
        if (!registrars[msg.sender]) revert OnlyRegistrar();
        _;
    }

    // ============ Constructor ============

    /// @notice Initializes the registry with the deployer as owner and initial registrar
    constructor() {
        owner = msg.sender;
        registrars[msg.sender] = true;
        emit RegistrarAdded(msg.sender);
    }

    // ============ Registrar Management ============

    /// @inheritdoc IIdentityRegistry
    function addRegistrar(address registrar) external onlyOwner {
        registrars[registrar] = true;
        emit RegistrarAdded(registrar);
    }

    /// @inheritdoc IIdentityRegistry
    function removeRegistrar(address registrar) external onlyOwner {
        registrars[registrar] = false;
        emit RegistrarRemoved(registrar);
    }

    // ============ Identity Attestation ============

    /// @inheritdoc IIdentityRegistry
    function attestIdentity(
        address user,
        externalEuint8 encBirthYearOffset,
        externalEuint16 encCountryCode,
        externalEuint8 encKycLevel,
        externalEbool encIsBlacklisted,
        bytes calldata inputProof
    ) external onlyRegistrar {
        // Convert and store encrypted values
        euint8 birthYear = FHE.fromExternal(encBirthYearOffset, inputProof);
        euint16 country = FHE.fromExternal(encCountryCode, inputProof);
        euint8 kyc = FHE.fromExternal(encKycLevel, inputProof);
        ebool blacklisted = FHE.fromExternal(encIsBlacklisted, inputProof);

        birthYearOffsets[user] = birthYear;
        countryCodes[user] = country;
        kycLevels[user] = kyc;
        isBlacklisted[user] = blacklisted;

        // Grant contract permission to all values
        FHE.allowThis(birthYear);
        FHE.allowThis(country);
        FHE.allowThis(kyc);
        FHE.allowThis(blacklisted);

        // Grant user permission to their own data
        FHE.allow(birthYear, user);
        FHE.allow(country, user);
        FHE.allow(kyc, user);
        FHE.allow(blacklisted, user);

        attestationTimestamp[user] = block.timestamp;

        emit IdentityAttested(user, msg.sender);
    }

    /// @inheritdoc IIdentityRegistry
    function revokeIdentity(address user) external onlyRegistrar {
        if (attestationTimestamp[user] == 0) revert NotAttested();

        // Set encrypted values to encrypted zeros
        birthYearOffsets[user] = FHE.asEuint8(0);
        countryCodes[user] = FHE.asEuint16(0);
        kycLevels[user] = FHE.asEuint8(0);
        isBlacklisted[user] = FHE.asEbool(false);
        attestationTimestamp[user] = 0;

        emit IdentityRevoked(user);
    }

    // ============ Encrypted Queries ============

    /// @inheritdoc IIdentityRegistry
    function getBirthYearOffset(address user) external view returns (euint8) {
        if (attestationTimestamp[user] == 0) revert NotAttested();
        if (!FHE.isSenderAllowed(birthYearOffsets[user])) revert AccessProhibited();
        return birthYearOffsets[user];
    }

    /// @inheritdoc IIdentityRegistry
    function getCountryCode(address user) external view returns (euint16) {
        if (attestationTimestamp[user] == 0) revert NotAttested();
        if (!FHE.isSenderAllowed(countryCodes[user])) revert AccessProhibited();
        return countryCodes[user];
    }

    /// @inheritdoc IIdentityRegistry
    function getKycLevel(address user) external view returns (euint8) {
        if (attestationTimestamp[user] == 0) revert NotAttested();
        if (!FHE.isSenderAllowed(kycLevels[user])) revert AccessProhibited();
        return kycLevels[user];
    }

    /// @inheritdoc IIdentityRegistry
    function getBlacklistStatus(address user) external view returns (ebool) {
        if (attestationTimestamp[user] == 0) revert NotAttested();
        if (!FHE.isSenderAllowed(isBlacklisted[user])) revert AccessProhibited();
        return isBlacklisted[user];
    }

    // ============ Verification Helpers ============

    /// @inheritdoc IIdentityRegistry
    function hasMinKycLevel(address user, uint8 minLevel) external returns (ebool) {
        if (attestationTimestamp[user] == 0) revert NotAttested();
        if (!FHE.isSenderAllowed(kycLevels[user])) revert AccessProhibited();
        ebool result = FHE.ge(kycLevels[user], FHE.asEuint8(minLevel));

        // Store result for later retrieval
        bytes32 key = keccak256(abi.encodePacked(user, uint8(0), uint256(minLevel)));
        verificationResults[key] = result;

        // Grant caller permission to decrypt the result
        FHE.allowThis(result);
        FHE.allow(result, msg.sender);

        return result;
    }

    /// @inheritdoc IIdentityRegistry
    function isFromCountry(address user, uint16 country) external returns (ebool) {
        if (attestationTimestamp[user] == 0) revert NotAttested();
        if (!FHE.isSenderAllowed(countryCodes[user])) revert AccessProhibited();
        ebool result = FHE.eq(countryCodes[user], FHE.asEuint16(country));

        // Store result for later retrieval
        bytes32 key = keccak256(abi.encodePacked(user, uint8(1), uint256(country)));
        verificationResults[key] = result;

        // Grant caller permission to decrypt the result
        FHE.allowThis(result);
        FHE.allow(result, msg.sender);

        return result;
    }

    /// @inheritdoc IIdentityRegistry
    function isNotBlacklisted(address user) external returns (ebool) {
        if (attestationTimestamp[user] == 0) revert NotAttested();
        if (!FHE.isSenderAllowed(isBlacklisted[user])) revert AccessProhibited();
        ebool result = FHE.not(isBlacklisted[user]);

        // Store result for later retrieval
        bytes32 key = keccak256(abi.encodePacked(user, uint8(2), uint256(0)));
        verificationResults[key] = result;

        // Grant caller permission to decrypt the result
        FHE.allowThis(result);
        FHE.allow(result, msg.sender);

        return result;
    }

    // ============ Result Getters ============

    /**
     * @notice Get the last KYC level verification result
     * @param user Address that was checked
     * @param minLevel Level that was checked
     * @return Encrypted boolean result
     */
    function getKycLevelResult(address user, uint8 minLevel) external view returns (ebool) {
        bytes32 key = keccak256(abi.encodePacked(user, uint8(0), uint256(minLevel)));
        ebool result = verificationResults[key];
        if (!FHE.isSenderAllowed(result)) revert AccessProhibited();
        return result;
    }

    /**
     * @notice Get the last country verification result
     * @param user Address that was checked
     * @param country Country code that was checked
     * @return Encrypted boolean result
     */
    function getCountryResult(address user, uint16 country) external view returns (ebool) {
        bytes32 key = keccak256(abi.encodePacked(user, uint8(1), uint256(country)));
        ebool result = verificationResults[key];
        if (!FHE.isSenderAllowed(result)) revert AccessProhibited();
        return result;
    }

    /**
     * @notice Get the last blacklist verification result
     * @param user Address that was checked
     * @return Encrypted boolean result
     */
    function getBlacklistResult(address user) external view returns (ebool) {
        bytes32 key = keccak256(abi.encodePacked(user, uint8(2), uint256(0)));
        ebool result = verificationResults[key];
        if (!FHE.isSenderAllowed(result)) revert AccessProhibited();
        return result;
    }

    // ============ Access Control ============

    /// @inheritdoc IIdentityRegistry
    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert InvalidOwner();
        pendingOwner = newOwner;
        emit OwnershipTransferStarted(owner, newOwner);
    }

    /// @inheritdoc IIdentityRegistry
    function acceptOwnership() external {
        if (msg.sender != pendingOwner) revert OnlyPendingOwner();
        address previousOwner = owner;
        owner = pendingOwner;
        pendingOwner = address(0);
        emit OwnershipTransferred(previousOwner, owner);
    }

    /// @inheritdoc IIdentityRegistry
    function grantAccessTo(address grantee) external {
        if (attestationTimestamp[msg.sender] == 0) revert NotAttested();

        FHE.allow(birthYearOffsets[msg.sender], grantee);
        FHE.allow(countryCodes[msg.sender], grantee);
        FHE.allow(kycLevels[msg.sender], grantee);
        FHE.allow(isBlacklisted[msg.sender], grantee);

        emit AccessGranted(msg.sender, grantee);
    }

    /// @inheritdoc IIdentityRegistry
    function isAttested(address user) external view returns (bool) {
        return attestationTimestamp[user] > 0;
    }
}

Pitfalls to avoid

No pitfalls are highlighted in the tests for this example.

API Reference

Overview

On-chain encrypted identity registry for KYC or compliance platforms

Developer Notes

Example for fhEVM Examples - Identity Category

attestationTimestamp

Timestamp of last attestation

owner

Owner of the registry

pendingOwner

Pending owner for two-step ownership transfer

registrars

Authorized registrars who can attest identities

AccessProhibited

Thrown when caller lacks permission for encrypted data

onlyOwner

onlyRegistrar

constructor

Initializes the registry with the deployer as owner and initial registrar

addRegistrar

Add a new authorized registrar

Parameters

Name
Type
Description

registrar

address

Address to authorize as registrar

removeRegistrar

Remove an authorized registrar

Parameters

Name
Type
Description

registrar

address

Address to remove from registrars

attestIdentity

Attest a user's encrypted identity data on-chain

Parameters

Name
Type
Description

user

address

Address of the user being attested

encBirthYearOffset

externalEuint8

Encrypted birth year offset (years since 1900)

encCountryCode

externalEuint16

Encrypted ISO 3166-1 numeric country code

encKycLevel

externalEuint8

Encrypted KYC verification level (0-3)

encIsBlacklisted

externalEbool

Encrypted blacklist status

inputProof

bytes

FHE proof for encrypted inputs

revokeIdentity

Revoke a user's identity attestation

Parameters

Name
Type
Description

user

address

Address of the user to revoke

getBirthYearOffset

Get user's encrypted birth year offset

Parameters

Name
Type
Description

user

address

Address of the user

Return Values

Name
Type
Description

[0]

euint8

Encrypted birth year offset (years since 1900)

getCountryCode

Get user's encrypted country code

Parameters

Name
Type
Description

user

address

Address of the user

Return Values

Name
Type
Description

[0]

euint16

Encrypted ISO 3166-1 numeric country code

getKycLevel

Get user's encrypted KYC level

Parameters

Name
Type
Description

user

address

Address of the user

Return Values

Name
Type
Description

[0]

euint8

Encrypted KYC verification level (0-3)

getBlacklistStatus

Get user's encrypted blacklist status

Parameters

Name
Type
Description

user

address

Address of the user

Return Values

Name
Type
Description

[0]

ebool

Encrypted blacklist status (true if blacklisted)

hasMinKycLevel

Check if user has minimum KYC level (encrypted comparison)

Parameters

Name
Type
Description

user

address

Address of the user

minLevel

uint8

Minimum KYC level required

Return Values

Name
Type
Description

[0]

ebool

Encrypted boolean result of comparison

isFromCountry

Check if user is from a specific country (encrypted comparison)

Parameters

Name
Type
Description

user

address

Address of the user

country

uint16

ISO 3166-1 numeric country code to check

Return Values

Name
Type
Description

[0]

ebool

Encrypted boolean result of comparison

isNotBlacklisted

Check if user is not blacklisted (encrypted)

Parameters

Name
Type
Description

user

address

Address of the user

Return Values

Name
Type
Description

[0]

ebool

Encrypted boolean (true if NOT blacklisted)

getKycLevelResult

Get the last KYC level verification result

Parameters

Name
Type
Description

user

address

Address that was checked

minLevel

uint8

Level that was checked

Return Values

Name
Type
Description

[0]

ebool

Encrypted boolean result

getCountryResult

Get the last country verification result

Parameters

Name
Type
Description

user

address

Address that was checked

country

uint16

Country code that was checked

Return Values

Name
Type
Description

[0]

ebool

Encrypted boolean result

getBlacklistResult

Get the last blacklist verification result

Parameters

Name
Type
Description

user

address

Address that was checked

Return Values

Name
Type
Description

[0]

ebool

Encrypted boolean result

transferOwnership

Initiate transfer of contract ownership

Parameters

Name
Type
Description

newOwner

address

Address that can accept ownership

acceptOwnership

Accept ownership transfer

grantAccessTo

Grant a contract access to caller's encrypted identity data

Parameters

Name
Type
Description

grantee

address

Address to grant access to

isAttested

Check if a user has been attested

Parameters

Name
Type
Description

user

address

Address of the user

Return Values

Name
Type
Description

[0]

bool

True if user has valid attestation

Last updated