@openzeppelin/upgrades-core 1.20.6 → 1.22.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.
@@ -1,6 +1,6 @@
1
1
  import { Node } from 'solidity-ast/node';
2
- import { isNodeType, findAll } from 'solidity-ast/utils';
3
- import type { ContractDefinition } from 'solidity-ast';
2
+ import { isNodeType, findAll, ASTDereferencer } from 'solidity-ast/utils';
3
+ import type { ContractDefinition, FunctionDefinition } from 'solidity-ast';
4
4
 
5
5
  import { SolcOutput, SolcBytecode } from '../solc-api';
6
6
  import { SrcDecoder } from '../src-decoder';
@@ -68,6 +68,26 @@ interface ValidationErrorOpcode extends ValidationErrorBase {
68
68
  kind: 'delegatecall' | 'selfdestruct';
69
69
  }
70
70
 
71
+ interface OpcodePattern {
72
+ kind: 'delegatecall' | 'selfdestruct';
73
+ pattern: RegExp;
74
+ }
75
+
76
+ const OPCODES = {
77
+ delegatecall: {
78
+ kind: 'delegatecall',
79
+ pattern: /^t_function_baredelegatecall_/,
80
+ },
81
+ selfdestruct: {
82
+ kind: 'selfdestruct',
83
+ pattern: /^t_function_selfdestruct_/,
84
+ },
85
+ } as const;
86
+
87
+ export function isOpcodeError(error: ValidationErrorBase): error is ValidationErrorOpcode {
88
+ return error.kind === 'delegatecall' || error.kind === 'selfdestruct';
89
+ }
90
+
71
91
  interface ValidationErrorUpgradeability extends ValidationErrorBase {
72
92
  kind: 'missing-public-upgradeto';
73
93
  }
@@ -84,34 +104,49 @@ function* execall(re: RegExp, text: string) {
84
104
  }
85
105
  }
86
106
 
