白皮书 · 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 测试 |
| 钩子按注册顺序求值;移除时保持顺序 | removeHook 中的数组移位 | 排序测试 |
| 被拒绝的操作不改变任何余额,也不发出转移事件 | 检查在 super._update 之前运行 | 拒绝测试(已执行计数不变) |
| 权限绝不源自余额 | 每个会改变状态的函数都由角色或 ERC-1155 授权把关 | “持有代币不授予任何权利”测试 |
canTransfer == (transferCheck == 0) | canTransfer 即按此定义 | 一致性测试 |
威胁模型
参与者与能力
| 参与者 | 可以 | 不可以 |
|---|---|---|
| 持有人 | 向具备资格的接收方转移未冻结的单位;销毁;在商品和单位相同且数量守恒的前提下重新分批 | 更改状态、铸造、加工成另一种商品、移动已冻结的单位 |
| 获批准的操作方 | 对授权账户而言与持有人相同 | 持有人不能做的任何事 |
ISSUER_ROLE | 创建批次 | 移动他人的代币 |
TRANSFORMER_ROLE | 对其持有或已获授权的代币执行加工类转化 | 未经授权转化代币 |
STATUS_ROLE | 更改状态(包括注销)、更新元数据 | 移动代币 |
ENFORCER_ROLE | 冻结任意数量;强制任意转移 | 绕过钩子(钩子会以 OP_FORCED_TRANSFER 运行) |
HOOK_ADMIN_ROLE | 添加/移除钩子 | 直接更改余额 |
PAUSER_ROLE | 暂停除强制转移之外的一切 | — |
UPGRADER_ROLE | 替换实现 | —(这是完全的权力;见密钥管理) |
| 钩子合约 | 拒绝操作;观察操作;回答资格 | 重入;更改余额 |
场景
恶意或有缺陷的钩子(拒绝服务):一个始终拒绝的钩子会阻塞该代币的所有转移;在 check 中回滚的钩子也是如此。缓解措施:HOOK_ADMIN_ROLE 可以随时移除任何钩子(顺序保持不变);PAUSER_ROLE 可以在调查期间暂停;强制转移仍会运行,因此强制执行能力不会丧失。注册时会验证 ERC-165 支持和非零标志,并拒绝 EOA。建议:在注册之前审计钩子;保持钩子数量很少;把管理员置于时间锁之后,使钩子的变更在生效之前即可见。
钩子重入:某个钩子的 onExecuted 回调 safeTransferFrom。缓解措施:重入锁;该调用以 ARexReentrantCall 回滚。请注意,ERC-1155 的接收方回调(onERC1155Received)与所有 ERC-1155 代币一样,在锁被释放之后运行;到那时代币的状态已是最终状态。
强制执行密钥被攻破:持有 ENFORCER_ROLE 的任何人都可以移动任何代币。缓解措施:采用多签并配以有文档记录的程序(法院命令、质押协议、制裁通知);每次操作都会发出 ForcedTransfer/Frozen;钩子可以要求在转移数据中带有命令引用(OP_FORCED_TRANSFER 并不豁免于钩子)。对于非紧急的强制执行,可以考虑使用时间锁。
升级者被攻破:完全沦陷。缓解措施:多签加时间锁;在 CI 中运行 OpenZeppelin 升级验证;使用 ERC-7201 存储以避免布局事故。
状态滥用:STATUS_ROLE 可以注销由他人持有的批次。这是有意为之——仓库、检验员或监管者宣布某个批次不合格——并且必须向持有人披露。缓解措施:授予负有问责责任的机构;每一次变更都是带有原因的事件。
签发方欺诈:ISSUER_ROLE 可以创建一个实物上并不存在的批次。代币无法阻止这一点;它使这种行为可归因(issuer、originRef、metadataHash),并且可以通过 AttestationHook 要求在转移之前取得独立各方(仓库、检验员、登记簿)的证明。
元数据替换: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),因此留置权可以覆盖未来的存入。
升级安全
- 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 的既定前提条件(见未来发展)。 - 对管线不变量的形式化验证已列入路线图;这些不变量足够小,可以表述为性质。
标准无法消除的剩余风险
- 批次的实物存在性和质量依靠证明来背书,而不是被证实;代币使主张可归因,而不使其为真。
- 链下元数据的可用性取决于所选择的存储;哈希保证的是完整性,而非可用性。
- 钩子和模块的治理,其可靠程度只取决于控制它的密钥。