@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
|
@@ -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`
|
|
Binary file
|
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,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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
}
|
package/src/storage/namespace.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
});
|
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,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
|
+
}
|