Skip to main contentA-REX Core

Whitepaper · 11

Implementierungsleitfaden

Vom sauberen Klon bis zum ausgerollten Chargen-Token mit Hooks und Attestierungen, Schritt für Schritt. Jeder JavaScript-Block in diesem Kapitel stammt aus test/standard/ImplementationGuide.test.js und läuft in der CI; wäre ein Block hier falsch, würde der Build fehlschlagen.

So wird dieser Leitfaden verwendet

Die Schritte bauen aufeinander auf und teilen Variablen: token, attestations, die Signer und die unterwegs erzeugten IDs. Beim ersten Mal sind sie der Reihe nach zu lesen. Selbst ausführen lassen sie sich mit:

npx hardhat test test/standard/ImplementationGuide.test.js
SchrittWas entstehtWer handeltWichtigste Aufrufe
1 · Einrichtenein kompilierter, getesteter CheckoutEntwicklernpm ci, npm test
2 · DeploymentToken-Proxy + AttestierungsregisterBetreiberupgrades.deployProxy, ARexAttestations
3 · RollenOperatoren mit minimalen RechtenAdministratorgrantRole
4 · Erstelleneine Charge mit Metadaten-CommitmentAusstellercreateBatch
5 · Lesender Chargendatensatz und der BestandjederbatchOf, totalSupply(uint256), parentsOf
6 · Transferein geprüfter TransferInhabertransferCheck, safeTransferFrom
7 · AbstammungTeilen, Zusammenführen, VerarbeitenInhaber / Verarbeitertransform
8 · StatusAktualisierungen von Lebenszyklus und MetadatenStatusrollesetBatchStatus, setBatchMetadata
9 · Hookseine geordnete Regel-PipelineHook-AdministratorregisterHook, getHooks
10 · AttestierenNachweise, die Transfers freischaltenvertrauenswürdiger AusstellersetTrustedIssuer, attest
11 · SanktionenBerechtigung mit Quelle und TTLBerechtigungsverwaltersetSanctioned
12 · DurchsetzungEinfrieren, Pause, erzwungener TransferDurchsetzungsrolle / PausierungsrollesetFrozenTokens, forcedTransfer
13 · StilllegungLebensende einer ChargeStatusrolle, InhabersetBatchStatus(…, 255, …), burn
14 · Indexierungder Abstammungsgraph aus EreignissenIndexerqueryFilter(BatchTransformed)
15 · Upgradeein validiertes Upgrade der ImplementierungUpgradervalidateUpgrade, upgradeProxy

1 · Einrichten

Node.js 20 oder 22 LTS und npm. Die Abhängigkeiten sind in package-lock.json fixiert (OpenZeppelin 5.3.0); npm ci ist zu verwenden, damit der eigene Build derselbe ist, den die CI baut.

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

Die folgenden Snippets verwenden drei kleine Hilfsfunktionen und benennen zehn Konten. explain wandelt einen Ablehnungscode zurück in seinen Namen; die Liste ist die Menge der in ARexCodes definierten Codes.

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 · Deployment

Der Referenz-Token ist UUPS-upgradefähig und wird daher mit dem OpenZeppelin-Upgrades-Plugin hinter einem ERC-1967-Proxy ausgerollt; das Plugin validiert zudem das Speicherlayout. Das Attestierungsregister ist ein einfacher, an den Token gebundener Contract.

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

Über die Kommandozeile wird derselbe Stack samt Berechtigungs- und Ablauf-Hooks ausgerollt mit:

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

oder mit Hardhat Ignition:

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

Für ein Testnet oder L2 definiert hardhat.config.js die Netzwerke sepolia, polygon und arbitrum; die RPC-URL und PRIVATE_KEY sind in .env zu setzen (Vorlage in .env.example). ARexToken kompiliert mit optimizer.runs = 1 zu 24,136 Byte und liegt damit unter dem EIP-170-Limit von 24,576 Byte.

3 · Rollen

