Skip to content

ERC-3643 (T-REX)

Status: Spec técnica — fase de planejamento Autor: Time Axia Data: 2026-06-01 Escopo decidido:

  • Ativos: NFTs (ERC-721) + Crowdfunding/CreditCore (fungível)
  • Chain alvo: Polygon / L2 (EVM)
  • Custódia: Custodial (modelo atual mantido)
  • Provisionamento de identidade: Lazy / Just-In-Time, puxando do CPM no momento do mint
  • Escolha do padrão de contrato: por coleção, no fim do crowdfunding (ERC721 puro vs ERC-3643 / ERC-721-compliant)
  • Smart contract modelo será desenvolvido in-house e auditado por terceiro antes de produção.

1. Objetivo

Permitir que, ao emitir NFTs/tokens no encerramento de um crowdfunding, o emissor escolha o padrão do contrato:

  • Padrão (atual): ERC-721 puro (NFT) ou ERC-20 whitelabel (fungível) — sem compliance on-chain.
  • Regulado: ERC-3643 (fungível) ou ERC-721-compliant (NFT que consulta a mesma camada de identidade do 3643) — com identidade on-chain + enforcement de transferência.

Toda a orquestração vive no TokenController. A identidade on-chain é provisionada lazy/JIT no job de mint, puxando KYC do CustomerProfileService via cpm-lib.


2. Princípios de arquitetura

  1. Camada de identidade compartilhada, construída uma vez. ONCHAINID + Identity Registry (+ Storage) + Trusted Issuers Registry + Claim Topics Registry são deploy-once por chain. Servem simultaneamente o token fungível 3643 e o ERC-721-compliant.
  2. Token-contract deploy-per-coleção. Cada crowdfunding regulado deploya seu próprio Token + Modular Compliance via factory, reusando os registries compartilhados.
  3. CPM é a fonte da verdade de KYC. O TokenController nunca duplica dado de identidade — consulta cpm-lib no mint.
  4. Lazy/JIT. Só paga gas de identidade (ONCHAINID + claims + registro) para quem efetivamente recebe ativo regulado.
  5. Custodial preservado. mainWallet segue pagando gas; wallets custodiais (TKN_OWNER, sinks) são registradas como identidades institucionais.
  6. Idempotência por (userId, chainId) na identidade e por txHash nas transações on-chain (padrões já existentes).
  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 (track auditado)

Este é o item de maior lead time e maior risco. Estratégia: reusar o máximo da suíte T-REX auditada da Tokeny e auditar apenas o que for custom.

Status (2026-06-01): AxiaCompliantERC721.sol implementado, compilando e testado em Eniato/Backend/TokenController/packages/processor/contracts/ — Hardhat (solc 0.8.28, evmVersion cancun), 23 testes verdes, 100% stmts/lines/funcs, solhint limpo. Pronto para hardening (Foundry/Slither) e auditoria externa.

3.1 Repositório e toolchain

  • Novo diretório: Eniato/Backend/TokenController/packages/processor/contracts/ (substitui o legado sol/).
  • Toolchain: Hardhat (TS, alinhado ao stack) + Foundry para fuzzing/invariantes na suíte custom.
  • Solidity: ^0.8.20; OpenZeppelin Contracts v5 (ou v4.9 se necessário p/ compat com T-REX upstream — fixar na fase 0).
  • Output de build (ABI + bytecode) versionado em contracts/artifacts/ e consumido pelo deploy tool (igual ao NFT.json hoje).

Status (2026-06-01) — suíte 3643 integrada e validada: unit dedicado em Eniato/Backend/TokenController/packages/processor/contracts-trex/ (Hardhat, solc 0.8.17, OZ v4 — separado do unit custom OZ v5 por incompatibilidade de versão do OpenZeppelin). 73 arquivos compilam; 5 testes de integração ponta a ponta verdes provando: deploy via TREXFactory → ONCHAINID + claim KYC assinado pela Axia (trusted issuer) → registro → mint (verificado vs bloqueado) → transfer (verificado vs bloqueado) → freeze. Reuso total da Tokeny (incl. TREXGateway pronto — gateway custom dispensado).

3.2 Contratos REUSADOS (auditados pela Tokeny — não reescrever)

ContratoOrigemObservação
Token (ERC-3643)@tokenysolutions/t-rexToken fungível regulado
IdentityRegistry / IdentityRegistryStorageT-REXDeploy-once compartilhado
TrustedIssuersRegistry / ClaimTopicsRegistryT-REXDeploy-once compartilhado
ModularComplianceT-REXHost dos módulos
Identity (ONCHAINID)@onchain-id/solidityERC-734/735, 1 por investidor
TREXFactory / TREXImplementationAuthorityT-REXDeploy determinístico da suíte

