Skip to content

ERC-3643 (T-REX)

Status: Spec técnica — fase de planificación Autor: Equipo Axia Fecha: 2026-06-01 Alcance decidido:

  • Activos: NFTs (ERC-721) + Crowdfunding/CreditCore (fungible)
  • Chain destino: Polygon / L2 (EVM)
  • Custodia: Custodial (modelo actual mantenido)
  • Aprovisionamiento de identidad: Lazy / Just-In-Time, obteniéndola del CPM en el momento del mint
  • Elección del estándar de contrato: por colección, al final del crowdfunding (ERC721 puro vs ERC-3643 / ERC-721-compliant)
  • El smart contract modelo será desarrollado in-house y auditado por un tercero antes de producción.

1. Objetivo

Permitir que, al emitir NFTs/tokens al cierre de un crowdfunding, el emisor elija el estándar del contrato:

  • Estándar (actual): ERC-721 puro (NFT) o ERC-20 whitelabel (fungible) — sin compliance on-chain.
  • Regulado: ERC-3643 (fungible) o ERC-721-compliant (NFT que consulta la misma capa de identidad del 3643) — con identidad on-chain + enforcement de transferencia.

Toda la orquestación vive en el TokenController. La identidad on-chain se aprovisiona lazy/JIT en el job de mint, obteniendo el KYC del CustomerProfileService vía cpm-lib.


2. Principios de arquitectura

  1. Capa de identidad compartida, construida una sola vez. ONCHAINID + Identity Registry (+ Storage) + Trusted Issuers Registry + Claim Topics Registry son deploy-once por chain. Sirven simultáneamente al token fungible 3643 y al ERC-721-compliant.
  2. Token-contract deploy-per-colección. Cada crowdfunding regulado despliega su propio Token + Modular Compliance vía factory, reutilizando los registries compartidos.
  3. CPM es la fuente de la verdad de KYC. El TokenController nunca duplica dato de identidad — consulta cpm-lib en el mint.
  4. Lazy/JIT. Solo paga gas de identidad (ONCHAINID + claims + registro) para quien efectivamente recibe un activo regulado.
  5. Custodial preservado. mainWallet sigue pagando gas; las wallets custodiales (TKN_OWNER, sinks) se registran como identidades institucionales.
  6. Idempotencia por (userId, chainId) en la identidad y por txHash en las transacciones on-chain (patrones ya 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 es el ítem de mayor lead time y mayor riesgo. Estrategia: reutilizar lo máximo posible de la suite T-REX auditada de Tokeny y auditar solo lo que sea custom.

Status (2026-06-01): AxiaCompliantERC721.sol implementado, compilando y testeado en Eniato/Backend/TokenController/packages/processor/contracts/ — Hardhat (solc 0.8.28, evmVersion cancun), 23 tests verdes, 100% stmts/lines/funcs, solhint limpio. Listo para hardening (Foundry/Slither) y auditoría externa.

3.1 Repositorio y toolchain

  • Nuevo directorio: Eniato/Backend/TokenController/packages/processor/contracts/ (sustituye el legado sol/).
  • Toolchain: Hardhat (TS, alineado al stack) + Foundry para fuzzing/invariantes en la suite custom.
  • Solidity: ^0.8.20; OpenZeppelin Contracts v5 (o v4.9 si es necesario p/ compat con T-REX upstream — fijar en la fase 0).
  • Output de build (ABI + bytecode) versionado en contracts/artifacts/ y consumido por la deploy tool (igual que el NFT.json hoy).

Status (2026-06-01) — suite 3643 integrada y validada: unit dedicado en Eniato/Backend/TokenController/packages/processor/contracts-trex/ (Hardhat, solc 0.8.17, OZ v4 — separado del unit custom OZ v5 por incompatibilidad de versión de OpenZeppelin). 73 archivos compilan; 5 tests de integración punta a punta verdes que prueban: deploy vía TREXFactory → ONCHAINID + claim KYC firmado por Axia (trusted issuer) → registro → mint (verificado vs bloqueado) → transfer (verificado vs bloqueado) → freeze. Reuso total de Tokeny (incl. TREXGateway listo — gateway custom descartado).

3.2 Contratos REUTILIZADOS (auditados por Tokeny — no reescribir)

ContratoOrigenObservación
Token (ERC-3643)@tokenysolutions/t-rexToken fungible regulado
IdentityRegistry / IdentityRegistryStorageT-REXDeploy-once compartido
TrustedIssuersRegistry / ClaimTopicsRegistryT-REXDeploy-once compartido
ModularComplianceT-REXHost de los módulos
Identity (ONCHAINID)@onchain-id/solidityERC-734/735, 1 por inversor
TREXFactory / TREXImplementationAuthorityT-REXDeploy determinístico de la suite

Reescribir esos contratos es el mayor error posible en un activo regulado. Usar los artefactos auditados.

3.3 Contratos CUSTOM (necesitan auditoría)

  1. AxiaCompliantERC721.sol — el corazón del trabajo nuevo. ERC-721 (OZ) que, en el _update/_beforeTokenTransfer, consulta la misma IdentityRegistry e IModularCompliance del 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) reflejando la semántica 3643.
    • Mapea block_resell/block_sell a trabas de compliance + estado por token (tokenFrozen).
  2. Módulos de compliance custom (si es necesario más allá de los estándar de Tokeny): ej. CrowdfundingLockupModule (lock-up hasta liquidación), JurisdictionModule (allowlist por COUNTRY claim). Preferir módulos listos de Tokeny cuando existan.
  3. AxiaTREXGateway.sol (opcional) — wrapper de deploy que estandariza parámetros Axia en la TREXFactory (claim topics default, compliance default), reduciendo la superficie de error del backend.

