Skip to main contentA-REX Core

Whitepaper · 11

Guia de Implementação

Do clone limpo a um token de lote implantado, com hooks e atestações, passo a passo. Todo bloco de JavaScript deste capítulo é copiado de test/standard/ImplementationGuide.test.js e executado na CI; se um bloco daqui estivesse errado, o build falharia.

Como usar este guia

As etapas se apoiam umas nas outras e compartilham variáveis: token, attestations, os signers e os ids criados ao longo do caminho. Leia-as em ordem na primeira vez. Execute-as por conta própria com:

npx hardhat test test/standard/ImplementationGuide.test.js
EtapaO que se obtémQuem ageChamadas principais
1 · Configuraçãouma cópia de trabalho compilada e testadadesenvolvedornpm ci, npm test
2 · Implantaçãoproxy do token + registro de atestaçõesimplantadorupgrades.deployProxy, ARexAttestations
3 · Papéisoperadores com privilégio mínimoadministradorgrantRole
4 · Criaçãoum lote com compromisso de metadadosemissorcreateBatch
5 · Leiturao registro do lote e a ofertaqualquer pessoabatchOf, totalSupply(uint256), parentsOf
6 · Transferênciauma transferência verificadatitulartransferCheck, safeTransferFrom
7 · Linhagemdivisão, união, processamentotitular / transformadortransform
8 · Statusatualizações de ciclo de vida e de metadadospapel de statussetBatchStatus, setBatchMetadata
9 · Hooksum pipeline ordenado de regrasadministrador de hooksregisterHook, getHooks
10 · Atestaçãoevidência que libera transferênciasemissor confiávelsetTrustedIssuer, attest
11 · Sançõeselegibilidade com fonte e TTLgestor de elegibilidadesetSanctioned
12 · Enforcementcongelamento, pausa, transferência forçadaresponsável pelo enforcement / responsável pela pausasetFrozenTokens, forcedTransfer
13 · Baixafim de vida de um lotepapel de status, titularsetBatchStatus(…, 255, …), burn
14 · Indexaçãoo grafo de linhagem a partir dos eventosindexadorqueryFilter(BatchTransformed)
15 · Atualizaçãouma atualização de implementação validadaatualizadorvalidateUpgrade, upgradeProxy

1 · Configuração

Node.js 20 ou 22 LTS e npm. As dependências estão fixadas em package-lock.json (OpenZeppelin 5.3.0); use npm ci para que o build local seja o mesmo que a CI constrói.

git clone https://github.com/Arex-Corp/A-REX.git && cd A-REX
npm ci
npm run compile
npm test                     # 46 tests: reference implementation, hooks, extensions, invariants, this guide
npm run interface-ids        # prints the ERC-165 ids; they must match the specification

Os trechos a seguir usam três pequenos auxiliares e nomeiam dez contas. explain converte um código de rejeição de volta em seu nome; a lista é o conjunto de códigos definidos em ARexCodes.

const commodity = (scheme, code) => ethers.keccak256(ethers.toUtf8Bytes(`${scheme}:${code}`));
const unit = (u) => ethers.encodeBytes32String(u);
const ref = (text) => ethers.keccak256(ethers.toUtf8Bytes(text));
const REASONS = Object.fromEntries(
  ["PAUSED", "FROZEN", "SENDER_INELIGIBLE", "RECEIVER_INELIGIBLE", "SANCTIONED", "BATCH_NOT_FOUND",
   "BATCH_RETIRED", "BATCH_EXPIRED", "BATCH_NOT_PRODUCED", "ATTESTATION_MISSING", "SEASON_CLOSED",
   "IN_TRANSIT", "LIMIT_EXCEEDED", "DOCUMENT_MISSING"].map((n) => [ethers.keccak256(ethers.toUtf8Bytes(`AREX:${n}`)), n]),
);
const explain = (code) => (code === ethers.ZeroHash ? "OK" : REASONS[code] ?? code);


const [admin, issuer, farmer, buyer, coop, mill, warehouse, certifier, enforcer, custodian] = await ethers.getSigners();

2 · Implantação

O token de referência é atualizável via UUPS, portanto é implantado atrás de um proxy ERC-1967 com o plugin de atualizações do OpenZeppelin, que também valida o layout de armazenamento. O registro de atestações é um contrato simples vinculado ao token.

