RWA & STO Deep Dive Series #04

Under the Hood of Permissioned Tokens: ERC-3643 (T-REX) Compliance Engine & ONCHAINID

Published: August 28, 2026 • 15 min read

1. Why Standard ERC-20 Fails for Securities

The standard ERC-20 token specification was designed under the philosophy of permissionless open access: anyone holding a private key can transfer tokens to any arbitrary Ethereum address (0x...) with zero restrictions.

Why ERC-20 Violates Securities Law

  • No KYC/AML Check at Runtime: Tokens can be transferred to sanctioned individuals (OFAC list) or non-accredited retail buyers.
  • Inability to Enforce Jurisdictional Caps: Cannot restrict maximum token holders per country (e.g., US 2,000 shareholder cap under Exchange Act Rule 12g-1).
  • No Asset Recovery Mechanism: If an investor loses their private key or a court issues a freeze/seizure order, tokens cannot be legally reissued or burned.

To solve this without sacrificing public blockchain interoperability, the Ethereum community finalized ERC-3643 (formerly T-REX: Token for Regulated EXchanges).

2. The 4-Pillar Architecture of ERC-3643

ERC-3643 decouples the security token into four interconnected smart contracts:

Core Token IERC3643

1. Token Contract

Extends standard ERC-20 interfaces for balance queries and transfers, but intercepts every transfer() and transferFrom() call to enforce mandatory compliance checks before altering balances.

Identity Registry IIdentityRegistry

2. Identity Registry

Maintains the mapping between user wallet addresses (0x...), their sovereign identity smart contract (ONCHAINID), and their ISO country code.

Issuer Registry IClaimTopicsRegistry

3. Claim Topics & Trusted Issuers

Defines which verified claim topics (e.g., Topic 1 = KYC Cleared, Topic 7 = Accredited Investor) and which authorized trusted KYC providers (e.g., Securitize, Synaps) are valid for this token.

Rule Engine ICompliance

4. Modular Compliance Contract

A pluggable rule engine that evaluates portfolio-level and market-level rules (e.g., maximum tokens per investor, holding period locks, nationality transfer restrictions).

3. ONCHAINID: Verifiable Claims without Exposing PII

Storing personal data (names, social security numbers, passport copies) on a public blockchain violates privacy laws like GDPR and PIPA. ERC-3643 resolves this by leveraging ONCHAINID (based on ERC-734 / ERC-735):

// On-Chain Identity Verification Scheme
1. Investor conducts off-chain KYC with a Trusted KYC Provider.
2. KYC Provider signs an attestation hash: keccak256(InvestorAddress, ClaimTopic, Expiration).
3. The signature hash is stored in the investor's ONCHAINID smart contract.
4. Result: Zero plain text PII on-chain. Smart contracts verify only cryptographic signatures!

4. The Transfer Execution Lifecycle (Step-by-Step)

When Alice calls token.transfer(Bob, 1000), the token contract executes the following verification sequence before updating ledger balances:

  1. Address Validation: Checks that Alice and Bob are both registered in the IdentityRegistry.
  2. Claim Verification: Reads Bob's ONCHAINID to verify that Bob holds valid, unexpired claims issued by a trusted KYC provider for all required ClaimTopics.
  3. Compliance Check: Invokes compliance.canTransfer(Alice, Bob, 1000) to evaluate dynamic rules (e.g., maximum investor limits, lockup schedules).
  4. State Update & Notification: If and only if all checks return true, balances are updated and compliance.transferred(Alice, Bob, 1000) is triggered to update state counters.

5. Modular Compliance Engine in Solidity

Here is a simplified architectural implementation of how the canTransfer and transfer hooks are constructed in Solidity:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface ICompliance {
    function canTransfer(address _from, address _to, uint256 _amount) external view returns (bool);
    function transferred(address _from, address _to, uint256 _amount) external;
}

contract ERC3643Token {
    IIdentityRegistry public identityRegistry;
    ICompliance public compliance;
    mapping(address => uint256) private _balances;

    function transfer(address _to, uint256 _amount) public returns (bool) {
        require(identityRegistry.isVerified(_to), "Receiver not KYC verified");
        require(compliance.canTransfer(msg.sender, _to, _amount), "Compliance rule violation");

        _balances[msg.sender] -= _amount;
        _balances[_to] += _amount;

        compliance.transferred(msg.sender, _to, _amount);
        return true;
    }
}
G

Giri (Dong-gil Nam)

Backend Software Engineer

Passionate about building scalable systems and sharing technical insights. Specializing in JVM internals, distributed systems, and performance optimization.