English | 简体中文
Differential check against OpenZeppelin Upgrades Core
The storage engine is compared with the mature reference implementation,
@openzeppelin/upgrades-core,
on every fixture pair. Both tools are given the same artifacts and asked for a
verdict; the question is not whether they word things the same way, but whether
they agree on whether a change blocks an upgrade, and whether each disagreement
is understood.
The comparison runs the reference against the compiler's own storageLayout
object, which is the shape its comparator consumes, and runs the CLI's storage
subcommand. A pair counts as compared when the engine reached a verdict (exit
0 or 1); an engine exit of 2 means the artifact was refused, which is not
a verdict to compare.
This is a development tool, not part of CI: it needs Node and an installed copy of the reference package, while the test suite stays offline.
Reproducing
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
Run against @openzeppelin/upgrades-core 1.46.0, the output is:
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
What the agreement covers
- Storage gaps.
storage-gap-shrinkis the case that motivated the rule: a base contract spends one slot of its__gapon a new variable, so the gap shrinks while still ending where it ended. Both tools pass it. The reference decides this withendMatchesGap, and the engine uses the same test, which is why the two agree here rather than by accident. - A dynamic array is not a gap.
storage-gap-dynamicreplaces a dynamic array named__gapwith a new variable: the reference reportsreplace(the name and the type both change in one position, and a dynamic array's slot is not a gap it recognizes), and both block. The engine compares the dynamic array as an ordinary variable and reaches the same verdict more directly: that slot holds an array length, not reserved bytes. - Finishing a gap.
storage-gap-finishedreplaces a__gapwholesale with a struct that ends where the gap ended. Both tools accept it, again because the rule is the same one: a gap covers unused bytes, so only its end has to be preserved. - Appends.
appendadds a variable, a function, and an event: both tools pass, because an addition that moves nothing is safe. - Moves, removals, and type changes.
storage-moved,storage-removed, andstruct-changeare blocking in both. In particular a struct that gains a member is a type change for both tools, so the engine's conservative reading of recursive type changes matches the reference rather than exceeding it. - Compiler versions. The
schema-solc-*pairs, produced by real solc 0.5.17, 0.6.12, and 0.8.28, pass in both. - Real Foundry artifacts. The two flat artifacts that append a variable or narrow its width respectively pass and block in both tools.
- Real upstream releases. ERC20Upgradeable 4.9.3 and 4.9.6 from OpenZeppelin Contracts Upgradeable produce different
astIdvalues with the same compiler, while inherited variables, mappings, and gaps retain their storage meaning. Both tools pass the pair. - Major-version upgrade. Old ordinary storage variables disappear from the layout between ERC20Upgradeable 4.9.3 and 5.0.0, so both tools block the pair; comparing these two
storageLayoutobjects does not validate the new ERC-7201 namespace.
The one deliberate difference
| pair | engine | reference |
|---|---|---|
storage-renamed |
warning, exit 0 |
rename, blocking |
Renaming a variable without moving it leaves every byte where it was, so the
storage layout is still compatible. The reference treats a rename as unsafe
unless unsafeAllowRenames is passed, because the new name may carry a new
meaning. The engine reports a warning instead, and says so in the public
documentation: the layout is compatible, the meaning needs review.
Switching to the stricter policy is a severity change plus snapshot updates, not a reimplementation, but it would change the compatibility contract for existing users, so it is left as an explicit decision rather than a silent one.
Pairs the comparison cannot cover
ambiguous-build-infoandcontract-selectorhold several contracts, so they need--contract; the harness passes no selector and the engine reports the ambiguity.- Compiler wrappers containing several contracts with layouts still need an explicit contract selection; the harness extracts only a unique layout automatically.
invalid-jsonandmissing-fileare rejected before any layout exists.unsupported-artifactis a Hardhat per-contract artifact, which carries no storage layout at all.schema-unsupporteduses anencodingthe engine does not know. The engine refuses the artifact (exit2) because the encoding decides how bytes are read; the reference does not validate the field and reports an unrelated rename. Refusing is the deliberate choice: an unknown shape that can affect compatibility must not be reported as compatible.- Pairs that carry a transient storage layout are skipped: this harness gives the reference the regular layout, so its verdict says nothing about the transient namespace, which this engine compares separately.
namespaced-storageis refused by the engine (exit2). This fixture has a hand-written contract struct AST annotation; the harness gives only the ordinarystorageLayoutto the reference comparator, so itspassdoes not mean it checked the namespace. This tool recognizes the AST annotation but does not analyse namespace members.real-oz-erc20-5.0.0-asthas a real compiler AST with an ERC-7201 annotation on its new side, so the engine refuses it (exit2). The harness passes only the two ordinarystorageLayoutobjects to the reference comparator; itspasson empty layouts does not mean the reference checked the namespace, so this pair is not comparable.
Provenance
The reference was used to understand public behaviour and its published source
(storage/gap.ts, storage/compare.ts) was read to align the gap rule. No
Upgrades Core source, test, or fixture was copied; the real ERC20 cases store only compiler output from MIT-licensed Contracts Upgradeable releases, with provenance in the fixture catalogue. The harness compares behaviour only.