slotdiff 0.0.0-stage → 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +10 -0
  3. package/README.md +213 -2
  4. package/dist/bytecode.d.ts +46 -0
  5. package/dist/bytecode.d.ts.map +1 -0
  6. package/dist/bytecode.js +101 -0
  7. package/dist/bytecode.js.map +1 -0
  8. package/dist/chain.d.ts +20 -0
  9. package/dist/chain.d.ts.map +1 -0
  10. package/dist/chain.js +52 -0
  11. package/dist/chain.js.map +1 -0
  12. package/dist/diff.d.ts +15 -0
  13. package/dist/diff.d.ts.map +1 -0
  14. package/dist/diff.js +11 -0
  15. package/dist/diff.js.map +1 -0
  16. package/dist/errors.d.ts +18 -0
  17. package/dist/errors.d.ts.map +1 -0
  18. package/dist/errors.js +24 -0
  19. package/dist/errors.js.map +1 -0
  20. package/dist/explorer.d.ts +42 -0
  21. package/dist/explorer.d.ts.map +1 -0
  22. package/dist/explorer.js +131 -0
  23. package/dist/explorer.js.map +1 -0
  24. package/dist/index.d.ts +11 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +10 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/reconstruct.d.ts +27 -0
  29. package/dist/reconstruct.d.ts.map +1 -0
  30. package/dist/reconstruct.js +140 -0
  31. package/dist/reconstruct.js.map +1 -0
  32. package/dist/record.d.ts +48 -0
  33. package/dist/record.d.ts.map +1 -0
  34. package/dist/record.js +91 -0
  35. package/dist/record.js.map +1 -0
  36. package/dist/resolve.d.ts +54 -0
  37. package/dist/resolve.d.ts.map +1 -0
  38. package/dist/resolve.js +80 -0
  39. package/dist/resolve.js.map +1 -0
  40. package/dist/solc.d.ts +38 -0
  41. package/dist/solc.d.ts.map +1 -0
  42. package/dist/solc.js +194 -0
  43. package/dist/solc.js.map +1 -0
  44. package/package.json +70 -4
  45. package/src/bytecode.ts +141 -0
  46. package/src/chain.ts +75 -0
  47. package/src/diff.ts +31 -0
  48. package/src/errors.ts +24 -0
  49. package/src/explorer.ts +190 -0
  50. package/src/index.ts +36 -0
  51. package/src/reconstruct.ts +228 -0
  52. package/src/record.ts +127 -0
  53. package/src/resolve.ts +138 -0
  54. package/src/solc.ts +268 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Proven layouts of deployed code, as `LayoutRecord`s. Nothing here writes to