3.4 Alcance y proceso de auditoría

  • En auditoría: AxiaCompliantERC721, módulos custom, AxiaTREXGateway, y la configuración de deploy (parámetros pasados a la factory).
  • Fuera (ya auditado): núcleo T-REX/ONCHAINID — citar versiones y hashes en el informe.
  • Pre-auditoría interna: 100% de cobertura de tests (Hardhat + Foundry invariantes), Slither/Mythril en CI, checklist de acceso (quién puede mintar/freeze/recover).
  • Auditor externo (ej.: firmas reconocidas en security token). Lead time típico 3–6 semanas + ventana de corrección. Empezar temprano.
  • Deploy en mainnet solo después del informe final + correcciones aplicadas + re-review.

3.5 Gestión de claves (crítico)

  • La clave del Trusted Issuer (firma claims KYC on-chain) es un objetivo de altísimo valor: comprometerla = forjar KYC. Guardarla con rigor ≥ al mnemonicParameter actual (idealmente KMS/HSM, firma vía servicio dedicado, rotación documentada).
  • AGENT_ROLE de los tokens (mint/freeze/recover) ídem.

4. Modelo de datos (Liquibase + Entities)

Liquibase centralizado en el DataInitializerService (ver guía). Grupo del TokenController. Siempre VARCHAR, CREATE INDEX IF NOT EXISTS, rollback granular, registrado en el changelog.base.yml.

4.0 crowdfundings — flag opcional de oferta regulada (origen de la elección)

La elección es opcional y por crowdfunding. El admin marca un único toggle "Oferta regulada"; el sistema resuelve el contract_standard concreto cruzando el flag con el tipo de activo.

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

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

Status (2026-06-01) — flag regulated implementado y conectado. Liquibase 0027-add_regulated_to_crowdfunding.yml (DataInitializerService, grupo common/crowdfunding, tabla crowdfunding_entries). Campo regulated en crowdfunding.entity.ts + crowdfunding.dto.ts (@DtoAttribute) + ICrowdfunding (crowdfunding-lib). En categories.core.handler.ts, la creación de la collection (myTknLib.createCollections) resuelve contract_standard: crowdfunding?.regulated ? 'ERC721_COMPLIANT' : 'ERC721'. Origen de la cadena cerrada: el admin marca regulated → la collection nace con contract_standard → el scheduler despliega AxiaCompliantERC721 → el mint aprovisiona identidad + minta compliant. Falta el toggle en el form del BackOffice (UI) + i18n.

Tabla de resolución (flag × tipo de activo → contract_standard):