const ARexToken = await ethers.getContractFactory("ARexToken");
const token = await upgrades.deployProxy(ARexToken, [admin.address, "ipfs://<cid>/{id}.json"], { kind: "uups" });
await token.waitForDeployment();
const attestations = await (await ethers.getContractFactory("ARexAttestations"))
  .deploy(admin.address, await token.getAddress());

Pela linha de comando, a mesma pilha, mais os hooks de elegibilidade e de validade, é implantada com:

npx hardhat node                                                     # terminal 1
npx hardhat run scripts/deploy-standard.js --network localhost       # token, attestations, 2 hooks
npx hardhat run scripts/deploy-full-standard.js --network localhost  # every hook in order + extensions

ou com o Hardhat Ignition:

npx hardhat ignition deploy ignition/modules/ARexToken.js --network localhost \
  --parameters '{"ARexTokenModule":{"baseURI":"ipfs://<cid>/{id}.json","allowPreHarvestTransfers":true}}'

Para uma rede de teste ou uma L2, hardhat.config.js define sepolia, polygon e arbitrum; defina a URL de RPC e PRIVATE_KEY em .env (modelo em .env.example). ARexToken compila para 24,136 bytes com optimizer.runs = 1, abaixo do limite de 24,576 bytes do EIP-170.

3 · Papéis

initialize(admin, baseURI) concede todos os papéis a admin, o que é conveniente para uma primeira implantação e inadequado para produção. Conceda cada papel operacional ao serviço ou ao multisig que o exerce:

await token.grantRole(await token.ISSUER_ROLE(), issuer.address);
await token.grantRole(await token.TRANSFORMER_ROLE(), mill.address);
await token.grantRole(await token.STATUS_ROLE(), warehouse.address);
await token.grantRole(await token.ENFORCER_ROLE(), enforcer.address);
PapelPermiteTitular recomendado
DEFAULT_ADMIN_ROLEconceder e revogar todos os papéis; setBaseURImultisig de governança atrás de um timelock
UPGRADER_ROLEsubstituir a implementaçãoo mesmo timelock
HOOK_ADMIN_ROLEregisterHook, removeHooko mesmo timelock (as mudanças de hooks se tornam visíveis antes de serem aplicadas)
ENFORCER_ROLEsetFrozenTokens, forcedTransfermultisig de enforcement (conformidade e jurídico)
PAUSER_ROLEpause, unpausechave de resposta a incidentes
ISSUER_ROLEcreateBatchserviço de originação
STATUS_ROLEsetBatchStatus, setBatchMetadataarmazém, inspetor ou registro
TRANSFORMER_ROLEtransformações de processamento (alteração de commodity, unidade ou quantidade)processadores
ATTEST_ADMIN_ROLE (em ARexAttestations)setTrustedIssuer, revogar qualquer atestaçãoautoridade de certificação ou plataforma

Quando todos os papéis tiverem seus titulares, o implantador renuncia ao que deixou de ser necessário com renounceRole(role, deployer).

4 · Criar um lote

Um lote é um identificador ERC-1155. Seu registro on-chain é pequeno e fixo; tudo o que é descritivo fica em um documento de metadados cujo hash é registrado on-chain (esquema: standard/schema/batch-metadata.schema.json).

const metadata = { schemaVersion: "1.0.0", commodity: { scheme: "HS", code: "100199" }, unit: "KGM", quantity: 250000 };
const canonical = JSON.stringify(metadata);                 // RFC 8785 canonicalization in production
const metadataHash = ethers.keccak256(ethers.toUtf8Bytes(canonical));
const uri = "ipfs://<cid>/batch-1.json";                    // wherever you stored `canonical`


const init = {
  commodity: commodity("HS", "100199"),                     // wheat
  unit: unit("KGM"),
  producedAt: Math.floor(Date.parse("2026-03-18T00:00:00Z") / 1000), // 0 = not yet harvested
  expiresAt: 0,                                             // 0 = no expiry
  originRef: ref("DDS-2026-AR-000123"),                     // a reference, never coordinates
  metadataHash,
  uri,
};
const id = await token.connect(issuer).createBatch.staticCall(init, 250_000, farmer.address, "0x");
await (await token.connect(issuer).createBatch(init, 250_000, farmer.address, "0x")).wait();

