@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
@@ -94,6 +94,21 @@ test('validate - single contract, reference', async t => {
94
94
  t.snapshot(expectation.join('\n'));
95
95
  });
96
96
 
97
+ test('validate - single contract, reference with same build info dir', async t => {
98
+ const temp = await getTempDir(t);
99
+ const buildInfoDir = path.join(temp, 'build-info');
100
+ await fs.mkdir(buildInfoDir);
101
+
102
+ const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:BecomesBadLayout`);
103
+ await fs.writeFile(path.join(buildInfoDir, 'validate.json'), JSON.stringify(buildInfo));
104
+
105
+ const error = await t.throwsAsync(
106
+ execAsync(`${CLI} validate ${buildInfoDir} --contract BecomesBadLayout --reference build-info:StorageV1`),
107
+ );
108
+ const expectation: string[] = [`Stdout: ${(error as any).stdout}`, `Stderr: ${(error as any).stderr}`];
109
+ t.snapshot(expectation.join('\n'));
110
+ });
111
+
97
112
  test('validate - single contract, reference, fully qualified names', async t => {
98
113
  const temp = await getTempDir(t);
99
114
  const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:BecomesBadLayout`);
@@ -298,3 +313,262 @@ test('validate - references fully qualified version of ambiguous contract name',
298
313
  ).stdout;
299
314
  t.snapshot(output);
300
315
  });
