slotdiff 0.1.0-alpha.1 → 0.3.0

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 (65) hide show
  1. package/CHANGELOG.md +50 -9
  2. package/README.md +219 -111
  3. package/dist/bin.d.ts +3 -0
  4. package/dist/bin.d.ts.map +1 -0
  5. package/dist/bin.js +22 -0
  6. package/dist/bin.js.map +1 -0
  7. package/dist/build-info.d.ts +45 -0
  8. package/dist/build-info.d.ts.map +1 -0
  9. package/dist/build-info.js +319 -0
  10. package/dist/build-info.js.map +1 -0
  11. package/dist/cli.d.ts +25 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +389 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/erc7201.d.ts +88 -0
  16. package/dist/erc7201.d.ts.map +1 -0
  17. package/dist/erc7201.js +509 -0
  18. package/dist/erc7201.js.map +1 -0
  19. package/dist/errors.d.ts +19 -0
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +28 -0
  22. package/dist/errors.js.map +1 -1
  23. package/dist/explorer.d.ts.map +1 -1
  24. package/dist/explorer.js +29 -10
  25. package/dist/explorer.js.map +1 -1
  26. package/dist/index.d.ts +6 -3
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +5 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/reconstruct.d.ts +70 -7
  31. package/dist/reconstruct.d.ts.map +1 -1
  32. package/dist/reconstruct.js +157 -20
  33. package/dist/reconstruct.js.map +1 -1
  34. package/dist/record.d.ts +13 -0
  35. package/dist/record.d.ts.map +1 -1
  36. package/dist/record.js +8 -0
  37. package/dist/record.js.map +1 -1
  38. package/dist/report.d.ts +29 -0
  39. package/dist/report.d.ts.map +1 -0
  40. package/dist/report.js +138 -0
  41. package/dist/report.js.map +1 -0
  42. package/dist/resolve.d.ts +16 -0
  43. package/dist/resolve.d.ts.map +1 -1
  44. package/dist/resolve.js +17 -2
  45. package/dist/resolve.js.map +1 -1
  46. package/dist/solc.js +20 -4
  47. package/dist/solc.js.map +1 -1
  48. package/dist/upgrade.d.ts +48 -0
  49. package/dist/upgrade.d.ts.map +1 -0
  50. package/dist/upgrade.js +36 -0
  51. package/dist/upgrade.js.map +1 -0
  52. package/package.json +9 -6
  53. package/src/bin.ts +25 -0
  54. package/src/build-info.ts +405 -0
  55. package/src/cli.ts +459 -0
  56. package/src/erc7201.ts +603 -0
  57. package/src/errors.ts +31 -0
  58. package/src/explorer.ts +40 -11
  59. package/src/index.ts +28 -3
  60. package/src/reconstruct.ts +279 -29
  61. package/src/record.ts +18 -0
  62. package/src/report.ts +186 -0
  63. package/src/resolve.ts +28 -3
  64. package/src/solc.ts +20 -4
  65. package/src/upgrade.ts +91 -0
package/src/explorer.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { SolcInput } from "@openzeppelin/upgrades-core";
11
- import { LayoutUnavailableError } from "./errors.js";
11
+ import { errorCode, LayoutUnavailableError, printable } from "./errors.js";
12
12
 
13
13
  export const ETHERSCAN_V2_API_URL = "https://api.etherscan.io/v2/api";
14
14
 
@@ -87,7 +87,7 @@ async function requestWithRetry(url: string, fetchImpl: FetchLike): Promise<Sour
87
87
  res = await fetchImpl(url);
88
88
  } catch (e) {
89
89
  // Messages never include the URL, which carries the API key.
90
- throw new LayoutUnavailableError(`Explorer unreachable: ${(e as Error).message}`);
90
+ throw new LayoutUnavailableError(`Explorer unreachable (${errorCode(e)}).`);
91
91
  }
92
92
 
93
93
  if (res.status === 429 && retry) {
@@ -115,8 +115,12 @@ async function requestWithRetry(url: string, fetchImpl: FetchLike): Promise<Sour
115
115
  return entry as SourceCodeEntry;
116
116
  }
117
117
 
118
- const detail =
119
- typeof body.result === "string" ? body.result : (body.message ?? "unknown error");
118
+ // One line: a line break could draw a verdict of its own under the error.
119
+ const detail = printable(
120
+ String(typeof body.result === "string" ? body.result : (body.message ?? "unknown error")),
121
+ )
122
+ .replace(/\s+/g, " ")
123
+ .slice(0, 300);
120
124
  // Before the key check: Etherscan's rate-limit message mentions the API key.
