@openzeppelin/upgrades-core 1.46.0 → 1.47.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 (63) hide show
  1. package/dist/cli/validate/validate-upgrade-safety.test.js +18 -0
  2. package/dist/cli/validate/validate-upgrade-safety.test.js.map +1 -1
  3. package/dist/storage/compare.d.ts.map +1 -1
  4. package/dist/storage/compare.js +3 -3
  5. package/dist/storage/compare.js.map +1 -1
  6. package/dist/storage/extract.d.ts +15 -3
  7. package/dist/storage/extract.d.ts.map +1 -1
  8. package/dist/storage/extract.js +47 -15
  9. package/dist/storage/extract.js.map +1 -1
  10. package/dist/storage/namespace.d.ts.map +1 -1
  11. package/dist/storage/namespace.js +8 -2
  12. package/dist/storage/namespace.js.map +1 -1
  13. package/dist/storage/report-rename-retype.test.js +30 -0
  14. package/dist/storage/report-rename-retype.test.js.map +1 -1
  15. package/dist/storage/report.js +9 -1
  16. package/dist/storage/report.js.map +1 -1
  17. package/dist/storage-namespaced-renamed-retyped.test.d.ts +2 -0
  18. package/dist/storage-namespaced-renamed-retyped.test.d.ts.map +1 -0
  19. package/dist/storage-namespaced-renamed-retyped.test.js +278 -0
  20. package/dist/storage-namespaced-renamed-retyped.test.js.map +1 -0
  21. package/dist/storage-struct-member-renamed-retyped.test.d.ts +2 -0
  22. package/dist/storage-struct-member-renamed-retyped.test.d.ts.map +1 -0
  23. package/dist/storage-struct-member-renamed-retyped.test.js +256 -0
  24. package/dist/storage-struct-member-renamed-retyped.test.js.map +1 -0
  25. package/dist/utils/annotations.d.ts +1 -0
  26. package/dist/utils/annotations.d.ts.map +1 -1
  27. package/dist/utils/annotations.js +4 -1
  28. package/dist/utils/annotations.js.map +1 -1
  29. package/dist/utils/struct-member-docs.d.ts +52 -0
  30. package/dist/utils/struct-member-docs.d.ts.map +1 -0
  31. package/dist/utils/struct-member-docs.js +264 -0
  32. package/dist/utils/struct-member-docs.js.map +1 -0
  33. package/dist/utils/struct-member-docs.test.d.ts +2 -0
  34. package/dist/utils/struct-member-docs.test.d.ts.map +1 -0
  35. package/dist/utils/struct-member-docs.test.js +320 -0
  36. package/dist/utils/struct-member-docs.test.js.map +1 -0
  37. package/dist/validate/data.d.ts +1 -1
  38. package/dist/validate/data.d.ts.map +1 -1
  39. package/dist/validate/data.js +1 -1
  40. package/dist/validate/run.d.ts +1 -1
  41. package/dist/validate/run.d.ts.map +1 -1
  42. package/dist/validate/run.js +22 -2
  43. package/dist/validate/run.js.map +1 -1
  44. package/package.json +3 -3
  45. package/src/cli/validate/validate-upgrade-safety.test.ts +35 -0
  46. package/src/cli/validate/validate-upgrade-safety.test.ts.md +10 -0
  47. package/src/cli/validate/validate-upgrade-safety.test.ts.snap +0 -0
  48. package/src/storage/compare.ts +3 -4
  49. package/src/storage/extract.ts +55 -18
  50. package/src/storage/namespace.ts +10 -3
  51. package/src/storage/report-rename-retype.test.ts +41 -0
  52. package/src/storage/report.ts +9 -1
  53. package/src/storage-namespaced-renamed-retyped.test.ts +338 -0
  54. package/src/storage-namespaced-renamed-retyped.test.ts.md +183 -0
  55. package/src/storage-namespaced-renamed-retyped.test.ts.snap +0 -0
  56. package/src/storage-struct-member-renamed-retyped.test.ts +299 -0
  57. package/src/storage-struct-member-renamed-retyped.test.ts.md +260 -0
  58. package/src/storage-struct-member-renamed-retyped.test.ts.snap +0 -0
  59. package/src/utils/annotations.ts +4 -4
  60. package/src/utils/struct-member-docs.test.ts +351 -0
  61. package/src/utils/struct-member-docs.ts +293 -0
  62. package/src/validate/data.ts +2 -2
  63. package/src/validate/run.ts +23 -1
