Skip to main contentA-REX Core

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);
}
CallbackWhenMust
checkbefore balances change, per affected id, hooks in registration order, stop at first non-zerobe a view; never revert for policy
onExecutedafter balances change, per id, in ordernot call the token's balance-changing functions (the token is locked)
canSend / canReceiveon every transfer and for the token's ERC-7943 viewsbe 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:

ExtensionWhat it does
CommodityPriceOraclereporters submit prices per commodity; rounds are numbered, finalized by quorum with the median, or manually once a minimum is reached; latestAnswer, isStale
CarbonImmobilizationWrapperrecords 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
ParametricInsuranceERC-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:

  1. Return OK for OP_FORCED_TRANSFER unless your hook governs enforcement itself.
  2. Do not block burns unless you mean it: burns are deliveries and write-offs.
  3. Keep check cheap and bounded; it runs on every transfer of every id. Never iterate history.
  4. Namespace your codes: ARexCodes.code("MYNS", "NAME").
  5. Keep flags constant; the token caches them.
  6. Put state changes in onExecuted, never in check.

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.