Skip to main contentA-REX Core

Whitepaper · 11

Guía de implementación

Desde un clon limpio hasta un token de lote desplegado, con hooks y atestaciones, paso a paso. Cada bloque de JavaScript de este capítulo está copiado de test/standard/ImplementationGuide.test.js y se ejecuta en CI; si un bloque de aquí fuera incorrecto, la compilación fallaría.

Cómo usar esta guía

Los pasos se construyen unos sobre otros y comparten variables: token, attestations, los firmantes y los ids creados por el camino. Léanse en orden la primera vez. Para ejecutarlos por cuenta propia:

npx hardhat test test/standard/ImplementationGuide.test.js
PasoQué se obtieneQuién actúaLlamadas principales
1 · Preparaciónuna copia de trabajo compilada y probadadesarrolladornpm ci, npm test
2 · Despliegueproxy del token + registro de atestacionesdesplegadorupgrades.deployProxy, ARexAttestations
3 · Rolesoperadores con privilegios mínimosadministradorgrantRole
4 · Creaciónun lote con compromiso de metadatosemisorcreateBatch
5 · Lecturael registro del lote y el suministrocualquierabatchOf, totalSupply(uint256), parentsOf
6 · Transferenciauna transferencia comprobadatitulartransferCheck, safeTransferFrom
7 · Linajedivisión, fusión, procesamientotitular / transformadortransform
8 · Estadoactualizaciones del ciclo de vida y de los metadatosrol de estadosetBatchStatus, setBatchMetadata
9 · Hooksuna canalización ordenada de reglasadministrador de hooksregisterHook, getHooks
10 · Atestaciónevidencia que desbloquea transferenciasemisor de confianzasetTrustedIssuer, attest
11 · Sancioneselegibilidad con fuente y TTLgestor de elegibilidadsetSanctioned
12 · Ejecución forzosacongelación, pausa, transferencia forzosaejecutor forzoso / pausadorsetFrozenTokens, forcedTransfer
13 · Retirofin de la vida útil de un loterol de estado, titularsetBatchStatus(…, 255, …), burn
14 · Indexaciónel grafo de linaje a partir de los eventosindexadorqueryFilter(BatchTransformed)
15 · Actualizaciónuna actualización validada de la implementaciónresponsable de actualizacionesvalidateUpgrade, upgradeProxy

1 · Preparación

Node.js 20 o 22 LTS y npm. Las dependencias están fijadas en package-lock.json (OpenZeppelin 5.3.0); úsese npm ci para que la compilación propia sea la que construye CI.

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

Los fragmentos siguientes usan tres pequeñas funciones auxiliares y nombran diez cuentas. explain convierte un código de rechazo de nuevo en su nombre; la lista es el conjunto de códigos definidos en 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 · Despliegue

El token de referencia es actualizable mediante UUPS, por lo que se despliega detrás de un proxy ERC-1967 con el plugin de actualizaciones de OpenZeppelin, que también valida la disposición del almacenamiento. El registro de atestaciones es un contrato sencillo vinculado al 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());

Desde la línea de comandos, el mismo conjunto, más los hooks de elegibilidad y de caducidad, se despliega con:

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

o con Hardhat Ignition:

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

Para una red de pruebas o una L2, hardhat.config.js define sepolia, polygon y arbitrum; configúrense la URL del RPC y PRIVATE_KEY en .env (plantilla en .env.example). ARexToken se compila a 24,136 bytes con optimizer.runs = 1, por debajo del límite de 24,576 bytes de EIP-170.

3 · Roles

