@openzeppelin/upgrades-core 1.45.0-alpha.1 → 1.46.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-v5/proxy/ERC1967/ERC1967Proxy.sol/ERC1967Proxy.json +2 -2
- package/artifacts/@openzeppelin/contracts-v5/proxy/transparent/ProxyAdmin.sol/ProxyAdmin.json +2 -2
- package/artifacts/@openzeppelin/contracts-v5/proxy/transparent/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json +2 -2
- package/artifacts/build-info-v5.json +22 -22
- package/dist/cli/validate/build-info-file.js +4 -1
- package/dist/cli/validate/build-info-file.js.map +1 -1
- package/dist/cli/validate/build-info-file.test.js +115 -1
- package/dist/cli/validate/build-info-file.test.js.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +1 -0
- package/dist/provider.js.map +1 -1
- package/dist/storage/extract.d.ts.map +1 -1
- package/dist/storage/extract.js +21 -7
- package/dist/storage/extract.js.map +1 -1
- package/dist/storage/namespace.d.ts.map +1 -1
- package/dist/storage/namespace.js +18 -11
- package/dist/storage/namespace.js.map +1 -1
- package/dist/storage-erc7201-builtin.test.d.ts +2 -0
- package/dist/storage-erc7201-builtin.test.d.ts.map +1 -0
- package/dist/storage-erc7201-builtin.test.js +105 -0
- package/dist/storage-erc7201-builtin.test.js.map +1 -0
- package/dist/utils/erc7201-builtin.d.ts +24 -0
- package/dist/utils/erc7201-builtin.d.ts.map +1 -0
- package/dist/utils/erc7201-builtin.js +83 -0
- package/dist/utils/erc7201-builtin.js.map +1 -0
- package/dist/utils/erc7201.d.ts.map +1 -1
- package/dist/utils/erc7201.js +5 -3
- package/dist/utils/erc7201.js.map +1 -1
- package/dist/utils/erc7201.test.js +10 -0
- package/dist/utils/erc7201.test.js.map +1 -1
- package/dist/utils/integer-literals.d.ts +19 -6
- package/dist/utils/integer-literals.d.ts.map +1 -1
- package/dist/utils/integer-literals.js +28 -10
- package/dist/utils/integer-literals.js.map +1 -1
- package/dist/utils/integer-literals.test.js +12 -0
- package/dist/utils/integer-literals.test.js.map +1 -1
- package/package.json +3 -3
- package/src/cli/validate/build-info-file.test.ts +156 -1
- package/src/cli/validate/build-info-file.ts +6 -1
- package/src/provider.ts +1 -0
- package/src/storage/extract.ts +28 -12
- package/src/storage/namespace.ts +21 -11
- package/src/storage-erc7201-builtin.test.ts +127 -0
- package/src/utils/erc7201-builtin.ts +91 -0
- package/src/utils/erc7201.test.ts +12 -0
- package/src/utils/erc7201.ts +6 -4
- package/src/utils/integer-literals.test.ts +15 -1
- package/src/utils/integer-literals.ts +29 -10
|
@@ -313,6 +313,55 @@ test.serial('get build info files - default foundry', async t => {
|
|
|
313
313
|
assertBuildInfoFiles(t, buildInfoFiles);
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
+
test.serial('get build info files - Foundry format success path', async t => {
|
|
317
|
+
const dir = 'foundry-format-success';
|
|
318
|
+
|
|
319
|
+
await fs.mkdir(dir, { recursive: true });
|
|
320
|
+
await fs.writeFile(
|
|
321
|
+
`${dir}/build-info.json`,
|
|
322
|
+
JSON.stringify({
|
|
323
|
+
_format: 'ethers-rs-sol-build-info-1',
|
|
324
|
+
...BUILD_INFO,
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const buildInfoFiles = await getBuildInfoFiles(dir);
|
|
329
|
+
|
|
330
|
+
t.is(buildInfoFiles.length, 1);
|
|
331
|
+
t.is(buildInfoFiles[0]?.dirShortName, dir);
|
|
332
|
+
t.is(buildInfoFiles[0]?.solcVersion, BUILD_INFO.solcVersion);
|
|
333
|
+
t.deepEqual(buildInfoFiles[0]?.input, BUILD_INFO.input);
|
|
334
|
+
t.deepEqual(buildInfoFiles[0]?.output, BUILD_INFO.output);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test.serial('get build info files - Hardhat 3 split format success path', async t => {
|
|
338
|
+
const dir = 'hh3-format-success';
|
|
339
|
+
|
|
340
|
+
await fs.mkdir(dir, { recursive: true });
|
|
341
|
+
await fs.writeFile(
|
|
342
|
+
`${dir}/solc-0_8_9-abc123.json`,
|
|
343
|
+
JSON.stringify({
|
|
344
|
+
_format: 'hh3-sol-build-info-1',
|
|
345
|
+
input: BUILD_INFO.input,
|
|
346
|
+
solcVersion: BUILD_INFO.solcVersion,
|
|
347
|
+
}),
|
|
348
|
+
);
|
|
349
|
+
await fs.writeFile(
|
|
350
|
+
`${dir}/solc-0_8_9-abc123.output.json`,
|
|
351
|
+
JSON.stringify({
|
|
352
|
+
output: BUILD_INFO.output,
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const buildInfoFiles = await getBuildInfoFiles(dir);
|
|
357
|
+
|
|
358
|
+
t.is(buildInfoFiles.length, 1);
|
|
359
|
+
t.is(buildInfoFiles[0]?.dirShortName, dir);
|
|
360
|
+
t.is(buildInfoFiles[0]?.solcVersion, BUILD_INFO.solcVersion);
|
|
361
|
+
t.deepEqual(buildInfoFiles[0]?.input, BUILD_INFO.input);
|
|
362
|
+
t.deepEqual(buildInfoFiles[0]?.output, BUILD_INFO.output);
|
|
363
|
+
});
|
|
364
|
+
|
|
316
365
|
test.serial('get build info files - both hardhat and foundry dirs exist', async t => {
|
|
317
366
|
await fs.mkdir('artifacts/build-info', { recursive: true });
|
|
318
367
|
await fs.writeFile('artifacts/build-info/build-info.json', JSON.stringify(BUILD_INFO));
|
|
@@ -363,6 +412,7 @@ test.serial('generic invalid build info file', async t => {
|
|
|
363
412
|
await fs.writeFile('invalid-build-info/invalid.json', JSON.stringify({ output: {} }));
|
|
364
413
|
const error = await t.throwsAsync(getBuildInfoFiles('invalid-build-info'));
|
|
365
414
|
t.true(error?.message.includes('must include Solidity compiler input, output, and solcVersion'));
|
|
415
|
+
t.true(error?.message.includes('Got format: unknown'));
|
|
366
416
|
});
|
|
367
417
|
|
|
368
418
|
test.serial('Foundry format (ethers-rs) with missing output suggests forge clean && forge build', async t => {
|
|
@@ -383,6 +433,21 @@ test.serial('Foundry format (ethers-rs) with missing output suggests forge clean
|
|
|
383
433
|
t.true(error?.message.includes('forge clean && forge build'));
|
|
384
434
|
});
|
|
385
435
|
|
|
436
|
+
test.serial('Hardhat 3 format with missing input and solcVersion suggests hardhat compile', async t => {
|
|
437
|
+
await fs.mkdir('hh3-format-missing-input', { recursive: true });
|
|
438
|
+
|
|
439
|
+
await fs.writeFile(
|
|
440
|
+
'hh3-format-missing-input/solc-0_8_0-abc123.json',
|
|
441
|
+
JSON.stringify({
|
|
442
|
+
_format: 'hh3-sol-build-info-1',
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
const error = await t.throwsAsync(getBuildInfoFiles('hh3-format-missing-input'));
|
|
446
|
+
t.true(error?.message.includes('Hardhat 3 format'));
|
|
447
|
+
t.true(error?.message.includes('must contain input and solcVersion'));
|
|
448
|
+
t.true(error?.message.includes('hardhat compile'));
|
|
449
|
+
});
|
|
450
|
+
|
|
386
451
|
test.serial('Hardhat 3 format with missing .output.json suggests hardhat compile', async t => {
|
|
387
452
|
await fs.mkdir('hh3-format-missing-output', { recursive: true });
|
|
388
453
|
|
|
@@ -396,7 +461,29 @@ test.serial('Hardhat 3 format with missing .output.json suggests hardhat compile
|
|
|
396
461
|
}),
|
|
397
462
|
);
|
|
398
463
|
const error = await t.throwsAsync(getBuildInfoFiles('hh3-format-missing-output'));
|
|
399
|
-
t.true(error?.message.includes('could not be read')
|
|
464
|
+
t.true(error?.message.includes('could not be read'));
|
|
465
|
+
t.false(error?.message.includes('missing Solidity compiler output'));
|
|
466
|
+
t.true(error?.message.includes('Hardhat 3'));
|
|
467
|
+
t.true(error?.message.includes('hardhat compile'));
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test.serial('Hardhat 3 format with empty .output.json reports missing compiler output', async t => {
|
|
471
|
+
await fs.mkdir('hh3-format-empty-output', { recursive: true });
|
|
472
|
+
|
|
473
|
+
await fs.writeFile(
|
|
474
|
+
'hh3-format-empty-output/solc-0_8_0-abc123.json',
|
|
475
|
+
JSON.stringify({
|
|
476
|
+
_format: 'hh3-sol-build-info-1',
|
|
477
|
+
input: BUILD_INFO.input,
|
|
478
|
+
solcVersion: '0.8.9',
|
|
479
|
+
}),
|
|
480
|
+
);
|
|
481
|
+
await fs.writeFile('hh3-format-empty-output/solc-0_8_0-abc123.output.json', JSON.stringify({}));
|
|
482
|
+
|
|
483
|
+
const error = await t.throwsAsync(getBuildInfoFiles('hh3-format-empty-output'));
|
|
484
|
+
t.true(error?.message.includes('missing Solidity compiler output'));
|
|
485
|
+
t.false(error?.message.includes('could not be read'));
|
|
486
|
+
t.true(error?.message.includes('Hardhat 3'));
|
|
400
487
|
t.true(error?.message.includes('hardhat compile'));
|
|
401
488
|
});
|
|
402
489
|
|
|
@@ -494,6 +581,74 @@ test.serial('no output selection', async t => {
|
|
|
494
581
|
t.true(error?.message.includes('is not from a full compilation'));
|
|
495
582
|
});
|
|
496
583
|
|
|
584
|
+
test.serial('Hardhat 3 format with userSourceNameMap remaps input and output sources', async t => {
|
|
585
|
+
const dir = 'hh3-source-name-map';
|
|
586
|
+
|
|
587
|
+
await fs.mkdir(dir, { recursive: true });
|
|
588
|
+
await fs.writeFile(
|
|
589
|
+
`${dir}/solc-0_8_9-abc123.json`,
|
|
590
|
+
JSON.stringify({
|
|
591
|
+
_format: 'hh3-sol-build-info-1',
|
|
592
|
+
solcVersion: '0.8.9',
|
|
593
|
+
input: {
|
|
594
|
+
language: 'Solidity',
|
|
595
|
+
sources: {
|
|
596
|
+
'project/contracts/MyContract.sol': {
|
|
597
|
+
content: 'contract MyContract {}',
|
|
598
|
+
},
|
|
599
|
+
'lib/Helper.sol': {
|
|
600
|
+
content: 'contract Helper {}',
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
settings: {
|
|
604
|
+
outputSelection: {
|
|
605
|
+
'*': {
|
|
606
|
+
'*': ['storageLayout'],
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
// Only maps project sources, not library sources
|
|
612
|
+
userSourceNameMap: {
|
|
613
|
+
'contracts/MyContract.sol': 'project/contracts/MyContract.sol',
|
|
614
|
+
},
|
|
615
|
+
}),
|
|
616
|
+
);
|
|
617
|
+
await fs.writeFile(
|
|
618
|
+
`${dir}/solc-0_8_9-abc123.output.json`,
|
|
619
|
+
JSON.stringify({
|
|
620
|
+
sources: {
|
|
621
|
+
'project/contracts/MyContract.sol': { ast: {}, id: 0 },
|
|
622
|
+
'lib/Helper.sol': { ast: {}, id: 1 },
|
|
623
|
+
},
|
|
624
|
+
contracts: {
|
|
625
|
+
'project/contracts/MyContract.sol': { MyContract: {} },
|
|
626
|
+
},
|
|
627
|
+
}),
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
const buildInfoFiles = await getBuildInfoFiles(dir);
|
|
631
|
+
|
|
632
|
+
t.is(buildInfoFiles.length, 1);
|
|
633
|
+
const file = buildInfoFiles[0];
|
|
634
|
+
|
|
635
|
+
const user = 'contracts/MyContract.sol';
|
|
636
|
+
const canonical = 'project/contracts/MyContract.sol';
|
|
637
|
+
const lib = 'lib/Helper.sol';
|
|
638
|
+
|
|
639
|
+
// Mapped: canonical paths replaced with user paths
|
|
640
|
+
t.truthy(file.input.sources[user]);
|
|
641
|
+
t.falsy(file.input.sources[canonical]);
|
|
642
|
+
t.truthy(file.output.sources[user]);
|
|
643
|
+
t.falsy(file.output.sources[canonical]);
|
|
644
|
+
t.truthy(file.output.contracts?.[user]);
|
|
645
|
+
t.falsy(file.output.contracts?.[canonical]);
|
|
646
|
+
|
|
647
|
+
// Unmapped: library deps preserved as-is
|
|
648
|
+
t.truthy(file.input.sources[lib]);
|
|
649
|
+
t.truthy(file.output.sources[lib]);
|
|
650
|
+
});
|
|
651
|
+
|
|
497
652
|
function assertBuildInfoFiles(t: ExecutionContext, buildInfoFiles: BuildInfoFile[]) {
|
|
498
653
|
t.is(buildInfoFiles.length, 2);
|
|
499
654
|
|
|
@@ -222,7 +222,12 @@ async function loadBuildInfo(buildInfoFilePath: string): Promise<{
|
|
|
222
222
|
);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
if (
|
|
225
|
+
if (
|
|
226
|
+
outputData === undefined ||
|
|
227
|
+
outputData === null ||
|
|
228
|
+
typeof outputData !== 'object' ||
|
|
229
|
+
(!('sources' in outputData) && !('contracts' in outputData) && !('errors' in outputData))
|
|
230
|
+
) {
|
|
226
231
|
throw new ValidateCommandError(
|
|
227
232
|
`Build info file ${buildInfoFilePath} does not contain output, and output file ${outputFilePath} is missing Solidity compiler output.`,
|
|
228
233
|
() => HH3_BUILD_INFO_HELP,
|
package/src/provider.ts
CHANGED
|
@@ -141,6 +141,7 @@ export const networkNames: { [chainId in number]?: string } = Object.freeze({
|
|
|
141
141
|
42161: 'arbitrum-one',
|
|
142
142
|
42170: 'arbitrum-nova',
|
|
143
143
|
421613: 'arbitrum-goerli',
|
|
144
|
+
421614: 'arbitrum-sepolia',
|
|
144
145
|
43113: 'avalanche-fuji',
|
|
145
146
|
43114: 'avalanche',
|
|
146
147
|
42220: 'celo',
|
package/src/storage/extract.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { pick } from '../utils/pick';
|
|
|
16
16
|
import { execall } from '../utils/execall';
|
|
17
17
|
import { loadNamespaces } from './namespace';
|
|
18
18
|
import { UpgradesError } from '../error';
|
|
19
|
+
import { resolveBaseSlot } from '../utils/erc7201-builtin';
|
|
19
20
|
|
|
20
21
|
const currentLayoutVersion = '1.2';
|
|
21
22
|
|
|
@@ -106,19 +107,34 @@ export function extractStorageLayout(
|
|
|
106
107
|
|
|
107
108
|
function loadBaseSlot(contractDef: ContractDefinition, decodeSrc: SrcDecoder, layout: StorageLayout) {
|
|
108
109
|
if (contractDef.storageLayout !== undefined) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Then when comparing layouts in `storage/index.ts`, it will only be used when detailed storage layout is not available, and the comparison
|
|
114
|
-
// can be changed to a string comparison when `layout.baseSlot` not an integer literal.
|
|
115
|
-
throw new UpgradesError(
|
|
116
|
-
`${src}: Custom storage layout expression with node type ${contractDef.storageLayout.baseSlotExpression.nodeType} is not currently supported`,
|
|
117
|
-
() =>
|
|
118
|
-
'Please report this at https://zpl.in/upgrades/report and include the `layout at` expression from your contract.',
|
|
119
|
-
);
|
|
110
|
+
const baseSlotExpression = contractDef.storageLayout.baseSlotExpression;
|
|
111
|
+
if (baseSlotExpression.nodeType === 'Literal') {
|
|
112
|
+
layout.baseSlot = baseSlotExpression.value ?? undefined;
|
|
113
|
+
return;
|
|
120
114
|
}
|
|
121
|
-
|
|
115
|
+
|
|
116
|
+
// Solidity 0.8.35+ allows the `erc7201(string)` comptime builtin as a base slot, e.g.
|
|
117
|
+
// `layout at erc7201("example.main")`. Resolve it to its computed storage location.
|
|
118
|
+
const resolvedSlot = resolveBaseSlot(baseSlotExpression);
|
|
119
|
+
if (resolvedSlot !== undefined) {
|
|
120
|
+
layout.baseSlot = resolvedSlot;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const src = decodeSrc(baseSlotExpression);
|
|
125
|
+
|
|
126
|
+
// TODO if a base slot expression other than a numeric literal or an `erc7201(...)` builtin call
|
|
127
|
+
// needs to be supported, evaluate the right approach per expression — either resolve it to a
|
|
128
|
+
// computed value (like the erc7201 builtin) or compare its source text across upgrades. A
|
|
129
|
+
// source-text approach could use Slang to extract the expression's source span (Slang supports
|
|
130
|
+
// `StorageLayoutSpecifier` from 0.8.29) and store it as a string on `layout.baseSlot`, with the
|
|
131
|
+
// upgrade-time comparison in `storage/index.ts:validateBaseSlotUnchanged` falling back to a
|
|
132
|
+
// string compare when `layout.baseSlot` is not a numeric literal.
|
|
133
|
+
throw new UpgradesError(
|
|
134
|
+
`${src}: Custom storage layout expression with node type ${baseSlotExpression.nodeType} is not currently supported`,
|
|
135
|
+
() =>
|
|
136
|
+
'Please report this at https://zpl.in/upgrades/report and include the `layout at` expression from your contract.',
|
|
137
|
+
);
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
|
package/src/storage/namespace.ts
CHANGED
|
@@ -8,8 +8,8 @@ import { Node } from 'solidity-ast/node';
|
|
|
8
8
|
import { CompilationContext, getTypeMembers, loadLayoutType } from './extract';
|
|
9
9
|
import { UpgradesError } from '../error';
|
|
10
10
|
import * as versions from 'compare-versions';
|
|
11
|
-
import { normalizeUint256Literal } from '../utils/integer-literals';
|
|
12
11
|
import { calculateERC7201StorageLocation, ERC7201_FORMULA_PREFIX } from '../utils/erc7201';
|
|
12
|
+
import { getErc7201BuiltinNamespaceId, resolveBaseSlot } from '../utils/erc7201-builtin';
|
|
13
13
|
import { logWarning } from '../utils/log';
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -175,15 +175,18 @@ function checkCustomLayoutClashWithNamespace(
|
|
|
175
175
|
contractName: string,
|
|
176
176
|
src: string,
|
|
177
177
|
) {
|
|
178
|
-
if (contractDef.storageLayout
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
178
|
+
if (contractDef.storageLayout === undefined || !storageLocation.startsWith(ERC7201_FORMULA_PREFIX)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const namespaceId = storageLocation.substring(ERC7201_FORMULA_PREFIX.length);
|
|
183
|
+
|
|
184
|
+
// Resolve the base slot (a numeric literal or an erc7201(...) builtin call) and compare it against
|
|
185
|
+
// the namespace's computed storage location. This covers `layout at erc7201("<id>")` using the same
|
|
186
|
+
// id as the namespace, since both resolve to the same hash.
|
|
187
|
+
const baseSlotNormalized = resolveBaseSlot(contractDef.storageLayout.baseSlotExpression);
|
|
188
|
+
if (baseSlotNormalized !== undefined && baseSlotNormalized === calculateERC7201StorageLocation(namespaceId)) {
|
|
189
|
+
throw new CustomLayoutClashWithNamespaceError(storageLocation, contractName, src);
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
|
|
@@ -192,8 +195,15 @@ function warnIfCustomLayoutAndNamespacesFound(
|
|
|
192
195
|
decodeSrc: SrcDecoder,
|
|
193
196
|
origContractDef: ContractDefinition,
|
|
194
197
|
) {
|
|
195
|
-
// TODO: when Solidity supports an erc7201 helper function, only give this warning if the custom storage layout is not using the erc7201 helper function
|
|
196
198
|
if (Object.entries(namespacesWithSrc).length > 0 && origContractDef.storageLayout !== undefined) {
|
|
199
|
+
// The warning is meant for users who pick an arbitrary literal slot that could land inside a
|
|
200
|
+
// namespace's range. When the base slot uses the erc7201 comptime builtin, it relies on the
|
|
201
|
+
// same collision resistance that namespaces rely on against each other, so suppress the warning;
|
|
202
|
+
// the tool likewise does not warn when multiple namespaces coexist.
|
|
203
|
+
if (getErc7201BuiltinNamespaceId(origContractDef.storageLayout.baseSlotExpression) !== undefined) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
197
207
|
const contractName = origContractDef.canonicalName ?? origContractDef.name;
|
|
198
208
|
logWarning(
|
|
199
209
|
`${decodeSrc(origContractDef.storageLayout)}: Ensure custom storage layout does not overlap with namespaced storage layout.`,
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import _test, { TestFn } from 'ava';
|
|
2
|
+
import { ContractDefinition } from 'solidity-ast';
|
|
3
|
+
import { findAll, astDereferencer } from 'solidity-ast/utils';
|
|
4
|
+
import { artifacts } from 'hardhat';
|
|
5
|
+
|
|
6
|
+
import { SolcOutput } from './solc-api';
|
|
7
|
+
import { getStorageUpgradeErrors } from './storage';
|
|
8
|
+
import { StorageLayout } from './storage/layout';
|
|
9
|
+
import { extractStorageLayout } from './storage/extract';
|
|
10
|
+
|
|
11
|
+
interface Context {
|
|
12
|
+
extractStorageLayout: (contract: string) => ReturnType<typeof extractStorageLayout>;
|
|
13
|
+
extractStorageLayoutFallback: (contract: string) => ReturnType<typeof extractStorageLayout>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const test = _test as TestFn<Context>;
|
|
17
|
+
|
|
18
|
+
const SOURCE = 'contracts/test/Erc7201Builtin.sol';
|
|
19
|
+
|
|
20
|
+
test.before(async t => {
|
|
21
|
+
const buildInfo = await artifacts.getBuildInfo(`${SOURCE}:Erc7201Builtin`);
|
|
22
|
+
if (buildInfo === undefined) {
|
|
23
|
+
throw new Error('Build info not found');
|
|
24
|
+
}
|
|
25
|
+
const solcOutput: SolcOutput = buildInfo.output;
|
|
26
|
+
const contracts: Record<string, ContractDefinition> = {};
|
|
27
|
+
const storageLayouts: Record<string, StorageLayout> = {};
|
|
28
|
+
for (const def of findAll('ContractDefinition', solcOutput.sources[SOURCE].ast)) {
|
|
29
|
+
contracts[def.name] = def;
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
31
|
+
storageLayouts[def.name] = solcOutput.contracts[SOURCE][def.name].storageLayout!;
|
|
32
|
+
}
|
|
33
|
+
const deref = astDereferencer(solcOutput);
|
|
34
|
+
|
|
35
|
+
t.context.extractStorageLayout = name =>
|
|
36
|
+
extractStorageLayout(contracts[name], dummyDecodeSrc, deref, storageLayouts[name]);
|
|
37
|
+
|
|
38
|
+
t.context.extractStorageLayoutFallback = name =>
|
|
39
|
+
extractStorageLayout(contracts[name], dummyDecodeSrc, deref, undefined);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const dummyDecodeSrc = () => 'file.sol:1';
|
|
43
|
+
|
|
44
|
+
test('erc7201 builtin base slot - extract', t => {
|
|
45
|
+
const layout = t.context.extractStorageLayout('Erc7201Builtin');
|
|
46
|
+
t.is(layout.baseSlot, '0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500');
|
|
47
|
+
t.is(layout.storage.length, 1);
|
|
48
|
+
t.is(layout.storage[0].label, 'a');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('erc7201 builtin base slot - append - ok', t => {
|
|
52
|
+
const v1 = t.context.extractStorageLayout('Erc7201Builtin');
|
|
53
|
+
const v2 = t.context.extractStorageLayout('Erc7201Builtin_Append_Ok');
|
|
54
|
+
t.deepEqual(getStorageUpgradeErrors(v1, v2), []);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('erc7201 builtin base slot - append - fallback - ok', t => {
|
|
58
|
+
const v1 = t.context.extractStorageLayoutFallback('Erc7201Builtin');
|
|
59
|
+
const v2 = t.context.extractStorageLayoutFallback('Erc7201Builtin_Append_Ok');
|
|
60
|
+
t.deepEqual(getStorageUpgradeErrors(v1, v2), []);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('erc7201 builtin base slot - changed id - bad', t => {
|
|
64
|
+
const v1 = t.context.extractStorageLayout('Erc7201Builtin');
|
|
65
|
+
const v2 = t.context.extractStorageLayout('Erc7201Builtin_Changed_Id_Bad');
|
|
66
|
+
const comparison = getStorageUpgradeErrors(v1, v2);
|
|
67
|
+
t.true(comparison.length > 0);
|
|
68
|
+
t.is(comparison[0].kind, 'layoutchange');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('erc7201 builtin base slot - changed id - fallback - bad', t => {
|
|
72
|
+
const v1 = t.context.extractStorageLayoutFallback('Erc7201Builtin');
|
|
73
|
+
const v2 = t.context.extractStorageLayoutFallback('Erc7201Builtin_Changed_Id_Bad');
|
|
74
|
+
// Base slot differs between the two erc7201 ids, so the fallback base-slot check must fail.
|
|
75
|
+
t.throws(() => getStorageUpgradeErrors(v1, v2), {
|
|
76
|
+
message: /Base slot for custom storage layout changed/,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('erc7201 builtin and literal-equivalent base slots compare equal', t => {
|
|
81
|
+
const builtin = t.context.extractStorageLayout('Erc7201Builtin');
|
|
82
|
+
const literal = t.context.extractStorageLayout('Erc7201Builtin_Literal_Equivalent');
|
|
83
|
+
// Both resolve to the same base slot; the fallback (base-slot) check must treat them as unchanged.
|
|
84
|
+
const builtinFallback = t.context.extractStorageLayoutFallback('Erc7201Builtin');
|
|
85
|
+
const literalFallback = t.context.extractStorageLayoutFallback('Erc7201Builtin_Literal_Equivalent');
|
|
86
|
+
t.deepEqual(getStorageUpgradeErrors(builtin, literal), []);
|
|
87
|
+
t.deepEqual(getStorageUpgradeErrors(builtinFallback, literalFallback), []);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('erc7201 builtin base slot clashes with namespace - error', t => {
|
|
91
|
+
t.throws(() => t.context.extractStorageLayout('Erc7201Builtin_Clash'), {
|
|
92
|
+
message:
|
|
93
|
+
/Custom layout for contract Erc7201Builtin_Clash clashes with the storage location for namespace erc7201:example\.main/,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('erc7201 builtin base slot different from namespace - no error and no warning', t => {
|
|
98
|
+
const warnings = captureWarnings(() => {
|
|
99
|
+
const layout = t.context.extractStorageLayout('Erc7201Builtin_NoClash_NoWarning');
|
|
100
|
+
t.truthy(layout.baseSlot);
|
|
101
|
+
});
|
|
102
|
+
t.deepEqual(warnings, []);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('erc7201 builtin accepts a unicode string literal argument', t => {
|
|
106
|
+
const layout = t.context.extractStorageLayout('Erc7201Builtin_Unicode');
|
|
107
|
+
t.is(layout.baseSlot, '0x2d3e72b7d91644a80150e2643e65016def7067dc9568719707e7eca1a5dab800');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('literal base slot with namespace - warning not suppressed', t => {
|
|
111
|
+
const warnings = captureWarnings(() => t.context.extractStorageLayout('Erc7201Builtin_LiteralBase_Warns'));
|
|
112
|
+
t.true(warnings.some(w => w.includes('custom storage layout')));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
function captureWarnings(fn: () => void): string[] {
|
|
116
|
+
const warnings: string[] = [];
|
|
117
|
+
const original = console.error;
|
|
118
|
+
console.error = (...args: unknown[]) => {
|
|
119
|
+
warnings.push(args.map(String).join(' '));
|
|
120
|
+
};
|
|
121
|
+
try {
|
|
122
|
+
fn();
|
|
123
|
+
} finally {
|
|
124
|
+
console.error = original;
|
|
125
|
+
}
|
|
126
|
+
return warnings;
|
|
127
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Expression, Literal } from 'solidity-ast';
|
|
2
|
+
import { calculateERC7201StorageLocation } from './erc7201';
|
|
3
|
+
import { normalizeUint256Literal } from './integer-literals';
|
|
4
|
+
import { UpgradesError } from '../error';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Recognizes the Solidity 0.8.35+ `erc7201(string)` comptime builtin, which evaluates the ERC-7201
|
|
8
|
+
* storage location for a namespace id and can be used in a compile-time context such as a `layout at`
|
|
9
|
+
* specifier (e.g. `contract C layout at erc7201("example.main")`).
|
|
10
|
+
*
|
|
11
|
+
* The builtin compiles to an ordinary `FunctionCall` node, so it is matched by its shape rather than
|
|
12
|
+
* by a dedicated AST node type.
|
|
13
|
+
*
|
|
14
|
+
* @param node The expression to check.
|
|
15
|
+
* @returns The namespace id passed to the builtin (without the `erc7201:` prefix), or undefined if the
|
|
16
|
+
* expression is not a call to the `erc7201` builtin with a single string literal argument.
|
|
17
|
+
*/
|
|
18
|
+
export function getErc7201BuiltinNamespaceId(node: Expression): string | undefined {
|
|
19
|
+
// Check that this is a function call to an identifier named `erc7201`. Return undefined for
|
|
20
|
+
// anything else so the caller can fall through to other base-slot forms.
|
|
21
|
+
if (node.nodeType !== 'FunctionCall' || node.kind !== 'functionCall') {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const callee = node.expression;
|
|
25
|
+
if (callee.nodeType !== 'Identifier' || callee.name !== 'erc7201') {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Beyond this point, the call is to `erc7201(...)`. Check its expected shape — one string-literal
|
|
30
|
+
// argument with no user-defined declaration shadowing the builtin — and throw if anything is off.
|
|
31
|
+
// Valid Solidity 0.8.35+ cannot produce these mismatches in a call to the builtin:
|
|
32
|
+
// - User-defined `erc7201` (`referencedDeclaration >= 0`): solc resolves the call to the builtin
|
|
33
|
+
// when both are in scope, so this shape doesn't reach a successful compile.
|
|
34
|
+
// - Wrong number of arguments: enforced by the builtin's signature.
|
|
35
|
+
// - Non-`Literal` argument or non-string literal `kind`: the builtin's signature requires a
|
|
36
|
+
// comptime-constant string literal. `hex"..."` is type `bytes` and wouldn't typecheck.
|
|
37
|
+
// We throw instead of returning undefined so a future solc emitting one of these surfaces loudly.
|
|
38
|
+
if (typeof callee.referencedDeclaration === 'number' && callee.referencedDeclaration >= 0) {
|
|
39
|
+
throw unexpectedErc7201BuiltinAst('callee resolves to a user-defined declaration');
|
|
40
|
+
}
|
|
41
|
+
if (node.arguments.length !== 1) {
|
|
42
|
+
throw unexpectedErc7201BuiltinAst(`expected 1 argument, got ${node.arguments.length}`);
|
|
43
|
+
}
|
|
44
|
+
const arg = node.arguments[0];
|
|
45
|
+
if (arg.nodeType !== 'Literal') {
|
|
46
|
+
throw unexpectedErc7201BuiltinAst(`argument is ${arg.nodeType}, expected Literal`);
|
|
47
|
+
}
|
|
48
|
+
if (arg.kind !== 'string' && arg.kind !== 'unicodeString') {
|
|
49
|
+
throw unexpectedErc7201BuiltinAst(`argument literal kind is ${arg.kind}, expected string or unicodeString`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return getStringLiteralValue(arg);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function unexpectedErc7201BuiltinAst(detail: string): UpgradesError {
|
|
56
|
+
return new UpgradesError(
|
|
57
|
+
`Unexpected AST for \`erc7201(...)\` builtin call: ${detail}`,
|
|
58
|
+
() =>
|
|
59
|
+
'Please report this at https://zpl.in/upgrades/report and include the `erc7201(...)` expression from your contract.',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolves an expression to its base slot as a normalized uint256 hex string, for either a numeric
|
|
65
|
+
* literal or an `erc7201(...)` builtin call.
|
|
66
|
+
*
|
|
67
|
+
* @param node The expression to resolve.
|
|
68
|
+
* @returns The normalized base slot hex string, or undefined if the expression is neither a numeric
|
|
69
|
+
* literal nor a resolvable `erc7201(...)` builtin call.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveBaseSlot(node: Expression): string | undefined {
|
|
72
|
+
if (node.nodeType === 'Literal') {
|
|
73
|
+
return normalizeUint256Literal(node.value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const id = getErc7201BuiltinNamespaceId(node);
|
|
77
|
+
if (id !== undefined) {
|
|
78
|
+
return calculateERC7201StorageLocation(id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getStringLiteralValue(literal: Literal): string {
|
|
85
|
+
// solc populates `value` for `kind: 'string'`/`'unicodeString'` literals; throw defensively if
|
|
86
|
+
// a future solc ever produces a null/missing `value` for such a literal.
|
|
87
|
+
if (literal.value === undefined || literal.value === null) {
|
|
88
|
+
throw unexpectedErc7201BuiltinAst('string literal `value` is null or missing');
|
|
89
|
+
}
|
|
90
|
+
return literal.value;
|
|
91
|
+
}
|
|
@@ -8,3 +8,15 @@ test('calculateERC7201StorageLocation', t => {
|
|
|
8
8
|
'0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500',
|
|
9
9
|
);
|
|
10
10
|
});
|
|
11
|
+
|
|
12
|
+
// `keccak256("ns2") - 1` is 63 hex chars (odd-length), which must be zero-padded to 32 bytes
|
|
13
|
+
// before hashing. Regression test for truncation of the intermediate buffer.
|
|
14
|
+
test('calculateERC7201StorageLocation - odd-length intermediate hash', t => {
|
|
15
|
+
t.is(calculateERC7201StorageLocation('ns2'), '0xa61069020885c09e922542fb3fa98c8d50e6481fc1251ff86c3319a70f0f7d00');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// `keccak256("ns297") - 1` is 62 hex chars (i.e., its top byte is `0x00`). Without zero-padding,
|
|
19
|
+
// `Buffer.from('hex')` would interpret it as a 31-byte buffer rather than 32, producing the wrong hash.
|
|
20
|
+
test('calculateERC7201StorageLocation - leading-zero-byte intermediate hash', t => {
|
|
21
|
+
t.is(calculateERC7201StorageLocation('ns297'), '0xe06f9faa11c8032ea604e1764972c1a5594987f1678d8fabf921c28d1a7a4c00');
|
|
22
|
+
});
|
package/src/utils/erc7201.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { keccak256 } from 'ethereumjs-util';
|
|
2
2
|
|
|
3
|
+
import { toBytes32Hex } from './integer-literals';
|
|
4
|
+
|
|
3
5
|
export const ERC7201_FORMULA_PREFIX = 'erc7201:';
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -12,14 +14,14 @@ export const ERC7201_FORMULA_PREFIX = 'erc7201:';
|
|
|
12
14
|
export function calculateERC7201StorageLocation(id: string): string {
|
|
13
15
|
const firstHash = keccak256(Buffer.from(id));
|
|
14
16
|
const minusOne = BigInt(`0x${firstHash.toString('hex')}`) - 1n;
|
|
15
|
-
|
|
17
|
+
// Pad to 64 hex chars so that `Buffer.from` doesn't silently truncate odd-length input;
|
|
18
|
+
// `abi.encode(uint256)` is always 32 bytes.
|
|
19
|
+
const minusOneBuffer = Buffer.from(minusOne.toString(16).padStart(64, '0'), 'hex');
|
|
16
20
|
|
|
17
21
|
const secondHash = keccak256(minusOneBuffer);
|
|
18
22
|
|
|
19
23
|
const mask = BigInt('0xff');
|
|
20
24
|
const masked = BigInt(`0x${secondHash.toString('hex')}`) & ~mask;
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return `0x${padded}`;
|
|
26
|
+
return toBytes32Hex(masked);
|
|
25
27
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from 'ava';
|
|
2
2
|
|
|
3
|
-
import { normalizeUint256Literal } from './integer-literals';
|
|
3
|
+
import { normalizeUint256Literal, toBytes32Hex } from './integer-literals';
|
|
4
4
|
|
|
5
5
|
const ZEROES = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
|
6
6
|
|
|
@@ -71,3 +71,17 @@ test('full hex with leading and trailing zeroes', t => {
|
|
|
71
71
|
'0x00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00',
|
|
72
72
|
);
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
test('rejects values above uint256 max', t => {
|
|
76
|
+
const tooLarge = '0x10000000000000000000000000000000000000000000000000000000000000000';
|
|
77
|
+
const error = t.throws(() => normalizeUint256Literal(tooLarge));
|
|
78
|
+
t.regex(error?.message ?? '', /uint256 range/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('toBytes32Hex - boundaries', t => {
|
|
82
|
+
const MAX_HEX = '0x' + 'f'.repeat(64);
|
|
83
|
+
t.is(toBytes32Hex(0n), '0x' + '0'.repeat(64));
|
|
84
|
+
t.is(toBytes32Hex(BigInt(MAX_HEX)), MAX_HEX);
|
|
85
|
+
t.throws(() => toBytes32Hex(BigInt(MAX_HEX) + 1n), { message: /uint256 range/ });
|
|
86
|
+
t.throws(() => toBytes32Hex(-1n), { message: /uint256 range/ });
|
|
87
|
+
});
|