白皮书 · 08
安全框架
A-REX Core 参考实现的威胁模型、不变量、访问控制、钩子信任边界、强制执行密钥管理、升级安全和审计状况。
一段话说明安全态势
参考代币只有一个必经关口(_update)、一个固定的求值顺序、围绕整个钩子序列的重入锁、绝不源自余额的基于角色的权限、ERC-7201 存储,以及一个很小的暴露面:在 ERC-1155 之外仅有八个会改变状态的外部函数。最强大的能力——冻结、强制转移、钩子管理、升级——是各自独立的角色,意在交给多签和时间锁,并且对它们的每一次使用都是一个事件。
不变量
| 不变量 | 由谁保证 | 由何测试 |
|---|---|---|
totalSupply(id) 等于 id 所有余额之和 | OpenZeppelin ERC1155Supply;transform 通过 _update 先销毁后铸造 | 谱系测试(拆分、合并、加工) |
status == 255 为终态 | setBatchStatus 以 ARexInvalidStatusTransition 回滚 | 状态测试 |
| 已注销的批次不能被铸造或转移,只能被销毁 | _update | 已注销批次测试 |
持有人发起的移动须满足 amount ≤ balance − frozen | _update;forcedTransfer 将 frozen 下调至 balance − amount | 冻结/强制转移测试 |
| 任何钩子都不能重新进入会改变余额的函数 | 检查之前设置存储锁,通知之后清除 | ReentrantHook 测试 |
每一次余额变动都作为其自身的操作、以其自身的 data 进行评估 | _update 在任何检查或外部调用之前读取并清除操作上下文 | OpContext.test.js |
| 钩子按注册顺序求值;移除时保持顺序 | removeHook 中的数组移位 | 排序测试 |
| 被拒绝的操作不改变任何余额,也不发出转移事件 | 检查在 super._update 之前运行 | 拒绝测试(已执行计数不变) |
| 权限绝不源自余额 | 每个会改变状态的函数都由角色或 ERC-1155 授权把关 | “持有代币不授予任何权利”测试 |
canTransfer == (transferCheck == 0) | canTransfer 即按此定义 | 一致性测试 |
威胁模型
参与者与能力
| 参与者 | 可以 | 不可以 |
|---|---|---|
| 持有人 | 向具备资格的接收方转移未冻结的单位;销毁;在商品和单位相同且数量守恒的前提下重新分批到其自己的账户(to == from) | 更改状态、铸造、加工成另一种商品、重新分批到另一账户、移动已冻结的单位 |
| 获批准的操作方 | 对授权账户而言与持有人相同 | 持有人不能做的任何事 |
ISSUER_ROLE | 创建批次 | 移动他人的代币 |
TRANSFORMER_ROLE | 对其持有或已获授权的代币执行加工类转化,以及输出交付到另一账户的转化 | 未经授权转化代币 |
STATUS_ROLE | 更改状态(包括注销)、更新元数据 | 移动代币 |
ENFORCER_ROLE | 冻结任意数量;强制任意转移 | 绕过钩子(钩子会以 OP_FORCED_TRANSFER 运行) |
HOOK_ADMIN_ROLE | 添加/移除钩子 | 直接更改余额 |
PAUSER_ROLE | 暂停除强制转移之外的一切 | — |
UPGRADER_ROLE | 替换实现 | —(这是完全的权力;见密钥管理) |
ATTEST_ADMIN_ROLE | 决定每个证明主题信任哪些签发方;在 2.0.0-alpha.4 及之前的参考版本中,还可以撤销任何证明(ADR-019,提议中:管理性撤销在 2.x 中不应存在,自 3.0 起不得存在) | 以签发方的名义出具证明 |
| 受信任的签发方 | 为其主题对批次出具证明;撤销自己的证明,或委托他人撤销 | 为其不受信任的主题出具证明 |
| 钩子合约 | 拒绝操作;观察操作;回答资格 | 重入;更改余额 |
场景
恶意或有缺陷的钩子(拒绝服务):一个始终拒绝的钩子会阻塞该代币的所有转移;在 check 中回滚的钩子也是如此。缓解措施:HOOK_ADMIN_ROLE 可以随时移除任何钩子(顺序保持不变);PAUSER_ROLE 可以在调查期间暂停;强制转移仍会运行,因此强制执行能力不会丧失。注册时会验证 ERC-165 支持和非零标志,并拒绝 EOA。建议:在注册之前审计钩子;保持钩子数量很少;把管理员置于时间锁之后,使钩子的变更在生效之前即可见。
钩子重入:某个钩子的 onExecuted 回调 safeTransferFrom。缓解措施:重入锁;该调用以 ARexReentrantCall 回滚。请注意,ERC-1155 的接收方回调(onERC1155Received)与所有 ERC-1155 代币一样,在锁被释放之后运行;到那时代币的状态已是最终状态。
接收方回调中的操作上下文:合约接收方可以在 onERC1155Received 内部发起一次转移或销毁。这一嵌套调用是带有其自身 data 的 OP_TRANSFER 或 OP_BURN,并不属于交付这些代币的那次操作。在 2.0.0-alpha.2 及之前的版本中,参考代币会把外层操作保存在存储中,直到外层调用返回,因此在 forcedTransfer 之后的嵌套调用会作为 OP_FORCED_TRANSFER 运行(跳过暂停、资格和冻结余额检查),而在 transform 之后的嵌套调用会作为 OP_TRANSFORM 运行,只把关 OP_TRANSFER 的钩子会将其放行。缓解措施:_update 在任何检查或外部调用之前读取并清除操作上下文,因此从回调中发起的调用按其自身进行评估;OpContext.test.js 使用一个在回调内部进行转移和销毁的接收方。
通过重新分批交付:transform 的两笔变动都以 OP_TRANSFORM 运行钩子。如果持有人可以重新分批到另一账户,货物就会易手,而不经过只把关 OP_TRANSFER 的钩子的检查,例如销售前必需的证明或为交付而锁定的单位。缓解措施:没有 TRANSFORMER_ROLE 时,只有在 to == from 时才允许重新分批;重新分批后的货物通过运行每一项转移规则的普通转移到达另一持有人(ADR-007,2026-10-20 修订)。
通过销毁或重新分批逃避锁定:按批次 id 保存的钩子状态不会随批次进入其输出。只锁定单位且只检查 OP_TRANSFER 的钩子,无法阻止持有人销毁已锁定的单位,或将其原地重新分批到一个不受锁定覆盖的新 id。缓解措施:对于任何会使余额低于锁定单位的销毁或 transform 输入变动,DeliveryHook 都以 IN_TRANSIT 拒绝;强制转移仍不受限制,并且在扣押之后钩子返回一个代码,而不是回滚。预留单位的钩子除 OP_TRANSFER 外,还应检查 OP_BURN 和 OP_TRANSFORM。
强制执行密钥被攻破:持有 ENFORCER_ROLE 的任何人都可以移动任何代币。缓解措施:采用多签并配以有文档记录的程序(法院命令、质押协议、制裁通知);每次操作都会发出 ForcedTransfer/Frozen;钩子可以要求在转移数据中带有命令引用(OP_FORCED_TRANSFER 并不豁免于钩子)。对于非紧急的强制执行,可以考虑使用时间锁。
升级者被攻破:完全沦陷。缓解措施:多签加时间锁;在 CI 中运行 OpenZeppelin 升级验证;使用 ERC-7201 存储以避免布局事故。
状态滥用:STATUS_ROLE 可以注销由他人持有的批次。这是有意为之——仓库、检验员或监管者宣布某个批次不合格——并且必须向持有人披露。缓解措施:授予负有问责责任的机构;每一次变更都是带有原因的事件。
签发方欺诈:ISSUER_ROLE 可以创建一个实物上并不存在的批次。代币无法阻止这一点;它使这种行为可归因(issuer、originRef、metadataHash),并且可以通过 AttestationHook 要求在转移之前取得独立各方(仓库、检验员、登记簿)的证明。
隐藏在来源引用中的位置:originRef 中的哈希、geohash 或对地块坐标的任何其他编码看似匿名,却可以从猜测的地块重新计算出来,而且永远无法从账本中删除。缓解措施:originRef 是指向证据的引用或一个随机引用;地理位置保留在链下(对于 EUDR,保存在欧盟信息系统中)。随 ADR-019(提议中)提出的草案把“绝不由位置推导”定为 MUST;这原本就是 ADR-008 的决定。
未经验证的上游引用:由另一代币的批次制成的批次,在其元数据的 upstream 列表中指明该批次。这是下游签发方的主张:它可能指明一个从未持有过的批次,也可能漏掉一个确实持有过的批次。缓解措施:索引器、审计方和召回工具对照上游代币核查每一条引用(批次存在、元数据哈希一致、在给出交易哈希时确有向该签发方的转移),并把无法核查的引用标示为未经验证。
元数据替换:setBatchMetadata 会更改哈希和 URI。缓解措施:由角色把关;BatchMetadataUpdated 和 URI 事件保留历史;较早的文档仍可对照较早的哈希进行验证。
对 transferCheck 的抢先交易:预检结果可能在交易执行之前过时(钩子的状态发生变化)。交易会重新求值;最坏的情况是带着精确代码回滚。
通过钩子恶意消耗 Gas:每个钩子在每个阶段对每个受影响的 id 增加一次外部调用。带有无界循环的钩子会使转移变得无法承受。缓解措施:钩子编写规则(check 有界)、注册前的审查。
可升级钩子的信任:部署在代理之后的钩子可以在代币本身不发生任何变化的情况下改变转移政策。缓解措施:对待钩子管理密钥,要与对待 HOOK_ADMIN_ROLE 一样谨慎;生产环境优先使用不可变钩子。
访问控制模型
OpenZeppelin AccessControl,代币上有八个角色,证明注册表上有一个角色;DEFAULT_ADMIN_ROLE 管理所有这些角色。推荐的生产环境布局:
| 角色 | 持有者 |
|---|---|
DEFAULT_ADMIN_ROLE、UPGRADER_ROLE、HOOK_ADMIN_ROLE | 位于时间锁之后的治理多签 |
ENFORCER_ROLE | 强制执行多签(合规 + 法务) |
PAUSER_ROLE | 事件响应密钥(快速) |
ISSUER_ROLE、STATUS_ROLE、TRANSFORMER_ROLE | 遵循最小权限并定期轮换的运营服务 |
ATTEST_ADMIN_ROLE | 代币的治理方:它决定每个主题信任哪些签发方,而不决定它们证明什么 |
initialize 为方便起见把所有权限授予同一个管理员;部署检查清单要求在上线生产之前重新分配。
强制执行与暂停语义
- 暂停会阻止铸造、转移、销毁和转化;它不会阻止强制转移,因此合法命令可以在事件期间被执行。
- 强制转移会跳过
canSend/canReceive和冻结余额检查,把冻结数量下调至剩余余额,并且仍会以OP_FORCED_TRANSFER运行钩子。参考钩子对该操作返回OK;规范强制执行的钩子可以要求在data中带有命令引用。 - 冻结数量可以超过余额(ERC-7943),因此留置权可以覆盖未来的存入。
能力在标准中,政策在配置中
ADR-019(提议中)在每一项强制执行功能中划出同一条界线。标准确定存在什么以及保证什么:角色、事件、暂停所阻止的内容、代码 AREX:PAUSED 和 AREX:SANCTIONED、谁可以撤销证明,以及隐私规则。政策配置按司法辖区和组织分别维护,并像法规一样带版本和日期,规定谁可以在什么条件下使用每一项能力:
| 能力 | 由标准确定 | 由政策配置设定 |
|---|---|---|
| 冻结与强制转移 | setFrozenTokens、forcedTransfer 及其事件,一个与管理权分离的专用角色,钩子以 OP_FORCED_TRANSFER 运行 | 是否允许启用该角色、由谁持有以及需要多少签名人、每次使用必须附带哪项法律依据、需要通知谁 |
| 暂停 | 暂停所阻止的内容、AREX:PAUSED、强制转移仍被允许 | 谁可以暂停和恢复、所需的确认和理由、告知谁 |
| 制裁 | AREX:SANCTIONED 以及 canSend / canReceive 的含义 | 适用哪些官方名单(UN、OFAC、EU、OFSI 或其组合)、如何保持更新,以及在标记账户之前须由人确认匹配 |
| 证明撤销 | 由签发方撤销,其委托的撤销方也可以撤销;管理性角色在 2.x 中不应撤销,自 3.0 起不得撤销 | 代币对每个主题信任哪些签发方;运营方如何记录争议 |
| 来源隐私 | originRef 不含坐标或个人标识,也不由位置推导 | 签发方使用哪种引用,以及地理位置在链下保存于何处 |
这样一来,法律变化就成为一次配置更新,绝不需要新的合约或新版本的标准。
升级安全
- UUPS;
_authorizeUpgrade由UPGRADER_ROLE把关。 - ERC-7201 命名空间存储结构体;仅追加。
- OpenZeppelin 升级插件的验证在测试中运行,并且应当在任何升级交易之前于 CI 中运行。
- 按文件设置的
optimizer.runs = 1使实现保持在 EIP-170 之下;使其超限的更改会在编译时以警告暴露,并在部署时因回滚而失败。
依赖与构建安全
- OpenZeppelin Contracts 和 Contracts-Upgradeable 固定为 5.3.0;锁文件已提交;CI 使用
npm ci。 - Solidity 0.8.22、viaIR、EVM
paris(不依赖瞬态存储,可部署在尚未采用 Cancun 的链上)。 - 不链接外部库;除 UUPS 代理之外不使用 delegatecall。
审计状况
- 针对参考实现、钩子和扩展的 46 项测试覆盖上述每一个不变量,其中包括一次带种子的随机化不变量运行。
- Gas 和合约大小由仓库中的脚本测量。
- 对
ARexToken、ARexAttestations和参考钩子进行第三方审计,是生产环境锚点和提交 ERC 的既定前提条件(见未来发展)。 - 对管线不变量的形式化验证已列入路线图;这些不变量足够小,可以表述为性质。
安全公告
| 公告 | 影响范围 | 修复版本 | 问题 |
|---|---|---|---|
| AREX-2026-001 | 2.0.0-alpha.2 及之前版本的参考实现 | 2.0.0-alpha.3 | 操作上下文泄漏到 ERC-1155 接收方回调中;无角色的重新分批可以绕过 OP_TRANSFER 钩子把货物交付给另一持有人;可以通过销毁或重新分批逃避 DeliveryHook 锁定 |
2.0.0-alpha.3 取代 2.0.0-alpha.2。已部署的代币代理通过 UPGRADER_ROLE 升级到新的实现;存储布局不变。DeliveryHook 不可升级:部署新版本,用 registerHook 注册它,用 removeHook 移除旧版本,并重新安排进行中的交付,因为它们的锁定保存在旧钩子的存储中。直接拆分或合并到买方账户的集成必须先原地重新分批再转移,或使用拥有 TRANSFORMER_ROLE 的账户。标准的接口不变。
标准无法消除的剩余风险
- 批次的实物存在性和质量依靠证明来背书,而不是被证实;代币使主张可归因,而不使其为真。
- 链下元数据的可用性取决于所选择的存储;哈希保证的是完整性,而非可用性。
- 钩子和模块的治理,其可靠程度只取决于控制它的密钥。