Skip to main contentA-REX Foundation

Whitepaper · 07

Hooks y módulos de referencia

Cómo agregar funcionalidad a un token conforme sin tocar el estándar: qué debe cumplir un hook, los hooks de referencia, las extensiones complementarias y cómo escribir, probar y registrar tus propios hooks.

Los módulos son demostraciones, no parte del estándar

A-REX Core se relaciona con sus módulos igual que ERC-3643 con los módulos de cumplimiento de T-REX: la interfaz con la que se conectan los módulos está estandarizada; los módulos, no. Un token conforme sin ninguna regla externa (hook) es un token A-REX válido. Cualquier hook, lo escriba quien lo escriba, es compatible con todos los tokens conformes. Por eso el ecosistema puede crecer sin que cambie el estándar.

graph LR
    T["ARexToken<br/>token conforme"] -->|"check / onExecuted / canSend / canReceive"| H1[EligibilityHook]
    T --> H2[ExpiryHook]
    T --> H3[AttestationHook]
    T --> H4["Tu hook"]
    H3 --> A[ARexAttestations]

Qué debe cumplir un hook

CallbackCuándo se llamaDebe
checkantes de que cambien los saldos, por cada id afectado, con los hooks en orden de registro; se detiene en el primer código distinto de ceroser una vista; nunca revertir por motivos de política
onExecuteddespués de que cambien los saldos, por cada id, en ordenno llamar a las funciones del token que modifican saldos (el token está bloqueado)
canSend / canReceiveen cada transferencia y en las vistas ERC-7943 del tokenser vistas

op toma uno de estos valores: OP_MINT (1), OP_TRANSFER (2), OP_BURN (3), OP_FORCED_TRANSFER (4), OP_TRANSFORM (5). En una transformación, las entradas llegan con to == 0 y las salidas con from == 0. data contiene los datos de transferencia de ERC-1155; en las transformaciones es abi.encode(processRef) y en las transferencias forzosas va vacío.

El token que llama es msg.sender. Si un hook necesita los datos del lote, los lee con IARexBatch(msg.sender).batchOf(id). Un hook vinculado a un solo token al desplegarse (ADR-025, Propuesto) lo expone como token() y rechaza las llamadas de cualquier otra dirección.

Cuando más de una regla lee data, el ADR-023 (Propuesto) lo convierte en un sobre etiquetado: hasta 8 entradas (bytes32 key, bytes32 value), con claves en espacios de nombres como los códigos (keccak256("AREX:DELIVERY_ID"), keccak256("AREX:DOCUMENT_REF")), y cada regla lee solo su propia clave. Unos data de 32 bytes conservan el único significado que tenían antes para cada regla; cualquier otro contenido no aporta ningún valor, y la lectura nunca revierte. Una regla escrita antes del sobre lo lee como ausencia de valor, así que el remitente solo construye un sobre cuando todas las reglas del token que leen data leen sobres. Los DeliveryHook y TraceabilityHook de referencia lo hacen desde la versión 2.0.0-alpha.7.

Hooks de referencia

EligibilityHook — permitir, bloquear, sancionar

Muestra cómo funciona HOOK_ELIGIBILITY. Mantiene una lista opcional de permitidos, una lista de bloqueados y una lista de sanciones. canSend/canReceive devuelven la conjunción de las tres. check devuelve SANCTIONED, SENDER_INELIGIBLE o RECEIVER_INELIGIBLE, para que las interfaces puedan explicar el rechazo. Las transferencias forzosas siempre se permiten: embargar bienes de una cuenta sancionada es justamente el objetivo de los controles de cumplimiento. En producción, las listas se alimentan de un registro de identidad (ONCHAINID, EAS, credenciales verificables) o de un oráculo de sanciones; al hook le da igual cuál.

ExpiryHook — perecibilidad sin un módulo dedicado

