Skip to main contentA-REX Core

白皮书 · 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]

钩子约定

interface IARexHook is IERC165 {
    function hookFlags() external view returns (uint8);   // CHECK=1 · EXECUTED=2 · ELIGIBILITY=4
    function check(uint8 op, address operator, address from, address to, uint256 id, uint256 amount, bytes calldata data)
        external view returns (bytes32 code);            // 0 = allowed
    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);
}
回调时机必须
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)。

参考钩子

EligibilityHook:白名单、黑名单、制裁

演示 HOOK_ELIGIBILITY。维护一个可选的白名单、一个黑名单和一个制裁名单;canSend/canReceive 返回它们的合取;check 返回 SANCTIONED、SENDER_INELIGIBLE 或 RECEIVER_INELIGIBLE,以便界面能够解释拒绝的原因。强制转移始终被允许:从被制裁账户手中扣押货物,正是强制执行的意义所在。在生产环境中,这些名单由身份注册表(ONCHAINID、EAS、可验证凭证)或制裁预言机提供数据;钩子并不关心是哪一种。

ExpiryHook:无需模块的易腐性

无状态。从批次中读取 expiresAt 和 producedAt。阻止已到期批次的铸造和转移(BATCH_EXPIRED),但不阻止销毁(核销)或强制转移(强制执行)。可选地阻止尚未生产的批次的转移(BATCH_NOT_PRODUCED)——为收获前融资的平台将该标志设为允许转移;现货市场则将其设为阻止转移。这一个钩子涵盖了易腐性中的阻止部分;衰减曲线则成为一个视图辅助函数或链下估值。

AttestationHook:按商品的监管门槛

把商品代码映射到所需的证明主题。转移之前,它读取批次、查找要求、向证明注册表询问 isAttested(id, topic),若结果为否则返回 ATTESTATION_MISSING。“咖啡需要有 EUDR 尽职调查引用”“带有机标签的批次需要有效的有机认证证书”“牛需要卫生证书”,各自只需调用一次 setRequirement。

PerishabilityHook:衰减模型

按商品代码配置衰减,并可按批次覆盖;模型有 NONE、LINEAR(衰减至到期时的下限)、EXPONENTIAL(半衰期)和 STEP(每个区间下降一次)。视图函数有 valueBps、secondsRemaining、isExpired。阻止已到期批次的铸造和转移(BATCH_EXPIRED),并阻止剩余价值为零的批次的转移(PERISH:NO_VALUE);绝不阻止销毁或强制执行。

SeasonalHook:收获窗口与交易窗口

按商品和操作设置绝对的 [start, end] 窗口(每个键最多 16 个)。没有窗口的商品/操作不受限制。典型用法:OP_MINT 限于收获窗口,OP_TRANSFER 限于交易窗口;其余时间返回 SEASON_CLOSED。

DeliveryHook:保管与结算

发送方安排通过承运方向接收方交付 amount 个单位;这些单位被锁定(O(1) 计数器,因此 check 绝不会遍历历史记录)。承运方和传感器记录提货、运输和温度读数(越限标志);接收方确认交付;争议提交给仲裁方。结算就是 ERC-1155 转移本身:发送方 → 接收方,数量精确,且 data = abi.encode(deliveryId);只有在交付状态为 DELIVERED 时,check 才会允许,onExecuted 将其标记为 SETTLED 并解锁。自由(未锁定)的单位可继续移动。

TraceabilityHook:EPCIS 形态的事件日志

观察每一次代币操作,并以 CBV 业务步骤(commissioning、shipping、destroying、transforming)和处置状态(active、in_transit、destroyed、seized)对其分类;RECORDER_ROLE 记录非代币的关键追踪事件(冷却、包装、收货),并带有 readPoint 和 bizLocation。有两种模式:仅发出事件(每次转移约增加 17 k Gas;由索引器重建日志)或持久化(约增加 184 k;getEvents 在链上应答,支持分页)。

配套扩展