@@ -203,12 +203,11 @@ export class StorageLayoutComparator {
203
203
  original.label !== updated.renamedFrom &&
204
204
  (updated.label !== original.label ||
205
205
  (updated.renamedFrom !== undefined && updated.renamedFrom !== original.renamedFrom));
206
- const typeChange =
207
- !this.isRetypedFromOriginal(original, updated) &&
208
- this.getTypeChange(original.type, updated.type, { allowAppend: false });
206
+ const retypedFromOriginal = this.isRetypedFromOriginal(original, updated);
207
+ const typeChange = !retypedFromOriginal && this.getTypeChange(original.type, updated.type, { allowAppend: false });
209
208
  const layoutChange = this.getLayoutChange(original, updated);
210
209
 
211
- if (updated.retypedFrom && layoutChange && (!layoutChange.uncertain || !layoutChange.knownCompatible)) {
210
+ if (retypedFromOriginal && layoutChange && (!layoutChange.uncertain || !layoutChange.knownCompatible)) {
212
211
  return { kind: 'layoutchange', original, updated, change: layoutChange, cost: LAYOUTCHANGE_COST };
213
212
  } else if (nameChange && endMatchesGap(original, updated)) {
214
213
  // original is a gap that was consumed by a non-gap ending at the same slot, which is safe
@@ -8,12 +8,14 @@ import {
8
8
  TypeName,
9
9
  } from 'solidity-ast';
10
10
  import { isNodeType, findAll, ASTDereferencer } from 'solidity-ast/utils';
11
- import { StorageLayout, StructMember, TypeItem, isStructMembers, EnumMember } from './layout';
11
+ import { StorageLayout, StorageField, StructMember, TypeItem, isStructMembers, EnumMember } from './layout';
12
12
  import { normalizeTypeIdentifier } from '../utils/type-id';
13
13
  import { SrcDecoder } from '../src-decoder';
14
14
  import { mapValues } from '../utils/map-values';
15
15
  import { pick } from '../utils/pick';
16
16
  import { execall } from '../utils/execall';
17
+ import { ANNOTATION_REGEX, getDocumentation } from '../utils/annotations';
18
+ import { StructDocsResolver } from '../utils/struct-member-docs';
17
19
  import { loadNamespaces } from './namespace';
18
20
  import { UpgradesError } from '../error';
19
21
  import { resolveBaseSlot } from '../utils/erc7201-builtin';
@@ -28,6 +30,7 @@ export interface CompilationContext {
28
30
  deref: ASTDereferencer;
29
31
  contractDef: ContractDefinition;
30
32
  storageLayout?: StorageLayout;
33
+ structDocs?: StructDocsResolver;
31
34
  }
32
35
 
33
36
  export function extractStorageLayout(
@@ -36,6 +39,7 @@ export function extractStorageLayout(
36
39
  deref: ASTDereferencer,
37
40
  storageLayout?: StorageLayout,
38
41
  namespacedContext?: CompilationContext,
42
+ structDocs?: StructDocsResolver,
39
43
  ): StorageLayout {
40
44
  const layout: StorageLayout = { storage: [], types: {}, layoutVersion: currentLayoutVersion, flat: false };
41
45
 
@@ -72,7 +76,7 @@ export function extractStorageLayout(
72
76
  const { varDecl, contract } = origin;
73
77
  const { renamedFrom, retypedFrom } = getRetypedRenamed(varDecl);
74
78
  // Solc layout doesn't bring members for enums so we get them using the ast method
75
- loadLayoutType(varDecl.typeName, layout, deref);
79
+ loadLayoutType(varDecl.typeName, layout, deref, structDocs);
76
80
  const { label, offset, slot, type } = storage;
77
81
  const src = decodeSrc(varDecl);
78
82
  layout.storage.push({ label, offset, slot, type, contract, src, retypedFrom, renamedFrom });
@@ -93,13 +97,13 @@ export function extractStorageLayout(
93
97
  renamedFrom,
94
98
  });
95
99
 
96
- loadLayoutType(varDecl.typeName, layout, deref);
100
+ loadLayoutType(varDecl.typeName, layout, deref, structDocs);
97
101
  }
98
102
  }
99
103
  }
100
104
  }
101
105
 
102
- const origContext = { deref, contractDef, storageLayout };
106
+ const origContext = { deref, contractDef, storageLayout, structDocs };
103
107
  loadNamespaces(decodeSrc, layout, origContext, namespacedContext);
104
108
 
105
109
  return layout;
@@ -201,7 +205,12 @@ function getOriginContract(contract: ContractDefinition, astId: number | undefin
201
205
  }
202
206
  }
203
207
 
