白皮书 · 07
钩子与参考模块
如何在不触及标准的前提下为符合一致性要求的代币添加功能:钩子约定、参考钩子、配套扩展,以及如何编写、测试和注册自己的钩子。
模块只是演示,而非标准
A-REX Core 与其模块之间的关系,就是 ERC-3643 与 T-REX 合规模块之间的关系:模块接入所经由的接口是标准化的;模块则不是。一个没有任何钩子的、符合一致性要求的代币,是有效的 A-REX 代币。任何人编写的钩子,都与每一个符合一致性要求的代币兼容。这正是生态系统在标准不变的情况下得以增长的原因。
graph LR
T["ARexToken<br/>符合一致性要求的代币"] -->|"check / onExecuted / canSend / canReceive"| H1[EligibilityHook]
T --> H2[ExpiryHook]
T --> H3[AttestationHook]
T --> H4["自定义钩子"]
H3 --> A[ARexAttestations]钩子约定
| 回调 | 时机 | 必须 |
|---|---|---|
check | 余额变动之前,针对每个受影响的 id,钩子按注册顺序执行,遇到第一个非零值即停止 | 是视图函数;绝不因政策原因回滚 |
onExecuted | 余额变动之后,针对每个 id,按顺序 | 不得调用代币中会改变余额的函数(代币已被锁定) |
canSend / canReceive | 每次转移时,以及用于代币的 ERC-7943 视图 | 是视图函数 |
op 是 OP_MINT (1)、OP_TRANSFER (2)、OP_BURN (3)、OP_FORCED_TRANSFER (4)、OP_TRANSFORM (5) 之一。在转化中,输入以 to == 0 到达,输出以 from == 0 到达。data 是 ERC-1155 的转移数据;对于 transform 为 abi.encode(processRef),对于强制转移为空。
调用方代币即 msg.sender。需要批次信息的钩子读取 IARexBatch(msg.sender).batchOf(id)。在部署时绑定到单一代币的钩子(ADR-025,提议中)通过 token() 公开该代币,并拒绝来自任何其他地址的调用。
当多个规则读取 data 时,ADR-023(提议中)将其定为带标签的信封:最多 8 个 (bytes32 key, bytes32 value) 条目,键像代码一样带命名空间(keccak256("AREX:DELIVERY_ID")、keccak256("AREX:DOCUMENT_REF")),每个规则只读取自己的键。32 字节的 data 对每个规则保持其原有的单一含义;其他任何内容都视为无值,读取绝不会回滚。在信封出现之前编写的规则会把信封读作无值,因此只有当代币中所有读取 data 的规则都能读取信封时,发送方才构建信封。参考 DeliveryHook 和 TraceabilityHook 自版本 2.0.0-alpha.7 起读取信封。
参考钩子
EligibilityHook:白名单、黑名单、制裁
演示 HOOK_ELIGIBILITY。维护一个可选的白名单、一个黑名单和一个制裁名单;canSend/canReceive 返回它们的合取;check 返回 SANCTIONED、SENDER_INELIGIBLE 或 RECEIVER_INELIGIBLE,以便界面能够解释拒绝的原因。强制转移始终被允许:从被制裁账户手中扣押货物,正是强制执行的意义所在。在生产环境中,这些名单由身份注册表(ONCHAINID、EAS、可验证凭证)或制裁预言机提供数据;钩子并不关心是哪一种。
ExpiryHook:无需模块的易腐性
无状态。从批次中读取 expiresAt 和 producedAt。阻止已到期批次的铸造和转移(BATCH_EXPIRED),但不阻止销毁(核销)或强制转移(强制执行)。可选地阻止尚未生产的批次的转移(BATCH_NOT_PRODUCED)——为收获前融资的平台将该标志设为允许转移;现货市场则将其设为阻止转移。这一个钩子涵盖了易腐性中的阻止部分;衰减曲线则成为一个视图辅助函数或链下估值。
AttestationHook:按商品的监管门槛
把商品代码映射到所需的证明主题。转移之前,它读取批次、查找要求、向证明注册表询问 isAttested,若结果为否则返回 ATTESTATION_MISSING。“咖啡需要有 EUDR 尽职调查引用”“带有机标签的批次需要有效的有机认证证书”“牛需要卫生证书”,各自只需调用一次 setRequirement。自参考版本 2.0.0-alpha.7 起,其所有者可以开启 checksTransform(默认关闭):此时 transform 的输入批次也需要其证书,因此持有 TRANSFORMER_ROLE 的持有人无法把未经认证的批次作为新批次交给另一个账户。输出是新批次,不在此检查。
PerishabilityHook:衰减模型
按商品代码配置衰减,并可按批次覆盖;模型有 NONE、LINEAR(衰减至到期时的下限)、EXPONENTIAL(半衰期)和 STEP(每个区间下降一次)。视图函数有 valueBps、secondsRemaining、isExpired。阻止已到期批次的铸造和转移(BATCH_EXPIRED),并阻止剩余价值为零的批次的转移(PERISH:NO_VALUE);绝不阻止销毁或强制执行。
SeasonalHook:收获窗口与交易窗口
按商品和操作设置绝对的 [start, end] 窗口(每个键最多 16 个)。没有窗口的商品/操作不受限制。典型用法:OP_MINT 限于收获窗口,OP_TRANSFER 限于交易窗口;其余时间返回 SEASON_CLOSED。自参考版本 2.0.0-alpha.7 起,管理者按边界(removeWindow)而不是按位置删除单个窗口,已结束的窗口会腾出空间:某个键已满时,添加窗口会先删除其中已结束的窗口。每次删除都会发出 WindowRemoved,因此索引器可以根据事件重建窗口。
DeliveryHook:保管与结算
发送方安排通过承运方向接收方交付 amount 个单位;这些单位被锁定(O(1) 计数器,因此 check 绝不会遍历历史记录)。承运方和传感器记录提货、运输和温度读数(越限标志);接收方确认交付;争议提交给仲裁方。结算就是 ERC-1155 转移本身:发送方 → 接收方,数量精确,且 data 中带有交付编号(32 字节的 abi.encode(deliveryId),或自版本 2.0.0-alpha.7 起信封中的 AREX:DELIVERY_ID 条目);只有在交付状态为 DELIVERED 时,check 才会允许,onExecuted 将其标记为 SETTLED 并解锁。自由(未锁定)的单位可继续移动。已锁定的单位不能被转移、销毁,也不能用作 transform 的输入:对于任何会使持有人余额低于锁定数量的操作,check 都返回 IN_TRANSIT,结算转移、铸造和强制转移除外。在强制转移扣押了已锁定的单位之后,check 仍会返回一个代码,而不是回滚。自参考版本 2.0.0-alpha.7 起,被代币冻结的单位(ERC-7943 getFrozenTokens)不能用于安排交付(freeUnits 给出可用数量);如果某笔交付的单位已被强制转移拿走,则不能将其确认或接受为已交付(LockedUnitsMoved),因为其结算永远无法发生,但仍可对其提出争议并退回;最低温度不低于最高温度的温度范围会被拒绝(InvalidRange)。
TraceabilityHook:EPCIS 形态的事件日志
观察每一次代币操作,并以 CBV 业务步骤(commissioning、shipping、destroying、transforming)和处置状态(active、in_transit、destroyed、seized)对其分类;RECORDER_ROLE 记录非代币的关键追踪事件(冷却、包装、收货),并带有 readPoint 和 bizLocation。有两种模式:仅发出事件(每次转移约增加 17 k Gas;由索引器重建日志)或持久化(约增加 184 k;getEvents 在链上应答,支持分页)。该钩子的第 2 版(参考版本 2.0.0-alpha.7)在部署时绑定到唯一一个代币:只有该代币可以报告操作,recordEvent 只接受该代币上存在的批次,因此任何人都无法以该代币的名义写入条目。其 EPCISEvent 带有条目的全部字段(操作、读取点、地点和记录者),因此仅发出事件模式不会丢失任何信息。货物移动所涉文件的引用来自 32 字节的 data 或信封中的 AREX:DOCUMENT_REF 条目,绝不来自其他规则的条目。该钩子不可升级:第 1 版实例及其事件保持原样,第 2 版是一次新的部署。
CompanyFactsHook — 企业自己的记录
仅演示 HOOK_EXECUTED:它从不拒绝操作,其 onExecuted 从不回滚,并且只绑定一个代币。它按照标准的词汇描述符(interop/VOCABULARY.md,“Records on the ledger”)记录企业自己的记录:关于某个批次、某个场地或企业本身的事实(一个 recordHash 加上最多 8 个公开值)、阶段标记(位于位索引 0–255 的加盐阶段引用,每笔交易一个或最多 50 个)、从另一家企业的代币接收的阶段、每日锚定(一个默克尔根和一个计数)以及作废记录。每次写入都带有 approvalHash,即企业中记录由谁批准的那条记录的哈希。企业的 Safe 注册定义和阶段;可委托的记录者密钥写入记录。在 transform 时,它把每个父批次经过的阶段带到输出批次上。只有对在同一区块中承诺的 processRef(commitRun),它才加入该工艺类型的阶段,且每个承诺只兑现一次。它以同样方式传递监管链模型(cocModel,取父批次中最低者)和 EUDR 覆盖(eudrCovered,父批次的逻辑与)。事件:FactRecorded, StageRecorded, ReceivedStageRecorded, Anchored, FactVoided, RunCommitted, StagesCarried, CustodyDeclared。参考版本 2.0.0-alpha.9。
RequireStageHook — 缺少要求的阶段就不能交付
在 CompanyFactsHook 之上演示 HOOK_CHECK。最多 16 项要求,每项写明其适用的操作(交付,以及可选的 transform 输入)、商品和批次必须经过的阶段。缺少某个阶段的批次以 REQUIRE_STAGE:MISSING 被拒绝;它无法读取的批次或阶段模块以 REQUIRE_STAGE:UNREADABLE 被拒绝,绝不回滚。铸造、销毁和依法令进行的转移从不被拒绝。每项要求都带有自己的 appliesFrom:在此之前创建的批次不受检查。企业的 Safe 通过 configure 设定这些要求,因此替换要求时起始点可能改变;每次变更都会发出 Configured。参考版本 2.0.0-alpha.9。
配套扩展
有三类关注点从来不需要成为转移规则;它们是读取或写入证明的普通合约:
| 扩展 | 作用 |
|---|---|
CommodityPriceOracle | 报告方按商品提交价格;轮次带编号,由法定人数以中位数最终确定,或在达到最低数量后手动最终确定;latestAnswer、isStale |
CarbonImmobilizationWrapper | 记录在登记簿中被锁定的信用(登记簿、序列号范围、年份、吨数、证明),把吨数关联到批次,同步注销和解除锁定,写入证明 AREX:CARBON:IMMOBILIZED;绝不铸造信用 |
ParametricInsurance | ERC-20 资金池;按批次投保,保单带有触发主题;当触发条件在到期前获得证明时,理赔即获赔付 |
旧批次与新批次
对已持有批次的代币启用一项规则时,该规则要么管辖每一个批次,要么只管辖自某一时间点起创建的批次。参考 ExpiryHook、AttestationHook、SeasonalHook 和 PerishabilityHook 在部署时接收这个时间点(appliesFrom,0 表示所有批次)。它此后不能更改,因此没有人能在日后豁免更多批次;要作出不同的选择,就需要重新部署该规则。RequireStageHook 是例外:它的每项要求都带有自己的 appliesFrom,企业的 Safe 可以通过 configure 替换这些要求。
向使用规则的系统描述规则
规则或附加组件可以在其代码旁发布一个 arex.module/1 描述符。它是一份带版本的 JSON 文档,列出合约的参数和设置、它读取的批次字段、它期望的证书类型、它的事件和拒绝代码,以及其状态如何迁移到新版本。平台、钱包、索引器或审计方读取它,就能展示该规则、配置它并解释一次拒绝,而无需为该规则编写专门的代码。自修订版 1.1 起,描述符还可以列出该规则所管辖的操作(ops:mint、transfer、burn、forced_transfer、transform)。该字段仅供参考:缺省时表示操作未知,任何使用方都不得依据它执行管控,因为起决定作用的是钩子自身的检查。修订版 1.2 新增两点。其一,设置可以指定一个地点(ref:site):保存的是在该地创建的批次所携带的 originRef,或者批次改为携带的链下原产地证据的引用(例如一份 EUDR 尽职调查声明),绝不是地址或坐标。其二,check 和 onExecuted 的声明 gas 在设置已填满的情况下测量:测量前,验证方用测试值对每个设置或添加值的操作按每个键调用 setup 次(除非描述符另有说明,默认为 8,最多 32),这样遍历可配置列表的规则所声明的就是列表填满时的成本。修订版 1.3 把描述符的首个使用方已在使用的内容写入规范,使其描述符能通过已发布模式的校验:人们通过审批记录的操作(附加组件的记录,每项都注明可以调用它的角色)、合约自身错误的文字说明、基于其记录的指标、规则读取的每次交付数据、事件的完整形式及其字段和文字,以及更多值类型(批次、模块的一条记录、短文本、只保存指纹的文本)。新增的每个字段都是可选的,因此此前的每个描述符仍然有效。修订版 1.4 对每个值的描述足以据此构建表单:新的值类型(带币种的金额、日期和时间、只保存指纹的文件、带格式的代码如 GTIN、国家或地区、计量单位、目录中的人员或参与方)、写入账本的内容(值本身、仅其指纹或不写入;个人数据绝不写入)、每个字段的标题、帮助、示例、分组和显示条件、来自封闭列表的跨字段校验、计算值、保留在账本之外的详细信息、显示在批次旁的只读视图,以及记录的展示方式:始终标明为记录它的组织所声明,绝不标为已验证。 修订版 1.5 为每个指纹加盐:声明 fingerprint: { "scheme": "salted-jcs/1" } 的描述符会将每个值与其各自的 32 字节盐值一起计算哈希并记录,因此无法通过逐一尝试候选值来找出短文本、数字或地块轮廓。修订版 1.6 增加了非总量的度量:除 count 和 sum 之外,度量还可以是记录中某个数值字段的平均值、最小值或最大值(avg、min、max)。它们需要该数值字段,并且从不跨分组或跨企业相加:两家企业的平均值是其全部记录的均值,而不是各自平均值的均值。度量还可以按创建批次的参与方(party)及其场地(site)分组。参考版本 2.0.0-alpha.10 的水分附加组件模板声明了一个这样的度量:按产品和场地统计的收货时平均水分。对以基点记录的数值进行的度量声明 unit: "bp",使用方将其除以 100 后以百分比显示。修订版 1.7 允许字段的 visibleWhen 使用词汇描述符的条件树,作用于同一表单的其他字段:叶子 { field, op, value } 通过 all、any 和 not 组合,最多 3 层、12 个叶子,从不是表达式,也从不读取记录的上下文。修订版 1.4 的单一条件仍然有效,但已弃用,并按等价的叶子读取。
该描述符是资料性的,状态为提议中:它由其首个使用方 A-REX Platform 提出,在第二个独立实现者使用它之后成为推荐格式。规则添加到批次元数据文档中的字段将位于 extensions.<key> 之下,由描述符的模式片段验证(ADR-017,提议中,针对元数据模式 1.3.0)。
组织还可以将自己的词汇发布为 arex.vocab/1 定义:田间作业类型(修剪、土壤采样)、工艺类型(72 小时发酵、按筛号分级)以及每种产品的阶段列表(采收、干燥、脱壳、分级、出口)。它们是数据,而不是代码。每个定义都使用描述符的字段对象,注明其版本,并映射到一个参考阶段类别和一个 GS1 CBV 业务步骤,因此不了解该组织的读者也能理解它。工艺类型是带有类型的 transform,绝不是新的操作:其 processRef 携带该类型的 4 字节标签和该次工艺数值的指纹,因此规则可以依据类型执行,任何人都可以核验该次工艺。记录逐条上链,或以每日 Merkle 根的形式上链,每个数值都用各自的盐值承诺,因此组织可以公开部分字段而保留其他字段。指纹相符表明记录自写入以来未被更改,绝不表明工作确实按声明完成。该格式为资料性,处于提议中(词汇描述符修订版 1.5)。修订版 1.1 新增可选的 form,把字段分成区块和宽度,并规定每个字段在表单中的行为(条件、默认值、只读值和计算值、掩码和模式);它只收窄字段可接受的内容,因此忽略它的读者仍按原样读取每条记录。修订版 1.2 为条件提供一种与规则描述符共用的小型语言:not、最多 3 层和 12 个叶子,以及用在各种语言中读法相同的代码读取平台对记录所知信息的叶子(作物的 EPPO 代码、种植以来的天数、场地类型、国家、批次产品的 HS 品目、其最新阶段)。未知的上下文按空值读取,修订版 1.1 的每个条件按原样仍然有效。修订版 1.3 允许阶段列表声明其顺序(order):在默认的 strict 下,只要批次缺少一个必需的前序阶段(从批次进入列表的位置算起),就不会在某一阶段记录该批次;在 flexible 下,阶段可按任意顺序记录,缺口留给报告。由批次自身历史得出的阶段(其登记、一次转换、一次接收)从不被拒绝,此前的每个列表都按 strict 读取。修订版 1.4 允许加工类型声明其产出所带的咖啡处理法(washed、natural、honey 及其黄、红、黑变体、pulped_natural、wet_hulled、anaerobic、other),取自运行中的某个字段或由类型固定,批次的 process 元数据可携带处理法(method;mixed 仅用于批次)。这是组织的声明,而非认证,即使步骤数值保持私密,它也随批次流转。修订版 1.5 允许映射到宽泛类别 separating、conditioning、sorting_grading 或 heat_treating 之一时,从每个类别的封闭列表中添加 subclass(例如 hulling、curing、screen、roasting),读者只按子类或按每个发送方的明确映射来匹配另一组织属于宽泛类别的步骤。修订版 1.6 新增 transporting 的子类 to_processor:货物送交加工方,由其为货主加工(来料加工),因此保管权转移而所有权不变。
编写自己的钩子
示例 contracts/examples/SeasonHook.sol 留在仓库中。test/standard/ImplementationGuide.test.js 为已获访问的读者演练它。SeasonalHook 是按商品和按操作设置多个窗口的参考模块。
经验法则:
- 符号
OK和OP_FORCED_TRANSFER在规范性文本中定义。 - 销毁行为在规范性文本中定义。
- 回调
check在规范性文本中定义。 - 诸如
ARexCodes.code("MYNS", "NAME")的代码名在规范性文本中定义。 - 标志值在规范性文本中定义。
- 回调
onExecuted和check在规范性文本中定义。
注册与排序
把成本低、拒绝率高的规则放在前面。HOOK_ADMIN_ROLE 应当置于多签或时间锁之后:谁控制管线,谁就控制哪些转移是可能的。
测试
test/standard/helpers.js 部署参考技术栈。RecordingHook 记录每一次回调,并且可以被指示以任意代码拒绝;ReentrantHook 尝试从 onExecuted 重新进入代币,以验证锁的有效性。DomainHooks.test.js 和 Extensions.test.js 覆盖移植而来的钩子和配套合约;Invariants.test.js 运行一个带种子的随机操作序列,并检查供应量守恒、终态,以及 canTransfer 能否预测每一种结果。自参考版本 2.0.0-alpha.8 起,参考实现还提供一套钩子测试工具包(conformance/hooks/),即任何规则都必须通过的 Foundry 测试套件,以及一个模块模板(templates/module/)。版本 2.0.0-alpha.9 增加了附加组件测试工具包(conformance/addons/)和三个经其测试的附加组件模板:收货时的水分(GrainMoisture)、作为企业声明事实记录的公平贸易证书(FairTradeCertificate),以及支付给生产者的溢价,其金额和收款方只以加盐指纹记录(PremiumPayments)。
与 ERC-3643 合规的互操作
ERC-3643 合规模块公开 moduleCheck(from, to, value, compliance)。一个针对 OP_TRANSFER 调用它、并把 false 映射为某个代码的钩子,可以让为证券型代币编写的国家限制、余额上限和时间锁在批次代币上复用。反过来——把 A-REX 钩子暴露给 ERC-3643 代币——同样是机械性的。Chainlink 的 Automated Compliance Engine 通过同样的模式集成:其策略引擎成为一个钩子。这两个方向都只是设计,尚未实现。已有的参考适配器走的是相反方向,并且只读:它通过一个仅发送通知、不能允许或拒绝转移的钩子,让 ERC-3643 钱包和合规工具把每个批次视为 ERC-3643 代币(ADR-020,提议中)。