Skip to content

Commit 51b63f4

Browse files
committed
fix(storage): pass 0x-prefixed storage slot instead of base 10
1 parent bc00fc3 commit 51b63f4

File tree

8 files changed

+492
-192
lines changed

8 files changed

+492
-192
lines changed

dist/index.js

Lines changed: 350 additions & 85 deletions
Large diffs are not rendered by default.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/licenses.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,6 +1524,33 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
15241524
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15251525

15261526

1527+
shell-quote
1528+
MIT
1529+
The MIT License
1530+
1531+
Copyright (c) 2013 James Halliday ([email protected])
1532+
1533+
Permission is hereby granted, free of charge,
1534+
to any person obtaining a copy of this software and
1535+
associated documentation files (the "Software"), to
1536+
deal in the Software without restriction, including
1537+
without limitation the rights to use, copy, modify,
1538+
merge, publish, distribute, sublicense, and/or sell
1539+
copies of the Software, and to permit persons to whom
1540+
the Software is furnished to do so,
1541+
subject to the following conditions:
1542+
1543+
The above copyright notice and this permission notice
1544+
shall be included in all copies or substantial portions of the Software.
1545+
1546+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1547+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
1548+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
1549+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
1550+
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
1551+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
1552+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1553+
15271554
tmp
15281555
MIT
15291556
The MIT License (MIT)

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
"@octokit/core": "^4.2.0",
4949
"@solidity-parser/parser": "^0.16.0",
5050
"js-sha3": "^0.8.0",
51-
"lodash": "^4.17.21"
51+
"lodash": "^4.17.21",
52+
"shell-quote": "^1.8.1"
5253
},
5354
"devDependencies": {
5455
"@actions/exec": "^1.1.1",
@@ -58,6 +59,7 @@
5859
"@types/jest": "^29.5.1",
5960
"@types/lodash": "^4.14.194",
6061
"@types/node": "^18.16.3",
62+
"@types/shell-quote": "^1.7.1",
6163
"@typescript-eslint/eslint-plugin": "^5.59.2",
6264
"@typescript-eslint/parser": "^5.59.2",
6365
"@vercel/ncc": "^0.36.1",
@@ -87,4 +89,4 @@
8789
},
8890
"verbose": true
8991
}
90-
}
92+
}

