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
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);
}
| 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(id, topic) 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.
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 |
Writing your own hook
The hook below is contracts/examples/SeasonHook.sol; it compiles in CI and test/standard/ImplementationGuide.test.js exercises it on a token. For production use SeasonalHook, which supports several windows per commodity and per operation.
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;
}
}
Rules of thumb:
- Return
OKforOP_FORCED_TRANSFERunless your hook governs enforcement itself. - Do not block burns unless you mean it: burns are deliveries and write-offs.
- Keep
checkcheap and bounded; it runs on every transfer of every id. Never iterate history. - Namespace your codes:
ARexCodes.code("MYNS", "NAME"). - Keep flags constant; the token caches them.
- Put state changes in
onExecuted, never incheck.
Registering and ordering
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
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.