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
| Schritt | Was entsteht | Wer handelt | Wichtigste Aufrufe |
|---|---|---|---|
| 1 · Einrichten | ein kompilierter, getesteter Checkout | Entwickler | npm ci, npm test |
| 2 · Deployment | Token-Proxy + Attestierungsregister | Betreiber | upgrades.deployProxy, ARexAttestations |
| 3 · Rollen | Operatoren mit minimalen Rechten | Administrator | grantRole |
| 4 · Erstellen | eine Charge mit Metadaten-Commitment | Aussteller | createBatch |
| 5 · Lesen | der Chargendatensatz und der Bestand | jeder | batchOf, totalSupply(uint256), parentsOf |
| 6 · Transfer | ein geprüfter Transfer | Inhaber | transferCheck, safeTransferFrom |
| 7 · Abstammung | Teilen, Zusammenführen, Verarbeiten | Inhaber / Verarbeiter | transform |
| 8 · Status | Aktualisierungen von Lebenszyklus und Metadaten | Statusrolle | setBatchStatus, setBatchMetadata |
| 9 · Hooks | eine geordnete Regel-Pipeline | Hook-Administrator | registerHook, getHooks |
| 10 · Attestieren | Nachweise, die Transfers freischalten | vertrauenswürdiger Aussteller | setTrustedIssuer, attest |
| 11 · Sanktionen | Berechtigung mit Quelle und TTL | Berechtigungsverwalter | setSanctioned |
| 12 · Durchsetzung | Einfrieren, Pause, erzwungener Transfer | Durchsetzungsrolle / Pausierungsrolle | setFrozenTokens, forcedTransfer |
| 13 · Stilllegung | Lebensende einer Charge | Statusrolle, Inhaber | setBatchStatus(…, 255, …), burn |
| 14 · Indexierung | der Abstammungsgraph aus Ereignissen | Indexer | queryFilter(BatchTransformed) |
| 15 · Upgrade | ein validiertes Upgrade der Implementierung | Upgrader | validateUpgrade, 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);
| Rolle | Erlaubt | Empfohlener Inhaber |
|---|---|---|
DEFAULT_ADMIN_ROLE | jede Rolle erteilen und entziehen; setBaseURI | Governance-Multisig hinter einem Timelock |
UPGRADER_ROLE | die Implementierung ersetzen | derselbe Timelock |
HOOK_ADMIN_ROLE | registerHook, removeHook | derselbe Timelock (Hook-Änderungen werden sichtbar, bevor sie wirksam werden) |
ENFORCER_ROLE | setFrozenTokens, forcedTransfer | Durchsetzungs-Multisig (Compliance und Recht) |
PAUSER_ROLE | pause, unpause | Schlüssel für die Störfallreaktion |
ISSUER_ROLE | createBatch | Originierungsdienst |
STATUS_ROLE | setBatchStatus, setBatchMetadata | Lager, Prüfer oder Register |
TRANSFORMER_ROLE | Verarbeitungs-Transformationen (Änderung von Rohstoff, Einheit oder Menge) | Verarbeiter |
ATTEST_ADMIN_ROLE (bei ARexAttestations) | setTrustedIssuer, jede Attestierung widerrufen | Zertifizierungsstelle 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:
| Bedingung | Fehler |
|---|---|
dem Aufrufer fehlt ISSUER_ROLE | AccessControlUnauthorizedAccount(account, role) |
to ist die Nulladresse | ARexInvalidRecipient() |
quantity == 0 | ARexInvalidBatch("ZERO_QUANTITY") |
commodity == 0 oder unit == 0 | ARexInvalidBatch("COMMODITY_REQUIRED") / ("UNIT_REQUIRED") |
beide Daten gesetzt und expiresAt <= producedAt | ARexInvalidBatch("EXPIRY_BEFORE_PRODUCTION") |
pausiert, Empfänger nicht berechtigt oder ein Hook lehnt OP_MINT ab | EnforcedPause(), ERC7943CannotReceive(to), ARexHookRejected(hook, code) |
Feldkonventionen:
| Feld | Konvention | Beispiel |
|---|---|---|
commodity | keccak256("<scheme>:<code>"), Schema aus HS, FAO, GTIN, UNSPSC oder ein eigenes | keccak256("HS:100199") Weizen |
unit | Code der UN/CEFACT-Empfehlung 20 als bytes32 | "KGM", "TNE", "LTR", "BX" |
producedAt | Unix-Sekunden; 0 für eine noch nicht geerntete Charge | Terminverkauf, Ernteforderung |
expiresAt | Unix-Sekunden; 0 für keinen Ablauf | verderbliche Frischware |
originRef | Hash einer Referenz auf Herkunftsnachweise; niemals Koordinaten | EUDR-DDS-Referenz, Register-ID |
metadataHash | keccak256 des kanonischen (RFC 8785) Metadatendokuments | |
uri | wo das Dokument liegt; kurz halten, es wird On-Chain gespeichert | ipfs://<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:
| Reihenfolge | Prüfung | Von transferCheck zurückgegebener Code | Revert der Transaktion |
|---|---|---|---|
| 1 | Token pausiert | AREX:PAUSED | EnforcedPause() |
| 2 | Sender berechtigt (jeder ELIGIBILITY-Hook) | AREX:SENDER_INELIGIBLE | ERC7943CannotSend(from) |
| 3 | Empfänger berechtigt | AREX:RECEIVER_INELIGIBLE | ERC7943CannotReceive(to) |
| 4 | Charge existiert | AREX:BATCH_NOT_FOUND | ARexBatchNotFound(id) |
| 5 | Charge nicht stillgelegt | AREX:BATCH_RETIRED | ARexBatchRetired(id) |
| 6 | amount <= balance − frozen | AREX:FROZEN | ERC7943InsufficientUnfrozenBalance(from, id, amount, unfrozen) |
| 7 | jeder CHECK-Hook in Registrierungsreihenfolge | der Code des Hooks | ARexHookRejected(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"));
| Regel | Durchgesetzt durch |
|---|---|
Aufrufer ist from oder von from freigegeben | transform |
| jede Eingabe existiert, ist nicht stillgelegt und hat genügend nicht eingefrorenen Saldo | _update |
| Re-Batch: gleicher Rohstoff und gleiche Einheit, Σ Eingaben = Σ Ausgaben | keine Rolle erforderlich |
| alles andere | TRANSFORMER_ROLE, sonst AccessControlUnauthorizedAccount |
| Hooks sehen jeden Teilvorgang | OP_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
| Hook | Flags | Konstruktor | Konfigurieren mit |
|---|---|---|---|
EligibilityHook | CHECK, ELIGIBILITY | (admin) | setAllowlistEnabled, setAllowlisted, setBlocklisted, setSanctioned |
ExpiryHook | CHECK | (allowPreHarvestTransfers) | — (liest den Chargendatensatz) |
AttestationHook | CHECK | (owner, attestationRegistry) | setRequirement(commodity, topic) |
PerishabilityHook | CHECK | (admin) | setCommodityDecay, setBatchDecay |
SeasonalHook | CHECK | (admin) | addWindow(commodity, op, start, end), clearWindows |
DeliveryHook | CHECK, EXECUTED | (admin) | schedule, pickUp, markInTransit, recordReading, confirmDelivered, dispute, resolve |
TraceabilityHook | EXECUTED | (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]
| Ereignis | Emittiert von | Verwendung |
|---|---|---|
BatchCreated(id, issuer, commodity, unit, quantity, to) | createBatch | neue Charge; ObjectEvent ADD in EPCIS |
BatchTransformed(operator, inputIds, inputAmounts, outputIds, outputAmounts, processRef) | transform | Abstammungskante; TransformationEvent in EPCIS |
BatchStatusChanged(id, previousStatus, newStatus, reason) | setBatchStatus | Lebenszyklus |
BatchMetadataUpdated(id, metadataHash, uri) | setBatchMetadata | Dokumenthistorie |
TransferSingle, TransferBatch | jede Saldenänderung | Bestände; ObjectEvent OBSERVE in EPCIS |
Frozen(account, tokenId, amount), ForcedTransfer(from, to, tokenId, amount) | Durchsetzung | Pfandrechte, Beschlagnahmen |
HookRegistered(hook, flags, position), HookRemoved(hook) | Hook-Administrator | Regeländerungen |
Attested, AttestationRevoked, TrustedIssuerUpdated | ARexAttestations | Nachweise |
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
- Reproduzierbarer Build (
npm ci), CI grün, Schnittstellen-IDs entsprechen der Spezifikation. - Rollen wie in Schritt 3 neu verteilt; Rollen des Deployment-Betreibers abgegeben.
- Hooks auditiert, Reihenfolge dokumentiert,
getHooks()veröffentlicht. - Version des Metadatenschemas und Statustabelle veröffentlicht;
originRefund eingeschränkte Daten frei von personenbezogenen Daten. - Rechtsinstrumente in den Metadaten referenziert; Einordnung des Angebots mit Rechtsberatern geklärt.
- Audit von Token, Attestierungsregister und jedem registrierten Hook durch Dritte (Paket:
docs/AUDIT-READINESS.md). - Upgrade an einem Fork mit
validateUpgradeerprobt.