Estándar
A-REX Core — Agricultural Batch Token Standard
A minimal extension of ERC-1155 and ERC-7943 for tokenized agricultural batches — batch identity, lineage, an ordered hook pipeline and open attestations.
Status note. This document is written in the format of an Ethereum Request for Comments so that it can be submitted to
ethereum/ERCsonce the interfaces have been exercised in production by independent implementers (see §9). Until then it is the normative text of the A-REX project. The specification text is released under CC0-1.0; the reference implementation under MIT.
Abstract
This standard defines the minimal on-chain representation of an agricultural batch (a lot of a physical commodity, or the right to a future lot) as an ERC-1155 token id, together with:
- Batch identity and lineage (
IARexBatch): open commodity and unit codes, production and expiry times, an origin reference, a metadata commitment, an issuer-controlled lifecycle status, and atransformoperation that records the provenance of every split, merge or processing step. - Enforcement (ERC-7943 multi-token interface): freezing, forced transfer and account eligibility, so that the token can honour court orders, liens, sanctions and recovery procedures.
- An ordered hook pipeline (
IARexHook,IARexHookManager): the single extension point through which any rule about a batch (perishability, seasonality, custody, documents, quotas, escrow) is enforced by independently deployed contracts returning machine-readable rejection codes. - Open attestations (
IARexAttest): certifications, inspections and regulatory declarations recorded as(batch, topic) → (issuer, reference, validity)with a per-topic trusted-issuer list.
Everything else — decay curves, delivery workflows, oracles, carbon accounting, insurance — is deliberately out of scope. Such logic is implemented as hooks or companion contracts and is demonstrated, not standardized, by the reference implementation.
Motivation
Agricultural commodities are the largest class of physical assets without an interoperable on-chain representation. Existing standards cover the two ends of the problem but not the middle:
- ERC-3643 standardizes permissioned securities on ERC-20. It has no notion of a lot, of a harvest date, of processing one good into another, or of many lots living in one contract.
- ERC-7943 standardizes the enforcement surface any real-world-asset token needs (freeze, forced transfer, eligibility), for ERC-20, ERC-721 and ERC-1155, but says nothing about what the asset is.
- Traceability standards (GS1 EPCIS 2.0, the FDA Food Traceability Rule, the EU Deforestation Regulation) define what must be recorded about a lot and its transformations, but not how a token representing that lot should behave.
A-REX Core sits between them. It adds to ERC-1155 the smallest set of fields and operations that let a token be a batch — and, above all, it makes lineage a first-class operation, because a batch of wheat that cannot become a batch of flour on-chain cannot be used for traceability, for a recall, or for the transformation events that FSMA 204 and EPCIS require.
The standard is small on purpose. The history of token standards shows that broad, opinionated interfaces (ERC-1400) fail to gather independent implementations, while narrow interfaces backed by a working reference implementation and a coalition of adopters (ERC-3643, ERC-7943) become de-facto standards before they become official ones. A-REX Core therefore:
- contains no closed enumerations (no list of crops, grades or certifications),
- stores no personal data (no coordinates, names or identifiers of natural persons),
- mandates no identity provider, oracle or registry,
- and standardizes interfaces and events only, never a deployment topology.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
1. Conformance
A conformant token MUST:
- implement ERC-1155 and ERC-165;
- implement
IERC7943MultiTokenexactly as published in ERC-7943 (interface id0x41c4fbad); - implement
IARexBatch(interface id0xe22639e0); - expose its hook pipeline through
IARexHookManager.getHooks()(interface id0x964f4861); - return
truefromsupportsInterfacefor every interface it implements.
A conformant token SHOULD implement IARexAttest (interface id 0xad603a6c) directly or through a companion contract that it references.
A conformant hook MUST implement IARexHook (interface id 0x332afcbb) and ERC-165.
Interface ids are computed as type(I).interfaceId over the Solidity interfaces in contracts/standard/ and are reproduced by npm run interface-ids.
2. Definitions
- Batch: an ERC-1155 token id whose units represent a quantity of one commodity measured in one unit, sharing the same origin, production time and metadata.
- Issuer: the account that created a batch (via
createBatchor as the operator of atransform). - Operation (
op): one ofOP_MINT (1),OP_TRANSFER (2),OP_BURN (3),OP_FORCED_TRANSFER (4),OP_TRANSFORM (5). These values are fixed by this standard. - Rejection code: a
bytes32value;bytes32(0)means allowed. Codes are open; the conventionkeccak256("NAMESPACE:NAME")and the codes inARexCodesare RECOMMENDED.
3. Layer 1 — IARexBatch
interface IARexBatch is IERC165 {
struct BatchInit {
bytes32 commodity; // namespaced code, e.g. keccak256("HS:100199")
bytes32 unit; // UN/CEFACT Rec. 20 code, e.g. bytes32("KGM")
uint64 producedAt; // 0 = not yet produced
uint64 expiresAt; // 0 = none
bytes32 originRef; // reference to origin evidence; never raw coordinates
bytes32 metadataHash; // keccak256 of the off-chain metadata document
string uri; // location of the metadata document
}
struct Batch {
address issuer; uint64 createdAt; uint8 status;
bytes32 commodity; bytes32 unit; uint64 producedAt; uint64 expiresAt;
bytes32 originRef; bytes32 metadataHash; string uri;
}
event BatchCreated(uint256 indexed id, address indexed issuer, bytes32 indexed commodity, bytes32 unit, uint256 quantity, address to);
event BatchStatusChanged(uint256 indexed id, uint8 previousStatus, uint8 newStatus, bytes32 reason);
event BatchMetadataUpdated(uint256 indexed id, bytes32 metadataHash, string uri);
event BatchTransformed(address indexed operator, uint256[] inputIds, uint256[] inputAmounts, uint256[] outputIds, uint256[] outputAmounts, bytes32 processRef);
error ARexBatchNotFound(uint256 id);
error ARexBatchRetired(uint256 id);
error ARexInvalidBatch(bytes32 reason);
error ARexInvalidStatusTransition(uint256 id, uint8 from, uint8 to);
error ARexLengthMismatch();
function createBatch(BatchInit calldata init, uint256 quantity, address to, bytes calldata data) external returns (uint256 id);
function batchOf(uint256 id) external view returns (Batch memory);
function batchExists(uint256 id) external view returns (bool);
function totalSupply(uint256 id) external view returns (uint256);
function setBatchStatus(uint256 id, uint8 status, bytes32 reason) external;
function setBatchMetadata(uint256 id, bytes32 metadataHash, string calldata uri) external;
function transform(address from, address to, uint256[] calldata inputIds, uint256[] calldata inputAmounts, BatchInit[] calldata outputs, uint256[] calldata outputAmounts, bytes32 processRef) external returns (uint256[] memory outputIds);
function parentsOf(uint256 id) external view returns (uint256[] memory);
}
3.1 Creation
createBatchMUST be restricted to authorized accounts. Authorization MUST NOT be derived from holding tokens.- It MUST revert with
ARexInvalidBatchifquantity == 0,init.commodity == 0,init.unit == 0, orinit.expiresAt != 0 && init.producedAt != 0 && init.expiresAt <= init.producedAt. producedAt == 0MUST be accepted and MUST be interpreted as "not yet produced" (pre-harvest, forward, crop receivable).- It MUST store
issuer = msg.sender,createdAt = block.timestamp,status = 0. - It MUST run the hook pipeline with
op = OP_MINT(see §5) and MUST emitBatchCreatedfollowed by the ERC-1155TransferSingleevent.
3.2 Reading
batchOfMUST revert withARexBatchNotFoundfor unknown ids;batchExistsMUST NOT revert.totalSupply(id)MUST return the circulating units of the batch and MUST be kept exact across mints, burns and transformations.- Implementations SHOULD return
Batch.urifrom ERC-1155uri(id)when it is non-empty.
3.3 Status
statusis an openuint8.0(STATUS_ACTIVE) and255(STATUS_RETIRED) are fixed by this standard;1..254are defined by the issuer and documented in metadata.setBatchStatusMUST be restricted to an authorized role; token balance MUST NOT confer the right to change status.- Once
status == 255it MUST NOT change again (ARexInvalidStatusTransition). - A token MUST reject mints and transfers of a retired batch with
ARexBatchRetired; it MUST still allow burns. - Implementations MAY restrict other transitions; if they do, they MUST document the transition table in metadata or in the contract.
3.4 Lineage
transformis the only standardized way to split, merge or process batches. It MUST burninputAmounts[i]ofinputIds[i]fromfromand mintoutputAmounts[j]of newly created batchesoutputs[j]toto.msg.senderMUST befromor an operator approved byfromunder ERC-1155.- It MUST revert with
ARexLengthMismatchifinputIds.length != inputAmounts.lengthoroutputs.length != outputAmounts.length, and MUST revert if either list is empty. - Every output batch MUST record
inputIdsas its parents, retrievable viaparentsOf, and MUST haveissuer = msg.sender. - It MUST run the hook pipeline with
op = OP_TRANSFORMfor every input burn (to = address(0)) and every output mint (from = address(0)), passingabi.encode(processRef)asdata. - It MUST emit one
BatchCreatedper output, the ERC-1155 transfer events, and exactly oneBatchTransformed. - Conservation of quantity is NOT required by this standard: processing changes units and mass. Implementations SHOULD distinguish re-batching (same commodity, same unit, conserved quantity) from processing and MAY require a stronger authorization for the latter.
BatchTransformed maps one-to-one to an EPCIS 2.0 TransformationEvent (inputQuantityList / outputQuantityList, transformationID = processRef).
3.5 Metadata
metadataHashMUST be the keccak256 of the canonical metadata document available aturi. Off-chain metadata SHOULD followstandard/schema/batch-metadata.schema.json, which aligns field names with GS1 EPCIS 2.0 and the FDA Food Traceability Rule key data elements.originRefMUST NOT contain raw geographic coordinates or any identifier of a natural person. It SHOULD be a hash of, or a reference number to, origin evidence held off-chain (an EUDR due-diligence statement reference, a registry identifier, a truncated geohash whose precision is documented).
4. Enforcement — IERC7943MultiToken
A conformant token MUST implement ERC-7943's multi-token interface with the following clarifications:
canTransfer(from, to, id, amount)MUST returnfalseif the token is paused, the batch does not exist or is retired,canSend(from)orcanReceive(to)isfalse,amountexceeds the unfrozen balance offrom, or any hook with theHOOK_CHECKflag returns a non-zero code forOP_TRANSFER.- Public transfers MUST NOT succeed where
canTransferwould returnfalse, and MUST revert with the ERC-7943 errors for eligibility and frozen-balance violations and withARexHookRejectedfor hook rejections. forcedTransferMUST be restricted to an authorized role, MUST NOT be subject tocanSend/canReceive, MUST reduce the frozen amount offromso that it never exceeds the remaining balance (emittingFrozen), MUST run the hook pipeline withop = OP_FORCED_TRANSFER, and MUST emitForcedTransfer. Implementations SHOULD allowforcedTransferwhile paused.setFrozenTokensMUST accept amounts greater than the balance.canSend/canReceiveMUST be the conjunction of every registered hook that declaresHOOK_ELIGIBILITY.- A token SHOULD expose
transferCheck(from, to, id, amount) → bytes32returning the first rejection code, so that interfaces can explain why a transfer is not allowed.
5. Layer 2 — hooks
interface IARexHook is IERC165 {
error ARexHookRejected(address hook, bytes32 code);
function hookFlags() external view returns (uint8); // bit0 CHECK, bit1 EXECUTED, bit2 ELIGIBILITY
function check(uint8 op, address operator, address from, address to, uint256 id, uint256 amount, bytes calldata data) external view returns (bytes32 code);
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);
}
interface IARexHookManager {
event HookRegistered(address indexed hook, uint8 flags, uint256 position);
event HookRemoved(address indexed hook);
error ARexHookAlreadyRegistered(address hook);
error ARexHookNotRegistered(address hook);
error ARexInvalidHook(address hook);
function getHooks() external view returns (address[] memory);
function hookFlagsOf(address hook) external view returns (uint8);
function registerHook(address hook) external; // RECOMMENDED
function removeHook(address hook) external; // RECOMMENDED
}
Token-side rules:
- The token MUST maintain an ordered list of hooks and MUST evaluate them in that order. Removing a hook MUST preserve the relative order of the others.
- On registration the token MUST verify
supportsInterface(0x332afcbb)and MUST cachehookFlags(); it MUST reject a hook whose flags are0. - For every operation and every affected id, the token MUST call
checkon each hook withHOOK_CHECKand MUST revert withARexHookRejected(hook, code)at the first non-zero code, before any balance changes. - After balances have changed, the token MUST call
onExecutedon each hook withHOOK_EXECUTED, in order. - The token MUST prevent re-entrance of its balance-changing functions during the hook sequence.
datapassed to hooks MUST be thedataargument of the ERC-1155 transfer or ofcreateBatch, orabi.encode(processRef)fortransform, or empty forforcedTransfer.- Hook lists and flags MUST be publicly readable (
getHooks,hookFlagsOf). Registration and removal MUST emit the events above.
Hook-side rules:
hookFlags()MUST be constant for the life of the hook.checkMUST be aviewand MUST NOT revert for policy reasons; it returns a code instead. It MAY revert on malformed input.- A hook MUST NOT assume it serves a single token; the calling token is
msg.sender. - Hooks SHOULD return
bytes32(0)forOP_FORCED_TRANSFERunless the rule specifically governs enforcement actions (for example, requiring a court-order reference indata). onExecutedMUST NOT call balance-changing functions of the token.
6. Layer 3 — IARexAttest
interface IARexAttest is IERC165 {
struct Attestation { address issuer; bytes32 ref; uint64 issuedAt; uint64 expiresAt; bool revoked; }
event Attested(uint256 indexed id, bytes32 indexed topic, address indexed issuer, bytes32 ref, uint64 expiresAt);
event AttestationRevoked(uint256 indexed id, bytes32 indexed topic, address indexed issuer, bytes32 reason);
event TrustedIssuerUpdated(bytes32 indexed topic, address indexed issuer, bool trusted);
error ARexUntrustedIssuer(bytes32 topic, address issuer);
error ARexAttestationNotFound(uint256 id, bytes32 topic);
function attest(uint256 id, bytes32 topic, bytes32 ref, uint64 expiresAt) external;
function revokeAttestation(uint256 id, bytes32 topic, bytes32 reason) external;
function getAttestation(uint256 id, bytes32 topic) external view returns (Attestation memory);
function isAttested(uint256 id, bytes32 topic) external view returns (bool);
function isTrustedIssuer(bytes32 topic, address issuer) external view returns (bool);
}
attestMUST revert withARexUntrustedIssuerunlessisTrustedIssuer(topic, msg.sender); it MUST revert if the batch does not exist.isAttestedMUST returnfalseif no record exists, if it is revoked, ifexpiresAt != 0 && block.timestamp >= expiresAt, or if the issuer is no longer trusted for the topic.revokeAttestationMUST be callable by the attestation's issuer and MAY be callable by an administrative role.- Topics are open
bytes32values.keccak256("<SCHEME>:<NAME>")is RECOMMENDED. ERC-735 claim topics (uint256) and Ethereum Attestation Service schema UIDs (bytes32) MAY be used directly.
7. Rejection codes and identifiers (ARexCodes)
The following are conventions, RECOMMENDED for interoperability of user interfaces and indexers:
| Code | Meaning |
|---|---|
keccak256("AREX:PAUSED") | token paused |
keccak256("AREX:FROZEN") | amount exceeds unfrozen balance |
keccak256("AREX:SENDER_INELIGIBLE") / RECEIVER_INELIGIBLE | account-level eligibility |
keccak256("AREX:SANCTIONED") | sanctions list hit |
keccak256("AREX:BATCH_NOT_FOUND") / BATCH_RETIRED / BATCH_INACTIVE | batch state |
keccak256("AREX:BATCH_EXPIRED") / BATCH_NOT_PRODUCED | temporal rules |
keccak256("AREX:LIMIT_EXCEEDED") / DOCUMENT_MISSING / SEASON_CLOSED / IN_TRANSIT / ATTESTATION_MISSING | domain rules |
Commodity codes SHOULD be keccak256("<SCHEME>:<CODE>") with SCHEME ∈ {HS, FAO, CPC, GTIN, NCM, …}. Unit codes SHOULD be UN/CEFACT Recommendation 20 codes packed as bytes32.
8. Security considerations
- Enforcement keys.
forcedTransferandsetFrozenTokensare the most powerful functions in the system. Implementations MUST restrict them to a dedicated role and SHOULD place that role behind a multisig or timelock. Every use emitsForcedTransfer/Frozenso that enforcement is auditable. - Hook trust. Hooks execute inside every transfer. A malicious or buggy hook can block all transfers (denial of service) or, if the token does not lock, re-enter it. Tokens MUST lock; administrators SHOULD only register audited hooks and SHOULD keep an emergency removal path (this reference implementation:
removeHookbyHOOK_ADMIN_ROLE, andpause). - Gas. Each hook adds at least one external call per affected id. Implementations SHOULD keep the number of hooks small and SHOULD cache flags (this standard requires it).
- Status authority. Because status is not derived from balances, the role holding
setBatchStatuscan retire a batch held by others. This is intentional (it mirrors a warehouse, inspector or regulator declaring a lot unfit) and MUST be documented to holders. - Lineage integrity.
transformcan create outputs of any commodity. Implementations SHOULD require a stronger role for processing than for re-batching and SHOULD rely on hooks or off-chain audit for mass-balance checks. - Personal data. Storing plot coordinates, names or identifiers of natural persons on a public chain is incompatible with the GDPR (EDPB Guidelines 02/2025) and similar laws. This standard forbids it in
originRefand recommends hashes and references throughout. - Upgradeability. If a token is upgradeable, it SHOULD use ERC-7201 namespaced storage and SHOULD restrict upgrades to a role distinct from operational roles.
9. Path to standardization
This text will be submitted to ethereum/ERCs when all of the following hold: (a) at least one production deployment with real batches has run for six months; (b) at least three independent implementations of IARexHook exist; (c) the reference implementation has a published third-party audit; (d) the interfaces have been unchanged for one release cycle. Until then, the interfaces MAY change with a major version bump of this repository and a migration note.
Rationale
- Why ERC-1155 and not ERC-20 per lot. A single contract holding thousands of lots with batch transfers is the natural fit for agricultural inventory; ERC-20-per-lot explodes deployment cost and breaks lineage.
- Why ERC-7943 rather than ERC-3643. ERC-3643 is ERC-20-only and prescribes an identity stack. ERC-7943 is multi-token, Final, minimal, and is the common denominator that ERC-3643-compatible compliance engines already target. An ERC-3643
ICompliancemodule can be wrapped as anIARexHookin a few lines. - Why one hook interface instead of capability interfaces. A single
check/onExecutedpair with a flags bitmap gives the token a constant number of calls per hook and lets a hook serve many tokens. Probing several capability interfaces withsupportsInterfaceon every transfer costsids × modules × probesexternal calls before any rule runs. - Why codes and not strings.
bytes32codes are indexable, translatable, comparable in tests and cheap. Strings were none of those. - Why status is a
uint8and not an enum. Lifecycles differ per commodity and per jurisdiction (grain elevator vs. cold chain vs. livestock). Fixing onlyACTIVEandRETIREDkeeps the invariant that matters (retired is final) and leaves the rest to issuers and hooks. - Why
transforminstead of separatesplit/merge. They are the same operation with different cardinalities; one function, one event, one EPCIS mapping. - Why attestations are per batch and not per identity. Certifying a producer is an identity problem already solved elsewhere (ONCHAINID, EAS, verifiable credentials). Certifying a lot is the missing piece, and it is what buyers, customs and regulators verify.
Backwards compatibility
A-REX Core tokens are fully compatible ERC-1155 and ERC-7943 tokens. Wallets, marketplaces and indexers that know ERC-1155 work unchanged; ERC-7943-aware compliance tooling recognizes the enforcement surface.
Rule engines written for other tokens can be attached through an IARexHook adapter as long as their decisions depend only on the call parameters (from, to, id, amount, data) and the batch record.
Reference implementation
contracts/reference/core/ARexToken.sol (token; UUPS, ERC-7201 storage), contracts/reference/core/ARexAttestations.sol (layer 3), contracts/reference/hooks/ (EligibilityHook, ExpiryHook, AttestationHook, PerishabilityHook, SeasonalHook, DeliveryHook, TraceabilityHook) and contracts/reference/extensions/. Tests in test/standard/. Interface ids printed by npm run interface-ids.
Copyright
Copyright and related rights to this specification waived via CC0.