No guarda estado. Lee expiresAt y producedAt del lote. Bloquea las acuñaciones y transferencias de lotes vencidos (BATCH_EXPIRED), pero no las quemas (baja contable) ni las transferencias forzosas (controles de cumplimiento). De forma opcional, bloquea las transferencias de lotes que todavía no se produjeron (BATCH_NOT_PRODUCED). Una plataforma que financia la precosecha configura el indicador para permitirlas; un marketplace spot lo configura para bloquearlas. Este único hook cubre la parte de bloqueo de la perecibilidad; las curvas de deterioro pasan a ser una vista auxiliar o una valoración fuera de la cadena.

AttestationHook — requisitos regulatorios por producto

Asocia cada código de producto con el tema de atestación (certificado o constancia verificable) que exige. Antes de una transferencia, lee el lote, busca el requisito y consulta isAttested en el registro de atestaciones; si la atestación no es válida, devuelve ATTESTATION_MISSING. Reglas como «el café necesita una referencia de debida diligencia EUDR», «los lotes con etiqueta orgánica necesitan un certificado orgánico vigente» o «el ganado necesita un certificado sanitario» se configuran cada una con una llamada a setRequirement. Desde la versión de referencia 2.0.0-alpha.7, su propietario puede activar checksTransform (desactivado por defecto): entonces los lotes de entrada de un transform también necesitan su certificado, de modo que un titular con TRANSFORMER_ROLE no puede entregar a otra cuenta un lote sin certificar convertido en un lote nuevo. Las salidas son lotes nuevos y no se comprueban.

PerishabilityHook — modelos de deterioro

Configura el deterioro por código de producto, con la opción de sobrescribirlo para un lote concreto. Ofrece los modelos NONE, LINEAR (hasta un valor mínimo al vencer), EXPONENTIAL (vida media) y STEP (caída en cada intervalo), y las vistas valueBps, secondsRemaining e isExpired. Bloquea las acuñaciones y transferencias de lotes vencidos (BATCH_EXPIRED) y las transferencias de lotes cuyo valor restante es cero (PERISH:NO_VALUE). Nunca bloquea las quemas ni los controles de cumplimiento.

SeasonalHook — ventanas de cosecha y de comercialización

Define ventanas absolutas [start, end] por producto y por operación (hasta 16 por clave). Si una combinación de producto y operación no tiene ventanas, no tiene restricciones. Uso típico: limitar OP_MINT a la ventana de cosecha y OP_TRANSFER a la ventana de comercialización; fuera de ellas, el hook devuelve SEASON_CLOSED. Desde la versión de referencia 2.0.0-alpha.7, un gestor elimina una ventana por sus límites (removeWindow), nunca por su posición, y las ventanas ya terminadas dejan sitio: cuando una clave está llena, añadir una ventana descarta primero las terminadas. Cada eliminación emite WindowRemoved, para que un indexador pueda reconstruir las ventanas a partir de los eventos.

DeliveryHook — custodia y liquidación

El remitente programa la entrega de amount unidades a un destinatario a través de un transportista, y esas unidades quedan bloqueadas. El bloqueo usa un contador O(1), así que check nunca recorre el historial. El transportista y los sensores registran la recogida, el tránsito y las lecturas de temperatura (con un indicador de desvío). El destinatario confirma la entrega, y las disputas pasan a un árbitro. La liquidación es la propia transferencia ERC-1155: del remitente al destinatario, por la cantidad exacta y con el número de la entrega en data (32 bytes, abi.encode(deliveryId), o desde la versión 2.0.0-alpha.7 la entrada AREX:DELIVERY_ID de un sobre). check solo la permite cuando la entrega está en DELIVERED, y onExecuted la marca como SETTLED y libera las unidades. Las unidades libres (no bloqueadas) siguen circulando. Las unidades bloqueadas no se pueden transferir, quemar ni usar como entradas de un transform: check devuelve IN_TRANSIT para cualquier operación que deje el saldo del titular por debajo de la cantidad bloqueada, salvo la transferencia de liquidación, las acuñaciones y las transferencias forzosas. Después de que una transferencia forzosa haya embargado unidades bloqueadas, check sigue devolviendo un código en lugar de revertir. Desde la versión de referencia 2.0.0-alpha.7, las unidades congeladas por el token (getFrozenTokens de ERC-7943) no están libres para programar una entrega (freeUnits indica cuántas lo están); una entrega cuyas unidades se llevó una transferencia forzosa no se puede confirmar ni aceptar como entregada (LockedUnitsMoved), porque su liquidación ya no podría producirse, aunque todavía se puede disputar y devolver; y se rechaza un rango de temperatura cuyo mínimo no esté por debajo de su máximo (InvalidRange).