87
- function getAllowed(node: Node): string[] {
107
+ function getAllowed(node: Node, reachable: boolean): string[] {
88
108
  if ('documentation' in node) {
109
+ const tag = `oz-upgrades-unsafe-allow${reachable ? '-reachable' : ''}`;
89
110
  const doc = typeof node.documentation === 'string' ? node.documentation : node.documentation?.text ?? '';
111
+ return getAnnotationArgs(doc, tag);
112
+ } else {
113
+ return [];
114
+ }
115
+ }
90
116
 
91
- const result: string[] = [];
92
- for (const { groups } of execall(
93
- /^\s*(?:@(?<title>\w+)(?::(?<tag>[a-z][a-z-]*))? )?(?<args>(?:(?!^\s@\w+)[^])*)/m,
94
- doc,
95
- )) {
96
- if (groups && groups.title === 'custom' && groups.tag === 'oz-upgrades-unsafe-allow') {
97
- result.push(...groups.args.split(/\s+/));
117
+ /**
118
+ * Get args from the doc string matching the given tag
119
+ */
120
+ export function getAnnotationArgs(doc: string, tag: string) {
121
+ const result: string[] = [];
122
+ for (const { groups } of execall(
123
+ /^\s*(?:@(?<title>\w+)(?::(?<tag>[a-z][a-z-]*))? )?(?<args>(?:(?!^\s*@\w+)[^])*)/m,
124
+ doc,
125
+ )) {
126
+ if (groups && groups.title === 'custom' && groups.tag === tag) {
127
+ const trimmedArgs = groups.args.trim();
128
+ if (trimmedArgs.length > 0) {
129
+ result.push(...trimmedArgs.split(/\s+/));
98
130
  }
99
131
  }
132
+ }
100
133
 
101
- result.forEach(arg => {
102
- if (!(errorKinds as readonly string[]).includes(arg)) {
103
- throw new Error(`NatSpec: oz-upgrades-unsafe-allow argument not recognized: ${arg}`);
104
- }
105
- });
134
+ result.forEach(arg => {
135
+ if (!(errorKinds as readonly string[]).includes(arg)) {
136
+ throw new Error(`NatSpec: ${tag} argument not recognized: ${arg}`);
137
+ }
138
+ });
106
139
 
107
- return result;
108
- } else {
109
- return [];
110
- }
140
+ return result;
141
+ }
142
+
143
+ function skipCheckReachable(error: string, node: Node): boolean {
144
+ return getAllowed(node, true).includes(error);
111
145
  }
112
146
 
113
147
  function skipCheck(error: string, node: Node): boolean {
114
- return getAllowed(node).includes(error);
148
+ // skip both allow and allow-reachable errors in the lexical scope
149
+ return getAllowed(node, false).includes(error) || getAllowed(node, true).includes(error);
115
150
  }
116
151
 
117
152
  function getFullyQualifiedName(source: string, contractName: string) {
@@ -126,6 +161,9 @@ export function validate(solcOutput: SolcOutput, decodeSrc: SrcDecoder, solcVers
126
161
 
127
162
  const deref = astDereferencer(solcOutput);
128
163
 
164
+ const delegateCallCache = initOpcodeCache();
165
+ const selfDestructCache = initOpcodeCache();
166
+
129
167
  for (const source in solcOutput.contracts) {
130
168
  for (const contractName in solcOutput.contracts[source]) {
131
169
  const bytecode = solcOutput.contracts[source][contractName].evm.bytecode;
@@ -163,7 +201,7 @@ export function validate(solcOutput: SolcOutput, decodeSrc: SrcDecoder, solcVers
163
201
  validation[key].src = decodeSrc(contractDef);
164
202
  validation[key].errors = [
165
203
  ...getConstructorErrors(contractDef, decodeSrc),
166
- ...getOpcodeErrors(contractDef, decodeSrc),
204
+ ...getOpcodeErrors(contractDef, deref, decodeSrc, delegateCallCache, selfDestructCache),
167
205
  ...getStateVariableErrors(contractDef, decodeSrc),
168
206
  // TODO: add linked libraries support
169
207
  // https://github.com/OpenZeppelin/openzeppelin-upgrades/issues/52
@@ -206,27 +244,178 @@ function* getConstructorErrors(contractDef: ContractDefinition, decodeSrc: SrcDe
206
244
  }
207
245
  }
208
246
 
209
- function* getOpcodeErrors(contractDef: ContractDefinition, decodeSrc: SrcDecoder): Generator<ValidationErrorOpcode> {
210
- for (const fnCall of findAll('FunctionCall', contractDef, node => skipCheck('delegatecall', node))) {
247
+ function initOpcodeCache(): OpcodeCache {
248
+ return {
249
+ mainContractErrors: new Map(),
250
+ inheritedContractErrors: new Map(),
251
+ };
252
+ }
253
+
254
+ interface OpcodeCache {
255
+ mainContractErrors: Map<number, ValidationErrorOpcode[]>;
256
+ inheritedContractErrors: Map<number, ValidationErrorOpcode[]>;
257
+ }
258
+
259
+ function* getOpcodeErrors(
260
+ contractDef: ContractDefinition,
261
+ deref: ASTDereferencer,
262
+ decodeSrc: SrcDecoder,
263
+ delegateCallCache: OpcodeCache,
264
+ selfDestructCache: OpcodeCache,
265
+ ): Generator<ValidationErrorOpcode, void, undefined> {
266
+ yield* getContractOpcodeErrors(contractDef, deref, decodeSrc, OPCODES.delegatecall, 'main', delegateCallCache);
267
+ yield* getContractOpcodeErrors(contractDef, deref, decodeSrc, OPCODES.selfdestruct, 'main', selfDestructCache);
268
+ }
269
+
270
+ /**
271
+ * Whether this node is being visited as part of a main contract, or as an inherited contract or function
272
+ */
273
+ type Scope = 'main' | 'inherited';
274
+
275
+ function* getContractOpcodeErrors(
276
+ contractDef: ContractDefinition,
277
+ deref: ASTDereferencer,
278
+ decodeSrc: SrcDecoder,
279
+ opcode: OpcodePattern,
280
+ scope: Scope,
281
+ cache: OpcodeCache,
282
+ ): Generator<ValidationErrorOpcode, void, undefined> {
283
+ const cached = getCachedOpcodes(contractDef.id, scope, cache);
284
+ if (cached !== undefined) {
285
+ yield* cached;
286
+ } else {
287
+ const errors: ValidationErrorOpcode[] = [];
288
+ setCachedOpcodes(contractDef.id, scope, cache, errors);
289
+ errors.push(
290
+ ...getFunctionOpcodeErrors(contractDef, deref, decodeSrc, opcode, scope, cache),
291
+ ...getInheritedContractOpcodeErrors(contractDef, deref, decodeSrc, opcode, cache),
292
+ );
293
+ yield* errors;
294
+ }
295
+ }
296
+
297
+ function getCachedOpcodes(key: number, scope: string, cache: OpcodeCache) {
298
+ return scope === 'main' ? cache.mainContractErrors.get(key) : cache.inheritedContractErrors.get(key);
299
+ }
300
+
301
+ function* getFunctionOpcodeErrors(
302
+ contractOrFunctionDef: ContractDefinition | FunctionDefinition,
303
+ deref: ASTDereferencer,
304
+ decodeSrc: SrcDecoder,
305
+ opcode: OpcodePattern,
306
+ scope: Scope,
307
+ cache: OpcodeCache,
308
+ ): Generator<ValidationErrorOpcode, void, undefined> {
309
+ const parentContractDef = getParentDefinition(deref, contractOrFunctionDef);
310
+ if (parentContractDef === undefined || !skipCheck(opcode.kind, parentContractDef)) {
311
+ yield* getDirectFunctionOpcodeErrors(contractOrFunctionDef, decodeSrc, opcode, scope);
312
+ }
313
+ if (parentContractDef === undefined || !skipCheckReachable(opcode.kind, parentContractDef)) {
314
+ yield* getReferencedFunctionOpcodeErrors(contractOrFunctionDef, deref, decodeSrc, opcode, scope, cache);
315
+ }
316
+ }
317
+
318
+ function* getDirectFunctionOpcodeErrors(
319
+ contractOrFunctionDef: ContractDefinition | FunctionDefinition,
320
+ decodeSrc: SrcDecoder,
321
+ opcode: OpcodePattern,
322
+ scope: Scope,
323
+ ) {
324
+ for (const fnCall of findAll(
325
+ 'FunctionCall',
326
+ contractOrFunctionDef,
327
+ node => skipCheck(opcode.kind, node) || (scope === 'inherited' && isInternalFunction(node)),
328
+ )) {
211
329
  const fn = fnCall.expression;
212
- if (fn.typeDescriptions.typeIdentifier?.match(/^t_function_baredelegatecall_/)) {
330
+ if (fn.typeDescriptions.typeIdentifier?.match(opcode.pattern)) {
213
331
  yield {
214
- kind: 'delegatecall',
332
+ kind: opcode.kind,
215
333
  src: decodeSrc(fnCall),
216
334
  };
217
335
  }
218
336
  }
219
- for (const fnCall of findAll('FunctionCall', contractDef, node => skipCheck('selfdestruct', node))) {
337
+ }
338
+
339
+ function* getReferencedFunctionOpcodeErrors(
340
+ contractOrFunctionDef: ContractDefinition | FunctionDefinition,
341
+ deref: ASTDereferencer,
342
+ decodeSrc: SrcDecoder,
343
+ opcode: OpcodePattern,
344
+ scope: Scope,
345
+ cache: OpcodeCache,
346
+ ) {
347
+ for (const fnCall of findAll(
348
+ 'FunctionCall',
349
+ contractOrFunctionDef,
350
+ node => skipCheckReachable(opcode.kind, node) || (scope === 'inherited' && isInternalFunction(node)),
351
+ )) {
220
352
  const fn = fnCall.expression;
221
- if (fn.typeDescriptions.typeIdentifier?.match(/^t_function_selfdestruct_/)) {
222
- yield {
223
- kind: 'selfdestruct',
224
- src: decodeSrc(fnCall),
225
- };
353
+ if ('referencedDeclaration' in fn && fn.referencedDeclaration && fn.referencedDeclaration > 0) {
354
+ // non-positive references refer to built-in functions
355
+ const referencedNode = tryDerefFunction(deref, fn.referencedDeclaration);
356
+ if (referencedNode !== undefined) {
357
+ const cached = getCachedOpcodes(referencedNode.id, scope, cache);
358
+ if (cached !== undefined) {
359
+ yield* cached;
360
+ } else {
361
+ const errors: ValidationErrorOpcode[] = [];
362
+ setCachedOpcodes(referencedNode.id, scope, cache, errors);
363
+ errors.push(...getFunctionOpcodeErrors(referencedNode, deref, decodeSrc, opcode, scope, cache));
364
+ yield* errors;
365
+ }
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ function setCachedOpcodes(key: number, scope: string, cache: OpcodeCache, errors: ValidationErrorOpcode[]) {
372
+ if (scope === 'main') {
373
+ cache.mainContractErrors.set(key, errors);
374
+ } else {
375
+ cache.inheritedContractErrors.set(key, errors);
376
+ }
377
+ }
378
+
379
+ function tryDerefFunction(deref: ASTDereferencer, referencedDeclaration: number): FunctionDefinition | undefined {
380
+ try {
381
+ return deref(['FunctionDefinition'], referencedDeclaration);
382
+ } catch (e: any) {
383
+ if (!e.message.includes('No node with id')) {
384
+ throw e;
226
385
  }
227
386
  }
228
387
  }
229
388
 
389
+ function* getInheritedContractOpcodeErrors(
390
+ contractDef: ContractDefinition,
391
+ deref: ASTDereferencer,
392
+ decodeSrc: SrcDecoder,
393
+ opcode: OpcodePattern,
394
+ cache: OpcodeCache,
395
+ ) {
396
+ if (!skipCheckReachable(opcode.kind, contractDef)) {
397
+ for (const base of contractDef.baseContracts) {
398
+ const referencedContract = deref('ContractDefinition', base.baseName.referencedDeclaration);
399
+ yield* getContractOpcodeErrors(referencedContract, deref, decodeSrc, opcode, 'inherited', cache);
400
+ }
401
+ }
402
+ }
403
+
404
+ function getParentDefinition(deref: ASTDereferencer, contractOrFunctionDef: ContractDefinition | FunctionDefinition) {
405
+ const parentNode = deref(['ContractDefinition', 'SourceUnit'], contractOrFunctionDef.scope);
406
+ if (parentNode.nodeType === 'ContractDefinition') {
407
+ return parentNode;
408
+ }
409
+ }
410
+
411
+ function isInternalFunction(node: Node) {
412
+ return (
413
+ node.nodeType === 'FunctionDefinition' &&
414
+ node.kind !== 'constructor' && // do not consider constructors as internal, because they are always called by children contracts' constructors
415
+ (node.visibility === 'internal' || node.visibility === 'private')
416
+ );
417
+ }
418
+
230
419
  function* getStateVariableErrors(
231
420
  contractDef: ContractDefinition,
232
421
  decodeSrc: SrcDecoder,
@@ -0,0 +1,137 @@
1
+ import _test, { TestFn } from 'ava';
2
+ import { artifacts } from 'hardhat';
3
+
4
+ import { validate, getContractVersion, assertUpgradeSafe, ValidationOptions, RunValidation } from './validate';
5
+ import { solcInputOutputDecoder } from './src-decoder';
6
+
7
+ interface Context {
8
+ validation: RunValidation;
9
+ }
10
+
11
+ const test = _test as TestFn<Context>;
12
+
13
+ test.before(async t => {
14
+ const contracts = [
15
+ 'contracts/test/ignore-errors/SafeContract.sol:SafeContract',
16
+ 'contracts/test/ignore-errors/SafeContractWithFreeFunctionCall.sol:SafeContractWithFreeFunctionCall',
17
+ 'contracts/test/ignore-errors/SafeContractWithLibraryCall.sol:SafeContractWithLibraryCall',
18
+ 'contracts/test/ignore-errors/SafeContractWithLibraryImport.sol:SafeContractWithLibraryImport',
19
+ 'contracts/test/ignore-errors/SafeContractWithLibraryUsingFor.sol:SafeContractWithLibraryUsingFor',
20
+ 'contracts/test/ignore-errors/SafeContractWithTransitiveLibraryCall.sol:SafeContractWithTransitiveLibraryCall',
21
+ 'contracts/test/ignore-errors/SafeContractWithParentCall.sol:SafeContractWithParentCall',
22
+ 'contracts/test/ignore-errors/RiskyParentContract.sol:RiskyParentContract',
23
+ 'contracts/test/ignore-errors/UnsafeContract.sol:UnsafeContract',
24
+ 'contracts/test/ignore-errors/UnsafeContractWithFreeFunctionCall.sol:UnsafeContractWithFreeFunctionCall',
25
+ 'contracts/test/ignore-errors/UnsafeContractWithLibraryCall.sol:UnsafeContractWithLibraryCall',
26
+ 'contracts/test/ignore-errors/UnsafeContractWithLibraryUsingFor.sol:UnsafeContractWithLibraryUsingFor',
27
+ 'contracts/test/ignore-errors/UnsafeContractWithTransitiveLibraryCall.sol:UnsafeContractWithTransitiveLibraryCall',
28
+ 'contracts/test/ignore-errors/UnsafeContractWithParentCall.sol:UnsafeContractWithParentCall',
29
+ 'contracts/test/ignore-errors/UnsafeContractWithInheritedParent.sol:UnsafeContractWithInheritedParent',
30
+ 'contracts/test/ignore-errors/UnsafeContractWithInheritedTransitiveParent.sol:UnsafeContractWithInheritedTransitiveParent',
31
+ 'contracts/test/ignore-errors/UnsafeAllow.sol:UnsafeAllow',
32
+ 'contracts/test/ignore-errors/AllowReachable.sol:AllowReachable',
33
+ 'contracts/test/ignore-errors/UnsafeAllowReachableDifferentOpcode.sol:UnsafeAllowReachableDifferentOpcode',
34
+ 'contracts/test/ignore-errors/UnsafeAllowParent.sol:UnsafeAllowParent',
35
+ 'contracts/test/ignore-errors/AllowReachableParent.sol:AllowReachableParent',
36
+ 'contracts/test/ignore-errors/UnsafeAllowReachableParentDifferentOpcode.sol:UnsafeAllowReachableParentDifferentOpcode',
37
+ 'contracts/test/ignore-errors/AllowChildInheritedTransitive.sol:AllowChildInheritedTransitive',
38
+ 'contracts/test/ignore-errors/AllowChild.sol:AllowChild',
39
+ 'contracts/test/ignore-errors/AllowChildCallTransitive.sol:AllowChildCallTransitive',
40
+ 'contracts/test/ignore-errors/AllowChildSelfReachable.sol:AllowChildSelfReachable',
41
+ 'contracts/test/ignore-errors/SafeRecursion.sol:SafeRecursion',
42
+ 'contracts/test/ignore-errors/UnsafeRecursion.sol:UnsafeRecursion',
43
+ 'contracts/test/ignore-errors/Constructors.sol:UnsafeChild1',
44
+ 'contracts/test/ignore-errors/Constructors.sol:UnsafeChild2',
45
+ 'contracts/test/ignore-errors/Constructors.sol:UnsafeChild3',
46
+ 'contracts/test/ignore-errors/Constructors.sol:UnsafeChild4',
47
+ 'contracts/test/ignore-errors/Constructors.sol:AllowChild5',
48
+ 'contracts/test/ignore-errors/Constructors.sol:AllowChild6',
49
+ 'contracts/test/ignore-errors/Constructors.sol:UnsafeAllowChild7',
50
+ 'contracts/test/ignore-errors/Constructors.sol:AllowReachableChild8',
51
+ 'contracts/test/ignore-errors/Constructors.sol:UnsafeChild9',
52
+ // 'contracts/test/ignore-errors/Modifiers.sol:ModifierNotUsed',
53
+ 'contracts/test/ignore-errors/Modifiers.sol:ModifierUsed',
54
+ ];
55
+
56
+ t.context.validation = {} as RunValidation;
57
+ for (const contract of contracts) {
58
+ const buildInfo = await artifacts.getBuildInfo(contract);
59
+ if (buildInfo === undefined) {
60
+ throw new Error(`Build info not found for contract ${contract}`);
61
+ }
62
+ const solcOutput = buildInfo.output;
63
+ const solcInput = buildInfo.input;
64
+ const decodeSrc = solcInputOutputDecoder(solcInput, solcOutput);
65
+ Object.assign(t.context.validation, validate(solcOutput, decodeSrc));
66
+ }
67
+ });
68
+
69
+ function testValid(name: string, kind: ValidationOptions['kind'], valid: boolean) {
70
+ testOverride(name, kind, {}, valid);
71
+ }
72
+
73
+ function testOverride(name: string, kind: ValidationOptions['kind'], opts: ValidationOptions, valid: boolean) {
74
+ const optKeys = Object.keys(opts);
75
+ const describeOpts = optKeys.length > 0 ? '(' + optKeys.join(', ') + ')' : '';
76
+ const testName = [valid ? 'accepts' : 'rejects', kind, name, describeOpts].join(' ');
77
+ test(testName, t => {
78
+ const version = getContractVersion(t.context.validation, name);
79
+ const assertUpgSafe = () => assertUpgradeSafe([t.context.validation], version, { kind, ...opts });
80
+ if (valid) {
81
+ t.notThrows(assertUpgSafe);
82
+ } else {
83
+ t.throws(assertUpgSafe);
84
+ }
85
+ });
86
+ }
87
+
88
+ testValid('SafeContract', 'transparent', true);
89
+ testValid('SafeContractWithFreeFunctionCall', 'transparent', true);
90
+ testValid('SafeContractWithLibraryCall', 'transparent', true);
91
+ testValid('SafeContractWithLibraryImport', 'transparent', true);
92
+ testValid('SafeContractWithLibraryUsingFor', 'transparent', true);
93
+ testValid('SafeContractWithTransitiveLibraryCall', 'transparent', true);
94
+ testValid('SafeContractWithParentCall', 'transparent', true);
95
+
96
+ testValid('RiskyParentContract', 'transparent', false);
97
+
98
+ testValid('UnsafeContract', 'transparent', false);
99
+ testValid('UnsafeContractWithFreeFunctionCall', 'transparent', false);
100
+ testValid('UnsafeContractWithLibraryCall', 'transparent', false);
101
+ testValid('UnsafeContractWithLibraryUsingFor', 'transparent', false);
102
+ testValid('UnsafeContractWithTransitiveLibraryCall', 'transparent', false);
103
+ testValid('UnsafeContractWithParentCall', 'transparent', false);
104
+ testValid('UnsafeContractWithInheritedParent', 'transparent', false);
105
+ testValid('UnsafeContractWithInheritedTransitiveParent', 'transparent', false);
106
+
107
+ testValid('UnsafeAllow', 'transparent', false);
108
+ testValid('AllowReachable', 'transparent', true);
109
+ testValid('UnsafeAllowReachableDifferentOpcode', 'transparent', false);
110
+
111
+ testValid('UnsafeAllowParent', 'transparent', false);
112
+ testValid('AllowReachableParent', 'transparent', true);
113
+ testValid('UnsafeAllowReachableParentDifferentOpcode', 'transparent', false);
114
+
115
+ testValid('AllowChildInheritedTransitive', 'transparent', true);
116
+ testValid('AllowChild', 'transparent', true);
117
+ testValid('AllowChildCallTransitive', 'transparent', true);
118
+
119
+ testValid('AllowChildSelfReachable', 'transparent', true);
120
+
121
+ testValid('SafeRecursion', 'transparent', true);
122
+ testValid('UnsafeRecursion', 'transparent', false);
123
+
124
+ testValid('UnsafeChild1', 'transparent', false);
125
+ testValid('UnsafeChild2', 'transparent', false);
126
+ testValid('UnsafeChild3', 'transparent', false);
127
+ testValid('UnsafeChild4', 'transparent', false);
128
+ testValid('AllowChild5', 'transparent', true);
129
+ testValid('AllowChild6', 'transparent', true);
130
+ testValid('UnsafeAllowChild7', 'transparent', false);
131
+ testValid('AllowReachableChild8', 'transparent', true);
132
+ testValid('UnsafeChild9', 'transparent', false);
133
+
134
+ // TODO: do not throw an error in this case
135
+ // testValid('ModifierNotUsed', 'transparent', true);
136
+
137
+ testValid('ModifierUsed', 'transparent', false);