initialize(admin, baseURI) concede todos los roles a admin, lo cual es cómodo para un primer despliegue e incorrecto para producción. Concédase cada rol operativo al servicio o a la multifirma que lo ejerce:

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);
RolPermiteTitular recomendado
DEFAULT_ADMIN_ROLEconceder y revocar cualquier rol; setBaseURImultifirma de gobernanza detrás de un timelock
UPGRADER_ROLEsustituir la implementaciónel mismo timelock
HOOK_ADMIN_ROLEregisterHook, removeHookel mismo timelock (los cambios de hooks son visibles antes de aplicarse)
ENFORCER_ROLEsetFrozenTokens, forcedTransfermultifirma de ejecución forzosa (cumplimiento y asuntos jurídicos)
PAUSER_ROLEpause, unpauseclave de respuesta a incidentes
ISSUER_ROLEcreateBatchservicio de originación
STATUS_ROLEsetBatchStatus, setBatchMetadataalmacén, inspector o registro
TRANSFORMER_ROLEtransformaciones de procesamiento (cambio de producto básico, de unidad o de cantidad)procesadores
ATTEST_ADMIN_ROLE (en ARexAttestations)setTrustedIssuer, revocar cualquier atestaciónautoridad de certificación o plataforma

Cuando cada rol tiene su titular, el desplegador renuncia a lo que ya no necesita con renounceRole(role, deployer).

4 · Crear un lote

