@openzeppelin/upgrades-core 1.45.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 (88) 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 +68 -22
  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 +26 -13
  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-erc7201-builtin.test.d.ts +2 -0
  18. package/dist/storage-erc7201-builtin.test.d.ts.map +1 -0
  19. package/dist/storage-erc7201-builtin.test.js +105 -0
  20. package/dist/storage-erc7201-builtin.test.js.map +1 -0
  21. package/dist/storage-namespaced-renamed-retyped.test.d.ts +2 -0
  22. package/dist/storage-namespaced-renamed-retyped.test.d.ts.map +1 -0
  23. package/dist/storage-namespaced-renamed-retyped.test.js +278 -0
  24. package/dist/storage-namespaced-renamed-retyped.test.js.map +1 -0
  25. package/dist/storage-struct-member-renamed-retyped.test.d.ts +2 -0
  26. package/dist/storage-struct-member-renamed-retyped.test.d.ts.map +1 -0
  27. package/dist/storage-struct-member-renamed-retyped.test.js +256 -0
  28. package/dist/storage-struct-member-renamed-retyped.test.js.map +1 -0
  29. package/dist/utils/annotations.d.ts +1 -0
  30. package/dist/utils/annotations.d.ts.map +1 -1
  31. package/dist/utils/annotations.js +4 -1
  32. package/dist/utils/annotations.js.map +1 -1
  33. package/dist/utils/erc7201-builtin.d.ts +24 -0
  34. package/dist/utils/erc7201-builtin.d.ts.map +1 -0
  35. package/dist/utils/erc7201-builtin.js +83 -0
  36. package/dist/utils/erc7201-builtin.js.map +1 -0
  37. package/dist/utils/erc7201.d.ts.map +1 -1
  38. package/dist/utils/erc7201.js +5 -3
  39. package/dist/utils/erc7201.js.map +1 -1
  40. package/dist/utils/erc7201.test.js +10 -0
  41. package/dist/utils/erc7201.test.js.map +1 -1
  42. package/dist/utils/integer-literals.d.ts +19 -6
  43. package/dist/utils/integer-literals.d.ts.map +1 -1
  44. package/dist/utils/integer-literals.js +28 -10
  45. package/dist/utils/integer-literals.js.map +1 -1
  46. package/dist/utils/integer-literals.test.js +12 -0
  47. package/dist/utils/integer-literals.test.js.map +1 -1
  48. package/dist/utils/struct-member-docs.d.ts +52 -0
  49. package/dist/utils/struct-member-docs.d.ts.map +1 -0
  50. package/dist/utils/struct-member-docs.js +264 -0
  51. package/dist/utils/struct-member-docs.js.map +1 -0
  52. package/dist/utils/struct-member-docs.test.d.ts +2 -0
  53. package/dist/utils/struct-member-docs.test.d.ts.map +1 -0
  54. package/dist/utils/struct-member-docs.test.js +320 -0
  55. package/dist/utils/struct-member-docs.test.js.map +1 -0
  56. package/dist/validate/data.d.ts +1 -1
  57. package/dist/validate/data.d.ts.map +1 -1
  58. package/dist/validate/data.js +1 -1
  59. package/dist/validate/run.d.ts +1 -1
  60. package/dist/validate/run.d.ts.map +1 -1
  61. package/dist/validate/run.js +22 -2
  62. package/dist/validate/run.js.map +1 -1
  63. package/package.json +5 -5
  64. package/src/cli/validate/validate-upgrade-safety.test.ts +35 -0
  65. package/src/cli/validate/validate-upgrade-safety.test.ts.md +10 -0
  66. package/src/cli/validate/validate-upgrade-safety.test.ts.snap +0 -0
  67. package/src/storage/compare.ts +3 -4
  68. package/src/storage/extract.ts +83 -30
  69. package/src/storage/namespace.ts +31 -14
  70. package/src/storage/report-rename-retype.test.ts +41 -0
  71. package/src/storage/report.ts +9 -1
  72. package/src/storage-erc7201-builtin.test.ts +127 -0
  73. package/src/storage-namespaced-renamed-retyped.test.ts +338 -0
  74. package/src/storage-namespaced-renamed-retyped.test.ts.md +183 -0
  75. package/src/storage-namespaced-renamed-retyped.test.ts.snap +0 -0
  76. package/src/storage-struct-member-renamed-retyped.test.ts +299 -0
  77. package/src/storage-struct-member-renamed-retyped.test.ts.md +260 -0
  78. package/src/storage-struct-member-renamed-retyped.test.ts.snap +0 -0
  79. package/src/utils/annotations.ts +4 -4
  80. package/src/utils/erc7201-builtin.ts +91 -0
  81. package/src/utils/erc7201.test.ts +12 -0
  82. package/src/utils/erc7201.ts +6 -4
  83. package/src/utils/integer-literals.test.ts +15 -1
  84. package/src/utils/integer-literals.ts +29 -10
  85. package/src/utils/struct-member-docs.test.ts +351 -0
  86. package/src/utils/struct-member-docs.ts +293 -0
  87. package/src/validate/data.ts +2 -2
  88. package/src/validate/run.ts +23 -1