initialize(admin, baseURI) weist admin jede Rolle zu, was für ein erstes Deployment bequem und für den Produktivbetrieb falsch ist. Jede operative Rolle ist demjenigen Dienst oder Multisig zu erteilen, der sie ausübt:

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);
RolleErlaubtEmpfohlener Inhaber
DEFAULT_ADMIN_ROLEjede Rolle erteilen und entziehen; setBaseURIGovernance-Multisig hinter einem Timelock
UPGRADER_ROLEdie Implementierung ersetzenderselbe Timelock
HOOK_ADMIN_ROLEregisterHook, removeHookderselbe Timelock (Hook-Änderungen werden sichtbar, bevor sie wirksam werden)
ENFORCER_ROLEsetFrozenTokens, forcedTransferDurchsetzungs-Multisig (Compliance und Recht)
PAUSER_ROLEpause, unpauseSchlüssel für die Störfallreaktion
ISSUER_ROLEcreateBatchOriginierungsdienst
STATUS_ROLEsetBatchStatus, setBatchMetadataLager, Prüfer oder Register
TRANSFORMER_ROLEVerarbeitungs-Transformationen (Änderung von Rohstoff, Einheit oder Menge)Verarbeiter
ATTEST_ADMIN_ROLE (bei ARexAttestations)setTrustedIssuer, jede Attestierung widerrufenZertifizierungsstelle oder Plattform

Sobald jede Rolle ihren Inhaber hat, verzichtet der Betreiber des Deployments mit renounceRole(role, deployer) auf alles, was er nicht mehr benötigt.

4 · Charge erstellen

