Tenzro Testnet is live —request testnet TNZO
← Back to Tutorials
ComplianceAdvanced

Build a Compliant Security Token

Create a fully compliant security token on Tenzro Network using the ERC-3643 (T-REX) standard. This tutorial covers creating an ERC-20 token, registering compliance rules (KYC requirements, holder limits, country restrictions), checking transfers against compliance, freezing and unfreezing addresses, and integrating with TDIP identities for KYC tier verification.

What We're Building

Prerequisites

  • Tenzro SDK installed
  • A funded wallet on testnet
  • Understanding of ERC-20 tokens and KYC concepts

Estimated time: 40 minutes

What is ERC-3643?

ERC-3643 (T-REX) is the standard for permissioned token transfers on EVM chains. It adds an identity and compliance layer on top of ERC-20, enabling issuers to enforce KYC requirements, holder limits, country restrictions, and balance caps at the protocol level. Every transfer is checked against the compliance rules before execution. Tenzro implements ERC-3643 via the ComplianceClient which works with the unified token registry.

Step 1: Create the Token

Create an ERC-20 token that will be subject to compliance rules. Use the TokenClient to register it in the unified token registry:

Rust

use tenzro_sdk::{TenzroClient, config::SdkConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = SdkConfig::testnet();
    let client = TenzroClient::connect(config).await?;
    let token = client.token();

    // Create a security token
    let token_info = token.create_token(
        "MetropolisToken",       // name
        "MTK",                    // symbol
        18,                       // decimals
        "1000000",                // initial supply (1M tokens)
        "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb4", // creator/issuer
    ).await?;

    println!("Token created!");
    println!("  Token ID: {}", token_info.token_id);
    println!("  Symbol: {}", token_info.symbol);
    println!("  EVM Address: {}", token_info.evm_address);
    println!("  Total Supply: {}", token_info.total_supply);

    Ok(())
}

TypeScript

import { TenzroClient, TESTNET_CONFIG } from "tenzro-sdk";

const client = new TenzroClient(TESTNET_CONFIG);

// Create a security token
const tokenInfo = await client.token.createToken({
  name: "MetropolisToken",
  symbol: "MTK",
  decimals: 18,
  initial_supply: "1000000",
  creator: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb4",
});

console.log("Token created!");
console.log("  Token ID:", tokenInfo.token_id);
console.log("  Symbol:", tokenInfo.symbol);
console.log("  EVM Address:", tokenInfo.evm_address);

Step 2: Register Compliance Rules

Attach ERC-3643 compliance rules to the token. Once registered, all transfers are subject to these rules:

Rust

let compliance = client.compliance();

// Register compliance rules for the token
let rules = compliance.register_compliance(
    "MTK",                              // token ID or symbol
    true,                               // KYC required
    500,                                // max 500 holders
    Some(&["KP", "IR", "CU", "SY"]),   // blocked countries
    Some("1000000000000000000000"),      // max 1000 tokens per holder
).await?;

println!("Compliance rules registered!");
println!("  KYC Required: {}", rules.kyc_required);
println!("  Holder Limit: {}", rules.holder_limit);
println!("  Blocked Countries: {:?}", rules.country_restrictions);
println!("  Balance Cap: {}", rules.balance_cap.unwrap_or_default());
println!("  Status: {}", rules.status);

TypeScript

// Register compliance rules
const rules = await client.compliance.registerCompliance(
  "MTK",                              // token ID or symbol
  true,                               // KYC required
  500,                                // max 500 holders
  ["KP", "IR", "CU", "SY"],          // blocked countries (ISO 3166-1 alpha-2)
  "1000000000000000000000",           // max 1000 tokens per holder (in wei)
);

console.log("Compliance rules registered!");
console.log("  KYC Required:", rules.kyc_required);
console.log("  Holder Limit:", rules.holder_limit);
console.log("  Blocked Countries:", rules.country_restrictions);

Compliance Rule Types

RuleDescription
kyc_requiredBoth sender and recipient must have a verified TDIP identity with KYC tier ≥ 1
holder_limitMaximum number of unique addresses that can hold the token (0 = unlimited)
country_restrictionsISO 3166-1 alpha-2 codes of countries blocked from holding the token
balance_capMaximum balance any single address can hold (in wei)

Step 3: Check Transfer Compliance

Before executing a transfer, check whether it complies with all registered rules. The compliance check evaluates KYC status, holder limits, country restrictions, balance caps, and frozen addresses:

Rust

// Check if a transfer is compliant
let check = compliance.check_compliance(
    "MTK",
    "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb4",   // sender
    "0xdAC17F958D2ee523a2206206994597C13D831ec7",   // recipient
    "100000000000000000000",                          // 100 tokens (in wei)
).await?;

println!("Compliance check:");
println!("  Compliant: {}", check.compliant);
if !check.compliant {
    println!("  Violations: {:?}", check.violations);
}

// Example: check a transfer that would exceed the balance cap
let over_cap = compliance.check_compliance(
    "MTK",
    "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb4",
    "0xdAC17F958D2ee523a2206206994597C13D831ec7",
    "2000000000000000000000",                         // 2000 tokens (exceeds 1000 cap)
).await?;

println!("Over-cap transfer:");
println!("  Compliant: {}", over_cap.compliant);       // false
println!("  Violations: {:?}", over_cap.violations);   // ["balance_cap_exceeded"]

TypeScript

// Check transfer compliance
const check = await client.compliance.checkCompliance(
  "MTK",
  "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb4",  // sender
  "0xdAC17F958D2ee523a2206206994597C13D831ec7",  // recipient
  "100000000000000000000",                         // 100 tokens
);

