@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.
- package/dist/cli/validate/validate-upgrade-safety.test.js +18 -0
- package/dist/cli/validate/validate-upgrade-safety.test.js.map +1 -1
- package/dist/storage/compare.d.ts.map +1 -1
- package/dist/storage/compare.js +3 -3
- package/dist/storage/compare.js.map +1 -1
- package/dist/storage/extract.d.ts +15 -3
- package/dist/storage/extract.d.ts.map +1 -1
- package/dist/storage/extract.js +68 -22
- package/dist/storage/extract.js.map +1 -1
- package/dist/storage/namespace.d.ts.map +1 -1
- package/dist/storage/namespace.js +26 -13
- package/dist/storage/namespace.js.map +1 -1
- package/dist/storage/report-rename-retype.test.js +30 -0
- package/dist/storage/report-rename-retype.test.js.map +1 -1
- package/dist/storage/report.js +9 -1
- package/dist/storage/report.js.map +1 -1
- package/dist/storage-erc7201-builtin.test.d.ts +2 -0
- package/dist/storage-erc7201-builtin.test.d.ts.map +1 -0
- package/dist/storage-erc7201-builtin.test.js +105 -0
- package/dist/storage-erc7201-builtin.test.js.map +1 -0
- package/dist/storage-namespaced-renamed-retyped.test.d.ts +2 -0
- package/dist/storage-namespaced-renamed-retyped.test.d.ts.map +1 -0
- package/dist/storage-namespaced-renamed-retyped.test.js +278 -0
- package/dist/storage-namespaced-renamed-retyped.test.js.map +1 -0
- package/dist/storage-struct-member-renamed-retyped.test.d.ts +2 -0
- package/dist/storage-struct-member-renamed-retyped.test.d.ts.map +1 -0
- package/dist/storage-struct-member-renamed-retyped.test.js +256 -0
- package/dist/storage-struct-member-renamed-retyped.test.js.map +1 -0
- package/dist/utils/annotations.d.ts +1 -0
- package/dist/utils/annotations.d.ts.map +1 -1
- package/dist/utils/annotations.js +4 -1
- package/dist/utils/annotations.js.map +1 -1
- package/dist/utils/erc7201-builtin.d.ts +24 -0
- package/dist/utils/erc7201-builtin.d.ts.map +1 -0
- package/dist/utils/erc7201-builtin.js +83 -0
- package/dist/utils/erc7201-builtin.js.map +1 -0
- package/dist/utils/erc7201.d.ts.map +1 -1
- package/dist/utils/erc7201.js +5 -3
- package/dist/utils/erc7201.js.map +1 -1
- package/dist/utils/erc7201.test.js +10 -0
- package/dist/utils/erc7201.test.js.map +1 -1
- package/dist/utils/integer-literals.d.ts +19 -6
- package/dist/utils/integer-literals.d.ts.map +1 -1
- package/dist/utils/integer-literals.js +28 -10
- package/dist/utils/integer-literals.js.map +1 -1
- package/dist/utils/integer-literals.test.js +12 -0
- package/dist/utils/integer-literals.test.js.map +1 -1
- package/dist/utils/struct-member-docs.d.ts +52 -0
- package/dist/utils/struct-member-docs.d.ts.map +1 -0
- package/dist/utils/struct-member-docs.js +264 -0
- package/dist/utils/struct-member-docs.js.map +1 -0
- package/dist/utils/struct-member-docs.test.d.ts +2 -0
- package/dist/utils/struct-member-docs.test.d.ts.map +1 -0
- package/dist/utils/struct-member-docs.test.js +320 -0
- package/dist/utils/struct-member-docs.test.js.map +1 -0
- package/dist/validate/data.d.ts +1 -1
- package/dist/validate/data.d.ts.map +1 -1
- package/dist/validate/data.js +1 -1
- package/dist/validate/run.d.ts +1 -1
- package/dist/validate/run.d.ts.map +1 -1
- package/dist/validate/run.js +22 -2
- package/dist/validate/run.js.map +1 -1
- package/package.json +5 -5
- package/src/cli/validate/validate-upgrade-safety.test.ts +35 -0
- package/src/cli/validate/validate-upgrade-safety.test.ts.md +10 -0
- package/src/cli/validate/validate-upgrade-safety.test.ts.snap +0 -0
- package/src/storage/compare.ts +3 -4
- package/src/storage/extract.ts +83 -30
- package/src/storage/namespace.ts +31 -14
- package/src/storage/report-rename-retype.test.ts +41 -0
- package/src/storage/report.ts +9 -1
- package/src/storage-erc7201-builtin.test.ts +127 -0
- package/src/storage-namespaced-renamed-retyped.test.ts +338 -0
- package/src/storage-namespaced-renamed-retyped.test.ts.md +183 -0
- package/src/storage-namespaced-renamed-retyped.test.ts.snap +0 -0
- package/src/storage-struct-member-renamed-retyped.test.ts +299 -0
- package/src/storage-struct-member-renamed-retyped.test.ts.md +260 -0
- package/src/storage-struct-member-renamed-retyped.test.ts.snap +0 -0
- package/src/utils/annotations.ts +4 -4
- package/src/utils/erc7201-builtin.ts +91 -0
- package/src/utils/erc7201.test.ts +12 -0
- package/src/utils/erc7201.ts +6 -4
- package/src/utils/integer-literals.test.ts +15 -1
- package/src/utils/integer-literals.ts +29 -10
- package/src/utils/struct-member-docs.test.ts +351 -0
- package/src/utils/struct-member-docs.ts +293 -0
- package/src/validate/data.ts +2 -2
- package/src/validate/run.ts +23 -1
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Snapshot report for `src/storage-struct-member-renamed-retyped.test.ts`
|
|
2
|
+
|
|
3
|
+
The actual snapshot is saved in `storage-struct-member-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
|
+
storage: [
|
|
15
|
+
{
|
|
16
|
+
contract: 'StructMemberRenamedRetypedV2',
|
|
17
|
+
label: 's',
|
|
18
|
+
offset: 0,
|
|
19
|
+
renamedFrom: undefined,
|
|
20
|
+
retypedFrom: undefined,
|
|
21
|
+
slot: '0',
|
|
22
|
+
src: 'contracts/test/StructMemberRenamedRetyped.sol:62',
|
|
23
|
+
type: 't_struct(S)storage',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
contract: 'StructMemberRenamedRetypedV2',
|
|
27
|
+
label: 'm',
|
|
28
|
+
offset: 0,
|
|
29
|
+
renamedFrom: undefined,
|
|
30
|
+
retypedFrom: undefined,
|
|
31
|
+
slot: '5',
|
|
32
|
+
src: 'contracts/test/StructMemberRenamedRetyped.sol:63',
|
|
33
|
+
type: 't_mapping(t_uint256,t_struct(S)storage)',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
contract: 'StructMemberRenamedRetypedV2',
|
|
37
|
+
label: 'arr',
|
|
38
|
+
offset: 0,
|
|
39
|
+
renamedFrom: undefined,
|
|
40
|
+
retypedFrom: undefined,
|
|
41
|
+
slot: '6',
|
|
42
|
+
src: 'contracts/test/StructMemberRenamedRetyped.sol:64',
|
|
43
|
+
type: 't_array(t_struct(S)storage)dyn_storage',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
contract: 'StructMemberRenamedRetypedV2',
|
|
47
|
+
label: 'outer',
|
|
48
|
+
offset: 0,
|
|
49
|
+
renamedFrom: undefined,
|
|
50
|
+
retypedFrom: undefined,
|
|
51
|
+
slot: '7',
|
|
52
|
+
src: 'contracts/test/StructMemberRenamedRetyped.sol:65',
|
|
53
|
+
type: 't_struct(Outer)storage',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
contract: 'StructMemberRenamedRetypedV2',
|
|
57
|
+
label: 'other',
|
|
58
|
+
offset: 0,
|
|
59
|
+
renamedFrom: undefined,
|
|
60
|
+
retypedFrom: undefined,
|
|
61
|
+
slot: '10',
|
|
62
|
+
src: 'contracts/test/StructMemberRenamedRetyped.sol:66',
|
|
63
|
+
type: 't_struct(OtherFileV2)storage',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
types: [
|
|
67
|
+
[
|
|
68
|
+
't_array(t_struct(S)storage)dyn_storage',
|
|
69
|
+
{
|
|
70
|
+
label: 'struct StructMemberRenamedRetypedV2.S[]',
|
|
71
|
+
members: undefined,
|
|
72
|
+
numberOfBytes: '32',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
't_bool',
|
|
77
|
+
{
|
|
78
|
+
label: 'bool',
|
|
79
|
+
members: undefined,
|
|
80
|
+
numberOfBytes: '1',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
't_bytes32',
|
|
85
|
+
{
|
|
86
|
+
label: 'bytes32',
|
|
87
|
+
members: undefined,
|
|
88
|
+
numberOfBytes: '32',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
't_mapping(t_uint256,t_struct(S)storage)',
|
|
93
|
+
{
|
|
94
|
+
label: 'mapping(uint256 => struct StructMemberRenamedRetypedV2.S)',
|
|
95
|
+
members: undefined,
|
|
96
|
+
numberOfBytes: '32',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
't_struct(Inner)storage',
|
|
101
|
+
{
|
|
102
|
+
label: 'struct StructMemberRenamedRetypedV2.Inner',
|
|
103
|
+
members: [
|
|
104
|
+
{
|
|
105
|
+
label: 'g',
|
|
106
|
+
offset: 0,
|
|
107
|
+
renamedFrom: 'a',
|
|
108
|
+
retypedFrom: 'uint8',
|
|
109
|
+
slot: '0',
|
|
110
|
+
type: 't_bool',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
label: 'bb',
|
|
114
|
+
offset: 0,
|
|
115
|
+
renamedFrom: 'b',
|
|
116
|
+
slot: '1',
|
|
117
|
+
type: 't_bytes32',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
numberOfBytes: '64',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
't_struct(OtherFileV2)storage',
|
|
125
|
+
{
|
|
126
|
+
label: 'struct OtherFileV2',
|
|
127
|
+
members: [
|
|
128
|
+
{
|
|
129
|
+
label: 'a',
|
|
130
|
+
offset: 0,
|
|
131
|
+
retypedFrom: 'bytes32',
|
|
132
|
+
slot: '0',
|
|
133
|
+
type: 't_struct(TBytes32)storage',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
label: 'c',
|
|
137
|
+
offset: 0,
|
|
138
|
+
renamedFrom: 'b',
|
|
139
|
+
slot: '1',
|
|
140
|
+
type: 't_uint256',
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
numberOfBytes: '64',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
't_struct(Outer)storage',
|
|
148
|
+
{
|
|
149
|
+
label: 'struct StructMemberRenamedRetypedV2.Outer',
|
|
150
|
+
members: [
|
|
151
|
+
{
|
|
152
|
+
label: 'inner',
|
|
153
|
+
offset: 0,
|
|
154
|
+
slot: '0',
|
|
155
|
+
type: 't_struct(Inner)storage',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
label: 'n',
|
|
159
|
+
offset: 0,
|
|
160
|
+
slot: '2',
|
|
161
|
+
type: 't_uint256',
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
numberOfBytes: '96',
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
[
|
|
168
|
+
't_struct(S)storage',
|
|
169
|
+
{
|
|
170
|
+
label: 'struct StructMemberRenamedRetypedV2.S',
|
|
171
|
+
members: [
|
|
172
|
+
{
|
|
173
|
+
label: 'x',
|
|
174
|
+
offset: 0,
|
|
175
|
+
retypedFrom: 'bytes32',
|
|
176
|
+
slot: '0',
|
|
177
|
+
type: 't_struct(TBytes32)storage',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
label: 'z',
|
|
181
|
+
offset: 0,
|
|
182
|
+
renamedFrom: 'y',
|
|
183
|
+
slot: '1',
|
|
184
|
+
type: 't_uint256',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
label: 'f',
|
|
188
|
+
offset: 0,
|
|
189
|
+
retypedFrom: 'uint8',
|
|
190
|
+
slot: '2',
|
|
191
|
+
type: 't_bool',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
label: 'c',
|
|
195
|
+
offset: 0,
|
|
196
|
+
slot: '3',
|
|
197
|
+
type: 't_uint256',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
label: 'd',
|
|
201
|
+
offset: 0,
|
|
202
|
+
renamedFrom: 'oldD',
|
|
203
|
+
slot: '4',
|
|
204
|
+
type: 't_uint256',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
numberOfBytes: '160',
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
[
|
|
211
|
+
't_struct(TBytes32)storage',
|
|
212
|
+
{
|
|
213
|
+
label: 'struct TransientSlot.TBytes32',
|
|
214
|
+
members: [
|
|
215
|
+
{
|
|
216
|
+
label: '_placeholder',
|
|
217
|
+
offset: 0,
|
|
218
|
+
slot: '0',
|
|
219
|
+
type: 't_bytes32',
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
numberOfBytes: '32',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
[
|
|
226
|
+
't_uint256',
|
|
227
|
+
{
|
|
228
|
+
label: 'uint256',
|
|
229
|
+
members: undefined,
|
|
230
|
+
numberOfBytes: '32',
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
],
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
## annotated members without layout info
|
|
237
|
+
|
|
238
|
+
> Snapshot 1
|
|
239
|
+
|
|
240
|
+
`contracts/test/StructMemberRenamedRetyped.sol:62: Upgraded \`s\` to an incompatible type␊
|
|
241
|
+
- Bad upgrade from struct StructMemberRenamedRetypedV1.S to struct StructMemberRenamedRetypedV2.S␊
|
|
242
|
+
- In struct StructMemberRenamedRetypedV2.S␊
|
|
243
|
+
- Layout could have changed for \`x\` (bytes32 -> struct TransientSlot.TBytes32)␊
|
|
244
|
+
␊
|
|
245
|
+
contracts/test/StructMemberRenamedRetyped.sol:63: Upgraded \`m\` to an incompatible type␊
|
|
246
|
+
- In mapping(uint256 => struct StructMemberRenamedRetypedV2.S)␊
|
|
247
|
+
- Bad upgrade from struct StructMemberRenamedRetypedV1.S to struct StructMemberRenamedRetypedV2.S␊
|
|
248
|
+
- In struct StructMemberRenamedRetypedV2.S␊
|
|
249
|
+
- Layout could have changed for \`x\` (bytes32 -> struct TransientSlot.TBytes32)␊
|
|
250
|
+
␊
|
|
251
|
+
contracts/test/StructMemberRenamedRetyped.sol:64: Upgraded \`arr\` to an incompatible type␊
|
|
252
|
+
- In struct StructMemberRenamedRetypedV2.S[]␊
|
|
253
|
+
- Bad upgrade from struct StructMemberRenamedRetypedV1.S to struct StructMemberRenamedRetypedV2.S␊
|
|
254
|
+
- In struct StructMemberRenamedRetypedV2.S␊
|
|
255
|
+
- Layout could have changed for \`x\` (bytes32 -> struct TransientSlot.TBytes32)␊
|
|
256
|
+
␊
|
|
257
|
+
contracts/test/StructMemberRenamedRetyped.sol:66: Upgraded \`other\` to an incompatible type␊
|
|
258
|
+
- Bad upgrade from struct OtherFileV1 to struct OtherFileV2␊
|
|
259
|
+
- In struct OtherFileV2␊
|
|
260
|
+
- Layout could have changed for \`a\` (bytes32 -> struct TransientSlot.TBytes32)`
|
|
Binary file
|
package/src/utils/annotations.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { execall } from './execall';
|
|
2
2
|
import { Node } from 'solidity-ast/node';
|
|
3
3
|
|
|
4
|
+
// Matches one NatSpec tag: `@title:tag args`, where the args run until the next line that starts with a tag
|
|
5
|
+
export const ANNOTATION_REGEX = /^\s*(?:@(?<title>\w+)(?::(?<tag>[a-z][a-z-]*))? )?(?<args>(?:(?!^\s*@\w+)[^])*)/m;
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* Whether the given doc string has an annotation tag.
|
|
6
9
|
*/
|
|
@@ -18,10 +21,7 @@ export function hasAnnotationTag(doc: string, tag: string): boolean {
|
|
|
18
21
|
*/
|
|
19
22
|
export function getAnnotationArgs(doc: string, tag: string, supportedArgs?: readonly string[]) {
|
|
20
23
|
const result: string[] = [];
|
|
21
|
-
for (const { groups } of execall(
|
|
22
|
-
/^\s*(?:@(?<title>\w+)(?::(?<tag>[a-z][a-z-]*))? )?(?<args>(?:(?!^\s*@\w+)[^])*)/m,
|
|
23
|
-
doc,
|
|
24
|
-
)) {
|
|
24
|
+
for (const { groups } of execall(ANNOTATION_REGEX, doc)) {
|
|
25
25
|
if (groups && groups.title === 'custom' && groups.tag === tag) {
|
|
26
26
|
const trimmedArgs = groups.args.trim();
|
|
27
27
|
if (trimmedArgs.length > 0) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Expression, Literal } from 'solidity-ast';
|
|
2
|
+
import { calculateERC7201StorageLocation } from './erc7201';
|
|
3
|
+
import { normalizeUint256Literal } from './integer-literals';
|
|
4
|
+
import { UpgradesError } from '../error';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Recognizes the Solidity 0.8.35+ `erc7201(string)` comptime builtin, which evaluates the ERC-7201
|
|
8
|
+
* storage location for a namespace id and can be used in a compile-time context such as a `layout at`
|
|
9
|
+
* specifier (e.g. `contract C layout at erc7201("example.main")`).
|
|
10
|
+
*
|
|
11
|
+
* The builtin compiles to an ordinary `FunctionCall` node, so it is matched by its shape rather than
|
|
12
|
+
* by a dedicated AST node type.
|
|
13
|
+
*
|
|
14
|
+
* @param node The expression to check.
|
|
15
|
+
* @returns The namespace id passed to the builtin (without the `erc7201:` prefix), or undefined if the
|
|
16
|
+
* expression is not a call to the `erc7201` builtin with a single string literal argument.
|
|
17
|
+
*/
|
|
18
|
+
export function getErc7201BuiltinNamespaceId(node: Expression): string | undefined {
|
|
19
|
+
// Check that this is a function call to an identifier named `erc7201`. Return undefined for
|
|
20
|
+
// anything else so the caller can fall through to other base-slot forms.
|
|
21
|
+
if (node.nodeType !== 'FunctionCall' || node.kind !== 'functionCall') {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const callee = node.expression;
|
|
25
|
+
if (callee.nodeType !== 'Identifier' || callee.name !== 'erc7201') {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Beyond this point, the call is to `erc7201(...)`. Check its expected shape — one string-literal
|
|
30
|
+
// argument with no user-defined declaration shadowing the builtin — and throw if anything is off.
|
|
31
|
+
// Valid Solidity 0.8.35+ cannot produce these mismatches in a call to the builtin:
|
|
32
|
+
// - User-defined `erc7201` (`referencedDeclaration >= 0`): solc resolves the call to the builtin
|
|
33
|
+
// when both are in scope, so this shape doesn't reach a successful compile.
|
|
34
|
+
// - Wrong number of arguments: enforced by the builtin's signature.
|
|
35
|
+
// - Non-`Literal` argument or non-string literal `kind`: the builtin's signature requires a
|
|
36
|
+
// comptime-constant string literal. `hex"..."` is type `bytes` and wouldn't typecheck.
|
|
37
|
+
// We throw instead of returning undefined so a future solc emitting one of these surfaces loudly.
|
|
38
|
+
if (typeof callee.referencedDeclaration === 'number' && callee.referencedDeclaration >= 0) {
|
|
39
|
+
throw unexpectedErc7201BuiltinAst('callee resolves to a user-defined declaration');
|
|
40
|
+
}
|
|
41
|
+
if (node.arguments.length !== 1) {
|
|
42
|
+
throw unexpectedErc7201BuiltinAst(`expected 1 argument, got ${node.arguments.length}`);
|
|
43
|
+
}
|
|
44
|
+
const arg = node.arguments[0];
|
|
45
|
+
if (arg.nodeType !== 'Literal') {
|
|
46
|
+
throw unexpectedErc7201BuiltinAst(`argument is ${arg.nodeType}, expected Literal`);
|
|
47
|
+
}
|
|
48
|
+
if (arg.kind !== 'string' && arg.kind !== 'unicodeString') {
|
|
49
|
+
throw unexpectedErc7201BuiltinAst(`argument literal kind is ${arg.kind}, expected string or unicodeString`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return getStringLiteralValue(arg);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function unexpectedErc7201BuiltinAst(detail: string): UpgradesError {
|
|
56
|
+
return new UpgradesError(
|
|
57
|
+
`Unexpected AST for \`erc7201(...)\` builtin call: ${detail}`,
|
|
58
|
+
() =>
|
|
59
|
+
'Please report this at https://zpl.in/upgrades/report and include the `erc7201(...)` expression from your contract.',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolves an expression to its base slot as a normalized uint256 hex string, for either a numeric
|
|
65
|
+
* literal or an `erc7201(...)` builtin call.
|
|
66
|
+
*
|
|
67
|
+
* @param node The expression to resolve.
|
|
68
|
+
* @returns The normalized base slot hex string, or undefined if the expression is neither a numeric
|
|
69
|
+
* literal nor a resolvable `erc7201(...)` builtin call.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveBaseSlot(node: Expression): string | undefined {
|
|
72
|
+
if (node.nodeType === 'Literal') {
|
|
73
|
+
return normalizeUint256Literal(node.value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const id = getErc7201BuiltinNamespaceId(node);
|
|
77
|
+
if (id !== undefined) {
|
|
78
|
+
return calculateERC7201StorageLocation(id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getStringLiteralValue(literal: Literal): string {
|
|
85
|
+
// solc populates `value` for `kind: 'string'`/`'unicodeString'` literals; throw defensively if
|
|
86
|
+
// a future solc ever produces a null/missing `value` for such a literal.
|
|
87
|
+
if (literal.value === undefined || literal.value === null) {
|
|
88
|
+
throw unexpectedErc7201BuiltinAst('string literal `value` is null or missing');
|
|
89
|
+
}
|
|
90
|
+
return literal.value;
|
|
91
|
+
}
|
|
@@ -8,3 +8,15 @@ test('calculateERC7201StorageLocation', t => {
|
|
|
8
8
|
'0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500',
|
|
9
9
|
);
|
|
10
10
|
});
|
|
11
|
+
|
|
12
|
+
// `keccak256("ns2") - 1` is 63 hex chars (odd-length), which must be zero-padded to 32 bytes
|
|
13
|
+
// before hashing. Regression test for truncation of the intermediate buffer.
|
|
14
|
+
test('calculateERC7201StorageLocation - odd-length intermediate hash', t => {
|
|
15
|
+
t.is(calculateERC7201StorageLocation('ns2'), '0xa61069020885c09e922542fb3fa98c8d50e6481fc1251ff86c3319a70f0f7d00');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// `keccak256("ns297") - 1` is 62 hex chars (i.e., its top byte is `0x00`). Without zero-padding,
|
|
19
|
+
// `Buffer.from('hex')` would interpret it as a 31-byte buffer rather than 32, producing the wrong hash.
|
|
20
|
+
test('calculateERC7201StorageLocation - leading-zero-byte intermediate hash', t => {
|
|
21
|
+
t.is(calculateERC7201StorageLocation('ns297'), '0xe06f9faa11c8032ea604e1764972c1a5594987f1678d8fabf921c28d1a7a4c00');
|
|
22
|
+
});
|