@openzeppelin/upgrades-core 1.35.0 → 1.36.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 (54) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cli/cli.test.js +160 -0
  3. package/dist/cli/cli.test.js.map +1 -1
  4. package/dist/cli/validate/build-info-file.d.ts +4 -0
  5. package/dist/cli/validate/build-info-file.d.ts.map +1 -1
  6. package/dist/cli/validate/build-info-file.js +7 -2
  7. package/dist/cli/validate/build-info-file.js.map +1 -1
  8. package/dist/cli/validate/contract-report.d.ts +5 -5
  9. package/dist/cli/validate/contract-report.d.ts.map +1 -1
  10. package/dist/cli/validate/contract-report.js +12 -6
  11. package/dist/cli/validate/contract-report.js.map +1 -1
  12. package/dist/cli/validate/find-contract.d.ts +7 -2
  13. package/dist/cli/validate/find-contract.d.ts.map +1 -1
  14. package/dist/cli/validate/find-contract.js +32 -4
  15. package/dist/cli/validate/find-contract.js.map +1 -1
  16. package/dist/cli/validate/upgradeability-assessment.d.ts +2 -1
  17. package/dist/cli/validate/upgradeability-assessment.d.ts.map +1 -1
  18. package/dist/cli/validate/upgradeability-assessment.js +2 -2
  19. package/dist/cli/validate/upgradeability-assessment.js.map +1 -1
  20. package/dist/cli/validate/validate-upgrade-safety.d.ts +11 -3
  21. package/dist/cli/validate/validate-upgrade-safety.d.ts.map +1 -1
  22. package/dist/cli/validate/validate-upgrade-safety.js +27 -7
  23. package/dist/cli/validate/validate-upgrade-safety.js.map +1 -1
  24. package/dist/cli/validate/validate-upgrade-safety.test.js +1 -1
  25. package/dist/cli/validate/validations.d.ts +1 -0
  26. package/dist/cli/validate/validations.d.ts.map +1 -1
  27. package/dist/cli/validate/validations.js +1 -0
  28. package/dist/cli/validate/validations.js.map +1 -1
  29. package/dist/cli/validate/validations.test.js +8 -4
  30. package/dist/cli/validate/validations.test.js.map +1 -1
  31. package/dist/cli/validate.d.ts +1 -0
  32. package/dist/cli/validate.d.ts.map +1 -1
  33. package/dist/cli/validate.js +6 -3
  34. package/dist/cli/validate.js.map +1 -1
  35. package/dist/utils/make-namespaced.d.ts +6 -2
  36. package/dist/utils/make-namespaced.d.ts.map +1 -1
  37. package/dist/utils/make-namespaced.js +27 -14
  38. package/dist/utils/make-namespaced.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/cli/cli.test.ts +274 -0
  41. package/src/cli/cli.test.ts.md +119 -0
  42. package/src/cli/cli.test.ts.snap +0 -0
  43. package/src/cli/validate/build-info-file.ts +15 -4
  44. package/src/cli/validate/contract-report.ts +12 -7
  45. package/src/cli/validate/find-contract.ts +50 -4
  46. package/src/cli/validate/upgradeability-assessment.ts +3 -2
  47. package/src/cli/validate/validate-upgrade-safety.test.ts +1 -1
  48. package/src/cli/validate/validate-upgrade-safety.ts +36 -6
  49. package/src/cli/validate/validations.test.ts +8 -4
  50. package/src/cli/validate/validations.ts +2 -0
  51. package/src/cli/validate.ts +7 -2
  52. package/src/utils/make-namespaced.test.ts.md +32 -2
  53. package/src/utils/make-namespaced.test.ts.snap +0 -0
  54. package/src/utils/make-namespaced.ts +36 -21
@@ -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,11 @@ 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 Absolute path of build info directory, or undefined to use the default Hardhat or Foundry build-info dir.
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.
29
31
  * @returns The project report.
30
32
  */
