@openzeppelin/upgrades-core 1.29.0 → 1.30.1

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 (161) hide show
  1. package/dist/call-optional-signature.d.ts +3 -0
  2. package/dist/call-optional-signature.d.ts.map +1 -0
  3. package/dist/call-optional-signature.js +24 -0
  4. package/dist/call-optional-signature.js.map +1 -0
  5. package/dist/cli/validate/build-info-file.d.ts +4 -0
  6. package/dist/cli/validate/build-info-file.d.ts.map +1 -1
  7. package/dist/cli/validate/build-info-file.js +5 -2
  8. package/dist/cli/validate/build-info-file.js.map +1 -1
  9. package/dist/cli/validate/build-info-file.test.js +4 -1
  10. package/dist/cli/validate/build-info-file.test.js.map +1 -1
  11. package/dist/cli/validate/upgradeability-assessment.js +2 -6
  12. package/dist/cli/validate/upgradeability-assessment.js.map +1 -1
  13. package/dist/cli/validate/validations.d.ts.map +1 -1
  14. package/dist/cli/validate/validations.js +5 -4
  15. package/dist/cli/validate/validations.js.map +1 -1
  16. package/dist/impl-address.d.ts.map +1 -1
  17. package/dist/impl-address.js +9 -16
  18. package/dist/impl-address.js.map +1 -1
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +7 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/levenshtein.d.ts +6 -0
  24. package/dist/levenshtein.d.ts.map +1 -1
  25. package/dist/levenshtein.js.map +1 -1
  26. package/dist/solc-api.d.ts +14 -0
  27. package/dist/solc-api.d.ts.map +1 -1
  28. package/dist/standalone.d.ts +4 -1
  29. package/dist/standalone.d.ts.map +1 -1
  30. package/dist/standalone.js +6 -3
  31. package/dist/standalone.js.map +1 -1
  32. package/dist/standalone.test.js +17 -0
  33. package/dist/standalone.test.js.map +1 -1
  34. package/dist/storage/compare.d.ts +2 -1
  35. package/dist/storage/compare.d.ts.map +1 -1
  36. package/dist/storage/compare.js +25 -2
  37. package/dist/storage/compare.js.map +1 -1
  38. package/dist/storage/extract.d.ts +15 -3
  39. package/dist/storage/extract.d.ts.map +1 -1
  40. package/dist/storage/extract.js +48 -21
  41. package/dist/storage/extract.js.map +1 -1
  42. package/dist/storage/index.d.ts.map +1 -1
  43. package/dist/storage/index.js +15 -1
  44. package/dist/storage/index.js.map +1 -1
  45. package/dist/storage/layout.d.ts +1 -0
  46. package/dist/storage/layout.d.ts.map +1 -1
  47. package/dist/storage/layout.js.map +1 -1
  48. package/dist/storage/namespace.d.ts +35 -0
  49. package/dist/storage/namespace.d.ts.map +1 -0
  50. package/dist/storage/namespace.js +219 -0
  51. package/dist/storage/namespace.js.map +1 -0
  52. package/dist/storage/report.js +6 -0
  53. package/dist/storage/report.js.map +1 -1
  54. package/dist/storage-namespaced-conflicts-layout.test.d.ts +2 -0
  55. package/dist/storage-namespaced-conflicts-layout.test.d.ts.map +1 -0
  56. package/dist/storage-namespaced-conflicts-layout.test.js +69 -0
  57. package/dist/storage-namespaced-conflicts-layout.test.js.map +1 -0
  58. package/dist/storage-namespaced-conflicts.test.d.ts +2 -0
  59. package/dist/storage-namespaced-conflicts.test.d.ts.map +1 -0
  60. package/dist/storage-namespaced-conflicts.test.js +45 -0
  61. package/dist/storage-namespaced-conflicts.test.js.map +1 -0
  62. package/dist/storage-namespaced-layout.test.d.ts +2 -0
  63. package/dist/storage-namespaced-layout.test.d.ts.map +1 -0
  64. package/dist/storage-namespaced-layout.test.js +59 -0
  65. package/dist/storage-namespaced-layout.test.js.map +1 -0
  66. package/dist/storage-namespaced-outside-contract.test.d.ts +2 -0
  67. package/dist/storage-namespaced-outside-contract.test.d.ts.map +1 -0
  68. package/dist/storage-namespaced-outside-contract.test.js +22 -0
  69. package/dist/storage-namespaced-outside-contract.test.js.map +1 -0
  70. package/dist/storage-namespaced-solc-versions.test.d.ts +2 -0
  71. package/dist/storage-namespaced-solc-versions.test.d.ts.map +1 -0
  72. package/dist/storage-namespaced-solc-versions.test.js +42 -0
  73. package/dist/storage-namespaced-solc-versions.test.js.map +1 -0
  74. package/dist/storage-namespaced-udvt.test.d.ts +2 -0
  75. package/dist/storage-namespaced-udvt.test.d.ts.map +1 -0
  76. package/dist/storage-namespaced-udvt.test.js +130 -0
  77. package/dist/storage-namespaced-udvt.test.js.map +1 -0
  78. package/dist/storage-namespaced.test.d.ts +2 -0
  79. package/dist/storage-namespaced.test.d.ts.map +1 -0
  80. package/dist/storage-namespaced.test.js +132 -0
  81. package/dist/storage-namespaced.test.js.map +1 -0
  82. package/dist/upgrade-interface-version.d.ts +3 -0
  83. package/dist/upgrade-interface-version.d.ts.map +1 -0
  84. package/dist/upgrade-interface-version.js +25 -0
  85. package/dist/upgrade-interface-version.js.map +1 -0
  86. package/dist/upgrade-interface-version.test.d.ts +2 -0
  87. package/dist/upgrade-interface-version.test.d.ts.map +1 -0
  88. package/dist/upgrade-interface-version.test.js +32 -0
  89. package/dist/upgrade-interface-version.test.js.map +1 -0
  90. package/dist/utils/annotations.d.ts +4 -0
  91. package/dist/utils/annotations.d.ts.map +1 -1
  92. package/dist/utils/annotations.js +9 -1
  93. package/dist/utils/annotations.js.map +1 -1
  94. package/dist/utils/make-namespaced.d.ts +20 -0
  95. package/dist/utils/make-namespaced.d.ts.map +1 -0
  96. package/dist/utils/make-namespaced.js +131 -0
  97. package/dist/utils/make-namespaced.js.map +1 -0
  98. package/dist/utils/make-namespaced.test.d.ts +2 -0
  99. package/dist/utils/make-namespaced.test.d.ts.map +1 -0
  100. package/dist/utils/make-namespaced.test.js +59 -0
  101. package/dist/utils/make-namespaced.test.js.map +1 -0
  102. package/dist/utils/stabilize-layout.d.ts +11 -0
  103. package/dist/utils/stabilize-layout.d.ts.map +1 -1
  104. package/dist/utils/stabilize-layout.js +5 -0
  105. package/dist/utils/stabilize-layout.js.map +1 -1
  106. package/dist/validate/query.d.ts.map +1 -1
  107. package/dist/validate/query.js +4 -2
  108. package/dist/validate/query.js.map +1 -1
  109. package/dist/validate/run.d.ts +18 -2
  110. package/dist/validate/run.d.ts.map +1 -1
  111. package/dist/validate/run.js +80 -2
  112. package/dist/validate/run.js.map +1 -1
  113. package/package.json +3 -3
  114. package/src/call-optional-signature.ts +20 -0
  115. package/src/cli/validate/build-info-file.test.ts +4 -1
  116. package/src/cli/validate/build-info-file.ts +12 -2
  117. package/src/cli/validate/upgradeability-assessment.ts +1 -6
  118. package/src/cli/validate/validations.ts +6 -5
  119. package/src/impl-address.ts +9 -20
  120. package/src/index.ts +4 -0
  121. package/src/levenshtein.ts +7 -0
  122. package/src/solc-api.ts +14 -0
  123. package/src/standalone.test.ts +26 -0
  124. package/src/standalone.ts +6 -2
  125. package/src/storage/compare.ts +34 -2
  126. package/src/storage/extract.ts +68 -20
  127. package/src/storage/index.ts +22 -1
  128. package/src/storage/layout.ts +1 -0
  129. package/src/storage/namespace.ts +267 -0
  130. package/src/storage/report.ts +8 -0
  131. package/src/storage-0.8.test.ts.md +4 -0
  132. package/src/storage-0.8.test.ts.snap +0 -0
  133. package/src/storage-memory-0.5.test.ts.md +2 -0
  134. package/src/storage-memory-0.5.test.ts.snap +0 -0
  135. package/src/storage-namespaced-conflicts-layout.test.ts +96 -0
  136. package/src/storage-namespaced-conflicts-layout.test.ts.md +53 -0
  137. package/src/storage-namespaced-conflicts-layout.test.ts.snap +0 -0
  138. package/src/storage-namespaced-conflicts.test.ts +54 -0
  139. package/src/storage-namespaced-conflicts.test.ts.md +53 -0
  140. package/src/storage-namespaced-conflicts.test.ts.snap +0 -0
  141. package/src/storage-namespaced-layout.test.ts +77 -0
  142. package/src/storage-namespaced-layout.test.ts.md +67 -0
  143. package/src/storage-namespaced-layout.test.ts.snap +0 -0
  144. package/src/storage-namespaced-outside-contract.test.ts +24 -0
  145. package/src/storage-namespaced-solc-versions.test.ts +59 -0
  146. package/src/storage-namespaced-udvt.test.ts +160 -0
  147. package/src/storage-namespaced.test.ts +146 -0
  148. package/src/storage-namespaced.test.ts.md +89 -0
  149. package/src/storage-namespaced.test.ts.snap +0 -0
  150. package/src/storage.test.ts.md +2 -0
  151. package/src/storage.test.ts.snap +0 -0
  152. package/src/upgrade-interface-version.test.ts +37 -0
  153. package/src/upgrade-interface-version.ts +27 -0
  154. package/src/utils/annotations.ts +8 -0
  155. package/src/utils/make-namespaced.test.ts +74 -0
  156. package/src/utils/make-namespaced.test.ts.md +154 -0
  157. package/src/utils/make-namespaced.test.ts.snap +0 -0
  158. package/src/utils/make-namespaced.ts +154 -0
  159. package/src/utils/stabilize-layout.ts +5 -0
  160. package/src/validate/query.ts +4 -2
  161. package/src/validate/run.ts +108 -2
