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 (
ERC721puro vsERC-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
- 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.
- Token-contract deploy-per-colección. Cada crowdfunding regulado despliega su propio Token + Modular Compliance vía factory, reutilizando los registries compartidos.
- CPM es la fuente de la verdad de KYC. El TokenController nunca duplica dato de identidad — consulta
cpm-liben el mint. - Lazy/JIT. Solo paga gas de identidad (ONCHAINID + claims + registro) para quien efectivamente recibe un activo regulado.
- Custodial preservado.
mainWalletsigue pagando gas; las wallets custodiales (TKN_OWNER, sinks) se registran como identidades institucionales. - Idempotencia por
(userId, chainId)en la identidad y portxHashen 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.solimplementado, compilando y testeado enEniato/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 legadosol/). - 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 elNFT.jsonhoy).
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íaTREXFactory→ ONCHAINID + claim KYC firmado por Axia (trusted issuer) → registro → mint (verificado vs bloqueado) → transfer (verificado vs bloqueado) → freeze. Reuso total de Tokeny (incl.TREXGatewaylisto — gateway custom descartado).
3.2 Contratos REUTILIZADOS (auditados por Tokeny — no reescribir)
| Contrato | Origen | Observación |
|---|---|---|
Token (ERC-3643) | @tokenysolutions/t-rex | Token fungible regulado |
IdentityRegistry / IdentityRegistryStorage | T-REX | Deploy-once compartido |
TrustedIssuersRegistry / ClaimTopicsRegistry | T-REX | Deploy-once compartido |
ModularCompliance | T-REX | Host de los módulos |
Identity (ONCHAINID) | @onchain-id/solidity | ERC-734/735, 1 por inversor |
TREXFactory / TREXImplementationAuthority | T-REX | Deploy 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)
AxiaCompliantERC721.sol— el corazón del trabajo nuevo. ERC-721 (OZ) que, en el_update/_beforeTokenTransfer, consulta la mismaIdentityRegistryeIModularCompliancedel 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_sella trabas de compliance + estado por token (tokenFrozen).
- Roles:
- 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 porCOUNTRYclaim). Preferir módulos listos de Tokeny cuando existan. AxiaTREXGateway.sol(opcional) — wrapper de deploy que estandariza parámetros Axia en laTREXFactory(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
mnemonicParameteractual (idealmente KMS/HSM, firma vía servicio dedicado, rotación documentada). AGENT_ROLEde 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.
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, grupocommon/crowdfunding, tablacrowdfunding_entries). Camporegulatedencrowdfunding.entity.ts+crowdfunding.dto.ts(@DtoAttribute) +ICrowdfunding(crowdfunding-lib). Encategories.core.handler.ts, la creación de la collection (myTknLib.createCollections) resuelvecontract_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):
regulated | Tipo de activo | contract_standard | Contrato |
|---|---|---|---|
| false (default) | NFT | ERC721 | NFT.json (actual, intacto) |
| false (default) | Fungible | ERC20 | whitelabel (actual, intacto) |
| true (opt-in) | NFT | ERC721_COMPLIANT | AxiaCompliantERC721 |
| true (opt-in) | Fungible | ERC3643 | Token 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
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: agregar contract_standard, compliance_address, identity_registry_address en CollectionsEntity.
4.2 Nueva tabla — vínculo de identidad on-chain (idempotencia lazy)
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
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.tsrefactorizado en un dispatcher (resolveBaseContract(contract_standard)→nft-whitelabel | nft-3643 | erc3643):deployAxiaCompliantErc721(leesol/AxiaCompliantERC721.json, construye con IR/Compliance compartidas de la config 3643) ydeployTRexSuite(llamaTREXFactory.deployTREXSuitevíasol/TREXFactory.json, retorna el token).process.bc.contract.transactions.tspasaresolveBaseContract(transaction.contract_standard). Camposcontract_standard/compliance_address/ identity_registry_addressagregados al modeltkn-lib/collections+CollectionsEntity.ITrexChainConfigextendido (trexFactoryAddress,complianceAddress). Mint-call ERC721_COMPLIANT (hecho):mint.service.tsahora elige ABI+método por standard —ERC721_COMPLIANTusaAxiaCompliantERC721.mint(to, tokenId, uri)(ABI encore/utils/AxiaCompliantERC721.json), el legado sigue conmintWithTokenURI. AGENT_ROLE coincide (mint firmado por el mnemonicParameter = admin del deploy). Las transferencias de NFT compliant usan eltransferNftexistente (safeTransferFrom, enforcement en el_update). Ciclo NFT regulado cerrado: flag regulated → deploy → aprovisionamiento lazy → mint.Pendiente: el mint del fungibleERC3643estoken.mint(to, amount)— flujo distinto del NFT, fuera delmintNft(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:
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}`);
}
}deployTRexSuitellamaTREXFactory.deployTREXSuite(salt, tokenDetails, claimDetails)→ retorna{ token, ir, irs, tir, ctr, mc }. Persistircompliance_addresseidentity_registry_addressen la collection.deployAxiaCompliantErc721despliega el contrato custom apuntando a la Identity Registry compartida de la chain (detrex_chain_config) + unaModularCompliancepropia.- 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 acore/utils/identity/abis/), DAOonchain.identity.dao.ts(5 métodos + getByUserAndChain/setStatus), entity/dto, IoC (Utils.IdentityProvisioningService+Dao.OnchainIdentityDao),ConfigurationReaderService.getTrexParameters(chainId), Liquibase0068(collections.contract_standard) +0069(onchain_identities). Hook enmint.service.tsllamaensureVerifiedIdentityantes del mint EVM regulado. Idempotente por (user,chain) vía constraint única + status. Custodial: el agente es management key (sin clave del inversor). Falta: campocontract_standarden el model@axia/tkn-lib/models/collections, dispatcher de deploy enactions.tools.ts, y Controller/ClientLib admin (retry/status) — el aprovisionamiento core es interno al 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 }>;
}Flujo interno:
onchainIdentitiesDao.getByUserAndChain(userId, chainId)→ siREGISTERED, retorna (skip).- Lock atómico
(userId, chainId)(patróntryAcquireLockSET NX EX ya existente). - 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'); - Deploy ONCHAINID (
Identityde la ONCHAINID lib) para la wallet —idFactory.createIdentity(wallet, salt). - Firma claims como Trusted Issuer Axia: topic
KYC(yCOUNTRY=ISO del país,PEPsi aplica). Firma ECDSA del issuer →identity.addClaim(topic, scheme, issuer, signature, data, uri). identityRegistry.registerIdentity(wallet, onchainId, countryCode)(agente = wallet del issuer/agente).- Persiste
onchain_identitiesconstatus=REGISTERED,claims,last_tx_hash. - 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}:
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 verificadoPuntos de atención en el patrón custodial:
- Si el mint va para
TKN_OWNERy 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 estrategiaNATIVE_BURNv2 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/Recoverycustom. - 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 tipadosact<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, validauser.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
- Selector de estándar de contrato en la configuración de la colección/cierre de crowdfunding:
- Componente de selección:
ERC721 (estándar)vsRegulado (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).
- Componente de selección:
- Pantalla de Identidades On-Chain: status por inversor (
PENDING/REGISTERED/REVOKED/ERROR), botón "reaprovisionar", link al explorer (network.scan_url). - Pantalla de Trusted Issuers / Claim Topics por chain (config de
trex_chain_config). - 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;txHashen 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.