204
- export function loadLayoutType(typeName: TypeName | null | undefined, layout: StorageLayout, deref: ASTDereferencer) {
208
+ export function loadLayoutType(
209
+ typeName: TypeName | null | undefined,
210
+ layout: StorageLayout,
211
+ deref: ASTDereferencer,
212
+ structDocs?: StructDocsResolver,
213
+ ) {
205
214
  // Note: A UserDefinedTypeName can also refer to a ContractDefinition but we won't care about those.
206
215
  const derefUserDefinedType = deref(['StructDefinition', 'EnumDefinition', 'UserDefinedValueTypeDefinition']);
207
216
 
@@ -224,6 +233,10 @@ export function loadLayoutType(typeName: TypeName | null | undefined, layout: St
224
233
  layout.types[type].underlying = typeDef.underlyingType.typeDescriptions.typeIdentifier ?? undefined;
225
234
  } else {
226
235
  layout.types[type].members ??= getTypeMembers(typeDef);
236
+ if (typeDef.nodeType === 'StructDefinition' && structDocs !== undefined) {
237
+ // Add annotations whether the members came from the compiler's storage layout or from the AST above
238
+ setStructMemberAnnotations(layout.types[type], structDocs(typeDef));
239
+ }
227
240
  }
228
241
 
229
242
  // Recursively look for the types referenced in this definition and add them to the queue.
@@ -238,22 +251,46 @@ export function loadLayoutType(typeName: TypeName | null | undefined, layout: St
238
251
  }
239
252
 
240
253
  function getRetypedRenamed(varDecl: VariableDeclaration) {
254
+ return getRetypedRenamedFromDoc(getDocumentation(varDecl));
255
+ }
256
+
257
+ export function getRetypedRenamedFromDoc(doc: string) {
241
258
  let retypedFrom, renamedFrom;
242
- if ('documentation' in varDecl) {
243
- const docs =
244
- typeof varDecl.documentation === 'string' ? varDecl.documentation : (varDecl.documentation?.text ?? '');
245
- for (const { groups } of execall(
246
- /^\s*(?:@(?<title>\w+)(?::(?<tag>[a-z][a-z-]*))? )?(?<args>(?:(?!^\s@\w+)[^])*)/m,
247
- docs,
248
- )) {
249
- if (groups?.title === 'custom') {
250
- if (groups.tag === 'oz-retyped-from') {
251
- retypedFrom = groups.args.trim();
252
- } else if (groups.tag === 'oz-renamed-from') {
253
- renamedFrom = groups.args.trim();
254
- }
259
+ for (const { groups } of execall(ANNOTATION_REGEX, doc)) {
260
+ if (groups?.title === 'custom') {
261
+ if (groups.tag === 'oz-retyped-from') {
262
+ retypedFrom = groups.args.trim();
263
+ } else if (groups.tag === 'oz-renamed-from') {
264
+ renamedFrom = groups.args.trim();
255
265
  }
256
266
  }
257
267
  }
258
268
  return { retypedFrom, renamedFrom };
259
269
  }
270
+
271
+ /**
272
+ * Sets `renamedFrom` and `retypedFrom` on the given layout field (a namespace member or a member of a struct type)
273
+ * from the annotations in that member's NatSpec, for the storage layout comparison to use. Each field is only set if
274
+ * the NatSpec has the corresponding annotation, so unannotated members have no `renamedFrom` or `retypedFrom` key.
275
+ */
276
+ export function setMemberAnnotations(field: StorageField, docs: Map<string, string>) {
277
+ const doc = docs.get(field.label);
278
+ if (doc !== undefined) {
279
+ const { retypedFrom, renamedFrom } = getRetypedRenamedFromDoc(doc);
280
+ if (retypedFrom !== undefined) {
281
+ field.retypedFrom = retypedFrom;
282
+ }
283
+ if (renamedFrom !== undefined) {
284
+ field.renamedFrom = renamedFrom;
285
+ }
286
+ }
287
+ }
288
+
289
+ function setStructMemberAnnotations(typeItem: TypeItem, docs: Map<string, string> | undefined) {
290
+ if (docs !== undefined) {
291
+ assert(typeItem.members !== undefined && isStructMembers(typeItem.members));
292
+ for (const member of typeItem.members) {
293
+ setMemberAnnotations(member, docs);
294
+ }
295
+ }
296
+ }
@@ -5,7 +5,7 @@ import { StorageItem, StorageLayout, TypeItem, isStructMembers } from './layout'
5
5
  import { SrcDecoder } from '../src-decoder';
6
6
  import { getAnnotationArgs, getDocumentation, hasAnnotationTag } from '../utils/annotations';
7
7
  import { Node } from 'solidity-ast/node';
8
- import { CompilationContext, getTypeMembers, loadLayoutType } from './extract';
8
+ import { CompilationContext, getTypeMembers, loadLayoutType, setMemberAnnotations } from './extract';
9
9
  import { UpgradesError } from '../error';
10
10
  import * as versions from 'compare-versions';
11
11
  import { calculateERC7201StorageLocation, ERC7201_FORMULA_PREFIX } from '../utils/erc7201';
@@ -49,7 +49,9 @@ export function loadNamespaces(
49
49
  const linearized = namespacedLinearized ?? origLinearized;
50
50
  for (const [i, contractDef] of linearized.entries()) {
51
51
  const origContractDef = origLinearized[i];
52
- const contractContext = { ...context, contractDef };
52
+ // Member documentation is read from the original sources, so the resolver comes from the original context
53
+ // even when the namespaced context is used to look up the structs.
54
+ const contractContext = { ...context, contractDef, structDocs: origContext.structDocs };
53
55
  addContractNamespacesWithSrc(
54
56
  namespacesWithSrc,
55
57
  decodeSrc,
@@ -248,6 +250,7 @@ function getNamespacedStorageItems(
248
250
  origContractDef: ContractDefinition,
249
251
  ): StorageItem[] {
250
252
  const storageItems: StorageItem[] = [];
253
+ const docs = context.structDocs === undefined ? undefined : context.structDocs(node);
251
254
 
252
255
  for (const astMember of getTypeMembers(node, { typeName: true })) {
253
256
  const item: StorageItem = {
@@ -257,6 +260,10 @@ function getNamespacedStorageItems(
257
260
  src: decodeSrc({ src: getOriginalMemberSrc(node.canonicalName, astMember.label, origContractDef) }),
258
261
  };
259
262
 
263
+ if (docs !== undefined) {
264
+ setMemberAnnotations(item, docs);
265
+ }
266
+
260
267
  const layoutMember = findLayoutStructMember(
261
268
  context.storageLayout?.types ?? {},
262
269
  node.canonicalName,
@@ -272,7 +279,7 @@ function getNamespacedStorageItems(
272
279
 
273
280
  // If context is namespaced, we have storage layout, and this will fill in enum members just like in extractStorageLayout.
274
281
  // If context is original, this will add the types from the namespace structs to the layout.
275
- loadLayoutType(astMember.typeName, layout, context.deref);
282
+ loadLayoutType(astMember.typeName, layout, context.deref, context.structDocs);
276
283
  }
277
284
  return storageItems;
278
285
  }
@@ -9,6 +9,8 @@ import { getStorageUpgradeErrors } from '.';
9
9
 
10
10
  interface Context {
11
11
  extractStorageLayout: (contract: string) => ReturnType<typeof extractStorageLayout>;
12
+ // Without the compiler's storage layout, the variables have no slots
13
+ extractStorageLayoutWithoutSlots: (contract: string) => ReturnType<typeof extractStorageLayout>;
12
14
  }
13
15
 
14
16
  const test = _test as TestFn<Context>;
@@ -50,6 +52,8 @@ test.before(async t => {
50
52
 
51
53
  t.context.extractStorageLayout = name =>
52
54
  extractStorageLayout(contracts[name], dummyDecodeSrc, deref[name], storageLayout[name]);
55
+ t.context.extractStorageLayoutWithoutSlots = name =>
56
+ extractStorageLayout(contracts[name], dummyDecodeSrc, deref[name]);
53
57
  });
54
58
 
55
59
  function getReport(original: StorageLayout, updated: StorageLayout) {
@@ -130,3 +134,40 @@ test('retype with layout change', t => {
130
134
  t.false(report.ok);
131
135
  t.snapshot(report.explain());
132
136
  });
137
+
138
+ test('retype without layout info', t => {
139
+ // Without slots, a retype is uncertain unless the types are known to be compatible
140
+ const v1 = t.context.extractStorageLayoutWithoutSlots('NonHardcodedRetypeV1');
141
+ const v2 = t.context.extractStorageLayoutWithoutSlots('NonHardcodedRetypeV2');
142
+ const report = getReport(v1, v2);
143
+ t.false(report.ok);
144
+ t.true(report.explain().includes('Layout could have changed for `a` (address -> bytes20)'), report.explain());
145
+
146
+ const compatibleV1 = t.context.extractStorageLayoutWithoutSlots('RetypeV1');
147
+ const compatibleV2 = t.context.extractStorageLayoutWithoutSlots('RetypeV2');
148
+ t.true(getReport(compatibleV1, compatibleV2).ok);
149
+ });
150
+
151
+ test('kept retype annotation without layout info', t => {
152
+ // Upgrading from and to the same contract: `a` stays `bytes20` and keeps its `@custom:oz-retyped-from address`
153
+ // annotation from an earlier retype, which doesn't apply to this upgrade
154
+ const original = t.context.extractStorageLayoutWithoutSlots('NonHardcodedRetypeV2');
155
+ const updated = t.context.extractStorageLayoutWithoutSlots('NonHardcodedRetypeV2');
156
+ t.true(getReport(original, updated).ok);
157
+ });
158
+
159
+ test('rename annotation without rename', t => {
160
+ const v1 = t.context.extractStorageLayout('StaleRenameV1');
161
+ const v2 = t.context.extractStorageLayout('StaleRenameV2');
162
+ const report = getReport(v1, v2);
163
+ t.false(report.ok);
164
+ t.is(
165
+ report.explain(),
166
+ "file.sol:1: `y` has a '@custom:oz-renamed-from x' annotation, but it was not renamed\n" +
167
+ ' > If the annotation is left over from an earlier upgrade, remove it.',
168
+ );
169
+
170
+ // A rename to a different name is reported as such
171
+ const renamed = getReport(v1, t.context.extractStorageLayout('RenameV1'));
172
+ t.is(renamed.explain(), 'file.sol:1: Renamed `y` to `x`');
173
+ });
@@ -109,8 +109,16 @@ function explainStorageOperation(op: StorageOperation<StorageField>, ctx: Storag
109
109
  case 'finishgap':
110
110
  return `Converted end of storage gap ${label(op.original)} to ${label(op.updated)}`;
111
111
 
112
- case 'rename':
112
+ case 'rename': {
113
+ if (op.original.label === op.updated.label && op.updated.renamedFrom !== undefined) {
114
+ // The field keeps its name, so the annotation describes a rename that did not happen in this upgrade
115
+ return printWithHints({
116
+ title: `${label(op.updated)} has a '@custom:oz-renamed-from ${op.updated.renamedFrom}' annotation, but it was not renamed`,
117
+ hints: ['If the annotation is left over from an earlier upgrade, remove it.'],
118
+ });
119
+ }
113
120
  return `Renamed ${label(op.original)} to ${label(op.updated)}`;
121
+ }
114
122
 
115
123
  case 'replace':
116
124
  return `Replaced ${label(op.original)} with ${label(op.updated)} of incompatible type`;
@@ -0,0 +1,338 @@
1
+ import _test, { ExecutionContext, TestFn } from 'ava';
2
+ import { artifacts, run } from 'hardhat';
3
+ import {
4
+ TASK_COMPILE_SOLIDITY_GET_SOLC_BUILD,
5
+ TASK_COMPILE_SOLIDITY_RUN_SOLC,
6
+ TASK_COMPILE_SOLIDITY_RUN_SOLCJS,
7
+ } from 'hardhat/builtin-tasks/task-names';
8
+ import { SolcBuild } from 'hardhat/types/builtin-tasks';
9
+
10
+ import { SolcInput, SolcOutput } from './solc-api';
11
+ import { validate, ValidationRunData } from './validate';
12
+ import { withValidationDefaults } from './validate/overrides';
13
+ import { solcInputOutputDecoder } from './src-decoder';
14
+ import { makeNamespacedInput, trySanitizeNatSpec } from './utils/make-namespaced';
15
+ import { getStorageUpgradeErrors, getStorageUpgradeReport } from './storage';
16
+ import { StorageLayout, isStructMembers } from './storage/layout';
17
+ import { stabilizeStorageLayout } from './utils/stabilize-layout';
18
+
19
+ const source = 'contracts/test/NamespacedRenamedRetyped.sol';
20
+ const crossFileSource = 'contracts/test/NamespacedCrossFile.sol';
21
+
22
+ interface Context {
23
+ input: SolcInput;
24
+ output: SolcOutput;
25
+ namespacedOutput: SolcOutput;
26
+ solcVersion: string;
27
+ validation: ValidationRunData;
28
+ // Validation without the namespaced compilation, as with the build info files of the CLI, so that namespace members
29
+ // have no slots
30
+ validationWithoutSlots: ValidationRunData;
31
+ // Validation of the fixture whose namespace has a member of a struct type from another source file
32
+ crossFile: { output: SolcOutput; validation: ValidationRunData };
33
+ }
34
+
35
+ const test = _test as TestFn<Context>;
36
+
37
+ test.before(async t => {
38
+ const { input, output, solcVersion } = await getBuildInfo(`${source}:NamespacedRenamedRetypedV1`);
39
+ const namespacedOutput = await compileNamespaced(input, output, solcVersion);
40
+ const decodeSrc = solcInputOutputDecoder(input, output);
41
+ t.context.input = input;
42
+ t.context.output = output;
43
+ t.context.namespacedOutput = namespacedOutput;
44
+ t.context.solcVersion = solcVersion;
45
+ t.context.validation = validate(output, decodeSrc, solcVersion, input, namespacedOutput);
46
+ t.context.validationWithoutSlots = validate(output, decodeSrc, solcVersion, input);
47
+
48
+ const crossFile = await getBuildInfo(`${crossFileSource}:NamespacedCrossFileV1`);
49
+ t.context.crossFile = {
50
+ output: crossFile.output,
51
+ validation: await validateWithNamespacedOutput(crossFile.input, crossFile.output, crossFile.solcVersion),
52
+ };
53
+ });
54
+
55
+ async function getBuildInfo(contract: string) {
56
+ const buildInfo = await artifacts.getBuildInfo(contract);
57
+ if (buildInfo === undefined) {
58
+ throw new Error(`Build info not found for ${contract}`);
59
+ }
60
+ return buildInfo;
61
+ }
62
+
63
+ /**
64
+ * Compiles the namespaced input for the given compilation, following the same steps as the Hardhat plugin.
65
+ */
66
+ async function compileNamespaced(input: SolcInput, output: SolcOutput, solcVersion: string) {
67
+ const namespacedInput = await trySanitizeNatSpec(makeNamespacedInput(input, output, solcVersion), solcVersion);
68
+ return hardhatCompile(namespacedInput, solcVersion);
69
+ }
70
+
71
+ /**
72
+ * Validates with a namespaced compilation.
73
+ */
74
+ async function validateWithNamespacedOutput(input: SolcInput, output: SolcOutput, solcVersion: string) {
75
+ const namespacedOutput = await compileNamespaced(input, output, solcVersion);
76
+ return validate(output, solcInputOutputDecoder(input, output), solcVersion, input, namespacedOutput);
77
+ }
78
+
79
+ /**
80
+ * Compiles the fixture again from the given source content and validates with a namespaced compilation.
81
+ */
82
+ async function compileAndValidate(input: SolcInput, content: string, solcVersion: string) {
83
+ const newInput = { ...input, sources: { [source]: { content } } };
84
+ const output = await hardhatCompile(newInput, solcVersion);
85
+ return validateWithNamespacedOutput(newInput, output, solcVersion);
86
+ }
87
+
88
+ async function hardhatCompile(input: SolcInput, solcVersion: string): Promise<SolcOutput> {
89
+ const solcBuild: SolcBuild = await run(TASK_COMPILE_SOLIDITY_GET_SOLC_BUILD, {
90
+ quiet: true,
91
+ solcVersion,
92
+ });
93
+
94
+ const output: SolcOutput = solcBuild.isSolcJs
95
+ ? await run(TASK_COMPILE_SOLIDITY_RUN_SOLCJS, { input, solcJsPath: solcBuild.compilerPath })
96
+ : await run(TASK_COMPILE_SOLIDITY_RUN_SOLC, { input, solcPath: solcBuild.compilerPath });
97
+
98
+ const errors = output.errors?.filter(e => e.severity === 'error');
99
+ if (errors !== undefined && errors.length > 0) {
100
+ throw new Error(errors.map(e => e.formattedMessage).join('\n'));
101
+ }
102
+ return output;
103
+ }
104
+
105
+ function getLayout(validation: ValidationRunData, contract: string): StorageLayout {
106
+ return validation[`${source}:${contract}`].layout;
107
+ }
108
+
109
+ function assertAnnotationsApplied(t: ExecutionContext<Context>, validation: ValidationRunData) {
110
+ const v1 = getLayout(validation, 'NamespacedRenamedRetypedV1');
111
+ const v2 = getLayout(validation, 'NamespacedRenamedRetypedV2');
112
+ t.deepEqual(getStorageUpgradeErrors(v1, v2), []);
113
+ assertExactMap(t, getLayout(validation, 'NamespacedExactMap'));
114
+ }
115
+
116
+ function assertExactMap(t: ExecutionContext<Context>, layout: StorageLayout) {
117
+ const items = layout.namespaces?.['erc7201:example.exact'];
118
+ if (items === undefined) {
119
+ throw new Error('Namespace not found');
120
+ }
121
+ t.is(items.length, 6);
122
+ for (const item of items) {
123
+ t.is(item.renamedFrom, `old_${item.label}`);
124
+ t.is(item.retypedFrom, undefined);
125
+ // Slots come from the namespaced compilation
126
+ t.not(item.slot, undefined);
127
+ }
128
+
129
+ // The struct used inside the namespace is looked up through the namespaced compilation
130
+ const members = Object.values(layout.types).find(type => type.label === 'struct NamespacedExactMap.Inner')?.members;
131
+ if (members === undefined || !isStructMembers(members)) {
132
+ throw new Error('Struct members not found');
133
+ }
134
+ t.is(members.length, 3);
135
+ for (const member of members) {
136
+ t.is(member.renamedFrom, `old_${member.label}`);
137
+ t.is(member.retypedFrom, undefined);
138
+ t.not(member.slot, undefined);
139
+ }
140
+ }
141
+
142
+ test('layout', t => {
143
+ t.snapshot(stabilizeStorageLayout(getLayout(t.context.validation, 'NamespacedRenamedRetypedV2')));
144
+ });
145
+
146
+ test('upgrade ok', t => {
147
+ assertAnnotationsApplied(t, t.context.validation);
148
+ });
149
+
150
+ test('inherited namespace upgrade ok', t => {
151
+ const v1 = getLayout(t.context.validation, 'NamespacedRenamedRetypedInheritedV1');
152
+ const v2 = getLayout(t.context.validation, 'NamespacedRenamedRetypedInheritedV2');
153
+ t.deepEqual(getStorageUpgradeErrors(v1, v2), []);
154
+ });
155
+
156
+ test('wrong retype', t => {
157
+ const v1 = getLayout(t.context.validation, 'NamespacedBadV1');
158
+ const v2 = getLayout(t.context.validation, 'NamespacedWrongRetypeV2');
159
+ t.like(getStorageUpgradeErrors(v1, v2), {
160
+ length: 1,
161
+ 0: { kind: 'typechange', original: { label: 'x' }, updated: { label: 'x', retypedFrom: 'address' } },
162
+ });
163
+ });
164
+
165
+ test('annotation on the wrong member', t => {
166
+ // The annotation makes `x` match the original `y` with a different type, and `x` and `z` are left unmatched
167
+ const v1 = getLayout(t.context.validation, 'NamespacedBadV1');
168
+ const v2 = getLayout(t.context.validation, 'NamespacedWrongMemberV2');
169
+ t.like(getStorageUpgradeErrors(v1, v2), {
170
+ length: 3,
171
+ 0: { kind: 'delete', original: { label: 'x' } },
172
+ 1: { kind: 'typechange', original: { label: 'y' }, updated: { label: 'x', renamedFrom: 'y' } },
173
+ 2: { kind: 'insert', updated: { label: 'z' } },
174
+ });
175
+ });
176
+
177
+ test('stale rename annotation', t => {
178
+ const v1 = getLayout(t.context.validation, 'NamespacedBadV1');
179
+ const v2 = getLayout(t.context.validation, 'NamespacedStaleRenameV2');
180
+ t.like(getStorageUpgradeErrors(v1, v2), {
181
+ length: 1,
182
+ 0: { kind: 'rename', original: { label: 'y' }, updated: { label: 'y', renamedFrom: 'q' } },
183
+ });
184
+ });
185
+
186
+ test('unannotated rename and retype', t => {
187
+ const v1 = getLayout(t.context.validation, 'NamespacedBadV1');
188
+ const v2 = getLayout(t.context.validation, 'NamespacedUnannotatedV2');
189
+ t.like(getStorageUpgradeErrors(v1, v2), {
190
+ length: 2,
191
+ 0: { kind: 'typechange', original: { label: 'x' }, updated: { label: 'x' } },
192
+ 1: { kind: 'rename', original: { label: 'y' }, updated: { label: 'z' } },
193
+ });
194
+ });
195
+
196
+ test('namespaced output with different source ids', t => {
197
+ const { input, output, namespacedOutput, solcVersion } = t.context;
198
+ const sources = Object.fromEntries(
199
+ Object.entries(namespacedOutput.sources).map(([path, s]) => [path, { ...s, id: s.id + 1 }]),
200
+ );
201
+ t.throws(
202
+ () => validate(output, solcInputOutputDecoder(input, output), solcVersion, input, { ...namespacedOutput, sources }),
203
+ { message: /Source ids differ between the solc output and the namespaced solc output/ },
204
+ );
205
+ });
206
+
207
+ test('two annotated namespaces in one contract', t => {
208
+ const v1 = getLayout(t.context.validation, 'NamespacedTwoV1');
209
+ const v2 = getLayout(t.context.validation, 'NamespacedTwoV2');
210
+ t.like(v2.namespaces, {
211
+ 'erc7201:example.first': {
212
+ length: 2,
213
+ 0: { label: 'x', retypedFrom: 'bytes32' },
214
+ 1: { label: 'y', renamedFrom: 'oldY' },
215
+ },
216
+ 'erc7201:example.second': {
217
+ length: 2,
218
+ 0: { label: 'f', retypedFrom: 'uint8' },
219
+ 1: { label: 'g', renamedFrom: 'oldG' },
220
+ },
221
+ });
222
+ t.deepEqual(getStorageUpgradeErrors(v1, v2), []);
223
+ });
224
+
225
+ test('kept retype annotation', t => {
226
+ const v1 = getLayout(t.context.validation, 'NamespacedKeptRetypeV1');
227
+ const v2 = getLayout(t.context.validation, 'NamespacedKeptRetypeV2');
228
+ t.like(v2.namespaces, { 'erc7201:example.main': { length: 1, 0: { label: 'x', retypedFrom: 'uint8', slot: '0' } } });
229
+ t.deepEqual(getStorageUpgradeErrors(v1, v2), []);
230
+ });
231
+
232
+ test('kept retype annotation without layout info', t => {
233
+ const v1 = getLayout(t.context.validationWithoutSlots, 'NamespacedKeptRetypeV1');
234
+ const v2 = getLayout(t.context.validationWithoutSlots, 'NamespacedKeptRetypeV2');
235
+ t.like(v2.namespaces, {
236
+ 'erc7201:example.main': { length: 1, 0: { label: 'x', retypedFrom: 'uint8', slot: undefined } },
237
+ });
238
+ t.deepEqual(getStorageUpgradeErrors(v1, v2), []);
239
+ });
240
+
241
+ test('retype without layout info', t => {
242
+ // A retype that is not known to be compatible is uncertain without slots
243
+ const v1 = getLayout(t.context.validationWithoutSlots, 'NamespacedBadV1');
244
+ const v2 = getLayout(t.context.validationWithoutSlots, 'NamespacedRetypeV2');
245
+ const report = getStorageUpgradeReport(v1, v2, withValidationDefaults({}));
246
+ t.false(report.ok);
247
+ t.true(
248
+ report.explain().includes('Layout could have changed for `x` (bytes32 -> struct TransientSlot.TBytes32)'),
249
+ report.explain(),
250
+ );
251
+ });
252
+
253
+ test('wrong retype without layout info', t => {
254
+ const v1 = getLayout(t.context.validationWithoutSlots, 'NamespacedBadV1');
255
+ const v2 = getLayout(t.context.validationWithoutSlots, 'NamespacedWrongRetypeV2');
256
+ t.like(getStorageUpgradeErrors(v1, v2), {
257
+ length: 1,
258
+ 0: { kind: 'typechange', original: { label: 'x' }, updated: { label: 'x', retypedFrom: 'address' } },
259
+ });
260
+ });
261
+
262
+ test('retype with layout change', t => {
263
+ // A retype annotation that names the original type does not allow the member's size to change
264
+ const v1 = getLayout(t.context.validation, 'NamespacedRetypeLayoutChangeV1');
265
+ const v2 = getLayout(t.context.validation, 'NamespacedRetypeLayoutChangeV2');
266
+ t.like(getStorageUpgradeErrors(v1, v2), {
267
+ length: 1,
268
+ 0: {
269
+ kind: 'layoutchange',
270
+ original: { label: 'y' },
271
+ updated: { label: 'y', retypedFrom: 'uint256' },
272
+ change: { bytes: { from: '32', to: '16' } },
273
+ },
274
+ });
275
+ });
276
+
277
+ test('struct from another source file', t => {
278
+ // The annotations on the members of the struct are read from the source file that defines it
279
+ const { output, validation } = t.context.crossFile;
280
+ t.not(output.sources['contracts/test/NamespacedCrossFileStruct.sol'].id, 0);
281
+ const v1 = validation[`${crossFileSource}:NamespacedCrossFileV1`].layout;
282
+ const v2 = validation[`${crossFileSource}:NamespacedCrossFileV2`].layout;
283
+ t.like(v2.namespaces, { 'erc7201:example.main': { length: 1, 0: { label: 'data', slot: '0' } } });
284
+ const members = Object.values(v2.types).find(type => type.label === 'struct CrossFileV2.Data')?.members;
285
+ if (members === undefined || !isStructMembers(members)) {
286
+ throw new Error('Struct members not found');
287
+ }
288
+ t.like(members, {
289
+ length: 2,
290
+ 0: { label: 'a', retypedFrom: 'bytes32', slot: '0' },
291
+ 1: { label: 'c', renamedFrom: 'b', slot: '1' },
292
+ });
293
+ t.deepEqual(getStorageUpgradeErrors(v1, v2), []);
294
+ });
295
+
296
+ /**
297
+ * Validates the fixture compiled again with the given line endings.
298
+ */
299
+ async function validateWithLineEndings(t: ExecutionContext<Context>, lineEnding: string) {
300
+ const content = t.context.input.sources[source].content;
301
+ if (content === undefined) {
302
+ throw new Error('Unexpected fixture content');
303
+ }
304
+ return compileAndValidate(t.context.input, content.replace(/\r\n|\r|\n/g, lineEnding), t.context.solcVersion);
305
+ }
306
+
307
+ test('CRLF line endings', async t => {
308
+ assertAnnotationsApplied(t, await validateWithLineEndings(t, '\r\n'));
309
+ });
310
+
311
+ test('CR line endings', async t => {
312
+ assertAnnotationsApplied(t, await validateWithLineEndings(t, '\r'));
313
+ });
314
+
315
+ test('solc 0.8.20', async t => {
316
+ const content = t.context.input.sources[source].content;
317
+ if (content === undefined) {
318
+ throw new Error('Unexpected fixture content');
319
+ }
320
+ const validation = await compileAndValidate(t.context.input, content, '0.8.20');
321
+ assertAnnotationsApplied(t, validation);
322
+ });
323
+
324
+ test('stale rename annotation report', t => {
325
+ const v1 = getLayout(t.context.validation, 'NamespacedBadV1');
326
+ const v2 = getLayout(t.context.validation, 'NamespacedStaleRenameV2');
327
+ const report = getStorageUpgradeReport(v1, v2, withValidationDefaults({}));
328
+ t.false(report.ok);
329
+ const items = v2.namespaces?.['erc7201:example.main'];
330
+ if (items === undefined) {
331
+ throw new Error('Namespace not found');
332
+ }
333
+ t.is(
334
+ report.explain(),
335
+ `${items[1].src}: \`y\` has a '@custom:oz-renamed-from q' annotation, but it was not renamed\n` +
336
+ ' > If the annotation is left over from an earlier upgrade, remove it.',
337
+ );
338
+ });