@@ -96,3 +96,13 @@ Generated by [AVA](https://avajs.dev).
96
96
  Different representation sizes␊
97
97
  ␊
98
98
  FAILED (1 upgradeable contract detected, 0 passed, 1 failed)`
99
+
100
+ ## struct member annotations - namespace retype without layout info
101
+
102
+ > Snapshot 1
103
+
104
+ ` ✘ contracts/test/cli/StructMemberAnnotations.sol:NamespaceRetypeV2 (upgrades from contracts/test/cli/StructMemberAnnotations.sol:NamespaceRetypeV1)␊
105
+ ␊
106
+ contracts/test/cli/StructMemberAnnotations.sol:52: Layout could have changed for \`x\` (bytes32 -> struct TransientSlot.TBytes32)␊
107
+ ␊
108
+ FAILED`
@@ -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,14 +8,17 @@ 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';
21
+ import { resolveBaseSlot } from '../utils/erc7201-builtin';
19
22
 
20
23
  const currentLayoutVersion = '1.2';
21
24
 
@@ -27,6 +30,7 @@ export interface CompilationContext {
27
30
  deref: ASTDereferencer;
28
31
  contractDef: ContractDefinition;
29
32
  storageLayout?: StorageLayout;
33
+ structDocs?: StructDocsResolver;
30
34
  }
31
35
 
32
36
  export function extractStorageLayout(
@@ -35,6 +39,7 @@ export function extractStorageLayout(
35
39
  deref: ASTDereferencer,
36
40
  storageLayout?: StorageLayout,
37
41
  namespacedContext?: CompilationContext,
42
+ structDocs?: StructDocsResolver,
38
43
  ): StorageLayout {
39
44
  const layout: StorageLayout = { storage: [], types: {}, layoutVersion: currentLayoutVersion, flat: false };
40
45
 
@@ -71,7 +76,7 @@ export function extractStorageLayout(
71
76
  const { varDecl, contract } = origin;
72
77
  const { renamedFrom, retypedFrom } = getRetypedRenamed(varDecl);
73
78
  // Solc layout doesn't bring members for enums so we get them using the ast method
74
- loadLayoutType(varDecl.typeName, layout, deref);
79
+ loadLayoutType(varDecl.typeName, layout, deref, structDocs);
75
80
  const { label, offset, slot, type } = storage;
76
81
  const src = decodeSrc(varDecl);
77
82
  layout.storage.push({ label, offset, slot, type, contract, src, retypedFrom, renamedFrom });
@@ -92,13 +97,13 @@ export function extractStorageLayout(
92
97
  renamedFrom,
93
98
  });
94
99
 
95
- loadLayoutType(varDecl.typeName, layout, deref);
100
+ loadLayoutType(varDecl.typeName, layout, deref, structDocs);
96
101
  }
97
102
  }
98
103
  }
99
104
  }
100
105
 
101
- const origContext = { deref, contractDef, storageLayout };
106
+ const origContext = { deref, contractDef, storageLayout, structDocs };
102
107
  loadNamespaces(decodeSrc, layout, origContext, namespacedContext);
103
108
 
104
109
  return layout;