Un lote es un id de ERC-1155. Su registro en la cadena es pequeño y fijo; todo lo descriptivo reside en un documento de metadatos cuyo hash queda comprometido en la cadena (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 devuelve el id que asignará la transacción; los ids empiezan en 1 y aumentan de uno en uno. El token rechaza:

CondiciónError
quien llama carece de ISSUER_ROLEAccessControlUnauthorizedAccount(account, role)
to es la dirección ceroARexInvalidRecipient()
quantity == 0ARexInvalidBatch("ZERO_QUANTITY")
commodity == 0 o unit == 0ARexInvalidBatch("COMMODITY_REQUIRED") / ("UNIT_REQUIRED")
ambas fechas definidas y expiresAt <= producedAtARexInvalidBatch("EXPIRY_BEFORE_PRODUCTION")
en pausa, receptor no elegible, o un hook rechaza OP_MINTEnforcedPause(), ERC7943CannotReceive(to), ARexHookRejected(hook, code)

Convenciones de los campos:

CampoConvenciónEjemplo
commoditykeccak256("<scheme>:<code>"), con scheme entre HS, FAO, GTIN, UNSPSC o uno propiokeccak256("HS:100199") trigo
unitcódigo de la Recomendación 20 de UN/CEFACT como bytes32"KGM", "TNE", "LTR", "BX"
producedAtsegundos Unix; 0 para un lote aún no cosechadoventa a plazo, derecho de cobro sobre cosecha
expiresAtsegundos Unix; 0 si no caducaproductos perecederos
originRefhash de una referencia a la evidencia de origen; nunca coordenadasreferencia de DDS del EUDR, id de registro
metadataHashkeccak256 del documento de metadatos canónico (RFC 8785)
uridónde reside el documento; manténgase corta, porque se almacena en la cadenaipfs://<cid>, GS1 Digital Link

5 · Leer un 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 está sobrecargado en ERC-1155 Supply (totalSupply() y totalSupply(uint256)), por lo que en ethers v6 debe llamarse por su firma. Batch.status es 0 (ACTIVE), 255 (RETIRED) o un valor definido por el emisor entre ambos.

6 · Transferencia

Consúltese siempre primero. transferCheck reproduce la canalización de transferencia como una vista y devuelve el primer código de rechazo; 0x00…00 significa permitido. canTransfer es la versión booleana ERC-7943 de la misma respuesta.

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");

El orden de la canalización es fijo, de modo que el código indica qué regla ha fallado:

OrdenComprobaciónCódigo devuelto por transferCheckReversión en la transacción
1token en pausaAREX:PAUSEDEnforcedPause()
2remitente elegible (todos los hooks ELIGIBILITY)AREX:SENDER_INELIGIBLEERC7943CannotSend(from)
3destinatario elegibleAREX:RECEIVER_INELIGIBLEERC7943CannotReceive(to)
4el lote existeAREX:BATCH_NOT_FOUNDARexBatchNotFound(id)
5el lote no está retiradoAREX:BATCH_RETIREDARexBatchRetired(id)
6amount <= balance − frozenAREX:FROZENERC7943InsufficientUnfrozenBalance(from, id, amount, unfrozen)
7cada hook CHECK, en orden de registroel código del hookARexHookRejected(hook, code)

La vista y la transacción evalúan en el mismo orden, de modo que el código siempre nombra la comprobación que la transacción incumple primero.

Dos detalles importan en la práctica. AREX:FROZEN también se devuelve cuando el remitente simplemente no posee unidades suficientes, porque la comprobación se hace sobre el saldo no congelado. Y transferCheck pasa data vacío a los hooks; un hook que lee los datos de la transferencia (como la liquidación de DeliveryHook) puede responder de forma distinta para la transacción real.

7 · Linaje: dividir, fusionar, procesar

transform es la única forma de crear un lote a partir de lotes. Quema las entradas de from, acuña las salidas a to, registra parentsOf(output) = inputIds y emite un BatchTransformed, que se corresponde con un TransformationEvent de EPCIS. transform.staticCall devuelve los nuevos ids.

Una reagrupación conserva el producto básico, la unidad y la cantidad total, y no necesita ningún rol, solo la titularidad o la aprobación de ERC-1155. Para dividir un lote en dos para un 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"));

Para fusionar dos lotes en un único lote agrupado para una 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"));

El procesamiento cambia el producto básico, la unidad o la cantidad, por lo que necesita TRANSFORMER_ROLE además de la aprobación. El molino convierte 1,000 kg del trigo del agricultor en 750 kg de harina:

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"));
ReglaSe garantiza mediante
quien llama es from o está aprobado por fromtransform
cada entrada existe, no está retirada y tiene saldo no congelado suficiente_update
reagrupación: mismo producto básico y misma unidad, Σ entradas = Σ salidasno se necesita rol
cualquier otro casoTRANSFORMER_ROLE; de lo contrario, AccessControlUnauthorizedAccount
los hooks ven cada tramoOP_TRANSFORM para las quemas y las acuñaciones

El linaje construido en esta guía:

flowchart LR
    B1["#1 trigo 250,000 kg<br/>agricultor"] -->|"división · SALE-42"| B2["#2 trigo 250"]
    B1 -->|"división · SALE-42"| B3["#3 trigo 150"]
    B2 -->|"fusión · POOL-7"| B4["#4 trigo 400<br/>cooperativa"]
    B3 -->|"fusión · POOL-7"| B4
    B1 -->|"procesamiento · MILL-RUN-7"| B5["#5 harina 750<br/>molino"]

8 · Estado y metadatos

Solo STATUS_ROLE cambia el estado; poseer unidades no otorga nada. Los valores 1..254 los define el propio emisor; publíquese su significado en el documento de metadatos (status.table). La actualización de los metadatos sustituye el hash y la URI y emite BatchMetadataUpdated, de modo que el historial de documentos sigue siendo verificable.

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

Los hooks son contratos independientes a los que el token llama en orden de registro. Aquí se ejecuta primero el hook de elegibilidad, luego el de caducidad y después una regla según la cual el café necesita una declaración de diligencia debida del EUDR atestada antes de poder venderse.

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
HookIndicadoresConstructorSe configura con
EligibilityHookCHECK, ELIGIBILITY(admin)setAllowlistEnabled, setAllowlisted, setBlocklisted, setSanctioned
ExpiryHookCHECK(allowPreHarvestTransfers)— (lee el registro del 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)

El registro rechaza las EOA, los contratos sin soporte de ERC-165 para IARexHook y los hooks cuyos indicadores son 0. Colóquense primero los hooks baratos y que rechazan con frecuencia: un rechazo detiene la canalización. Para escribir un hook propio: docs/HOOK-DEVELOPMENT.md.

10 · Atestaciones

Una atestación es (batch, topic) → (issuer, reference, issuedAt, expiresAt, revoked), válida solo mientras su emisor sea de confianza para el tema. Con el requisito del paso 9 establecido, el lote de café no puede moverse hasta que la declaración esté 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 es falso cuando no hay atestación, cuando está revocada, cuando ha pasado expiresAt o cuando el emisor ya no es de confianza para el tema. El emisor o ATTEST_ADMIN_ROLE pueden revocar. Los temas son valores bytes32 libres; úsese keccak256("<NAMESPACE>:<NAME>"), por ejemplo AREX:EUDR:DDS o AREX:CERT:ORGANIC_EU.

11 · Sanciones y elegibilidad

EligibilityHook responde a canSend y canReceive en nombre del token. Una sanción registra su fuente y una caducidad opcional, de modo que una actualización de la lista puede expirar automáticamente.

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 · Ejecución forzosa

La congelación y las transferencias forzosas son la superficie de ejecución forzosa de ERC-7943, para gravámenes, órdenes judiciales e incautaciones. Una congelación puede superar el saldo actual y entonces cubre también los depósitos futuros. Una transferencia forzosa omite la elegibilidad y la comprobación de congelación, funciona mientras el token está en pausa, reduce la cantidad congelada a lo que queda y sigue ejecutando los hooks con 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();

Documéntese el procedimiento con arreglo al cual actúa ENFORCER_ROLE (orden judicial, contrato de prenda, notificación de sanciones) y manténgase el rol detrás de una multifirma. Cada acción emite Frozen o ForcedTransfer.

13 · Retiro

RETIRED (255) es terminal: no admite transferencias, acuñaciones ni transformaciones, y no hay vuelta atrás. La quema sigue siendo posible para que los bienes entregados o destruidos salgan del suministro.

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 · Indexación

El grafo de linaje es la lista de eventos BatchTransformed; no hace falta reconstruir nada a partir de transferencias individuales.

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)createBatchlote nuevo; ObjectEvent ADD de EPCIS
BatchTransformed(operator, inputIds, inputAmounts, outputIds, outputAmounts, processRef)transformarista de linaje; TransformationEvent de EPCIS
BatchStatusChanged(id, previousStatus, newStatus, reason)setBatchStatusciclo de vida
BatchMetadataUpdated(id, metadataHash, uri)setBatchMetadatahistorial de documentos
TransferSingle, TransferBatchcada cambio de saldoposiciones; ObjectEvent OBSERVE de EPCIS
Frozen(account, tokenId, amount), ForcedTransfer(from, to, tokenId, amount)ejecución forzosagravámenes, incautaciones
HookRegistered(hook, flags, position), HookRemoved(hook)administración de hookscambios de reglas
Attested, AttestationRevoked, TrustedIssuerUpdatedARexAttestationsevidencia