@@ -1,18 +1,20 @@
1
- import assert from 'assert';
1
+ import assert from 'assert/strict';
2
2
  import {
3
3
  ContractDefinition,
4
4
  StructDefinition,
5
5
  EnumDefinition,
6
6
  TypeDescriptions,
7
7
  VariableDeclaration,
8
+ TypeName,
8
9
  } from 'solidity-ast';
9
10
  import { isNodeType, findAll, ASTDereferencer } from 'solidity-ast/utils';
10
- import { StorageLayout, TypeItem } from './layout';
11
+ import { StorageLayout, StructMember, TypeItem, isStructMembers, EnumMember } from './layout';
11
12
  import { normalizeTypeIdentifier } from '../utils/type-id';
12
13
  import { SrcDecoder } from '../src-decoder';
13
14
  import { mapValues } from '../utils/map-values';
14
15
  import { pick } from '../utils/pick';
15
16
  import { execall } from '../utils/execall';
17
+ import { loadNamespaces } from './namespace';
16
18
 
17
19
  const currentLayoutVersion = '1.2';
18
20
 
@@ -20,31 +22,53 @@ export function isCurrentLayoutVersion(layout: StorageLayout): boolean {
20
22
  return layout?.layoutVersion === currentLayoutVersion;
21
23
  }
