@openzeppelin/upgrades-core 1.13.1 → 1.14.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.
Files changed (40) hide show
  1. package/dist/storage/compare.d.ts +15 -4
  2. package/dist/storage/compare.d.ts.map +1 -1
  3. package/dist/storage/compare.js +46 -4
  4. package/dist/storage/compare.js.map +1 -1
  5. package/dist/storage/extract.d.ts.map +1 -1
  6. package/dist/storage/extract.js +26 -2
  7. package/dist/storage/extract.js.map +1 -1
  8. package/dist/storage/layout.d.ts +14 -0
  9. package/dist/storage/layout.d.ts.map +1 -1
  10. package/dist/storage/layout.js +6 -2
  11. package/dist/storage/layout.js.map +1 -1
  12. package/dist/storage/report-rename-retype.test.d.ts +2 -0
  13. package/dist/storage/report-rename-retype.test.d.ts.map +1 -0
  14. package/dist/storage/report-rename-retype.test.js +93 -0
  15. package/dist/storage/report-rename-retype.test.js.map +1 -0
  16. package/dist/storage/report.d.ts.map +1 -1
  17. package/dist/storage/report.js +32 -2
  18. package/dist/storage/report.js.map +1 -1
  19. package/dist/storage/report.test.js +6 -0
  20. package/dist/storage/report.test.js.map +1 -1
  21. package/dist/storage.test.js +1 -1
  22. package/dist/utils/execall.d.ts +2 -0
  23. package/dist/utils/execall.d.ts.map +1 -0
  24. package/dist/utils/execall.js +17 -0
  25. package/dist/utils/execall.js.map +1 -0
  26. package/package.json +2 -2
  27. package/src/storage/compare.ts +57 -8
  28. package/src/storage/extract.ts +30 -4
  29. package/src/storage/layout.ts +17 -2
  30. package/src/storage/report-rename-retype.test.ts +106 -0
  31. package/src/storage/report-rename-retype.test.ts.md +44 -0
  32. package/src/storage/report-rename-retype.test.ts.snap +0 -0
  33. package/src/storage/report.test.ts +7 -0
  34. package/src/storage/report.test.ts.md +11 -0
  35. package/src/storage/report.test.ts.snap +0 -0
  36. package/src/storage/report.ts +39 -4
  37. package/src/storage.test.ts +1 -1
  38. package/src/storage.test.ts.md +146 -0
  39. package/src/storage.test.ts.snap +0 -0
  40. package/src/utils/execall.ts +11 -0