createBatch.staticCall retorna o identificador que a transação atribuirá; os identificadores começam em 1 e aumentam de um em um. O token rejeita:

CondiçãoErro
o chamador não tem ISSUER_ROLEAccessControlUnauthorizedAccount(account, role)
to é o endereço zeroARexInvalidRecipient()
quantity == 0ARexInvalidBatch("ZERO_QUANTITY")
commodity == 0 ou unit == 0ARexInvalidBatch("COMMODITY_REQUIRED") / ("UNIT_REQUIRED")
as duas datas definidas e expiresAt <= producedAtARexInvalidBatch("EXPIRY_BEFORE_PRODUCTION")
pausado, destinatário inelegível ou um hook rejeita OP_MINTEnforcedPause(), ERC7943CannotReceive(to), ARexHookRejected(hook, code)

Convenções dos campos:

CampoConvençãoExemplo
commoditykeccak256("<scheme>:<code>"), com scheme em HS, FAO, GTIN, UNSPSC ou um esquema própriokeccak256("HS:100199") trigo
unitcódigo da Recomendação 20 da UN/CEFACT como bytes32"KGM", "TNE", "LTR", "BX"
producedAtsegundos Unix; 0 para um lote ainda não colhidovenda a termo, recebível de safra
expiresAtsegundos Unix; 0 para sem validadeprodutos perecíveis
originRefhash de uma referência às evidências de origem; nunca coordenadasreferência da DDS do EUDR, identificador de registro
metadataHashkeccak256 do documento de metadados canônico (RFC 8785)
urionde o documento está; manter curta, pois é armazenada on-chainipfs://<cid>, GS1 Digital Link

5 · Ler um lote

const batch = await token.batchOf(id);                      // reverts ARexBatchNotFound if unknown
await token.batchExists(id);                                // true / false, never reverts
await token["totalSupply(uint256)"](id);                    // units in circulation (overloaded name)
await token.parentsOf(id);                                  // [] for an original lot
await token.uri(id);                                        // batch URI, or the base URI if empty
await token.isExpired(id);                                  // reference-implementation helper

totalSupply é sobrecarregado no ERC-1155 Supply (totalSupply() e totalSupply(uint256)), portanto, no ethers v6, é preciso chamá-lo pela assinatura. Batch.status é 0 (ACTIVE), 255 (RETIRED) ou um valor definido pelo emissor entre esses dois.

6 · Transferência

Consulte sempre antes. transferCheck reproduz o pipeline de transferência como uma view e retorna o primeiro código de rejeição; 0x00…00 significa permitido. canTransfer é a versão booleana do ERC-7943 da mesma resposta.

const code = await token.transferCheck(farmer.address, buyer.address, id, 10_000);
if (code !== ethers.ZeroHash) throw new Error(`transfer blocked: ${explain(code)}`);
await token.connect(farmer).safeTransferFrom(farmer.address, buyer.address, id, 10_000, "0x");

A ordem do pipeline é fixa, de modo que o código indica qual regra falhou:

OrdemVerificaçãoCódigo retornado por transferCheckRevert na transação
1token pausadoAREX:PAUSEDEnforcedPause()
2remetente elegível (todo hook ELIGIBILITY)AREX:SENDER_INELIGIBLEERC7943CannotSend(from)
3destinatário elegívelAREX:RECEIVER_INELIGIBLEERC7943CannotReceive(to)
4o lote existeAREX:BATCH_NOT_FOUNDARexBatchNotFound(id)
5o lote não foi baixadoAREX:BATCH_RETIREDARexBatchRetired(id)
6amount <= balance − frozenAREX:FROZENERC7943InsufficientUnfrozenBalance(from, id, amount, unfrozen)
7todo hook CHECK, na ordem de registroo código do hookARexHookRejected(hook, code)

A view e a transação avaliam na mesma ordem, de modo que o código sempre nomeia a primeira verificação em que a transação falha.

Dois detalhes importam na prática. AREX:FROZEN também é retornado quando o remetente simplesmente não possui unidades suficientes, porque a verificação incide sobre o saldo não congelado. E transferCheck passa data vazio aos hooks; um hook que lê os dados da transferência (como a liquidação do DeliveryHook) pode responder de forma diferente para a transação real.

7 · Linhagem: dividir, unir, processar

