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.
- package/CHANGELOG.md +50 -9
- package/README.md +219 -111
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +22 -0
- package/dist/bin.js.map +1 -0
- package/dist/build-info.d.ts +45 -0
- package/dist/build-info.d.ts.map +1 -0
- package/dist/build-info.js +319 -0
- package/dist/build-info.js.map +1 -0
- package/dist/cli.d.ts +25 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +389 -0
- package/dist/cli.js.map +1 -0
- package/dist/erc7201.d.ts +88 -0
- package/dist/erc7201.d.ts.map +1 -0
- package/dist/erc7201.js +509 -0
- package/dist/erc7201.js.map +1 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -1
- package/dist/explorer.d.ts.map +1 -1
- package/dist/explorer.js +29 -10
- package/dist/explorer.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/reconstruct.d.ts +70 -7
- package/dist/reconstruct.d.ts.map +1 -1
- package/dist/reconstruct.js +157 -20
- package/dist/reconstruct.js.map +1 -1
- package/dist/record.d.ts +13 -0
- package/dist/record.d.ts.map +1 -1
- package/dist/record.js +8 -0
- package/dist/record.js.map +1 -1
- package/dist/report.d.ts +29 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +138 -0
- package/dist/report.js.map +1 -0
- package/dist/resolve.d.ts +16 -0
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +17 -2
- package/dist/resolve.js.map +1 -1
- package/dist/solc.js +20 -4
- package/dist/solc.js.map +1 -1
- package/dist/upgrade.d.ts +48 -0
- package/dist/upgrade.d.ts.map +1 -0
- package/dist/upgrade.js +36 -0
- package/dist/upgrade.js.map +1 -0
- package/package.json +9 -6
- package/src/bin.ts +25 -0
- package/src/build-info.ts +405 -0
- package/src/cli.ts +459 -0
- package/src/erc7201.ts +603 -0
- package/src/errors.ts +31 -0
- package/src/explorer.ts +40 -11
- package/src/index.ts +28 -3
- package/src/reconstruct.ts +279 -29
- package/src/record.ts +18 -0
- package/src/report.ts +186 -0
- package/src/resolve.ts +28 -3
- package/src/solc.ts +20 -4
- 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
|
|
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
|
-
|
|
119
|
-
|
|
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}";
|
|
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
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 {
|
|
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";
|
package/src/reconstruct.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
|
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:
|
|
158
|
+
solc: BuildCompiler,
|
|
83
159
|
contract: string,
|
|
84
|
-
): Promise<
|
|
160
|
+
): Promise<SourceAnalysis> {
|
|
85
161
|
const selected = withOutputSelection(input, FULL_SELECTION);
|
|
86
162
|
const output = await compileOrThrow(solc, selected, "source");
|
|
87
|
-
return
|
|
163
|
+
return (await analyzeBuild(selected, output, solc)).contract(contract);
|
|
88
164
|
}
|
|
89
165
|
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
):
|
|
96
|
-
const
|
|
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
|
-
|
|
363
|
+
compiler,
|
|
104
364
|
withOutputSelection(namespacedInput, LAYOUT_SELECTION),
|
|
105
365
|
"namespaced pass",
|
|
106
366
|
);
|
|
107
367
|
}
|
|
108
368
|
|
|
109
|
-
|
|
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:
|
|
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 ` +
|