@openzeppelin/upgrades-core 1.27.2 → 1.28.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 (109) hide show
  1. package/dist/cli/cli.test.js +93 -3
  2. package/dist/cli/cli.test.js.map +1 -1
  3. package/dist/cli/index.d.ts +1 -1
  4. package/dist/cli/index.d.ts.map +1 -1
  5. package/dist/cli/index.js +2 -2
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/cli/validate/build-info-file.test.js +2 -4
  8. package/dist/cli/validate/build-info-file.test.js.map +1 -1
  9. package/dist/cli/validate/contract-report.d.ts +3 -1
  10. package/dist/cli/validate/contract-report.d.ts.map +1 -1
  11. package/dist/cli/validate/contract-report.js +7 -6
  12. package/dist/cli/validate/contract-report.js.map +1 -1
  13. package/dist/cli/validate/find-contract.d.ts +14 -0
  14. package/dist/cli/validate/find-contract.d.ts.map +1 -0
  15. package/dist/cli/validate/find-contract.js +32 -0
  16. package/dist/cli/validate/find-contract.js.map +1 -0
  17. package/dist/cli/validate/project-report.test.js +1 -2
  18. package/dist/cli/validate/project-report.test.js.map +1 -1
  19. package/dist/cli/validate/upgradeability-assessment.d.ts +1 -12
  20. package/dist/cli/validate/upgradeability-assessment.d.ts.map +1 -1
  21. package/dist/cli/validate/upgradeability-assessment.js +17 -40
  22. package/dist/cli/validate/upgradeability-assessment.js.map +1 -1
  23. package/dist/cli/validate/validate-upgrade-safety.d.ts +8 -1
  24. package/dist/cli/validate/validate-upgrade-safety.d.ts.map +1 -1
  25. package/dist/cli/validate/validate-upgrade-safety.js +20 -2
  26. package/dist/cli/validate/validate-upgrade-safety.js.map +1 -1
  27. package/dist/cli/validate/validate-upgrade-safety.test.js +4 -6
  28. package/dist/cli/validate/validate-upgrade-safety.test.js.map +1 -1
  29. package/dist/cli/validate.d.ts +2 -0
  30. package/dist/cli/validate.d.ts.map +1 -1
  31. package/dist/cli/validate.js +19 -3
  32. package/dist/cli/validate.js.map +1 -1
  33. package/dist/cli/validate.test.js.map +1 -1
  34. package/dist/deployment.d.ts.map +1 -1
  35. package/dist/deployment.js.map +1 -1
  36. package/dist/impl-store.test.js +2 -4
  37. package/dist/impl-store.test.js.map +1 -1
  38. package/dist/scripts/migrate-oz-cli-project.test.js +2 -4
  39. package/dist/scripts/migrate-oz-cli-project.test.js.map +1 -1
  40. package/dist/standalone.d.ts.map +1 -1
  41. package/dist/standalone.js.map +1 -1
  42. package/dist/storage/compare.d.ts.map +1 -1
  43. package/dist/storage/compare.js.map +1 -1
  44. package/dist/storage/extract.d.ts +1 -1
  45. package/dist/storage/extract.d.ts.map +1 -1
  46. package/dist/storage/extract.js.map +1 -1
  47. package/dist/storage/report-gap.test.js +1 -2
  48. package/dist/storage/report-gap.test.js.map +1 -1
  49. package/dist/storage/report-rename-retype.test.js +1 -2
  50. package/dist/storage/report-rename-retype.test.js.map +1 -1
  51. package/dist/storage/report-retype-from-contract.test.js +1 -2
  52. package/dist/storage/report-retype-from-contract.test.js.map +1 -1
  53. package/dist/storage/report.test.js +1 -2
  54. package/dist/storage/report.test.js.map +1 -1
  55. package/dist/storage-0.8.test.js +31 -2
  56. package/dist/storage-0.8.test.js.map +1 -1
  57. package/dist/storage-memory-0.5.test.js +1 -2
  58. package/dist/storage-memory-0.5.test.js.map +1 -1
  59. package/dist/storage.test.js +1 -2
  60. package/dist/storage.test.js.map +1 -1
  61. package/dist/utils/function.d.ts +1 -1
  62. package/dist/utils/function.d.ts.map +1 -1
  63. package/dist/utils/function.test.js +1 -2
  64. package/dist/utils/function.test.js.map +1 -1
  65. package/dist/utils/is-value-type.js +1 -1
  66. package/dist/utils/is-value-type.js.map +1 -1
  67. package/dist/validate/error.d.ts.map +1 -1
  68. package/dist/validate/error.js.map +1 -1
  69. package/dist/validate/run.d.ts +1 -1
  70. package/dist/validate/run.d.ts.map +1 -1
  71. package/dist/validate/run.js +1 -2
  72. package/dist/validate/run.js.map +1 -1
  73. package/package.json +7 -7
  74. package/src/cli/cli.test.ts +131 -2
  75. package/src/cli/cli.test.ts.md +94 -0
  76. package/src/cli/cli.test.ts.snap +0 -0
  77. package/src/cli/index.ts +1 -1
  78. package/src/cli/validate/build-info-file.test.ts +1 -4
  79. package/src/cli/validate/contract-report.ts +19 -7
  80. package/src/cli/validate/find-contract.ts +44 -0
  81. package/src/cli/validate/project-report.test.ts +1 -2
  82. package/src/cli/validate/upgradeability-assessment.ts +18 -50
  83. package/src/cli/validate/validate-upgrade-safety.test.ts +2 -5
  84. package/src/cli/validate/validate-upgrade-safety.ts +32 -2
  85. package/src/cli/validate.test.ts +1 -1
  86. package/src/cli/validate.ts +28 -4
  87. package/src/deployment.ts +5 -1
  88. package/src/impl-store.test.ts +1 -4
  89. package/src/scripts/migrate-oz-cli-project.test.ts +1 -3
  90. package/src/standalone.ts +6 -1
  91. package/src/storage/compare.ts +4 -1
  92. package/src/storage/extract.ts +1 -2
  93. package/src/storage/report-gap.test.ts +1 -2
  94. package/src/storage/report-rename-retype.test.ts +1 -2
  95. package/src/storage/report-retype-from-contract.test.ts +1 -2
  96. package/src/storage/report.test.ts +1 -2
  97. package/src/storage-0.8.test.ts +33 -2
  98. package/src/storage-memory-0.5.test.ts +1 -2
  99. package/src/storage.test.ts +1 -2
  100. package/src/utils/function.test.ts +1 -2
  101. package/src/utils/function.ts +1 -1
  102. package/src/utils/is-value-type.ts +1 -1
  103. package/src/validate/error.ts +4 -1
  104. package/src/validate/run.ts +2 -3
  105. package/dist/ast-dereferencer.d.ts +0 -8
  106. package/dist/ast-dereferencer.d.ts.map +0 -1
  107. package/dist/ast-dereferencer.js +0 -29
  108. package/dist/ast-dereferencer.js.map +0 -1
  109. package/src/ast-dereferencer.ts +0 -42