22
24
 
25
+ export interface CompilationContext {
26
+ deref: ASTDereferencer;
27
+ contractDef: ContractDefinition;
28
+ storageLayout?: StorageLayout;
29
+ }
30
+
23
31
  export function extractStorageLayout(
24
32
  contractDef: ContractDefinition,
25
33
  decodeSrc: SrcDecoder,
26
34
  deref: ASTDereferencer,
27
- storageLayout?: StorageLayout | undefined,
35
+ storageLayout?: StorageLayout,
36
+ namespacedContext?: CompilationContext,
28
37
  ): StorageLayout {
29
38
  const layout: StorageLayout = { storage: [], types: {}, layoutVersion: currentLayoutVersion, flat: false };
30
- if (storageLayout !== undefined) {
31
- layout.types = mapValues(storageLayout.types, m => {
32
- return {
33
- label: m.label,
34
- members: m.members?.map(m =>
35
- typeof m === 'string' ? m : pick(m, ['label', 'type', 'offset', 'slot']),
36
- ) as TypeItem['members'],
37
- numberOfBytes: m.numberOfBytes,
38
- };
39
- });
40
39
 
40
+ // The namespaced context will contain the types of namespaces that may not be included
41
+ // in the original storage layout.
42
+ // Some types will be present in both and they must be exactly equivalent.
43
+ // If they are not, it fails an assertion because this may be a clash between different types.
44
+ const combinedTypes = { ...namespacedContext?.storageLayout?.types, ...storageLayout?.types };
45
+ if (namespacedContext?.storageLayout?.types) {
46
+ for (const t in storageLayout?.types) {
47
+ if (t in namespacedContext.storageLayout.types) {
48
+ assert.deepEqual(namespacedContext.storageLayout.types[t], storageLayout?.types[t]);
49
+ }
50
+ }
51
+ }
52
+
53
+ layout.types = mapValues(combinedTypes, m => {
54
+ return {
55
+ label: m.label,
56
+ members:
57
+ m.members && isStructMembers(m.members)
58
+ ? m.members.map(m => pick(m, ['label', 'type', 'offset', 'slot']))
59
+ : m.members,
60
+ numberOfBytes: m.numberOfBytes,
61
+ };
62
+ });
63
+
64
+ if (storageLayout !== undefined) {
41
65
  for (const storage of storageLayout.storage) {
42
66
  const origin = getOriginContract(contractDef, storage.astId, deref);
43
67
  assert(origin, `Did not find variable declaration node for '${storage.label}'`);
44
68
  const { varDecl, contract } = origin;
45
69
  const { renamedFrom, retypedFrom } = getRetypedRenamed(varDecl);
46
70
  // Solc layout doesn't bring members for enums so we get them using the ast method
47
- loadLayoutType(varDecl, layout, deref);
71
+ loadLayoutType(varDecl.typeName, layout, deref);
48
72
  const { label, offset, slot, type } = storage;
49
73
  const src = decodeSrc(varDecl);
50
74
  layout.storage.push({ label, offset, slot, type, contract, src, retypedFrom, renamedFrom });
@@ -65,11 +89,15 @@ export function extractStorageLayout(
65
89
  renamedFrom,
66
90
  });
67
91
 
68
- loadLayoutType(varDecl, layout, deref);
92
+ loadLayoutType(varDecl.typeName, layout, deref);
69
93
  }
70
94
  }
71
95
  }