Reescrever esses contratos é o maior erro possível em ativo regulado. Usar os artefatos auditados.

3.3 Contratos CUSTOM (precisam de auditoria)

  1. AxiaCompliantERC721.sol — o coração do trabalho novo. ERC-721 (OZ) que, no _update/_beforeTokenTransfer, consulta a mesma IdentityRegistry e IModularCompliance do 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) espelhando semântica 3643.
    • Mapeia block_resell/block_sell para travas de compliance + estado por token (tokenFrozen).
  2. Módulos de compliance custom (se necessário além dos padrão da Tokeny): ex. CrowdfundingLockupModule (lock-up até liquidação), JurisdictionModule (allowlist por COUNTRY claim). Preferir módulos prontos da Tokeny quando existirem.
  3. AxiaTREXGateway.sol (opcional) — wrapper de deploy que padroniza parâmetros Axia na TREXFactory (claim topics default, compliance default), reduzindo superfície de erro do backend.

3.4 Escopo e processo de auditoria

  • Em auditoria: AxiaCompliantERC721, módulos custom, AxiaTREXGateway, e a configuração de deploy (parâmetros passados à factory).
  • Fora (já auditado): núcleo T-REX/ONCHAINID — citar versões e hashes no relatório.
  • Pré-auditoria interna: 100% de cobertura de testes (Hardhat + Foundry invariantes), Slither/Mythril no CI, checklist de acesso (quem pode mintar/freeze/recover).
  • Auditor externo (ex.: firmas reconhecidas em security token). Lead time típico 3–6 semanas + janela de correção. Começar cedo.
  • Deploy em mainnet só após relatório final + correções aplicadas + re-review.

3.5 Gestão de chaves (crítico)

  • A chave do Trusted Issuer (assina claims KYC on-chain) é alvo de altíssimo valor: comprometê-la = forjar KYC. Guardar com rigor ≥ ao mnemonicParameter atual (idealmente KMS/HSM, assinatura via serviço dedicado, rotação documentada).
  • AGENT_ROLE dos tokens (mint/freeze/recover) idem.

4. Modelo de dados (Liquibase + Entities)

Liquibase centralizado no DataInitializerService (ver guia). Grupo do TokenController. Sempre VARCHAR, CREATE INDEX IF NOT EXISTS, rollback granular, registrado no changelog.base.yml.

4.0 crowdfundings — flag opcional de oferta regulada (origem da escolha)

A escolha é opcional e por crowdfunding. O admin marca um único toggle "Oferta regulada"; o sistema resolve o contract_standard concreto cruzando o flag com o tipo de ativo.

sql
ALTER TABLE public.crowdfundings ADD COLUMN IF NOT EXISTS regulated BOOLEAN DEFAULT false;

Entity: CrowdFundingsEntity (Common/Backend/CrowdfundingService/.../crowdfunding.entity.ts) ganha regulated?: boolean.

Status (2026-06-01) — flag regulated implementado e ligado. Liquibase 0027-add_regulated_to_crowdfunding.yml (DataInitializerService, grupo common/crowdfunding, tabela crowdfunding_entries). Campo regulated em crowdfunding.entity.ts + crowdfunding.dto.ts (@DtoAttribute) + ICrowdfunding (crowdfunding-lib). No categories.core.handler.ts, a criação da collection (myTknLib.createCollections) resolve contract_standard: crowdfunding?.regulated ? 'ERC721_COMPLIANT' : 'ERC721'. Origem da cadeia fechada: admin marca regulated → collection nasce com contract_standard → scheduler deploya AxiaCompliantERC721 → mint provisiona identidade + minta compliant. Falta o toggle no form do BackOffice (UI) + i18n.

Tabela de resolução (flag × tipo de ativo → contract_standard):

regulatedTipo de ativocontract_standardContrato
false (default)NFTERC721NFT.json (atual, intacto)
false (default)FungívelERC20whitelabel (atual, intacto)
true (opt-in)NFTERC721_COMPLIANTAxiaCompliantERC721
true (opt-in)FungívelERC3643Token T-REX (Tokeny)

Default = não-regulado → zero impacto em ofertas existentes. A resolução roda no encerramento do crowdfunding, ao criar a collection, gravando collections.contract_standard.

4.1 collections — novo campo de padrão

sql
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 consultada

Entity: adicionar contract_standard, compliance_address, identity_registry_address em CollectionsEntity.