Eine Charge ist eine ERC-1155-ID. Ihr On-Chain-Datensatz ist klein und fest; alles Beschreibende liegt in einem Metadatendokument, dessen Hash On-Chain festgeschrieben wird (Schema: 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 liefert die ID, die die Transaktion vergeben wird; IDs beginnen bei 1 und steigen um eins. Der Token weist ab:

BedingungFehler
dem Aufrufer fehlt ISSUER_ROLEAccessControlUnauthorizedAccount(account, role)
to ist die NulladresseARexInvalidRecipient()
quantity == 0ARexInvalidBatch("ZERO_QUANTITY")
commodity == 0 oder unit == 0ARexInvalidBatch("COMMODITY_REQUIRED") / ("UNIT_REQUIRED")
beide Daten gesetzt und expiresAt <= producedAtARexInvalidBatch("EXPIRY_BEFORE_PRODUCTION")
pausiert, Empfänger nicht berechtigt oder ein Hook lehnt OP_MINT abEnforcedPause(), ERC7943CannotReceive(to), ARexHookRejected(hook, code)

Feldkonventionen:

FeldKonventionBeispiel
commoditykeccak256("<scheme>:<code>"), Schema aus HS, FAO, GTIN, UNSPSC oder ein eigeneskeccak256("HS:100199") Weizen
unitCode der UN/CEFACT-Empfehlung 20 als bytes32"KGM", "TNE", "LTR", "BX"
producedAtUnix-Sekunden; 0 für eine noch nicht geerntete ChargeTerminverkauf, Ernteforderung
expiresAtUnix-Sekunden; 0 für keinen Ablaufverderbliche Frischware
originRefHash einer Referenz auf Herkunftsnachweise; niemals KoordinatenEUDR-DDS-Referenz, Register-ID
metadataHashkeccak256 des kanonischen (RFC 8785) Metadatendokuments
uriwo das Dokument liegt; kurz halten, es wird On-Chain gespeichertipfs://<cid>, GS1 Digital Link

5 · Charge lesen

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 ist in ERC-1155 Supply überladen (totalSupply() und totalSupply(uint256)), daher ist es in ethers v6 über die Signatur aufzurufen. Batch.status ist 0 (ACTIVE), 255 (RETIRED) oder ein vom Aussteller definierter Wert dazwischen.

6 · Transfer

Immer zuerst fragen. transferCheck bildet die Transfer-Pipeline als View nach und liefert den ersten Ablehnungscode; 0x00…00 bedeutet erlaubt. canTransfer ist die boolesche ERC-7943-Variante derselben Antwort.

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

Die Reihenfolge der Pipeline ist fest, daher benennt der Code die fehlgeschlagene Regel:

ReihenfolgePrüfungVon transferCheck zurückgegebener CodeRevert der Transaktion
1Token pausiertAREX:PAUSEDEnforcedPause()
2Sender berechtigt (jeder ELIGIBILITY-Hook)AREX:SENDER_INELIGIBLEERC7943CannotSend(from)
3Empfänger berechtigtAREX:RECEIVER_INELIGIBLEERC7943CannotReceive(to)
4Charge existiertAREX:BATCH_NOT_FOUNDARexBatchNotFound(id)
5Charge nicht stillgelegtAREX:BATCH_RETIREDARexBatchRetired(id)
6amount <= balance − frozenAREX:FROZENERC7943InsufficientUnfrozenBalance(from, id, amount, unfrozen)
7jeder CHECK-Hook in Registrierungsreihenfolgeder Code des HooksARexHookRejected(hook, code)

Die View und die Transaktion werten in derselben Reihenfolge aus, sodass der Code stets die Prüfung benennt, an der die Transaktion zuerst scheitert.

In der Praxis sind zwei Details wichtig. AREX:FROZEN wird auch zurückgegeben, wenn der Sender schlicht nicht genügend Einheiten hält, weil die Prüfung den nicht eingefrorenen Saldo betrifft. Und transferCheck übergibt leere data an Hooks; ein Hook, der Transferdaten liest (etwa die Abwicklung durch DeliveryHook), kann bei der tatsächlichen Transaktion anders antworten.

7 · Abstammung: teilen, zusammenführen, verarbeiten

transform ist der einzige Weg, Chargen aus Chargen zu erzeugen. Die Funktion verbrennt die Eingaben von from, mintet die Ausgaben an to, erfasst parentsOf(output) = inputIds und emittiert ein BatchTransformed, das auf ein TransformationEvent aus EPCIS abgebildet wird. transform.staticCall liefert die neuen IDs.

Ein Re-Batch behält Rohstoff, Einheit und Gesamtmenge bei und benötigt keine Rolle, nur Eigentum oder eine ERC-1155-Freigabe. Eine Charge für einen Käufer in zwei aufteilen:

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

Zwei Chargen zu einer gepoolten Charge für eine Genossenschaft zusammenführen:

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

Verarbeitung ändert den Rohstoff, die Einheit oder die Menge und benötigt daher zusätzlich zur Freigabe TRANSFORMER_ROLE. Die Mühle verarbeitet 1,000 kg Weizen des Landwirts zu 750 kg Mehl:

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"));
RegelDurchgesetzt durch
Aufrufer ist from oder von from freigegebentransform
jede Eingabe existiert, ist nicht stillgelegt und hat genügend nicht eingefrorenen Saldo_update
Re-Batch: gleicher Rohstoff und gleiche Einheit, Σ Eingaben = Σ Ausgabenkeine Rolle erforderlich
alles andereTRANSFORMER_ROLE, sonst AccessControlUnauthorizedAccount
Hooks sehen jeden TeilvorgangOP_TRANSFORM für Burns und Mints

Die in diesem Leitfaden aufgebaute Abstammung:

flowchart LR
    B1["#1 Weizen 250,000 kg<br/>Landwirt"] -->|"Teilen · SALE-42"| B2["#2 Weizen 250"]
    B1 -->|"Teilen · SALE-42"| B3["#3 Weizen 150"]
    B2 -->|"Zusammenführen · POOL-7"| B4["#4 Weizen 400<br/>Genossenschaft"]
    B3 -->|"Zusammenführen · POOL-7"| B4
    B1 -->|"Verarbeiten · MILL-RUN-7"| B5["#5 Mehl 750<br/>Mühle"]

8 · Status und Metadaten

Nur STATUS_ROLE ändert den Status; das Halten von Einheiten verleiht nichts. Die Werte 1..254 sind frei zu definieren; ihre Bedeutung ist im Metadatendokument zu veröffentlichen (status.table). Das Aktualisieren der Metadaten ersetzt Hash und URI und emittiert BatchMetadataUpdated, sodass die Historie der Dokumente überprüfbar bleibt.

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

Hooks sind unabhängige Contracts, die der Token in Registrierungsreihenfolge aufruft. Hier läuft zuerst der Berechtigungs-Hook, dann der Ablauf-Hook, dann eine Regel, wonach Kaffee eine attestierte EUDR-Sorgfaltserklärung benötigt, bevor er verkauft werden kann.

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
HookFlagsKonstruktorKonfigurieren mit
EligibilityHookCHECK, ELIGIBILITY(admin)setAllowlistEnabled, setAllowlisted, setBlocklisted, setSanctioned
ExpiryHookCHECK(allowPreHarvestTransfers)— (liest den Chargendatensatz)
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)

Die Registrierung weist EOAs, Contracts ohne ERC-165-Unterstützung für IARexHook und Hooks mit Flags 0 zurück. Günstige, häufig ablehnende Hooks gehören an den Anfang: Eine Ablehnung stoppt die Pipeline. Einen eigenen Hook schreiben: docs/HOOK-DEVELOPMENT.md.

10 · Attestierungen

Eine Attestierung ist (batch, topic) → (issuer, reference, issuedAt, expiresAt, revoked) und nur gültig, solange ihr Aussteller für das Thema vertrauenswürdig ist. Mit der Anforderung aus Schritt 9 kann die Kaffee-Charge nicht bewegt werden, bis die Erklärung attestiert ist:

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 ist falsch, wenn keine Attestierung vorliegt, wenn sie widerrufen ist, wenn expiresAt verstrichen ist oder wenn der Aussteller für das Thema nicht mehr vertrauenswürdig ist. Der Aussteller oder ATTEST_ADMIN_ROLE darf widerrufen. Themen sind freie bytes32-Werte; zu verwenden ist keccak256("<NAMESPACE>:<NAME>"), zum Beispiel AREX:EUDR:DDS oder AREX:CERT:ORGANIC_EU.

