Skip to main contentA-REX Core

Conception du système · AREX-HOOK

Writing an A-REX hook

A hook is the only extension point of an A REX Core token. If you can express a rule as "given this operation on this batch between these accounts, allow or return a code", it is a hook.

Current
Identifiant du document
AREX-HOOK
Version
2.0
Date
2026-09-21
Disponible uniquement en anglais
Ce document est publié en anglais. Les chapitres du livre blanc et les diagrammes sont traduits ; le standard normatif, les guides de conception du système et les enregistrements de décision restent en anglais afin qu'il n'existe qu'un seul texte faisant foi.
FieldValue
Document IDAREX-HOOK
Version2.0
Date2026-09-21
StatusCurrent
RelatedAREX-SDD-001, AREX-SPEC

A hook is the only extension point of an A-REX Core token. If you can express a rule as "given this operation on this batch between these accounts, allow or return a code", it is a hook.

1. Contract skeleton

Source: contracts/examples/MaxLotPerBuyerHook.sol, compiled and tested in CI. It imports IERC1155 from OpenZeppelin and ARexHookBase from contracts/reference/hooks/.

contract MaxLotPerBuyerHook is ARexHookBase {
    uint256 public immutable maxUnits;


    constructor(uint256 _maxUnits) {
        maxUnits = _maxUnits;
    }


    // 1. Declare which callbacks you implement. Constant for the life of the hook.
    function hookFlags() public pure override returns (uint8) {
        return ARexCodes.HOOK_CHECK;
    }


    // 2. Validate. View. Return a code, never revert for policy.
    function check(uint8 op, address, address, address to, uint256 id, uint256 amount, bytes calldata)
        external
        view
        override
        returns (bytes32)
    {
        if (op != ARexCodes.OP_TRANSFER && op != ARexCodes.OP_MINT) return ARexCodes.OK;
        uint256 after_ = IERC1155(msg.sender).balanceOf(to, id) + amount; // msg.sender is the token
        return after_ > maxUnits ? ARexCodes.code("MAXLOT", "EXCEEDED") : ARexCodes.OK;
    }
}

ARexHookBase gives you ERC-165 registration (supportsInterface(0x332afcbb)) and no-op defaults for check, onExecuted, canSend, canReceive.

2. The callback contract

CallbackCalled whenFlagConstraints
check(op, operator, from, to, id, amount, data) → bytes32before balances change, for each affected id, hooks in registration order, stops at first non-zeroHOOK_CHECKview; MUST NOT revert for policy; bytes32(0) = allowed
onExecuted(op, operator, from, to, id, amount, data)after balances changed, for each id, in orderHOOK_EXECUTEDMUST NOT call balance-changing functions of the token (the token is locked; the call reverts)
canSend(account) / canReceive(account)eligibility, per transfer and for the token's ERC-7943 viewsHOOK_ELIGIBILITYview

op values: OP_MINT = 1, OP_TRANSFER = 2, OP_BURN = 3, OP_FORCED_TRANSFER = 4, OP_TRANSFORM = 5 (for transform, inputs arrive with to == address(0) and outputs with from == address(0)).

data: the ERC-1155 data argument (or createBatch's), abi.encode(processRef) for transforms, empty for forced transfers.

3. Rules of thumb

  • Return OK for OP_FORCED_TRANSFER unless your hook exists to govern enforcement (for example, requiring a court-order reference in data). Seizures must not be blocked by expiry or eligibility rules.
  • Do not block burns unless you really mean it. Burns are write-offs and deliveries.
  • Read the batch through IARexBatch(msg.sender).batchOf(id). Do not store the token address; one hook can serve many tokens.
  • Use namespaced codes: ARexCodes.code("MYNS", "NAME") = keccak256("MYNS:NAME"). Reuse the standard codes in ARexCodes when the meaning matches; front-ends already know them.
  • Keep check cheap. It runs on every transfer of every id. Avoid unbounded loops over history; DeliveryHook keeps an O(1) lock counter for exactly this reason.
  • Constant flags. The token caches hookFlags() at registration. Changing it later has no effect until re-registration.
  • Prefer stateless hooks. If you need state (quotas, counters), write it in onExecuted, never in check.

3a. Reference hooks to copy from

HookFlagsPattern it demonstrates
EligibilityHookCHECK, ELIGIBILITYaccount lists; sanctions with source and TTL
ExpiryHookCHECKstateless rule from the batch record
AttestationHookCHECKrule that consults IARexAttest
PerishabilityHookCHECKper-commodity config with per-batch override; value views
SeasonalHookCHECKbounded configuration (≤ 16 windows per key)
DeliveryHookCHECK, EXECUTEDstate machine; unit locks; settlement via transfer data; O(1) check
TraceabilityHookEXECUTEDobserver with emit-only vs persist modes

4. Testing a hook

Use the fixtures in test/standard/helpers.js:

const { deployStack, batchInit } = require("../standard/helpers");   // from a folder next to test/standard
const { token, admin, issuer, farmer, buyer } = await deployStack();
await token.connect(issuer).createBatch(batchInit(), 1000, farmer.address, "0x");  // before the hook exists
const hook = await (await ethers.getContractFactory("MaxLotPerBuyerHook")).deploy(100);
await token.connect(admin).registerHook(await hook.getAddress());
expect(await token.transferCheck(farmer.address, buyer.address, 1, 100)).to.equal(ethers.ZeroHash);
expect(await token.transferCheck(farmer.address, buyer.address, 1, 101))
  .to.equal(ethers.keccak256(ethers.toUtf8Bytes("MAXLOT:EXCEEDED")));
await expect(token.connect(farmer).safeTransferFrom(farmer.address, buyer.address, 1, 101, "0x"))
  .to.be.revertedWithCustomError(token, "ARexHookRejected");
await expect(token.connect(issuer).createBatch(batchInit(), 101, buyer.address, "0x"))  // mints are checked too
  .to.be.revertedWithCustomError(token, "ARexHookRejected");

RecordingHook (records every callback, can be told to reject) and ReentrantHook (tries to re-enter) in contracts/reference/hooks/mocks/ are useful for pipeline tests.

5. Wrapping an ERC-3643 compliance module

An ERC-3643 IModule exposes moduleCheck(from, to, value, compliance). A thin hook can call it for OP_TRANSFER and map false to a code, so existing compliance logic (country restrictions, max balance, time locks) is reusable on batch tokens. The reverse direction — an A-REX hook as an ERC-3643 module — is equally mechanical.

6. Registering on a token

await token.connect(hookAdmin).registerHook(hookAddress);   // appended: evaluated last
await token.getHooks();                                     // ordered list
await token.hookFlagsOf(hookAddress);                       // cached flags
await token.connect(hookAdmin).removeHook(hookAddress);     // order of the rest preserved

Order matters: put cheap, high-rejection hooks first (sanctions, pause-like rules), expensive ones last.