• SettleMintSettleMint
    • Introduction
    • Market pain points
    • Lifecycle platform approach
    • Platform capabilities
    • Use cases
    • Compliance & security
    • Glossary
    • Core component overview
    • Frontend layer
    • API layer
    • Blockchain layer
    • Data layer
    • Deployment layer
    • System architecture
    • Smart contracts
      • SMART protocol (ERC-3643)
      • Asset contracts
      • Identity & compliance
      • Addon modules
      • Factory patterns & upgradeability
    • Application layer
    • Data & indexing
    • Integration & operations
    • Performance
    • Quality
    • Getting started
    • Asset issuance
    • Platform operations
    • Troubleshooting
    • Development environment
    • Code structure
    • Smart contracts
    • API integration
    • Data model
    • Deployment & ops
    • Testing and QA
    • Developer FAQ
Back to the application
  1. Documentation
  2. Architecture
  3. Smart contracts

Identity and compliance system - OnchainID and KYC/AML workflow

The identity and compliance system ensures regulatory adherence throughout the DALP lifecycle. Built on ERC-734/735 OnchainID standards and modular compliance rules, it enables sophisticated identity verification and flexible enforcement for transfers, DvP settlements, and redemptions across all regulated asset types.

The identity and compliance architecture provides regulatory enforcement at every stage of the Digital Asset Lifecycle Protocol (DALP). When a transfer executes, a DvP settlement locks funds, or a redemption pays out principal, the compliance engine validates that all participants hold the required claims and meet the configured rules. This pre-execution validation ensures non-compliant operations never reach the blockchain state, protecting issuers from regulatory violations and investors from invalid transactions.

Compliance in the DALP lifecycle

Every DALP operation—transfer, issuance, redemption, DvP settlement—passes through the compliance engine before execution. The engine loads participant identities from the registry, evaluates each configured module sequentially, and reverts the entire operation if any rule fails. This architecture embeds regulatory requirements directly into the protocol layer rather than relying on off-chain processes.

Rendering chart...

DvP settlement integration: When a DvP smart contract executes, both the security token transfer and the payment transfer must pass their respective compliance checks. The atomic settlement only completes when both sides validate successfully, preventing partial settlements that could create regulatory or operational failures.

Yield distribution compliance: Automated dividend and coupon distributions respect compliance rules. When the yield engine calculates payouts, it only transfers funds to recipients whose identities remain valid and compliant. Identities that lost required claims (expired KYC, sanctioned status) automatically skip distribution until compliance restores.

Redemption enforcement: Bond and deposit redemptions validate that the recipient identity maintains all required claims before releasing principal. This prevents redeemed funds from reaching non-compliant addresses even when the original purchase predated current compliance requirements.

The ERC-3643 standard

ERC-3643 defines the security token standard that the SMART Protocol implements. Originally developed by Tokeny and adopted by the Ethereum community, the standard specifies interfaces for identity-bound tokens that enforce transfer restrictions through modular compliance rules.

Core specifications:

  • Identity binding: Every token holder must have an OnchainID (ERC-734/735 identity contract) registered in the token's identity registry
  • Compliance validation: Transfers execute only when the compliance contract returns true for all configured modules
  • Claim-based permissions: Identity contracts hold claims issued by trusted authorities (KYC providers, regulators, issuers)
  • Modular architecture: Compliance rules live in separate module contracts that the compliance engine orchestrates

The standard separates concerns between token state management (balances, allowances), identity registry (address-to-identity mapping), and compliance enforcement (rule evaluation), enabling flexible configurations per jurisdiction and asset type without modifying core token logic.

ATK extends ERC-3643 with SMART Protocol enhancements including logical expressions for claim verification, supply limit tracking, and investor counting across multiple tokens.

OnchainID protocol integration

OnchainID provides the decentralized identity layer for ERC-3643 compliance. Built on ERC-734 (key management) and ERC-735 (claim management) standards, OnchainID contracts serve as on-chain identity repositories that hold cryptographically-signed claims from trusted issuers.

Identity contract structure:

Rendering chart...

Key types and purposes: ERC-734 defines four key purposes that control identity operations. Management keys (purpose 1) add and remove other keys. Action keys (purpose 2) execute operations on behalf of the identity. Claim signer keys (purpose 3) sign and approve claims. Encryption keys (purpose 4) decrypt data sent to the identity. Each key type uses a numerical identifier that smart contracts validate when authorizing operations.

Claim structure: ERC-735 claims contain a topic identifier (what the claim certifies), the issuer's signature, the claim data (arbitrary bytes), and a URI pointing to additional proof. Topic identifiers follow the keccak256 hash of descriptive strings—topic keccak256("KYC") certifies identity verification, topic keccak256("AML") certifies anti-money-laundering checks, topic keccak256("ACCREDITED_INVESTOR") certifies investor accreditation status.

Claim verification flow: When a compliance module evaluates an identity, it queries the identity contract for claims matching required topics. The identity contract returns the claim data and issuer address. The module then validates that the issuer address appears in the token's trusted issuer registry for that topic. This two-step validation ensures both that the claim exists and that a trusted authority issued it.

OnchainID extensions and composition

