Skip to main contentA-REX Core

Livre blanc · 07

Hooks et modules de référence

Comment des fonctionnalités s'ajoutent à un jeton conforme sans toucher au standard : le contrat des hooks, les hooks de référence, les extensions compagnons, et la manière d'écrire, de tester et d'enregistrer les vôtres.

Les modules sont des démonstrations, non le standard

La relation entre A-REX Core et ses modules est celle qui existe entre ERC-3643 et les modules de conformité de T-REX : l'interface par laquelle les modules se branchent est standardisée ; les modules ne le sont pas. Un jeton conforme sans aucun hook est un jeton A-REX valide. Un hook écrit par n'importe qui est compatible avec tous les jetons conformes. C'est ce qui permet à l'écosystème de croître sans que le standard change.

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

Le contrat de hook

interface IARexHook is IERC165 {
    function hookFlags() external view returns (uint8);   // CHECK=1 · EXECUTED=2 · ELIGIBILITY=4
    function check(uint8 op, address operator, address from, address to, uint256 id, uint256 amount, bytes calldata data)
        external view returns (bytes32 code);            // 0 = allowed
    function onExecuted(uint8 op, address operator, address from, address to, uint256 id, uint256 amount, bytes calldata data) external;
    function canSend(address account) external view returns (bool);
    function canReceive(address account) external view returns (bool);
}
CallbackQuandDoit
checkavant la modification des soldes, par identifiant concerné, hooks dans l'ordre d'enregistrement, arrêt au premier code non nulêtre une fonction de lecture seule (view) ; ne jamais annuler la transaction pour des motifs de politique
onExecutedaprès la modification des soldes, par identifiant, dans l'ordrene pas appeler les fonctions du jeton qui modifient les soldes (le jeton est verrouillé)
canSend / canReceiveà chaque transfert et pour les vues ERC-7943 du jetonêtre des fonctions de lecture seule (view)

op vaut l'un des OP_MINT (1), OP_TRANSFER (2), OP_BURN (3), OP_FORCED_TRANSFER (4), OP_TRANSFORM (5). Dans une transformation, les entrées arrivent avec to == 0 et les sorties avec from == 0. data correspond aux données de transfert ERC-1155, abi.encode(processRef) pour les transformations, vide pour les transferts forcés.

Le jeton appelant est msg.sender. Un hook qui a besoin du lot lit IARexBatch(msg.sender).batchOf(id).

Hooks de référence

EligibilityHook — autoriser, bloquer, sanctionner

Démontre HOOK_ELIGIBILITY. Maintient une liste d'autorisation facultative, une liste de blocage et une liste de sanctions ; canSend/canReceive renvoient leur conjonction ; check renvoie SANCTIONED, SENDER_INELIGIBLE ou RECEIVER_INELIGIBLE afin que les interfaces puissent expliquer le refus. Les transferts forcés sont toujours autorisés : saisir des biens sur un compte sanctionné est précisément l'objet de la mise en application. En production, les listes sont alimentées par un registre d'identité (ONCHAINID, EAS, justificatifs vérifiables) ou par un oracle de sanctions ; peu importe au hook lequel.

ExpiryHook — la périssabilité sans module

Sans état. Lit expiresAt et producedAt dans le lot. Bloque les émissions et les transferts de lots expirés (BATCH_EXPIRED), mais pas les destructions (passage en pertes) ni les transferts forcés (mise en application). Bloque facultativement les transferts de lots pas encore produits (BATCH_NOT_PRODUCED) — une plateforme qui finance l'avant-récolte positionne le drapeau pour les autoriser ; une place de marché au comptant le positionne pour les bloquer. Ce seul hook couvre la partie bloquante de la périssabilité ; les courbes de décroissance deviennent une fonction d'aide en lecture seule (view) ou une valorisation off-chain.

AttestationHook — filtrage réglementaire par denrée

Associe un code de denrée à un thème d'attestation requis. Avant un transfert, lit le lot, recherche l'exigence, interroge le registre d'attestations avec isAttested(id, topic) et renvoie ATTESTATION_MISSING dans le cas contraire. « Le café exige une référence de diligence raisonnée EUDR », « les lots étiquetés biologiques exigent un certificat biologique valide », « les bovins exigent un certificat sanitaire » correspondent chacun à un appel à setRequirement.

PerishabilityHook — modèles de décroissance

Configuration de la décroissance par code de denrée, avec dérogation par lot ; modèles NONE, LINEAR (jusqu'à un plancher à l'expiration), EXPONENTIAL (demi-vie) et STEP (baisse à chaque intervalle). Vues valueBps, secondsRemaining, isExpired. Bloque les émissions et les transferts de lots expirés (BATCH_EXPIRED) ainsi que les transferts de lots dont la valeur restante est nulle (PERISH:NO_VALUE) ; ne bloque jamais les destructions ni la mise en application.

SeasonalHook — fenêtres de récolte et de négociation

Fenêtres absolues [start, end] par denrée et par opération (16 au plus par clé). Une denrée/opération sans fenêtre n'est soumise à aucune restriction. Usage typique : OP_MINT limité à la fenêtre de récolte, OP_TRANSFER à la fenêtre de négociation ; SEASON_CLOSED dans les autres cas.

DeliveryHook — garde et règlement