316
+
317
+ test('validate - references other build info dir by command - ok', async t => {
318
+ const temp = await getTempDir(t);
319
+ const referenceDir = path.join(temp, 'build-info-v1');
320
+ await fs.mkdir(referenceDir);
321
+
322
+ const referenceBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV1.sol:MyContract`);
323
+ await fs.writeFile(path.join(referenceDir, 'validate.json'), JSON.stringify(referenceBuildInfo));
324
+
325
+ const updatedDir = path.join(temp, 'build-info');
326
+ await fs.mkdir(updatedDir);
327
+
328
+ const updatedBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract`);
329
+ await fs.writeFile(path.join(updatedDir, 'validate.json'), JSON.stringify(updatedBuildInfo));
330
+
331
+ const output = await execAsync(
332
+ `${CLI} validate ${updatedDir} --referenceBuildInfoDirs ${referenceDir} --contract MyContract --reference build-info-v1:MyContract`,
333
+ );
334
+ t.snapshot(output);
335
+ });
336
+
337
+ test('validate - references other build info dir by command - reference not found', async t => {
338
+ const temp = await getTempDir(t);
339
+ const referenceDir = path.join(temp, 'build-info-v1');
340
+ await fs.mkdir(referenceDir);
341
+
342
+ const referenceBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV1.sol:MyContract`);
343
+ await fs.writeFile(path.join(referenceDir, 'validate.json'), JSON.stringify(referenceBuildInfo));
344
+
345
+ const updatedDir = path.join(temp, 'build-info');
346
+ await fs.mkdir(updatedDir);
347
+
348
+ const updatedBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract`);
349
+ await fs.writeFile(path.join(updatedDir, 'validate.json'), JSON.stringify(updatedBuildInfo));
350
+
351
+ const error = await t.throwsAsync(
352
+ execAsync(
353
+ `${CLI} validate ${updatedDir} --referenceBuildInfoDirs ${referenceDir} --contract MyContract --reference build-info-not-found:MyContract`,
354
+ ),
355
+ );
356
+ t.true(error?.message.includes('Could not find contract build-info-not-found:MyContract.'), error?.message);
357
+ });
358
+
359
+ test('validate - references other build info dir by command - dir not found', async t => {
360
+ const temp = await getTempDir(t);
361
+
362
+ const updatedDir = path.join(temp, 'build-info');
363
+ await fs.mkdir(updatedDir);
364
+
365
+ const updatedBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract`);
366
+ await fs.writeFile(path.join(updatedDir, 'validate.json'), JSON.stringify(updatedBuildInfo));
367
+
368
+ const error = await t.throwsAsync(
369
+ execAsync(
370
+ `${CLI} validate ${updatedDir} --referenceBuildInfoDirs build-info-not-found --contract MyContract --reference build-info-not-found:MyContract`,
371
+ ),
372
+ );
373
+ t.true(
374
+ error?.message.includes(
375
+ "The directory 'build-info-not-found' does not exist or does not contain any build info files.",
376
+ ),
377
+ error?.message,
378
+ );
379
+ });
380
+
381
+ test('validate - references other build info dir by command with fully qualified names - ok', async t => {
382
+ const temp = await getTempDir(t);
383
+ const referenceDir = path.join(temp, 'build-info-v1');
384
+ await fs.mkdir(referenceDir);
385
+
386
+ const referenceBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV1.sol:MyContract`);
387
+ await fs.writeFile(path.join(referenceDir, 'validate.json'), JSON.stringify(referenceBuildInfo));
388
+
389
+ const updatedDir = path.join(temp, 'build-info');
390
+ await fs.mkdir(updatedDir);
391
+
392
+ const updatedBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract`);
393
+ await fs.writeFile(path.join(updatedDir, 'validate.json'), JSON.stringify(updatedBuildInfo));
394
+
395
+ const output = await execAsync(
396
+ `${CLI} validate ${updatedDir} --referenceBuildInfoDirs ${referenceDir} --contract contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract --reference build-info-v1:contracts/test/cli/ValidateBuildInfoV1.sol:MyContract`,
397
+ );
398
+ t.snapshot(output);
399
+ });
400
+
401
+ test('validate - references other build info dir by command - bad', async t => {
402
+ const temp = await getTempDir(t);
403
+ const referenceDir = path.join(temp, 'build-info-v1');
404
+ await fs.mkdir(referenceDir);
405
+
406
+ const referenceBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV1.sol:MyContract`);
407
+ await fs.writeFile(path.join(referenceDir, 'validate.json'), JSON.stringify(referenceBuildInfo));
408
+
409
+ const updatedDir = path.join(temp, 'build-info');
410
+ await fs.mkdir(updatedDir);
411
+
412
+ const updatedBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV2_Bad.sol:MyContract`);
413
+ await fs.writeFile(path.join(updatedDir, 'validate.json'), JSON.stringify(updatedBuildInfo));
414
+
415
+ const error = await t.throwsAsync(
416
+ execAsync(
417
+ `${CLI} validate ${updatedDir} --referenceBuildInfoDirs ${referenceDir} --contract MyContract --reference build-info-v1:MyContract`,
418
+ ),
419
+ );
420
+ const expectation: string[] = [`Stdout: ${(error as any).stdout}`, `Stderr: ${(error as any).stderr}`];
421
+ t.snapshot(expectation.join('\n'));
422
+ });
423
+
424
+ test('validate - references other build info dir by command with fully qualified names - bad', async t => {
425
+ const temp = await getTempDir(t);
426
+ const referenceDir = path.join(temp, 'build-info-v1');
427
+ await fs.mkdir(referenceDir);
428
+
429
+ const referenceBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV1.sol:MyContract`);
430
+ await fs.writeFile(path.join(referenceDir, 'validate.json'), JSON.stringify(referenceBuildInfo));
431
+
432
+ const updatedDir = path.join(temp, 'build-info');
433
+ await fs.mkdir(updatedDir);
434
+
435
+ const updatedBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV2_Bad.sol:MyContract`);
436
+ await fs.writeFile(path.join(updatedDir, 'validate.json'), JSON.stringify(updatedBuildInfo));
437
+
438
+ const error = await t.throwsAsync(
439
+ execAsync(
440
+ `${CLI} validate ${updatedDir} --referenceBuildInfoDirs ${referenceDir} --contract contracts/test/cli/ValidateBuildInfoV2_Bad.sol:MyContract --reference build-info-v1:contracts/test/cli/ValidateBuildInfoV1.sol:MyContract`,
441
+ ),
442
+ );
443
+ const expectation: string[] = [`Stdout: ${(error as any).stdout}`, `Stderr: ${(error as any).stderr}`];
444
+ t.snapshot(expectation.join('\n'));
445
+ });
446
+
447
+ test('validate - references multiple build info dirs by annotation', async t => {
448
+ const temp = await getTempDir(t);
449
+
450
+ const v1Dir = path.join(temp, 'build-info-v1');
451
+ await fs.mkdir(v1Dir);
452
+ const v1BuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV1.sol:MyContract`);
453
+ await fs.writeFile(path.join(v1Dir, 'validate.json'), JSON.stringify(v1BuildInfo));
454
+
455
+ const v2Dir = path.join(temp, 'build-info-v2');
456
+ await fs.mkdir(v2Dir);
457
+ const v2BuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract`);
458
+ await fs.writeFile(path.join(v2Dir, 'validate.json'), JSON.stringify(v2BuildInfo));
459
+
460
+ const v2BranchDir = path.join(temp, 'build-info-v2-branch');
461
+ await fs.mkdir(v2BranchDir);
462
+ const v2BranchBuildInfoOk = await artifacts.getBuildInfo(
463
+ `contracts/test/cli/ValidateBuildInfoV2_Branch_Ok.sol:MyContract`,
464
+ );
465
+ const v2BranchBuildInfoBad = await artifacts.getBuildInfo(
466
+ `contracts/test/cli/ValidateBuildInfoV2_Branch_Bad.sol:MyContract`,
467
+ );
468
+ await fs.writeFile(path.join(v2BranchDir, 'ok.json'), JSON.stringify(v2BranchBuildInfoOk));
469
+ await fs.writeFile(path.join(v2BranchDir, 'bad.json'), JSON.stringify(v2BranchBuildInfoBad));
470
+
471
+ const error = await t.throwsAsync(
472
+ execAsync(`${CLI} validate ${v2BranchDir} --referenceBuildInfoDirs ${v1Dir},${v2Dir}`),
473
+ );
474
+ const expectation: string[] = [`Stdout: ${(error as any).stdout}`, `Stderr: ${(error as any).stderr}`];
475
+ t.snapshot(expectation.join('\n'));
476
+ });
477
+
478
+ test('validate - references other build info dir by annotation - ok', async t => {
479
+ const temp = await getTempDir(t);
480
+ const referenceDir = path.join(temp, 'build-info-v1');
481
+ await fs.mkdir(referenceDir);
482
+
483
+ const referenceBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV1.sol:MyContract`);
484
+ await fs.writeFile(path.join(referenceDir, 'validate.json'), JSON.stringify(referenceBuildInfo));
485
+
486
+ const updatedDir = path.join(temp, 'build-info');
487
+ await fs.mkdir(updatedDir);
488
+
489
+ const updatedBuildInfo = await artifacts.getBuildInfo(
490
+ `contracts/test/cli/ValidateBuildInfoV2_Annotation_Ok.sol:MyContract`,
491
+ );
492
+ await fs.writeFile(path.join(updatedDir, 'validate.json'), JSON.stringify(updatedBuildInfo));
493
+
494
+ const output = await execAsync(
495
+ `${CLI} validate ${updatedDir} --referenceBuildInfoDirs ${referenceDir} --contract MyContract`,
496
+ );
497
+ t.snapshot(output);
498
+ });
499
+
500
+ test('validate - references other build info dir by annotation - bad', async t => {
501
+ const temp = await getTempDir(t);
502
+ const referenceDir = path.join(temp, 'build-info-v1');
503
+ await fs.mkdir(referenceDir);
504
+
505
+ const referenceBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV1.sol:MyContract`);
506
+ await fs.writeFile(path.join(referenceDir, 'validate.json'), JSON.stringify(referenceBuildInfo));
507
+
508
+ const updatedDir = path.join(temp, 'build-info');
509
+ await fs.mkdir(updatedDir);
510
+
511
+ t.assert(updatedDir !== referenceDir);
512
+ const updatedBuildInfo = await artifacts.getBuildInfo(
513
+ `contracts/test/cli/ValidateBuildInfoV2_Annotation_Bad.sol:MyContract`,
514
+ );
515
+ await fs.writeFile(path.join(updatedDir, 'validate.json'), JSON.stringify(updatedBuildInfo));
516
+
517
+ const error = await t.throwsAsync(
518
+ execAsync(`${CLI} validate ${updatedDir} --referenceBuildInfoDirs ${referenceDir} --contract MyContract`),
519
+ );
520
+ const expectation: string[] = [`Stdout: ${(error as any).stdout}`, `Stderr: ${(error as any).stderr}`];
521
+ t.snapshot(expectation.join('\n'));
522
+ });
523
+
524
+ test('validate - contract must not have build info dir name', async t => {
525
+ const temp = await getTempDir(t);
526
+ const referenceDir = path.join(temp, 'build-info-v1');
527
+ await fs.mkdir(referenceDir);
528
+
529
+ const referenceBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV1.sol:MyContract`);
530
+ await fs.writeFile(path.join(referenceDir, 'validate.json'), JSON.stringify(referenceBuildInfo));
531
+
532
+ const updatedDir = path.join(temp, 'build-info');
533
+ await fs.mkdir(updatedDir);
534
+
535
+ const updatedBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract`);
536
+ await fs.writeFile(path.join(updatedDir, 'validate.json'), JSON.stringify(updatedBuildInfo));
537
+
538
+ const error = await t.throwsAsync(
539
+ execAsync(
540
+ `${CLI} validate ${updatedDir} --referenceBuildInfoDirs ${referenceDir} --contract build-info:MyContract --reference build-info-v1:MyContract`,
541
+ ),
542
+ );
543
+ t.true(
544
+ error?.message.includes('Contract build-info:MyContract must be specified without a build info directory name'),
545
+ error?.message,
546
+ );
547
+ });
548
+
549
+ test('validate - contract must not have build info dir name - fully qualified', async t => {
550
+ const temp = await getTempDir(t);
551
+ const referenceDir = path.join(temp, 'build-info-v1');
552
+ await fs.mkdir(referenceDir);
553
+
554
+ const referenceBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV1.sol:MyContract`);
555
+ await fs.writeFile(path.join(referenceDir, 'validate.json'), JSON.stringify(referenceBuildInfo));
556
+
557
+ const updatedDir = path.join(temp, 'build-info');
558
+ await fs.mkdir(updatedDir);
559
+
560
+ const updatedBuildInfo = await artifacts.getBuildInfo(`contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract`);
561
+ await fs.writeFile(path.join(updatedDir, 'validate.json'), JSON.stringify(updatedBuildInfo));
562
+
563
+ const error = await t.throwsAsync(
564
+ execAsync(
565
+ `${CLI} validate ${updatedDir} --referenceBuildInfoDirs ${referenceDir} --contract build-info:contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract --reference build-info-v1:MyContract`,
566
+ ),
567
+ );
568
+ t.true(
569
+ error?.message.includes(
570
+ 'Contract build-info:contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract must be specified without a build info directory name',
571
+ ),
572
+ error?.message,
573
+ );
574
+ });
@@ -19,6 +19,7 @@ Generated by [AVA](https://avajs.dev).
19
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
20
  --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.␊
21
21
  --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.␊
22
+ --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.␊
22
23
  --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, internal-function-storage␊
23
24
  --unsafeAllowRenames Configure storage layout check to allow variable renaming.␊
24
25
  --unsafeSkipStorageCheck Skips checking for storage layout compatibility errors. This is a dangerous option meant to be used as a last resort.␊
@@ -39,6 +40,7 @@ Generated by [AVA](https://avajs.dev).
39
40
  --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.␊
40
41
  --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.␊
41
42
  --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.␊
43
+ --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.␊
42
44
  --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, internal-function-storage␊
43
45
  --unsafeAllowRenames Configure storage layout check to allow variable renaming.␊
44
46
  --unsafeSkipStorageCheck Skips checking for storage layout compatibility errors. This is a dangerous option meant to be used as a last resort.␊
@@ -187,6 +189,22 @@ Generated by [AVA](https://avajs.dev).
187
189
 
188
190
  ## validate - single contract, reference
189
191
 
192
+ > Snapshot 1
193
+
194
+ `Stdout: ✘ contracts/test/cli/Validate.sol:BecomesBadLayout (upgrades from contracts/test/cli/Validate.sol:StorageV1)␊
195
+ ␊
196
+ StorageV1: Deleted \`x\`␊
197
+ > Keep the variable even if unused␊
198
+ ␊
199
+ StorageV1: Deleted \`__gap\`␊
200
+ > Keep the variable even if unused␊
201
+ ␊
202
+ FAILED␊
203
+ ␊
204
+ Stderr: `
205
+
206
+ ## validate - single contract, reference with same build info dir
207
+
190
208
  > Snapshot 1
191
209
 
192
210
  `Stdout: ✘ contracts/test/cli/Validate.sol:BecomesBadLayout (upgrades from contracts/test/cli/Validate.sol:StorageV1)␊
@@ -304,3 +322,104 @@ Generated by [AVA](https://avajs.dev).
304
322
  ␊
305
323
  SUCCESS␊
306
324
  `
325
+
326
+ ## validate - references other build info dir by command - ok
327
+
328
+ > Snapshot 1
329
+
330
+ {
331
+ stderr: '',
332
+ stdout: ` ✔ contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract (upgrades from build-info-v1:contracts/test/cli/ValidateBuildInfoV1.sol:MyContract)␊
333
+ ␊
334
+ SUCCESS␊
335
+ `,
336
+ }
337
+
338
+ ## validate - references other build info dir by command with fully qualified names - ok
339
+
340
+ > Snapshot 1
341
+
342
+ {
343
+ stderr: '',
344
+ stdout: ` ✔ contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract (upgrades from build-info-v1:contracts/test/cli/ValidateBuildInfoV1.sol:MyContract)␊
345
+ ␊
346
+ SUCCESS␊
347
+ `,
348
+ }
349
+
350
+ ## validate - references other build info dir by command - bad
351
+
352
+ > Snapshot 1
353
+
354
+ `Stdout: ✘ contracts/test/cli/ValidateBuildInfoV2_Bad.sol:MyContract (upgrades from build-info-v1:contracts/test/cli/ValidateBuildInfoV1.sol:MyContract)␊
355
+ ␊
356
+ MyContract: Deleted \`y\`␊
357
+ > Keep the variable even if unused␊
358
+ ␊
359
+ contracts/test/cli/ValidateBuildInfoV2_Bad.sol:6: Upgraded \`__gap\` to an incompatible type␊
360
+ - Bad storage gap resize from 48 to 49␊
361
+ Size cannot increase␊
362
+ ␊
363
+ FAILED␊
364
+ ␊
365
+ Stderr: `
366
+
367
+ ## validate - references other build info dir by command with fully qualified names - bad
368
+
369
+ > Snapshot 1
370
+
371
+ `Stdout: ✘ contracts/test/cli/ValidateBuildInfoV2_Bad.sol:MyContract (upgrades from build-info-v1:contracts/test/cli/ValidateBuildInfoV1.sol:MyContract)␊
372
+ ␊
373
+ MyContract: Deleted \`y\`␊
374
+ > Keep the variable even if unused␊
375
+ ␊
376
+ contracts/test/cli/ValidateBuildInfoV2_Bad.sol:6: Upgraded \`__gap\` to an incompatible type␊
377
+ - Bad storage gap resize from 48 to 49␊
378
+ Size cannot increase␊
379
+ ␊
380
+ FAILED␊
381
+ ␊
382
+ Stderr: `
383
+
384
+ ## validate - references multiple build info dirs by annotation
385
+
386
+ > Snapshot 1
387
+
388
+ `Stdout: ✘ contracts/test/cli/ValidateBuildInfoV2_Branch_Bad.sol:MyContract (upgrades from build-info-v2:contracts/test/cli/ValidateBuildInfoV2_Ok.sol:MyContract)␊
389
+ ␊
390
+ contracts/test/cli/ValidateBuildInfoV2_Branch_Bad.sol:8: Renamed \`z\` to \`zBranch\`␊
391
+ ␊
392
+ ✔ contracts/test/cli/ValidateBuildInfoV2_Branch_Ok.sol:MyContract (upgrades from build-info-v1:contracts/test/cli/ValidateBuildInfoV1.sol:MyContract)␊
393
+ ␊
394
+ FAILED (2 upgradeable contracts detected, 1 passed, 1 failed)␊
395
+ ␊
396
+ Stderr: `
397
+
398
+ ## validate - references other build info dir by annotation - ok
399
+
400
+ > Snapshot 1
401
+
402
+ {
403
+ stderr: '',
404
+ stdout: ` ✔ contracts/test/cli/ValidateBuildInfoV2_Annotation_Ok.sol:MyContract (upgrades from build-info-v1:contracts/test/cli/ValidateBuildInfoV1.sol:MyContract)␊
405
+ ␊
406
+ SUCCESS␊
407
+ `,
408
+ }
409
+
410
+ ## validate - references other build info dir by annotation - bad
411
+
412
+ > Snapshot 1
413
+
414
+ `Stdout: ✘ contracts/test/cli/ValidateBuildInfoV2_Annotation_Bad.sol:MyContract (upgrades from build-info-v1:contracts/test/cli/ValidateBuildInfoV1.sol:MyContract)␊
415
+ ␊
416
+ MyContract: Deleted \`y\`␊
417
+ > Keep the variable even if unused␊
418
+ ␊
419
+ contracts/test/cli/ValidateBuildInfoV2_Annotation_Bad.sol:7: Upgraded \`__gap\` to an incompatible type␊
420
+ - Bad storage gap resize from 48 to 49␊
421
+ Size cannot increase␊
422
+ ␊
423
+ FAILED␊
424
+ ␊
425
+ Stderr: `
Binary file
@@ -49,6 +49,11 @@ export interface BuildInfoFile {
49
49
  * The Solidity compiler output JSON object.
50
50
  */
51
51
  output: SolcOutput;
52
+
53
+ /**
54
+ * Short name of the build info dir containing this file.
55
+ */
56
+ dirShortName: string;
52
57
  }
53
58
 
54
59
  /**
@@ -57,13 +62,18 @@ export interface BuildInfoFile {
57
62
  * @param buildInfoDir Build info directory, or undefined to use the default Hardhat or Foundry build-info dir.
58
63
  * @returns The build info files with Solidity compiler input and output.
59
64
  */
60
- export async function getBuildInfoFiles(buildInfoDir?: string) {
65
+ export async function getBuildInfoFiles(buildInfoDir?: string): Promise<BuildInfoFile[]> {
61
66
  const dir = await findDir(buildInfoDir);
67
+ const shortName = path.basename(dir);
62
68
  const jsonFiles = await getJsonFiles(dir);
63
- return await readBuildInfo(jsonFiles);
69
+
70
+ return await readBuildInfo(jsonFiles, shortName);
64
71
  }
65
72
 
66
- async function findDir(buildInfoDir: string | undefined) {
73
+ /**
74
+ * Finds the build info dir if provided, otherwise finds the default Hardhat or Foundry build info dir. Throws an error if no build info files were found in the expected dir.
75
+ */
76
+ async function findDir(buildInfoDir?: string): Promise<string> {
67
77
  if (buildInfoDir !== undefined && !(await hasJsonFiles(buildInfoDir))) {
68
78
  throw new ValidateCommandError(
69
79
  `The directory '${buildInfoDir}' does not exist or does not contain any build info files.`,
@@ -123,7 +133,7 @@ async function getJsonFiles(dir: string): Promise<string[]> {
123
133
  return jsonFiles.map(file => path.join(dir, file));
124
134
  }
125
135
 
126
- async function readBuildInfo(buildInfoFilePaths: string[]) {
136
+ async function readBuildInfo(buildInfoFilePaths: string[], dirShortName: string) {
127
137
  const buildInfoFiles: BuildInfoFile[] = [];
128
138
 
129
139
  for (const buildInfoFilePath of buildInfoFilePaths) {
@@ -143,6 +153,7 @@ async function readBuildInfo(buildInfoFilePaths: string[]) {
143
153
  input: buildInfoJson.input,
144
154
  output: buildInfoJson.output,
145
155
  solcVersion: buildInfoJson.solcVersion,
156
+ dirShortName: dirShortName,
146
157
  });
147
158
  }
148
159
  }
@@ -17,7 +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
+ import { BuildInfoDictionary, SpecifiedContracts } from './validate-upgrade-safety';
21
21
 
22
22
  /**
23
23
  * Report for an upgradeable contract.
@@ -58,29 +58,30 @@ export class UpgradeableContractReport implements Report {
58
58
  }
59
59
 
60
60
  /**
61
- * Gets upgradeble contract reports for the upgradeable contracts in the given set of source contracts.
61
+ * Gets upgradeble contract reports for the upgradeable contracts in the set of source contracts at dictionary key ''.
62
+ * Reference contracts can come from source contracts at the corresponding dictionary key.
62
63
  * Only contracts that are detected as upgradeable will be included in the reports.
63
64
  * Reports include upgradeable contracts regardless of whether they pass or fail upgrade safety checks.
64
65
  *
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.
66
+ * @param buildInfoDictionary Dictionary of build info directories and the source contracts they contain.
66
67
  * @param opts The validation options.
67
68
  * @param specifiedContracts If provided, only the specified contract (upgrading from its reference contract) will be reported.
68
69
  * @returns The upgradeable contract reports.
69
70
  */
70
71
  export function getContractReports(
71
- sourceContracts: SourceContract[],
72
+ buildInfoDictionary: BuildInfoDictionary,
72
73
  opts: Required<ValidateUpgradeSafetyOptions>,
73
74
  specifiedContracts?: SpecifiedContracts,
74
75
  ) {
75
76
  const upgradeableContractReports: UpgradeableContractReport[] = [];
76
77
 
77
78
  const contractsToReport: SourceContract[] =
78
- specifiedContracts !== undefined ? [specifiedContracts.contract] : sourceContracts;
79
+ specifiedContracts !== undefined ? [specifiedContracts.contract] : buildInfoDictionary[''];
79
80
 
80
81
  for (const sourceContract of contractsToReport) {
81
82
  const upgradeabilityAssessment = getUpgradeabilityAssessment(
82
83
  sourceContract,
83
- sourceContracts,
84
+ buildInfoDictionary,
84
85
  specifiedContracts?.reference,
85
86
  );
86
87
  if (opts.requireReference && upgradeabilityAssessment.referenceContract === undefined) {
@@ -129,7 +130,11 @@ function getUpgradeableContractReport(
129
130
  const referenceVersion = getContractVersion(referenceContract.validationData, referenceContract.fullyQualifiedName);
130
131
  const referenceLayout = getStorageLayout(referenceContract.validationData, referenceVersion);
131
132
 
132
- reference = referenceContract.fullyQualifiedName;
133
+ if (referenceContract.buildInfoDirShortName !== contract.buildInfoDirShortName) {
134
+ reference = `${referenceContract.buildInfoDirShortName}:${referenceContract.fullyQualifiedName}`;
135
+ } else {
136
+ reference = referenceContract.fullyQualifiedName;
137
+ }
133
138
  storageLayoutReport = getStorageUpgradeReport(referenceLayout, layout, withValidationDefaults(opts));
134
139
  }
135
140
 
@@ -1,4 +1,6 @@
1
+ import { assert } from '../../utils/assert';
1
2
  import { ValidateCommandError } from './error';
3
+ import { BuildInfoDictionary } from './validate-upgrade-safety';
2
4
  import { SourceContract } from './validations';
3
5
 
4
6
  export class ReferenceContractNotFound extends Error {
@@ -12,7 +14,12 @@ export class ReferenceContractNotFound extends Error {
12
14
  */
13
15
  readonly origin?: string;
14
16
 
15
- constructor(reference: string, origin?: string) {
17
+ /**
18
+ * Build info directories that were also searched.
19
+ */
20
+ readonly buildInfoDirs?: string[];
21
+
22
+ constructor(reference: string, origin?: string, buildInfoDirs?: string[]) {
16
23
  const msg =
17
24
  origin !== undefined
18
25
  ? `Could not find contract ${reference} referenced in ${origin}.`
@@ -20,11 +27,30 @@ export class ReferenceContractNotFound extends Error {
20
27
  super(msg);
21
28
  this.reference = reference;
22
29
  this.origin = origin;
30
+ this.buildInfoDirs = buildInfoDirs;
23
31
  }
24
32
  }
25
33
 
26
- export function findContract(contractName: string, origin: SourceContract | undefined, allContracts: SourceContract[]) {
27
- const foundContracts = allContracts.filter(c => c.fullyQualifiedName === contractName || c.name === contractName);
34
+ export function findContract(
35
+ contractName: string,
36
+ origin: SourceContract | undefined,
37
+ buildInfoDictionary: BuildInfoDictionary,
38
+ onlyMainBuildInfoDir = false,
39
+ ) {
40
+ const foundContracts: SourceContract[] = [];
41
+ if (onlyMainBuildInfoDir) {
42
+ if (hasBuildInfoDirWithContractName(contractName) || hasBuildInfoDirWithFullyQualifiedName(contractName)) {
43
+ throw new ValidateCommandError(
44
+ `Contract ${contractName} must be specified without a build info directory name`,
45
+ () => `Build info directory names can only be specified for reference contracts.`,
46
+ );
47
+ }
48
+ foundContracts.push(...buildInfoDictionary[''].filter(c => isMatchFound(contractName, c, '')));
49
+ } else {
50
+ for (const [dir, contracts] of Object.entries(buildInfoDictionary)) {
51
+ foundContracts.push(...contracts.filter(c => isMatchFound(contractName, c, dir)));
52
+ }
53
+ }
28
54
 
29
55
  if (foundContracts.length > 1) {
30
56
  const msg =
@@ -39,6 +65,26 @@ export function findContract(contractName: string, origin: SourceContract | unde
39
65
  } else if (foundContracts.length === 1) {
40
66
  return foundContracts[0];
41
67
  } else {
42
- throw new ReferenceContractNotFound(contractName, origin?.fullyQualifiedName);
68
+ throw new ReferenceContractNotFound(contractName, origin?.fullyQualifiedName, Object.keys(buildInfoDictionary));
69
+ }
70
+ }
71
+
72
+ function isMatchFound(contractName: string, foundContract: SourceContract, buildInfoDirShortName: string): boolean {
73
+ let prefix = '';
74
+ if (buildInfoDirShortName.length > 0) {
75
+ assert(foundContract.buildInfoDirShortName === buildInfoDirShortName);
76
+ prefix = `${buildInfoDirShortName}:`;
43
77
  }
78
+ return (
79
+ `${prefix}${foundContract.fullyQualifiedName}` === contractName || `${prefix}${foundContract.name}` === contractName
80
+ );
81
+ }
82
+
83
+ function hasBuildInfoDirWithContractName(contractName: string): boolean {
84
+ return contractName.split(':').length === 2 && !contractName.includes('.sol:');
85
+ }
86
+
87
+ function hasBuildInfoDirWithFullyQualifiedName(contractName: string): boolean {
88
+ const tokens = contractName.split(':');
89
+ return tokens.length === 3 && tokens[1].endsWith('.sol');
44
90
  }
@@ -2,6 +2,7 @@ import { getAnnotationArgs, getDocumentation, hasAnnotationTag } from '../../uti
2
2
  import { inferInitializable, inferUUPS } from '../../validate/query';
3
3
  import { ValidateCommandError } from './error';
4
4
  import { findContract } from './find-contract';
5
+ import { BuildInfoDictionary } from './validate-upgrade-safety';
5
6
  import { SourceContract } from './validations';
6
7
 
7
8
  interface AnnotationAssessment {
@@ -17,7 +18,7 @@ export interface UpgradeabilityAssessment {
17
18
 
18
19
  export function getUpgradeabilityAssessment(
19
20
  contract: SourceContract,
20
- allContracts: SourceContract[],
21
+ buildInfoDictionary: BuildInfoDictionary,
21
22
  overrideReferenceContract?: SourceContract,
22
23
  ): UpgradeabilityAssessment {
23
24
  const fullContractName = contract.fullyQualifiedName;
@@ -29,7 +30,7 @@ export function getUpgradeabilityAssessment(
29
30
 
30
31
  let referenceContract = overrideReferenceContract;
31
32
  if (referenceContract === undefined && annotationAssessment.referenceName !== undefined) {
32
- referenceContract = findContract(annotationAssessment.referenceName, contract, allContracts);
33
+ referenceContract = findContract(annotationAssessment.referenceName, contract, buildInfoDictionary);
33
34
  }
34
35
 
35
36
  let isReferenceUUPS = false;
@@ -100,7 +100,7 @@ test('invalid annotation args - upgrades', async t => {
100
100
 
101
101
  test('findSpecifiedContracts - requireReference option without contract', async t => {
102
102
  try {
103
- findSpecifiedContracts([], withCliDefaults({ requireReference: true }));
103
+ findSpecifiedContracts({}, withCliDefaults({ requireReference: true }));
104
104
  } catch (e: any) {
105
105
  t.true(
106
106
  e.message.includes(