regulatedTipo de activocontract_standardContrato
false (default)NFTERC721NFT.json (actual, intacto)
false (default)FungibleERC20whitelabel (actual, intacto)
true (opt-in)NFTERC721_COMPLIANTAxiaCompliantERC721
true (opt-in)FungibleERC3643Token T-REX (Tokeny)

Default = no regulado → cero impacto en ofertas existentes. La resolución corre al cierre del crowdfunding, al crear la collection, grabando collections.contract_standard.

4.1 collections — nuevo campo de estándar

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: agregar contract_standard, compliance_address, identity_registry_address en CollectionsEntity.

4.2 Nueva tabla — vínculo de identidad on-chain (idempotencia 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 Nueva tabla — 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 tabla: 5 métodos obligatorios (create, getById, deleteById, update, readAllRelations).


5. Pipeline de deploy del contrato (TokenController)

Status (2026-06-01) — dispatcher de deploy implementado. actions.tools.ts refactorizado en un dispatcher (resolveBaseContract(contract_standard)nft-whitelabel | nft-3643 | erc3643): deployAxiaCompliantErc721 (lee sol/AxiaCompliantERC721.json, construye con IR/Compliance compartidas de la config 3643) y deployTRexSuite (llama TREXFactory.deployTREXSuite vía sol/TREXFactory.json, retorna el token). process.bc.contract.transactions.ts pasa resolveBaseContract(transaction.contract_standard). Campos contract_standard/compliance_address/ identity_registry_address agregados al model tkn-lib/collections + CollectionsEntity. ITrexChainConfig extendido (trexFactoryAddress, complianceAddress). Mint-call ERC721_COMPLIANT (hecho): mint.service.ts ahora elige ABI+método por standard — ERC721_COMPLIANT usa AxiaCompliantERC721.mint(to, tokenId, uri) (ABI en core/utils/AxiaCompliantERC721.json), el legado sigue con mintWithTokenURI. AGENT_ROLE coincide (mint firmado por el mnemonicParameter = admin del deploy). Las transferencias de NFT compliant usan el transferNft existente (safeTransferFrom, enforcement en el _update). Ciclo NFT regulado cerrado: flag regulated → deploy → aprovisionamiento lazy → mint.Pendiente: el mint del fungible ERC3643 es token.mint(to, amount) — flujo distinto del NFT, fuera del mintNft (dondequiera que se emitan tokens fungibles).

5.1 actions.tools.ts — branching real por baseContract

Hoy el callFunction ignora baseContract y siempre despliega NFT.json. Refactorizar a un 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 llama TREXFactory.deployTREXSuite(salt, tokenDetails, claimDetails) → retorna { token, ir, irs, tir, ctr, mc }. Persistir compliance_address e identity_registry_address en la collection.
  • deployAxiaCompliantErc721 despliega el contrato custom apuntando a la Identity Registry compartida de la chain (de trex_chain_config) + una ModularCompliance propia.
  • Mantener el patrón actual de gas/EIP-1559 (getFeeData, +20% margen, bump) ya presente en el archivo.

5.2 process.bc.contract.transactions.ts — elegir por el contract_standard

En el loop de deploy de colecciones pendientes, leer collection.contract_standard y mapear a baseContract. XRPL/Solana siguen intactos (3643 es EVM-only — si contract_standard != ERC721 en una red no-EVM, rechazar con error claro).


6. Capa de identidad lazy (nuevo módulo en el TokenController)

Status (2026-06-01) — implementado en el TokenController. core/utils/identity/identity.provisioning.service.ts (ABIs reales extraídas de la suite a 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 en mint.service.ts llama ensureVerifiedIdentity antes del mint EVM regulado. Idempotente por (user,chain) vía constraint única + status. Custodial: el agente es management key (sin clave del inversor). Falta: campo contract_standard en el model @axia/tkn-lib/models/collections, dispatcher de deploy en actions.tools.ts, y Controller/ClientLib admin (retry/status) — el aprovisionamiento core es interno al 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 }>;
}

