Skip to main contentA-REX Core

Стандарт

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.

Доступно только на английском
Этот документ опубликован на английском языке. Главы whitepaper и диаграммы переведены; нормативный стандарт, руководства по системному проектированию и записи о решениях остаются на английском, чтобы существовал единый авторитетный текст.

Status note. This document is written in the format of an Ethereum Request for Comments so that it can be submitted to ethereum/ERCs once 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:

  1. 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 a transform operation that records the provenance of every split, merge or processing step.
  2. 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.
  3. 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.
  4. 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 IERC7943MultiToken exactly as published in ERC-7943 (interface id 0x41c4fbad);
  • implement IARexBatch (interface id 0xe22639e0);
  • expose its hook pipeline through IARexHookManager.getHooks() (interface id 0x964f4861);
  • return true from supportsInterface for 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 createBatch or as the operator of a transform).
  • Operation (op): one of OP_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 bytes32 value; bytes32(0) means allowed. Codes are open; the convention keccak256("NAMESPACE:NAME") and the codes in ARexCodes are 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

  • createBatch MUST be restricted to authorized accounts. Authorization MUST NOT be derived from holding tokens.
  • It MUST revert with ARexInvalidBatch if quantity == 0, init.commodity == 0, init.unit == 0, or init.expiresAt != 0 && init.producedAt != 0 && init.expiresAt <= init.producedAt.
  • producedAt == 0 MUST 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 emit BatchCreated followed by the ERC-1155 TransferSingle event.

3.2 Reading

  • batchOf MUST revert with ARexBatchNotFound for unknown ids; batchExists MUST 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.uri from ERC-1155 uri(id) when it is non-empty.

3.3 Status

  • status is an open uint8. 0 (STATUS_ACTIVE) and 255 (STATUS_RETIRED) are fixed by this standard; 1..254 are defined by the issuer and documented in metadata.
  • setBatchStatus MUST be restricted to an authorized role; token balance MUST NOT confer the right to change status.
  • Once status == 255 it 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

  • transform is the only standardized way to split, merge or process batches. It MUST burn inputAmounts[i] of inputIds[i] from from and mint outputAmounts[j] of newly created batches outputs[j] to to.
  • msg.sender MUST be from or an operator approved by from under ERC-1155.
  • It MUST revert with ARexLengthMismatch if inputIds.length != inputAmounts.length or outputs.length != outputAmounts.length, and MUST revert if either list is empty.
  • Every output batch MUST record inputIds as its parents, retrievable via parentsOf, and MUST have issuer = msg.sender.
  • It MUST run the hook pipeline with op = OP_TRANSFORM for every input burn (to = address(0)) and every output mint (from = address(0)), passing abi.encode(processRef) as data.
  • It MUST emit one BatchCreated per output, the ERC-1155 transfer events, and exactly one BatchTransformed.
  • 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

  • metadataHash MUST be the keccak256 of the canonical metadata document available at uri. Off-chain metadata SHOULD follow standard/schema/batch-metadata.schema.json, which aligns field names with GS1 EPCIS 2.0 and the FDA Food Traceability Rule key data elements.
  • originRef MUST 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 return false if the token is paused, the batch does not exist or is retired, canSend(from) or canReceive(to) is false, amount exceeds the unfrozen balance of from, or any hook with the HOOK_CHECK flag returns a non-zero code for OP_TRANSFER.
  • Public transfers MUST NOT succeed where canTransfer would return false, and MUST revert with the ERC-7943 errors for eligibility and frozen-balance violations and with ARexHookRejected for hook rejections.
  • forcedTransfer MUST be restricted to an authorized role, MUST NOT be subject to canSend/canReceive, MUST reduce the frozen amount of from so that it never exceeds the remaining balance (emitting Frozen), MUST run the hook pipeline with op = OP_FORCED_TRANSFER, and MUST emit ForcedTransfer. Implementations SHOULD allow forcedTransfer while paused.
  • setFrozenTokens MUST accept amounts greater than the balance.
  • canSend / canReceive MUST be the conjunction of every registered hook that declares HOOK_ELIGIBILITY.
  • A token SHOULD expose transferCheck(from, to, id, amount) → bytes32 returning 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 cache hookFlags(); it MUST reject a hook whose flags are 0.
  • For every operation and every affected id, the token MUST call check on each hook with HOOK_CHECK and MUST revert with ARexHookRejected(hook, code) at the first non-zero code, before any balance changes.
  • After balances have changed, the token MUST call onExecuted on each hook with HOOK_EXECUTED, in order.
  • The token MUST prevent re-entrance of its balance-changing functions during the hook sequence.
  • data passed to hooks MUST be the data argument of the ERC-1155 transfer or of createBatch, or abi.encode(processRef) for transform, or empty for forcedTransfer.
  • 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.
  • check MUST be a view and 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) for OP_FORCED_TRANSFER unless the rule specifically governs enforcement actions (for example, requiring a court-order reference in data).
  • onExecuted MUST 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);
}
  • attest MUST revert with ARexUntrustedIssuer unless isTrustedIssuer(topic, msg.sender); it MUST revert if the batch does not exist.
  • isAttested MUST return false if no record exists, if it is revoked, if expiresAt != 0 && block.timestamp >= expiresAt, or if the issuer is no longer trusted for the topic.
  • revokeAttestation MUST be callable by the attestation's issuer and MAY be callable by an administrative role.
  • Topics are open bytes32 values. 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:

CodeMeaning
keccak256("AREX:PAUSED")token paused
keccak256("AREX:FROZEN")amount exceeds unfrozen balance
keccak256("AREX:SENDER_INELIGIBLE") / RECEIVER_INELIGIBLEaccount-level eligibility
keccak256("AREX:SANCTIONED")sanctions list hit
keccak256("AREX:BATCH_NOT_FOUND") / BATCH_RETIRED / BATCH_INACTIVEbatch state
keccak256("AREX:BATCH_EXPIRED") / BATCH_NOT_PRODUCEDtemporal rules
keccak256("AREX:LIMIT_EXCEEDED") / DOCUMENT_MISSING / SEASON_CLOSED / IN_TRANSIT / ATTESTATION_MISSINGdomain 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. forcedTransfer and setFrozenTokens are 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 emits ForcedTransfer / Frozen so 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: removeHook by HOOK_ADMIN_ROLE, and pause).
  • 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 setBatchStatus can 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. transform can 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 originRef and 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 ICompliance module can be wrapped as an IARexHook in a few lines.
  • Why one hook interface instead of capability interfaces. A single check/onExecuted pair 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 with supportsInterface on every transfer costs ids × modules × probes external calls before any rule runs.
  • Why codes and not strings. bytes32 codes are indexable, translatable, comparable in tests and cheap. Strings were none of those.
  • Why status is a uint8 and not an enum. Lifecycles differ per commodity and per jurisdiction (grain elevator vs. cold chain vs. livestock). Fixing only ACTIVE and RETIRED keeps the invariant that matters (retired is final) and leaves the rest to issuers and hooks.
  • Why transform instead of separate split/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 and related rights to this specification waived via CC0.