@openzeppelin/upgrades-core 1.28.0 → 1.30.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/call-optional-signature.d.ts +3 -0
- package/dist/call-optional-signature.d.ts.map +1 -0
- package/dist/call-optional-signature.js +24 -0
- package/dist/call-optional-signature.js.map +1 -0
- package/dist/cli/validate/build-info-file.d.ts +4 -0
- package/dist/cli/validate/build-info-file.d.ts.map +1 -1
- package/dist/cli/validate/build-info-file.js +5 -2
- package/dist/cli/validate/build-info-file.js.map +1 -1
- package/dist/cli/validate/build-info-file.test.js +4 -1
- package/dist/cli/validate/build-info-file.test.js.map +1 -1
- package/dist/cli/validate/upgradeability-assessment.js +2 -6
- package/dist/cli/validate/upgradeability-assessment.js.map +1 -1
- package/dist/cli/validate/validations.d.ts.map +1 -1
- package/dist/cli/validate/validations.js +5 -4
- package/dist/cli/validate/validations.js.map +1 -1
- package/dist/impl-address.d.ts.map +1 -1
- package/dist/impl-address.js +9 -16
- package/dist/impl-address.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/levenshtein.d.ts +6 -0
- package/dist/levenshtein.d.ts.map +1 -1
- package/dist/levenshtein.js.map +1 -1
- package/dist/solc-api.d.ts +14 -0
- package/dist/solc-api.d.ts.map +1 -1
- package/dist/standalone.d.ts +4 -1
- package/dist/standalone.d.ts.map +1 -1
- package/dist/standalone.js +6 -3
- package/dist/standalone.js.map +1 -1
- package/dist/standalone.test.js +17 -0
- package/dist/standalone.test.js.map +1 -1
- package/dist/storage/compare.d.ts +2 -1
- package/dist/storage/compare.d.ts.map +1 -1
- package/dist/storage/compare.js +25 -2
- 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 +48 -21
- package/dist/storage/extract.js.map +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +15 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/layout.d.ts +1 -0
- package/dist/storage/layout.d.ts.map +1 -1
- package/dist/storage/layout.js.map +1 -1
- package/dist/storage/namespace.d.ts +34 -0
- package/dist/storage/namespace.d.ts.map +1 -0
- package/dist/storage/namespace.js +191 -0
- package/dist/storage/namespace.js.map +1 -0
- package/dist/storage/report.js +6 -0
- package/dist/storage/report.js.map +1 -1
- package/dist/storage-namespaced-conflicts-layout.test.d.ts +2 -0
- package/dist/storage-namespaced-conflicts-layout.test.d.ts.map +1 -0
- package/dist/storage-namespaced-conflicts-layout.test.js +69 -0
- package/dist/storage-namespaced-conflicts-layout.test.js.map +1 -0
- package/dist/storage-namespaced-conflicts.test.d.ts +2 -0
- package/dist/storage-namespaced-conflicts.test.d.ts.map +1 -0
- package/dist/storage-namespaced-conflicts.test.js +45 -0
- package/dist/storage-namespaced-conflicts.test.js.map +1 -0
- package/dist/storage-namespaced-layout.test.d.ts +2 -0
- package/dist/storage-namespaced-layout.test.d.ts.map +1 -0
- package/dist/storage-namespaced-layout.test.js +59 -0
- package/dist/storage-namespaced-layout.test.js.map +1 -0
- package/dist/storage-namespaced-outside-contract.test.d.ts +2 -0
- package/dist/storage-namespaced-outside-contract.test.d.ts.map +1 -0
- package/dist/storage-namespaced-outside-contract.test.js +22 -0
- package/dist/storage-namespaced-outside-contract.test.js.map +1 -0
- package/dist/storage-namespaced-solc-versions.test.d.ts +2 -0
- package/dist/storage-namespaced-solc-versions.test.d.ts.map +1 -0
- package/dist/storage-namespaced-solc-versions.test.js +42 -0
- package/dist/storage-namespaced-solc-versions.test.js.map +1 -0
- package/dist/storage-namespaced-udvt.test.d.ts +2 -0
- package/dist/storage-namespaced-udvt.test.d.ts.map +1 -0
- package/dist/storage-namespaced-udvt.test.js +130 -0
- package/dist/storage-namespaced-udvt.test.js.map +1 -0
- package/dist/storage-namespaced.test.d.ts +2 -0
- package/dist/storage-namespaced.test.d.ts.map +1 -0
- package/dist/storage-namespaced.test.js +132 -0
- package/dist/storage-namespaced.test.js.map +1 -0
- package/dist/upgrade-interface-version.d.ts +3 -0
- package/dist/upgrade-interface-version.d.ts.map +1 -0
- package/dist/upgrade-interface-version.js +25 -0
- package/dist/upgrade-interface-version.js.map +1 -0
- package/dist/upgrade-interface-version.test.d.ts +2 -0
- package/dist/upgrade-interface-version.test.d.ts.map +1 -0
- package/dist/upgrade-interface-version.test.js +32 -0
- package/dist/upgrade-interface-version.test.js.map +1 -0
- package/dist/utils/annotations.d.ts +4 -0
- package/dist/utils/annotations.d.ts.map +1 -1
- package/dist/utils/annotations.js +9 -1
- package/dist/utils/annotations.js.map +1 -1
- package/dist/utils/make-namespaced.d.ts +20 -0
- package/dist/utils/make-namespaced.d.ts.map +1 -0
- package/dist/utils/make-namespaced.js +131 -0
- package/dist/utils/make-namespaced.js.map +1 -0
- package/dist/utils/make-namespaced.test.d.ts +2 -0
- package/dist/utils/make-namespaced.test.d.ts.map +1 -0
- package/dist/utils/make-namespaced.test.js +59 -0
- package/dist/utils/make-namespaced.test.js.map +1 -0
- package/dist/utils/stabilize-layout.d.ts +11 -0
- package/dist/utils/stabilize-layout.d.ts.map +1 -1
- package/dist/utils/stabilize-layout.js +5 -0
- package/dist/utils/stabilize-layout.js.map +1 -1
- package/dist/validate/overrides.js +1 -1
- package/dist/validate/overrides.js.map +1 -1
- package/dist/validate/query.d.ts.map +1 -1
- package/dist/validate/query.js +7 -3
- package/dist/validate/query.js.map +1 -1
- package/dist/validate/report.js +2 -2
- package/dist/validate/report.js.map +1 -1
- package/dist/validate/run.d.ts +18 -2
- package/dist/validate/run.d.ts.map +1 -1
- package/dist/validate/run.js +104 -2
- package/dist/validate/run.js.map +1 -1
- package/dist/validate.test.js +3 -0
- package/dist/validate.test.js.map +1 -1
- package/package.json +3 -3
- package/src/call-optional-signature.ts +20 -0
- package/src/cli/cli.test.ts.md +4 -4
- package/src/cli/cli.test.ts.snap +0 -0
- package/src/cli/validate/build-info-file.test.ts +4 -1
- package/src/cli/validate/build-info-file.ts +12 -2
- package/src/cli/validate/project-report.test.ts.md +4 -4
- package/src/cli/validate/project-report.test.ts.snap +0 -0
- package/src/cli/validate/upgradeability-assessment.ts +1 -6
- package/src/cli/validate/validate-upgrade-safety.test.ts.md +2 -2
- package/src/cli/validate/validate-upgrade-safety.test.ts.snap +0 -0
- package/src/cli/validate/validations.test.ts.md +2 -2
- package/src/cli/validate/validations.test.ts.snap +0 -0
- package/src/cli/validate/validations.ts +6 -5
- package/src/impl-address.ts +9 -20
- package/src/index.ts +3 -0
- package/src/levenshtein.ts +7 -0
- package/src/solc-api.ts +14 -0
- package/src/standalone.test.ts +26 -0
- package/src/standalone.ts +6 -2
- package/src/storage/compare.ts +34 -2
- package/src/storage/extract.ts +68 -20
- package/src/storage/index.ts +22 -1
- package/src/storage/layout.ts +1 -0
- package/src/storage/namespace.ts +262 -0
- package/src/storage/report.ts +8 -0
- package/src/storage-0.8.test.ts.md +4 -0
- package/src/storage-0.8.test.ts.snap +0 -0
- package/src/storage-memory-0.5.test.ts.md +2 -0
- package/src/storage-memory-0.5.test.ts.snap +0 -0
- package/src/storage-namespaced-conflicts-layout.test.ts +96 -0
- package/src/storage-namespaced-conflicts-layout.test.ts.md +53 -0
- package/src/storage-namespaced-conflicts-layout.test.ts.snap +0 -0
- package/src/storage-namespaced-conflicts.test.ts +54 -0
- package/src/storage-namespaced-conflicts.test.ts.md +53 -0
- package/src/storage-namespaced-conflicts.test.ts.snap +0 -0
- package/src/storage-namespaced-layout.test.ts +77 -0
- package/src/storage-namespaced-layout.test.ts.md +67 -0
- package/src/storage-namespaced-layout.test.ts.snap +0 -0
- package/src/storage-namespaced-outside-contract.test.ts +24 -0
- package/src/storage-namespaced-solc-versions.test.ts +59 -0
- package/src/storage-namespaced-udvt.test.ts +160 -0
- package/src/storage-namespaced.test.ts +146 -0
- package/src/storage-namespaced.test.ts.md +89 -0
- package/src/storage-namespaced.test.ts.snap +0 -0
- package/src/storage.test.ts.md +2 -0
- package/src/storage.test.ts.snap +0 -0
- package/src/upgrade-interface-version.test.ts +37 -0
- package/src/upgrade-interface-version.ts +27 -0
- package/src/utils/annotations.ts +8 -0
- package/src/utils/make-namespaced.test.ts +74 -0
- package/src/utils/make-namespaced.test.ts.md +154 -0
- package/src/utils/make-namespaced.test.ts.snap +0 -0
- package/src/utils/make-namespaced.ts +154 -0
- package/src/utils/stabilize-layout.ts +5 -0
- package/src/validate/overrides.ts +1 -1
- package/src/validate/query.ts +9 -3
- package/src/validate/report.ts +3 -2
- package/src/validate/run.ts +109 -2
- package/src/validate.test.ts +3 -0
package/src/impl-address.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { keccak256 } from 'ethereumjs-util';
|
|
2
1
|
import {
|
|
3
|
-
call,
|
|
4
2
|
EIP1967BeaconNotFound,
|
|
5
3
|
EIP1967ImplementationNotFound,
|
|
6
4
|
getBeaconAddress,
|
|
7
5
|
getImplementationAddress,
|
|
8
6
|
UpgradesError,
|
|
9
7
|
} from '.';
|
|
8
|
+
import { callOptionalSignature } from './call-optional-signature';
|
|
10
9
|
|
|
11
10
|
import { EthereumProvider } from './provider';
|
|
12
11
|
import { parseAddress } from './utils/address';
|
|
@@ -24,27 +23,17 @@ export async function getImplementationAddressFromBeacon(
|
|
|
24
23
|
provider: EthereumProvider,
|
|
25
24
|
beaconAddress: string,
|
|
26
25
|
): Promise<string> {
|
|
27
|
-
const
|
|
28
|
-
let
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
result = parseAddress(implAddress);
|
|
32
|
-
} catch (e: any) {
|
|
33
|
-
if (
|
|
34
|
-
!(
|
|
35
|
-
e.message.includes('function selector was not recognized') ||
|
|
36
|
-
e.message.includes('invalid opcode') ||
|
|
37
|
-
e.message.includes('revert') ||
|
|
38
|
-
e.message.includes('execution error')
|
|
39
|
-
)
|
|
40
|
-
) {
|
|
41
|
-
throw e;
|
|
42
|
-
} // otherwise fall through with no result
|
|
26
|
+
const impl = await callOptionalSignature(provider, beaconAddress, 'implementation()');
|
|
27
|
+
let parsedImplAddress;
|
|
28
|
+
if (impl !== undefined) {
|
|
29
|
+
parsedImplAddress = parseAddress(impl);
|
|
43
30
|
}
|
|
44
|
-
|
|
31
|
+
|
|
32
|
+
if (parsedImplAddress === undefined) {
|
|
45
33
|
throw new InvalidBeacon(`Contract at ${beaconAddress} doesn't look like a beacon`);
|
|
34
|
+
} else {
|
|
35
|
+
return parsedImplAddress;
|
|
46
36
|
}
|
|
47
|
-
return result;
|
|
48
37
|
}
|
|
49
38
|
|
|
50
39
|
/**
|
package/src/index.ts
CHANGED
|
@@ -57,3 +57,6 @@ export {
|
|
|
57
57
|
} from './usage-error';
|
|
58
58
|
|
|
59
59
|
export { ValidateUpgradeSafetyOptions, validateUpgradeSafety, ProjectReport, ReferenceContractNotFound } from './cli';
|
|
60
|
+
|
|
61
|
+
export { getUpgradeInterfaceVersion } from './upgrade-interface-version';
|
|
62
|
+
export { makeNamespacedInput } from './utils/make-namespaced';
|
package/src/levenshtein.ts
CHANGED
package/src/solc-api.ts
CHANGED
|
@@ -22,6 +22,13 @@ export interface SolcOutput {
|
|
|
22
22
|
id: number;
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
|
+
errors?: {
|
|
26
|
+
severity: 'error' | 'warning';
|
|
27
|
+
formattedMessage: string;
|
|
28
|
+
sourceLocation?: {
|
|
29
|
+
file: string;
|
|
30
|
+
};
|
|
31
|
+
}[];
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export interface SolcInput {
|
|
@@ -30,6 +37,13 @@ export interface SolcInput {
|
|
|
30
37
|
content?: string;
|
|
31
38
|
};
|
|
32
39
|
};
|
|
40
|
+
settings?: {
|
|
41
|
+
outputSelection?: {
|
|
42
|
+
[file in string]: {
|
|
43
|
+
[contract in string]: string[];
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
};
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
export type SolcLinkReferences = {
|
package/src/standalone.test.ts
CHANGED
|
@@ -61,3 +61,29 @@ test('dont report renamed version update', t => {
|
|
|
61
61
|
const goodReport2 = v2.getStorageUpgradeReport(v3);
|
|
62
62
|
t.true(goodReport2.ok);
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
test('namespaced output', async t => {
|
|
66
|
+
const buildInfo = await artifacts.getBuildInfo('contracts/test/Namespaced.sol:Example');
|
|
67
|
+
if (buildInfo === undefined) {
|
|
68
|
+
throw new Error('Build info not found');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const impl = new UpgradeableContract('Example', buildInfo.input, buildInfo.output, {}, '0.8.20');
|
|
72
|
+
const report = impl.getErrorReport();
|
|
73
|
+
t.true(report.ok);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('namespaced output without version', async t => {
|
|
77
|
+
const buildInfo = await artifacts.getBuildInfo('contracts/test/Namespaced.sol:Example');
|
|
78
|
+
if (buildInfo === undefined) {
|
|
79
|
+
throw new Error('Build info not found');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const error = t.throws(() => new UpgradeableContract('Example', buildInfo.input, buildInfo.output));
|
|
83
|
+
t.assert(
|
|
84
|
+
error?.message.includes(
|
|
85
|
+
`contracts/test/Namespaced.sol: Namespace annotations require Solidity version >= 0.8.20, but no solcVersion parameter was provided`,
|
|
86
|
+
),
|
|
87
|
+
error?.message,
|
|
88
|
+
);
|
|
89
|
+
});
|
package/src/standalone.ts
CHANGED
|
@@ -18,6 +18,9 @@ export interface Report {
|
|
|
18
18
|
explain(color?: boolean): string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated Use `validateUpgradeSafety` instead.
|
|
23
|
+
*/
|
|
21
24
|
export class UpgradeableContract {
|
|
22
25
|
readonly version: Version;
|
|
23
26
|
readonly errors: ValidationError[];
|
|
@@ -28,11 +31,12 @@ export class UpgradeableContract {
|
|
|
28
31
|
solcInput: SolcInput,
|
|
29
32
|
solcOutput: SolcOutput,
|
|
30
33
|
opts: ValidationOptions = {},
|
|
34
|
+
solcVersion?: string,
|
|
31
35
|
) {
|
|
32
36
|
const decodeSrc = solcInputOutputDecoder(solcInput, solcOutput);
|
|
33
|
-
const validation = validate(solcOutput, decodeSrc);
|
|
37
|
+
const validation = validate(solcOutput, decodeSrc, solcVersion, solcInput);
|
|
34
38
|
this.version = getContractVersion(validation, name);
|
|
35
|
-
this.errors = getErrors(validation, this.version, opts);
|
|
39
|
+
this.errors = getErrors(validation, this.version, withValidationDefaults(opts));
|
|
36
40
|
this.layout = getStorageLayout(validation, this.version);
|
|
37
41
|
}
|
|
38
42
|
|
package/src/storage/compare.ts
CHANGED
|
@@ -108,8 +108,40 @@ export class StorageLayoutComparator {
|
|
|
108
108
|
readonly unsafeAllowRenames = false,
|
|
109
109
|
) {}
|
|
110
110
|
|
|
111
|
-
compareLayouts(
|
|
112
|
-
|
|
111
|
+
compareLayouts(
|
|
112
|
+
original: StorageItem[],
|
|
113
|
+
updated: StorageItem[],
|
|
114
|
+
originalNamespaces?: Record<string, StorageItem[]>,
|
|
115
|
+
updatedNamespaces?: Record<string, StorageItem[]>,
|
|
116
|
+
): LayoutCompatibilityReport {
|
|
117
|
+
const ops = this.layoutLevenshtein(original, updated, { allowAppend: true });
|
|
118
|
+
const namespacedOps = this.getNamespacedStorageOperations(originalNamespaces, updatedNamespaces);
|
|
119
|
+
|
|
120
|
+
return new LayoutCompatibilityReport([...ops, ...namespacedOps]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private getNamespacedStorageOperations(
|
|
124
|
+
originalNamespaces?: Record<string, StorageItem[]>,
|
|
125
|
+
updatedNamespaces?: Record<string, StorageItem[]>,
|
|
126
|
+
) {
|
|
127
|
+
const ops: StorageOperation<StorageItem>[] = [];
|
|
128
|
+
if (originalNamespaces !== undefined) {
|
|
129
|
+
for (const [storageLocation, origNamespacedLayout] of Object.entries(originalNamespaces)) {
|
|
130
|
+
const updatedNamespacedLayout = updatedNamespaces?.[storageLocation];
|
|
131
|
+
if (updatedNamespacedLayout !== undefined) {
|
|
132
|
+
ops.push(...this.layoutLevenshtein(origNamespacedLayout, updatedNamespacedLayout, { allowAppend: true }));
|
|
133
|
+
} else if (origNamespacedLayout.length > 0) {
|
|
134
|
+
ops.push({
|
|
135
|
+
kind: 'delete-namespace',
|
|
136
|
+
namespace: storageLocation,
|
|
137
|
+
original: {
|
|
138
|
+
contract: origNamespacedLayout[0].contract,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return ops;
|
|
113
145
|
}
|
|
114
146
|
|
|
115
147
|
private layoutLevenshtein<F extends StorageField>(
|
package/src/storage/extract.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import assert from 'assert';
|
|
1
|
+
import assert from 'assert/strict';
|
|
2
2
|
import {
|
|
3
3
|
ContractDefinition,
|
|
4
4
|
StructDefinition,
|
|
5
5
|
EnumDefinition,
|
|
6
6
|
TypeDescriptions,
|
|
7
7
|
VariableDeclaration,
|
|
8
|
+
TypeName,
|
|
8
9
|
} from 'solidity-ast';
|
|
9
10
|
import { isNodeType, findAll, ASTDereferencer } from 'solidity-ast/utils';
|
|
10
|
-
import { StorageLayout, TypeItem } from './layout';
|
|
11
|
+
import { StorageLayout, StructMember, TypeItem, isStructMembers, EnumMember } from './layout';
|
|
11
12
|
import { normalizeTypeIdentifier } from '../utils/type-id';
|
|
12
13
|
import { SrcDecoder } from '../src-decoder';
|
|
13
14
|
import { mapValues } from '../utils/map-values';
|
|
14
15
|
import { pick } from '../utils/pick';
|
|
15
16
|
import { execall } from '../utils/execall';
|
|
17
|
+
import { loadNamespaces } from './namespace';
|
|
16
18
|
|
|
17
19
|
const currentLayoutVersion = '1.2';
|
|
18
20
|
|
|
@@ -20,31 +22,53 @@ export function isCurrentLayoutVersion(layout: StorageLayout): boolean {
|
|
|
20
22
|
return layout?.layoutVersion === currentLayoutVersion;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
export interface CompilationContext {
|
|
26
|
+
deref: ASTDereferencer;
|
|
27
|
+
contractDef: ContractDefinition;
|
|
28
|
+
storageLayout?: StorageLayout;
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
export function extractStorageLayout(
|
|
24
32
|
contractDef: ContractDefinition,
|
|
25
33
|
decodeSrc: SrcDecoder,
|
|
26
34
|
deref: ASTDereferencer,
|
|
27
|
-
storageLayout?: StorageLayout
|
|
35
|
+
storageLayout?: StorageLayout,
|
|
36
|
+
namespacedContext?: CompilationContext,
|
|
28
37
|
): StorageLayout {
|
|
29
38
|
const layout: StorageLayout = { storage: [], types: {}, layoutVersion: currentLayoutVersion, flat: false };
|
|
30
|
-
if (storageLayout !== undefined) {
|
|
31
|
-
layout.types = mapValues(storageLayout.types, m => {
|
|
32
|
-
return {
|
|
33
|
-
label: m.label,
|
|
34
|
-
members: m.members?.map(m =>
|
|
35
|
-
typeof m === 'string' ? m : pick(m, ['label', 'type', 'offset', 'slot']),
|
|
36
|
-
) as TypeItem['members'],
|
|
37
|
-
numberOfBytes: m.numberOfBytes,
|
|
38
|
-
};
|
|
39
|
-
});
|
|
40
39
|
|
|
40
|
+
// The namespaced context will contain the types of namespaces that may not be included
|
|
41
|
+
// in the original storage layout.
|
|
42
|
+
// Some types will be present in both and they must be exactly equivalent.
|
|
43
|
+
// If they are not, it fails an assertion because this may be a clash between different types.
|
|
44
|
+
const combinedTypes = { ...namespacedContext?.storageLayout?.types, ...storageLayout?.types };
|
|
45
|
+
if (namespacedContext?.storageLayout?.types) {
|
|
46
|
+
for (const t in storageLayout?.types) {
|
|
47
|
+
if (t in namespacedContext.storageLayout.types) {
|
|
48
|
+
assert.deepEqual(namespacedContext.storageLayout.types[t], storageLayout?.types[t]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
layout.types = mapValues(combinedTypes, m => {
|
|
54
|
+
return {
|
|
55
|
+
label: m.label,
|
|
56
|
+
members:
|
|
57
|
+
m.members && isStructMembers(m.members)
|
|
58
|
+
? m.members.map(m => pick(m, ['label', 'type', 'offset', 'slot']))
|
|
59
|
+
: m.members,
|
|
60
|
+
numberOfBytes: m.numberOfBytes,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (storageLayout !== undefined) {
|
|
41
65
|
for (const storage of storageLayout.storage) {
|
|
42
66
|
const origin = getOriginContract(contractDef, storage.astId, deref);
|
|
43
67
|
assert(origin, `Did not find variable declaration node for '${storage.label}'`);
|
|
44
68
|
const { varDecl, contract } = origin;
|
|
45
69
|
const { renamedFrom, retypedFrom } = getRetypedRenamed(varDecl);
|
|
46
70
|
// Solc layout doesn't bring members for enums so we get them using the ast method
|
|
47
|
-
loadLayoutType(varDecl, layout, deref);
|
|
71
|
+
loadLayoutType(varDecl.typeName, layout, deref);
|
|
48
72
|
const { label, offset, slot, type } = storage;
|
|
49
73
|
const src = decodeSrc(varDecl);
|
|
50
74
|
layout.storage.push({ label, offset, slot, type, contract, src, retypedFrom, renamedFrom });
|
|
@@ -65,11 +89,15 @@ export function extractStorageLayout(
|
|
|
65
89
|
renamedFrom,
|
|
66
90
|
});
|
|
67
91
|
|
|
68
|
-
loadLayoutType(varDecl, layout, deref);
|
|
92
|
+
loadLayoutType(varDecl.typeName, layout, deref);
|
|
69
93
|
}
|
|
70
94
|
}
|
|
71
95
|
}
|
|
72
96
|
}
|
|
97
|
+
|
|
98
|
+
const origContext = { deref, contractDef, storageLayout };
|
|
99
|
+
loadNamespaces(decodeSrc, layout, origContext, namespacedContext);
|
|
100
|
+
|
|
73
101
|
return layout;
|
|
74
102
|
}
|
|
75
103
|
|
|
@@ -92,14 +120,34 @@ function typeDescriptions(x: { typeDescriptions: TypeDescriptions }): RequiredTy
|
|
|
92
120
|
return x.typeDescriptions as RequiredTypeDescriptions;
|
|
93
121
|
}
|
|
94
122
|
|
|
95
|
-
|
|
123
|
+
type GotTypeMembers<
|
|
124
|
+
D extends EnumDefinition | StructDefinition,
|
|
125
|
+
F extends 'src' | 'typeName',
|
|
126
|
+
> = D extends EnumDefinition ? EnumMember[] : (StructMember & Pick<VariableDeclaration, F>)[];
|
|
127
|
+
|
|
128
|
+
export function getTypeMembers<D extends EnumDefinition | StructDefinition>(typeDef: D): GotTypeMembers<D, never>;
|
|
129
|
+
export function getTypeMembers<D extends EnumDefinition | StructDefinition, F extends 'src' | 'typeName'>(
|
|
130
|
+
typeDef: D,
|
|
131
|
+
includeFields: { [f in F]: true },
|
|
132
|
+
): GotTypeMembers<D, F>;
|
|
133
|
+
export function getTypeMembers(
|
|
134
|
+
typeDef: StructDefinition | EnumDefinition,
|
|
135
|
+
includeFields: { src?: boolean; typeName?: boolean } = {},
|
|
136
|
+
): TypeItem['members'] {
|
|
96
137
|
if (typeDef.nodeType === 'StructDefinition') {
|
|
97
138
|
return typeDef.members.map(m => {
|
|
98
139
|
assert(typeof m.typeDescriptions.typeIdentifier === 'string');
|
|
99
|
-
|
|
140
|
+
const member: StructMember & Partial<Pick<VariableDeclaration, 'src' | 'typeName'>> = {
|
|
100
141
|
label: m.name,
|
|
101
142
|
type: normalizeTypeIdentifier(m.typeDescriptions.typeIdentifier),
|
|
102
143
|
};
|
|
144
|
+
if (includeFields.src && m.src) {
|
|
145
|
+
member.src = m.src;
|
|
146
|
+
}
|
|
147
|
+
if (includeFields.typeName && m.typeName) {
|
|
148
|
+
member.typeName = m.typeName;
|
|
149
|
+
}
|
|
150
|
+
return member;
|
|
103
151
|
});
|
|
104
152
|
} else {
|
|
105
153
|
return typeDef.members.map(m => m.name);
|
|
@@ -116,16 +164,16 @@ function getOriginContract(contract: ContractDefinition, astId: number | undefin
|
|
|
116
164
|
}
|
|
117
165
|
}
|
|
118
166
|
|
|
119
|
-
function loadLayoutType(
|
|
167
|
+
export function loadLayoutType(typeName: TypeName | null | undefined, layout: StorageLayout, deref: ASTDereferencer) {
|
|
120
168
|
// Note: A UserDefinedTypeName can also refer to a ContractDefinition but we won't care about those.
|
|
121
169
|
const derefUserDefinedType = deref(['StructDefinition', 'EnumDefinition', 'UserDefinedValueTypeDefinition']);
|
|
122
170
|
|
|
123
|
-
assert(
|
|
171
|
+
assert(typeName != null);
|
|
124
172
|
|
|
125
173
|
// We will recursively look for all types involved in this variable declaration in order to store their type
|
|
126
174
|
// information. We iterate over a Map that is indexed by typeIdentifier to ensure we visit each type only once.
|
|
127
175
|
// Note that there can be recursive types.
|
|
128
|
-
const typeNames = new Map([...findTypeNames(
|
|
176
|
+
const typeNames = new Map([...findTypeNames(typeName)].map(n => [typeDescriptions(n).typeIdentifier, n]));
|
|
129
177
|
|
|
130
178
|
for (const typeName of typeNames.values()) {
|
|
131
179
|
const { typeIdentifier, typeString: label } = typeDescriptions(typeName);
|
package/src/storage/index.ts
CHANGED
|
@@ -43,8 +43,16 @@ export function getStorageUpgradeReport(
|
|
|
43
43
|
): LayoutCompatibilityReport {
|
|
44
44
|
const originalDetailed = getDetailedLayout(original);
|
|
45
45
|
const updatedDetailed = getDetailedLayout(updated);
|
|
46
|
+
const originalDetailedNamespaces = getDetailedNamespacedLayout(original);
|
|
47
|
+
const updatedDetailedNamespaces = getDetailedNamespacedLayout(updated);
|
|
48
|
+
|
|
46
49
|
const comparator = new StorageLayoutComparator(opts.unsafeAllowCustomTypes, opts.unsafeAllowRenames);
|
|
47
|
-
const report = comparator.compareLayouts(
|
|
50
|
+
const report = comparator.compareLayouts(
|
|
51
|
+
originalDetailed,
|
|
52
|
+
updatedDetailed,
|
|
53
|
+
originalDetailedNamespaces,
|
|
54
|
+
updatedDetailedNamespaces,
|
|
55
|
+
);
|
|
48
56
|
|
|
49
57
|
if (comparator.hasAllowedUncheckedCustomTypes) {
|
|
50
58
|
logWarning(`Potentially unsafe deployment`, [
|
|
@@ -58,6 +66,19 @@ export function getStorageUpgradeReport(
|
|
|
58
66
|
return report;
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
function getDetailedNamespacedLayout(layout: StorageLayout): Record<string, StorageItem[]> {
|
|
70
|
+
const detailedNamespaces: Record<string, StorageItem[]> = {};
|
|
71
|
+
if (layout.namespaces !== undefined) {
|
|
72
|
+
for (const [storageLocation, namespacedLayout] of Object.entries(layout.namespaces)) {
|
|
73
|
+
detailedNamespaces[storageLocation] = getDetailedLayout({
|
|
74
|
+
storage: namespacedLayout,
|
|
75
|
+
types: layout.types,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return detailedNamespaces;
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
export class StorageUpgradeErrors extends UpgradesError {
|
|
62
83
|
constructor(readonly report: LayoutCompatibilityReport) {
|
|
63
84
|
super(`New storage layout is incompatible`, () => report.explain());
|
package/src/storage/layout.ts
CHANGED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { ContractDefinition, StructDefinition } from 'solidity-ast';
|
|
3
|
+
import { isNodeType } from 'solidity-ast/utils';
|
|
4
|
+
import { StorageItem, StorageLayout, TypeItem, isStructMembers } from './layout';
|
|
5
|
+
import { SrcDecoder } from '../src-decoder';
|
|
6
|
+
import { getAnnotationArgs, getDocumentation, hasAnnotationTag } from '../utils/annotations';
|
|
7
|
+
import { Node } from 'solidity-ast/node';
|
|
8
|
+
import { CompilationContext, getTypeMembers, loadLayoutType } from './extract';
|
|
9
|
+
import { UpgradesError } from '../error';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Loads a contract's namespaces and namespaced type information into the storage layout.
|
|
13
|
+
*
|
|
14
|
+
* The provided compilation contexts must include both the original compilation and optionally
|
|
15
|
+
* a namespaced compilation where contracts have been modified to include namespaced type information.
|
|
16
|
+
*
|
|
17
|
+
* If namespaced compilation is included, storage slots and offsets will be included in the loaded namespaces and types.
|
|
18
|
+
*
|
|
19
|
+
* This function looks up namespaces and their members from the namespaced compilation context's AST if available
|
|
20
|
+
* (meaning node ids would be from the namespaced compilation), and looks up slots and offsets from the compiled type information.
|
|
21
|
+
* However, it saves the original source locations from the original context so that line numbers are
|
|
22
|
+
* consistent with the original source code.
|
|
23
|
+
*
|
|
24
|
+
* @param decodeSrc Source decoder for the original source code.
|
|
25
|
+
* @param layout The storage layout object to load namespaces into.
|
|
26
|
+
* @param origContext The original compilation context, which is used to lookup original source locations.
|
|
27
|
+
* @param namespacedContext The namespaced compilation context, which represents a namespaced compilation.
|
|
28
|
+
*/
|
|
29
|
+
export function loadNamespaces(
|
|
30
|
+
decodeSrc: SrcDecoder,
|
|
31
|
+
layout: StorageLayout,
|
|
32
|
+
origContext: CompilationContext,
|
|
33
|
+
namespacedContext?: CompilationContext,
|
|
34
|
+
) {
|
|
35
|
+
const namespacesWithSrc: Record<string, NamespaceWithSrc> = {};
|
|
36
|
+
|
|
37
|
+
const origLinearized = origContext.contractDef.linearizedBaseContracts.map(id =>
|
|
38
|
+
getReferencedContract(origContext, id),
|
|
39
|
+
);
|
|
40
|
+
const namespacedLinearized = namespacedContext?.contractDef.linearizedBaseContracts.map(id =>
|
|
41
|
+
getReferencedContract(namespacedContext, id),
|
|
42
|
+
);
|
|
43
|
+
assert(namespacedLinearized === undefined || origLinearized.length === namespacedLinearized.length);
|
|
44
|
+
const context = namespacedContext ?? origContext;
|
|
45
|
+
const linearized = namespacedLinearized ?? origLinearized;
|
|
46
|
+
for (const [i, contractDef] of linearized.entries()) {
|
|
47
|
+
const origContractDef = origLinearized[i];
|
|
48
|
+
const contractContext = { ...context, contractDef };
|
|
49
|
+
addContractNamespacesWithSrc(
|
|
50
|
+
namespacesWithSrc,
|
|
51
|
+
decodeSrc,
|
|
52
|
+
layout,
|
|
53
|
+
contractContext,
|
|
54
|
+
origContractDef,
|
|
55
|
+
origContext.contractDef.canonicalName ?? origContext.contractDef.name,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Add to layout without the namespaced structs' src locations, since those are no longer needed
|
|
60
|
+
// as they were only used to give duplicate namespace errors above.
|
|
61
|
+
layout.namespaces = Object.fromEntries(
|
|
62
|
+
Object.entries(namespacesWithSrc).map(([id, namespaceWithSrc]) => [id, namespaceWithSrc.namespace]),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class DuplicateNamespaceError extends UpgradesError {
|
|
67
|
+
constructor(id: string, contractName: string, src1: string, src2: string) {
|
|
68
|
+
super(
|
|
69
|
+
`Namespace ${id} is defined multiple times for contract ${contractName}`,
|
|
70
|
+
() => `\
|
|
71
|
+
The namespace ${id} was found in structs at the following locations:
|
|
72
|
+
- ${src1}
|
|
73
|
+
- ${src2}
|
|
74
|
+
|
|
75
|
+
Use a unique namespace id for each struct annotated with '@custom:storage-location erc7201:<NAMESPACE_ID>' in your contract and its inherited contracts.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Namespaced storage items, along with the original source location of the namespace struct.
|
|
82
|
+
*/
|
|
83
|
+
interface NamespaceWithSrc {
|
|
84
|
+
namespace: StorageItem<string>[];
|
|
85
|
+
src: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Gets the contract definition for the given referenced id.
|
|
90
|
+
*/
|
|
91
|
+
function getReferencedContract(context: CompilationContext, referencedId: number) {
|
|
92
|
+
// Optimization to avoid dereferencing if the referenced id is the same as the current contract
|
|
93
|
+
return context.contractDef.id === referencedId
|
|
94
|
+
? context.contractDef
|
|
95
|
+
: context.deref(['ContractDefinition'], referencedId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Add namespaces and source locations for the given compilation context's contract.
|
|
100
|
+
* Does not include inherited contracts.
|
|
101
|
+
*
|
|
102
|
+
* @param namespacesWithSrc The record of namespaces with source locations to add to.
|
|
103
|
+
* @param decodeSrc Source decoder for the original source code.
|
|
104
|
+
* @param layout The storage layout object to load types into.
|
|
105
|
+
* @param contractContext The compilation context for this specific contract to load namespaces for.
|
|
106
|
+
* @param origContractDef The AST node for this specific contract but from the original compilation context.
|
|
107
|
+
* @param leastDerivedContractName The name of the least derived contract in the inheritance list.
|
|
108
|
+
* @throws DuplicateNamespaceError if a duplicate namespace is found when adding to the `namespaces` record.
|
|
109
|
+
*/
|
|
110
|
+
function addContractNamespacesWithSrc(
|
|
111
|
+
namespacesWithSrc: Record<string, NamespaceWithSrc>,
|
|
112
|
+
decodeSrc: SrcDecoder,
|
|
113
|
+
layout: StorageLayout,
|
|
114
|
+
contractContext: CompilationContext,
|
|
115
|
+
origContractDef: ContractDefinition,
|
|
116
|
+
leastDerivedContractName: string,
|
|
117
|
+
) {
|
|
118
|
+
for (const node of contractContext.contractDef.nodes) {
|
|
119
|
+
if (isNodeType('StructDefinition', node)) {
|
|
120
|
+
const storageLocation = getStorageLocationAnnotation(node);
|
|
121
|
+
if (storageLocation !== undefined) {
|
|
122
|
+
const origSrc = decodeSrc(getOriginalStruct(node.canonicalName, origContractDef));
|
|
123
|
+
|
|
124
|
+
if (namespacesWithSrc[storageLocation] !== undefined) {
|
|
125
|
+
throw new DuplicateNamespaceError(
|
|
126
|
+
storageLocation,
|
|
127
|
+
leastDerivedContractName,
|
|
128
|
+
namespacesWithSrc[storageLocation].src,
|
|
129
|
+
origSrc,
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
namespacesWithSrc[storageLocation] = {
|
|
133
|
+
namespace: getNamespacedStorageItems(node, decodeSrc, layout, contractContext, origContractDef),
|
|
134
|
+
src: origSrc,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Gets the storage location string from the `@custom:storage-location` annotation.
|
|
144
|
+
*
|
|
145
|
+
* For example, when using ERC-7201 (https://eips.ethereum.org/EIPS/eip-7201), the result will be `erc7201:<NAMESPACE_ID>`
|
|
146
|
+
*
|
|
147
|
+
* @param node The node that may have a `@custom:storage-location` annotation.
|
|
148
|
+
* @returns The storage location string, or undefined if the node does not have a `@custom:storage-location` annotation.
|
|
149
|
+
* @throws Error if the node has the annotation `@custom:storage-location` but it does not have exactly one argument.
|
|
150
|
+
*/
|
|
151
|
+
export function getStorageLocationAnnotation(node: Node) {
|
|
152
|
+
const doc = getDocumentation(node);
|
|
153
|
+
if (hasAnnotationTag(doc, 'storage-location')) {
|
|
154
|
+
const storageLocationArgs = getAnnotationArgs(doc, 'storage-location');
|
|
155
|
+
if (storageLocationArgs.length !== 1) {
|
|
156
|
+
throw new Error('@custom:storage-location annotation must have exactly one argument');
|
|
157
|
+
}
|
|
158
|
+
return storageLocationArgs[0];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Gets the storage items for the given struct node.
|
|
164
|
+
* Includes loading recursive type information, and adds slot and offset if they are available in the given compilation context's layout.
|
|
165
|
+
*/
|
|
166
|
+
function getNamespacedStorageItems(
|
|
167
|
+
node: StructDefinition,
|
|
168
|
+
decodeSrc: SrcDecoder,
|
|
169
|
+
layout: StorageLayout,
|
|
170
|
+
context: CompilationContext,
|
|
171
|
+
origContractDef: ContractDefinition,
|
|
172
|
+
): StorageItem[] {
|
|
173
|
+
const storageItems: StorageItem[] = [];
|
|
174
|
+
|
|
175
|
+
for (const astMember of getTypeMembers(node, { typeName: true })) {
|
|
176
|
+
const item: StorageItem = {
|
|
177
|
+
contract: context.contractDef.name,
|
|
178
|
+
label: astMember.label,
|
|
179
|
+
type: astMember.type,
|
|
180
|
+
src: decodeSrc({ src: getOriginalMemberSrc(node.canonicalName, astMember.label, origContractDef) }),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const layoutMember = findLayoutStructMember(
|
|
184
|
+
context.storageLayout?.types ?? {},
|
|
185
|
+
node.canonicalName,
|
|
186
|
+
astMember.label,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (layoutMember?.offset !== undefined && layoutMember?.slot !== undefined) {
|
|
190
|
+
item.offset = layoutMember.offset;
|
|
191
|
+
item.slot = layoutMember.slot;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
storageItems.push(item);
|
|
195
|
+
|
|
196
|
+
// If context is namespaced, we have storage layout, and this will fill in enum members just like in extractStorageLayout.
|
|
197
|
+
// If context is original, this will add the types from the namespace structs to the layout.
|
|
198
|
+
loadLayoutType(astMember.typeName, layout, context.deref);
|
|
199
|
+
}
|
|
200
|
+
return storageItems;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Gets the struct definition matching the given canonical name from the original contract definition.
|
|
205
|
+
*/
|
|
206
|
+
function getOriginalStruct(structCanonicalName: string, origContractDef: ContractDefinition) {
|
|
207
|
+
for (const node of origContractDef.nodes) {
|
|
208
|
+
if (isNodeType('StructDefinition', node)) {
|
|
209
|
+
if (node.canonicalName === structCanonicalName) {
|
|
210
|
+
return node;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Could not find original source location for namespace struct with name ${structCanonicalName} from contract ${origContractDef.name}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Gets the original source location for the given struct canonical name and struct member label.
|
|
221
|
+
*/
|
|
222
|
+
function getOriginalMemberSrc(structCanonicalName: string, memberLabel: string, origContractDef: ContractDefinition) {
|
|
223
|
+
const node = getOriginalStruct(structCanonicalName, origContractDef);
|
|
224
|
+
if (node !== undefined) {
|
|
225
|
+
for (const member of getTypeMembers(node, { src: true })) {
|
|
226
|
+
if (member.label === memberLabel) {
|
|
227
|
+
return member.src;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Could not find original source location for namespace struct with name ${structCanonicalName} and member ${memberLabel}`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* From the given layout types, gets the struct member matching the given struct canonical name and struct member label.
|
|
239
|
+
*/
|
|
240
|
+
function findLayoutStructMember(
|
|
241
|
+
types: Record<string, TypeItem<string>>,
|
|
242
|
+
structCanonicalName: string,
|
|
243
|
+
memberLabel: string,
|
|
244
|
+
) {
|
|
245
|
+
const structType = findTypeWithLabel(types, `struct ${structCanonicalName}`);
|
|
246
|
+
const structMembers = structType?.members;
|
|
247
|
+
if (structMembers !== undefined) {
|
|
248
|
+
assert(isStructMembers(structMembers));
|
|
249
|
+
for (const structMember of structMembers) {
|
|
250
|
+
if (structMember.label === memberLabel) {
|
|
251
|
+
return structMember;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* From the given layout types, gets the type matching the given type label.
|
|
259
|
+
*/
|
|
260
|
+
function findTypeWithLabel(types: Record<string, TypeItem>, label: string) {
|
|
261
|
+
return Object.values(types).find(type => type.label === label);
|
|
262
|
+
}
|