diff --git a/.changeset/swift-planets-juggle.md b/.changeset/swift-planets-juggle.md new file mode 100644 index 00000000000..526a0b709ad --- /dev/null +++ b/.changeset/swift-planets-juggle.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Arrays`: Add `slice` and `splice` functions for value types (`uint256[]`, `bytes32[]`, `address[]`). diff --git a/CHANGELOG.md b/CHANGELOG.md index a9955665a2c..884ec620ae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - `ERC6909` and the its extensions (`ERC6909ContentURI`, `ERC6909Metadata` and `ERC6909TokenSupply`) are no longer marked as draft since [EIP-6909](https://eips.ethereum.org/EIPS/eip-6909) is now final. Developers must update the import paths. Contracts behavior is not modified. - `SignerERC7702` is renamed as `SignerEIP7702`. Imports and inheritance must be updated to that new name and path. Behavior is unmodified. - `ERC721Holder`, `ERC1155Holder`, `ReentrancyGuard` and `ReentrancyGuardTransient` are flagged as stateless and are no longer transpiled. Developers using their upgradeable variants from `@openzeppelin/contracts-upgradeable` must update their imports to use the equivalent version available in `@openzeppelin/contracts`. -- Update minimum pragma to 0.8.24 in `Votes`, `VotesExtended`, `ERC20Votes`, `Strings`, `ERC1155URIStorage`, `MessageHashUtils`, `ERC721URIStorage`, `ERC721Votes`, `ERC721Wrapper`, `ERC721Burnable`, `ERC721Consecutive`, `ERC721Enumerable`, `ERC721Pausable`, `ERC721Royalty`, `ERC721Wrapper`, `EIP712`, `ERC4626` and `ERC7739`. ([#5726](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5726)) +- Update minimum pragma to 0.8.24 in `AccessControlEnumerable`, `Arrays`, `CircularBuffer`, `EIP712`, `EnumerableMap`, `EnumerableSet`, `ERC1155`, `ERC1155Burnable`, `ERC1155Pausable`, `ERC1155Supply`, `ERC1155URIStorage`, `ERC20Votes`, `ERC4626`,`ERC721Burnable`, `ERC721Consecutive`, `ERC721Enumerable`, `ERC721Pausable`, `ERC721Royalty`, `ERC721URIStorage`, `ERC721Votes`, `ERC721Wrapper`, `ERC7739`, `Heap`, `MerkleTree`, `MessageHashUtils`, `Strings`, `Votes` and `VotesExtended`. ([#5723](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/5723), [#5726](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5726), [#5965](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5965)) ### Deprecation diff --git a/contracts/access/extensions/AccessControlEnumerable.sol b/contracts/access/extensions/AccessControlEnumerable.sol index f2d79fd57c5..caf50a32923 100644 --- a/contracts/access/extensions/AccessControlEnumerable.sol +++ b/contracts/access/extensions/AccessControlEnumerable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.4.0) (access/extensions/AccessControlEnumerable.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {IAccessControlEnumerable} from "./IAccessControlEnumerable.sol"; import {AccessControl} from "../AccessControl.sol"; diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index 8582e0cbd49..f5f01c51001 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.4.0) (token/ERC1155/ERC1155.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {IERC1155} from "./IERC1155.sol"; import {IERC1155MetadataURI} from "./extensions/IERC1155MetadataURI.sol"; diff --git a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol index fd6ad61ddde..b355804fb58 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/extensions/ERC1155Burnable.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {ERC1155} from "../ERC1155.sol"; diff --git a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol index a0de999f0cf..240413411cc 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.1.0) (token/ERC1155/extensions/ERC1155Pausable.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {ERC1155} from "../ERC1155.sol"; import {Pausable} from "../../../utils/Pausable.sol"; diff --git a/contracts/token/ERC1155/extensions/ERC1155Supply.sol b/contracts/token/ERC1155/extensions/ERC1155Supply.sol index 96d5e606f4f..55fe9c78a17 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Supply.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.4.0) (token/ERC1155/extensions/ERC1155Supply.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {ERC1155} from "../ERC1155.sol"; import {Arrays} from "../../../utils/Arrays.sol"; diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index 511354a5f67..fa699a71597 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -2,7 +2,7 @@ // OpenZeppelin Contracts (last updated v5.4.0) (utils/Arrays.sol) // This file was procedurally generated from scripts/generate/templates/Arrays.js. -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {Comparators} from "./Comparators.sol"; import {SlotDerivation} from "./SlotDerivation.sol"; @@ -375,6 +375,195 @@ library Arrays { return low; } + /** + * @dev Copies the content of `array`, from `start` (included) to the end of `array` into a new address array in + * memory. + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`] + */ + function slice(address[] memory array, uint256 start) internal pure returns (address[] memory) { + return slice(array, start, array.length); + } + + /** + * @dev Copies the content of `array`, from `start` (included) to `end` (excluded) into a new address array in + * memory. The `end` argument is truncated to the length of the `array`. + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`] + */ + function slice(address[] memory array, uint256 start, uint256 end) internal pure returns (address[] memory) { + // sanitize + uint256 length = array.length; + end = Math.min(end, length); + start = Math.min(start, end); + + // allocate and copy + address[] memory result = new address[](end - start); + assembly ("memory-safe") { + mcopy(add(result, 0x20), add(add(array, 0x20), mul(start, 0x20)), mul(sub(end, start), 0x20)) + } + + return result; + } + + /** + * @dev Copies the content of `array`, from `start` (included) to the end of `array` into a new bytes32 array in + * memory. + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`] + */ + function slice(bytes32[] memory array, uint256 start) internal pure returns (bytes32[] memory) { + return slice(array, start, array.length); + } + + /** + * @dev Copies the content of `array`, from `start` (included) to `end` (excluded) into a new bytes32 array in + * memory. The `end` argument is truncated to the length of the `array`. + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`] + */ + function slice(bytes32[] memory array, uint256 start, uint256 end) internal pure returns (bytes32[] memory) { + // sanitize + uint256 length = array.length; + end = Math.min(end, length); + start = Math.min(start, end); + + // allocate and copy + bytes32[] memory result = new bytes32[](end - start); + assembly ("memory-safe") { + mcopy(add(result, 0x20), add(add(array, 0x20), mul(start, 0x20)), mul(sub(end, start), 0x20)) + } + + return result; + } + + /** + * @dev Copies the content of `array`, from `start` (included) to the end of `array` into a new uint256 array in + * memory. + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`] + */ + function slice(uint256[] memory array, uint256 start) internal pure returns (uint256[] memory) { + return slice(array, start, array.length); + } + + /** + * @dev Copies the content of `array`, from `start` (included) to `end` (excluded) into a new uint256 array in + * memory. The `end` argument is truncated to the length of the `array`. + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`] + */ + function slice(uint256[] memory array, uint256 start, uint256 end) internal pure returns (uint256[] memory) { + // sanitize + uint256 length = array.length; + end = Math.min(end, length); + start = Math.min(start, end); + + // allocate and copy + uint256[] memory result = new uint256[](end - start); + assembly ("memory-safe") { + mcopy(add(result, 0x20), add(add(array, 0x20), mul(start, 0x20)), mul(sub(end, start), 0x20)) + } + + return result; + } + + /** + * @dev Moves the content of `array`, from `start` (included) to the end of `array` to the start of that array. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead. + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice(address[] memory array, uint256 start) internal pure returns (address[] memory) { + return splice(array, start, array.length); + } + + /** + * @dev Moves the content of `array`, from `start` (included) to `end` (excluded) to the start of that array. The + * `end` argument is truncated to the length of the `array`. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead. + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice(address[] memory array, uint256 start, uint256 end) internal pure returns (address[] memory) { + // sanitize + uint256 length = array.length; + end = Math.min(end, length); + start = Math.min(start, end); + + // move and resize + assembly ("memory-safe") { + mcopy(add(array, 0x20), add(add(array, 0x20), mul(start, 0x20)), mul(sub(end, start), 0x20)) + mstore(array, sub(end, start)) + } + + return array; + } + + /** + * @dev Moves the content of `array`, from `start` (included) to the end of `array` to the start of that array. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead. + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice(bytes32[] memory array, uint256 start) internal pure returns (bytes32[] memory) { + return splice(array, start, array.length); + } + + /** + * @dev Moves the content of `array`, from `start` (included) to `end` (excluded) to the start of that array. The + * `end` argument is truncated to the length of the `array`. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead. + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice(bytes32[] memory array, uint256 start, uint256 end) internal pure returns (bytes32[] memory) { + // sanitize + uint256 length = array.length; + end = Math.min(end, length); + start = Math.min(start, end); + + // move and resize + assembly ("memory-safe") { + mcopy(add(array, 0x20), add(add(array, 0x20), mul(start, 0x20)), mul(sub(end, start), 0x20)) + mstore(array, sub(end, start)) + } + + return array; + } + + /** + * @dev Moves the content of `array`, from `start` (included) to the end of `array` to the start of that array. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead. + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice(uint256[] memory array, uint256 start) internal pure returns (uint256[] memory) { + return splice(array, start, array.length); + } + + /** + * @dev Moves the content of `array`, from `start` (included) to `end` (excluded) to the start of that array. The + * `end` argument is truncated to the length of the `array`. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead. + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice(uint256[] memory array, uint256 start, uint256 end) internal pure returns (uint256[] memory) { + // sanitize + uint256 length = array.length; + end = Math.min(end, length); + start = Math.min(start, end); + + // move and resize + assembly ("memory-safe") { + mcopy(add(array, 0x20), add(add(array, 0x20), mul(start, 0x20)), mul(sub(end, start), 0x20)) + mstore(array, sub(end, start)) + } + + return array; + } + /** * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check. * diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index 7a7d20553b6..7b948fea0e0 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -102,6 +102,7 @@ library Bytes { * @dev Moves the content of `buffer`, from `start` (included) to the end of `buffer` to the start of that buffer. * * NOTE: This function modifies the provided buffer in place. If you need to preserve the original buffer, use {slice} instead + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] */ function splice(bytes memory buffer, uint256 start) internal pure returns (bytes memory) { return splice(buffer, start, buffer.length); @@ -112,6 +113,7 @@ library Bytes { * `end` argument is truncated to the length of the `buffer`. * * NOTE: This function modifies the provided buffer in place. If you need to preserve the original buffer, use {slice} instead + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] */ function splice(bytes memory buffer, uint256 start, uint256 end) internal pure returns (bytes memory) { // sanitize diff --git a/contracts/utils/structs/CircularBuffer.sol b/contracts/utils/structs/CircularBuffer.sol index 8a479c3044d..69094eb4bf1 100644 --- a/contracts/utils/structs/CircularBuffer.sol +++ b/contracts/utils/structs/CircularBuffer.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.3.0) (utils/structs/CircularBuffer.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {Math} from "../math/Math.sol"; import {Arrays} from "../Arrays.sol"; diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index 68ce32230a3..3f2819803ac 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -2,7 +2,7 @@ // OpenZeppelin Contracts (last updated v5.4.0) (utils/structs/EnumerableMap.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableMap.js. -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {EnumerableSet} from "./EnumerableSet.sol"; diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index fbf742a60e7..fae15074cc6 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -2,7 +2,7 @@ // OpenZeppelin Contracts (last updated v5.4.0) (utils/structs/EnumerableSet.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableSet.js. -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {Arrays} from "../Arrays.sol"; import {Math} from "../math/Math.sol"; diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index c97bb432a33..377994087aa 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.1.0) (utils/structs/Heap.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {Math} from "../math/Math.sol"; import {SafeCast} from "../math/SafeCast.sol"; diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 010ccfe8b6b..6e4c1248cb4 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.3.0) (utils/structs/MerkleTree.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {Hashes} from "../cryptography/Hashes.sol"; import {Arrays} from "../Arrays.sol"; diff --git a/scripts/generate/templates/Arrays.js b/scripts/generate/templates/Arrays.js index 6f9380a4355..29d03f92b19 100644 --- a/scripts/generate/templates/Arrays.js +++ b/scripts/generate/templates/Arrays.js @@ -3,7 +3,7 @@ const { capitalize } = require('../../helpers'); const { TYPES } = require('./Arrays.opts'); const header = `\ -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {Comparators} from "./Comparators.sol"; import {SlotDerivation} from "./SlotDerivation.sol"; @@ -359,6 +359,73 @@ function unsafeSetLength(${type.name}[] storage array, uint256 len) internal { } `; +const slice = type => `\ +/** + * @dev Copies the content of \`array\`, from \`start\` (included) to the end of \`array\` into a new ${type.name} array in + * memory. + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's \`Array.slice\`] + */ +function slice(${type.name}[] memory array, uint256 start) internal pure returns (${type.name}[] memory) { + return slice(array, start, array.length); +} + +/** + * @dev Copies the content of \`array\`, from \`start\` (included) to \`end\` (excluded) into a new ${type.name} array in + * memory. The \`end\` argument is truncated to the length of the \`array\`. + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's \`Array.slice\`] + */ +function slice(${type.name}[] memory array, uint256 start, uint256 end) internal pure returns (${type.name}[] memory) { + // sanitize + uint256 length = array.length; + end = Math.min(end, length); + start = Math.min(start, end); + + // allocate and copy + ${type.name}[] memory result = new ${type.name}[](end - start); + assembly ("memory-safe") { + mcopy(add(result, 0x20), add(add(array, 0x20), mul(start, 0x20)), mul(sub(end, start), 0x20)) + } + + return result; +} +`; + +const splice = type => `\ +/** + * @dev Moves the content of \`array\`, from \`start\` (included) to the end of \`array\` to the start of that array. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead. + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's \`Array.splice\`] + */ +function splice(${type.name}[] memory array, uint256 start) internal pure returns (${type.name}[] memory) { + return splice(array, start, array.length); +} + +/** + * @dev Moves the content of \`array\`, from \`start\` (included) to \`end\` (excluded) to the start of that array. The + * \`end\` argument is truncated to the length of the \`array\`. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead. + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's \`Array.splice\`] + */ +function splice(${type.name}[] memory array, uint256 start, uint256 end) internal pure returns (${type.name}[] memory) { + // sanitize + uint256 length = array.length; + end = Math.min(end, length); + start = Math.min(start, end); + + // move and resize + assembly ("memory-safe") { + mcopy(add(array, 0x20), add(add(array, 0x20), mul(start, 0x20)), mul(sub(end, start), 0x20)) + mstore(array, sub(end, start)) + } + + return array; +} +`; + // GENERATE module.exports = format( header.trimEnd(), @@ -376,6 +443,9 @@ module.exports = format( TYPES.filter(type => type.isValueType && type.name !== 'uint256').map(castComparator), // lookup search, + // slice and splice for value types only + TYPES.filter(type => type.isValueType).map(slice), + TYPES.filter(type => type.isValueType).map(splice), // unsafe (direct) storage and memory access TYPES.map(unsafeAccessStorage), TYPES.map(unsafeAccessMemory), diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index f8deb88f85d..a7d061d771a 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -3,7 +3,7 @@ const { fromBytes32, toBytes32 } = require('./conversion'); const { MAP_TYPES } = require('./Enumerable.opts'); const header = `\ -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {EnumerableSet} from "./EnumerableSet.sol"; diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index eb6a0a26c00..f69faea8566 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -3,7 +3,7 @@ const { fromBytes32, toBytes32 } = require('./conversion'); const { SET_TYPES } = require('./Enumerable.opts'); const header = `\ -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {Arrays} from "../Arrays.sol"; import {Math} from "../math/Math.sol"; diff --git a/test/utils/Arrays.t.sol b/test/utils/Arrays.t.sol index e45d29c919d..0daac5e3713 100644 --- a/test/utils/Arrays.t.sol +++ b/test/utils/Arrays.t.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {SymTest} from "halmos-cheatcodes/SymTest.sol"; import {Arrays} from "@openzeppelin/contracts/utils/Arrays.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; contract ArraysTest is Test, SymTest { function testSort(uint256[] memory values) public pure { @@ -21,6 +22,166 @@ contract ArraysTest is Test, SymTest { _assertSort(values); } + /// Slice + + function testSliceAddressWithStartOnly(address[] memory values, uint256 start) public pure { + address[] memory originalValues = _copyArray(values); + address[] memory result = Arrays.slice(values, start); + + // Original buffer was not modified + assertEq(values, originalValues); + + // Result should match originalValues over the specified slice + uint256 expectedLength = Math.saturatingSub(values.length, start); + _assertSliceOf(result, originalValues, start, expectedLength); + } + + function testSliceAddress(address[] memory values, uint256 start, uint256 end) public pure { + address[] memory originalValues = _copyArray(values); + address[] memory result = Arrays.slice(values, start, end); + + // Original buffer was not modified + assertEq(values, originalValues); + + // Calculate expected bounds after sanitization + uint256 sanitizedEnd = Math.min(end, values.length); + uint256 sanitizedStart = Math.min(start, sanitizedEnd); + uint256 expectedLength = sanitizedEnd - sanitizedStart; + _assertSliceOf(result, originalValues, sanitizedStart, expectedLength); + } + + function testSliceBytes32WithStartOnly(bytes32[] memory values, uint256 start) public pure { + bytes32[] memory originalValues = _copyArray(values); + bytes32[] memory result = Arrays.slice(values, start); + + // Original buffer was not modified + assertEq(values, originalValues); + + // Result should match originalValues over the specified slice + uint256 expectedLength = Math.saturatingSub(values.length, start); + _assertSliceOf(result, originalValues, start, expectedLength); + } + + function testSliceBytes32(bytes32[] memory values, uint256 start, uint256 end) public pure { + bytes32[] memory originalValues = _copyArray(values); + bytes32[] memory result = Arrays.slice(values, start, end); + + // Original buffer was not modified + assertEq(values, originalValues); + + // Calculate expected bounds after sanitization + uint256 sanitizedEnd = Math.min(end, values.length); + uint256 sanitizedStart = Math.min(start, sanitizedEnd); + uint256 expectedLength = sanitizedEnd - sanitizedStart; + _assertSliceOf(result, originalValues, sanitizedStart, expectedLength); + } + + function testSliceUint256WithStartOnly(uint256[] memory values, uint256 start) public pure { + uint256[] memory originalValues = _copyArray(values); + uint256[] memory result = Arrays.slice(values, start); + + // Original buffer was not modified + assertEq(values, originalValues); + + // Result should match originalValues over the specified slice + uint256 expectedLength = Math.saturatingSub(values.length, start); + _assertSliceOf(result, originalValues, start, expectedLength); + } + + function testSliceUint256(uint256[] memory values, uint256 start, uint256 end) public pure { + uint256[] memory originalValues = _copyArray(values); + uint256[] memory result = Arrays.slice(values, start, end); + + // Original buffer was not modified + assertEq(values, originalValues); + + // Calculate expected bounds after sanitization + uint256 sanitizedEnd = Math.min(end, values.length); + uint256 sanitizedStart = Math.min(start, sanitizedEnd); + uint256 expectedLength = sanitizedEnd - sanitizedStart; + _assertSliceOf(result, originalValues, sanitizedStart, expectedLength); + } + + /// Splice + + function testSpliceAddressWithStartOnly(address[] memory values, uint256 start) public pure { + address[] memory originalValues = _copyArray(values); + address[] memory result = Arrays.splice(values, start); + + // Result should be the same object as input (modified in place) + assertEq(result, values); + + // Result should match originalValues over the specified slice + uint256 expectedLength = Math.saturatingSub(originalValues.length, start); + _assertSliceOf(result, originalValues, start, expectedLength); + } + + function testSpliceAddress(address[] memory values, uint256 start, uint256 end) public pure { + address[] memory originalValues = _copyArray(values); + address[] memory result = Arrays.splice(values, start, end); + + // Result should be the same object as input (modified in place) + assertEq(result, values); + + // Calculate expected bounds after sanitization + uint256 sanitizedEnd = Math.min(end, originalValues.length); + uint256 sanitizedStart = Math.min(start, sanitizedEnd); + uint256 expectedLength = sanitizedEnd - sanitizedStart; + _assertSliceOf(result, originalValues, sanitizedStart, expectedLength); + } + + function testSpliceBytes32WithStartOnly(bytes32[] memory values, uint256 start) public pure { + bytes32[] memory originalValues = _copyArray(values); + bytes32[] memory result = Arrays.splice(values, start); + + // Result should be the same object as input (modified in place) + assertEq(result, values); + + // Result should match originalValues over the specified slice + uint256 expectedLength = Math.saturatingSub(originalValues.length, start); + _assertSliceOf(result, originalValues, start, expectedLength); + } + + function testSpliceBytes32(bytes32[] memory values, uint256 start, uint256 end) public pure { + bytes32[] memory originalValues = _copyArray(values); + bytes32[] memory result = Arrays.splice(values, start, end); + + // Result should be the same object as input (modified in place) + assertEq(result, values); + + // Calculate expected bounds after sanitization + uint256 sanitizedEnd = Math.min(end, originalValues.length); + uint256 sanitizedStart = Math.min(start, sanitizedEnd); + uint256 expectedLength = sanitizedEnd - sanitizedStart; + _assertSliceOf(result, originalValues, sanitizedStart, expectedLength); + } + + function testSpliceUint256WithStartOnly(uint256[] memory values, uint256 start) public pure { + uint256[] memory originalValues = _copyArray(values); + uint256[] memory result = Arrays.splice(values, start); + + // Result should be the same object as input (modified in place) + assertEq(result, values); + + // Result should match originalValues over the specified slice + uint256 expectedLength = Math.saturatingSub(originalValues.length, start); + _assertSliceOf(result, originalValues, start, expectedLength); + } + + function testSpliceUint256(uint256[] memory values, uint256 start, uint256 end) public pure { + uint256[] memory originalValues = _copyArray(values); + uint256[] memory result = Arrays.splice(values, start, end); + + // Result should be the same object as input (modified in place) + assertEq(result, values); + + // Calculate expected bounds after sanitization + uint256 sanitizedEnd = Math.min(end, originalValues.length); + uint256 sanitizedStart = Math.min(start, sanitizedEnd); + uint256 expectedLength = sanitizedEnd - sanitizedStart; + _assertSliceOf(result, originalValues, sanitizedStart, expectedLength); + } + /// Asserts function _assertSort(uint256[] memory values) internal pure { @@ -28,4 +189,60 @@ contract ArraysTest is Test, SymTest { assertLe(values[i - 1], values[i]); } } + + function _assertSliceOf( + address[] memory result, + address[] memory original, + uint256 offset, + uint256 expectedLength + ) internal pure { + assertEq(result.length, expectedLength); + for (uint256 i = 0; i < expectedLength; ++i) { + assertEq(result[i], original[offset + i]); + } + } + + function _assertSliceOf( + bytes32[] memory result, + bytes32[] memory original, + uint256 offset, + uint256 expectedLength + ) internal pure { + assertEq(result.length, expectedLength); + for (uint256 i = 0; i < expectedLength; ++i) { + assertEq(result[i], original[offset + i]); + } + } + + function _assertSliceOf( + uint256[] memory result, + uint256[] memory original, + uint256 offset, + uint256 expectedLength + ) internal pure { + assertEq(result.length, expectedLength); + for (uint256 i = 0; i < expectedLength; ++i) { + assertEq(result[i], original[offset + i]); + } + } + + /// Helpers + + function _copyArray(uint256[] memory values) internal pure returns (uint256[] memory) { + uint256[] memory copy = new uint256[](values.length); + for (uint256 i = 0; i < values.length; ++i) copy[i] = values[i]; + return copy; + } + + function _copyArray(bytes32[] memory values) internal pure returns (bytes32[] memory) { + bytes32[] memory copy = new bytes32[](values.length); + for (uint256 i = 0; i < values.length; ++i) copy[i] = values[i]; + return copy; + } + + function _copyArray(address[] memory values) internal pure returns (address[] memory) { + address[] memory copy = new address[](values.length); + for (uint256 i = 0; i < values.length; ++i) copy[i] = values[i]; + return copy; + } } diff --git a/test/utils/Arrays.test.js b/test/utils/Arrays.test.js index a4bc9d701f7..c3bee1492c9 100644 --- a/test/utils/Arrays.test.js +++ b/test/utils/Arrays.test.js @@ -98,20 +98,20 @@ describe('Arrays', function () { it('[deprecated] findUpperBound', async function () { // findUpperBound does not support duplicated if (hasDuplicates(array)) { - expect(await this.instance.findUpperBound(input)).to.equal(upperBound(array, input) - 1); + await expect(this.instance.findUpperBound(input)).to.eventually.equal(upperBound(array, input) - 1); } else { - expect(await this.instance.findUpperBound(input)).to.equal(lowerBound(array, input)); + await expect(this.instance.findUpperBound(input)).to.eventually.equal(lowerBound(array, input)); } }); it('lowerBound', async function () { - expect(await this.instance.lowerBound(input)).to.equal(lowerBound(array, input)); - expect(await this.instance.lowerBoundMemory(array, input)).to.equal(lowerBound(array, input)); + await expect(this.instance.lowerBound(input)).to.eventually.equal(lowerBound(array, input)); + await expect(this.instance.lowerBoundMemory(array, input)).to.eventually.equal(lowerBound(array, input)); }); it('upperBound', async function () { - expect(await this.instance.upperBound(input)).to.equal(upperBound(array, input)); - expect(await this.instance.upperBoundMemory(array, input)).to.equal(upperBound(array, input)); + await expect(this.instance.upperBound(input)).to.eventually.equal(upperBound(array, input)); + await expect(this.instance.upperBoundMemory(array, input)).to.eventually.equal(upperBound(array, input)); }); }); } @@ -142,8 +142,8 @@ describe('Arrays', function () { afterEach(async function () { const expected = Array.from(this.array).sort(comparator); const reversed = Array.from(expected).reverse(); - expect(await this.instance.sort(this.array)).to.deep.equal(expected); - expect(await this.instance.sortReverse(this.array)).to.deep.equal(reversed); + await expect(this.instance.sort(this.array)).to.eventually.deep.equal(expected); + await expect(this.instance.sortReverse(this.array)).to.eventually.deep.equal(reversed); }); it('sort array', async function () { @@ -175,13 +175,70 @@ describe('Arrays', function () { }); } }); + + for (const fn of ['slice', 'splice']) { + const array = Array.from({ length: 10 }, generators[name]); + + describe(fn, function () { + const fragment = `$${fn}(${name}[] arr, uint256 start)`; + const rangeFragment = `$${fn}(${name}[] arr, uint256 start, uint256 end)`; + + it(`${fn} from start to end`, async function () { + const start = 2; + const end = 7; + await expect(this.mock[rangeFragment](array, start, end)).to.eventually.deep.equal( + array.slice(start, end), + ); + }); + + it(`${fn} from start to end of array`, async function () { + const start = 3; + await expect(this.mock[fragment](array, start)).to.eventually.deep.equal(array.slice(start)); + }); + + it(`${fn} entire array`, async function () { + await expect(this.mock[fragment](array, 0)).to.eventually.deep.equal(array); + await expect(this.mock[rangeFragment](array, 0, array.length)).to.eventually.deep.equal(array); + }); + + it(`${fn} empty range`, async function () { + await expect(this.mock[rangeFragment](array, 5, 5)).to.eventually.deep.equal([]); + await expect(this.mock[rangeFragment](array, 7, 3)).to.eventually.deep.equal([]); + }); + + it(`${fn} with out of bounds indices`, async function () { + // start beyond array length + await expect(this.mock[fragment](array, array.length + 5)).to.eventually.deep.equal([]); + + // end beyond array length (should be truncated) + const start = 5; + await expect(this.mock[rangeFragment](array, start, array.length + 10)).to.eventually.deep.equal( + array.slice(start), + ); + }); + + it(`${fn} empty array`, async function () { + const emptyArray = []; + await expect(this.mock[fragment](emptyArray, 0)).to.eventually.deep.equal([]); + await expect(this.mock[fragment](emptyArray, 5)).to.eventually.deep.equal([]); + await expect(this.mock[rangeFragment](emptyArray, 0, 5)).to.eventually.deep.equal([]); + }); + + it(`${fn} single element`, async function () { + const singleArray = [array[0]]; + await expect(this.mock[fragment](singleArray, 0)).to.eventually.deep.equal(singleArray); + await expect(this.mock[fragment](singleArray, 1)).to.eventually.deep.equal([]); + await expect(this.mock[rangeFragment](singleArray, 0, 1)).to.eventually.deep.equal(singleArray); + }); + }); + } } describe('unsafeAccess', function () { describe('storage', function () { for (const i in elements) { it(`unsafeAccess within bounds #${i}`, async function () { - expect(await this.instance.unsafeAccess(i)).to.equal(elements[i]); + await expect(this.instance.unsafeAccess(i)).to.eventually.equal(elements[i]); }); } @@ -192,9 +249,9 @@ describe('Arrays', function () { it('unsafeSetLength changes the length or the array', async function () { const newLength = generators.uint256(); - expect(await this.instance.length()).to.equal(elements.length); + await expect(this.instance.length()).to.eventually.equal(elements.length); await expect(this.instance.unsafeSetLength(newLength)).to.not.be.rejected; - expect(await this.instance.length()).to.equal(newLength); + await expect(this.instance.length()).to.eventually.equal(newLength); }); }); @@ -203,7 +260,7 @@ describe('Arrays', function () { for (const i in elements) { it(`unsafeMemoryAccess within bounds #${i}`, async function () { - expect(await this.mock[fragment](elements, i)).to.equal(elements[i]); + await expect(this.mock[fragment](elements, i)).to.eventually.equal(elements[i]); }); } @@ -213,11 +270,11 @@ describe('Arrays', function () { it('unsafeMemoryAccess loop around', async function () { for (let i = 251n; i < 256n; ++i) { - expect(await this.mock[fragment](elements, 2n ** i - 1n)).to.equal( + await expect(this.mock[fragment](elements, 2n ** i - 1n)).to.eventually.equal( isValueType ? BigInt(elements.length) : generators[name].zero, ); - expect(await this.mock[fragment](elements, 2n ** i + 0n)).to.equal(elements[0]); - expect(await this.mock[fragment](elements, 2n ** i + 1n)).to.equal(elements[1]); + await expect(this.mock[fragment](elements, 2n ** i + 0n)).to.eventually.equal(elements[0]); + await expect(this.mock[fragment](elements, 2n ** i + 1n)).to.eventually.equal(elements[1]); } }); }); diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index e01d933460d..9412ed53c98 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";