ATK provides modular OnchainID extensions that compose into complete identity implementations. Rather than monolithic contracts, these building blocks allow customization for specific requirements while maintaining ERC-734/735 compliance.

Core building blocks:

ComponentPurposeLocationGitHub Link
ERC734KeyPurposesKey purpose constants (MANAGEMENT, ACTION, CLAIM, ENCRYPTION)ConstantsView on GitHub
ERC734KeyTypesCryptographic key type constants (ECDSA, RSA)ConstantsView on GitHub
ERC735ClaimSchemesSignature scheme constants (ECDSA, RSA, CONTRACT)ConstantsView on GitHub
ERC734Key management implementationExtensionView on GitHub
ERC735Claim management implementationExtensionView on GitHub
OnChainIdentityStandard user identityImplementationView on GitHub
OnChainContractIdentityContract identity with direct issuanceImplementationView on GitHub
OnChainIdentityWithRevocationIdentity supporting claim revocationImplementationView on GitHub
ClaimAuthorizationExtensionProgrammatic claim authorization logicExtensionView on GitHub
IContractIdentityContract identity interfaceInterfaceView on GitHub
IClaimAuthorizerClaim authorizer interfaceInterfaceView on GitHub
Rendering chart...

Composition pattern: Identity implementations inherit from extension contracts that each manage specific functionality. OnChainIdentity combines ERC734 key management with ERC735 claim management. OnChainContractIdentity adds the issueClaimTo function that allows the contract itself to issue claims to other identities without going through the authorization system. OnChainIdentityWithRevocation extends both standards with claim revocation capabilities using the ClaimAuthorizationExtension.

Claim issuance mechanisms

The system supports two distinct mechanisms for issuing claims, each serving different authorization models and operational patterns.

Direct contract issuance: Contract identities using OnChainContractIdentity can issue claims directly to other identities using the issueClaimTo method. This bypasses the authorization system and allows smart contracts to programmatically issue claims. Use cases include automated claim issuance based on on-chain events, system-generated claims for protocol operations, and temporary claims for specific transaction contexts.

// Contract identity issuing a claim directly
IContractIdentity contractIdentity = IContractIdentity(contractOnchainID);
contractIdentity.issueClaimTo(
    targetIdentity,     // Address of identity contract receiving the claim
    topicId,            // keccak256 hash of claim topic (e.g., keccak256("KYC"))
    claimData,          // Arbitrary claim data (ABI-encoded details)
    proofUri            // IPFS or HTTPS URI to additional proof documents
);

Trusted issuer authorization: Human or organizational KYC providers add claims directly to identity contracts through the trusted issuer registry. The identity contract validates the issuer's authorization by checking whether the caller's address appears in the registry for the claim topic before accepting the claim.

// Step 1: Register KYC provider as trusted issuer for specific topic
ITrustedIssuersRegistry(registry).addTrustedIssuer(
    IClaimIssuer(kycProviderAddress),
    [KYC_TOPIC_ID]  // Array of topic IDs this issuer can certify
);

// Step 2: Register the registry as a claim authorizer on the identity
IIdentity(userIdentity).registerClaimAuthorizationContract(registry);

// Step 3: KYC provider adds claim directly to the identity
// The identity validates authorization by querying the registry
IIdentity(userIdentity).addClaim(
    KYC_TOPIC_ID,                      // Topic identifier
    1,                                  // ECDSA signature scheme
    kycProviderAddress,                 // Issuer address
    signature,                          // Cryptographic signature over claim data
    kycData,                            // ABI-encoded claim details
    "https://kyc-proof.example.com"     // URI to proof documentation
);

Claim authorization system: The ClaimAuthorizationExtension provides programmatic control over claim management for trusted issuers. This system controls which trusted issuers can add claims through the registry—it does not control contract identities using issueClaimTo directly.

The extension maintains mappings of authorized addresses for adding and removing claims, provides topic-specific control for fine-grained permissions, supports revocable authorization that can be granted and revoked dynamically, and integrates with trusted issuer registries for centralized authorization management.

Monitor claim issuance patterns through the identity registry dashboard in the observability stack. The dashboard tracks claims added per topic, issuer activity patterns, and authorization validation failures, helping operators identify suspicious claim patterns or misconfigured authorization rules.

Identity verification flow

The complete identity verification flow spans off-chain KYC processes, on-chain claim issuance, and runtime validation during token operations. Understanding this flow explains how compliance rules connect to real-world identity verification.

Rendering chart...

Off-chain verification: Users submit identity documents to KYC providers through secure channels. The provider performs identity verification, AML screening, sanctions checks, and jurisdiction-specific compliance checks. Once verification completes, the provider prepares claim data containing verification results, expiration dates, and jurisdiction information.

On-chain claim issuance: The KYC provider's Ethereum address (registered as a trusted issuer in the token's trusted issuer registry) calls the addClaim function on the user's OnchainID contract. The identity contract validates that the caller is authorized for the claim topic by querying registered claim authorization contracts. If authorized, the identity stores the claim with the issuer's signature and emits a ClaimAdded event.

Identity registration: The user (or an administrator) registers the wallet address to identity contract mapping in the token's identity registry by calling registerIdentity. This mapping persists throughout the token's lifetime and connects the user's transaction wallet to their verified identity claims.

