@openzeppelin/upgrades-core 1.41.0 → 1.42.1
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.js +3 -1
- package/dist/cli/validate.js.map +1 -1
- package/dist/validate/overrides.d.ts +6 -2
- package/dist/validate/overrides.d.ts.map +1 -1
- package/dist/validate/overrides.js +36 -10
- package/dist/validate/overrides.js.map +1 -1
- package/dist/validate/report.d.ts +1 -0
- package/dist/validate/report.d.ts.map +1 -1
- package/dist/validate/report.js +23 -1
- package/dist/validate/report.js.map +1 -1
- package/dist/validate/run/initializer.d.ts +13 -0
- package/dist/validate/run/initializer.d.ts.map +1 -0
- package/dist/validate/run/initializer.js +246 -0
- package/dist/validate/run/initializer.js.map +1 -0
- package/dist/validate/run.d.ts +23 -2
- package/dist/validate/run.d.ts.map +1 -1
- package/dist/validate/run.js +9 -1
- package/dist/validate/run.js.map +1 -1
- package/dist/validate-initializers.test.d.ts +2 -0
- package/dist/validate-initializers.test.d.ts.map +1 -0
- package/dist/validate-initializers.test.js +181 -0
- package/dist/validate-initializers.test.js.map +1 -0
- package/package.json +2 -2
- package/src/cli/cli.test.ts.md +6 -2
- package/src/cli/cli.test.ts.snap +0 -0
- package/src/cli/validate.ts +4 -4
- package/src/validate/overrides.ts +40 -15
- package/src/validate/report.ts +24 -2
- package/src/validate/run/initializer.ts +316 -0
- package/src/validate/run.ts +38 -2
- package/src/validate-initializers.test.ts +224 -0
- package/CHANGELOG.md +0 -421
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import type { ContractDefinition, FunctionDefinition } from 'solidity-ast';
|
|
2
|
+
import { ASTDereferencer, findAll } from 'solidity-ast/utils';
|
|
3
|
+
import { SrcDecoder } from '../../src-decoder';
|
|
4
|
+
import { ValidationExceptionInitializer, skipCheck } from '../run';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reports if this contract is non-abstract and any of the following are true:
|
|
8
|
+
* - 1. Missing initializer: This contract does not appear to have an initializer, but parent contracts require initialization.
|
|
9
|
+
* - 2. Missing initializer call: This contract's initializer is missing a call to a parent initializer.
|
|
10
|
+
* - 3. Duplicate initializer call: This contract has duplicate calls to the same parent initializer function.
|
|
11
|
+
* - 4. Incorrect initializer order (warning): This contract does not call parent initializers in the correct order.
|
|
12
|
+
*/
|
|
13
|
+
export function* getInitializerExceptions(
|
|
14
|
+
contractDef: ContractDefinition,
|
|
15
|
+
deref: ASTDereferencer,
|
|
16
|
+
decodeSrc: SrcDecoder,
|
|
17
|
+
): Generator<ValidationExceptionInitializer> {
|
|
18
|
+
if (contractDef.abstract) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const linearizedParentContracts = getLinearizedParentContracts(contractDef, deref);
|
|
23
|
+
const parentNameToInitializersMap = getParentNameToInitializersMap(linearizedParentContracts);
|
|
24
|
+
|
|
25
|
+
const remainingParents = getParentsNotInitializedByOtherParents(parentNameToInitializersMap);
|
|
26
|
+
|
|
27
|
+
if (remainingParents.length > 0) {
|
|
28
|
+
const contractInitializers = getPossibleInitializers(contractDef, false);
|
|
29
|
+
|
|
30
|
+
// Report if there is no initializer but parents need initialization
|
|
31
|
+
if (
|
|
32
|
+
checkNeedsInitialization(remainingParents, parentNameToInitializersMap) &&
|
|
33
|
+
contractInitializers.length === 0 &&
|
|
34
|
+
!skipCheck('missing-initializer', contractDef)
|
|
35
|
+
) {
|
|
36
|
+
yield {
|
|
37
|
+
kind: 'missing-initializer',
|
|
38
|
+
src: decodeSrc(contractDef),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Linearized list of parent contracts that have initializers
|
|
43
|
+
const expectedLinearized: string[] = [...parentNameToInitializersMap.keys()];
|
|
44
|
+
|
|
45
|
+
// The parent contracts that need to be directly initialized by this contract, which excludes parents that are initialized by other parents.
|
|
46
|
+
// Calling these will initialize all parent contracts so that the entire state is initialized in one transaction.
|
|
47
|
+
const expectedDirectCalls = remainingParents;
|
|
48
|
+
|
|
49
|
+
for (const contractInitializer of contractInitializers) {
|
|
50
|
+
yield* getInitializerCallExceptions(
|
|
51
|
+
contractInitializer,
|
|
52
|
+
expectedLinearized,
|
|
53
|
+
expectedDirectCalls,
|
|
54
|
+
parentNameToInitializersMap,
|
|
55
|
+
contractDef,
|
|
56
|
+
deref,
|
|
57
|
+
decodeSrc,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getLinearizedParentContracts(contractDef: ContractDefinition, deref: ASTDereferencer) {
|
|
64
|
+
const parents = contractDef.linearizedBaseContracts.map(base => deref('ContractDefinition', base));
|
|
65
|
+
parents.reverse(); // use most derived first
|
|
66
|
+
parents.pop(); // remove self
|
|
67
|
+
return parents;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Gets a map of parent contract names to their possible initializers.
|
|
72
|
+
* If a parent contract has no initializers, it is not included in the map.
|
|
73
|
+
*/
|
|
74
|
+
function getParentNameToInitializersMap(linearizedParentContracts: ContractDefinition[]) {
|
|
75
|
+
const map = new Map();
|
|
76
|
+
for (const parent of linearizedParentContracts) {
|
|
77
|
+
const initializers = getPossibleInitializers(parent, true);
|
|
78
|
+
if (initializers.length > 0) {
|
|
79
|
+
map.set(parent.name, initializers);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return map;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns true if this contract must have its own initializer to call parent initializers.
|
|
87
|
+
*
|
|
88
|
+
* If there are multiple parents with initializers, regardless of whether they are internal or public,
|
|
89
|
+
* this contract must have its own initializer to call them so that the state is initialized in one transaction.
|
|
90
|
+
*
|
|
91
|
+
* Otherwise, if there is only one parent with initializers, they only need to be called if they are internal, since public initializers can be called directly.
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
function checkNeedsInitialization(
|
|
95
|
+
remainingParents: string[],
|
|
96
|
+
parentNameToInitializersMap: Map<string, FunctionDefinition[]>,
|
|
97
|
+
) {
|
|
98
|
+
if (remainingParents.length > 1) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
const [parent] = remainingParents;
|
|
102
|
+
const parentInitializers = parentNameToInitializersMap.get(parent)!;
|
|
103
|
+
return parentInitializers.every(init => init.visibility === 'internal');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns parent contract names that are not initialized by other parent contracts,
|
|
108
|
+
* keeping the same order as they appear in the original linearization.
|
|
109
|
+
*/
|
|
110
|
+
function getParentsNotInitializedByOtherParents(
|
|
111
|
+
parentNameToInitializersMap: Map<string, FunctionDefinition[]>,
|
|
112
|
+
): string[] {
|
|
113
|
+
const remainingParents = [...parentNameToInitializersMap.keys()];
|
|
114
|
+
|
|
115
|
+
for (const parent of remainingParents) {
|
|
116
|
+
const parentInitializers = parentNameToInitializersMap.get(parent)!;
|
|
117
|
+
for (const initializer of parentInitializers) {
|
|
118
|
+
const expressionStatements =
|
|
119
|
+
initializer.body?.statements?.filter(stmt => stmt.nodeType === 'ExpressionStatement') ?? [];
|
|
120
|
+
for (const stmt of expressionStatements) {
|
|
121
|
+
const fnCall = stmt.expression;
|
|
122
|
+
if (
|
|
123
|
+
fnCall.nodeType === 'FunctionCall' &&
|
|
124
|
+
(fnCall.expression.nodeType === 'Identifier' || fnCall.expression.nodeType === 'MemberAccess')
|
|
125
|
+
) {
|
|
126
|
+
const referencedFn = fnCall.expression.referencedDeclaration;
|
|
127
|
+
if (referencedFn && referencedFn > 0) {
|
|
128
|
+
const earlierParents = remainingParents.slice(0, remainingParents.indexOf(parent));
|
|
129
|
+
const callsEarlierParentInitializer = earlierParents.find(base =>
|
|
130
|
+
parentNameToInitializersMap.get(base)!.some(init => init.id === referencedFn),
|
|
131
|
+
);
|
|
132
|
+
if (callsEarlierParentInitializer) {
|
|
133
|
+
remainingParents.splice(remainingParents.indexOf(callsEarlierParentInitializer), 1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return remainingParents;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Reports exceptions for missing initializer calls, duplicate initializer calls, and incorrect initializer order.
|
|
146
|
+
*
|
|
147
|
+
* @param contractInitializer An initializer function for the current contract
|
|
148
|
+
* @param expectedLinearized Linearized list of parent contracts that have initializers
|
|
149
|
+
* @param expectedDirectCalls The parent contracts that need to be directly initialized by this contract
|
|
150
|
+
* @param parentNameToInitializersMap Map of parent contract names to their possible initializers
|
|
151
|
+
* @param initializersCalledByParents List of parent initializers that have already been called by other parents
|
|
152
|
+
* @param contractDef The current contract
|
|
153
|
+
* @param deref AST dereferencer
|
|
154
|
+
* @param decodeSrc Source decoder
|
|
155
|
+
*/
|
|
156
|
+
function* getInitializerCallExceptions(
|
|
157
|
+
contractInitializer: FunctionDefinition,
|
|
158
|
+
expectedLinearized: string[],
|
|
159
|
+
expectedDirectCalls: string[],
|
|
160
|
+
parentNameToInitializersMap: Map<string, FunctionDefinition[]>,
|
|
161
|
+
contractDef: ContractDefinition,
|
|
162
|
+
deref: ASTDereferencer,
|
|
163
|
+
decodeSrc: SrcDecoder,
|
|
164
|
+
): Generator<ValidationExceptionInitializer> {
|
|
165
|
+
const foundParents: string[] = [];
|
|
166
|
+
const remainingLinearized: string[] = [...expectedLinearized];
|
|
167
|
+
const remainingDirectCalls: string[] = [...expectedDirectCalls];
|
|
168
|
+
const calledInitializerIds: number[] = [];
|
|
169
|
+
|
|
170
|
+
const expressionStatements =
|
|
171
|
+
contractInitializer.body?.statements?.filter(stmt => stmt.nodeType === 'ExpressionStatement') ?? [];
|
|
172
|
+
for (const stmt of expressionStatements) {
|
|
173
|
+
const fnCall = stmt.expression;
|
|
174
|
+
if (
|
|
175
|
+
fnCall.nodeType === 'FunctionCall' &&
|
|
176
|
+
(fnCall.expression.nodeType === 'Identifier' || fnCall.expression.nodeType === 'MemberAccess')
|
|
177
|
+
) {
|
|
178
|
+
let recursiveFunctionIds: number[] = [];
|
|
179
|
+
const referencedFn = fnCall.expression.referencedDeclaration;
|
|
180
|
+
if (referencedFn && referencedFn > 0) {
|
|
181
|
+
recursiveFunctionIds = getRecursiveFunctionIds(referencedFn, deref);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// For each recursively called function, if it is a parent initializer, then:
|
|
185
|
+
// - Check if it was already called (duplicate call)
|
|
186
|
+
// - Otherwise, check if the parent initializer is called in linearized order
|
|
187
|
+
for (const calledFn of recursiveFunctionIds) {
|
|
188
|
+
for (const parent of parentNameToInitializersMap.keys()) {
|
|
189
|
+
const parentInitializers = parentNameToInitializersMap.get(parent)!;
|
|
190
|
+
const callsParentInitializer = parentInitializers.find(init => init.id === calledFn);
|
|
191
|
+
|
|
192
|
+
if (calledFn && callsParentInitializer) {
|
|
193
|
+
const duplicate = calledInitializerIds.includes(calledFn);
|
|
194
|
+
if (
|
|
195
|
+
duplicate &&
|
|
196
|
+
!skipCheck('duplicate-initializer-call', contractDef) &&
|
|
197
|
+
!skipCheck('duplicate-initializer-call', contractInitializer) &&
|
|
198
|
+
!skipCheck('duplicate-initializer-call', stmt)
|
|
199
|
+
) {
|
|
200
|
+
yield {
|
|
201
|
+
kind: 'duplicate-initializer-call',
|
|
202
|
+
src: decodeSrc(fnCall),
|
|
203
|
+
parentInitializer: callsParentInitializer.name,
|
|
204
|
+
parentContract: parent,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
calledInitializerIds.push(calledFn);
|
|
208
|
+
|
|
209
|
+
// Omit multiple initializer calls of the same parent via different functions e.g. `__X_init` which calls `__X_init_unchained`
|
|
210
|
+
if (!foundParents.includes(parent)) {
|
|
211
|
+
foundParents.push(parent);
|
|
212
|
+
|
|
213
|
+
const indexLinearized = remainingLinearized.indexOf(parent);
|
|
214
|
+
if (
|
|
215
|
+
// Omit duplicate calls to avoid treating them as out of order. Duplicates are either reported above or they were skipped.
|
|
216
|
+
!duplicate &&
|
|
217
|
+
// If the parent is not the next expected linearized parent, report it as out of order
|
|
218
|
+
indexLinearized !== 0 &&
|
|
219
|
+
!skipCheck('incorrect-initializer-order', contractDef) &&
|
|
220
|
+
!skipCheck('incorrect-initializer-order', contractInitializer)
|
|
221
|
+
) {
|
|
222
|
+
yield {
|
|
223
|
+
kind: 'incorrect-initializer-order',
|
|
224
|
+
src: decodeSrc(fnCall),
|
|
225
|
+
expectedLinearization: expectedDirectCalls,
|
|
226
|
+
foundOrder: foundParents,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (indexLinearized !== -1) {
|
|
230
|
+
remainingLinearized.splice(indexLinearized, 1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const indexDirect = remainingDirectCalls.indexOf(parent);
|
|
234
|
+
if (indexDirect !== -1) {
|
|
235
|
+
remainingDirectCalls.splice(indexDirect, 1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Report any remaining parents that were not directly initialized
|
|
245
|
+
if (
|
|
246
|
+
remainingDirectCalls.length > 0 &&
|
|
247
|
+
!skipCheck('missing-initializer-call', contractDef) &&
|
|
248
|
+
!skipCheck('missing-initializer-call', contractInitializer)
|
|
249
|
+
) {
|
|
250
|
+
yield {
|
|
251
|
+
kind: 'missing-initializer-call',
|
|
252
|
+
src: decodeSrc(contractInitializer),
|
|
253
|
+
parentContracts: remainingDirectCalls,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Gets the IDs of all functions that are recursively called by the given function, including the given function itself at the end of the list.
|
|
260
|
+
*
|
|
261
|
+
* @param referencedFn The ID of the function to start from
|
|
262
|
+
* @param deref AST dereferencer
|
|
263
|
+
* @param visited Set of function IDs that have already been visited
|
|
264
|
+
* @returns The IDs of all functions that are recursively called by the given function, including the given function itself at the end of the list.
|
|
265
|
+
*/
|
|
266
|
+
function getRecursiveFunctionIds(referencedFn: number, deref: ASTDereferencer, visited?: Set<number>): number[] {
|
|
267
|
+
const result: number[] = [];
|
|
268
|
+
|
|
269
|
+
if (visited === undefined) {
|
|
270
|
+
visited = new Set();
|
|
271
|
+
}
|
|
272
|
+
if (visited.has(referencedFn)) {
|
|
273
|
+
return result;
|
|
274
|
+
} else {
|
|
275
|
+
visited.add(referencedFn);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const fn = deref('FunctionDefinition', referencedFn);
|
|
279
|
+
const expressionStatements = fn.body?.statements?.filter(stmt => stmt.nodeType === 'ExpressionStatement') ?? [];
|
|
280
|
+
for (const stmt of expressionStatements) {
|
|
281
|
+
const fnCall = stmt.expression;
|
|
282
|
+
if (
|
|
283
|
+
fnCall.nodeType === 'FunctionCall' &&
|
|
284
|
+
(fnCall.expression.nodeType === 'Identifier' || fnCall.expression.nodeType === 'MemberAccess')
|
|
285
|
+
) {
|
|
286
|
+
const referencedId = fnCall.expression.referencedDeclaration;
|
|
287
|
+
if (referencedId && referencedId > 0) {
|
|
288
|
+
result.push(...getRecursiveFunctionIds(referencedId, deref, visited));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
result.push(referencedFn);
|
|
293
|
+
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get all functions that could be initializers. Does not include private functions.
|
|
299
|
+
* For parent contracts, only internal and public functions which contain statements are included.
|
|
300
|
+
*/
|
|
301
|
+
function getPossibleInitializers(contractDef: ContractDefinition, isParentContract: boolean) {
|
|
302
|
+
const fns = [...findAll('FunctionDefinition', contractDef)];
|
|
303
|
+
return fns.filter(
|
|
304
|
+
fnDef =>
|
|
305
|
+
(fnDef.modifiers.some(modifier => ['initializer', 'onlyInitializing'].includes(modifier.modifierName.name)) ||
|
|
306
|
+
['initialize', 'initializer'].includes(fnDef.name)) &&
|
|
307
|
+
// Skip virtual functions without a body, since that indicates an abstract function and is not itself an initializer
|
|
308
|
+
!(fnDef.virtual && !fnDef.body) &&
|
|
309
|
+
// Ignore private functions, since they cannot be called outside the contract
|
|
310
|
+
fnDef.visibility !== 'private' &&
|
|
311
|
+
// For parent contracts, only internal and public functions which contain statements need to be called
|
|
312
|
+
(isParentContract
|
|
313
|
+
? fnDef.body?.statements?.length && (fnDef.visibility === 'internal' || fnDef.visibility === 'public')
|
|
314
|
+
: true),
|
|
315
|
+
);
|
|
316
|
+
}
|
package/src/validate/run.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { getStorageLocationAnnotation, isNamespaceSupported } from '../storage/n
|
|
|
24
24
|
import { UpgradesError } from '../error';
|
|
25
25
|
import { assertUnreachable } from '../utils/assert';
|
|
26
26
|
import { logWarning } from '../utils/log';
|
|
27
|
+
import { getInitializerExceptions } from './run/initializer';
|
|
27
28
|
|
|
28
29
|
export type ValidationRunData = Record<string, ContractValidation>;
|
|
29
30
|
|
|
@@ -50,13 +51,20 @@ export const errorKinds = [
|
|
|
50
51
|
'selfdestruct',
|
|
51
52
|
'missing-public-upgradeto',
|
|
52
53
|
'internal-function-storage',
|
|
54
|
+
'missing-initializer',
|
|
55
|
+
'missing-initializer-call',
|
|
56
|
+
'duplicate-initializer-call',
|
|
57
|
+
'incorrect-initializer-order',
|
|
53
58
|
] as const;
|
|
54
59
|
|
|
60
|
+
export const convertToWarnings: (typeof errorKinds)[number][] = ['incorrect-initializer-order'] as const;
|
|
61
|
+
|
|
55
62
|
export type ValidationError =
|
|
56
63
|
| ValidationErrorConstructor
|
|
57
64
|
| ValidationErrorOpcode
|
|
58
65
|
| ValidationErrorWithName
|
|
59
|
-
| ValidationErrorUpgradeability
|
|
66
|
+
| ValidationErrorUpgradeability
|
|
67
|
+
| ValidationExceptionInitializer;
|
|
60
68
|
|
|
61
69
|
interface ValidationErrorBase {
|
|
62
70
|
src: string;
|
|
@@ -74,6 +82,33 @@ interface ValidationErrorWithName extends ValidationErrorBase {
|
|
|
74
82
|
| 'internal-function-storage';
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
interface ValidationErrorMissingInitializer extends ValidationErrorBase {
|
|
86
|
+
kind: 'missing-initializer';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface ValidationErrorMissingInitializerCall extends ValidationErrorBase {
|
|
90
|
+
kind: 'missing-initializer-call';
|
|
91
|
+
parentContracts: string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface ValidationErrorDuplicateInitializerCall extends ValidationErrorBase {
|
|
95
|
+
kind: 'duplicate-initializer-call';
|
|
96
|
+
parentInitializer: string;
|
|
97
|
+
parentContract: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface ValidationWarningIncorrectInitializerOrder extends ValidationErrorBase {
|
|
101
|
+
kind: 'incorrect-initializer-order';
|
|
102
|
+
expectedLinearization: string[];
|
|
103
|
+
foundOrder: string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type ValidationExceptionInitializer =
|
|
107
|
+
| ValidationErrorMissingInitializer
|
|
108
|
+
| ValidationErrorMissingInitializerCall
|
|
109
|
+
| ValidationErrorDuplicateInitializerCall
|
|
110
|
+
| ValidationWarningIncorrectInitializerOrder;
|
|
111
|
+
|
|
77
112
|
interface ValidationErrorConstructor extends ValidationErrorBase {
|
|
78
113
|
kind: 'constructor';
|
|
79
114
|
contract: string;
|
|
@@ -125,7 +160,7 @@ function skipCheckReachable(error: string, node: Node): boolean {
|
|
|
125
160
|
return getAllowed(node, true).includes(error);
|
|
126
161
|
}
|
|
127
162
|
|
|
128
|
-
function skipCheck(error:
|
|
163
|
+
export function skipCheck(error: ValidationError['kind'], node: Node): boolean {
|
|
129
164
|
// skip both allow and allow-reachable errors in the lexical scope
|
|
130
165
|
return getAllowed(node, false).includes(error) || getAllowed(node, true).includes(error);
|
|
131
166
|
}
|
|
@@ -209,6 +244,7 @@ export function validate(
|
|
|
209
244
|
// https://github.com/OpenZeppelin/openzeppelin-upgrades/issues/52
|
|
210
245
|
...getLinkingErrors(contractDef, bytecode),
|
|
211
246
|
...getInternalFunctionStorageErrors(contractDef, deref, decodeSrc),
|
|
247
|
+
...getInitializerExceptions(contractDef, deref, decodeSrc),
|
|
212
248
|
];
|
|
213
249
|
|
|
214
250
|
validation[key].layout = extractStorageLayout(
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import _test, { TestFn } from 'ava';
|
|
2
|
+
import { artifacts } from 'hardhat';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
validate,
|
|
6
|
+
getContractVersion,
|
|
7
|
+
assertUpgradeSafe,
|
|
8
|
+
ValidationOptions,
|
|
9
|
+
RunValidation,
|
|
10
|
+
ValidationErrors,
|
|
11
|
+
} from './validate';
|
|
12
|
+
import { solcInputOutputDecoder } from './src-decoder';
|
|
13
|
+
|
|
14
|
+
interface Context {
|
|
15
|
+
validation: RunValidation;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const test = _test as TestFn<Context>;
|
|
19
|
+
|
|
20
|
+
test.before(async t => {
|
|
21
|
+
const contracts = ['contracts/test/ValidationsInitializer.sol:Parent_NoInitializer'];
|
|
22
|
+
|
|
23
|
+
t.context.validation = {} as RunValidation;
|
|
24
|
+
for (const contract of contracts) {
|
|
25
|
+
const buildInfo = await artifacts.getBuildInfo(contract);
|
|
26
|
+
if (buildInfo === undefined) {
|
|
27
|
+
throw new Error(`Build info not found for contract ${contract}`);
|
|
28
|
+
}
|
|
29
|
+
const solcOutput = buildInfo.output;
|
|
30
|
+
const solcInput = buildInfo.input;
|
|
31
|
+
const decodeSrc = solcInputOutputDecoder(solcInput, solcOutput);
|
|
32
|
+
Object.assign(t.context.validation, validate(solcOutput, decodeSrc));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function testAccepts(name: string, kind: ValidationOptions['kind']) {
|
|
37
|
+
testOverride(name, kind, {}, undefined);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ExpectedErrors {
|
|
41
|
+
contains: string[];
|
|
42
|
+
count: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function testRejects(name: string, kind: ValidationOptions['kind'], expectedErrors?: ExpectedErrors) {
|
|
46
|
+
testOverride(name, kind, {}, expectedErrors);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function testOverride(
|
|
50
|
+
name: string,
|
|
51
|
+
kind: ValidationOptions['kind'],
|
|
52
|
+
opts: ValidationOptions,
|
|
53
|
+
expectedErrors?: ExpectedErrors,
|
|
54
|
+
) {
|
|
55
|
+
const expectValid = expectedErrors === undefined;
|
|
56
|
+
|
|
57
|
+
const optKeys = Object.keys(opts);
|
|
58
|
+
const describeOpts = optKeys.length > 0 ? '(' + optKeys.join(', ') + ')' : '';
|
|
59
|
+
const testName = [expectValid ? 'accepts' : 'rejects', kind, name, describeOpts].join(' ');
|
|
60
|
+
test(testName, t => {
|
|
61
|
+
const version = getContractVersion(t.context.validation, name);
|
|
62
|
+
const assertUpgSafe = () => assertUpgradeSafe([t.context.validation], version, { kind, ...opts });
|
|
63
|
+
if (expectValid) {
|
|
64
|
+
t.notThrows(assertUpgSafe);
|
|
65
|
+
} else {
|
|
66
|
+
const error = t.throws(assertUpgSafe) as ValidationErrors;
|
|
67
|
+
t.true(
|
|
68
|
+
error.errors.length === expectedErrors.count,
|
|
69
|
+
`Expected ${expectedErrors.count} errors, got ${error.errors.length}:\n${error.message}`,
|
|
70
|
+
);
|
|
71
|
+
t.true(
|
|
72
|
+
expectedErrors?.contains.every(c => error.message.includes(c)),
|
|
73
|
+
error.message,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
testAccepts('Child_Of_NoInitializer_Ok', 'transparent');
|
|
80
|
+
|
|
81
|
+
testAccepts('Child_Of_InitializerModifier_Ok', 'transparent');
|
|
82
|
+
testRejects('Child_Of_InitializerModifier_Bad', 'transparent', {
|
|
83
|
+
contains: ['Missing initializer calls for one or more parent contracts: `Parent_InitializerModifier`'],
|
|
84
|
+
count: 1,
|
|
85
|
+
});
|
|
86
|
+
testAccepts('Child_Of_InitializerModifier_UsesSuper_Ok', 'transparent');
|
|
87
|
+
|
|
88
|
+
testAccepts('Child_Of_OnlyInitializingModifier_Ok', 'transparent');
|
|
89
|
+
testRejects('Child_Of_OnlyInitializingModifier_Bad', 'transparent', {
|
|
90
|
+
contains: ['Missing initializer calls for one or more parent contracts: `Parent__OnlyInitializingModifier`'],
|
|
91
|
+
count: 1,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
testRejects('MissingInitializer_Bad', 'transparent', {
|
|
95
|
+
contains: ['Missing initializer'],
|
|
96
|
+
count: 1,
|
|
97
|
+
});
|
|
98
|
+
testAccepts('MissingInitializer_UnsafeAllow_Contract', 'transparent');
|
|
99
|
+
testOverride('MissingInitializer_Bad', 'transparent', { unsafeAllow: ['missing-initializer'] });
|
|
100
|
+
|
|
101
|
+
testAccepts('InitializationOrder_Ok', 'transparent');
|
|
102
|
+
testAccepts('InitializationOrder_Ok_2', 'transparent');
|
|
103
|
+
|
|
104
|
+
testAccepts('InitializationOrder_WrongOrder_Bad', 'transparent'); // warn 'Expected initializers to be called for parent contracts in the following order: A, B, C'
|
|
105
|
+
testAccepts('InitializationOrder_WrongOrder_UnsafeAllow_Contract', 'transparent');
|
|
106
|
+
testAccepts('InitializationOrder_WrongOrder_UnsafeAllow_Function', 'transparent');
|
|
107
|
+
testOverride('InitializationOrder_WrongOrder_Bad', 'transparent', { unsafeAllow: ['incorrect-initializer-order'] }); // skips the warning
|
|
108
|
+
|
|
109
|
+
testRejects('InitializationOrder_MissingCall_Bad', 'transparent', {
|
|
110
|
+
contains: ['Missing initializer calls for one or more parent contracts: `C`'],
|
|
111
|
+
count: 1,
|
|
112
|
+
});
|
|
113
|
+
testAccepts('InitializationOrder_MissingCall_UnsafeAllow_Contract', 'transparent');
|
|
114
|
+
testAccepts('InitializationOrder_MissingCall_UnsafeAllow_Function', 'transparent');
|
|
115
|
+
testOverride('InitializationOrder_MissingCall_Bad', 'transparent', { unsafeAllow: ['missing-initializer-call'] });
|
|
116
|
+
|
|
117
|
+
testRejects('InitializationOrder_Duplicate_Bad', 'transparent', {
|
|
118
|
+
contains: ['Duplicate calls found to initializer `__B_init` for contract `B`'],
|
|
119
|
+
count: 1,
|
|
120
|
+
});
|
|
121
|
+
testAccepts('InitializationOrder_Duplicate_UnsafeAllow_Contract', 'transparent');
|
|
122
|
+
testAccepts('InitializationOrder_Duplicate_UnsafeAllow_Function', 'transparent');
|
|
123
|
+
testAccepts('InitializationOrder_Duplicate_UnsafeAllow_Call', 'transparent');
|
|
124
|
+
testOverride('InitializationOrder_Duplicate_Bad', 'transparent', { unsafeAllow: ['duplicate-initializer-call'] });
|
|
125
|
+
testAccepts('InitializationOrder_UnsafeAllowDuplicate_But_WrongOrder', 'transparent'); // warn 'Expected initializers to be called for parent contracts in the following order: A, B, C'
|
|
126
|
+
|
|
127
|
+
testAccepts('WithRequire_Ok', 'transparent');
|
|
128
|
+
testRejects('WithRequire_Bad', 'transparent', {
|
|
129
|
+
contains: ['Missing initializer calls for one or more parent contracts: `Parent__OnlyInitializingModifier`'],
|
|
130
|
+
count: 1,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
testAccepts('Child_Of_Private_Ok', 'transparent');
|
|
134
|
+
testAccepts('Child_Of_Public_Ok', 'transparent');
|
|
135
|
+
testRejects('Child_Of_Public_MissingCall_Bad', 'transparent', {
|
|
136
|
+
contains: ['Missing initializer calls for one or more parent contracts: `Parent_Public`'],
|
|
137
|
+
count: 1,
|
|
138
|
+
});
|
|
139
|
+
testAccepts('Child_Of_External_Ok', 'transparent');
|
|
140
|
+
testRejects('Child_Of_Internal_Bad', 'transparent', {
|
|
141
|
+
contains: ['Missing initializer'],
|
|
142
|
+
count: 1,
|
|
143
|
+
});
|
|
144
|
+
testRejects('Child_Of_Internal_Has_Private_Bad', 'transparent', {
|
|
145
|
+
contains: ['Missing initializer'],
|
|
146
|
+
count: 1,
|
|
147
|
+
});
|
|
148
|
+
testAccepts('Child_Of_Internal_Has_Public_Ok', 'transparent');
|
|
149
|
+
testAccepts('Child_Of_Internal_Has_Internal_Ok', 'transparent');
|
|
150
|
+
testAccepts('Child_Of_Internal_Has_External_Ok', 'transparent');
|
|
151
|
+
testAccepts('Child_Of_PrivatePublicExternal_Ok', 'transparent');
|
|
152
|
+
testRejects('Child_Of_AllVisibilities_Bad', 'transparent', {
|
|
153
|
+
contains: ['Missing initializer'],
|
|
154
|
+
count: 1,
|
|
155
|
+
});
|
|
156
|
+
testRejects('Child_Of_AllVisibilities_EmptyInitializer_Bad', 'transparent', {
|
|
157
|
+
contains: ['Missing initializer calls for one or more parent contracts: `Parent_Public, Parent_Internal`'],
|
|
158
|
+
count: 1,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
testRejects('Child_Of_MultiplePublic_MissingInitializer_Bad', 'transparent', {
|
|
162
|
+
contains: ['Missing initializer'],
|
|
163
|
+
count: 1,
|
|
164
|
+
});
|
|
165
|
+
testRejects('Child_Of_MultiplePublic_MissingCall_Bad', 'transparent', {
|
|
166
|
+
contains: ['Missing initializer calls for one or more parent contracts: `Parent_Public_2`'],
|
|
167
|
+
count: 1,
|
|
168
|
+
});
|
|
169
|
+
testAccepts('Child_Of_MultiplePublic_Ok', 'transparent');
|
|
170
|
+
|
|
171
|
+
testAccepts('TransitiveParent_Ok', 'transparent');
|
|
172
|
+
testRejects('TransitiveParent_Bad', 'transparent', {
|
|
173
|
+
contains: ['Missing initializer calls for one or more parent contracts: `TransitiveGrandparent2`'],
|
|
174
|
+
count: 1,
|
|
175
|
+
});
|
|
176
|
+
testRejects('TransitiveChild_Bad_Parent', 'transparent', {
|
|
177
|
+
contains: [
|
|
178
|
+
'Missing initializer calls for one or more parent contracts: `TransitiveGrandparent2`', // occurs twice: missing initializer for child, missing initializer for parent
|
|
179
|
+
],
|
|
180
|
+
count: 2,
|
|
181
|
+
}); // warn 'Expected initializers to be called for parent contracts in the following order: TransitiveGrandparent2, TransitiveParent_Bad'
|
|
182
|
+
testRejects('TransitiveChild_Bad_Order', 'transparent', {
|
|
183
|
+
contains: ['Missing initializer calls for one or more parent contracts: `TransitiveGrandparent2`'],
|
|
184
|
+
count: 1,
|
|
185
|
+
}); // warn 'Expected initializers to be called for parent contracts in the following order: TransitiveGrandparent2, TransitiveParent_Bad'
|
|
186
|
+
testRejects('TransitiveChild_Good_Order_Bad_Parent', 'transparent', {
|
|
187
|
+
contains: ['Missing initializer calls for one or more parent contracts: `TransitiveGrandparent2`'],
|
|
188
|
+
count: 1,
|
|
189
|
+
});
|
|
190
|
+
testRejects('TransitiveDuplicate_Bad', 'transparent', {
|
|
191
|
+
contains: [
|
|
192
|
+
'Duplicate calls found to initializer `__TransitiveGrandparent1_init` for contract `TransitiveGrandparent1`',
|
|
193
|
+
],
|
|
194
|
+
count: 1,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
testAccepts('Ownable_Ok', 'transparent');
|
|
198
|
+
testAccepts('Ownable2Step_Ok', 'transparent');
|
|
199
|
+
testRejects('Ownable2Step_Bad', 'transparent', {
|
|
200
|
+
contains: ['Missing initializer calls for one or more parent contracts: `OwnableUpgradeable`'],
|
|
201
|
+
count: 1,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
testRejects('SkipsParent_Bad', 'transparent', {
|
|
205
|
+
contains: ['Missing initializer calls for one or more parent contracts: `Parent`'],
|
|
206
|
+
count: 1,
|
|
207
|
+
});
|
|
208
|
+
testRejects('DuplicateInHelpers_Bad', 'transparent', {
|
|
209
|
+
contains: ['Duplicate calls found to initializer `__Grandparent_init` for contract `Grandparent`'],
|
|
210
|
+
count: 1,
|
|
211
|
+
});
|
|
212
|
+
testRejects('Recursive_Bad', 'transparent', {
|
|
213
|
+
contains: ['Missing initializer calls for one or more parent contracts: `Parent`'],
|
|
214
|
+
count: 1,
|
|
215
|
+
});
|
|
216
|
+
testAccepts('Recursive_Ok', 'transparent');
|
|
217
|
+
|
|
218
|
+
testAccepts('ERC20_Ok', 'uups');
|
|
219
|
+
testRejects('ERC20_Bad', 'uups', {
|
|
220
|
+
contains: [
|
|
221
|
+
'Missing initializer calls for one or more parent contracts: `ERC20Upgradeable, ERC20PausableUpgradeable, OwnableUpgradeable, ERC20PermitUpgradeable`',
|
|
222
|
+
],
|
|
223
|
+
count: 1,
|
|
224
|
+
});
|