Livre blanc · 11
Guide d'implémentation
D'un clone propre à un jeton de lot déployé, doté de hooks et attesté, pas à pas. Chaque bloc JavaScript de ce chapitre est copié depuis test/standard/ImplementationGuide.test.js et s'exécute en CI ; si un bloc ici était erroné, le build échouerait.
Comment utiliser ce guide
Les étapes s'appuient les unes sur les autres et partagent des variables : token, attestations, les signataires et les identifiants créés en cours de route. Lisez-les dans l'ordre la première fois. Exécutez-les vous-même avec :
npx hardhat test test/standard/ImplementationGuide.test.js
| Étape | Ce que vous obtenez | Qui agit | Appels principaux |
|---|---|---|---|
| 1 · Préparation | une copie de travail compilée et testée | développeur | npm ci, npm test |
| 2 · Déploiement | proxy du jeton + registre d'attestations | déployeur | upgrades.deployProxy, ARexAttestations |
| 3 · Rôles | des opérateurs au moindre privilège | administrateur | grantRole |
| 4 · Création | un lot avec engagement sur les métadonnées | émetteur | createBatch |
| 5 · Lecture | l'enregistrement du lot et l'offre | n'importe qui | batchOf, totalSupply(uint256), parentsOf |
| 6 · Transfert | un transfert vérifié | détenteur | transferCheck, safeTransferFrom |
| 7 · Filiation | fractionnement, fusion, traitement | détenteur / transformateur | transform |
| 8 · Statut | cycle de vie et mises à jour des métadonnées | rôle de statut | setBatchStatus, setBatchMetadata |
| 9 · Hooks | un pipeline de règles ordonné | administrateur des hooks | registerHook, getHooks |
| 10 · Attestation | des preuves qui débloquent les transferts | émetteur de confiance | setTrustedIssuer, attest |
| 11 · Sanctions | éligibilité avec source et TTL | gestionnaire d'éligibilité | setSanctioned |
| 12 · Mise en application | gel, pause, transfert forcé | agent de mise en application / responsable de la pause | setFrozenTokens, forcedTransfer |
| 13 · Retrait | fin de vie d'un lot | rôle de statut, détenteur | setBatchStatus(…, 255, …), burn |
| 14 · Indexation | le graphe de filiation à partir des événements | indexeur | queryFilter(BatchTransformed) |
| 15 · Mise à niveau | une mise à niveau validée de l'implémentation | responsable de la mise à niveau | validateUpgrade, upgradeProxy |
1 · Préparation
Node.js 20 ou 22 LTS et npm. Les dépendances sont épinglées dans package-lock.json (OpenZeppelin 5.3.0) ; utilisez npm ci afin que votre build soit celui que construit la 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
Les extraits ci-dessous utilisent trois petites fonctions utilitaires et nomment dix comptes. explain retraduit un code de rejet en son nom ; la liste est l'ensemble des codes définis dans 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 · Déploiement
Le jeton de référence peut être mis à niveau via UUPS ; il est donc déployé derrière un proxy ERC-1967 avec le plugin de mises à niveau d'OpenZeppelin, qui valide aussi la disposition du stockage. Le registre d'attestations est un contrat ordinaire lié au jeton.
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());
En ligne de commande, la même pile, avec en plus les hooks d'éligibilité et d'expiration, se déploie avec :
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
ou avec Hardhat Ignition :
npx hardhat ignition deploy ignition/modules/ARexToken.js --network localhost \
--parameters '{"ARexTokenModule":{"baseURI":"ipfs://<cid>/{id}.json","allowPreHarvestTransfers":true}}'
Pour un testnet ou un L2, hardhat.config.js définit sepolia, polygon et arbitrum ; renseignez l'URL RPC et PRIVATE_KEY dans .env (modèle dans .env.example). ARexToken compile en 24,136 octets avec optimizer.runs = 1, sous la limite de 24,576 octets d'EIP-170.
3 · Rôles
initialize(admin, baseURI) attribue tous les rôles à admin, ce qui est commode pour un premier déploiement et inadapté à la production. Attribuez chaque rôle opérationnel au service ou au multisig qui l'exerce :
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);
| Rôle | Autorise | Titulaire recommandé |
|---|---|---|
DEFAULT_ADMIN_ROLE | attribuer et révoquer tous les rôles ; setBaseURI | multisig de gouvernance derrière un timelock |
UPGRADER_ROLE | remplacer l'implémentation | même timelock |
HOOK_ADMIN_ROLE | registerHook, removeHook | même timelock (les changements de hooks deviennent visibles avant leur application) |
ENFORCER_ROLE | setFrozenTokens, forcedTransfer | multisig de mise en application (conformité et juridique) |
PAUSER_ROLE | pause, unpause | clé de réponse aux incidents |
ISSUER_ROLE | createBatch | service d'origination |
STATUS_ROLE | setBatchStatus, setBatchMetadata | entrepôt, inspecteur ou registre |
TRANSFORMER_ROLE | transformations de traitement (changement de denrée, d'unité ou de quantité) | transformateurs |
ATTEST_ADMIN_ROLE (sur ARexAttestations) | setTrustedIssuer, révoquer n'importe quelle attestation | autorité de certification ou plateforme |
Lorsque chaque rôle a son titulaire, le déployeur renonce à ce dont il n'a plus besoin avec renounceRole(role, deployer).
4 · Créer un lot
Un lot est un identifiant ERC-1155. Son enregistrement on-chain est petit et fixe ; tout ce qui est descriptif vit dans un document de métadonnées dont le hash est engagé on-chain (schéma : 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 renvoie l'identifiant que la transaction attribuera ; les identifiants commencent à 1 et augmentent de un en un. Le jeton rejette :
| Condition | Erreur |
|---|---|
l'appelant n'a pas ISSUER_ROLE | AccessControlUnauthorizedAccount(account, role) |
to est l'adresse zéro | ARexInvalidRecipient() |
quantity == 0 | ARexInvalidBatch("ZERO_QUANTITY") |
commodity == 0 ou unit == 0 | ARexInvalidBatch("COMMODITY_REQUIRED") / ("UNIT_REQUIRED") |
les deux dates sont renseignées et expiresAt <= producedAt | ARexInvalidBatch("EXPIRY_BEFORE_PRODUCTION") |
jeton en pause, destinataire inéligible, ou un hook rejette OP_MINT | EnforcedPause(), ERC7943CannotReceive(to), ARexHookRejected(hook, code) |
Conventions des champs :
| Champ | Convention | Exemple |
|---|---|---|
commodity | keccak256("<scheme>:<code>"), nomenclature parmi HS, FAO, GTIN, UNSPSC ou la vôtre | keccak256("HS:100199") blé |
unit | code de la Recommandation 20 de l'UN/CEFACT sous forme de bytes32 | "KGM", "TNE", "LTR", "BX" |
producedAt | secondes Unix ; 0 pour un lot pas encore récolté | vente à terme, créance sur récolte |
expiresAt | secondes Unix ; 0 pour aucune expiration | produits périssables |
originRef | hash d'une référence vers des preuves d'origine ; jamais des coordonnées | référence DDS EUDR, identifiant de registre |
metadataHash | keccak256 du document de métadonnées canonique (RFC 8785) | |
uri | emplacement du document ; gardez-la courte, elle est stockée on-chain | ipfs://<cid>, GS1 Digital Link |
5 · Lire un lot
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 surchargée dans ERC-1155 Supply (totalSupply() et totalSupply(uint256)), il faut donc l'appeler par sa signature dans ethers v6. Batch.status vaut 0 (ACTIVE), 255 (RETIRED) ou une valeur définie par l'émetteur entre les deux.
6 · Transfert
Demandez toujours d'abord. transferCheck rejoue le pipeline de transfert sous forme de vue (view) et renvoie le premier code de rejet ; 0x00…00 signifie autorisé. canTransfer est la version booléenne ERC-7943 de la même réponse.
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");
L'ordre du pipeline est fixe ; le code vous indique donc quelle règle a échoué :
| Ordre | Vérification | Code renvoyé par transferCheck | Revert de la transaction |
|---|---|---|---|
| 1 | jeton en pause | AREX:PAUSED | EnforcedPause() |
| 2 | expéditeur éligible (chaque hook ELIGIBILITY) | AREX:SENDER_INELIGIBLE | ERC7943CannotSend(from) |
| 3 | destinataire éligible | AREX:RECEIVER_INELIGIBLE | ERC7943CannotReceive(to) |
| 4 | le lot existe | AREX:BATCH_NOT_FOUND | ARexBatchNotFound(id) |
| 5 | le lot n'est pas retiré | AREX:BATCH_RETIRED | ARexBatchRetired(id) |
| 6 | amount <= balance − frozen | AREX:FROZEN | ERC7943InsufficientUnfrozenBalance(from, id, amount, unfrozen) |
| 7 | chaque hook CHECK, dans l'ordre d'enregistrement | le code du hook | ARexHookRejected(hook, code) |
La vue et la transaction évaluent dans le même ordre ; le code désigne donc toujours la vérification que la transaction échoue en premier.
Deux détails comptent en pratique. AREX:FROZEN est aussi renvoyé lorsque l'expéditeur ne détient simplement pas assez d'unités, car la vérification porte sur le solde non gelé. Et transferCheck transmet des data vides aux hooks ; un hook qui lit les données de transfert (comme le règlement de DeliveryHook) peut répondre différemment pour la transaction réelle.
7 · Filiation : fractionner, fusionner, traiter
transform est le seul moyen de fabriquer un lot à partir de lots. Elle détruit les entrées de from, émet les sorties vers to, enregistre parentsOf(output) = inputIds et émet un BatchTransformed, qui correspond à un TransformationEvent d'EPCIS. transform.staticCall renvoie les nouveaux identifiants.
Une réorganisation de lots conserve la denrée, l'unité et la quantité totale, et ne nécessite aucun rôle, seulement la propriété ou une approbation ERC-1155. Fractionner un lot en deux pour un acheteur :
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"));
Fusionner deux lots en un lot mutualisé pour une coopérative :
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"));
Le traitement modifie la denrée, l'unité ou la quantité ; il exige donc TRANSFORMER_ROLE en plus de l'approbation. La minoterie transforme 1,000 kg du blé de l'agriculteur en 750 kg de farine :
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"));
| Règle | Garantie par |
|---|---|
l'appelant est from ou approuvé par from | transform |
| chaque entrée existe, n'est pas retirée et dispose d'un solde non gelé suffisant | _update |
| réorganisation : même denrée et même unité, Σ entrées = Σ sorties | aucun rôle nécessaire |
| tout le reste | TRANSFORMER_ROLE, sinon AccessControlUnauthorizedAccount |
| les hooks voient chaque mouvement | OP_TRANSFORM pour les destructions et les émissions |
La filiation construite par ce guide :
flowchart LR
B1["#1 blé 250,000 kg<br/>agriculteur"] -->|"fractionnement · SALE-42"| B2["#2 blé 250"]
B1 -->|"fractionnement · SALE-42"| B3["#3 blé 150"]
B2 -->|"fusion · POOL-7"| B4["#4 blé 400<br/>coop"]
B3 -->|"fusion · POOL-7"| B4
B1 -->|"traitement · MILL-RUN-7"| B5["#5 farine 750<br/>minoterie"]8 · Statut et métadonnées
Seul STATUS_ROLE modifie le statut ; détenir des unités ne confère rien. Les valeurs 1..254 sont à définir par vous ; publiez leur signification dans le document de métadonnées (status.table). La mise à jour des métadonnées remplace le hash et l'URI et émet BatchMetadataUpdated, de sorte que l'historique des documents reste vérifiable.
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
Les hooks sont des contrats indépendants que le jeton appelle dans l'ordre d'enregistrement. Ici, le hook d'éligibilité s'exécute en premier, puis celui d'expiration, puis une règle selon laquelle le café exige une déclaration de diligence raisonnée EUDR attestée avant de pouvoir être vendu.
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 | Drapeaux | Constructeur | Se configure avec |
|---|---|---|---|
EligibilityHook | CHECK, ELIGIBILITY | (admin) | setAllowlistEnabled, setAllowlisted, setBlocklisted, setSanctioned |
ExpiryHook | CHECK | (allowPreHarvestTransfers) | — (lit l'enregistrement du lot) |
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) |
L'enregistrement rejette les EOA, les contrats sans prise en charge ERC-165 de IARexHook et les hooks dont les drapeaux valent 0. Placez en premier les hooks peu coûteux qui rejettent fréquemment : un rejet interrompt le pipeline. Pour écrire votre propre hook : docs/HOOK-DEVELOPMENT.md.
10 · Attestations
Une attestation est (batch, topic) → (issuer, reference, issuedAt, expiresAt, revoked), valide uniquement tant que son émetteur est de confiance pour le thème. Avec l'exigence de l'étape 9 en place, le lot de café ne peut pas circuler tant que la déclaration n'est pas attestée :
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 vaut faux lorsqu'il n'existe aucune attestation, lorsqu'elle est révoquée, lorsque expiresAt est dépassé, ou lorsque l'émetteur n'est plus de confiance pour le thème. L'émetteur ou ATTEST_ADMIN_ROLE peut la révoquer. Les thèmes sont des valeurs bytes32 libres ; utilisez keccak256("<NAMESPACE>:<NAME>"), par exemple AREX:EUDR:DDS ou AREX:CERT:ORGANIC_EU.
11 · Sanctions et éligibilité
EligibilityHook répond à canSend et canReceive pour le jeton. Une sanction enregistre sa source et une expiration facultative, de sorte qu'une mise à jour de liste peut expirer automatiquement.
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 · Mise en application
Le gel et les transferts forcés constituent la surface de mise en application d'ERC-7943, pour les sûretés, les ordonnances judiciaires et les saisies. Un gel peut dépasser le solde courant et couvre alors aussi les dépôts futurs. Un transfert forcé ignore l'éligibilité et la vérification du gel, fonctionne pendant que le jeton est en pause, abaisse le montant gelé à ce qui reste, et exécute tout de même les hooks avec 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();
Documentez la procédure selon laquelle ENFORCER_ROLE agit (ordonnance judiciaire, contrat de nantissement, avis de sanctions) et gardez le rôle derrière un multisig. Chaque action émet Frozen ou ForcedTransfer.
13 · Retrait
RETIRED (255) est terminal : plus de transfert, d'émission ni de transformation, et aucun retour en arrière. La destruction reste possible afin que les biens livrés ou détruits quittent l'offre.
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 · Indexation
Le graphe de filiation est la liste des événements BatchTransformed ; rien n'a besoin d'être reconstitué à partir des transferts individuels.
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]
| Événement | Émis par | Usage |
|---|---|---|
BatchCreated(id, issuer, commodity, unit, quantity, to) | createBatch | nouveau lot ; ObjectEvent ADD d'EPCIS |
BatchTransformed(operator, inputIds, inputAmounts, outputIds, outputAmounts, processRef) | transform | arête de filiation ; TransformationEvent d'EPCIS |
BatchStatusChanged(id, previousStatus, newStatus, reason) | setBatchStatus | cycle de vie |
BatchMetadataUpdated(id, metadataHash, uri) | setBatchMetadata | historique des documents |
TransferSingle, TransferBatch | chaque modification de solde | avoirs ; ObjectEvent OBSERVE d'EPCIS |
Frozen(account, tokenId, amount), ForcedTransfer(from, to, tokenId, amount) | mise en application | sûretés, saisies |
HookRegistered(hook, flags, position), HookRemoved(hook) | administration des hooks | changements de règles |
Attested, AttestationRevoked, TrustedIssuerUpdated | ARexAttestations | preuves |
15 · Mise à niveau
Le stockage utilise un espace de noms ERC-7201 (erc7201:arex.storage.token) ; ajoutez des champs à TokenStorage, ne les réordonnez ni ne les supprimez jamais. Validez avant chaque mise à niveau ; le plugin refuse les dispositions qui corrompraient le stockage.
await upgrades.validateUpgrade(await token.getAddress(), ARexToken, { kind: "uups" });
await upgrades.upgradeProxy(await token.getAddress(), ARexToken); // UPGRADER_ROLE
En production, UPGRADER_ROLE se trouve derrière un multisig et un timelock, et la mise à niveau est d'abord répétée sur un fork de la chaîne cible.
Tester votre intégration
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 exporte deployStack(), batchInit(), les constantes OP, HOOK, STATUS et CODES ainsi que supply(), pour vos propres tests.
Passage en production : liste de contrôle
- Build reproductible (
npm ci), CI au vert, identifiants d'interface égaux à ceux de la spécification. - Rôles redistribués comme à l'étape 3 ; rôles du déployeur abandonnés.
- Hooks audités, ordre documenté,
getHooks()publié. - Version du schéma de métadonnées et table des statuts publiées ;
originRefet données restreintes exemptes de données personnelles. - Instruments juridiques référencés dans les métadonnées ; qualification de l'offre arrêtée avec un conseil.
- Audit par un tiers du jeton, du registre d'attestations et de chaque hook enregistré (dossier :
docs/AUDIT-READINESS.md). - Mise à niveau répétée sur un fork avec
validateUpgrade.