3
+ * disk; see `record.ts` for saving and loading records.
4
+ *
5
+ * Under lazy or queued (multisig) upgrades, a project's files describe its
6
+ * latest build while proxies keep running older implementations. Reading the
7
+ * implementation from the chain and proving its source against the deployed
8
+ * code gives the layout of what actually runs.
9
+ */
10
+ import { compareDeployedBytecode, isProvingMatch, } from "./bytecode.js";
11
+ import { codeSha256, readChainId, readCode, readProxy } from "./chain.js";
12
+ import { LayoutUnavailableError } from "./errors.js";
13
+ import { canQueryExplorer, fetchVerifiedSource } from "./explorer.js";
14
+ import { reconstructLayout } from "./reconstruct.js";
15
+ import { parseRecord } from "./record.js";
16
+ import { getSolc } from "./solc.js";
17
+ /**
18
+ * Rebuilds the layout of the code at `address` from its verified source and
19
+ * proves that source is the deployed code. Always fetches and compiles; check
20
+ * your own saved record first (`verifyRecord`) to skip that.
21
+ */
22
+ export async function resolveLayout(provider, address, options) {
23
+ const lower = address.toLowerCase();
24
+ if (!canQueryExplorer(options.explorer)) {
25
+ throw new LayoutUnavailableError(`Cannot rebuild the layout of ${lower}: the explorer needs an apiKey or an apiUrl.`);
26
+ }
27
+ const code = await readCode(provider, lower);
28
+ const chainId = await readChainId(provider);
29
+ const source = await fetchVerifiedSource(chainId, lower, options.explorer);
30
+ const solc = await getSolc(source.solcLongVersion, options.solc);
31
+ const rebuilt = await reconstructLayout(source, solc, code);
32
+ return {
33
+ format: 1,
34
+ address: lower,
35
+ chainId,
36
+ codeSha256: codeSha256(code),
37
+ contract: rebuilt.contract,
38
+ compiler: rebuilt.compiler,
39
+ bytecodeMatch: rebuilt.bytecodeMatch,
40
+ source: "explorer",
41
+ recordedAt: new Date().toISOString(),
42
+ layout: rebuilt.layout,
43
+ };
44
+ }
45
+ /** `resolveLayout` for whatever implementation `proxy` runs right now (ERC-1967 or beacon). */
46
+ export async function resolveProxyLayout(provider, proxy, options) {
47
+ const state = await readProxy(provider, proxy);
48
+ if (state === undefined) {
49
+ throw new LayoutUnavailableError(`${proxy} has no ERC-1967 implementation or beacon slot set on this chain.`);
50
+ }
51
+ const record = await resolveLayout(provider, state.implementation, options);
52
+ return { ...state, record };
53
+ }
54
+ /**
55
+ * Makes a record from a local build of the code at `address`, but only after
56
+ * proving the chain runs that build. A `metadata-only` or `none` match gives
57
+ * no record, so a layout is never attributed to code that is not there.
58
+ */
59
+ export async function proveLocalBuild(provider, address, build) {
60
+ const lower = address.toLowerCase();
61
+ const code = await readCode(provider, lower);
62
+ const bytecodeMatch = compareDeployedBytecode(code, build.deployedBytecode, build.immutableReferences);
63
+ if (!isProvingMatch(bytecodeMatch))
64
+ return { bytecodeMatch };
65
+ const record = {
66
+ format: 1,
67
+ address: lower,
68
+ chainId: await readChainId(provider),
69
+ codeSha256: codeSha256(code),
70
+ contract: build.contract,
71
+ ...(build.compiler !== undefined ? { compiler: build.compiler } : {}),
72
+ bytecodeMatch,
73
+ source: "local-compile",
74
+ recordedAt: new Date().toISOString(),
75
+ layout: build.layout,
76
+ };
77
+ // A record this returns must load back through parseRecord, so check it now.
78
+ return { bytecodeMatch, record: parseRecord(JSON.parse(JSON.stringify(record)), lower) };
79
+ }
80
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"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"}
package/dist/solc.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Native solc acquisition and invocation, independent of Hardhat's compiler
3
+ * manager so the core stays usable outside a Hardhat runtime.
4
+ *
5
+ * Lookup order: explicit override, our cache, Hardhat's compiler caches (v3
6
+ * then v2, so a project that already compiled with that version pays nothing),
7
+ * then a download from binaries.soliditylang.org verified against the
8
+ * published sha256.
9
+ *
10
+ * Takes a long version (`0.8.24+commit.e11b9ed9`) or a release version
11
+ * (`0.8.24`, which is what Foundry's build-info records); a release version
12
+ * resolves to that release's build, never a nightly.
13
+ */
14
+ import type { SolcInput, SolcOutput } from "@openzeppelin/upgrades-core";
15
+ export interface SolcRunner {
16
+ /**
17
+ * Resolved, e.g. `0.8.24+commit.e11b9ed9` even when asked for `0.8.24`. With
18
+ * `resolvePath`, the version as passed.
19
+ */
20
+ longVersion: string;
21
+ path: string;
22
+ compile(input: SolcInput): Promise<SolcOutput>;
23
+ }
24
+ export interface SolcOptions {
25
+ /** Where downloaded compilers are kept. Defaults to `<os cache>/slotdiff/compilers`. */
26
+ cacheDir?: string;
27
+ /** Explicit binary for a version (as passed to `getSolc`), e.g. on platforms with no official native build. */
28
+ resolvePath?: (version: string) => string | undefined;
29
+ }
30
+ type FetchBinary = (url: string) => Promise<{
31
+ ok: boolean;
32
+ status: number;
33
+ json(): Promise<unknown>;
34
+ arrayBuffer(): Promise<ArrayBuffer>;
35
+ }>;
36
+ export declare function getSolc(version: string, options?: SolcOptions, fetchImpl?: FetchBinary): Promise<SolcRunner>;
37
+ export {};
38
+ //# sourceMappingURL=solc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solc.d.ts","sourceRoot":"","sources":["../src/solc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAWzE,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,WAAW;IAC1B,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+GAA+G;IAC/G,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CACvD;AAED,KAAK,WAAW,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC1C,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACzB,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;CACrC,CAAC,CAAC;AAEH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,WAAgB,EACzB,SAAS,GAAE,WAAmB,GAC7B,OAAO,CAAC,UAAU,CAAC,CAGrB"}
package/dist/solc.js ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Native solc acquisition and invocation, independent of Hardhat's compiler
3
+ * manager so the core stays usable outside a Hardhat runtime.
4
+ *
5
+ * Lookup order: explicit override, our cache, Hardhat's compiler caches (v3
6
+ * then v2, so a project that already compiled with that version pays nothing),
7
+ * then a download from binaries.soliditylang.org verified against the
8
+ * published sha256.
9
+ *
10
+ * Takes a long version (`0.8.24+commit.e11b9ed9`) or a release version
11
+ * (`0.8.24`, which is what Foundry's build-info records); a release version
12
+ * resolves to that release's build, never a nightly.
13
+ */
14
+ import { spawn } from "node:child_process";
15
+ import { createHash, randomUUID } from "node:crypto";
16
+ import { access, chmod, mkdir, readdir, rename, writeFile } from "node:fs/promises";
17
+ import { constants } from "node:fs";
18
+ import { homedir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { LayoutIntegrityError, LayoutUnavailableError } from "./errors.js";
21
+ const BINARIES_URL = "https://binaries.soliditylang.org";
22
+ const RELEASE_VERSION = /^\d+\.\d+\.\d+$/;
23
+ // Validated before use: the version becomes part of a path that gets executed.
24
+ const SOLC_VERSION = /^\d+\.\d+\.\d+(?:-nightly\.\d+\.\d+\.\d+|-pre\.\d+)?(?:\+commit\.[0-9a-f]+)?$/;
25
+ const RELEASE_LONG_VERSION = /^\d+\.\d+\.\d+\+commit\.[0-9a-f]+$/;
26
+ export async function getSolc(version, options = {}, fetchImpl = fetch) {
27
+ const { path, longVersion } = await locateSolc(version, options, fetchImpl);
28
+ return { longVersion, path, compile: (input) => runSolc(path, input) };
29
+ }
30
+ const downloads = new Map();
31
+ async function locateSolc(version, options, fetchImpl) {
32
+ if (!SOLC_VERSION.test(version)) {
33
+ throw new LayoutUnavailableError(`"${version}" is not a solc version.`);
34
+ }
35
+ const explicit = options.resolvePath?.(version);
36
+ if (explicit !== undefined)
37
+ return { path: explicit, longVersion: version };
38
+ const platform = nativePlatform();
39
+ const cacheDir = options.cacheDir ?? join(osCacheDir(), "slotdiff", "compilers");
40
+ // Hardhat's cache follows env-paths, which adds a `Cache` folder on Windows.
41
+ const hardhatCache = process.platform === "win32"
42
+ ? join(osCacheDir(), "hardhat-nodejs", "Cache")
43
+ : join(osCacheDir(), "hardhat-nodejs");
44
+ const dirs = [
45
+ join(cacheDir, platform),
46
+ join(hardhatCache, "compilers-v3", platform),
47
+ join(hardhatCache, "compilers-v2", platform),
48
+ ];
49
+ for (const dir of dirs) {
50
+ const cached = await findCached(dir, platform, version);
51
+ if (cached !== undefined)
52
+ return cached;
53
+ }
54
+ const build = await findBuild(platform, version, fetchImpl);
55
+ const dest = join(cacheDir, platform, binaryName(platform, build.longVersion));
56
+ // Concurrent callers (one per proxy, say) share one download per binary.
57
+ let pending = downloads.get(dest);
58
+ if (pending === undefined) {
59
+ pending = download(platform, build, dest, fetchImpl).finally(() => downloads.delete(dest));
60
+ downloads.set(dest, pending);
61
+ }
62
+ await pending;
63
+ return { path: dest, longVersion: build.longVersion };
64
+ }
65
+ function binaryName(platform, longVersion) {
66
+ return `solc-${platform}-v${longVersion}${platform === "windows-amd64" ? ".exe" : ""}`;
67
+ }
68
+ async function findCached(dir, platform, version) {
69
+ if (!RELEASE_VERSION.test(version)) {
70
+ const path = join(dir, binaryName(platform, version));
71
+ return (await isExecutable(path)) ? { path, longVersion: version } : undefined;
72
+ }
73
+ let names;
74
+ try {
75
+ names = await readdir(dir);
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ // Only finished release binaries of this version: no nightlies (`0.8.24-nightly...`),
81
+ // no longer versions (`0.8.240`), no in-progress `.tmp` downloads.
82
+ const prefix = `solc-${platform}-v`;
83
+ const ext = platform === "windows-amd64" ? ".exe" : "";
84
+ for (const name of names.sort()) {
85
+ if (!name.startsWith(prefix) || !name.endsWith(ext))
86
+ continue;
87
+ const longVersion = name.slice(prefix.length, name.length - ext.length);
88
+ if (!RELEASE_LONG_VERSION.test(longVersion) || !longVersion.startsWith(`${version}+`))
89
+ continue;
90
+ const path = join(dir, name);
91
+ if (await isExecutable(path))
92
+ return { path, longVersion };
93
+ }
94
+ return undefined;
95
+ }
96
+ async function findBuild(platform, version, fetchImpl) {
97
+ // Failing to obtain a compiler means the layout cannot be rebuilt, not
98
+ // that it is wrong; only a checksum mismatch is an integrity problem.
99
+ const listRes = await fetchOrUnavailable(fetchImpl, `${BINARIES_URL}/${platform}/list.json`);
100
+ if (!listRes.ok) {
101
+ throw new LayoutUnavailableError(`Could not fetch the solc build list (HTTP ${listRes.status}).`);
102
+ }
103
+ const list = (await listRes.json());
104
+ const build = RELEASE_VERSION.test(version)
105
+ ? // A nightly reads `0.8.24-nightly...`, so only the release starts with `0.8.24+`.
106
+ list.builds.find((b) => b.longVersion.startsWith(`${version}+`))
107
+ : list.builds.find((b) => b.longVersion === version);
108
+ if (build === undefined) {
109
+ throw new LayoutUnavailableError(`solc ${version} has no native ${platform} build.`);
110
+ }
111
+ // Old Windows releases (up to 0.7.1) ship as zip archives, not executables.
112
+ if (build.path.endsWith(".zip")) {
113
+ throw new LayoutUnavailableError(`solc ${build.longVersion} for ${platform} ships as a zip archive; pass a compiler via solc.resolvePath.`);
114
+ }
115
+ return build;
116
+ }
117
+ async function download(platform, build, dest, fetchImpl) {
118
+ const binRes = await fetchOrUnavailable(fetchImpl, `${BINARIES_URL}/${platform}/${build.path}`);
119
+ if (!binRes.ok) {
120
+ throw new LayoutUnavailableError(`Could not download solc ${build.longVersion} (HTTP ${binRes.status}).`);
121
+ }
122
+ const bytes = Buffer.from(await binRes.arrayBuffer());
123
+ const actual = createHash("sha256").update(bytes).digest("hex");
124
+ if (`0x${actual}` !== build.sha256.toLowerCase()) {
125
+ throw new LayoutIntegrityError(`solc ${build.longVersion} download failed its sha256 check; refusing to run it.`);
126
+ }
127
+ await mkdir(join(dest, ".."), { recursive: true });
128
+ const tmp = `${dest}.${randomUUID()}.tmp`;
129
+ await writeFile(tmp, bytes);
130
+ await chmod(tmp, 0o755);
131
+ await rename(tmp, dest);
132
+ }
133
+ function runSolc(path, input) {
134
+ return new Promise((resolve, reject) => {
135
+ const child = spawn(path, ["--standard-json"], { stdio: ["pipe", "pipe", "pipe"] });
136
+ const out = [];
137
+ const err = [];
138
+ child.stdout.on("data", (d) => out.push(d));
139
+ child.stderr.on("data", (d) => err.push(d));
140
+ child.on("error", reject);
141
+ // A compiler that exits before reading all input closes the pipe; reject
142
+ // instead of crashing the process with an unhandled EPIPE.
143
+ child.stdin.on("error", reject);
144
+ child.on("close", (code) => {
145
+ if (code !== 0) {
146
+ reject(new Error(`solc exited with code ${code}: ${Buffer.concat(err).toString()}`));
147
+ return;
148
+ }
149
+ try {
150
+ resolve(JSON.parse(Buffer.concat(out).toString()));
151
+ }
152
+ catch (e) {
153
+ reject(e);
154
+ }
155
+ });
156
+ child.stdin.end(JSON.stringify(input));
157
+ });
158
+ }
159
+ function nativePlatform() {
160
+ const { platform, arch } = process;
161
+ if (platform === "darwin")
162
+ return "macosx-amd64"; // universal binaries for recent versions
163
+ if (platform === "linux" && arch === "x64")
164
+ return "linux-amd64";
165
+ if (platform === "win32" && arch === "x64")
166
+ return "windows-amd64";
167
+ throw new LayoutUnavailableError(`No official native solc build for ${platform}/${arch}; pass a compiler path via solc.resolvePath.`);
168
+ }
169
+ async function fetchOrUnavailable(fetchImpl, url) {
170
+ try {
171
+ return await fetchImpl(url);
172
+ }
173
+ catch (e) {
174
+ throw new LayoutUnavailableError(`Could not reach ${BINARIES_URL}: ${e.message}`);
175
+ }
176
+ }
177
+ function osCacheDir() {
178
+ if (process.platform === "darwin")
179
+ return join(homedir(), "Library", "Caches");
180
+ if (process.platform === "win32") {
181
+ return process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
182
+ }
183
+ return process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
184
+ }
185
+ async function isExecutable(path) {
186
+ try {
187
+ await access(path, constants.X_OK);
188
+ return true;
189
+ }
190
+ catch {
191
+ return false;
192
+ }
193
+ }
194
+ //# sourceMappingURL=solc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solc.js","sourceRoot":"","sources":["../src/solc.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "slotdiff",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Storage layout diff for upgradeable Solidity contracts: compare a local build or a live deployment against another",
5
+ "license": "MIT",
6
+ "author": "Rohit Solia (https://github.com/rokso)",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/rokso/slotdiff.git"
10
+ },
11
+ "homepage": "https://github.com/rokso/slotdiff#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/rokso/slotdiff/issues"
14
+ },
15
+ "type": "module",
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "src",
29
+ "CHANGELOG.md"
30
+ ],
31
+ "scripts": {
32
+ "build": "rm -rf dist && tsc --project tsconfig.build.json",
33
+ "build:watch": "tsc --project tsconfig.build.json --watch",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest",
36
+ "typecheck": "tsc",
37
+ "lint": "eslint . && prettier --check .",
38
+ "format": "prettier --write .",
39
+ "prepublishOnly": "pnpm lint && pnpm test",
40
+ "clean": "rm -rf dist",
41
+ "prepack": "pnpm build"
42
+ },
43
+ "packageManager": "pnpm@9.0.0",
44
+ "dependencies": {
45
+ "@openzeppelin/upgrades-core": "^1.46.0"
46
+ },
47
+ "devDependencies": {
48
+ "@eslint/js": "^10.0.1",
49
+ "@types/node": "^22.0.0",
50
+ "eslint": "^10.2.0",
51
+ "eslint-config-prettier": "^10.1.8",
52
+ "globals": "^17.5.0",
53
+ "prettier": "^3.8.2",
54
+ "typescript": "^5.7.0",
55
+ "typescript-eslint": "^8.58.2",
56
+ "vitest": "^4.1.0"
57
+ },
58
+ "engines": {
59
+ "node": ">=22.12.0"
60
+ },
61
+ "keywords": [
62
+ "ethereum",
63
+ "solidity",
64
+ "upgradeable",
65
+ "proxy",
66
+ "storage-layout",
67
+ "upgrade-safety",
68
+ "foundry",
69
+ "hardhat"
70
+ ],
71
+ "sideEffects": false
72
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Bytecode comparison utilities.
3
+ *
4
+ * Solidity appends a CBOR-encoded metadata hash to the end of every compiled
5
+ * contract's bytecode. The last two bytes encode the length of that metadata
6
+ * section. Stripping it allows comparing contracts that are logically identical
7
+ * but were compiled in different environments (different metadata).
8
+ */
9
+
10
+ export function stripBytecodeMetadata(bytecode: string): string {
11
+ const buf = toBuffer(bytecode);
12
+ const start = cborStart(buf);
13
+ if (start === undefined) return bytecode;
14
+ return "0x" + buf.subarray(0, start).toString("hex");
15
+ }
16
+
17
+ function toBuffer(bytecode: string): Buffer {
18
+ return Buffer.from(bytecode.startsWith("0x") ? bytecode.slice(2) : bytecode, "hex");
19
+ }
20
+
21
+ // Where solc's CBOR metadata starts, or undefined when the tail is not CBOR.
22
+ function cborStart(buf: Buffer): number | undefined {
23
+ if (buf.length < 2) return undefined;
24
+ const metadataLength = buf.readUInt16BE(buf.length - 2);
25
+ if (metadataLength === 0 || metadataLength + 2 > buf.length) return undefined;
26
+ // solc's metadata is a CBOR map (major type 5). Without that marker the
27
+ // tail is code (e.g. compiled with appendCBOR: false), and stripping it
28
+ // would hide real differences.
29
+ const start = buf.length - 2 - metadataLength;
30
+ return buf[start] >> 5 === 5 ? start : undefined;
31
+ }
32
+
33
+ // The CBOR keys solc uses for the metadata hash, each with its byte-string header.
34
+ const SOURCE_HASH_KEYS = [
35
+ Buffer.from("6469706673" + "5822", "hex"), // "ipfs", 34 bytes
36
+ Buffer.from("65627a7a7230" + "5820", "hex"), // "bzzr0", 32 bytes
37
+ Buffer.from("65627a7a7231" + "5820", "hex"), // "bzzr1", 32 bytes
38
+ ];
39
+
40
+ /**
41
+ * Whether compiled code embeds the hash of its metadata, which covers every
42
+ * source file and the settings. Only then does matching code also mean
43
+ * matching source. `bytecodeHash: "none"` and `appendCBOR: false` drop it.
44
+ */
45
+ function hasSourceHash(bytecode: string): boolean {
46
+ const buf = toBuffer(bytecode);
47
+ const start = cborStart(buf);
48
+ if (start === undefined) return false;
49
+ const cbor = buf.subarray(start, buf.length - 2);
50
+ return SOURCE_HASH_KEYS.some((key) => cbor.includes(key));
51
+ }
52
+
53
+ function normalize(s: string): string {
54
+ return s.toLowerCase().replace(/^0x/, "");
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Compiled-vs-on-chain comparison
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * solc's `evm.deployedBytecode.immutableReferences`: AST id to the byte spans
63
+ * (relative to the deployed bytecode) that the constructor fills in.
64
+ */
65
+ export type ImmutableReferences = Record<string, ReadonlyArray<{ start: number; length: number }>>;
66
+
67
+ /**
68
+ * - `exact`: byte-identical.
69
+ * - `immutables-only`: identical once immutable (and library-link) spans are
70
+ * masked. Same code, deployment-specific values. Every UUPS implementation
71
+ * lands here because `UUPSUpgradeable.__self` is immutable.
72
+ * - `metadata-only`: identical after masking and stripping the CBOR metadata,
73
+ * so the compiler input was not byte-identical (source hashes or settings).
74
+ * NOT proof of storage layout: variables no code reads, gap sizes and field
75
+ * names do not reach the bytecode, so different layouts can share code.
76
+ * - `code-only`: identical (exactly or once masked), but the build embeds no
77
+ * source hash (`bytecodeHash: "none"` or `appendCBOR: false`), so nothing
78
+ * ties the code to its source. NOT proof of layout, for the same reason.
79
+ * - `none`: different code.
80
+ *
81
+ * `exact` and `immutables-only` therefore always mean the source is proven too.
82
+ */
83
+ export type DeployedBytecodeMatch =
84
+ "exact" | "immutables-only" | "metadata-only" | "code-only" | "none";
85
+
86
+ /** The matches that prove a source is the deployed code, and so prove its layout. */
87
+ export type ProvingMatch = "exact" | "immutables-only";
88
+
89
+ export function isProvingMatch(match: DeployedBytecodeMatch): match is ProvingMatch {
90
+ return match === "exact" || match === "immutables-only";
91
+ }
92
+
93
+ // solc leaves `__$<34 hex>$__` (20 bytes) where an external library address is linked.
94
+ const LINK_PLACEHOLDER = /__\$[0-9a-fA-F]{34}\$__/g;
95
+
96
+ /**
97
+ * Compares runtime code read with `eth_getCode` against compiler output.
98
+ *
99
+ * On-chain code has immutables filled in while compiler output has them
100
+ * zeroed, and the difference sits in the code body where metadata stripping
101
+ * cannot reach, so the spans are zeroed on both sides before comparing.
102
+ */
103
+ export function compareDeployedBytecode(
104
+ onchain: string,
105
+ compiled: string,
106
+ immutableReferences: ImmutableReferences = {},
107
+ ): DeployedBytecodeMatch {
108
+ const live = normalize(onchain);
109
+ const built = normalize(compiled);
110
+ if (live === "" || built === "") return "none";
111
+ if (live === built) return hasSourceHash(built) ? "exact" : "code-only";
112
+
113
+ const spans: Array<{ start: number; length: number }> = Object.values(immutableReferences).flat();
114
+ for (const m of built.matchAll(LINK_PLACEHOLDER)) {
115
+ spans.push({ start: m.index / 2, length: 20 });
116
+ }
117
+ const builtHex = built.replace(LINK_PLACEHOLDER, "0".repeat(40));
118
+
119
+ const liveMasked = maskSpans(live, spans);
120
+ const builtMasked = maskSpans(builtHex, spans);
121
+ if (liveMasked === undefined || builtMasked === undefined) return "none";
122
+ if (liveMasked === builtMasked) return hasSourceHash(builtHex) ? "immutables-only" : "code-only";
123
+ if (stripBytecodeMetadata(liveMasked) === stripBytecodeMetadata(builtMasked)) {
124
+ return "metadata-only";
125
+ }
126
+ return "none";
127
+ }
128
+
129
+ // Returns undefined when the hex is malformed or a span falls outside the code.
130
+ function maskSpans(
131
+ hex: string,
132
+ spans: ReadonlyArray<{ start: number; length: number }>,
133
+ ): string | undefined {
134
+ const buf = Buffer.from(hex, "hex");
135
+ if (buf.length * 2 !== hex.length) return undefined;
136
+ for (const { start, length } of spans) {
137
+ if (start + length > buf.length) return undefined;
138
+ buf.fill(0, start, start + length);
139
+ }
140
+ return buf.toString("hex");
141
+ }
package/src/chain.ts ADDED
@@ -0,0 +1,75 @@
1
+ import {
2
+ EIP1967BeaconNotFound,
3
+ EIP1967ImplementationNotFound,
4
+ getBeaconAddress,
5
+ getImplementationAddress,
6
+ getImplementationAddressFromBeacon,
7
+ type EthereumProvider,
8
+ } from "@openzeppelin/upgrades-core";
9
+ import { createHash } from "node:crypto";
10
+ import { LayoutUnavailableError } from "./errors.js";
11
+
12
+ /** The one JSON-RPC method used; Hardhat's network provider and ethers' JsonRpcProvider both fit. */
13
+ export interface EthProvider {
14
+ send(method: string, params: unknown[]): Promise<unknown>;
15
+ }
16
+
17
+ export interface ProxyState {
18
+ implementation: string;
19
+ /** Set for a beacon proxy; `implementation` is then the beacon's. */
20
+ beacon?: string;
21
+ }
22
+
23
+ /**
24
+ * Reads what a proxy runs right now: the ERC-1967 implementation slot, or the
25
+ * beacon's implementation for a beacon proxy. Undefined when neither slot is
26
+ * set, i.e. the address is not a proxy this tool understands.
27
+ */
28
+ export async function readProxy(
29
+ provider: EthProvider,
30
+ address: string,
31
+ ): Promise<ProxyState | undefined> {
32
+ const oz = provider as unknown as EthereumProvider;
33
+ try {
34
+ return { implementation: (await getImplementationAddress(oz, address)).toLowerCase() };
35
+ } catch (e) {
36
+ if (!(e instanceof EIP1967ImplementationNotFound)) throw e;
37
+ }
38
+ let beacon: string;
39
+ try {
40
+ beacon = (await getBeaconAddress(oz, address)).toLowerCase();
41
+ } catch (e) {
42
+ if (e instanceof EIP1967BeaconNotFound) return undefined;
43
+ throw e;
44
+ }
45
+ const implementation = (await getImplementationAddressFromBeacon(oz, beacon)).toLowerCase();
46
+ return { implementation, beacon };
47
+ }
48
+
49
+ export async function readImplementation(provider: EthProvider, proxy: string): Promise<string> {
50
+ const state = await readProxy(provider, proxy);
51
+ if (state === undefined) {
52
+ throw new LayoutUnavailableError(
53
+ `${proxy} has no ERC-1967 implementation or beacon slot set on this chain.`,
54
+ );
55
+ }
56
+ return state.implementation;
57
+ }
58
+
59
+ export async function readCode(provider: EthProvider, address: string): Promise<string> {
60
+ const code = (await provider.send("eth_getCode", [address, "latest"])) as string;
61
+ if (code === "0x" || code === "") {
62
+ throw new LayoutUnavailableError(`No code at ${address} on this chain.`);
63
+ }
64
+ return code.toLowerCase();
65
+ }
66
+
67
+ export async function readChainId(provider: EthProvider): Promise<number> {
68
+ return Number(BigInt((await provider.send("eth_chainId", [])) as string));
69
+ }
70
+
71
+ export function codeSha256(code: string): string {
72
+ return createHash("sha256")
73
+ .update(Buffer.from(code.replace(/^0x/, ""), "hex"))
74
+ .digest("hex");
75
+ }
package/src/diff.ts ADDED
@@ -0,0 +1,31 @@
1
+ import {
2
+ getStorageUpgradeReport,
3
+ withValidationDefaults,
4
+ type StorageLayout,
5
+ } from "@openzeppelin/upgrades-core";
6
+
7
+ /** OZ's storage comparison. `ok` is the verdict; `explain()` describes each problem. */
8
+ export type LayoutReport = ReturnType<typeof getStorageUpgradeReport>;
9
+
10
+ export interface DiffOptions {
11
+ /** Accept renamed variables without `@custom:oz-renamed-from` (OZ's `unsafeAllowRenames`). */
12
+ unsafeAllowRenames?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Whether `after` can safely replace `before` behind a proxy, by OZ's rules:
17
+ * appends pass; inserts, removals, reorders and incompatible type changes
18
+ * fail, in regular and ERC-7201 namespaced storage. Renames and retypes pass
19
+ * when `after` approves them (`@custom:oz-renamed-from` / `oz-retyped-from`).
20
+ */
21
+ export function diffLayouts(
22
+ before: StorageLayout,
23
+ after: StorageLayout,
24
+ options: DiffOptions = {},
25
+ ): LayoutReport {
26
+ return getStorageUpgradeReport(
27
+ before,
28
+ after,
29
+ withValidationDefaults({ unsafeAllowRenames: options.unsafeAllowRenames }),
30
+ );
31
+ }