@openzeppelin/upgrades-core 1.36.0 → 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.
Files changed (36) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/cli/cli.test.js +99 -1
  3. package/dist/cli/cli.test.js.map +1 -1
  4. package/dist/cli/validate/contract-report.d.ts +2 -1
  5. package/dist/cli/validate/contract-report.d.ts.map +1 -1
  6. package/dist/cli/validate/contract-report.js +43 -8
  7. package/dist/cli/validate/contract-report.js.map +1 -1
  8. package/dist/cli/validate/default-exclude.d.ts +2 -0
  9. package/dist/cli/validate/default-exclude.d.ts.map +1 -0
  10. package/dist/cli/validate/default-exclude.js +6 -0
  11. package/dist/cli/validate/default-exclude.js.map +1 -0
  12. package/dist/cli/validate/validate-upgrade-safety.d.ts +2 -1
  13. package/dist/cli/validate/validate-upgrade-safety.d.ts.map +1 -1
  14. package/dist/cli/validate/validate-upgrade-safety.js +3 -2
  15. package/dist/cli/validate/validate-upgrade-safety.js.map +1 -1
  16. package/dist/cli/validate.d.ts +1 -0
  17. package/dist/cli/validate.d.ts.map +1 -1
  18. package/dist/cli/validate.js +38 -16
  19. package/dist/cli/validate.js.map +1 -1
  20. package/dist/cli/validate.test.js +20 -0
  21. package/dist/cli/validate.test.js.map +1 -1
  22. package/dist/storage/extract.js +1 -1
  23. package/dist/storage/extract.js.map +1 -1
  24. package/dist/utils/annotations.js +1 -1
  25. package/dist/utils/annotations.js.map +1 -1
  26. package/package.json +4 -3
  27. package/src/cli/cli.test.ts +141 -1
  28. package/src/cli/cli.test.ts.md +136 -4
  29. package/src/cli/cli.test.ts.snap +0 -0
  30. package/src/cli/validate/contract-report.ts +59 -9
  31. package/src/cli/validate/default-exclude.ts +3 -0
  32. package/src/cli/validate/validate-upgrade-safety.ts +3 -1
  33. package/src/cli/validate.test.ts +22 -0
  34. package/src/cli/validate.ts +43 -16
  35. package/src/storage/extract.ts +2 -1
  36. package/src/utils/annotations.ts +1 -1
