ERC-3643 (T-REX)
Status: Technical spec — planning phase Author: Axia Team Date: 2026-06-01 Decided scope:
- Assets: NFTs (ERC-721) + Crowdfunding/CreditCore (fungible)
- Target chain: Polygon / L2 (EVM)
- Custody: Custodial (current model maintained)
- Identity provisioning: Lazy / Just-In-Time, pulling from CPM at mint time
- Contract standard choice: per collection, at the end of the crowdfunding (pure
ERC721vsERC-3643 / ERC-721-compliant) - The model smart contract will be developed in-house and audited by a third party before production.
1. Objective
Allow the issuer, when minting NFTs/tokens at the close of a crowdfunding, to choose the contract standard:
- Standard (current): pure ERC-721 (NFT) or
whitelabelERC-20 (fungible) — no on-chain compliance. - Regulated: ERC-3643 (fungible) or ERC-721-compliant (an NFT that queries the same identity layer as 3643) — with on-chain identity + transfer enforcement.
All orchestration lives in the TokenController. The on-chain identity is provisioned lazy/JIT in the mint job, pulling KYC from the CustomerProfileService via cpm-lib.
2. Architecture principles
- Shared identity layer, built once. ONCHAINID + Identity Registry (+ Storage) + Trusted Issuers Registry + Claim Topics Registry are deploy-once per chain. They serve the fungible 3643 token and the ERC-721-compliant token simultaneously.
- Token-contract deploy-per-collection. Each regulated crowdfunding deploys its own Token + Modular Compliance via factory, reusing the shared registries.
- CPM is the source of truth for KYC. The TokenController never duplicates identity data — it queries
cpm-libat mint. - Lazy/JIT. It only pays identity gas (ONCHAINID + claims + registration) for whoever effectively receives a regulated asset.
- Custodial preserved.
mainWalletkeeps paying gas; custodial wallets (TKN_OWNER, sinks) are registered as institutional identities. - Idempotency by
(userId, chainId)on identity and bytxHashon on-chain transactions (patterns that already exist).
Webhook DIDIT/BRLA TokenController (mint job, lazy)
status=APPROVED ──┐ │
(NÃO registra) │ 1. lê asset/collection.contract_standard
│ 2. se ERC3643/ERC721_COMPLIANT:
cpm-lib ◀───────┼────── 3. ensureIdentity(userWallet):
(KYC, cpf, │ - já na Identity Registry? skip
país, PEP) │ - não → deploy ONCHAINID + sign claims
│ (Axia = Trusted Issuer) + register
│ 4. mint no contrato (reverte se não verificado)
▼
┌─────────────────────── CAMADA COMPARTILHADA (deploy-once / chain) ──────────────┐
│ IdentityRegistry + IdentityRegistryStorage │
│ TrustedIssuersRegistry (Axia) + ClaimTopicsRegistry (KYC, COUNTRY, PEP, ...) │
│ ModularCompliance (módulos: transfer-restrict, country-allow, max-holders, ...) │
└────────────────▲───────────────────────────────────▲─────────────────────────────┘
│ consultam │ consultam
┌──────────┴───────────┐ ┌───────────┴────────────┐
│ Token ERC-3643 │ │ ERC-721-compliant │
│ (Crowdfunding/Credit)│ │ (NFTs) │
└──────────────────────┘ └────────────────────────┘3. Smart Contracts (audited track)
This is the item with the highest lead time and highest risk. Strategy: reuse as much as possible of Tokeny's audited T-REX suite and audit only what is custom.
Status (2026-06-01):
AxiaCompliantERC721.solimplemented, compiling and tested inEniato/Backend/TokenController/packages/processor/contracts/— Hardhat (solc 0.8.28, evmVersion cancun), 23 green tests, 100% stmts/lines/funcs, solhint clean. Ready for hardening (Foundry/Slither) and external audit.
3.1 Repository and toolchain
- New directory:
Eniato/Backend/TokenController/packages/processor/contracts/(replaces the legacysol/). - Toolchain: Hardhat (TS, aligned with the stack) + Foundry for fuzzing/invariants on the custom suite.
- Solidity: ^0.8.20; OpenZeppelin Contracts v5 (or v4.9 if needed for compatibility with T-REX upstream — fix in phase 0).
- Build output (ABI + bytecode) versioned in
contracts/artifacts/and consumed by the deploy tool (same asNFT.jsontoday).
Status (2026-06-01) — 3643 suite integrated and validated: dedicated unit in
Eniato/Backend/TokenController/packages/processor/contracts-trex/(Hardhat, solc 0.8.17, OZ v4 — separated from the custom OZ v5 unit due to OpenZeppelin version incompatibility). 73 files compile; 5 end-to-end integration tests green proving: deploy viaTREXFactory→ ONCHAINID + KYC claim signed by Axia (trusted issuer) → registration → mint (verified vs blocked) → transfer (verified vs blocked) → freeze. Full reuse of Tokeny (incl.TREXGatewayready — custom gateway dispensed with).
3.2 REUSED contracts (audited by Tokeny — do not rewrite)
| Contract | Origin | Note |
|---|---|---|
Token (ERC-3643) | @tokenysolutions/t-rex | Regulated fungible token |
IdentityRegistry / IdentityRegistryStorage | T-REX | Shared deploy-once |
TrustedIssuersRegistry / ClaimTopicsRegistry | T-REX | Shared deploy-once |
ModularCompliance | T-REX | Host of the modules |
Identity (ONCHAINID) | @onchain-id/solidity | ERC-734/735, 1 per investor |
TREXFactory / TREXImplementationAuthority | T-REX | Deterministic deploy of the suite |
Rewriting these contracts is the worst possible mistake in a regulated asset. Use the audited artifacts.
3.3 CUSTOM contracts (need auditing)
AxiaCompliantERC721.sol— the heart of the new work. An ERC-721 (OZ) that, in_update/_beforeTokenTransfer, queries the sameIdentityRegistryandIModularComplianceas 3643:solidity// pseudo — sujeito ao desenho final e à auditoria function _update(address to, uint256 tokenId, address auth) internal override returns (address from) { from = super._update(to, tokenId, auth); if (from != address(0) && to != address(0)) { // ignora mint/burn na checagem de transfer require(_identityRegistry.isVerified(to), "AXIA721: receiver not verified"); require(_compliance.canTransfer(from, to, tokenId), "AXIA721: compliance"); } return from; }- Roles:
AGENT_ROLE(mint/burn/forcedTransfer),DEFAULT_ADMIN_ROLE. freeze(address),recover(oldWallet, newWallet)mirroring 3643 semantics.- Maps
block_resell/block_sellto compliance locks + per-token state (tokenFrozen).
- Roles:
- Custom compliance modules (if needed beyond Tokeny's standard ones): e.g.
CrowdfundingLockupModule(lock-up until settlement),JurisdictionModule(allowlist byCOUNTRYclaim). Prefer Tokeny's ready-made modules when they exist. AxiaTREXGateway.sol(optional) — a deploy wrapper that standardizes Axia parameters in theTREXFactory(default claim topics, default compliance), reducing the backend's error surface.
3.4 Audit scope and process
- In audit:
AxiaCompliantERC721, custom modules,AxiaTREXGateway, and the deploy configuration (parameters passed to the factory). - Out (already audited): T-REX/ONCHAINID core — cite versions and hashes in the report.
- Internal pre-audit: 100% test coverage (Hardhat + Foundry invariants), Slither/Mythril in CI, access checklist (who can mint/freeze/recover).
- External auditor (e.g.: firms recognized in security tokens). Typical lead time 3–6 weeks + correction window. Start early.
- Mainnet deploy only after the final report + corrections applied + re-review.
3.5 Key management (critical)
- The Trusted Issuer key (signs on-chain KYC claims) is an extremely high-value target: compromising it = forging KYC. Guard it with rigor ≥ the current
mnemonicParameter(ideally KMS/HSM, signing via a dedicated service, documented rotation). - The tokens'
AGENT_ROLE(mint/freeze/recover) likewise.
4. Data model (Liquibase + Entities)
Liquibase centralized in the DataInitializerService (see guide). TokenController group. Always VARCHAR, CREATE INDEX IF NOT EXISTS, granular rollback, registered in changelog.base.yml.
4.0 crowdfundings — optional regulated-offering flag (origin of the choice)
The choice is optional and per crowdfunding. The admin toggles a single "Regulated offering"; the system resolves the concrete contract_standard by crossing the flag with the asset type.
ALTER TABLE public.crowdfundings ADD COLUMN IF NOT EXISTS regulated BOOLEAN DEFAULT false;Entity: CrowdFundingsEntity (Common/Backend/CrowdfundingService/.../crowdfunding.entity.ts) gains regulated?: boolean.
Status (2026-06-01) — regulated flag implemented and wired. Liquibase
0027-add_regulated_to_crowdfunding.yml(DataInitializerService, groupcommon/crowdfunding, tablecrowdfunding_entries). Fieldregulatedincrowdfunding.entity.ts+crowdfunding.dto.ts(@DtoAttribute) +ICrowdfunding(crowdfunding-lib). Incategories.core.handler.ts, the collection creation (myTknLib.createCollections) resolvescontract_standard: crowdfunding?.regulated ? 'ERC721_COMPLIANT' : 'ERC721'. Chain origin closed: admin marks regulated → collection is born with contract_standard → scheduler deploys AxiaCompliantERC721 → mint provisions identity + mints compliant. Missing the toggle in the BackOffice form (UI) + i18n.
Resolution table (flag × asset type → contract_standard):
regulated | Asset type | contract_standard | Contract |
|---|---|---|---|
| false (default) | NFT | ERC721 | NFT.json (current, untouched) |
| false (default) | Fungible | ERC20 | whitelabel (current, untouched) |
| true (opt-in) | NFT | ERC721_COMPLIANT | AxiaCompliantERC721 |
| true (opt-in) | Fungible | ERC3643 | Token T-REX (Tokeny) |
Default = non-regulated → zero impact on existing offerings. The resolution runs at the close of the crowdfunding, when creating the collection, writing collections.contract_standard.
4.1 collections — new standard field
ALTER TABLE public.collections ADD COLUMN IF NOT EXISTS contract_standard VARCHAR(30) DEFAULT 'ERC721';
-- valores: 'ERC721' | 'ERC3643' | 'ERC721_COMPLIANT'
ALTER TABLE public.collections ADD COLUMN IF NOT EXISTS compliance_address VARCHAR(200) NULL; -- ModularCompliance do token
ALTER TABLE public.collections ADD COLUMN IF NOT EXISTS identity_registry_address VARCHAR(200) NULL; -- registry consultadaEntity: add contract_standard, compliance_address, identity_registry_address to CollectionsEntity.
4.2 New table — on-chain identity link (lazy idempotency)
CREATE TABLE IF NOT EXISTS public.onchain_identities (
id VARCHAR(200) PRIMARY KEY,
user_id VARCHAR(200) NOT NULL,
chain_id VARCHAR(50) NOT NULL,
wallet_address VARCHAR(200) NOT NULL,
onchainid_address VARCHAR(200) NULL, -- contrato ONCHAINID deployado
registry_address VARCHAR(200) NOT NULL, -- Identity Registry onde foi registrado
status VARCHAR(30) NOT NULL, -- PENDING | DEPLOYING | REGISTERED | REVOKED | ERROR
claims JSONB NULL, -- topics gravados (KYC, COUNTRY, PEP...)
last_tx_hash VARCHAR(200) NULL,
error_message TEXT NULL,
created_at TIMESTAMP DEFAULT now(),
last_update TIMESTAMP DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_onchain_identity_user_chain ON public.onchain_identities (user_id, chain_id);
CREATE INDEX IF NOT EXISTS idx_onchain_identity_wallet ON public.onchain_identities (wallet_address);4.3 New table — Trusted Issuer / Claim Topics config per chain
CREATE TABLE IF NOT EXISTS public.trex_chain_config (
id VARCHAR(200) PRIMARY KEY,
chain_id VARCHAR(50) NOT NULL UNIQUE,
identity_registry_addr VARCHAR(200) NOT NULL,
identity_storage_addr VARCHAR(200) NOT NULL,
trusted_issuers_addr VARCHAR(200) NOT NULL,
claim_topics_addr VARCHAR(200) NOT NULL,
trex_factory_addr VARCHAR(200) NOT NULL,
trusted_issuer_wallet VARCHAR(200) NOT NULL, -- endereço público do issuer Axia
claim_topics JSONB NOT NULL, -- [KYC, COUNTRY, PEP, ...]
enabled BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT now()
);DAO for each table: 5 mandatory methods (create, getById, deleteById, update, readAllRelations).
5. Contract deploy pipeline (TokenController)
Status (2026-06-01) — deploy dispatcher implemented.
actions.tools.tsrefactored into a dispatcher (resolveBaseContract(contract_standard)→nft-whitelabel | nft-3643 | erc3643):deployAxiaCompliantErc721(readssol/AxiaCompliantERC721.json, builds with shared IR/Compliance from the 3643 config) anddeployTRexSuite(callsTREXFactory.deployTREXSuiteviasol/TREXFactory.json, returns the token).process.bc.contract.transactions.tspassesresolveBaseContract(transaction.contract_standard). Fieldscontract_standard/compliance_address/ identity_registry_addressadded to thetkn-lib/collectionsmodel +CollectionsEntity.ITrexChainConfigextended (trexFactoryAddress,complianceAddress). ERC721_COMPLIANT mint-call (done):mint.service.tsnow chooses ABI+method by standard —ERC721_COMPLIANTusesAxiaCompliantERC721.mint(to, tokenId, uri)(ABI incore/utils/AxiaCompliantERC721.json), legacy keepsmintWithTokenURI. AGENT_ROLE checks out (mint signed by the mnemonicParameter = deploy admin). Compliant NFT transfers use the existingtransferNft(safeTransferFrom, enforcement in_update). Regulated NFT cycle closed: regulated flag → deploy → lazy provisioning → mint.Pending: theERC3643fungible mint istoken.mint(to, amount)— a flow distinct from the NFT, outsidemintNft(wherever fungible tokens are issued).
5.1 actions.tools.ts — real branching by baseContract
Today callFunction ignores baseContract and always deploys NFT.json. Refactor into a dispatcher:
public async callFunction(..., baseContract: string, name: string, symbol: string, ...): Promise<DeployResult> {
switch (baseContract) {
case 'nft-whitelabel': return this.deployErc721Plain(network, from, privateKey, name, symbol); // legado intacto
case 'erc3643': return this.deployTRexSuite({ network, signer, name, symbol, ...claimTopics, compliance });
case 'nft-3643': return this.deployAxiaCompliantErc721({ network, signer, name, symbol, identityRegistry, compliance });
default: throw new Error(`Unknown baseContract: ${baseContract}`);
}
}deployTRexSuitecallsTREXFactory.deployTREXSuite(salt, tokenDetails, claimDetails)→ returns{ token, ir, irs, tir, ctr, mc }. Persistcompliance_addressandidentity_registry_addresson the collection.deployAxiaCompliantErc721deploys the custom contract pointing to the chain's shared Identity Registry (fromtrex_chain_config) + its ownModularCompliance.- Keep the current gas/EIP-1559 pattern (
getFeeData, +20% margin, bump) already present in the file.
5.2 process.bc.contract.transactions.ts — choose by contract_standard
In the pending-collections deploy loop, read collection.contract_standard and map it to baseContract. XRPL/Solana remain untouched (3643 is EVM-only — if contract_standard != ERC721 on a non-EVM network, reject with a clear error).
6. Lazy identity layer (new module in the TokenController)
Status (2026-06-01) — implemented in the TokenController.
core/utils/identity/identity.provisioning.service.ts(real ABIs extracted from the suite intocore/utils/identity/abis/), DAOonchain.identity.dao.ts(5 methods + getByUserAndChain/setStatus), entity/dto, IoC (Utils.IdentityProvisioningService+Dao.OnchainIdentityDao),ConfigurationReaderService.getTrexParameters(chainId), Liquibase0068(collections.contract_standard) +0069(onchain_identities). Hook inmint.service.tscallsensureVerifiedIdentitybefore the regulated EVM mint. Idempotent by (user,chain) via unique constraint + status. Custodial: the agent is the management key (no investor key). Missing:contract_standardfield in the@axia/tkn-lib/models/collectionsmodel, deploy dispatcher inactions.tools.ts, and admin Controller/ClientLib (retry/status) — the core provisioning is internal to the mint.
6.1 IdentityProvisioningService
@Provide(injectionTokenConstant.Identity.ProvisioningService)
export class IdentityProvisioningService {
/**
* Garante que `walletAddress` está verificada na Identity Registry da chain.
* Idempotente por (userId, chainId). Chamado pelo mint job ANTES do mint 3643.
*/
public async ensureVerifiedIdentity(params: {
userId: string;
walletAddress: string;
network: Network;
}): Promise<{ onchainIdAddress: string; alreadyRegistered: boolean }>;
}Internal flow:
onchainIdentitiesDao.getByUserAndChain(userId, chainId)→ ifREGISTERED, return (skip).- Atomic lock
(userId, chainId)(the already-existingtryAcquireLockSET NX EX pattern). - Pull KYC from CPM:ts
const info = await this.cpmLib.getUserBasicInformationFromBasicInfo({ id: userId, sensitive: true }); // país (info.address.country), cpf/cnpj, status === APPROVED, PEP, dateOfBirth if (info.status !== 'APPROVED') throw new Error('User not KYC-approved — cannot register on-chain identity'); - Deploy ONCHAINID (the ONCHAINID lib's
Identity) for the wallet —idFactory.createIdentity(wallet, salt). - Sign claims as Axia Trusted Issuer: topic
KYC(andCOUNTRY=ISO of the country,PEPif applicable). Issuer ECDSA signature →identity.addClaim(topic, scheme, issuer, signature, data, uri). identityRegistry.registerIdentity(wallet, onchainId, countryCode)(agent = issuer/agent wallet).- Persist
onchain_identitieswithstatus=REGISTERED,claims,last_tx_hash. - Errors →
status=ERROR, log, and the mint for that recipient fails in a controlled way (does not block the entire batch).
6.2 Integration in mint.service.ts
In mintNft() / the fungible mint, before the on-chain call, if collection.contract_standard ∈ {ERC3643, ERC721_COMPLIANT}:
if (isRegulated(collection.contract_standard) && isEvmNetwork(network)) {
await this.identityProvisioning.ensureVerifiedIdentity({ userId: buyer.id, walletAddress: walletId, network });
// se custódia TKN_OWNER intermedia, garantir TKN_OWNER/sink também registrados (institucional)
}
// ... segue o mint atual (web3 sign + send) — agora reverte se não verificadoAttention points in the custodial pattern:
- If the mint goes to
TKN_OWNERand then transfers to the user, both need to be in the registry. Register the custodial mesh (TKN_OWNER, sinks, mainWallet) as institutional identities in a single bootstrap per chain. - 3643/compliant burn uses the agent's
burn()(aligns with the already-existingNATIVE_BURNv2 strategy).
7. Event watcher (evm.reprocess.service.ts)
Add decoding of the new events, keeping idempotency by txHash:
- 3643/registry:
IdentityRegistered,IdentityRemoved,TokensFrozen,TokensUnfrozen,AddressFrozen,RecoverySuccess,ComplianceAdded. - ERC-721-compliant:
Transfer(already handled) + customAddressFrozen/Recovery. - Reconcile on-chain ↔ off-chain state (e.g.:
onchain_identities.status,assets.block_resell).
8. ClientLib / Gateway / Controller
Complete delivery per feature (guide): Liquibase + Entity + DTO + Model + DAO + Handler + JSONIC + ClientLib + Controller + Postman + GitBook.
- ClientLib (
@axia/...of the TokenController): typed methodsact<MicroserviceRequest<T>, MicroserviceResponse<U>>for:setCollectionContractStandard,getIdentityStatus(userId, chainId),retryIdentityProvisioning,freezeInvestor,recoverWallet. - Gateway (REST API-Gateway): admin routes under
@AuthenticationRequired+ Swagger. - Controller: uses Model (never DTO),
@AuthenticationRequired, validatesuser.status === 'APPROVED'where applicable to a financial operation. - JSONIC tokens registered.
9. Frontend
9.1 General BackOffice (Common/Frontend/BackOffice/) — where the tokenization admin lives
- Contract standard selector in the collection configuration / crowdfunding close:
- Selection component:
ERC721 (standard)vsRegulated (ERC-3643 / ERC-721-compliant). - Do not expose raw "ERC721 vs ERC3643" — model it as a compliance toggle + asset type (fungible/NFT), and the backend resolves the
baseContract. - Irreversibility warning + secondary-market implication (transfer only to KYC wallets) in a confirmation modal.
- Default per asset class: regulated offerings may force regulated (locked field).
- Selection component:
- On-Chain Identities screen: status per investor (
PENDING/REGISTERED/REVOKED/ERROR), "reprovision" button, link to the explorer (network.scan_url). - Trusted Issuers / Claim Topics screen per chain (config of
trex_chain_config). - Compliance actions: freeze/unfreeze investor, token freeze, wallet recovery — with dual permission CPM + DB module and ideally
@StepUpRequired()since they are sensitive actions.
9.2 Midas-Web (investor) — minimum
- "Regulated Asset (ERC-3643)" badge on the investment/NFT detail.
- A clear message when a transfer/resale is blocked by compliance (e.g.: destination without KYC) — not a generic error.
- Reuse of the existing UI infra (no
<app-shell>wrapper; redesign patterns).
9.3 i18n (mandatory)
Every new string in pt-br.json, en.json, es.json (and the Midas-Web devops overrides: devops/{b4,tokeniza}/pt-br.json Core and Discord; Discord uses TAB). Nothing hardcoded.
10. Security
- Trusted Issuer key and
AGENT_ROLE: KMS/HSM, dedicated signing service, rotation. Compromise = forged KYC / undue mint. - Admin actions (freeze/recover/contract-standard): dual permission CPM + DB module +
@StepUpRequired(). - Validate
user.status === 'APPROVED'before provisioning identity and before a regulated mint. - Idempotency:
(userId, chainId)on identity;txHashon transactions. Avoid double-deploy of ONCHAINID under a race (atomic lock). - Off-chain auditing of the actions (AuditService) in addition to the on-chain events.