4.2 Nova tabela — vínculo de identidade on-chain (idempotência lazy)

sql
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 Nova tabela — config de Trusted Issuer / Claim Topics por chain

sql
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 de cada tabela: 5 métodos obrigatórios (create, getById, deleteById, update, readAllRelations).


5. Pipeline de deploy do contrato (TokenController)

Status (2026-06-01) — dispatcher de deploy implementado. actions.tools.ts refatorado num dispatcher (resolveBaseContract(contract_standard)nft-whitelabel | nft-3643 | erc3643): deployAxiaCompliantErc721 (lê sol/AxiaCompliantERC721.json, constrói com IR/Compliance compartilhadas da config 3643) e deployTRexSuite (chama TREXFactory.deployTREXSuite via sol/TREXFactory.json, retorna o token). process.bc.contract.transactions.ts passa resolveBaseContract(transaction.contract_standard). Campos contract_standard/compliance_address/ identity_registry_address adicionados ao model tkn-lib/collections + CollectionsEntity. ITrexChainConfig estendido (trexFactoryAddress, complianceAddress). Mint-call ERC721_COMPLIANT (feito): mint.service.ts agora escolhe ABI+método por standard — ERC721_COMPLIANT usa AxiaCompliantERC721.mint(to, tokenId, uri) (ABI em core/utils/AxiaCompliantERC721.json), legado segue mintWithTokenURI. AGENT_ROLE confere (mint assinado pelo mnemonicParameter = admin do deploy). Transferências de NFT compliant usam o transferNft existente (safeTransferFrom, enforcement no _update). Ciclo NFT regulado fechado: flag regulated → deploy → provisionamento lazy → mint.Pendente: mint do fungível ERC3643 é token.mint(to, amount) — fluxo distinto do NFT, fora do mintNft (onde quer que tokens fungíveis sejam emitidos).

5.1 actions.tools.ts — branching real por baseContract

Hoje o callFunction ignora baseContract e sempre deploya NFT.json. Refatorar para um dispatcher:

