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
- Create a regulated ERC-20 token in the unified token registry
- Register ERC-3643 compliance rules (KYC, holder limits, country blocks)
- Check transfers against compliance rules before execution
- Freeze and unfreeze specific addresses
- Integrate with TDIP identity for automated KYC tier verification
- Build a complete compliance workflow for institutional tokens
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
| Rule | Description |
|---|---|
kyc_required | Both sender and recipient must have a verified TDIP identity with KYC tier ≥ 1 |
holder_limit | Maximum number of unique addresses that can hold the token (0 = unlimited) |
country_restrictions | ISO 3166-1 alpha-2 codes of countries blocked from holding the token |
balance_cap | Maximum 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 limitTypeScript
// 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
- Token creation -- creating ERC-20 tokens in the unified registry
- ERC-3643 compliance -- registering KYC, holder limits, country restrictions, and balance caps
- Transfer checks -- validating transfers against compliance rules before execution
- Address freezing -- freeze/unfreeze for enforcement actions
- TDIP integration -- linking token compliance to decentralized identity KYC tiers
Next Steps
- Build an NFT Marketplace -- cross-VM NFT trading with escrow
- Canton Institutional Repo -- DvP settlement for regulated assets
- Build an AML Agent -- automated compliance monitoring
Build More with Tenzro
Explore compliance, token, and identity capabilities in the SDK documentation.