121
125
  if (RATE_LIMIT.test(detail)) {
122
126
  if (retry) {
@@ -134,6 +138,10 @@ function backoff(attempt: number): Promise<void> {
134
138
  return new Promise((r) => setTimeout(r, 1000 * attempt));
135
139
  }
136
140
 
141
+ // Control characters other than tabs and line breaks (\v, \f and \r included).
142
+ // eslint-disable-next-line no-control-regex
143
+ const CONTROL = /[\u0000-\u0008\u000e-\u001f\u007f-\u009f]/;
144
+
137
145
  /** Exported for tests. */
138
146
  export function parseSourceCodeEntry(entry: SourceCodeEntry, address: string): VerifiedSource {
139
147
  const raw = entry.SourceCode ?? "";
@@ -144,7 +152,8 @@ export function parseSourceCodeEntry(entry: SourceCodeEntry, address: string): V
144
152
  const compilerVersion = entry.CompilerVersion ?? "";
145
153
  if (!compilerVersion.startsWith("v0.")) {
146
154
  throw new LayoutUnavailableError(
147
- `Contract ${address} was verified with "${compilerVersion}"; only Solidity is supported.`,
155
+ `Contract ${address} was verified with "${printable(compilerVersion).slice(0, 80)}"; ` +
156
+ "only Solidity is supported.",
148
157
  );
149
158
  }
150
159
 
@@ -154,7 +163,8 @@ export function parseSourceCodeEntry(entry: SourceCodeEntry, address: string): V
154
163
  )
155
164
  ) {
156
165
  throw new LayoutUnavailableError(
157
- `Contract ${address} reports an unrecognized compiler version "${compilerVersion}".`,
166
+ `Contract ${address} reports an unrecognized compiler version ` +
167
+ `"${printable(compilerVersion).slice(0, 80)}".`,
158
168
  );
159
169
  }
160
170
 
@@ -166,11 +176,30 @@ export function parseSourceCodeEntry(entry: SourceCodeEntry, address: string): V
166
176
  );
167
177
  }
168
178
 
169
- return {
170
- input,
171
- solcLongVersion: compilerVersion.slice(1),
172
- contractName: entry.ContractName ?? "",
173
- };
179
+ // Names, paths and source text (annotations, quoted by solc) reach the
180
+ // screen; real source holds no control characters but tabs and line breaks.
181
+ const contractName = entry.ContractName ?? "";
182
+ const sources = Object.values(input.sources) as Array<{ content?: unknown }>;
183
+ // Every source inline: solc would fetch `urls` itself.
184
+ if (sources.some((s) => typeof s?.content !== "string")) {
185
+ throw new LayoutUnavailableError(
186
+ `Contract ${address} is verified with sources that are not inline (urls); only content is read.`,
187
+ );
188
+ }
189
+ const contents = sources.map((s) => s.content as string);
190
+ if (
191
+ // Names and paths are printed on one line: no whitespace but a space.
192
+ [contractName, ...Object.keys(input.sources)].some(
193
+ (s) => printable(s) !== s || /[^\S ]/.test(s),
194
+ ) ||
195
+ contents.some((s) => CONTROL.test(s))
196
+ ) {
197
+ throw new LayoutUnavailableError(
198
+ `Contract ${address} is verified with control characters in its name, paths or source.`,
199
+ );
200
+ }
201
+
202
+ return { input, solcLongVersion: compilerVersion.slice(1), contractName };
174
203
  }
175
204
 
176
205
  // Etherscan wraps standard-json in an extra pair of braces (`{{...}}`); some
package/src/index.ts CHANGED
@@ -24,7 +24,32 @@ export {
24
24
  type VerifiedSource,
25
25
  } from "./explorer.js";
26
26
  export { getSolc, type SolcOptions, type SolcRunner } from "./solc.js";