transform é a única forma de produzir um lote a partir de lotes. Ele queima as entradas de from, cunha as saídas para to, registra parentsOf(output) = inputIds e emite um único BatchTransformed, que corresponde a um TransformationEvent do EPCIS. transform.staticCall retorna os novos identificadores.

Um reagrupamento mantém a commodity, a unidade e a quantidade total, e não exige papel, apenas a titularidade ou a aprovação do ERC-1155. Dividir um lote em dois para um comprador:

const [lotA, lotB] = await token.connect(farmer).transform.staticCall(
  farmer.address, buyer.address, [id], [400], [init, init], [250, 150], ref("SALE-42"));
await token.connect(farmer).transform(
  farmer.address, buyer.address, [id], [400], [init, init], [250, 150], ref("SALE-42"));

Unir dois lotes em um lote agrupado para uma cooperativa:

const [pooled] = await token.connect(buyer).transform.staticCall(
  buyer.address, coop.address, [lotA, lotB], [250, 150], [init], [400], ref("POOL-7"));
await token.connect(buyer).transform(
  buyer.address, coop.address, [lotA, lotB], [250, 150], [init], [400], ref("POOL-7"));

O processamento altera a commodity, a unidade ou a quantidade, de modo que exige TRANSFORMER_ROLE, além da aprovação. O moinho transforma 1,000 kg do trigo do produtor em 750 kg de farinha:

const now = (await ethers.provider.getBlock("latest")).timestamp;
const flourInit = { ...init, commodity: commodity("HS", "110100"), producedAt: now };
await token.connect(farmer).setApprovalForAll(mill.address, true);
const [flour] = await token.connect(mill).transform.staticCall(
  farmer.address, mill.address, [id], [1_000], [flourInit], [750], ref("MILL-RUN-7"));
await token.connect(mill).transform(
  farmer.address, mill.address, [id], [1_000], [flourInit], [750], ref("MILL-RUN-7"));
RegraGarantida por
o chamador é from ou foi aprovado por fromtransform
toda entrada existe, não foi baixada e tem saldo não congelado suficiente_update
reagrupamento: mesma commodity e mesma unidade, Σ entradas = Σ saídasnenhum papel necessário
qualquer outro casoTRANSFORMER_ROLE, caso contrário AccessControlUnauthorizedAccount
os hooks veem cada etapaOP_TRANSFORM para queimas e cunhagens

A linhagem construída por este guia:

flowchart LR
    B1["#1 trigo 250,000 kg<br/>produtor"] -->|"divisão · SALE-42"| B2["#2 trigo 250"]
    B1 -->|"divisão · SALE-42"| B3["#3 trigo 150"]
    B2 -->|"união · POOL-7"| B4["#4 trigo 400<br/>cooperativa"]
    B3 -->|"união · POOL-7"| B4
    B1 -->|"processamento · MILL-RUN-7"| B5["#5 farinha 750<br/>moinho"]

8 · Status e metadados

Somente STATUS_ROLE altera o status; possuir unidades não confere nada. Os valores 1..254 são definidos pelo emissor; o significado deles deve ser publicado no documento de metadados (status.table). Atualizar os metadados substitui o hash e a URI e emite BatchMetadataUpdated, de modo que o histórico de documentos permanece verificável.

await token.connect(warehouse).setBatchStatus(flour, 1, ref("IN_STORAGE"));  // 1..254 are yours
const labReport = { ...metadata, lab: { moisture: 14.2 } };
await token.connect(warehouse).setBatchMetadata(
  flour, ethers.keccak256(ethers.toUtf8Bytes(JSON.stringify(labReport))), "ipfs://<cid>/flour-lab.json");

9 · Hooks

Os hooks são contratos independentes que o token chama na ordem de registro. Aqui, o hook de elegibilidade é executado primeiro, depois o de validade e, por fim, uma regra segundo a qual o café precisa de uma declaração de devida diligência do EUDR atestada antes de poder ser vendido.

const eligibility = await (await ethers.getContractFactory("EligibilityHook")).deploy(admin.address);
const expiry = await (await ethers.getContractFactory("ExpiryHook")).deploy(true); // allow pre-harvest
const attestationHook = await (await ethers.getContractFactory("AttestationHook"))
  .deploy(admin.address, await attestations.getAddress());