TraceabilityHook — registro de eventos con formato EPCIS

Observa cada operación del token y la clasifica según los pasos de negocio de CBV (commissioning, shipping, destroying, transforming) y sus disposiciones (active, in_transit, destroyed, seized). RECORDER_ROLE registra eventos críticos de seguimiento ajenos al token (enfriamiento, empaque, recepción) con readPoint y bizLocation. Tiene dos modos: solo emisión (unos +17 k de gas por transferencia; un indexador reconstruye el registro) o persistencia (unos +184 k; getEvents responde en la cadena, con paginación). La versión 2 del hook, incluida en la versión de referencia 2.0.0-alpha.7, queda vinculada a un solo token al desplegarse: solo ese token informa de operaciones, y recordEvent solo acepta lotes que existen en él, así que nadie puede escribir entradas en nombre del token. Su EPCISEvent lleva todos los campos de una entrada (operación, punto de lectura, ubicación y quién la registró), de modo que el modo de solo emisión no pierde nada. La referencia del documento de un movimiento sale de unos data de 32 bytes o de la entrada AREX:DOCUMENT_REF de un sobre, nunca de la entrada de otra regla. El hook no se puede actualizar: las instancias de la versión 1 y sus eventos quedan como estaban, y la versión 2 es un despliegue nuevo.

CompanyFactsHook — los registros propios de una empresa

Muestra cómo funciona solo HOOK_EXECUTED: nunca rechaza una operación, su onExecuted nunca revierte y está vinculado a un solo token. Registra los registros propios de una empresa tal como los describe el descriptor de vocabulario del estándar (interop/VOCABULARY.md, "Records on the ledger"): hechos sobre un lote, un sitio o la empresa (un recordHash más un máximo de 8 valores públicos), marcas de etapa (referencias de etapa con sal en los índices de bit 0–255, una o hasta 50 por transacción), etapas recibidas del token de otra empresa, anclajes diarios (una raíz de Merkle y un recuento) y anulaciones. Cada escritura lleva un approvalHash, el hash del registro de la empresa sobre quién la aprobó. La Safe de la empresa registra las definiciones y las etapas; una clave registradora, que se puede delegar, escribe los registros. En un transform traslada a las salidas las etapas por las que pasó cada lote de origen. Añade la etapa de un tipo de proceso solo para un processRef comprometido en el mismo bloque (commitRun), y cada compromiso se respeta una sola vez. Del mismo modo traslada el modelo de custodia (cocModel, el más bajo de los lotes de origen) y la cobertura EUDR (eudrCovered, el AND de los lotes de origen). Eventos: FactRecorded, StageRecorded, ReceivedStageRecorded, Anchored, FactVoided, RunCommitted, StagesCarried, CustodyDeclared. Versión de referencia 2.0.0-alpha.9.

RequireStageHook — ninguna entrega sin las etapas exigidas

Muestra cómo funciona HOOK_CHECK sobre CompanyFactsHook. Hasta 16 requisitos, cada uno con las operaciones que cubre (entregas y, de forma opcional, las entradas de un transform), los productos y las etapas por las que debe haber pasado un lote. Un lote al que le falta una se rechaza con REQUIRE_STAGE:MISSING; un lote o un módulo de etapas que no puede leer se rechaza con REQUIRE_STAGE:UNREADABLE, nunca con un revert. Las acuñaciones, las quemas y los movimientos por orden legal nunca se rechazan. Cada requisito lleva su propio appliesFrom: los lotes creados antes no se comprueban. El Safe de la empresa fija los requisitos con configure, así que el punto de partida puede cambiar cuando se reemplazan los requisitos; cada cambio emite Configured. Versión de referencia 2.0.0-alpha.9.

Extensiones complementarias

