@openzeppelin/upgrades-core 1.35.1 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/cli/cli.test.js +258 -0
- package/dist/cli/cli.test.js.map +1 -1
- package/dist/cli/validate/build-info-file.d.ts +4 -0
- package/dist/cli/validate/build-info-file.d.ts.map +1 -1
- package/dist/cli/validate/build-info-file.js +7 -2
- package/dist/cli/validate/build-info-file.js.map +1 -1
- package/dist/cli/validate/contract-report.d.ts +6 -5
- package/dist/cli/validate/contract-report.d.ts.map +1 -1
- package/dist/cli/validate/contract-report.js +54 -13
- package/dist/cli/validate/contract-report.js.map +1 -1
- package/dist/cli/validate/default-exclude.d.ts +2 -0
- package/dist/cli/validate/default-exclude.d.ts.map +1 -0
- package/dist/cli/validate/default-exclude.js +6 -0
- package/dist/cli/validate/default-exclude.js.map +1 -0
- package/dist/cli/validate/find-contract.d.ts +7 -2
- package/dist/cli/validate/find-contract.d.ts.map +1 -1
- package/dist/cli/validate/find-contract.js +32 -4
- package/dist/cli/validate/find-contract.js.map +1 -1
- package/dist/cli/validate/upgradeability-assessment.d.ts +2 -1
- package/dist/cli/validate/upgradeability-assessment.d.ts.map +1 -1
- package/dist/cli/validate/upgradeability-assessment.js +2 -2
- package/dist/cli/validate/upgradeability-assessment.js.map +1 -1
- package/dist/cli/validate/validate-upgrade-safety.d.ts +12 -3
- package/dist/cli/validate/validate-upgrade-safety.d.ts.map +1 -1
- package/dist/cli/validate/validate-upgrade-safety.js +28 -7
- package/dist/cli/validate/validate-upgrade-safety.js.map +1 -1
- package/dist/cli/validate/validate-upgrade-safety.test.js +1 -1
- package/dist/cli/validate/validations.d.ts +1 -0
- package/dist/cli/validate/validations.d.ts.map +1 -1
- package/dist/cli/validate/validations.js +1 -0
- package/dist/cli/validate/validations.js.map +1 -1
- package/dist/cli/validate/validations.test.js +8 -4
- package/dist/cli/validate/validations.test.js.map +1 -1
- package/dist/cli/validate.d.ts +2 -0
- package/dist/cli/validate.d.ts.map +1 -1
- package/dist/cli/validate.js +39 -14
- package/dist/cli/validate.js.map +1 -1
- package/dist/cli/validate.test.js +20 -0
- package/dist/cli/validate.test.js.map +1 -1
- package/dist/storage/extract.js +1 -1
- package/dist/storage/extract.js.map +1 -1
- package/dist/utils/annotations.js +1 -1
- package/dist/utils/annotations.js.map +1 -1
- package/package.json +4 -3
- package/src/cli/cli.test.ts +414 -0
- package/src/cli/cli.test.ts.md +253 -2
- package/src/cli/cli.test.ts.snap +0 -0
- package/src/cli/validate/build-info-file.ts +15 -4
- package/src/cli/validate/contract-report.ts +71 -16
- package/src/cli/validate/default-exclude.ts +3 -0
- package/src/cli/validate/find-contract.ts +50 -4
- package/src/cli/validate/upgradeability-assessment.ts +3 -2
- package/src/cli/validate/validate-upgrade-safety.test.ts +1 -1
- package/src/cli/validate/validate-upgrade-safety.ts +38 -6
- package/src/cli/validate/validations.test.ts +8 -4
- package/src/cli/validate/validations.ts +2 -0
- package/src/cli/validate.test.ts +22 -0
- package/src/cli/validate.ts +46 -14
- package/src/storage/extract.ts +2 -1
- package/src/utils/annotations.ts +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { ValidationOptions, withValidationDefaults } from '../..';
|
|
2
3
|
|
|
3
4
|
import { getBuildInfoFiles } from './build-info-file';
|
|
@@ -22,10 +23,12 @@ export type SpecifiedContracts = {
|
|
|
22
23
|
* Validates the upgrade safety of all contracts in the build info dir's build info files.
|
|
23
24
|
* Only contracts that are detected as upgradeable will be validated.
|
|
24
25
|
*
|
|
25
|
-
* @param buildInfoDir
|
|
26
|
+
* @param buildInfoDir Path of build info directory, or undefined to use the default Hardhat or Foundry build-info dir.
|
|
26
27
|
* @param contract The name or fully qualified name of the contract to validate. If not specified, all upgradeable contracts in the build info directory will be validated.
|
|
27
28
|
* @param reference The name or fully qualified name of the reference contract to use for storage layout comparisons. Can only be used along with `contract`. If not specified, uses the `@custom:oz-upgrades-from` annotation in the contract that is being validated.
|
|
28
29
|
* @param opts Validation options, or undefined to use the default validation options.
|
|
30
|
+
* @param referenceBuildInfoDirs Optional paths of additional build info directories from previous versions of the project to use for storage layout comparisons. When using this option, refer to one of these directories using prefix `<dirName>:` before the contract name or fully qualified name in the `reference` param or `@custom:oz-upgrades-from` annotation, where `<dirName>` is the directory short name. Each directory short name must be unique, including compared to the main build info directory.
|
|
31
|
+
* @param exclude Exclude validations for contracts in source file paths that match any of the given glob patterns.
|
|
29
32
|
* @returns The project report.
|
|
30
33
|
*/
|
|
31
34
|
export async function validateUpgradeSafety(
|
|
@@ -33,28 +36,57 @@ export async function validateUpgradeSafety(
|
|
|
33
36
|
contract?: string,
|
|
34
37
|
reference?: string,
|
|
35
38
|
opts: ValidateUpgradeSafetyOptions = {},
|
|
39
|
+
referenceBuildInfoDirs?: string[],
|
|
40
|
+
exclude?: string[],
|
|
36
41
|
): Promise<ProjectReport> {
|
|
37
42
|
const allOpts = withCliDefaults(opts);
|
|
38
43
|
|
|
39
44
|
const buildInfoFiles = await getBuildInfoFiles(buildInfoDir);
|
|
40
45
|
const sourceContracts = validateBuildInfoContracts(buildInfoFiles);
|
|
41
46
|
|
|
42
|
-
const
|
|
47
|
+
const buildInfoDictionary: BuildInfoDictionary = {};
|
|
48
|
+
buildInfoDictionary[''] = sourceContracts;
|
|
49
|
+
if (buildInfoFiles.length > 0) {
|
|
50
|
+
buildInfoDictionary[buildInfoFiles[0].dirShortName] = sourceContracts;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (referenceBuildInfoDirs !== undefined) {
|
|
54
|
+
for (const referenceBuildInfoDir of referenceBuildInfoDirs) {
|
|
55
|
+
const key = path.basename(referenceBuildInfoDir);
|
|
56
|
+
|
|
57
|
+
if (buildInfoDictionary[key] !== undefined) {
|
|
58
|
+
throw new Error(`Reference build info directory short name '${key}' is not unique.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const referenceBuildInfoFiles = await getBuildInfoFiles(referenceBuildInfoDir);
|
|
62
|
+
buildInfoDictionary[key] = validateBuildInfoContracts(referenceBuildInfoFiles);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
43
65
|
|
|
44
|
-
const
|
|
66
|
+
const specifiedContracts = findSpecifiedContracts(buildInfoDictionary, allOpts, contract, reference);
|
|
67
|
+
|
|
68
|
+
const contractReports = getContractReports(buildInfoDictionary, allOpts, specifiedContracts, exclude);
|
|
45
69
|
return getProjectReport(contractReports, specifiedContracts !== undefined);
|
|
46
70
|
}
|
|
47
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Dictionary of build info directories and the contracts they contain.
|
|
74
|
+
* Main build info directory can be found with the key '' and also with its short name.
|
|
75
|
+
*/
|
|
76
|
+
export interface BuildInfoDictionary {
|
|
77
|
+
[buildInfoDirName: string]: SourceContract[];
|
|
78
|
+
}
|
|
79
|
+
|
|
48
80
|
export function findSpecifiedContracts(
|
|
49
|
-
|
|
81
|
+
buildInfoDictionary: BuildInfoDictionary,
|
|
50
82
|
opts: Required<ValidateUpgradeSafetyOptions>,
|
|
51
83
|
contractName?: string,
|
|
52
84
|
referenceName?: string,
|
|
53
85
|
): SpecifiedContracts | undefined {
|
|
54
86
|
if (contractName !== undefined) {
|
|
55
87
|
return {
|
|
56
|
-
contract: findContract(contractName, undefined,
|
|
57
|
-
reference: referenceName !== undefined ? findContract(referenceName, undefined,
|
|
88
|
+
contract: findContract(contractName, undefined, buildInfoDictionary, true), // only search main build info dir for the specified contract
|
|
89
|
+
reference: referenceName !== undefined ? findContract(referenceName, undefined, buildInfoDictionary) : undefined,
|
|
58
90
|
};
|
|
59
91
|
} else if (referenceName !== undefined) {
|
|
60
92
|
throw new Error(`The reference option can only be specified when the contract option is also specified.`);
|
|
@@ -14,13 +14,17 @@ const test = _test as TestFn<Context>;
|
|
|
14
14
|
const SOURCE_FILE = 'contracts/test/cli/Validate.sol';
|
|
15
15
|
|
|
16
16
|
test.before(async t => {
|
|
17
|
-
const
|
|
17
|
+
const hardhatBuildInfo = await artifacts.getBuildInfo(`${SOURCE_FILE}:Safe`);
|
|
18
18
|
|
|
19
|
-
if (
|
|
19
|
+
if (hardhatBuildInfo === undefined) {
|
|
20
20
|
t.fail();
|
|
21
21
|
} else {
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
const buildInfoFile = {
|
|
23
|
+
...hardhatBuildInfo,
|
|
24
|
+
dirShortName: 'build-info',
|
|
25
|
+
};
|
|
26
|
+
const sourceContracts = validateBuildInfoContracts([buildInfoFile]);
|
|
27
|
+
t.context.reports = getContractReports({ '': sourceContracts }, withCliDefaults({}));
|
|
24
28
|
}
|
|
25
29
|
});
|
|
26
30
|
|
|
@@ -13,6 +13,7 @@ export interface SourceContract {
|
|
|
13
13
|
name: string;
|
|
14
14
|
fullyQualifiedName: string;
|
|
15
15
|
validationData: ValidationRunData;
|
|
16
|
+
buildInfoDirShortName: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function validateBuildInfoContracts(buildInfoFiles: BuildInfoFile[]): SourceContract[] {
|
|
@@ -48,6 +49,7 @@ function addContractsFromBuildInfo(
|
|
|
48
49
|
name: contractDef.name,
|
|
49
50
|
fullyQualifiedName,
|
|
50
51
|
validationData: validationData,
|
|
52
|
+
buildInfoDirShortName: buildInfoFile.dirShortName,
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
55
|
}
|
package/src/cli/validate.test.ts
CHANGED
|
@@ -110,3 +110,25 @@ test('withDefaults - invalid unsafeAllow', t => {
|
|
|
110
110
|
)}`,
|
|
111
111
|
});
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
test('withDefaults - empty unsafeAllow', t => {
|
|
115
|
+
const parsedArgs = minimist(['validate', 'build-info.json', '--unsafeAllow']);
|
|
116
|
+
t.throws(() => withDefaults(parsedArgs), {
|
|
117
|
+
message: `Invalid option: --unsafeAllow cannot be empty`,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('withDefaults - unsafeAllow multiple times', t => {
|
|
122
|
+
const parsedArgs = minimist([
|
|
123
|
+
'validate',
|
|
124
|
+
'build-info.json',
|
|
125
|
+
'--unsafeAllow',
|
|
126
|
+
'selfdestruct',
|
|
127
|
+
'--unsafeAllow',
|
|
128
|
+
'delegatecall',
|
|
129
|
+
'--unsafeAllow',
|
|
130
|
+
'constructor',
|
|
131
|
+
]);
|
|
132
|
+
const opts = withDefaults(parsedArgs);
|
|
133
|
+
t.deepEqual(opts.unsafeAllow, ['selfdestruct', 'delegatecall', 'constructor']);
|
|
134
|
+
});
|
package/src/cli/validate.ts
CHANGED
|
@@ -16,7 +16,9 @@ Options:
|
|
|
16
16
|
--contract <CONTRACT> The name or fully qualified name of the contract to validate. If not specified, all upgradeable contracts in the build info directory will be validated.
|
|
17
17
|
--reference <REFERENCE_CONTRACT> Can only be used when the --contract option is also provided. The name or fully qualified name of the reference contract to use for storage layout comparisons. If not specified, uses the @custom:oz-upgrades-from annotation if it is defined in the contract that is being validated.
|
|
18
18
|
--requireReference Can only be used when the --contract option is also provided. Not compatible with --unsafeSkipStorageCheck. If specified, requires either the --reference option to be provided or the contract to have a @custom:oz-upgrades-from annotation.
|
|
19
|
-
--
|
|
19
|
+
--referenceBuildInfoDirs "<BUILD_INFO_DIR>[,<BUILD_INFO_DIR>...]" Optional paths of additional build info directories from previous versions of the project to use for storage layout comparisons. When using this option, refer to one of these directories using prefix '<dirName>:' before the contract name or fully qualified name in the --reference option or @custom:oz-upgrades-from annotation, where <dirName> is the directory short name. Each directory short name must be unique, including compared to the main build info directory. If passing in multiple directories, separate them with commas or call the option multiple times, once for each directory.
|
|
20
|
+
--exclude "<GLOB_PATTERN>" [--exclude "<GLOB_PATTERN>"...] Exclude validations for contracts in source file paths that match any of the given glob patterns. For example, --exclude "contracts/mocks/**/*.sol". Does not apply to reference contracts. If passing in multiple patterns, call the option multiple times, once for each pattern.
|
|
21
|
+
--unsafeAllow "<VALIDATION_ERROR>[,<VALIDATION_ERROR>...]" Selectively disable one or more validation errors. Comma-separated list with one or more of the following: ${errorKinds.join(
|
|
20
22
|
', ',
|
|
21
23
|
)}
|
|
22
24
|
--unsafeAllowRenames Configure storage layout check to allow variable renaming.
|
|
@@ -32,6 +34,8 @@ export async function main(args: string[]): Promise<void> {
|
|
|
32
34
|
functionArgs.contract,
|
|
33
35
|
functionArgs.reference,
|
|
34
36
|
functionArgs.opts,
|
|
37
|
+
functionArgs.referenceBuildInfoDirs,
|
|
38
|
+
functionArgs.exclude,
|
|
35
39
|
);
|
|
36
40
|
console.log(result.explain());
|
|
37
41
|
process.exitCode = result.ok ? 0 : 1;
|
|
@@ -48,7 +52,7 @@ function parseArgs(args: string[]) {
|
|
|
48
52
|
'unsafeAllowLinkedLibraries',
|
|
49
53
|
'requireReference',
|
|
50
54
|
],
|
|
51
|
-
string: ['unsafeAllow', 'contract', 'reference'],
|
|
55
|
+
string: ['unsafeAllow', 'contract', 'reference', 'referenceBuildInfoDirs', 'exclude'],
|
|
52
56
|
alias: { h: 'help' },
|
|
53
57
|
});
|
|
54
58
|
const extraArgs = parsedArgs._;
|
|
@@ -71,6 +75,8 @@ interface FunctionArgs {
|
|
|
71
75
|
contract?: string;
|
|
72
76
|
reference?: string;
|
|
73
77
|
opts: Required<ValidateUpgradeSafetyOptions>;
|
|
78
|
+
referenceBuildInfoDirs?: string[];
|
|
79
|
+
exclude?: string[];
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
/**
|
|
@@ -90,6 +96,8 @@ export function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: stri
|
|
|
90
96
|
const contract = getAndValidateString(parsedArgs, 'contract');
|
|
91
97
|
const reference = getAndValidateString(parsedArgs, 'reference');
|
|
92
98
|
const opts = withDefaults(parsedArgs);
|
|
99
|
+
const referenceBuildInfoDirs = getAndValidateStringArray(parsedArgs, 'referenceBuildInfoDirs', true);
|
|
100
|
+
const exclude = getAndValidateStringArray(parsedArgs, 'exclude', false);
|
|
93
101
|
|
|
94
102
|
if (contract === undefined) {
|
|
95
103
|
if (reference !== undefined) {
|
|
@@ -98,18 +106,45 @@ export function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: stri
|
|
|
98
106
|
throw new Error('The --requireReference option can only be used along with the --contract option.');
|
|
99
107
|
}
|
|
100
108
|
}
|
|
101
|
-
return { buildInfoDir, contract, reference, opts };
|
|
109
|
+
return { buildInfoDir, contract, reference, opts, referenceBuildInfoDirs, exclude };
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
function getAndValidateString(parsedArgs: minimist.ParsedArgs, option: string): string | undefined {
|
|
106
114
|
const value = parsedArgs[option];
|
|
107
|
-
if (value !== undefined
|
|
108
|
-
|
|
115
|
+
if (value !== undefined) {
|
|
116
|
+
assertNonEmptyString(value, option);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getAndValidateStringArray(
|
|
122
|
+
parsedArgs: minimist.ParsedArgs,
|
|
123
|
+
option: string,
|
|
124
|
+
useCommaDelimiter: boolean,
|
|
125
|
+
): string[] | undefined {
|
|
126
|
+
const value = parsedArgs[option];
|
|
127
|
+
if (value !== undefined) {
|
|
128
|
+
if (Array.isArray(value)) {
|
|
129
|
+
return value;
|
|
130
|
+
} else {
|
|
131
|
+
assertNonEmptyString(value, option);
|
|
132
|
+
if (useCommaDelimiter) {
|
|
133
|
+
return value.split(/[\s,]+/);
|
|
134
|
+
} else {
|
|
135
|
+
return [value];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
109
138
|
}
|
|
110
139
|
return value;
|
|
111
140
|
}
|
|
112
141
|
|
|
142
|
+
function assertNonEmptyString(value: unknown, option: string) {
|
|
143
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
144
|
+
throw new Error(`Invalid option: --${option} cannot be empty`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
113
148
|
function validateOptions(parsedArgs: minimist.ParsedArgs) {
|
|
114
149
|
const invalidArgs = Object.keys(parsedArgs).filter(
|
|
115
150
|
key =>
|
|
@@ -125,6 +160,8 @@ function validateOptions(parsedArgs: minimist.ParsedArgs) {
|
|
|
125
160
|
'contract',
|
|
126
161
|
'reference',
|
|
127
162
|
'requireReference',
|
|
163
|
+
'referenceBuildInfoDirs',
|
|
164
|
+
'exclude',
|
|
128
165
|
].includes(key),
|
|
129
166
|
);
|
|
130
167
|
if (invalidArgs.length > 0) {
|
|
@@ -132,18 +169,13 @@ function validateOptions(parsedArgs: minimist.ParsedArgs) {
|
|
|
132
169
|
}
|
|
133
170
|
}
|
|
134
171
|
|
|
135
|
-
function getUnsafeAllowKinds(
|
|
172
|
+
function getUnsafeAllowKinds(parseArgs: minimist.ParsedArgs): ValidationError['kind'][] {
|
|
136
173
|
type errorKindsType = (typeof errorKinds)[number];
|
|
137
174
|
|
|
138
|
-
|
|
139
|
-
return [];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const unsafeAllowTokens: string[] = unsafeAllow.split(/[\s,]+/);
|
|
175
|
+
const unsafeAllowTokens: string[] = getAndValidateStringArray(parseArgs, 'unsafeAllow', true) ?? [];
|
|
143
176
|
if (unsafeAllowTokens.some(token => !errorKinds.includes(token as errorKindsType))) {
|
|
144
|
-
// This includes empty strings
|
|
145
177
|
throw new Error(
|
|
146
|
-
`Invalid option: --unsafeAllow "${unsafeAllow}". Supported values for the --unsafeAllow option are: ${errorKinds.join(
|
|
178
|
+
`Invalid option: --unsafeAllow "${parseArgs['unsafeAllow']}". Supported values for the --unsafeAllow option are: ${errorKinds.join(
|
|
147
179
|
', ',
|
|
148
180
|
)}`,
|
|
149
181
|
);
|
|
@@ -159,7 +191,7 @@ export function withDefaults(parsedArgs: minimist.ParsedArgs): Required<Validate
|
|
|
159
191
|
unsafeSkipStorageCheck: parsedArgs['unsafeSkipStorageCheck'],
|
|
160
192
|
unsafeAllowCustomTypes: parsedArgs['unsafeAllowCustomTypes'],
|
|
161
193
|
unsafeAllowLinkedLibraries: parsedArgs['unsafeAllowLinkedLibraries'],
|
|
162
|
-
unsafeAllow: getUnsafeAllowKinds(parsedArgs
|
|
194
|
+
unsafeAllow: getUnsafeAllowKinds(parsedArgs),
|
|
163
195
|
requireReference: parsedArgs['requireReference'],
|
|
164
196
|
};
|
|
165
197
|
|
package/src/storage/extract.ts
CHANGED
|
@@ -203,7 +203,8 @@ export function loadLayoutType(typeName: TypeName | null | undefined, layout: St
|
|
|
203
203
|
function getRetypedRenamed(varDecl: VariableDeclaration) {
|
|
204
204
|
let retypedFrom, renamedFrom;
|
|
205
205
|
if ('documentation' in varDecl) {
|
|
206
|
-
const docs =
|
|
206
|
+
const docs =
|
|
207
|
+
typeof varDecl.documentation === 'string' ? varDecl.documentation : (varDecl.documentation?.text ?? '');
|
|
207
208
|
for (const { groups } of execall(
|
|
208
209
|
/^\s*(?:@(?<title>\w+)(?::(?<tag>[a-z][a-z-]*))? )?(?<args>(?:(?!^\s@\w+)[^])*)/m,
|
|
209
210
|
docs,
|
package/src/utils/annotations.ts
CHANGED
|
@@ -48,7 +48,7 @@ export function getAnnotationArgs(doc: string, tag: string, supportedArgs?: read
|
|
|
48
48
|
*/
|
|
49
49
|
export function getDocumentation(node: Node) {
|
|
50
50
|
if ('documentation' in node) {
|
|
51
|
-
return typeof node.documentation === 'string' ? node.documentation : node.documentation?.text ?? '';
|
|
51
|
+
return typeof node.documentation === 'string' ? node.documentation : (node.documentation?.text ?? '');
|
|
52
52
|
} else {
|
|
53
53
|
return '';
|
|
54
54
|
}
|