From 788a9c2ae7f40ce41531b89448cca52cd32db6da Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 26 Mar 2024 21:18:12 +0100 Subject: [PATCH 01/13] Add slot derivation library --- .changeset/gorgeous-badgers-vanish.md | 5 + contracts/mocks/StorageSlotMock.sol | 8 + contracts/utils/Arrays.sol | 26 +-- contracts/utils/SlotDerivation.sol | 146 +++++++++++++ contracts/utils/StorageSlot.sol | 14 ++ scripts/generate/run.js | 2 + scripts/generate/templates/Slot.opts.js | 13 ++ scripts/generate/templates/SlotDerivation.js | 101 +++++++++ .../generate/templates/SlotDerivation.t.js | 74 +++++++ scripts/generate/templates/StorageSlot.js | 32 ++- test/utils/SlotDerivation.t.sol | 204 ++++++++++++++++++ test/utils/SlotDerivation.test.js | 28 +++ test/utils/StorageSlot.test.js | 8 +- 13 files changed, 620 insertions(+), 41 deletions(-) create mode 100644 .changeset/gorgeous-badgers-vanish.md create mode 100644 contracts/utils/SlotDerivation.sol create mode 100644 scripts/generate/templates/Slot.opts.js create mode 100644 scripts/generate/templates/SlotDerivation.js create mode 100644 scripts/generate/templates/SlotDerivation.t.js create mode 100644 test/utils/SlotDerivation.t.sol create mode 100644 test/utils/SlotDerivation.test.js diff --git a/.changeset/gorgeous-badgers-vanish.md b/.changeset/gorgeous-badgers-vanish.md new file mode 100644 index 00000000000..ce75ed6ebae --- /dev/null +++ b/.changeset/gorgeous-badgers-vanish.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`SlotDerivation`: Add a library of methods for derivating common storage slots. diff --git a/contracts/mocks/StorageSlotMock.sol b/contracts/mocks/StorageSlotMock.sol index 36f0f5af022..c02378b4688 100644 --- a/contracts/mocks/StorageSlotMock.sol +++ b/contracts/mocks/StorageSlotMock.sol @@ -23,6 +23,10 @@ contract StorageSlotMock { slot.getUint256Slot().value = value; } + function setInt256Slot(bytes32 slot, int256 value) public { + slot.getInt256Slot().value = value; + } + function getBooleanSlot(bytes32 slot) public view returns (bool) { return slot.getBooleanSlot().value; } @@ -39,6 +43,10 @@ contract StorageSlotMock { return slot.getUint256Slot().value; } + function getInt256Slot(bytes32 slot) public view returns (int256) { + return slot.getInt256Slot().value; + } + mapping(uint256 key => string) public stringMap; function setStringSlot(bytes32 slot, string calldata value) public { diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index 02252059898..74233235d39 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; +import {SlotDerivation} from "./SlotDerivation.sol"; import {StorageSlot} from "./StorageSlot.sol"; import {Math} from "./math/Math.sol"; @@ -10,6 +11,7 @@ import {Math} from "./math/Math.sol"; * @dev Collection of functions related to array types. */ library Arrays { + using SlotDerivation for bytes32; using StorageSlot for bytes32; /** @@ -361,15 +363,11 @@ library Arrays { */ function unsafeAccess(address[] storage arr, uint256 pos) internal pure returns (StorageSlot.AddressSlot storage) { bytes32 slot; - // We use assembly to calculate the storage slot of the element at index `pos` of the dynamic array `arr` - // following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays. - /// @solidity memory-safe-assembly assembly { - mstore(0, arr.slot) - slot := add(keccak256(0, 0x20), pos) + slot := arr.slot } - return slot.getAddressSlot(); + return slot.deriveArray().offset(pos).getAddressSlot(); } /** @@ -379,15 +377,11 @@ library Arrays { */ function unsafeAccess(bytes32[] storage arr, uint256 pos) internal pure returns (StorageSlot.Bytes32Slot storage) { bytes32 slot; - // We use assembly to calculate the storage slot of the element at index `pos` of the dynamic array `arr` - // following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays. - /// @solidity memory-safe-assembly assembly { - mstore(0, arr.slot) - slot := add(keccak256(0, 0x20), pos) + slot := arr.slot } - return slot.getBytes32Slot(); + return slot.deriveArray().offset(pos).getBytes32Slot(); } /** @@ -397,15 +391,11 @@ library Arrays { */ function unsafeAccess(uint256[] storage arr, uint256 pos) internal pure returns (StorageSlot.Uint256Slot storage) { bytes32 slot; - // We use assembly to calculate the storage slot of the element at index `pos` of the dynamic array `arr` - // following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays. - /// @solidity memory-safe-assembly assembly { - mstore(0, arr.slot) - slot := add(keccak256(0, 0x20), pos) + slot := arr.slot } - return slot.getUint256Slot(); + return slot.deriveArray().offset(pos).getUint256Slot(); } /** diff --git a/contracts/utils/SlotDerivation.sol b/contracts/utils/SlotDerivation.sol new file mode 100644 index 00000000000..2f4e8b3f476 --- /dev/null +++ b/contracts/utils/SlotDerivation.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT +// This file was procedurally generated from scripts/generate/templates/SlotDerivation.js. + +pragma solidity ^0.8.20; + +/** + * @dev Library for computing storage (and transient storage) locations from namespaces and deriving slots + * corresponding to standard patterns. The derivation method for array and mapping matches the storage layout used by + * the solidity language / compiler. + * + * See https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays[Solidity docs for mappings and dynamic arrays.]. + */ +library SlotDerivation { + /** + * @dev Derive an ERC-1967 slot from a string (namespace). + */ + function erc1967slot(string memory namespace) internal pure returns (bytes32 slot) { + /// @solidity memory-safe-assembly + assembly { + slot := sub(keccak256(add(namespace, 0x20), mload(namespace)), 1) + } + } + + /** + * @dev Derive an ERC-7201 slot from a string (namespace). + */ + function erc7201slot(string memory namespace) internal pure returns (bytes32 slot) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1)) + slot := and(keccak256(0x00, 0x20), not(0xff)) + } + } + + /** + * @dev Add an offset to a slot to get the n-th element of a structure or an array. + */ + function offset(bytes32 slot, uint256 pos) internal pure returns (bytes32 result) { + unchecked { + return bytes32(uint256(slot) + pos); + } + } + + /** + * @dev Derive the location of the first element in an array from the slot where the length is stored. + */ + function deriveArray(bytes32 slot) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, slot) + result := keccak256(0x00, 0x20) + } + } + + /** + * @dev Derive the location of a mapping element from the key. + */ + function deriveMapping(bytes32 slot, address key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } + } + + /** + * @dev Derive the location of a mapping element from the key. + */ + function deriveMapping(bytes32 slot, bool key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } + } + + /** + * @dev Derive the location of a mapping element from the key. + */ + function deriveMapping(bytes32 slot, bytes32 key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } + } + + /** + * @dev Derive the location of a mapping element from the key. + */ + function deriveMapping(bytes32 slot, uint256 key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } + } + + /** + * @dev Derive the location of a mapping element from the key. + */ + function deriveMapping(bytes32 slot, int256 key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } + } + + /** + * @dev Derive the location of a mapping element from the key. + */ + function deriveMapping(bytes32 slot, string memory key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + let length := mload(key) + let begin := add(key, 0x20) + let end := add(begin, length) + let cache := mload(end) + mstore(end, slot) + result := keccak256(begin, add(length, 0x20)) + mstore(end, cache) + } + } + + /** + * @dev Derive the location of a mapping element from the key. + */ + function deriveMapping(bytes32 slot, bytes memory key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + let length := mload(key) + let begin := add(key, 0x20) + let end := add(begin, length) + let cache := mload(end) + mstore(end, slot) + result := keccak256(begin, add(length, 0x20)) + mstore(end, cache) + } + } +} diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index 4e02bfe746d..5dad9479770 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -45,6 +45,10 @@ library StorageSlot { uint256 value; } + struct Int256Slot { + int256 value; + } + struct StringSlot { string value; } @@ -93,6 +97,16 @@ library StorageSlot { } } + /** + * @dev Returns an `Int256Slot` with member `value` located at `slot`. + */ + function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + /** * @dev Returns an `StringSlot` with member `value` located at `slot`. */ diff --git a/scripts/generate/run.js b/scripts/generate/run.js index 53589455ab5..19f98a7eaef 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -36,6 +36,7 @@ for (const [file, template] of Object.entries({ 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', 'utils/structs/Checkpoints.sol': './templates/Checkpoints.js', + 'utils/SlotDerivation.sol': './templates/SlotDerivation.js', 'utils/StorageSlot.sol': './templates/StorageSlot.js', })) { generateFromTemplate(file, template, './contracts/'); @@ -44,6 +45,7 @@ for (const [file, template] of Object.entries({ // Tests for (const [file, template] of Object.entries({ 'utils/structs/Checkpoints.t.sol': './templates/Checkpoints.t.js', + 'utils/SlotDerivation.t.sol': './templates/SlotDerivation.t.js', })) { generateFromTemplate(file, template, './test/'); } diff --git a/scripts/generate/templates/Slot.opts.js b/scripts/generate/templates/Slot.opts.js new file mode 100644 index 00000000000..aed1f988845 --- /dev/null +++ b/scripts/generate/templates/Slot.opts.js @@ -0,0 +1,13 @@ +const { capitalize } = require('../../helpers'); + +const TYPES = [ + { type: 'address', isValueType: true }, + { type: 'bool', isValueType: true, name: 'Boolean' }, + { type: 'bytes32', isValueType: true, variants: ['bytes4'] }, + { type: 'uint256', isValueType: true, variants: ['uint32'] }, + { type: 'int256', isValueType: true, variants: ['int32'] }, + { type: 'string', isValueType: false }, + { type: 'bytes', isValueType: false }, +].map(type => Object.assign(type, { name: type.name ?? capitalize(type.type) })); + +module.exports = { TYPES }; diff --git a/scripts/generate/templates/SlotDerivation.js b/scripts/generate/templates/SlotDerivation.js new file mode 100644 index 00000000000..dc62ad7665d --- /dev/null +++ b/scripts/generate/templates/SlotDerivation.js @@ -0,0 +1,101 @@ +const format = require('../format-lines'); +const { TYPES } = require('./Slot.opts'); + +const header = `\ +pragma solidity ^0.8.20; + +/** + * @dev Library for computing storage (and transient storage) locations from namespaces and deriving slots + * corresponding to standard patterns. The derivation method for array and mapping matches the storage layout used by + * the solidity language / compiler. + * + * See https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays[Solidity docs for mappings and dynamic arrays.]. + */ +`; + +const namespace = `\ +/** + * @dev Derive an ERC-1967 slot from a string (namespace). + */ +function erc1967slot(string memory namespace) internal pure returns (bytes32 slot) { + /// @solidity memory-safe-assembly + assembly { + slot := sub(keccak256(add(namespace, 0x20), mload(namespace)), 1) + } +} + +/** + * @dev Derive an ERC-7201 slot from a string (namespace). + */ +function erc7201slot(string memory namespace) internal pure returns (bytes32 slot) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1)) + slot := and(keccak256(0x00, 0x20), not(0xff)) + } +} +`; + +const array = `\ +/** + * @dev Add an offset to a slot to get the n-th element of a structure or an array. + */ +function offset(bytes32 slot, uint256 pos) internal pure returns (bytes32 result) { + unchecked { + return bytes32(uint256(slot) + pos); + } +} + +/** + * @dev Derive the location of the first element in an array from the slot where the length is stored. + */ +function deriveArray(bytes32 slot) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, slot) + result := keccak256(0x00, 0x20) + } +} +`; + +const mapping = ({ type }) => `\ +/** + * @dev Derive the location of a mapping element from the key. + */ +function deriveMapping(bytes32 slot, ${type} key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } +} +`; + +const mapping2 = ({ type }) => `\ +/** + * @dev Derive the location of a mapping element from the key. + */ +function deriveMapping(bytes32 slot, ${type} memory key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + let length := mload(key) + let begin := add(key, 0x20) + let end := add(begin, length) + let cache := mload(end) + mstore(end, slot) + result := keccak256(begin, add(length, 0x20)) + mstore(end, cache) + } +} +`; + +// GENERATE +module.exports = format( + header.trimEnd(), + 'library SlotDerivation {', + namespace, + array, + TYPES.map(type => (type.isValueType ? mapping(type) : mapping2(type))), + '}', +); diff --git a/scripts/generate/templates/SlotDerivation.t.js b/scripts/generate/templates/SlotDerivation.t.js new file mode 100644 index 00000000000..9a570053fa6 --- /dev/null +++ b/scripts/generate/templates/SlotDerivation.t.js @@ -0,0 +1,74 @@ +const format = require('../format-lines'); +const { capitalize } = require('../../helpers'); +const { TYPES } = require('./Slot.opts'); + +const header = `\ +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol"; +`; + +const array = `\ +bytes[] private _array; + +function testDeriveArray(uint256 length, uint256 offset) public { + length = bound(length, 1, type(uint256).max); + offset = bound(offset, 0, length - 1); + + bytes32 baseSlot; + assembly { + baseSlot := _array.slot + sstore(baseSlot, length) // store length so solidity access does not revert + } + + bytes storage derived = _array[offset]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveArray().offset(offset), derivedSlot); +} +`; + +const mapping = ({ type, name, isValueType }) => `\ +mapping(${type} => bytes) private _${type}Mapping; + +function testDeriveMapping${name}(${type} ${isValueType ? '' : 'memory'} key) public { + bytes32 baseSlot; + assembly { + baseSlot := _${type}Mapping.slot + } + + bytes storage derived = _${type}Mapping[key]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveMapping(key), derivedSlot); +} +`; + +// GENERATE +module.exports = format( + header.trimEnd(), + '// solhint-disable func-name-mixedcase', + 'contract SlotDerivationTest is Test {', + 'using SlotDerivation for bytes32;', + '', + array, + TYPES.flatMap(type => + [].concat( + type, + (type.variants ?? []).map(variant => ({ + type: variant, + name: capitalize(variant), + isValueType: type.isValueType, + })), + ), + ).map(type => mapping(type)), + '}', +); diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index 3d2a62a9230..bcd14ad5fc3 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -1,14 +1,5 @@ const format = require('../format-lines'); -const { capitalize } = require('../../helpers'); - -const TYPES = [ - { type: 'address', isValueType: true }, - { type: 'bool', isValueType: true, name: 'Boolean' }, - { type: 'bytes32', isValueType: true }, - { type: 'uint256', isValueType: true }, - { type: 'string', isValueType: false }, - { type: 'bytes', isValueType: false }, -].map(type => Object.assign(type, { struct: (type.name ?? capitalize(type.type)) + 'Slot' })); +const { TYPES } = require('./Slot.opts'); const header = `\ pragma solidity ^0.8.20; @@ -39,17 +30,17 @@ pragma solidity ^0.8.20; */ `; -const struct = type => `\ -struct ${type.struct} { - ${type.type} value; +const struct = ({ type, name }) => `\ +struct ${name}Slot { + ${type} value; } `; -const get = type => `\ +const get = ({ name }) => `\ /** - * @dev Returns an \`${type.struct}\` with member \`value\` located at \`slot\`. + * @dev Returns an \`${name}Slot\` with member \`value\` located at \`slot\`. */ -function get${type.struct}(bytes32 slot) internal pure returns (${type.struct} storage r) { +function get${name}Slot(bytes32 slot) internal pure returns (${name}Slot storage r) { /// @solidity memory-safe-assembly assembly { r.slot := slot @@ -57,11 +48,11 @@ function get${type.struct}(bytes32 slot) internal pure returns (${type.struct} s } `; -const getStorage = type => `\ +const getStorage = ({ type, name }) => `\ /** - * @dev Returns an \`${type.struct}\` representation of the ${type.type} storage pointer \`store\`. + * @dev Returns an \`${name}Slot\` representation of the ${type} storage pointer \`store\`. */ -function get${type.struct}(${type.type} storage store) internal pure returns (${type.struct} storage r) { +function get${name}Slot(${type} storage store) internal pure returns (${name}Slot storage r) { /// @solidity memory-safe-assembly assembly { r.slot := store.slot @@ -73,6 +64,7 @@ function get${type.struct}(${type.type} storage store) internal pure returns (${ module.exports = format( header.trimEnd(), 'library StorageSlot {', - [...TYPES.map(struct), ...TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)])], + TYPES.map(type => struct(type)), + TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)]), '}', ); diff --git a/test/utils/SlotDerivation.t.sol b/test/utils/SlotDerivation.t.sol new file mode 100644 index 00000000000..2551ef22057 --- /dev/null +++ b/test/utils/SlotDerivation.t.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT +// This file was procedurally generated from scripts/generate/templates/SlotDerivation.t.js. + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol"; + +// solhint-disable func-name-mixedcase +contract SlotDerivationTest is Test { + using SlotDerivation for bytes32; + + bytes[] private _array; + + function testDeriveArray(uint256 length, uint256 offset) public { + length = bound(length, 1, type(uint256).max); + offset = bound(offset, 0, length - 1); + + bytes32 baseSlot; + assembly { + baseSlot := _array.slot + sstore(baseSlot, length) // store length so solidity access does not revert + } + + bytes storage derived = _array[offset]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveArray().offset(offset), derivedSlot); + } + + mapping(address => bytes) private _addressMapping; + + function testDeriveMappingAddress(address key) public { + bytes32 baseSlot; + assembly { + baseSlot := _addressMapping.slot + } + + bytes storage derived = _addressMapping[key]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveMapping(key), derivedSlot); + } + + mapping(bool => bytes) private _boolMapping; + + function testDeriveMappingBoolean(bool key) public { + bytes32 baseSlot; + assembly { + baseSlot := _boolMapping.slot + } + + bytes storage derived = _boolMapping[key]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveMapping(key), derivedSlot); + } + + mapping(bytes32 => bytes) private _bytes32Mapping; + + function testDeriveMappingBytes32(bytes32 key) public { + bytes32 baseSlot; + assembly { + baseSlot := _bytes32Mapping.slot + } + + bytes storage derived = _bytes32Mapping[key]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveMapping(key), derivedSlot); + } + + mapping(bytes4 => bytes) private _bytes4Mapping; + + function testDeriveMappingBytes4(bytes4 key) public { + bytes32 baseSlot; + assembly { + baseSlot := _bytes4Mapping.slot + } + + bytes storage derived = _bytes4Mapping[key]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveMapping(key), derivedSlot); + } + + mapping(uint256 => bytes) private _uint256Mapping; + + function testDeriveMappingUint256(uint256 key) public { + bytes32 baseSlot; + assembly { + baseSlot := _uint256Mapping.slot + } + + bytes storage derived = _uint256Mapping[key]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveMapping(key), derivedSlot); + } + + mapping(uint32 => bytes) private _uint32Mapping; + + function testDeriveMappingUint32(uint32 key) public { + bytes32 baseSlot; + assembly { + baseSlot := _uint32Mapping.slot + } + + bytes storage derived = _uint32Mapping[key]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveMapping(key), derivedSlot); + } + + mapping(int256 => bytes) private _int256Mapping; + + function testDeriveMappingInt256(int256 key) public { + bytes32 baseSlot; + assembly { + baseSlot := _int256Mapping.slot + } + + bytes storage derived = _int256Mapping[key]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveMapping(key), derivedSlot); + } + + mapping(int32 => bytes) private _int32Mapping; + + function testDeriveMappingInt32(int32 key) public { + bytes32 baseSlot; + assembly { + baseSlot := _int32Mapping.slot + } + + bytes storage derived = _int32Mapping[key]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveMapping(key), derivedSlot); + } + + mapping(string => bytes) private _stringMapping; + + function testDeriveMappingString(string memory key) public { + bytes32 baseSlot; + assembly { + baseSlot := _stringMapping.slot + } + + bytes storage derived = _stringMapping[key]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveMapping(key), derivedSlot); + } + + mapping(bytes => bytes) private _bytesMapping; + + function testDeriveMappingBytes(bytes memory key) public { + bytes32 baseSlot; + assembly { + baseSlot := _bytesMapping.slot + } + + bytes storage derived = _bytesMapping[key]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveMapping(key), derivedSlot); + } +} diff --git a/test/utils/SlotDerivation.test.js b/test/utils/SlotDerivation.test.js new file mode 100644 index 00000000000..c91feba6494 --- /dev/null +++ b/test/utils/SlotDerivation.test.js @@ -0,0 +1,28 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { erc1967slot, erc7201slot } = require('../helpers/storage'); + +async function fixture() { + const [account] = await ethers.getSigners(); + const mock = await ethers.deployContract('$SlotDerivation'); + return { mock, account }; +} + +describe('SlotDerivation', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('namespaces', function () { + const namespace = 'example.main'; + + it('erc-1967', async function () { + expect(await this.mock.$erc1967slot(namespace)).to.equal(erc1967slot(namespace)); + }); + + it('erc-7201', async function () { + expect(await this.mock.$erc7201slot(namespace)).to.equal(erc7201slot(namespace)); + }); + }); +}); diff --git a/test/utils/StorageSlot.test.js b/test/utils/StorageSlot.test.js index ab237b700a0..bb218ee446f 100644 --- a/test/utils/StorageSlot.test.js +++ b/test/utils/StorageSlot.test.js @@ -19,10 +19,12 @@ describe('StorageSlot', function () { for (const { type, value, zero } of [ { type: 'Boolean', value: true, zero: false }, - { type: 'Address', value: generators.address(), zero: ethers.ZeroAddress }, - { type: 'Bytes32', value: generators.bytes32(), zero: ethers.ZeroHash }, + { type: 'Address', value: generators.address(), zero: generators.address.zero }, + { type: 'Bytes32', value: generators.bytes32(), zero: generators.bytes32.zero }, + { type: 'Uint256', value: generators.uint256(), zero: generators.uint256.zero }, + { type: 'Int256', value: generators.int256(), zero: generators.int256.zero }, + { type: 'Bytes', value: generators.hexBytes(128), zero: generators.hexBytes.zero }, { type: 'String', value: 'lorem ipsum', zero: '' }, - { type: 'Bytes', value: generators.hexBytes(128), zero: '0x' }, ]) { describe(`${type} storage slot`, function () { it('set', async function () { From 994dde257acf8ec03fe3010316c73d0f8468b18e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 26 Mar 2024 22:05:10 +0100 Subject: [PATCH 02/13] unit tests for coverage --- test/helpers/random.js | 2 ++ test/utils/SlotDerivation.test.js | 34 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/test/helpers/random.js b/test/helpers/random.js index 48b97768b6e..3adeed0a447 100644 --- a/test/helpers/random.js +++ b/test/helpers/random.js @@ -4,12 +4,14 @@ const generators = { address: () => ethers.Wallet.createRandom().address, bytes32: () => ethers.hexlify(ethers.randomBytes(32)), uint256: () => ethers.toBigInt(ethers.randomBytes(32)), + int256: () => ethers.toBigInt(ethers.randomBytes(32)) + ethers.MinInt256, hexBytes: length => ethers.hexlify(ethers.randomBytes(length)), }; generators.address.zero = ethers.ZeroAddress; generators.bytes32.zero = ethers.ZeroHash; generators.uint256.zero = 0n; +generators.int256.zero = 0n; generators.hexBytes.zero = '0x'; module.exports = { diff --git a/test/utils/SlotDerivation.test.js b/test/utils/SlotDerivation.test.js index c91feba6494..a4090cdbe5d 100644 --- a/test/utils/SlotDerivation.test.js +++ b/test/utils/SlotDerivation.test.js @@ -2,6 +2,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { erc1967slot, erc7201slot } = require('../helpers/storage'); +const { generators } = require('../helpers/random'); async function fixture() { const [account] = await ethers.getSigners(); @@ -25,4 +26,37 @@ describe('SlotDerivation', function () { expect(await this.mock.$erc7201slot(namespace)).to.equal(erc7201slot(namespace)); }); }); + + describe('derivation', function () { + it('offset', async function () { + const base = generators.bytes32(); + const offset = generators.uint256(); + expect(await this.mock.$offset(base, offset)).to.equal((ethers.toBigInt(base) + offset) % 2n ** 256n); + }); + + it('array', async function () { + const base = generators.bytes32(); + expect(await this.mock.$deriveArray(base)).to.equal(ethers.keccak256(base)); + }); + + describe('mapping', function () { + for (const { type, key, isValueType } of [ + { type: 'bool', key: true, isValueType: true }, + { type: 'address', key: generators.address(), isValueType: true }, + { type: 'bytes32', key: generators.bytes32(), isValueType: true }, + { type: 'uint256', key: generators.uint256(), isValueType: true }, + { type: 'int256', key: generators.int256(), isValueType: true }, + { type: 'bytes', key: generators.hexBytes(128), isValueType: false }, + { type: 'string', key: 'lorem ipsum', isValueType: false }, + ]) { + it(type, async function () { + const base = generators.bytes32(); + const expected = isValueType + ? ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode([type, 'bytes32'], [key, base])) + : ethers.solidityPackedKeccak256([type, 'bytes32'], [key, base]); + expect(await this.mock[`$deriveMapping(bytes32,${type})`](base, key)).to.equal(expected); + }); + } + }); + }); }); From 5d87da3c63568e44a23220a1e6cf79a0dd438fe8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 26 Mar 2024 22:10:13 +0100 Subject: [PATCH 03/13] update --- test/utils/SlotDerivation.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/SlotDerivation.test.js b/test/utils/SlotDerivation.test.js index a4090cdbe5d..b9c15584631 100644 --- a/test/utils/SlotDerivation.test.js +++ b/test/utils/SlotDerivation.test.js @@ -31,7 +31,7 @@ describe('SlotDerivation', function () { it('offset', async function () { const base = generators.bytes32(); const offset = generators.uint256(); - expect(await this.mock.$offset(base, offset)).to.equal((ethers.toBigInt(base) + offset) % 2n ** 256n); + expect(await this.mock.$offset(base, offset)).to.equal((ethers.toBigInt(base) + offset) & ethers.MaxUint256); }); it('array', async function () { From d691ec9cc178edcf37b8e9499a3d64b4c67f1903 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 27 Mar 2024 11:11:44 +0000 Subject: [PATCH 04/13] Apply PR suggestions --- contracts/utils/SlotDerivation.sol | 21 ++++++++++++++++++-- contracts/utils/StorageSlot.sol | 3 +++ scripts/generate/templates/SlotDerivation.js | 21 ++++++++++++++++++-- scripts/generate/templates/StorageSlot.js | 3 +++ test/helpers/storage.js | 20 +++++++++---------- test/utils/SlotDerivation.test.js | 6 +++--- 6 files changed, 57 insertions(+), 17 deletions(-) diff --git a/contracts/utils/SlotDerivation.sol b/contracts/utils/SlotDerivation.sol index 2f4e8b3f476..9a57a50a651 100644 --- a/contracts/utils/SlotDerivation.sol +++ b/contracts/utils/SlotDerivation.sol @@ -9,12 +9,29 @@ pragma solidity ^0.8.20; * the solidity language / compiler. * * See https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays[Solidity docs for mappings and dynamic arrays.]. + * + * Example usage: + * ```solidity + * contract Example { + * // Add the library methods + * using SlotDerivation for bytes32; + * + * // Declare a namespace + * string private constant _NAMESPACE = "" // eg. OpenZeppelin.Slot + * + * function storagePointer() internal view returns (bytes32) { + * return _NAMESPACE.erc7201Slot(); // or erc1967Slot() + * } + * } + * ``` + * + * TIP: Consider using this library along with {StorageSlot}. */ library SlotDerivation { /** * @dev Derive an ERC-1967 slot from a string (namespace). */ - function erc1967slot(string memory namespace) internal pure returns (bytes32 slot) { + function erc1967Slot(string memory namespace) internal pure returns (bytes32 slot) { /// @solidity memory-safe-assembly assembly { slot := sub(keccak256(add(namespace, 0x20), mload(namespace)), 1) @@ -24,7 +41,7 @@ library SlotDerivation { /** * @dev Derive an ERC-7201 slot from a string (namespace). */ - function erc7201slot(string memory namespace) internal pure returns (bytes32 slot) { + function erc7201Slot(string memory namespace) internal pure returns (bytes32 slot) { /// @solidity memory-safe-assembly assembly { mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1)) diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index 5dad9479770..bb54de13a1f 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -15,6 +15,7 @@ pragma solidity ^0.8.20; * Example usage to set ERC-1967 implementation slot: * ```solidity * contract ERC1967 { + * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot. * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; * * function _getImplementation() internal view returns (address) { @@ -27,6 +28,8 @@ pragma solidity ^0.8.20; * } * } * ``` + * + * TIP: Consider using this library along with {SlotDerivation}. */ library StorageSlot { struct AddressSlot { diff --git a/scripts/generate/templates/SlotDerivation.js b/scripts/generate/templates/SlotDerivation.js index dc62ad7665d..57697d1266e 100644 --- a/scripts/generate/templates/SlotDerivation.js +++ b/scripts/generate/templates/SlotDerivation.js @@ -10,6 +10,23 @@ pragma solidity ^0.8.20; * the solidity language / compiler. * * See https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays[Solidity docs for mappings and dynamic arrays.]. + * + * Example usage: + * \`\`\`solidity + * contract Example { + * // Add the library methods + * using SlotDerivation for bytes32; + * + * // Declare a namespace + * string private constant _NAMESPACE = "" // eg. OpenZeppelin.Slot + * + * function storagePointer() internal view returns (bytes32) { + * return _NAMESPACE.erc7201Slot(); // or erc1967Slot() + * } + * } + * \`\`\` + * + * TIP: Consider using this library along with {StorageSlot}. */ `; @@ -17,7 +34,7 @@ const namespace = `\ /** * @dev Derive an ERC-1967 slot from a string (namespace). */ -function erc1967slot(string memory namespace) internal pure returns (bytes32 slot) { +function erc1967Slot(string memory namespace) internal pure returns (bytes32 slot) { /// @solidity memory-safe-assembly assembly { slot := sub(keccak256(add(namespace, 0x20), mload(namespace)), 1) @@ -27,7 +44,7 @@ function erc1967slot(string memory namespace) internal pure returns (bytes32 slo /** * @dev Derive an ERC-7201 slot from a string (namespace). */ -function erc7201slot(string memory namespace) internal pure returns (bytes32 slot) { +function erc7201Slot(string memory namespace) internal pure returns (bytes32 slot) { /// @solidity memory-safe-assembly assembly { mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1)) diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index bcd14ad5fc3..80e5ab1395c 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -15,6 +15,7 @@ pragma solidity ^0.8.20; * Example usage to set ERC-1967 implementation slot: * \`\`\`solidity * contract ERC1967 { + * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot. * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; * * function _getImplementation() internal view returns (address) { @@ -27,6 +28,8 @@ pragma solidity ^0.8.20; * } * } * \`\`\` + * + * TIP: Consider using this library along with {SlotDerivation}. */ `; diff --git a/test/helpers/storage.js b/test/helpers/storage.js index 2b688200229..a75a3060d4b 100644 --- a/test/helpers/storage.js +++ b/test/helpers/storage.js @@ -5,18 +5,18 @@ const ImplementationLabel = 'eip1967.proxy.implementation'; const AdminLabel = 'eip1967.proxy.admin'; const BeaconLabel = 'eip1967.proxy.beacon'; -const erc1967slot = label => ethers.toBeHex(ethers.toBigInt(ethers.id(label)) - 1n); -const erc7201slot = label => ethers.toBeHex(ethers.toBigInt(ethers.keccak256(erc1967slot(label))) & ~0xffn); +const erc1967Slot = label => ethers.toBeHex(ethers.toBigInt(ethers.id(label)) - 1n); +const erc7201Slot = label => ethers.toBeHex(ethers.toBigInt(ethers.keccak256(erc1967Slot(label))) & ~0xffn); const erc7201format = contractName => `openzeppelin.storage.${contractName}`; const getSlot = (address, slot) => - ethers.provider.getStorage(address, ethers.isBytesLike(slot) ? slot : erc1967slot(slot)); + ethers.provider.getStorage(address, ethers.isBytesLike(slot) ? slot : erc1967Slot(slot)); const setSlot = (address, slot, value) => Promise.all([ ethers.isAddressable(address) ? address.getAddress() : Promise.resolve(address), ethers.isAddressable(value) ? value.getAddress() : Promise.resolve(value), - ]).then(([address, value]) => setStorageAt(address, ethers.isBytesLike(slot) ? slot : erc1967slot(slot), value)); + ]).then(([address, value]) => setStorageAt(address, ethers.isBytesLike(slot) ? slot : erc1967Slot(slot), value)); const getAddressInSlot = (address, slot) => getSlot(address, slot).then(slotValue => ethers.AbiCoder.defaultAbiCoder().decode(['address'], slotValue)[0]); @@ -25,7 +25,7 @@ const upgradeableSlot = (contractName, offset) => { try { // Try to get the artifact paths, will throw if it doesn't exist artifacts._getArtifactPathSync(`${contractName}Upgradeable`); - return offset + ethers.toBigInt(erc7201slot(erc7201format(contractName))); + return offset + ethers.toBigInt(erc7201Slot(erc7201format(contractName))); } catch (_) { return offset; } @@ -35,11 +35,11 @@ module.exports = { ImplementationLabel, AdminLabel, BeaconLabel, - ImplementationSlot: erc1967slot(ImplementationLabel), - AdminSlot: erc1967slot(AdminLabel), - BeaconSlot: erc1967slot(BeaconLabel), - erc1967slot, - erc7201slot, + ImplementationSlot: erc1967Slot(ImplementationLabel), + AdminSlot: erc1967Slot(AdminLabel), + BeaconSlot: erc1967Slot(BeaconLabel), + erc1967Slot, + erc7201Slot, erc7201format, setSlot, getSlot, diff --git a/test/utils/SlotDerivation.test.js b/test/utils/SlotDerivation.test.js index b9c15584631..b4d0c742fdd 100644 --- a/test/utils/SlotDerivation.test.js +++ b/test/utils/SlotDerivation.test.js @@ -1,7 +1,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { erc1967slot, erc7201slot } = require('../helpers/storage'); +const { erc1967Slot, erc7201Slot } = require('../helpers/storage'); const { generators } = require('../helpers/random'); async function fixture() { @@ -19,11 +19,11 @@ describe('SlotDerivation', function () { const namespace = 'example.main'; it('erc-1967', async function () { - expect(await this.mock.$erc1967slot(namespace)).to.equal(erc1967slot(namespace)); + expect(await this.mock.$erc1967Slot(namespace)).to.equal(erc1967Slot(namespace)); }); it('erc-7201', async function () { - expect(await this.mock.$erc7201slot(namespace)).to.equal(erc7201slot(namespace)); + expect(await this.mock.$erc7201Slot(namespace)).to.equal(erc7201Slot(namespace)); }); }); From c07237ca96c0b303811c889d1162877f4e406db4 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 27 Mar 2024 11:56:27 +0000 Subject: [PATCH 05/13] Improve documentation --- contracts/utils/README.adoc | 3 + docs/modules/ROOT/pages/utilities.adoc | 77 ++++++++++++++++++- .../generate/templates/SlotDerivation.t.js | 1 - test/utils/SlotDerivation.t.sol | 1 - 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index b7c1baf8db6..bf98a07a96d 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -28,6 +28,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Base64}: On-chain base64 and base64URL encoding according to https://datatracker.ietf.org/doc/html/rfc4648[RFC-4648]. * {Strings}: Common operations for strings formatting. * {ShortString}: Library to encode (and decode) short strings into (or from) a single bytes32 slot for optimizing costs. Short strings are limited to 31 characters. + * {SlotDerivation}: Methods for deriving storage slot from ERC-1967 and ERC-7201 namespaces as well as from constructions such as mapping and arrays. * {StorageSlot}: Methods for accessing specific storage slots formatted as common primitive types. * {Multicall}: Abstract contract with an utility to allow batching together multiple calls in a single transaction. Useful for allowing EOAs to perform multiple operations at once. * {Context}: An utility for abstracting the sender and calldata in the current execution context. @@ -108,6 +109,8 @@ Ethereum contracts have no native concept of an interface, so applications must {{ShortStrings}} +{{SlotDerivation}} + {{StorageSlot}} {{Multicall}} diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 41df1f32b1e..83cf9083ff7 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -1,6 +1,7 @@ = Utilities -The OpenZeppelin Contracts provide a ton of useful utilities that you can use in your project. Here are some of the more popular ones. +The OpenZeppelin Contracts provide a ton of useful utilities that you can use in your project. For a complete list, check out the xref:api:utils.adoc[API Reference]. +Here are some of the more popular ones. [[cryptography]] == Cryptography @@ -9,7 +10,7 @@ The OpenZeppelin Contracts provide a ton of useful utilities that you can use in xref:api:utils.adoc#ECDSA[`ECDSA`] provides functions for recovering and managing Ethereum account ECDSA signatures. These are often generated via https://web3js.readthedocs.io/en/v1.7.3/web3-eth.html#sign[`web3.eth.sign`], and are a 65 byte array (of type `bytes` in Solidity) arranged the following way: `[[v (1)], [r (32)], [s (32)]]`. -The data signer can be recovered with xref:api:utils.adoc#ECDSA-recover-bytes32-bytes-[`ECDSA.recover`], and its address compared to verify the signature. Most wallets will hash the data to sign and add the prefix '\x19Ethereum Signed Message:\n', so when attempting to recover the signer of an Ethereum signed message hash, you'll want to use xref:api:utils.adoc#MessageHashUtils-toEthSignedMessageHash-bytes32-[`toEthSignedMessageHash`]. +The data signer can be recovered with xref:api:utils.adoc#ECDSA-recover-bytes32-bytes-[`ECDSA.recover`], and its address compared to verify the signature. Most wallets will hash the data to sign and add the prefix `\x19Ethereum Signed Message:\n`, so when attempting to recover the signer of an Ethereum signed message hash, you'll want to use xref:api:utils.adoc#MessageHashUtils-toEthSignedMessageHash-bytes32-[`toEthSignedMessageHash`]. [source,solidity] ---- @@ -27,12 +28,18 @@ WARNING: Getting signature verification right is not trivial: make sure you full === Verifying Merkle Proofs +Developers can build a Merkle Tree off-chain, which allows for verifying that an element (leaf) is part of a set by using a Merkle Proof. This technique is widely used for creating whitelists (e.g. for airdrops) and other advanced use cases. + +TIP: OpenZeppelin Contracts provides a https://github.com/OpenZeppelin/merkle-tree[JavaScript library] for building trees off-chain and generating proofs. + xref:api:utils.adoc#MerkleProof[`MerkleProof`] provides: * xref:api:utils.adoc#MerkleProof-verify-bytes32---bytes32-bytes32-[`verify`] - can prove that some value is part of a https://en.wikipedia.org/wiki/Merkle_tree[Merkle tree]. * xref:api:utils.adoc#MerkleProof-multiProofVerify-bytes32-bytes32---bytes32---bool---[`multiProofVerify`] - can prove multiple values are part of a Merkle tree. +For an on-chain Merkle Tree, see the xref:api:utils.adoc#MerkleTree[`MerkleTree`] library. + [[introspection]] == Introspection @@ -98,6 +105,8 @@ contract MyContract { Easy! +TIP: While working with different data types that might require casting, you can use xref:api:utils.adoc#SafeCast[`SafeCast`] for type casting with added overflow checks. + [[structures]] == Structures @@ -108,12 +117,76 @@ Some use cases require more powerful data structures than arrays and mappings of - xref:api:utils.adoc#DoubleEndedQueue[`DoubleEndedQueue`]: Store items in a queue with `pop()` and `queue()` constant time operations. - xref:api:utils.adoc#EnumerableSet[`EnumerableSet`]: A https://en.wikipedia.org/wiki/Set_(abstract_data_type)[set] with enumeration capabilities. - xref:api:utils.adoc#EnumerableMap[`EnumerableMap`]: A `mapping` variant with enumeration capabilities. +- xref:api:utils.adoc#MerkleTree[`MerkleTree`]: An on-chain https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] with helper functions. The `Enumerable*` structures are similar to mappings in that they store and remove elements in constant time and don't allow for repeated entries, but they also support _enumeration_, which means you can easily query all stored entries both on and off-chain. +=== Building a Merkle Tree + +Building an on-chain Merkle Tree allow developers to keep track of the history of roots in a decentralized manner. For these cases, the xref:api:utils.adoc#MerkleTree[`MerkleTree`] includes a predefined structure with functions to manipulate the tree (e.g. pushing values or resetting the tree). + +The Merkle Tree does not keep track of the roots purposely, so that developers can choose their tracking mechanism. Setting up and using an Merkle Tree in Solidity is as simple as follows: + +[source,solidity] +---- +// NOTE: Functions are exposed without access control for demonstration purposes + +using MerkleTree for MerkleTree.Bytes32PushTree; +MerkleTree.Bytes32PushTree private _tree; + +function setup(uint8 _depth, bytes32 _zero) public /* onlyOwner */ { + root = _tree.setup(_depth, _zero); +} + +function push(bytes32 leaf) public /* onlyOwner */ { + (uint256 leafIndex, bytes32 currentRoot) = _tree.push(leaf); + // Store the new root. +} +---- + [[misc]] == Misc +=== Storage Slots + +Solidity allocates a storage pointer for each variable declared in a contract. However, there are cases when it's required to access storage pointers that can't be derived by using regular Solidity. +For those cases, the xref:api:utils.adoc#StorageSlot[`StorageSlot`] library allows for manipulating storage slots directly. + +[source,solidity] +---- +bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + +function _getImplementation() internal view returns (address) { + return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; +} + +function _setImplementation(address newImplementation) internal { + require(newImplementation.code.length > 0); + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; +} +---- + +WARNING: Manipulating storage slots directly is an advanced practice. Developers MUST make sure that the storage pointer is not colliding with other variables. + +The most common use cases for writing directly to storage slots include ERC-1967 for upgradeable slots and ERC-7201 for namespaced storage. The latter is guaranteed to not collide with other storage slots derived by Solidity. + +Users can leverage these standards using the xref:api:utils.adoc#SlotDerivation[`SlotDerivation`] library. + +[source,solidity] +---- +using SlotDerivation for bytes32; +string private constant _NAMESPACE = "" // eg. example.main +string private constant _UPGRADEABLE_NAMESPACE = "" // eg. eip1967.proxy.implementation + +function erc7201Pointer() internal view returns (bytes32) { + return _NAMESPACE.erc7201Slot(); +} + +function implementationPointer() internal view returns (bytes32) { + return _UPGRADEABLE_NAMESPACE.erc7201Slot(); +} +---- + === Base64 xref:api:utils.adoc#Base64[`Base64`] util allows you to transform `bytes32` data into its Base64 `string` representation. diff --git a/scripts/generate/templates/SlotDerivation.t.js b/scripts/generate/templates/SlotDerivation.t.js index 9a570053fa6..052ada89422 100644 --- a/scripts/generate/templates/SlotDerivation.t.js +++ b/scripts/generate/templates/SlotDerivation.t.js @@ -55,7 +55,6 @@ function testDeriveMapping${name}(${type} ${isValueType ? '' : 'memory'} key) pu // GENERATE module.exports = format( header.trimEnd(), - '// solhint-disable func-name-mixedcase', 'contract SlotDerivationTest is Test {', 'using SlotDerivation for bytes32;', '', diff --git a/test/utils/SlotDerivation.t.sol b/test/utils/SlotDerivation.t.sol index 2551ef22057..db9c789a481 100644 --- a/test/utils/SlotDerivation.t.sol +++ b/test/utils/SlotDerivation.t.sol @@ -7,7 +7,6 @@ import {Test} from "forge-std/Test.sol"; import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol"; -// solhint-disable func-name-mixedcase contract SlotDerivationTest is Test { using SlotDerivation for bytes32; From 21bded7fad947bcb0dd79b4058f99b589a136b62 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 27 Mar 2024 18:08:27 +0100 Subject: [PATCH 06/13] remove erc1967Slot from SlotDerivation.sol --- contracts/utils/SlotDerivation.sol | 10 ---------- test/utils/SlotDerivation.test.js | 6 +----- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/contracts/utils/SlotDerivation.sol b/contracts/utils/SlotDerivation.sol index 9a57a50a651..ff1654ba73f 100644 --- a/contracts/utils/SlotDerivation.sol +++ b/contracts/utils/SlotDerivation.sol @@ -28,16 +28,6 @@ pragma solidity ^0.8.20; * TIP: Consider using this library along with {StorageSlot}. */ library SlotDerivation { - /** - * @dev Derive an ERC-1967 slot from a string (namespace). - */ - function erc1967Slot(string memory namespace) internal pure returns (bytes32 slot) { - /// @solidity memory-safe-assembly - assembly { - slot := sub(keccak256(add(namespace, 0x20), mload(namespace)), 1) - } - } - /** * @dev Derive an ERC-7201 slot from a string (namespace). */ diff --git a/test/utils/SlotDerivation.test.js b/test/utils/SlotDerivation.test.js index b4d0c742fdd..22582b37572 100644 --- a/test/utils/SlotDerivation.test.js +++ b/test/utils/SlotDerivation.test.js @@ -1,7 +1,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { erc1967Slot, erc7201Slot } = require('../helpers/storage'); +const { erc7201Slot } = require('../helpers/storage'); const { generators } = require('../helpers/random'); async function fixture() { @@ -18,10 +18,6 @@ describe('SlotDerivation', function () { describe('namespaces', function () { const namespace = 'example.main'; - it('erc-1967', async function () { - expect(await this.mock.$erc1967Slot(namespace)).to.equal(erc1967Slot(namespace)); - }); - it('erc-7201', async function () { expect(await this.mock.$erc7201Slot(namespace)).to.equal(erc7201Slot(namespace)); }); From e28ecbcbd94b86dae03d07e6e03c06c78e64ae32 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 27 Mar 2024 18:09:31 +0100 Subject: [PATCH 07/13] update documentation --- contracts/utils/README.adoc | 2 +- contracts/utils/SlotDerivation.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index bf98a07a96d..83ba6241414 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -28,7 +28,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Base64}: On-chain base64 and base64URL encoding according to https://datatracker.ietf.org/doc/html/rfc4648[RFC-4648]. * {Strings}: Common operations for strings formatting. * {ShortString}: Library to encode (and decode) short strings into (or from) a single bytes32 slot for optimizing costs. Short strings are limited to 31 characters. - * {SlotDerivation}: Methods for deriving storage slot from ERC-1967 and ERC-7201 namespaces as well as from constructions such as mapping and arrays. + * {SlotDerivation}: Methods for deriving storage slot from ERC-7201 namespaces as well as from constructions such as mapping and arrays. * {StorageSlot}: Methods for accessing specific storage slots formatted as common primitive types. * {Multicall}: Abstract contract with an utility to allow batching together multiple calls in a single transaction. Useful for allowing EOAs to perform multiple operations at once. * {Context}: An utility for abstracting the sender and calldata in the current execution context. diff --git a/contracts/utils/SlotDerivation.sol b/contracts/utils/SlotDerivation.sol index ff1654ba73f..bd18ba8493f 100644 --- a/contracts/utils/SlotDerivation.sol +++ b/contracts/utils/SlotDerivation.sol @@ -20,7 +20,7 @@ pragma solidity ^0.8.20; * string private constant _NAMESPACE = "" // eg. OpenZeppelin.Slot * * function storagePointer() internal view returns (bytes32) { - * return _NAMESPACE.erc7201Slot(); // or erc1967Slot() + * return _NAMESPACE.erc7201Slot(); * } * } * ``` From f004ab759f683f49616d43f2c424faab09dec53e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 27 Mar 2024 18:11:06 +0100 Subject: [PATCH 08/13] add note about storage checks --- contracts/utils/SlotDerivation.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/utils/SlotDerivation.sol b/contracts/utils/SlotDerivation.sol index bd18ba8493f..6b539b1141a 100644 --- a/contracts/utils/SlotDerivation.sol +++ b/contracts/utils/SlotDerivation.sol @@ -26,6 +26,9 @@ pragma solidity ^0.8.20; * ``` * * TIP: Consider using this library along with {StorageSlot}. + * + * NOTE: This library provides a way to manipulate storage locations in a non-standard way. Tooling for checking + * upgrade safety will ignore the slots accessed through this library. */ library SlotDerivation { /** From 7fa858bfd9ee5e7a89be49b8dbb9dd6f6f34cbd9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 27 Mar 2024 18:15:06 +0100 Subject: [PATCH 09/13] more complete example --- contracts/utils/SlotDerivation.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contracts/utils/SlotDerivation.sol b/contracts/utils/SlotDerivation.sol index 6b539b1141a..c75941b5886 100644 --- a/contracts/utils/SlotDerivation.sol +++ b/contracts/utils/SlotDerivation.sol @@ -14,13 +14,18 @@ pragma solidity ^0.8.20; * ```solidity * contract Example { * // Add the library methods + * using StorageSlot for bytes32; * using SlotDerivation for bytes32; * * // Declare a namespace * string private constant _NAMESPACE = "" // eg. OpenZeppelin.Slot * - * function storagePointer() internal view returns (bytes32) { - * return _NAMESPACE.erc7201Slot(); + * function setValueInNamespace(uint256 key, address newValue) internal { + * _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value = newValue; + * } + * + * function getValueInNamespace(uint256 key) internal view returns (address) { + * return _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value; * } * } * ``` From 3fd00aba263624cd26d3633bc74b876c7bca96a3 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 27 Mar 2024 20:28:31 +0000 Subject: [PATCH 10/13] Update templates --- docs/modules/ROOT/pages/utilities.adoc | 9 ++------ scripts/generate/templates/SlotDerivation.js | 24 +++++++++----------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 83cf9083ff7..5e69b8ba385 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -168,23 +168,18 @@ function _setImplementation(address newImplementation) internal { WARNING: Manipulating storage slots directly is an advanced practice. Developers MUST make sure that the storage pointer is not colliding with other variables. -The most common use cases for writing directly to storage slots include ERC-1967 for upgradeable slots and ERC-7201 for namespaced storage. The latter is guaranteed to not collide with other storage slots derived by Solidity. +One of the most common use cases for writing directly to storage slots is ERC-7201 for namespaced storage, which is guaranteed to not collide with other storage slots derived by Solidity. -Users can leverage these standards using the xref:api:utils.adoc#SlotDerivation[`SlotDerivation`] library. +Users can leverage this standard using the xref:api:utils.adoc#SlotDerivation[`SlotDerivation`] library. [source,solidity] ---- using SlotDerivation for bytes32; string private constant _NAMESPACE = "" // eg. example.main -string private constant _UPGRADEABLE_NAMESPACE = "" // eg. eip1967.proxy.implementation function erc7201Pointer() internal view returns (bytes32) { return _NAMESPACE.erc7201Slot(); } - -function implementationPointer() internal view returns (bytes32) { - return _UPGRADEABLE_NAMESPACE.erc7201Slot(); -} ---- === Base64 diff --git a/scripts/generate/templates/SlotDerivation.js b/scripts/generate/templates/SlotDerivation.js index 57697d1266e..5afe9a33ded 100644 --- a/scripts/generate/templates/SlotDerivation.js +++ b/scripts/generate/templates/SlotDerivation.js @@ -15,32 +15,30 @@ pragma solidity ^0.8.20; * \`\`\`solidity * contract Example { * // Add the library methods + * using StorageSlot for bytes32; * using SlotDerivation for bytes32; * * // Declare a namespace * string private constant _NAMESPACE = "" // eg. OpenZeppelin.Slot - * - * function storagePointer() internal view returns (bytes32) { - * return _NAMESPACE.erc7201Slot(); // or erc1967Slot() + * + * function setValueInNamespace(uint256 key, address newValue) internal { + * _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value = newValue; + * } + * + * function getValueInNamespace(uint256 key) internal view returns (address) { + * return _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value; * } * } * \`\`\` * * TIP: Consider using this library along with {StorageSlot}. + * + * NOTE: This library provides a way to manipulate storage locations in a non-standard way. Tooling for checking + * upgrade safety will ignore the slots accessed through this library. */ `; const namespace = `\ -/** - * @dev Derive an ERC-1967 slot from a string (namespace). - */ -function erc1967Slot(string memory namespace) internal pure returns (bytes32 slot) { - /// @solidity memory-safe-assembly - assembly { - slot := sub(keccak256(add(namespace, 0x20), mload(namespace)), 1) - } -} - /** * @dev Derive an ERC-7201 slot from a string (namespace). */ From 0337d2544b6d748ee6b9571deb80587088855e0e Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 27 Mar 2024 20:36:12 +0000 Subject: [PATCH 11/13] Generate --- contracts/utils/Arrays.sol | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index d4fc2cd311c..ac4dcba2c53 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.20; -import {SlotDerivation} from "./SlotDerivation.sol"; import {StorageSlot} from "./StorageSlot.sol"; import {Math} from "./math/Math.sol"; @@ -12,7 +11,6 @@ import {Math} from "./math/Math.sol"; * @dev Collection of functions related to array types. */ library Arrays { - using SlotDerivation for bytes32; using StorageSlot for bytes32; /** @@ -381,11 +379,15 @@ library Arrays { */ function unsafeAccess(address[] storage arr, uint256 pos) internal pure returns (StorageSlot.AddressSlot storage) { bytes32 slot; + // We use assembly to calculate the storage slot of the element at index `pos` of the dynamic array `arr` + // following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays. + /// @solidity memory-safe-assembly assembly { - slot := arr.slot + mstore(0, arr.slot) + slot := add(keccak256(0, 0x20), pos) } - return slot.deriveArray().offset(pos).getAddressSlot(); + return slot.getAddressSlot(); } /** @@ -395,11 +397,15 @@ library Arrays { */ function unsafeAccess(bytes32[] storage arr, uint256 pos) internal pure returns (StorageSlot.Bytes32Slot storage) { bytes32 slot; + // We use assembly to calculate the storage slot of the element at index `pos` of the dynamic array `arr` + // following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays. + /// @solidity memory-safe-assembly assembly { - slot := arr.slot + mstore(0, arr.slot) + slot := add(keccak256(0, 0x20), pos) } - return slot.deriveArray().offset(pos).getBytes32Slot(); + return slot.getBytes32Slot(); } /** @@ -409,11 +415,15 @@ library Arrays { */ function unsafeAccess(uint256[] storage arr, uint256 pos) internal pure returns (StorageSlot.Uint256Slot storage) { bytes32 slot; + // We use assembly to calculate the storage slot of the element at index `pos` of the dynamic array `arr` + // following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays. + /// @solidity memory-safe-assembly assembly { - slot := arr.slot + mstore(0, arr.slot) + slot := add(keccak256(0, 0x20), pos) } - return slot.deriveArray().offset(pos).getUint256Slot(); + return slot.getUint256Slot(); } /** From f849734460a75fbcbfc988a7945b75a361602b55 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 27 Mar 2024 21:42:49 +0100 Subject: [PATCH 12/13] re-enable using SlotDerivation in Arrays.sol for unsafeAccess --- contracts/utils/Arrays.sol | 25 +++++++------------------ scripts/generate/templates/Arrays.js | 17 +++++++---------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index ac4dcba2c53..8267fd76c29 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; +import {SlotDerivation} from "./SlotDerivation.sol"; import {StorageSlot} from "./StorageSlot.sol"; import {Math} from "./math/Math.sol"; @@ -379,15 +380,11 @@ library Arrays { */ function unsafeAccess(address[] storage arr, uint256 pos) internal pure returns (StorageSlot.AddressSlot storage) { bytes32 slot; - // We use assembly to calculate the storage slot of the element at index `pos` of the dynamic array `arr` - // following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays. - /// @solidity memory-safe-assembly assembly { - mstore(0, arr.slot) - slot := add(keccak256(0, 0x20), pos) + slot := arr.slot } - return slot.getAddressSlot(); + return slot.deriveArray().offset(pos).getAddressSlot(); } /** @@ -397,15 +394,11 @@ library Arrays { */ function unsafeAccess(bytes32[] storage arr, uint256 pos) internal pure returns (StorageSlot.Bytes32Slot storage) { bytes32 slot; - // We use assembly to calculate the storage slot of the element at index `pos` of the dynamic array `arr` - // following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays. - /// @solidity memory-safe-assembly assembly { - mstore(0, arr.slot) - slot := add(keccak256(0, 0x20), pos) + slot := arr.slot } - return slot.getBytes32Slot(); + return slot.deriveArray().offset(pos).getBytes32Slot(); } /** @@ -415,15 +408,11 @@ library Arrays { */ function unsafeAccess(uint256[] storage arr, uint256 pos) internal pure returns (StorageSlot.Uint256Slot storage) { bytes32 slot; - // We use assembly to calculate the storage slot of the element at index `pos` of the dynamic array `arr` - // following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays. - /// @solidity memory-safe-assembly assembly { - mstore(0, arr.slot) - slot := add(keccak256(0, 0x20), pos) + slot := arr.slot } - return slot.getUint256Slot(); + return slot.deriveArray().offset(pos).getUint256Slot(); } /** diff --git a/scripts/generate/templates/Arrays.js b/scripts/generate/templates/Arrays.js index bd0daa1f5bf..b89c6ae2e1e 100644 --- a/scripts/generate/templates/Arrays.js +++ b/scripts/generate/templates/Arrays.js @@ -5,6 +5,7 @@ const { TYPES } = require('./Arrays.opts'); const header = `\ pragma solidity ^0.8.20; +import {SlotDerivation} from "./SlotDerivation.sol"; import {StorageSlot} from "./StorageSlot.sol"; import {Math} from "./math/Math.sol"; @@ -327,16 +328,12 @@ const unsafeAccessStorage = type => ` function unsafeAccess(${type}[] storage arr, uint256 pos) internal pure returns (StorageSlot.${capitalize( type, )}Slot storage) { - bytes32 slot; - // We use assembly to calculate the storage slot of the element at index \`pos\` of the dynamic array \`arr\` - // following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays. - - /// @solidity memory-safe-assembly - assembly { - mstore(0, arr.slot) - slot := add(keccak256(0, 0x20), pos) - } - return slot.get${capitalize(type)}Slot(); + bytes32 slot; + /// @solidity memory-safe-assembly + assembly { + slot := arr.slot + } + return slot.deriveArray().offset(pos).get${capitalize(type)}Slot(); }`; const unsafeAccessMemory = type => ` From 3b60f2d67c7ad9f205742bd4b281a348fcf08c82 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 27 Mar 2024 21:45:13 +0100 Subject: [PATCH 13/13] missing 'using for' statement --- contracts/utils/Arrays.sol | 1 + scripts/generate/templates/Arrays.js | 1 + 2 files changed, 2 insertions(+) diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index 8267fd76c29..d4fc2cd311c 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -12,6 +12,7 @@ import {Math} from "./math/Math.sol"; * @dev Collection of functions related to array types. */ library Arrays { + using SlotDerivation for bytes32; using StorageSlot for bytes32; /** diff --git a/scripts/generate/templates/Arrays.js b/scripts/generate/templates/Arrays.js index b89c6ae2e1e..bc279466b7f 100644 --- a/scripts/generate/templates/Arrays.js +++ b/scripts/generate/templates/Arrays.js @@ -365,6 +365,7 @@ function unsafeSetLength(${type}[] storage array, uint256 len) internal { module.exports = format( header.trimEnd(), 'library Arrays {', + 'using SlotDerivation for bytes32;', 'using StorageSlot for bytes32;', // sorting, comparator, helpers and internal sort('bytes32'),