Hay tres necesidades que nunca tuvieron que ser reglas de transferencia. Se resuelven con contratos simples que leen o escriben atestaciones:

ExtensiónQué hace
CommodityPriceOraclelos informantes envían precios por producto; las rondas están numeradas y se cierran por quórum con la mediana, o manualmente cuando se alcanza un mínimo; latestAnswer, isStale
CarbonImmobilizationWrapperregistra créditos inmovilizados en un registro (registro, rango de seriales, vintage, toneladas, prueba), vincula toneladas con lotes, replica el retiro y la liberación, y escribe la atestación AREX:CARBON:IMMOBILIZED; nunca acuña créditos
ParametricInsurancepool ERC-20; una póliza por lote, con un tema que actúa como disparador; la indemnización se paga cuando el disparador se atesta antes del vencimiento

Lotes antiguos y lotes nuevos

Una regla que se activa en un token que ya tiene lotes rige para todos los lotes o solo para los creados a partir de un momento determinado. Los ExpiryHook, AttestationHook, SeasonalHook y PerishabilityHook de referencia reciben ese momento (appliesFrom, 0 para todos los lotes) al desplegarse. No se puede cambiar después, así que nadie puede eximir más lotes más adelante; para elegir otra cosa hay que desplegar la regla de nuevo. RequireStageHook es la excepción: cada uno de sus requisitos lleva su propio appliesFrom, y el Safe de la empresa puede reemplazar los requisitos con configure.

Describir una regla a los sistemas que la usan

Una regla o un complemento puede publicar un descriptor arex.module/1 junto a su código. Es un documento JSON versionado que indica los parámetros y ajustes del contrato, los campos del lote que lee, los tipos de certificado que espera, sus eventos y códigos de rechazo, y cómo pasa su estado a una versión nueva. Una plataforma, una billetera, un indexador o un auditor lo lee para mostrar la regla, configurarla y explicar un rechazo sin escribir código específico para esa regla. Desde la revisión 1.1 el descriptor también puede indicar las operaciones que gobierna la regla (ops: mint, transfer, burn, forced_transfer, transform). El campo es informativo: si falta, las operaciones son desconocidas, y ningún consumidor puede apoyarse en él para aplicar controles, porque deciden las verificaciones del propio hook. La revisión 1.2 agrega dos cosas. Un ajuste puede nombrar un lugar (ref:site): se guarda como el originRef que llevan los lotes creados allí, o como la referencia a la evidencia de origen fuera de la cadena que llevan en su lugar (una declaración de diligencia debida EUDR, por ejemplo), nunca como una dirección o coordenadas. Y el gas declarado de check y onExecuted se mide con los ajustes cargados: antes de medir, un validador llama a cada operación que fija o agrega un valor setup veces por clave con valores de prueba (8 salvo que el descriptor diga otra cosa, como máximo 32), de modo que una regla que recorre una lista configurable declara lo que cuesta con esa lista llena. La revisión 1.3 deja por escrito lo que el primer consumidor del descriptor ya usa, para que sus descriptores pasen la validación del esquema publicado: las acciones que las personas registran mediante una aprobación (los registros de un complemento, cada uno con el rol que puede llamarlo), los errores propios del contrato en palabras, medidas sobre sus registros, el dato por entrega que lee una regla, una forma completa de sus eventos con sus campos y sus textos, y más tipos de valor (un lote, un registro del módulo, un texto corto, un texto guardado solo por su huella). Todo campo que agrega es opcional, así que todo descriptor anterior sigue siendo válido. La revisión 1.4 describe cada valor lo suficiente como para armar un formulario a partir de él: nuevos tipos de valor (un monto con su moneda, una fecha y hora, un documento guardado por su huella, un código con formato como un GTIN, un país o región, una unidad, una persona o una parte del directorio), qué llega al registro (el valor, solo su huella o nada; nunca datos personales), el título, la ayuda, el ejemplo, la sección y las condiciones de cada campo, controles entre campos de una lista cerrada, valores calculados, detalles que quedan fuera del registro, vistas de solo lectura junto a un lote, y cómo se muestra un registro: siempre como declarado por la organización que lo registró, nunca como verificado. La revisión 1.5 sala cada huella: un descriptor que declara fingerprint: { "scheme": "salted-jcs/1" } registra el hash de cada valor junto con su propia sal de 32 bytes, de modo que textos cortos, números o contornos de parcelas no pueden encontrarse probando candidatos. La revisión 1.6 añade medidas que no son totales: además de count y sum, una medida puede ser el valor medio, el más bajo o el más alto (avg, min, max) de un campo numérico de los registros. Necesitan ese campo numérico y nunca se suman entre grupos ni entre empresas: la media de dos empresas es la media de sus registros, no la media de sus medias. Las medidas también pueden agruparse por la parte que creó el lote (party) y por su sitio (site). La plantilla del complemento de humedad de la versión de referencia 2.0.0-alpha.10 declara una: la humedad media a la recepción por producto y sitio. Una medida sobre un valor registrado en puntos básicos declara unit: "bp", y quien la consume la muestra dividida por 100 como porcentaje. La revisión 1.7 permite que el visibleWhen de un campo use el árbol de condiciones del descriptor de vocabulario sobre los demás campos del mismo formulario: hojas { field, op, value } combinadas con all, any y not, como máximo 3 niveles y 12 hojas, nunca una expresión y nunca el contexto del registro. La condición única de la revisión 1.4 sigue siendo válida, queda obsoleta y se lee como la hoja equivalente.