Runtime validation: When the user initiates a token transfer, the token contract queries its compliance contract. The compliance contract retrieves both sender and recipient identities from the registry, loads required claims from each identity contract, validates that trusted issuers issued those claims, and evaluates additional rules (transfer limits, investor counts, holding periods). Only when all validations pass does the token contract execute the state change.

Use the compliance validation dashboard to monitor verification flow metrics including claim issuance latency (time from KYC completion to on-chain claim), identity registration rates, and validation failure reasons. These metrics help identify bottlenecks in the onboarding flow and common compliance issues that require process improvements.

Compliance module architecture

The compliance system separates orchestration (the compliance contract) from rule enforcement (individual compliance modules). This architecture enables sharing a single compliance infrastructure across multiple tokens while each token configures its own specific rules.

Rendering chart...

Separation of concerns: The token contract defines what compliance is required by configuring modules and parameters. The SMARTCompliance contract orchestrates rule evaluation and maintains per-token module configurations. Individual modules implement specific compliance rules with their own state management. This separation allows upgrading module implementations without touching token contracts and enables reusing module instances across hundreds of tokens while maintaining isolated configurations.

Module lifecycle: When an issuer deploys a new token, they configure which compliance modules apply and set initial parameters for each module. The compliance contract stores these configurations in mappings indexed by token address and module address. During transfers, the compliance contract iterates through configured modules for that token and invokes each module's canTransfer function with the token address, sender, recipient, amount, and token-specific parameters. If any module returns false, the compliance contract immediately reverts the transaction with the module's reason string. After successful transfers, the compliance contract invokes optional transferred hooks that modules use to update state (investor counts, supply tracking, time locks).

Configuration management: Module parameters encode as ABI-encoded bytes that the compliance contract passes to modules during evaluation. This flexible encoding supports simple parameters (country codes, investor limits) and complex structures (logical expressions, per-country limits, exemption rules). Modules define their own parameter structures and implement the validateParameters function that the compliance contract calls before accepting configuration changes. This validation prevents misconfigurations that could inadvertently block all transfers or allow non-compliant ones.

Monitor module performance through the compliance execution dashboard showing average gas consumption per module type, module evaluation latency percentiles (p50, p95, p99), and frequent rejection reasons by module. High gas consumption or latency indicates modules requiring optimization. Frequent rejections for specific modules suggest misconfigured rules or onboarding process issues.

Transfer validation sequence

Understanding the exact sequence of operations during transfer validation clarifies how compliance rules interact with the DALP lifecycle and where to diagnose issues.

Rendering chart...

Pre-transfer validation: The token contract's transfer or transferFrom function immediately calls the compliance contract's canTransfer function before any state changes. This ensures that validation failures cost only the gas for identity lookups and module evaluation, not the full transfer execution gas.

Identity resolution: The compliance contract queries the identity registry to map wallet addresses to OnchainID contract addresses for both sender and recipient. If either mapping does not exist, the compliance check fails immediately with "Identity not registered" unless the transfer qualifies for exemptions (some modules exempt zero addresses for token burns or specific system addresses).

Sequential module evaluation: The compliance contract iterates through modules in the order they were configured. Each module's canTransfer function receives the token address (allowing the module to load token-specific configuration), sender and recipient addresses, transfer amount, and encoded parameters. Modules return a boolean result—false immediately reverts the entire transaction with a descriptive reason string, while true continues to the next module. This fail-fast approach minimizes gas consumption for non-compliant transfers.

Claim validation: Identity verification modules query the sender's and recipient's OnchainID contracts for required claims. The module validates that claims exist for required topics, that trusted issuers issued those claims (by checking the trusted issuer registry), that claims have not expired (by comparing block.timestamp to claim expiration), and that claim data matches expected values (for claims containing specific flags or codes). Complex expressions (using AND, OR, NOT operators) evaluate recursively, with short-circuit evaluation stopping as soon as the result is determined.

State update hooks: After the token contract successfully executes the balance transfer, it calls the compliance contract's transferred function. The compliance contract forwards this notification to all configured modules. Modules use this hook to update tracking state such as incrementing investor counts when a new holder receives tokens for the first time, adding to supply accumulation for period-based limits, recording token acquisition timestamps for time lock tracking, and consuming single-use transfer approvals. These state updates never revert—they must succeed or the entire transfer reverts.

Use the transaction trace viewer in the observability stack to debug failed transfers. The viewer displays the complete call graph showing which module rejected the transfer, the specific claim check that failed, the identity contract addresses queried, and the gas consumption per contract call. This visibility accelerates troubleshooting compared to decoding raw transaction revert reasons.

Compliance module categories

ATK provides eleven compliance module implementations covering the most common regulatory requirements. Each module focuses on a single concern, allowing issuers to compose combinations that match their specific jurisdiction and asset class requirements.

Rendering chart...

All modules inherit from AbstractComplianceModule, which implements the ISMARTComplianceModule interface and provides common functionality for access control, event emission, and parameter decoding. Individual modules override the canTransfer and optionally the transferred, created, and destroyed functions to implement their specific logic.

Country-based restrictions

Country modules control which jurisdictions can hold tokens based on ISO 3166-1 numeric country codes stored in identity claims. The identity registry storage maintains a mapping from identity addresses to country codes, which KYC providers set during identity verification.

