**简体中文** | [English](oz-differential.en.md)

# 与 OpenZeppelin Upgrades Core 的差分对比

存储引擎会针对每一对 fixture 与成熟参考实现
[`@openzeppelin/upgrades-core`](https://github.com/OpenZeppelin/openzeppelin-upgrades)
对比。两者拿到同样的产物并被要求给出结论；重点不是它们的措辞是否一致，而是**对于某个变更是否阻断升级，两者是否一致**，以及每一处分歧是否都被理解。

对比时把参考实现作用在编译器自己的 `storageLayout` 对象上（那正是它的比较器所消费的形态），并运行 CLI 的 `storage` 子命令。只有当引擎给出了结论（退出码 `0` 或 `1`）时，这一对才算"已比较"；
退出码 `2` 表示产物被拒绝，那不是可比较的结论。

这是开发工具，不在 CI 中运行：它需要 Node 与一份安装好的参考实现，而测试套件保持离线。

## 复现方式

```bash
npm install --prefix /tmp/ozdiff @openzeppelin/upgrades-core@1.46.0
moon build --target native
OZ_UPGRADES_CORE=/tmp/ozdiff/node_modules/@openzeppelin/upgrades-core \
  node scripts/oz-differential.mjs
```

针对 `@openzeppelin/upgrades-core` 1.46.0 运行，输出为：

```console
reference: @openzeppelin/upgrades-core 1.46.0
engine:    _build/native/debug/build/cmd/moonupgradeguard/moonupgradeguard.exe

- abi-event-indexed: engine exit 0 | reference pass | agree
- abi-fallback-kind: engine exit 0 | reference pass | agree
- abi-function-removed: engine exit 0 | reference pass | agree
- ambiguous-build-info: engine exit 2 | reference skip (no storageLayout) | n/a
- append: engine exit 0 | reference pass | agree
- compatible: engine exit 0 | reference pass | agree
- contract-selector: engine exit 2 | reference skip (no storageLayout) | n/a
- fractional-offset: engine exit 2 | reference pass | n/a
- invalid-json: engine exit 2 | reference skip (error: Expected ',' or '}' after property value in JSON at position 50 (line 4 column 1)) | n/a
- missing-file: no pair
- namespaced-storage: engine exit 2 | reference pass | n/a
- real-complex-compatible: engine exit 0 | reference pass | agree
- real-complex-incompatible: engine exit 1 | reference layoutchange, typechange | agree
- real-custom-layout-compatible: engine exit 0 | reference pass | agree
- real-custom-layout-moved: engine exit 1 | reference layoutchange | agree
- real-foundry-compatible: engine exit 0 | reference pass | agree
- real-foundry-incompatible: engine exit 1 | reference typechange | agree
- real-foundry-no-layout: engine exit 2 | reference skip (no storageLayout) | n/a
- real-hardhat-compatible: engine exit 0 | reference pass | agree
- real-hardhat-incompatible: engine exit 1 | reference typechange | agree
- real-hardhat-per-contract: engine exit 2 | reference skip (no storageLayout) | n/a
- real-oz-erc20-4.9.3-to-4.9.6: engine exit 0 | reference pass | agree
- real-oz-erc20-4.9.3-to-5.0.0: engine exit 1 | reference delete | agree
- real-oz-erc20-5.0.0-ast: engine exit 2 | reference pass | n/a
- real-solc-compatible: engine exit 0 | reference pass | agree
- real-solc-incompatible: engine exit 1 | reference typechange | agree
- real-solc-no-layout: engine exit 2 | reference skip (no storageLayout) | n/a
- schema-solc-0.5-to-0.8: engine exit 0 | reference pass | agree
- schema-solc-0.6-to-0.8: engine exit 0 | reference pass | agree
- schema-unsupported: engine exit 2 | reference rename | n/a
- storage-gap-dynamic: engine exit 1 | reference replace | agree
- storage-gap-finished: engine exit 0 | reference pass | agree
- storage-gap-shrink: engine exit 0 | reference pass | agree
- storage-gap-unsafe: engine exit 1 | reference layoutchange | agree
- storage-moved: engine exit 1 | reference delete, insert, layoutchange | agree
- storage-removed: engine exit 1 | reference delete | agree
- storage-renamed: engine exit 0 | reference rename | DIFFER
- struct-change: engine exit 1 | reference typechange | agree
- transient-append: engine exit 0 | reference skip (transient layout) | n/a
- transient-moved: engine exit 1 | reference skip (transient layout) | n/a
- transient-removed: engine exit 1 | reference skip (transient layout) | n/a
- unsupported-artifact: engine exit 2 | reference skip (no storageLayout) | n/a

compared 27 pairs, 1 divergences
  storage-renamed: engine exit 0 vs reference rename
```

## 一致的部分

- **存储 gap。** `storage-gap-shrink` 正是催生该规则的情形：基础合约把 `__gap` 的一个 slot
  用于新变量，因此 gap 缩小，但结束位置不变。两者都通过。参考实现用 `endMatchesGap` 判定，而引擎用的是同一个判据，所以这里的一致并非偶然。
- **动态数组不是 gap。** `storage-gap-dynamic` 用一个新变量替换名为 `__gap` 的动态数组：参考实现报告 `replace`（同一位置上名字与类型都变了，而动态数组的 slot 不是它认得的 gap），两者都阻断。引擎把动态数组当作普通变量比较，得到同一结论的路径更直接：那个 slot 里是数组长度，
  不是保留字节。
- **整体替换 gap。** `storage-gap-finished` 把一个 `__gap` 整体替换为结束位置相同的 struct。
  两者都接受，原因仍是同一条判据：gap 覆盖的是未使用字节，因此只有结束位置必须保持不变。
- **追加。** `append` 追加了一个变量、一个函数和一个事件：两者都通过，因为没有移动任何东西的新增是安全的。
- **移动、删除与类型变化。** `storage-moved`、`storage-removed`、`struct-change` 在两者中都阻断。特别是"struct 增加成员"对两者都是类型变化，说明引擎对递归类型变化的保守解读与参考实现一致，而不是更严格。
- **编译器版本。** 由真实 solc 0.5.17、0.6.12、0.8.28 产出的 `schema-solc-*` 各对，在两者中都通过。
- **Foundry 真实产物。** 追加变量与缩小变量宽度的两对平铺 artifact，在两者中分别通过和阻断。
- **真实开源版本。** OpenZeppelin Contracts Upgradeable 的 ERC20Upgradeable 4.9.3 与 4.9.6 使用相同编译器生成的布局有不同 `astId`，但继承变量、mapping 和 gap 的存储语义不变，两者均通过。
- **跨大版本升级。** ERC20Upgradeable 4.9.3 到 5.0.0 的旧版普通存储变量在新版布局中消失，两者均阻断；仅比较这两个 `storageLayout` 不代表已验证新版 ERC-7201 命名空间。

## 唯一一处刻意差异

| 样例对 | 引擎 | 参考实现 |
| --- | --- | --- |
| `storage-renamed` | 警告，退出码 `0` | `rename`，阻断 |

在不移动位置的前提下重命名变量，会让每个字节都留在原处，因此存储布局仍然兼容。参考实现把重命名视为不安全（除非传入 `unsafeAllowRenames`），因为新名字可能承载新的含义。引擎则报告警告，并把这一点写进公开文档：布局兼容，含义需要人工确认。

改为更严格的策略是一次严重度变更加上快照更新，而不是重写实现；但它会改变现有用户的兼容性契约，
因此留作显式决策，而不是悄悄改变。

## 对比覆盖不到的情况

- `ambiguous-build-info` 与 `contract-selector` 包含多个合约，需要 `--contract`；脚本不传选择器，引擎会报告歧义。
- 有多个带布局合约的编译器外层产物仍需显式选择合约；当前脚本只自动提取唯一布局。
- `invalid-json` 与 `missing-file` 在任何布局出现之前就被拒绝。
- `unsupported-artifact` 是 Hardhat 的单合约产物，本身不含存储布局。
- `schema-unsupported` 使用了引擎不认识的 `encoding`。引擎拒绝该产物（退出码 `2`），因为编码决定了字节如何被读取；参考实现不校验该字段，并报告了一个无关的重命名。拒绝是刻意的选择：可能影响兼容性的未知形态，绝不能被报告为兼容。
- 带有 transient storage 布局的样例对会被跳过：脚本把常规布局交给参考实现，所以它的结论与
  transient 命名空间无关，而引擎会单独比较后者。
- `namespaced-storage` 被引擎拒绝（退出码 `2`）。该 fixture 带有手写的合约结构体 AST 标注；脚本只把普通 `storageLayout` 交给参考比较器，因此它的 `pass` 不代表检查过命名空间。本工具识别该 AST 标注，但不分析命名空间成员。
- `real-oz-erc20-5.0.0-ast` 的新端含真实编译器 AST 和 ERC-7201 标注，引擎因此拒绝（退出码 `2`）；脚本只把两侧普通 `storageLayout` 交给参考比较器，空布局的 `pass` 不代表参考实现检查了命名空间，故不计入可比较案例。

## 来源说明

参考实现用于理解公开行为；为对齐 gap 规则，阅读了它公开的源码（`storage/gap.ts`、
`storage/compare.ts`）。没有复制 Upgrades Core 的源码、测试或 fixture；真实 ERC20 案例只保存 Contracts Upgradeable MIT 授权版本的编译输出，来源见[产物样例](../fixtures/README.md)。脚本只对比行为。
