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/report.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's report: the ERC-7201 namespace grid and the check list. Pure
|
|
3
|
+
* functions from results to lines, so the layout is tested as text. Colors
|
|
4
|
+
* come from `paint`, applied after alignment so they never shift a column.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { NamespacePair, NamespaceSlot } from "./erc7201.js";
|
|
8
|
+
|
|
9
|
+
/** Node's `util.styleText`, or `plain` when colors are off. */
|
|
10
|
+
export type Paint = (
|
|
11
|
+
format: Parameters<typeof import("node:util").styleText>[0],
|
|
12
|
+
text: string,
|
|
13
|
+
) => string;
|
|
14
|
+
|
|
15
|
+
export const plain: Paint = (_, text) => text;
|
|
16
|
+
|
|
17
|
+
export type CheckStatus = "pass" | "fail" | "skip";
|
|
18
|
+
|
|
19
|
+
export interface Check {
|
|
20
|
+
name: string;
|
|
21
|
+
status: CheckStatus;
|
|
22
|
+
summary: string;
|
|
23
|
+
/** Multi-line explanation shown under the check, such as OpenZeppelin's report. */
|
|
24
|
+
details?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MARK: Record<CheckStatus, string> = { pass: "✓", fail: "✗", skip: "-" };
|
|
28
|
+
const TONE = { pass: "green", fail: "red", skip: "yellow" } as const;
|
|
29
|
+
|
|
30
|
+
/** One line per check, names aligned, details indented under their check. */
|
|
31
|
+
export function checkLines(checks: Check[], paint: Paint = plain): string[] {
|
|
32
|
+
const width = Math.max(...checks.map((c) => c.name.length));
|
|
33
|
+
return checks.flatMap((c) => [
|
|
34
|
+
`${paint(TONE[c.status], MARK[c.status])} ${paint("bold", c.name.padEnd(width))} ${c.summary}`,
|
|
35
|
+
...(c.details?.split("\n").map((line) => ` ${line}`) ?? []),
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A pair with nothing in question: no problem, and each side matches its
|
|
40
|
+
// annotation or is not assigned at all, except data the new code stops assigning.
|
|
41
|
+
// A side that is absent is not a problem here; OpenZeppelin reports a deleted namespace.
|
|
42
|
+
function quiet({ old, next, problem }: NamespacePair): boolean {
|
|
43
|
+
const plain = (s?: NamespaceSlot) =>
|
|
44
|
+
s === undefined || s.status === "match" || s.status === "unused";
|
|
45
|
+
const dropped = old?.actual !== undefined && next?.status === "unused";
|
|
46
|
+
return problem === undefined && plain(old) && plain(next) && !dropped;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** How many namespaces are compared by annotation alone because no code assigns them. */
|
|
50
|
+
export function unassignedCount(pairs: NamespacePair[]): number {
|
|
51
|
+
return pairs.filter((p) => quiet(p) && [p.old, p.next].some((s) => s?.status === "unused"))
|
|
52
|
+
.length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A grid for each namespace whose slot is in question on either side: the
|
|
57
|
+
* annotation, the slot it implies and the slot the code uses, for old and new,
|
|
58
|
+
* with a sentence saying what that means. Namespaces that line up, or that no
|
|
59
|
+
* code assigns on either side, are left out.
|
|
60
|
+
*/
|
|
61
|
+
export function namespaceLines(pairs: NamespacePair[], paint: Paint = plain): string[] {
|
|
62
|
+
const blocks = pairs
|
|
63
|
+
.filter((p) => !quiet(p))
|
|
64
|
+
.map((p) => {
|
|
65
|
+
const fmt = formatter([p.old, p.next]);
|
|
66
|
+
return {
|
|
67
|
+
rows: [
|
|
68
|
+
...(p.old !== undefined ? [row("old", p.old, fmt)] : []),
|
|
69
|
+
...(p.next !== undefined ? [row("new", p.next, fmt)] : []),
|
|
70
|
+
],
|
|
71
|
+
meaning: meaning(p, fmt),
|
|
72
|
+
tone: (p.problem === undefined
|
|
73
|
+
? "dim"
|
|
74
|
+
: p.problem === "unread"
|
|
75
|
+
? "yellow"
|
|
76
|
+
: "red") as Row["tone"],
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
if (blocks.length === 0) return [];
|
|
80
|
+
|
|
81
|
+
const header = {
|
|
82
|
+
side: "",
|
|
83
|
+
id: "annotation",
|
|
84
|
+
implied: "slot from annotation",
|
|
85
|
+
inCode: "slot in code",
|
|
86
|
+
mark: "",
|
|
87
|
+
tone: "dim",
|
|
88
|
+
} as Row;
|
|
89
|
+
const all = [header, ...blocks.flatMap((b) => b.rows)];
|
|
90
|
+
const width = (key: "id" | "implied" | "inCode") => Math.max(...all.map((r) => r[key].length));
|
|
91
|
+
const cells = (r: Row) =>
|
|
92
|
+
` ${r.side.padEnd(5)}${r.id.padEnd(width("id"))} ${r.implied.padEnd(width("implied"))} ` +
|
|
93
|
+
r.inCode.padEnd(width("inCode"));
|
|
94
|
+
const line = (r: Row) => `${cells(r)} ${paint(r.tone, r.mark)}`;
|
|
95
|
+
|
|
96
|
+
return [
|
|
97
|
+
paint("bold", "ERC-7201 namespaces"),
|
|
98
|
+
paint("dim", cells(header).trimEnd()),
|
|
99
|
+
...blocks.flatMap((b) => [...b.rows.map(line), ` ${paint(b.tone, b.meaning)}`]),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface Row {
|
|
104
|
+
side: string;
|
|
105
|
+
id: string;
|
|
106
|
+
implied: string;
|
|
107
|
+
inCode: string;
|
|
108
|
+
mark: string;
|
|
109
|
+
tone: "green" | "red" | "yellow" | "dim";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function row(side: string, s: NamespaceSlot, fmt: (slot: string) => string): Row {
|
|
113
|
+
return {
|
|
114
|
+
side,
|
|
115
|
+
id: s.namespace.slice("erc7201:".length),
|
|
116
|
+
implied: fmt(s.expected),
|
|
117
|
+
inCode:
|
|
118
|
+
s.status === "conflict"
|
|
119
|
+
? "several"
|
|
120
|
+
: s.status === "unused"
|
|
121
|
+
? "none"
|
|
122
|
+
: s.actual !== undefined
|
|
123
|
+
? fmt(s.actual)
|
|
124
|
+
: "not read",
|
|
125
|
+
mark: {
|
|
126
|
+
match: "✓",
|
|
127
|
+
mismatch: "✗ annotation does not match code",
|
|
128
|
+
conflict: "✗ code uses more than one slot",
|
|
129
|
+
unused: "· not assigned in code",
|
|
130
|
+
unread: "? slot in code not read",
|
|
131
|
+
}[s.status],
|
|
132
|
+
tone: (
|
|
133
|
+
{ match: "green", mismatch: "red", conflict: "red", unused: "dim", unread: "yellow" } as const
|
|
134
|
+
)[s.status],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function meaning({ old, next, problem }: NamespacePair, fmt: (slot: string) => string): string {
|
|
139
|
+
switch (problem) {
|
|
140
|
+
case "conflict":
|
|
141
|
+
return "The new code assigns more than one slot to this namespace; it must use one.";
|
|
142
|
+
case "moved":
|
|
143
|
+
return (
|
|
144
|
+
`The new code moves this namespace from ${fmt(old!.actual!)} to ${fmt(next!.actual!)}: ` +
|
|
145
|
+
"its data would be left behind. Keep the slot the deployed code uses, under an annotation that matches it."
|
|
146
|
+
);
|
|
147
|
+
case "mismatch":
|
|
148
|
+
return old === undefined || old.status === "unused"
|
|
149
|
+
? "The new code keeps this namespace in a different slot than its annotation says. Fix one of the two before deploying."
|
|
150
|
+
: "The storage did not move, but the new annotation does not match its code. Fix it before deploying.";
|
|
151
|
+
case "dropped":
|
|
152
|
+
return (
|
|
153
|
+
`The new code stops assigning this namespace while its data is at ${fmt(old!.actual!)}, ` +
|
|
154
|
+
`not at ${fmt(old!.expected)} where its annotation points: a later version that follows ` +
|
|
155
|
+
"the annotation would miss it. Correct the annotation to match the data first."
|
|
156
|
+
);
|
|
157
|
+
case "unread":
|
|
158
|
+
return old !== undefined && old.actual === undefined && old.status !== "unused"
|
|
159
|
+
? "The old slot is not known (not read from its code, or missing from an older record), so this namespace could not be checked."
|
|
160
|
+
: "The slot in the new code could not be read (set from a value slotdiff does not evaluate), so this namespace could not be checked.";
|
|
161
|
+
}
|
|
162
|
+
// The only unused case shown without a problem: the new code stops assigning old data.
|
|
163
|
+
if (next?.status === "unused" && old?.actual !== undefined) {
|
|
164
|
+
return `The new code no longer assigns this namespace; its data stays at ${fmt(old.actual)}.`;
|
|
165
|
+
}
|
|
166
|
+
return next !== undefined
|
|
167
|
+
? "Same slot in code on both sides: the storage did not move. Compared as one namespace."
|
|
168
|
+
: "The deployed annotation does not match its code, and no namespace in the new code uses its slot.";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Short slots for a block, unless two different slots in it would then look
|
|
173
|
+
* the same: a hand-written constant can share both ends with another.
|
|
174
|
+
*/
|
|
175
|
+
function formatter(sides: Array<NamespaceSlot | undefined>): (slot: string) => string {
|
|
176
|
+
const slots = new Set(
|
|
177
|
+
sides.flatMap((s) => (s === undefined ? [] : [s.expected, s.actual ?? s.expected])),
|
|
178
|
+
);
|
|
179
|
+
const shorts = new Set([...slots].map(short));
|
|
180
|
+
return shorts.size === slots.size ? short : (slot) => slot;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** `0x5ea2…5c00`: enough to tell slots apart; slotdiff does the comparing. */
|
|
184
|
+
function short(slot: string): string {
|
|
185
|
+
return `${slot.slice(0, 6)}…${slot.slice(-4)}`;
|
|
186
|
+
}
|
package/src/resolve.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { codeSha256, readChainId, readCode, readProxy, type EthProvider } from "./chain.js";
|
|
19
19
|
import { LayoutUnavailableError } from "./errors.js";
|
|
20
20
|
import { canQueryExplorer, fetchVerifiedSource, type ExplorerConfig } from "./explorer.js";
|
|
21
|
-
import { reconstructLayout } from "./reconstruct.js";
|
|
21
|
+
import { reconstructLayout, type ProxyKind, type ReconstructedLayout } from "./reconstruct.js";
|
|
22
22
|
import { parseRecord, type LayoutRecord } from "./record.js";
|
|
23
23
|
import { getSolc, type SolcOptions } from "./solc.js";
|
|
24
24
|
|
|
@@ -37,6 +37,20 @@ export async function resolveLayout(
|
|
|
37
37
|
address: string,
|
|
38
38
|
options: ResolveOptions,
|
|
39
39
|
): Promise<LayoutRecord> {
|
|
40
|
+
return (await rebuild(provider, address, options)).record;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `resolveLayout` plus the analysis of the verified source: the proxy kind,
|
|
45
|
+
* inferred from the code as OpenZeppelin recommends (not from the admin slot,
|
|
46
|
+
* which a UUPS proxy can have set), the ERC-7201 slots and the safety check.
|
|
47
|
+
* Internal, for the CLI.
|
|
48
|
+
*/
|
|
49
|
+
export async function rebuild(
|
|
50
|
+
provider: EthProvider,
|
|
51
|
+
address: string,
|
|
52
|
+
options: ResolveOptions,
|
|
53
|
+
): Promise<{ record: LayoutRecord; analysis: ReconstructedLayout }> {
|
|
40
54
|
const lower = address.toLowerCase();
|
|
41
55
|
if (!canQueryExplorer(options.explorer)) {
|
|
42
56
|
throw new LayoutUnavailableError(
|
|
@@ -49,7 +63,7 @@ export async function resolveLayout(
|
|
|
49
63
|
const solc = await getSolc(source.solcLongVersion, options.solc);
|
|
50
64
|
const rebuilt = await reconstructLayout(source, solc, code);
|
|
51
65
|
|
|
52
|
-
|
|
66
|
+
const record: LayoutRecord = {
|
|
53
67
|
format: 1,
|
|
54
68
|
address: lower,
|
|
55
69
|
chainId,
|
|
@@ -60,7 +74,10 @@ export async function resolveLayout(
|
|
|
60
74
|
source: "explorer",
|
|
61
75
|
recordedAt: new Date().toISOString(),
|
|
62
76
|
layout: rebuilt.layout,
|
|
77
|
+
kind: rebuilt.kind,
|
|
78
|
+
namespaces: rebuilt.namespaces,
|
|
63
79
|
};
|
|
80
|
+
return { record, analysis: rebuilt };
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
export interface ProxyLayout {
|
|
@@ -68,6 +85,8 @@ export interface ProxyLayout {
|
|
|
68
85
|
implementation: string;
|
|
69
86
|
/** Set for a beacon proxy. */
|
|
70
87
|
beacon?: string;
|
|
88
|
+
/** The proxy's kind, for `checkUpgrade`: `beacon` behind a beacon, else the record's. */
|
|
89
|
+
kind: ProxyKind;
|
|
71
90
|
record: LayoutRecord;
|
|
72
91
|
}
|
|
73
92
|
|
|
@@ -84,7 +103,8 @@ export async function resolveProxyLayout(
|
|
|
84
103
|
);
|
|
85
104
|
}
|
|
86
105
|
const record = await resolveLayout(provider, state.implementation, options);
|
|
87
|
-
|
|
106
|
+
// A rebuilt record always has its kind.
|
|
107
|
+
return { ...state, kind: state.beacon !== undefined ? "beacon" : record.kind!, record };
|
|
88
108
|
}
|
|
89
109
|
|
|
90
110
|
export interface LocalBuild {
|
|
@@ -94,6 +114,9 @@ export interface LocalBuild {
|
|
|
94
114
|
layout: StorageLayout;
|
|
95
115
|
deployedBytecode: string;
|
|
96
116
|
immutableReferences?: ImmutableReferences;
|
|
117
|
+
/** From `analyzeSource`, so the record carries them (see `LayoutRecord`). */
|
|
118
|
+
kind?: LayoutRecord["kind"];
|
|
119
|
+
namespaces?: LayoutRecord["namespaces"];
|
|
97
120
|
}
|
|
98
121
|
|
|
99
122
|
export interface LocalBuildResult {
|
|
@@ -132,6 +155,8 @@ export async function proveLocalBuild(
|
|
|
132
155
|
source: "local-compile",
|
|
133
156
|
recordedAt: new Date().toISOString(),
|
|
134
157
|
layout: build.layout,
|
|
158
|
+
...(build.kind !== undefined ? { kind: build.kind } : {}),
|
|
159
|
+
...(build.namespaces !== undefined ? { namespaces: build.namespaces } : {}),
|
|
135
160
|
};
|
|
136
161
|
// A record this returns must load back through parseRecord, so check it now.
|
|
137
162
|
return { bytecodeMatch, record: parseRecord(JSON.parse(JSON.stringify(record)), lower) };
|
package/src/solc.ts
CHANGED
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
16
|
import { createHash, randomUUID } from "node:crypto";
|
|
17
|
-
import { access, chmod, mkdir, readdir, rename, writeFile } from "node:fs/promises";
|
|
17
|
+
import { access, chmod, mkdir, mkdtemp, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
18
18
|
import { constants } from "node:fs";
|
|
19
|
-
import { homedir } from "node:os";
|
|
19
|
+
import { homedir, tmpdir } from "node:os";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import type { SolcInput, SolcOutput } from "@openzeppelin/upgrades-core";
|
|
22
22
|
import { LayoutIntegrityError, LayoutUnavailableError } from "./errors.js";
|
|
@@ -169,6 +169,10 @@ async function findBuild(
|
|
|
169
169
|
if (build === undefined) {
|
|
170
170
|
throw new LayoutUnavailableError(`solc ${version} has no native ${platform} build.`);
|
|
171
171
|
}
|
|
172
|
+
// Both become paths: a release is matched by prefix, so its long version comes from the list.
|
|
173
|
+
if (!SOLC_VERSION.test(build.longVersion) || !/^[\w.+-]+$/.test(build.path)) {
|
|
174
|
+
throw new LayoutUnavailableError(`The solc build list has a malformed entry for ${version}.`);
|
|
175
|
+
}
|
|
172
176
|
// Old Windows releases (up to 0.7.1) ship as zip archives, not executables.
|
|
173
177
|
if (build.path.endsWith(".zip")) {
|
|
174
178
|
throw new LayoutUnavailableError(
|
|
@@ -206,9 +210,21 @@ async function download(
|
|
|
206
210
|
await rename(tmp, dest);
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
|
|
213
|
+
// solc reads imports it is not given from its working directory, so it runs in
|
|
214
|
+
// an empty one: verified source could otherwise import, and quote in an error,
|
|
215
|
+
// any file under the caller's (an .env, say).
|
|
216
|
+
async function runSolc(path: string, input: SolcInput): Promise<SolcOutput> {
|
|
217
|
+
const cwd = await mkdtemp(join(tmpdir(), "slotdiff-solc-"));
|
|
218
|
+
try {
|
|
219
|
+
return await runSolcIn(cwd, path, input);
|
|
220
|
+
} finally {
|
|
221
|
+
await rm(cwd, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function runSolcIn(cwd: string, path: string, input: SolcInput): Promise<SolcOutput> {
|
|
210
226
|
return new Promise((resolve, reject) => {
|
|
211
|
-
const child = spawn(path, ["--standard-json"], { stdio: ["pipe", "pipe", "pipe"] });
|
|
227
|
+
const child = spawn(path, ["--standard-json"], { cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
212
228
|
const out: Buffer[] = [];
|
|
213
229
|
const err: Buffer[] = [];
|
|
214
230
|
child.stdout.on("data", (d: Buffer) => out.push(d));
|
package/src/upgrade.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole upgrade check in one call, as the CLI runs it: the storage layout,
|
|
3
|
+
* the ERC-7201 slots and OpenZeppelin's safety rules, with one verdict.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { StorageLayout } from "@openzeppelin/upgrades-core";
|
|
7
|
+
import { diffLayouts, type DiffOptions, type LayoutReport } from "./diff.js";
|
|
8
|
+
import { checkNamespaces, type NamespaceCheck, type NamespaceSlot } from "./erc7201.js";
|
|
9
|
+
import type { ProxyKind, SafetyErrorKind, SafetyReport, SourceAnalysis } from "./reconstruct.js";
|
|
10
|
+
|
|
11
|
+
export interface UpgradeOptions extends DiffOptions {
|
|
12
|
+
/** Error kinds to accept anyway (OZ's `unsafeAllow`). Prefer NatSpec exceptions in the source. */
|
|
13
|
+
unsafeAllow?: SafetyErrorKind[];
|
|
14
|
+
/** Skip the safety rules; the layout and slots are still checked. */
|
|
15
|
+
skipSafety?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A layout with the slot of each namespace, such as a `LayoutRecord` or a `SourceAnalysis`. */
|
|
19
|
+
export interface UpgradeSide {
|
|
20
|
+
layout: StorageLayout;
|
|
21
|
+
/** Without them, the namespaces are unchecked. */
|
|
22
|
+
namespaces?: NamespaceSlot[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UpgradeCheck {
|
|
26
|
+
/** OpenZeppelin's comparison, with each namespace paired by the slot in code. */
|
|
27
|
+
layout: LayoutReport;
|
|
28
|
+
namespaces: NamespaceCheck;
|
|
29
|
+
/** Absent with `skipSafety`: then `pass` covers the layout and slots only. */
|
|
30
|
+
safety?: SafetyReport;
|
|
31
|
+
/**
|
|
32
|
+
* `fail`: a check failed. `unchecked`: none failed, but an ERC-7201 slot
|
|
33
|
+
* could not be read (`unread` in `namespaces`). `pass`: every check that ran passed.
|
|
34
|
+
*/
|
|
35
|
+
result: "pass" | "fail" | "unchecked";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PROXY_KINDS: readonly string[] = ["uups", "transparent", "beacon"];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether `next` can replace `old` behind a proxy. `old.kind` is the kind of
|
|
42
|
+
* the proxy being upgraded (`beacon` when a beacon serves it): taking it from
|
|
43
|
+
* `next` would let a UUPS implementation that lost its upgrade function pass.
|
|
44
|
+
* `old` may be a `SourceAnalysis`, or a `LayoutRecord` with its `kind` given.
|
|
45
|
+
* With `skipSafety`, `next` may be a record too, and `old.kind` is not needed.
|
|
46
|
+
*/
|
|
47
|
+
export function checkUpgrade(
|
|
48
|
+
old: UpgradeSide & { kind: ProxyKind },
|
|
49
|
+
next: SourceAnalysis,
|
|
50
|
+
options?: UpgradeOptions,
|
|
51
|
+
): UpgradeCheck;
|
|
52
|
+
export function checkUpgrade(
|
|
53
|
+
old: UpgradeSide & { kind?: ProxyKind },
|
|
54
|
+
next: UpgradeSide,
|
|
55
|
+
options: UpgradeOptions & { skipSafety: true },
|
|
56
|
+
): UpgradeCheck;
|
|
57
|
+
export function checkUpgrade(
|
|
58
|
+
old: UpgradeSide & { kind?: ProxyKind },
|
|
59
|
+
next: UpgradeSide & { safety?: SourceAnalysis["safety"] },
|
|
60
|
+
options: UpgradeOptions = {},
|
|
61
|
+
): UpgradeCheck {
|
|
62
|
+
if (!options.skipSafety) {
|
|
63
|
+
// Checked here for JavaScript callers: OpenZeppelin treats an unknown kind as uups.
|
|
64
|
+
if (!PROXY_KINDS.includes(old.kind as string)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`checkUpgrade needs the kind of the proxy being upgraded (${PROXY_KINDS.join(", ")}), ` +
|
|
67
|
+
`got ${JSON.stringify(old.kind)}.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (typeof next.safety !== "function") {
|
|
71
|
+
throw new Error("checkUpgrade needs an analysis of next (analyzeSource), or skipSafety.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// OpenZeppelin matches namespaces by annotation; pairing them by slot first
|
|
75
|
+
// makes it compare the structs of the same storage.
|
|
76
|
+
const namespaces = checkNamespaces(old, next);
|
|
77
|
+
const layout = diffLayouts(namespaces.layout, next.layout, {
|
|
78
|
+
unsafeAllowRenames: options.unsafeAllowRenames,
|
|
79
|
+
});
|
|
80
|
+
const safety = options.skipSafety
|
|
81
|
+
? undefined
|
|
82
|
+
: next.safety!({ kind: old.kind!, unsafeAllow: options.unsafeAllow });
|
|
83
|
+
// A failure outranks an unread slot, so a known problem is never reported as merely unchecked.
|
|
84
|
+
const result =
|
|
85
|
+
!layout.ok || namespaces.result === "fail" || safety?.ok === false
|
|
86
|
+
? "fail"
|
|
87
|
+
: namespaces.result === "unchecked"
|
|
88
|
+
? "unchecked"
|
|
89
|
+
: "pass";
|
|
90
|
+
return { layout, namespaces, ...(safety !== undefined ? { safety } : {}), result };
|
|
91
|
+
}
|