CountryAllowListComplianceModule: Only investors from explicitly listed countries can receive tokens. The module's canTransfer function loads the recipient's country code from the identity registry storage and checks whether it appears in the configured allow list. Transfers to unlisted countries revert with "Country not allowed". Use cases include restricted offerings for specific jurisdictions (US and UK only), regional compliance requirements (EU member states only), and export control compliance (technology transfer restrictions).

// Configuration structure
struct Config {
    uint16[] allowedCountries;  // ISO 3166-1 numeric codes
}

// Example: Only allow US (840) and UK (826)
Config memory config = Config({
    allowedCountries: [840, 826]
});

CountryBlockListComplianceModule: Investors from explicitly listed countries cannot receive tokens. The module checks the recipient's country code against the block list and reverts with "Country blocked" for matches. Use cases include sanctions compliance (OFAC sanctioned countries, EU sanctions lists), regulatory restrictions (jurisdictions where the security is not registered), and risk management (high-risk jurisdictions for AML purposes).

// Example: Block sanctioned countries
Config memory config = Config({
    blockedCountries: [
        408,  // North Korea
        364,  // Iran
        760   // Syria
    ]
});

Country modules require that the identity registry storage properly maps identities to country codes. The KYC provider sets these codes during identity verification by calling setCountry on the identity registry storage contract. Monitor country distribution through the compliance metrics dashboard showing token holders per country code, blocked transfer attempts per country, and country code update frequency. These metrics help identify geographic concentration risks and sanctions compliance effectiveness.

Identity-based restrictions

Identity modules control access at the identity contract level, implementing allowlists, blocklists, and sophisticated claim-based verification logic.

IdentityAllowListComplianceModule: Only specific identity contracts can hold tokens. The module maintains a per-token mapping of allowed identity addresses and blocks transfers to identities not explicitly added to the list. Use cases include private placements to specific investors (Series A funding rounds), institutional-only offerings (qualified institutional buyers), and closed investor groups (family offices, strategic investors).

IdentityBlockListComplianceModule: Specific identity contracts are permanently blocked from holding tokens. Use cases include compliance violations (investors who failed ongoing KYC renewal), legal disputes (contested ownership, bankruptcy), and risk management (suspicious activity detected).

AddressBlockListComplianceModule: Specific wallet addresses are blocked regardless of their identity mapping. This module operates at the address level rather than identity level, useful for sanctions compliance (OFAC SDN wallet addresses), fraud prevention (addresses linked to hacks or scams), and temporary restrictions (administrative holds pending investigation). Unlike identity blocklists, address blocklists persist even if the user remaps their identity to a different address.

SMARTIdentityVerificationComplianceModule: Advanced claim-based verification using logical expressions in postfix notation (Reverse Polish Notation). This module evaluates complex requirements combining multiple claims with AND, OR, and NOT operators.

Logical expression evaluation: The module processes expression arrays containing TOPIC nodes (representing required claims), AND nodes (both operands must be true), OR nodes (at least one operand must be true), and NOT nodes (invert the following operand). Expression arrays follow postfix notation where operators appear after their operands, enabling stack-based evaluation without parentheses.

Postfix expression examples:

RequirementPostfix Notation
KYC AND AML[KYC, AML, AND]
CONTRACT OR (KYC AND AML)[CONTRACT, KYC, AML, AND, OR]
ACCREDITED OR (KYC AND AML AND JURISDICTION)[ACCREDITED, KYC, AML, AND, JURISDICTION, AND, OR]
(INSTITUTION AND APPROVAL) OR (INDIVIDUAL AND KYC AND AML)[INSTITUTION, APPROVAL, AND, INDIVIDUAL, KYC, AML, AND, AND, OR]
KYC AND NOT SANCTIONED[KYC, SANCTIONED, NOT, AND]
// Configuration structure
struct Config {
    ExpressionNode[] requiredExpression;  // Postfix expression
}

// Solidity construction for: CONTRACT OR (KYC AND AML)
// Postfix: [CONTRACT, KYC, AML, AND, OR]
ExpressionNode[] memory expression = new ExpressionNode[](5);
expression[0] = ExpressionNode(ExpressionType.TOPIC, CONTRACT_TOPIC_ID);
expression[1] = ExpressionNode(ExpressionType.TOPIC, KYC_TOPIC_ID);
expression[2] = ExpressionNode(ExpressionType.TOPIC, AML_TOPIC_ID);
expression[3] = ExpressionNode(ExpressionType.AND, 0);
expression[4] = ExpressionNode(ExpressionType.OR, 0);

Evaluation algorithm: The module uses a boolean stack to evaluate postfix expressions. For TOPIC nodes, it checks whether the identity holds a valid claim for that topic (issued by a trusted issuer and not expired) and pushes true or false onto the stack. For AND nodes, it pops two values, computes the logical AND, and pushes the result. For OR nodes, it pops two values, computes the logical OR, and pushes the result. For NOT nodes, it pops one value, inverts it, and pushes the result. After processing all nodes, the stack contains a single boolean—true if the identity meets requirements, false otherwise.

