From 591c12d22de283a297e2b551175ccc914c938391 Mon Sep 17 00:00:00 2001 From: Francisco Date: Mon, 30 Jan 2023 21:00:56 -0300 Subject: [PATCH 1/5] Remove outdated note about virtual view functions (#4014) --- docs/modules/ROOT/pages/extending-contracts.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/modules/ROOT/pages/extending-contracts.adoc b/docs/modules/ROOT/pages/extending-contracts.adoc index 7330fc99715..a440f406746 100644 --- a/docs/modules/ROOT/pages/extending-contracts.adoc +++ b/docs/modules/ROOT/pages/extending-contracts.adoc @@ -66,8 +66,6 @@ contract ModifiedAccessControl is AccessControl { The `super.revokeRole` statement at the end will invoke ``AccessControl``'s original version of `revokeRole`, the same code that would've run if there were no overrides in place. -NOTE: As of v3.0.0, `view` functions are not `virtual` in OpenZeppelin, and therefore cannot be overridden. We're considering https://github.com/OpenZeppelin/openzeppelin-contracts/issues/2154[lifting this restriction] in an upcoming release. Let us know if this is something you care about! - [[using-hooks]] == Using Hooks From 501a78e13426ec10249e44386239c74783478dd0 Mon Sep 17 00:00:00 2001 From: Mio Date: Tue, 31 Jan 2023 21:11:37 +0800 Subject: [PATCH 2/5] Fix typos (#4015) --- contracts/governance/IGovernor.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 18cf57fb163..60a4b54700d 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -58,7 +58,7 @@ abstract contract IGovernor is IERC165 { * @dev Emitted when a vote is cast with params. * * Note: `support` values should be seen as buckets. Their interpretation depends on the voting module used. - * `params` are additional encoded parameters. Their intepepretation also depends on the voting module used. + * `params` are additional encoded parameters. Their interpepretation also depends on the voting module used. */ event VoteCastWithParams( address indexed voter, @@ -140,7 +140,7 @@ abstract contract IGovernor is IERC165 { /** * @notice module:user-config - * @dev Delay, in number of block, between the proposal is created and the vote starts. This can be increassed to + * @dev Delay, in number of block, between the proposal is created and the vote starts. This can be increased to * leave time for users to buy voting power, or delegate it, before the voting of a proposal starts. */ function votingDelay() public view virtual returns (uint256); From a70ee4e3bbee565e49217c9ec59de5354f8af9d8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 31 Jan 2023 21:45:24 +0100 Subject: [PATCH 3/5] Check storage layout consistency in PRs (#3967) Co-authored-by: Francisco --- .github/actions/storage-layout/action.yml | 55 +++++++++ .github/workflows/checks.yml | 4 + package-lock.json | 144 ++++++++++++++++++++++ package.json | 1 + scripts/checks/compare-layout.js | 20 +++ scripts/checks/extract-layout.js | 40 ++++++ 6 files changed, 264 insertions(+) create mode 100644 .github/actions/storage-layout/action.yml create mode 100644 scripts/checks/compare-layout.js create mode 100644 scripts/checks/extract-layout.js diff --git a/.github/actions/storage-layout/action.yml b/.github/actions/storage-layout/action.yml new file mode 100644 index 00000000000..e3e104a01e3 --- /dev/null +++ b/.github/actions/storage-layout/action.yml @@ -0,0 +1,55 @@ +name: Compare storage layouts +inputs: + token: + description: github token + required: true + buildinfo: + description: compilation artifacts + required: false + default: artifacts/build-info/*.json + layout: + description: extracted storage layout + required: false + default: HEAD.layout.json + out_layout: + description: storage layout to upload + required: false + default: ${{ github.ref_name }}.layout.json + ref_layout: + description: storage layout for the reference branch + required: false + default: ${{ github.base_ref }}.layout.json + +runs: + using: composite + steps: + - name: Extract layout + run: | + node scripts/checks/extract-layout.js ${{ inputs.buildinfo }} > ${{ inputs.layout }} + shell: bash + - name: Download reference + if: github.event_name == 'pull_request' + run: | + RUN_ID=`gh run list --repo ${{ github.repository }} --branch ${{ github.base_ref }} --workflow ${{ github.workflow }} --limit 100 --json 'conclusion,databaseId,event' --jq 'map(select(.conclusion=="success" and .event!="pull_request"))[0].databaseId'` + gh run download ${RUN_ID} --repo ${{ github.repository }} -n layout + env: + GITHUB_TOKEN: ${{ inputs.token }} + shell: bash + continue-on-error: true + id: reference + - name: Compare layouts + if: steps.reference.outcome == 'success' && github.event_name == 'pull_request' + run: | + node scripts/checks/compare-layout.js --head ${{ inputs.layout }} --ref ${{ inputs.ref_layout }} >> $GITHUB_STEP_SUMMARY + shell: bash + - name: Rename artifacts for upload + if: github.event_name != 'pull_request' + run: | + mv ${{ inputs.layout }} ${{ inputs.out_layout }} + shell: bash + - name: Save artifacts + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v3 + with: + name: layout + path: ${{ inputs.out_layout }} diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 17d9e35789a..02664e85c8c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -42,6 +42,10 @@ jobs: uses: ./.github/actions/gas-compare with: token: ${{ github.token }} + - name: Check storage layout + uses: ./.github/actions/storage-layout + with: + token: ${{ github.token }} foundry-tests: if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' diff --git a/package-lock.json b/package-lock.json index 51a71854cf4..51d350ac78a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/docs-utils": "^0.1.3", "@openzeppelin/test-helpers": "^0.5.13", + "@openzeppelin/upgrades-core": "^1.20.6", "chai": "^4.2.0", "eslint": "^8.30.0", "eslint-config-prettier": "^8.5.0", @@ -2405,6 +2406,58 @@ "semver": "bin/semver" } }, + "node_modules/@openzeppelin/upgrades-core": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.20.6.tgz", + "integrity": "sha512-KWdtlahm+iunlAlzLsdpBueanwEx0LLPfAkDL1p0C4SPjMiUqHHFlyGtmmWwdiqDpJ//605vfwkd5RqfnFrHSg==", + "dev": true, + "dependencies": { + "cbor": "^8.0.0", + "chalk": "^4.1.0", + "compare-versions": "^5.0.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.0.3", + "proper-lockfile": "^4.1.1", + "solidity-ast": "^0.4.15" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/cbor": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "dev": true, + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=12.19" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "dev": true, + "engines": { + "node": ">=12.19" + } + }, "node_modules/@scure/base": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", @@ -4571,6 +4624,12 @@ "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", "dev": true }, + "node_modules/compare-versions": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", + "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -11430,6 +11489,17 @@ "asap": "~2.0.6" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -12019,6 +12089,15 @@ "node": ">=4" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -17993,6 +18072,48 @@ } } }, + "@openzeppelin/upgrades-core": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.20.6.tgz", + "integrity": "sha512-KWdtlahm+iunlAlzLsdpBueanwEx0LLPfAkDL1p0C4SPjMiUqHHFlyGtmmWwdiqDpJ//605vfwkd5RqfnFrHSg==", + "dev": true, + "requires": { + "cbor": "^8.0.0", + "chalk": "^4.1.0", + "compare-versions": "^5.0.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.0.3", + "proper-lockfile": "^4.1.1", + "solidity-ast": "^0.4.15" + }, + "dependencies": { + "cbor": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "dev": true, + "requires": { + "nofilter": "^3.1.0" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "dev": true + } + } + }, "@scure/base": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", @@ -19767,6 +19888,12 @@ "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", "dev": true }, + "compare-versions": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", + "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -25154,6 +25281,17 @@ "asap": "~2.0.6" } }, + "proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -25598,6 +25736,12 @@ "signal-exit": "^3.0.2" } }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", diff --git a/package.json b/package.json index b4035b820fc..de2951bd193 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/docs-utils": "^0.1.3", "@openzeppelin/test-helpers": "^0.5.13", + "@openzeppelin/upgrades-core": "^1.20.6", "chai": "^4.2.0", "eslint": "^8.30.0", "eslint-config-prettier": "^8.5.0", diff --git a/scripts/checks/compare-layout.js b/scripts/checks/compare-layout.js new file mode 100644 index 00000000000..ff8cb712e82 --- /dev/null +++ b/scripts/checks/compare-layout.js @@ -0,0 +1,20 @@ +const fs = require('fs'); +const { getStorageUpgradeReport } = require('@openzeppelin/upgrades-core/dist/storage'); + +const { ref, head } = require('yargs').argv; + +const oldLayout = JSON.parse(fs.readFileSync(ref)); +const newLayout = JSON.parse(fs.readFileSync(head)); + +for (const name in oldLayout) { + if (name in newLayout) { + const report = getStorageUpgradeReport(oldLayout[name], newLayout[name], {}); + if (!report.ok) { + console.log(`ERROR: Storage incompatibility in ${name}`); + console.log(report.explain()); + process.exitCode = 1; + } + } else { + console.log(`WARNING: ${name} is missing from the current branch`); + } +} diff --git a/scripts/checks/extract-layout.js b/scripts/checks/extract-layout.js new file mode 100644 index 00000000000..d0c9cf36b4b --- /dev/null +++ b/scripts/checks/extract-layout.js @@ -0,0 +1,40 @@ +const fs = require('fs'); +const { findAll } = require('solidity-ast/utils'); +const { astDereferencer } = require('@openzeppelin/upgrades-core/dist/ast-dereferencer'); +const { solcInputOutputDecoder } = require('@openzeppelin/upgrades-core/dist/src-decoder'); +const { extractStorageLayout } = require('@openzeppelin/upgrades-core/dist/storage/extract'); + +const { _ } = require('yargs').argv; + +const skipPath = ['contracts/mocks/', 'contracts-exposed/']; +const skipKind = ['interface', 'library']; + +function extractLayouts(path) { + const layout = {}; + const { input, output } = JSON.parse(fs.readFileSync(path)); + + const decoder = solcInputOutputDecoder(input, output); + const deref = astDereferencer(output); + + for (const src in output.contracts) { + if (skipPath.some(prefix => src.startsWith(prefix))) { + continue; + } + + for (const contractDef of findAll('ContractDefinition', output.sources[src].ast)) { + if (skipKind.includes(contractDef.contractKind)) { + continue; + } + + layout[contractDef.name] = extractStorageLayout( + contractDef, + decoder, + deref, + output.contracts[src][contractDef.name].storageLayout, + ); + } + } + return layout; +} + +console.log(JSON.stringify(Object.assign(..._.map(extractLayouts)))); From 91e8d0ba3c3beb8a1db31310d8599664e48639ef Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 1 Feb 2023 22:46:05 +0100 Subject: [PATCH 4/5] Add `string` and `bytes` support to the `StorageSlots` library (#4008) Co-authored-by: Francisco --- .changeset/modern-games-exist.md | 5 ++ contracts/mocks/StorageSlotMock.sol | 38 +++++++- contracts/utils/StorageSlot.sol | 52 ++++++++++- scripts/generate/run.js | 1 + scripts/generate/templates/StorageSlot.js | 87 +++++++++++++++++++ scripts/helpers.js | 5 ++ test/utils/StorageSlot.test.js | 100 ++++++++++++++++++++++ 7 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 .changeset/modern-games-exist.md create mode 100644 scripts/generate/templates/StorageSlot.js diff --git a/.changeset/modern-games-exist.md b/.changeset/modern-games-exist.md new file mode 100644 index 00000000000..bd89b4f1658 --- /dev/null +++ b/.changeset/modern-games-exist.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`StorageSlot`: Add support for `string` and `bytes`. diff --git a/contracts/mocks/StorageSlotMock.sol b/contracts/mocks/StorageSlotMock.sol index 5d099fca83d..1da577c19fe 100644 --- a/contracts/mocks/StorageSlotMock.sol +++ b/contracts/mocks/StorageSlotMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../utils/StorageSlot.sol"; contract StorageSlotMock { - using StorageSlot for bytes32; + using StorageSlot for *; function setBoolean(bytes32 slot, bool value) public { slot.getBooleanSlot().value = value; @@ -38,4 +38,40 @@ contract StorageSlotMock { function getUint256(bytes32 slot) public view returns (uint256) { return slot.getUint256Slot().value; } + + mapping(uint256 => string) public stringMap; + + function setString(bytes32 slot, string calldata value) public { + slot.getStringSlot().value = value; + } + + function setStringStorage(uint256 key, string calldata value) public { + stringMap[key].getStringSlot().value = value; + } + + function getString(bytes32 slot) public view returns (string memory) { + return slot.getStringSlot().value; + } + + function getStringStorage(uint256 key) public view returns (string memory) { + return stringMap[key].getStringSlot().value; + } + + mapping(uint256 => bytes) public bytesMap; + + function setBytes(bytes32 slot, bytes calldata value) public { + slot.getBytesSlot().value = value; + } + + function setBytesStorage(uint256 key, bytes calldata value) public { + bytesMap[key].getBytesSlot().value = value; + } + + function getBytes(bytes32 slot) public view returns (bytes memory) { + return slot.getBytesSlot().value; + } + + function getBytesStorage(uint256 key) public view returns (bytes memory) { + return bytesMap[key].getBytesSlot().value; + } } diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index d23363bd632..44285c90035 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (utils/StorageSlot.sol) +// This file was procedurally generated from scripts/generate/templates/StorageSlot.js. pragma solidity ^0.8.0; @@ -27,7 +28,8 @@ pragma solidity ^0.8.0; * } * ``` * - * _Available since v4.1 for `address`, `bool`, `bytes32`, and `uint256`._ + * _Available since v4.1 for `address`, `bool`, `bytes32`, `uint256`._ + * _Available since v4.9 for `string`, `bytes`._ */ library StorageSlot { struct AddressSlot { @@ -46,6 +48,14 @@ library StorageSlot { uint256 value; } + struct StringSlot { + string value; + } + + struct BytesSlot { + bytes value; + } + /** * @dev Returns an `AddressSlot` with member `value` located at `slot`. */ @@ -85,4 +95,44 @@ library StorageSlot { r.slot := slot } } + + /** + * @dev Returns an `StringSlot` with member `value` located at `slot`. + */ + function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` representation of the string storage pointer `store`. + */ + function getStringSlot(string storage store) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } + + /** + * @dev Returns an `BytesSlot` with member `value` located at `slot`. + */ + function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. + */ + function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } } diff --git a/scripts/generate/run.js b/scripts/generate/run.js index a3482322301..e68681e9d4c 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -18,6 +18,7 @@ for (const [file, template] of Object.entries({ 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', 'utils/Checkpoints.sol': './templates/Checkpoints.js', + 'utils/StorageSlot.sol': './templates/StorageSlot.js', })) { const script = path.relative(path.join(__dirname, '../..'), __filename); const input = path.join(path.dirname(script), template); diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js new file mode 100644 index 00000000000..69fa7ccc0ef --- /dev/null +++ b/scripts/generate/templates/StorageSlot.js @@ -0,0 +1,87 @@ +const format = require('../format-lines'); +const { capitalize, unique } = require('../../helpers'); + +const TYPES = [ + { type: 'address', isValueType: true, version: '4.1' }, + { type: 'bool', isValueType: true, name: 'Boolean', version: '4.1' }, + { type: 'bytes32', isValueType: true, version: '4.1' }, + { type: 'uint256', isValueType: true, version: '4.1' }, + { type: 'string', isValueType: false, version: '4.9' }, + { type: 'bytes', isValueType: false, version: '4.9' }, +].map(type => Object.assign(type, { struct: (type.name ?? capitalize(type.type)) + 'Slot' })); + +const VERSIONS = unique(TYPES.map(t => t.version)).map( + version => + `_Available since v${version} for ${TYPES.filter(t => t.version == version) + .map(t => `\`${t.type}\``) + .join(', ')}._`, +); + +const header = `\ +pragma solidity ^0.8.0; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a \`value\` member that can be used to read or write. + * + * Example usage to set ERC1967 implementation slot: + * \`\`\`solidity + * contract ERC1967 { + * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * \`\`\` + * +${VERSIONS.map(s => ` * ${s}`).join('\n')} + */ +`; + +const struct = type => `\ +struct ${type.struct} { + ${type.type} value; +} +`; + +const get = type => `\ +/** + * @dev Returns an \`${type.struct}\` with member \`value\` located at \`slot\`. + */ +function get${type.struct}(bytes32 slot) internal pure returns (${type.struct} storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } +} +`; + +const getStorage = type => `\ +/** + * @dev Returns an \`${type.struct}\` representation of the ${type.type} storage pointer \`store\`. + */ +function get${type.struct}(${type.type} storage store) internal pure returns (${type.struct} storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } +} +`; + +// GENERATE +module.exports = format( + header.trimEnd(), + 'library StorageSlot {', + [...TYPES.map(struct), ...TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)])], + '}', +); diff --git a/scripts/helpers.js b/scripts/helpers.js index 26d0a2baad2..fb9aad4fcd6 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -24,9 +24,14 @@ function zip(...args) { .map((_, i) => args.map(arg => arg[i])); } +function capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + module.exports = { chunk, range, unique, zip, + capitalize, }; diff --git a/test/utils/StorageSlot.test.js b/test/utils/StorageSlot.test.js index 9d428875f44..846512ed2ad 100644 --- a/test/utils/StorageSlot.test.js +++ b/test/utils/StorageSlot.test.js @@ -107,4 +107,104 @@ contract('StorageSlot', function (accounts) { }); }); }); + + describe('string storage slot', function () { + beforeEach(async function () { + this.value = 'lorem ipsum'; + }); + + it('set', async function () { + await this.store.setString(slot, this.value); + }); + + describe('get', function () { + beforeEach(async function () { + await this.store.setString(slot, this.value); + }); + + it('from right slot', async function () { + expect(await this.store.getString(slot)).to.be.equal(this.value); + }); + + it('from other slot', async function () { + expect(await this.store.getString(otherSlot)).to.be.equal(''); + }); + }); + }); + + describe('string storage pointer', function () { + beforeEach(async function () { + this.value = 'lorem ipsum'; + }); + + it('set', async function () { + await this.store.setStringStorage(slot, this.value); + }); + + describe('get', function () { + beforeEach(async function () { + await this.store.setStringStorage(slot, this.value); + }); + + it('from right slot', async function () { + expect(await this.store.stringMap(slot)).to.be.equal(this.value); + expect(await this.store.getStringStorage(slot)).to.be.equal(this.value); + }); + + it('from other slot', async function () { + expect(await this.store.stringMap(otherSlot)).to.be.equal(''); + expect(await this.store.getStringStorage(otherSlot)).to.be.equal(''); + }); + }); + }); + + describe('bytes storage slot', function () { + beforeEach(async function () { + this.value = web3.utils.randomHex(128); + }); + + it('set', async function () { + await this.store.setBytes(slot, this.value); + }); + + describe('get', function () { + beforeEach(async function () { + await this.store.setBytes(slot, this.value); + }); + + it('from right slot', async function () { + expect(await this.store.getBytes(slot)).to.be.equal(this.value); + }); + + it('from other slot', async function () { + expect(await this.store.getBytes(otherSlot)).to.be.equal(null); + }); + }); + }); + + describe('bytes storage pointer', function () { + beforeEach(async function () { + this.value = web3.utils.randomHex(128); + }); + + it('set', async function () { + await this.store.setBytesStorage(slot, this.value); + }); + + describe('get', function () { + beforeEach(async function () { + await this.store.setBytesStorage(slot, this.value); + }); + + it('from right slot', async function () { + expect(await this.store.bytesMap(slot)).to.be.equal(this.value); + expect(await this.store.getBytesStorage(slot)).to.be.equal(this.value); + }); + + it('from other slot', async function () { + expect(await this.store.bytesMap(otherSlot)).to.be.equal(null); + expect(await this.store.getBytesStorage(otherSlot)).to.be.equal(null); + }); + }); + }); }); From 740ce2d440766e5013640f0e47640fae57f5d1d5 Mon Sep 17 00:00:00 2001 From: Francisco Date: Thu, 2 Feb 2023 21:08:41 -0300 Subject: [PATCH 5/5] Update renovate.json --- renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 875bf6ec45a..c0b97d8d7a7 100644 --- a/renovate.json +++ b/renovate.json @@ -1,4 +1,4 @@ { "extends": ["github>OpenZeppelin/configs"], - "labels": ["ignore-changelog"] + "labels": ["ignore-changeset"] }