@@ -1 +1 @@
1
- {"version":3,"file":"report.test.js","sourceRoot":"","sources":["../../src/storage/report.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"report.test.js","sourceRoot":"","sources":["../../src/storage/report.test.ts"],"names":[],"mappings":"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"}
@@ -160,7 +160,7 @@ test('storage upgrade with structs', t => {
160
160
  change: {
161
161
  kind: 'struct members',
162
162
  ops: {
163
- length: 1,
163
+ length: 2,
164
164
  0: { kind: 'delete' },
165
165
  },
166
166
  },
@@ -0,0 +1,2 @@
1
+ export declare function execall(re: RegExp, text: string): Generator<RegExpExecArray, void, unknown>;
2
+ //# sourceMappingURL=execall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execall.d.ts","sourceRoot":"","sources":["../../src/utils/execall.ts"],"names":[],"mappings":"AAAA,wBAAiB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,6CAUhD"}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.execall = void 0;
4
+ function* execall(re, text) {
5
+ re = new RegExp(re, re.flags + (re.sticky ? '' : 'y'));
6
+ while (true) {
7
+ const match = re.exec(text);
8
+ if (match && match[0] !== '') {
9
+ yield match;
10
+ }
11
+ else {
12
+ break;
13
+ }
14
+ }
15
+ }
16
+ exports.execall = execall;
17
+ //# sourceMappingURL=execall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execall.js","sourceRoot":"","sources":["../../src/utils/execall.ts"],"names":[],"mappings":";;;AAAA,QAAe,CAAC,CAAC,OAAO,CAAC,EAAU,EAAE,IAAY;IAC/C,EAAE,GAAG,IAAI,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,OAAO,IAAI,EAAE;QACX,MAAM,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE;YAC5B,MAAM,KAAK,CAAC;SACb;aAAM;YACL,MAAM;SACP;KACF;AACH,CAAC;AAVD,0BAUC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openzeppelin/upgrades-core",
3
- "version": "1.13.1",
3
+ "version": "1.14.0",
4
4
  "description": "",
5
5
  "repository": "https://github.com/OpenZeppelin/openzeppelin-upgrades/tree/master/packages/core",
6
6
  "license": "MIT",
@@ -59,5 +59,5 @@
59
59
  "proper-lockfile": "^4.1.1",
60
60
  "solidity-ast": "^0.4.15"
61
61
  },
62
- "gitHead": "986cfff65ecbe4706ff62391ddc22ba31c547eb8"
62
+ "gitHead": "7bb2763c321c55a02e07e4b2b969f49921c5a048"
63
63
  }
@@ -1,7 +1,7 @@
1
1
  import { levenshtein, Operation } from '../levenshtein';
2
- import { ParsedTypeDetailed, StorageItem as _StorageItem } from './layout';
2
+ import { hasLayout, ParsedTypeDetailed, isEnumMembers, isStructMembers } from './layout';
3
3
  import { UpgradesError } from '../error';
4
- import { StructMember as _StructMember, isEnumMembers, isStructMembers } from './layout';
4
+ import { StorageItem as _StorageItem, StructMember as _StructMember, StorageField as _StorageField } from './layout';
5
5
  import { LayoutCompatibilityReport } from './report';
6
6
  import { assert } from '../utils/assert';
7
7
  import { isValueType } from '../utils/is-value-type';
@@ -9,7 +9,7 @@ import { isValueType } from '../utils/is-value-type';
9
9
  export type StorageItem = _StorageItem<ParsedTypeDetailed>;
10
10
  type StructMember = _StructMember<ParsedTypeDetailed>;
11
11
 
12
- export type StorageField = StorageItem | StructMember;
12
+ export type StorageField = _StorageField<ParsedTypeDetailed>;
13
13
  export type StorageOperation<F extends StorageField> = Operation<F, StorageFieldChange<F>>;
14
14
 
15
15
  export type EnumOperation = Operation<string, { kind: 'replace'; original: string; updated: string }>;
@@ -17,6 +17,7 @@ export type EnumOperation = Operation<string, { kind: 'replace'; original: strin
17
17
  type StorageFieldChange<F extends StorageField> = (
18
18
  | { kind: 'replace' | 'rename' }
19
19
  | { kind: 'typechange'; change: TypeChange }
20
+ | { kind: 'layoutchange'; change: LayoutChange }
20
21
  ) & {
21
22
  original: F;
22
23
  updated: F;
@@ -28,6 +29,7 @@ export type TypeChange = (
28
29
  | 'obvious mismatch'
29
30
  | 'unknown'
30
31
  | 'array grow'
32
+ | 'visibility change'
31
33
  | 'array shrink'
32
34
  | 'array dynamic'
33
35
  | 'enum resize'
@@ -51,6 +53,13 @@ export type TypeChange = (
51
53
  updated: ParsedTypeDetailed;
52
54
  };
53
55
 
56
+ export interface LayoutChange {
57
+ uncertain?: boolean;
58
+ slot?: Record<'from' | 'to', string>;
59
+ offset?: Record<'from' | 'to', number>;
60
+ bytes?: Record<'from' | 'to', string>;
61
+ }
62
+
54
63
  export class StorageLayoutComparator {
55
64
  hasAllowedUncheckedCustomTypes = false;
56
65
 
@@ -78,16 +87,53 @@ export class StorageLayoutComparator {
78
87
  }
79
88
  }
80
89
 
81
- getFieldChange<F extends StorageField>(original: F, updated: F): StorageFieldChange<F> | undefined {
82
- const nameChange = !this.unsafeAllowRenames && original.label !== updated.label;
83
- const typeChange = this.getTypeChange(original.type, updated.type, { allowAppend: false });
90
+ getVisibilityChange(original: ParsedTypeDetailed, updated: ParsedTypeDetailed): TypeChange | undefined {
91
+ const re = /^t_function_(internal|external)/;
92
+ const originalVisibility = original.head.match(re);
93
+ const updatedVisibility = updated.head.match(re);
94
+ assert(originalVisibility && updatedVisibility);
95
+ if (originalVisibility[0] !== updatedVisibility[0]) {
96
+ return { kind: 'visibility change', original, updated };
97
+ }
98
+ }
84
99
 
85
- if (typeChange && nameChange) {
100
+ getFieldChange<F extends StorageField>(original: F, updated: F): StorageFieldChange<F> | undefined {
101
+ const nameChange = !this.unsafeAllowRenames && original.label !== (updated.renamedFrom ?? updated.label);
102
+ const retypedFromOriginal = original.type.item.label === updated.retypedFrom?.trim();
103
+ const typeChange = !retypedFromOriginal && this.getTypeChange(original.type, updated.type, { allowAppend: false });
104
+ const layoutChange = this.getLayoutChange(original, updated);
105
+
106
+ if (updated.retypedFrom && layoutChange) {
107
+ return { kind: 'layoutchange', original, updated, change: layoutChange };
108
+ } else if (typeChange && nameChange) {
86
109
  return { kind: 'replace', original, updated };
87
110
  } else if (nameChange) {
88
111
  return { kind: 'rename', original, updated };
89
112
  } else if (typeChange) {
90
113
  return { kind: 'typechange', change: typeChange, original, updated };
114
+ } else if (layoutChange && !layoutChange.uncertain) {
115
+ // Any layout change should be caught earlier as a type change, but we
116
+ // add this check as a safety fallback.
117
+ return { kind: 'layoutchange', original, updated, change: layoutChange };
118
+ }
119
+ }
120
+
121
+ getLayoutChange(original: StorageField, updated: StorageField): LayoutChange | undefined {
122
+ const validPair = ['uint8', 'bool'];
123
+ const knownCompatibleTypes =
124
+ validPair.includes(original.type.item.label) && validPair.includes(updated.type.item.label);
125
+ if (original.type.item.label == updated.type.item.label || knownCompatibleTypes) {
126
+ return undefined;
127
+ } else if (hasLayout(original) && hasLayout(updated)) {
128
+ const change = <T>(from: T, to: T) => (from === to ? undefined : { from, to });
129
+ const slot = change(original.slot, updated.slot);
130
+ const offset = change(original.offset, updated.offset);
131
+ const bytes = change(original.type.item.numberOfBytes, updated.type.item.numberOfBytes);
132
+ if (slot || offset || bytes) {
133
+ return { slot, offset, bytes };
134
+ }
135
+ } else {
136
+ return { uncertain: true };
91
137
  }
92
138
  }
93
139
 
@@ -97,7 +143,6 @@ export class StorageLayoutComparator {
97
143
  { allowAppend }: { allowAppend: boolean },
98
144
  ): TypeChange | undefined {
99
145
  const key = JSON.stringify({ original: original.id, updated: updated.id, allowAppend });
100
-
101
146
  if (this.cache.has(key)) {
102
147
  return this.cache.get(key);
103
148
  }
@@ -121,6 +166,10 @@ export class StorageLayoutComparator {
121
166
  updated: ParsedTypeDetailed,
122
167
  { allowAppend }: { allowAppend: boolean },
123
168
  ): TypeChange | undefined {
169
+ if (updated.head.startsWith('t_function')) {
170
+ return this.getVisibilityChange(original, updated);
171
+ }
172
+
124
173
  if (original.head !== updated.head) {
125
174
  return { kind: 'obvious mismatch', original, updated };
126
175
  }
@@ -13,6 +13,7 @@ import { SrcDecoder } from '../src-decoder';
13
13
  import { ASTDereferencer } from '../ast-dereferencer';
14
14
  import { mapValues } from '../utils/map-values';
15
15
  import { pick } from '../utils/pick';
16
+ import { execall } from '../utils/execall';
16
17
 
17
18
  const currentLayoutVersion = '1.1';
18
19
 
@@ -31,7 +32,10 @@ export function extractStorageLayout(
31
32
  layout.types = mapValues(storageLayout.types, m => {
32
33
  return {
33
34
  label: m.label,
34
- members: m.members?.map(m => (typeof m === 'string' ? m : pick(m, ['label', 'type']))) as TypeItem['members'],
35
+ members: m.members?.map(m =>
36
+ typeof m === 'string' ? m : pick(m, ['label', 'type', 'offset', 'slot']),
37
+ ) as TypeItem['members'],
38
+ numberOfBytes: m.numberOfBytes,
35
39
  };
36
40
  });
37
41
 
@@ -39,11 +43,12 @@ export function extractStorageLayout(
39
43
  const origin = getOriginContract(contractDef, storage.astId, deref);
40
44
  assert(origin, `Did not find variable declaration node for '${storage.label}'`);
41
45
  const { varDecl, contract } = origin;
46
+ const { renamedFrom, retypedFrom } = getRetypedRenamed(varDecl);
42
47
  // Solc layout doesn't bring members for enums so we get them using the ast method
43
48
  loadLayoutType(varDecl, layout, deref);
44
49
  const { label, offset, slot, type } = storage;
45
50
  const src = decodeSrc(varDecl);
46
- layout.storage.push({ label, offset, slot, type, contract, src });
51
+ layout.storage.push({ label, offset, slot, type, contract, src, retypedFrom, renamedFrom });
47
52
  layout.flat = true;
48
53
  }
49
54
  } else {
@@ -51,12 +56,14 @@ export function extractStorageLayout(
51
56
  if (isNodeType('VariableDeclaration', varDecl)) {
52
57
  if (!varDecl.constant && varDecl.mutability !== 'immutable') {
53
58
  const type = normalizeTypeIdentifier(typeDescriptions(varDecl).typeIdentifier);
54
-
59
+ const { renamedFrom, retypedFrom } = getRetypedRenamed(varDecl);
55
60
  layout.storage.push({
56
61
  contract: contractDef.name,
57
62
  label: varDecl.name,
58
63
  type,
59
64
  src: decodeSrc(varDecl),
65
+ retypedFrom,
66
+ renamedFrom,
60
67
  });
61
68
 
62
69
  loadLayoutType(varDecl, layout, deref);
@@ -103,7 +110,6 @@ function getTypeMembers(typeDef: StructDefinition | EnumDefinition): TypeItem['m
103
110
  function getOriginContract(contract: ContractDefinition, astId: number | undefined, deref: ASTDereferencer) {
104
111
  for (const id of contract.linearizedBaseContracts.reverse()) {
105
112
  const parentContract = deref(['ContractDefinition'], id);
106
-
107
113
  const varDecl = parentContract.nodes.find(n => n.id == astId);
108
114
  if (varDecl && isNodeType('VariableDeclaration', varDecl)) {
109
115
  return { varDecl, contract: parentContract.name };
@@ -141,3 +147,23 @@ function loadLayoutType(varDecl: VariableDeclaration, layout: StorageLayout, der
141
147
  }
142
148
  }
143
149
  }
150
+
151
+ function getRetypedRenamed(varDecl: VariableDeclaration) {
152
+ let retypedFrom, renamedFrom;
153
+ if ('documentation' in varDecl) {
154
+ const docs = typeof varDecl.documentation === 'string' ? varDecl.documentation : varDecl.documentation?.text ?? '';
155
+ for (const { groups } of execall(
156
+ /^\s*(?:@(?<title>\w+)(?::(?<tag>[a-z][a-z-]*))? )?(?<args>(?:(?!^\s@\w+)[^])*)/m,
157
+ docs,
158
+ )) {
159
+ if (groups?.title === 'custom') {
160
+ if (groups.tag === 'oz-retyped-from') {
161
+ retypedFrom = groups.args;
162
+ } else if (groups.tag === 'oz-renamed-from') {
163
+ renamedFrom = groups.args;
164
+ }
165
+ }
166
+ }
167
+ }
168
+ return { retypedFrom, renamedFrom };
169
+ }
@@ -12,6 +12,8 @@ export interface StorageLayout {
12
12
  flat?: boolean;
13
13
  }
14
14
 
15
+ export type StorageField<Type = string> = StorageItem<Type> | StructMember<Type>;
16
+
15
17
  export interface StorageItem<Type = string> {
16
18
  astId?: number;
17
19
  contract: string;
@@ -20,11 +22,14 @@ export interface StorageItem<Type = string> {
20
22
  src: string;
21
23
  offset?: number;
22
24
  slot?: string;
25
+ retypedFrom?: string;
26
+ renamedFrom?: string;
23
27
  }
24
28
 
25
29
  export interface TypeItem<Type = string> {
26
30
  label: string;
27
31
  members?: TypeItemMembers<Type>;
32
+ numberOfBytes?: string;
28
33
  }
29
34
 
30
35
  export type TypeItemMembers<Type = string> = StructMember<Type>[] | EnumMember[];
@@ -32,6 +37,10 @@ export type TypeItemMembers<Type = string> = StructMember<Type>[] | EnumMember[]
32
37
  export interface StructMember<Type = string> {
33
38
  label: string;
34
39
  type: Type;
40
+ retypedFrom?: string;
41
+ renamedFrom?: string;
42
+ offset?: number;
43
+ slot?: string;
35
44
  }
36
45
 
37
46
  export type EnumMember = string;
@@ -46,7 +55,6 @@ type Replace<T, K extends string, V> = Omit<T, K> & Record<K, V>;
46
55
 
47
56
  export function getDetailedLayout(layout: StorageLayout): StorageItem<ParsedTypeDetailed>[] {
48
57
  const cache: Record<string, ParsedTypeDetailed> = {};
49
-
50
58
  return layout.storage.map(parseWithDetails);
51
59
 
52
60
  function parseWithDetails<I extends { type: string }>(item: I): Replace<I, 'type', ParsedTypeDetailed> {
@@ -75,7 +83,7 @@ export function getDetailedLayout(layout: StorageLayout): StorageItem<ParsedType
75
83
  cache[parsed.id] = detailed;
76
84
 
77
85
  detailed.args = parsed.args?.map(addDetailsToParsedType);
78
- detailed.rets = parsed.args?.map(addDetailsToParsedType);
86
+ detailed.rets = parsed.rets?.map(addDetailsToParsedType);
79
87
  detailed.item.members =
80
88
  item?.members && (isStructMembers(item?.members) ? item.members.map(parseWithDetails) : item?.members);
81
89
 
@@ -90,3 +98,10 @@ export function isEnumMembers<T>(members: TypeItemMembers<T>): members is EnumMe
90
98
  export function isStructMembers<T>(members: TypeItemMembers<T>): members is StructMember<T>[] {
91
99
  return members.length === 0 || typeof members[0] === 'object';
92
100
  }
101
+
102
+ export type StorageFieldWithLayout = StorageField<ParsedTypeDetailed> &
103
+ Required<Pick<StorageField, 'offset' | 'slot'>> & { type: { item: Required<Pick<TypeItem, 'numberOfBytes'>> } };
104
+
105
+ export function hasLayout(field: StorageField<ParsedTypeDetailed>): field is StorageFieldWithLayout {
106
+ return field.offset !== undefined && field.slot !== undefined && field.type.item.numberOfBytes !== undefined;
107
+ }
@@ -0,0 +1,106 @@
1
+ import _test, { TestFn } from 'ava';
2
+ import { ContractDefinition } from 'solidity-ast';
3
+ import { ASTDereferencer, findAll } from 'solidity-ast/utils';
4
+ import { artifacts } from 'hardhat';
5
+ import { astDereferencer } from '../ast-dereferencer';
6
+ import { extractStorageLayout } from './extract';
7
+ import { StorageLayoutComparator } from './compare';
8
+ import { StorageLayout, getDetailedLayout } from './layout';
9
+
10
+ interface Context {
11
+ extractStorageLayout: (contract: string) => ReturnType<typeof extractStorageLayout>;
12
+ }
13
+
14
+ const test = _test as TestFn<Context>;
15
+
16
+ const dummyDecodeSrc = () => 'file.sol:1';
17
+ const testContracts = [
18
+ 'contracts/test/RenamedRetyped.sol:RenameV1',
19
+ 'contracts/test/RenamedRetyped.sol:RenameV2',
20
+ 'contracts/test/RenamedRetyped.sol:RetypeV1',
21
+ 'contracts/test/RenamedRetyped.sol:RetypeV2',
22
+ 'contracts/test/RenamedRetyped.sol:WronglyReportedRetypeV3',
23
+ 'contracts/test/RenamedRetyped.sol:MissmatchingTypeRetypeV4',
24
+ 'contracts/test/RenamedRetyped.sol:ConfusingRetypeV1',
25
+ 'contracts/test/RenamedRetyped.sol:ConfusingRetypeV2',
26
+ 'contracts/test/RenamedRetyped.sol:NonHardcodedRetypeV1',
27
+ 'contracts/test/RenamedRetyped.sol:NonHardcodedRetypeV2',
28
+ ];
29
+
30
+ test.before(async t => {
31
+ const contracts: Record<string, ContractDefinition> = {};
32
+ const deref: Record<string, ASTDereferencer> = {};
33
+ const storageLayout: Record<string, StorageLayout> = {};
34
+ for (const contract of testContracts) {
35
+ const buildInfo = await artifacts.getBuildInfo(contract);
36
+ if (buildInfo === undefined) {
37
+ throw new Error(`Build info not found for contract ${contract}`);
38
+ }
39
+ const solcOutput = buildInfo.output;
40
+ for (const def of findAll('ContractDefinition', solcOutput.sources['contracts/test/RenamedRetyped.sol'].ast)) {
41
+ contracts[def.name] = def;
42
+ deref[def.name] = astDereferencer(solcOutput);
43
+ storageLayout[def.name] = (
44
+ solcOutput.contracts['contracts/test/RenamedRetyped.sol'][def.name] as any
45
+ ).storageLayout;
46
+ }
47
+ }
48
+
49
+ t.context.extractStorageLayout = name =>
50
+ extractStorageLayout(contracts[name], dummyDecodeSrc, deref[name], storageLayout[name]);
51
+ });
52
+
53
+ function getReport(original: StorageLayout, updated: StorageLayout) {
54
+ const originalDetailed = getDetailedLayout(original);
55
+ const updatedDetailed = getDetailedLayout(updated);
56
+ const comparator = new StorageLayoutComparator();
57
+ return comparator.compareLayouts(originalDetailed, updatedDetailed);
58
+ }
59
+
60
+ test('succesful rename', t => {
61
+ const v1 = t.context.extractStorageLayout('RenameV1');
62
+ const v2 = t.context.extractStorageLayout('RenameV2');
63
+ const report = getReport(v1, v2);
64
+ t.true(report.ok);
65
+ t.snapshot(report.explain());
66
+ });
67
+
68
+ test('succesful retype', t => {
69
+ const v1 = t.context.extractStorageLayout('RetypeV1');
70
+ const v2 = t.context.extractStorageLayout('RetypeV2');
71
+ const report = getReport(v1, v2);
72
+ t.true(report.ok);
73
+ t.snapshot(report.explain());
74
+ });
75
+
76
+ test('wrongly reported retype', t => {
77
+ const v1 = t.context.extractStorageLayout('RetypeV1');
78
+ const v2 = t.context.extractStorageLayout('WronglyReportedRetypeV3');
79
+ const report = getReport(v1, v2);
80
+ t.false(report.ok);
81
+ t.snapshot(report.explain());
82
+ });
83
+
84
+ test('rightly reported retype but incompatible new type', t => {
85
+ const v1 = t.context.extractStorageLayout('RetypeV1');
86
+ const v2 = t.context.extractStorageLayout('MissmatchingTypeRetypeV4');
87
+ const report = getReport(v1, v2);
88
+ t.false(report.ok);
89
+ t.snapshot(report.explain());
90
+ });
91
+
92
+ test('confusing bad retype', t => {
93
+ const v1 = t.context.extractStorageLayout('ConfusingRetypeV1');
94
+ const v2 = t.context.extractStorageLayout('ConfusingRetypeV2');
95
+ const report = getReport(v1, v2);
96
+ t.false(report.ok);
97
+ t.snapshot(report.explain());
98
+ });
99
+
100
+ test('non-hardcoded retype', t => {
101
+ const v1 = t.context.extractStorageLayout('NonHardcodedRetypeV1');
102
+ const v2 = t.context.extractStorageLayout('NonHardcodedRetypeV2');
103
+ const report = getReport(v1, v2);
104
+ t.true(report.ok);
105
+ t.snapshot(report.explain());
106
+ });
@@ -0,0 +1,44 @@
1
+ # Snapshot report for `src/storage/report-rename-retype.test.ts`
2
+
3
+ The actual snapshot is saved in `report-rename-retype.test.ts.snap`.
4
+
5
+ Generated by [AVA](https://avajs.dev).
6
+
7
+ ## succesful rename
8
+
9
+ > Snapshot 1
10
+
11
+ ''
12
+
13
+ ## succesful retype
14
+
15
+ > Snapshot 1
16
+
17
+ ''
18
+
19
+ ## wrongly reported retype
20
+
21
+ > Snapshot 1
22
+
23
+ `file.sol:1: Upgraded \`x\` to an incompatible type␊
24
+ - Bad upgrade from bool to uint8`
25
+
26
+ ## rightly reported retype but incompatible new type
27
+
28
+ > Snapshot 1
29
+
30
+ `file.sol:1: Layout changed for \`x\` (bool -> bytes32)␊
31
+ - Number of bytes changed from 1 to 32`
32
+
33
+ ## confusing bad retype
34
+
35
+ > Snapshot 1
36
+
37
+ `file.sol:1: Upgraded \`x\` to an incompatible type␊
38
+ - Bad upgrade from bool to uint8`
39
+
40
+ ## non-hardcoded retype
41
+
42
+ > Snapshot 1
43
+
44
+ ''
@@ -59,6 +59,13 @@ test('obvious mismatch', t => {
59
59
  t.snapshot(report.explain());
60
60
  });
61
61
 
62
+ test('function visibility change', t => {
63
+ const v1 = t.context.extractStorageLayout('StorageUpgrade_FunctionType_Visibility_V1');
64
+ const v2 = t.context.extractStorageLayout('StorageUpgrade_FunctionType_Visibility_V2_Bad');
65
+ const report = getReport(v1, v2);
66
+ t.snapshot(report.explain());
67
+ });
68
+
62
69
  test('array', t => {
63
70
  const v1 = t.context.extractStorageLayout('StorageUpgrade_Array_V1');
64
71
  const v2 = t.context.extractStorageLayout('StorageUpgrade_Array_V2_Bad');
@@ -90,6 +90,17 @@ Generated by [AVA](https://avajs.dev).
90
90
  file.sol:1: Upgraded \`a1\` to an incompatible type␊
91
91
  - Bad upgrade from uint256[] to mapping(uint256 => uint256)`
92
92
 
93
+ ## function visibility change
94
+
95
+ > Snapshot 1
96
+
97
+ `file.sol:1: Upgraded \`s1\` to an incompatible type␊
98
+ - Bad upgrade from struct StorageUpgrade_FunctionType_Visibility_V1.S to struct StorageUpgrade_FunctionType_Visibility_V2_Bad.S␊
99
+ - In struct StorageUpgrade_FunctionType_Visibility_V2_Bad.S␊
100
+ - Upgraded \`m\` to an incompatible type␊
101
+ - Bad upgrade from function () to function () external␊
102
+ Different visibility`
103
+
93
104
  ## array
94
105
 
95
106
  > Snapshot 1
Binary file
@@ -2,7 +2,7 @@ import _chalk from 'chalk';
2
2
 
3
3
  import type { BasicOperation } from '../levenshtein';
4
4
  import type { ParsedTypeDetailed } from './layout';
5
- import type { StorageOperation, StorageItem, StorageField, TypeChange, EnumOperation } from './compare';
5
+ import type { StorageOperation, StorageItem, StorageField, TypeChange, EnumOperation, LayoutChange } from './compare';
6
6
  import { itemize, itemizeWith } from '../utils/itemize';
7
7
  import { indent } from '../utils/indent';
8
8
  import { assert } from '../utils/assert';
@@ -22,8 +22,12 @@ export class LayoutCompatibilityReport {
22
22
  const chalk = new _chalk.Instance({ level: color && _chalk.supportsColor ? _chalk.supportsColor.level : 0 });
23
23
  const res = [];
24
24
 
25
- for (const op of this.ops) {
25
+ for (const [i, op] of this.ops.entries()) {
26
26
  const src = 'updated' in op ? op.updated.src : op.original.contract;
27
+ // Only print layoutchange if it's the first op, otherwise we assume it will be explained by previous ops.
28
+ if (op.kind === 'layoutchange' && i !== 0) {
29
+ continue;
30
+ }
27
31
  res.push(
28
32
  chalk.bold(src) + ':' + indent(explainStorageOperation(op, { kind: 'layout', allowAppend: true }), 2, 1),
29
33
  );
@@ -52,13 +56,20 @@ function explainStorageOperation(op: StorageOperation<StorageField>, ctx: Storag
52
56
  : [];
53
57
  return `Upgraded ${label(op.updated)} to an incompatible type\n` + itemize(basic, ...details);
54
58
  }
55
-
56
59
  case 'rename':
57
60
  return `Renamed ${label(op.original)} to ${label(op.updated)}`;
58
61
 
59
62
  case 'replace':
60
63
  return `Replaced ${label(op.original)} with ${label(op.updated)} of incompatible type`;
61
64
 
65
+ case 'layoutchange': {
66
+ return (
67
+ `Layout ${op.change.uncertain ? 'could have changed' : 'changed'} for ${label(op.updated)} ` +
68
+ `(${op.original.type.item.label} -> ${op.updated.type.item.label})\n` +
69
+ describeLayoutTransition(op.change)
70
+ ).trimEnd();
71
+ }
72
+
62
73
  default: {
63
74
  const title = explainBasicOperation(op, t => t.label);
64
75
  const hints = [];
@@ -90,6 +101,9 @@ function explainStorageOperation(op: StorageOperation<StorageField>, ctx: Storag
90
101
 
91
102
  function explainTypeChange(ch: TypeChange): string {
92
103
  switch (ch.kind) {
104
+ case 'visibility change':
105
+ return `Bad upgrade ${describeTransition(ch.original, ch.updated)}\nDifferent visibility`;
106
+
93
107
  case 'obvious mismatch':
94
108
  case 'struct members':
95
109
  case 'enum members':
@@ -172,7 +186,16 @@ function explainTypeChangeDetails(ch: TypeChange): string | undefined {
172
186
  const { allowAppend } = ch;
173
187
  return (
174
188
  `In ${ch.updated.item.label}\n` +
175
- itemize(...ch.ops.map(op => explainStorageOperation(op, { kind: 'struct', allowAppend })))
189
+ itemize(
190
+ ...ch.ops.flatMap((op, i) => {
191
+ if (op.kind === 'layoutchange' && i !== 0) {
192
+ // Only print layoutchange if it's the first op, otherwise we assume it will be explained by previous ops.
193
+ return [];
194
+ } else {
195
+ return [explainStorageOperation(op, { kind: 'struct', allowAppend })];
196
+ }
197
+ }),
198
+ )
176
199
  );
177
200
  }
178
201
 
@@ -215,6 +238,18 @@ function describeTransition(original: ParsedTypeDetailed, updated: ParsedTypeDet
215
238
  }
216
239
  }
217
240
 
241
+ function describeLayoutTransition(change: LayoutChange): string {
242
+ const res = [];
243
+ for (const k of ['slot', 'offset', 'bytes'] as const) {
244
+ const ch = change[k];
245
+ if (ch) {
246
+ const label = (k === 'bytes' ? 'number of bytes' : k).replace(/^./, c => c.toUpperCase());
247
+ res.push(`${label} changed from ${ch.from} to ${ch.to}`);
248
+ }
249
+ }
250
+ return itemize(...res);
251
+ }
252
+
218
253
  function label(variable: { label: string }): string {
219
254
  return '`' + variable.label + '`';
220
255
  }
@@ -179,7 +179,7 @@ test('storage upgrade with structs', t => {
179
179
  change: {
180
180
  kind: 'struct members',
181
181
  ops: {
182
- length: 1,
182
+ length: 2,
183
183
  0: { kind: 'delete' },
184
184
  },
185
185
  },