@@ -4,7 +4,6 @@ import {
4
4
  getContractVersion,
5
5
  getStorageLayout,
6
6
  ValidationOptions,
7
- withValidationDefaults,
8
7
  Version,
9
8
  ValidationData,
10
9
  ValidateUpgradeSafetyOptions,
@@ -18,6 +17,9 @@ import { SourceContract } from './validations';
18
17
  import { LayoutCompatibilityReport } from '../../storage/report';
19
18
  import { indent } from '../../utils/indent';
20
19
  import { BuildInfoDictionary, SpecifiedContracts } from './validate-upgrade-safety';
20
+ import { minimatch } from 'minimatch';
21
+ import { ValidateCommandError } from './error';
22
+ import { defaultExclude } from './default-exclude';
21
23
 
22
24
  /**
23
25
  * Report for an upgradeable contract.
@@ -29,7 +31,16 @@ export class UpgradeableContractReport implements Report {
29
31
  readonly reference: string | undefined,
30
32
  readonly standaloneReport: UpgradeableContractErrorReport,
31
33
  readonly storageLayoutReport: LayoutCompatibilityReport | undefined,
32
- ) {}
34
+ ) {
35
+ if (reference === contract) {
36
+ throw new ValidateCommandError(
37
+ `The contract ${contract} must not use itself as a reference for storage layout comparisons.`,
38
+ () => `\
39
+ If this is the first version of your contract, do not specify a reference.
40
+ If this is a subsequent version, keep the previous version of the contract in another file and specify that as the reference, or specify a reference from another build info directory containing the previous version. If you do not have the previous version available, you can skip the storage layout check using the \`unsafeSkipStorageCheck\` option, which is a dangerous option meant to be used as a last resort.`,
41
+ );
42
+ }
43
+ }
33
44
 
34
45
  get ok(): boolean {
35
46
  return this.standaloneReport.ok && (this.storageLayoutReport === undefined || this.storageLayoutReport.ok);
@@ -66,12 +77,14 @@ export class UpgradeableContractReport implements Report {
66
77
  * @param buildInfoDictionary Dictionary of build info directories and the source contracts they contain.
67
78
  * @param opts The validation options.
68
79
  * @param specifiedContracts If provided, only the specified contract (upgrading from its reference contract) will be reported.
80
+ * @param exclude Exclude validations for contracts in source file paths that match any of the given glob patterns.
69
81
  * @returns The upgradeable contract reports.
70
82
  */
71
83
  export function getContractReports(
72
84
  buildInfoDictionary: BuildInfoDictionary,
73
85
  opts: Required<ValidateUpgradeSafetyOptions>,
74
86
  specifiedContracts?: SpecifiedContracts,
87
+ exclude?: string[],
75
88
  ) {
76
89
  const upgradeableContractReports: UpgradeableContractReport[] = [];
77
90
 
@@ -91,20 +104,43 @@ export function getContractReports(
91
104
  } else if (specifiedContracts !== undefined || upgradeabilityAssessment.upgradeable) {
92
105
  const reference = upgradeabilityAssessment.referenceContract;
93
106
  const kind = upgradeabilityAssessment.uups ? 'uups' : 'transparent';
94
- const report = getUpgradeableContractReport(sourceContract, reference, { ...opts, kind: kind });
107
+ const report = getUpgradeableContractReport(sourceContract, reference, { ...opts, kind: kind }, exclude);
95
108
  if (report !== undefined) {
96
109
  upgradeableContractReports.push(report);
110
+ } else if (specifiedContracts !== undefined) {
111
+ // If there was no report for the specified contract, it was excluded or is abstract.
112
+ const userAction =
113
+ exclude !== undefined
114
+ ? `Ensure the contract is not abstract and is not excluded by the exclude option.`
115
+ : `Ensure the contract is not abstract.`;
116
+ throw new ValidateCommandError(
117
+ `No validation report found for contract ${specifiedContracts.contract.fullyQualifiedName}`,
118
+ () => userAction,
119
+ );
97
120
  }
98
121
  }
99
122
  }
123
+
100
124
  return upgradeableContractReports;
101
125
  }
102
126
 
