@openzeppelin/upgrades-core 1.26.1 → 1.27.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/artifacts/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol/ITransparentUpgradeableProxy.json +126 -0
- package/dist/cli/cli.d.ts +3 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +9 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/cli.test.d.ts +2 -0
- package/dist/cli/cli.test.d.ts.map +1 -0
- package/dist/cli/cli.test.js +52 -0
- package/dist/cli/cli.test.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/validate/build-info-file.d.ts +22 -0
- package/dist/cli/validate/build-info-file.d.ts.map +1 -0
- package/dist/cli/validate/build-info-file.js +110 -0
- package/dist/cli/validate/build-info-file.js.map +1 -0
- package/dist/cli/validate/build-info-file.test.d.ts +2 -0
- package/dist/cli/validate/build-info-file.test.d.ts.map +1 -0
- package/dist/cli/validate/build-info-file.test.js +175 -0
- package/dist/cli/validate/build-info-file.test.js.map +1 -0
- package/dist/cli/validate/contract-report.d.ts +31 -0
- package/dist/cli/validate/contract-report.d.ts.map +1 -0
- package/dist/cli/validate/contract-report.js +107 -0
- package/dist/cli/validate/contract-report.js.map +1 -0
- package/dist/cli/validate/error.d.ts +4 -0
- package/dist/cli/validate/error.d.ts.map +1 -0
- package/dist/cli/validate/error.js +8 -0
- package/dist/cli/validate/error.js.map +1 -0
- package/dist/cli/validate/project-report.d.ts +18 -0
- package/dist/cli/validate/project-report.d.ts.map +1 -0
- package/dist/cli/validate/project-report.js +42 -0
- package/dist/cli/validate/project-report.js.map +1 -0
- package/dist/cli/validate/project-report.test.d.ts +2 -0
- package/dist/cli/validate/project-report.test.d.ts.map +1 -0
- package/dist/cli/validate/project-report.test.js +90 -0
- package/dist/cli/validate/project-report.test.js.map +1 -0
- package/dist/cli/validate/upgradeability-assessment.d.ts +19 -0
- package/dist/cli/validate/upgradeability-assessment.d.ts.map +1 -0
- package/dist/cli/validate/upgradeability-assessment.js +103 -0
- package/dist/cli/validate/upgradeability-assessment.js.map +1 -0
- package/dist/cli/validate/validate-upgrade-safety.d.ts +16 -0
- package/dist/cli/validate/validate-upgrade-safety.d.ts.map +1 -0
- package/dist/cli/validate/validate-upgrade-safety.js +23 -0
- package/dist/cli/validate/validate-upgrade-safety.js.map +1 -0
- package/dist/cli/validate/validate-upgrade-safety.test.d.ts +2 -0
- package/dist/cli/validate/validate-upgrade-safety.test.d.ts.map +1 -0
- package/dist/cli/validate/validate-upgrade-safety.test.js +74 -0
- package/dist/cli/validate/validate-upgrade-safety.test.js.map +1 -0
- package/dist/cli/validate/validations.d.ts +11 -0
- package/dist/cli/validate/validations.d.ts.map +1 -0
- package/dist/cli/validate/validations.js +40 -0
- package/dist/cli/validate/validations.js.map +1 -0
- package/dist/cli/validate/validations.test.d.ts +2 -0
- package/dist/cli/validate/validations.test.d.ts.map +1 -0
- package/dist/cli/validate/validations.test.js +64 -0
- package/dist/cli/validate/validations.test.js.map +1 -0
- package/dist/cli/validate.d.ts +16 -0
- package/dist/cli/validate.d.ts.map +1 -0
- package/dist/cli/validate.js +119 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/cli/validate.test.d.ts +2 -0
- package/dist/cli/validate.test.d.ts.map +1 -0
- package/dist/cli/validate.test.js +108 -0
- package/dist/cli/validate.test.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest.test.js +2 -1
- package/dist/manifest.test.js.map +1 -1
- package/dist/utils/annotations.d.ts +16 -0
- package/dist/utils/annotations.d.ts.map +1 -0
- package/dist/utils/annotations.js +46 -0
- package/dist/utils/annotations.js.map +1 -0
- package/dist/utils/contract-name.d.ts +2 -0
- package/dist/utils/contract-name.d.ts.map +1 -0
- package/dist/utils/contract-name.js +8 -0
- package/dist/utils/contract-name.js.map +1 -0
- package/dist/validate/index.d.ts +1 -1
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +2 -1
- package/dist/validate/index.js.map +1 -1
- package/dist/validate/query.d.ts +8 -1
- package/dist/validate/query.d.ts.map +1 -1
- package/dist/validate/query.js +16 -3
- package/dist/validate/query.js.map +1 -1
- package/dist/validate/run.d.ts +1 -4
- package/dist/validate/run.d.ts.map +1 -1
- package/dist/validate/run.js +8 -38
- package/dist/validate/run.js.map +1 -1
- package/package.json +9 -3
- package/src/cli/cli.test.ts +57 -0
- package/src/cli/cli.test.ts.md +138 -0
- package/src/cli/cli.test.ts.snap +0 -0
- package/src/cli/cli.ts +9 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/validate/build-info-file.test.ts +211 -0
- package/src/cli/validate/build-info-file.ts +145 -0
- package/src/cli/validate/contract-report.ts +130 -0
- package/src/cli/validate/error.ts +3 -0
- package/src/cli/validate/project-report.test.ts +124 -0
- package/src/cli/validate/project-report.test.ts.md +40 -0
- package/src/cli/validate/project-report.test.ts.snap +0 -0
- package/src/cli/validate/project-report.ts +44 -0
- package/src/cli/validate/upgradeability-assessment.ts +137 -0
- package/src/cli/validate/validate-upgrade-safety.test.ts +102 -0
- package/src/cli/validate/validate-upgrade-safety.test.ts.md +98 -0
- package/src/cli/validate/validate-upgrade-safety.test.ts.snap +0 -0
- package/src/cli/validate/validate-upgrade-safety.ts +29 -0
- package/src/cli/validate/validations.test.ts +72 -0
- package/src/cli/validate/validations.test.ts.md +87 -0
- package/src/cli/validate/validations.test.ts.snap +0 -0
- package/src/cli/validate/validations.ts +53 -0
- package/src/cli/validate.test.ts +112 -0
- package/src/cli/validate.ts +134 -0
- package/src/index.ts +2 -0
- package/src/manifest.test.ts +2 -1
- package/src/utils/annotations.ts +47 -0
- package/src/utils/contract-name.ts +3 -0
- package/src/validate/index.ts +1 -0
- package/src/validate/query.ts +16 -3
- package/src/validate/run.ts +5 -41
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import test, { ExecutionContext } from 'ava';
|
|
2
|
+
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import rimrafAsync from 'rimraf';
|
|
5
|
+
import util from 'util';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { BuildInfoFile, getBuildInfoFiles } from './build-info-file';
|
|
9
|
+
|
|
10
|
+
const rimraf = util.promisify(rimrafAsync);
|
|
11
|
+
|
|
12
|
+
test.beforeEach(async t => {
|
|
13
|
+
process.chdir(await fs.mkdtemp(path.join(os.tmpdir(), `upgrades-core-test-${t.title.replace(/\s/g, '-')}-`)));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test.afterEach(async () => {
|
|
17
|
+
await rimraf(process.cwd());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const BUILD_INFO = {
|
|
21
|
+
input: {
|
|
22
|
+
language: 'Solidity',
|
|
23
|
+
sources: {
|
|
24
|
+
'mypath/MyContract.sol': {
|
|
25
|
+
content: 'contract MyContract {}',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
settings: {
|
|
29
|
+
outputSelection: {
|
|
30
|
+
'*': {
|
|
31
|
+
'*': ['storageLayout'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
output: {
|
|
37
|
+
sources: {
|
|
38
|
+
'mypath/MyContract.sol': {
|
|
39
|
+
ast: {},
|
|
40
|
+
id: 123,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const BUILD_INFO_2 = {
|
|
47
|
+
input: {
|
|
48
|
+
language: 'Solidity',
|
|
49
|
+
sources: {
|
|
50
|
+
'mypath/MyContract.sol': {
|
|
51
|
+
content: 'contract MyContractModified {}',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
settings: {
|
|
55
|
+
outputSelection: {
|
|
56
|
+
'*': {
|
|
57
|
+
'*': ['storageLayout'],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
output: {
|
|
63
|
+
sources: {
|
|
64
|
+
'mypath/MyContract.sol': {
|
|
65
|
+
ast: {},
|
|
66
|
+
id: 456,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const BUILD_INFO_NO_LAYOUT = {
|
|
73
|
+
input: {
|
|
74
|
+
language: 'Solidity',
|
|
75
|
+
sources: {
|
|
76
|
+
'mypath/MyContract.sol': {
|
|
77
|
+
content: 'contract MyContract {}',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
settings: {
|
|
81
|
+
outputSelection: {
|
|
82
|
+
'*': {
|
|
83
|
+
'*': ['abi'],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
output: {
|
|
89
|
+
sources: {
|
|
90
|
+
'mypath/MyContract.sol': {
|
|
91
|
+
ast: {},
|
|
92
|
+
id: 123,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
test.serial('get build info files - default hardhat', async t => {
|
|
99
|
+
await fs.mkdir('artifacts/build-info', { recursive: true });
|
|
100
|
+
await fs.mkdir('out/build-info', { recursive: true }); // should be ignored since it's empty
|
|
101
|
+
|
|
102
|
+
await fs.writeFile('artifacts/build-info/build-info.json', JSON.stringify(BUILD_INFO));
|
|
103
|
+
await fs.writeFile('artifacts/build-info/build-info-2.json', JSON.stringify(BUILD_INFO_2));
|
|
104
|
+
|
|
105
|
+
const buildInfoFiles = await getBuildInfoFiles();
|
|
106
|
+
|
|
107
|
+
assertBuildInfoFiles(t, buildInfoFiles);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test.serial('get build info files - default foundry', async t => {
|
|
111
|
+
await fs.mkdir('out/build-info', { recursive: true });
|
|
112
|
+
await fs.mkdir('artifacts/build-info', { recursive: true }); // should be ignored since it's empty
|
|
113
|
+
|
|
114
|
+
await fs.writeFile('out/build-info/build-info.json', JSON.stringify(BUILD_INFO));
|
|
115
|
+
await fs.writeFile('out/build-info/build-info-2.json', JSON.stringify(BUILD_INFO_2));
|
|
116
|
+
|
|
117
|
+
const buildInfoFiles = await getBuildInfoFiles();
|
|
118
|
+
|
|
119
|
+
assertBuildInfoFiles(t, buildInfoFiles);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test.serial('get build info files - both hardhat and foundry dirs exist', async t => {
|
|
123
|
+
await fs.mkdir('artifacts/build-info', { recursive: true });
|
|
124
|
+
await fs.writeFile('artifacts/build-info/build-info.json', JSON.stringify(BUILD_INFO));
|
|
125
|
+
|
|
126
|
+
await fs.mkdir('out/build-info', { recursive: true });
|
|
127
|
+
await fs.writeFile('out/build-info/build-info-2.json', JSON.stringify(BUILD_INFO_2));
|
|
128
|
+
|
|
129
|
+
const error = await t.throwsAsync(getBuildInfoFiles());
|
|
130
|
+
t.true(error?.message.includes('Found both Hardhat and Foundry build info directories'));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test.serial('get build info files - no default dirs exist', async t => {
|
|
134
|
+
const error = await t.throwsAsync(getBuildInfoFiles());
|
|
135
|
+
t.true(error?.message.includes('Could not find the default Hardhat or Foundry build info directory'));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test.serial('get build info files - override with custom relative path', async t => {
|
|
139
|
+
await fs.mkdir('artifacts/build-info', { recursive: true });
|
|
140
|
+
await fs.mkdir('out/build-info', { recursive: true });
|
|
141
|
+
|
|
142
|
+
await fs.mkdir('custom/build-info', { recursive: true });
|
|
143
|
+
|
|
144
|
+
await fs.writeFile('custom/build-info/build-info.json', JSON.stringify(BUILD_INFO));
|
|
145
|
+
await fs.writeFile('custom/build-info/build-info-2.json', JSON.stringify(BUILD_INFO_2));
|
|
146
|
+
|
|
147
|
+
const buildInfoFiles = await getBuildInfoFiles('custom/build-info');
|
|
148
|
+
|
|
149
|
+
assertBuildInfoFiles(t, buildInfoFiles);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test.serial('get build info files - override with custom absolute path', async t => {
|
|
153
|
+
await fs.mkdir('artifacts/build-info', { recursive: true });
|
|
154
|
+
await fs.mkdir('out/build-info', { recursive: true });
|
|
155
|
+
|
|
156
|
+
await fs.mkdir('custom/build-info', { recursive: true });
|
|
157
|
+
|
|
158
|
+
await fs.writeFile('custom/build-info/build-info.json', JSON.stringify(BUILD_INFO));
|
|
159
|
+
await fs.writeFile('custom/build-info/build-info-2.json', JSON.stringify(BUILD_INFO_2));
|
|
160
|
+
|
|
161
|
+
const buildInfoFiles = await getBuildInfoFiles(path.join(process.cwd(), 'custom/build-info'));
|
|
162
|
+
|
|
163
|
+
assertBuildInfoFiles(t, buildInfoFiles);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test.serial('invalid build info file', async t => {
|
|
167
|
+
await fs.mkdir('invalid-build-info', { recursive: true });
|
|
168
|
+
|
|
169
|
+
await fs.writeFile('invalid-build-info/invalid.json', JSON.stringify({ output: {} }));
|
|
170
|
+
const error = await t.throwsAsync(getBuildInfoFiles('invalid-build-info'));
|
|
171
|
+
t.true(error?.message.includes('must contain Solidity compiler input and output'));
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test.serial('dir does not exist', async t => {
|
|
175
|
+
const error = await t.throwsAsync(getBuildInfoFiles('invalid-dir'));
|
|
176
|
+
t.true(error?.message.includes('does not exist'));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test.serial('no build info files', async t => {
|
|
180
|
+
await fs.mkdir('empty-dir', { recursive: true });
|
|
181
|
+
await fs.writeFile('empty-dir/notjson.txt', 'abc');
|
|
182
|
+
|
|
183
|
+
const error = await t.throwsAsync(getBuildInfoFiles('empty-dir'));
|
|
184
|
+
t.true(error?.message.includes('does not contain any build info files'));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test.serial('no storage layout', async t => {
|
|
188
|
+
await fs.mkdir('no-storage-layout', { recursive: true });
|
|
189
|
+
await fs.writeFile('no-storage-layout/build-info.json', JSON.stringify(BUILD_INFO_NO_LAYOUT));
|
|
190
|
+
|
|
191
|
+
const error = await t.throwsAsync(getBuildInfoFiles('no-storage-layout'));
|
|
192
|
+
t.true(error?.message.includes('does not contain storage layout'));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
function assertBuildInfoFiles(t: ExecutionContext, buildInfoFiles: BuildInfoFile[]) {
|
|
196
|
+
t.is(buildInfoFiles.length, 2);
|
|
197
|
+
|
|
198
|
+
const buildInfoFile1 = buildInfoFiles.find(
|
|
199
|
+
b => b.input.sources['mypath/MyContract.sol'].content === 'contract MyContract {}',
|
|
200
|
+
);
|
|
201
|
+
const buildInfoFile2 = buildInfoFiles.find(
|
|
202
|
+
b => b.input.sources['mypath/MyContract.sol'].content === 'contract MyContractModified {}',
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (buildInfoFile1 === undefined || buildInfoFile2 === undefined) {
|
|
206
|
+
t.fail('build info files not found');
|
|
207
|
+
} else {
|
|
208
|
+
t.is(buildInfoFile1.output.sources['mypath/MyContract.sol'].id, 123);
|
|
209
|
+
t.is(buildInfoFile2.output.sources['mypath/MyContract.sol'].id, 456);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { SolcOutput, SolcInput } from '../..';
|
|
2
|
+
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { ValidateCommandError } from './error';
|
|
6
|
+
|
|
7
|
+
const HARDHAT_COMPILE_COMMAND = 'npx hardhat clean && npx hardhat compile';
|
|
8
|
+
const FOUNDRY_COMPILE_COMMAND = 'forge clean && forge build --build-info --extra-output storageLayout';
|
|
9
|
+
const STORAGE_LAYOUT_HELP = `\
|
|
10
|
+
If using Hardhat, include the 'storageLayout' output selection in your Hardhat config:
|
|
11
|
+
module.exports = {
|
|
12
|
+
solidity: {
|
|
13
|
+
settings: {
|
|
14
|
+
outputSelection: {
|
|
15
|
+
'*': {
|
|
16
|
+
'*': ['storageLayout'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
Then recompile your contracts with '${HARDHAT_COMPILE_COMMAND}' and try again.
|
|
23
|
+
|
|
24
|
+
If using Foundry, recompile your contracts with '${FOUNDRY_COMPILE_COMMAND}' and try again.`;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A build info file containing Solidity compiler input and output JSON objects.
|
|
28
|
+
*/
|
|
29
|
+
export interface BuildInfoFile {
|
|
30
|
+
/**
|
|
31
|
+
* The Solidity compiler input JSON object.
|
|
32
|
+
*/
|
|
33
|
+
input: SolcInput;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The Solidity compiler output JSON object.
|
|
37
|
+
*/
|
|
38
|
+
output: SolcOutput;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Gets the build info files from the build info directory.
|
|
43
|
+
*
|
|
44
|
+
* @param buildInfoDir Build info directory, or undefined to use the default Hardhat or Foundry build-info dir.
|
|
45
|
+
* @returns The build info files with Solidity compiler input and output.
|
|
46
|
+
*/
|
|
47
|
+
export async function getBuildInfoFiles(buildInfoDir?: string) {
|
|
48
|
+
const dir = await findDir(buildInfoDir);
|
|
49
|
+
const jsonFiles = await getJsonFiles(dir);
|
|
50
|
+
return await readBuildInfo(jsonFiles);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function findDir(buildInfoDir: string | undefined) {
|
|
54
|
+
if (buildInfoDir !== undefined && !(await hasJsonFiles(buildInfoDir))) {
|
|
55
|
+
throw new ValidateCommandError(
|
|
56
|
+
`The directory '${buildInfoDir}' does not exist or does not contain any build info files.`,
|
|
57
|
+
() =>
|
|
58
|
+
`Compile your contracts with '${HARDHAT_COMPILE_COMMAND}' or '${FOUNDRY_COMPILE_COMMAND}' and try again with the correct path to the build info directory.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const dir = buildInfoDir ?? (await findDefaultDir());
|
|
62
|
+
return dir;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function findDefaultDir() {
|
|
66
|
+
const hardhatRelativeDir = path.join('artifacts', 'build-info');
|
|
67
|
+
const foundryRelativeDir = path.join('out', 'build-info');
|
|
68
|
+
|
|
69
|
+
const hardhatDir = path.join(process.cwd(), hardhatRelativeDir);
|
|
70
|
+
const foundryDir = path.join(process.cwd(), foundryRelativeDir);
|
|
71
|
+
|
|
72
|
+
const hasHardhatBuildInfo = await hasJsonFiles(hardhatDir);
|
|
73
|
+
const hasFoundryBuildInfo = await hasJsonFiles(foundryDir);
|
|
74
|
+
|
|
75
|
+
if (hasHardhatBuildInfo && hasFoundryBuildInfo) {
|
|
76
|
+
throw new ValidateCommandError(
|
|
77
|
+
`Found both Hardhat and Foundry build info directories: '${hardhatRelativeDir}' and '${foundryRelativeDir}'.`,
|
|
78
|
+
() => `Specify the build info directory that you want to validate.`,
|
|
79
|
+
);
|
|
80
|
+
} else if (hasHardhatBuildInfo) {
|
|
81
|
+
return hardhatDir;
|
|
82
|
+
} else if (hasFoundryBuildInfo) {
|
|
83
|
+
return foundryDir;
|
|
84
|
+
} else {
|
|
85
|
+
throw new ValidateCommandError(
|
|
86
|
+
`Could not find the default Hardhat or Foundry build info directory.`,
|
|
87
|
+
() =>
|
|
88
|
+
`Compile your contracts with '${HARDHAT_COMPILE_COMMAND}' or '${FOUNDRY_COMPILE_COMMAND}', or specify the build info directory that you want to validate.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function hasJsonFiles(dir: string) {
|
|
94
|
+
return (await exists(dir)) && (await getJsonFiles(dir)).length > 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function exists(dir: string) {
|
|
98
|
+
try {
|
|
99
|
+
await fs.access(dir);
|
|
100
|
+
return true;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function getJsonFiles(dir: string): Promise<string[]> {
|
|
107
|
+
const files = await fs.readdir(dir);
|
|
108
|
+
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
|
109
|
+
return jsonFiles.map(file => path.join(dir, file));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function readBuildInfo(buildInfoFilePaths: string[]) {
|
|
113
|
+
const buildInfoFiles: BuildInfoFile[] = [];
|
|
114
|
+
|
|
115
|
+
for (const buildInfoFilePath of buildInfoFilePaths) {
|
|
116
|
+
const buildInfoJson = await readJSON(buildInfoFilePath);
|
|
117
|
+
if (buildInfoJson.input === undefined || buildInfoJson.output === undefined) {
|
|
118
|
+
throw new ValidateCommandError(
|
|
119
|
+
`Build info file ${buildInfoFilePath} must contain Solidity compiler input and output.`,
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
if (!hasStorageLayoutSetting(buildInfoJson)) {
|
|
123
|
+
throw new ValidateCommandError(
|
|
124
|
+
`Build info file ${buildInfoFilePath} does not contain storage layout.`,
|
|
125
|
+
() => STORAGE_LAYOUT_HELP,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
buildInfoFiles.push({
|
|
130
|
+
input: buildInfoJson.input,
|
|
131
|
+
output: buildInfoJson.output,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return buildInfoFiles;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function hasStorageLayoutSetting(buildInfoJson: any) {
|
|
139
|
+
const o = buildInfoJson.input.settings?.outputSelection;
|
|
140
|
+
return o?.['*']?.['*'] && (o['*']['*'].includes('storageLayout') || o['*']['*'].includes('*'));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function readJSON(path: string) {
|
|
144
|
+
return JSON.parse(await fs.readFile(path, 'utf8'));
|
|
145
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import _chalk from 'chalk';
|
|
2
|
+
import debug from '../../utils/debug';
|
|
3
|
+
import {
|
|
4
|
+
getContractVersion,
|
|
5
|
+
getStorageLayout,
|
|
6
|
+
ValidationOptions,
|
|
7
|
+
withValidationDefaults,
|
|
8
|
+
Version,
|
|
9
|
+
ValidationData,
|
|
10
|
+
ValidateUpgradeSafetyOptions,
|
|
11
|
+
getErrors,
|
|
12
|
+
UpgradeableContractErrorReport,
|
|
13
|
+
getStorageUpgradeReport,
|
|
14
|
+
} from '../..';
|
|
15
|
+
import { Report } from '../../standalone';
|
|
16
|
+
import { getUpgradeabilityAssessment } from './upgradeability-assessment';
|
|
17
|
+
import { SourceContract } from './validations';
|
|
18
|
+
import { LayoutCompatibilityReport } from '../../storage/report';
|
|
19
|
+
import { indent } from '../../utils/indent';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Report for an upgradeable contract.
|
|
23
|
+
* Contains the standalone report, and if there is a reference contract, the reference contract name and storage layout report.
|
|
24
|
+
*/
|
|
25
|
+
export class UpgradeableContractReport implements Report {
|
|
26
|
+
constructor(
|
|
27
|
+
readonly contract: string,
|
|
28
|
+
readonly reference: string | undefined,
|
|
29
|
+
readonly standaloneReport: UpgradeableContractErrorReport,
|
|
30
|
+
readonly storageLayoutReport: LayoutCompatibilityReport | undefined,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
get ok(): boolean {
|
|
34
|
+
return this.standaloneReport.ok && (this.storageLayoutReport === undefined || this.storageLayoutReport.ok);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Explain any errors in the report.
|
|
39
|
+
*/
|
|
40
|
+
explain(color = true): string {
|
|
41
|
+
const result: string[] = [];
|
|
42
|
+
const chalk = new _chalk.Instance({ level: color && _chalk.supportsColor ? _chalk.supportsColor.level : 0 });
|
|
43
|
+
const icon = this.ok ? chalk.green('✔') : chalk.red('✘');
|
|
44
|
+
if (this.reference === undefined) {
|
|
45
|
+
result.push(` ${icon} ${this.contract}`);
|
|
46
|
+
} else {
|
|
47
|
+
result.push(` ${icon} ${this.contract} (upgrades from ${this.reference})`);
|
|
48
|
+
}
|
|
49
|
+
if (!this.standaloneReport.ok) {
|
|
50
|
+
result.push(indent(this.standaloneReport.explain(color), 6));
|
|
51
|
+
}
|
|
52
|
+
if (this.storageLayoutReport !== undefined && !this.storageLayoutReport.ok) {
|
|
53
|
+
result.push(indent(this.storageLayoutReport.explain(color), 6));
|
|
54
|
+
}
|
|
55
|
+
return result.join('\n\n');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Gets upgradeble contract reports for the upgradeable contracts in the given set of source contracts.
|
|
61
|
+
* Only contracts that are detected as upgradeable will be included in the reports.
|
|
62
|
+
* Reports include upgradeable contracts regardless of whether they pass or fail upgrade safety checks.
|
|
63
|
+
*
|
|
64
|
+
* @param sourceContracts The source contracts to check, which must include all contracts that are referenced by the given contracts. Can also include non-upgradeable contracts, which will be ignored.
|
|
65
|
+
* @param opts The validation options.
|
|
66
|
+
* @returns The upgradeable contract reports.
|
|
67
|
+
*/
|
|
68
|
+
export function getContractReports(sourceContracts: SourceContract[], opts: ValidateUpgradeSafetyOptions) {
|
|
69
|
+
const upgradeableContractReports: UpgradeableContractReport[] = [];
|
|
70
|
+
for (const sourceContract of sourceContracts) {
|
|
71
|
+
const upgradeabilityAssessment = getUpgradeabilityAssessment(sourceContract, sourceContracts);
|
|
72
|
+
if (upgradeabilityAssessment.upgradeable) {
|
|
73
|
+
const reference = upgradeabilityAssessment.referenceContract;
|
|
74
|
+
const uups = upgradeabilityAssessment.uups;
|
|
75
|
+
const kind = uups ? 'uups' : 'transparent';
|
|
76
|
+
|
|
77
|
+
const report = getUpgradeableContractReport(sourceContract, reference, { ...opts, kind: kind });
|
|
78
|
+
if (report !== undefined) {
|
|
79
|
+
upgradeableContractReports.push(report);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return upgradeableContractReports;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getUpgradeableContractReport(
|
|
87
|
+
contract: SourceContract,
|
|
88
|
+
referenceContract: SourceContract | undefined,
|
|
89
|
+
opts: ValidationOptions,
|
|
90
|
+
): UpgradeableContractReport | undefined {
|
|
91
|
+
let version;
|
|
92
|
+
try {
|
|
93
|
+
version = getContractVersion(contract.validationData, contract.name);
|
|
94
|
+
} catch (e: any) {
|
|
95
|
+
if (e.message.endsWith('is abstract')) {
|
|
96
|
+
// Skip abstract upgradeable contracts - they will be validated as part of their caller contracts
|
|
97
|
+
// for the functions that are in use.
|
|
98
|
+
return undefined;
|
|
99
|
+
} else {
|
|
100
|
+
throw e;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
debug('Checking: ' + contract.fullyQualifiedName);
|
|
105
|
+
const standaloneReport = getStandaloneReport(contract.validationData, version, opts);
|
|
106
|
+
|
|
107
|
+
let reference: string | undefined;
|
|
108
|
+
let storageLayoutReport: LayoutCompatibilityReport | undefined;
|
|
109
|
+
|
|
110
|
+
if (opts.unsafeSkipStorageCheck !== true && referenceContract !== undefined) {
|
|
111
|
+
const layout = getStorageLayout(contract.validationData, version);
|
|
112
|
+
|
|
113
|
+
const referenceVersion = getContractVersion(referenceContract.validationData, referenceContract.name);
|
|
114
|
+
const referenceLayout = getStorageLayout(referenceContract.validationData, referenceVersion);
|
|
115
|
+
|
|
116
|
+
reference = referenceContract.fullyQualifiedName;
|
|
117
|
+
storageLayoutReport = getStorageUpgradeReport(referenceLayout, layout, withValidationDefaults(opts));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return new UpgradeableContractReport(contract.fullyQualifiedName, reference, standaloneReport, storageLayoutReport);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getStandaloneReport(
|
|
124
|
+
data: ValidationData,
|
|
125
|
+
version: Version,
|
|
126
|
+
opts: ValidationOptions,
|
|
127
|
+
): UpgradeableContractErrorReport {
|
|
128
|
+
const errors = getErrors(data, version, withValidationDefaults(opts));
|
|
129
|
+
return new UpgradeableContractErrorReport(errors);
|
|
130
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { getProjectReport } from './project-report';
|
|
2
|
+
import { UpgradeableContractErrorReport } from '../../validate';
|
|
3
|
+
|
|
4
|
+
import _test, { TestFn } from 'ava';
|
|
5
|
+
import { ContractDefinition } from 'solidity-ast';
|
|
6
|
+
import { findAll } from 'solidity-ast/utils';
|
|
7
|
+
import { artifacts } from 'hardhat';
|
|
8
|
+
|
|
9
|
+
import { SolcOutput } from '../../solc-api';
|
|
10
|
+
import { astDereferencer } from '../../ast-dereferencer';
|
|
11
|
+
import { extractStorageLayout } from '../../storage/extract';
|
|
12
|
+
import { StorageLayoutComparator } from '../../storage/compare';
|
|
13
|
+
import { StorageLayout, getDetailedLayout } from '../../storage/layout';
|
|
14
|
+
import { UpgradeableContractReport } from './contract-report';
|
|
15
|
+
|
|
16
|
+
interface Context {
|
|
17
|
+
extractStorageLayout: (contract: string) => ReturnType<typeof extractStorageLayout>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const test = _test as TestFn<Context>;
|
|
21
|
+
|
|
22
|
+
const dummyDecodeSrc = () => 'file.sol:1';
|
|
23
|
+
|
|
24
|
+
test.before(async t => {
|
|
25
|
+
const buildInfo = await artifacts.getBuildInfo('contracts/test/Storage.sol:Storage1');
|
|
26
|
+
if (buildInfo === undefined) {
|
|
27
|
+
throw new Error('Build info not found');
|
|
28
|
+
}
|
|
29
|
+
const solcOutput: SolcOutput = buildInfo.output;
|
|
30
|
+
const contracts: Record<string, ContractDefinition> = {};
|
|
31
|
+
for (const def of findAll('ContractDefinition', solcOutput.sources['contracts/test/Storage.sol'].ast)) {
|
|
32
|
+
contracts[def.name] = def;
|
|
33
|
+
}
|
|
34
|
+
const deref = astDereferencer(solcOutput);
|
|
35
|
+
t.context.extractStorageLayout = name => extractStorageLayout(contracts[name], dummyDecodeSrc, deref);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function getLayoutReport(original: StorageLayout, updated: StorageLayout) {
|
|
39
|
+
const originalDetailed = getDetailedLayout(original);
|
|
40
|
+
const updatedDetailed = getDetailedLayout(updated);
|
|
41
|
+
const comparator = new StorageLayoutComparator();
|
|
42
|
+
return comparator.compareLayouts(originalDetailed, updatedDetailed);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('get project report - ok - no upgradeable', async t => {
|
|
46
|
+
const report = getProjectReport([]);
|
|
47
|
+
t.true(report.ok);
|
|
48
|
+
t.is(report.numPassed, 0);
|
|
49
|
+
t.is(report.numTotal, 0);
|
|
50
|
+
t.is(report.explain(), 'No upgradeable contracts detected.');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('get project report - ok - console', async t => {
|
|
54
|
+
const report = getProjectReport([
|
|
55
|
+
new UpgradeableContractReport(
|
|
56
|
+
'mypath/MyContract.sol:MyContract1',
|
|
57
|
+
undefined,
|
|
58
|
+
new UpgradeableContractErrorReport([]),
|
|
59
|
+
undefined,
|
|
60
|
+
),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
t.true(report.ok);
|
|
64
|
+
t.is(report.numPassed, 1);
|
|
65
|
+
t.is(report.numTotal, 1);
|
|
66
|
+
t.regex(report.explain(), /SUCCESS \(1 upgradeable contracts detected, 1 passed, 0 failed\)/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('get project report - errors - console', async t => {
|
|
70
|
+
const v1 = t.context.extractStorageLayout('StorageUpgrade_Replace_V1');
|
|
71
|
+
const v2 = t.context.extractStorageLayout('StorageUpgrade_Replace_V2');
|
|
72
|
+
const layoutReport = getLayoutReport(v1, v2);
|
|
73
|
+
|
|
74
|
+
const report = getProjectReport([
|
|
75
|
+
new UpgradeableContractReport(
|
|
76
|
+
'mypath/MyContract.sol:MyContract',
|
|
77
|
+
undefined,
|
|
78
|
+
new UpgradeableContractErrorReport([
|
|
79
|
+
{
|
|
80
|
+
src: 'MyContract.sol:10',
|
|
81
|
+
kind: 'missing-public-upgradeto',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
src: 'MyContract.sol:20',
|
|
85
|
+
kind: 'delegatecall',
|
|
86
|
+
},
|
|
87
|
+
]),
|
|
88
|
+
undefined,
|
|
89
|
+
),
|
|
90
|
+
new UpgradeableContractReport('MyContract2', 'MyContract', new UpgradeableContractErrorReport([]), layoutReport),
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
t.false(report.ok);
|
|
94
|
+
t.is(report.numPassed, 0);
|
|
95
|
+
t.is(report.numTotal, 2);
|
|
96
|
+
t.snapshot(report.explain());
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('get project report - some passed', async t => {
|
|
100
|
+
const report = getProjectReport([
|
|
101
|
+
new UpgradeableContractReport(
|
|
102
|
+
'mypath/MyContract.sol:MyContract1',
|
|
103
|
+
undefined,
|
|
104
|
+
new UpgradeableContractErrorReport([
|
|
105
|
+
{
|
|
106
|
+
src: 'MyContract.sol:10',
|
|
107
|
+
kind: 'missing-public-upgradeto',
|
|
108
|
+
},
|
|
109
|
+
]),
|
|
110
|
+
undefined,
|
|
111
|
+
),
|
|
112
|
+
new UpgradeableContractReport(
|
|
113
|
+
'mypath/MyContract.sol:MyContract2',
|
|
114
|
+
undefined,
|
|
115
|
+
new UpgradeableContractErrorReport([]),
|
|
116
|
+
undefined,
|
|
117
|
+
),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
t.false(report.ok);
|
|
121
|
+
t.is(report.numPassed, 1);
|
|
122
|
+
t.is(report.numTotal, 2);
|
|
123
|
+
t.snapshot(report.explain());
|
|
124
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Snapshot report for `src/cli/validate/project-report.test.ts`
|
|
2
|
+
|
|
3
|
+
The actual snapshot is saved in `project-report.test.ts.snap`.
|
|
4
|
+
|
|
5
|
+
Generated by [AVA](https://avajs.dev).
|
|
6
|
+
|
|
7
|
+
## get project report - errors - console
|
|
8
|
+
|
|
9
|
+
> Snapshot 1
|
|
10
|
+
|
|
11
|
+
` ✘ mypath/MyContract.sol:MyContract␊
|
|
12
|
+
␊
|
|
13
|
+
MyContract.sol:10: Implementation is missing a public \`upgradeTo(address)\` function␊
|
|
14
|
+
Inherit UUPSUpgradeable to include this function in your contract␊
|
|
15
|
+
@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol␊
|
|
16
|
+
https://zpl.in/upgrades/error-008␊
|
|
17
|
+
␊
|
|
18
|
+
MyContract.sol:20: Use of delegatecall is not allowed␊
|
|
19
|
+
https://zpl.in/upgrades/error-002␊
|
|
20
|
+
␊
|
|
21
|
+
✘ MyContract2 (upgrades from MyContract)␊
|
|
22
|
+
␊
|
|
23
|
+
file.sol:1: Replaced \`x2\` with \`renamed\` of incompatible type␊
|
|
24
|
+
␊
|
|
25
|
+
FAILED (2 upgradeable contracts detected, 0 passed, 2 failed)`
|
|
26
|
+
|
|
27
|
+
## get project report - some passed
|
|
28
|
+
|
|
29
|
+
> Snapshot 1
|
|
30
|
+
|
|
31
|
+
` ✘ mypath/MyContract.sol:MyContract1␊
|
|
32
|
+
␊
|
|
33
|
+
MyContract.sol:10: Implementation is missing a public \`upgradeTo(address)\` function␊
|
|
34
|
+
Inherit UUPSUpgradeable to include this function in your contract␊
|
|
35
|
+
@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol␊
|
|
36
|
+
https://zpl.in/upgrades/error-008␊
|
|
37
|
+
␊
|
|
38
|
+
✔ mypath/MyContract.sol:MyContract2␊
|
|
39
|
+
␊
|
|
40
|
+
FAILED (2 upgradeable contract detected, 1 passed, 1 failed)`
|
|
Binary file
|