Skip to main content

Identity Registry & AML/KYC Verification

The IdentityRegistry contract is a core component of the security token ecosystem that manages identity verification, AML/KYC compliance, and accreditation status for wallet addresses. It works closely with the RestrictedLockupToken to enforce transfer restrictions based on compliance status.

IdentityRegistry Overview

The IdentityRegistry maintains identity information for each wallet address, including:

  • Regions: Array of numeric region identifiers (supports multiple jurisdictions)
  • AML/KYC Status: Boolean flag indicating whether the wallet has passed Anti-Money Laundering and Know Your Customer verification
  • Accreditation Type: Numeric type indicating the wallet's accreditation status (0 = no accreditation)

Key Features

  • Centralized Identity Management: One contract manages all identity data for the ecosystem
  • Flexible Identity Updates: Wallets Admin can update individual components of identity information
  • Event Logging: All identity changes are logged via events for audit trails
  • ERC-165 Interface Support: Implements standardized interface detection

Identity Information Structure

struct IdentityInfo {
uint256[] regions; // Array of region IDs for multi-jurisdiction support
uint256 accreditationType; // Accreditation type (0 = none)
uint256 lastAmlKycChangeTimestamp; // Timestamp of last AML/KYC status change
uint256 lastAccreditationChangeTimestamp; // Timestamp of last accreditation change
bool amlKycPassed; // Current AML/KYC verification status
}

Field Descriptions:

  • regions: Array of numeric region IDs representing the holder's jurisdictions (supports multiple citizenships/residencies)
  • accreditationType: Investor classification (0=None, 1=Retail, 2=Accredited, 3=Qualified, 4=Institutional etc.)
  • lastAmlKycChangeTimestamp: Updated when AML/KYC status changes via grantAmlKyc() or revokeAmlKyc() - supports custom timestamps or defaults to block.timestamp
  • lastAccreditationChangeTimestamp: Updated when accreditation type changes via grantAccreditation() or revokeAccreditation() - supports custom timestamps or defaults to block.timestamp
  • amlKycPassed: Boolean flag indicating whether the holder has passed AML/KYC verification

AML/KYC Validity Duration

The IdentityRegistry includes a configurable AML/KYC validity duration system:

  • Global Setting: amlKycValidityDuration is set during contract deployment and can be updated by Contract Admin
  • Zero Duration Behavior: When set to 0, AML/KYC status never expires
  • Non-Zero Duration: When set to a value > 0, AML/KYC status expires after the specified duration in seconds
  • Validity Checking: The isAmlKycPassed() function considers both the AML/KYC status flag AND the validity duration
  • Automatic Expiration: AML/KYC status automatically becomes invalid when block.timestamp exceeds lastAmlKycChangeTimestamp + amlKycValidityDuration

Connection with RestrictedLockupToken

The RestrictedLockupToken integrates with IdentityRegistry to enforce compliance-based transfer restrictions:

  1. Integration Point: The token contract holds a reference to the IdentityRegistry via the identityRegistry property
  2. Transfer Validation: During every token transfer, the system validates both sender and recipient AML/KYC status
  3. Compliance Enforcement: Transfers are automatically blocked if either party fails AML/KYC verification
  4. Administrative Control: Only Wallets Admin can update identity information

AML/KYC Transfer Validation Flow

Administrative Functions

The IdentityRegistry provides several administrative functions for managing wallet identities:

FunctionDescriptionAccess Control
setIdentity(owner, info)Set complete identity information with IdentityInfo struct (supports custom timestamps)Wallets Admin
removeIdentity(owner)Remove all identity information for a walletWallets Admin
setRegions(owner, regions[])Replace entire regions arrayWallets Admin
addRegion(owner, region)Add a single region to existing regionsWallets Admin
removeRegion(owner, region)Remove a specific region from regions arrayWallets Admin
grantAmlKyc(owner, timestamp)Mark wallet as AML/KYC passed with optional custom timestampWallets Admin
revokeAmlKyc(owner, timestamp)Mark wallet as AML/KYC failed with optional custom timestampWallets Admin
grantAccreditation(owner, level, timestamp)Set accreditation level with optional custom timestampWallets Admin
revokeAccreditation(owner, timestamp)Remove accreditation with optional custom timestampWallets Admin
setAmlKycValidityDuration(duration)Set global AML/KYC validity duration in secondsContract Admin

Timestamp Functionality