console.log("Compliant:", check.compliant);
if (!check.compliant) {
  console.log("Violations:", check.violations);
}

Step 4: Freeze and Unfreeze Addresses

Compliance officers can freeze addresses to prevent all transfers to or from a specific address:

Rust

// Freeze a suspicious address
let freeze = compliance.freeze_address(
    "MTK",
    "0xSuspiciousAddress1234567890abcdef12345678",
    "Suspicious activity detected -- pending investigation",
).await?;

println!("Address frozen: {}", freeze.status);

// Transfers from/to frozen addresses are now blocked
let blocked = compliance.check_compliance(
    "MTK",
    "0xSuspiciousAddress1234567890abcdef12345678",
    "0xdAC17F958D2ee523a2206206994597C13D831ec7",
    "1000000000000000000",
).await?;
println!("Transfer from frozen address: compliant={}", blocked.compliant); // false

// Unfreeze after investigation
let unfreeze = compliance.unfreeze_address(
    "MTK",
    "0xSuspiciousAddress1234567890abcdef12345678",
).await?;
println!("Address unfrozen: {}", unfreeze.status);

TypeScript

// Freeze address
const freeze = await client.compliance.freezeAddress(
  "MTK",
  "0xSuspiciousAddress1234567890abcdef12345678",
  "Suspicious activity -- pending investigation",
);
console.log("Frozen:", freeze.status);

// Unfreeze
const unfreeze = await client.compliance.unfreezeAddress(
  "MTK",
  "0xSuspiciousAddress1234567890abcdef12345678",
);
console.log("Unfrozen:", unfreeze.status);

Step 5: TDIP Identity Integration for KYC

When kyc_required is true, the compliance engine checks that both sender and recipient have a verified TDIP identity with a KYC tier of at least 1 (Basic). Here is how to set up identities for token holders:

Rust

let identity = client.identity();

// Register identity for token holder
let holder_id = identity.register_human("Bob (Token Holder)").await?;
println!("Holder DID: {}", holder_id.did);
// At this point, KYC tier is 0 (Unverified)

// In production, the KYC provider issues a verifiable credential
// that upgrades the KYC tier. For testnet, use the identity RPC:

// The compliance engine checks KYC status automatically:
// - KYC tier 0 (Unverified): blocked from receiving regulated tokens
// - KYC tier 1 (Basic): can hold up to $10,000 in regulated tokens
// - KYC tier 2 (Enhanced): can hold up to $100,000
// - KYC tier 3 (Full): no limit

TypeScript

// Register identity for token holder
const holderId = await client.identity.registerHuman("Bob (Token Holder)");
console.log("Holder DID:", holderId.did);

// Resolve to check KYC tier
const resolved = await client.identity.resolve(holderId.did);
console.log("KYC Tier:", resolved.kyc_tier); // 0 (Unverified)

// After KYC verification by an external provider, the tier is updated.
// The compliance check in check_compliance automatically queries the
// TDIP registry for sender/recipient KYC status.

Step 6: Complete Compliance Workflow

Here is a complete workflow combining token creation, compliance, identity, and transfer:

import { TenzroClient, TESTNET_CONFIG } from "tenzro-sdk";

async function main() {
  const client = new TenzroClient(TESTNET_CONFIG);

  // 1. Create security token
  const token = await client.token.createToken({
    name: "RegulatedFund",
    symbol: "RFND",
    decimals: 18,
    initial_supply: "1000000",
    creator: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb4",
  });
  console.log("Token:", token.symbol, "at", token.evm_address);

  // 2. Set compliance rules
  const rules = await client.compliance.registerCompliance(
    "RFND",
    true,          // KYC required
    200,           // max 200 holders
    ["KP", "IR"],  // blocked countries
    null,          // no balance cap
  );
  console.log("Compliance active:", rules.status);

  // 3. Register holder identities
  const alice = await client.identity.registerHuman("Alice (Issuer)");
  const bob = await client.identity.registerHuman("Bob (Investor)");
  console.log("Alice DID:", alice.did);
  console.log("Bob DID:", bob.did);

  // 4. Check compliance before transfer
  const preCheck = await client.compliance.checkCompliance(
    "RFND",
    "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb4",
    "0xdAC17F958D2ee523a2206206994597C13D831ec7",
    "500000000000000000000", // 500 tokens
  );

  if (preCheck.compliant) {
    console.log("Transfer is compliant -- executing...");
    // Execute the transfer via the token client
  } else {
    console.log("Transfer blocked:", preCheck.violations);
  }

  // 5. Freeze a compromised address
  await client.compliance.freezeAddress(
    "RFND",
    "0xCompromisedAddress0000000000000000000001",
    "Account compromise reported",
  );
  console.log("Address frozen for investigation");

  // 6. Query current compliance rules
  const currentRules = await client.compliance.getComplianceRules("RFND");
  console.log("Current rules:", JSON.stringify(currentRules, null, 2));
}

main().catch(console.error);

Production Considerations

KYC Provider Integration

In production, KYC verification is performed by an external provider (e.g., Jumio, Onfido, Sumsub) who issues a verifiable credential via the TDIP update_kyc_tier_with_credential method. The credential must be of type KycAttestation, signed by the KYC provider, and contain the appropriate tier claim. See the Identity documentation for details.

Canton Integration for Institutional Settlement

For institutional use cases requiring legal finality, combine ERC-3643 compliance with Canton/DAML settlement. Canton provides atomic delivery-versus-payment (DvP) with legal certainty. See the Canton Institutional Repo tutorial.

What You Learned

Next Steps

Build More with Tenzro

Explore compliance, token, and identity capabilities in the SDK documentation.