slotdiff 0.1.0-alpha.1 → 0.2.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.
- package/CHANGELOG.md +16 -9
- package/README.md +196 -105
- 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 +363 -0
- package/dist/cli.js.map +1 -0
- package/dist/erc7201.d.ts +72 -0
- package/dist/erc7201.d.ts.map +1 -0
- package/dist/erc7201.js +340 -0
- package/dist/erc7201.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +8 -0
- package/dist/errors.js.map +1 -1
- package/dist/explorer.js +2 -2
- package/dist/explorer.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/reconstruct.d.ts +51 -3
- package/dist/reconstruct.d.ts.map +1 -1
- package/dist/reconstruct.js +37 -8
- package/dist/reconstruct.js.map +1 -1
- package/dist/report.d.ts +26 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +109 -0
- package/dist/report.js.map +1 -0
- package/dist/resolve.d.ts +11 -0
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +11 -1
- package/dist/resolve.js.map +1 -1
- package/package.json +6 -3
- package/src/bin.ts +25 -0
- package/src/build-info.ts +405 -0
- package/src/cli.ts +423 -0
- package/src/erc7201.ts +412 -0
- package/src/errors.ts +9 -0
- package/src/explorer.ts +2 -2
- package/src/index.ts +18 -1
- package/src/reconstruct.ts +111 -10
- package/src/report.ts +155 -0
- package/src/resolve.ts +17 -2
package/src/report.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
/**
|
|
40
|
+
* A grid for each namespace whose slot is in question on either side: the
|
|
41
|
+
* annotation, the slot it implies and the slot the code uses, for old and new,
|
|
42
|
+
* with a sentence saying what that means. Namespaces that line up are left out.
|
|
43
|
+
*/
|
|
44
|
+
export function namespaceLines(pairs: NamespacePair[], paint: Paint = plain): string[] {
|
|
45
|
+
const blocks = pairs
|
|
46
|
+
// A side that is absent is not a problem here; OpenZeppelin reports a deleted namespace.
|
|
47
|
+
.filter(
|
|
48
|
+
(p) =>
|
|
49
|
+
p.problem !== undefined ||
|
|
50
|
+
[p.old, p.next].some((s) => s !== undefined && s.status !== "match"),
|
|
51
|
+
)
|
|
52
|
+
.map((p) => {
|
|
53
|
+
const fmt = formatter([p.old, p.next]);
|
|
54
|
+
return {
|
|
55
|
+
rows: [
|
|
56
|
+
...(p.old !== undefined ? [row("old", p.old, fmt)] : []),
|
|
57
|
+
...(p.next !== undefined ? [row("new", p.next, fmt)] : []),
|
|
58
|
+
],
|
|
59
|
+
meaning: meaning(p, fmt),
|
|
60
|
+
tone: (p.problem === undefined
|
|
61
|
+
? "dim"
|
|
62
|
+
: p.problem === "unread"
|
|
63
|
+
? "yellow"
|
|
64
|
+
: "red") as Row["tone"],
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
if (blocks.length === 0) return [];
|
|
68
|
+
|
|
69
|
+
const header = {
|
|
70
|
+
side: "",
|
|
71
|
+
id: "annotation",
|
|
72
|
+
implied: "slot from annotation",
|
|
73
|
+
inCode: "slot in code",
|
|
74
|
+
mark: "",
|
|
75
|
+
tone: "dim",
|
|
76
|
+
} as Row;
|
|
77
|
+
const all = [header, ...blocks.flatMap((b) => b.rows)];
|
|
78
|
+
const width = (key: "id" | "implied" | "inCode") => Math.max(...all.map((r) => r[key].length));
|
|
79
|
+
const cells = (r: Row) =>
|
|
80
|
+
` ${r.side.padEnd(5)}${r.id.padEnd(width("id"))} ${r.implied.padEnd(width("implied"))} ` +
|
|
81
|
+
r.inCode.padEnd(width("inCode"));
|
|
82
|
+
const line = (r: Row) => `${cells(r)} ${paint(r.tone, r.mark)}`;
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
paint("bold", "ERC-7201 namespaces"),
|
|
86
|
+
paint("dim", cells(header).trimEnd()),
|
|
87
|
+
...blocks.flatMap((b) => [...b.rows.map(line), ` ${paint(b.tone, b.meaning)}`]),
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface Row {
|
|
92
|
+
side: string;
|
|
93
|
+
id: string;
|
|
94
|
+
implied: string;
|
|
95
|
+
inCode: string;
|
|
96
|
+
mark: string;
|
|
97
|
+
tone: "green" | "red" | "yellow" | "dim";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function row(side: string, s: NamespaceSlot, fmt: (slot: string) => string): Row {
|
|
101
|
+
return {
|
|
102
|
+
side,
|
|
103
|
+
id: s.namespace.slice("erc7201:".length),
|
|
104
|
+
implied: fmt(s.expected),
|
|
105
|
+
inCode:
|
|
106
|
+
s.status === "conflict" ? "several" : s.actual !== undefined ? fmt(s.actual) : "not read",
|
|
107
|
+
mark: {
|
|
108
|
+
match: "✓",
|
|
109
|
+
mismatch: "✗ annotation does not match code",
|
|
110
|
+
conflict: "✗ code uses more than one slot",
|
|
111
|
+
unread: "? slot in code not read",
|
|
112
|
+
}[s.status],
|
|
113
|
+
tone: ({ match: "green", mismatch: "red", conflict: "red", unread: "yellow" } as const)[
|
|
114
|
+
s.status
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function meaning({ old, next, problem }: NamespacePair, fmt: (slot: string) => string): string {
|
|
120
|
+
switch (problem) {
|
|
121
|
+
case "conflict":
|
|
122
|
+
return "The new code assigns more than one slot to this namespace; it must use one.";
|
|
123
|
+
case "moved":
|
|
124
|
+
return (
|
|
125
|
+
`The new code moves this namespace from ${fmt(old!.actual!)} to ${fmt(next!.actual!)}: ` +
|
|
126
|
+
"its data would be left behind. Keep the slot the deployed code uses, under an annotation that matches it."
|
|
127
|
+
);
|
|
128
|
+
case "mismatch":
|
|
129
|
+
return old === undefined
|
|
130
|
+
? "The new code keeps this namespace in a different slot than its annotation says. Fix one of the two before deploying."
|
|
131
|
+
: "The storage did not move, but the new annotation does not match its code. Fix it before deploying.";
|
|
132
|
+
case "unread":
|
|
133
|
+
return "The slot in code could not be read (computed at run time?), so this namespace could not be checked.";
|
|
134
|
+
}
|
|
135
|
+
return next !== undefined
|
|
136
|
+
? "Same slot in code on both sides: the storage did not move. Compared as one namespace."
|
|
137
|
+
: "The deployed annotation does not match its code, and no namespace in the new code uses its slot.";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Short slots for a block, unless two different slots in it would then look
|
|
142
|
+
* the same: a hand-written constant can share both ends with another.
|
|
143
|
+
*/
|
|
144
|
+
function formatter(sides: Array<NamespaceSlot | undefined>): (slot: string) => string {
|
|
145
|
+
const slots = new Set(
|
|
146
|
+
sides.flatMap((s) => (s === undefined ? [] : [s.expected, s.actual ?? s.expected])),
|
|
147
|
+
);
|
|
148
|
+
const shorts = new Set([...slots].map(short));
|
|
149
|
+
return shorts.size === slots.size ? short : (slot) => slot;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `0x5ea2…5c00`: enough to tell slots apart; slotdiff does the comparing. */
|
|
153
|
+
function short(slot: string): string {
|
|
154
|
+
return `${slot.slice(0, 6)}…${slot.slice(-4)}`;
|
|
155
|
+
}
|
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 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,
|
|
@@ -61,6 +75,7 @@ export async function resolveLayout(
|
|
|
61
75
|
recordedAt: new Date().toISOString(),
|
|
62
76
|
layout: rebuilt.layout,
|
|
63
77
|
};
|
|
78
|
+
return { record, analysis: rebuilt };
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
export interface ProxyLayout {
|