@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
@@ -0,0 +1,183 @@
1
+ # Snapshot report for `src/storage-namespaced-renamed-retyped.test.ts`
2
+
3
+ The actual snapshot is saved in `storage-namespaced-renamed-retyped.test.ts.snap`.
4
+
5
+ Generated by [AVA](https://avajs.dev).
6
+
7
+ ## layout
8
+
9
+ > Snapshot 1
10
+
11
+ {
12
+ baseSlot: undefined,
13
+ namespaces: [
14
+ [
15
+ 'erc7201:example.main',
16
+ [
17
+ {
18
+ contract: 'NamespacedRenamedRetypedV2',
19
+ label: 'x',
20
+ offset: 0,
21
+ retypedFrom: 'bytes32',
22
+ slot: '0',
23
+ src: 'contracts/test/NamespacedRenamedRetyped.sol:40',
24
+ type: 't_struct(TBytes32)storage',
25
+ },
26
+ {
27
+ contract: 'NamespacedRenamedRetypedV2',
28
+ label: 'z',
29
+ offset: 0,
30
+ renamedFrom: 'y',
31
+ slot: '1',
32
+ src: 'contracts/test/NamespacedRenamedRetyped.sol:46',
33
+ type: 't_uint256',
34
+ },
35
+ {
36
+ contract: 'NamespacedRenamedRetypedV2',
37
+ label: 'f',
38
+ offset: 0,
39
+ retypedFrom: 'uint8',
40
+ slot: '2',
41
+ src: 'contracts/test/NamespacedRenamedRetyped.sol:48',
42
+ type: 't_bool',
43
+ },
44
+ {
45
+ contract: 'NamespacedRenamedRetypedV2',
46
+ label: 'inner',
47
+ offset: 0,
48
+ slot: '3',
49
+ src: 'contracts/test/NamespacedRenamedRetyped.sol:49',
50
+ type: 't_struct(Inner)storage',
51
+ },
52
+ {
53
+ contract: 'NamespacedRenamedRetypedV2',
54
+ label: 'c',
55
+ offset: 0,
56
+ slot: '5',
57
+ src: 'contracts/test/NamespacedRenamedRetyped.sol:50',
58
+ type: 't_uint256',
59
+ },
60
+ {
61
+ contract: 'NamespacedRenamedRetypedV2',
62
+ label: 'd',
63
+ offset: 0,
64
+ renamedFrom: 'oldD',
65
+ slot: '6',
66
+ src: 'contracts/test/NamespacedRenamedRetyped.sol:51',
67
+ type: 't_uint256',
68
+ },
69
+ ],
70
+ ],
71
+ ],
72
+ storage: [],
73
+ types: [
74
+ [
75
+ 't_bool',
76
+ {
77
+ label: 'bool',
78
+ members: undefined,
79
+ numberOfBytes: '1',
80
+ },
81
+ ],
82
+ [
83
+ 't_bytes32',
84
+ {
85
+ label: 'bytes32',
86
+ members: undefined,
87
+ numberOfBytes: '32',
88
+ },
89
+ ],
90
+ [
91
+ 't_struct(Inner)storage',
92
+ {
93
+ label: 'struct NamespacedRenamedRetypedV2.Inner',
94
+ members: [
95
+ {
96
+ label: 'g',
97
+ offset: 0,
98
+ renamedFrom: 'a',
99
+ retypedFrom: 'uint8',
100
+ slot: '0',
101
+ type: 't_bool',
102
+ },
103
+ {
104
+ label: 'bb',
105
+ offset: 0,
106
+ renamedFrom: 'b',
107
+ slot: '1',
108
+ type: 't_bytes32',
109
+ },
110
+ ],
111
+ numberOfBytes: '64',
112
+ },
113
+ ],
114
+ [
115
+ 't_struct(MainStorage)storage',
116
+ {
117
+ label: 'struct NamespacedRenamedRetypedV2.MainStorage',
118
+ members: [
119
+ {
120
+ label: 'x',
121
+ offset: 0,
122
+ slot: '0',
123
+ type: 't_struct(TBytes32)storage',
124
+ },
125
+ {
126
+ label: 'z',
127
+ offset: 0,
128
+ slot: '1',
129
+ type: 't_uint256',
130
+ },
131
+ {
132
+ label: 'f',
133
+ offset: 0,
134
+ slot: '2',
135
+ type: 't_bool',
136
+ },
137
+ {
138
+ label: 'inner',
139
+ offset: 0,
140
+ slot: '3',
141
+ type: 't_struct(Inner)storage',
142
+ },
143
+ {
144
+ label: 'c',
145
+ offset: 0,
146
+ slot: '5',
147
+ type: 't_uint256',
148
+ },
149
+ {
150
+ label: 'd',
151
+ offset: 0,
152
+ slot: '6',
153
+ type: 't_uint256',
154
+ },
155
+ ],
156
+ numberOfBytes: '224',
157
+ },
158
+ ],
159
+ [
160
+ 't_struct(TBytes32)storage',
161
+ {
162
+ label: 'struct TransientSlot.TBytes32',
163
+ members: [
164
+ {
165
+ label: '_placeholder',
166
+ offset: 0,
167
+ slot: '0',
168
+ type: 't_bytes32',
169
+ },
170
+ ],
171
+ numberOfBytes: '32',
172
+ },
173
+ ],
174
+ [
175
+ 't_uint256',
176
+ {
177
+ label: 'uint256',
178
+ members: undefined,
179
+ numberOfBytes: '32',
180
+ },
181
+ ],
182
+ ],
183
+ }
@@ -0,0 +1,299 @@
1
+ import _test, { TestFn } from 'ava';
2
+ import { artifacts } from 'hardhat';
3
+ import { BuildInfo } from 'hardhat/types';
4
+ import { findAll } from 'solidity-ast/utils';
5
+
6
+ import { SolcInput, SolcOutput } from './solc-api';
7
+ import { validate, ValidationRunData } from './validate';
8
+ import { withValidationDefaults } from './validate/overrides';
9
+ import { solcInputOutputDecoder } from './src-decoder';
10
+ import { getStorageUpgradeErrors, getStorageUpgradeReport } from './storage';
11
+ import { StorageLayout, isStructMembers } from './storage/layout';
12
+ import { stabilizeStorageLayout } from './utils/stabilize-layout';
13
+
14
+ const source = 'contracts/test/StructMemberRenamedRetyped.sol';
15
+
16
+ interface Context {
17
+ buildInfo: BuildInfo;
18
+ validate: (solcInput?: SolcInput, solcOutput?: SolcOutput) => ValidationRunData;
19
+ layout: (contract: string) => StorageLayout;
20
+ }
21
+
22
+ const test = _test as TestFn<Context>;
23
+
24
+ test.before(async t => {
25
+ const buildInfo = await artifacts.getBuildInfo(`${source}:StructMemberRenamedRetypedV1`);
26
+ if (buildInfo === undefined) {
27
+ throw new Error('Build info not found');
28
+ }
29
+ t.context.buildInfo = buildInfo;
30
+ // The source decoder always uses the original build info, since the tests below only modify the input in ways
31
+ // that keep line numbers unchanged.
32
+ const decodeSrc = solcInputOutputDecoder(buildInfo.input, buildInfo.output);
33
+ t.context.validate = (solcInput = buildInfo.input, solcOutput = buildInfo.output) =>
34
+ validate(solcOutput, decodeSrc, buildInfo.solcVersion, solcInput);
35
+ const validation = t.context.validate();
36
+ t.context.layout = contract => validation[`${source}:${contract}`].layout;
37
+ });
38
+
39
+ function getStructMembers(layout: StorageLayout, label: string) {
40
+ const members = Object.values(layout.types).find(type => type.label === label)?.members;
41
+ if (members === undefined || !isStructMembers(members)) {
42
+ throw new Error(`Struct members not found for ${label}`);
43
+ }
44
+ return members;
45
+ }
46
+
47
+ /**
48
+ * Replaces the given comment inside a struct body with code of the same length, so that the AST offsets still line up
49
+ * but the text between two members is not whitespace or comments, as it would be with syntax that is not supported.
50
+ */
51
+ function withUnexpectedStructSyntax(solcInput: SolcInput, comment: string): SolcInput {
52
+ const content = solcInput.sources[source].content;
53
+ if (content === undefined || !content.includes(comment)) {
54
+ throw new Error(`Comment ${comment} not found in ${source}`);
55
+ }
56
+ const code = 'uint256 q;'.padEnd(comment.length);
57
+ return { ...solcInput, sources: { ...solcInput.sources, [source]: { content: content.replace(comment, code) } } };
58
+ }
59
+
60
+ test('layout', t => {
61
+ t.snapshot(stabilizeStorageLayout(t.context.layout('StructMemberRenamedRetypedV2')));
62
+ });
63
+
64
+ test('upgrade ok', t => {
65
+ const v1 = t.context.layout('StructMemberRenamedRetypedV1');
66
+ const v2 = t.context.layout('StructMemberRenamedRetypedV2');
67
+ t.deepEqual(getStorageUpgradeErrors(v1, v2), []);
68
+ });
69
+
70
+ test('exact map', t => {
71
+ const members = getStructMembers(t.context.layout('StructMemberExactMap'), 'struct StructMemberExactMap.S');
72
+ t.is(members.length, 5);
73
+ for (const member of members) {
74
+ t.is(member.renamedFrom, `old_${member.label}`);
75
+ t.is(member.retypedFrom, undefined);
76
+ }
77
+ });
78
+
79
+ test('wrong retype', t => {
80
+ const v1 = t.context.layout('StructMemberBadV1');
81
+ const v2 = t.context.layout('StructMemberWrongRetypeV2');
82
+ t.like(getStorageUpgradeErrors(v1, v2), {
83
+ length: 1,
84
+ 0: {
85
+ kind: 'typechange',
86
+ change: {
87
+ kind: 'struct members',
88
+ ops: {
89
+ length: 1,
90
+ 0: { kind: 'typechange', original: { label: 'x' }, updated: { label: 'x', retypedFrom: 'address' } },
91
+ },
92
+ },
93
+ original: { label: 's' },
94
+ updated: { label: 's' },
95
+ },
96
+ });
97
+ });
98
+
99
+ test('retype with layout change', t => {
100
+ // A retype annotation that names the original type does not allow the member's size to change
101
+ const v1 = t.context.layout('StructMemberRetypeLayoutChangeV1');
102
+ const v2 = t.context.layout('StructMemberRetypeLayoutChangeV2');
103
+ t.like(getStorageUpgradeErrors(v1, v2), {
104
+ length: 1,
105
+ 0: {
106
+ kind: 'typechange',
107
+ change: {
108
+ kind: 'struct members',
109
+ ops: {
110
+ length: 1,
111
+ 0: {
112
+ kind: 'layoutchange',
113
+ original: { label: 'y' },
114
+ updated: { label: 'y', retypedFrom: 'uint256' },
115
+ change: { bytes: { from: '32', to: '16' } },
116
+ },
117
+ },
118
+ },
119
+ original: { label: 's' },
120
+ updated: { label: 's' },
121
+ },
122
+ });
123
+ });
124
+
125
+ test('annotation on the wrong member', t => {
126
+ // The annotation makes `x` match the original `y` with a different type, and `x` and `z` are left unmatched
127
+ const v1 = t.context.layout('StructMemberBadV1');
128
+ const v2 = t.context.layout('StructMemberWrongMemberV2');
129
+ t.like(getStorageUpgradeErrors(v1, v2), {
130
+ length: 1,
131
+ 0: {
132
+ kind: 'typechange',
133
+ change: {
134
+ kind: 'struct members',
135
+ ops: {
136
+ length: 3,
137
+ 0: { kind: 'delete', original: { label: 'x' } },
138
+ 1: { kind: 'typechange', original: { label: 'y' }, updated: { label: 'x', renamedFrom: 'y' } },
139
+ 2: { kind: 'insert', updated: { label: 'z' } },
140
+ },
141
+ },
142
+ original: { label: 's' },
143
+ updated: { label: 's' },
144
+ },
145
+ });
146
+ });
147
+
148
+ test('stale rename annotation', t => {
149
+ const v1 = t.context.layout('StructMemberBadV1');
150
+ const v2 = t.context.layout('StructMemberStaleRenameV2');
151
+ t.like(getStorageUpgradeErrors(v1, v2), {
152
+ length: 1,
153
+ 0: {
154
+ kind: 'typechange',
155
+ change: {
156
+ kind: 'struct members',
157
+ ops: {
158
+ length: 1,
159
+ 0: { kind: 'rename', original: { label: 'y' }, updated: { label: 'y', renamedFrom: 'q' } },
160
+ },
161
+ },
162
+ original: { label: 's' },
163
+ updated: { label: 's' },
164
+ },
165
+ });
166
+ });
167
+
168
+ test('unannotated rename and retype', t => {
169
+ const v1 = t.context.layout('StructMemberBadV1');
170
+ const v2 = t.context.layout('StructMemberUnannotatedV2');
171
+ t.like(getStorageUpgradeErrors(v1, v2), {
172
+ length: 1,
173
+ 0: {
174
+ kind: 'typechange',
175
+ change: {
176
+ kind: 'struct members',
177
+ ops: {
178
+ length: 2,
179
+ 0: { kind: 'typechange', original: { label: 'x' }, updated: { label: 'x' } },
180
+ 1: { kind: 'rename', original: { label: 'y' }, updated: { label: 'z' } },
181
+ },
182
+ },
183
+ original: { label: 's' },
184
+ updated: { label: 's' },
185
+ },
186
+ });
187
+ });
188
+
189
+ test('annotated members without layout info', t => {
190
+ // Without the compiler's storage layout, members have no slots. A retype that is not known to be compatible is then uncertain.
191
+ const solcOutput: SolcOutput = JSON.parse(JSON.stringify(t.context.buildInfo.output));
192
+ for (const contracts of Object.values(solcOutput.contracts)) {
193
+ for (const contract of Object.values(contracts)) {
194
+ delete contract.storageLayout;
195
+ }
196
+ }
197
+ const validation = t.context.validate(undefined, solcOutput);
198
+ const v1 = validation[`${source}:StructMemberRenamedRetypedV1`].layout;
199
+ const v2 = validation[`${source}:StructMemberRenamedRetypedV2`].layout;
200
+ const report = getStorageUpgradeReport(v1, v2, withValidationDefaults({}));
201
+ t.false(report.ok);
202
+ t.like(report.ops, {
203
+ length: 4,
204
+ 0: { kind: 'typechange', original: { label: 's' } },
205
+ 1: { kind: 'typechange', original: { label: 'm' } },
206
+ 2: { kind: 'typechange', original: { label: 'arr' } },
207
+ 3: { kind: 'typechange', original: { label: 'other' } },
208
+ });
209
+ t.true(
210
+ report.explain().includes('Layout could have changed for `x` (bytes32 -> struct TransientSlot.TBytes32)'),
211
+ report.explain(),
212
+ );
213
+ t.snapshot(report.explain());
214
+ });
215
+
216
+ test('annotations are not read without source content', t => {
217
+ const { input } = t.context.buildInfo;
218
+ const sources = Object.fromEntries(Object.keys(input.sources).map(path => [path, {}]));
219
+ const validation = t.context.validate({ ...input, sources });
220
+ const v1 = validation[`${source}:StructMemberRenamedRetypedV1`].layout;
221
+ const v2 = validation[`${source}:StructMemberRenamedRetypedV2`].layout;
222
+ t.like(getStorageUpgradeErrors(v1, v2), {
223
+ length: 5,
224
+ 0: { kind: 'typechange', original: { label: 's' } },
225
+ 1: { kind: 'typechange', original: { label: 'm' } },
226
+ 2: { kind: 'typechange', original: { label: 'arr' } },
227
+ 3: { kind: 'typechange', original: { label: 'outer' } },
228
+ 4: { kind: 'typechange', original: { label: 'other' } },
229
+ });
230
+ });
231
+
232
+ test('unexpected syntax in struct with annotations', t => {
233
+ const { input, output } = t.context.buildInfo;
234
+ const structDef = [...findAll('StructDefinition', output.sources[source].ast)].find(
235
+ s => s.canonicalName === 'StructMemberUnreadableAnnotated.S',
236
+ );
237
+ if (structDef === undefined) {
238
+ throw new Error('Struct not found');
239
+ }
240
+ const structSrc = solcInputOutputDecoder(input, output)(structDef);
241
+
242
+ const error = t.throws(() => t.context.validate(withUnexpectedStructSyntax(input, '// 12345678')));
243
+ t.true(
244
+ error?.message.includes(
245
+ `${structSrc}: Could not read storage annotations on members of struct \`StructMemberUnreadableAnnotated.S\``,
246
+ ),
247
+ error?.message,
248
+ );
249
+ t.true(
250
+ error?.message.includes(
251
+ 'Please report this at https://zpl.in/upgrades/report and include the struct definition from your contract.',
252
+ ),
253
+ error?.message,
254
+ );
255
+ });
256
+
257
+ test('unexpected syntax in struct without annotations', t => {
258
+ const validation = t.context.validate(withUnexpectedStructSyntax(t.context.buildInfo.input, '// 87654321'));
259
+ const layout = validation[`${source}:StructMemberUnreadableUnannotated`].layout;
260
+ t.like(getStructMembers(layout, 'struct StructMemberUnreadableUnannotated.S'), {
261
+ length: 2,
262
+ 0: { label: 'b', renamedFrom: undefined, retypedFrom: undefined },
263
+ 1: { label: 'c', renamedFrom: undefined, retypedFrom: undefined },
264
+ });
265
+ // Other structs are unaffected
266
+ const v1 = validation[`${source}:StructMemberRenamedRetypedV1`].layout;
267
+ const v2 = validation[`${source}:StructMemberRenamedRetypedV2`].layout;
268
+ t.deepEqual(getStorageUpgradeErrors(v1, v2), []);
269
+ });
270
+
271
+ test('unexpected syntax in struct with other annotations', t => {
272
+ const validation = t.context.validate(withUnexpectedStructSyntax(t.context.buildInfo.input, '// 13572468'));
273
+ const layout = validation[`${source}:StructMemberUnreadableOtherAnnotation`].layout;
274
+ t.like(getStructMembers(layout, 'struct StructMemberUnreadableOtherAnnotation.S'), {
275
+ length: 2,
276
+ 0: { label: 'b', renamedFrom: undefined, retypedFrom: undefined },
277
+ 1: { label: 'c', renamedFrom: undefined, retypedFrom: undefined },
278
+ });
279
+ });
280
+
281
+ test('multiple annotations in a doc block without line prefixes', t => {
282
+ const layout = t.context.layout('StateVariableDocBlock');
283
+ t.like(layout.storage, { length: 1, 0: { label: 'x', renamedFrom: 'a', retypedFrom: 'uint8' } });
284
+ });
285
+
286
+ test('stale rename annotation report', t => {
287
+ const v1 = t.context.layout('StructMemberBadV1');
288
+ const v2 = t.context.layout('StructMemberStaleRenameV2');
289
+ const report = getStorageUpgradeReport(v1, v2, withValidationDefaults({}));
290
+ t.false(report.ok);
291
+ t.is(
292
+ report.explain(),
293
+ `${v2.storage[0].src}: Upgraded \`s\` to an incompatible type\n` +
294
+ ' - Bad upgrade from struct StructMemberBadV1.S to struct StructMemberStaleRenameV2.S\n' +
295
+ ' - In struct StructMemberStaleRenameV2.S\n' +
296
+ " - `y` has a '@custom:oz-renamed-from q' annotation, but it was not renamed\n" +
297
+ ' > If the annotation is left over from an earlier upgrade, remove it.',
298
+ );
299
+ });