Flujo interno:

  1. onchainIdentitiesDao.getByUserAndChain(userId, chainId) → si REGISTERED, retorna (skip).
  2. Lock atómico (userId, chainId) (patrón tryAcquireLock SET NX EX ya existente).
  3. Obtiene el KYC del 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 de la ONCHAINID lib) para la wallet — idFactory.createIdentity(wallet, salt).
  5. Firma claims como Trusted Issuer Axia: topic KYC (y COUNTRY=ISO del país, PEP si aplica). Firma ECDSA del issuer → identity.addClaim(topic, scheme, issuer, signature, data, uri).
  6. identityRegistry.registerIdentity(wallet, onchainId, countryCode) (agente = wallet del issuer/agente).
  7. Persiste onchain_identities con status=REGISTERED, claims, last_tx_hash.
  8. Errores → status=ERROR, log, y el mint de ese destinatario falla de forma controlada (no traba el batch entero).

6.2 Integración en mint.service.ts

En mintNft() / mint del fungible, antes de la llamada on-chain, si 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

Puntos de atención en el patrón custodial:

  • Si el mint va para TKN_OWNER y luego se transfiere al usuario, ambos deben estar en la registry. Registrar la malla custodial (TKN_OWNER, sinks, mainWallet) como identidades institucionales en un bootstrap único por chain.
  • El burn 3643/compliant usa burn() del agente (se alinea con la estrategia NATIVE_BURN v2 ya existente).

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

Agregar la decodificación de los eventos nuevos, manteniendo idempotencia por txHash:

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

8. ClientLib / Gateway / Controller

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

  • ClientLib (@axia/... del TokenController): métodos tipados act<MicroserviceRequest<T>, MicroserviceResponse<U>> para: setCollectionContractStandard, getIdentityStatus(userId, chainId), retryIdentityProvisioning, freezeInvestor, recoverWallet.
  • Gateway (API-Gateway REST): rutas admin bajo @AuthenticationRequired + Swagger.
  • Controller: usa Model (nunca DTO), @AuthenticationRequired, valida user.status === 'APPROVED' donde aplique a operación financiera.
  • JSONIC tokens registrados.

9. Frontend

9.1 BackOffice general (Common/Frontend/BackOffice/) — donde vive el admin de tokenización

  1. Selector de estándar de contrato en la configuración de la colección/cierre de crowdfunding:
    • Componente de selección: ERC721 (estándar) vs Regulado (ERC-3643 / ERC-721-compliant).
    • No exponer "ERC721 vs ERC3643" crudo — modelarlo como toggle de compliance + tipo de activo (fungible/NFT), y el backend resuelve el baseContract.
    • Aviso de irreversibilidad + implicación de mercado secundario (transferencia solo a wallets KYC) en un modal de confirmación.
    • Default por asset class: las ofertas reguladas pueden forzar el modo regulado (campo bloqueado).
  2. Pantalla de Identidades On-Chain: status por inversor (PENDING/REGISTERED/REVOKED/ERROR), botón "reaprovisionar", link al explorer (network.scan_url).
  3. Pantalla de Trusted Issuers / Claim Topics por chain (config de trex_chain_config).
  4. Acciones de compliance: freeze/unfreeze inversor, freeze de token, recovery de wallet — con permiso doble CPM + módulo DB e idealmente @StepUpRequired() por ser acciones sensibles.

9.2 Midas-Web (inversor) — mínimo

  • Badge "Activo Regulado (ERC-3643)" en el detalle de la inversión/NFT.
  • Mensaje claro cuando una transferencia/reventa es bloqueada por compliance (ej.: destino sin KYC) — no un error genérico.
  • Reuso de la infra de UI existente (sin <app-shell> wrapper; patrones del redesign).

9.3 i18n (obligatorio)

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


10. Seguridad

  • Clave del Trusted Issuer y AGENT_ROLE: KMS/HSM, servicio de firma dedicado, rotación. Comprometimiento = KYC forjado / mint indebido.
  • Acciones admin (freeze/recover/contract-standard): permiso doble CPM + módulo DB + @StepUpRequired().
  • Validar user.status === 'APPROVED' antes de aprovisionar identidad y antes del mint regulado.
  • Idempotencia: (userId, chainId) en la identidad; txHash en las transacciones. Evitar doble-deploy de ONCHAINID bajo carrera (lock atómico).
  • Auditoría off-chain de las acciones (AuditService) además de los eventos on-chain.