src/check.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ const checkAddedStorageSlots = async (
287287

288288
const storage: { [slot: string]: string } = {};
289289
for (const diff of sortDiffs(added)) {
290-
const slot = diff.location.slot.toString();
290+
const slot = "0x" + diff.location.slot.toString(16);
291291

292292
const memoized = storage[slot];
293293
let value = memoized ?? (await provider.getStorageAt(address, slot));

src/index.ts

Lines changed: 97 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ const provider = rpcUrl ? getDefaultProvider(rpcUrl) : undefined;
4040
let srcContent: string;
4141
let refCommitHash: string | undefined = undefined;
4242

43+
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
44+
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
45+
// throw an uncaught exception.
46+
process.on("uncaughtException", (error) => {
47+
core.setFailed(error);
48+
if (error.stack) core.debug(error.stack);
49+
50+
return process.exit();
51+
});
52+
4353
async function run() {
4454
core.startGroup(`Generate storage layout of contract "${contract}" using foundry forge`);
4555
core.info(`Start forge process`);
@@ -48,130 +58,115 @@ async function run() {
4858
const cmpLayout = parseLayout(cmpContent);
4959
core.endGroup();
5060

51-
try {
52-
const localReportPath = resolve(outReport);
53-
fs.writeFileSync(localReportPath, cmpContent);
61+
const localReportPath = resolve(outReport);
62+
fs.writeFileSync(localReportPath, cmpContent);
5463

55-
core.startGroup(`Upload new report from "${localReportPath}" as artifact named "${outReport}"`);
56-
const uploadResponse = await artifactClient.uploadArtifact(
57-
outReport,
58-
[localReportPath],
59-
dirname(localReportPath),
60-
{ continueOnError: false }
61-
);
64+
core.startGroup(`Upload new report from "${localReportPath}" as artifact named "${outReport}"`);
65+
const uploadResponse = await artifactClient.uploadArtifact(
66+
outReport,
67+
[localReportPath],
68+
dirname(localReportPath),
69+
{ continueOnError: false }
70+
);
6271

63-
if (uploadResponse.failedItems.length > 0)
64-
throw Error("Failed to upload storage layout report.");
72+
if (uploadResponse.failedItems.length > 0) throw Error("Failed to upload storage layout report.");
6573

66-
core.info(`Artifact ${uploadResponse.artifactName} has been successfully uploaded!`);
67-
} catch (error: any) {
68-
return core.setFailed(error.message);
69-
}
74+
core.info(`Artifact ${uploadResponse.artifactName} has been successfully uploaded!`);
7075
core.endGroup();
7176

7277
// cannot use artifactClient because downloads are limited to uploads in the same workflow run
7378
// cf. https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts#downloading-or-deleting-artifacts
7479
let artifactId: number | null = null;
7580
if (context.eventName === "pull_request") {
76-
try {
81+
core.startGroup(
82+
`Searching artifact "${baseReport}" on repository "${repository}", on branch "${baseBranch}"`
83+
);
84+
// Note that the artifacts are returned in most recent first order.
85+
for await (const res of octokit.paginate.iterator(octokit.rest.actions.listArtifactsForRepo, {
86+
owner,
87+
repo,
88+
})) {
89+
const artifact = res.data.find(
90+
(artifact) => !artifact.expired && artifact.name === baseReport
91+
);
92+
if (!artifact) {
93+
await new Promise((resolve) => setTimeout(resolve, 800)); // avoid reaching the API rate limit
94+
95+
continue;
96+
}
97+
98+
artifactId = artifact.id;
99+
refCommitHash = artifact.workflow_run?.head_sha;
100+
core.info(
101+
`Found artifact named "${baseReport}" with ID "${artifactId}" from commit "${refCommitHash}"`
102+
);
103+
break;
104+
}
105+
core.endGroup();
106+
107+
if (artifactId) {
77108
core.startGroup(
78-
`Searching artifact "${baseReport}" on repository "${repository}", on branch "${baseBranch}"`
109+
`Downloading artifact "${baseReport}" of repository "${repository}" with ID "${artifactId}"`
79110
);
80-
// Note that the artifacts are returned in most recent first order.
81-
for await (const res of octokit.paginate.iterator(octokit.rest.actions.listArtifactsForRepo, {
111+
const res = await octokit.rest.actions.downloadArtifact({
82112
owner,
83113
repo,
84-
})) {
85-
const artifact = res.data.find(
86-
(artifact) => !artifact.expired && artifact.name === baseReport
87-
);
88-
if (!artifact) {
89-
await new Promise((resolve) => setTimeout(resolve, 800)); // avoid reaching the API rate limit
90-
91-
continue;
92-
}
93-
94-
artifactId = artifact.id;
95-
refCommitHash = artifact.workflow_run?.head_sha;
96-
core.info(
97-
`Found artifact named "${baseReport}" with ID "${artifactId}" from commit "${refCommitHash}"`
98-
);
99-
break;
114+
artifact_id: artifactId,
115+
archive_format: "zip",
116+
});
117+
118+
// @ts-ignore data is unknown
119+
const zip = new Zip(Buffer.from(res.data));
120+
for (const entry of zip.getEntries()) {
121+
core.info(`Loading storage layout report from "${entry.entryName}"`);
122+
srcContent = zip.readAsText(entry);
100123
}
101124
core.endGroup();
102-
103-
if (artifactId) {
104-
core.startGroup(
105-
`Downloading artifact "${baseReport}" of repository "${repository}" with ID "${artifactId}"`
106-
);
107-
const res = await octokit.rest.actions.downloadArtifact({
108-
owner,
109-
repo,
110-
artifact_id: artifactId,
111-
archive_format: "zip",
112-
});
113-
114-
// @ts-ignore data is unknown
115-
const zip = new Zip(Buffer.from(res.data));
116-
for (const entry of zip.getEntries()) {
117-
core.info(`Loading storage layout report from "${entry.entryName}"`);
118-
srcContent = zip.readAsText(entry);
119-
}
120-
core.endGroup();
121-
} else return core.error(`No workflow run found with an artifact named "${baseReport}"`);
122-
} catch (error: any) {
123-
return core.setFailed(error.message);
124-
}
125+
} else throw Error(`No workflow run found with an artifact named "${baseReport}"`);
125126
}
126127

127-
try {
128-
core.info(`Mapping reference storage layout report`);
129-
const srcLayout = parseLayout(srcContent);
130-
core.endGroup();
131-
132-
core.startGroup("Check storage layout");
133-
const diffs = await checkLayouts(srcLayout, cmpLayout, {
134-
address,
135-
provider,
136-
checkRemovals: failOnRemoval,
137-
});
128+
core.info(`Mapping reference storage layout report`);
129+
const srcLayout = parseLayout(srcContent);
130+
core.endGroup();
138131

139-
if (diffs.length > 0) {
140-
core.info(`Parse source code`);
141-
const cmpDef = parseSource(contractAbs);
142-
143-
const formattedDiffs = diffs.map((diff) => {
144-
const formattedDiff = formatDiff(cmpDef, diff);
145-
146-
const title = diffTitles[formattedDiff.type];
147-
const level = diffLevels[formattedDiff.type] || "error";
148-
core[level](formattedDiff.message, {
149-
title,
150-
file: cmpDef.path,
151-
startLine: formattedDiff.loc.start.line,
152-
endLine: formattedDiff.loc.end.line,
153-
startColumn: formattedDiff.loc.start.column,
154-
endColumn: formattedDiff.loc.end.column,
155-
});
156-
157-
return formattedDiff;
132+
core.startGroup("Check storage layout");
133+
const diffs = await checkLayouts(srcLayout, cmpLayout, {
134+
address,
135+
provider,
136+
checkRemovals: failOnRemoval,
137+
});
138+
139+
if (diffs.length > 0) {
140+
core.info(`Parse source code`);
141+
const cmpDef = parseSource(contractAbs);
142+
143+
const formattedDiffs = diffs.map((diff) => {
144+
const formattedDiff = formatDiff(cmpDef, diff);
145+
146+
const title = diffTitles[formattedDiff.type];
147+
const level = diffLevels[formattedDiff.type] || "error";
148+
core[level](formattedDiff.message, {
149+
title,
150+
file: cmpDef.path,
151+
startLine: formattedDiff.loc.start.line,
152+
endLine: formattedDiff.loc.end.line,
153+
startColumn: formattedDiff.loc.start.column,
154+
endColumn: formattedDiff.loc.end.column,
158155
});
159156

160-
if (
161-
formattedDiffs.filter((diff) => diffLevels[diff.type] === "error").length > 0 ||
162-
(failOnRemoval &&
163-
formattedDiffs.filter((diff) => diff.type === StorageLayoutDiffType.VARIABLE_REMOVED)
164-
.length > 0)
165-
)
166-
return core.setFailed(
167-
"Unsafe storage layout changes detected. Please see above for details."
168-
);
169-
}
157+
return formattedDiff;
158+
});
170159

171-
core.endGroup();
172-
} catch (error: any) {
173-
core.setFailed(error.message);
160+
if (
161+
formattedDiffs.filter((diff) => diffLevels[diff.type] === "error").length > 0 ||
162+
(failOnRemoval &&
163+
formattedDiffs.filter((diff) => diff.type === StorageLayoutDiffType.VARIABLE_REMOVED)
164+
.length > 0)
165+
)
166+
throw Error("Unsafe storage layout changes detected. Please see above for details.");
174167
}
168+
169+
core.endGroup();
175170
}
176171

177172
run();

src/input.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { execSync } from "child_process";
22
import fs from "fs";
3+
import { quote } from "shell-quote";
34

45
import * as parser from "@solidity-parser/parser";
56
import { ContractDefinition } from "@solidity-parser/parser/src/ast-types";
@@ -19,7 +20,7 @@ const exactify = (variable: StorageVariable): StorageVariableExact => ({
1920
});
2021

2122
export const createLayout = (contract: string, cwd = ".") => {
22-
return execSync(`forge inspect ${contract} storage-layout`, {
23+
return execSync(quote(["forge", "inspect", contract, "storage-layout"]), {
2324
encoding: "utf-8",
2425
cwd,
2526
});

yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,11 @@
12401240
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
12411241
integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
12421242

1243+
"@types/shell-quote@^1.7.1":
1244+
version "1.7.1"
1245+
resolved "https://registry.yarnpkg.com/@types/shell-quote/-/shell-quote-1.7.1.tgz#2d059091214a02c29f003f591032172b2aff77e8"
1246+
integrity sha512-SWZ2Nom1pkyXCDohRSrkSKvDh8QOG9RfAsrt5/NsPQC4UQJ55eG0qClA40I+Gkez4KTQ0uDUT8ELRXThf3J5jw==
1247+
12431248
"@types/stack-utils@^2.0.0":
12441249
version "2.0.1"
12451250
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
@@ -3948,6 +3953,11 @@ shebang-regex@^3.0.0:
39483953
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
39493954
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
39503955

3956+
shell-quote@^1.8.1:
3957+
version "1.8.1"
3958+
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
3959+
integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
3960+
39513961
side-channel@^1.0.4:
39523962
version "1.0.4"
39533963
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"

0 commit comments

Comments
 (0)