Custom Timestamp Support: The IdentityRegistry functions support flexible timestamp management for audit trails and compliance tracking:

  • setIdentity(owner, info): The IdentityInfo struct includes lastAmlKycChangeTimestamp and lastAccreditationChangeTimestamp fields

    • When these timestamp fields are non-zero, the contract uses the provided custom timestamps
    • When these timestamp fields are 0, the contract automatically sets them to block.timestamp
  • AML/KYC Functions - grantAmlKyc(owner, timestamp) and revokeAmlKyc(owner, timestamp):

    • timestamp parameter: when non-zero, uses the custom timestamp; when 0, uses block.timestamp
    • Updates the lastAmlKycChangeTimestamp field in the wallet's identity record
  • Accreditation Functions - grantAccreditation(owner, level, timestamp) and revokeAccreditation(owner, timestamp):

    • timestamp parameter: when non-zero, uses the custom timestamp; when 0, uses block.timestamp
    • Updates the lastAccreditationChangeTimestamp field in the wallet's identity record

Benefits of Custom Timestamps:

  • Retroactive Data Migration: Import historical compliance data with original timestamps
  • Off-chain Integration: Synchronize with external compliance systems that track verification dates
  • Audit Compliance: Maintain accurate historical records for regulatory reporting
  • Flexibility: Wallets Admin can choose between automatic timestamping (pass 0) or precise control (provide specific timestamp)

Example Usage:

// Using automatic timestamp (current block time)
identityRegistry.grantAmlKyc(walletAddress, 0);

// Using custom timestamp (e.g., when the verification actually occurred off-chain)
uint256 verificationDate = 1640995200; // Jan 1, 2022
identityRegistry.grantAmlKyc(walletAddress, verificationDate);

// Setting identity with mixed timestamp approach
uint256[] memory regions = new uint256[](2);
regions[0] = 840; // United States
regions[1] = 826; // United Kingdom

IdentityInfo memory identity = IdentityInfo({
regions: regions,
accreditationType: 2, // Accredited
lastAmlKycChangeTimestamp: 1640995200, // Custom timestamp
lastAccreditationChangeTimestamp: 0, // Will use block.timestamp
amlKycPassed: true
});
identityRegistry.setIdentity(walletAddress, identity);

Enhanced Region Management

The IdentityRegistry now supports multiple regions per wallet, enabling complex scenarios like dual citizenship, cross-border investments, and multi-jurisdictional compliance.

Region Management Functions

1. Set Complete Regions Array

// Replace entire regions array
uint256[] memory newRegions = new uint256[](3);
newRegions[0] = 840; // United States
newRegions[1] = 826; // United Kingdom
newRegions[2] = 124; // Canada

identityRegistry.setRegions(walletAddress, newRegions);

2. Add Individual Region

// Add a single region to existing regions
identityRegistry.addRegion(walletAddress, 756); // Switzerland

3. Remove Individual Region

// Remove a specific region from regions array
identityRegistry.removeRegion(walletAddress, 826); // Remove UK

4. Query Region Information

// Get all regions for a wallet
uint256[] memory walletRegions = identityRegistry.regions(walletAddress);

// Check if wallet has specific region
bool hasUSRegion = identityRegistry.hasRegion(walletAddress, 840);
Multi-Region Use Cases

Dual Citizenship Scenario:

// Investor has both US and Canadian citizenship
uint256[] memory dualCitizenship = new uint256[](2);
dualCitizenship[0] = 840; // United States
dualCitizenship[1] = 124; // Canada

identityRegistry.setRegions(investor, dualCitizenship);

Corporate Entity with Multiple Jurisdictions:

// Investment fund operating in multiple regions
uint256[] memory operatingRegions = new uint256[](4);
operatingRegions[0] = 840; // United States
operatingRegions[1] = 826; // United Kingdom
operatingRegions[2] = 276; // Germany
operatingRegions[3] = 392; // Japan

identityRegistry.setRegions(institutionalInvestor, operatingRegions);

Dynamic Region Updates:

// Investor relocates and gains new residency
identityRegistry.addRegion(investor, 756); // Add Switzerland

// Later, renounces previous citizenship
identityRegistry.removeRegion(investor, 840); // Remove United States
Region Validation Rules

The contract enforces several validation rules for regions:

  • No Empty Arrays: setRegions() rejects empty region arrays
  • No Duplicates: Regions array cannot contain duplicate region IDs
  • No Zero Values: Region ID cannot be 0 (reserved as invalid)
  • Existence Checking: addRegion() prevents adding regions already present
  • Removal Validation: removeRegion() only removes regions that exist
Error Handling

The contract provides specific error messages for region operations:

// Attempting to add existing region
identityRegistry.addRegion(wallet, 840); // Reverts: IdentityRegistry_WalletAlreadyHasRegion

// Attempting to remove non-existent region
identityRegistry.removeRegion(wallet, 999); // Reverts: IdentityRegistry_WalletDoesNotHaveRegion

// Setting empty regions array
uint256[] memory empty;
identityRegistry.setRegions(wallet, empty); // Reverts: IdentityRegistry_EmptyRegionsArray

Query Functions

The IdentityRegistry provides comprehensive query functions for accessing identity information:

