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 viagrantAmlKyc()
orrevokeAmlKyc()
- supports custom timestamps or defaults toblock.timestamp
lastAccreditationChangeTimestamp
: Updated when accreditation type changes viagrantAccreditation()
orrevokeAccreditation()
- supports custom timestamps or defaults toblock.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
exceedslastAmlKycChangeTimestamp + amlKycValidityDuration
Connection with RestrictedLockupToken
The RestrictedLockupToken
integrates with IdentityRegistry
to enforce compliance-based transfer restrictions:
- Integration Point: The token contract holds a reference to the IdentityRegistry via the
identityRegistry
property - Transfer Validation: During every token transfer, the system validates both sender and recipient AML/KYC status
- Compliance Enforcement: Transfers are automatically blocked if either party fails AML/KYC verification
- 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:
Function | Description | Access Control |
---|---|---|
setIdentity(owner, info) | Set complete identity information with IdentityInfo struct (supports custom timestamps) | Wallets Admin |
removeIdentity(owner) | Remove all identity information for a wallet | Wallets Admin |
setRegions(owner, regions[]) | Replace entire regions array | Wallets Admin |
addRegion(owner, region) | Add a single region to existing regions | Wallets Admin |
removeRegion(owner, region) | Remove a specific region from regions array | Wallets Admin |
grantAmlKyc(owner, timestamp) | Mark wallet as AML/KYC passed with optional custom timestamp | Wallets Admin |
revokeAmlKyc(owner, timestamp) | Mark wallet as AML/KYC failed with optional custom timestamp | Wallets Admin |
grantAccreditation(owner, level, timestamp) | Set accreditation level with optional custom timestamp | Wallets Admin |
revokeAccreditation(owner, timestamp) | Remove accreditation with optional custom timestamp | Wallets Admin |
setAmlKycValidityDuration(duration) | Set global AML/KYC validity duration in seconds | Contract Admin |
Timestamp Functionality
Custom Timestamp Support: The IdentityRegistry functions support flexible timestamp management for audit trails and compliance tracking:
-
setIdentity(owner, info)
: TheIdentityInfo
struct includeslastAmlKycChangeTimestamp
andlastAccreditationChangeTimestamp
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)
andrevokeAmlKyc(owner, timestamp)
:timestamp
parameter: when non-zero, uses the custom timestamp; when 0, usesblock.timestamp
- Updates the
lastAmlKycChangeTimestamp
field in the wallet's identity record
-
Accreditation Functions -
grantAccreditation(owner, level, timestamp)
andrevokeAccreditation(owner, timestamp)
:timestamp
parameter: when non-zero, uses the custom timestamp; when 0, usesblock.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:
Function | Description | Returns | Example Usage |
---|---|---|---|
identity(owner) | Get complete identity information | IdentityInfo struct | IdentityInfo memory info = identityRegistry.identity(wallet); |
regions(owner) | Get all regions for a wallet | uint256[] memory | uint256[] memory walletRegions = identityRegistry.regions(wallet); |
hasRegion(owner, region) | Check if wallet has specific region | boolean | bool hasUS = identityRegistry.hasRegion(wallet, 840); |
isAmlKycPassed(owner) | Check AML/KYC status with validity duration | boolean | bool verified = identityRegistry.isAmlKycPassed(wallet); |
accreditationType(owner) | Get accreditation level | uint256 | uint256 level = identityRegistry.accreditationType(wallet); |
amlKycValidityDuration() | Get global AML/KYC validity duration | uint256 | uint256 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()
returnsfalse
for expired status even ifamlKycPassed
istrue
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