@@ -106,19 +111,34 @@ export function extractStorageLayout(
106
111
 
107
112
  function loadBaseSlot(contractDef: ContractDefinition, decodeSrc: SrcDecoder, layout: StorageLayout) {
108
113
  if (contractDef.storageLayout !== undefined) {
109
- if (contractDef.storageLayout.baseSlotExpression.nodeType !== 'Literal') {
110
- const src = decodeSrc(contractDef.storageLayout.baseSlotExpression);
111
-
112
- // TODO when Slang supports Solidity 0.8.29, use it to parse the expression (of any node type) into a string, then just save it to `layout.baseSlot`.
113
- // Then when comparing layouts in `storage/index.ts`, it will only be used when detailed storage layout is not available, and the comparison
114
- // can be changed to a string comparison when `layout.baseSlot` not an integer literal.
115
- throw new UpgradesError(
116
- `${src}: Custom storage layout expression with node type ${contractDef.storageLayout.baseSlotExpression.nodeType} is not currently supported`,
117
- () =>
118
- 'Please report this at https://zpl.in/upgrades/report and include the `layout at` expression from your contract.',
119
- );
114
+ const baseSlotExpression = contractDef.storageLayout.baseSlotExpression;
115
+ if (baseSlotExpression.nodeType === 'Literal') {
116
+ layout.baseSlot = baseSlotExpression.value ?? undefined;
117
+ return;
120
118
  }
121
- layout.baseSlot = contractDef.storageLayout.baseSlotExpression.value ?? undefined;
119
+
120
+ // Solidity 0.8.35+ allows the `erc7201(string)` comptime builtin as a base slot, e.g.
121
+ // `layout at erc7201("example.main")`. Resolve it to its computed storage location.
122
+ const resolvedSlot = resolveBaseSlot(baseSlotExpression);
123
+ if (resolvedSlot !== undefined) {
124
+ layout.baseSlot = resolvedSlot;
125
+ return;
126
+ }
127
+
128
+ const src = decodeSrc(baseSlotExpression);
129
+
130
+ // TODO if a base slot expression other than a numeric literal or an `erc7201(...)` builtin call
131
+ // needs to be supported, evaluate the right approach per expression — either resolve it to a
132
+ // computed value (like the erc7201 builtin) or compare its source text across upgrades. A
133
+ // source-text approach could use Slang to extract the expression's source span (Slang supports
134
+ // `StorageLayoutSpecifier` from 0.8.29) and store it as a string on `layout.baseSlot`, with the
135
+ // upgrade-time comparison in `storage/index.ts:validateBaseSlotUnchanged` falling back to a
136
+ // string compare when `layout.baseSlot` is not a numeric literal.
137
+ throw new UpgradesError(
138
+ `${src}: Custom storage layout expression with node type ${baseSlotExpression.nodeType} is not currently supported`,
139
+ () =>
140
+ 'Please report this at https://zpl.in/upgrades/report and include the `layout at` expression from your contract.',
141
+ );
122
142
  }
123
143
  }
124
144
 
@@ -185,7 +205,12 @@ function getOriginContract(contract: ContractDefinition, astId: number | undefin
185
205
  }
186
206
  }
187
207
 
188
- 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
+ ) {
189
214
  // Note: A UserDefinedTypeName can also refer to a ContractDefinition but we won't care about those.
190
215
  const derefUserDefinedType = deref(['StructDefinition', 'EnumDefinition', 'UserDefinedValueTypeDefinition']);
191
216
 
@@ -208,6 +233,10 @@ export function loadLayoutType(typeName: TypeName | null | undefined, layout: St
208
233
  layout.types[type].underlying = typeDef.underlyingType.typeDescriptions.typeIdentifier ?? undefined;
209
234
  } else {
210
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
+ }
211
240
  }
212
241
 
213
242
  // Recursively look for the types referenced in this definition and add them to the queue.
@@ -222,22 +251,46 @@ export function loadLayoutType(typeName: TypeName | null | undefined, layout: St
222
251
  }
223
252
 
