Whitepaper · 07
Hooks e Módulos de Referência
Como a funcionalidade é acrescentada a um token conforme sem tocar no padrão: o contrato de hook, os hooks de referência, as extensões complementares e como escrever, testar e registrar um hook próprio.
Módulos são demonstrações, e não o padrão
A relação entre o A-REX Core e seus módulos é a mesma que existe entre o ERC-3643 e os módulos de conformidade do T-REX: a interface pela qual os módulos se conectam é padronizada; os módulos, não. Um token conforme com zero hooks é um token A-REX válido. Um hook escrito por qualquer pessoa é compatível com todos os tokens conformes. É isso que faz o ecossistema crescer sem que o padrão mude.
graph LR
T["ARexToken<br/>token conforme"] -->|"check / onExecuted / canSend / canReceive"| H1[EligibilityHook]
T --> H2[ExpiryHook]
T --> H3[AttestationHook]
T --> H4["Seu hook"]
H3 --> A[ARexAttestations]O contrato de hook
interface IARexHook is IERC165 {
function hookFlags() external view returns (uint8); // CHECK=1 · EXECUTED=2 · ELIGIBILITY=4
function check(uint8 op, address operator, address from, address to, uint256 id, uint256 amount, bytes calldata data)
external view returns (bytes32 code); // 0 = allowed
function onExecuted(uint8 op, address operator, address from, address to, uint256 id, uint256 amount, bytes calldata data) external;
function canSend(address account) external view returns (bool);
function canReceive(address account) external view returns (bool);
}
| Callback | Quando | Deve |
|---|---|---|
check | antes de os saldos mudarem, por id afetado, hooks na ordem de registro, parando no primeiro código diferente de zero | ser uma view; nunca reverter por motivo de política |
onExecuted | depois de os saldos mudarem, por id, em ordem | não chamar as funções do token que alteram saldos (o token está travado) |
canSend / canReceive | em toda transferência e para as views do ERC-7943 do token | ser views |
op é um entre OP_MINT (1), OP_TRANSFER (2), OP_BURN (3), OP_FORCED_TRANSFER (4), OP_TRANSFORM (5). Em uma transformação, as entradas chegam com to == 0 e as saídas com from == 0. data são os dados de transferência do ERC-1155: abi.encode(processRef) para transformações e vazio para transferências forçadas.
O token chamador é msg.sender. Um hook que precisa do lote lê IARexBatch(msg.sender).batchOf(id).
Hooks de referência
EligibilityHook — permitir, bloquear, sancionar
Demonstra HOOK_ELIGIBILITY. Mantém uma lista de permissão opcional, uma lista de bloqueio e uma lista de sanções; canSend/canReceive retornam a conjunção delas; check retorna SANCTIONED, SENDER_INELIGIBLE ou RECEIVER_INELIGIBLE para que as interfaces possam explicar a recusa. As transferências forçadas são sempre permitidas: apreender bens de uma conta sancionada é o propósito do enforcement. Em produção, as listas são alimentadas por um registro de identidade (ONCHAINID, EAS, credenciais verificáveis) ou por um oráculo de sanções; o hook é indiferente quanto a isso.
ExpiryHook — perecibilidade sem um módulo
Sem estado. Lê expiresAt e producedAt do lote. Bloqueia cunhagens e transferências de lotes vencidos (BATCH_EXPIRED), mas não queimas (baixa por perda) nem transferências forçadas (enforcement). Opcionalmente bloqueia transferências de lotes ainda não produzidos (BATCH_NOT_PRODUCED) — uma plataforma que financia a pré-colheita define a flag para permiti-las; um marketplace à vista a define para bloqueá-las. Esse único hook cobre a parte de bloqueio da perecibilidade; as curvas de decaimento passam a ser uma função auxiliar de view ou uma valoração off-chain.
AttestationHook — condicionamento regulatório por commodity
Mapeia um código de commodity para um tópico de atestação exigido. Antes de uma transferência, lê o lote, consulta o requisito, pergunta ao registro de atestações isAttested(id, topic) e retorna ATTESTATION_MISSING caso contrário. "O café precisa de uma referência de devida diligência do EUDR", "lotes com rótulo orgânico precisam de um certificado orgânico válido", "o gado precisa de um certificado sanitário" são, cada um, uma chamada a setRequirement.
PerishabilityHook — modelos de decaimento
Configuração de decaimento por código de commodity, com sobrescrita por lote; modelos NONE, LINEAR (até um piso no vencimento), EXPONENTIAL (meia-vida) e STEP (queda a cada intervalo). Views valueBps, secondsRemaining, isExpired. Bloqueia cunhagens e transferências de lotes vencidos (BATCH_EXPIRED) e transferências de lotes cujo valor restante é zero (PERISH:NO_VALUE); nunca bloqueia queimas nem enforcement.
SeasonalHook — janelas de colheita e de negociação
Janelas absolutas [start, end] por commodity e por operação (no máximo 16 por chave). Uma commodity/operação sem janelas não tem restrição. Uso típico: OP_MINT limitado à janela de colheita, OP_TRANSFER à janela de negociação; SEASON_CLOSED nos demais casos.
DeliveryHook — custódia e liquidação
Um remetente agenda uma entrega de amount unidades a um destinatário por meio de uma transportadora; as unidades são travadas (um contador O(1), de modo que check nunca itera sobre o histórico). A transportadora e os sensores registram a coleta, o trânsito e as leituras de temperatura (flag de violação); o destinatário confirma a entrega; as disputas vão a um árbitro. A liquidação é a própria transferência ERC-1155: remetente → destinatário, quantidade exata, com data = abi.encode(deliveryId); check só a permite quando a entrega está DELIVERED, e onExecuted a marca como SETTLED e destrava. As unidades livres (não travadas) continuam a circular.
TraceabilityHook — log de eventos no formato EPCIS
Observa cada operação do token e a classifica com etapas de negócio do CBV (commissioning, shipping, destroying, transforming) e disposições (active, in_transit, destroyed, seized); RECORDER_ROLE registra eventos críticos de rastreamento que não envolvem o token (resfriamento, embalagem, recebimento) com readPoint e bizLocation. Dois modos: apenas emitir (cerca de +17 k de gás por transferência; um indexador reconstrói o log) ou persistir (cerca de +184 k; getEvents responde on-chain, com paginação).
Extensões complementares
Três preocupações nunca precisaram ser regras de transferência; são contratos simples que leem ou escrevem atestações:
| Extensão | O que faz |
|---|---|
CommodityPriceOracle | os reportadores enviam preços por commodity; as rodadas são numeradas e finalizadas por quórum com a mediana, ou manualmente quando um mínimo é atingido; latestAnswer, isStale |
CarbonImmobilizationWrapper | registra créditos imobilizados em um registro (registro, faixa de números de série, vintage, toneladas, prova), vincula toneladas a lotes, espelha a aposentadoria e a liberação, grava a atestação AREX:CARBON:IMMOBILIZED; nunca cunha créditos |
ParametricInsurance | pool ERC-20; apólice por lote com um tópico de gatilho; o sinistro é pago quando o gatilho é atestado antes do vencimento |
Como escrever um hook próprio
O hook abaixo é contracts/examples/SeasonHook.sol; ele compila na CI e test/standard/ImplementationGuide.test.js o exercita sobre um token. Em produção, recomenda-se usar SeasonalHook, que admite várias janelas por commodity e por operação.
contract SeasonHook is ARexHookBase, Ownable {
mapping(bytes32 => uint64[2]) public window; // commodity → [open, close]
constructor(address owner_) Ownable(owner_) {}
function setWindow(bytes32 commodity, uint64 open, uint64 close) external onlyOwner {
window[commodity] = [open, close];
}
function hookFlags() public pure override returns (uint8) {
return ARexCodes.HOOK_CHECK;
}
function check(uint8 op, address, address, address, uint256 id, uint256, bytes calldata)
external
view
override
returns (bytes32)
{
if (op != ARexCodes.OP_TRANSFER) return ARexCodes.OK; // never block mints, burns or enforcement
IARexBatch.Batch memory b = IARexBatch(msg.sender).batchOf(id);
uint64[2] memory w = window[b.commodity];
if (w[1] == 0) return ARexCodes.OK; // no window configured
return (block.timestamp >= w[0] && block.timestamp <= w[1]) ? ARexCodes.OK : ARexCodes.SEASON_CLOSED;
}
}
Regras práticas:
- Retornar
OKparaOP_FORCED_TRANSFER, a menos que o próprio hook trate do enforcement. - Não bloquear queimas, salvo se essa for a intenção: queimas são entregas e baixas por perda.
- Manter
checkbarato e limitado; ele é executado em toda transferência de todo id. Nunca iterar sobre o histórico. - Usar namespace nos códigos:
ARexCodes.code("MYNS", "NAME"). - Manter as flags constantes; o token as armazena em cache.
- Colocar as mudanças de estado em
onExecuted, nunca emcheck.
Registro e ordenação
await token.registerHook(sanctionsHook); // evaluated first
await token.registerHook(expiryHook);
await token.registerHook(attestationHook); // evaluated last
await token.getHooks(); // ordered list
await token.removeHook(expiryHook); // order of the rest preserved
Coloque primeiro as regras baratas e de alta taxa de rejeição. HOOK_ADMIN_ROLE deveria ficar atrás de um multisig ou timelock: quem controla o pipeline controla quais transferências são possíveis.
Testes
test/standard/helpers.js implanta a pilha de referência. RecordingHook registra cada callback e pode ser instruído a rejeitar com qualquer código; ReentrantHook tenta reentrar no token a partir de onExecuted e comprova a trava. DomainHooks.test.js e Extensions.test.js cobrem os hooks e os complementares migrados; Invariants.test.js executa uma sequência aleatória de operações com semente e verifica a conservação da oferta, o status terminal e que canTransfer prevê todos os resultados.
Interoperabilidade com a conformidade do ERC-3643
Um módulo de conformidade do ERC-3643 expõe moduleCheck(from, to, value, compliance). Um hook que o chama para OP_TRANSFER e mapeia false para um código torna restrições por país, limites de saldo e bloqueios temporais, escritos para tokens de valores mobiliários, reutilizáveis em tokens de lote. O caminho inverso — expor um hook do A-REX a um token ERC-3643 — é igualmente mecânico. O Automated Compliance Engine da Chainlink se integra pelo mesmo esquema: seu mecanismo de políticas se torna um hook.