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,131 @@
1
+ /**
2
+ * Verified-source lookup against an Etherscan-compatible explorer API.
3
+ *
4
+ * Only standard-json verifications are accepted. Flattened and multi-file
5
+ * verifications do not record every compiler setting (`viaIR` among them), so
6
+ * rebuilding from them means guessing; the bytecode proof would reject a wrong
7
+ * guess, but refusing up front gives a clearer error.
8
+ */
9
+ import { LayoutUnavailableError } from "./errors.js";
10
+ export const ETHERSCAN_V2_API_URL = "https://api.etherscan.io/v2/api";
11
+ const RATE_LIMIT = /rate limit/i;
12
+ // Key problems are the user's to fix and must surface; anything else the
13
+ // explorer refuses (unsupported chain, outage) only means it cannot answer.
14
+ const KEY_ERROR = /api.?key/i;
15
+ const MAX_ATTEMPTS = 4;
16
+ /**
17
+ * Etherscan needs a key; a custom Etherscan-compatible endpoint (e.g. a
18
+ * Blockscout instance) may not.
19
+ */
20
+ export function canQueryExplorer(config) {
21
+ return Boolean(config.apiKey) || Boolean(config.apiUrl);
22
+ }
23
+ export async function fetchVerifiedSource(chainId, address, config, fetchImpl = fetch) {
24
+ if (!canQueryExplorer(config)) {
25
+ throw new LayoutUnavailableError(`No explorer API key configured for chain ${chainId}; cannot fetch verified source for ${address}.`);
26
+ }
27
+ const url = new URL(config.apiUrl ?? ETHERSCAN_V2_API_URL);
28
+ url.searchParams.set("chainid", String(chainId));
29
+ url.searchParams.set("module", "contract");
30
+ url.searchParams.set("action", "getsourcecode");
31
+ url.searchParams.set("address", address);
32
+ if (config.apiKey)
33
+ url.searchParams.set("apikey", config.apiKey);
34
+ const entry = await requestWithRetry(url.toString(), fetchImpl);
35
+ return parseSourceCodeEntry(entry, address);
36
+ }
37
+ async function requestWithRetry(url, fetchImpl) {
38
+ for (let attempt = 1;; attempt++) {
39
+ const retry = attempt < MAX_ATTEMPTS;
40
+ let res;
41
+ try {
42
+ res = await fetchImpl(url);
43
+ }
44
+ catch (e) {
45
+ // Messages never include the URL, which carries the API key.
46
+ throw new LayoutUnavailableError(`Explorer unreachable: ${e.message}`);
47
+ }
48
+ if (res.status === 429 && retry) {
49
+ await backoff(attempt);
50
+ continue;
51
+ }
52
+ if (!res.ok) {
53
+ throw new LayoutUnavailableError(`Explorer request failed with HTTP ${res.status}.`);
54
+ }
55
+ let body;
56
+ try {
57
+ body = (await res.json());
58
+ }
59
+ catch {
60
+ throw new LayoutUnavailableError("Explorer returned a response that is not JSON.");
61
+ }
62
+ if (typeof body !== "object" || body === null) {
63
+ throw new LayoutUnavailableError("Explorer returned an unexpected response.");
64
+ }
65
+ if (body.status === "1" && Array.isArray(body.result) && body.result.length > 0) {
66
+ const entry = body.result[0];
67
+ if (typeof entry !== "object" || entry === null) {
68
+ throw new LayoutUnavailableError("Explorer returned an unexpected source entry.");
69
+ }
70
+ return entry;
71
+ }
72
+ const detail = typeof body.result === "string" ? body.result : (body.message ?? "unknown error");
73
+ // Before the key check: Etherscan's rate-limit message mentions the API key.
74
+ if (RATE_LIMIT.test(detail)) {
75
+ if (retry) {
76
+ await backoff(attempt);
77
+ continue;
78
+ }
79
+ throw new LayoutUnavailableError(`Explorer kept rate-limiting the request: ${detail}`);
80
+ }
81
+ if (KEY_ERROR.test(detail))
82
+ throw new Error(`Explorer rejected the API key: ${detail}`);
83
+ throw new LayoutUnavailableError(`Explorer could not answer: ${detail}`);
84
+ }
85
+ }
86
+ function backoff(attempt) {
87
+ return new Promise((r) => setTimeout(r, 1000 * attempt));
88
+ }
89
+ /** Exported for tests. */
90
+ export function parseSourceCodeEntry(entry, address) {
91
+ const raw = entry.SourceCode ?? "";
92
+ if (raw === "") {
93
+ throw new LayoutUnavailableError(`Contract ${address} is not verified on the explorer.`);
94
+ }
95
+ const compilerVersion = entry.CompilerVersion ?? "";
96
+ if (!compilerVersion.startsWith("v0.")) {
97
+ throw new LayoutUnavailableError(`Contract ${address} was verified with "${compilerVersion}"; only Solidity is supported.`);
98
+ }
99
+ if (!/^v\d+\.\d+\.\d+(?:-nightly\.\d+\.\d+\.\d+|-pre\.\d+)?\+commit\.[0-9a-f]+$/.test(compilerVersion)) {
100
+ throw new LayoutUnavailableError(`Contract ${address} reports an unrecognized compiler version "${compilerVersion}".`);
101
+ }
102
+ const input = parseStandardJson(raw);
103
+ if (input === undefined) {
104
+ throw new LayoutUnavailableError(`Contract ${address} is verified as flattened or multi-file source, not standard-json. ` +
105
+ `Its compiler settings cannot be reconstructed exactly.`);
106
+ }
107
+ return {
108
+ input,
109
+ solcLongVersion: compilerVersion.slice(1),
110
+ contractName: entry.ContractName ?? "",
111
+ };
112
+ }
113
+ // Etherscan wraps standard-json in an extra pair of braces (`{{...}}`); some
114
+ // compatible explorers return it unwrapped.
115
+ function parseStandardJson(raw) {
116
+ const text = raw.startsWith("{{") && raw.endsWith("}}") ? raw.slice(1, -1) : raw;
117
+ if (!text.startsWith("{"))
118
+ return undefined;
119
+ let parsed;
120
+ try {
121
+ parsed = JSON.parse(text);
122
+ }
123
+ catch {
124
+ return undefined;
125
+ }
126
+ const candidate = parsed;
127
+ if (candidate.language !== "Solidity" || typeof candidate.sources !== "object")
128
+ return undefined;
129
+ return candidate;
130
+ }
131
+ //# sourceMappingURL=explorer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explorer.js","sourceRoot":"","sources":["../src/explorer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export { diffLayouts, type DiffOptions, type LayoutReport } from "./diff.js";
2
+ export { resolveLayout, resolveProxyLayout, proveLocalBuild, type ResolveOptions, type ProxyLayout, type LocalBuild, type LocalBuildResult, } from "./resolve.js";
3
+ export { parseRecord, verifyRecord, type LayoutRecord } from "./record.js";
4
+ export { readProxy, readImplementation, readCode, readChainId, type EthProvider, type ProxyState, } from "./chain.js";
5
+ export { fetchVerifiedSource, ETHERSCAN_V2_API_URL, type ExplorerConfig, type VerifiedSource, } from "./explorer.js";
6
+ export { getSolc, type SolcOptions, type SolcRunner } from "./solc.js";
7
+ export { layoutFromSource } from "./reconstruct.js";
8
+ export { compareDeployedBytecode, isProvingMatch, type DeployedBytecodeMatch, type ProvingMatch, type ImmutableReferences, } from "./bytecode.js";
9
+ export { LayoutIntegrityError, LayoutUnavailableError } from "./errors.js";
10
+ export type { SolcInput, StorageLayout } from "@openzeppelin/upgrades-core";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,WAAW,CAAC;AAC7E,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,eAAe,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,QAAQ,EACR,WAAW,EACX,KAAK,WAAW,EAChB,KAAK,UAAU,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,mBAAmB,GACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC3E,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { diffLayouts } from "./diff.js";
2
+ export { resolveLayout, resolveProxyLayout, proveLocalBuild, } from "./resolve.js";
3
+ export { parseRecord, verifyRecord } from "./record.js";
4
+ export { readProxy, readImplementation, readCode, readChainId, } from "./chain.js";
5
+ export { fetchVerifiedSource, ETHERSCAN_V2_API_URL, } from "./explorer.js";
6
+ export { getSolc } from "./solc.js";
7
+ export { layoutFromSource } from "./reconstruct.js";
8
+ export { compareDeployedBytecode, isProvingMatch, } from "./bytecode.js";
9
+ export { LayoutIntegrityError, LayoutUnavailableError } from "./errors.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAuC,MAAM,WAAW,CAAC;AAC7E,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,eAAe,GAKhB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAqB,MAAM,aAAa,CAAC;AAC3E,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,QAAQ,EACR,WAAW,GAGZ,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GAGrB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAqC,MAAM,WAAW,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EACL,uBAAuB,EACvB,cAAc,GAIf,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Rebuilds the storage layout of deployed code from its verified source, and
3
+ * proves the source is the deployed code before trusting the layout.
4
+ *
5
+ * The layout comes out of the same oz-core pipeline the compile hook runs for
6
+ * the local build (full compile, namespaced second pass, `validate()`,
7
+ * `getStorageLayout`), so both sides of a comparison are extracted identically.
8
+ */
9
+ import { type SolcInput, type StorageLayout } from "@openzeppelin/upgrades-core";
10
+ import { type ProvingMatch } from "./bytecode.js";
11
+ import type { VerifiedSource } from "./explorer.js";
12
+ import type { SolcRunner } from "./solc.js";
13
+ export interface ReconstructedLayout {
14
+ /** Fully qualified name, `source.sol:Contract`. */
15
+ contract: string;
16
+ compiler: string;
17
+ bytecodeMatch: ProvingMatch;
18
+ layout: StorageLayout;
19
+ }
20
+ export declare function reconstructLayout(source: VerifiedSource, solc: SolcRunner, onchainCode: string): Promise<ReconstructedLayout>;
21
+ /**
22
+ * The layout of `contract` (`source.sol:Name`) in `input`, with no bytecode
23
+ * proof. Only for sources whose provenance is already known, such as a
24
+ * project's own build-info.
25
+ */
26
+ export declare function layoutFromSource(input: SolcInput, solc: SolcRunner, contract: string): Promise<StorageLayout>;
27
+ //# sourceMappingURL=reconstruct.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconstruct.d.ts","sourceRoot":"","sources":["../src/reconstruct.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAOL,KAAK,SAAS,EAEd,KAAK,aAAa,EACnB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAA2C,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAE3F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,WAAW,mBAAmB;IAClC,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,YAAY,CAAC;IAC5B,MAAM,EAAE,aAAa,CAAC;CACvB;AAoBD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,UAAU,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,CAiB9B;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,aAAa,CAAC,CAIxB"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Rebuilds the storage layout of deployed code from its verified source, and
3
+ * proves the source is the deployed code before trusting the layout.
4
+ *
5
+ * The layout comes out of the same oz-core pipeline the compile hook runs for
6
+ * the local build (full compile, namespaced second pass, `validate()`,
7
+ * `getStorageLayout`), so both sides of a comparison are extracted identically.
8
+ */
9
+ import { getStorageLayout, isNamespaceSupported, makeNamespacedInput, solcInputOutputDecoder, trySanitizeNatSpec, validate, } from "@openzeppelin/upgrades-core";
10
+ import { compareDeployedBytecode, isProvingMatch } from "./bytecode.js";
11
+ import { LayoutIntegrityError } from "./errors.js";
12
+ // validate() reads evm.bytecode for every contract, so codegen is requested
13
+ // for all of them. A verified input holds only the target's dependency graph.
14
+ const FULL_SELECTION = {
15
+ "*": {
16
+ "*": [
17
+ "storageLayout",
18
+ "evm.bytecode.object",
19
+ "evm.bytecode.linkReferences",
20
+ "evm.deployedBytecode.object",
21
+ "evm.deployedBytecode.immutableReferences",
22
+ ],
23
+ "": ["ast"],
24
+ },
25
+ };
26
+ // The namespaced pass only needs layouts and ASTs; skipping codegen keeps it cheap.
27
+ const LAYOUT_SELECTION = { "*": { "*": ["storageLayout"], "": ["ast"] } };
28
+ export async function reconstructLayout(source, solc, onchainCode) {
29
+ const input = withOutputSelection(source.input, FULL_SELECTION);
30
+ const output = await compileOrThrow(solc, input, "verified source");
31
+ const { contract, bytecodeMatch } = proveDeployedCode(output, source.contractName, onchainCode, embedsSourceHash(source.input));
32
+ return {
33
+ contract,
34
+ compiler: source.solcLongVersion,
35
+ bytecodeMatch,
36
+ layout: await extractLayout(input, output, solc, contract),
37
+ };
38
+ }
39
+ /**
40
+ * The layout of `contract` (`source.sol:Name`) in `input`, with no bytecode
41
+ * proof. Only for sources whose provenance is already known, such as a
42
+ * project's own build-info.
43
+ */
44
+ export async function layoutFromSource(input, solc, contract) {
45
+ const selected = withOutputSelection(input, FULL_SELECTION);
46
+ const output = await compileOrThrow(solc, selected, "source");
47
+ return extractLayout(selected, output, solc, contract);
48
+ }
49
+ async function extractLayout(input, output, solc, contract) {
50
+ const solcVersion = solc.longVersion.split("+")[0];
51
+ let namespacedOutput;
52
+ if (isNamespaceSupported(solcVersion)) {
53
+ let namespacedInput = makeNamespacedInput(input, output, solcVersion);
54
+ namespacedInput = await trySanitizeNatSpec(namespacedInput, solcVersion);
55
+ namespacedOutput = await compileOrThrow(solc, withOutputSelection(namespacedInput, LAYOUT_SELECTION), "namespaced pass");
56
+ }
57
+ const runData = validate(output, solcInputOutputDecoder(input, output), solcVersion, input, namespacedOutput);
58
+ const version = runData[contract]?.version;
59
+ if (version === undefined) {
60
+ throw new LayoutIntegrityError(`${contract} compiled to no bytecode; cannot derive its layout.`);
61
+ }
62
+ // OZ looks the contract up by bytecode version and takes the first match, so
63
+ // two contracts with identical code (possible without a source hash) could
64
+ // swap layouts. Listing the target first makes the lookup land on it.
65
+ return getStorageLayout({ [contract]: runData[contract], ...runData }, version);
66
+ }
67
+ /**
68
+ * Finds the compiled contract that matches the deployed code. The verified
69
+ * name narrows the search, but two files may define a contract with the same
70
+ * name, so the bytecode decides, and the strongest match wins. Only a proving
71
+ * match is accepted: `metadata-only` can hide a different storage layout.
72
+ */
73
+ function proveDeployedCode(output, contractName, onchainCode, hashed) {
74
+ let candidates = 0;
75
+ let best;
76
+ let weak;
77
+ for (const [sourcePath, contracts] of Object.entries(output.contracts ?? {})) {
78
+ const compiled = contracts[contractName];
79
+ const deployed = compiled?.evm?.deployedBytecode;
80
+ if (deployed?.object === undefined)
81
+ continue;
82
+ candidates++;
83
+ const compared = compareDeployedBytecode(onchainCode, deployed.object, deployed.immutableReferences);
84
+ const match = !hashed && isProvingMatch(compared) ? "code-only" : compared;
85
+ if (match === "metadata-only" || match === "code-only")
86
+ weak ??= match;
87
+ if (!isProvingMatch(match))
88
+ continue;
89
+ if (best === undefined || (match === "exact" && best.bytecodeMatch !== "exact")) {
90
+ best = { contract: `${sourcePath}:${contractName}`, bytecodeMatch: match };
91
+ }
92
+ }
93
+ if (best !== undefined)
94
+ return best;
95
+ if (candidates === 0) {
96
+ throw new LayoutIntegrityError(`Verified source has no contract named "${contractName}".`);
97
+ }
98
+ // Neither weak match proves the layout: unread variables and gap sizes never reach the bytecode.
99
+ throw new LayoutIntegrityError(weak === "metadata-only"
100
+ ? `Verified source for "${contractName}" matches the deployed code only after stripping ` +
101
+ `metadata, so the compiler input differed. That does not prove the storage layout ` +
102
+ `(unread variables and gap sizes never reach the bytecode). Refusing to trust it.`
103
+ : weak === "code-only"
104
+ ? `Verified source for "${contractName}" compiles to the deployed code, but the build ` +
105
+ `embeds no source hash (bytecodeHash "none" or appendCBOR false), so the code does ` +
106
+ `not prove the source or its storage layout. Refusing to trust it.`
107
+ : `Verified source for "${contractName}" does not compile to the deployed code, ` +
108
+ `even with immutables masked. Refusing to trust its layout.`);
109
+ }
110
+ /**
111
+ * Whether the verified settings embed a metadata hash. The bytecode check alone
112
+ * can be fooled: without CBOR (`appendCBOR: false`) the code can end in a data
113
+ * literal shaped like solc's metadata. The settings cannot lie, since the code
114
+ * only matches when compiled with the settings that were really used.
115
+ */
116
+ function embedsSourceHash(input) {
117
+ const metadata = input.settings.metadata;
118
+ return metadata?.appendCBOR !== false && metadata?.bytecodeHash !== "none";
119
+ }
120
+ async function compileOrThrow(solc, input, what) {
121
+ const output = await solc.compile(input);
122
+ const errors = (output.errors ?? []).filter((e) => e.severity === "error");
123
+ if (errors.length > 0) {
124
+ throw new LayoutIntegrityError(`solc ${solc.longVersion} failed to compile the ${what}: ${errors[0].formattedMessage}`);
125
+ }
126
+ return output;
127
+ }
128
+ // Only solc's standard-json keys: build tools add their own (Foundry's build-info
129
+ // input carries `version`, `allowPaths`, `basePath`, `includePaths`), and solc
130
+ // rejects any key it does not know.
131
+ function withOutputSelection(input, outputSelection) {
132
+ // upgrades-core's SolcInput type omits `language`, which standard json requires.
133
+ const { language } = input;
134
+ return {
135
+ language,
136
+ sources: input.sources,
137
+ settings: { ...input.settings, outputSelection },
138
+ };
139
+ }
140
+ //# sourceMappingURL=reconstruct.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconstruct.js","sourceRoot":"","sources":["../src/reconstruct.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Layout records: the proven storage layout of the code at one address on one
3
+ * chain. slotdiff never writes them anywhere. A record is plain JSON, so the
4
+ * caller saves it however it likes and loads it back through `parseRecord`
5
+ * (offline) or `verifyRecord` (against the chain):
6
+ *
7
+ * await writeFile(path, JSON.stringify(record, null, 2));
8
+ * const record = await verifyRecord(provider, address, JSON.parse(await readFile(path, "utf8")));
9
+ */
10
+ import type { StorageLayout } from "@openzeppelin/upgrades-core";
11
+ import { type ProvingMatch } from "./bytecode.js";
12
+ import { type EthProvider } from "./chain.js";
13
+ /**
14
+ * Deployed code never changes, so a record cannot go stale for its chain and
15
+ * address. `codeSha256` binds it to the exact runtime code it was proven
16
+ * against; `verifyRecord` refuses it for any other code or chain.
17
+ */
18
+ export interface LayoutRecord {
19
+ format: 1;
20
+ /** Lowercase. */
21
+ address: string;
22
+ chainId: number;
23
+ codeSha256: string;
24
+ /** Fully qualified name, `source.sol:Contract`. */
25
+ contract: string;
26
+ /** solc long version, when known. */
27
+ compiler?: string;
28
+ bytecodeMatch: ProvingMatch;
29
+ /** `explorer`: rebuilt from verified source. `local-compile`: proven against a local build. */
30
+ source: "explorer" | "local-compile";
31
+ recordedAt: string;
32
+ layout: StorageLayout;
33
+ }
34
+ /**
35
+ * Checks a loaded record without touching the chain: its format, its shape,
36
+ * and that it rests on a match that proves a layout. Pass `address` to also
37
+ * require that it describes that address. Use `verifyRecord` whenever an RPC
38
+ * is available; this alone cannot tell that the code at the address changed
39
+ * or that the record came from another chain.
40
+ */
41
+ export declare function parseRecord(value: unknown, address?: string): LayoutRecord;
42
+ /**
43
+ * `parseRecord`, then checks the record against the chain: it must have been
44
+ * made on this chain, and the code at `address` must be the code it was
45
+ * proven against. Returns the record, ready to diff.
46
+ */
47
+ export declare function verifyRecord(provider: EthProvider, address: string, value: unknown): Promise<LayoutRecord>;
48
+ //# sourceMappingURL=record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAGjF;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,CAAC,CAAC;IACV,iBAAiB;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,YAAY,CAAC;IAC5B,+FAA+F;IAC/F,MAAM,EAAE,UAAU,GAAG,eAAe,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,aAAa,CAAC;CACvB;AAKD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,YAAY,CA0C1E;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,GACb,OAAO,CAAC,YAAY,CAAC,CA0BvB"}
package/dist/record.js ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Layout records: the proven storage layout of the code at one address on one
3
+ * chain. slotdiff never writes them anywhere. A record is plain JSON, so the
4
+ * caller saves it however it likes and loads it back through `parseRecord`
5
+ * (offline) or `verifyRecord` (against the chain):
6
+ *
7
+ * await writeFile(path, JSON.stringify(record, null, 2));
8
+ * const record = await verifyRecord(provider, address, JSON.parse(await readFile(path, "utf8")));
9
+ */
10
+ import { isProvingMatch } from "./bytecode.js";
11
+ import { codeSha256, readChainId, readCode } from "./chain.js";
12
+ import { LayoutIntegrityError, LayoutUnavailableError } from "./errors.js";
13
+ const ADDRESS = /^0x[0-9a-f]{40}$/;
14
+ const SHA256 = /^[0-9a-f]{64}$/;
15
+ /**
16
+ * Checks a loaded record without touching the chain: its format, its shape,
17
+ * and that it rests on a match that proves a layout. Pass `address` to also
18
+ * require that it describes that address. Use `verifyRecord` whenever an RPC
19
+ * is available; this alone cannot tell that the code at the address changed
20
+ * or that the record came from another chain.
21
+ */
22
+ export function parseRecord(value, address) {
23
+ if (typeof value !== "object" || value === null) {
24
+ throw new LayoutIntegrityError("A layout record must be a JSON object.");
25
+ }
26
+ const r = value;
27
+ if (r.format !== 1) {
28
+ throw new Error(`Unsupported layout record format ${String(r.format)}; upgrade slotdiff.`);
29
+ }
30
+ const who = typeof r.address === "string" ? r.address : "an unknown address";
31
+ const malformed = (field) => new LayoutIntegrityError(`The layout record for ${who} has a missing or malformed ${field}.`);
32
+ if (typeof r.address !== "string" || !ADDRESS.test(r.address))
33
+ throw malformed("address");
34
+ if (!Number.isSafeInteger(r.chainId) || r.chainId <= 0)
35
+ throw malformed("chainId");
36
+ if (typeof r.codeSha256 !== "string" || !SHA256.test(r.codeSha256)) {
37
+ throw malformed("codeSha256");
38
+ }
39
+ if (typeof r.contract !== "string" || !r.contract.includes(":"))
40
+ throw malformed("contract");
41
+ if (r.compiler !== undefined && typeof r.compiler !== "string")
42
+ throw malformed("compiler");
43
+ if (r.source !== "explorer" && r.source !== "local-compile")
44
+ throw malformed("source");
45
+ if (typeof r.recordedAt !== "string")
46
+ throw malformed("recordedAt");
47
+ if (typeof r.layout !== "object" ||
48
+ r.layout === null ||
49
+ !Array.isArray(r.layout.storage) ||
50
+ typeof r.layout.types !== "object" ||
51
+ r.layout.types === null) {
52
+ throw malformed("layout");
53
+ }
54
+ if (typeof r.bytecodeMatch !== "string" || !isProvingMatch(r.bytecodeMatch)) {
55
+ throw new LayoutIntegrityError(`The layout record for ${who} rests on a "${String(r.bytecodeMatch)}" match, which does ` +
56
+ `not prove a storage layout.`);
57
+ }
58
+ if (address !== undefined && r.address !== address.toLowerCase()) {
59
+ throw new LayoutIntegrityError(`The layout record for ${r.address} was loaded for ${address.toLowerCase()}.`);
60
+ }
61
+ return r;
62
+ }
63
+ /**
64
+ * `parseRecord`, then checks the record against the chain: it must have been
65
+ * made on this chain, and the code at `address` must be the code it was
66
+ * proven against. Returns the record, ready to diff.
67
+ */
68
+ export async function verifyRecord(provider, address, value) {
69
+ const record = parseRecord(value, address);
70
+ const chainId = await readChainId(provider);
71
+ if (record.chainId !== chainId) {
72
+ throw new LayoutIntegrityError(`The layout record for ${record.address} was made on chain ${record.chainId}, not on ` +
73
+ `chain ${chainId}.`);
74
+ }
75
+ let code;
76
+ try {
77
+ code = await readCode(provider, record.address);
78
+ }
79
+ catch (e) {
80
+ // The code the record was proven against is gone; never a reason to fall back.
81
+ if (!(e instanceof LayoutUnavailableError))
82
+ throw e;
83
+ throw new LayoutIntegrityError(`The layout record for ${record.address} describes code that is no longer at that address.`);
84
+ }
85
+ if (codeSha256(code) !== record.codeSha256) {
86
+ throw new LayoutIntegrityError(`The layout record for ${record.address} was proven against different code than this ` +
87
+ `chain has at that address.`);
88
+ }
89
+ return record;
90
+ }
91
+ //# sourceMappingURL=record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.js","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,cAAc,EAAqB,MAAM,eAAe,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAoB,MAAM,YAAY,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAwB3E,MAAM,OAAO,GAAG,kBAAkB,CAAC;AACnC,MAAM,MAAM,GAAG,gBAAgB,CAAC;AAEhC;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc,EAAE,OAAgB;IAC1D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,MAAM,IAAI,oBAAoB,CAAC,wCAAwC,CAAC,CAAC;IAC3E,CAAC;IACD,MAAM,CAAC,GAAG,KAA8B,CAAC;IACzC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,oCAAoC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC;IAC7F,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC;IAC7E,MAAM,SAAS,GAAG,CAAC,KAAa,EAAE,EAAE,CAClC,IAAI,oBAAoB,CAAC,yBAAyB,GAAG,+BAA+B,KAAK,GAAG,CAAC,CAAC;IAEhG,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;QAAE,MAAM,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1F,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,IAAK,CAAC,CAAC,OAAkB,IAAI,CAAC;QAAE,MAAM,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/F,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC;QACnE,MAAM,SAAS,CAAC,YAAY,CAAC,CAAC;IAChC,CAAC;IACD,IAAI,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,MAAM,SAAS,CAAC,UAAU,CAAC,CAAC;IAC7F,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ;QAAE,MAAM,SAAS,CAAC,UAAU,CAAC,CAAC;IAC5F,IAAI,CAAC,CAAC,MAAM,KAAK,UAAU,IAAI,CAAC,CAAC,MAAM,KAAK,eAAe;QAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC;IACvF,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ;QAAE,MAAM,SAAS,CAAC,YAAY,CAAC,CAAC;IACpE,IACE,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;QAC5B,CAAC,CAAC,MAAM,KAAK,IAAI;QACjB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;QAChC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,QAAQ;QAClC,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,EACvB,CAAC;QACD,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC;IACD,IAAI,OAAO,CAAC,CAAC,aAAa,KAAK,QAAQ,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC;QAC5E,MAAM,IAAI,oBAAoB,CAC5B,yBAAyB,GAAG,gBAAgB,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,sBAAsB;YACvF,6BAA6B,CAChC,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;QACjE,MAAM,IAAI,oBAAoB,CAC5B,yBAAyB,CAAC,CAAC,OAAO,mBAAmB,OAAO,CAAC,WAAW,EAAE,GAAG,CAC9E,CAAC;IACJ,CAAC;IACD,OAAO,CAAiB,CAAC;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,QAAqB,EACrB,OAAe,EACf,KAAc;IAEd,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC5C,IAAI,MAAM,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;QAC/B,MAAM,IAAI,oBAAoB,CAC5B,yBAAyB,MAAM,CAAC,OAAO,sBAAsB,MAAM,CAAC,OAAO,WAAW;YACpF,SAAS,OAAO,GAAG,CACtB,CAAC;IACJ,CAAC;IACD,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAClD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,+EAA+E;QAC/E,IAAI,CAAC,CAAC,CAAC,YAAY,sBAAsB,CAAC;YAAE,MAAM,CAAC,CAAC;QACpD,MAAM,IAAI,oBAAoB,CAC5B,yBAAyB,MAAM,CAAC,OAAO,oDAAoD,CAC5F,CAAC;IACJ,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,UAAU,EAAE,CAAC;QAC3C,MAAM,IAAI,oBAAoB,CAC5B,yBAAyB,MAAM,CAAC,OAAO,+CAA+C;YACpF,4BAA4B,CAC/B,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,54 @@
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 type { StorageLayout } from "@openzeppelin/upgrades-core";
11
+ import { type DeployedBytecodeMatch, type ImmutableReferences } from "./bytecode.js";
12
+ import { type EthProvider } from "./chain.js";
13
+ import { type ExplorerConfig } from "./explorer.js";
14
+ import { type LayoutRecord } from "./record.js";
15
+ import { type SolcOptions } from "./solc.js";
16
+ export interface ResolveOptions {
17
+ explorer: ExplorerConfig;
18
+ solc?: SolcOptions;
19
+ }
20
+ /**
21
+ * Rebuilds the layout of the code at `address` from its verified source and
22
+ * proves that source is the deployed code. Always fetches and compiles; check
23
+ * your own saved record first (`verifyRecord`) to skip that.
24
+ */
25
+ export declare function resolveLayout(provider: EthProvider, address: string, options: ResolveOptions): Promise<LayoutRecord>;
26
+ export interface ProxyLayout {
27
+ /** The implementation the proxy runs now. */
28
+ implementation: string;
29
+ /** Set for a beacon proxy. */
30
+ beacon?: string;
31
+ record: LayoutRecord;
32
+ }
33
+ /** `resolveLayout` for whatever implementation `proxy` runs right now (ERC-1967 or beacon). */
34
+ export declare function resolveProxyLayout(provider: EthProvider, proxy: string, options: ResolveOptions): Promise<ProxyLayout>;
35
+ export interface LocalBuild {
36
+ /** Fully qualified name, `source.sol:Contract`. */
37
+ contract: string;
38
+ compiler?: string;
39
+ layout: StorageLayout;
40
+ deployedBytecode: string;
41
+ immutableReferences?: ImmutableReferences;
42
+ }
43
+ export interface LocalBuildResult {
44
+ bytecodeMatch: DeployedBytecodeMatch;
45
+ /** Set only when the match proves the layout (`exact` or `immutables-only`). */
46
+ record?: LayoutRecord;
47
+ }
48
+ /**
49
+ * Makes a record from a local build of the code at `address`, but only after
50
+ * proving the chain runs that build. A `metadata-only` or `none` match gives
51
+ * no record, so a layout is never attributed to code that is not there.
52
+ */
53
+ export declare function proveLocalBuild(provider: EthProvider, address: string, build: LocalBuild): Promise<LocalBuildResult>;
54
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAGL,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAgD,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAE5F,OAAO,EAAyC,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAE3F,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAW,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAEtD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,cAAc,CAAC;IACzB,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,YAAY,CAAC,CAyBvB;AAED,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,cAAc,EAAE,MAAM,CAAC;IACvB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,+FAA+F;AAC/F,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,WAAW,EACrB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,WAAW,CAAC,CAStB;AAED,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,qBAAqB,CAAC;IACrC,gFAAgF;IAChF,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,UAAU,GAChB,OAAO,CAAC,gBAAgB,CAAC,CAwB3B"}