27
- export { layoutFromSource } from "./reconstruct.js";
27
+ export { findBuildInfo, type BuildInfoOptions, type BuildInfoContract } from "./build-info.js";
28
+ export {
29
+ checkNamespaces,
30
+ type NamespaceCheck,
31
+ type NamespacePair,
32
+ type NamespaceSlot,
33
+ } from "./erc7201.js";
34
+ export {
35
+ checkUpgrade,
36
+ type UpgradeCheck,
37
+ type UpgradeOptions,
38
+ type UpgradeSide,
39
+ } from "./upgrade.js";
40
+ export {
41
+ analyzeSource,
42
+ analyzeBuild,
43
+ type BuildAnalysis,
44
+ type BuildCompiler,
45
+ SAFETY_ERROR_KINDS,
46
+ type SourceAnalysis,
47
+ type SafetyOptions,
48
+ type SafetyReport,
49
+ type SafetyError,
50
+ type SafetyErrorKind,
51
+ type ProxyKind,
52
+ } from "./reconstruct.js";
28
53
  export {
29
54
  compareDeployedBytecode,
30
55
  isProvingMatch,
@@ -32,5 +57,5 @@ export {
32
57
  type ProvingMatch,
33
58
  type ImmutableReferences,
34
59
  } from "./bytecode.js";
35
- export { LayoutIntegrityError, LayoutUnavailableError } from "./errors.js";
36
- export type { SolcInput, StorageLayout } from "@openzeppelin/upgrades-core";
60
+ export { IncompleteBuildError, LayoutIntegrityError, LayoutUnavailableError } from "./errors.js";
61
+ export type { SolcInput, SolcOutput, StorageLayout } from "@openzeppelin/upgrades-core";
@@ -8,27 +8,33 @@
8
8
  */
9
9
 
10
10
  import {
11
+ getErrors,
11
12
  getStorageLayout,
13
+ hashBytecode,
14
+ inferProxyKind,
12
15
  isNamespaceSupported,
13
16
  makeNamespacedInput,
14
17
  solcInputOutputDecoder,
15
18
  trySanitizeNatSpec,
19
+ UpgradeableContractErrorReport,
16
20
  validate,
21
+ type ProxyDeployment,
17
22
  type SolcInput,
18
23
  type SolcOutput,
19
24
  type StorageLayout,
25
+ type ValidationRunData,
20
26
  } from "@openzeppelin/upgrades-core";
21
27
  import { compareDeployedBytecode, isProvingMatch, type ProvingMatch } from "./bytecode.js";
22
- import { LayoutIntegrityError } from "./errors.js";
28
+ import { namespaceSlots, type NamespaceSlot } from "./erc7201.js";
29
+ import { IncompleteBuildError, LayoutIntegrityError, printable } from "./errors.js";
23
30
  import type { VerifiedSource } from "./explorer.js";
24
31
  import type { SolcRunner } from "./solc.js";
25
32
 
26
- export interface ReconstructedLayout {
33
+ export interface ReconstructedLayout extends SourceAnalysis {
27
34
  /** Fully qualified name, `source.sol:Contract`. */
28
35
  contract: string;
29
36
  compiler: string;
30
37
  bytecodeMatch: ProvingMatch;
31
- layout: StorageLayout;
32
38
  }
33
39
 
34
40
  // validate() reads evm.bytecode for every contract, so codegen is requested
@@ -41,6 +47,7 @@ const FULL_SELECTION = {
41
47
  "evm.bytecode.linkReferences",
42
48
  "evm.deployedBytecode.object",
43
49
  "evm.deployedBytecode.immutableReferences",
50
+ "metadata",
44
51
  ],
45
52
  "": ["ast"],
46
53
  },
@@ -68,61 +75,304 @@ export async function reconstructLayout(
68
75
  contract,
69
76
  compiler: source.solcLongVersion,
70
77
  bytecodeMatch,
71
- layout: await extractLayout(input, output, solc, contract),
78
+ ...(await analyzeBuild(input, output, solc)).contract(contract),
72
79
  };
73
80
  }
74
81
 
82
+ export type ProxyKind = ProxyDeployment["kind"];
83
+
84
+ type ValidationError = ReturnType<typeof getErrors>[number];
85
+
75
86
  /**
76
- * The layout of `contract` (`source.sol:Name`) in `input`, with no bytecode
77
- * proof. Only for sources whose provenance is already known, such as a
78
- * project's own build-info.
87
+ * OpenZeppelin's safety error kinds, without the deprecated struct-definition
88
+ * and enum-definition, which the check never reports. Listed here, not taken
89
+ * from OpenZeppelin, so its releases cannot change this API unseen; a test
90
+ * compares the two.
79
91
  */