FunctionDescriptionReturnsExample Usage
identity(owner)Get complete identity informationIdentityInfo structIdentityInfo memory info = identityRegistry.identity(wallet);
regions(owner)Get all regions for a walletuint256[] memoryuint256[] memory walletRegions = identityRegistry.regions(wallet);
hasRegion(owner, region)Check if wallet has specific regionbooleanbool hasUS = identityRegistry.hasRegion(wallet, 840);
isAmlKycPassed(owner)Check AML/KYC status with validity durationbooleanbool verified = identityRegistry.isAmlKycPassed(wallet);
accreditationType(owner)Get accreditation leveluint256uint256 level = identityRegistry.accreditationType(wallet);
amlKycValidityDuration()Get global AML/KYC validity durationuint256uint256 duration = identityRegistry.amlKycValidityDuration();

Detailed Query Function Descriptions

1. Complete Identity Information

// Get all identity data at once
IdentityInfo memory info = identityRegistry.identity(walletAddress);

// Access individual fields
uint256[] memory regions = info.regions;
uint256 accreditation = info.accreditationType;
bool amlKycStatus = info.amlKycPassed;
uint256 lastAmlKycChange = info.lastAmlKycChangeTimestamp;
uint256 lastAccreditationChange = info.lastAccreditationChangeTimestamp;

2. Region-Specific Queries

// Get all regions for a wallet
uint256[] memory allRegions = identityRegistry.regions(walletAddress);

// Check for specific region membership
bool isUSResident = identityRegistry.hasRegion(walletAddress, 840); // United States
bool isEUResident = identityRegistry.hasRegion(walletAddress, 276); // Germany

// Example: Check for multi-jurisdiction compliance
bool canTradeGlobally = identityRegistry.hasRegion(investor, 840) || // US
identityRegistry.hasRegion(investor, 826) || // UK
identityRegistry.hasRegion(investor, 276); // Germany

3. AML/KYC Status Validation

// Check current AML/KYC status (considers validity duration)
bool isCompliant = identityRegistry.isAmlKycPassed(walletAddress);

// This function performs automatic time-based validation:
// - Returns true only if amlKycPassed == true AND status hasn't expired
// - Expiration calculated as: lastAmlKycChangeTimestamp + amlKycValidityDuration

4. Accreditation Level Queries

// Get accreditation type
uint256 accredLevel = identityRegistry.accreditationType(walletAddress);

// Common accreditation levels:
// 0 = No accreditation (retail investor)
// 1 = Retail investor
// 2 = Accredited investor
// 3 = Qualified investor
// 4 = Institutional investor

Advanced Query Patterns

Multi-Condition Compliance Checking:

function checkInvestorEligibility(address investor) public view returns (bool eligible) {
// Must have AML/KYC approval
if (!identityRegistry.isAmlKycPassed(investor)) return false;

// Must be from allowed region
if (!identityRegistry.hasRegion(investor, 840)) return false; // US only

// Must have minimum accreditation
if (identityRegistry.accreditationType(investor) < 2) return false; // Accredited or higher

return true;
}

Region-Based Token Type Determination:

function determineTokenType(address investor) public view returns (uint256 tokenType) {
if (identityRegistry.hasRegion(investor, 840)) {
// US investor - check accreditation
uint256 accreditation = identityRegistry.accreditationType(investor);
return accreditation >= 2 ? 2 : 3; // RegD if accredited, RegCF if retail
} else {
// Non-US investor
return 1; // RegS
}
}

Batch Identity Verification:

function batchCheckCompliance(address[] memory investors) 
public view returns (bool[] memory results) {
results = new bool[](investors.length);

for (uint256 i = 0; i < investors.length; i++) {
results[i] = identityRegistry.isAmlKycPassed(investors[i]);
}
}

Time-Based Validation Notes

AML/KYC Validity Duration Behavior:

  • Duration = 0: AML/KYC status never expires (permanent until manually revoked)
  • Duration > 0: Status expires after specified seconds from lastAmlKycChangeTimestamp
  • Automatic Expiration: isAmlKycPassed() returns false for expired status even if amlKycPassed is true

Example of Time-Based Validation:

// Check validity duration setting
uint256 duration = identityRegistry.amlKycValidityDuration(); // e.g., 31536000 (1 year)

// Get identity info to check timestamps
IdentityInfo memory info = identityRegistry.identity(investor);

// Manual calculation (for reference - the contract does this automatically)
bool isExpired = (duration > 0) &&
(block.timestamp > info.lastAmlKycChangeTimestamp + duration);

// This matches what isAmlKycPassed() returns
bool isValid = info.amlKycPassed && !isExpired;

Implementation Notes

  • Upgrade Safety: IdentityRegistry can be upgraded independently of the token contract using the upgradeIdentityRegistry() function
  • Interface Validation: The system validates that any new IdentityRegistry contract implements the required IIdentityRegistry interface
  • Event Emission: All identity changes emit events for compliance monitoring and audit trails