From dd1e8988ab44c60de1dcb797b766d47bb98ecbd8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 30 May 2024 17:16:12 +0200 Subject: [PATCH] Generate already lint code from procedural generation (#5060) --- scripts/generate/run.js | 6 +- scripts/generate/templates/Arrays.js | 149 +++++++++--------- scripts/generate/templates/Checkpoints.js | 49 +++--- scripts/generate/templates/Checkpoints.t.js | 48 +++--- scripts/generate/templates/EnumerableMap.js | 30 ++-- scripts/generate/templates/EnumerableSet.js | 9 +- scripts/generate/templates/SafeCast.js | 71 +++++---- scripts/generate/templates/SlotDerivation.js | 74 +++++---- .../generate/templates/SlotDerivation.t.js | 118 +++++++------- scripts/generate/templates/StorageSlot.js | 50 +++--- scripts/generate/templates/StorageSlotMock.js | 21 ++- 11 files changed, 315 insertions(+), 310 deletions(-) diff --git a/scripts/generate/run.js b/scripts/generate/run.js index 1b7d81094..f6cf21f4e 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -const cp = require('child_process'); +// const cp = require('child_process'); const fs = require('fs'); const path = require('path'); const format = require('./format-lines'); @@ -23,11 +23,11 @@ function generateFromTemplate(file, template, outputPrefix = '') { ...(version ? [version + ` (${file})`] : []), `// This file was procedurally generated from ${input}.`, '', - require(template), + require(template).trimEnd(), ); fs.writeFileSync(output, content); - cp.execFileSync('prettier', ['--write', output]); + // cp.execFileSync('prettier', ['--write', output]); } // Contracts diff --git a/scripts/generate/templates/Arrays.js b/scripts/generate/templates/Arrays.js index 6166d6f6d..30a6e069a 100644 --- a/scripts/generate/templates/Arrays.js +++ b/scripts/generate/templates/Arrays.js @@ -15,39 +15,39 @@ import {Math} from "./math/Math.sol"; `; const sort = type => `\ - /** - * @dev Sort an array of ${type} (in memory) following the provided comparator function. - * - * This function does the sorting "in place", meaning that it overrides the input. The object is returned for - * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array. - * - * NOTE: this function's cost is \`O(n · log(n))\` in average and \`O(n²)\` in the worst case, with n the length of the - * array. Using it in view functions that are executed through \`eth_call\` is safe, but one should be very careful - * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may - * consume more gas than is available in a block, leading to potential DoS. - */ - function sort( - ${type}[] memory array, - function(${type}, ${type}) pure returns (bool) comp - ) internal pure returns (${type}[] memory) { - ${ - type === 'bytes32' - ? '_quickSort(_begin(array), _end(array), comp);' - : 'sort(_castToBytes32Array(array), _castToBytes32Comp(comp));' - } - return array; +/** + * @dev Sort an array of ${type} (in memory) following the provided comparator function. + * + * This function does the sorting "in place", meaning that it overrides the input. The object is returned for + * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array. + * + * NOTE: this function's cost is \`O(n · log(n))\` in average and \`O(n²)\` in the worst case, with n the length of the + * array. Using it in view functions that are executed through \`eth_call\` is safe, but one should be very careful + * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may + * consume more gas than is available in a block, leading to potential DoS. + */ +function sort( + ${type}[] memory array, + function(${type}, ${type}) pure returns (bool) comp +) internal pure returns (${type}[] memory) { + ${ + type === 'bytes32' + ? '_quickSort(_begin(array), _end(array), comp);' + : 'sort(_castToBytes32Array(array), _castToBytes32Comp(comp));' } + return array; +} - /** - * @dev Variant of {sort} that sorts an array of ${type} in increasing order. - */ - function sort(${type}[] memory array) internal pure returns (${type}[] memory) { - ${type === 'bytes32' ? 'sort(array, _defaultComp);' : 'sort(_castToBytes32Array(array), _defaultComp);'} - return array; - } +/** + * @dev Variant of {sort} that sorts an array of ${type} in increasing order. + */ +function sort(${type}[] memory array) internal pure returns (${type}[] memory) { + ${type === 'bytes32' ? 'sort(array, _defaultComp);' : 'sort(_castToBytes32Array(array), _defaultComp);'} + return array; +} `; -const quickSort = ` +const quickSort = `\ /** * @dev Performs a quick sort of a segment of memory. The segment sorted starts at \`begin\` (inclusive), and stops * at end (exclusive). Sorting follows the \`comp\` comparator. @@ -123,34 +123,34 @@ function _swap(uint256 ptr1, uint256 ptr2) private pure { } `; -const defaultComparator = ` - /// @dev Comparator for sorting arrays in increasing order. - function _defaultComp(bytes32 a, bytes32 b) private pure returns (bool) { - return a < b; - } +const defaultComparator = `\ +/// @dev Comparator for sorting arrays in increasing order. +function _defaultComp(bytes32 a, bytes32 b) private pure returns (bool) { + return a < b; +} `; const castArray = type => `\ - /// @dev Helper: low level cast ${type} memory array to uint256 memory array - function _castToBytes32Array(${type}[] memory input) private pure returns (bytes32[] memory output) { - assembly { - output := input - } +/// @dev Helper: low level cast ${type} memory array to uint256 memory array +function _castToBytes32Array(${type}[] memory input) private pure returns (bytes32[] memory output) { + assembly { + output := input } +} `; const castComparator = type => `\ - /// @dev Helper: low level cast ${type} comp function to bytes32 comp function - function _castToBytes32Comp( - function(${type}, ${type}) pure returns (bool) input - ) private pure returns (function(bytes32, bytes32) pure returns (bool) output) { - assembly { - output := input - } +/// @dev Helper: low level cast ${type} comp function to bytes32 comp function +function _castToBytes32Comp( + function(${type}, ${type}) pure returns (bool) input +) private pure returns (function(bytes32, bytes32) pure returns (bool) output) { + assembly { + output := input } +} `; -const search = ` +const search = `\ /** * @dev Searches a sorted \`array\` and returns the first index that contains * a value greater or equal to \`element\`. If no such index exists (i.e. all @@ -319,12 +319,12 @@ function upperBoundMemory(uint256[] memory array, uint256 element) internal pure } `; -const unsafeAccessStorage = type => ` +const unsafeAccessStorage = type => `\ /** -* @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check. -* -* WARNING: Only use if you are certain \`pos\` is lower than the array length. -*/ + * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check. + * + * WARNING: Only use if you are certain \`pos\` is lower than the array length. + */ function unsafeAccess(${type}[] storage arr, uint256 pos) internal pure returns (StorageSlot.${capitalize( type, )}Slot storage) { @@ -334,9 +334,10 @@ function unsafeAccess(${type}[] storage arr, uint256 pos) internal pure returns slot := arr.slot } return slot.deriveArray().offset(pos).get${capitalize(type)}Slot(); -}`; +} +`; -const unsafeAccessMemory = type => ` +const unsafeAccessMemory = type => `\ /** * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check. * @@ -349,7 +350,7 @@ function unsafeMemoryAccess(${type}[] memory arr, uint256 pos) internal pure ret } `; -const unsafeSetLength = type => ` +const unsafeSetLength = type => `\ /** * @dev Helper to set the length of an dynamic array. Directly writing to \`.length\` is forbidden. * @@ -360,26 +361,32 @@ function unsafeSetLength(${type}[] storage array, uint256 len) internal { assembly { sstore(array.slot, len) } -}`; +} +`; // GENERATE module.exports = format( header.trimEnd(), 'library Arrays {', - 'using SlotDerivation for bytes32;', - 'using StorageSlot for bytes32;', - // sorting, comparator, helpers and internal - sort('bytes32'), - TYPES.filter(type => type !== 'bytes32').map(sort), - quickSort, - defaultComparator, - TYPES.filter(type => type !== 'bytes32').map(castArray), - TYPES.filter(type => type !== 'bytes32').map(castComparator), - // lookup - search, - // unsafe (direct) storage and memory access - TYPES.map(unsafeAccessStorage), - TYPES.map(unsafeAccessMemory), - TYPES.map(unsafeSetLength), + format( + [].concat( + 'using SlotDerivation for bytes32;', + 'using StorageSlot for bytes32;', + '', + // sorting, comparator, helpers and internal + sort('bytes32'), + TYPES.filter(type => type !== 'bytes32').map(sort), + quickSort, + defaultComparator, + TYPES.filter(type => type !== 'bytes32').map(castArray), + TYPES.filter(type => type !== 'bytes32').map(castComparator), + // lookup + search, + // unsafe (direct) storage and memory access + TYPES.map(unsafeAccessStorage), + TYPES.map(unsafeAccessMemory), + TYPES.map(unsafeSetLength), + ), + ).trimEnd(), '}', ); diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js index 0a677e27f..2291bab57 100644 --- a/scripts/generate/templates/Checkpoints.js +++ b/scripts/generate/templates/Checkpoints.js @@ -17,10 +17,10 @@ import {Math} from "../math/Math.sol"; `; const errors = `\ - /** - * @dev A value was attempted to be inserted on a past checkpoint. - */ - error CheckpointUnorderedInsertion(); +/** + * @dev A value was attempted to be inserted on a past checkpoint. + */ +error CheckpointUnorderedInsertion(); `; const template = opts => `\ @@ -37,15 +37,11 @@ struct ${opts.checkpointTypeName} { * @dev Pushes a (\`key\`, \`value\`) pair into a ${opts.historyTypeName} so that it is stored as the checkpoint. * * Returns previous value and new value. - * + * * IMPORTANT: Never accept \`key\` as a user input, since an arbitrary \`type(${opts.keyTypeName}).max\` key set will disable the * library. */ -function push( - ${opts.historyTypeName} storage self, - ${opts.keyTypeName} key, - ${opts.valueTypeName} value -) internal returns (${opts.valueTypeName}, ${opts.valueTypeName}) { +function push(${opts.historyTypeName} storage self, ${opts.keyTypeName} key, ${opts.valueTypeName} value) internal returns (${opts.valueTypeName}, ${opts.valueTypeName}) { return _insert(self.${opts.checkpointFieldName}, key, value); } @@ -108,15 +104,7 @@ function latest(${opts.historyTypeName} storage self) internal view returns (${o * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value * in the most recent checkpoint. */ -function latestCheckpoint(${opts.historyTypeName} storage self) - internal - view - returns ( - bool exists, - ${opts.keyTypeName} ${opts.keyFieldName}, - ${opts.valueTypeName} ${opts.valueFieldName} - ) -{ +function latestCheckpoint(${opts.historyTypeName} storage self) internal view returns (bool exists, ${opts.keyTypeName} ${opts.keyFieldName}, ${opts.valueTypeName} ${opts.valueFieldName}) { uint256 pos = self.${opts.checkpointFieldName}.length; if (pos == 0) { return (false, 0, 0); @@ -144,11 +132,7 @@ function at(${opts.historyTypeName} storage self, uint32 pos) internal view retu * @dev Pushes a (\`key\`, \`value\`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, * or by updating the last one. */ -function _insert( - ${opts.checkpointTypeName}[] storage self, - ${opts.keyTypeName} key, - ${opts.valueTypeName} value -) private returns (${opts.valueTypeName}, ${opts.valueTypeName}) { +function _insert(${opts.checkpointTypeName}[] storage self, ${opts.keyTypeName} key, ${opts.valueTypeName} value) private returns (${opts.valueTypeName}, ${opts.valueTypeName}) { uint256 pos = self.length; if (pos > 0) { @@ -225,11 +209,10 @@ function _lowerBinaryLookup( /** * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. */ -function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos) - private - pure - returns (${opts.checkpointTypeName} storage result) -{ +function _unsafeAccess( + ${opts.checkpointTypeName}[] storage self, + uint256 pos +) private pure returns (${opts.checkpointTypeName} storage result) { assembly { mstore(0, self.slot) result.slot := add(keccak256(0, 0x20), pos) @@ -242,7 +225,11 @@ function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos) module.exports = format( header.trimEnd(), 'library Checkpoints {', - errors, - OPTS.flatMap(opts => template(opts)), + format( + [].concat( + errors, + OPTS.map(opts => template(opts)), + ), + ).trimEnd(), '}', ); diff --git a/scripts/generate/templates/Checkpoints.t.js b/scripts/generate/templates/Checkpoints.t.js index baea5c315..dd564e59b 100644 --- a/scripts/generate/templates/Checkpoints.t.js +++ b/scripts/generate/templates/Checkpoints.t.js @@ -22,18 +22,13 @@ uint8 internal constant _KEY_MAX_GAP = 64; Checkpoints.${opts.historyTypeName} internal _ckpts; // helpers -function _bound${capitalize(opts.keyTypeName)}( - ${opts.keyTypeName} x, - ${opts.keyTypeName} min, - ${opts.keyTypeName} max -) internal pure returns (${opts.keyTypeName}) { +function _bound${capitalize(opts.keyTypeName)}(${opts.keyTypeName} x, ${opts.keyTypeName} min, ${ + opts.keyTypeName +} max) internal pure returns (${opts.keyTypeName}) { return SafeCast.to${capitalize(opts.keyTypeName)}(bound(uint256(x), uint256(min), uint256(max))); } -function _prepareKeys( - ${opts.keyTypeName}[] memory keys, - ${opts.keyTypeName} maxSpread -) internal pure { +function _prepareKeys(${opts.keyTypeName}[] memory keys, ${opts.keyTypeName} maxSpread) internal pure { ${opts.keyTypeName} lastKey = 0; for (uint256 i = 0; i < keys.length; ++i) { ${opts.keyTypeName} key = _bound${capitalize(opts.keyTypeName)}(keys[i], lastKey, lastKey + maxSpread); @@ -42,11 +37,7 @@ function _prepareKeys( } } -function _assertLatestCheckpoint( - bool exist, - ${opts.keyTypeName} key, - ${opts.valueTypeName} value -) internal { +function _assertLatestCheckpoint(bool exist, ${opts.keyTypeName} key, ${opts.valueTypeName} value) internal { (bool _exist, ${opts.keyTypeName} _key, ${opts.valueTypeName} _value) = _ckpts.latestCheckpoint(); assertEq(_exist, exist); assertEq(_key, key); @@ -54,11 +45,9 @@ function _assertLatestCheckpoint( } // tests -function testPush( - ${opts.keyTypeName}[] memory keys, - ${opts.valueTypeName}[] memory values, - ${opts.keyTypeName} pastKey -) public { +function testPush(${opts.keyTypeName}[] memory keys, ${opts.valueTypeName}[] memory values, ${ + opts.keyTypeName +} pastKey) public { vm.assume(values.length > 0 && values.length <= keys.length); _prepareKeys(keys, _KEY_MAX_GAP); @@ -71,7 +60,7 @@ function testPush( for (uint256 i = 0; i < keys.length; ++i) { ${opts.keyTypeName} key = keys[i]; ${opts.valueTypeName} value = values[i % values.length]; - if (i > 0 && key == keys[i-1]) ++duplicates; + if (i > 0 && key == keys[i - 1]) ++duplicates; // push _ckpts.push(key, value); @@ -95,14 +84,12 @@ function testPush( // used to test reverts function push(${opts.keyTypeName} key, ${opts.valueTypeName} value) external { - _ckpts.push(key, value); + _ckpts.push(key, value); } -function testLookup( - ${opts.keyTypeName}[] memory keys, - ${opts.valueTypeName}[] memory values, - ${opts.keyTypeName} lookup -) public { +function testLookup(${opts.keyTypeName}[] memory keys, ${opts.valueTypeName}[] memory values, ${ + opts.keyTypeName +} lookup) public { vm.assume(values.length > 0 && values.length <= keys.length); _prepareKeys(keys, _KEY_MAX_GAP); @@ -124,7 +111,7 @@ function testLookup( upper = value; } // find the first key that is not smaller than the lookup key - if (key >= lookup && (i == 0 || keys[i-1] < lookup)) { + if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) { lowerKey = key; } if (key == lowerKey) { @@ -142,5 +129,10 @@ function testLookup( // GENERATE module.exports = format( header, - ...OPTS.flatMap(opts => [`contract Checkpoints${opts.historyTypeName}Test is Test {`, [template(opts)], '}']), + ...OPTS.flatMap(opts => [ + `contract Checkpoints${opts.historyTypeName}Test is Test {`, + [template(opts).trimEnd()], + '}', + '', + ]), ); diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index cb77557f0..bcc8edd76 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -54,7 +54,7 @@ import {EnumerableSet} from "./EnumerableSet.sol"; `; /* eslint-enable max-len */ -const defaultMap = () => `\ +const defaultMap = `\ // To implement this library for multiple types with as little code repetition as possible, we write it in // terms of a generic Map type with bytes32 keys and values. The Map implementation uses private functions, // and user-facing implementations such as \`UintToAddressMap\` are just wrappers around the underlying Map. @@ -78,11 +78,7 @@ struct Bytes32ToBytes32Map { * Returns true if the key was added to the map, that is if it was not * already present. */ -function set( - Bytes32ToBytes32Map storage map, - bytes32 key, - bytes32 value -) internal returns (bool) { +function set(Bytes32ToBytes32Map storage map, bytes32 key, bytes32 value) internal returns (bool) { map._values[key] = value; return map._keys.add(key); } @@ -148,7 +144,7 @@ function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view retu */ function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) { bytes32 value = map._values[key]; - if(value == 0 && !contains(map, key)) { + if (value == 0 && !contains(map, key)) { revert EnumerableMapNonexistentKey(key); } return value; @@ -181,11 +177,7 @@ struct ${name} { * Returns true if the key was added to the map, that is if it was not * already present. */ -function set( - ${name} storage map, - ${keyType} key, - ${valueType} value -) internal returns (bool) { +function set(${name} storage map, ${keyType} key, ${valueType} value) internal returns (bool) { return set(map._inner, ${toBytes32(keyType, 'key')}, ${toBytes32(valueType, 'value')}); } @@ -271,11 +263,13 @@ function keys(${name} storage map) internal view returns (${keyType}[] memory) { module.exports = format( header.trimEnd(), 'library EnumerableMap {', - [ - 'using EnumerableSet for EnumerableSet.Bytes32Set;', - '', - defaultMap(), - TYPES.map(details => customMap(details).trimEnd()).join('\n\n'), - ], + format( + [].concat( + 'using EnumerableSet for EnumerableSet.Bytes32Set;', + '', + defaultMap, + TYPES.map(details => customMap(details)), + ), + ).trimEnd(), '}', ); diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index e25242cbb..d1878775a 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -43,7 +43,7 @@ pragma solidity ^0.8.20; `; /* eslint-enable max-len */ -const defaultSet = () => `\ +const defaultSet = `\ // To implement this library for multiple types with as little code // repetition as possible, we write it in terms of a generic Set type with // bytes32 values. @@ -240,6 +240,11 @@ function values(${name} storage set) internal view returns (${type}[] memory) { module.exports = format( header.trimEnd(), 'library EnumerableSet {', - [defaultSet(), TYPES.map(details => customSet(details).trimEnd()).join('\n\n')], + format( + [].concat( + defaultSet, + TYPES.map(details => customSet(details)), + ), + ).trimEnd(), '}', ); diff --git a/scripts/generate/templates/SafeCast.js b/scripts/generate/templates/SafeCast.js index a10ee75c9..8a9ce9fc7 100644 --- a/scripts/generate/templates/SafeCast.js +++ b/scripts/generate/templates/SafeCast.js @@ -21,25 +21,25 @@ pragma solidity ^0.8.20; `; const errors = `\ - /** - * @dev Value doesn't fit in an uint of \`bits\` size. - */ - error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); - - /** - * @dev An int value doesn't fit in an uint of \`bits\` size. - */ - error SafeCastOverflowedIntToUint(int256 value); - - /** - * @dev Value doesn't fit in an int of \`bits\` size. - */ - error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); - - /** - * @dev An uint value doesn't fit in an int of \`bits\` size. - */ - error SafeCastOverflowedUintToInt(uint256 value); +/** + * @dev Value doesn't fit in an uint of \`bits\` size. + */ +error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); + +/** + * @dev An int value doesn't fit in an uint of \`bits\` size. + */ +error SafeCastOverflowedIntToUint(int256 value); + +/** + * @dev Value doesn't fit in an int of \`bits\` size. + */ +error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); + +/** + * @dev An uint value doesn't fit in an int of \`bits\` size. + */ +error SafeCastOverflowedUintToInt(uint256 value); `; const toUintDownCast = length => `\ @@ -55,7 +55,7 @@ const toUintDownCast = length => `\ */ function toUint${length}(uint256 value) internal pure returns (uint${length}) { if (value > type(uint${length}).max) { - revert SafeCastOverflowedUintDowncast(${length}, value); + revert SafeCastOverflowedUintDowncast(${length}, value); } return uint${length}(value); } @@ -77,7 +77,7 @@ const toIntDownCast = length => `\ function toInt${length}(int256 value) internal pure returns (int${length} downcasted) { downcasted = int${length}(value); if (downcasted != value) { - revert SafeCastOverflowedIntDowncast(${length}, value); + revert SafeCastOverflowedIntDowncast(${length}, value); } } `; @@ -94,7 +94,7 @@ const toInt = length => `\ function toInt${length}(uint${length} value) internal pure returns (int${length}) { // Note: Unsafe cast below is okay because \`type(int${length}).max\` is guaranteed to be positive if (value > uint${length}(type(int${length}).max)) { - revert SafeCastOverflowedUintToInt(value); + revert SafeCastOverflowedUintToInt(value); } return int${length}(value); } @@ -110,29 +110,30 @@ const toUint = length => `\ */ function toUint${length}(int${length} value) internal pure returns (uint${length}) { if (value < 0) { - revert SafeCastOverflowedIntToUint(value); + revert SafeCastOverflowedIntToUint(value); } return uint${length}(value); } `; -const boolToUint = ` - /** - * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump. - */ - function toUint(bool b) internal pure returns (uint256 u) { - /// @solidity memory-safe-assembly - assembly { - u := iszero(iszero(b)) - } - } +const boolToUint = `\ +/** + * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump. + */ +function toUint(bool b) internal pure returns (uint256 u) { + /// @solidity memory-safe-assembly + assembly { + u := iszero(iszero(b)) + } +} `; // GENERATE module.exports = format( header.trimEnd(), 'library SafeCast {', - errors, - [...LENGTHS.map(toUintDownCast), toUint(256), ...LENGTHS.map(toIntDownCast), toInt(256), boolToUint], + format( + [].concat(errors, LENGTHS.map(toUintDownCast), toUint(256), LENGTHS.map(toIntDownCast), toInt(256), boolToUint), + ).trimEnd(), '}', ); diff --git a/scripts/generate/templates/SlotDerivation.js b/scripts/generate/templates/SlotDerivation.js index 5afe9a33d..d8ab35d6f 100644 --- a/scripts/generate/templates/SlotDerivation.js +++ b/scripts/generate/templates/SlotDerivation.js @@ -10,7 +10,7 @@ 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 { @@ -30,9 +30,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. */ @@ -43,11 +43,11 @@ const namespace = `\ * @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)) - } + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1)) + slot := and(keccak256(0x00, 0x20), not(0xff)) + } } `; @@ -56,20 +56,20 @@ 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); - } + 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) - } + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, slot) + result := keccak256(0x00, 0x20) + } } `; @@ -78,12 +78,12 @@ 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) - } + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } } `; @@ -92,16 +92,16 @@ 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) - } + /// @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) + } } `; @@ -109,8 +109,12 @@ function deriveMapping(bytes32 slot, ${type} memory key) internal pure returns ( module.exports = format( header.trimEnd(), 'library SlotDerivation {', - namespace, - array, - TYPES.map(type => (type.isValueType ? mapping(type) : mapping2(type))), + format( + [].concat( + namespace, + array, + TYPES.map(type => (type.isValueType ? mapping(type) : mapping2(type))), + ), + ).trimEnd(), '}', ); diff --git a/scripts/generate/templates/SlotDerivation.t.js b/scripts/generate/templates/SlotDerivation.t.js index 7e5ae5ea9..dc7b07ff4 100644 --- a/scripts/generate/templates/SlotDerivation.t.js +++ b/scripts/generate/templates/SlotDerivation.t.js @@ -14,31 +14,31 @@ const array = `\ bytes[] private _array; function symbolicDeriveArray(uint256 length, uint256 offset) public { - vm.assume(length > 0); - vm.assume(offset < length); - _assertDeriveArray(length, offset); + vm.assume(length > 0); + vm.assume(offset < length); + _assertDeriveArray(length, offset); } function testDeriveArray(uint256 length, uint256 offset) public { - length = bound(length, 1, type(uint256).max); - offset = bound(offset, 0, length - 1); - _assertDeriveArray(length, offset); + length = bound(length, 1, type(uint256).max); + offset = bound(offset, 0, length - 1); + _assertDeriveArray(length, offset); } function _assertDeriveArray(uint256 length, uint256 offset) public { - 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); + 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); } `; @@ -46,18 +46,18 @@ const mapping = ({ type, name }) => `\ mapping(${type} => bytes) private _${type}Mapping; function testSymbolicDeriveMapping${name}(${type} 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); + bytes32 baseSlot; + assembly { + baseSlot := _${type}Mapping.slot + } + + bytes storage derived = _${type}Mapping[key]; + bytes32 derivedSlot; + assembly { + derivedSlot := derived.slot + } + + assertEq(baseSlot.deriveMapping(key), derivedSlot); } `; @@ -65,45 +65,49 @@ const boundedMapping = ({ type, name }) => `\ mapping(${type} => bytes) private _${type}Mapping; function testDeriveMapping${name}(${type} memory key) public { - _assertDeriveMapping${name}(key); + _assertDeriveMapping${name}(key); } function symbolicDeriveMapping${name}() public { - _assertDeriveMapping${name}(svm.create${name}(256, "DeriveMapping${name}Input")); + _assertDeriveMapping${name}(svm.create${name}(256, "DeriveMapping${name}Input")); } function _assertDeriveMapping${name}(${type} memory key) internal { - bytes32 baseSlot; - assembly { - baseSlot := _${type}Mapping.slot - } - - bytes storage derived = _${type}Mapping[key]; - bytes32 derivedSlot; - assembly { - derivedSlot := derived.slot - } - - assertEq(baseSlot.deriveMapping(key), derivedSlot); + 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(), + header, 'contract SlotDerivationTest is Test, SymTest {', - 'using SlotDerivation for bytes32;', - '', - array, - TYPES.flatMap(type => + format( [].concat( - type, - (type.variants ?? []).map(variant => ({ - type: variant, - name: capitalize(variant), - isValueType: type.isValueType, - })), + 'using SlotDerivation for bytes32;', + '', + array, + TYPES.flatMap(type => + [].concat( + type, + (type.variants ?? []).map(variant => ({ + type: variant, + name: capitalize(variant), + isValueType: type.isValueType, + })), + ), + ).map(type => (type.isValueType ? mapping(type) : boundedMapping(type))), ), - ).map(type => (type.isValueType ? mapping(type) : boundedMapping(type))), + ).trimEnd(), '}', ); diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index 54cb4ec26..aa0c325b7 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -53,7 +53,7 @@ pragma solidity ^0.8.24; const struct = ({ type, name }) => `\ struct ${name}Slot { - ${type} value; + ${type} value; } `; @@ -62,10 +62,10 @@ const get = ({ name }) => `\ * @dev Returns an \`${name}Slot\` with member \`value\` located at \`slot\`. */ function get${name}Slot(bytes32 slot) internal pure returns (${name}Slot storage r) { - /// @solidity memory-safe-assembly - assembly { - r.slot := slot - } + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } } `; @@ -74,10 +74,10 @@ const getStorage = ({ type, name }) => `\ * @dev Returns an \`${name}Slot\` representation of the ${type} storage pointer \`store\`. */ function get${name}Slot(${type} storage store) internal pure returns (${name}Slot storage r) { - /// @solidity memory-safe-assembly - assembly { - r.slot := store.slot - } + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } } `; @@ -86,11 +86,12 @@ const udvt = ({ type, name }) => `\ * @dev UDVT that represent a slot holding a ${type}. */ type ${name}SlotType is bytes32; + /** * @dev Cast an arbitrary slot to a ${name}SlotType. */ function as${name}(bytes32 slot) internal pure returns (${name}SlotType) { - return ${name}SlotType.wrap(slot); + return ${name}SlotType.wrap(slot); } `; @@ -99,19 +100,20 @@ const transient = ({ type, name }) => `\ * @dev Load the value held at location \`slot\` in transient storage. */ function tload(${name}SlotType slot) internal view returns (${type} value) { - /// @solidity memory-safe-assembly - assembly { - value := tload(slot) - } + /// @solidity memory-safe-assembly + assembly { + value := tload(slot) + } } + /** * @dev Store \`value\` at location \`slot\` in transient storage. */ function tstore(${name}SlotType slot, ${type} value) internal { - /// @solidity memory-safe-assembly - assembly { - tstore(slot, value) - } + /// @solidity memory-safe-assembly + assembly { + tstore(slot, value) + } } `; @@ -119,9 +121,13 @@ function tstore(${name}SlotType slot, ${type} value) internal { module.exports = format( header.trimEnd(), 'library StorageSlot {', - TYPES.map(type => struct(type)), - TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)]), - TYPES.filter(type => type.isValueType).map(type => udvt(type)), - TYPES.filter(type => type.isValueType).map(type => transient(type)), + format( + [].concat( + TYPES.map(type => struct(type)), + TYPES.flatMap(type => [get(type), !type.isValueType && getStorage(type)].filter(Boolean)), + TYPES.filter(type => type.isValueType).map(type => udvt(type)), + TYPES.filter(type => type.isValueType).map(type => transient(type)), + ), + ).trimEnd(), '}', ); diff --git a/scripts/generate/templates/StorageSlotMock.js b/scripts/generate/templates/StorageSlotMock.js index 62506e172..623a67592 100644 --- a/scripts/generate/templates/StorageSlotMock.js +++ b/scripts/generate/templates/StorageSlotMock.js @@ -44,22 +44,27 @@ const transient = ({ type, name }) => `\ event ${name}Value(bytes32 slot, ${type} value); function tload${name}(bytes32 slot) public { - emit ${name}Value(slot, slot.as${name}().tload()); + emit ${name}Value(slot, slot.as${name}().tload()); } function tstore(bytes32 slot, ${type} value) public { - slot.as${name}().tstore(value); + slot.as${name}().tstore(value); } `; // GENERATE module.exports = format( - header.trimEnd(), + header, 'contract StorageSlotMock is Multicall {', - 'using StorageSlot for *;', - TYPES.filter(type => type.isValueType).map(type => storageSetValueType(type)), - TYPES.filter(type => type.isValueType).map(type => storageGetValueType(type)), - TYPES.filter(type => !type.isValueType).map(type => storageSetNonValueType(type)), - TYPES.filter(type => type.isValueType).map(type => transient(type)), + format( + [].concat( + 'using StorageSlot for *;', + '', + TYPES.filter(type => type.isValueType).map(type => storageSetValueType(type)), + TYPES.filter(type => type.isValueType).map(type => storageGetValueType(type)), + TYPES.filter(type => !type.isValueType).map(type => storageSetNonValueType(type)), + TYPES.filter(type => type.isValueType).map(type => transient(type)), + ), + ).trimEnd(), '}', );