15 · Actualización

El almacenamiento tiene espacio de nombres ERC-7201 (erc7201:arex.storage.token); añádanse campos a TokenStorage, sin reordenarlos ni eliminarlos nunca. Valídese antes de cada actualización; el plugin rechaza las disposiciones que corromperían el almacenamiento.

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

En producción, UPGRADER_ROLE está detrás de una multifirma y un timelock, y la actualización se ensaya antes contra un fork de la cadena de destino.

Probar la integración

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(), las constantes OP, HOOK, STATUS y CODES y supply(), para las pruebas propias.

Paso a producción: lista de verificación

  1. Compilación reproducible (npm ci), CI en verde, ids de interfaz iguales a los de la especificación.
  2. Roles redistribuidos como en el paso 3; roles del desplegador renunciados.
  3. Hooks auditados, orden documentado, getHooks() publicado.
  4. Versión del esquema de metadatos y tabla de estados publicadas; originRef y los datos restringidos libres de datos personales.
  5. Instrumentos jurídicos referenciados en los metadatos; clasificación de la oferta acordada con los asesores jurídicos.
  6. Auditoría de terceros del token, del registro de atestaciones y de cada hook registrado (paquete: docs/AUDIT-READINESS.md).
  7. Actualización ensayada en un fork con validateUpgrade.