224
253
  function getRetypedRenamed(varDecl: VariableDeclaration) {
254
+ return getRetypedRenamedFromDoc(getDocumentation(varDecl));
255
+ }
256
+
257
+ export function getRetypedRenamedFromDoc(doc: string) {
225
258
  let retypedFrom, renamedFrom;
226
- if ('documentation' in varDecl) {
227
- const docs =
228
- typeof varDecl.documentation === 'string' ? varDecl.documentation : (varDecl.documentation?.text ?? '');
229
- for (const { groups } of execall(
230
- /^\s*(?:@(?<title>\w+)(?::(?<tag>[a-z][a-z-]*))? )?(?<args>(?:(?!^\s@\w+)[^])*)/m,
231
- docs,
232
- )) {
233
- if (groups?.title === 'custom') {
234
- if (groups.tag === 'oz-retyped-from') {
235
- retypedFrom = groups.args.trim();
236
- } else if (groups.tag === 'oz-renamed-from') {
237
- renamedFrom = groups.args.trim();
238
- }
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();
239
265
  }
240
266
  }
241
267
  }
242
268
  return { retypedFrom, renamedFrom };
243
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,11 +5,11 @@ 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
- import { normalizeUint256Literal } from '../utils/integer-literals';
12
11
  import { calculateERC7201StorageLocation, ERC7201_FORMULA_PREFIX } from '../utils/erc7201';
12
+ import { getErc7201BuiltinNamespaceId, resolveBaseSlot } from '../utils/erc7201-builtin';
13
13
  import { logWarning } from '../utils/log';
14
14
 
15
15
  /**
@@ -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,
@@ -175,15 +177,18 @@ function checkCustomLayoutClashWithNamespace(
175
177
  contractName: string,
176
178
  src: string,
177
179
  ) {
178
- if (contractDef.storageLayout !== undefined && storageLocation.startsWith(ERC7201_FORMULA_PREFIX)) {
179
- const namespaceId = storageLocation.substring(ERC7201_FORMULA_PREFIX.length);
180
- const calculatedStorageLocation = calculateERC7201StorageLocation(namespaceId);
181
- if (contractDef.storageLayout.baseSlotExpression.nodeType === 'Literal') {
182
- const baseSlotNormalized = normalizeUint256Literal(contractDef.storageLayout.baseSlotExpression.value); // TODO: when Solidity supports an erc7201 helper function, also check that for clash
183
- if (baseSlotNormalized === calculatedStorageLocation) {
184
- throw new CustomLayoutClashWithNamespaceError(storageLocation, contractName, src);
185
- }
186
- }
180
+ if (contractDef.storageLayout === undefined || !storageLocation.startsWith(ERC7201_FORMULA_PREFIX)) {
181
+ return;
182
+ }
183
+
184
+ const namespaceId = storageLocation.substring(ERC7201_FORMULA_PREFIX.length);
185
+
186
+ // Resolve the base slot (a numeric literal or an erc7201(...) builtin call) and compare it against
187
+ // the namespace's computed storage location. This covers `layout at erc7201("<id>")` using the same
188
+ // id as the namespace, since both resolve to the same hash.
189
+ const baseSlotNormalized = resolveBaseSlot(contractDef.storageLayout.baseSlotExpression);
190
+ if (baseSlotNormalized !== undefined && baseSlotNormalized === calculateERC7201StorageLocation(namespaceId)) {
191
+ throw new CustomLayoutClashWithNamespaceError(storageLocation, contractName, src);
187
192
  }
188
193
  }
189
194
 
@@ -192,8 +197,15 @@ function warnIfCustomLayoutAndNamespacesFound(
192
197
  decodeSrc: SrcDecoder,
193
198
  origContractDef: ContractDefinition,
194
199
  ) {
195
- // TODO: when Solidity supports an erc7201 helper function, only give this warning if the custom storage layout is not using the erc7201 helper function
196
200
  if (Object.entries(namespacesWithSrc).length > 0 && origContractDef.storageLayout !== undefined) {
201
+ // The warning is meant for users who pick an arbitrary literal slot that could land inside a
202
+ // namespace's range. When the base slot uses the erc7201 comptime builtin, it relies on the
203
+ // same collision resistance that namespaces rely on against each other, so suppress the warning;
204
+ // the tool likewise does not warn when multiple namespaces coexist.
205
+ if (getErc7201BuiltinNamespaceId(origContractDef.storageLayout.baseSlotExpression) !== undefined) {
206
+ return;
207
+ }
208
+
197
209
  const contractName = origContractDef.canonicalName ?? origContractDef.name;
198
210
  logWarning(
199
211
  `${decodeSrc(origContractDef.storageLayout)}: Ensure custom storage layout does not overlap with namespaced storage layout.`,
@@ -238,6 +250,7 @@ function getNamespacedStorageItems(
238
250
  origContractDef: ContractDefinition,
239
251
  ): StorageItem[] {
240
252
  const storageItems: StorageItem[] = [];
253
+ const docs = context.structDocs === undefined ? undefined : context.structDocs(node);
241
254
 
242
255
  for (const astMember of getTypeMembers(node, { typeName: true })) {
243
256
  const item: StorageItem = {
@@ -247,6 +260,10 @@ function getNamespacedStorageItems(
247
260
  src: decodeSrc({ src: getOriginalMemberSrc(node.canonicalName, astMember.label, origContractDef) }),
248
261
  };
249
262
 
263
+ if (docs !== undefined) {
264
+ setMemberAnnotations(item, docs);
265
+ }
266
+
250
267
  const layoutMember = findLayoutStructMember(
251
268
  context.storageLayout?.types ?? {},
252
269
  node.canonicalName,
@@ -262,7 +279,7 @@ function getNamespacedStorageItems(
262
279
 
263
280
  // If context is namespaced, we have storage layout, and this will fill in enum members just like in extractStorageLayout.
264
281
  // If context is original, this will add the types from the namespace structs to the layout.
265
- loadLayoutType(astMember.typeName, layout, context.deref);
282
+ loadLayoutType(astMember.typeName, layout, context.deref, context.structDocs);
266
283
  }
267
284
  return storageItems;
268
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,127 @@
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 { getStorageUpgradeErrors } from './storage';
8
+ import { StorageLayout } from './storage/layout';
9
+ import { extractStorageLayout } from './storage/extract';
10
+
11
+ interface Context {
12
+ extractStorageLayout: (contract: string) => ReturnType<typeof extractStorageLayout>;
13
+ extractStorageLayoutFallback: (contract: string) => ReturnType<typeof extractStorageLayout>;
14
+ }
15
+
16
+ const test = _test as TestFn<Context>;
17
+
18
+ const SOURCE = 'contracts/test/Erc7201Builtin.sol';
19
+
20
+ test.before(async t => {
21
+ const buildInfo = await artifacts.getBuildInfo(`${SOURCE}:Erc7201Builtin`);
22
+ if (buildInfo === undefined) {
23
+ throw new Error('Build info not found');
24
+ }
25
+ const solcOutput: SolcOutput = buildInfo.output;
26
+ const contracts: Record<string, ContractDefinition> = {};
27
+ const storageLayouts: Record<string, StorageLayout> = {};
28
+ for (const def of findAll('ContractDefinition', solcOutput.sources[SOURCE].ast)) {
29
+ contracts[def.name] = def;
30
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
31
+ storageLayouts[def.name] = solcOutput.contracts[SOURCE][def.name].storageLayout!;
32
+ }
33
+ const deref = astDereferencer(solcOutput);
34
+
35
+ t.context.extractStorageLayout = name =>
36
+ extractStorageLayout(contracts[name], dummyDecodeSrc, deref, storageLayouts[name]);
37
+
38
+ t.context.extractStorageLayoutFallback = name =>
39
+ extractStorageLayout(contracts[name], dummyDecodeSrc, deref, undefined);
40
+ });
41
+
42
+ const dummyDecodeSrc = () => 'file.sol:1';
43
+
44
+ test('erc7201 builtin base slot - extract', t => {
45
+ const layout = t.context.extractStorageLayout('Erc7201Builtin');
46
+ t.is(layout.baseSlot, '0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500');
47
+ t.is(layout.storage.length, 1);
48
+ t.is(layout.storage[0].label, 'a');
49
+ });
50
+
51
+ test('erc7201 builtin base slot - append - ok', t => {
52
+ const v1 = t.context.extractStorageLayout('Erc7201Builtin');
53
+ const v2 = t.context.extractStorageLayout('Erc7201Builtin_Append_Ok');
54
+ t.deepEqual(getStorageUpgradeErrors(v1, v2), []);
55
+ });
56
+
57
+ test('erc7201 builtin base slot - append - fallback - ok', t => {
58
+ const v1 = t.context.extractStorageLayoutFallback('Erc7201Builtin');
59
+ const v2 = t.context.extractStorageLayoutFallback('Erc7201Builtin_Append_Ok');
60
+ t.deepEqual(getStorageUpgradeErrors(v1, v2), []);
61
+ });
62
+
63
+ test('erc7201 builtin base slot - changed id - bad', t => {
64
+ const v1 = t.context.extractStorageLayout('Erc7201Builtin');
65
+ const v2 = t.context.extractStorageLayout('Erc7201Builtin_Changed_Id_Bad');
66
+ const comparison = getStorageUpgradeErrors(v1, v2);
67
+ t.true(comparison.length > 0);
68
+ t.is(comparison[0].kind, 'layoutchange');
69
+ });
70
+
71
+ test('erc7201 builtin base slot - changed id - fallback - bad', t => {
72
+ const v1 = t.context.extractStorageLayoutFallback('Erc7201Builtin');
73
+ const v2 = t.context.extractStorageLayoutFallback('Erc7201Builtin_Changed_Id_Bad');
74
+ // Base slot differs between the two erc7201 ids, so the fallback base-slot check must fail.
75
+ t.throws(() => getStorageUpgradeErrors(v1, v2), {
76
+ message: /Base slot for custom storage layout changed/,
77
+ });
78
+ });
79
+
80
+ test('erc7201 builtin and literal-equivalent base slots compare equal', t => {
81
+ const builtin = t.context.extractStorageLayout('Erc7201Builtin');
82
+ const literal = t.context.extractStorageLayout('Erc7201Builtin_Literal_Equivalent');
83
+ // Both resolve to the same base slot; the fallback (base-slot) check must treat them as unchanged.
84
+ const builtinFallback = t.context.extractStorageLayoutFallback('Erc7201Builtin');
85
+ const literalFallback = t.context.extractStorageLayoutFallback('Erc7201Builtin_Literal_Equivalent');
86
+ t.deepEqual(getStorageUpgradeErrors(builtin, literal), []);
87
+ t.deepEqual(getStorageUpgradeErrors(builtinFallback, literalFallback), []);
88
+ });
89
+
90
+ test('erc7201 builtin base slot clashes with namespace - error', t => {
91
+ t.throws(() => t.context.extractStorageLayout('Erc7201Builtin_Clash'), {
92
+ message:
93
+ /Custom layout for contract Erc7201Builtin_Clash clashes with the storage location for namespace erc7201:example\.main/,
94
+ });
95
+ });
96
+
97
+ test('erc7201 builtin base slot different from namespace - no error and no warning', t => {
98
+ const warnings = captureWarnings(() => {
99
+ const layout = t.context.extractStorageLayout('Erc7201Builtin_NoClash_NoWarning');
100
+ t.truthy(layout.baseSlot);
101
+ });
102
+ t.deepEqual(warnings, []);
103
+ });
104
+
105
+ test('erc7201 builtin accepts a unicode string literal argument', t => {
106
+ const layout = t.context.extractStorageLayout('Erc7201Builtin_Unicode');
107
+ t.is(layout.baseSlot, '0x2d3e72b7d91644a80150e2643e65016def7067dc9568719707e7eca1a5dab800');
108
+ });
109
+
110
+ test('literal base slot with namespace - warning not suppressed', t => {
111
+ const warnings = captureWarnings(() => t.context.extractStorageLayout('Erc7201Builtin_LiteralBase_Warns'));
112
+ t.true(warnings.some(w => w.includes('custom storage layout')));
113
+ });
114
+
115
+ function captureWarnings(fn: () => void): string[] {
116
+ const warnings: string[] = [];
117
+ const original = console.error;
118
+ console.error = (...args: unknown[]) => {
119
+ warnings.push(args.map(String).join(' '));
120
+ };
121
+ try {
122
+ fn();
123
+ } finally {
124
+ console.error = original;
125
+ }
126
+ return warnings;
127
+ }