سجلات القرارات · ADR-025
ADR-025: A hook may be bound to one token
AREX CORE §5 says, among the hook side rules: "A hook MUST NOT assume it serves a single token; the calling token is msg.sender." The rule was written so that one hook deployment can serve many tokens, and so that a hook
- معرّف المستند
- ADR-025
- تاريخ القرار
- 2026-10-31
- المصدر
- docs/adr/ADR-025.md
A hook that keeps records in a token's name may be bound to one token at deployment. A bound hook names its token
publicly and refuses every call from any other address with a named error; a hook that is not bound keeps taking
the calling token from msg.sender.
| Field | Value |
|---|---|
| ID | ADR-025 |
| Subject area | Extensibility, hook-side rules |
| Status | Proposed (2026-10-31). Relaxes a MUST of AREX-CORE §5, so it is a major change under GOVERNANCE.md and is not released; the text in AREX-CORE.md §5 is marked Proposed (unreleased) |
| Date | 2026-10-31 |
| Owners | A-REX Contributors; requested by the A-REX Platform (gap audit 2026-10-31, item 17; lot-rules audit, items K1 and K2) |
| Related requirements | AREX-CORE §5 (hook-side rules), §8 (hook trust, hook denial of service), ADR-004, ADR-009, ADR-019 |
| Supersedes / superseded by | — |
Context
AREX-CORE §5 says, among the hook-side rules: "A hook MUST NOT assume it serves a single token; the calling token is
msg.sender." The rule was written so that one hook deployment can serve many tokens, and so that a hook never
applies the rules or the records of one token to another.
The reference TraceabilityHook of arex-contracts 2.0.0-alpha.7 (commit 87962d7, "TraceabilityHook v2 — bound to
one token, every field of an entry in its event (K1, K2)") is bound to one token at deployment: the constructor takes
the token, token() returns it, onExecuted from any other address reverts with WrongToken(caller), and
recordEvent refuses another token (WrongToken) and a lot that does not exist on it (LotNotFound). A reader of
§5 would call that hook non-conformant, and a second implementer who follows the text would build the opposite.
The binding fixes item K2 of the A-REX Platform's lot-rules audit. Version 1 of the hook keyed its entries by
msg.sender but accepted onExecuted from anyone, and recordEvent took any token and any id, so any contract
could write traceability entries "for" a token, and a holder of RECORDER_ROLE on the hook could write entries for
lots of tokens it had nothing to do with, including lots that do not exist. The EPCISEvent log is what an EPCIS
exporter reads, so these entries end up in documents that relying parties trust. The same release (item K1) put
every field of an entry in the event, so a bound instance in emit-only mode loses nothing.
A hook that serves many tokens and keeps settings or records per token needs an answer to a question the standard
does not ask: who acts for token X inside the hook? IARexBatch and IARexHookManager expose no administrator, so a
multi-token hook has to invent its own per-token roles, or trust whoever calls. ADR-009 met the same question for the
attestation companion and bound ARexAttestations to one token.
Issue
May a hook be written for one token only, and if so, what must it do so that a token administrator, an indexer and a second implementer can rely on it?
Assumptions
- Hooks are cheap to deploy compared with the cost of an authorization model that the standard does not define. The A-REX Platform deploys one instance of each rule per company token.
- The token cannot tell at registration whether a hook is bound. §5 asks the token to check
supportsInterfaceand cachehookFlags(); it does not and should not ask the hook which tokens it accepts. - A hook registered on the wrong token is a mistake of that token's administrator, and must fail loudly, not silently apply another token's rules or records.
Constraints
- No change to
IARexHook,IARexHookManageror any interface id: the binding is hook-side behaviour, not a new function of the interface. - The 2.0.0-alpha.7 bytecode stays as released (released reference builds are immutable; the A-REX Platform has deployed instances).
- Hooks that already conform (they take the token from
msg.sender) stay conformant.
Alternatives considered
| Alternative | For | Against | Outcome |
|---|---|---|---|
Keep the MUST and make TraceabilityHook multi-token in 2.0.0-alpha.8 (entries keyed by msg.sender, recorders per token) | the text stays as it is; one deployment for many tokens | needs a per-token authorization the standard does not define (who may record for token X?); reopens the K2 surface; a new release and a swap of every deployed instance with no gain for the platform, which deploys one instance per token anyway | Rejected |
| Keep the MUST and treat the reference hook as an exception, documented only in arex-contracts | no spec change | the specification and its reference implementation disagree on a MUST; a second implementer cannot tell which one to follow | Rejected |
| Remove the rule altogether | simplest text | loses what the rule protects: a hook that silently applies one token's settings to another | Rejected |
| Allow binding under conditions: the bound token is public, every call from another address is refused with a named error, the binding is declared in the rule descriptor; unbound hooks keep the rule | matches the reference and ADR-009; a wrong registration fails loudly; nothing changes for unbound hooks or for tokens | a bound deployment serves one token; a consumer must read the binding before registering a hook | Proposed |
Decision (proposed)
AREX-CORE §5, hook-side rules. The rule "A hook MUST NOT assume it serves a single token; the calling token is
msg.sender" becomes:
- A hook that is not bound to a token MUST NOT assume it serves a single token; the calling token is
msg.sender. - A hook MAY be bound to one token when it is deployed. A bound hook:
- MUST make the token it is bound to publicly readable, and SHOULD do so as
token() returns (address); the binding MUST NOT change for the life of the hook; - MUST revert, with an error that names the calling address, every call that the token makes under its flags
(
check,onExecuted,canSend,canReceive) whenmsg.senderis not that token; - MUST refuse, with a named error, any other function that writes records or settings in the name of another token;
- SHOULD declare the binding in its rule descriptor, as a constructor parameter of business type
token(interop/RULE-DESCRIPTOR.md).
- MUST make the token it is bound to publicly readable, and SHOULD do so as
- Reverting for a wrong caller is not a policy decision, so it does not conflict with "
checkMUST NOT revert for policy reasons". - A bound hook may check more than the token, for example that a lot exists on it before recording an entry
(the reference
recordEventrefuses an unknown lot withLotNotFound); the standard does not require it.
Nothing changes for tokens: they still register, order and call hooks as §5 says. A token administrator, or the tool
acting for one, SHOULD read a hook's binding (its descriptor, or token()) before registering it.
Migration: none for tokens and for unbound hooks. A bound hook built before this record (the reference
TraceabilityHook 2.0.0-alpha.7) already meets it: token() is public and immutable, onExecuted reverts with
WrongToken(caller), and recordEvent reverts with WrongToken(token) or LotNotFound(id). It has the
HOOK_EXECUTED flag only, so the token never calls its check, canSend or canReceive.
Argument
The rule protects one thing: a hook never applies one token's rules or records to another. Binding protects it more
strictly than msg.sender keying does, because a bound hook cannot be used by another token at all, and the named
error makes a wrong registration visible at the first operation. Requiring every multi-token hook to define per-token
authorization would put into the standard a model it has avoided so far (ADR-004: a hook is an independent contract
with its own administration). The reference already took this path once, in ADR-009, for the same reason. The
conditions (public binding, named refusal, declared in the descriptor) are what a consumer needs to avoid the
mistake before it happens.
Consequences
Positive
- The specification and the reference implementation agree again; a second implementer can build either kind of hook and know what each owes its users.
- Hooks that write records in a token's name (traceability logs, delivery records) can be bound and need no invented per-token roles.
- A hook registered on the wrong token fails at its first call with an error that names the caller.
Negative
- A bound deployment serves one token; a platform with many tokens deploys one instance per token (the A-REX Platform already does).
- A consumer can no longer assume that any conformant hook may be registered on any token: it has to read the binding first. The token itself cannot check it at registration.
- A relaxed MUST is a major change; until it is accepted, a bound hook conforms only to the proposed text.
Obligations it creates
- AREX-CORE.md §5 and erc/erc-draft.md carry the proposed text, marked Proposed (unreleased).
- interop/RULE-DESCRIPTOR.md says how a bound rule declares its binding.
- The conformance suite does not cover hook-side rules (they bind hooks, not tokens, see conformance/README.md); the reference hook's own tests are the evidence.
Implementation and evidence
| What | Where |
|---|---|
| Normative text (proposed) | AREX-CORE.md §5, hook-side rules; the same in erc/erc-draft.md (node scripts/check-erc-sync.mjs) |
| Reference implementation | arex-contracts 2.0.0-alpha.7, contracts/reference/hooks/TraceabilityHook.sol (token, WrongToken, LotNotFound), commit 87962d7 |
| Tests | arex-contracts test/foundry/TraceabilityHookV2.t.sol (bound token, refusals of another token and of a lot that does not exist, every field in the event) and test/standard/DomainHooks.test.js |
| Descriptor | interop/RULE-DESCRIPTOR.md, "Rules bound to one token" |
| First consumer | A-REX Platform: one trace_log instance per company token (packages/releases, release 2.0.0-alpha.7) |
Related decisions
- ADR-004: every rule is an independent hook with its own administration
- ADR-009: the reference attestation companion is bound to one token for the same reason
- ADR-019: capabilities in the standard, policy elsewhere; who may record for a token is a deployment choice, not a standard role
- ADR-023: the transfer-data envelope that
TraceabilityHookversion 2 reads