31
33
  export async function validateUpgradeSafety(
@@ -33,28 +35,56 @@ export async function validateUpgradeSafety(
33
35
  contract?: string,
34
36
  reference?: string,
35
37
  opts: ValidateUpgradeSafetyOptions = {},
38
+ referenceBuildInfoDirs?: string[],
36
39
  ): Promise<ProjectReport> {
37
40
  const allOpts = withCliDefaults(opts);
38
41
 
39
42
  const buildInfoFiles = await getBuildInfoFiles(buildInfoDir);
40
43
  const sourceContracts = validateBuildInfoContracts(buildInfoFiles);
41
44
 
42
- const specifiedContracts = findSpecifiedContracts(sourceContracts, allOpts, contract, reference);
45
+ const buildInfoDictionary: BuildInfoDictionary = {};
46
+ buildInfoDictionary[''] = sourceContracts;
47
+ if (buildInfoFiles.length > 0) {
48
+ buildInfoDictionary[buildInfoFiles[0].dirShortName] = sourceContracts;
49
+ }
50
+
51
+ if (referenceBuildInfoDirs !== undefined) {
52
+ for (const referenceBuildInfoDir of referenceBuildInfoDirs) {
53
+ const key = path.basename(referenceBuildInfoDir);
54
+
55
+ if (buildInfoDictionary[key] !== undefined) {
56
+ throw new Error(`Reference build info directory short name '${key}' is not unique.`);
57
+ }
58
+
59
+ const referenceBuildInfoFiles = await getBuildInfoFiles(referenceBuildInfoDir);
60
+ buildInfoDictionary[key] = validateBuildInfoContracts(referenceBuildInfoFiles);
61
+ }
62
+ }
43
63
 
44
- const contractReports = getContractReports(sourceContracts, allOpts, specifiedContracts);
64
+ const specifiedContracts = findSpecifiedContracts(buildInfoDictionary, allOpts, contract, reference);
65
+
66
+ const contractReports = getContractReports(buildInfoDictionary, allOpts, specifiedContracts);
45
67
  return getProjectReport(contractReports, specifiedContracts !== undefined);
46
68
  }
47
69
 
70
+ /**
71
+ * Dictionary of build info directories and the contracts they contain.
72
+ * Main build info directory can be found with the key '' and also with its short name.
73
+ */
74
+ export interface BuildInfoDictionary {
75
+ [buildInfoDirName: string]: SourceContract[];
76
+ }
77
+
48
78
  export function findSpecifiedContracts(
49
- sourceContracts: SourceContract[],
79
+ buildInfoDictionary: BuildInfoDictionary,
50
80
  opts: Required<ValidateUpgradeSafetyOptions>,
51
81
  contractName?: string,
52
82
  referenceName?: string,
53
83
  ): SpecifiedContracts | undefined {
54
84
  if (contractName !== undefined) {
55
85
  return {
56
- contract: findContract(contractName, undefined, sourceContracts),
57
- reference: referenceName !== undefined ? findContract(referenceName, undefined, sourceContracts) : undefined,
86
+ contract: findContract(contractName, undefined, buildInfoDictionary, true), // only search main build info dir for the specified contract
87
+ reference: referenceName !== undefined ? findContract(referenceName, undefined, buildInfoDictionary) : undefined,
58
88
  };
59
89
  } else if (referenceName !== undefined) {
60
90
  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 buildInfo = await artifacts.getBuildInfo(`${SOURCE_FILE}:Safe`);
17
+ const hardhatBuildInfo = await artifacts.getBuildInfo(`${SOURCE_FILE}:Safe`);
18
18
 
19
- if (buildInfo === undefined) {
19
+ if (hardhatBuildInfo === undefined) {
20
20
  t.fail();
21
21
  } else {
22
- const sourceContracts = validateBuildInfoContracts([buildInfo]);
23
- t.context.reports = getContractReports(sourceContracts, withCliDefaults({}));
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
  }
@@ -16,6 +16,7 @@ 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.
19
20
  --unsafeAllow "<VALIDATION_ERRORS>" Selectively disable one or more validation errors. Comma-separated list with one or more of the following: ${errorKinds.join(
20
21
  ', ',
21
22
  )}
@@ -32,6 +33,7 @@ export async function main(args: string[]): Promise<void> {
32
33
  functionArgs.contract,
33
34
  functionArgs.reference,
34
35
  functionArgs.opts,
36
+ functionArgs.referenceBuildInfoDirs,
35
37
  );
36
38
  console.log(result.explain());
37
39
  process.exitCode = result.ok ? 0 : 1;
@@ -48,7 +50,7 @@ function parseArgs(args: string[]) {
48
50
  'unsafeAllowLinkedLibraries',
49
51
  'requireReference',
50
52
  ],
51
- string: ['unsafeAllow', 'contract', 'reference'],
53
+ string: ['unsafeAllow', 'contract', 'reference', 'referenceBuildInfoDirs'],
52
54
  alias: { h: 'help' },
53
55
  });
54
56
  const extraArgs = parsedArgs._;
@@ -71,6 +73,7 @@ interface FunctionArgs {
71
73
  contract?: string;
72
74
  reference?: string;
73
75
  opts: Required<ValidateUpgradeSafetyOptions>;
76
+ referenceBuildInfoDirs?: string[];
74
77
  }
75
78
 
76
79
  /**
@@ -90,6 +93,7 @@ export function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: stri
90
93
  const contract = getAndValidateString(parsedArgs, 'contract');
91
94
  const reference = getAndValidateString(parsedArgs, 'reference');
92
95
  const opts = withDefaults(parsedArgs);
96
+ const referenceBuildInfoDirs = getAndValidateString(parsedArgs, 'referenceBuildInfoDirs')?.split(/,+/);
93
97
 
94
98
  if (contract === undefined) {
95
99
  if (reference !== undefined) {
@@ -98,7 +102,7 @@ export function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: stri
98
102
  throw new Error('The --requireReference option can only be used along with the --contract option.');
99
103
  }
100
104
  }
101
- return { buildInfoDir, contract, reference, opts };
105
+ return { buildInfoDir, contract, reference, opts, referenceBuildInfoDirs };
102
106
  }
103
107
  }
104
108
 
@@ -125,6 +129,7 @@ function validateOptions(parsedArgs: minimist.ParsedArgs) {
125
129
  'contract',
126
130
  'reference',
127
131
  'requireReference',
132
+ 'referenceBuildInfoDirs',
128
133
  ].includes(key),
129
134
  );
130
135
  if (invalidArgs.length > 0) {
@@ -97,6 +97,8 @@ Generated by [AVA](https://avajs.dev).
97
97
  ␊
98
98
  abstract contract HasFunction {␊
99
99
  ␊
100
+ enum $astId_id_random { dummy }␊
101
+ ␊
100
102
  function foo() pure public returns (bool) {}␊
101
103
  }␊
102
104
  ␊
@@ -228,6 +230,19 @@ Generated by [AVA](https://avajs.dev).
228
230
  ␊
229
231
  abstract contract DeploysContractToImmutable {␊
230
232
  enum $astId_id_random { dummy }␊
233
+ }␊
234
+ ␊
235
+ abstract contract HasSpecialFunctions {␊
236
+ ␊
237
+ ␊
238
+ enum $astId_id_random { dummy }␊
239
+ ␊
240
+ enum $astId_id_random { dummy }␊
241
+ ␊
242
+ // Regular function but payable␊
243
+ function hasPayable() public payable {}␊
244
+ ␊
245
+ bytes4 constant PAYABLE_SELECTOR = this.hasPayable.selector;␊
231
246
  }`,
232
247
  },
233
248
  'contracts/test/NamespacedToModifyImported.sol': {
@@ -344,7 +359,9 @@ Generated by [AVA](https://avajs.dev).
344
359
  }␊
345
360
  ␊
346
361
  abstract contract HasFunction {␊
347
- ␊
362
+ /// @param myInt an integer␊
363
+ enum $astId_id_random { dummy }␊
364
+ ␊
348
365
  function foo() pure public returns (bool) {}␊
349
366
  }␊
350
367
  ␊
@@ -517,6 +534,19 @@ Generated by [AVA](https://avajs.dev).
517
534
  ␊
518
535
  abstract contract DeploysContractToImmutable {␊
519
536
  enum $astId_id_random { dummy }␊
537
+ }␊
538
+ ␊
539
+ abstract contract HasSpecialFunctions {␊
540
+ /// @param data call data␊
541
+ /// @return dataReturn returned data␊
542
+ enum $astId_id_random { dummy }␊
543
+ ␊
544
+ enum $astId_id_random { dummy }␊
545
+ ␊
546
+ // Regular function but payable␊
547
+ function hasPayable() public payable {}␊
548
+ ␊
549
+ bytes4 constant PAYABLE_SELECTOR = this.hasPayable.selector;␊
520
550
  }`,
521
551
  },
522
552
  'contracts/test/NamespacedToModifyImported.sol': {
@@ -559,7 +589,7 @@ Generated by [AVA](https://avajs.dev).
559
589
  pragma solidity 0.7.6;␊
560
590
  ␊
561
591
  abstract contract HasFunction {␊
562
- ␊
592
+ enum $astId_id_random { dummy }␊
563
593
  function foo() pure public returns (bool) {}␊
564
594
  }␊
565
595
  ␊
@@ -2,7 +2,7 @@ import { isNodeType } from 'solidity-ast/utils';
2
2
  import { Node } from 'solidity-ast/node';
3
3
  import { SolcInput, SolcOutput } from '../solc-api';
4
4
  import { getStorageLocationAnnotation } from '../storage/namespace';
5
- import { assert } from './assert';
5
+ import { assert, assertUnreachable } from './assert';
6
6
  import { FunctionDefinition, VariableDeclaration } from 'solidity-ast';
7
7
 
8
8
  const OUTPUT_SELECTION = {
@@ -18,9 +18,13 @@ const OUTPUT_SELECTION = {
18
18
  *
19
19
  * This makes the following modifications to the input:
20
20
  * - Adds a state variable for each namespaced struct definition
21
- * - For each contract, for all node types that are not needed for storage layout or may reference deleted functions and constructors, converts them to dummy enums with random id
21
+ * - For each contract, for all node types that are not needed for storage layout or may call functions and constructors, converts them to dummy enums with random id
22
22
  * - Converts all using for directives (at file level and in contracts) to dummy enums with random id (do not delete them to avoid orphaning possible NatSpec documentation)
23
- * - Converts all custom errors, free functions and constants (at file level) to dummy enums with the same name (do not delete them since they might be imported by other files)
23
+ * - Converts all custom errors and constants (at file level) to dummy enums with the same name (do not delete them since they might be imported by other files)
24
+ * - Replaces functions as follows:
25
+ * - For regular function and free function, keep declarations since they may be referenced by constants (free functions may also be imported by other files). But simplify compilation by removing modifiers and body, and replace return parameters with bools which can be default initialized.
26
+ * - Constructors are not needed, since we removed anything that may call constructors. Convert to dummy enums to avoid orphaning possible NatSpec.
27
+ * - Fallback and receive functions are not needed, since they don't have signatures. Convert to dummy enums to avoid orphaning possible NatSpec.
24
28
  *
25
29
  * Also sets the outputSelection to only include storageLayout and ast, since the other outputs are not needed.
26
30
  *
@@ -228,28 +232,39 @@ function toDummyEnumWithAstId(astId: number) {
228
232
  }
229
233
 
230
234
  function replaceFunction(node: FunctionDefinition, orig: Buffer, modifications: Modification[]) {
231
- if (node.kind === 'constructor') {
232
- modifications.push(makeDelete(node, orig));
233
- } else {
234
- if (node.modifiers.length > 0) {
235
- for (const modifier of node.modifiers) {
236
- modifications.push(makeDelete(modifier, orig));
235
+ switch (node.kind) {
236
+ case 'freeFunction':
237
+ case 'function': {
238
+ if (node.modifiers.length > 0) {
239
+ for (const modifier of node.modifiers) {
240
+ modifications.push(makeDelete(modifier, orig));
241
+ }
237
242
  }
238
- }
239
243
 
240
- if (node.returnParameters.parameters.length > 0) {
241
- modifications.push(
242
- makeReplace(
243
- node.returnParameters,
244
- orig,
245
- `(${node.returnParameters.parameters.map(param => toReturnParameterReplacement(param)).join(', ')})`,
246
- ),
247
- );
248
- }
244
+ if (node.returnParameters.parameters.length > 0) {
245
+ modifications.push(
246
+ makeReplace(
247
+ node.returnParameters,
248
+ orig,
249
+ `(${node.returnParameters.parameters.map(param => toReturnParameterReplacement(param)).join(', ')})`,
250
+ ),
251
+ );
252
+ }
253
+
254
+ if (node.body) {
255
+ modifications.push(makeReplace(node.body, orig, '{}'));
256
+ }
249
257
 
250
- if (node.body) {
251
- modifications.push(makeReplace(node.body, orig, '{}'));
258
+ break;
259
+ }
260
+ case 'constructor':
261
+ case 'fallback':
262
+ case 'receive': {
263
+ modifications.push(makeReplace(node, orig, toDummyEnumWithAstId(node.id)));
264
+ break;
252
265
  }
266
+ default:
267
+ return assertUnreachable(node.kind);
253
268
  }
254
269
  }
255
270