Whitepaper · 07
Hooks and Reference Modules
How functionality is added to a conformant token without touching the standard: the hook contract, the reference hooks, the companion extensions, and how to write, test and register your own.
Modules are demonstrations, not the standard
The relationship between A-REX Core and its modules is the one between ERC-3643 and T-REX's compliance modules: the interface through which modules plug in is standardized; the modules are not. A conformant token with zero hooks is a valid A-REX token. A hook written by anyone is compatible with every conformant token. This is what makes the ecosystem grow without the standard changing.
graph LR
T[ARexToken<br/>conformant token] -->|"check / onExecuted / canSend / canReceive"| H1[EligibilityHook]
T --> H2[ExpiryHook]
T --> H3[AttestationHook]
T --> H4[Your hook]
H3 --> A[ARexAttestations]The hook contract
| Callback | When | Must |
|---|---|---|
check | before balances change, per affected id, hooks in registration order, stop at first non-zero | be a view; never revert for policy |
onExecuted | after balances change, per id, in order | not call the token's balance-changing functions (the token is locked) |
canSend / canReceive | on every transfer and for the token's ERC-7943 views | be views |
op is one of OP_MINT (1), OP_TRANSFER (2), OP_BURN (3), OP_FORCED_TRANSFER (4), OP_TRANSFORM (5). In a transformation, inputs arrive with to == 0 and outputs with from == 0. data is the ERC-1155 transfer data, abi.encode(processRef) for transforms, empty for forced transfers.
The calling token is msg.sender. A hook that needs the batch reads IARexBatch(msg.sender).batchOf(id).
Reference hooks
EligibilityHook — allow, block, sanction
Demonstrates HOOK_ELIGIBILITY. Maintains an optional allowlist, a blocklist and a sanctions list; canSend/canReceive return their conjunction; check returns SANCTIONED, SENDER_INELIGIBLE or RECEIVER_INELIGIBLE so that interfaces can explain the refusal. Forced transfers are always allowed: seizing goods from a sanctioned account is the point of enforcement. In production the lists are fed by an identity registry (ONCHAINID, EAS, verifiable credentials) or a sanctions oracle; the hook does not care which.
ExpiryHook — perishability without a module
Stateless. Reads expiresAt and producedAt from the batch. Blocks mints and transfers of expired batches (BATCH_EXPIRED) but not burns (write-off) or forced transfers (enforcement). Optionally blocks transfers of batches that are not yet produced (BATCH_NOT_PRODUCED) — a platform that finances pre-harvest sets the flag to allow them; a spot marketplace sets it to block them. This one hook covers the blocking part of perishability; decay curves become a view helper or an off-chain valuation.
AttestationHook — regulatory gating per commodity
Maps a commodity code to a required attestation topic. Before a transfer, reads the batch, looks up the requirement, asks the attestation registry isAttested and returns ATTESTATION_MISSING otherwise. "Coffee needs an EUDR due-diligence reference", "organic-labelled batches need a valid organic certificate", "cattle needs a sanitary certificate" are each one call to setRequirement.
PerishabilityHook — decay models
Decay configuration per commodity code with per-batch override; models NONE, LINEAR (to a floor at expiry), EXPONENTIAL (half-life) and STEP (drop every interval). Views valueBps, secondsRemaining, isExpired. Blocks mints and transfers of expired batches (BATCH_EXPIRED) and transfers of batches whose remaining value is zero (PERISH:NO_VALUE); never blocks burns or enforcement.
SeasonalHook — harvest and trading windows
Absolute [start, end] windows per commodity and operation (at most 16 per key). A commodity/op with no windows is unrestricted. Typical use: OP_MINT limited to the harvest window, OP_TRANSFER to the trading window; SEASON_CLOSED otherwise.
DeliveryHook — custody and settlement
A sender schedules a delivery of amount units to a receiver via a carrier; the units are locked (an O(1) counter, so check never iterates over history). Carrier and sensors record pickup, transit and temperature readings (breach flag); the receiver confirms delivery; disputes go to an arbiter. Settlement is the ERC-1155 transfer itself: sender → receiver, exact amount, with data = abi.encode(deliveryId); check allows it only when the delivery is DELIVERED and onExecuted marks it SETTLED and unlocks. Free (unlocked) units keep moving. Locked units cannot be transferred, burned or used as inputs of a transform: check returns IN_TRANSIT for any operation that would take the holder's balance below the locked amount, except the settlement transfer, mints and forced transfers. After a forced transfer has seized locked units, check still returns a code instead of reverting.
TraceabilityHook — EPCIS-shaped event log
Observes every token operation and classifies it with CBV business steps (commissioning, shipping, destroying, transforming) and dispositions (active, in_transit, destroyed, seized); RECORDER_ROLE records non-token critical tracking events (cooling, packing, receiving) with readPoint and bizLocation. Two modes: emit-only (about +17 k gas per transfer; an indexer rebuilds the log) or persist (about +184 k; getEvents answers on-chain, paginated).
Companion extensions
Three concerns never needed to be transfer rules; they are plain contracts that read or write attestations:
| Extension | What it does |
|---|---|
CommodityPriceOracle | reporters submit prices per commodity; rounds are numbered, finalized by quorum with the median, or manually once a minimum is reached; latestAnswer, isStale |
CarbonImmobilizationWrapper | records credits immobilised in a registry (registry, serial range, vintage, tonnes, proof), links tonnes to batches, mirrors retirement and release, writes the attestation AREX:CARBON:IMMOBILIZED; never mints credits |
ParametricInsurance | ERC-20 pool; policy per batch with a trigger topic; the claim pays when the trigger is attested before expiry |
Old lots and new lots
A rule switched on for a token that already holds lots either governs every lot or only the lots created from a point in time on. The reference ExpiryHook, AttestationHook, SeasonalHook and PerishabilityHook take that point (appliesFrom, 0 for every lot) when they are deployed. It cannot be changed afterwards, so nobody can exempt more lots later; a different choice is a new deployment of the rule.
Describing a rule to the systems that use it
A rule or an add-on can publish an arex.module/1 descriptor next to its code. It is a versioned JSON document that names the contract's parameters and settings, the batch fields it reads, the certificate types it expects, its events and rejection codes, and how its state carries over to a new version. A platform, a wallet, an indexer or an auditor reads it to show the rule, configure it and explain a refusal without code written for that rule. Since revision 1.1 the descriptor may also list the operations the rule governs (ops: mint, transfer, burn, forced_transfer, transform). The field is informative: when it is absent the operations are unknown, and no consumer may rely on it for enforcement, because the hook's own checks decide.
The descriptor is informative and Proposed: it was proposed by the A-REX Platform, its first consumer, and becomes a recommended format when a second, independent implementer consumes it. Fields that a rule adds to a batch's metadata document would live under extensions.<key>, validated by the descriptor's schema fragment (ADR-017, Proposed, for metadata schema 1.3.0).
Writing your own hook
The worked example contracts/examples/SeasonHook.sol stays in the repository. test/standard/ImplementationGuide.test.js exercises it for readers with access. SeasonalHook is the reference module for several windows per commodity and per operation.
Rules of thumb:
- The symbols
OKandOP_FORCED_TRANSFERare defined in the normative text. - Burn behaviour is defined in the normative text.
- The callback
checkis defined in the normative text. - Code names such as
ARexCodes.code("MYNS", "NAME")are defined in the normative text. - Flag values are defined in the normative text.
- The callbacks
onExecutedandcheckare defined in the normative text.
Registering and ordering
Put cheap, high-rejection rules first. HOOK_ADMIN_ROLE should sit behind a multisig or timelock: whoever controls the pipeline controls which transfers are possible.
Testing
test/standard/helpers.js deploys the reference stack. RecordingHook records every callback and can be told to reject with any code; ReentrantHook attempts to re-enter the token from onExecuted and proves the lock. DomainHooks.test.js and Extensions.test.js cover the ported hooks and companions; Invariants.test.js runs a seeded random sequence of operations and checks supply conservation, terminal status and that canTransfer predicts every outcome.
Interoperating with ERC-3643 compliance
An ERC-3643 compliance module exposes moduleCheck(from, to, value, compliance). A hook that calls it for OP_TRANSFER and maps false to a code makes country restrictions, balance caps and time locks written for security tokens reusable on batch tokens. The reverse — exposing an A-REX hook to an ERC-3643 token — is equally mechanical. Chainlink's Automated Compliance Engine integrates through the same pattern: its policy engine becomes one hook. These two directions are designs. The reference adapter that exists goes the other way round and only reads: it lets ERC-3643 wallets and compliance tooling see each lot as an ERC-3643 token through a notification-only hook that cannot allow or refuse a transfer (ADR-020, Proposed).