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