This expression system enables flexible entity types (contracts, institutions, individuals with different verification levels), regulatory efficiency (accredited investors may bypass full KYC in some jurisdictions), DeFi compatibility (smart contracts can hold tokens without individual KYC when expressions include CONTRACT claims), multi-jurisdiction support (different claim combinations per jurisdiction), and dynamic requirements (add new claim types without modifying module code).

Use the claim verification dashboard to monitor expression evaluation statistics including evaluation time per expression complexity, common failure patterns by claim topic, and claim expiration frequency. High evaluation times for complex expressions may indicate opportunities to simplify requirements or cache intermediate results.

Transfer and supply modules

Transfer and supply modules enforce quantitative limits on token issuance and holder distribution, implementing regulatory caps and investor protection rules.

TokenSupplyLimitComplianceModule: Enforces maximum token supply limits based on jurisdictional caps. This module implements three limit types, each serving different regulatory frameworks.

Limit types:

  • LIFETIME: Total supply cap across the token's entire lifetime. Once the cumulative issued supply reaches the limit, no additional tokens can be minted. Used for absolute regulatory caps (MiCA EUR 8M limit for asset-referenced tokens).
  • FIXED_PERIOD: Cap within specific fixed periods (calendar months, quarters, years). The module resets the accumulated supply at the start of each new period. Used for periodic issuance limits (quarterly fundraising caps, annual offering limits).
  • ROLLING_PERIOD: Cap within rolling time windows that move continuously. The module tracks supply issued within the last N days from the current timestamp. Used for continuous monitoring requirements (rolling 12-month issuance limits, sliding window compliance).

Currency conversion: The module supports base currency limits using on-chain price claims. When useBaseCurrency is true, the module loads the token's current price claim from the trusted issuer registry (topic keccak256("PRICE")), converts the token amount to base currency value, and checks whether the base currency equivalent exceeds the configured limit. This enables compliance with EUR or USD denominated limits for tokens with fluctuating prices.

// Configuration structure
struct Config {
    uint256 maxSupply;              // Maximum token supply (logical units)
    LimitType limitType;            // LIFETIME | FIXED_PERIOD | ROLLING_PERIOD
    uint256 periodLength;           // Period length in days (for period-based limits)
    bool useBaseCurrency;           // Whether to enforce base currency limits
    uint256 maxBaseCurrencyValue;   // Maximum value in base currency
    uint8 baseCurrencyDecimals;     // Decimals for base currency calculations
}

// Example: MiCA EUR 8M lifetime limit
Config memory config = Config({
    maxSupply: type(uint256).max,    // No token amount limit
    limitType: LimitType.LIFETIME,
    periodLength: 0,
    useBaseCurrency: true,
    maxBaseCurrencyValue: 8_000_000, // EUR 8M
    baseCurrencyDecimals: 2          // Cents precision
});

State tracking: The module maintains per-token supply tracking state including totalIssued (cumulative tokens issued across all time), periodStart (timestamp when current period started, for period-based limits), periodIssued (tokens issued in current period), and rollingWindow (mapping from day index to tokens issued that day, for rolling period calculations).

The created hook updates these counters when tokens are minted. The canTransfer function blocks minting operations (transfers from the zero address) when limits would be exceeded.

Monitor supply utilization through the issuance metrics dashboard showing supply consumed versus limit by token, remaining capacity before hitting limits, issuance velocity (tokens per day trend), and limit reset timestamps for period-based configurations. These metrics help issuers plan additional offerings and avoid hitting limits unexpectedly.

InvestorCountComplianceModule: Restricts the number of unique investors who can hold tokens, implementing regulatory requirements for private placements and crowdfunding offerings.

Global and per-country limits: The module tracks investors both globally (total unique holders) and per-country (unique holders from each jurisdiction). Configuration specifies maxInvestors for the global limit (0 means no global limit), whether to track globally across all issuer tokens or per-token, and arrays of country codes and corresponding per-country limits.

Expression filtering: The topicFilter expression determines which investors count towards limits. Only holders whose identities satisfy the filter expression increment the investor count. This enables sophisticated counting rules like "count only non-accredited investors towards the 100 investor limit" or "count institutional investors separately from retail investors".

// Configuration structure
struct Config {
    uint256 maxInvestors;             // Maximum total investors (0 = no global limit)
    bool global;                      // Track globally across all issuer tokens
    uint16[] countryCodes;            // Country codes for per-country limits
    uint256[] countryLimits;          // Corresponding limits per country
    ExpressionNode[] topicFilter;     // Filter which investors to count
}

// Example: Max 100 non-accredited US investors
ExpressionNode[] memory filter = new ExpressionNode[](3);
filter[0] = ExpressionNode(ExpressionType.TOPIC, US_INVESTOR_TOPIC);
filter[1] = ExpressionNode(ExpressionType.TOPIC, ACCREDITED_TOPIC);
filter[2] = ExpressionNode(ExpressionType.NOT, 0);
filter[3] = ExpressionNode(ExpressionType.AND, 0);

Config memory config = Config({
    maxInvestors: 100,
    global: false,
    countryCodes: [840],  // US
    countryLimits: [100],
    topicFilter: filter
});

Critical distinction: The topicFilter determines which investors are COUNTED towards limits, not which investors are BLOCKED. Investors who do not match the filter can still receive tokens but will not count towards investor limits. To block non-qualifying investors, use SMARTIdentityVerificationComplianceModule alongside this module with the same expression criteria.