El descriptor es informativo y Propuesto: lo propuso la A-REX Platform, su primer consumidor, y pasará a ser un formato recomendado cuando lo consuma un segundo implementador independiente. Los campos que una regla agrega al documento de metadatos de un lote irían en extensions.<key> y se validarían con el fragmento de esquema del descriptor (ADR-017, Propuesto, para el esquema de metadatos 1.3.0).

Una organización también puede publicar su propio vocabulario como definiciones arex.vocab/1: tipos de trabajo de campo (poda, muestreo de suelo), tipos de proceso (una fermentación de 72 horas, una clasificación por zaranda) y listas de etapas por producto (cosecha, secado, trilla, clasificación, exportación). Son datos, no código. Cada una usa el objeto de campo del descriptor, indica su versión y se asocia a una clase de etapa de referencia y a un paso de negocio GS1 CBV, de modo que quien no conoce a la organización igual la entiende. Un tipo de proceso es un transform con un tipo, nunca una operación nueva: su processRef lleva una etiqueta de 4 bytes del tipo y una huella de los valores del proceso, así una regla puede actuar según el tipo y cualquiera puede comprobar el proceso. Los registros llegan al libro mayor uno por uno o como una raíz de Merkle diaria, con cada valor comprometido con su propia sal, de modo que una organización puede revelar algunos campos y reservarse otros. Una huella que coincide muestra que un registro no cambió desde que se registró, nunca que el trabajo se hizo como se declaró. El formato es informativo y Propuesto (revisión 1.5 del descriptor de vocabulario). La revisión 1.1 añade un form opcional que organiza los campos en secciones y anchos y fija cómo se comporta cada uno (condiciones, valores por defecto, valores de solo lectura y calculados, máscaras y patrones); solo restringe lo que aceptan los campos, de modo que un lector que lo ignora lee cada registro como antes. La revisión 1.2 da a las condiciones un solo lenguaje pequeño, compartido con el descriptor de reglas: not, hasta 3 niveles y 12 hojas, y hojas que leen lo que la plataforma sabe del registro en códigos que se leen igual en todos los idiomas (el cultivo como código EPPO, los días desde la siembra, el tipo de sitio, el país, la partida SA del producto del lote, su última etapa). El contexto desconocido se lee como vacío, y toda condición de la revisión 1.1 sigue siendo válida tal como está escrita. La revisión 1.3 permite que una lista de etapas indique su orden (order): con strict, el valor por defecto, un lote no se registra en una etapa mientras le falte una etapa anterior obligatoria, contando desde donde entró en la lista; con flexible, las etapas se registran en cualquier orden y los huecos quedan para los informes. Una etapa que resulta de la propia historia del lote (su registro, una transformación, una recepción) nunca se rechaza, y toda lista anterior se lee como strict. La revisión 1.4 permite que un tipo de proceso indique qué método de beneficio del café llevan sus salidas (washed, natural, honey con sus variantes amarilla, roja y negra, pulped_natural, wet_hulled, anaerobic, other), tomado de un campo de la ejecución o fijado por el tipo, y los metadatos process de un lote pueden llevar el método (method; mixed solo para un lote). Es una declaración de la organización, no una certificación, y viaja con el lote aun donde los valores de los pasos siguen siendo privados. La revisión 1.5 permite que una correspondencia con una de las clases amplias separating, conditioning, sorting_grading o heat_treating agregue una subclass de una lista cerrada por clase (por ejemplo hulling, curing, screen, roasting), y un lector empareja el paso de otra organización de una clase amplia solo por su subclase o por una correspondencia explícita por remitente. La revisión 1.6 agrega to_processor, una subclase de transporting: mercadería enviada a un procesador que la trabaja para su dueño (maquila), de modo que cambia la custodia y no la titularidad.

