Building Compliant Security Tokens on Stellar with ERC-3643: A Developer's Guide to Permissioned Assets
Stellar's membership in the ERC-3643 Association, combined with the network surpassing $2 billion in tokenized real-world assets, signals a clear direction: Stellar is becoming the preferred settlement layer for regulated, permissioned tokens. For developers, this means understanding how to issue compliant security tokens, manage permissioned transfers, and query on-chain compliance state through APIs.
What Is ERC-3643
ERC-3643, also known as T-REX (Token for Regulated EXchanges), is a standard originally created for Ethereum that defines how security tokens should handle identity verification, transfer restrictions, and compliance enforcement on-chain.
The standard has three core components:
| Component | Purpose | Stellar Equivalent |
|---|---|---|
| Identity Registry | Maps wallet addresses to verified identities | Issuer account + SEP-0012 KYC |
| Compliance Contract | Enforces transfer rules (jurisdiction, holding period, investor limits) | Authorization flags + Soroban contracts |
| Token Contract | The security token itself | Native Stellar asset |
Stellar does not implement ERC-3643 identically to Ethereum. Instead, Stellar's native asset model provides several of the same guarantees at the protocol level, and Soroban smart contracts fill the gaps.
Stellar's Native Compliance Features
Asset Authorization Flags
Every Stellar asset issuer can set flags that control how the asset behaves:
const StellarSdk = require('@stellar/stellar-sdk');
const server = new StellarSdk.Horizon.Server('https://horizon.stellar.org');
// Configure an issuer account with compliance flags
async function configureIssuer(issuerKeypair) {
const account = await server.loadAccount(issuerKeypair.publicKey());
const tx = new StellarSdk.TransactionBuilder(account, {
fee: '100',
networkPassphrase: StellarSdk.Networks.PUBLIC,
})
.addOperation(StellarSdk.Operation.setOptions({
setFlags:
StellarSdk.AuthRequiredFlag |
StellarSdk.AuthRevocableFlag |
StellarSdk.AuthClawbackEnabledFlag,
}))
.setTimeout(30)
.build();
tx.sign(issuerKeypair);
return server.submitTransaction(tx);
}Flag Behavior Matrix
| Flag | Effect | Use Case |
|---|---|---|
| AUTH_REQUIRED | Holders must be approved by issuer before receiving | KYC enforcement |
| AUTH_REVOCABLE | Issuer can freeze individual accounts | Regulatory freeze orders |
| AUTH_CLAWBACK_ENABLED | Issuer can seize tokens from any holder | Court orders, compliance enforcement |
| AUTH_IMMUTABLE | Flags cannot be changed once set | Lock configuration permanently |
Trustline Authorization Flow
With AUTH_REQUIRED enabled, every new holder must be explicitly approved:
// Investor creates a trustline (requests to hold the asset)
async function createTrustline(investorKeypair, assetCode, issuerPublicKey) {
const account = await server.loadAccount(investorKeypair.publicKey());
const asset = new StellarSdk.Asset(assetCode, issuerPublicKey);
const tx = new StellarSdk.TransactionBuilder(account, {
fee: '100',
networkPassphrase: StellarSdk.Networks.PUBLIC,
})
.addOperation(StellarSdk.Operation.changeTrust({ asset }))
.setTimeout(30)
.build();
tx.sign(investorKeypair);
return server.submitTransaction(tx);
}
// Issuer approves the trustline after KYC verification
async function approveTrustline(issuerKeypair, investorPublicKey, assetCode) {
const issuerAccount = await server.loadAccount(issuerKeypair.publicKey());
const asset = new StellarSdk.Asset(assetCode, issuerKeypair.publicKey());
const tx = new StellarSdk.TransactionBuilder(issuerAccount, {
fee: '100',
networkPassphrase: StellarSdk.Networks.PUBLIC,
})
.addOperation(StellarSdk.Operation.setTrustLineFlags({
trustor: investorPublicKey,
asset,
flags: {
authorized: true,
authorizedToMaintainLiabilities: false,
},
}))
.setTimeout(30)
.build();
tx.sign(issuerKeypair);
return server.submitTransaction(tx);
}Adding ERC-3643 Compliance with Soroban
Stellar's native flags handle the basics. But ERC-3643 compliance requires additional logic: transfer restrictions between verified investors, holding period enforcement, and investor count limits. This is where Soroban contracts come in.
Compliance Contract Architecture
Identity Registry (Soroban) --> Compliance Module (Soroban) --> Token Issuer Logic (Off-chain)
Maps addresses to claims Evaluates transfer rules Approves/denies trustlinesExample: Identity Registry Contract
// Soroban contract: Identity Registry (simplified)
#[contract]
pub struct IdentityRegistry;
#[contractimpl]
impl IdentityRegistry {
pub fn register_identity(
env: Env,
investor: Address,
jurisdiction: Symbol,
accreditation_level: u32,
kyc_expiry: u64,
) {
let admin: Address = env.storage().instance()
.get(&symbol!("admin")).unwrap();
admin.require_auth();
env.storage().persistent().set(&investor, &IdentityClaim {
jurisdiction,
accreditation_level,
kyc_expiry,
registered_at: env.ledger().timestamp(),
});
}
pub fn can_transfer(
env: Env,
from: Address,
to: Address,
_amount: i128,
) -> bool {
let from_claim: IdentityClaim = env.storage().persistent()
.get(&from).unwrap();
let to_claim: IdentityClaim = env.storage().persistent()
.get(&to).unwrap();
let now = env.ledger().timestamp();
// Rule 1: Both parties must have valid KYC
if from_claim.kyc_expiry < now || to_claim.kyc_expiry < now {
return false;
}
// Rule 2: US accredited investors only
if to_claim.jurisdiction == symbol!("US")
&& to_claim.accreditation_level < 1 {
return false;
}
// Rule 3: 12-month holding period
let holding_period = 365 * 24 * 60 * 60;
if now - from_claim.registered_at < holding_period {
return false;
}
true
}
}Querying Compliant Asset State
List All Holders of a Permissioned Asset
const HORIZON = 'https://horizon.stellar.org';
async function getAuthorizedHolders(assetCode, issuerPublicKey) {
const res = await fetch(
`${HORIZON}/accounts?asset=${assetCode}:${issuerPublicKey}&limit=200`
);
const data = await res.json();
return data._embedded.records.map(account => {
const trustline = account.balances.find(
b => b.asset_code === assetCode && b.asset_issuer === issuerPublicKey
);
return {
accountId: account.id,
balance: trustline?.balance,
authorized: trustline?.is_authorized,
frozen: !trustline?.is_authorized &&
trustline?.is_authorized_to_maintain_liabilities,
};
}).filter(h => h.balance);
}Monitor Authorization Changes
Track when the issuer approves, freezes, or claws back assets:
async function monitorAuthorizationChanges(issuerPublicKey) {
const res = await fetch(
`${HORIZON}/accounts/${issuerPublicKey}/operations?limit=200&order=desc`
);
const data = await res.json();
return data._embedded.records
.filter(op =>
op.type === 'set_trust_line_flags' ||
op.type === 'allow_trust' ||
op.type === 'clawback'
)
.map(op => ({
type: op.type,
trustor: op.trustor,
asset: op.asset_code,
timestamp: op.created_at,
transactionHash: op.transaction_hash,
}));
}Real-World Asset Categories on Stellar
The $2B+ RWA milestone includes multiple asset categories:
| Category | Examples | Compliance Level |
|---|---|---|
| Government bonds | U.S. Treasuries, EU bonds | High (accredited investors) |
| Corporate bonds | Investment-grade debt | High |
| Real estate | Tokenized property shares | Medium-High |
| Fund shares | ETF tokens, money market funds | High |
| Carbon credits | Verified emission reductions | Medium |
Each category has different compliance requirements, but the Stellar infrastructure (auth flags + Soroban contracts) supports all of them.
How LumenQuery Helps
LumenQuery provides the API infrastructure to build and monitor compliant security tokens:
*Build compliant security tokens on reliable infrastructure. LumenQuery provides managed Horizon API and Soroban RPC with compliance monitoring tools built in. Start free.*