Investor TypeIdentity ModuleCount ModuleFinal Result
No KYC+AML❌ BlockedN/A (never counted)❌ Transfer blocked
Has KYC+AML, count < 100✅ Allowed✅ Counted✅ Transfer allowed
Has KYC+AML, count = 100✅ Allowed❌ Blocked (over limit)❌ Transfer blocked

State tracking: The module maintains investorCount (total unique investors holding nonzero balance), countryInvestorCount (mapping from country code to investor count for that country), and investorCountryRegistered (mapping tracking whether an investor already counted for a country). The transferred hook increments counts when a new investor receives their first tokens and decrements counts when an investor's balance reaches zero.

Monitor investor capacity through the holder distribution dashboard showing current investor count versus limits by token, per-country investor counts with utilization percentages, and investor onboarding rate (new investors per day). These metrics help issuers plan marketing campaigns and avoid hitting investor limits during active sales periods.

TransferApprovalComplianceModule: Enforces pre-approved, identity-bound transfers by requiring explicit on-chain authorization before transfers execute. This module implements Japanese FSA compliance requirements and other jurisdictions requiring issuer or intermediary involvement in secondary sales.

Identity-bound approvals: Approvals bind to identity contracts, not wallet addresses. When an approval authority authorizes a transfer from identity A to identity B, the approval remains valid even if either party changes their wallet address mapping. This prevents bypassing approvals through address changes.

One-time use: Each approval can be configured for single-use execution. When oneTimeUse is true, the transferred hook marks the approval as consumed after successful transfer. Subsequent attempts with the same parameters fail with "Approval already used". This prevents approval reuse for regulatory compliance.

Approval expiry: Time-limited approvals prevent indefinite authorizations. Each approval includes an expiration timestamp. The canTransfer function checks block.timestamp against the expiration and rejects expired approvals with "Approval expired". Default expiry is configurable per token.

Exemption support: Specific identities (e.g., qualified institutional investors) can bypass approval requirements. The module evaluates the exemptionExpression against both sender and recipient identities. If either identity satisfies the expression, the transfer proceeds without checking for approvals.

Authority model: Configuration specifies identity addresses that can grant approvals. Only these authorities can call approveTransfer successfully. Typical authorities include the issuer's identity contract, registered transfer agents' identity contracts, and licensed intermediaries' identity contracts.

// Configuration structure
struct Config {
    address[] approvalAuthorities;        // Identity addresses allowed to grant approvals
    bool allowExemptions;                 // Whether exemptions are enabled
    ExpressionNode[] exemptionExpression; // Expression defining exemption logic
    uint256 approvalExpiry;               // Default expiry in seconds
    bool oneTimeUse;                      // Whether approvals are single-use
}

// Workflow example
// 1. Configure approval authorities (issuer identity address)
// 2. User attempts transfer → ❌ FAILS (no approval exists)
// 3. Authority calls approveTransfer(token, fromIdentity, toIdentity, amount, expiry)
// 4. User retries same transfer → ✅ SUCCEEDS (approval now exists)
// 5. If oneTimeUse=true, approval marked consumed
// 6. Subsequent attempts → ❌ FAIL (approval consumed)

Monitor approval workflows through the transfer authorization dashboard showing pending approval requests (transfers that failed with "Approval required"), approval latency (time from request to approval grant), expired approval frequency, and authority activity patterns (which authorities process most approvals). These metrics help identify bottlenecks in the approval process and measure compliance with processing time SLAs.

Time-based modules

TimeLockComplianceModule: Enforces minimum holding periods before tokens can be transferred, implementing Regulation D lock-up periods, insider trading restrictions, and vesting schedules.

FIFO token tracking: The module tracks multiple token batches per holder with acquisition timestamps using a first-in-first-out accounting system. When a holder acquires tokens, the module records the amount and block.timestamp as a new batch. When the holder transfers tokens, the module consumes batches in chronological order—oldest batches first—until the transfer amount is satisfied.

Configurable hold periods: The holdPeriod parameter specifies the minimum time in seconds that tokens must be held before transfer. Typical configurations include 180 days for Regulation D Rule 506(b) securities, 365 days for Regulation S restricted securities, and custom periods for insider lock-ups or vesting schedules.

Exemption support: Identity-based exemptions allow specific holders to bypass holding period requirements. The exemptionExpression uses the same postfix notation as identity verification modules. Typical exemptions include qualified institutional buyers (QIBs) under Rule 144A, registered broker-dealers for market-making activities, and the issuer itself for corporate actions.

// Configuration structure
struct Config {
    uint256 holdPeriod;                  // Minimum holding period in seconds
    bool allowExemptions;                // Whether to allow exemptions
    ExpressionNode[] exemptionExpression; // Logical expression for exemptions
}

// Example: 6-month lock-up with exemptions for accredited investors
Config memory config = Config({
    holdPeriod: 180 days,
    allowExemptions: true,
    exemptionExpression: [
        ExpressionNode(ExpressionType.TOPIC, ACCREDITED_TOPIC_ID)
    ]
});

