@openzeppelin/upgrades-core 1.12.0 → 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.
- package/artifacts/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol/ERC1967Proxy.dbg.json +1 -1
- package/artifacts/@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol/BeaconProxy.dbg.json +1 -1
- package/artifacts/@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol/UpgradeableBeacon.dbg.json +1 -1
- package/dist/add-proxy-to-manifest.d.ts +3 -0
- package/dist/add-proxy-to-manifest.d.ts.map +1 -0
- package/dist/add-proxy-to-manifest.js +15 -0
- package/dist/add-proxy-to-manifest.js.map +1 -0
- package/dist/deployment.d.ts +21 -1
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +65 -15
- package/dist/deployment.js.map +1 -1
- package/dist/eip-1967-type.d.ts +1 -0
- package/dist/eip-1967-type.d.ts.map +1 -1
- package/dist/eip-1967-type.js +6 -1
- package/dist/eip-1967-type.js.map +1 -1
- package/dist/eip-1967.d.ts +1 -0
- package/dist/eip-1967.d.ts.map +1 -1
- package/dist/eip-1967.js +3 -2
- package/dist/eip-1967.js.map +1 -1
- package/dist/impl-address.js +1 -1
- package/dist/impl-address.js.map +1 -1
- package/dist/impl-store.d.ts +22 -2
- package/dist/impl-store.d.ts.map +1 -1
- package/dist/impl-store.js +59 -15
- package/dist/impl-store.js.map +1 -1
- package/dist/impl-store.test.js +24 -0
- package/dist/impl-store.test.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest-storage-layout.d.ts.map +1 -1
- package/dist/manifest-storage-layout.js +3 -2
- package/dist/manifest-storage-layout.js.map +1 -1
- package/dist/manifest-storage-layout.test.js.map +1 -1
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +6 -4
- package/dist/manifest.js.map +1 -1
- package/dist/manifest.test.js +32 -0
- package/dist/manifest.test.js.map +1 -1
- package/dist/provider.d.ts +1 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +6 -2
- package/dist/provider.js.map +1 -1
- package/dist/proxy-kind.d.ts +9 -0
- package/dist/proxy-kind.d.ts.map +1 -1
- package/dist/proxy-kind.js +27 -1
- package/dist/proxy-kind.js.map +1 -1
- package/dist/scripts/migrate-oz-cli-project.test.js +1 -1
- package/dist/scripts/migrate-oz-cli-project.test.js.map +1 -1
- package/dist/solc-api.d.ts +5 -0
- package/dist/solc-api.d.ts.map +1 -1
- package/dist/standalone.test.js.map +1 -1
- package/dist/storage/compare.d.ts +15 -4
- package/dist/storage/compare.d.ts.map +1 -1
- package/dist/storage/compare.js +46 -4
- package/dist/storage/compare.js.map +1 -1
- package/dist/storage/extract.d.ts +1 -1
- package/dist/storage/extract.d.ts.map +1 -1
- package/dist/storage/extract.js +96 -38
- package/dist/storage/extract.js.map +1 -1
- package/dist/storage/layout.d.ts +18 -0
- package/dist/storage/layout.d.ts.map +1 -1
- package/dist/storage/layout.js +6 -2
- package/dist/storage/layout.js.map +1 -1
- package/dist/storage/report-rename-retype.test.d.ts +2 -0
- package/dist/storage/report-rename-retype.test.d.ts.map +1 -0
- package/dist/storage/report-rename-retype.test.js +93 -0
- package/dist/storage/report-rename-retype.test.js.map +1 -0
- package/dist/storage/report.d.ts.map +1 -1
- package/dist/storage/report.js +32 -2
- package/dist/storage/report.js.map +1 -1
- package/dist/storage/report.test.js +6 -0
- package/dist/storage/report.test.js.map +1 -1
- package/dist/storage.test.js +11 -3
- package/dist/storage.test.js.map +1 -1
- package/dist/usage-error.d.ts +3 -0
- package/dist/usage-error.d.ts.map +1 -1
- package/dist/usage-error.js +7 -1
- package/dist/usage-error.js.map +1 -1
- package/dist/utils/execall.d.ts +2 -0
- package/dist/utils/execall.d.ts.map +1 -0
- package/dist/utils/execall.js +17 -0
- package/dist/utils/execall.js.map +1 -0
- package/dist/validate/query.d.ts.map +1 -1
- package/dist/validate/query.js +10 -5
- package/dist/validate/query.js.map +1 -1
- package/dist/validate/run.d.ts.map +1 -1
- package/dist/validate/run.js +1 -1
- package/dist/validate/run.js.map +1 -1
- package/dist/validate.test.js.map +1 -1
- package/package.json +4 -4
- package/src/add-proxy-to-manifest.ts +12 -0
- package/src/deployment.ts +81 -14
- package/src/eip-1967-type.ts +7 -0
- package/src/eip-1967.ts +4 -3
- package/src/impl-address.ts +1 -1
- package/src/impl-store.test.ts +33 -1
- package/src/impl-store.ts +69 -12
- package/src/index.ts +22 -4
- package/src/manifest-storage-layout.test.ts +3 -2
- package/src/manifest-storage-layout.ts +8 -3
- package/src/manifest.test.ts +34 -1
- package/src/manifest.ts +9 -4
- package/src/provider.ts +5 -1
- package/src/proxy-kind.ts +24 -1
- package/src/scripts/migrate-oz-cli-project.test.ts +1 -1
- package/src/scripts/migrate-oz-cli-project.test.ts.snap +0 -0
- package/src/solc-api.ts +5 -0
- package/src/standalone.test.ts +2 -2
- package/src/storage/compare.ts +57 -8
- package/src/storage/extract.ts +111 -52
- package/src/storage/layout.ts +21 -2
- package/src/storage/report-rename-retype.test.ts +106 -0
- package/src/storage/report-rename-retype.test.ts.md +44 -0
- package/src/storage/report-rename-retype.test.ts.snap +0 -0
- package/src/storage/report.test.ts +9 -2
- package/src/storage/report.test.ts.md +11 -0
- package/src/storage/report.test.ts.snap +0 -0
- package/src/storage/report.ts +39 -4
- package/src/storage.test.ts +15 -5
- package/src/storage.test.ts.md +348 -109
- package/src/storage.test.ts.snap +0 -0
- package/src/usage-error.ts +9 -0
- package/src/utils/execall.ts +11 -0
- package/src/utils/parse-type-id.test.ts.md +106 -106
- package/src/utils/parse-type-id.test.ts.snap +0 -0
- package/src/validate/query.ts +9 -5
- package/src/validate/run.ts +6 -1
- package/src/validate.test.ts +2 -2
package/src/eip-1967.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { EthereumProvider, getStorageAt } from './provider';
|
|
|
5
5
|
import { parseAddress } from './utils/address';
|
|
6
6
|
|
|
7
7
|
export class EIP1967ImplementationNotFound extends UpgradesError {}
|
|
8
|
-
|
|
9
8
|
export class EIP1967BeaconNotFound extends UpgradesError {}
|
|
10
9
|
|
|
11
10
|
export async function getAdminAddress(provider: EthereumProvider, address: string): Promise<string> {
|
|
@@ -28,7 +27,9 @@ export async function getImplementationAddress(provider: EthereumProvider, addre
|
|
|
28
27
|
);
|
|
29
28
|
|
|
30
29
|
if (isEmptySlot(storage)) {
|
|
31
|
-
throw new EIP1967ImplementationNotFound(
|
|
30
|
+
throw new EIP1967ImplementationNotFound(
|
|
31
|
+
`Contract at ${address} doesn't look like an ERC 1967 proxy with a logic contract address`,
|
|
32
|
+
);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
return parseAddressFromStorage(storage);
|
|
@@ -67,7 +68,7 @@ export function toEip1967Hash(label: string): string {
|
|
|
67
68
|
return '0x' + bigNumber.toString(16);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function isEmptySlot(storage: string): boolean {
|
|
71
|
+
export function isEmptySlot(storage: string): boolean {
|
|
71
72
|
storage = storage.replace(/^0x/, '');
|
|
72
73
|
return new BN(storage, 'hex').isZero();
|
|
73
74
|
}
|
package/src/impl-address.ts
CHANGED
|
@@ -34,7 +34,7 @@ export async function getImplementationAddressFromBeacon(
|
|
|
34
34
|
!(
|
|
35
35
|
e.message.includes('function selector was not recognized') ||
|
|
36
36
|
e.message.includes('invalid opcode') ||
|
|
37
|
-
e.message.includes('
|
|
37
|
+
e.message.includes('revert')
|
|
38
38
|
)
|
|
39
39
|
) {
|
|
40
40
|
throw e;
|
package/src/impl-store.test.ts
CHANGED
|
@@ -6,9 +6,10 @@ import util from 'util';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import os from 'os';
|
|
8
8
|
|
|
9
|
-
import { fetchOrDeploy } from './impl-store';
|
|
9
|
+
import { fetchOrDeploy, mergeAddresses } from './impl-store';
|
|
10
10
|
import { getVersion } from './version';
|
|
11
11
|
import { stubProvider } from './stub-provider';
|
|
12
|
+
import { ImplDeployment } from './manifest';
|
|
12
13
|
|
|
13
14
|
const rimraf = util.promisify(rimrafAsync);
|
|
14
15
|
|
|
@@ -57,3 +58,34 @@ test('cleans up invalid deployment', async t => {
|
|
|
57
58
|
// the failed deployment has been cleaned up
|
|
58
59
|
await fetchOrDeploy(version1, provider2, provider2.deploy);
|
|
59
60
|
});
|
|
61
|
+
|
|
62
|
+
test('merge addresses', async t => {
|
|
63
|
+
const depl1 = { address: '0x1' } as ImplDeployment;
|
|
64
|
+
const depl2 = { address: '0x2' } as ImplDeployment;
|
|
65
|
+
|
|
66
|
+
const { address, allAddresses } = await mergeAddresses(depl1, depl2);
|
|
67
|
+
t.is(address, '0x1');
|
|
68
|
+
t.true(unorderedEqual(allAddresses, ['0x1', '0x2']), allAddresses.toString());
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('merge multiple existing addresses', async t => {
|
|
72
|
+
const depl1 = { address: '0x1', allAddresses: ['0x1a', '0x1b'] } as ImplDeployment;
|
|
73
|
+
const depl2 = { address: '0x2' } as ImplDeployment;
|
|
74
|
+
|
|
75
|
+
const { address, allAddresses } = await mergeAddresses(depl1, depl2);
|
|
76
|
+
t.is(address, '0x1');
|
|
77
|
+
t.true(unorderedEqual(allAddresses, ['0x1', '0x1a', '0x1b', '0x2']), allAddresses.toString());
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('merge all addresses', async t => {
|
|
81
|
+
const depl1 = { address: '0x1', allAddresses: ['0x1a', '0x1b'] } as ImplDeployment;
|
|
82
|
+
const depl2 = { address: '0x2', allAddresses: ['0x2a', '0x2b'] } as ImplDeployment;
|
|
83
|
+
|
|
84
|
+
const { address, allAddresses } = await mergeAddresses(depl1, depl2);
|
|
85
|
+
t.is(address, '0x1');
|
|
86
|
+
t.true(unorderedEqual(allAddresses, ['0x1', '0x1a', '0x1b', '0x2', '0x2a', '0x2b']), allAddresses.toString());
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function unorderedEqual(arr1: string[], arr2: string[]) {
|
|
90
|
+
return arr1.every(i => arr2.includes(i)) && arr2.every(i => arr1.includes(i));
|
|
91
|
+
}
|
package/src/impl-store.ts
CHANGED
|
@@ -2,7 +2,7 @@ import debug from './utils/debug';
|
|
|
2
2
|
import { Manifest, ManifestData, ImplDeployment } from './manifest';
|
|
3
3
|
import { EthereumProvider, isDevelopmentNetwork } from './provider';
|
|
4
4
|
import { Deployment, InvalidDeployment, resumeOrDeploy, waitAndValidateDeployment } from './deployment';
|
|
5
|
-
import
|
|
5
|
+
import { Version } from './version';
|
|
6
6
|
import assert from 'assert';
|
|
7
7
|
import { DeployOpts } from '.';
|
|
8
8
|
|
|
@@ -12,9 +12,10 @@ interface ManifestLens<T> {
|
|
|
12
12
|
(data: ManifestData): ManifestField<T>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
interface ManifestField<T> {
|
|
15
|
+
export interface ManifestField<T> {
|
|
16
16
|
get(): T | undefined;
|
|
17
17
|
set(value: T | undefined): void;
|
|
18
|
+
merge?(value: T | undefined): void;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -24,6 +25,7 @@ interface ManifestField<T> {
|
|
|
24
25
|
* @param provider the Ethereum provider
|
|
25
26
|
* @param deploy the deploy function
|
|
26
27
|
* @param opts options containing the timeout and pollingInterval parameters. If undefined, assumes the timeout is not configurable and will not mention those parameters in the error message for TransactionMinedTimeout.
|
|
28
|
+
* @param merge if true, adds a deployment to existing deployment by merging their addresses. Defaults to false.
|
|
27
29
|
* @returns the deployment address
|
|
28
30
|
* @throws {InvalidDeployment} if the deployment is invalid
|
|
29
31
|
* @throws {TransactionMinedTimeout} if the transaction was not confirmed within the timeout period
|
|
@@ -33,6 +35,7 @@ async function fetchOrDeployGeneric<T extends Deployment>(
|
|
|
33
35
|
provider: EthereumProvider,
|
|
34
36
|
deploy: () => Promise<T>,
|
|
35
37
|
opts?: DeployOpts,
|
|
38
|
+
merge?: boolean,
|
|
36
39
|
): Promise<string> {
|
|
37
40
|
const manifest = await Manifest.forNetwork(provider);
|
|
38
41
|
|
|
@@ -41,14 +44,24 @@ async function fetchOrDeployGeneric<T extends Deployment>(
|
|
|
41
44
|
debug('fetching deployment of', lens.description);
|
|
42
45
|
const data = await manifest.read();
|
|
43
46
|
const deployment = lens(data);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
if (merge && !deployment.merge) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'fetchOrDeployGeneric was called with merge set to true but the deployment lens does not have a merge function',
|
|
50
|
+
);
|
|
47
51
|
}
|
|
48
|
-
|
|
52
|
+
|
|
53
|
+
const stored = deployment.get();
|
|
54
|
+
const updated = await resumeOrDeploy(provider, stored, deploy, lens.type, opts, deployment, merge);
|
|
49
55
|
if (updated !== stored) {
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
if (merge && deployment.merge) {
|
|
57
|
+
// only check primary addresses for clashes, since the address could already exist in an allAddresses field
|
|
58
|
+
// but the above updated and stored objects are different instances representing the same entry
|
|
59
|
+
await checkForAddressClash(provider, data, updated, false);
|
|
60
|
+
deployment.merge(updated);
|
|
61
|
+
} else {
|
|
62
|
+
await checkForAddressClash(provider, data, updated, true);
|
|
63
|
+
deployment.set(updated);
|
|
64
|
+
}
|
|
52
65
|
await manifest.write(data);
|
|
53
66
|
}
|
|
54
67
|
return updated;
|
|
@@ -77,21 +90,57 @@ async function fetchOrDeployGeneric<T extends Deployment>(
|
|
|
77
90
|
}
|
|
78
91
|
}
|
|
79
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Deletes the deployment by setting it to undefined.
|
|
95
|
+
* Should only be used during a manifest run.
|
|
96
|
+
*/
|
|
97
|
+
export function deleteDeployment(deployment: ManifestField<Deployment>) {
|
|
98
|
+
deployment.set(undefined);
|
|
99
|
+
}
|
|
100
|
+
|
|
80
101
|
export async function fetchOrDeploy(
|
|
81
102
|
version: Version,
|
|
82
103
|
provider: EthereumProvider,
|
|
83
104
|
deploy: () => Promise<ImplDeployment>,
|
|
84
105
|
opts?: DeployOpts,
|
|
106
|
+
merge?: boolean,
|
|
85
107
|
): Promise<string> {
|
|
86
|
-
return fetchOrDeployGeneric(implLens(version.linkedWithoutMetadata), provider, deploy, opts);
|
|
108
|
+
return fetchOrDeployGeneric(implLens(version.linkedWithoutMetadata), provider, deploy, opts, merge);
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
const implLens = (versionWithoutMetadata: string) =>
|
|
90
112
|
lens(`implementation ${versionWithoutMetadata}`, 'implementation', data => ({
|
|
91
113
|
get: () => data.impls[versionWithoutMetadata],
|
|
92
114
|
set: (value?: ImplDeployment) => (data.impls[versionWithoutMetadata] = value),
|
|
115
|
+
merge: (value?: ImplDeployment) => {
|
|
116
|
+
const existing = data.impls[versionWithoutMetadata];
|
|
117
|
+
if (existing !== undefined && value !== undefined) {
|
|
118
|
+
const { address, allAddresses } = mergeAddresses(existing, value);
|
|
119
|
+
data.impls[versionWithoutMetadata] = { ...existing, address, allAddresses };
|
|
120
|
+
} else {
|
|
121
|
+
data.impls[versionWithoutMetadata] = value;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
93
124
|
}));
|
|
94
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Merge the addresses in the deployments and returns them.
|
|
128
|
+
*
|
|
129
|
+
* @param existing existing deployment
|
|
130
|
+
* @param value deployment to add
|
|
131
|
+
*/
|
|
132
|
+
export function mergeAddresses(existing: ImplDeployment, value: ImplDeployment) {
|
|
133
|
+
const merged = new Set<string>();
|
|
134
|
+
|
|
135
|
+
merged.add(existing.address);
|
|
136
|
+
merged.add(value.address);
|
|
137
|
+
|
|
138
|
+
existing.allAddresses?.forEach(item => merged.add(item));
|
|
139
|
+
value.allAddresses?.forEach(item => merged.add(item));
|
|
140
|
+
|
|
141
|
+
return { address: existing.address, allAddresses: Array.from(merged) };
|
|
142
|
+
}
|
|
143
|
+
|
|
95
144
|
export async function fetchOrDeployAdmin(
|
|
96
145
|
provider: EthereumProvider,
|
|
97
146
|
deploy: () => Promise<Deployment>,
|
|
@@ -113,8 +162,9 @@ async function checkForAddressClash(
|
|
|
113
162
|
provider: EthereumProvider,
|
|
114
163
|
data: ManifestData,
|
|
115
164
|
updated: Deployment,
|
|
165
|
+
checkAllAddresses: boolean,
|
|
116
166
|
): Promise<void> {
|
|
117
|
-
const clash = lookupDeployment(data, updated.address);
|
|
167
|
+
const clash = lookupDeployment(data, updated.address, checkAllAddresses);
|
|
118
168
|
if (clash !== undefined) {
|
|
119
169
|
if (await isDevelopmentNetwork(provider)) {
|
|
120
170
|
debug('deleting a previous deployment at address', updated.address);
|
|
@@ -129,13 +179,20 @@ async function checkForAddressClash(
|
|
|
129
179
|
}
|
|
130
180
|
}
|
|
131
181
|
|
|
132
|
-
function lookupDeployment(
|
|
182
|
+
function lookupDeployment(
|
|
183
|
+
data: ManifestData,
|
|
184
|
+
address: string,
|
|
185
|
+
checkAllAddresses: boolean,
|
|
186
|
+
): ManifestField<Deployment> | undefined {
|
|
133
187
|
if (data.admin?.address === address) {
|
|
134
188
|
return adminLens(data);
|
|
135
189
|
}
|
|
136
190
|
|
|
137
191
|
for (const versionWithoutMetadata in data.impls) {
|
|
138
|
-
if (
|
|
192
|
+
if (
|
|
193
|
+
data.impls[versionWithoutMetadata]?.address === address ||
|
|
194
|
+
(checkAllAddresses && data.impls[versionWithoutMetadata]?.allAddresses?.includes(address))
|
|
195
|
+
) {
|
|
139
196
|
return implLens(versionWithoutMetadata)(data);
|
|
140
197
|
}
|
|
141
198
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
export * from './validate';
|
|
2
|
-
export
|
|
2
|
+
export { fetchOrDeploy, fetchOrDeployAdmin } from './impl-store';
|
|
3
3
|
export * from './version';
|
|
4
|
-
export * from './manifest';
|
|
5
4
|
export * from './storage';
|
|
6
|
-
export
|
|
5
|
+
export {
|
|
6
|
+
EIP1967BeaconNotFound,
|
|
7
|
+
EIP1967ImplementationNotFound,
|
|
8
|
+
getAdminAddress,
|
|
9
|
+
getBeaconAddress,
|
|
10
|
+
getImplementationAddress,
|
|
11
|
+
toEip1967Hash,
|
|
12
|
+
toFallbackEip1967Hash,
|
|
13
|
+
} from './eip-1967';
|
|
7
14
|
export * from './provider';
|
|
8
15
|
export * from './src-decoder';
|
|
9
16
|
export * from './solc-api';
|
|
@@ -11,18 +18,28 @@ export * from './deployment';
|
|
|
11
18
|
export * from './link-refs';
|
|
12
19
|
export * from './error';
|
|
13
20
|
|
|
21
|
+
export {
|
|
22
|
+
ManifestData,
|
|
23
|
+
ImplDeployment,
|
|
24
|
+
ProxyDeployment,
|
|
25
|
+
Manifest,
|
|
26
|
+
migrateManifest,
|
|
27
|
+
DeploymentNotFound,
|
|
28
|
+
} from './manifest';
|
|
29
|
+
|
|
14
30
|
export { getStorageLayoutForAddress } from './manifest-storage-layout';
|
|
15
31
|
|
|
16
32
|
export * from './scripts/migrate-oz-cli-project';
|
|
17
33
|
|
|
18
34
|
export { logWarning } from './utils/log';
|
|
19
|
-
export { setProxyKind, processProxyKind } from './proxy-kind';
|
|
35
|
+
export { setProxyKind, processProxyKind, detectProxyKind } from './proxy-kind';
|
|
20
36
|
|
|
21
37
|
export { UpgradeableContract } from './standalone';
|
|
22
38
|
|
|
23
39
|
export { isTransparentOrUUPSProxy, isBeaconProxy } from './eip-1967-type';
|
|
24
40
|
export { getImplementationAddressFromBeacon, getImplementationAddressFromProxy } from './impl-address';
|
|
25
41
|
export { isBeacon } from './beacon';
|
|
42
|
+
export { addProxyToManifest } from './add-proxy-to-manifest';
|
|
26
43
|
|
|
27
44
|
export {
|
|
28
45
|
BeaconProxyUnsupportedError,
|
|
@@ -31,5 +48,6 @@ export {
|
|
|
31
48
|
DeployBeaconProxyUnsupportedError,
|
|
32
49
|
DeployBeaconProxyImplUnknownError,
|
|
33
50
|
DeployBeaconProxyKindError,
|
|
51
|
+
ForceImportUnsupportedError,
|
|
34
52
|
assertNotProxy,
|
|
35
53
|
} from './usage-error';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import _test, {
|
|
1
|
+
import _test, { TestFn } from 'ava';
|
|
2
2
|
import assert from 'assert';
|
|
3
3
|
|
|
4
4
|
import { artifacts } from 'hardhat';
|
|
@@ -17,7 +17,7 @@ interface Context {
|
|
|
17
17
|
validationData: ValidationData;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const test = _test as
|
|
20
|
+
const test = _test as TestFn<Context>;
|
|
21
21
|
|
|
22
22
|
test.before(async t => {
|
|
23
23
|
const buildInfo = await artifacts.getBuildInfo('contracts/test/ManifestMigrate.sol:ManifestMigrateUnique');
|
|
@@ -44,6 +44,7 @@ test('getStorageLayoutForAddress - update layout', async t => {
|
|
|
44
44
|
},
|
|
45
45
|
proxies: [],
|
|
46
46
|
});
|
|
47
|
+
|
|
47
48
|
const layout = await getStorageLayoutForAddress(manifest, t.context.validationData, address);
|
|
48
49
|
t.deepEqual(layout, updatedLayout);
|
|
49
50
|
t.like(manifest.data, {
|
|
@@ -5,6 +5,7 @@ import { ValidationData } from './validate/data';
|
|
|
5
5
|
import { findVersionWithoutMetadataMatches, unfoldStorageLayout } from './validate/query';
|
|
6
6
|
import { stabilizeTypeIdentifier } from './utils/type-id';
|
|
7
7
|
import { DeepArray, deepEqual } from './utils/deep-array';
|
|
8
|
+
import { UpgradesError } from './error';
|
|
8
9
|
|
|
9
10
|
export async function getStorageLayoutForAddress(
|
|
10
11
|
manifest: Manifest,
|
|
@@ -12,13 +13,17 @@ export async function getStorageLayoutForAddress(
|
|
|
12
13
|
implAddress: string,
|
|
13
14
|
): Promise<StorageLayout> {
|
|
14
15
|
const data = await manifest.read();
|
|
15
|
-
const versionWithoutMetadata = Object.keys(data.impls).find(
|
|
16
|
+
const versionWithoutMetadata = Object.keys(data.impls).find(
|
|
17
|
+
v => data.impls[v]?.address === implAddress || data.impls[v]?.allAddresses?.includes(implAddress),
|
|
18
|
+
);
|
|
16
19
|
if (versionWithoutMetadata === undefined) {
|
|
17
|
-
throw new
|
|
20
|
+
throw new UpgradesError(
|
|
21
|
+
`Deployment at address ${implAddress} is not registered`,
|
|
22
|
+
() => 'To register a previously deployed proxy for upgrading, use the forceImport function.',
|
|
23
|
+
);
|
|
18
24
|
}
|
|
19
25
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
20
26
|
const { layout } = data.impls[versionWithoutMetadata]!;
|
|
21
|
-
|
|
22
27
|
if (isCurrentLayoutVersion(layout)) {
|
|
23
28
|
return layout;
|
|
24
29
|
} else {
|
package/src/manifest.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import test from 'ava';
|
|
2
|
-
import { Manifest } from './manifest';
|
|
2
|
+
import { Manifest, ManifestData, normalizeManifestData } from './manifest';
|
|
3
3
|
|
|
4
4
|
test('manifest name for a known network', t => {
|
|
5
5
|
const manifest = new Manifest(1);
|
|
@@ -11,3 +11,36 @@ test('manifest name for an unknown network', t => {
|
|
|
11
11
|
const manifest = new Manifest(id);
|
|
12
12
|
t.is(manifest.file, `.openzeppelin/unknown-${id}.json`);
|
|
13
13
|
});
|
|
14
|
+
|
|
15
|
+
test('normalize manifest', t => {
|
|
16
|
+
const deployment = {
|
|
17
|
+
address: '0x1234',
|
|
18
|
+
txHash: '0x1234',
|
|
19
|
+
kind: 'uups' as const,
|
|
20
|
+
layout: { types: {}, storage: [] },
|
|
21
|
+
deployTransaction: {},
|
|
22
|
+
};
|
|
23
|
+
const input: ManifestData = {
|
|
24
|
+
manifestVersion: '3.0',
|
|
25
|
+
admin: deployment,
|
|
26
|
+
impls: { a: deployment },
|
|
27
|
+
proxies: [deployment],
|
|
28
|
+
};
|
|
29
|
+
const norm = normalizeManifestData(input);
|
|
30
|
+
t.like(norm.admin, {
|
|
31
|
+
...deployment,
|
|
32
|
+
kind: undefined,
|
|
33
|
+
layout: undefined,
|
|
34
|
+
deployTransaction: undefined,
|
|
35
|
+
});
|
|
36
|
+
t.like(norm.impls.a, {
|
|
37
|
+
...deployment,
|
|
38
|
+
kind: undefined,
|
|
39
|
+
deployTransaction: undefined,
|
|
40
|
+
});
|
|
41
|
+
t.like(norm.proxies[0], {
|
|
42
|
+
...deployment,
|
|
43
|
+
layout: undefined,
|
|
44
|
+
deployTransaction: undefined,
|
|
45
|
+
});
|
|
46
|
+
});
|
package/src/manifest.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface ManifestData {
|
|
|
22
22
|
|
|
23
23
|
export interface ImplDeployment extends Deployment {
|
|
24
24
|
layout: StorageLayout;
|
|
25
|
+
allAddresses?: string[];
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface ProxyDeployment extends Deployment {
|
|
@@ -57,7 +58,10 @@ export class Manifest {
|
|
|
57
58
|
|
|
58
59
|
async getDeploymentFromAddress(address: string): Promise<ImplDeployment> {
|
|
59
60
|
const data = await this.read();
|
|
60
|
-
const deployment = Object.values(data.impls).find(
|
|
61
|
+
const deployment = Object.values(data.impls).find(
|
|
62
|
+
d => d?.address === address || d?.allAddresses?.includes(address),
|
|
63
|
+
);
|
|
64
|
+
|
|
61
65
|
if (deployment === undefined) {
|
|
62
66
|
throw new DeploymentNotFound(`Deployment at address ${address} is not registered`);
|
|
63
67
|
}
|
|
@@ -160,11 +164,12 @@ export function migrateManifest(data: ManifestData): ManifestData {
|
|
|
160
164
|
|
|
161
165
|
export class DeploymentNotFound extends Error {}
|
|
162
166
|
|
|
163
|
-
function normalizeManifestData(input: ManifestData): ManifestData {
|
|
167
|
+
export function normalizeManifestData(input: ManifestData): ManifestData {
|
|
164
168
|
return {
|
|
165
|
-
|
|
169
|
+
manifestVersion: input.manifestVersion,
|
|
170
|
+
admin: input.admin && normalizeDeployment(input.admin),
|
|
166
171
|
proxies: input.proxies.map(p => normalizeDeployment(p, ['kind'])),
|
|
167
|
-
impls: mapValues(input.impls, i => i && normalizeDeployment(i, ['layout'])),
|
|
172
|
+
impls: mapValues(input.impls, i => i && normalizeDeployment(i, ['layout', 'allAddresses'])),
|
|
168
173
|
};
|
|
169
174
|
}
|
|
170
175
|
|
package/src/provider.ts
CHANGED
|
@@ -71,7 +71,11 @@ export async function call(
|
|
|
71
71
|
|
|
72
72
|
export async function hasCode(provider: EthereumProvider, address: string, block?: string): Promise<boolean> {
|
|
73
73
|
const code = await getCode(provider, address, block);
|
|
74
|
-
return code
|
|
74
|
+
return !isEmpty(code);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isEmpty(code: string) {
|
|
78
|
+
return code.replace(/^0x/, '') === '';
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
export async function getTransactionByHash(
|
package/src/proxy-kind.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { EthereumProvider } from './provider';
|
|
2
2
|
import { inferProxyKind, ValidationData, ValidationOptions } from './validate';
|
|
3
3
|
import { Manifest, DeploymentNotFound, ProxyDeployment } from './manifest';
|
|
4
|
-
import { isBeaconProxy } from './eip-1967-type';
|
|
4
|
+
import { isBeaconProxy, isTransparentOrUUPSProxy, isTransparentProxy } from './eip-1967-type';
|
|
5
5
|
import { Version } from './version';
|
|
6
6
|
import { BeaconProxyUnsupportedError } from './usage-error';
|
|
7
|
+
import { UpgradesError } from '.';
|
|
7
8
|
|
|
8
9
|
export async function setProxyKind(
|
|
9
10
|
provider: EthereumProvider,
|
|
@@ -57,3 +58,25 @@ export async function processProxyKind(
|
|
|
57
58
|
throw new BeaconProxyUnsupportedError();
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Detects the kind of proxy at an address by reading its ERC 1967 storage slots.
|
|
64
|
+
*
|
|
65
|
+
* @param provider the Ethereum provider
|
|
66
|
+
* @param proxyAddress the proxy address
|
|
67
|
+
* @returns the proxy kind
|
|
68
|
+
* @throws {UpgradesError} if the contract at address does not look like an ERC 1967 proxy
|
|
69
|
+
*/
|
|
70
|
+
export async function detectProxyKind(provider: EthereumProvider, proxyAddress: string) {
|
|
71
|
+
let importKind: ProxyDeployment['kind'];
|
|
72
|
+
if (await isTransparentProxy(provider, proxyAddress)) {
|
|
73
|
+
importKind = 'transparent';
|
|
74
|
+
} else if (await isTransparentOrUUPSProxy(provider, proxyAddress)) {
|
|
75
|
+
importKind = 'uups';
|
|
76
|
+
} else if (await isBeaconProxy(provider, proxyAddress)) {
|
|
77
|
+
importKind = 'beacon';
|
|
78
|
+
} else {
|
|
79
|
+
throw new UpgradesError(`Contract at ${proxyAddress} doesn't look like an ERC 1967 proxy`);
|
|
80
|
+
}
|
|
81
|
+
return importKind;
|
|
82
|
+
}
|
|
@@ -50,7 +50,7 @@ test('produces network export data', async t => {
|
|
|
50
50
|
|
|
51
51
|
test('deletes project file', async t => {
|
|
52
52
|
const error = await t.throwsAsync(() => fs.access(PROJECT_FILE));
|
|
53
|
-
t.is(error
|
|
53
|
+
t.is(error?.message, `ENOENT: no such file or directory, access '${PROJECT_FILE}'`);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
test('export file contains networks and compiler data', async t => {
|
|
Binary file
|
package/src/solc-api.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SourceUnit } from 'solidity-ast';
|
|
2
|
+
import { StorageItem, TypeItem } from './storage';
|
|
2
3
|
|
|
3
4
|
export interface SolcOutput {
|
|
4
5
|
contracts: {
|
|
@@ -8,6 +9,10 @@ export interface SolcOutput {
|
|
|
8
9
|
bytecode: SolcBytecode;
|
|
9
10
|
methodIdentifiers?: Record<string, string>;
|
|
10
11
|
};
|
|
12
|
+
storageLayout?: {
|
|
13
|
+
storage: StorageItem[];
|
|
14
|
+
types: Record<string, TypeItem>;
|
|
15
|
+
};
|
|
11
16
|
};
|
|
12
17
|
};
|
|
13
18
|
};
|
package/src/standalone.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import _test, {
|
|
1
|
+
import _test, { TestFn } from 'ava';
|
|
2
2
|
import { artifacts } from 'hardhat';
|
|
3
3
|
|
|
4
4
|
import { SolcInput, SolcOutput } from './solc-api';
|
|
@@ -9,7 +9,7 @@ interface Context {
|
|
|
9
9
|
solcOutput: SolcOutput;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const test = _test as
|
|
12
|
+
const test = _test as TestFn<Context>;
|
|
13
13
|
|
|
14
14
|
test.before(async t => {
|
|
15
15
|
const buildInfo = await artifacts.getBuildInfo('contracts/test/Standalone.sol:StandaloneV1');
|
package/src/storage/compare.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { levenshtein, Operation } from '../levenshtein';
|
|
2
|
-
import { ParsedTypeDetailed,
|
|
2
|
+
import { hasLayout, ParsedTypeDetailed, isEnumMembers, isStructMembers } from './layout';
|
|
3
3
|
import { UpgradesError } from '../error';
|
|
4
|
-
import { StructMember as _StructMember,
|
|
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 =
|
|
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
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
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
|
-
|
|
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
|
}
|