80
- export async function layoutFromSource(
92
+ export const SAFETY_ERROR_KINDS = [
93
+ "state-variable-assignment",
94
+ "state-variable-immutable",
95
+ "external-library-linking",
96
+ "constructor",
97
+ "delegatecall",
98
+ "selfdestruct",
99
+ "missing-public-upgradeto",
100
+ "internal-function-storage",
101
+ "missing-initializer",
102
+ "missing-initializer-call",
103
+ "duplicate-initializer-call",
104
+ "incorrect-initializer-order",
105
+ ] as const;
106
+
107
+ export type SafetyErrorKind = (typeof SAFETY_ERROR_KINDS)[number];
108
+
109
+ export interface SafetyError {
110
+ /** OpenZeppelin's error kind, such as `constructor`. */
111
+ kind: SafetyErrorKind;
112
+ /** Where, as `file.sol:line`. */
113
+ src: string;
114
+ }
115
+
116
+ export interface SafetyOptions {
117
+ /**
118
+ * The proxy being upgraded; only `uups` requires an `upgradeToAndCall`.
119
+ * Required: the new contract's own `kind` would let a UUPS implementation
120
+ * that lost its upgrade function pass.
121
+ */
122
+ kind: ProxyKind;
123
+ /** Error kinds to accept anyway (OZ's `unsafeAllow`). Prefer NatSpec exceptions in the source. */
124
+ unsafeAllow?: SafetyErrorKind[];
125
+ }
126
+
127
+ export interface SafetyReport {
128
+ ok: boolean;
129
+ errors: SafetyError[];
130
+ /** OpenZeppelin's description of each error; `color` (default true) adds terminal colors. */
131
+ explain(color?: boolean): string;
132
+ }
133
+
134
+ export interface SourceAnalysis {
135
+ layout: StorageLayout;
136
+ /** `uups` when the contract has `upgradeTo` or `upgradeToAndCall`, else `transparent`. */
137
+ kind: Exclude<ProxyKind, "beacon">;
138
+ /**
139
+ * The slot each ERC-7201 namespace really uses, read from the code
140
+ * (`$.slot := LOCATION`); OpenZeppelin trusts the annotation alone.
141
+ */
142
+ namespaces: NamespaceSlot[];
143
+ /**
144
+ * OpenZeppelin's safety check (constructors, `selfdestruct`,
145
+ * `delegatecall`, initial values, immutables, initializers), honoring
146
+ * `@custom:oz-upgrades-unsafe-allow` in the source.
147
+ */
148
+ safety(options: SafetyOptions): SafetyReport;
149
+ }
150
+
151
+ /**
152
+ * The layout of `contract` (`source.sol:Name`) in `input`, its ERC-7201 slots
153
+ * and OpenZeppelin's safety check, from one compile. No bytecode proof: only
154
+ * for sources whose provenance is already known, such as a project's own build info.
155
+ */
156
+ export async function analyzeSource(
81
157
  input: SolcInput,
82
- solc: SolcRunner,
158
+ solc: BuildCompiler,
83
159
  contract: string,
84
- ): Promise<StorageLayout> {
160
+ ): Promise<SourceAnalysis> {
85
161
  const selected = withOutputSelection(input, FULL_SELECTION);
86
162
  const output = await compileOrThrow(solc, selected, "source");
87
- return extractLayout(selected, output, solc, contract);
163
+ return (await analyzeBuild(selected, output, solc)).contract(contract);
88
164
  }
89
165
 
90
- async function extractLayout(
166
+ /** The compiler for OpenZeppelin's namespaced pass: a `SolcRunner`, or a build tool's. */
167
+ export type BuildCompiler = Pick<SolcRunner, "longVersion" | "compile">;
168
+
169
+ export interface BuildAnalysis {
170
+ /** The analysis of one contract in the build, `source.sol:Name`. */
171
+ contract(contract: string): SourceAnalysis;
172
+ }
173
+
174
+ /**
175
+ * `analyzeSource` for a build you already compiled, such as a build tool's,
176
+ * without compiling it again: one namespaced pass and one validation serve
177
+ * every contract in it. `output` must hold every source's `ast` and, for each
178
+ * contract, its `metadata`, `storageLayout` (an interface or library may lack
179
+ * it) and `evm.bytecode` (`object`, `linkReferences`); without them it throws
180
+ * `IncompleteBuildError`. The metadata proves `output` was compiled from
181
+ * `input`'s sources by `compiler`'s version (not with its settings).
182
+ */
183
+ export async function analyzeBuild(
91
184
  input: SolcInput,
92
185
  output: SolcOutput,
93
- solc: SolcRunner,
186
+ compiler: BuildCompiler,
187
+ ): Promise<BuildAnalysis> {
188
+ checkBuild(input, output, compiler.longVersion);
189
+ const runData = await validateBuild(input, output, compiler);
190
+ return { contract: (contract) => analyzeContract(runData, output, contract) };
191
+ }
192
+
193
+ interface AstNode {
194
+ nodeType?: string;
195
+ name?: unknown;
196
+ contractKind?: unknown;
197
+ }
198
+
199
+ interface CompiledContract {
200
+ metadata?: unknown;
201
+ storageLayout?: unknown;
202
+ evm?: { bytecode?: { object?: unknown; linkReferences?: unknown } };
203
+ }
204
+
205
+ /**
206
+ * The analysis reads layouts from `output` and edits `input` at the offsets in
207
+ * `output`'s ASTs, so the two must be one build, complete: an output from other
208
+ * sources could pass a changed layout, and one without an AST would leave every
209
+ * namespace unassigned, compared by annotation alone. Anything else is refused.
210
+ */
211
+ function checkBuild(input: SolcInput, output: SolcOutput, longVersion: string): void {
212
+ if (output.sources === undefined && Object.hasOwn(output, "_format")) {
213
+ throw new Error(
214
+ "The compiler output is wrapped in a build info file; pass its output field " +
215
+ "(Hardhat 3 writes { _format, output } to <id>.output.json).",
216
+ );
217
+ }
218
+ const paths = Object.keys(input.sources);
219
+ const outputPaths = Object.keys(output.sources ?? {});
220
+ if (
221
+ paths.length !== outputPaths.length ||
222
+ paths.some((path) => !Object.hasOwn(output.sources, path))
223
+ ) {
224
+ throw new Error("The compiler output does not hold the same sources as the input.");
225
+ }
226
+ for (const path of paths) {
227
+ if (typeof (input.sources[path] as { content?: unknown }).content !== "string") {
228
+ throw new Error(`The compiler input has no content for ${path}.`);
229
+ }
230
+ const ast = output.sources[path].ast as { nodes?: AstNode[] } | undefined;
231
+ if (ast === undefined) {
232
+ throw new IncompleteBuildError(
233
+ `The compiler output has no ast for ${path}; it needs every source's ast.`,
234
+ );
235
+ }
236
+ for (const node of ast.nodes ?? []) {
237
+ if (node.nodeType !== "ContractDefinition") continue;
238
+ const name = `${path}:${String(node.name)}`;
239
+ const c = output.contracts?.[path]?.[String(node.name)] as CompiledContract | undefined;
240
+ if (c === undefined)
241
+ throw new IncompleteBuildError(`The compiler output has no contract output for ${name}.`);
242
+ // Neither can hold state, and some tools (Foundry) leave their layout out.
243
+ const stateless = node.contractKind === "interface" || node.contractKind === "library";
244
+ if (
245
+ typeof c.metadata !== "string" ||
246
+ (c.storageLayout === undefined && !stateless) ||
247
+ typeof c.evm?.bytecode?.object !== "string" ||
248
+ typeof c.evm.bytecode.linkReferences !== "object"
249
+ ) {
250
+ throw new IncompleteBuildError(
251
+ `The compiler output for ${name} lacks metadata, storageLayout or evm.bytecode ` +
252
+ `(object, linkReferences); compile with them in outputSelection.`,
253
+ );
254
+ }
255
+ proveSameBuild(input, path, name, c.metadata, longVersion);
256
+ }
257
+ }
258
+ }
259
+
260
+ function proveSameBuild(
261
+ input: SolcInput,
262
+ path: string,
263
+ name: string,
264
+ metadata: string,
265
+ longVersion: string,
266
+ ) {
267
+ let parsed: {
268
+ compiler?: { version?: unknown };
269
+ sources?: Record<string, { keccak256?: unknown }>;
270
+ };
271
+ try {
272
+ parsed = JSON.parse(metadata);
273
+ } catch {
274
+ throw new LayoutIntegrityError(`The metadata for ${name} is not JSON.`);
275
+ }
276
+ const [version, commit] = String(parsed.compiler?.version).split("+");
277
+ const [wanted, wantedCommit] = longVersion.split("+");
278
+ if (
279
+ version !== wanted ||
280
+ (commit !== undefined && wantedCommit !== undefined && commit !== wantedCommit)
281
+ ) {
282
+ throw new LayoutIntegrityError(
283
+ `${name} was compiled by solc ${String(parsed.compiler?.version)}, not ${longVersion}.`,
284
+ );
285
+ }
286
+ // Its own source at least: metadata that lists none would prove nothing.
287
+ const sources: Record<string, { keccak256?: unknown }> = { [path]: {}, ...parsed.sources };
288
+ for (const [source, { keccak256 }] of Object.entries(sources)) {
289
+ const content = (input.sources[source] as { content?: string } | undefined)?.content;
290
+ const hash =
291
+ content === undefined ? undefined : "0x" + hashBytecode(Buffer.from(content).toString("hex"));
292
+ if (hash !== keccak256) {
293
+ throw new LayoutIntegrityError(
294
+ `The compiler output for ${name} was not compiled from this input (${source} differs).`,
295
+ );
296
+ }
297
+ }
298
+ }
299
+
300
+ function analyzeContract(
301
+ runData: ValidationRunData,
302
+ output: SolcOutput,
94
303
  contract: string,
95
- ): Promise<StorageLayout> {
96
- const solcVersion = solc.longVersion.split("+")[0];
304
+ ): SourceAnalysis {
305
+ const colon = contract.lastIndexOf(":");
306
+ if (output.contracts?.[contract.slice(0, colon)]?.[contract.slice(colon + 1)] === undefined) {
307
+ throw new Error(
308
+ `${contract} is not in the compiler input. Name it as source.sol:Contract, with the ` +
309
+ `source path exactly as the input has it (Hardhat 3 starts it with project/).`,
310
+ );
311
+ }
312
+ const version = runData[contract]?.version;
313
+ if (version === undefined) {
314
+ throw new LayoutIntegrityError(
315
+ `${contract} compiled to no bytecode (an abstract contract or interface?); ` +
316
+ `cannot derive its layout.`,
317
+ );
318
+ }
319
+ // OZ looks the contract up by bytecode version and takes the first match, so
320
+ // two contracts with identical code (possible without a source hash) could
321
+ // swap layouts or safety errors. Listing the target first makes every lookup land on it.
322
+ const data: ValidationRunData = { [contract]: runData[contract], ...runData };
323
+ // A copy: OpenZeppelin's layout points into the run data that every call shares.
324
+ const layout = structuredClone(getStorageLayout(data, version));
325
+ return {
326
+ layout,
327
+ // Typed as any ProxyKind, but only ever uups or transparent.
328
+ kind: inferProxyKind(data, version) as SourceAnalysis["kind"],
329
+ namespaces: namespaceSlots(output, contract, Object.keys(layout.namespaces ?? {})),
330
+ safety({ kind, unsafeAllow = [] }) {
331
+ // OpenZeppelin ignores a kind it does not know, so a typo would accept nothing silently.
332
+ const unknown = unsafeAllow.find((k) => !SAFETY_ERROR_KINDS.includes(k));
333
+ if (unknown !== undefined) {
334
+ throw new Error(
335
+ `Unknown unsafeAllow error kind "${String(unknown)}". Known: ${SAFETY_ERROR_KINDS.join(", ")}.`,
336
+ );
337
+ }
338
+ // A copy: OpenZeppelin appends to the array it is given.
339
+ const opts = { kind, unsafeAllow: [...unsafeAllow] };
340
+ // The deprecated kinds are never reported, so every error has a SafetyErrorKind.
341
+ const errors = getErrors(data, version, opts) as Array<ValidationError & SafetyError>;
342
+ return {
343
+ ok: errors.length === 0,
344
+ errors,
345
+ explain: (color?: boolean) => new UpgradeableContractErrorReport(errors).explain(color),
346
+ };
347
+ },
348
+ };
349
+ }
350
+
351
+ async function validateBuild(
352
+ input: SolcInput,
353
+ output: SolcOutput,
354
+ compiler: BuildCompiler,
355
+ ): Promise<ValidationRunData> {
356
+ const solcVersion = compiler.longVersion.split("+")[0];
97
357
 
98
358
  let namespacedOutput: SolcOutput | undefined;
99
359
  if (isNamespaceSupported(solcVersion)) {
100
360
  let namespacedInput = makeNamespacedInput(input, output, solcVersion);
101
361
  namespacedInput = await trySanitizeNatSpec(namespacedInput, solcVersion);
102
362
  namespacedOutput = await compileOrThrow(
103
- solc,
363
+ compiler,
104
364
  withOutputSelection(namespacedInput, LAYOUT_SELECTION),
105
365
  "namespaced pass",
106
366
  );
107
367
  }
108
368
 
109
- const runData = validate(
369
+ return validate(
110
370
  output,
111
371
  solcInputOutputDecoder(input, output),
112
372
  solcVersion,
113
373
  input,
114
374
  namespacedOutput,
115
375
  );
116
- const version = runData[contract]?.version;
117
- if (version === undefined) {
118
- throw new LayoutIntegrityError(
119
- `${contract} compiled to no bytecode; cannot derive its layout.`,
120
- );
121
- }
122
- // OZ looks the contract up by bytecode version and takes the first match, so
123
- // two contracts with identical code (possible without a source hash) could
124
- // swap layouts. Listing the target first makes the lookup land on it.
125
- return getStorageLayout({ [contract]: runData[contract], ...runData }, version);
126
376
  }
127
377
 
128
378
  /**
@@ -200,7 +450,7 @@ function embedsSourceHash(input: SolcInput): boolean {
200
450
  }
201
451
 
202
452
  async function compileOrThrow(
203
- solc: SolcRunner,
453
+ solc: BuildCompiler,
204
454
  input: SolcInput,
205
455
  what: string,
206
456
  ): Promise<SolcOutput> {
@@ -208,7 +458,7 @@ async function compileOrThrow(
208
458
  const errors = (output.errors ?? []).filter((e) => e.severity === "error");
209
459
  if (errors.length > 0) {
210
460
  throw new LayoutIntegrityError(
211
- `solc ${solc.longVersion} failed to compile the ${what}: ${errors[0].formattedMessage}`,
461
+ `solc ${solc.longVersion} failed to compile the ${what}: ${printable(String(errors[0].formattedMessage))}`,
212
462
  );
213
463
  }
214
464
  return output;
package/src/record.ts CHANGED
@@ -11,6 +11,8 @@
11
11
  import type { StorageLayout } from "@openzeppelin/upgrades-core";
12
12
  import { isProvingMatch, type ProvingMatch } from "./bytecode.js";
13
13
  import { codeSha256, readChainId, readCode, type EthProvider } from "./chain.js";
14
+ import { slotsMisfit, type NamespaceSlot } from "./erc7201.js";
15
+ import type { SourceAnalysis } from "./reconstruct.js";
14
16
  import { LayoutIntegrityError, LayoutUnavailableError } from "./errors.js";
15
17
 
16
18
  /**
@@ -33,6 +35,17 @@ export interface LayoutRecord {
33
35
  source: "explorer" | "local-compile";
34
36
  recordedAt: string;
35
37
  layout: StorageLayout;
38
+ /**
39
+ * The proxy kind this code implies: `uups` when it has `upgradeTo` or
40
+ * `upgradeToAndCall`, else `transparent`. Pass `beacon` to the safety check
41
+ * instead when a beacon serves it. Absent in older records.
42
+ */
43
+ kind?: SourceAnalysis["kind"];
44
+ /**
45
+ * The slot each ERC-7201 namespace in `layout` really uses. Without it,
46
+ * `checkNamespaces` reports the record's namespaces as unread.
47
+ */
48
+ namespaces?: NamespaceSlot[];
36
49
  }
37
50
 
38
51
  const ADDRESS = /^0x[0-9a-f]{40}$/;
@@ -75,6 +88,11 @@ export function parseRecord(value: unknown, address?: string): LayoutRecord {
75
88
  ) {
76
89
  throw malformed("layout");
77
90
  }
91
+ if (r.kind !== undefined && r.kind !== "uups" && r.kind !== "transparent") {
92
+ throw malformed("kind");
93
+ }
94
+ if (slotsMisfit(r.layout, undefined) !== undefined) throw malformed("layout");
95
+ if (slotsMisfit(r.layout, r.namespaces) !== undefined) throw malformed("namespaces");
78
96
  if (typeof r.bytecodeMatch !== "string" || !isProvingMatch(r.bytecodeMatch)) {
79
97
  throw new LayoutIntegrityError(
80
98
  `The layout record for ${who} rests on a "${String(r.bytecodeMatch)}" match, which does ` +