FIFO algorithm: The transferred hook manages batch addition and removal. When a recipient receives tokens, the module appends a new batch (amount, block.timestamp) to their batch array. When a sender transfers tokens, the module iterates through their batches from oldest to newest, subtracting the transfer amount from each batch until fully satisfied. If a batch is partially consumed, the module updates the batch amount. If fully consumed, the module removes the batch and continues to the next.

The canTransfer function calculates the sender's unlocked balance by iterating through batches and summing amounts where block.timestamp >= acquisitionTimestamp + holdPeriod. The transfer fails if the amount exceeds the unlocked balance with "Insufficient unlocked balance".

Monitor time lock status through the holding period dashboard showing distribution of locked token amounts by time remaining, upcoming unlock events (tokens becoming transferable in next 7/30/90 days), and average holding period by investor cohort. These metrics help issuers plan liquidity events and communicate unlock schedules to investors.

Creating custom compliance modules

The modular architecture enables developers to implement domain-specific compliance rules by inheriting from AbstractComplianceModule and overriding specific functions.

Required implementations:

contract CustomComplianceModule is AbstractComplianceModule {
    /// Return human-readable module name
    function name() external pure override returns (string memory) {
        return "Custom Compliance Module";
    }

    /// Return unique module type identifier (keccak256 of module name)
    function typeId() external pure override returns (bytes32) {
        return keccak256("CustomComplianceModule");
    }

    /// Validate parameter structure before configuration acceptance
    function validateParameters(bytes calldata params) external pure override {
        // Decode parameters and validate constraints
        // Revert with descriptive message if invalid
    }

    /// Evaluate whether transfer complies with module rules
    function canTransfer(
        address token,
        address from,
        address to,
        uint256 value,
        bytes calldata params
    ) external view override returns (bool) {
        // Load module configuration from params
        // Query identity registry for identities
        // Evaluate module-specific rules
        // Return true if compliant, false otherwise
    }
}

Optional lifecycle hooks:

/// Called after successful transfer (state updates)
function transferred(
    address token,
    address from,
    address to,
    uint256 value,
    bytes calldata params
) external override {
    // Update module state tracking
    // Increment counters, record timestamps, etc.
}

/// Called after token minting (creation)
function created(
    address token,
    address to,
    uint256 value,
    bytes calldata params
) external override {
    // Update state for newly created tokens
}

/// Called after token burning (destruction)
function destroyed(
    address token,
    address from,
    uint256 value,
    bytes calldata params
) external override {
    // Update state for burned tokens
}

Development workflow:

  1. Define the compliance requirement and parameter structure
  2. Implement the module contract inheriting from AbstractComplianceModule
  3. Write comprehensive unit tests covering edge cases
  4. Deploy the module contract (single global instance serves all tokens)
  5. Register the module type in the compliance contract
  6. Configure tokens to use the module with token-specific parameters

Best practices:

  • Keep canTransfer gas-efficient—this function executes for every transfer
  • Validate all parameters in validateParameters to prevent misconfigurations
  • Emit events from lifecycle hooks to enable off-chain monitoring
  • Document parameter structures and provide example configurations
  • Consider exemption patterns for system operations (minting, burning)
  • Test interaction with existing modules in common configurations

Integration patterns

Common compliance configurations combine multiple modules to implement complete regulatory frameworks for specific jurisdictions and asset types.

US Regulation D private placement:

// Module 1: US investors only
CountryAllowListConfig memory countryConfig = CountryAllowListConfig({
    allowedCountries: [840]  // US only
});

// Module 2: Accredited investors OR (retail AND KYC AND AML)
ExpressionNode[] memory expression = new ExpressionNode[](5);
expression[0] = ExpressionNode(ExpressionType.TOPIC, ACCREDITED_TOPIC_ID);
expression[1] = ExpressionNode(ExpressionType.TOPIC, KYC_TOPIC_ID);
expression[2] = ExpressionNode(ExpressionType.TOPIC, AML_TOPIC_ID);
expression[3] = ExpressionNode(ExpressionType.AND, 0);
expression[4] = ExpressionNode(ExpressionType.OR, 0);

// Module 3: Max 99 non-accredited investors
ExpressionNode[] memory filter = new ExpressionNode[](2);
filter[0] = ExpressionNode(ExpressionType.TOPIC, ACCREDITED_TOPIC_ID);
filter[1] = ExpressionNode(ExpressionType.NOT, 0);

InvestorCountConfig memory countConfig = InvestorCountConfig({
    maxInvestors: 99,
    global: false,
    countryCodes: [],
    countryLimits: [],
    topicFilter: filter
});

// Module 4: 1-year holding period for unregistered securities
TimeLockConfig memory lockConfig = TimeLockConfig({
    holdPeriod: 365 days,
    allowExemptions: false,
    exemptionExpression: []
});

EU MiCA asset-referenced token:

// Module 1: EU member states only
CountryAllowListConfig memory countryConfig = CountryAllowListConfig({
    allowedCountries: [
        // EU member state codes (abbreviated for example)
        276,  // Germany
        250,  // France
        380,  // Italy
        // ... all 27 member states
    ]
});

// Module 2: KYC AND AML required for all investors
ExpressionNode[] memory expression = new ExpressionNode[](3);
expression[0] = ExpressionNode(ExpressionType.TOPIC, KYC_TOPIC_ID);
expression[1] = ExpressionNode(ExpressionType.TOPIC, AML_TOPIC_ID);
expression[2] = ExpressionNode(ExpressionType.AND, 0);