Escribir tu propio hook

El ejemplo completo contracts/examples/SeasonHook.sol se mantiene en el repositorio. test/standard/ImplementationGuide.test.js lo pone a prueba para quienes tienen acceso. SeasonalHook es el módulo de referencia cuando necesitas varias ventanas por producto y por operación.

Reglas prácticas:

  1. Los símbolos OK y OP_FORCED_TRANSFER están definidos en el texto normativo.
  2. El comportamiento de la quema está definido en el texto normativo.
  3. El callback check está definido en el texto normativo.
  4. Los nombres de código como ARexCodes.code("MYNS", "NAME") están definidos en el texto normativo.
  5. Los valores de los indicadores están definidos en el texto normativo.
  6. Los callbacks onExecuted y check están definidos en el texto normativo.

Registro y orden

Pon primero las reglas baratas y con más probabilidad de rechazo. HOOK_ADMIN_ROLE debería quedar detrás de una multifirma o un timelock: quien controla la cadena de reglas decide qué transferencias son posibles.

Pruebas

test/standard/helpers.js despliega la pila de referencia. RecordingHook registra cada callback y se puede configurar para rechazar con cualquier código. ReentrantHook intenta volver a entrar al token desde onExecuted y demuestra que el bloqueo funciona. DomainHooks.test.js y Extensions.test.js cubren los hooks y las extensiones portados. Invariants.test.js ejecuta una secuencia aleatoria de operaciones a partir de una semilla y verifica tres cosas: que se conserva la emisión total, que el estado terminal se respeta y que canTransfer anticipa cada resultado. Desde la versión de referencia 2.0.0-alpha.8, la implementación de referencia también incluye un kit de hooks (conformance/hooks/), suites de Foundry que toda regla debe superar, y una plantilla de módulo (templates/module/). La versión 2.0.0-alpha.9 agrega un kit de complementos (conformance/addons/) y tres plantillas de complementos probadas con él: la humedad a la recepción (GrainMoisture), un certificado de comercio justo registrado como hecho declarado por la empresa (FairTradeCertificate) y una prima pagada a un productor, cuyo importe y beneficiario se registran solo como huellas con sal (PremiumPayments).

Interoperabilidad con el cumplimiento de ERC-3643

Un módulo de cumplimiento de ERC-3643 expone moduleCheck(from, to, value, compliance). Un hook que lo llama en OP_TRANSFER y traduce false a un código permite reutilizar en tokens de lote las restricciones por país, los límites de saldo y los bloqueos temporales escritos para security tokens. El camino inverso, exponer un hook de A-REX a un token ERC-3643, es igual de mecánico. El Automated Compliance Engine de Chainlink se integra con el mismo patrón: su motor de políticas se convierte en un hook. Estas dos direcciones son diseños sin implementar. El adaptador de referencia que existe va en sentido contrario y solo lee: permite que las billeteras y las herramientas de cumplimiento de ERC-3643 vean cada lote como un token ERC-3643 mediante un hook de solo notificación que no puede permitir ni rechazar una transferencia (ADR-020, Propuesto).