11 · Sanktionen und Berechtigung

EligibilityHook beantwortet canSend und canReceive für den Token. Eine Sanktion erfasst ihre Quelle und einen optionalen Ablauf, sodass eine Listenaktualisierung automatisch verfallen kann.

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 · Durchsetzung

Einfrieren und erzwungene Transfers sind die Durchsetzungsschnittstelle von ERC-7943, für Pfandrechte, Gerichtsbeschlüsse und Beschlagnahmen. Ein Einfrieren darf den aktuellen Saldo übersteigen und erfasst dann auch künftige Zugänge. Ein erzwungener Transfer überspringt die Berechtigung und die Prüfung des eingefrorenen Betrags, funktioniert bei pausiertem Token, senkt den eingefrorenen Betrag auf den verbleibenden Rest und führt die Hooks weiterhin mit OP_FORCED_TRANSFER aus.

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

Das Verfahren, nach dem ENFORCER_ROLE handelt (Gerichtsbeschluss, Pfandvertrag, Sanktionsmitteilung), ist zu dokumentieren, und die Rolle ist hinter einem Multisig zu halten. Jede Aktion emittiert Frozen oder ForcedTransfer.

13 · Stilllegung

RETIRED (255) ist terminal: kein Transfer, kein Mint, keine Transformation und kein Weg zurück. Burns bleiben möglich, damit gelieferte oder vernichtete Güter aus dem Bestand ausscheiden.

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 · Indexierung

Der Abstammungsgraph ist die Liste der BatchTransformed-Ereignisse; aus einzelnen Transfers muss nichts rekonstruiert werden.

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]
EreignisEmittiert vonVerwendung
BatchCreated(id, issuer, commodity, unit, quantity, to)createBatchneue Charge; ObjectEvent ADD in EPCIS
BatchTransformed(operator, inputIds, inputAmounts, outputIds, outputAmounts, processRef)transformAbstammungskante; TransformationEvent in EPCIS
BatchStatusChanged(id, previousStatus, newStatus, reason)setBatchStatusLebenszyklus
BatchMetadataUpdated(id, metadataHash, uri)setBatchMetadataDokumenthistorie
TransferSingle, TransferBatchjede SaldenänderungBestände; ObjectEvent OBSERVE in EPCIS
Frozen(account, tokenId, amount), ForcedTransfer(from, to, tokenId, amount)DurchsetzungPfandrechte, Beschlagnahmen
HookRegistered(hook, flags, position), HookRemoved(hook)Hook-AdministratorRegeländerungen
Attested, AttestationRevoked, TrustedIssuerUpdatedARexAttestationsNachweise

15 · Upgrade

Der Speicher ist nach ERC-7201 mit Namensraum organisiert (erc7201:arex.storage.token); Felder werden an TokenStorage angehängt und nie umsortiert oder entfernt. Vor jedem Upgrade ist zu validieren; das Plugin weist Layouts zurück, die den Speicher beschädigen würden.

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

Im Produktivbetrieb liegt UPGRADER_ROLE hinter einem Multisig und einem Timelock, und das Upgrade wird zuvor an einem Fork der Ziel-Chain erprobt.

Die eigene Integration testen

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 exportiert für eigene Tests deployStack(), batchInit(), die Konstanten OP, HOOK, STATUS und CODES sowie supply().

Weg in den Produktivbetrieb: Checkliste

  1. Reproduzierbarer Build (npm ci), CI grün, Schnittstellen-IDs entsprechen der Spezifikation.
  2. Rollen wie in Schritt 3 neu verteilt; Rollen des Deployment-Betreibers abgegeben.
  3. Hooks auditiert, Reihenfolge dokumentiert, getHooks() veröffentlicht.
  4. Version des Metadatenschemas und Statustabelle veröffentlicht; originRef und eingeschränkte Daten frei von personenbezogenen Daten.
  5. Rechtsinstrumente in den Metadaten referenziert; Einordnung des Angebots mit Rechtsberatern geklärt.
  6. Audit von Token, Attestierungsregister und jedem registrierten Hook durch Dritte (Paket: docs/AUDIT-READINESS.md).
  7. Upgrade an einem Fork mit validateUpgrade erprobt.