127
+ /**
128
+ * Gets a report for an upgradeable contract.
129
+ * Returns undefined if the contract is excluded or is abstract.
130
+ */
103
131
  function getUpgradeableContractReport(
104
132
  contract: SourceContract,
105
133
  referenceContract: SourceContract | undefined,
106
- opts: ValidationOptions,
134
+ opts: Required<ValidationOptions>,
135
+ exclude?: string[],
107
136
  ): UpgradeableContractReport | undefined {
137
+ const excludeWithDefaults = defaultExclude.concat(exclude ?? []);
138
+
139
+ if (excludeWithDefaults.some(glob => minimatch(getPath(contract.fullyQualifiedName), glob))) {
140
+ debug('Excluding contract: ' + contract.fullyQualifiedName);
141
+ return undefined;
142
+ }
143
+
108
144
  let version;
109
145
  try {
110
146
  version = getContractVersion(contract.validationData, contract.fullyQualifiedName);
@@ -119,7 +155,7 @@ function getUpgradeableContractReport(
119
155
  }
120
156
 
121
157
  debug('Checking: ' + contract.fullyQualifiedName);
122
- const standaloneReport = getStandaloneReport(contract.validationData, version, opts);
158
+ const standaloneReport = getStandaloneReport(contract.validationData, version, opts, excludeWithDefaults);
123
159
 
124
160
  let reference: string | undefined;
125
161
  let storageLayoutReport: LayoutCompatibilityReport | undefined;
@@ -135,7 +171,7 @@ function getUpgradeableContractReport(
135
171
  } else {
136
172
  reference = referenceContract.fullyQualifiedName;
137
173
  }
138
- storageLayoutReport = getStorageUpgradeReport(referenceLayout, layout, withValidationDefaults(opts));
174
+ storageLayoutReport = getStorageUpgradeReport(referenceLayout, layout, opts);
139
175
  }
140
176
 
141
177
  return new UpgradeableContractReport(contract.fullyQualifiedName, reference, standaloneReport, storageLayoutReport);
@@ -144,8 +180,22 @@ function getUpgradeableContractReport(
144
180
  function getStandaloneReport(
145
181
  data: ValidationData,
146
182
  version: Version,
147
- opts: ValidationOptions,
183
+ opts: Required<ValidationOptions>,
184
+ excludeWithDefaults: string[],
148
185
  ): UpgradeableContractErrorReport {
149
- const errors = getErrors(data, version, withValidationDefaults(opts));
150
- return new UpgradeableContractErrorReport(errors);
186
+ const allErrors = getErrors(data, version, opts);
187
+
188
+ const includeErrors = allErrors.filter(e => {
189
+ const shouldExclude = excludeWithDefaults.some(glob => minimatch(getPath(e.src), glob));
190
+ if (shouldExclude) {
191
+ debug('Excluding error: ' + e.src);
192
+ }
193
+ return !shouldExclude;
194
+ });
195
+
196
+ return new UpgradeableContractErrorReport(includeErrors);
197
+ }
198
+
199
+ function getPath(srcOrFullyQualifiedName: string): string {
200
+ return srcOrFullyQualifiedName.split(':')[0];
151
201
  }
@@ -0,0 +1,3 @@
1
+ const UPGRADEABLE_BEACON = '**/contracts/proxy/beacon/UpgradeableBeacon.sol' as const;
2
+
3
+ export const defaultExclude: string[] = [UPGRADEABLE_BEACON] as const;
@@ -28,6 +28,7 @@ export type SpecifiedContracts = {
28
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.
29
29
  * @param opts Validation options, or undefined to use the default validation options.
30
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.
31
32
  * @returns The project report.
32
33
  */
33
34
  export async function validateUpgradeSafety(
@@ -36,6 +37,7 @@ export async function validateUpgradeSafety(
36
37
  reference?: string,
37
38
  opts: ValidateUpgradeSafetyOptions = {},
38
39
  referenceBuildInfoDirs?: string[],
40
+ exclude?: string[],
39
41
  ): Promise<ProjectReport> {
40
42
  const allOpts = withCliDefaults(opts);
41
43
 
@@ -63,7 +65,7 @@ export async function validateUpgradeSafety(
63
65
 
64
66
  const specifiedContracts = findSpecifiedContracts(buildInfoDictionary, allOpts, contract, reference);
65
67
 
66
- const contractReports = getContractReports(buildInfoDictionary, allOpts, specifiedContracts);
68
+ const contractReports = getContractReports(buildInfoDictionary, allOpts, specifiedContracts, exclude);
67
69
  return getProjectReport(contractReports, specifiedContracts !== undefined);
68
70
  }
69
71
 
@@ -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
+ });
@@ -16,8 +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
- --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 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.
20
- --unsafeAllow "<VALIDATION_ERRORS>" Selectively disable one or more validation errors. Comma-separated list with one or more of the following: ${errorKinds.join(
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(
21
22
  ', ',
22
23
  )}
23
24
  --unsafeAllowRenames Configure storage layout check to allow variable renaming.
@@ -34,6 +35,7 @@ export async function main(args: string[]): Promise<void> {
34
35
  functionArgs.reference,
35
36
  functionArgs.opts,
36
37
  functionArgs.referenceBuildInfoDirs,
38
+ functionArgs.exclude,
37
39
  );
38
40
  console.log(result.explain());
39
41
  process.exitCode = result.ok ? 0 : 1;
@@ -50,7 +52,7 @@ function parseArgs(args: string[]) {
50
52
  'unsafeAllowLinkedLibraries',
51
53
  'requireReference',
52
54
  ],
53
- string: ['unsafeAllow', 'contract', 'reference', 'referenceBuildInfoDirs'],
55
+ string: ['unsafeAllow', 'contract', 'reference', 'referenceBuildInfoDirs', 'exclude'],
54
56
  alias: { h: 'help' },
55
57
  });
56
58
  const extraArgs = parsedArgs._;
@@ -74,6 +76,7 @@ interface FunctionArgs {
74
76
  reference?: string;
75
77
  opts: Required<ValidateUpgradeSafetyOptions>;
76
78
  referenceBuildInfoDirs?: string[];
79
+ exclude?: string[];
77
80
  }
78
81
 
79
82
  /**
@@ -93,7 +96,8 @@ export function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: stri
93
96
  const contract = getAndValidateString(parsedArgs, 'contract');
94
97
  const reference = getAndValidateString(parsedArgs, 'reference');
95
98
  const opts = withDefaults(parsedArgs);
96
- const referenceBuildInfoDirs = getAndValidateString(parsedArgs, 'referenceBuildInfoDirs')?.split(/,+/);
99
+ const referenceBuildInfoDirs = getAndValidateStringArray(parsedArgs, 'referenceBuildInfoDirs', true);
100
+ const exclude = getAndValidateStringArray(parsedArgs, 'exclude', false);
97
101
 
98
102
  if (contract === undefined) {
99
103
  if (reference !== undefined) {
@@ -102,18 +106,45 @@ export function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: stri
102
106
  throw new Error('The --requireReference option can only be used along with the --contract option.');
103
107
  }
104
108
  }
105
- return { buildInfoDir, contract, reference, opts, referenceBuildInfoDirs };
109
+ return { buildInfoDir, contract, reference, opts, referenceBuildInfoDirs, exclude };
106
110
  }
107
111
  }
108
112
 
109
113
  function getAndValidateString(parsedArgs: minimist.ParsedArgs, option: string): string | undefined {
110
114
  const value = parsedArgs[option];
111
- if (value !== undefined && value.trim().length === 0) {
112
- throw new Error(`Invalid option: --${option} cannot be empty`);
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
+ }
113
138
  }
114
139
  return value;
115
140
  }
116
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
+
117
148
  function validateOptions(parsedArgs: minimist.ParsedArgs) {
118
149
  const invalidArgs = Object.keys(parsedArgs).filter(
119
150
  key =>
@@ -130,6 +161,7 @@ function validateOptions(parsedArgs: minimist.ParsedArgs) {
130
161
  'reference',
131
162
  'requireReference',
132
163
  'referenceBuildInfoDirs',
164
+ 'exclude',
133
165
  ].includes(key),
134
166
  );
135
167
  if (invalidArgs.length > 0) {
@@ -137,18 +169,13 @@ function validateOptions(parsedArgs: minimist.ParsedArgs) {
137
169
  }
138
170
  }
139
171
 
140
- function getUnsafeAllowKinds(unsafeAllow: string | undefined): ValidationError['kind'][] {
172
+ function getUnsafeAllowKinds(parseArgs: minimist.ParsedArgs): ValidationError['kind'][] {
141
173
  type errorKindsType = (typeof errorKinds)[number];
142
174
 
143
- if (unsafeAllow === undefined) {
144
- return [];
145
- }
146
-
147
- const unsafeAllowTokens: string[] = unsafeAllow.split(/[\s,]+/);
175
+ const unsafeAllowTokens: string[] = getAndValidateStringArray(parseArgs, 'unsafeAllow', true) ?? [];
148
176
  if (unsafeAllowTokens.some(token => !errorKinds.includes(token as errorKindsType))) {
149
- // This includes empty strings
150
177
  throw new Error(
151
- `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(
152
179
  ', ',
153
180
  )}`,
154
181
  );
@@ -164,7 +191,7 @@ export function withDefaults(parsedArgs: minimist.ParsedArgs): Required<Validate
164
191
  unsafeSkipStorageCheck: parsedArgs['unsafeSkipStorageCheck'],
165
192
  unsafeAllowCustomTypes: parsedArgs['unsafeAllowCustomTypes'],
166
193
  unsafeAllowLinkedLibraries: parsedArgs['unsafeAllowLinkedLibraries'],
167
- unsafeAllow: getUnsafeAllowKinds(parsedArgs['unsafeAllow']),
194
+ unsafeAllow: getUnsafeAllowKinds(parsedArgs),
168
195
  requireReference: parsedArgs['requireReference'],
169
196
  };
170
197
 
@@ -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 = typeof varDecl.documentation === 'string' ? varDecl.documentation : varDecl.documentation?.text ?? '';
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,
@@ -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
  }