const COFFEE = commodity("HS", "090111");
const EUDR_DDS = ref("AREX:EUDR:DDS");
await attestationHook.setRequirement(COFFEE, EUDR_DDS);     // coffee needs a due-diligence statement


await token.registerHook(await eligibility.getAddress());      // evaluated first
await token.registerHook(await expiry.getAddress());
await token.registerHook(await attestationHook.getAddress());  // evaluated last
await token.getHooks();                                        // the ordered list
HookFlagsConstrutorConfigurar com
EligibilityHookCHECK, ELIGIBILITY(admin)setAllowlistEnabled, setAllowlisted, setBlocklisted, setSanctioned
ExpiryHookCHECK(allowPreHarvestTransfers)— (lê o registro do lote)
AttestationHookCHECK(owner, attestationRegistry)setRequirement(commodity, topic)
PerishabilityHookCHECK(admin)setCommodityDecay, setBatchDecay
SeasonalHookCHECK(admin)addWindow(commodity, op, start, end), clearWindows
DeliveryHookCHECK, EXECUTED(admin)schedule, pickUp, markInTransit, recordReading, confirmDelivered, dispute, resolve
TraceabilityHookEXECUTED(admin, persist)recordEvent (RECORDER_ROLE)

O registro rejeita EOAs, contratos sem suporte a ERC-165 de IARexHook e hooks cujas flags são 0. Coloque primeiro os hooks baratos e que rejeitam com frequência: uma rejeição interrompe o pipeline. Para escrever um hook próprio: docs/HOOK-DEVELOPMENT.md.

10 · Atestações

Uma atestação é (batch, topic) → (issuer, reference, issuedAt, expiresAt, revoked), válida apenas enquanto seu emissor for confiável para o tópico. Com o requisito da etapa 9 em vigor, o lote de café não pode se mover até que a declaração seja atestada:

const coffee = await token.connect(issuer).createBatch.staticCall({ ...init, commodity: COFFEE }, 60, farmer.address, "0x");
await token.connect(issuer).createBatch({ ...init, commodity: COFFEE }, 60, farmer.address, "0x");
explain(await token.transferCheck(farmer.address, buyer.address, coffee, 1));   // "ATTESTATION_MISSING"


await attestations.setTrustedIssuer(EUDR_DDS, certifier.address, true);         // ATTEST_ADMIN_ROLE
await attestations.connect(certifier).attest(coffee, EUDR_DDS, ref("DDS-2026-AR-000123"), 0);
await attestations.isAttested(coffee, EUDR_DDS);                                 // true
explain(await token.transferCheck(farmer.address, buyer.address, coffee, 1));   // "OK"

isAttested é falso quando não há atestação, quando ela foi revogada, quando expiresAt já passou ou quando o emissor deixou de ser confiável para o tópico. O emissor ou ATTEST_ADMIN_ROLE pode revogar. Os tópicos são valores bytes32 livres; use keccak256("<NAMESPACE>:<NAME>"), por exemplo AREX:EUDR:DDS ou AREX:CERT:ORGANIC_EU.

11 · Sanções e elegibilidade

EligibilityHook responde a canSend e canReceive para o token. Uma sanção registra sua fonte e uma validade opcional, de modo que uma atualização de lista possa expirar automaticamente.

await eligibility.setSanctioned(buyer.address, true, ethers.encodeBytes32String("OFAC-SDN"), 0);
explain(await token.transferCheck(farmer.address, buyer.address, id, 1));       // "RECEIVER_INELIGIBLE"
await eligibility.setSanctioned(buyer.address, false, ethers.encodeBytes32String("OFAC-SDN"), 0);

12 · Enforcement

Congelamento e transferências forçadas são a superfície de enforcement do ERC-7943, para gravames, ordens judiciais e apreensões. Um congelamento pode exceder o saldo atual e, nesse caso, cobre também depósitos futuros. Uma transferência forçada dispensa a elegibilidade e a verificação de saldo congelado, funciona com o token pausado, reduz o valor congelado ao que resta e ainda executa os hooks com OP_FORCED_TRANSFER.