ts
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}`);
  }
}
  • deployTRexSuite chama TREXFactory.deployTREXSuite(salt, tokenDetails, claimDetails) → retorna { token, ir, irs, tir, ctr, mc }. Persistir compliance_address e identity_registry_address na collection.
  • deployAxiaCompliantErc721 deploya o contrato custom apontando para a Identity Registry compartilhada da chain (de trex_chain_config) + uma ModularCompliance própria.
  • Manter o padrão atual de gas/EIP-1559 (getFeeData, +20% margem, bump) já presente no arquivo.

5.2 process.bc.contract.transactions.ts — escolher pelo contract_standard

No loop de deploy de coleções pendentes, ler collection.contract_standard e mapear para baseContract. XRPL/Solana continuam intocados (3643 é EVM-only — se contract_standard != ERC721 numa rede não-EVM, rejeitar com erro claro).


6. Camada de identidade lazy (novo módulo no TokenController)

Status (2026-06-01) — implementado no TokenController. core/utils/identity/identity.provisioning.service.ts (ABIs reais extraídas da suíte para core/utils/identity/abis/), DAO onchain.identity.dao.ts (5 métodos + getByUserAndChain/setStatus), entity/dto, IoC (Utils.IdentityProvisioningService + Dao.OnchainIdentityDao), ConfigurationReaderService.getTrexParameters(chainId), Liquibase 0068 (collections.contract_standard) + 0069 (onchain_identities). Hook em mint.service.ts chama ensureVerifiedIdentity antes do mint EVM regulado. Idempotente por (user,chain) via constraint única + status. Custodial: agente é management key (sem chave do investidor). Falta: campo contract_standard no model @axia/tkn-lib/models/collections, dispatcher de deploy em actions.tools.ts, e Controller/ClientLib admin (retry/status) — provisionamento core é interno ao mint.

6.1 IdentityProvisioningService

ts
@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 }>;
}

Fluxo interno:

  1. onchainIdentitiesDao.getByUserAndChain(userId, chainId) → se REGISTERED, retorna (skip).
  2. Lock atômico (userId, chainId) (padrão tryAcquireLock SET NX EX já existente).
  3. Puxa KYC do 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');
  4. Deploy ONCHAINID (Identity da ONCHAINID lib) para a wallet — idFactory.createIdentity(wallet, salt).
  5. Assina claims como Trusted Issuer Axia: topic KYC (e COUNTRY=ISO do país, PEP se aplicável). Assinatura ECDSA do issuer → identity.addClaim(topic, scheme, issuer, signature, data, uri).
  6. identityRegistry.registerIdentity(wallet, onchainId, countryCode) (agente = wallet do issuer/agente).
  7. Persiste onchain_identities com status=REGISTERED, claims, last_tx_hash.
  8. Erros → status=ERROR, log, e o mint daquele destinatário falha de forma controlada (não trava o batch inteiro).

6.2 Integração no mint.service.ts

Em mintNft() / mint do fungível, antes da chamada on-chain, se collection.contract_standard ∈ {ERC3643, ERC721_COMPLIANT}:

ts
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 verificado

Pontos de atenção no padrão custodial:

  • Se o mint vai para TKN_OWNER e depois transfere ao usuário, ambos precisam estar na registry. Registrar a malha custodial (TKN_OWNER, sinks, mainWallet) como identidades institucionais num bootstrap único por chain.
  • Burn 3643/compliant usa burn() do agente (alinha com a estratégia NATIVE_BURN v2 já existente).

7. Watcher de eventos (evm.reprocess.service.ts)

Adicionar decodificação dos eventos novos, mantendo idempotência por txHash:

  • 3643/registry: IdentityRegistered, IdentityRemoved, TokensFrozen, TokensUnfrozen, AddressFrozen, RecoverySuccess, ComplianceAdded.
  • ERC-721-compliant: Transfer (já tratado) + AddressFrozen/Recovery custom.
  • Reconciliar estado on-chain ↔ off-chain (ex.: onchain_identities.status, assets.block_resell).

8. ClientLib / Gateway / Controller

Entrega completa por feature (guia): Liquibase + Entity + DTO + Model + DAO + Handler + JSONIC + ClientLib + Controller + Postman + GitBook.

  • ClientLib (@axia/... do TokenController): métodos tipados act<MicroserviceRequest<T>, MicroserviceResponse<U>> para: setCollectionContractStandard, getIdentityStatus(userId, chainId), retryIdentityProvisioning, freezeInvestor, recoverWallet.
  • Gateway (API-Gateway REST): rotas admin sob @AuthenticationRequired + Swagger.
  • Controller: usa Model (nunca DTO), @AuthenticationRequired, valida user.status === 'APPROVED' onde aplicável a operação financeira.
  • JSONIC tokens registrados.

9. Frontend

9.1 BackOffice geral (Common/Frontend/BackOffice/) — onde mora o admin de tokenização

  1. Seletor de padrão de contrato na configuração da coleção/encerramento de crowdfunding:
    • Componente de seleção: ERC721 (padrão) vs Regulado (ERC-3643 / ERC-721-compliant).
    • Não expor "ERC721 vs ERC3643" cru — modelar como toggle de compliance + tipo de ativo (fungível/NFT), e o backend resolve o baseContract.
    • Aviso de irreversibilidade + implicação de mercado secundário (transferência só para wallets KYC) num modal de confirmação.
    • Default por asset class: ofertas reguladas podem forçar regulado (campo travado).
  2. Tela de Identidades On-Chain: status por investidor (PENDING/REGISTERED/REVOKED/ERROR), botão "reprovisionar", link para o explorer (network.scan_url).
  3. Tela de Trusted Issuers / Claim Topics por chain (config de trex_chain_config).
  4. Ações de compliance: freeze/unfreeze investidor, freeze de token, recovery de wallet — com permissão dupla CPM + módulo DB e idealmente @StepUpRequired() por serem ações sensíveis.

9.2 Midas-Web (investidor) — mínimo

  • Badge "Ativo Regulado (ERC-3643)" no detalhe do investimento/NFT.
  • Mensagem clara quando uma transferência/revenda é bloqueada por compliance (ex.: destino sem KYC) — não erro genérico.
  • Reuso da infra de UI existente (sem <app-shell> wrapper; padrões do redesign).

9.3 i18n (obrigatório)

Toda string nova em pt-br.json, en.json, es.json (e devops overrides do Midas-Web: devops/{b4,tokeniza}/pt-br.json Core e Discord; Discord usa TAB). Nada hardcoded.


10. Segurança

  • Chave do Trusted Issuer e AGENT_ROLE: KMS/HSM, serviço de assinatura dedicado, rotação. Comprometimento = KYC forjado / mint indevido.
  • Ações admin (freeze/recover/contract-standard): permissão dupla CPM + módulo DB + @StepUpRequired().
  • Validar user.status === 'APPROVED' antes de provisionar identidade e antes de mint regulado.
  • Idempotência: (userId, chainId) na identidade; txHash nas transações. Evitar duplo-deploy de ONCHAINID sob corrida (lock atômico).
  • Auditoria off-chain das ações (AuditService) além dos eventos on-chain.