Un expéditeur planifie la livraison de amount unités à un destinataire par l'intermédiaire d'un transporteur ; les unités sont verrouillées (un compteur en O(1), de sorte que check ne parcourt jamais l'historique). Le transporteur et les capteurs consignent l'enlèvement, le transit et les relevés de température (indicateur de dépassement) ; le destinataire confirme la livraison ; les litiges sont portés devant un arbitre. Le règlement est le transfert ERC-1155 lui-même : expéditeur → destinataire, montant exact, avec data = abi.encode(deliveryId) ; check ne l'autorise que lorsque la livraison est DELIVERED, et onExecuted la marque SETTLED et déverrouille. Les unités libres (non verrouillées) continuent de circuler.

TraceabilityHook — journal d'événements calqué sur EPCIS

Observe chaque opération sur les jetons et la classe selon les étapes métier CBV (commissioning, shipping, destroying, transforming) et les dispositions (active, in_transit, destroyed, seized) ; RECORDER_ROLE consigne les événements de suivi critique hors jeton (refroidissement, emballage, réception) avec readPoint et bizLocation. Deux modes : émission seule (environ +17 k de gaz par transfert ; un indexeur reconstruit le journal) ou persistance (environ +184 k ; getEvents répond on-chain, avec pagination).

Extensions compagnons

Trois préoccupations n'ont jamais eu besoin d'être des règles de transfert ; ce sont de simples contrats qui lisent ou écrivent des attestations :

ExtensionCe qu'elle fait
CommodityPriceOracledes déclarants soumettent des prix par denrée ; les tours sont numérotés et finalisés par quorum avec la médiane, ou manuellement une fois un minimum atteint ; latestAnswer, isStale
CarbonImmobilizationWrapperenregistre les crédits immobilisés dans un registre (registre, plage de numéros de série, millésime, tonnes, preuve), relie des tonnes à des lots, reflète le retrait et la libération, écrit l'attestation AREX:CARBON:IMMOBILIZED ; n'émet jamais de crédits
ParametricInsurancepool ERC-20 ; police par lot avec un thème déclencheur ; la réclamation est payée lorsque le déclencheur est attesté avant l'expiration

Écrire votre propre hook

Le hook ci-dessous est contracts/examples/SeasonHook.sol ; il compile en CI et test/standard/ImplementationGuide.test.js l'exerce sur un jeton. En production, utilisez SeasonalHook, qui prend en charge plusieurs fenêtres par denrée et par opération.

contract SeasonHook is ARexHookBase, Ownable {
    mapping(bytes32 => uint64[2]) public window; // commodity → [open, close]


    constructor(address owner_) Ownable(owner_) {}


    function setWindow(bytes32 commodity, uint64 open, uint64 close) external onlyOwner {
        window[commodity] = [open, close];
    }


    function hookFlags() public pure override returns (uint8) {
        return ARexCodes.HOOK_CHECK;
    }


    function check(uint8 op, address, address, address, uint256 id, uint256, bytes calldata)
        external
        view
        override
        returns (bytes32)
    {
        if (op != ARexCodes.OP_TRANSFER) return ARexCodes.OK; // never block mints, burns or enforcement
        IARexBatch.Batch memory b = IARexBatch(msg.sender).batchOf(id);
        uint64[2] memory w = window[b.commodity];
        if (w[1] == 0) return ARexCodes.OK; // no window configured
        return (block.timestamp >= w[0] && block.timestamp <= w[1]) ? ARexCodes.OK : ARexCodes.SEASON_CLOSED;
    }
}

Règles pratiques :

  1. Renvoyez OK pour OP_FORCED_TRANSFER, sauf si votre hook régit lui-même la mise en application.
  2. Ne bloquez pas les destructions sauf intention délibérée : les destructions sont des livraisons et des passages en pertes.
  3. Gardez check peu coûteux et borné ; il s'exécute à chaque transfert de chaque identifiant. N'itérez jamais sur l'historique.
  4. Donnez un espace de noms à vos codes : ARexCodes.code("MYNS", "NAME").
  5. Gardez les drapeaux constants ; le jeton les met en cache.
  6. Placez les changements d'état dans onExecuted, jamais dans check.

Enregistrement et ordre

await token.registerHook(sanctionsHook);   // evaluated first
await token.registerHook(expiryHook);
await token.registerHook(attestationHook); // evaluated last
await token.getHooks();                    // ordered list
await token.removeHook(expiryHook);        // order of the rest preserved

Placez en premier les règles peu coûteuses qui rejettent beaucoup. HOOK_ADMIN_ROLE devrait se trouver derrière un multisig ou un timelock : quiconque contrôle le pipeline contrôle quels transferts sont possibles.

Tests

test/standard/helpers.js déploie la pile de référence. RecordingHook enregistre chaque callback et peut recevoir l'instruction de rejeter avec n'importe quel code ; ReentrantHook tente de ré-entrer dans le jeton depuis onExecuted et prouve le verrou. DomainHooks.test.js et Extensions.test.js couvrent les hooks et les compagnons portés ; Invariants.test.js exécute une séquence aléatoire d'opérations à graine fixée et vérifie la conservation de l'offre, le statut terminal et le fait que canTransfer prédit chaque résultat.

Interopérer avec la conformité ERC-3643

Un module de conformité ERC-3643 expose moduleCheck(from, to, value, compliance). Un hook qui l'appelle pour OP_TRANSFER et fait correspondre false à un code rend réutilisables sur des jetons de lot les restrictions par pays, les plafonds de solde et les verrous temporels écrits pour des jetons de valeurs mobilières. L'inverse — exposer un hook A-REX à un jeton ERC-3643 — est tout aussi mécanique. L'Automated Compliance Engine de Chainlink s'intègre selon le même schéma : son moteur de politiques devient un hook.