await token.connect(enforcer).setFrozenTokens(buyer.address, id, 6_000);          // may exceed the balance
await token.getFrozenTokens(buyer.address, id);                                   // 6_000
explain(await token.transferCheck(buyer.address, farmer.address, id, 5_000));    // "FROZEN"
await token.pause();                                                              // PAUSER_ROLE
await token.connect(enforcer).forcedTransfer(buyer.address, custodian.address, id, 6_000); // works while paused
await token.unpause();

Documente o procedimento segundo o qual ENFORCER_ROLE atua (ordem judicial, contrato de penhor, notificação de sanções) e mantenha o papel atrás de um multisig. Toda ação emite Frozen ou ForcedTransfer.

13 · Baixa

RETIRED (255) é terminal: nenhuma transferência, cunhagem ou transformação, e nenhum caminho de volta. A queima continua possível para que os bens entregues ou destruídos saiam da oferta.

await token.connect(warehouse).setBatchStatus(flour, 255, ref("DELIVERED"));    // RETIRED is terminal
explain(await token.transferCheck(mill.address, buyer.address, flour, 1));       // "BATCH_RETIRED"
await token.connect(mill).burn(mill.address, flour, 750);                         // burning still works

14 · Indexação

O grafo de linhagem é a lista de eventos BatchTransformed; nada precisa ser reconstruído a partir de transferências individuais.

const transforms = await token.queryFilter(token.filters.BatchTransformed());
const edges = transforms.flatMap((e) =>
  e.args.inputIds.flatMap((input) => e.args.outputIds.map((output) => [input, output, e.args.processRef])));
// edges: [1 → 2], [1 → 3], [2 → 4], [3 → 4], [1 → 5]
EventoEmitido porUso
BatchCreated(id, issuer, commodity, unit, quantity, to)createBatchnovo lote; ObjectEvent ADD do EPCIS
BatchTransformed(operator, inputIds, inputAmounts, outputIds, outputAmounts, processRef)transformaresta de linhagem; TransformationEvent do EPCIS
BatchStatusChanged(id, previousStatus, newStatus, reason)setBatchStatusciclo de vida
BatchMetadataUpdated(id, metadataHash, uri)setBatchMetadatahistórico de documentos
TransferSingle, TransferBatchtoda mudança de saldoposições; ObjectEvent OBSERVE do EPCIS
Frozen(account, tokenId, amount), ForcedTransfer(from, to, tokenId, amount)enforcementgravames, apreensões
HookRegistered(hook, flags, position), HookRemoved(hook)administração de hooksmudanças de regras
Attested, AttestationRevoked, TrustedIssuerUpdatedARexAttestationsevidências

15 · Atualização

O armazenamento usa namespace ERC-7201 (erc7201:arex.storage.token); acrescente campos a TokenStorage, sem nunca reordená-los nem removê-los. Valide antes de cada atualização; o plugin recusa layouts que corromperiam o armazenamento.

await upgrades.validateUpgrade(await token.getAddress(), ARexToken, { kind: "uups" });
await upgrades.upgradeProxy(await token.getAddress(), ARexToken);               // UPGRADER_ROLE

Em produção, UPGRADER_ROLE fica atrás de um multisig e de um timelock, e a atualização é ensaiada antes em um fork da rede de destino.

Como testar a integração

npx hardhat test test/standard/ImplementationGuide.test.js   # this chapter, executed
npx hardhat test test/standard/ARexToken.test.js             # token behaviour and conformance
npx hardhat run scripts/smoke-standard.js                    # narrated walkthrough of one lot
npx hardhat run scripts/gas-report-standard.js               # gas with 0, 1 and 2 hooks, and per hook

test/standard/helpers.js exporta deployStack(), batchInit(), as constantes OP, HOOK, STATUS e CODES e supply(), para testes próprios.

Rumo à produção: lista de verificação

  1. Build reprodutível (npm ci), CI verde, identificadores de interface iguais aos da especificação.
  2. Papéis redistribuídos conforme a etapa 3; papéis do implantador renunciados.
  3. Hooks auditados, ordem documentada, getHooks() publicado.
  4. Versão do esquema de metadados e tabela de status publicadas; originRef e dados restritos livres de dados pessoais.
  5. Instrumentos jurídicos referenciados nos metadados; classificação da oferta definida com assessoria jurídica.
  6. Auditoria de terceiros do token, do registro de atestações e de cada hook registrado (pacote: docs/AUDIT-READINESS.md).
  7. Atualização ensaiada em um fork com validateUpgrade.