diamond-detect 0.1.0 → 0.2.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/dist/cli.js +540 -115
- package/dist/index.js +496 -44
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -72,6 +72,18 @@ function extractSourcePath(artifactPath, parsed) {
|
|
|
72
72
|
}
|
|
73
73
|
return path.basename(path.dirname(artifactPath));
|
|
74
74
|
}
|
|
75
|
+
async function loadRawSources(inputPath, sourcePaths) {
|
|
76
|
+
const { root } = await resolveFoundryRoot(inputPath);
|
|
77
|
+
const out = /* @__PURE__ */ new Map();
|
|
78
|
+
for (const sourcePath of new Set(sourcePaths)) {
|
|
79
|
+
if (out.has(sourcePath)) continue;
|
|
80
|
+
try {
|
|
81
|
+
out.set(sourcePath, await fs.readFile(path.join(root, sourcePath), "utf8"));
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
75
87
|
async function loadFoundryArtifacts(inputPath, opts = {}) {
|
|
76
88
|
const { outDir } = await resolveFoundryRoot(inputPath);
|
|
77
89
|
if (!await fileExists(outDir)) {
|
|
@@ -105,55 +117,58 @@ async function loadFoundryArtifacts(inputPath, opts = {}) {
|
|
|
105
117
|
return artifacts;
|
|
106
118
|
}
|
|
107
119
|
|
|
108
|
-
// src/detector/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
function buildIgnore(userGlobs, noDefault) {
|
|
123
|
-
const globs = [
|
|
124
|
-
...noDefault ? [] : DEFAULT_IGNORE_GLOBS,
|
|
125
|
-
...userGlobs ?? []
|
|
126
|
-
];
|
|
127
|
-
if (globs.length === 0) return void 0;
|
|
128
|
-
const patterns = compilePatterns(globs);
|
|
129
|
-
return (sourcePath) => patterns.some((p) => p.test(sourcePath));
|
|
120
|
+
// src/detector/analyzers/diamondStorage.ts
|
|
121
|
+
import { keccak_256 as keccak_2562 } from "@noble/hashes/sha3";
|
|
122
|
+
|
|
123
|
+
// src/lib/eip7201.ts
|
|
124
|
+
import { keccak_256 } from "@noble/hashes/sha3";
|
|
125
|
+
var MASK_LAST_BYTE = (() => {
|
|
126
|
+
const m = new Uint8Array(32).fill(255);
|
|
127
|
+
m[31] = 0;
|
|
128
|
+
return m;
|
|
129
|
+
})();
|
|
130
|
+
function utf8(s) {
|
|
131
|
+
return new TextEncoder().encode(s);
|
|
130
132
|
}
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
return
|
|
133
|
+
function toHex(bytes) {
|
|
134
|
+
let out = "0x";
|
|
135
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
136
|
+
return out;
|
|
135
137
|
}
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
};
|
|
145
|
-
const findings = [];
|
|
146
|
-
for (const analyzer of analyzers) {
|
|
147
|
-
const out = await analyzer.run(ctx);
|
|
148
|
-
findings.push(...out);
|
|
138
|
+
function subOne(bytes) {
|
|
139
|
+
const out = new Uint8Array(bytes);
|
|
140
|
+
for (let i = out.length - 1; i >= 0; i--) {
|
|
141
|
+
if (out[i] > 0) {
|
|
142
|
+
out[i] = out[i] - 1;
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
out[i] = 255;
|
|
149
146
|
}
|
|
150
|
-
return
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
function maskLastByte(bytes) {
|
|
150
|
+
const out = new Uint8Array(32);
|
|
151
|
+
for (let i = 0; i < 32; i++) out[i] = bytes[i] & MASK_LAST_BYTE[i];
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function erc7201Slot(namespaceId) {
|
|
155
|
+
const inner = keccak_256(utf8(namespaceId));
|
|
156
|
+
const decremented = subOne(inner);
|
|
157
|
+
const outer = keccak_256(decremented);
|
|
158
|
+
return toHex(maskLastByte(outer));
|
|
159
|
+
}
|
|
160
|
+
var ERC7201_PREFIX = "erc7201:";
|
|
161
|
+
function parseErc7201Annotation(text) {
|
|
162
|
+
const idx = text.indexOf(ERC7201_PREFIX);
|
|
163
|
+
if (idx === -1) return null;
|
|
164
|
+
const rest = text.slice(idx + ERC7201_PREFIX.length);
|
|
165
|
+
const match = rest.match(/^[A-Za-z0-9_.\-]+/);
|
|
166
|
+
return match ? match[0] : null;
|
|
151
167
|
}
|
|
152
168
|
|
|
153
169
|
// src/detector/analyzers/diamondStorage.ts
|
|
154
|
-
import { keccak_256 } from "@noble/hashes/sha3";
|
|
155
170
|
function keccak256Hex(input) {
|
|
156
|
-
const bytes =
|
|
171
|
+
const bytes = keccak_2562(new TextEncoder().encode(input));
|
|
157
172
|
let out = "0x";
|
|
158
173
|
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
159
174
|
return out;
|
|
@@ -176,6 +191,61 @@ function extractKeccakStringArg(value) {
|
|
|
176
191
|
if (arg.nodeType !== "Literal" || arg.kind !== "string") return null;
|
|
177
192
|
return typeof arg.value === "string" ? arg.value : null;
|
|
178
193
|
}
|
|
194
|
+
function extractBytes32HexLiteral(value) {
|
|
195
|
+
if (!value || typeof value !== "object") return null;
|
|
196
|
+
const v = value;
|
|
197
|
+
if (v.nodeType !== "Literal" || v.kind !== "number") return null;
|
|
198
|
+
if (typeof v.value !== "string") return null;
|
|
199
|
+
try {
|
|
200
|
+
const big = BigInt(v.value);
|
|
201
|
+
if (big < 0n) return null;
|
|
202
|
+
return "0x" + big.toString(16).padStart(64, "0");
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function findKeccakStringArg(node) {
|
|
208
|
+
if (!node || typeof node !== "object") return null;
|
|
209
|
+
const v = node;
|
|
210
|
+
if (v.nodeType !== "FunctionCall") return null;
|
|
211
|
+
const expr = v.expression;
|
|
212
|
+
if (expr?.nodeType !== "Identifier" || expr.name !== "keccak256") return null;
|
|
213
|
+
const args = v.arguments;
|
|
214
|
+
if (!Array.isArray(args) || args.length !== 1) return null;
|
|
215
|
+
const arg = args[0];
|
|
216
|
+
if (arg.nodeType !== "Literal" || arg.kind !== "string") return null;
|
|
217
|
+
return typeof arg.value === "string" ? arg.value : null;
|
|
218
|
+
}
|
|
219
|
+
function someNode(node, pred) {
|
|
220
|
+
if (!node || typeof node !== "object") return false;
|
|
221
|
+
const n = node;
|
|
222
|
+
if (typeof n.nodeType === "string" && pred(n)) return true;
|
|
223
|
+
for (const key of Object.keys(n)) {
|
|
224
|
+
const value = n[key];
|
|
225
|
+
if (Array.isArray(value)) {
|
|
226
|
+
if (value.some((c) => someNode(c, pred))) return true;
|
|
227
|
+
} else if (value && typeof value === "object") {
|
|
228
|
+
if (someNode(value, pred)) return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
function extractErc7201FormulaNamespace(value) {
|
|
234
|
+
const strings = [];
|
|
235
|
+
someNode(value, (n) => {
|
|
236
|
+
const s = findKeccakStringArg(n);
|
|
237
|
+
if (s !== null) strings.push(s);
|
|
238
|
+
return false;
|
|
239
|
+
});
|
|
240
|
+
if (strings.length !== 1) return null;
|
|
241
|
+
const hasSubOne = someNode(
|
|
242
|
+
value,
|
|
243
|
+
(n) => n.nodeType === "BinaryOperation" && n.operator === "-" && n.rightExpression?.nodeType === "Literal" && n.rightExpression.value === "1"
|
|
244
|
+
);
|
|
245
|
+
const hasMask = someNode(value, (n) => n.nodeType === "BinaryOperation" && n.operator === "&");
|
|
246
|
+
if (!hasSubOne || !hasMask) return null;
|
|
247
|
+
return strings[0];
|
|
248
|
+
}
|
|
179
249
|
function lineFromSrc(src, sourceText) {
|
|
180
250
|
if (typeof src !== "string" || !sourceText) return void 0;
|
|
181
251
|
const [startStr] = src.split(":");
|
|
@@ -217,6 +287,7 @@ function collectSlotConstants(ctx) {
|
|
|
217
287
|
if (!isBytes32Constant(node)) return;
|
|
218
288
|
const namespace = extractKeccakStringArg(node.value);
|
|
219
289
|
if (namespace === null) return;
|
|
290
|
+
const declarationId = typeof node.id === "number" ? node.id : -1;
|
|
220
291
|
const variableName = node.name ?? "<anon>";
|
|
221
292
|
const contract = declaringContract(parents) ?? artifact.contractName;
|
|
222
293
|
const src = node.src;
|
|
@@ -224,6 +295,7 @@ function collectSlotConstants(ctx) {
|
|
|
224
295
|
if (seen.has(dedupeKey)) return;
|
|
225
296
|
seen.add(dedupeKey);
|
|
226
297
|
out.push({
|
|
298
|
+
declarationId,
|
|
227
299
|
variableName,
|
|
228
300
|
namespace,
|
|
229
301
|
slot: keccak256Hex(namespace),
|
|
@@ -235,12 +307,195 @@ function collectSlotConstants(ctx) {
|
|
|
235
307
|
}
|
|
236
308
|
return out;
|
|
237
309
|
}
|
|
310
|
+
function collectLiteralSlotConstants(ctx) {
|
|
311
|
+
const seen = /* @__PURE__ */ new Set();
|
|
312
|
+
const out = [];
|
|
313
|
+
for (const artifact of ctx.artifacts) {
|
|
314
|
+
if (!artifact.ast) continue;
|
|
315
|
+
walkAst(artifact.ast, (node, parents) => {
|
|
316
|
+
if (!isBytes32Constant(node)) return;
|
|
317
|
+
if (extractKeccakStringArg(node.value) !== null) return;
|
|
318
|
+
const slot = extractBytes32HexLiteral(node.value);
|
|
319
|
+
if (slot === null) return;
|
|
320
|
+
const declarationId = typeof node.id === "number" ? node.id : -1;
|
|
321
|
+
const variableName = node.name ?? "<anon>";
|
|
322
|
+
const contract = declaringContract(parents) ?? artifact.contractName;
|
|
323
|
+
const src = node.src;
|
|
324
|
+
const dedupeKey = `${artifact.sourcePath}::${contract}::${variableName}::${src ?? ""}`;
|
|
325
|
+
if (seen.has(dedupeKey)) return;
|
|
326
|
+
seen.add(dedupeKey);
|
|
327
|
+
out.push({
|
|
328
|
+
declarationId,
|
|
329
|
+
variableName,
|
|
330
|
+
namespace: null,
|
|
331
|
+
slot,
|
|
332
|
+
contract,
|
|
333
|
+
sourcePath: artifact.sourcePath,
|
|
334
|
+
src
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
function collectFormulaSlotConstants(ctx) {
|
|
341
|
+
const seen = /* @__PURE__ */ new Set();
|
|
342
|
+
const out = [];
|
|
343
|
+
for (const artifact of ctx.artifacts) {
|
|
344
|
+
if (!artifact.ast) continue;
|
|
345
|
+
walkAst(artifact.ast, (node, parents) => {
|
|
346
|
+
if (!isBytes32Constant(node)) return;
|
|
347
|
+
if (extractKeccakStringArg(node.value) !== null) return;
|
|
348
|
+
const namespace = extractErc7201FormulaNamespace(node.value);
|
|
349
|
+
if (namespace === null) return;
|
|
350
|
+
const declarationId = typeof node.id === "number" ? node.id : -1;
|
|
351
|
+
const variableName = node.name ?? "<anon>";
|
|
352
|
+
const contract = declaringContract(parents) ?? artifact.contractName;
|
|
353
|
+
const src = node.src;
|
|
354
|
+
const dedupeKey = `${artifact.sourcePath}::${contract}::${variableName}::${src ?? ""}`;
|
|
355
|
+
if (seen.has(dedupeKey)) return;
|
|
356
|
+
seen.add(dedupeKey);
|
|
357
|
+
out.push({
|
|
358
|
+
declarationId,
|
|
359
|
+
variableName,
|
|
360
|
+
namespace,
|
|
361
|
+
slot: erc7201Slot(namespace),
|
|
362
|
+
contract,
|
|
363
|
+
sourcePath: artifact.sourcePath,
|
|
364
|
+
src
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
function collectAssemblyLiteralSlots(artifacts) {
|
|
371
|
+
const seen = /* @__PURE__ */ new Set();
|
|
372
|
+
const out = [];
|
|
373
|
+
for (const artifact of artifacts) {
|
|
374
|
+
if (!artifact.ast) continue;
|
|
375
|
+
walkAst(artifact.ast, (node, parents) => {
|
|
376
|
+
if (node.nodeType !== "YulAssignment") return;
|
|
377
|
+
const targets = node.variableNames;
|
|
378
|
+
const targetsSlot = Array.isArray(targets) && targets.some((t) => typeof t.name === "string" && t.name.endsWith(".slot"));
|
|
379
|
+
if (!targetsSlot) return;
|
|
380
|
+
const value = node.value;
|
|
381
|
+
if (value?.nodeType !== "YulLiteral" || value.kind !== "number") return;
|
|
382
|
+
if (typeof value.value !== "string") return;
|
|
383
|
+
let slot;
|
|
384
|
+
try {
|
|
385
|
+
const big = BigInt(value.value);
|
|
386
|
+
if (big < 0n) return;
|
|
387
|
+
slot = "0x" + big.toString(16).padStart(64, "0");
|
|
388
|
+
} catch {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const contract = declaringContract(parents) ?? artifact.contractName;
|
|
392
|
+
const src = node.src;
|
|
393
|
+
const dedupeKey = `${artifact.sourcePath}::${contract}::${slot}::${src ?? ""}`;
|
|
394
|
+
if (seen.has(dedupeKey)) return;
|
|
395
|
+
seen.add(dedupeKey);
|
|
396
|
+
out.push({
|
|
397
|
+
declarationId: -1,
|
|
398
|
+
variableName: "<assembly literal>",
|
|
399
|
+
namespace: null,
|
|
400
|
+
slot,
|
|
401
|
+
contract,
|
|
402
|
+
sourcePath: artifact.sourcePath,
|
|
403
|
+
src
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
function collectSlotAssignmentValueSrcs(yulNode, srcs) {
|
|
410
|
+
if (!yulNode || typeof yulNode !== "object") return;
|
|
411
|
+
const node = yulNode;
|
|
412
|
+
if (node.nodeType === "YulAssignment") {
|
|
413
|
+
const targets = node.variableNames;
|
|
414
|
+
const targetsSlot = Array.isArray(targets) && targets.some((t) => typeof t.name === "string" && t.name.endsWith(".slot"));
|
|
415
|
+
if (targetsSlot) {
|
|
416
|
+
const value = node.value;
|
|
417
|
+
if (value?.nodeType === "YulIdentifier" && typeof value.src === "string") {
|
|
418
|
+
srcs.add(value.src);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (const key of Object.keys(node)) {
|
|
423
|
+
const v = node[key];
|
|
424
|
+
if (Array.isArray(v)) {
|
|
425
|
+
for (const child of v) collectSlotAssignmentValueSrcs(child, srcs);
|
|
426
|
+
} else if (v && typeof v === "object") {
|
|
427
|
+
collectSlotAssignmentValueSrcs(v, srcs);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function collectSlotUsedDeclarationIds(artifacts) {
|
|
432
|
+
const ids = /* @__PURE__ */ new Set();
|
|
433
|
+
for (const artifact of artifacts) {
|
|
434
|
+
if (!artifact.ast) continue;
|
|
435
|
+
walkAst(artifact.ast, (node) => {
|
|
436
|
+
if (node.nodeType !== "InlineAssembly") return;
|
|
437
|
+
const yulAst = node.AST;
|
|
438
|
+
if (!yulAst) return;
|
|
439
|
+
const slotValueSrcs = /* @__PURE__ */ new Set();
|
|
440
|
+
collectSlotAssignmentValueSrcs(yulAst, slotValueSrcs);
|
|
441
|
+
if (slotValueSrcs.size === 0) return;
|
|
442
|
+
const refs = node.externalReferences;
|
|
443
|
+
if (!Array.isArray(refs)) return;
|
|
444
|
+
for (const ref of refs) {
|
|
445
|
+
if (typeof ref.src === "string" && slotValueSrcs.has(ref.src) && typeof ref.declaration === "number") {
|
|
446
|
+
ids.add(ref.declaration);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return ids;
|
|
452
|
+
}
|
|
453
|
+
function collectAliases(artifacts) {
|
|
454
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
455
|
+
for (const artifact of artifacts) {
|
|
456
|
+
if (!artifact.ast) continue;
|
|
457
|
+
walkAst(artifact.ast, (node) => {
|
|
458
|
+
if (node.nodeType !== "VariableDeclarationStatement") return;
|
|
459
|
+
const initialValue = node.initialValue;
|
|
460
|
+
if (initialValue?.nodeType !== "Identifier") return;
|
|
461
|
+
const referenced = initialValue.referencedDeclaration;
|
|
462
|
+
if (typeof referenced !== "number") return;
|
|
463
|
+
const decls = node.declarations;
|
|
464
|
+
if (!Array.isArray(decls)) return;
|
|
465
|
+
for (const d of decls) {
|
|
466
|
+
if (typeof d?.id === "number") aliases.set(d.id, referenced);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return aliases;
|
|
471
|
+
}
|
|
472
|
+
function isUsedAsSlot(constantId, slotUsedIds, aliases) {
|
|
473
|
+
if (constantId < 0) return false;
|
|
474
|
+
if (slotUsedIds.has(constantId)) return true;
|
|
475
|
+
for (const [aliasId, refId] of aliases) {
|
|
476
|
+
if (refId === constantId && slotUsedIds.has(aliasId)) return true;
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
function collectGatedSlotConstants(ctx) {
|
|
481
|
+
const constants = [
|
|
482
|
+
...collectSlotConstants(ctx),
|
|
483
|
+
...collectLiteralSlotConstants(ctx),
|
|
484
|
+
...collectFormulaSlotConstants(ctx)
|
|
485
|
+
];
|
|
486
|
+
const slotUsedIds = collectSlotUsedDeclarationIds(ctx.artifacts);
|
|
487
|
+
const aliases = collectAliases(ctx.artifacts);
|
|
488
|
+
return [
|
|
489
|
+
...constants.filter((c) => isUsedAsSlot(c.declarationId, slotUsedIds, aliases)),
|
|
490
|
+
...collectAssemblyLiteralSlots(ctx.artifacts)
|
|
491
|
+
];
|
|
492
|
+
}
|
|
238
493
|
var diamondStorageAnalyzer = {
|
|
239
494
|
name: "diamond-storage-namespace",
|
|
240
495
|
run(ctx) {
|
|
241
|
-
const
|
|
496
|
+
const slotConstants = collectGatedSlotConstants(ctx);
|
|
242
497
|
const bySlot = /* @__PURE__ */ new Map();
|
|
243
|
-
for (const c of
|
|
498
|
+
for (const c of slotConstants) {
|
|
244
499
|
const list = bySlot.get(c.slot) ?? [];
|
|
245
500
|
list.push(c);
|
|
246
501
|
bySlot.set(c.slot, list);
|
|
@@ -249,72 +504,38 @@ var diamondStorageAnalyzer = {
|
|
|
249
504
|
for (const [slot, group] of bySlot) {
|
|
250
505
|
const distinctSources = new Set(group.map((g) => g.sourcePath));
|
|
251
506
|
if (distinctSources.size < 2) continue;
|
|
252
|
-
const namespaces = Array.from(
|
|
507
|
+
const namespaces = Array.from(
|
|
508
|
+
new Set(group.map((g) => g.namespace).filter((n) => n !== null))
|
|
509
|
+
);
|
|
510
|
+
const variableNames = Array.from(new Set(group.map((g) => g.variableName)));
|
|
511
|
+
const hasLiteral = group.some((g) => g.namespace === null);
|
|
253
512
|
const facets = Array.from(new Set(group.map((g) => g.contract)));
|
|
254
513
|
const locations = group.map((g) => {
|
|
255
514
|
const sourceText = ctx.rawSources.get(g.sourcePath);
|
|
256
|
-
return { file: g.sourcePath, line: lineFromSrc(g.src, sourceText) };
|
|
515
|
+
return { file: g.sourcePath, line: lineFromSrc(g.src, sourceText), src: g.src };
|
|
257
516
|
});
|
|
517
|
+
let message;
|
|
518
|
+
if (!hasLiteral) {
|
|
519
|
+
message = namespaces.length === 1 ? `Diamond Storage namespace "${namespaces[0]}" is declared in ${distinctSources.size} different sources, all resolving to the same slot.` : `Distinct namespaces ${namespaces.map((n) => `"${n}"`).join(", ")} hash to the same slot.`;
|
|
520
|
+
} else if (namespaces.length === 0) {
|
|
521
|
+
message = `Hardcoded storage slot ${slot} is used as a Diamond Storage pointer by ${distinctSources.size} different sources (${variableNames.join(", ")}), so distinct facets share the same slot.`;
|
|
522
|
+
} else {
|
|
523
|
+
message = `Hardcoded slot ${slot} collides with namespace(s) ${namespaces.map((n) => `"${n}"`).join(", ")} \u2014 they resolve to the same storage slot.`;
|
|
524
|
+
}
|
|
258
525
|
findings.push({
|
|
259
526
|
kind: "diamond-storage-namespace",
|
|
260
527
|
severity: "error",
|
|
261
528
|
slot,
|
|
262
|
-
message
|
|
529
|
+
message,
|
|
263
530
|
facets,
|
|
264
531
|
locations,
|
|
265
|
-
detail: { namespaces, declarations: group }
|
|
532
|
+
detail: { namespaces, variableNames, declarations: group }
|
|
266
533
|
});
|
|
267
534
|
}
|
|
268
535
|
return findings;
|
|
269
536
|
}
|
|
270
537
|
};
|
|
271
538
|
|
|
272
|
-
// src/lib/eip7201.ts
|
|
273
|
-
import { keccak_256 as keccak_2562 } from "@noble/hashes/sha3";
|
|
274
|
-
var MASK_LAST_BYTE = (() => {
|
|
275
|
-
const m = new Uint8Array(32).fill(255);
|
|
276
|
-
m[31] = 0;
|
|
277
|
-
return m;
|
|
278
|
-
})();
|
|
279
|
-
function utf8(s) {
|
|
280
|
-
return new TextEncoder().encode(s);
|
|
281
|
-
}
|
|
282
|
-
function toHex(bytes) {
|
|
283
|
-
let out = "0x";
|
|
284
|
-
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
285
|
-
return out;
|
|
286
|
-
}
|
|
287
|
-
function subOne(bytes) {
|
|
288
|
-
const out = new Uint8Array(bytes);
|
|
289
|
-
for (let i = out.length - 1; i >= 0; i--) {
|
|
290
|
-
if (out[i] > 0) {
|
|
291
|
-
out[i] = out[i] - 1;
|
|
292
|
-
return out;
|
|
293
|
-
}
|
|
294
|
-
out[i] = 255;
|
|
295
|
-
}
|
|
296
|
-
return out;
|
|
297
|
-
}
|
|
298
|
-
function maskLastByte(bytes) {
|
|
299
|
-
const out = new Uint8Array(32);
|
|
300
|
-
for (let i = 0; i < 32; i++) out[i] = bytes[i] & MASK_LAST_BYTE[i];
|
|
301
|
-
return out;
|
|
302
|
-
}
|
|
303
|
-
function erc7201Slot(namespaceId) {
|
|
304
|
-
const inner = keccak_2562(utf8(namespaceId));
|
|
305
|
-
const decremented = subOne(inner);
|
|
306
|
-
const outer = keccak_2562(decremented);
|
|
307
|
-
return toHex(maskLastByte(outer));
|
|
308
|
-
}
|
|
309
|
-
var ERC7201_PREFIX = "erc7201:";
|
|
310
|
-
function parseErc7201Annotation(text) {
|
|
311
|
-
const idx = text.indexOf(ERC7201_PREFIX);
|
|
312
|
-
if (idx === -1) return null;
|
|
313
|
-
const rest = text.slice(idx + ERC7201_PREFIX.length);
|
|
314
|
-
const match = rest.match(/^[A-Za-z0-9_.\-]+/);
|
|
315
|
-
return match ? match[0] : null;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
539
|
// src/detector/analyzers/erc7201.ts
|
|
319
540
|
var NAMED_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
320
541
|
"ContractDefinition",
|
|
@@ -402,7 +623,7 @@ var erc7201Analyzer = {
|
|
|
402
623
|
if (distinctSources.size < 2) continue;
|
|
403
624
|
const ids = Array.from(new Set(group.map((g) => g.namespaceId)));
|
|
404
625
|
const facets = Array.from(new Set(group.map((g) => g.contract)));
|
|
405
|
-
const locations = group.map((g) => ({ file: g.sourcePath }));
|
|
626
|
+
const locations = group.map((g) => ({ file: g.sourcePath, src: g.src }));
|
|
406
627
|
findings.push({
|
|
407
628
|
kind: "erc7201-namespace",
|
|
408
629
|
severity: "error",
|
|
@@ -417,6 +638,102 @@ var erc7201Analyzer = {
|
|
|
417
638
|
}
|
|
418
639
|
};
|
|
419
640
|
|
|
641
|
+
// src/detector/inventory.ts
|
|
642
|
+
function lineOf(src, text) {
|
|
643
|
+
if (!src || !text) return void 0;
|
|
644
|
+
const [startStr] = src.split(":");
|
|
645
|
+
const start = Number(startStr);
|
|
646
|
+
if (!Number.isFinite(start)) return void 0;
|
|
647
|
+
let line = 1;
|
|
648
|
+
for (let i = 0; i < start && i < text.length; i++) {
|
|
649
|
+
if (text.charCodeAt(i) === 10) line++;
|
|
650
|
+
}
|
|
651
|
+
return line;
|
|
652
|
+
}
|
|
653
|
+
var PRIORITY = { erc7201: 0, namespace: 1, hardcoded: 2 };
|
|
654
|
+
function buildInventory(ctx) {
|
|
655
|
+
const bySlot = /* @__PURE__ */ new Map();
|
|
656
|
+
const add = (region) => {
|
|
657
|
+
const existing = bySlot.get(region.slot);
|
|
658
|
+
if (!existing || PRIORITY[region.kind] < PRIORITY[existing.kind]) {
|
|
659
|
+
bySlot.set(region.slot, region);
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
for (const c of collectGatedSlotConstants(ctx)) {
|
|
663
|
+
add({
|
|
664
|
+
slot: c.slot,
|
|
665
|
+
label: c.namespace ?? c.variableName,
|
|
666
|
+
kind: c.namespace ? "namespace" : "hardcoded",
|
|
667
|
+
contract: c.contract,
|
|
668
|
+
file: c.sourcePath,
|
|
669
|
+
line: lineOf(c.src, ctx.rawSources.get(c.sourcePath))
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
for (const a of collectErc7201Annotations(ctx.artifacts)) {
|
|
673
|
+
add({
|
|
674
|
+
slot: a.slot,
|
|
675
|
+
label: a.namespaceId,
|
|
676
|
+
kind: "erc7201",
|
|
677
|
+
contract: a.contract,
|
|
678
|
+
file: a.sourcePath,
|
|
679
|
+
line: lineOf(a.src, ctx.rawSources.get(a.sourcePath))
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
return [...bySlot.values()].sort(
|
|
683
|
+
(x, y) => x.file.localeCompare(y.file) || x.slot.localeCompare(y.slot)
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/detector/index.ts
|
|
688
|
+
var DEFAULT_IGNORE_GLOBS = [
|
|
689
|
+
"lib/**",
|
|
690
|
+
"test/**",
|
|
691
|
+
"script/**",
|
|
692
|
+
"**/*.t.sol",
|
|
693
|
+
"**/*.s.sol"
|
|
694
|
+
];
|
|
695
|
+
function compilePatterns(globs) {
|
|
696
|
+
return globs.map((g) => {
|
|
697
|
+
const escaped = g.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "::DOUBLESTAR::").replace(/\*/g, "[^/]*").replace(/::DOUBLESTAR::/g, ".*").replace(/\?/g, ".");
|
|
698
|
+
return new RegExp(`^${escaped}$`);
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
function buildIgnore(userGlobs, noDefault) {
|
|
702
|
+
const globs = [
|
|
703
|
+
...noDefault ? [] : DEFAULT_IGNORE_GLOBS,
|
|
704
|
+
...userGlobs ?? []
|
|
705
|
+
];
|
|
706
|
+
if (globs.length === 0) return void 0;
|
|
707
|
+
const patterns = compilePatterns(globs);
|
|
708
|
+
return (sourcePath) => patterns.some((p) => p.test(sourcePath));
|
|
709
|
+
}
|
|
710
|
+
function buildIsFacet(globs) {
|
|
711
|
+
if (!globs || globs.length === 0) return void 0;
|
|
712
|
+
const patterns = compilePatterns(globs);
|
|
713
|
+
return (artifact) => patterns.some((p) => p.test(artifact.sourcePath));
|
|
714
|
+
}
|
|
715
|
+
async function detect(options, analyzers) {
|
|
716
|
+
const artifacts = await loadFoundryArtifacts(options.path, {
|
|
717
|
+
ignoreSourcePath: buildIgnore(options.ignoreGlobs, options.noDefaultIgnore)
|
|
718
|
+
});
|
|
719
|
+
const rawSources = await loadRawSources(
|
|
720
|
+
options.path,
|
|
721
|
+
artifacts.map((a) => a.sourcePath)
|
|
722
|
+
);
|
|
723
|
+
const ctx = {
|
|
724
|
+
artifacts,
|
|
725
|
+
rawSources,
|
|
726
|
+
isFacet: buildIsFacet(options.facetGlobs)
|
|
727
|
+
};
|
|
728
|
+
const findings = [];
|
|
729
|
+
for (const analyzer of analyzers) {
|
|
730
|
+
const out = await analyzer.run(ctx);
|
|
731
|
+
findings.push(...out);
|
|
732
|
+
}
|
|
733
|
+
const inventory = buildInventory(ctx);
|
|
734
|
+
return { artifacts, findings, rawSources, inventory };
|
|
735
|
+
}
|
|
736
|
+
|
|
420
737
|
// src/detector/analyzers/appStorage.ts
|
|
421
738
|
function memberFingerprint(m) {
|
|
422
739
|
return { label: m.label, offset: m.offset, slot: m.slot, type: m.type };
|
|
@@ -581,7 +898,7 @@ var inlineAssemblyAnalyzer = {
|
|
|
581
898
|
slot: lit.slot,
|
|
582
899
|
message: `inline assembly writes to a hardcoded slot (sstore(${lit.rawValue}, \u2026)) \u2014 confirm no overlap with computed storage slots.`,
|
|
583
900
|
facets: [lit.artifact.contractName],
|
|
584
|
-
locations: [{ file: lit.artifact.sourcePath }],
|
|
901
|
+
locations: [{ file: lit.artifact.sourcePath, src: lit.src }],
|
|
585
902
|
detail: { rawValue: lit.rawValue, src: lit.src }
|
|
586
903
|
}));
|
|
587
904
|
}
|
|
@@ -672,37 +989,140 @@ var defaultAnalyzers = [
|
|
|
672
989
|
// src/reporter/terminal.ts
|
|
673
990
|
import pc from "picocolors";
|
|
674
991
|
var SEVERITY_RANK = { info: 0, warn: 1, error: 2 };
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
992
|
+
var SEV = {
|
|
993
|
+
error: { label: "error", glyph: "\u2716", paint: (s) => pc.red(s) },
|
|
994
|
+
warn: { label: "warning", glyph: "\u26A0", paint: (s) => pc.yellow(s) },
|
|
995
|
+
info: { label: "note", glyph: "\u25CF", paint: (s) => pc.cyan(s) }
|
|
996
|
+
};
|
|
997
|
+
var HELP = {
|
|
998
|
+
"diamond-storage-namespace": "give every facet a unique storage seed; never reuse a namespace string, precomputed slot, or formula across facets",
|
|
999
|
+
"erc7201-namespace": "use a distinct erc7201 namespace id per facet",
|
|
1000
|
+
"appstorage-fingerprint": "keep the shared struct layout identical across all facets; append fields, never reorder or insert",
|
|
1001
|
+
"inheritance-overlap": "facets must not declare sequential state variables; move state into namespaced Diamond Storage",
|
|
1002
|
+
"inline-assembly-slot": "confirm this hardcoded slot cannot overlap any namespaced storage region",
|
|
1003
|
+
"mapping-overlap": "ensure mapping base slots are derived from distinct, collision-resistant seeds"
|
|
1004
|
+
};
|
|
1005
|
+
function shortSlot(slot) {
|
|
1006
|
+
if (!slot.startsWith("0x") || slot.length <= 14) return slot;
|
|
1007
|
+
return `${slot.slice(0, 8)}\u2026${slot.slice(-6)}`;
|
|
1008
|
+
}
|
|
1009
|
+
function expandTabs(s) {
|
|
1010
|
+
return s.replace(/\t/g, " ");
|
|
1011
|
+
}
|
|
1012
|
+
function resolveSpan(loc, sourceText) {
|
|
1013
|
+
if (!sourceText || !loc.src) return null;
|
|
1014
|
+
const [offStr, lenStr] = loc.src.split(":");
|
|
1015
|
+
const offset = Number(offStr);
|
|
1016
|
+
const length = Number(lenStr);
|
|
1017
|
+
if (!Number.isFinite(offset) || offset < 0 || offset > sourceText.length) return null;
|
|
1018
|
+
let line = 1;
|
|
1019
|
+
let lineStart = 0;
|
|
1020
|
+
for (let i = 0; i < offset; i++) {
|
|
1021
|
+
if (sourceText.charCodeAt(i) === 10) {
|
|
1022
|
+
line++;
|
|
1023
|
+
lineStart = i + 1;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const nl = sourceText.indexOf("\n", lineStart);
|
|
1027
|
+
const lineEnd = nl === -1 ? sourceText.length : nl;
|
|
1028
|
+
const rawLine = sourceText.slice(lineStart, lineEnd);
|
|
1029
|
+
const rawPrefix = sourceText.slice(lineStart, offset);
|
|
1030
|
+
const column = expandTabs(rawPrefix).length + 1;
|
|
1031
|
+
const visibleLen = expandTabs(rawLine).length;
|
|
1032
|
+
const rawCaret = Number.isFinite(length) && length > 0 ? length : 1;
|
|
1033
|
+
const caretLen = Math.max(1, Math.min(rawCaret, visibleLen - (column - 1)));
|
|
1034
|
+
return { line, column, lineText: expandTabs(rawLine), caretLen };
|
|
679
1035
|
}
|
|
680
|
-
function
|
|
1036
|
+
function renderFrame(loc, span, sev, note) {
|
|
1037
|
+
const gutter = String(span.line);
|
|
1038
|
+
const pad = " ".repeat(gutter.length);
|
|
1039
|
+
const bar = pc.dim("\u2502");
|
|
1040
|
+
const arrow = pc.dim("\u256D\u2500[");
|
|
1041
|
+
const close = pc.dim("]");
|
|
1042
|
+
const caret = sev.paint("\u2500".repeat(span.caretLen));
|
|
1043
|
+
const caretPad = " ".repeat(span.column - 1);
|
|
1044
|
+
const label = note ? " " + sev.paint(note) : "";
|
|
1045
|
+
return [
|
|
1046
|
+
` ${arrow}${pc.cyan(`${loc.file}:${span.line}:${span.column}`)}${close}`,
|
|
1047
|
+
` ${pad} ${bar}`,
|
|
1048
|
+
` ${pc.dim(gutter)} ${bar} ${span.lineText}`,
|
|
1049
|
+
` ${pad} ${pc.dim("\xB7")} ${caretPad}${caret}${label}`,
|
|
1050
|
+
` ${pad} ${pc.dim("\u2570\u2500")}`
|
|
1051
|
+
];
|
|
1052
|
+
}
|
|
1053
|
+
var KIND_TAG = {
|
|
1054
|
+
erc7201: "erc7201",
|
|
1055
|
+
namespace: "namespace",
|
|
1056
|
+
hardcoded: "precomputed"
|
|
1057
|
+
};
|
|
1058
|
+
function renderInventory(inventory) {
|
|
681
1059
|
const lines = [];
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1060
|
+
const shown = inventory.slice(0, 60);
|
|
1061
|
+
const labelW = Math.max(...shown.map((r) => r.label.length), 0);
|
|
1062
|
+
const tagW = Math.max(...shown.map((r) => KIND_TAG[r.kind].length), 0);
|
|
1063
|
+
const contractW = Math.max(...shown.map((r) => r.contract.length), 0);
|
|
1064
|
+
lines.push("");
|
|
1065
|
+
lines.push(
|
|
1066
|
+
pc.bold(`Verified ${inventory.length} storage region${inventory.length === 1 ? "" : "s"}`) + pc.dim(", each on its own slot:")
|
|
1067
|
+
);
|
|
1068
|
+
lines.push("");
|
|
1069
|
+
for (const r of shown) {
|
|
1070
|
+
const where = r.line ? `${r.file}:${r.line}` : r.file;
|
|
1071
|
+
lines.push(
|
|
1072
|
+
` ${pc.green("\u2022")} ${pc.green(r.label.padEnd(labelW))} ${pc.dim(KIND_TAG[r.kind].padEnd(tagW))} ${pc.cyan(shortSlot(r.slot))} ${r.contract.padEnd(contractW)} ${pc.dim(where)}`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
if (inventory.length > shown.length) {
|
|
1076
|
+
lines.push(` ${pc.dim(`\u2026 and ${inventory.length - shown.length} more`)}`);
|
|
686
1077
|
}
|
|
687
|
-
|
|
688
|
-
|
|
1078
|
+
lines.push("");
|
|
1079
|
+
lines.push(
|
|
1080
|
+
pc.green("Every facet keeps to its own namespace, and no two regions share a slot. ") + pc.green(pc.bold("Nicely done."))
|
|
689
1081
|
);
|
|
1082
|
+
return lines;
|
|
1083
|
+
}
|
|
1084
|
+
function renderTerminal(findings, facetCount, rawSources, inventory = []) {
|
|
1085
|
+
const artifactsNote = pc.dim(`${facetCount} artifact${facetCount === 1 ? "" : "s"} scanned`);
|
|
1086
|
+
if (findings.length === 0) {
|
|
1087
|
+
const header = `${pc.green(pc.bold("\u2714 no storage collisions detected"))} ${pc.dim("\xB7")} ${artifactsNote}`;
|
|
1088
|
+
if (inventory.length === 0) return header;
|
|
1089
|
+
return [header, ...renderInventory(inventory)].join("\n");
|
|
1090
|
+
}
|
|
1091
|
+
const sorted = [...findings].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
|
|
1092
|
+
const lines = [];
|
|
690
1093
|
for (const f of sorted) {
|
|
1094
|
+
const sev = SEV[f.severity];
|
|
1095
|
+
const tag = `${pc.bold(sev.paint(sev.label))}${pc.dim(`[${f.kind}]`)}`;
|
|
691
1096
|
lines.push("");
|
|
692
|
-
lines.push(`${
|
|
693
|
-
|
|
1097
|
+
lines.push(`${tag}: ${pc.bold(f.message)}`);
|
|
1098
|
+
f.locations.forEach((loc, i) => {
|
|
1099
|
+
const span = resolveSpan(loc, rawSources?.get(loc.file));
|
|
1100
|
+
const note = i === 0 ? `slot ${shortSlot(f.slot)}` : "same slot here";
|
|
1101
|
+
if (span) {
|
|
1102
|
+
lines.push(...renderFrame(loc, span, sev, f.slot === "n/a" ? "here" : note));
|
|
1103
|
+
} else {
|
|
1104
|
+
const where = loc.line ? `${loc.file}:${loc.line}` : loc.file;
|
|
1105
|
+
lines.push(` ${pc.dim("\u256D\u2500[")}${pc.cyan(where)}${pc.dim("]")}`);
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
694
1108
|
if (f.facets.length > 0) {
|
|
695
|
-
lines.push(` ${pc.dim("facets:")} ${f.facets.join(", ")}`);
|
|
1109
|
+
lines.push(` ${pc.dim("= facets:")} ${f.facets.join(pc.dim(", "))}`);
|
|
696
1110
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
lines.push(` ${pc.dim("at")} ${where}`);
|
|
1111
|
+
if (f.slot && f.slot !== "n/a") {
|
|
1112
|
+
lines.push(` ${pc.dim("= slot: ")} ${pc.dim(f.slot)}`);
|
|
700
1113
|
}
|
|
1114
|
+
lines.push(` ${pc.dim("= help: ")} ${pc.dim(HELP[f.kind] ?? "")}`);
|
|
701
1115
|
}
|
|
702
1116
|
const errors = findings.filter((f) => f.severity === "error").length;
|
|
703
1117
|
const warns = findings.filter((f) => f.severity === "warn").length;
|
|
1118
|
+
const notes = findings.filter((f) => f.severity === "info").length;
|
|
1119
|
+
const parts = [];
|
|
1120
|
+
if (errors > 0) parts.push(pc.red(`${SEV.error.glyph} ${errors} error${errors === 1 ? "" : "s"}`));
|
|
1121
|
+
if (warns > 0)
|
|
1122
|
+
parts.push(pc.yellow(`${SEV.warn.glyph} ${warns} warning${warns === 1 ? "" : "s"}`));
|
|
1123
|
+
if (notes > 0) parts.push(pc.cyan(`${SEV.info.glyph} ${notes} note${notes === 1 ? "" : "s"}`));
|
|
704
1124
|
lines.push("");
|
|
705
|
-
lines.push(pc.
|
|
1125
|
+
lines.push(`${parts.join(pc.dim(" \xB7 "))} ${pc.dim("\xB7")} ${artifactsNote}`);
|
|
706
1126
|
return lines.join("\n");
|
|
707
1127
|
}
|
|
708
1128
|
|
|
@@ -832,7 +1252,12 @@ async function run(target, opts) {
|
|
|
832
1252
|
)
|
|
833
1253
|
);
|
|
834
1254
|
}
|
|
835
|
-
const output = opts.json ? renderJson(result.findings, result.artifacts.length) : opts.markdown ? renderMarkdown(result.findings, result.artifacts.length) : renderTerminal(
|
|
1255
|
+
const output = opts.json ? renderJson(result.findings, result.artifacts.length) : opts.markdown ? renderMarkdown(result.findings, result.artifacts.length) : renderTerminal(
|
|
1256
|
+
result.findings,
|
|
1257
|
+
result.artifacts.length,
|
|
1258
|
+
result.rawSources,
|
|
1259
|
+
result.inventory
|
|
1260
|
+
);
|
|
836
1261
|
process.stdout.write(output + "\n");
|
|
837
1262
|
const threshold = SEVERITY_RANK3[opts.severity];
|
|
838
1263
|
const hit = result.findings.some((f) => SEVERITY_RANK3[f.severity] >= threshold);
|
package/dist/index.js
CHANGED
|
@@ -68,6 +68,18 @@ function extractSourcePath(artifactPath, parsed) {
|
|
|
68
68
|
}
|
|
69
69
|
return path.basename(path.dirname(artifactPath));
|
|
70
70
|
}
|
|
71
|
+
async function loadRawSources(inputPath, sourcePaths) {
|
|
72
|
+
const { root } = await resolveFoundryRoot(inputPath);
|
|
73
|
+
const out = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const sourcePath of new Set(sourcePaths)) {
|
|
75
|
+
if (out.has(sourcePath)) continue;
|
|
76
|
+
try {
|
|
77
|
+
out.set(sourcePath, await fs.readFile(path.join(root, sourcePath), "utf8"));
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
71
83
|
async function loadFoundryArtifacts(inputPath, opts = {}) {
|
|
72
84
|
const { outDir } = await resolveFoundryRoot(inputPath);
|
|
73
85
|
if (!await fileExists(outDir)) {
|
|
@@ -101,50 +113,8 @@ async function loadFoundryArtifacts(inputPath, opts = {}) {
|
|
|
101
113
|
return artifacts;
|
|
102
114
|
}
|
|
103
115
|
|
|
104
|
-
// src/detector/
|
|
105
|
-
|
|
106
|
-
"lib/**",
|
|
107
|
-
"test/**",
|
|
108
|
-
"script/**",
|
|
109
|
-
"**/*.t.sol",
|
|
110
|
-
"**/*.s.sol"
|
|
111
|
-
];
|
|
112
|
-
function compilePatterns(globs) {
|
|
113
|
-
return globs.map((g) => {
|
|
114
|
-
const escaped = g.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "::DOUBLESTAR::").replace(/\*/g, "[^/]*").replace(/::DOUBLESTAR::/g, ".*").replace(/\?/g, ".");
|
|
115
|
-
return new RegExp(`^${escaped}$`);
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
function buildIgnore(userGlobs, noDefault) {
|
|
119
|
-
const globs = [
|
|
120
|
-
...noDefault ? [] : DEFAULT_IGNORE_GLOBS,
|
|
121
|
-
...userGlobs ?? []
|
|
122
|
-
];
|
|
123
|
-
if (globs.length === 0) return void 0;
|
|
124
|
-
const patterns = compilePatterns(globs);
|
|
125
|
-
return (sourcePath) => patterns.some((p) => p.test(sourcePath));
|
|
126
|
-
}
|
|
127
|
-
function buildIsFacet(globs) {
|
|
128
|
-
if (!globs || globs.length === 0) return void 0;
|
|
129
|
-
const patterns = compilePatterns(globs);
|
|
130
|
-
return (artifact) => patterns.some((p) => p.test(artifact.sourcePath));
|
|
131
|
-
}
|
|
132
|
-
async function detect(options, analyzers) {
|
|
133
|
-
const artifacts = await loadFoundryArtifacts(options.path, {
|
|
134
|
-
ignoreSourcePath: buildIgnore(options.ignoreGlobs, options.noDefaultIgnore)
|
|
135
|
-
});
|
|
136
|
-
const ctx = {
|
|
137
|
-
artifacts,
|
|
138
|
-
rawSources: /* @__PURE__ */ new Map(),
|
|
139
|
-
isFacet: buildIsFacet(options.facetGlobs)
|
|
140
|
-
};
|
|
141
|
-
const findings = [];
|
|
142
|
-
for (const analyzer of analyzers) {
|
|
143
|
-
const out = await analyzer.run(ctx);
|
|
144
|
-
findings.push(...out);
|
|
145
|
-
}
|
|
146
|
-
return { artifacts, findings };
|
|
147
|
-
}
|
|
116
|
+
// src/detector/analyzers/diamondStorage.ts
|
|
117
|
+
import { keccak_256 as keccak_2562 } from "@noble/hashes/sha3";
|
|
148
118
|
|
|
149
119
|
// src/lib/eip7201.ts
|
|
150
120
|
import { keccak_256 } from "@noble/hashes/sha3";
|
|
@@ -191,6 +161,488 @@ function parseErc7201Annotation(text) {
|
|
|
191
161
|
const match = rest.match(/^[A-Za-z0-9_.\-]+/);
|
|
192
162
|
return match ? match[0] : null;
|
|
193
163
|
}
|
|
164
|
+
|
|
165
|
+
// src/detector/analyzers/diamondStorage.ts
|
|
166
|
+
function keccak256Hex(input) {
|
|
167
|
+
const bytes = keccak_2562(new TextEncoder().encode(input));
|
|
168
|
+
let out = "0x";
|
|
169
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
function isBytes32Constant(node) {
|
|
173
|
+
if (node.nodeType !== "VariableDeclaration") return false;
|
|
174
|
+
if (node.constant !== true) return false;
|
|
175
|
+
const typeName = node.typeName;
|
|
176
|
+
return typeName?.nodeType === "ElementaryTypeName" && typeName.name === "bytes32";
|
|
177
|
+
}
|
|
178
|
+
function extractKeccakStringArg(value) {
|
|
179
|
+
if (!value || typeof value !== "object") return null;
|
|
180
|
+
const v = value;
|
|
181
|
+
if (v.nodeType !== "FunctionCall") return null;
|
|
182
|
+
const expr = v.expression;
|
|
183
|
+
if (expr?.nodeType !== "Identifier" || expr.name !== "keccak256") return null;
|
|
184
|
+
const args = v.arguments;
|
|
185
|
+
if (!Array.isArray(args) || args.length !== 1) return null;
|
|
186
|
+
const arg = args[0];
|
|
187
|
+
if (arg.nodeType !== "Literal" || arg.kind !== "string") return null;
|
|
188
|
+
return typeof arg.value === "string" ? arg.value : null;
|
|
189
|
+
}
|
|
190
|
+
function extractBytes32HexLiteral(value) {
|
|
191
|
+
if (!value || typeof value !== "object") return null;
|
|
192
|
+
const v = value;
|
|
193
|
+
if (v.nodeType !== "Literal" || v.kind !== "number") return null;
|
|
194
|
+
if (typeof v.value !== "string") return null;
|
|
195
|
+
try {
|
|
196
|
+
const big = BigInt(v.value);
|
|
197
|
+
if (big < 0n) return null;
|
|
198
|
+
return "0x" + big.toString(16).padStart(64, "0");
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function findKeccakStringArg(node) {
|
|
204
|
+
if (!node || typeof node !== "object") return null;
|
|
205
|
+
const v = node;
|
|
206
|
+
if (v.nodeType !== "FunctionCall") return null;
|
|
207
|
+
const expr = v.expression;
|
|
208
|
+
if (expr?.nodeType !== "Identifier" || expr.name !== "keccak256") return null;
|
|
209
|
+
const args = v.arguments;
|
|
210
|
+
if (!Array.isArray(args) || args.length !== 1) return null;
|
|
211
|
+
const arg = args[0];
|
|
212
|
+
if (arg.nodeType !== "Literal" || arg.kind !== "string") return null;
|
|
213
|
+
return typeof arg.value === "string" ? arg.value : null;
|
|
214
|
+
}
|
|
215
|
+
function someNode(node, pred) {
|
|
216
|
+
if (!node || typeof node !== "object") return false;
|
|
217
|
+
const n = node;
|
|
218
|
+
if (typeof n.nodeType === "string" && pred(n)) return true;
|
|
219
|
+
for (const key of Object.keys(n)) {
|
|
220
|
+
const value = n[key];
|
|
221
|
+
if (Array.isArray(value)) {
|
|
222
|
+
if (value.some((c) => someNode(c, pred))) return true;
|
|
223
|
+
} else if (value && typeof value === "object") {
|
|
224
|
+
if (someNode(value, pred)) return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
function extractErc7201FormulaNamespace(value) {
|
|
230
|
+
const strings = [];
|
|
231
|
+
someNode(value, (n) => {
|
|
232
|
+
const s = findKeccakStringArg(n);
|
|
233
|
+
if (s !== null) strings.push(s);
|
|
234
|
+
return false;
|
|
235
|
+
});
|
|
236
|
+
if (strings.length !== 1) return null;
|
|
237
|
+
const hasSubOne = someNode(
|
|
238
|
+
value,
|
|
239
|
+
(n) => n.nodeType === "BinaryOperation" && n.operator === "-" && n.rightExpression?.nodeType === "Literal" && n.rightExpression.value === "1"
|
|
240
|
+
);
|
|
241
|
+
const hasMask = someNode(value, (n) => n.nodeType === "BinaryOperation" && n.operator === "&");
|
|
242
|
+
if (!hasSubOne || !hasMask) return null;
|
|
243
|
+
return strings[0];
|
|
244
|
+
}
|
|
245
|
+
function walkAst(ast, visit, parents = []) {
|
|
246
|
+
if (!ast || typeof ast !== "object") return;
|
|
247
|
+
const node = ast;
|
|
248
|
+
if (typeof node.nodeType === "string") visit(node, parents);
|
|
249
|
+
const nextParents = typeof node.nodeType === "string" ? [...parents, node] : parents;
|
|
250
|
+
for (const key of Object.keys(node)) {
|
|
251
|
+
const value = node[key];
|
|
252
|
+
if (Array.isArray(value)) {
|
|
253
|
+
for (const child of value) walkAst(child, visit, nextParents);
|
|
254
|
+
} else if (value && typeof value === "object") {
|
|
255
|
+
walkAst(value, visit, nextParents);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function declaringContract(parents) {
|
|
260
|
+
for (let i = parents.length - 1; i >= 0; i--) {
|
|
261
|
+
const p = parents[i];
|
|
262
|
+
if (p.nodeType === "ContractDefinition") return p.name ?? null;
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
function collectSlotConstants(ctx) {
|
|
267
|
+
const seen = /* @__PURE__ */ new Set();
|
|
268
|
+
const out = [];
|
|
269
|
+
for (const artifact of ctx.artifacts) {
|
|
270
|
+
if (!artifact.ast) continue;
|
|
271
|
+
walkAst(artifact.ast, (node, parents) => {
|
|
272
|
+
if (!isBytes32Constant(node)) return;
|
|
273
|
+
const namespace = extractKeccakStringArg(node.value);
|
|
274
|
+
if (namespace === null) return;
|
|
275
|
+
const declarationId = typeof node.id === "number" ? node.id : -1;
|
|
276
|
+
const variableName = node.name ?? "<anon>";
|
|
277
|
+
const contract = declaringContract(parents) ?? artifact.contractName;
|
|
278
|
+
const src = node.src;
|
|
279
|
+
const dedupeKey = `${artifact.sourcePath}::${contract}::${variableName}::${src ?? ""}`;
|
|
280
|
+
if (seen.has(dedupeKey)) return;
|
|
281
|
+
seen.add(dedupeKey);
|
|
282
|
+
out.push({
|
|
283
|
+
declarationId,
|
|
284
|
+
variableName,
|
|
285
|
+
namespace,
|
|
286
|
+
slot: keccak256Hex(namespace),
|
|
287
|
+
contract,
|
|
288
|
+
sourcePath: artifact.sourcePath,
|
|
289
|
+
src
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
function collectLiteralSlotConstants(ctx) {
|
|
296
|
+
const seen = /* @__PURE__ */ new Set();
|
|
297
|
+
const out = [];
|
|
298
|
+
for (const artifact of ctx.artifacts) {
|
|
299
|
+
if (!artifact.ast) continue;
|
|
300
|
+
walkAst(artifact.ast, (node, parents) => {
|
|
301
|
+
if (!isBytes32Constant(node)) return;
|
|
302
|
+
if (extractKeccakStringArg(node.value) !== null) return;
|
|
303
|
+
const slot = extractBytes32HexLiteral(node.value);
|
|
304
|
+
if (slot === null) return;
|
|
305
|
+
const declarationId = typeof node.id === "number" ? node.id : -1;
|
|
306
|
+
const variableName = node.name ?? "<anon>";
|
|
307
|
+
const contract = declaringContract(parents) ?? artifact.contractName;
|
|
308
|
+
const src = node.src;
|
|
309
|
+
const dedupeKey = `${artifact.sourcePath}::${contract}::${variableName}::${src ?? ""}`;
|
|
310
|
+
if (seen.has(dedupeKey)) return;
|
|
311
|
+
seen.add(dedupeKey);
|
|
312
|
+
out.push({
|
|
313
|
+
declarationId,
|
|
314
|
+
variableName,
|
|
315
|
+
namespace: null,
|
|
316
|
+
slot,
|
|
317
|
+
contract,
|
|
318
|
+
sourcePath: artifact.sourcePath,
|
|
319
|
+
src
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
function collectFormulaSlotConstants(ctx) {
|
|
326
|
+
const seen = /* @__PURE__ */ new Set();
|
|
327
|
+
const out = [];
|
|
328
|
+
for (const artifact of ctx.artifacts) {
|
|
329
|
+
if (!artifact.ast) continue;
|
|
330
|
+
walkAst(artifact.ast, (node, parents) => {
|
|
331
|
+
if (!isBytes32Constant(node)) return;
|
|
332
|
+
if (extractKeccakStringArg(node.value) !== null) return;
|
|
333
|
+
const namespace = extractErc7201FormulaNamespace(node.value);
|
|
334
|
+
if (namespace === null) return;
|
|
335
|
+
const declarationId = typeof node.id === "number" ? node.id : -1;
|
|
336
|
+
const variableName = node.name ?? "<anon>";
|
|
337
|
+
const contract = declaringContract(parents) ?? artifact.contractName;
|
|
338
|
+
const src = node.src;
|
|
339
|
+
const dedupeKey = `${artifact.sourcePath}::${contract}::${variableName}::${src ?? ""}`;
|
|
340
|
+
if (seen.has(dedupeKey)) return;
|
|
341
|
+
seen.add(dedupeKey);
|
|
342
|
+
out.push({
|
|
343
|
+
declarationId,
|
|
344
|
+
variableName,
|
|
345
|
+
namespace,
|
|
346
|
+
slot: erc7201Slot(namespace),
|
|
347
|
+
contract,
|
|
348
|
+
sourcePath: artifact.sourcePath,
|
|
349
|
+
src
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
function collectAssemblyLiteralSlots(artifacts) {
|
|
356
|
+
const seen = /* @__PURE__ */ new Set();
|
|
357
|
+
const out = [];
|
|
358
|
+
for (const artifact of artifacts) {
|
|
359
|
+
if (!artifact.ast) continue;
|
|
360
|
+
walkAst(artifact.ast, (node, parents) => {
|
|
361
|
+
if (node.nodeType !== "YulAssignment") return;
|
|
362
|
+
const targets = node.variableNames;
|
|
363
|
+
const targetsSlot = Array.isArray(targets) && targets.some((t) => typeof t.name === "string" && t.name.endsWith(".slot"));
|
|
364
|
+
if (!targetsSlot) return;
|
|
365
|
+
const value = node.value;
|
|
366
|
+
if (value?.nodeType !== "YulLiteral" || value.kind !== "number") return;
|
|
367
|
+
if (typeof value.value !== "string") return;
|
|
368
|
+
let slot;
|
|
369
|
+
try {
|
|
370
|
+
const big = BigInt(value.value);
|
|
371
|
+
if (big < 0n) return;
|
|
372
|
+
slot = "0x" + big.toString(16).padStart(64, "0");
|
|
373
|
+
} catch {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const contract = declaringContract(parents) ?? artifact.contractName;
|
|
377
|
+
const src = node.src;
|
|
378
|
+
const dedupeKey = `${artifact.sourcePath}::${contract}::${slot}::${src ?? ""}`;
|
|
379
|
+
if (seen.has(dedupeKey)) return;
|
|
380
|
+
seen.add(dedupeKey);
|
|
381
|
+
out.push({
|
|
382
|
+
declarationId: -1,
|
|
383
|
+
variableName: "<assembly literal>",
|
|
384
|
+
namespace: null,
|
|
385
|
+
slot,
|
|
386
|
+
contract,
|
|
387
|
+
sourcePath: artifact.sourcePath,
|
|
388
|
+
src
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
function collectSlotAssignmentValueSrcs(yulNode, srcs) {
|
|
395
|
+
if (!yulNode || typeof yulNode !== "object") return;
|
|
396
|
+
const node = yulNode;
|
|
397
|
+
if (node.nodeType === "YulAssignment") {
|
|
398
|
+
const targets = node.variableNames;
|
|
399
|
+
const targetsSlot = Array.isArray(targets) && targets.some((t) => typeof t.name === "string" && t.name.endsWith(".slot"));
|
|
400
|
+
if (targetsSlot) {
|
|
401
|
+
const value = node.value;
|
|
402
|
+
if (value?.nodeType === "YulIdentifier" && typeof value.src === "string") {
|
|
403
|
+
srcs.add(value.src);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
for (const key of Object.keys(node)) {
|
|
408
|
+
const v = node[key];
|
|
409
|
+
if (Array.isArray(v)) {
|
|
410
|
+
for (const child of v) collectSlotAssignmentValueSrcs(child, srcs);
|
|
411
|
+
} else if (v && typeof v === "object") {
|
|
412
|
+
collectSlotAssignmentValueSrcs(v, srcs);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function collectSlotUsedDeclarationIds(artifacts) {
|
|
417
|
+
const ids = /* @__PURE__ */ new Set();
|
|
418
|
+
for (const artifact of artifacts) {
|
|
419
|
+
if (!artifact.ast) continue;
|
|
420
|
+
walkAst(artifact.ast, (node) => {
|
|
421
|
+
if (node.nodeType !== "InlineAssembly") return;
|
|
422
|
+
const yulAst = node.AST;
|
|
423
|
+
if (!yulAst) return;
|
|
424
|
+
const slotValueSrcs = /* @__PURE__ */ new Set();
|
|
425
|
+
collectSlotAssignmentValueSrcs(yulAst, slotValueSrcs);
|
|
426
|
+
if (slotValueSrcs.size === 0) return;
|
|
427
|
+
const refs = node.externalReferences;
|
|
428
|
+
if (!Array.isArray(refs)) return;
|
|
429
|
+
for (const ref of refs) {
|
|
430
|
+
if (typeof ref.src === "string" && slotValueSrcs.has(ref.src) && typeof ref.declaration === "number") {
|
|
431
|
+
ids.add(ref.declaration);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return ids;
|
|
437
|
+
}
|
|
438
|
+
function collectAliases(artifacts) {
|
|
439
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
440
|
+
for (const artifact of artifacts) {
|
|
441
|
+
if (!artifact.ast) continue;
|
|
442
|
+
walkAst(artifact.ast, (node) => {
|
|
443
|
+
if (node.nodeType !== "VariableDeclarationStatement") return;
|
|
444
|
+
const initialValue = node.initialValue;
|
|
445
|
+
if (initialValue?.nodeType !== "Identifier") return;
|
|
446
|
+
const referenced = initialValue.referencedDeclaration;
|
|
447
|
+
if (typeof referenced !== "number") return;
|
|
448
|
+
const decls = node.declarations;
|
|
449
|
+
if (!Array.isArray(decls)) return;
|
|
450
|
+
for (const d of decls) {
|
|
451
|
+
if (typeof d?.id === "number") aliases.set(d.id, referenced);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return aliases;
|
|
456
|
+
}
|
|
457
|
+
function isUsedAsSlot(constantId, slotUsedIds, aliases) {
|
|
458
|
+
if (constantId < 0) return false;
|
|
459
|
+
if (slotUsedIds.has(constantId)) return true;
|
|
460
|
+
for (const [aliasId, refId] of aliases) {
|
|
461
|
+
if (refId === constantId && slotUsedIds.has(aliasId)) return true;
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
function collectGatedSlotConstants(ctx) {
|
|
466
|
+
const constants = [
|
|
467
|
+
...collectSlotConstants(ctx),
|
|
468
|
+
...collectLiteralSlotConstants(ctx),
|
|
469
|
+
...collectFormulaSlotConstants(ctx)
|
|
470
|
+
];
|
|
471
|
+
const slotUsedIds = collectSlotUsedDeclarationIds(ctx.artifacts);
|
|
472
|
+
const aliases = collectAliases(ctx.artifacts);
|
|
473
|
+
return [
|
|
474
|
+
...constants.filter((c) => isUsedAsSlot(c.declarationId, slotUsedIds, aliases)),
|
|
475
|
+
...collectAssemblyLiteralSlots(ctx.artifacts)
|
|
476
|
+
];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/detector/analyzers/erc7201.ts
|
|
480
|
+
var NAMED_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
481
|
+
"ContractDefinition",
|
|
482
|
+
"StructDefinition",
|
|
483
|
+
"FunctionDefinition",
|
|
484
|
+
"VariableDeclaration",
|
|
485
|
+
"ErrorDefinition",
|
|
486
|
+
"EventDefinition",
|
|
487
|
+
"ModifierDefinition",
|
|
488
|
+
"EnumDefinition"
|
|
489
|
+
]);
|
|
490
|
+
function getDocText(node) {
|
|
491
|
+
const doc = node.documentation;
|
|
492
|
+
if (!doc) return null;
|
|
493
|
+
if (typeof doc === "string") return doc;
|
|
494
|
+
if (typeof doc === "object" && doc !== null) {
|
|
495
|
+
const text = doc.text;
|
|
496
|
+
if (typeof text === "string") return text;
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
function nearestContract(parents) {
|
|
501
|
+
for (let i = parents.length - 1; i >= 0; i--) {
|
|
502
|
+
const p = parents[i];
|
|
503
|
+
if (p.nodeType === "ContractDefinition") return p.name ?? null;
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
function walkAst2(ast, visit, parents = []) {
|
|
508
|
+
if (!ast || typeof ast !== "object") return;
|
|
509
|
+
const node = ast;
|
|
510
|
+
if (typeof node.nodeType === "string") visit(node, parents);
|
|
511
|
+
const nextParents = typeof node.nodeType === "string" ? [...parents, node] : parents;
|
|
512
|
+
for (const key of Object.keys(node)) {
|
|
513
|
+
const value = node[key];
|
|
514
|
+
if (Array.isArray(value)) {
|
|
515
|
+
for (const child of value) walkAst2(child, visit, nextParents);
|
|
516
|
+
} else if (value && typeof value === "object") {
|
|
517
|
+
walkAst2(value, visit, nextParents);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function collectErc7201Annotations(artifacts) {
|
|
522
|
+
const seen = /* @__PURE__ */ new Set();
|
|
523
|
+
const out = [];
|
|
524
|
+
for (const artifact of artifacts) {
|
|
525
|
+
if (!artifact.ast) continue;
|
|
526
|
+
walkAst2(artifact.ast, (node, parents) => {
|
|
527
|
+
if (!node.nodeType || !NAMED_NODE_TYPES.has(node.nodeType)) return;
|
|
528
|
+
const text = getDocText(node);
|
|
529
|
+
if (!text || !text.includes("erc7201:")) return;
|
|
530
|
+
const namespaceId = parseErc7201Annotation(text);
|
|
531
|
+
if (!namespaceId) return;
|
|
532
|
+
const attachedTo = `${node.nodeType}:${node.name ?? "<anon>"}`;
|
|
533
|
+
const contract = node.nodeType === "ContractDefinition" ? node.name ?? artifact.contractName : nearestContract(parents) ?? artifact.contractName;
|
|
534
|
+
const src = node.src;
|
|
535
|
+
const dedupeKey = `${artifact.sourcePath}::${attachedTo}::${src ?? ""}`;
|
|
536
|
+
if (seen.has(dedupeKey)) return;
|
|
537
|
+
seen.add(dedupeKey);
|
|
538
|
+
out.push({
|
|
539
|
+
namespaceId,
|
|
540
|
+
slot: erc7201Slot(namespaceId),
|
|
541
|
+
attachedTo,
|
|
542
|
+
contract,
|
|
543
|
+
sourcePath: artifact.sourcePath,
|
|
544
|
+
src
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/detector/inventory.ts
|
|
552
|
+
function lineOf(src, text) {
|
|
553
|
+
if (!src || !text) return void 0;
|
|
554
|
+
const [startStr] = src.split(":");
|
|
555
|
+
const start = Number(startStr);
|
|
556
|
+
if (!Number.isFinite(start)) return void 0;
|
|
557
|
+
let line = 1;
|
|
558
|
+
for (let i = 0; i < start && i < text.length; i++) {
|
|
559
|
+
if (text.charCodeAt(i) === 10) line++;
|
|
560
|
+
}
|
|
561
|
+
return line;
|
|
562
|
+
}
|
|
563
|
+
var PRIORITY = { erc7201: 0, namespace: 1, hardcoded: 2 };
|
|
564
|
+
function buildInventory(ctx) {
|
|
565
|
+
const bySlot = /* @__PURE__ */ new Map();
|
|
566
|
+
const add = (region) => {
|
|
567
|
+
const existing = bySlot.get(region.slot);
|
|
568
|
+
if (!existing || PRIORITY[region.kind] < PRIORITY[existing.kind]) {
|
|
569
|
+
bySlot.set(region.slot, region);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
for (const c of collectGatedSlotConstants(ctx)) {
|
|
573
|
+
add({
|
|
574
|
+
slot: c.slot,
|
|
575
|
+
label: c.namespace ?? c.variableName,
|
|
576
|
+
kind: c.namespace ? "namespace" : "hardcoded",
|
|
577
|
+
contract: c.contract,
|
|
578
|
+
file: c.sourcePath,
|
|
579
|
+
line: lineOf(c.src, ctx.rawSources.get(c.sourcePath))
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
for (const a of collectErc7201Annotations(ctx.artifacts)) {
|
|
583
|
+
add({
|
|
584
|
+
slot: a.slot,
|
|
585
|
+
label: a.namespaceId,
|
|
586
|
+
kind: "erc7201",
|
|
587
|
+
contract: a.contract,
|
|
588
|
+
file: a.sourcePath,
|
|
589
|
+
line: lineOf(a.src, ctx.rawSources.get(a.sourcePath))
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
return [...bySlot.values()].sort(
|
|
593
|
+
(x, y) => x.file.localeCompare(y.file) || x.slot.localeCompare(y.slot)
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/detector/index.ts
|
|
598
|
+
var DEFAULT_IGNORE_GLOBS = [
|
|
599
|
+
"lib/**",
|
|
600
|
+
"test/**",
|
|
601
|
+
"script/**",
|
|
602
|
+
"**/*.t.sol",
|
|
603
|
+
"**/*.s.sol"
|
|
604
|
+
];
|
|
605
|
+
function compilePatterns(globs) {
|
|
606
|
+
return globs.map((g) => {
|
|
607
|
+
const escaped = g.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "::DOUBLESTAR::").replace(/\*/g, "[^/]*").replace(/::DOUBLESTAR::/g, ".*").replace(/\?/g, ".");
|
|
608
|
+
return new RegExp(`^${escaped}$`);
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
function buildIgnore(userGlobs, noDefault) {
|
|
612
|
+
const globs = [
|
|
613
|
+
...noDefault ? [] : DEFAULT_IGNORE_GLOBS,
|
|
614
|
+
...userGlobs ?? []
|
|
615
|
+
];
|
|
616
|
+
if (globs.length === 0) return void 0;
|
|
617
|
+
const patterns = compilePatterns(globs);
|
|
618
|
+
return (sourcePath) => patterns.some((p) => p.test(sourcePath));
|
|
619
|
+
}
|
|
620
|
+
function buildIsFacet(globs) {
|
|
621
|
+
if (!globs || globs.length === 0) return void 0;
|
|
622
|
+
const patterns = compilePatterns(globs);
|
|
623
|
+
return (artifact) => patterns.some((p) => p.test(artifact.sourcePath));
|
|
624
|
+
}
|
|
625
|
+
async function detect(options, analyzers) {
|
|
626
|
+
const artifacts = await loadFoundryArtifacts(options.path, {
|
|
627
|
+
ignoreSourcePath: buildIgnore(options.ignoreGlobs, options.noDefaultIgnore)
|
|
628
|
+
});
|
|
629
|
+
const rawSources = await loadRawSources(
|
|
630
|
+
options.path,
|
|
631
|
+
artifacts.map((a) => a.sourcePath)
|
|
632
|
+
);
|
|
633
|
+
const ctx = {
|
|
634
|
+
artifacts,
|
|
635
|
+
rawSources,
|
|
636
|
+
isFacet: buildIsFacet(options.facetGlobs)
|
|
637
|
+
};
|
|
638
|
+
const findings = [];
|
|
639
|
+
for (const analyzer of analyzers) {
|
|
640
|
+
const out = await analyzer.run(ctx);
|
|
641
|
+
findings.push(...out);
|
|
642
|
+
}
|
|
643
|
+
const inventory = buildInventory(ctx);
|
|
644
|
+
return { artifacts, findings, rawSources, inventory };
|
|
645
|
+
}
|
|
194
646
|
export {
|
|
195
647
|
detect,
|
|
196
648
|
erc7201Slot,
|
package/package.json
CHANGED