@@ -4,11 +4,10 @@ import { exec } from 'child_process';
4
4
  import { promises as fs } from 'fs';
5
5
  import path from 'path';
6
6
  import os from 'os';
7
- import rimrafAsync from 'rimraf';
7
+ import { rimraf } from 'rimraf';
8
8
  import { artifacts } from 'hardhat';
9
9
 
10
10
  const execAsync = promisify(exec);
11
- const rimraf = promisify(rimrafAsync);
12
11
 
13
12
  const CLI = 'node dist/cli/cli.js';
14
13
 
@@ -38,6 +37,136 @@ test('validate - errors', async t => {
38
37
  t.snapshot(expectation.join('\n'));
39
38
  });
40
39
 
40
+ test('validate - single contract', async t => {
41
+ // This should check even though the contract is not detected as upgradeable, since the --contract option was used.
42
+ const temp = await getTempDir(t);
43
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:NonUpgradeable`);
44
+ await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));
45
+
46
+ const error = await t.throwsAsync(execAsync(`${CLI} validate ${temp} --contract NonUpgradeable`));
47
+ const expectation: string[] = [`Stdout: ${(error as any).stdout}`, `Stderr: ${(error as any).stderr}`];
48
+ t.snapshot(expectation.join('\n'));
49
+ });
50
+
51
+ test('validate - single contract, has upgrades-from', async t => {
52
+ const temp = await getTempDir(t);
53
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:UnsafeAndStorageLayoutErrors`);
54
+ await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));
55
+
56
+ const error = await t.throwsAsync(execAsync(`${CLI} validate ${temp} --contract UnsafeAndStorageLayoutErrors`));
57
+ const expectation: string[] = [`Stdout: ${(error as any).stdout}`, `Stderr: ${(error as any).stderr}`];
58
+ t.snapshot(expectation.join('\n'));
59
+ });
60
+
61
+ test('validate - single contract, reference overrides upgrades-from', async t => {
62
+ const temp = await getTempDir(t);
63
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:UnsafeAndStorageLayoutErrors`);
64
+ await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));
65
+
66
+ const error = await t.throwsAsync(
67
+ execAsync(`${CLI} validate ${temp} --contract UnsafeAndStorageLayoutErrors --reference Safe`),
68
+ );
69
+ const expectation: string[] = [`Stdout: ${(error as any).stdout}`, `Stderr: ${(error as any).stderr}`];
70
+ t.snapshot(expectation.join('\n'));
71
+ });
72
+
73
+ test('validate - single contract, reference is uups, overrides upgrades-from', async t => {
74
+ const temp = await getTempDir(t);
75
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:UnsafeAndStorageLayoutErrors`);
76
+ await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));
77
+
78
+ const error = await t.throwsAsync(
79
+ execAsync(`${CLI} validate ${temp} --contract BecomesBadLayout --reference HasUpgradeTo`),
80
+ );
81
+ const expectation: string[] = [`Stdout: ${(error as any).stdout}`, `Stderr: ${(error as any).stderr}`];
82
+ t.snapshot(expectation.join('\n'));
83
+ });
84
+
85
+ test('validate - single contract, reference', async t => {
86
+ const temp = await getTempDir(t);
87
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:BecomesBadLayout`);
88
+ await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));
89
+
90
+ const error = await t.throwsAsync(
91
+ execAsync(`${CLI} validate ${temp} --contract BecomesBadLayout --reference StorageV1`),
92
+ );
93
+ const expectation: string[] = [`Stdout: ${(error as any).stdout}`, `Stderr: ${(error as any).stderr}`];
94
+ t.snapshot(expectation.join('\n'));
95
+ });
96
+
97
+ test('validate - single contract, reference, fully qualified names', async t => {
98
+ const temp = await getTempDir(t);
99
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:BecomesBadLayout`);
100
+ await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));
101
+
102
+ const error = await t.throwsAsync(
103
+ execAsync(
104
+ `${CLI} validate ${temp} --contract contracts/test/cli/Validate.sol:BecomesBadLayout --reference contracts/test/cli/Validate.sol:StorageV1`,
105
+ ),
106
+ );
107
+ const expectation: string[] = [`Stdout: ${(error as any).stdout}`, `Stderr: ${(error as any).stderr}`];
108
+ t.snapshot(expectation.join('\n'));
109
+ });
110
+
111
+ test('validate - reference without contract option', async t => {
112
+ const temp = await getTempDir(t);
113
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:StorageV1`);
114
+ await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));
115
+
116
+ const error = await t.throwsAsync(execAsync(`${CLI} validate ${temp} --reference StorageV1`));
117
+ t.true(
118
+ error?.message.includes('The --reference option can only be used along with the --contract option.'),
119
+ error?.message,
120
+ );
121
+ });
122
+
123
+ test('validate - emtpy contract string', async t => {
124
+ const temp = await getTempDir(t);
125
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:StorageV1`);
126
+ await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));
127
+
128
+ const error = await t.throwsAsync(execAsync(`${CLI} validate ${temp} --contract --reference StorageV1`));
129
+ t.true(error?.message.includes('Invalid option: --contract cannot be empty'), error?.message);
130
+ });
131
+
132
+ test('validate - blank contract string', async t => {
133
+ const temp = await getTempDir(t);
134
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:StorageV1`);
135
+ await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));
136
+
137
+ const error = await t.throwsAsync(execAsync(`${CLI} validate ${temp} --contract ' ' --reference StorageV1`));
138
+ t.true(error?.message.includes('Invalid option: --contract cannot be empty'), error?.message);
139
+ });
140
+
141
+ test('validate - empty reference string', async t => {
142
+ const temp = await getTempDir(t);
143
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:StorageV1`);
144
+ await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));
145
+
146
+ const error = await t.throwsAsync(execAsync(`${CLI} validate ${temp} --contract StorageV1 --reference`));
147
+ t.true(error?.message.includes('Invalid option: --reference cannot be empty'), error?.message);
148
+ });
149
+
150
+ test('validate - single contract not found', async t => {
151
+ const temp = await getTempDir(t);
152
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:BecomesBadLayout`);
153
+ await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));
154
+
155
+ const error = await t.throwsAsync(execAsync(`${CLI} validate ${temp} --contract NonExistent`));
156
+ t.true(error?.message.includes('Could not find contract NonExistent.'), error?.message);
157
+ });
158
+
159
+ test('validate - reference not found', async t => {
160
+ const temp = await getTempDir(t);
161
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:BecomesBadLayout`);
162
+ await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));
163
+
164
+ const error = await t.throwsAsync(
165
+ execAsync(`${CLI} validate ${temp} --contract BecomesBadLayout --reference NonExistent`),
166
+ );
167
+ t.true(error?.message.includes('Could not find contract NonExistent.'), error?.message);
168
+ });
169
+
41
170
  test('validate - no upgradeable', async t => {
42
171
  const temp = await getTempDir(t);
43
172
  const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Storage088.sol:Storage088`);
@@ -16,6 +16,8 @@ Generated by [AVA](https://avajs.dev).
16
16
  <BUILD_INFO_DIR> Optional path to the build info directory which contains JSON files with Solidity compiler input and output. Defaults to 'artifacts/build-info' for Hardhat projects or 'out/build-info' for Foundry projects. If your project uses a custom output directory, you must specify its build info directory here.␊
17
17
  ␊
18
18
  Options:␊
19
+ --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.␊
20
+ --reference <REFERENCE_CONTRACT> The name or fully qualified name of the reference contract to use for storage layout comparisons. Can only be used along with the --contract option. If not specified, uses the @custom:oz-upgrades-from annotation in the contract that is being validated.␊
19
21
  --unsafeAllow "<VALIDATION_ERRORS>" Selectively disable one or more validation errors. Comma-separated list with one or more of the following: state-variable-assignment, state-variable-immutable, external-library-linking, struct-definition, enum-definition, constructor, delegatecall, selfdestruct, missing-public-upgradeto␊
20
22
  --unsafeAllowRenames Configure storage layout check to allow variable renaming.␊
21
23
  --unsafeSkipStorageCheck Skips checking for storage layout compatibility errors. This is a dangerous option meant to be used as a last resort.␊
@@ -33,6 +35,8 @@ Generated by [AVA](https://avajs.dev).
33
35
  <BUILD_INFO_DIR> Optional path to the build info directory which contains JSON files with Solidity compiler input and output. Defaults to 'artifacts/build-info' for Hardhat projects or 'out/build-info' for Foundry projects. If your project uses a custom output directory, you must specify its build info directory here.␊
34
36
  ␊
35
37
  Options:␊
38
+ --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.␊
39
+ --reference <REFERENCE_CONTRACT> The name or fully qualified name of the reference contract to use for storage layout comparisons. Can only be used along with the --contract option. If not specified, uses the @custom:oz-upgrades-from annotation in the contract that is being validated.␊
36
40
  --unsafeAllow "<VALIDATION_ERRORS>" Selectively disable one or more validation errors. Comma-separated list with one or more of the following: state-variable-assignment, state-variable-immutable, external-library-linking, struct-definition, enum-definition, constructor, delegatecall, selfdestruct, missing-public-upgradeto␊
37
41
  --unsafeAllowRenames Configure storage layout check to allow variable renaming.␊
38
42
  --unsafeSkipStorageCheck Skips checking for storage layout compatibility errors. This is a dangerous option meant to be used as a last resort.␊
@@ -121,6 +125,96 @@ Generated by [AVA](https://avajs.dev).
121
125
  ␊
122
126
  Stderr: `
123
127
 
128
+ ## validate - single contract
129
+
130
+ > Snapshot 1
131
+
132
+ `Stdout: ✘ contracts/test/cli/Validate.sol:NonUpgradeable␊
133
+ ␊
134
+ contracts/test/cli/Validate.sol:14: Use of selfdestruct is not allowed␊
135
+ https://zpl.in/upgrades/error-003␊
136
+ ␊
137
+ FAILED (1 upgradeable contract detected, 0 passed, 1 failed)␊
138
+ ␊
139
+ Stderr: `
140
+
141
+ ## validate - single contract, has upgrades-from
142
+
143
+ > Snapshot 1
144
+
145
+ `Stdout: ✘ contracts/test/cli/Validate.sol:UnsafeAndStorageLayoutErrors (upgrades from contracts/test/cli/Validate.sol:StorageV1)␊
146
+ ␊
147
+ contracts/test/cli/Validate.sol:97: Use of selfdestruct is not allowed␊
148
+ https://zpl.in/upgrades/error-003␊
149
+ ␊
150
+ contracts/test/cli/Validate.sol:94: Layout changed for \`__gap\` (uint256[49] -> uint256[49])␊
151
+ - Slot changed from 1 to 2␊
152
+ > Set __gap array to size 48␊
153
+ ␊
154
+ FAILED (1 upgradeable contract detected, 0 passed, 1 failed)␊
155
+ ␊
156
+ Stderr: `
157
+
158
+ ## validate - single contract, reference overrides upgrades-from
159
+
160
+ > Snapshot 1
161
+
162
+ `Stdout: ✘ contracts/test/cli/Validate.sol:UnsafeAndStorageLayoutErrors (upgrades from contracts/test/cli/Validate.sol:Safe)␊
163
+ ␊
164
+ contracts/test/cli/Validate.sol:97: Use of selfdestruct is not allowed␊
165
+ https://zpl.in/upgrades/error-003␊
166
+ ␊
167
+ FAILED (1 upgradeable contract detected, 0 passed, 1 failed)␊
168
+ ␊
169
+ Stderr: `
170
+
171
+ ## validate - single contract, reference is uups, overrides upgrades-from
172
+
173
+ > Snapshot 1
174
+
175
+ `Stdout: ✘ contracts/test/cli/Validate.sol:BecomesBadLayout (upgrades from contracts/test/cli/Validate.sol:HasUpgradeTo)␊
176
+ ␊
177
+ contracts/test/cli/Validate.sol:111: Implementation is missing a public \`upgradeTo(address)\` function␊
178
+ Inherit UUPSUpgradeable to include this function in your contract␊
179
+ @openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol␊
180
+ https://zpl.in/upgrades/error-008␊
181
+ ␊
182
+ FAILED (1 upgradeable contract detected, 0 passed, 1 failed)␊
183
+ ␊
184
+ Stderr: `
185
+
186
+ ## validate - single contract, reference
187
+
188
+ > Snapshot 1
189
+
190
+ `Stdout: ✘ contracts/test/cli/Validate.sol:BecomesBadLayout (upgrades from contracts/test/cli/Validate.sol:StorageV1)␊
191
+ ␊
192
+ StorageV1: Deleted \`x\`␊
193
+ > Keep the variable even if unused␊
194
+ ␊
195
+ StorageV1: Deleted \`__gap\`␊
196
+ > Keep the variable even if unused␊
197
+ ␊
198
+ FAILED (1 upgradeable contract detected, 0 passed, 1 failed)␊
199
+ ␊
200
+ Stderr: `
201
+
202
+ ## validate - single contract, reference, fully qualified names
203
+
204
+ > Snapshot 1
205
+
206
+ `Stdout: ✘ contracts/test/cli/Validate.sol:BecomesBadLayout (upgrades from contracts/test/cli/Validate.sol:StorageV1)␊
207
+ ␊
208
+ StorageV1: Deleted \`x\`␊
209
+ > Keep the variable even if unused␊
210
+ ␊
211
+ StorageV1: Deleted \`__gap\`␊
212
+ > Keep the variable even if unused␊
213
+ ␊
214
+ FAILED (1 upgradeable contract detected, 0 passed, 1 failed)␊
215
+ ␊
216
+ Stderr: `
217
+
124
218
  ## validate - no upgradeable
125
219
 
126
220
  > Snapshot 1
Binary file
package/src/cli/index.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { ValidateUpgradeSafetyOptions, validateUpgradeSafety } from './validate/validate-upgrade-safety';
2
2
  export { ProjectReport } from './validate/project-report';
3
- export { ReferenceContractNotFound } from './validate/upgradeability-assessment';
3
+ export { ReferenceContractNotFound } from './validate/find-contract';
@@ -1,14 +1,11 @@
1
1
  import test, { ExecutionContext } from 'ava';
2
2
 
3
3
  import { promises as fs } from 'fs';
4
- import rimrafAsync from 'rimraf';
5
- import util from 'util';
4
+ import { rimraf } from 'rimraf';
6
5
  import path from 'path';
7
6
  import os from 'os';
8
7
  import { BuildInfoFile, getBuildInfoFiles } from './build-info-file';
9
8
 
10
- const rimraf = util.promisify(rimrafAsync);
11
-
12
9
  test.beforeEach(async t => {
13
10
  process.chdir(await fs.mkdtemp(path.join(os.tmpdir(), `upgrades-core-test-${t.title.replace(/\s/g, '-')}-`)));
14
11
  });
@@ -17,6 +17,7 @@ import { getUpgradeabilityAssessment } from './upgradeability-assessment';
17
17
  import { SourceContract } from './validations';
18
18
  import { LayoutCompatibilityReport } from '../../storage/report';
19
19
  import { indent } from '../../utils/indent';
20
+ import { SpecifiedContracts } from './validate-upgrade-safety';
20
21
 
21
22
  /**
22
23
  * Report for an upgradeable contract.
@@ -63,17 +64,28 @@ export class UpgradeableContractReport implements Report {
63
64
  *
64
65
  * @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
66
  * @param opts The validation options.
67
+ * @param specifiedContracts If provided, only the specified contract (upgrading from its reference contract) will be reported.
66
68
  * @returns The upgradeable contract reports.
67
69
  */
68
- export function getContractReports(sourceContracts: SourceContract[], opts: ValidateUpgradeSafetyOptions) {
70
+ export function getContractReports(
71
+ sourceContracts: SourceContract[],
72
+ opts: ValidateUpgradeSafetyOptions,
73
+ specifiedContracts?: SpecifiedContracts,
74
+ ) {
69
75
  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
76
 
77
+ const contractsToReport: SourceContract[] =
78
+ specifiedContracts !== undefined ? [specifiedContracts.contract] : sourceContracts;
79
+
80
+ for (const sourceContract of contractsToReport) {
81
+ const upgradeabilityAssessment = getUpgradeabilityAssessment(
82
+ sourceContract,
83
+ sourceContracts,
84
+ specifiedContracts?.reference,
85
+ );
86
+ if (specifiedContracts !== undefined || upgradeabilityAssessment.upgradeable) {
87
+ const reference = upgradeabilityAssessment.referenceContract;
88
+ const kind = upgradeabilityAssessment.uups ? 'uups' : 'transparent';
77
89
  const report = getUpgradeableContractReport(sourceContract, reference, { ...opts, kind: kind });
78
90
  if (report !== undefined) {
79
91
  upgradeableContractReports.push(report);
@@ -0,0 +1,44 @@
1
+ import { ValidateCommandError } from './error';
2
+ import { SourceContract } from './validations';
3
+
4
+ export class ReferenceContractNotFound extends Error {
5
+ /**
6
+ * The contract reference that could not be found.
7
+ */
8
+ readonly reference: string;
9
+
10
+ /**
11
+ * The fully qualified name of the contract that referenced the missing contract.
12
+ */
13
+ readonly origin?: string;
14
+
15
+ constructor(reference: string, origin?: string) {
16
+ const msg =
17
+ origin !== undefined
18
+ ? `Could not find contract ${reference} referenced in ${origin}.`
19
+ : `Could not find contract ${reference}.`;
20
+ super(msg);
21
+ this.reference = reference;
22
+ this.origin = origin;
23
+ }
24
+ }
25
+
26
+ export function findContract(contractName: string, origin: SourceContract | undefined, allContracts: SourceContract[]) {
27
+ const foundContracts = allContracts.filter(c => c.fullyQualifiedName === contractName || c.name === contractName);
28
+
29
+ if (foundContracts.length > 1) {
30
+ const msg =
31
+ origin !== undefined
32
+ ? `Found multiple contracts with name ${contractName} referenced in ${origin.fullyQualifiedName}.`
33
+ : `Found multiple contracts with name ${contractName}.`;
34
+ throw new ValidateCommandError(
35
+ msg,
36
+ () =>
37
+ `This may be caused by old copies of build info files. Clean and recompile your project, then run the command again with the updated files.`,
38
+ );
39
+ } else if (foundContracts.length === 1) {
40
+ return foundContracts[0];
41
+ } else {
42
+ throw new ReferenceContractNotFound(contractName, origin?.fullyQualifiedName);
43
+ }
44
+ }
@@ -3,11 +3,10 @@ import { UpgradeableContractErrorReport } from '../../validate';
3
3
 
4
4
  import _test, { TestFn } from 'ava';
5
5
  import { ContractDefinition } from 'solidity-ast';
6
- import { findAll } from 'solidity-ast/utils';
6
+ import { findAll, astDereferencer } from 'solidity-ast/utils';
7
7
  import { artifacts } from 'hardhat';
8
8
 
9
9
  import { SolcOutput } from '../../solc-api';
10
- import { astDereferencer } from '../../ast-dereferencer';
11
10
  import { extractStorageLayout } from '../../storage/extract';
12
11
  import { StorageLayoutComparator } from '../../storage/compare';
13
12
  import { StorageLayout, getDetailedLayout } from '../../storage/layout';
@@ -1,26 +1,9 @@
1
1
  import { getAnnotationArgs, getDocumentation } from '../../utils/annotations';
2
2
  import { inferInitializable, inferUUPS } from '../../validate/query';
3
3
  import { ValidateCommandError } from './error';
4
+ import { findContract } from './find-contract';
4
5
  import { SourceContract } from './validations';
5
6
 
6
- export class ReferenceContractNotFound extends Error {
7
- /**
8
- * The contract reference that could not be found.
9
- */
10
- readonly reference: string;
11
-
12
- /**
13
- * The fully qualified name of the contract that referenced the missing contract.
14
- */
15
- readonly origin: string;
16
-
17
- constructor(reference: string, origin: string) {
18
- super(`Could not find contract ${reference} referenced in ${origin}.`);
19
- this.reference = reference;
20
- this.origin = origin;
21
- }
22
- }
23
-
24
7
  interface AnnotationAssessment {
25
8
  upgradeable: boolean;
26
9
  referenceName?: string;
@@ -35,6 +18,7 @@ export interface UpgradeabilityAssessment {
35
18
  export function getUpgradeabilityAssessment(
36
19
  contract: SourceContract,
37
20
  allContracts: SourceContract[],
21
+ overrideReferenceContract?: SourceContract,
38
22
  ): UpgradeabilityAssessment {
39
23
  const fullContractName = contract.fullyQualifiedName;
40
24
  const contractValidation = contract.validationData[fullContractName];
@@ -42,42 +26,26 @@ export function getUpgradeabilityAssessment(
42
26
  const isUUPS = inferUUPS(contract.validationData, fullContractName);
43
27
 
44
28
  const annotationAssessment = getAnnotationAssessment(contract);
45
- if (annotationAssessment.upgradeable) {
46
- let referenceContract = undefined;
47
- let isReferenceUUPS = false;
48
- if (annotationAssessment.referenceName !== undefined) {
49
- referenceContract = getReferenceContract(annotationAssessment.referenceName, contract, allContracts);
50
- isReferenceUUPS = inferUUPS(referenceContract.validationData, referenceContract.fullyQualifiedName);
51
- }
52
29
 
53
- return {
54
- upgradeable: true,
55
- referenceContract: referenceContract,
56
- uups: isReferenceUUPS || isUUPS,
57
- };
58
- } else {
59
- const initializable = inferInitializable(contractValidation);
60
- return {
61
- upgradeable: initializable || isUUPS,
62
- uups: isUUPS,
63
- };
30
+ let referenceContract = overrideReferenceContract;
31
+ if (referenceContract === undefined && annotationAssessment.referenceName !== undefined) {
32
+ referenceContract = findContract(annotationAssessment.referenceName, contract, allContracts);
64
33
  }
65
- }
66
-
67
- function getReferenceContract(reference: string, origin: SourceContract, allContracts: SourceContract[]) {
68
- const referenceContracts = allContracts.filter(c => c.fullyQualifiedName === reference || c.name === reference);
69
34
 
70
- if (referenceContracts.length > 1) {
71
- throw new ValidateCommandError(
72
- `Found multiple contracts with name ${reference} referenced in ${origin.fullyQualifiedName}.`,
73
- () =>
74
- `This may be caused by old copies of build info files. Clean and recompile your project, then run the command again with the updated files.`,
75
- );
76
- } else if (referenceContracts.length === 1) {
77
- return referenceContracts[0];
78
- } else {
79
- throw new ReferenceContractNotFound(reference, origin.fullyQualifiedName);
35
+ let isReferenceUUPS = false;
36
+ if (referenceContract !== undefined) {
37
+ isReferenceUUPS = inferUUPS(referenceContract.validationData, referenceContract.fullyQualifiedName);
80
38
  }
39
+
40
+ return {
41
+ upgradeable:
42
+ referenceContract !== undefined ||
43
+ annotationAssessment.upgradeable ||
44
+ inferInitializable(contractValidation) ||
45
+ isUUPS,
46
+ referenceContract: referenceContract,
47
+ uups: isReferenceUUPS || isUUPS,
48
+ };
81
49
  }
82
50
 
83
51
  function getAnnotationAssessment(contract: SourceContract): AnnotationAssessment {
@@ -1,16 +1,13 @@
1
1
  import test from 'ava';
2
2
 
3
3
  import { promises as fs } from 'fs';
4
- import rimrafAsync from 'rimraf';
5
- import util from 'util';
4
+ import { rimraf } from 'rimraf';
6
5
  import path from 'path';
7
6
  import os from 'os';
8
7
 
9
8
  import { artifacts } from 'hardhat';
10
9
  import { validateUpgradeSafety } from './validate-upgrade-safety';
11
- import { ReferenceContractNotFound } from './upgradeability-assessment';
12
-
13
- const rimraf = util.promisify(rimrafAsync);
10
+ import { ReferenceContractNotFound } from './find-contract';
14
11
 
15
12
  test.before(async () => {
16
13
  process.chdir(await fs.mkdtemp(path.join(os.tmpdir(), 'upgrades-core-test-')));
@@ -2,28 +2,58 @@ import { ValidationOptions } from '../..';
2
2
 
3
3
  import { getBuildInfoFiles } from './build-info-file';
4
4
  import { getContractReports } from './contract-report';
5
+ import { findContract } from './find-contract';
5
6
  import { ProjectReport, getProjectReport } from './project-report';
6
- import { validateBuildInfoContracts } from './validations';
7
+ import { SourceContract, validateBuildInfoContracts } from './validations';
7
8
 
8
9
  /**
9
10
  * Validation options for upgrade safety checks.
10
11
  */
11
12
  export type ValidateUpgradeSafetyOptions = Omit<ValidationOptions, 'kind'>;
12
13
 
14
+ export type SpecifiedContracts = {
15
+ contract: SourceContract;
16
+ reference?: SourceContract;
17
+ };
18
+
13
19
  /**
14
20
  * Validates the upgrade safety of all contracts in the build info dir's build info files.
15
21
  * Only contracts that are detected as upgradeable will be validated.
16
22
  *
17
23
  * @param buildInfoDir Absolute path of build info directory, or undefined to use the default Hardhat or Foundry build-info dir.
24
+ * @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.
25
+ * @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.
18
26
  * @param opts Validation options, or undefined to use the default validation options.
19
27
  * @returns The project report.
20
28
  */
21
29
  export async function validateUpgradeSafety(
22
30
  buildInfoDir?: string,
31
+ contract?: string,
32
+ reference?: string,
23
33
  opts: ValidateUpgradeSafetyOptions = {},
24
34
  ): Promise<ProjectReport> {
25
35
  const buildInfoFiles = await getBuildInfoFiles(buildInfoDir);
26
36
  const sourceContracts = validateBuildInfoContracts(buildInfoFiles);
27
- const contractReports = getContractReports(sourceContracts, opts);
37
+
38
+ const specifiedContracts = findSpecifiedContracts(sourceContracts, contract, reference);
39
+
40
+ const contractReports = getContractReports(sourceContracts, opts, specifiedContracts);
28
41
  return getProjectReport(contractReports);
29
42
  }
43
+
44
+ function findSpecifiedContracts(
45
+ sourceContracts: SourceContract[],
46
+ contractName?: string,
47
+ referenceName?: string,
48
+ ): SpecifiedContracts | undefined {
49
+ if (contractName !== undefined) {
50
+ return {
51
+ contract: findContract(contractName, undefined, sourceContracts),
52
+ reference: referenceName !== undefined ? findContract(referenceName, undefined, sourceContracts) : undefined,
53
+ };
54
+ } else if (referenceName !== undefined) {
55
+ throw new Error(`The reference option can only be specified when the contract option is also specified.`);
56
+ } else {
57
+ return undefined;
58
+ }
59
+ }
@@ -99,7 +99,7 @@ test('withDefaults - all', t => {
99
99
  t.is(opts.unsafeSkipStorageCheck, true);
100
100
  t.is(opts.unsafeAllowCustomTypes, true);
101
101
  t.is(opts.unsafeAllowLinkedLibraries, true);
102
- t.true(opts.unsafeAllow.every((kind: string) => errorKinds.includes(kind as typeof errorKinds[number])));
102
+ t.true(opts.unsafeAllow.every((kind: string) => errorKinds.includes(kind as (typeof errorKinds)[number])));
103
103
  });
104
104
 
105
105
  test('withDefaults - invalid unsafeAllow', t => {
@@ -13,6 +13,8 @@ Arguments:
13
13
  <BUILD_INFO_DIR> Optional path to the build info directory which contains JSON files with Solidity compiler input and output. Defaults to 'artifacts/build-info' for Hardhat projects or 'out/build-info' for Foundry projects. If your project uses a custom output directory, you must specify its build info directory here.
14
14
 
15
15
  Options:
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
+ --reference <REFERENCE_CONTRACT> The name or fully qualified name of the reference contract to use for storage layout comparisons. Can only be used along with the --contract option. If not specified, uses the @custom:oz-upgrades-from annotation in the contract that is being validated.
16
18
  --unsafeAllow "<VALIDATION_ERRORS>" Selectively disable one or more validation errors. Comma-separated list with one or more of the following: ${errorKinds.join(
17
19
  ', ',
18
20
  )}
@@ -24,7 +26,12 @@ export async function main(args: string[]): Promise<void> {
24
26
 
25
27
  if (!help(parsedArgs, extraArgs)) {
26
28
  const functionArgs = getFunctionArgs(parsedArgs, extraArgs);
27
- const result = await validateUpgradeSafety(functionArgs.buildInfoDir, functionArgs.opts);
29
+ const result = await validateUpgradeSafety(
30
+ functionArgs.buildInfoDir,
31
+ functionArgs.contract,
32
+ functionArgs.reference,
33
+ functionArgs.opts,
34
+ );
28
35
  console.log(result.explain());
29
36
  process.exitCode = result.ok ? 0 : 1;
30
37
  }
@@ -39,7 +46,7 @@ function parseArgs(args: string[]) {
39
46
  'unsafeAllowCustomTypes',
40
47
  'unsafeAllowLinkedLibraries',
41
48
  ],
42
- string: ['unsafeAllow'],
49
+ string: ['unsafeAllow', 'contract', 'reference'],
43
50
  alias: { h: 'help' },
44
51
  });
45
52
  const extraArgs = parsedArgs._;
@@ -59,6 +66,8 @@ function help(parsedArgs: minimist.ParsedArgs, extraArgs: string[]): boolean {
59
66
 
60
67
  interface FunctionArgs {
61
68
  buildInfoDir?: string;
69
+ contract?: string;
70
+ reference?: string;
62
71
  opts: Required<ValidateUpgradeSafetyOptions>;
63
72
  }
64
73
 
@@ -76,9 +85,22 @@ export function getFunctionArgs(parsedArgs: minimist.ParsedArgs, extraArgs: stri
76
85
  throw new Error('The validate command takes only one argument: the build info directory.');
77
86
  } else {
78
87
  const buildInfoDir = extraArgs.length === 1 ? undefined : extraArgs[1];
88
+ const contract = getAndValidateString(parsedArgs, 'contract');
89
+ const reference = getAndValidateString(parsedArgs, 'reference');
90
+ if (reference !== undefined && contract === undefined) {
91
+ throw new Error('The --reference option can only be used along with the --contract option.');
92
+ }
79
93
  const opts = withDefaults(parsedArgs);
80
- return { buildInfoDir, opts };
94
+ return { buildInfoDir, contract, reference, opts };
95
+ }
96
+ }
97
+
98
+ function getAndValidateString(parsedArgs: minimist.ParsedArgs, option: string): string | undefined {
99
+ const value = parsedArgs[option];
100
+ if (value !== undefined && value.trim().length === 0) {
101
+ throw new Error(`Invalid option: --${option} cannot be empty`);
81
102
  }
103
+ return value;
82
104
  }
83
105
 
84
106
  function validateOptions(parsedArgs: minimist.ParsedArgs) {
@@ -93,6 +115,8 @@ function validateOptions(parsedArgs: minimist.ParsedArgs) {
93
115
  'unsafeAllowCustomTypes',
94
116
  'unsafeAllowLinkedLibraries',
95
117
  'unsafeAllow',
118
+ 'contract',
119
+ 'reference',
96
120
  ].includes(key),
97
121
  );
98
122
  if (invalidArgs.length > 0) {
@@ -101,7 +125,7 @@ function validateOptions(parsedArgs: minimist.ParsedArgs) {
101
125
  }
102
126
 
103
127
  function getUnsafeAllowKinds(unsafeAllow: string | undefined): ValidationError['kind'][] {
104
- type errorKindsType = typeof errorKinds[number];
128
+ type errorKindsType = (typeof errorKinds)[number];
105
129
 
106
130
  if (unsafeAllow === undefined) {
107
131
  return [];