Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Hardhat compile error when library or interface has namespace struct, print warning for library #1086

Merged
merged 6 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 1.39.0 (2024-10-02)

- Fix Hardhat compile error when library or interface has struct with namespace annotation. ([#1086](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/1086))
- Log warning if library contains namespace annotation. ([#1086](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/1086))

> **Note**
> Namespaces should be defined in contracts according to [ERC-7201: Namespaced Storage Layouts](https://eips.ethereum.org/EIPS/eip-7201). Structs with namespace annotations defined in libraries or interfaces outside of a contract's inheritance are not included in storage layout validations.

## 1.38.0 (2024-09-23)

- Supports checking to ensure `initialOwner` is not a ProxyAdmin contract when deploying a transparent proxy from Hardhat. ([#1083](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/1083))
Expand Down
62 changes: 61 additions & 1 deletion packages/core/contracts/test/Namespaced.sol
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,64 @@ contract MultipleNamespacesAndRegularVariablesV2_Bad {
uint256 public c;
uint128 public a;
uint256 public b;
}
}

interface InterfaceWithNamespace {
/// @custom:storage-location erc7201:example.main
struct MainStorage {
uint256 x;
uint256 y;
}
}

contract UsesInterface is InterfaceWithNamespace {
// keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff));
bytes32 internal constant MAIN_STORAGE_LOCATION =
0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500;

function _getMainStorage() internal pure returns (MainStorage storage $) {
assembly {
$.slot := MAIN_STORAGE_LOCATION
}
}
}

interface InterfaceWithNamespaceV2_Ok {
/// @custom:storage-location erc7201:example.main
struct MainStorage {
uint256 x;
uint256 y;
uint256 z;
}
}

contract UsesInterfaceV2_Ok is InterfaceWithNamespaceV2_Ok {
// keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff));
bytes32 internal constant MAIN_STORAGE_LOCATION =
0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500;

function _getMainStorage() internal pure returns (MainStorage storage $) {
assembly {
$.slot := MAIN_STORAGE_LOCATION
}
}
}

interface InterfaceWithNamespaceV2_Bad {
/// @custom:storage-location erc7201:example.main
struct MainStorage {
uint256 y;
}
}

contract UsesInterfaceV2_Bad is InterfaceWithNamespaceV2_Bad {
// keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff));
bytes32 internal constant MAIN_STORAGE_LOCATION =
0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500;

function _getMainStorage() internal pure returns (MainStorage storage $) {
assembly {
$.slot := MAIN_STORAGE_LOCATION
}
}
}
28 changes: 28 additions & 0 deletions packages/core/contracts/test/NamespacedInLibrary.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// This is not considered a namespace according to ERC-7201 because the namespaced struct is outside of a contract.
library LibraryWithNamespace {
/// @custom:storage-location erc7201:example.main
struct MainStorage {
uint256 x;
uint256 y;
}
}

contract UsesLibraryWithNamespace {
// keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant MAIN_STORAGE_LOCATION =
0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500;

function _getMainStorage() private pure returns (LibraryWithNamespace.MainStorage storage $) {
assembly {
$.slot := MAIN_STORAGE_LOCATION
}
}

function _getXTimesY() internal view returns (uint256) {
LibraryWithNamespace.MainStorage storage $ = _getMainStorage();
return $.x * $.y;
}
}
19 changes: 18 additions & 1 deletion packages/core/contracts/test/NamespacedToModify.sol
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,21 @@ contract HasSpecialFunctions {
}

bytes4 constant PAYABLE_SELECTOR = this.hasPayable.selector;
}
}

/// This is not considered a namespace according to ERC-7201 because the namespaced struct is outside of a contract.
library LibraryWithNamespace {
/// @custom:storage-location erc7201:example.main
struct MainStorage {
uint256 x;
uint256 y;
}
}

interface InterfaceWithNamespace {
/// @custom:storage-location erc7201:example.main
struct MainStorage {
uint256 x;
uint256 y;
}
}
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openzeppelin/upgrades-core",
"version": "1.38.0",
"version": "1.39.0",
"description": "",
"repository": "https://github.com/OpenZeppelin/openzeppelin-upgrades/tree/master/packages/core",
"license": "MIT",
Expand Down
31 changes: 23 additions & 8 deletions packages/core/src/storage-namespaced-outside-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@ import { artifacts } from 'hardhat';
import { validate } from './validate';
import { solcInputOutputDecoder } from './src-decoder';

test('namespace outside contract', async t => {
test('namespace outside contract - error', async t => {
const contract = 'contracts/test/NamespacedOutsideContract.sol:Example';

const buildInfo = await artifacts.getBuildInfo(contract);
if (buildInfo === undefined) {
throw new Error(`Build info not found for contract ${contract}`);
}
const solcOutput = buildInfo.output;
const solcInput = buildInfo.input;
const decodeSrc = solcInputOutputDecoder(solcInput, solcOutput);
const { solcOutput, decodeSrc } = await getOutputAndDecoder(contract);

const error = t.throws(() => validate(solcOutput, decodeSrc));
t.assert(
error?.message.includes(
Expand All @@ -22,3 +17,23 @@ test('namespace outside contract', async t => {
error?.message,
);
});

test('namespace in library - warning', async t => {
const contract = 'contracts/test/NamespacedInLibrary.sol:UsesLibraryWithNamespace';

const { solcOutput, decodeSrc } = await getOutputAndDecoder(contract);
validate(solcOutput, decodeSrc);

t.pass();
});

async function getOutputAndDecoder(contract: string) {
const buildInfo = await artifacts.getBuildInfo(contract);
if (buildInfo === undefined) {
throw new Error(`Build info not found for contract ${contract}`);
}
const solcOutput = buildInfo.output;
const solcInput = buildInfo.input;
const decodeSrc = solcInputOutputDecoder(solcInput, solcOutput);
return { solcOutput, decodeSrc };
}
26 changes: 26 additions & 0 deletions packages/core/src/storage-namespaced.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,29 @@ test('multiple namespaces and regular variables bad', t => {
},
});
});

test('interface with namespace - upgrade ok', t => {
const v1 = t.context.extractStorageLayout('InterfaceWithNamespace');
const v2 = t.context.extractStorageLayout('InterfaceWithNamespaceV2_Ok');
const comparison = getStorageUpgradeErrors(v1, v2);
t.deepEqual(comparison, []);
});

test('interface with namespace - upgrade bad', t => {
const v1 = t.context.extractStorageLayout('InterfaceWithNamespace');
const v2 = t.context.extractStorageLayout('InterfaceWithNamespaceV2_Bad');
const comparison = getStorageUpgradeErrors(v1, v2);
t.like(comparison, {
length: 1,
0: {
kind: 'delete',
original: {
contract: 'InterfaceWithNamespace',
label: 'x',
type: {
id: 't_uint256',
},
},
},
});
});
40 changes: 38 additions & 2 deletions packages/core/src/utils/make-namespaced.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,25 @@ Generated by [AVA](https://avajs.dev).
function hasPayable() public payable {}␊
bytes4 constant PAYABLE_SELECTOR = this.hasPayable.selector;␊
}`,
}␊
library LibraryWithNamespace {␊
/// @custom:storage-location erc7201:example.main␊
struct MainStorage {␊
uint256 x;␊
uint256 y;␊
}␊
}␊
interface InterfaceWithNamespace {␊
/// @custom:storage-location erc7201:example.main␊
struct MainStorage {␊
uint256 x;␊
uint256 y;␊
}␊
}␊
`,
},
'contracts/test/NamespacedToModifyImported.sol': {
content: `// SPDX-License-Identifier: MIT␊
Expand Down Expand Up @@ -547,7 +565,25 @@ Generated by [AVA](https://avajs.dev).
function hasPayable() public payable {}␊
bytes4 constant PAYABLE_SELECTOR = this.hasPayable.selector;␊
}`,
}␊
/// This is not considered a namespace according to ERC-7201 because the namespaced struct is outside of a contract.␊
library LibraryWithNamespace {␊
/// @custom:storage-location erc7201:example.main␊
struct MainStorage {␊
uint256 x;␊
uint256 y;␊
}␊
}␊
interface InterfaceWithNamespace {␊
/// @custom:storage-location erc7201:example.main␊
struct MainStorage {␊
uint256 x;␊
uint256 y;␊
}␊
}␊
`,
},
'contracts/test/NamespacedToModifyImported.sol': {
content: `// SPDX-License-Identifier: MIT␊
Expand Down
Binary file modified packages/core/src/utils/make-namespaced.test.ts.snap
Binary file not shown.
4 changes: 4 additions & 0 deletions packages/core/src/utils/make-namespaced.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export function makeNamespacedInput(input: SolcInput, output: SolcOutput, solcVe
break;
}
case 'StructDefinition': {
// Do not add state variable in a library or interface, since that is not allowed by Solidity
if (contractDef.contractKind !== 'contract') {
continue;
}
const storageLocation = getStorageLocationAnnotation(contractNode);
if (storageLocation !== undefined) {
const structName = contractNode.name;
Expand Down
34 changes: 27 additions & 7 deletions packages/core/src/validate/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { getAnnotationArgs as getSupportedAnnotationArgs, getDocumentation } fro
import { getStorageLocationAnnotation, isNamespaceSupported } from '../storage/namespace';
import { UpgradesError } from '../error';
import { assertUnreachable } from '../utils/assert';
import { logWarning } from '../utils/log';

export type ValidationRunData = Record<string, ContractValidation>;

Expand Down Expand Up @@ -267,18 +268,37 @@ function checkNamespaceSolidityVersion(source: string, solcVersion?: string, sol
function checkNamespacesOutsideContract(source: string, solcOutput: SolcOutput, decodeSrc: SrcDecoder) {
for (const node of solcOutput.sources[source].ast.nodes) {
if (isNodeType('StructDefinition', node)) {
const storageLocation = getStorageLocationAnnotation(node);
if (storageLocation !== undefined) {
throw new UpgradesError(
`${decodeSrc(node)}: Namespace struct ${node.name} is defined outside of a contract`,
() =>
`Structs with the @custom:storage-location annotation must be defined within a contract. Move the struct definition into a contract, or remove the annotation if the struct is not used for namespaced storage.`,
);
// Namespace struct outside contract - error
assertNotNamespace(node, decodeSrc, true);
} else if (isNodeType('ContractDefinition', node) && node.contractKind === 'library') {
// Namespace struct in library - warning (don't give an error to avoid breaking changes, since this is quite common)
for (const child of node.nodes) {
if (isNodeType('StructDefinition', child)) {
assertNotNamespace(child, decodeSrc, false);
}
}
}
}
}

function assertNotNamespace(node: StructDefinition, decodeSrc: SrcDecoder, strict: boolean) {
const storageLocation = getStorageLocationAnnotation(node);
if (storageLocation !== undefined) {
const msg = `${decodeSrc(node)}: Namespace struct ${node.name} is defined outside of a contract`;
if (strict) {
throw new UpgradesError(
msg,
() =>
`Structs with the @custom:storage-location annotation must be defined within a contract. Move the struct definition into a contract, or remove the annotation if the struct is not used for namespaced storage.`,
);
} else {
logWarning(msg, [
'Structs with the @custom:storage-location annotation must be defined within a contract, otherwise they are not included in storage layout validations. Move the struct definition into a contract, or remove the annotation if the struct is not used for namespaced storage.',
]);
}
}
}

function getNamespacedCompilationContext(
source: string,
contractDef: ContractDefinition,
Expand Down
Loading