@openzeppelin/upgrades-core 1.26.2 → 1.27.1
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/LICENSE +22 -0
- package/dist/cli/cli.d.ts +3 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +9 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/cli.test.d.ts +2 -0
- package/dist/cli/cli.test.d.ts.map +1 -0
- package/dist/cli/cli.test.js +52 -0
- package/dist/cli/cli.test.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/validate/build-info-file.d.ts +22 -0
- package/dist/cli/validate/build-info-file.d.ts.map +1 -0
- package/dist/cli/validate/build-info-file.js +116 -0
- package/dist/cli/validate/build-info-file.js.map +1 -0
- package/dist/cli/validate/build-info-file.test.d.ts +2 -0
- package/dist/cli/validate/build-info-file.test.d.ts.map +1 -0
- package/dist/cli/validate/build-info-file.test.js +175 -0
- package/dist/cli/validate/build-info-file.test.js.map +1 -0
- package/dist/cli/validate/contract-report.d.ts +31 -0
- package/dist/cli/validate/contract-report.d.ts.map +1 -0
- package/dist/cli/validate/contract-report.js +107 -0
- package/dist/cli/validate/contract-report.js.map +1 -0
- package/dist/cli/validate/error.d.ts +4 -0
- package/dist/cli/validate/error.d.ts.map +1 -0
- package/dist/cli/validate/error.js +8 -0
- package/dist/cli/validate/error.js.map +1 -0
- package/dist/cli/validate/project-report.d.ts +18 -0
- package/dist/cli/validate/project-report.d.ts.map +1 -0
- package/dist/cli/validate/project-report.js +42 -0
- package/dist/cli/validate/project-report.js.map +1 -0
- package/dist/cli/validate/project-report.test.d.ts +2 -0
- package/dist/cli/validate/project-report.test.d.ts.map +1 -0
- package/dist/cli/validate/project-report.test.js +90 -0
- package/dist/cli/validate/project-report.test.js.map +1 -0
- package/dist/cli/validate/upgradeability-assessment.d.ts +19 -0
- package/dist/cli/validate/upgradeability-assessment.d.ts.map +1 -0
- package/dist/cli/validate/upgradeability-assessment.js +103 -0
- package/dist/cli/validate/upgradeability-assessment.js.map +1 -0
- package/dist/cli/validate/validate-upgrade-safety.d.ts +16 -0
- package/dist/cli/validate/validate-upgrade-safety.d.ts.map +1 -0
- package/dist/cli/validate/validate-upgrade-safety.js +23 -0
- package/dist/cli/validate/validate-upgrade-safety.js.map +1 -0
- package/dist/cli/validate/validate-upgrade-safety.test.d.ts +2 -0
- package/dist/cli/validate/validate-upgrade-safety.test.d.ts.map +1 -0
- package/dist/cli/validate/validate-upgrade-safety.test.js +74 -0
- package/dist/cli/validate/validate-upgrade-safety.test.js.map +1 -0
- package/dist/cli/validate/validations.d.ts +11 -0
- package/dist/cli/validate/validations.d.ts.map +1 -0
- package/dist/cli/validate/validations.js +40 -0
- package/dist/cli/validate/validations.js.map +1 -0
- package/dist/cli/validate/validations.test.d.ts +2 -0
- package/dist/cli/validate/validations.test.d.ts.map +1 -0
- package/dist/cli/validate/validations.test.js +64 -0
- package/dist/cli/validate/validations.test.js.map +1 -0
- package/dist/cli/validate.d.ts +16 -0
- package/dist/cli/validate.d.ts.map +1 -0
- package/dist/cli/validate.js +119 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/cli/validate.test.d.ts +2 -0
- package/dist/cli/validate.test.d.ts.map +1 -0
- package/dist/cli/validate.test.js +108 -0
- package/dist/cli/validate.test.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest.test.js +2 -1
- package/dist/manifest.test.js.map +1 -1
- package/dist/utils/annotations.d.ts +16 -0
- package/dist/utils/annotations.d.ts.map +1 -0
- package/dist/utils/annotations.js +46 -0
- package/dist/utils/annotations.js.map +1 -0
- package/dist/utils/contract-name.d.ts +2 -0
- package/dist/utils/contract-name.d.ts.map +1 -0
- package/dist/utils/contract-name.js +8 -0
- package/dist/utils/contract-name.js.map +1 -0
- package/dist/validate/index.d.ts +1 -1
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +2 -1
- package/dist/validate/index.js.map +1 -1
- package/dist/validate/query.d.ts +8 -1
- package/dist/validate/query.d.ts.map +1 -1
- package/dist/validate/query.js +16 -3
- package/dist/validate/query.js.map +1 -1
- package/dist/validate/run.d.ts +1 -4
- package/dist/validate/run.d.ts.map +1 -1
- package/dist/validate/run.js +8 -38
- package/dist/validate/run.js.map +1 -1
- package/package.json +9 -3
- package/src/cli/cli.test.ts +57 -0
- package/src/cli/cli.test.ts.md +138 -0
- package/src/cli/cli.test.ts.snap +0 -0
- package/src/cli/cli.ts +9 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/validate/build-info-file.test.ts +211 -0
- package/src/cli/validate/build-info-file.ts +150 -0
- package/src/cli/validate/contract-report.ts +130 -0
- package/src/cli/validate/error.ts +3 -0
- package/src/cli/validate/project-report.test.ts +124 -0
- package/src/cli/validate/project-report.test.ts.md +40 -0
- package/src/cli/validate/project-report.test.ts.snap +0 -0
- package/src/cli/validate/project-report.ts +44 -0
- package/src/cli/validate/upgradeability-assessment.ts +137 -0
- package/src/cli/validate/validate-upgrade-safety.test.ts +102 -0
- package/src/cli/validate/validate-upgrade-safety.test.ts.md +98 -0
- package/src/cli/validate/validate-upgrade-safety.test.ts.snap +0 -0
- package/src/cli/validate/validate-upgrade-safety.ts +29 -0
- package/src/cli/validate/validations.test.ts +72 -0
- package/src/cli/validate/validations.test.ts.md +87 -0
- package/src/cli/validate/validations.test.ts.snap +0 -0
- package/src/cli/validate/validations.ts +53 -0
- package/src/cli/validate.test.ts +112 -0
- package/src/cli/validate.ts +134 -0
- package/src/index.ts +2 -0
- package/src/manifest.test.ts +2 -1
- package/src/utils/annotations.ts +47 -0
- package/src/utils/contract-name.ts +3 -0
- package/src/validate/index.ts +1 -0
- package/src/validate/query.ts +16 -3
- package/src/validate/run.ts +5 -41
- package/CHANGELOG.md +0 -265
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { UpgradeableContractReport } from './contract-report';
|
|
2
|
+
import { Report } from '../../standalone';
|
|
3
|
+
|
|
4
|
+
export class ProjectReport implements Report {
|
|
5
|
+
constructor(readonly upgradeableContractReports: UpgradeableContractReport[]) {}
|
|
6
|
+
|
|
7
|
+
get ok(): boolean {
|
|
8
|
+
return this.upgradeableContractReports.every(r => r.ok);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
explain(color = true): string {
|
|
12
|
+
if (this.numTotal === 0) {
|
|
13
|
+
return 'No upgradeable contracts detected.';
|
|
14
|
+
} else {
|
|
15
|
+
const lines = this.upgradeableContractReports.map(r => r.explain(color));
|
|
16
|
+
const numFailed = this.numTotal - this.numPassed;
|
|
17
|
+
const plural = numFailed === 1 ? '' : 's';
|
|
18
|
+
const status = this.ok ? 'SUCCESS' : 'FAILED';
|
|
19
|
+
|
|
20
|
+
lines.push(
|
|
21
|
+
`${status} (${this.numTotal} upgradeable contract${plural} detected, ${this.numPassed} passed, ${numFailed} failed)`,
|
|
22
|
+
);
|
|
23
|
+
return lines.join('\n\n');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Number of contracts that passed upgrade safety checks.
|
|
29
|
+
*/
|
|
30
|
+
get numPassed(): number {
|
|
31
|
+
return this.upgradeableContractReports.filter(r => r.ok).length;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Total number of upgradeable contracts detected.
|
|
36
|
+
*/
|
|
37
|
+
get numTotal(): number {
|
|
38
|
+
return this.upgradeableContractReports.length;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getProjectReport(upgradeableContractReports: UpgradeableContractReport[]): ProjectReport {
|
|
43
|
+
return new ProjectReport(upgradeableContractReports);
|
|
44
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { getAnnotationArgs, getDocumentation } from '../../utils/annotations';
|
|
2
|
+
import { inferInitializable, inferUUPS } from '../../validate/query';
|
|
3
|
+
import { ValidateCommandError } from './error';
|
|
4
|
+
import { SourceContract } from './validations';
|
|
5
|
+
|
|
6
|
+
export class ReferenceContractNotFound extends Error {
|
|
7
|
+
/**
|
|
8
|
+
* The contract reference that could not be found.
|
|
9
|
+
*/
|
|
10
|
+
readonly reference: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The fully qualified name of the contract that referenced the missing contract.
|
|
14
|
+
*/
|
|
15
|
+
readonly origin: string;
|
|
16
|
+
|
|
17
|
+
constructor(reference: string, origin: string) {
|
|
18
|
+
super(`Could not find contract ${reference} referenced in ${origin}.`);
|
|
19
|
+
this.reference = reference;
|
|
20
|
+
this.origin = origin;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface AnnotationAssessment {
|
|
25
|
+
upgradeable: boolean;
|
|
26
|
+
referenceName?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UpgradeabilityAssessment {
|
|
30
|
+
upgradeable: boolean;
|
|
31
|
+
referenceContract?: SourceContract;
|
|
32
|
+
uups?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getUpgradeabilityAssessment(
|
|
36
|
+
contract: SourceContract,
|
|
37
|
+
allContracts: SourceContract[],
|
|
38
|
+
): UpgradeabilityAssessment {
|
|
39
|
+
const fullContractName = contract.fullyQualifiedName;
|
|
40
|
+
const contractValidation = contract.validationData[fullContractName];
|
|
41
|
+
|
|
42
|
+
const isUUPS = inferUUPS(contract.validationData, fullContractName);
|
|
43
|
+
|
|
44
|
+
const annotationAssessment = getAnnotationAssessment(contract);
|
|
45
|
+
if (annotationAssessment.upgradeable) {
|
|
46
|
+
let referenceContract = undefined;
|
|
47
|
+
let isReferenceUUPS = false;
|
|
48
|
+
if (annotationAssessment.referenceName !== undefined) {
|
|
49
|
+
referenceContract = getReferenceContract(annotationAssessment.referenceName, contract, allContracts);
|
|
50
|
+
isReferenceUUPS = inferUUPS(referenceContract.validationData, referenceContract.fullyQualifiedName);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
upgradeable: true,
|
|
55
|
+
referenceContract: referenceContract,
|
|
56
|
+
uups: isReferenceUUPS || isUUPS,
|
|
57
|
+
};
|
|
58
|
+
} else {
|
|
59
|
+
const initializable = inferInitializable(contractValidation);
|
|
60
|
+
return {
|
|
61
|
+
upgradeable: initializable || isUUPS,
|
|
62
|
+
uups: isUUPS,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getReferenceContract(reference: string, origin: SourceContract, allContracts: SourceContract[]) {
|
|
68
|
+
const referenceContracts = allContracts.filter(c => c.fullyQualifiedName === reference || c.name === reference);
|
|
69
|
+
|
|
70
|
+
if (referenceContracts.length > 1) {
|
|
71
|
+
throw new ValidateCommandError(
|
|
72
|
+
`Found multiple contracts with name ${reference} referenced in ${origin.fullyQualifiedName}.`,
|
|
73
|
+
() =>
|
|
74
|
+
`This may be caused by old copies of build info files. Clean and recompile your project, then run the command again with the updated files.`,
|
|
75
|
+
);
|
|
76
|
+
} else if (referenceContracts.length === 1) {
|
|
77
|
+
return referenceContracts[0];
|
|
78
|
+
} else {
|
|
79
|
+
throw new ReferenceContractNotFound(reference, origin.fullyQualifiedName);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getAnnotationAssessment(contract: SourceContract): AnnotationAssessment {
|
|
84
|
+
const node = contract.node;
|
|
85
|
+
|
|
86
|
+
if ('documentation' in node) {
|
|
87
|
+
const doc = getDocumentation(node);
|
|
88
|
+
|
|
89
|
+
const tag = 'oz-upgrades';
|
|
90
|
+
const hasUpgradeAnnotation = hasAnnotationTag(doc, tag);
|
|
91
|
+
if (hasUpgradeAnnotation) {
|
|
92
|
+
getAndValidateAnnotationArgs(doc, tag, contract, 0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const upgradesFrom = getUpgradesFrom(doc, contract);
|
|
96
|
+
if (upgradesFrom !== undefined) {
|
|
97
|
+
return {
|
|
98
|
+
upgradeable: true,
|
|
99
|
+
referenceName: upgradesFrom,
|
|
100
|
+
};
|
|
101
|
+
} else {
|
|
102
|
+
return {
|
|
103
|
+
upgradeable: hasUpgradeAnnotation,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
return {
|
|
108
|
+
upgradeable: false,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getAndValidateAnnotationArgs(doc: string, tag: string, contract: SourceContract, expectedLength: number) {
|
|
114
|
+
const annotationArgs = getAnnotationArgs(doc, tag, undefined);
|
|
115
|
+
if (annotationArgs.length !== expectedLength) {
|
|
116
|
+
throw new ValidateCommandError(
|
|
117
|
+
`Invalid number of arguments for @custom:${tag} annotation in contract ${contract.fullyQualifiedName}.`,
|
|
118
|
+
() => `Found ${annotationArgs.length}, expected ${expectedLength}.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return annotationArgs;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function hasAnnotationTag(doc: string, tag: string): boolean {
|
|
125
|
+
const regex = new RegExp(`^\\s*(@custom:${tag})(\\s|$)`, 'm');
|
|
126
|
+
return regex.test(doc);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getUpgradesFrom(doc: string, contract: SourceContract): string | undefined {
|
|
130
|
+
const tag = 'oz-upgrades-from';
|
|
131
|
+
if (hasAnnotationTag(doc, tag)) {
|
|
132
|
+
const annotationArgs = getAndValidateAnnotationArgs(doc, tag, contract, 1);
|
|
133
|
+
return annotationArgs[0];
|
|
134
|
+
} else {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import rimrafAsync from 'rimraf';
|
|
5
|
+
import util from 'util';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
|
|
9
|
+
import { artifacts } from 'hardhat';
|
|
10
|
+
import { validateUpgradeSafety } from './validate-upgrade-safety';
|
|
11
|
+
import { ReferenceContractNotFound } from './upgradeability-assessment';
|
|
12
|
+
|
|
13
|
+
const rimraf = util.promisify(rimrafAsync);
|
|
14
|
+
|
|
15
|
+
test.before(async () => {
|
|
16
|
+
process.chdir(await fs.mkdtemp(path.join(os.tmpdir(), 'upgrades-core-test-')));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test.after(async () => {
|
|
20
|
+
await rimraf(process.cwd());
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('validate upgrade safety', async t => {
|
|
24
|
+
const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:Safe`);
|
|
25
|
+
|
|
26
|
+
await fs.mkdir('validate-upgrade-safety');
|
|
27
|
+
await fs.writeFile('validate-upgrade-safety/1.json', JSON.stringify(buildInfo));
|
|
28
|
+
|
|
29
|
+
const report = await validateUpgradeSafety('validate-upgrade-safety');
|
|
30
|
+
t.false(report.ok);
|
|
31
|
+
t.snapshot(report.explain());
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('ambiguous upgrades-from', async t => {
|
|
35
|
+
const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:Safe`);
|
|
36
|
+
|
|
37
|
+
await fs.mkdir('ambiguous-upgrades-from');
|
|
38
|
+
await fs.writeFile('ambiguous-upgrades-from/1.json', JSON.stringify(buildInfo));
|
|
39
|
+
await fs.writeFile('ambiguous-upgrades-from/2.json', JSON.stringify(buildInfo));
|
|
40
|
+
|
|
41
|
+
const error = await t.throwsAsync(validateUpgradeSafety('ambiguous-upgrades-from'));
|
|
42
|
+
t.true(error?.message.includes('Found multiple contracts with name'), error?.message);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('bad upgrade from 0.8.8 to 0.8.9', async t => {
|
|
46
|
+
await fs.mkdir('bad-upgrade');
|
|
47
|
+
|
|
48
|
+
const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Storage088.sol:Storage088`);
|
|
49
|
+
await fs.writeFile('bad-upgrade/storage088.json', JSON.stringify(buildInfo));
|
|
50
|
+
|
|
51
|
+
const buildInfo2 = await artifacts.getBuildInfo(`contracts/test/cli/Storage089.sol:Storage089`);
|
|
52
|
+
await fs.writeFile('bad-upgrade/storage089.json', JSON.stringify(buildInfo2));
|
|
53
|
+
|
|
54
|
+
const report = await validateUpgradeSafety('bad-upgrade');
|
|
55
|
+
t.false(report.ok);
|
|
56
|
+
t.snapshot(report.explain());
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('reference contract not found', async t => {
|
|
60
|
+
const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Storage089.sol:Storage089`);
|
|
61
|
+
await fs.mkdir('ref-not-found');
|
|
62
|
+
await fs.writeFile('ref-not-found/storage089.json', JSON.stringify(buildInfo));
|
|
63
|
+
|
|
64
|
+
const error = await t.throwsAsync(validateUpgradeSafety('ref-not-found'));
|
|
65
|
+
t.true(error instanceof ReferenceContractNotFound);
|
|
66
|
+
t.is(
|
|
67
|
+
error?.message,
|
|
68
|
+
'Could not find contract Storage088 referenced in contracts/test/cli/Storage089.sol:Storage089.',
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('invalid annotation args - upgrades-from', async t => {
|
|
73
|
+
const fullyQualifiedName = `contracts/test/cli/InvalidAnnotationArgsUpgradesFrom.sol:InvalidAnnotationArgsUpgradesFrom`;
|
|
74
|
+
|
|
75
|
+
const buildInfo = await artifacts.getBuildInfo(fullyQualifiedName);
|
|
76
|
+
await fs.mkdir('invalid-annotation-args-upgrades-from');
|
|
77
|
+
await fs.writeFile('invalid-annotation-args-upgrades-from/1.json', JSON.stringify(buildInfo));
|
|
78
|
+
|
|
79
|
+
const error = await t.throwsAsync(validateUpgradeSafety('invalid-annotation-args-upgrades-from'));
|
|
80
|
+
t.true(
|
|
81
|
+
error?.message.includes(
|
|
82
|
+
`Invalid number of arguments for @custom:oz-upgrades-from annotation in contract ${fullyQualifiedName}.`,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
t.true(error?.message.includes('Found 0, expected 1'));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('invalid annotation args - upgrades', async t => {
|
|
89
|
+
const fullyQualifiedName = `contracts/test/cli/InvalidAnnotationArgsUpgrades.sol:InvalidAnnotationArgsUpgrades`;
|
|
90
|
+
|
|
91
|
+
const buildInfo = await artifacts.getBuildInfo(fullyQualifiedName);
|
|
92
|
+
await fs.mkdir('invalid-annotation-args-upgrades');
|
|
93
|
+
await fs.writeFile('invalid-annotation-args-upgrades/1.json', JSON.stringify(buildInfo));
|
|
94
|
+
|
|
95
|
+
const error = await t.throwsAsync(validateUpgradeSafety('invalid-annotation-args-upgrades'));
|
|
96
|
+
t.true(
|
|
97
|
+
error?.message.includes(
|
|
98
|
+
`Invalid number of arguments for @custom:oz-upgrades annotation in contract ${fullyQualifiedName}.`,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
t.true(error?.message.includes('Found 1, expected 0'));
|
|
102
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Snapshot report for `src/cli/validate/validate-upgrade-safety.test.ts`
|
|
2
|
+
|
|
3
|
+
The actual snapshot is saved in `validate-upgrade-safety.test.ts.snap`.
|
|
4
|
+
|
|
5
|
+
Generated by [AVA](https://avajs.dev).
|
|
6
|
+
|
|
7
|
+
## validate upgrade safety
|
|
8
|
+
|
|
9
|
+
> Snapshot 1
|
|
10
|
+
|
|
11
|
+
` ✔ contracts/test/cli/Validate.sol:Safe␊
|
|
12
|
+
␊
|
|
13
|
+
✔ contracts/test/cli/Validate.sol:HasInitializer␊
|
|
14
|
+
␊
|
|
15
|
+
✔ contracts/test/cli/Validate.sol:HasUpgradeTo␊
|
|
16
|
+
␊
|
|
17
|
+
✘ contracts/test/cli/Validate.sol:HasUpgradeToConstructorUnsafe␊
|
|
18
|
+
␊
|
|
19
|
+
contracts/test/cli/Validate.sol:33: Contract \`HasUpgradeToConstructorUnsafe\` has a constructor␊
|
|
20
|
+
Define an initializer instead␊
|
|
21
|
+
https://zpl.in/upgrades/error-001␊
|
|
22
|
+
␊
|
|
23
|
+
✘ contracts/test/cli/Validate.sol:MultipleUnsafe␊
|
|
24
|
+
␊
|
|
25
|
+
contracts/test/cli/Validate.sol:53: Use of delegatecall is not allowed␊
|
|
26
|
+
https://zpl.in/upgrades/error-002␊
|
|
27
|
+
␊
|
|
28
|
+
contracts/test/cli/Validate.sol:58: Use of delegatecall is not allowed␊
|
|
29
|
+
https://zpl.in/upgrades/error-002␊
|
|
30
|
+
␊
|
|
31
|
+
contracts/test/cli/Validate.sol:44: Use of selfdestruct is not allowed␊
|
|
32
|
+
https://zpl.in/upgrades/error-003␊
|
|
33
|
+
␊
|
|
34
|
+
✘ contracts/test/cli/Validate.sol:InheritsMultipleUnsafe␊
|
|
35
|
+
␊
|
|
36
|
+
contracts/test/cli/Validate.sol:53: Use of delegatecall is not allowed␊
|
|
37
|
+
https://zpl.in/upgrades/error-002␊
|
|
38
|
+
␊
|
|
39
|
+
contracts/test/cli/Validate.sol:58: Use of delegatecall is not allowed␊
|
|
40
|
+
https://zpl.in/upgrades/error-002␊
|
|
41
|
+
␊
|
|
42
|
+
contracts/test/cli/Validate.sol:44: Use of selfdestruct is not allowed␊
|
|
43
|
+
https://zpl.in/upgrades/error-003␊
|
|
44
|
+
␊
|
|
45
|
+
✘ contracts/test/cli/Validate.sol:UpgradesFromUUPS (upgrades from contracts/test/cli/Validate.sol:HasUpgradeTo)␊
|
|
46
|
+
␊
|
|
47
|
+
contracts/test/cli/Validate.sol:72: Implementation is missing a public \`upgradeTo(address)\` function␊
|
|
48
|
+
Inherit UUPSUpgradeable to include this function in your contract␊
|
|
49
|
+
@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol␊
|
|
50
|
+
https://zpl.in/upgrades/error-008␊
|
|
51
|
+
␊
|
|
52
|
+
✔ contracts/test/cli/Validate.sol:UpgradesFromTransparent (upgrades from contracts/test/cli/Validate.sol:HasInitializer)␊
|
|
53
|
+
␊
|
|
54
|
+
✘ contracts/test/cli/Validate.sol:UnsafeAndStorageLayoutErrors (upgrades from contracts/test/cli/Validate.sol:StorageV1)␊
|
|
55
|
+
␊
|
|
56
|
+
contracts/test/cli/Validate.sol:97: Use of selfdestruct is not allowed␊
|
|
57
|
+
https://zpl.in/upgrades/error-003␊
|
|
58
|
+
␊
|
|
59
|
+
contracts/test/cli/Validate.sol:94: Layout changed for \`__gap\` (uint256[49] -> uint256[49])␊
|
|
60
|
+
- Slot changed from 1 to 2␊
|
|
61
|
+
> Set __gap array to size 48␊
|
|
62
|
+
␊
|
|
63
|
+
✔ contracts/test/cli/Validate.sol:BecomesSafe (upgrades from contracts/test/cli/Validate.sol:MultipleUnsafe)␊
|
|
64
|
+
␊
|
|
65
|
+
✘ contracts/test/cli/Validate.sol:BecomesBadLayout (upgrades from contracts/test/cli/Validate.sol:BecomesSafe)␊
|
|
66
|
+
␊
|
|
67
|
+
BecomesSafe: Deleted \`x\`␊
|
|
68
|
+
> Keep the variable even if unused␊
|
|
69
|
+
␊
|
|
70
|
+
✘ contracts/test/cli/Validate.sol:StillUnsafe (upgrades from contracts/test/cli/Validate.sol:NonUpgradeable)␊
|
|
71
|
+
␊
|
|
72
|
+
contracts/test/cli/Validate.sol:119: Use of selfdestruct is not allowed␊
|
|
73
|
+
https://zpl.in/upgrades/error-003␊
|
|
74
|
+
␊
|
|
75
|
+
✔ contracts/test/cli/Validate.sol:InheritsAbstractUpgradeable␊
|
|
76
|
+
␊
|
|
77
|
+
✘ contracts/test/cli/Validate.sol:BothAnnotationsUnsafe (upgrades from contracts/test/cli/Validate.sol:StorageV1)␊
|
|
78
|
+
␊
|
|
79
|
+
contracts/test/cli/Validate.sol:140: Use of selfdestruct is not allowed␊
|
|
80
|
+
https://zpl.in/upgrades/error-003␊
|
|
81
|
+
␊
|
|
82
|
+
contracts/test/cli/Validate.sol:137: Layout changed for \`__gap\` (uint256[49] -> uint256[49])␊
|
|
83
|
+
- Slot changed from 1 to 2␊
|
|
84
|
+
> Set __gap array to size 48␊
|
|
85
|
+
␊
|
|
86
|
+
FAILED (14 upgradeable contracts detected, 6 passed, 8 failed)`
|
|
87
|
+
|
|
88
|
+
## bad upgrade from 0.8.8 to 0.8.9
|
|
89
|
+
|
|
90
|
+
> Snapshot 1
|
|
91
|
+
|
|
92
|
+
` ✘ contracts/test/cli/Storage089.sol:Storage089 (upgrades from contracts/test/cli/Storage088.sol:Storage088)␊
|
|
93
|
+
␊
|
|
94
|
+
contracts/test/cli/Storage089.sol:9: Upgraded \`my_user_value\` to an incompatible type␊
|
|
95
|
+
- Bad upgrade from Storage088.MyUserValueType to Storage089.MyUserValueType␊
|
|
96
|
+
Different representation sizes␊
|
|
97
|
+
␊
|
|
98
|
+
FAILED (1 upgradeable contract detected, 0 passed, 1 failed)`
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ValidationOptions } from '../..';
|
|
2
|
+
|
|
3
|
+
import { getBuildInfoFiles } from './build-info-file';
|
|
4
|
+
import { getContractReports } from './contract-report';
|
|
5
|
+
import { ProjectReport, getProjectReport } from './project-report';
|
|
6
|
+
import { validateBuildInfoContracts } from './validations';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validation options for upgrade safety checks.
|
|
10
|
+
*/
|
|
11
|
+
export type ValidateUpgradeSafetyOptions = Omit<ValidationOptions, 'kind'>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validates the upgrade safety of all contracts in the build info dir's build info files.
|
|
15
|
+
* Only contracts that are detected as upgradeable will be validated.
|
|
16
|
+
*
|
|
17
|
+
* @param buildInfoDir Absolute path of build info directory, or undefined to use the default Hardhat or Foundry build-info dir.
|
|
18
|
+
* @param opts Validation options, or undefined to use the default validation options.
|
|
19
|
+
* @returns The project report.
|
|
20
|
+
*/
|
|
21
|
+
export async function validateUpgradeSafety(
|
|
22
|
+
buildInfoDir?: string,
|
|
23
|
+
opts: ValidateUpgradeSafetyOptions = {},
|
|
24
|
+
): Promise<ProjectReport> {
|
|
25
|
+
const buildInfoFiles = await getBuildInfoFiles(buildInfoDir);
|
|
26
|
+
const sourceContracts = validateBuildInfoContracts(buildInfoFiles);
|
|
27
|
+
const contractReports = getContractReports(sourceContracts, opts);
|
|
28
|
+
return getProjectReport(contractReports);
|
|
29
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import _test, { ExecutionContext, TestFn } from 'ava';
|
|
2
|
+
|
|
3
|
+
import { artifacts } from 'hardhat';
|
|
4
|
+
import { validateBuildInfoContracts } from './validations';
|
|
5
|
+
import { UpgradeableContractReport, getContractReports } from './contract-report';
|
|
6
|
+
|
|
7
|
+
interface Context {
|
|
8
|
+
reports: UpgradeableContractReport[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const test = _test as TestFn<Context>;
|
|
12
|
+
|
|
13
|
+
const SOURCE_FILE = 'contracts/test/cli/Validate.sol';
|
|
14
|
+
|
|
15
|
+
test.before(async t => {
|
|
16
|
+
const buildInfo = await artifacts.getBuildInfo(`${SOURCE_FILE}:Safe`);
|
|
17
|
+
|
|
18
|
+
if (buildInfo === undefined) {
|
|
19
|
+
t.fail();
|
|
20
|
+
} else {
|
|
21
|
+
const sourceContracts = validateBuildInfoContracts([buildInfo]);
|
|
22
|
+
t.context.reports = getContractReports(sourceContracts, {});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function getReport(t: ExecutionContext<Context>, contractName: string) {
|
|
27
|
+
return t.context.reports.find(r => r.contract === `${SOURCE_FILE}:${contractName}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assertReport(
|
|
31
|
+
t: ExecutionContext<Context>,
|
|
32
|
+
report: UpgradeableContractReport | undefined,
|
|
33
|
+
valid: boolean | undefined,
|
|
34
|
+
) {
|
|
35
|
+
if (valid === undefined) {
|
|
36
|
+
t.true(report === undefined);
|
|
37
|
+
} else if (valid === true) {
|
|
38
|
+
t.true(report !== undefined);
|
|
39
|
+
t.true(report?.ok);
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
41
|
+
t.regex(report!.explain(), /✔/);
|
|
42
|
+
} else if (valid === false) {
|
|
43
|
+
t.true(report !== undefined);
|
|
44
|
+
t.false(report?.ok);
|
|
45
|
+
t.snapshot(report?.explain());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function testValid(name: string, valid: boolean | undefined) {
|
|
50
|
+
const expectationString = valid === undefined ? 'ignores' : valid ? 'accepts' : 'rejects';
|
|
51
|
+
const testName = [expectationString, name].join(' ');
|
|
52
|
+
test(testName, t => {
|
|
53
|
+
const report = getReport(t, name);
|
|
54
|
+
assertReport(t, report, valid);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
testValid('Safe', true);
|
|
59
|
+
testValid('MultipleUnsafe', false);
|
|
60
|
+
testValid('NonUpgradeable', undefined);
|
|
61
|
+
testValid('HasInitializer', true);
|
|
62
|
+
testValid('HasUpgradeTo', true);
|
|
63
|
+
testValid('HasUpgradeToConstructorUnsafe', false);
|
|
64
|
+
testValid('InheritsMultipleUnsafe', false);
|
|
65
|
+
testValid('UpgradesFromUUPS', false);
|
|
66
|
+
testValid('UpgradesFromTransparent', true);
|
|
67
|
+
testValid('UnsafeAndStorageLayoutErrors', false);
|
|
68
|
+
testValid('BecomesSafe', true);
|
|
69
|
+
testValid('BecomesBadLayout', false);
|
|
70
|
+
testValid('StillUnsafe', false);
|
|
71
|
+
testValid('AbstractUpgradeable', undefined);
|
|
72
|
+
testValid('InheritsAbstractUpgradeable', true);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Snapshot report for `src/cli/validate/validations.test.ts`
|
|
2
|
+
|
|
3
|
+
The actual snapshot is saved in `validations.test.ts.snap`.
|
|
4
|
+
|
|
5
|
+
Generated by [AVA](https://avajs.dev).
|
|
6
|
+
|
|
7
|
+
## rejects MultipleUnsafe
|
|
8
|
+
|
|
9
|
+
> Snapshot 1
|
|
10
|
+
|
|
11
|
+
` ✘ contracts/test/cli/Validate.sol:MultipleUnsafe␊
|
|
12
|
+
␊
|
|
13
|
+
contracts/test/cli/Validate.sol:53: Use of delegatecall is not allowed␊
|
|
14
|
+
https://zpl.in/upgrades/error-002␊
|
|
15
|
+
␊
|
|
16
|
+
contracts/test/cli/Validate.sol:58: Use of delegatecall is not allowed␊
|
|
17
|
+
https://zpl.in/upgrades/error-002␊
|
|
18
|
+
␊
|
|
19
|
+
contracts/test/cli/Validate.sol:44: Use of selfdestruct is not allowed␊
|
|
20
|
+
https://zpl.in/upgrades/error-003`
|
|
21
|
+
|
|
22
|
+
## rejects HasUpgradeToConstructorUnsafe
|
|
23
|
+
|
|
24
|
+
> Snapshot 1
|
|
25
|
+
|
|
26
|
+
` ✘ contracts/test/cli/Validate.sol:HasUpgradeToConstructorUnsafe␊
|
|
27
|
+
␊
|
|
28
|
+
contracts/test/cli/Validate.sol:33: Contract \`HasUpgradeToConstructorUnsafe\` has a constructor␊
|
|
29
|
+
Define an initializer instead␊
|
|
30
|
+
https://zpl.in/upgrades/error-001`
|
|
31
|
+
|
|
32
|
+
## rejects InheritsMultipleUnsafe
|
|
33
|
+
|
|
34
|
+
> Snapshot 1
|
|
35
|
+
|
|
36
|
+
` ✘ contracts/test/cli/Validate.sol:InheritsMultipleUnsafe␊
|
|
37
|
+
␊
|
|
38
|
+
contracts/test/cli/Validate.sol:53: Use of delegatecall is not allowed␊
|
|
39
|
+
https://zpl.in/upgrades/error-002␊
|
|
40
|
+
␊
|
|
41
|
+
contracts/test/cli/Validate.sol:58: Use of delegatecall is not allowed␊
|
|
42
|
+
https://zpl.in/upgrades/error-002␊
|
|
43
|
+
␊
|
|
44
|
+
contracts/test/cli/Validate.sol:44: Use of selfdestruct is not allowed␊
|
|
45
|
+
https://zpl.in/upgrades/error-003`
|
|
46
|
+
|
|
47
|
+
## rejects UpgradesFromUUPS
|
|
48
|
+
|
|
49
|
+
> Snapshot 1
|
|
50
|
+
|
|
51
|
+
` ✘ contracts/test/cli/Validate.sol:UpgradesFromUUPS (upgrades from contracts/test/cli/Validate.sol:HasUpgradeTo)␊
|
|
52
|
+
␊
|
|
53
|
+
contracts/test/cli/Validate.sol:72: Implementation is missing a public \`upgradeTo(address)\` function␊
|
|
54
|
+
Inherit UUPSUpgradeable to include this function in your contract␊
|
|
55
|
+
@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol␊
|
|
56
|
+
https://zpl.in/upgrades/error-008`
|
|
57
|
+
|
|
58
|
+
## rejects UnsafeAndStorageLayoutErrors
|
|
59
|
+
|
|
60
|
+
> Snapshot 1
|
|
61
|
+
|
|
62
|
+
` ✘ contracts/test/cli/Validate.sol:UnsafeAndStorageLayoutErrors (upgrades from contracts/test/cli/Validate.sol:StorageV1)␊
|
|
63
|
+
␊
|
|
64
|
+
contracts/test/cli/Validate.sol:97: Use of selfdestruct is not allowed␊
|
|
65
|
+
https://zpl.in/upgrades/error-003␊
|
|
66
|
+
␊
|
|
67
|
+
contracts/test/cli/Validate.sol:94: Layout changed for \`__gap\` (uint256[49] -> uint256[49])␊
|
|
68
|
+
- Slot changed from 1 to 2␊
|
|
69
|
+
> Set __gap array to size 48`
|
|
70
|
+
|
|
71
|
+
## rejects BecomesBadLayout
|
|
72
|
+
|
|
73
|
+
> Snapshot 1
|
|
74
|
+
|
|
75
|
+
` ✘ contracts/test/cli/Validate.sol:BecomesBadLayout (upgrades from contracts/test/cli/Validate.sol:BecomesSafe)␊
|
|
76
|
+
␊
|
|
77
|
+
BecomesSafe: Deleted \`x\`␊
|
|
78
|
+
> Keep the variable even if unused`
|
|
79
|
+
|
|
80
|
+
## rejects StillUnsafe
|
|
81
|
+
|
|
82
|
+
> Snapshot 1
|
|
83
|
+
|
|
84
|
+
` ✘ contracts/test/cli/Validate.sol:StillUnsafe (upgrades from contracts/test/cli/Validate.sol:NonUpgradeable)␊
|
|
85
|
+
␊
|
|
86
|
+
contracts/test/cli/Validate.sol:119: Use of selfdestruct is not allowed␊
|
|
87
|
+
https://zpl.in/upgrades/error-003`
|
|
Binary file
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { solcInputOutputDecoder, validate, SolcOutput, SolcInput, ValidationRunData } from '../..';
|
|
2
|
+
|
|
3
|
+
import debug from '../../utils/debug';
|
|
4
|
+
|
|
5
|
+
import { findAll } from 'solidity-ast/utils';
|
|
6
|
+
import { ContractDefinition } from 'solidity-ast';
|
|
7
|
+
|
|
8
|
+
import { getFullyQualifiedName } from '../../utils/contract-name';
|
|
9
|
+
import { BuildInfoFile } from './build-info-file';
|
|
10
|
+
|
|
11
|
+
export interface SourceContract {
|
|
12
|
+
node: ContractDefinition;
|
|
13
|
+
name: string;
|
|
14
|
+
fullyQualifiedName: string;
|
|
15
|
+
validationData: ValidationRunData;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validateBuildInfoContracts(buildInfoFiles: BuildInfoFile[]): SourceContract[] {
|
|
19
|
+
const sourceContracts: SourceContract[] = [];
|
|
20
|
+
for (const buildInfoFile of buildInfoFiles) {
|
|
21
|
+
const validations = runValidations(buildInfoFile.input, buildInfoFile.output);
|
|
22
|
+
addContractsFromBuildInfo(buildInfoFile, validations, sourceContracts);
|
|
23
|
+
}
|
|
24
|
+
return sourceContracts;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runValidations(solcInput: SolcInput, solcOutput: SolcOutput) {
|
|
28
|
+
const decodeSrc = solcInputOutputDecoder(solcInput, solcOutput);
|
|
29
|
+
const validation = validate(solcOutput, decodeSrc);
|
|
30
|
+
return validation;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function addContractsFromBuildInfo(
|
|
34
|
+
buildInfoFile: BuildInfoFile,
|
|
35
|
+
validationData: ValidationRunData,
|
|
36
|
+
sourceContracts: SourceContract[],
|
|
37
|
+
) {
|
|
38
|
+
for (const sourcePath in buildInfoFile.output.sources) {
|
|
39
|
+
const ast = buildInfoFile.output.sources[sourcePath].ast;
|
|
40
|
+
|
|
41
|
+
for (const contractDef of findAll('ContractDefinition', ast)) {
|
|
42
|
+
const fullyQualifiedName = getFullyQualifiedName(sourcePath, contractDef.name);
|
|
43
|
+
debug('Found: ' + fullyQualifiedName);
|
|
44
|
+
|
|
45
|
+
sourceContracts.push({
|
|
46
|
+
node: contractDef,
|
|
47
|
+
name: contractDef.name,
|
|
48
|
+
fullyQualifiedName,
|
|
49
|
+
validationData: validationData,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|