有三类关注点从来不需要成为转移规则;它们是读取或写入证明的普通合约:

扩展作用
CommodityPriceOracle报告方按商品提交价格;轮次带编号,由法定人数以中位数最终确定,或在达到最低数量后手动最终确定;latestAnswer、isStale
CarbonImmobilizationWrapper记录在登记簿中被锁定的信用(登记簿、序列号范围、年份、吨数、证明),把吨数关联到批次,同步注销和解除锁定,写入证明 AREX:CARBON:IMMOBILIZED;绝不铸造信用
ParametricInsuranceERC-20 资金池;按批次投保,保单带有触发主题;当触发条件在到期前获得证明时,理赔即获赔付

编写自己的钩子

下面的钩子是 contracts/examples/SeasonHook.sol;它在 CI 中编译,test/standard/ImplementationGuide.test.js 在一个代币上对其进行测试。生产环境请使用 SeasonalHook,它支持按商品和按操作设置多个窗口。

contract SeasonHook is ARexHookBase, Ownable {
    mapping(bytes32 => uint64[2]) public window; // commodity → [open, close]


    constructor(address owner_) Ownable(owner_) {}


    function setWindow(bytes32 commodity, uint64 open, uint64 close) external onlyOwner {
        window[commodity] = [open, close];
    }


    function hookFlags() public pure override returns (uint8) {
        return ARexCodes.HOOK_CHECK;
    }


    function check(uint8 op, address, address, address, uint256 id, uint256, bytes calldata)
        external
        view
        override
        returns (bytes32)
    {
        if (op != ARexCodes.OP_TRANSFER) return ARexCodes.OK; // never block mints, burns or enforcement
        IARexBatch.Batch memory b = IARexBatch(msg.sender).batchOf(id);
        uint64[2] memory w = window[b.commodity];
        if (w[1] == 0) return ARexCodes.OK; // no window configured
        return (block.timestamp >= w[0] && block.timestamp <= w[1]) ? ARexCodes.OK : ARexCodes.SEASON_CLOSED;
    }
}

经验法则:

  1. 对 OP_FORCED_TRANSFER 返回 OK,除非钩子本身就规范强制执行。
  2. 除非确有意为之,否则不要阻止销毁:销毁就是交付和核销。
  3. 让 check 保持低成本且有上界;它在每个 id 的每次转移时都会运行。绝不遍历历史记录。
  4. 为代码设置命名空间:ARexCodes.code("MYNS", "NAME")。
  5. 保持标志为常量;代币会缓存它们。
  6. 把状态变更放在 onExecuted 中,绝不放在 check 中。

注册与排序

await token.registerHook(sanctionsHook);   // evaluated first
await token.registerHook(expiryHook);
await token.registerHook(attestationHook); // evaluated last
await token.getHooks();                    // ordered list
await token.removeHook(expiryHook);        // order of the rest preserved

把成本低、拒绝率高的规则放在前面。HOOK_ADMIN_ROLE 应当置于多签或时间锁之后:谁控制管线,谁就控制哪些转移是可能的。

测试

test/standard/helpers.js 部署参考技术栈。RecordingHook 记录每一次回调,并且可以被指示以任意代码拒绝;ReentrantHook 尝试从 onExecuted 重新进入代币,以验证锁的有效性。DomainHooks.test.js 和 Extensions.test.js 覆盖移植而来的钩子和配套合约;Invariants.test.js 运行一个带种子的随机操作序列,并检查供应量守恒、终态,以及 canTransfer 能否预测每一种结果。

与 ERC-3643 合规的互操作

ERC-3643 合规模块公开 moduleCheck(from, to, value, compliance)。一个针对 OP_TRANSFER 调用它、并把 false 映射为某个代码的钩子,可以让为证券型代币编写的国家限制、余额上限和时间锁在批次代币上复用。反过来——把 A-REX 钩子暴露给 ERC-3643 代币——同样是机械性的。Chainlink 的 Automated Compliance Engine 通过同样的模式集成:其策略引擎成为一个钩子。