@openzeppelin/upgrades-core 1.16.1 → 1.19.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/contracts/Initializable.sol +4 -1
- package/dist/deployment.js +2 -3
- package/dist/deployment.js.map +1 -1
- package/dist/eip-1967.d.ts.map +1 -1
- package/dist/eip-1967.js +2 -7
- package/dist/eip-1967.js.map +1 -1
- package/dist/impl-store.js +6 -8
- package/dist/impl-store.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/levenshtein.d.ts +4 -1
- package/dist/levenshtein.d.ts.map +1 -1
- package/dist/levenshtein.js +1 -1
- package/dist/levenshtein.js.map +1 -1
- package/dist/manifest-storage-layout.js +1 -1
- package/dist/manifest-storage-layout.js.map +1 -1
- package/dist/manifest.d.ts +5 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +54 -10
- package/dist/manifest.js.map +1 -1
- package/dist/manifest.test.js +82 -0
- package/dist/manifest.test.js.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +6 -6
- package/dist/provider.js.map +1 -1
- package/dist/provider.test.js +5 -5
- package/dist/provider.test.js.map +1 -1
- package/dist/proxy-kind.js +1 -2
- package/dist/proxy-kind.js.map +1 -1
- package/dist/scripts/migrate-oz-cli-project.js +1 -1
- package/dist/scripts/migrate-oz-cli-project.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/src-decoder.js +1 -2
- package/dist/src-decoder.js.map +1 -1
- package/dist/storage/compare.d.ts +21 -3
- package/dist/storage/compare.d.ts.map +1 -1
- package/dist/storage/compare.js +83 -22
- package/dist/storage/compare.js.map +1 -1
- package/dist/storage/extract.js +6 -9
- package/dist/storage/extract.js.map +1 -1
- package/dist/storage/gap.d.ts +18 -0
- package/dist/storage/gap.d.ts.map +1 -0
- package/dist/storage/gap.js +29 -0
- package/dist/storage/gap.js.map +1 -0
- package/dist/storage/layout.js +3 -4
- package/dist/storage/layout.js.map +1 -1
- package/dist/storage/report-gap.test.d.ts +2 -0
- package/dist/storage/report-gap.test.d.ts.map +1 -0
- package/dist/storage/report-gap.test.js +106 -0
- package/dist/storage/report-gap.test.js.map +1 -0
- package/dist/storage/report-rename-retype.test.js +24 -0
- package/dist/storage/report-rename-retype.test.js.map +1 -1
- package/dist/storage/report.d.ts +1 -1
- package/dist/storage/report.d.ts.map +1 -1
- package/dist/storage/report.js +59 -10
- package/dist/storage/report.js.map +1 -1
- package/dist/storage.test.js +422 -3
- package/dist/storage.test.js.map +1 -1
- package/dist/stub-provider.d.ts +1 -1
- package/dist/stub-provider.d.ts.map +1 -1
- package/dist/stub-provider.js +5 -7
- package/dist/stub-provider.js.map +1 -1
- package/dist/usage-error.d.ts +9 -0
- package/dist/usage-error.d.ts.map +1 -1
- package/dist/usage-error.js +16 -1
- package/dist/usage-error.js.map +1 -1
- package/dist/utils/parse-type-id.js +2 -3
- package/dist/utils/parse-type-id.js.map +1 -1
- package/dist/validate/data.js +26 -7
- package/dist/validate/data.js.map +1 -1
- package/dist/validate/index.d.ts +1 -1
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js.map +1 -1
- package/dist/validate/overrides.d.ts +34 -3
- package/dist/validate/overrides.d.ts.map +1 -1
- package/dist/validate/overrides.js +7 -7
- package/dist/validate/overrides.js.map +1 -1
- package/dist/validate/query.js +3 -5
- package/dist/validate/query.js.map +1 -1
- package/dist/validate/report.js +1 -2
- package/dist/validate/report.js.map +1 -1
- package/dist/validate/run.js +6 -10
- package/dist/validate/run.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +3 -5
- package/src/eip-1967.ts +2 -4
- package/src/index.ts +2 -0
- package/src/levenshtein.ts +4 -2
- package/src/manifest.test.ts +103 -1
- package/src/manifest.ts +62 -6
- package/src/provider.ts +5 -3
- package/src/storage/compare.ts +88 -18
- package/src/storage/gap.ts +26 -0
- package/src/storage/report-gap.test.ts +119 -0
- package/src/storage/report-gap.test.ts.md +59 -0
- package/src/storage/report-gap.test.ts.snap +0 -0
- package/src/storage/report-rename-retype.test.ts +27 -0
- package/src/storage/report-rename-retype.test.ts.md +7 -0
- package/src/storage/report-rename-retype.test.ts.snap +0 -0
- package/src/storage/report.test.ts.md +1 -4
- package/src/storage/report.test.ts.snap +0 -0
- package/src/storage/report.ts +83 -12
- package/src/storage.test.ts +466 -2
- package/src/storage.test.ts.snap +0 -0
- package/src/usage-error.ts +22 -0
- package/src/validate/data.ts +1 -1
- package/src/validate/index.ts +1 -1
- package/src/validate/overrides.ts +39 -3
- package/artifacts/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol/ProxyAdmin.json +0 -160
package/src/manifest.test.ts
CHANGED
|
@@ -1,5 +1,107 @@
|
|
|
1
|
-
import test from 'ava';
|
|
1
|
+
import test, { ExecutionContext } from 'ava';
|
|
2
2
|
import { Manifest, ManifestData, normalizeManifestData } from './manifest';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
async function writeTestManifest(file: string) {
|
|
7
|
+
const testManifest = {
|
|
8
|
+
manifestVersion: '3.2',
|
|
9
|
+
impls: {},
|
|
10
|
+
proxies: [
|
|
11
|
+
{
|
|
12
|
+
address: '0x123',
|
|
13
|
+
txHash: '0x0',
|
|
14
|
+
kind: 'uups',
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
19
|
+
await fs.writeFile(file, JSON.stringify(testManifest, null, 2) + '\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function deleteFile(t: ExecutionContext, file: string) {
|
|
23
|
+
try {
|
|
24
|
+
await fs.unlink(file);
|
|
25
|
+
} catch (e: any) {
|
|
26
|
+
if (!e.message.includes('ENOENT')) {
|
|
27
|
+
t.fail(e);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function assertOldName(t: ExecutionContext<unknown>, id: number) {
|
|
33
|
+
await fs.access(`.openzeppelin/unknown-${id}.json`);
|
|
34
|
+
t.throwsAsync(fs.access(`.openzeppelin/polygon-mumbai.json`));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function assertNewName(t: ExecutionContext<unknown>, id: number) {
|
|
38
|
+
t.throwsAsync(fs.access(`.openzeppelin/unknown-${id}.json`));
|
|
39
|
+
await fs.access(`.openzeppelin/polygon-mumbai.json`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function deleteManifests(t: ExecutionContext<unknown>, id: number) {
|
|
43
|
+
await deleteFile(t, `.openzeppelin/unknown-${id}.json`);
|
|
44
|
+
await deleteFile(t, '.openzeppelin/polygon-mumbai.json');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test.serial('multiple manifests', async t => {
|
|
48
|
+
const id = 80001;
|
|
49
|
+
await deleteManifests(t, id);
|
|
50
|
+
|
|
51
|
+
await writeTestManifest(`.openzeppelin/unknown-${id}.json`);
|
|
52
|
+
await writeTestManifest(`.openzeppelin/polygon-mumbai.json`);
|
|
53
|
+
|
|
54
|
+
const manifest = new Manifest(id);
|
|
55
|
+
await manifest.lockedRun(async () => {
|
|
56
|
+
await t.throwsAsync(() => manifest.read(), {
|
|
57
|
+
message: new RegExp(
|
|
58
|
+
`Network files with different names .openzeppelin/unknown-${id}.json and .openzeppelin/polygon-mumbai.json were found for the same network.`,
|
|
59
|
+
),
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await deleteManifests(t, id);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test.serial('rename manifest', async t => {
|
|
67
|
+
const id = 80001;
|
|
68
|
+
await deleteManifests(t, id);
|
|
69
|
+
|
|
70
|
+
await writeTestManifest(`.openzeppelin/unknown-${id}.json`);
|
|
71
|
+
|
|
72
|
+
const manifest = new Manifest(id);
|
|
73
|
+
t.is(manifest.file, `.openzeppelin/polygon-mumbai.json`);
|
|
74
|
+
|
|
75
|
+
t.throwsAsync(fs.access(`.openzeppelin/chain-${id}.lock`));
|
|
76
|
+
|
|
77
|
+
await assertOldName(t, id);
|
|
78
|
+
await manifest.lockedRun(async () => {
|
|
79
|
+
await fs.access(`.openzeppelin/chain-${id}.lock`);
|
|
80
|
+
const data = await manifest.read();
|
|
81
|
+
data.proxies.push({
|
|
82
|
+
address: '0x456',
|
|
83
|
+
txHash: '0x0',
|
|
84
|
+
kind: 'uups',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await assertOldName(t, id);
|
|
88
|
+
await manifest.write(data);
|
|
89
|
+
await assertNewName(t, id);
|
|
90
|
+
});
|
|
91
|
+
await assertNewName(t, id);
|
|
92
|
+
|
|
93
|
+
t.throwsAsync(fs.access(`.openzeppelin/chain-${id}.lock`));
|
|
94
|
+
|
|
95
|
+
// check that the contents were persisted
|
|
96
|
+
const data = await new Manifest(id).read();
|
|
97
|
+
t.true(data.proxies[0].address === '0x123');
|
|
98
|
+
t.true(data.proxies[1].address === '0x456');
|
|
99
|
+
await assertNewName(t, id);
|
|
100
|
+
|
|
101
|
+
t.throwsAsync(fs.access(`.openzeppelin/chain-${id}.lock`));
|
|
102
|
+
|
|
103
|
+
await deleteManifests(t, id);
|
|
104
|
+
});
|
|
3
105
|
|
|
4
106
|
test('manifest name for a known network', t => {
|
|
5
107
|
const manifest = new Manifest(1);
|
package/src/manifest.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { Deployment } from './deployment';
|
|
|
8
8
|
import type { StorageLayout } from './storage';
|
|
9
9
|
import { pick } from './utils/pick';
|
|
10
10
|
import { mapValues } from './utils/map-values';
|
|
11
|
+
import { UpgradesError } from './error';
|
|
11
12
|
|
|
12
13
|
const currentManifestVersion = '3.2';
|
|
13
14
|
|
|
@@ -40,15 +41,23 @@ function defaultManifest(): ManifestData {
|
|
|
40
41
|
const manifestDir = '.openzeppelin';
|
|
41
42
|
|
|
42
43
|
export class Manifest {
|
|
44
|
+
readonly chainId: number;
|
|
43
45
|
readonly file: string;
|
|
46
|
+
private readonly fallbackFile: string;
|
|
47
|
+
|
|
44
48
|
private locked = false;
|
|
45
49
|
|
|
46
50
|
static async forNetwork(provider: EthereumProvider): Promise<Manifest> {
|
|
47
51
|
return new Manifest(await getChainId(provider));
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
constructor(
|
|
51
|
-
|
|
54
|
+
constructor(chainId: number) {
|
|
55
|
+
this.chainId = chainId;
|
|
56
|
+
|
|
57
|
+
const fallbackName = `unknown-${this.chainId}`;
|
|
58
|
+
this.fallbackFile = path.join(manifestDir, `${fallbackName}.json`);
|
|
59
|
+
|
|
60
|
+
const name = networkNames[this.chainId] ?? fallbackName;
|
|
52
61
|
this.file = path.join(manifestDir, `${name}.json`);
|
|
53
62
|
}
|
|
54
63
|
|
|
@@ -89,10 +98,55 @@ export class Manifest {
|
|
|
89
98
|
});
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
private async exists(file: string): Promise<boolean> {
|
|
102
|
+
try {
|
|
103
|
+
await fs.access(file);
|
|
104
|
+
return true;
|
|
105
|
+
} catch (e: any) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async readFile(): Promise<string> {
|
|
111
|
+
if (this.file === this.fallbackFile) {
|
|
112
|
+
return await fs.readFile(this.file, 'utf8');
|
|
113
|
+
} else {
|
|
114
|
+
const fallbackExists = await this.exists(this.fallbackFile);
|
|
115
|
+
const fileExists = await this.exists(this.file);
|
|
116
|
+
|
|
117
|
+
if (fileExists && fallbackExists) {
|
|
118
|
+
throw new UpgradesError(
|
|
119
|
+
`Network files with different names ${this.fallbackFile} and ${this.file} were found for the same network.`,
|
|
120
|
+
() =>
|
|
121
|
+
`More than one network file was found for chain ID ${this.chainId}. Determine which file is the most up to date version, then take a backup of and delete the other file.`,
|
|
122
|
+
);
|
|
123
|
+
} else if (fallbackExists) {
|
|
124
|
+
return await fs.readFile(this.fallbackFile, 'utf8');
|
|
125
|
+
} else {
|
|
126
|
+
return await fs.readFile(this.file, 'utf8');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async writeFile(content: string): Promise<void> {
|
|
132
|
+
await this.renameFileIfRequired();
|
|
133
|
+
await fs.writeFile(this.file, content);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async renameFileIfRequired() {
|
|
137
|
+
if (this.file !== this.fallbackFile && (await this.exists(this.fallbackFile))) {
|
|
138
|
+
try {
|
|
139
|
+
await fs.rename(this.fallbackFile, this.file);
|
|
140
|
+
} catch (e: any) {
|
|
141
|
+
throw new Error(`Failed to rename network file from ${this.fallbackFile} to ${this.file}: ${e.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
92
146
|
async read(): Promise<ManifestData> {
|
|
93
147
|
const release = this.locked ? undefined : await this.lock();
|
|
94
148
|
try {
|
|
95
|
-
const data = JSON.parse(await
|
|
149
|
+
const data = JSON.parse(await this.readFile()) as ManifestData;
|
|
96
150
|
return validateOrUpdateManifestVersion(data);
|
|
97
151
|
} catch (e: any) {
|
|
98
152
|
if (e.code === 'ENOENT') {
|
|
@@ -110,7 +164,7 @@ export class Manifest {
|
|
|
110
164
|
throw new Error('Manifest must be locked');
|
|
111
165
|
}
|
|
112
166
|
const normalized = normalizeManifestData(data);
|
|
113
|
-
await
|
|
167
|
+
await this.writeFile(JSON.stringify(normalized, null, 2) + '\n');
|
|
114
168
|
}
|
|
115
169
|
|
|
116
170
|
async lockedRun<T>(cb: () => Promise<T>): Promise<T> {
|
|
@@ -126,8 +180,10 @@ export class Manifest {
|
|
|
126
180
|
}
|
|
127
181
|
|
|
128
182
|
private async lock() {
|
|
129
|
-
|
|
130
|
-
|
|
183
|
+
const lockfileName = path.join(manifestDir, `chain-${this.chainId}`);
|
|
184
|
+
|
|
185
|
+
await fs.mkdir(path.dirname(lockfileName), { recursive: true });
|
|
186
|
+
const release = await lockfile.lock(lockfileName, { retries: 3, realpath: false });
|
|
131
187
|
this.locked = true;
|
|
132
188
|
return async () => {
|
|
133
189
|
await release();
|
package/src/provider.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import BN from 'bn.js';
|
|
2
|
-
|
|
3
1
|
export interface EthereumProvider {
|
|
4
2
|
send(method: 'web3_clientVersion', params: []): Promise<string>;
|
|
5
3
|
send(method: 'net_version', params: []): Promise<string>;
|
|
@@ -33,7 +31,7 @@ export async function getNetworkId(provider: EthereumProvider): Promise<string>
|
|
|
33
31
|
|
|
34
32
|
export async function getChainId(provider: EthereumProvider): Promise<number> {
|
|
35
33
|
const id = await provider.send('eth_chainId', []);
|
|
36
|
-
return
|
|
34
|
+
return parseInt(id.replace(/^0x/, ''), 16);
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
export async function getClientVersion(provider: EthereumProvider): Promise<string> {
|
|
@@ -104,6 +102,10 @@ export const networkNames: { [chainId in number]?: string } = Object.freeze({
|
|
|
104
102
|
4: 'rinkeby',
|
|
105
103
|
5: 'goerli',
|
|
106
104
|
42: 'kovan',
|
|
105
|
+
137: 'polygon',
|
|
106
|
+
80001: 'polygon-mumbai',
|
|
107
|
+
43113: 'avalanche-fuji',
|
|
108
|
+
43114: 'avalanche',
|
|
107
109
|
});
|
|
108
110
|
|
|
109
111
|
export async function isDevelopmentNetwork(provider: EthereumProvider): Promise<boolean> {
|
package/src/storage/compare.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { levenshtein, Operation } from '../levenshtein';
|
|
1
|
+
import { Cost, levenshtein, Operation } from '../levenshtein';
|
|
2
2
|
import { hasLayout, ParsedTypeDetailed, isEnumMembers, isStructMembers } from './layout';
|
|
3
3
|
import { UpgradesError } from '../error';
|
|
4
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';
|
|
8
|
+
import { endMatchesGap, isGap } from './gap';
|
|
8
9
|
|
|
9
10
|
export type StorageItem = _StorageItem<ParsedTypeDetailed>;
|
|
10
11
|
type StructMember = _StructMember<ParsedTypeDetailed>;
|
|
@@ -15,13 +16,14 @@ export type StorageOperation<F extends StorageField> = Operation<F, StorageField
|
|
|
15
16
|
export type EnumOperation = Operation<string, { kind: 'replace'; original: string; updated: string }>;
|
|
16
17
|
|
|
17
18
|
type StorageFieldChange<F extends StorageField> = (
|
|
18
|
-
| { kind: 'replace' | 'rename' }
|
|
19
|
+
| { kind: 'replace' | 'rename' | 'finishgap' }
|
|
19
20
|
| { kind: 'typechange'; change: TypeChange }
|
|
20
21
|
| { kind: 'layoutchange'; change: LayoutChange }
|
|
22
|
+
| { kind: 'shrinkgap'; change: TypeChange }
|
|
21
23
|
) & {
|
|
22
24
|
original: F;
|
|
23
25
|
updated: F;
|
|
24
|
-
};
|
|
26
|
+
} & Cost;
|
|
25
27
|
|
|
26
28
|
export type TypeChange = (
|
|
27
29
|
| {
|
|
@@ -55,11 +57,45 @@ export type TypeChange = (
|
|
|
55
57
|
|
|
56
58
|
export interface LayoutChange {
|
|
57
59
|
uncertain?: boolean;
|
|
60
|
+
knownCompatible?: boolean;
|
|
58
61
|
slot?: Record<'from' | 'to', string>;
|
|
59
62
|
offset?: Record<'from' | 'to', number>;
|
|
60
63
|
bytes?: Record<'from' | 'to', string>;
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Gets the storage field's begin position as the number of bytes from 0.
|
|
68
|
+
*
|
|
69
|
+
* @param field the storage field
|
|
70
|
+
* @returns the begin position, or undefined if the slot or offset is undefined
|
|
71
|
+
*/
|
|
72
|
+
export function storageFieldBegin(field: StorageField): bigint | undefined {
|
|
73
|
+
if (field.slot === undefined || field.offset === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return BigInt(field.slot) * 32n + BigInt(field.offset);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Gets the storage field's end position as the number of bytes from 0.
|
|
81
|
+
*
|
|
82
|
+
* @param field the storage field
|
|
83
|
+
* @returns the end position, or undefined if the slot or offset or number of bytes is undefined
|
|
84
|
+
*/
|
|
85
|
+
export function storageFieldEnd(field: StorageField): bigint | undefined {
|
|
86
|
+
const begin = storageFieldBegin(field);
|
|
87
|
+
const numberOfBytes = field.type.item.numberOfBytes;
|
|
88
|
+
if (begin === undefined || numberOfBytes === undefined) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return begin + BigInt(numberOfBytes);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const LAYOUTCHANGE_COST = 0;
|
|
95
|
+
const FINISHGAP_COST = 1;
|
|
96
|
+
const SHRINKGAP_COST = 0;
|
|
97
|
+
const TYPECHANGE_COST = 0;
|
|
98
|
+
|
|
63
99
|
export class StorageLayoutComparator {
|
|
64
100
|
hasAllowedUncheckedCustomTypes = false;
|
|
65
101
|
|
|
@@ -80,10 +116,39 @@ export class StorageLayoutComparator {
|
|
|
80
116
|
): StorageOperation<F>[] {
|
|
81
117
|
const ops = levenshtein(original, updated, (a, b) => this.getFieldChange(a, b));
|
|
82
118
|
|
|
83
|
-
if
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
119
|
+
// filter operations: the callback function returns true if operation is unsafe, false if safe
|
|
120
|
+
return ops.filter(o => {
|
|
121
|
+
if (o.kind === 'insert') {
|
|
122
|
+
const updatedStart = storageFieldBegin(o.updated);
|
|
123
|
+
const updatedEnd = storageFieldEnd(o.updated);
|
|
124
|
+
// If we cannot get the updated position, or if any entry in the original layout (that is not a gap) overlaps, then the insertion is unsafe.
|
|
125
|
+
return (
|
|
126
|
+
updatedStart === undefined ||
|
|
127
|
+
updatedEnd === undefined ||
|
|
128
|
+
original
|
|
129
|
+
.filter(entry => !isGap(entry))
|
|
130
|
+
.some(entry => {
|
|
131
|
+
const origStart = storageFieldBegin(entry);
|
|
132
|
+
const origEnd = storageFieldEnd(entry);
|
|
133
|
+
return (
|
|
134
|
+
origStart === undefined ||
|
|
135
|
+
origEnd === undefined ||
|
|
136
|
+
overlaps(origStart, origEnd, updatedStart, updatedEnd)
|
|
137
|
+
);
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
} else if ((o.kind === 'shrinkgap' || o.kind === 'finishgap') && endMatchesGap(o.original, o.updated)) {
|
|
141
|
+
// Gap shrink or gap replacement that finishes on the same slot is safe
|
|
142
|
+
return false;
|
|
143
|
+
} else if (o.kind === 'append' && allowAppend) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// https://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap
|
|
150
|
+
function overlaps(startA: bigint, endA: bigint, startB: bigint, endB: bigint) {
|
|
151
|
+
return startA < endB && startB < endA;
|
|
87
152
|
}
|
|
88
153
|
}
|
|
89
154
|
|
|
@@ -107,28 +172,29 @@ export class StorageLayoutComparator {
|
|
|
107
172
|
const typeChange = !retypedFromOriginal && this.getTypeChange(original.type, updated.type, { allowAppend: false });
|
|
108
173
|
const layoutChange = this.getLayoutChange(original, updated);
|
|
109
174
|
|
|
110
|
-
if (updated.retypedFrom && layoutChange) {
|
|
111
|
-
return { kind: 'layoutchange', original, updated, change: layoutChange };
|
|
175
|
+
if (updated.retypedFrom && layoutChange && (!layoutChange.uncertain || !layoutChange.knownCompatible)) {
|
|
176
|
+
return { kind: 'layoutchange', original, updated, change: layoutChange, cost: LAYOUTCHANGE_COST };
|
|
177
|
+
} else if (nameChange && endMatchesGap(original, updated)) {
|
|
178
|
+
// original is a gap that was consumed by a non-gap ending at the same slot, which is safe
|
|
179
|
+
return { kind: 'finishgap', original, updated, cost: FINISHGAP_COST };
|
|
180
|
+
} else if (typeChange && typeChange.kind === 'array shrink' && endMatchesGap(original, updated)) {
|
|
181
|
+
// original and updated are a gap that was shrunk and ends at the same slot, which is safe
|
|
182
|
+
return { kind: 'shrinkgap', change: typeChange, original, updated, cost: SHRINKGAP_COST };
|
|
112
183
|
} else if (typeChange && nameChange) {
|
|
113
184
|
return { kind: 'replace', original, updated };
|
|
114
185
|
} else if (nameChange) {
|
|
115
186
|
return { kind: 'rename', original, updated };
|
|
116
187
|
} else if (typeChange) {
|
|
117
|
-
return { kind: 'typechange', change: typeChange, original, updated };
|
|
188
|
+
return { kind: 'typechange', change: typeChange, original, updated, cost: TYPECHANGE_COST };
|
|
118
189
|
} else if (layoutChange && !layoutChange.uncertain) {
|
|
119
190
|
// Any layout change should be caught earlier as a type change, but we
|
|
120
191
|
// add this check as a safety fallback.
|
|
121
|
-
return { kind: 'layoutchange', original, updated, change: layoutChange };
|
|
192
|
+
return { kind: 'layoutchange', original, updated, change: layoutChange, cost: LAYOUTCHANGE_COST };
|
|
122
193
|
}
|
|
123
194
|
}
|
|
124
195
|
|
|
125
196
|
getLayoutChange(original: StorageField, updated: StorageField): LayoutChange | undefined {
|
|
126
|
-
|
|
127
|
-
const knownCompatibleTypes =
|
|
128
|
-
validPair.includes(original.type.item.label) && validPair.includes(updated.type.item.label);
|
|
129
|
-
if (original.type.item.label == updated.type.item.label || knownCompatibleTypes) {
|
|
130
|
-
return undefined;
|
|
131
|
-
} else if (hasLayout(original) && hasLayout(updated)) {
|
|
197
|
+
if (hasLayout(original) && hasLayout(updated)) {
|
|
132
198
|
const change = <T>(from: T, to: T) => (from === to ? undefined : { from, to });
|
|
133
199
|
const slot = change(original.slot, updated.slot);
|
|
134
200
|
const offset = change(original.offset, updated.offset);
|
|
@@ -137,7 +203,11 @@ export class StorageLayoutComparator {
|
|
|
137
203
|
return { slot, offset, bytes };
|
|
138
204
|
}
|
|
139
205
|
} else {
|
|
140
|
-
|
|
206
|
+
const validChanges = [['uint8', 'bool']];
|
|
207
|
+
const knownCompatible = validChanges.some(
|
|
208
|
+
group => group.includes(original.type.item.label) && group.includes(updated.type.item.label),
|
|
209
|
+
);
|
|
210
|
+
return { uncertain: true, knownCompatible };
|
|
141
211
|
}
|
|
142
212
|
}
|
|
143
213
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { StorageField, storageFieldEnd } from './compare';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns true if the field represents a storage gap.
|
|
5
|
+
*
|
|
6
|
+
* @param field the storage field
|
|
7
|
+
* @returns true if field is a gap, otherwise false
|
|
8
|
+
*/
|
|
9
|
+
export function isGap(field: StorageField): boolean {
|
|
10
|
+
return field.label === '__gap' && field.type.head === 't_array';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns true if original storage field is a gap and the updated storage field
|
|
15
|
+
* ends at the exact same position as the gap.
|
|
16
|
+
*
|
|
17
|
+
* @param original the original storage field
|
|
18
|
+
* @param updated the updated storage field
|
|
19
|
+
* @returns true if original is a gap and original and updated end at the same position, otherwise false
|
|
20
|
+
*/
|
|
21
|
+
export function endMatchesGap(original: StorageField, updated: StorageField) {
|
|
22
|
+
const originalEnd = storageFieldEnd(original);
|
|
23
|
+
const updatedEnd = storageFieldEnd(updated);
|
|
24
|
+
|
|
25
|
+
return isGap(original) && originalEnd !== undefined && updatedEnd !== undefined && originalEnd === updatedEnd;
|
|
26
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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/Storage.sol:StorageUpgrade_Gap_V1',
|
|
19
|
+
'contracts/test/Storage.sol:StorageUpgrade_Gap_V2_Ok',
|
|
20
|
+
'contracts/test/Storage.sol:StorageUpgrade_Gap_V2_Bad1',
|
|
21
|
+
'contracts/test/Storage.sol:StorageUpgrade_Gap_V2_Bad2',
|
|
22
|
+
'contracts/test/Storage.sol:StorageUpgrade_Gap_V2_Bad3',
|
|
23
|
+
'contracts/test/Storage.sol:StorageUpgrade_Gap_V2_Bad4',
|
|
24
|
+
'contracts/test/Storage.sol:StorageUpgrade_Gap_V2_Bad5',
|
|
25
|
+
'contracts/test/Storage.sol:StorageUpgrade_MultiConsumeGap_V1',
|
|
26
|
+
'contracts/test/Storage.sol:StorageUpgrade_MultiConsumeGap_V2_Ok',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
test.before(async t => {
|
|
30
|
+
const contracts: Record<string, ContractDefinition> = {};
|
|
31
|
+
const deref: Record<string, ASTDereferencer> = {};
|
|
32
|
+
const storageLayout: Record<string, StorageLayout> = {};
|
|
33
|
+
for (const contract of testContracts) {
|
|
34
|
+
const buildInfo = await artifacts.getBuildInfo(contract);
|
|
35
|
+
if (buildInfo === undefined) {
|
|
36
|
+
throw new Error(`Build info not found for contract ${contract}`);
|
|
37
|
+
}
|
|
38
|
+
const solcOutput = buildInfo.output;
|
|
39
|
+
for (const def of findAll('ContractDefinition', solcOutput.sources['contracts/test/Storage.sol'].ast)) {
|
|
40
|
+
contracts[def.name] = def;
|
|
41
|
+
deref[def.name] = astDereferencer(solcOutput);
|
|
42
|
+
storageLayout[def.name] = (solcOutput.contracts['contracts/test/Storage.sol'][def.name] as any).storageLayout;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
t.context.extractStorageLayout = name =>
|
|
47
|
+
extractStorageLayout(contracts[name], dummyDecodeSrc, deref[name], storageLayout[name]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function getReport(original: StorageLayout, updated: StorageLayout) {
|
|
51
|
+
const originalDetailed = getDetailedLayout(original);
|
|
52
|
+
const updatedDetailed = getDetailedLayout(updated);
|
|
53
|
+
const comparator = new StorageLayoutComparator();
|
|
54
|
+
return comparator.compareLayouts(originalDetailed, updatedDetailed);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('shrinkgap', t => {
|
|
58
|
+
const v1 = t.context.extractStorageLayout('StorageUpgrade_Gap_V1');
|
|
59
|
+
const v2 = t.context.extractStorageLayout('StorageUpgrade_Gap_V2_Ok');
|
|
60
|
+
const report = getReport(v1, v2);
|
|
61
|
+
t.true(report.ok);
|
|
62
|
+
t.is(report.explain(), '');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('finishgap', t => {
|
|
66
|
+
const v1 = t.context.extractStorageLayout('StorageUpgrade_MultiConsumeGap_V1');
|
|
67
|
+
const v2 = t.context.extractStorageLayout('StorageUpgrade_MultiConsumeGap_V2_Ok');
|
|
68
|
+
const report = getReport(v1, v2);
|
|
69
|
+
t.true(report.ok);
|
|
70
|
+
t.is(report.explain(), '');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('insert var without shrink gap', t => {
|
|
74
|
+
const v1 = t.context.extractStorageLayout('StorageUpgrade_Gap_V1');
|
|
75
|
+
const v2 = t.context.extractStorageLayout('StorageUpgrade_Gap_V2_Bad1');
|
|
76
|
+
const report = getReport(v1, v2);
|
|
77
|
+
t.false(report.ok);
|
|
78
|
+
t.snapshot(report.explain());
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('delete var and expand gap', t => {
|
|
82
|
+
const v1 = t.context.extractStorageLayout('StorageUpgrade_Gap_V1');
|
|
83
|
+
const v2 = t.context.extractStorageLayout('StorageUpgrade_Gap_V2_Bad2');
|
|
84
|
+
const report = getReport(v1, v2);
|
|
85
|
+
t.false(report.ok);
|
|
86
|
+
t.snapshot(report.explain());
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('shrink gap without adding var', t => {
|
|
90
|
+
const v1 = t.context.extractStorageLayout('StorageUpgrade_Gap_V1');
|
|
91
|
+
const v2 = t.context.extractStorageLayout('StorageUpgrade_Gap_V2_Bad3');
|
|
92
|
+
const report = getReport(v1, v2);
|
|
93
|
+
t.false(report.ok);
|
|
94
|
+
t.snapshot(report.explain());
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('insert var and shrink gap too much', t => {
|
|
98
|
+
const v1 = t.context.extractStorageLayout('StorageUpgrade_Gap_V1');
|
|
99
|
+
const v2 = t.context.extractStorageLayout('StorageUpgrade_Gap_V2_Bad4');
|
|
100
|
+
const report = getReport(v1, v2);
|
|
101
|
+
t.false(report.ok);
|
|
102
|
+
t.snapshot(report.explain());
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('insert vars and shrink gap not enough', t => {
|
|
106
|
+
const v1 = t.context.extractStorageLayout('StorageUpgrade_Gap_V1');
|
|
107
|
+
const v2 = t.context.extractStorageLayout('StorageUpgrade_Gap_V2_Bad5');
|
|
108
|
+
const report = getReport(v1, v2);
|
|
109
|
+
t.false(report.ok);
|
|
110
|
+
t.snapshot(report.explain());
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('insert vars without shrink gap (uint128)', t => {
|
|
114
|
+
const v1 = t.context.extractStorageLayout('StorageUpgrade_Uint128Gap_V1');
|
|
115
|
+
const v2 = t.context.extractStorageLayout('StorageUpgrade_Uint128Gap_V2_Bad');
|
|
116
|
+
const report = getReport(v1, v2);
|
|
117
|
+
t.false(report.ok);
|
|
118
|
+
t.snapshot(report.explain());
|
|
119
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Snapshot report for `src/storage/report-gap.test.ts`
|
|
2
|
+
|
|
3
|
+
The actual snapshot is saved in `report-gap.test.ts.snap`.
|
|
4
|
+
|
|
5
|
+
Generated by [AVA](https://avajs.dev).
|
|
6
|
+
|
|
7
|
+
## insert var without shrink gap
|
|
8
|
+
|
|
9
|
+
> Snapshot 1
|
|
10
|
+
|
|
11
|
+
`file.sol:1: Layout changed for \`__gap\` (uint256[48] -> uint256[48])␊
|
|
12
|
+
- Slot changed from 2 to 3␊
|
|
13
|
+
> Set __gap array to size 47`
|
|
14
|
+
|
|
15
|
+
## delete var and expand gap
|
|
16
|
+
|
|
17
|
+
> Snapshot 1
|
|
18
|
+
|
|
19
|
+
`StorageUpgrade_Gap_V1: Deleted \`b\`␊
|
|
20
|
+
> Keep the variable even if unused␊
|
|
21
|
+
␊
|
|
22
|
+
file.sol:1: Upgraded \`__gap\` to an incompatible type␊
|
|
23
|
+
- Bad storage gap resize from 48 to 49␊
|
|
24
|
+
Size cannot increase`
|
|
25
|
+
|
|
26
|
+
## shrink gap without adding var
|
|
27
|
+
|
|
28
|
+
> Snapshot 1
|
|
29
|
+
|
|
30
|
+
`file.sol:1: Upgraded \`__gap\` to an incompatible type␊
|
|
31
|
+
- Bad storage gap resize from 48 to 47␊
|
|
32
|
+
Size decrease must match with corresponding variable inserts␊
|
|
33
|
+
> Set __gap array to size 48`
|
|
34
|
+
|
|
35
|
+
## insert var and shrink gap too much
|
|
36
|
+
|
|
37
|
+
> Snapshot 1
|
|
38
|
+
|
|
39
|
+
`file.sol:1: Upgraded \`__gap\` to an incompatible type␊
|
|
40
|
+
- Bad storage gap resize from 48 to 46␊
|
|
41
|
+
Size decrease must match with corresponding variable inserts␊
|
|
42
|
+
> Set __gap array to size 47`
|
|
43
|
+
|
|
44
|
+
## insert vars and shrink gap not enough
|
|
45
|
+
|
|
46
|
+
> Snapshot 1
|
|
47
|
+
|
|
48
|
+
`file.sol:1: Upgraded \`__gap\` to an incompatible type␊
|
|
49
|
+
- Bad storage gap resize from 48 to 47␊
|
|
50
|
+
Size decrease must match with corresponding variable inserts␊
|
|
51
|
+
> Set __gap array to size 46`
|
|
52
|
+
|
|
53
|
+
## insert vars without shrink gap (uint128)
|
|
54
|
+
|
|
55
|
+
> Snapshot 1
|
|
56
|
+
|
|
57
|
+
`file.sol:1: Layout changed for \`__gap\` (uint128[49] -> uint128[49])␊
|
|
58
|
+
- Slot changed from 1 to 2␊
|
|
59
|
+
> Set __gap array to size 48`
|
|
Binary file
|
|
@@ -6,6 +6,7 @@ import { astDereferencer } from '../ast-dereferencer';
|
|
|
6
6
|
import { extractStorageLayout } from './extract';
|
|
7
7
|
import { StorageLayoutComparator } from './compare';
|
|
8
8
|
import { StorageLayout, getDetailedLayout } from './layout';
|
|
9
|
+
import { getStorageUpgradeErrors } from '.';
|
|
9
10
|
|
|
10
11
|
interface Context {
|
|
11
12
|
extractStorageLayout: (contract: string) => ReturnType<typeof extractStorageLayout>;
|
|
@@ -25,6 +26,8 @@ const testContracts = [
|
|
|
25
26
|
'contracts/test/RenamedRetyped.sol:ConfusingRetypeV2',
|
|
26
27
|
'contracts/test/RenamedRetyped.sol:NonHardcodedRetypeV1',
|
|
27
28
|
'contracts/test/RenamedRetyped.sol:NonHardcodedRetypeV2',
|
|
29
|
+
'contracts/test/RenamedRetyped.sol:LayoutChangeV1',
|
|
30
|
+
'contracts/test/RenamedRetyped.sol:LayoutChangeV2',
|
|
28
31
|
];
|
|
29
32
|
|
|
30
33
|
test.before(async t => {
|
|
@@ -104,3 +107,27 @@ test('non-hardcoded retype', t => {
|
|
|
104
107
|
t.true(report.ok);
|
|
105
108
|
t.snapshot(report.explain());
|
|
106
109
|
});
|
|
110
|
+
|
|
111
|
+
test('retype with layout change', t => {
|
|
112
|
+
const v1 = t.context.extractStorageLayout('LayoutChangeV1');
|
|
113
|
+
const v2 = t.context.extractStorageLayout('LayoutChangeV2');
|
|
114
|
+
|
|
115
|
+
// ensure both variables' layout changes appear in the internal errors
|
|
116
|
+
t.like(getStorageUpgradeErrors(v1, v2), {
|
|
117
|
+
length: 2,
|
|
118
|
+
0: {
|
|
119
|
+
kind: 'layoutchange',
|
|
120
|
+
original: { label: 'a' },
|
|
121
|
+
updated: { label: 'a' },
|
|
122
|
+
},
|
|
123
|
+
1: {
|
|
124
|
+
kind: 'layoutchange',
|
|
125
|
+
original: { label: 'b' },
|
|
126
|
+
updated: { label: 'b' },
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const report = getReport(v1, v2);
|
|
131
|
+
t.false(report.ok);
|
|
132
|
+
t.snapshot(report.explain());
|
|
133
|
+
});
|
|
Binary file
|