From c17b32662662f3974b1455f15c7772c5dac46b6e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 27 Jan 2023 15:19:43 +0100 Subject: [PATCH 01/10] add string and bytes support to the StorageSlots library --- .changeset/modern-games-exist.md | 5 ++ contracts/mocks/StorageSlotMock.sol | 38 ++++++++++- contracts/utils/StorageSlot.sol | 51 +++++++++++++- test/utils/StorageSlot.test.js | 100 ++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 .changeset/modern-games-exist.md 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..22776f6f429 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -27,7 +27,8 @@ pragma solidity ^0.8.0; * } * ``` * - * _Available since v4.1 for `address`, `bool`, `bytes32`, and `uint256`._ + * _Available since v4.1 for `address`, `bool`, `bytes32` and `uint256`._ + * _Available since v4.9 for `string` and `bytes`._ */ library StorageSlot { struct AddressSlot { @@ -46,6 +47,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 +94,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/test/utils/StorageSlot.test.js b/test/utils/StorageSlot.test.js index 9d428875f44..6ae7345d696 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 014851050ccd4642d36a90262ddf5c1b709df086 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 27 Jan 2023 16:08:35 +0100 Subject: [PATCH 02/10] fix lint --- test/utils/StorageSlot.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/StorageSlot.test.js b/test/utils/StorageSlot.test.js index 6ae7345d696..846512ed2ad 100644 --- a/test/utils/StorageSlot.test.js +++ b/test/utils/StorageSlot.test.js @@ -110,7 +110,7 @@ contract('StorageSlot', function (accounts) { describe('string storage slot', function () { beforeEach(async function () { - this.value = "lorem ipsum"; + this.value = 'lorem ipsum'; }); it('set', async function () { @@ -134,7 +134,7 @@ contract('StorageSlot', function (accounts) { describe('string storage pointer', function () { beforeEach(async function () { - this.value = "lorem ipsum"; + this.value = 'lorem ipsum'; }); it('set', async function () { From ab66578fb4e9b0bc61afa76b31944a968ff930dc Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 27 Jan 2023 16:41:43 +0100 Subject: [PATCH 03/10] generate StorageSlot.sol procedurally --- contracts/utils/StorageSlot.sol | 6 +- scripts/generate/run.js | 1 + scripts/generate/templates/StorageSlot.js | 89 +++++++++++++++++++++++ scripts/helpers.js | 5 ++ 4 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 scripts/generate/templates/StorageSlot.js diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index 22776f6f429..fcfec02beb7 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; @@ -26,9 +27,8 @@ pragma solidity ^0.8.0; * } * } * ``` - * - * _Available since v4.1 for `address`, `bool`, `bytes32` and `uint256`._ - * _Available since v4.9 for `string` and `bytes`._ + * _Available since v4.1 for `address`, `bool`, `bytes32`, `uint256`._ + * _Available since v4.9 for `string`, `bytes`._ */ library StorageSlot { struct AddressSlot { 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..71cd97de7c6 --- /dev/null +++ b/scripts/generate/templates/StorageSlot.js @@ -0,0 +1,89 @@ +// const assert = require('assert'); +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 => ({ version, types: TYPES.filter(t => t.version == version).map(t => t.type) })); + +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(v => ` * _Available since v${v.version} for ${v.types.map(t => `\`${t}\``).join(', ')}._ `).join('\n')} + */ +`; + +const struct = type => `\ +struct ${type.struct} { + ${type.type} value; +} +`; + +const accessor = 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 accessorPtr = 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 => [ + accessor(type), + !type.isValueType ? accessorPtr(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, }; From 1c0cfea550aaf206d3a076b7c3177f99a518d643 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 27 Jan 2023 16:43:05 +0100 Subject: [PATCH 04/10] fix lint --- scripts/generate/templates/StorageSlot.js | 32 ++++++++++------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index 71cd97de7c6..34968e14b06 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -3,16 +3,18 @@ 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' })) + { 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 => ({ version, types: TYPES.filter(t => t.version == version).map(t => t.type) })); +const VERSIONS = unique(TYPES.map(t => t.version)).map(version => ({ + version, + types: TYPES.filter(t => t.version == version).map(t => t.type), +})); const header = `\ pragma solidity ^0.8.0; @@ -50,7 +52,7 @@ struct ${type.struct} { } `; -const accessor = type => `\ +const get = type => `\ /** * @dev Returns an \`${type.struct}\` with member \`value\` located at \`slot\`. */ @@ -62,7 +64,7 @@ function get${type.struct}(bytes32 slot) internal pure returns (${type.struct} s } `; -const accessorPtr = type => `\ +const getStorage = type => `\ /** * @dev Returns an \`${type.struct}\` representation of the ${type.type} storage pointer \`store\`. */ @@ -78,12 +80,6 @@ function get${type.struct}(${type.type} storage store) internal pure returns (${ module.exports = format( header.trimEnd(), 'library StorageSlot {', - [ - ...TYPES.map(struct), - ...TYPES.flatMap(type => [ - accessor(type), - !type.isValueType ? accessorPtr(type) : '' - ]), - ], + [...TYPES.map(struct), ...TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)])], '}', ); From 8a0ebab271e0d7e51c3857a221d06926d9a085ad Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 27 Jan 2023 16:45:10 +0100 Subject: [PATCH 05/10] minor refactor of StorageSlot template --- scripts/generate/templates/StorageSlot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index 34968e14b06..0e1763e02ca 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -13,7 +13,7 @@ const TYPES = [ const VERSIONS = unique(TYPES.map(t => t.version)).map(version => ({ version, - types: TYPES.filter(t => t.version == version).map(t => t.type), + types: TYPES.filter(t => t.version == version), })); const header = `\ @@ -42,7 +42,7 @@ pragma solidity ^0.8.0; * } * } * \`\`\` -${VERSIONS.map(v => ` * _Available since v${v.version} for ${v.types.map(t => `\`${t}\``).join(', ')}._ `).join('\n')} +${VERSIONS.map(v => ` * _Available since v${v.version} for ${v.types.map(t => `\`${t.type}\``).join(', ')}._ `).join('\n')} */ `; From 8496bc3b17782254d5460d22392fc729a58d9cfe Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 27 Jan 2023 16:48:16 +0100 Subject: [PATCH 06/10] minor update --- scripts/generate/templates/StorageSlot.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index 0e1763e02ca..0a1acc7716b 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -11,10 +11,12 @@ const TYPES = [ { 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 => ({ - version, - types: TYPES.filter(t => t.version == version), -})); +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; @@ -42,7 +44,7 @@ pragma solidity ^0.8.0; * } * } * \`\`\` -${VERSIONS.map(v => ` * _Available since v${v.version} for ${v.types.map(t => `\`${t.type}\``).join(', ')}._ `).join('\n')} +${VERSIONS.map(s => ` * ${s}`).join('\n')} */ `; From b96081538798fc85434ffec29fa549a4ff5e802a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 27 Jan 2023 16:51:54 +0100 Subject: [PATCH 07/10] minimize change --- contracts/utils/StorageSlot.sol | 1 + scripts/generate/templates/StorageSlot.js | 1 + 2 files changed, 2 insertions(+) diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index fcfec02beb7..44285c90035 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -27,6 +27,7 @@ pragma solidity ^0.8.0; * } * } * ``` + * * _Available since v4.1 for `address`, `bool`, `bytes32`, `uint256`._ * _Available since v4.9 for `string`, `bytes`._ */ diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index 0a1acc7716b..b01d633b6be 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -44,6 +44,7 @@ pragma solidity ^0.8.0; * } * } * \`\`\` + * ${VERSIONS.map(s => ` * ${s}`).join('\n')} */ `; From ac13620b667f9e955eb5089d4b934abcf1ed485a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 27 Jan 2023 16:52:54 +0100 Subject: [PATCH 08/10] add changeset --- .changeset/rare-kids-punch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rare-kids-punch.md diff --git a/.changeset/rare-kids-punch.md b/.changeset/rare-kids-punch.md new file mode 100644 index 00000000000..375fccbf6d2 --- /dev/null +++ b/.changeset/rare-kids-punch.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`StorageSlot`: library is now generated from a template. From 0690c19b4c75668f83075c27b77c8df8cd24b116 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 30 Jan 2023 10:17:33 +0100 Subject: [PATCH 09/10] Update scripts/generate/templates/StorageSlot.js Co-authored-by: Francisco --- scripts/generate/templates/StorageSlot.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index b01d633b6be..69fa7ccc0ef 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -1,4 +1,3 @@ -// const assert = require('assert'); const format = require('../format-lines'); const { capitalize, unique } = require('../../helpers'); From 26db62a2ed081eb20a0da0356ef5a668d1593079 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 1 Feb 2023 09:59:03 +0100 Subject: [PATCH 10/10] remove unecessary changeset --- .changeset/rare-kids-punch.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/rare-kids-punch.md diff --git a/.changeset/rare-kids-punch.md b/.changeset/rare-kids-punch.md deleted file mode 100644 index 375fccbf6d2..00000000000 --- a/.changeset/rare-kids-punch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`StorageSlot`: library is now generated from a template.