72
96
  }
97
+
98
+ const origContext = { deref, contractDef, storageLayout };
99
+ loadNamespaces(decodeSrc, layout, origContext, namespacedContext);
100
+
73
101
  return layout;
74
102
  }
75
103
 
@@ -92,14 +120,34 @@ function typeDescriptions(x: { typeDescriptions: TypeDescriptions }): RequiredTy
92
120
  return x.typeDescriptions as RequiredTypeDescriptions;
93
121
  }
94
122
 
95
- function getTypeMembers(typeDef: StructDefinition | EnumDefinition): TypeItem['members'] {
123
+ type GotTypeMembers<
124
+ D extends EnumDefinition | StructDefinition,
125
+ F extends 'src' | 'typeName',
126
+ > = D extends EnumDefinition ? EnumMember[] : (StructMember & Pick<VariableDeclaration, F>)[];
127
+
128
+ export function getTypeMembers<D extends EnumDefinition | StructDefinition>(typeDef: D): GotTypeMembers<D, never>;
129
+ export function getTypeMembers<D extends EnumDefinition | StructDefinition, F extends 'src' | 'typeName'>(
130
+ typeDef: D,
131
+ includeFields: { [f in F]: true },
132
+ ): GotTypeMembers<D, F>;
133
+ export function getTypeMembers(
134
+ typeDef: StructDefinition | EnumDefinition,
135
+ includeFields: { src?: boolean; typeName?: boolean } = {},
136
+ ): TypeItem['members'] {
96
137
  if (typeDef.nodeType === 'StructDefinition') {
97
138
  return typeDef.members.map(m => {
98
139
  assert(typeof m.typeDescriptions.typeIdentifier === 'string');
99
- return {
140
+ const member: StructMember & Partial<Pick<VariableDeclaration, 'src' | 'typeName'>> = {
100
141
  label: m.name,
101
142
  type: normalizeTypeIdentifier(m.typeDescriptions.typeIdentifier),
102
143
  };
144
+ if (includeFields.src && m.src) {
145
+ member.src = m.src;
146
+ }
147
+ if (includeFields.typeName && m.typeName) {
148
+ member.typeName = m.typeName;
149
+ }
150
+ return member;
103
151
  });
104
152
  } else {
105
153
  return typeDef.members.map(m => m.name);
@@ -116,16 +164,16 @@ function getOriginContract(contract: ContractDefinition, astId: number | undefin
116
164
  }
117
165
  }
118
166
 
119
- function loadLayoutType(varDecl: VariableDeclaration, layout: StorageLayout, deref: ASTDereferencer) {
167
+ export function loadLayoutType(typeName: TypeName | null | undefined, layout: StorageLayout, deref: ASTDereferencer) {
120
168
  // Note: A UserDefinedTypeName can also refer to a ContractDefinition but we won't care about those.
121
169
  const derefUserDefinedType = deref(['StructDefinition', 'EnumDefinition', 'UserDefinedValueTypeDefinition']);
122
170
 
123
- assert(varDecl.typeName != null);
171
+ assert(typeName != null);
124
172
 
125
173
  // We will recursively look for all types involved in this variable declaration in order to store their type
126
174
  // information. We iterate over a Map that is indexed by typeIdentifier to ensure we visit each type only once.
127
175
  // Note that there can be recursive types.
128
- const typeNames = new Map([...findTypeNames(varDecl.typeName)].map(n => [typeDescriptions(n).typeIdentifier, n]));
176
+ const typeNames = new Map([...findTypeNames(typeName)].map(n => [typeDescriptions(n).typeIdentifier, n]));
129
177
 
130
178
  for (const typeName of typeNames.values()) {
131
179
  const { typeIdentifier, typeString: label } = typeDescriptions(typeName);
@@ -43,8 +43,16 @@ export function getStorageUpgradeReport(
43
43
  ): LayoutCompatibilityReport {
44
44
  const originalDetailed = getDetailedLayout(original);
45
45
  const updatedDetailed = getDetailedLayout(updated);
46
+ const originalDetailedNamespaces = getDetailedNamespacedLayout(original);
47
+ const updatedDetailedNamespaces = getDetailedNamespacedLayout(updated);
48
+
46
49
  const comparator = new StorageLayoutComparator(opts.unsafeAllowCustomTypes, opts.unsafeAllowRenames);
47
- const report = comparator.compareLayouts(originalDetailed, updatedDetailed);
50
+ const report = comparator.compareLayouts(
51
+ originalDetailed,
52
+ updatedDetailed,
53
+ originalDetailedNamespaces,
54
+ updatedDetailedNamespaces,
55
+ );
48
56
 
49
57
  if (comparator.hasAllowedUncheckedCustomTypes) {
50
58
  logWarning(`Potentially unsafe deployment`, [
@@ -58,6 +66,19 @@ export function getStorageUpgradeReport(
58
66
  return report;
59
67
  }
60
68
 
69
+ function getDetailedNamespacedLayout(layout: StorageLayout): Record<string, StorageItem[]> {
70
+ const detailedNamespaces: Record<string, StorageItem[]> = {};
71
+ if (layout.namespaces !== undefined) {
72
+ for (const [storageLocation, namespacedLayout] of Object.entries(layout.namespaces)) {
73
+ detailedNamespaces[storageLocation] = getDetailedLayout({
74
+ storage: namespacedLayout,
75
+ types: layout.types,
76
+ });
77
+ }
78
+ }
79
+ return detailedNamespaces;
80
+ }
81
+
61
82
  export class StorageUpgradeErrors extends UpgradesError {
62
83
  constructor(readonly report: LayoutCompatibilityReport) {
63
84
  super(`New storage layout is incompatible`, () => report.explain());
@@ -11,6 +11,7 @@ export interface StorageLayout {
11
11
  storage: StorageItem[];
12
12
  types: Record<string, TypeItem>;
13
13
  flat?: boolean;
14
+ namespaces?: Record<string, StorageItem[]>;
14
15
  }
15
16
 
16
17
  export type StorageField<Type = string> = StorageItem<Type> | StructMember<Type>;
@@ -0,0 +1,267 @@
1
+ import assert from 'assert';
2
+ import { ContractDefinition, StructDefinition } from 'solidity-ast';
3
+ import { isNodeType } from 'solidity-ast/utils';
4
+ import { StorageItem, StorageLayout, TypeItem, isStructMembers } from './layout';
5
+ import { SrcDecoder } from '../src-decoder';
6
+ import { getAnnotationArgs, getDocumentation, hasAnnotationTag } from '../utils/annotations';
7
+ import { Node } from 'solidity-ast/node';
8
+ import { CompilationContext, getTypeMembers, loadLayoutType } from './extract';
9
+ import { UpgradesError } from '../error';
10
+ import * as versions from 'compare-versions';
11
+
12
+ /**
13
+ * Loads a contract's namespaces and namespaced type information into the storage layout.
14
+ *
15
+ * The provided compilation contexts must include both the original compilation and optionally
16
+ * a namespaced compilation where contracts have been modified to include namespaced type information.
17
+ *
18
+ * If namespaced compilation is included, storage slots and offsets will be included in the loaded namespaces and types.
19
+ *
20
+ * This function looks up namespaces and their members from the namespaced compilation context's AST if available
21
+ * (meaning node ids would be from the namespaced compilation), and looks up slots and offsets from the compiled type information.
22
+ * However, it saves the original source locations from the original context so that line numbers are
23
+ * consistent with the original source code.
24
+ *
25
+ * @param decodeSrc Source decoder for the original source code.
26
+ * @param layout The storage layout object to load namespaces into.
27
+ * @param origContext The original compilation context, which is used to lookup original source locations.
28
+ * @param namespacedContext The namespaced compilation context, which represents a namespaced compilation.
29
+ */
30
+ export function loadNamespaces(
31
+ decodeSrc: SrcDecoder,
32
+ layout: StorageLayout,
33
+ origContext: CompilationContext,
34
+ namespacedContext?: CompilationContext,
35
+ ) {
36
+ const namespacesWithSrc: Record<string, NamespaceWithSrc> = {};
37
+
38
+ const origLinearized = origContext.contractDef.linearizedBaseContracts.map(id =>
39
+ getReferencedContract(origContext, id),
40
+ );
41
+ const namespacedLinearized = namespacedContext?.contractDef.linearizedBaseContracts.map(id =>
42
+ getReferencedContract(namespacedContext, id),
43
+ );
44
+ assert(namespacedLinearized === undefined || origLinearized.length === namespacedLinearized.length);
45
+ const context = namespacedContext ?? origContext;
46
+ const linearized = namespacedLinearized ?? origLinearized;
47
+ for (const [i, contractDef] of linearized.entries()) {
48
+ const origContractDef = origLinearized[i];
49
+ const contractContext = { ...context, contractDef };
50
+ addContractNamespacesWithSrc(
51
+ namespacesWithSrc,
52
+ decodeSrc,
53
+ layout,
54
+ contractContext,
55
+ origContractDef,
56
+ origContext.contractDef.canonicalName ?? origContext.contractDef.name,
57
+ );
58
+ }
59
+
60
+ // Add to layout without the namespaced structs' src locations, since those are no longer needed
61
+ // as they were only used to give duplicate namespace errors above.
62
+ layout.namespaces = Object.fromEntries(
63
+ Object.entries(namespacesWithSrc).map(([id, namespaceWithSrc]) => [id, namespaceWithSrc.namespace]),
64
+ );
65
+ }
66
+
67
+ class DuplicateNamespaceError extends UpgradesError {
68
+ constructor(id: string, contractName: string, src1: string, src2: string) {
69
+ super(
70
+ `Namespace ${id} is defined multiple times for contract ${contractName}`,
71
+ () => `\
72
+ The namespace ${id} was found in structs at the following locations:
73
+ - ${src1}
74
+ - ${src2}
75
+
76
+ Use a unique namespace id for each struct annotated with '@custom:storage-location erc7201:<NAMESPACE_ID>' in your contract and its inherited contracts.`,
77
+ );
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Namespaced storage items, along with the original source location of the namespace struct.
83
+ */
84
+ interface NamespaceWithSrc {
85
+ namespace: StorageItem<string>[];
86
+ src: string;
87
+ }
88
+
89
+ /**
90
+ * Gets the contract definition for the given referenced id.
91
+ */
92
+ function getReferencedContract(context: CompilationContext, referencedId: number) {
93
+ // Optimization to avoid dereferencing if the referenced id is the same as the current contract
94
+ return context.contractDef.id === referencedId
95
+ ? context.contractDef
96
+ : context.deref(['ContractDefinition'], referencedId);
97
+ }
98
+
99
+ /**
100
+ * Add namespaces and source locations for the given compilation context's contract.
101
+ * Does not include inherited contracts.
102
+ *
103
+ * @param namespacesWithSrc The record of namespaces with source locations to add to.
104
+ * @param decodeSrc Source decoder for the original source code.
105
+ * @param layout The storage layout object to load types into.
106
+ * @param contractContext The compilation context for this specific contract to load namespaces for.
107
+ * @param origContractDef The AST node for this specific contract but from the original compilation context.
108
+ * @param leastDerivedContractName The name of the least derived contract in the inheritance list.
109
+ * @throws DuplicateNamespaceError if a duplicate namespace is found when adding to the `namespaces` record.
110
+ */
111
+ function addContractNamespacesWithSrc(
112
+ namespacesWithSrc: Record<string, NamespaceWithSrc>,
113
+ decodeSrc: SrcDecoder,
114
+ layout: StorageLayout,
115
+ contractContext: CompilationContext,
116
+ origContractDef: ContractDefinition,
117
+ leastDerivedContractName: string,
118
+ ) {
119
+ for (const node of contractContext.contractDef.nodes) {
120
+ if (isNodeType('StructDefinition', node)) {
121
+ const storageLocation = getStorageLocationAnnotation(node);
122
+ if (storageLocation !== undefined) {
123
+ const origSrc = decodeSrc(getOriginalStruct(node.canonicalName, origContractDef));
124
+
125
+ if (namespacesWithSrc[storageLocation] !== undefined) {
126
+ throw new DuplicateNamespaceError(
127
+ storageLocation,
128
+ leastDerivedContractName,
129
+ namespacesWithSrc[storageLocation].src,
130
+ origSrc,
131
+ );
132
+ } else {
133
+ namespacesWithSrc[storageLocation] = {
134
+ namespace: getNamespacedStorageItems(node, decodeSrc, layout, contractContext, origContractDef),
135
+ src: origSrc,
136
+ };
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Gets the storage location string from the `@custom:storage-location` annotation.
145
+ *
146
+ * For example, when using ERC-7201 (https://eips.ethereum.org/EIPS/eip-7201), the result will be `erc7201:<NAMESPACE_ID>`
147
+ *
148
+ * @param node The node that may have a `@custom:storage-location` annotation.
149
+ * @returns The storage location string, or undefined if the node does not have a `@custom:storage-location` annotation.
150
+ * @throws Error if the node has the annotation `@custom:storage-location` but it does not have exactly one argument.
151
+ */
152
+ export function getStorageLocationAnnotation(node: Node) {
153
+ const doc = getDocumentation(node);
154
+ if (hasAnnotationTag(doc, 'storage-location')) {
155
+ const storageLocationArgs = getAnnotationArgs(doc, 'storage-location');
156
+ if (storageLocationArgs.length !== 1) {
157
+ throw new Error('@custom:storage-location annotation must have exactly one argument');
158
+ }
159
+ return storageLocationArgs[0];
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Gets the storage items for the given struct node.
165
+ * Includes loading recursive type information, and adds slot and offset if they are available in the given compilation context's layout.
166
+ */
167
+ function getNamespacedStorageItems(
168
+ node: StructDefinition,
169
+ decodeSrc: SrcDecoder,
170
+ layout: StorageLayout,
171
+ context: CompilationContext,
172
+ origContractDef: ContractDefinition,
173
+ ): StorageItem[] {
174
+ const storageItems: StorageItem[] = [];
175
+
176
+ for (const astMember of getTypeMembers(node, { typeName: true })) {
177
+ const item: StorageItem = {
178
+ contract: context.contractDef.name,
179
+ label: astMember.label,
180
+ type: astMember.type,
181
+ src: decodeSrc({ src: getOriginalMemberSrc(node.canonicalName, astMember.label, origContractDef) }),
182
+ };
183
+
184
+ const layoutMember = findLayoutStructMember(
185
+ context.storageLayout?.types ?? {},
186
+ node.canonicalName,
187
+ astMember.label,
188
+ );
189
+
190
+ if (layoutMember?.offset !== undefined && layoutMember?.slot !== undefined) {
191
+ item.offset = layoutMember.offset;
192
+ item.slot = layoutMember.slot;
193
+ }
194
+
195
+ storageItems.push(item);
196
+
197
+ // If context is namespaced, we have storage layout, and this will fill in enum members just like in extractStorageLayout.
198
+ // If context is original, this will add the types from the namespace structs to the layout.
199
+ loadLayoutType(astMember.typeName, layout, context.deref);
200
+ }
201
+ return storageItems;
202
+ }
203
+
204
+ /**
205
+ * Gets the struct definition matching the given canonical name from the original contract definition.
206
+ */
207
+ function getOriginalStruct(structCanonicalName: string, origContractDef: ContractDefinition) {
208
+ for (const node of origContractDef.nodes) {
209
+ if (isNodeType('StructDefinition', node)) {
210
+ if (node.canonicalName === structCanonicalName) {
211
+ return node;
212
+ }
213
+ }
214
+ }
215
+ throw new Error(
216
+ `Could not find original source location for namespace struct with name ${structCanonicalName} from contract ${origContractDef.name}`,
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Gets the original source location for the given struct canonical name and struct member label.
222
+ */
223
+ function getOriginalMemberSrc(structCanonicalName: string, memberLabel: string, origContractDef: ContractDefinition) {
224
+ const node = getOriginalStruct(structCanonicalName, origContractDef);
225
+ if (node !== undefined) {
226
+ for (const member of getTypeMembers(node, { src: true })) {
227
+ if (member.label === memberLabel) {
228
+ return member.src;
229
+ }
230
+ }
231
+ }
232
+
233
+ throw new Error(
234
+ `Could not find original source location for namespace struct with name ${structCanonicalName} and member ${memberLabel}`,
235
+ );
236
+ }
237
+
238
+ /**
239
+ * From the given layout types, gets the struct member matching the given struct canonical name and struct member label.
240
+ */
241
+ function findLayoutStructMember(
242
+ types: Record<string, TypeItem<string>>,
243
+ structCanonicalName: string,
244
+ memberLabel: string,
245
+ ) {
246
+ const structType = findTypeWithLabel(types, `struct ${structCanonicalName}`);
247
+ const structMembers = structType?.members;
248
+ if (structMembers !== undefined) {
249
+ assert(isStructMembers(structMembers));
250
+ for (const structMember of structMembers) {
251
+ if (structMember.label === memberLabel) {
252
+ return structMember;
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ /**
259
+ * From the given layout types, gets the type matching the given type label.
260
+ */
261
+ function findTypeWithLabel(types: Record<string, TypeItem>, label: string) {
262
+ return Object.values(types).find(type => type.label === label);
263
+ }
264
+
265
+ export function isNamespaceSupported(solcVersion: string) {
266
+ return versions.compare(solcVersion, '0.8.20', '>=');
267
+ }
@@ -154,6 +154,11 @@ function explainStorageOperation(op: StorageOperation<StorageField>, ctx: Storag
154
154
  hints.push('Keep the variable even if unused');
155
155
  break;
156
156
  }
157
+
158
+ case 'delete-namespace': {
159
+ hints.push(`Keep the struct with annotation '@custom:storage-location ${op.namespace}' even if unused`);
160
+ break;
161
+ }
157
162
  }
158
163
 
159
164
  return printWithHints({ title, hints });
@@ -299,6 +304,9 @@ function explainBasicOperation<T>(op: BasicOperation<T>, getName: (t: T) => stri
299
304
 
300
305
  case 'append':
301
306
  return `Added \`${getName(op.updated)}\``;
307
+
308
+ case 'delete-namespace':
309
+ return `Deleted namespace \`${op.namespace}\``;
302
310
  }
303
311
  }
304
312
 
@@ -9,6 +9,7 @@ Generated by [AVA](https://avajs.dev).
9
9
  > Snapshot 1
10
10
 
11
11
  {
12
+ namespaces: [],
12
13
  storage: [
13
14
  {
14
15
  contract: 'Storage088',
@@ -46,6 +47,7 @@ Generated by [AVA](https://avajs.dev).
46
47
  > Snapshot 1
47
48
 
48
49
  {
50
+ namespaces: [],
49
51
  storage: [
50
52
  {
51
53
  contract: 'Storage089',
@@ -83,6 +85,7 @@ Generated by [AVA](https://avajs.dev).
83
85
  > Snapshot 1
84
86
 
85
87
  {
88
+ namespaces: [],
86
89
  storage: [
87
90
  {
88
91
  contract: 'Storage089',
@@ -117,6 +120,7 @@ Generated by [AVA](https://avajs.dev).
117
120
  > Snapshot 1
118
121
 
119
122
  {
123
+ namespaces: [],
120
124
  storage: [
121
125
  {
122
126
  contract: 'StorageRenamedRetyped',
Binary file
@@ -9,6 +9,7 @@ Generated by [AVA](https://avajs.dev).
9
9
  > Snapshot 1
10
10
 
11
11
  {
12
+ namespaces: [],
12
13
  storage: [
13
14
  {
14
15
  contract: 'Memory05',
@@ -94,6 +95,7 @@ Generated by [AVA](https://avajs.dev).
94
95
  > Snapshot 1
95
96
 
96
97
  {
98
+ namespaces: [],
97
99
  storage: [
98
100
  {
99
101
  contract: 'Memory08',
Binary file
@@ -0,0 +1,96 @@
1
+ import _test, { TestFn } from 'ava';
2
+ import { ContractDefinition } from 'solidity-ast';
3
+ import { findAll, astDereferencer } from 'solidity-ast/utils';
4
+ import { artifacts } from 'hardhat';
5
+
6
+ import { SolcOutput } from './solc-api';
7
+ import { StorageLayout } from './storage/layout';
8
+ import { extractStorageLayout } from './storage/extract';
9
+ import { solcInputOutputDecoder } from './src-decoder';
10
+
11
+ interface Context {
12
+ extractStorageLayout: (contract: string) => ReturnType<typeof extractStorageLayout>;
13
+ }
14
+
15
+ const test = _test as TestFn<Context>;
16
+
17
+ test.before(async t => {
18
+ const origBuildInfo = await artifacts.getBuildInfo('contracts/test/NamespacedConflicts.sol:DuplicateNamespace');
19
+ const namespacedBuildInfo = await artifacts.getBuildInfo(
20
+ 'contracts/test/NamespacedConflictsLayout.sol:DuplicateNamespace',
21
+ );
22
+
23
+ if (origBuildInfo === undefined || namespacedBuildInfo === undefined) {
24
+ throw new Error('Build info not found');
25
+ }
26
+
27
+ const origSolcOutput: SolcOutput = origBuildInfo.output;
28
+ const origContracts: Record<string, ContractDefinition> = {};
29
+ const origStorageLayouts: Record<string, StorageLayout> = {};
30
+
31
+ const namespacedSolcOutput: SolcOutput = namespacedBuildInfo.output;
32
+ const namespacedContracts: Record<string, ContractDefinition> = {};
33
+ const namespacedStorageLayouts: Record<string, StorageLayout> = {};
34
+
35
+ const origContractDefs = [];
36
+ for (const def of findAll(
37
+ 'ContractDefinition',
38
+ origSolcOutput.sources['contracts/test/NamespacedConflicts.sol'].ast,
39
+ )) {
40
+ origContractDefs.push(def);
41
+ }
42
+ const namespacedContractDefs = [];
43
+ for (const def of findAll(
44
+ 'ContractDefinition',
45
+ namespacedSolcOutput.sources['contracts/test/NamespacedConflictsLayout.sol'].ast,
46
+ )) {
47
+ namespacedContractDefs.push(def);
48
+ }
49
+
50
+ for (let i = 0; i < origContractDefs.length; i++) {
51
+ const origContractDef = origContractDefs[i];
52
+ const namespacedContractDef = namespacedContractDefs[i];
53
+
54
+ origContracts[origContractDef.name] = origContractDef;
55
+ namespacedContracts[namespacedContractDef.name] = namespacedContractDef;
56
+
57
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
58
+ origStorageLayouts[origContractDef.name] =
59
+ origSolcOutput.contracts['contracts/test/NamespacedConflicts.sol'][origContractDef.name].storageLayout!;
60
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
61
+ namespacedStorageLayouts[namespacedContractDef.name] =
62
+ namespacedSolcOutput.contracts['contracts/test/NamespacedConflictsLayout.sol'][
63
+ namespacedContractDef.name
64
+ ].storageLayout!;
65
+ }
66
+ const origDeref = astDereferencer(origSolcOutput);
67
+ const namespacedDeref = astDereferencer(namespacedSolcOutput);
68
+
69
+ const decodeSrc = solcInputOutputDecoder(origBuildInfo.input, origSolcOutput);
70
+ t.context.extractStorageLayout = name =>
71
+ extractStorageLayout(origContracts[name], decodeSrc, origDeref, origStorageLayouts[name], {
72
+ deref: namespacedDeref,
73
+ contractDef: namespacedContracts[name],
74
+ storageLayout: namespacedStorageLayouts[name],
75
+ });
76
+ });
77
+
78
+ test('duplicate namespace', t => {
79
+ const error = t.throws(() => t.context.extractStorageLayout('DuplicateNamespace'));
80
+ t.snapshot(error?.message);
81
+ });
82
+
83
+ test('inherits duplicate', t => {
84
+ const error = t.throws(() => t.context.extractStorageLayout('InheritsDuplicate'));
85
+ t.snapshot(error?.message);
86
+ });
87
+
88
+ test('conflicts with parent', t => {
89
+ const error = t.throws(() => t.context.extractStorageLayout('ConflictsWithParent'));
90
+ t.snapshot(error?.message);
91
+ });
92
+
93
+ test('conflicts in both parents', t => {
94
+ const error = t.throws(() => t.context.extractStorageLayout('ConflictsInBothParents'));
95
+ t.snapshot(error?.message);
96
+ });