// Module 3: EUR 8M lifetime issuance limit
TokenSupplyLimitConfig memory supplyConfig = TokenSupplyLimitConfig({
    maxSupply: type(uint256).max,
    limitType: LimitType.LIFETIME,
    periodLength: 0,
    useBaseCurrency: true,
    maxBaseCurrencyValue: 8_000_000,  // EUR 8M
    baseCurrencyDecimals: 2
});

Japanese FSA compliant security token:

// Module 1: Identity verification with exemptions for QII
ExpressionNode[] memory expression = new ExpressionNode[](5);
expression[0] = ExpressionNode(ExpressionType.TOPIC, QII_TOPIC_ID);
expression[1] = ExpressionNode(ExpressionType.TOPIC, KYC_TOPIC_ID);
expression[2] = ExpressionNode(ExpressionType.TOPIC, AML_TOPIC_ID);
expression[3] = ExpressionNode(ExpressionType.AND, 0);
expression[4] = ExpressionNode(ExpressionType.OR, 0);

// Module 2: Transfer approval required (FSA compliance)
ExpressionNode[] memory exemption = new ExpressionNode[](1);
exemption[0] = ExpressionNode(ExpressionType.TOPIC, QII_TOPIC_ID);

TransferApprovalConfig memory approvalConfig = TransferApprovalConfig({
    approvalAuthorities: [issuerIdentity, transferAgentIdentity],
    allowExemptions: true,
    exemptionExpression: exemption,
    approvalExpiry: 30 days,
    oneTimeUse: true
});

Security considerations

Access control: Module configuration functions require COMPLIANCE_MANAGER_ROLE in the compliance contract. This role should be assigned to a multi-signature wallet or governance contract, not individual EOAs. Module state modification functions (adding to blocklists, granting approvals) enforce their own role-based access control inherited from AbstractComplianceModule.

Parameter validation: All modules validate parameters in the validateParameters function before the compliance contract accepts configuration changes. This validation prevents misconfigurations that could inadvertently block all transfers or allow non-compliant ones. For example, supply limit modules verify that period lengths are nonzero for period-based limits, and investor count modules verify that country code and limit arrays have matching lengths.

Reentrancy protection: Modules use the nonReentrant modifier on state-changing functions to prevent reentrancy attacks. The compliance contract enforces strict call ordering—canTransfer completes before the token contract executes the transfer, and transferred executes after the transfer completes but before returning to the caller.

Gas optimization: Modules optimize gas consumption by short-circuit evaluation (returning false immediately on first violation), efficient storage patterns (using mappings instead of arrays for lookups), and batch operations where possible (updating multiple tracking variables in a single transaction).

Upgrade safety: Modules store all per-token state indexed by token address, enabling module upgrades without migrating state. When upgrading a module implementation, deploy the new contract, configure tokens to use the new module address, and optionally migrate historical state using admin functions that copy data from the old module to the new one.

Monitoring and alerting: Configure observability dashboards to alert on suspicious patterns including sudden increases in compliance rejection rates, module configuration changes (especially to allowlists and limits), abnormal gas consumption by specific modules, and failed transactions from previously successful address pairs. These alerts help detect attacks or misconfigurations before they cause operational issues.

Operational monitoring

The observability stack provides comprehensive dashboards for monitoring compliance system health and performance. Access these dashboards through the Grafana interface deployed as part of the Helm chart installation.

Identity registry metrics:

  • Identity registration rate (new identities per day/hour)
  • Claim issuance latency (time from off-chain verification to on-chain claim)
  • Claim update frequency per topic
  • Identity-to-address remapping events
  • Failed registration attempts with reasons

Compliance validation metrics:

  • Transfer validation success rate by token
  • Rejection reasons by module type
  • Average validation time per transfer
  • Module evaluation gas consumption percentiles
  • Most frequently triggered modules

Module-specific metrics:

  • Supply utilization percentages vs configured limits
  • Investor count utilization by token and country
  • Time lock unlock schedules (tokens becoming transferable)
  • Approval request latency (time to grant or deny)
  • Country distribution of token holders

Alert configurations: Configure alerts through the Prometheus alert rules included in the Helm chart:

  • Compliance rejection rate exceeds 10% for any token (indicates misconfiguration or attack)
  • Supply utilization exceeds 90% of limit (warns before hitting cap)
  • Investor count within 5 of limit (warns before hitting cap)
  • Claim expiration for >10% of holders (indicates KYC renewal campaign needed)
  • Module configuration changes (audit trail for compliance officers)

Reference the observability documentation for dashboard access instructions and custom query examples.

Asset contracts
Addon modules
llms-full.txt

On this page

Compliance in the DALP lifecycleThe ERC-3643 standardOnchainID protocol integrationOnchainID extensions and compositionClaim issuance mechanismsIdentity verification flowCompliance module architectureTransfer validation sequenceCompliance module categoriesCountry-based restrictionsIdentity-based restrictionsTransfer and supply modulesTime-based modulesCreating custom compliance modulesIntegration patternsSecurity considerationsOperational monitoring