Skip to content

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 ERC721 vs ERC-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 whitelabel ERC-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

  1. 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.
  2. Token-contract deploy-per-collection. Each regulated crowdfunding deploys its own Token + Modular Compliance via factory, reusing the shared registries.
  3. CPM is the source of truth for KYC. The TokenController never duplicates identity data — it queries cpm-lib at mint.
  4. Lazy/JIT. It only pays identity gas (ONCHAINID + claims + registration) for whoever effectively receives a regulated asset.
  5. Custodial preserved. mainWallet keeps paying gas; custodial wallets (TKN_OWNER, sinks) are registered as institutional identities.
  6. Idempotency by (userId, chainId) on identity and by txHash on 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.sol implemented, compiling and tested in Eniato/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 legacy sol/).
  • 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 as NFT.json today).

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 via TREXFactory → ONCHAINID + KYC claim signed by Axia (trusted issuer) → registration → mint (verified vs blocked) → transfer (verified vs blocked) → freeze. Full reuse of Tokeny (incl. TREXGateway ready — custom gateway dispensed with).

3.2 REUSED contracts (audited by Tokeny — do not rewrite)

ContractOriginNote
Token (ERC-3643)@tokenysolutions/t-rexRegulated fungible token
IdentityRegistry / IdentityRegistryStorageT-REXShared deploy-once
TrustedIssuersRegistry / ClaimTopicsRegistryT-REXShared deploy-once
ModularComplianceT-REXHost of the modules
Identity (ONCHAINID)@onchain-id/solidityERC-734/735, 1 per investor
TREXFactory / TREXImplementationAuthorityT-REXDeterministic 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)

  1. AxiaCompliantERC721.sol — the heart of the new work. An ERC-721 (OZ) that, in _update/_beforeTokenTransfer, queries the same IdentityRegistry and IModularCompliance as 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_sell to compliance locks + per-token state (tokenFrozen).
  2. Custom compliance modules (if needed beyond Tokeny's standard ones): e.g. CrowdfundingLockupModule (lock-up until settlement), JurisdictionModule (allowlist by COUNTRY claim). Prefer Tokeny's ready-made modules when they exist.
  3. AxiaTREXGateway.sol (optional) — a deploy wrapper that standardizes Axia parameters in the TREXFactory (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.

sql
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, group common/crowdfunding, table crowdfunding_entries). Field regulated in crowdfunding.entity.ts + crowdfunding.dto.ts (@DtoAttribute) + ICrowdfunding (crowdfunding-lib). In categories.core.handler.ts, the collection creation (myTknLib.createCollections) resolves contract_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):

regulatedAsset typecontract_standardContract
false (default)NFTERC721NFT.json (current, untouched)
false (default)FungibleERC20whitelabel (current, untouched)
true (opt-in)NFTERC721_COMPLIANTAxiaCompliantERC721
true (opt-in)FungibleERC3643Token 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

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: add contract_standard, compliance_address, identity_registry_address to CollectionsEntity.

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 New table — Trusted Issuer / Claim Topics config per 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 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.ts refactored into a dispatcher (resolveBaseContract(contract_standard)nft-whitelabel | nft-3643 | erc3643): deployAxiaCompliantErc721 (reads sol/AxiaCompliantERC721.json, builds with shared IR/Compliance from the 3643 config) and deployTRexSuite (calls TREXFactory.deployTREXSuite via sol/TREXFactory.json, returns the token). process.bc.contract.transactions.ts passes resolveBaseContract(transaction.contract_standard). Fields contract_standard/compliance_address/ identity_registry_address added to the tkn-lib/collections model + CollectionsEntity. ITrexChainConfig extended (trexFactoryAddress, complianceAddress). ERC721_COMPLIANT mint-call (done): mint.service.ts now chooses ABI+method by standard — ERC721_COMPLIANT uses AxiaCompliantERC721.mint(to, tokenId, uri) (ABI in core/utils/AxiaCompliantERC721.json), legacy keeps mintWithTokenURI. AGENT_ROLE checks out (mint signed by the mnemonicParameter = deploy admin). Compliant NFT transfers use the existing transferNft (safeTransferFrom, enforcement in _update). Regulated NFT cycle closed: regulated flag → deploy → lazy provisioning → mint.Pending: the ERC3643 fungible mint is token.mint(to, amount) — a flow distinct from the NFT, outside mintNft (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:

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 calls TREXFactory.deployTREXSuite(salt, tokenDetails, claimDetails) → returns { token, ir, irs, tir, ctr, mc }. Persist compliance_address and identity_registry_address on the collection.
  • deployAxiaCompliantErc721 deploys the custom contract pointing to the chain's shared Identity Registry (from trex_chain_config) + its own ModularCompliance.
  • 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 into core/utils/identity/abis/), DAO onchain.identity.dao.ts (5 methods + getByUserAndChain/setStatus), entity/dto, IoC (Utils.IdentityProvisioningService + Dao.OnchainIdentityDao), ConfigurationReaderService.getTrexParameters(chainId), Liquibase 0068 (collections.contract_standard) + 0069 (onchain_identities). Hook in mint.service.ts calls ensureVerifiedIdentity before the regulated EVM mint. Idempotent by (user,chain) via unique constraint + status. Custodial: the agent is the management key (no investor key). Missing: contract_standard field in the @axia/tkn-lib/models/collections model, deploy dispatcher in actions.tools.ts, and admin Controller/ClientLib (retry/status) — the core provisioning is internal to the 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 }>;
}

Internal flow:

  1. onchainIdentitiesDao.getByUserAndChain(userId, chainId) → if REGISTERED, return (skip).
  2. Atomic lock (userId, chainId) (the already-existing tryAcquireLock SET NX EX pattern).
  3. 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');
  4. Deploy ONCHAINID (the ONCHAINID lib's Identity) for the wallet — idFactory.createIdentity(wallet, salt).
  5. Sign claims as Axia Trusted Issuer: topic KYC (and COUNTRY=ISO of the country, PEP if applicable). Issuer ECDSA signature → identity.addClaim(topic, scheme, issuer, signature, data, uri).
  6. identityRegistry.registerIdentity(wallet, onchainId, countryCode) (agent = issuer/agent wallet).
  7. Persist onchain_identities with status=REGISTERED, claims, last_tx_hash.
  8. 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}:

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

Attention points in the custodial pattern:

  • If the mint goes to TKN_OWNER and 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-existing NATIVE_BURN v2 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) + custom AddressFrozen/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 methods act<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, validates user.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

  1. Contract standard selector in the collection configuration / crowdfunding close:
    • Selection component: ERC721 (standard) vs Regulated (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).
  2. On-Chain Identities screen: status per investor (PENDING/REGISTERED/REVOKED/ERROR), "reprovision" button, link to the explorer (network.scan_url).
  3. Trusted Issuers / Claim Topics screen per chain (config of trex_chain_config).
  4. 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; txHash on 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.