diff --git a/.changeset/brave-islands-sparkle.md b/.changeset/brave-islands-sparkle.md new file mode 100644 index 00000000000..54cd6fb3a49 --- /dev/null +++ b/.changeset/brave-islands-sparkle.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`GovernorSequentialProposalId`: Adds a `Governor` extension that sequentially numbers proposal ids instead of using the hash. diff --git a/.changeset/cyan-taxis-travel.md b/.changeset/cyan-taxis-travel.md new file mode 100644 index 00000000000..bd90e07f3f3 --- /dev/null +++ b/.changeset/cyan-taxis-travel.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Address`: bubble up revert data on `sendValue` failed call diff --git a/.changeset/seven-insects-taste.md b/.changeset/seven-insects-taste.md new file mode 100644 index 00000000000..bfa8737d7de --- /dev/null +++ b/.changeset/seven-insects-taste.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC7579Utils`: Add ABI decoding checks on calldata bounds within `decodeBatch` diff --git a/.changeset/ten-fishes-fold.md b/.changeset/ten-fishes-fold.md new file mode 100644 index 00000000000..5c87ff36370 --- /dev/null +++ b/.changeset/ten-fishes-fold.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`IGovernor`: Add the `getProposalId` function to the governor interface. diff --git a/.changeset/thin-eels-cross.md b/.changeset/thin-eels-cross.md new file mode 100644 index 00000000000..7993d7d64df --- /dev/null +++ b/.changeset/thin-eels-cross.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC4626`: Use the `asset` getter in `totalAssets`, `_deposit` and `_withdraw`. diff --git a/.githooks/pre-push b/.githooks/pre-push deleted file mode 100755 index f028ce58e0b..00000000000 --- a/.githooks/pre-push +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -if [ "${CI:-"false"}" != "true" ]; then - npm run test:generation - npm run lint -fi diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 18a38b3c5c0..a4eea0a2bf3 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -97,7 +97,7 @@ jobs: uses: ./.github/actions/setup - name: Run coverage run: npm run coverage - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -118,11 +118,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up environment uses: ./.github/actions/setup - - run: rm foundry.toml - uses: crytic/slither-action@v0.4.0 - with: - node-version: 18.15 - slither-version: 0.10.1 codespell: runs-on: ubuntu-latest diff --git a/.github/workflows/formal-verification.yml b/.github/workflows/formal-verification.yml index e0475b195d8..86acca7f32b 100644 --- a/.github/workflows/formal-verification.yml +++ b/.github/workflows/formal-verification.yml @@ -52,7 +52,7 @@ jobs: - name: Install python packages run: pip install -r fv-requirements.txt - name: Install java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ env.JAVA_VERSION }} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000000..4738b05554b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +npm run test:generation +npx lint-staged diff --git a/audits/2024-12-v5.2.pdf b/audits/2024-12-v5.2.pdf new file mode 100644 index 00000000000..ef138f7ec61 Binary files /dev/null and b/audits/2024-12-v5.2.pdf differ diff --git a/audits/README.md b/audits/README.md index 34bda2af305..4e8c94653cd 100644 --- a/audits/README.md +++ b/audits/README.md @@ -1,13 +1,14 @@ # Audits -| Date | Version | Commit | Auditor | Scope | Links | -| ------------ | ------- | --------- | ------------ | -------------------- | ----------------------------------------------------------- | -| October 2024 | v5.1.0 | TBD | OpenZeppelin | v5.1 Changes | [π](./2024-10-v5.1.pdf) | -| October 2023 | v5.0.0 | `b5a3e69` | OpenZeppelin | v5.0 Changes | [π](./2023-10-v5.0.pdf) | -| May 2023 | v4.9.0 | `91df66c` | OpenZeppelin | v4.9 Changes | [π](./2023-05-v4.9.pdf) | -| October 2022 | v4.8.0 | `14f98db` | OpenZeppelin | ERC4626, Checkpoints | [π](./2022-10-ERC4626.pdf) [π](./2022-10-Checkpoints.pdf) | -| October 2018 | v2.0.0 | `dac5bcc` | LevelK | Everything | [π](./2018-10.pdf) | -| March 2017 | v1.0.4 | `9c5975a` | New Alchemy | Everything | [π](./2017-03.md) | +| Date | Version | Commit | Auditor | Scope | Links | +| ------------- | ------- | -------------------------------------------------------------------------------- | ------------ | -------------------- | ----------------------------------------------------------- | +| December 2024 | v5.2.0 | [`98d28f9`](https://github.com/openzeppelin/openzeppelin-contracts/tree/98d28f9) | OpenZeppelin | v5.2 Changes | [π](./2024-12-v5.2.pdf) | +| October 2024 | v5.1.0 | [`aba9ff6`](https://github.com/openzeppelin/openzeppelin-contracts/tree/aba9ff6) | OpenZeppelin | v5.1 Changes | [π](./2024-10-v5.1.pdf) | +| October 2023 | v5.0.0 | [`b5a3e69`](https://github.com/openzeppelin/openzeppelin-contracts/tree/b5a3e69) | OpenZeppelin | v5.0 Changes | [π](./2023-10-v5.0.pdf) | +| May 2023 | v4.9.0 | [`91df66c`](https://github.com/openzeppelin/openzeppelin-contracts/tree/91df66c) | OpenZeppelin | v4.9 Changes | [π](./2023-05-v4.9.pdf) | +| October 2022 | v4.8.0 | [`14f98db`](https://github.com/openzeppelin/openzeppelin-contracts/tree/14f98db) | OpenZeppelin | ERC4626, Checkpoints | [π](./2022-10-ERC4626.pdf) [π](./2022-10-Checkpoints.pdf) | +| October 2018 | v2.0.0 | [`dac5bcc`](https://github.com/openzeppelin/openzeppelin-contracts/tree/dac5bcc) | LevelK | Everything | [π](./2018-10.pdf) | +| March 2017 | v1.0.4 | [`9c5975a`](https://github.com/openzeppelin/openzeppelin-contracts/tree/9c5975a) | New Alchemy | Everything | [π](./2017-03.md) | # Formal Verification diff --git a/contracts/account/utils/draft-ERC4337Utils.sol b/contracts/account/utils/draft-ERC4337Utils.sol index 2caa3bd2112..78100b0c96e 100644 --- a/contracts/account/utils/draft-ERC4337Utils.sol +++ b/contracts/account/utils/draft-ERC4337Utils.sol @@ -24,9 +24,9 @@ library ERC4337Utils { function parseValidationData( uint256 validationData ) internal pure returns (address aggregator, uint48 validAfter, uint48 validUntil) { - validAfter = uint48(bytes32(validationData).extract_32_6(0x00)); - validUntil = uint48(bytes32(validationData).extract_32_6(0x06)); - aggregator = address(bytes32(validationData).extract_32_20(0x0c)); + validAfter = uint48(bytes32(validationData).extract_32_6(0)); + validUntil = uint48(bytes32(validationData).extract_32_6(6)); + aggregator = address(bytes32(validationData).extract_32_20(12)); if (validUntil == 0) validUntil = type(uint48).max; } @@ -59,7 +59,8 @@ library ERC4337Utils { (address aggregator1, uint48 validAfter1, uint48 validUntil1) = parseValidationData(validationData1); (address aggregator2, uint48 validAfter2, uint48 validUntil2) = parseValidationData(validationData2); - bool success = aggregator1 == address(0) && aggregator2 == address(0); + bool success = aggregator1 == address(uint160(SIG_VALIDATION_SUCCESS)) && + aggregator2 == address(uint160(SIG_VALIDATION_SUCCESS)); uint48 validAfter = uint48(Math.max(validAfter1, validAfter2)); uint48 validUntil = uint48(Math.min(validUntil1, validUntil2)); return packValidationData(success, validAfter, validUntil); @@ -110,22 +111,22 @@ library ERC4337Utils { /// @dev Returns `verificationGasLimit` from the {PackedUserOperation}. function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { - return uint128(self.accountGasLimits.extract_32_16(0x00)); + return uint128(self.accountGasLimits.extract_32_16(0)); } /// @dev Returns `accountGasLimits` from the {PackedUserOperation}. function callGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { - return uint128(self.accountGasLimits.extract_32_16(0x10)); + return uint128(self.accountGasLimits.extract_32_16(16)); } /// @dev Returns the first section of `gasFees` from the {PackedUserOperation}. function maxPriorityFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { - return uint128(self.gasFees.extract_32_16(0x00)); + return uint128(self.gasFees.extract_32_16(0)); } /// @dev Returns the second section of `gasFees` from the {PackedUserOperation}. function maxFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { - return uint128(self.gasFees.extract_32_16(0x10)); + return uint128(self.gasFees.extract_32_16(16)); } /// @dev Returns the total gas price for the {PackedUserOperation} (ie. `maxFeePerGas` or `maxPriorityFeePerGas + basefee`). @@ -153,7 +154,7 @@ library ERC4337Utils { return self.paymasterAndData.length < 52 ? 0 : uint128(bytes16(self.paymasterAndData[36:52])); } - /// @dev Returns the forth section of `paymasterAndData` from the {PackedUserOperation}. + /// @dev Returns the fourth section of `paymasterAndData` from the {PackedUserOperation}. function paymasterData(PackedUserOperation calldata self) internal pure returns (bytes calldata) { return self.paymasterAndData.length < 52 ? _emptyCalldataBytes() : self.paymasterAndData[52:]; } diff --git a/contracts/account/utils/draft-ERC7579Utils.sol b/contracts/account/utils/draft-ERC7579Utils.sol index 8cd46105aee..3a9769dcb39 100644 --- a/contracts/account/utils/draft-ERC7579Utils.sol +++ b/contracts/account/utils/draft-ERC7579Utils.sol @@ -38,9 +38,10 @@ library ERC7579Utils { /** * @dev Emits when an {EXECTYPE_TRY} execution fails. - * @param batchExecutionIndex The index of the failed transaction in the execution batch. + * @param batchExecutionIndex The index of the failed call in the execution batch. + * @param returndata The returned data from the failed call. */ - event ERC7579TryExecuteFail(uint256 batchExecutionIndex, bytes result); + event ERC7579TryExecuteFail(uint256 batchExecutionIndex, bytes returndata); /// @dev The provided {CallType} is not supported. error ERC7579UnsupportedCallType(CallType callType); @@ -60,10 +61,13 @@ library ERC7579Utils { /// @dev The module type is not supported. error ERC7579UnsupportedModuleType(uint256 moduleTypeId); + /// @dev Input calldata not properly formatted and possibly malicious. + error ERC7579DecodingError(); + /// @dev Executes a single call. function execSingle( - ExecType execType, - bytes calldata executionCalldata + bytes calldata executionCalldata, + ExecType execType ) internal returns (bytes[] memory returnData) { (address target, uint256 value, bytes calldata callData) = decodeSingle(executionCalldata); returnData = new bytes[](1); @@ -72,8 +76,8 @@ library ERC7579Utils { /// @dev Executes a batch of calls. function execBatch( - ExecType execType, - bytes calldata executionCalldata + bytes calldata executionCalldata, + ExecType execType ) internal returns (bytes[] memory returnData) { Execution[] calldata executionBatch = decodeBatch(executionCalldata); returnData = new bytes[](executionBatch.length); @@ -90,8 +94,8 @@ library ERC7579Utils { /// @dev Executes a delegate call. function execDelegateCall( - ExecType execType, - bytes calldata executionCalldata + bytes calldata executionCalldata, + ExecType execType ) internal returns (bytes[] memory returnData) { (address target, bytes calldata callData) = decodeDelegate(executionCalldata); returnData = new bytes[](1); @@ -168,12 +172,40 @@ library ERC7579Utils { } /// @dev Decodes a batch of executions. See {encodeBatch}. + /// + /// NOTE: This function runs some checks and will throw a {ERC7579DecodingError} if the input is not properly formatted. function decodeBatch(bytes calldata executionCalldata) internal pure returns (Execution[] calldata executionBatch) { - assembly ("memory-safe") { - let ptr := add(executionCalldata.offset, calldataload(executionCalldata.offset)) - // Extract the ERC7579 Executions - executionBatch.offset := add(ptr, 0x20) - executionBatch.length := calldataload(ptr) + unchecked { + uint256 bufferLength = executionCalldata.length; + + // Check executionCalldata is not empty. + if (bufferLength < 32) revert ERC7579DecodingError(); + + // Get the offset of the array (pointer to the array length). + uint256 arrayLengthOffset = uint256(bytes32(executionCalldata[0:32])); + + // The array length (at arrayLengthOffset) should be 32 bytes long. We check that this is within the + // buffer bounds. Since we know bufferLength is at least 32, we can subtract with no overflow risk. + if (arrayLengthOffset > bufferLength - 32) revert ERC7579DecodingError(); + + // Get the array length. arrayLengthOffset + 32 is bounded by bufferLength so it does not overflow. + uint256 arrayLength = uint256(bytes32(executionCalldata[arrayLengthOffset:arrayLengthOffset + 32])); + + // Check that the buffer is long enough to store the array elements as "offset pointer": + // - each element of the array is an "offset pointer" to the data. + // - each "offset pointer" (to an array element) takes 32 bytes. + // - validity of the calldata at that location is checked when the array element is accessed, so we only + // need to check that the buffer is large enough to hold the pointers. + // + // Since we know bufferLength is at least arrayLengthOffset + 32, we can subtract with no overflow risk. + // Solidity limits length of such arrays to 2**64-1, this guarantees `arrayLength * 32` does not overflow. + if (arrayLength > type(uint64).max || bufferLength - arrayLengthOffset - 32 < arrayLength * 32) + revert ERC7579DecodingError(); + + assembly ("memory-safe") { + executionBatch.offset := add(add(executionCalldata.offset, arrayLengthOffset), 0x20) + executionBatch.length := arrayLength + } } } diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index ea0d5c73582..14a67013feb 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -92,6 +92,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { return interfaceId == type(IGovernor).interfaceId || + interfaceId == type(IGovernor).interfaceId ^ IGovernor.getProposalId.selector || interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); } @@ -132,6 +133,18 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 return uint256(keccak256(abi.encode(targets, values, calldatas, descriptionHash))); } + /** + * @dev See {IGovernor-getProposalId}. + */ + function getProposalId( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public view virtual returns (uint256) { + return hashProposal(targets, values, calldatas, descriptionHash); + } + /** * @dev See {IGovernor-state}. */ @@ -317,7 +330,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 string memory description, address proposer ) internal virtual returns (uint256 proposalId) { - proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description))); + proposalId = getProposalId(targets, values, calldatas, keccak256(bytes(description))); if (targets.length != values.length || targets.length != calldatas.length || targets.length == 0) { revert GovernorInvalidProposalLength(targets.length, calldatas.length, values.length); @@ -358,7 +371,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 bytes[] memory calldatas, bytes32 descriptionHash ) public virtual returns (uint256) { - uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + uint256 proposalId = getProposalId(targets, values, calldatas, descriptionHash); _validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Succeeded)); @@ -406,7 +419,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 bytes[] memory calldatas, bytes32 descriptionHash ) public payable virtual returns (uint256) { - uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + uint256 proposalId = getProposalId(targets, values, calldatas, descriptionHash); _validateStateBitmap( proposalId, @@ -468,8 +481,8 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 ) public virtual returns (uint256) { // The proposalId will be recomputed in the `_cancel` call further down. However we need the value before we // do the internal call, because we need to check the proposal state BEFORE the internal `_cancel` call - // changes it. The `hashProposal` duplication has a cost that is limited, and that we accept. - uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + // changes it. The `getProposalId` duplication has a cost that is limited, and that we accept. + uint256 proposalId = getProposalId(targets, values, calldatas, descriptionHash); // public cancel restrictions (on top of existing _cancel restrictions). _validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Pending)); @@ -492,7 +505,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 bytes[] memory calldatas, bytes32 descriptionHash ) internal virtual returns (uint256) { - uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + uint256 proposalId = getProposalId(targets, values, calldatas, descriptionHash); _validateStateBitmap( proposalId, diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 28f8aaac044..36ef099a7d5 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -203,7 +203,9 @@ interface IGovernor is IERC165, IERC6372 { /** * @notice module:core - * @dev Hashing function used to (re)build the proposal id from the proposal details.. + * @dev Hashing function used to (re)build the proposal id from the proposal details. + * + * NOTE: For all off-chain and external calls, use {getProposalId}. */ function hashProposal( address[] memory targets, @@ -212,6 +214,17 @@ interface IGovernor is IERC165, IERC6372 { bytes32 descriptionHash ) external pure returns (uint256); + /** + * @notice module:core + * @dev Function used to get the proposal id from the proposal details. + */ + function getProposalId( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external view returns (uint256); + /** * @notice module:core * @dev Current state of a proposal, following Compound's convention diff --git a/contracts/governance/extensions/GovernorCountingOverridable.sol b/contracts/governance/extensions/GovernorCountingOverridable.sol index e89fb49d5af..db375a93f47 100644 --- a/contracts/governance/extensions/GovernorCountingOverridable.sol +++ b/contracts/governance/extensions/GovernorCountingOverridable.sol @@ -8,7 +8,7 @@ import {VotesExtended} from "../utils/VotesExtended.sol"; import {GovernorVotes} from "./GovernorVotes.sol"; /** - * @dev Extension of {Governor} which enables delegatees to override the vote of their delegates. This module requires a + * @dev Extension of {Governor} which enables delegators to override the vote of their delegates. This module requires a * token that inherits {VotesExtended}. */ abstract contract GovernorCountingOverridable is GovernorVotes { @@ -144,9 +144,9 @@ abstract contract GovernorCountingOverridable is GovernorVotes { revert GovernorAlreadyOverridenVote(account); } - uint256 proposalSnapshot = proposalSnapshot(proposalId); - uint256 overridenWeight = VotesExtended(address(token())).getPastBalanceOf(account, proposalSnapshot); - address delegate = VotesExtended(address(token())).getPastDelegate(account, proposalSnapshot); + uint256 snapshot = proposalSnapshot(proposalId); + uint256 overridenWeight = VotesExtended(address(token())).getPastBalanceOf(account, snapshot); + address delegate = VotesExtended(address(token())).getPastDelegate(account, snapshot); uint8 delegateCasted = proposalVote.voteReceipt[delegate].casted; proposalVote.voteReceipt[account].hasOverriden = true; @@ -162,7 +162,7 @@ abstract contract GovernorCountingOverridable is GovernorVotes { return overridenWeight; } - /// @dev Variant of {Governor-_castVote} that deals with vote overrides. + /// @dev Variant of {Governor-_castVote} that deals with vote overrides. Returns the overridden weight. function _castOverride( uint256 proposalId, address account, @@ -180,7 +180,7 @@ abstract contract GovernorCountingOverridable is GovernorVotes { return overridenWeight; } - /// @dev Public function for casting an override vote + /// @dev Public function for casting an override vote. Returns the overridden weight. function castOverrideVote( uint256 proposalId, uint8 support, @@ -190,7 +190,7 @@ abstract contract GovernorCountingOverridable is GovernorVotes { return _castOverride(proposalId, voter, support, reason); } - /// @dev Public function for casting an override vote using a voter's signature + /// @dev Public function for casting an override vote using a voter's signature. Returns the overridden weight. function castOverrideVoteBySig( uint256 proposalId, uint8 support, diff --git a/contracts/governance/extensions/GovernorSequentialProposalId.sol b/contracts/governance/extensions/GovernorSequentialProposalId.sol new file mode 100644 index 00000000000..36e8698ba86 --- /dev/null +++ b/contracts/governance/extensions/GovernorSequentialProposalId.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Governor} from "../Governor.sol"; + +/** + * @dev Extension of {Governor} that changes the numbering of proposal ids from the default hash-based approach to + * sequential ids. + */ +abstract contract GovernorSequentialProposalId is Governor { + uint256 private _latestProposalId; + mapping(uint256 proposalHash => uint256 proposalId) private _proposalIds; + + /** + * @dev The {latestProposalId} may only be initialized if it hasn't been set yet + * (through initialization or the creation of a proposal). + */ + error GovernorAlreadyInitializedLatestProposalId(); + + /** + * @dev See {IGovernor-getProposalId}. + */ + function getProposalId( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public view virtual override returns (uint256) { + uint256 proposalHash = hashProposal(targets, values, calldatas, descriptionHash); + uint256 storedProposalId = _proposalIds[proposalHash]; + if (storedProposalId == 0) { + revert GovernorNonexistentProposal(0); + } + return storedProposalId; + } + + /** + * @dev Returns the latest proposal id. A return value of 0 means no proposals have been created yet. + */ + function latestProposalId() public view virtual returns (uint256) { + return _latestProposalId; + } + + /** + * @dev See {IGovernor-_propose}. + * Hook into the proposing mechanism to increment proposal count. + */ + function _propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description, + address proposer + ) internal virtual override returns (uint256) { + uint256 proposalHash = hashProposal(targets, values, calldatas, keccak256(bytes(description))); + uint256 storedProposalId = _proposalIds[proposalHash]; + if (storedProposalId == 0) { + _proposalIds[proposalHash] = ++_latestProposalId; + } + return super._propose(targets, values, calldatas, description, proposer); + } + + /** + * @dev Internal function to set the {latestProposalId}. This function is helpful when transitioning + * from another governance system. The next proposal id will be `newLatestProposalId` + 1. + * + * May only call this function if the current value of {latestProposalId} is 0. + */ + function _initializeLatestProposalId(uint256 newLatestProposalId) internal virtual { + if (_latestProposalId != 0) { + revert GovernorAlreadyInitializedLatestProposalId(); + } + _latestProposalId = newLatestProposalId; + } +} diff --git a/contracts/governance/extensions/GovernorTimelockCompound.sol b/contracts/governance/extensions/GovernorTimelockCompound.sol index 309f9a4fa76..f98edc8362e 100644 --- a/contracts/governance/extensions/GovernorTimelockCompound.sol +++ b/contracts/governance/extensions/GovernorTimelockCompound.sol @@ -10,7 +10,7 @@ import {SafeCast} from "../../utils/math/SafeCast.sol"; /** * @dev Extension of {Governor} that binds the execution process to a Compound Timelock. This adds a delay, enforced by - * the external timelock to all successful proposal (in addition to the voting duration). The {Governor} needs to be + * the external timelock to all successful proposals (in addition to the voting duration). The {Governor} needs to be * the admin of the timelock for any operation to be performed. A public, unrestricted, * {GovernorTimelockCompound-__acceptAdmin} is available to accept ownership of the timelock. * diff --git a/contracts/governance/utils/VotesExtended.sol b/contracts/governance/utils/VotesExtended.sol index 27baaab84cf..70b0d92fb75 100644 --- a/contracts/governance/utils/VotesExtended.sol +++ b/contracts/governance/utils/VotesExtended.sol @@ -34,8 +34,8 @@ abstract contract VotesExtended is Votes { using Checkpoints for Checkpoints.Trace160; using Checkpoints for Checkpoints.Trace208; - mapping(address delegator => Checkpoints.Trace160) private _delegateCheckpoints; - mapping(address account => Checkpoints.Trace208) private _balanceOfCheckpoints; + mapping(address delegator => Checkpoints.Trace160) private _userDelegationCheckpoints; + mapping(address account => Checkpoints.Trace208) private _userVotingUnitsCheckpoints; /** * @dev Returns the delegate of an `account` at a specific moment in the past. If the `clock()` is @@ -46,7 +46,7 @@ abstract contract VotesExtended is Votes { * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. */ function getPastDelegate(address account, uint256 timepoint) public view virtual returns (address) { - return address(_delegateCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint))); + return address(_userDelegationCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint))); } /** @@ -58,14 +58,14 @@ abstract contract VotesExtended is Votes { * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. */ function getPastBalanceOf(address account, uint256 timepoint) public view virtual returns (uint256) { - return _balanceOfCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint)); + return _userVotingUnitsCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint)); } /// @inheritdoc Votes function _delegate(address account, address delegatee) internal virtual override { super._delegate(account, delegatee); - _delegateCheckpoints[account].push(clock(), uint160(delegatee)); + _userDelegationCheckpoints[account].push(clock(), uint160(delegatee)); } /// @inheritdoc Votes @@ -73,10 +73,10 @@ abstract contract VotesExtended is Votes { super._transferVotingUnits(from, to, amount); if (from != to) { if (from != address(0)) { - _balanceOfCheckpoints[from].push(clock(), SafeCast.toUint208(_getVotingUnits(from))); + _userVotingUnitsCheckpoints[from].push(clock(), SafeCast.toUint208(_getVotingUnits(from))); } if (to != address(0)) { - _balanceOfCheckpoints[to].push(clock(), SafeCast.toUint208(_getVotingUnits(to))); + _userVotingUnitsCheckpoints[to].push(clock(), SafeCast.toUint208(_getVotingUnits(to))); } } } diff --git a/contracts/interfaces/draft-IERC4337.sol b/contracts/interfaces/draft-IERC4337.sol index 5d765dbcde7..67ab146c053 100644 --- a/contracts/interfaces/draft-IERC4337.sol +++ b/contracts/interfaces/draft-IERC4337.sol @@ -45,10 +45,18 @@ struct PackedUserOperation { /** * @dev Aggregates and validates multiple signatures for a batch of user operations. + * + * A contract could implement this interface with custom validation schemes that allow signature aggregation, + * enabling significant optimizations and gas savings for execution and transaction data cost. + * + * Bundlers and clients whitelist supported aggregators. + * + * See https://eips.ethereum.org/EIPS/eip-7766[ERC-7766] */ interface IAggregator { /** * @dev Validates the signature for a user operation. + * Returns an alternative signature that should be used during bundling. */ function validateUserOpSignature( PackedUserOperation calldata userOp @@ -73,6 +81,12 @@ interface IAggregator { /** * @dev Handle nonce management for accounts. + * + * Nonces are used in accounts as a replay protection mechanism and to ensure the order of user operations. + * To avoid limiting the number of operations an account can perform, the interface allows using parallel + * nonces by using a `key` parameter. + * + * See https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support[ERC-4337 semi-abstracted nonce support]. */ interface IEntryPointNonces { /** @@ -84,7 +98,11 @@ interface IEntryPointNonces { } /** - * @dev Handle stake management for accounts. + * @dev Handle stake management for entities (i.e. accounts, paymasters, factories). + * + * The EntryPoint must implement the following API to let entities like paymasters have a stake, + * and thus have more flexibility in their storage access + * (see https://eips.ethereum.org/EIPS/eip-4337#reputation-scoring-and-throttlingbanning-for-global-entities[reputation, throttling and banning.]) */ interface IEntryPointStake { /** @@ -120,6 +138,8 @@ interface IEntryPointStake { /** * @dev Entry point for user operations. + * + * User operations are validated and executed by this contract. */ interface IEntryPoint is IEntryPointNonces, IEntryPointStake { /** @@ -158,11 +178,23 @@ interface IEntryPoint is IEntryPointNonces, IEntryPointStake { } /** - * @dev Base interface for an account. + * @dev Base interface for an ERC-4337 account. */ interface IAccount { /** * @dev Validates a user operation. + * + * * MUST validate the caller is a trusted EntryPoint + * * MUST validate that the signature is a valid signature of the userOpHash, and SHOULD + * return SIG_VALIDATION_FAILED (and not revert) on signature mismatch. Any other error MUST revert. + * * MUST pay the entryPoint (caller) at least the βmissingAccountFundsβ (which might + * be zero, in case the current accountβs deposit is high enough) + * + * Returns an encoded packed validation data that is composed of the following elements: + * + * - `authorizer` (`address`): 0 for success, 1 for failure, otherwise the address of an authorizer contract + * - `validUntil` (`uint48`): The UserOp is valid only up to this time. Zero for βinfiniteβ. + * - `validAfter` (`uint48`): The UserOp is valid only after this time. */ function validateUserOp( PackedUserOperation calldata userOp, @@ -195,7 +227,8 @@ interface IPaymaster { } /** - * @dev Validates whether the paymaster is willing to pay for the user operation. + * @dev Validates whether the paymaster is willing to pay for the user operation. See + * {IAccount-validateUserOp} for additional information on the return value. * * NOTE: Bundlers will reject this method if it modifies the state, unless it's whitelisted. */ @@ -207,6 +240,8 @@ interface IPaymaster { /** * @dev Verifies the sender is the entrypoint. + * @param actualGasCost the actual amount paid (by account or paymaster) for this UserOperation + * @param actualUserOpFeePerGas total gas used by this UserOperation (including preVerification, creation, validation and execution) */ function postOp( PostOpMode mode, diff --git a/contracts/interfaces/draft-IERC7579.sol b/contracts/interfaces/draft-IERC7579.sol index 61a862377f1..f97e33dc045 100644 --- a/contracts/interfaces/draft-IERC7579.sol +++ b/contracts/interfaces/draft-IERC7579.sol @@ -50,7 +50,7 @@ interface IERC7579Validator is IERC7579Module { * * MUST validate that the signature is a valid signature of the userOpHash * SHOULD return ERC-4337's SIG_VALIDATION_FAILED (and not revert) on signature mismatch - * See ERC-4337 for additional information on the return value + * See {IAccount-validateUserOp} for additional information on the return value */ function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256); @@ -127,6 +127,7 @@ interface IERC7579Execution { * This function is intended to be called by Executor Modules * @param mode The encoded execution mode of the transaction. See ModeLib.sol for details * @param executionCalldata The encoded execution call data + * @return returnData An array with the returned data of each executed subcall * * MUST ensure adequate authorization control: i.e. onlyExecutorModule * If a mode is requested that is not supported by the Account, it MUST revert @@ -140,7 +141,7 @@ interface IERC7579Execution { /** * @dev ERC-7579 Account Config. * - * Accounts should implement this interface to exposes information that identifies the account, supported modules and capabilities. + * Accounts should implement this interface to expose information that identifies the account, supported modules and capabilities. */ interface IERC7579AccountConfig { /** @@ -174,7 +175,7 @@ interface IERC7579AccountConfig { /** * @dev ERC-7579 Module Config. * - * Accounts should implement this interface to allows installing and uninstalling modules. + * Accounts should implement this interface to allow installing and uninstalling modules. */ interface IERC7579ModuleConfig { event ModuleInstalled(uint256 moduleTypeId, address module); diff --git a/contracts/mocks/governance/GovernorSequentialProposalIdMock.sol b/contracts/mocks/governance/GovernorSequentialProposalIdMock.sol new file mode 100644 index 00000000000..882ee0b2483 --- /dev/null +++ b/contracts/mocks/governance/GovernorSequentialProposalIdMock.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Governor} from "../../governance/Governor.sol"; +import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol"; +import {GovernorCountingSimple} from "../../governance/extensions/GovernorCountingSimple.sol"; +import {GovernorVotesQuorumFraction} from "../../governance/extensions/GovernorVotesQuorumFraction.sol"; +import {GovernorSequentialProposalId} from "../../governance/extensions/GovernorSequentialProposalId.sol"; + +abstract contract GovernorSequentialProposalIdMock is + GovernorSettings, + GovernorVotesQuorumFraction, + GovernorCountingSimple, + GovernorSequentialProposalId +{ + function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) { + return super.proposalThreshold(); + } + + function getProposalId( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public view virtual override(Governor, GovernorSequentialProposalId) returns (uint256) { + return super.getProposalId(targets, values, calldatas, descriptionHash); + } + + function _propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description, + address proposer + ) internal virtual override(Governor, GovernorSequentialProposalId) returns (uint256 proposalId) { + return super._propose(targets, values, calldatas, description, proposer); + } +} diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol index 6bef4b4845e..5bd45a0864b 100644 --- a/contracts/proxy/Clones.sol +++ b/contracts/proxy/Clones.sol @@ -163,7 +163,7 @@ library Clones { * access the arguments within the implementation, use {fetchCloneArgs}. * * This function uses the create2 opcode and a `salt` to deterministically deploy the clone. Using the same - * `implementation`, `args` and `salt` multiple time will revert, since the clones cannot be deployed twice + * `implementation`, `args` and `salt` multiple times will revert, since the clones cannot be deployed twice * at the same address. */ function cloneDeterministicWithImmutableArgs( @@ -227,9 +227,9 @@ library Clones { * function should only be used to check addresses that are known to be clones. */ function fetchCloneArgs(address instance) internal view returns (bytes memory) { - bytes memory result = new bytes(instance.code.length - 0x2d); // revert if length is too short + bytes memory result = new bytes(instance.code.length - 45); // revert if length is too short assembly ("memory-safe") { - extcodecopy(instance, add(result, 0x20), 0x2d, mload(result)) + extcodecopy(instance, add(result, 32), 45, mload(result)) } return result; } @@ -248,11 +248,11 @@ library Clones { address implementation, bytes memory args ) private pure returns (bytes memory) { - if (args.length > 0x5fd3) revert CloneArgumentsTooLong(); + if (args.length > 24531) revert CloneArgumentsTooLong(); return abi.encodePacked( hex"61", - uint16(args.length + 0x2d), + uint16(args.length + 45), hex"3d81600a3d39f3363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3", diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index acc841d78fc..952582a8404 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -48,7 +48,8 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @dev Moves a `value` amount of tokens from the caller's account to `to` - * and then calls {IERC1363Receiver-onTransferReceived} on `to`. + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. Returns a flag that indicates + * if the call succeeded. * * Requirements: * @@ -75,7 +76,8 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism - * and then calls {IERC1363Receiver-onTransferReceived} on `to`. + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. Returns a flag that indicates + * if the call succeeded. * * Requirements: * @@ -108,6 +110,7 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @dev Sets a `value` amount of tokens as the allowance of `spender` over the * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. + * Returns a flag that indicates if the call succeeded. * * Requirements: * diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index ec9a255076c..338b71d62c5 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -114,7 +114,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { /** @dev See {IERC4626-totalAssets}. */ function totalAssets() public view virtual returns (uint256) { - return _asset.balanceOf(address(this)); + return IERC20(asset()).balanceOf(address(this)); } /** @dev See {IERC4626-convertToShares}. */ @@ -237,14 +237,14 @@ abstract contract ERC4626 is ERC20, IERC4626 { * @dev Deposit/mint common workflow. */ function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual { - // If _asset is ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the + // If asset() is ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer, // calls the vault, which is assumed not malicious. // // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the // assets are transferred and before the shares are minted, which is a valid state. // slither-disable-next-line reentrancy-no-eth - SafeERC20.safeTransferFrom(_asset, caller, address(this), assets); + SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(this), assets); _mint(receiver, shares); emit Deposit(caller, receiver, assets, shares); @@ -264,14 +264,14 @@ abstract contract ERC4626 is ERC20, IERC4626 { _spendAllowance(owner, caller, shares); } - // If _asset is ERC-777, `transfer` can trigger a reentrancy AFTER the transfer happens through the + // If asset() is ERC-777, `transfer` can trigger a reentrancy AFTER the transfer happens through the // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer, // calls the vault, which is assumed not malicious. // // Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the // shares are burned and after the assets are transferred, which is a valid state. _burn(owner, shares); - SafeERC20.safeTransfer(_asset, receiver, assets); + SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); emit Withdraw(caller, receiver, owner, assets, shares); } diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index 5554720d699..b5aa6579808 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -22,7 +22,7 @@ OpenZeppelin Contracts provides implementations of all four interfaces: Additionally there are a few of other extensions: -* {ERC721Consecutive}: An implementation of https://eips.ethereum.org/EIPS/eip-2309[ERC-2309] for minting batchs of tokens during construction, in accordance with ERC-721. +* {ERC721Consecutive}: An implementation of https://eips.ethereum.org/EIPS/eip-2309[ERC-2309] for minting batches of tokens during construction, in accordance with ERC-721. * {ERC721URIStorage}: A more flexible but more expensive way of storing metadata. * {ERC721Votes}: Support for voting and vote delegation. * {ERC721Royalty}: A way to signal royalty information following ERC-2981. diff --git a/contracts/utils/Address.sol b/contracts/utils/Address.sol index b6b10ffb6fc..c255035a01b 100644 --- a/contracts/utils/Address.sol +++ b/contracts/utils/Address.sol @@ -35,9 +35,9 @@ library Address { revert Errors.InsufficientBalance(address(this).balance, amount); } - (bool success, ) = recipient.call{value: amount}(""); + (bool success, bytes memory returndata) = recipient.call{value: amount}(""); if (!success) { - revert Errors.FailedCall(); + _revert(returndata); } } diff --git a/contracts/utils/NoncesKeyed.sol b/contracts/utils/NoncesKeyed.sol index 27e0685b3a3..31cd0704e15 100644 --- a/contracts/utils/NoncesKeyed.sol +++ b/contracts/utils/NoncesKeyed.sol @@ -7,6 +7,10 @@ import {Nonces} from "./Nonces.sol"; * @dev Alternative to {Nonces}, that supports key-ed nonces. * * Follows the https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support[ERC-4337's semi-abstracted nonce system]. + * + * NOTE: This contract inherits from {Nonces} and reuses its storage for the first nonce key (i.e. `0`). This + * makes upgrading from {Nonces} to {NoncesKeyed} safe when using their upgradeable versions (e.g. `NoncesKeyedUpgradeable`). + * Doing so will NOT reset the current state of nonces, avoiding replay attacks where a nonce is reused after the upgrade. */ abstract contract NoncesKeyed is Nonces { mapping(address owner => mapping(uint192 key => uint64)) private _nonces; diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 432b806e3c4..8f08a86987f 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -42,7 +42,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Context}: A utility for abstracting the sender and calldata in the current execution context. * {Packing}: A library for packing and unpacking multiple values into bytes32 * {Panic}: A library to revert with https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require[Solidity panic codes]. - * {Comparators}: A library that contains comparator functions to use with with the {Heap} library. + * {Comparators}: A library that contains comparator functions to use with the {Heap} library. * {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers. [NOTE] diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 4cad8a2c5a0..9611115ace1 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -177,7 +177,8 @@ library Strings { } /** - * @dev Variant of {tryParseUint} that does not check bounds and returns (true, 0) if they are invalid. + * @dev Implementation of {tryParseUint} that does not check bounds. Caller should make sure that + * `begin <= end <= input.length`. Other inputs would result in undefined behavior. */ function _tryParseUintUncheckedBounds( string memory input, @@ -249,7 +250,8 @@ library Strings { } /** - * @dev Variant of {tryParseInt} that does not check bounds and returns (true, 0) if they are invalid. + * @dev Implementation of {tryParseInt} that does not check bounds. Caller should make sure that + * `begin <= end <= input.length`. Other inputs would result in undefined behavior. */ function _tryParseIntUncheckedBounds( string memory input, @@ -323,7 +325,8 @@ library Strings { } /** - * @dev Variant of {tryParseHexUint} that does not check bounds and returns (true, 0) if they are invalid. + * @dev Implementation of {tryParseHexUint} that does not check bounds. Caller should make sure that + * `begin <= end <= input.length`. Other inputs would result in undefined behavior. */ function _tryParseHexUintUncheckedBounds( string memory input, @@ -333,7 +336,7 @@ library Strings { bytes memory buffer = bytes(input); // skip 0x prefix if present - bool hasPrefix = (begin < end + 1) && bytes2(_unsafeReadBytesOffset(buffer, begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty + bool hasPrefix = (end > begin + 1) && bytes2(_unsafeReadBytesOffset(buffer, begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty uint256 offset = hasPrefix.toUint() * 2; uint256 result = 0; @@ -390,12 +393,13 @@ library Strings { uint256 begin, uint256 end ) internal pure returns (bool success, address value) { - // check that input is the correct length - bool hasPrefix = (begin < end + 1) && bytes2(_unsafeReadBytesOffset(bytes(input), begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty + if (end > bytes(input).length || begin > end) return (false, address(0)); + bool hasPrefix = (end > begin + 1) && bytes2(_unsafeReadBytesOffset(bytes(input), begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty uint256 expectedLength = 40 + hasPrefix.toUint() * 2; - if (end - begin == expectedLength && end <= bytes(input).length) { + // check that input is the correct length + if (end - begin == expectedLength) { // length guarantees that this does not overflow, and value is at most type(uint160).max (bool s, uint256 v) = _tryParseHexUintUncheckedBounds(input, begin, end); return (s, address(uint160(v))); diff --git a/docs/modules/ROOT/pages/erc1155.adoc b/docs/modules/ROOT/pages/erc1155.adoc index 7f00f3ea4b1..2c31db8b86c 100644 --- a/docs/modules/ROOT/pages/erc1155.adoc +++ b/docs/modules/ROOT/pages/erc1155.adoc @@ -106,7 +106,7 @@ A key difference when using xref:api:token/ERC1155.adoc#IERC1155-safeTransferFro ERC1155InvalidReceiver("
") ---- -This is a good thing! It means that the recipient contract has not registered itself as aware of the ERC-1155 protocol, so transfers to it are disabled to *prevent tokens from being locked forever*. As an example, https://etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d?a=0xa74476443119A942dE498590Fe1f2454d7D4aC0d[the Golem contract currently holds over 350k `GNT` tokens], worth multiple tens of thousands of dollars, and lacks methods to get them out of there. This has happened to virtually every ERC20-backed project, usually due to user error. +This is a good thing! It means that the recipient contract has not registered itself as aware of the ERC-1155 protocol, so transfers to it are disabled to *prevent tokens from being locked forever*. As an example, https://etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d?a=0xa74476443119A942dE498590Fe1f2454d7D4aC0d[the Golem contract currently holds over 350k `GNT` tokens], and lacks methods to get them out of there. This has happened to virtually every ERC20-backed project, usually due to user error. In order for our contract to receive ERC-1155 tokens we can inherit from the convenience contract xref:api:token/ERC1155.adoc#ERC1155Holder[`ERC1155Holder`] which handles the registering for us. However, we need to remember to implement functionality to allow tokens to be transferred out of our contract: diff --git a/foundry.toml b/foundry.toml index 3f60b7cbbf1..78dd0781224 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,6 +8,7 @@ out = 'out' libs = ['node_modules', 'lib'] test = 'test' cache_path = 'cache_forge' +fs_permissions = [{ access = "read", path = "./test/bin" }] [fuzz] runs = 5000 diff --git a/fv-requirements.txt b/fv-requirements.txt index 608b4de24a3..80917f2886a 100644 --- a/fv-requirements.txt +++ b/fv-requirements.txt @@ -1,4 +1,4 @@ certora-cli==4.13.1 # File uses a custom name (fv-requirements.txt) so that it isn't picked by Netlify's build # whose latest Python version is 0.3.8, incompatible with most recent versions of Halmos -halmos==0.2.0 +halmos==0.2.3 diff --git a/package-lock.json b/package-lock.json index bd5171b13f4..2b7f5083416 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,14 +24,16 @@ "chai": "^4.2.0", "eslint": "^9.0.0", "eslint-config-prettier": "^9.0.0", - "ethers": "^6.7.1", + "ethers": "^6.13.4", "glob": "^11.0.0", "globals": "^15.3.0", "graphlib": "^2.1.8", "hardhat": "^2.22.2", "hardhat-exposed": "^0.3.15", - "hardhat-gas-reporter": "^2.0.0", + "hardhat-gas-reporter": "^2.1.0", "hardhat-ignore-warnings": "^0.2.11", + "husky": "^9.1.7", + "lint-staged": "^15.2.10", "lodash.startcase": "^4.4.0", "micromatch": "^4.0.2", "p-limit": "^6.0.0", @@ -44,7 +46,7 @@ "solidity-ast": "^0.4.50", "solidity-coverage": "^0.8.5", "solidity-docgen": "^0.6.0-beta.29", - "undici": "^6.11.1", + "undici": "^7.0.0", "yargs": "^17.0.0" } }, @@ -58,10 +60,11 @@ } }, "node_modules/@adraffy/ens-normalize": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz", - "integrity": "sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg==", - "dev": true + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "dev": true, + "license": "MIT" }, "node_modules/@babel/code-frame": { "version": "7.22.13", @@ -2609,12 +2612,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", - "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/normalize-package-data": { @@ -3587,6 +3591,65 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3648,6 +3711,12 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4145,6 +4214,18 @@ "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -4852,9 +4933,9 @@ "dev": true }, "node_modules/ethers": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.7.1.tgz", - "integrity": "sha512-qX5kxIFMfg1i+epfgb0xF4WM7IqapIIu50pOJ17aebkxxa4BacW5jFrQRmCJpDEg2ZK2oNtR5QjrQ1WDBF29dA==", + "version": "6.13.4", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.4.tgz", + "integrity": "sha512-21YtnZVg4/zKkCQPjrDj38B1r4nQvTZLopUGMLQ1ePU2zV/joCfDC3t3iKQjWRzjjjbzR+mdAIoikeBRNkdllA==", "dev": true, "funding": [ { @@ -4866,36 +4947,45 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@adraffy/ens-normalize": "1.9.2", - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.7.1", - "@types/node": "18.15.13", + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", "aes-js": "4.0.0-beta.5", - "tslib": "2.4.0", - "ws": "8.5.0" + "tslib": "2.7.0", + "ws": "8.17.1" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/ethers/node_modules/@noble/hashes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", - "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, - "node_modules/ethers/node_modules/@types/node": { - "version": "18.15.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", - "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", - "dev": true + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/ethers/node_modules/aes-js": { "version": "4.0.0-beta.5", @@ -4904,22 +4994,24 @@ "dev": true }, "node_modules/ethers/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD" }, "node_modules/ethers/node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -4964,6 +5056,12 @@ "npm": ">=3" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -4974,6 +5072,53 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/extendable-error": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", @@ -5286,6 +5431,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -5684,6 +5841,7 @@ "resolved": "https://registry.npmjs.org/hardhat-gas-reporter/-/hardhat-gas-reporter-2.1.0.tgz", "integrity": "sha512-d/WU/qHhBFnbweAm2fAAjcaaE0M7BKZ4r+/bqcFlfP6um28BXtlv2FrJ6oyQUGSFD0ttbmB7sH4ZFDzkYw5GzA==", "dev": true, + "license": "MIT", "dependencies": { "@ethersproject/abi": "^5.7.0", "@ethersproject/bytes": "^5.7.0", @@ -6475,6 +6633,30 @@ "integrity": "sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==", "dev": true }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6707,6 +6889,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6811,6 +7005,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -7094,92 +7300,248 @@ "node": ">= 0.8.0" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/load-yaml-file": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", - "integrity": "sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==", + "node_modules/lint-staged": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", + "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", "dev": true, "dependencies": { - "graceful-fs": "^4.1.5", - "js-yaml": "^3.13.0", - "pify": "^4.0.1", - "strip-bom": "^3.0.0" + "chalk": "~5.3.0", + "commander": "~12.1.0", + "debug": "~4.3.6", + "execa": "~8.0.1", + "lilconfig": "~3.1.2", + "listr2": "~8.2.4", + "micromatch": "~4.0.8", + "pidtree": "~0.6.0", + "string-argv": "~0.3.2", + "yaml": "~2.5.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": ">=6" + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, "engines": { - "node": ">=8" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lodash.startcase": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", - "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", - "dev": true - }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/lint-staged/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/lint-staged/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/lint-staged/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/load-yaml-file": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", + "integrity": "sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.13.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" }, "engines": { "node": ">=8" @@ -7243,6 +7605,127 @@ "node": ">=8" } }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -7357,6 +7840,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7373,12 +7862,12 @@ "dev": true }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -7406,6 +7895,30 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", @@ -7963,6 +8476,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/number-to-bn": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", @@ -8034,6 +8574,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -8320,6 +8875,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -8861,6 +9428,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -8880,6 +9490,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, "node_modules/rimraf": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.0.tgz", @@ -10218,6 +10834,32 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", @@ -10242,6 +10884,39 @@ "node": ">=8" } }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", @@ -10321,6 +10996,18 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-hex-prefix": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", @@ -10807,19 +11494,21 @@ } }, "node_modules/undici": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.12.0.tgz", - "integrity": "sha512-d87yk8lqSFUYtR5fTFe2frpkMIrUEz+lgoJmhcL+J3StVl+8fj8ytE4lLnJOTPCE12YbumNGzf4LYsQyusdV5g==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.0.0.tgz", + "integrity": "sha512-c4xi3kWnQJrb7h2q8aJYKvUzmz7boCgz1cUCC6OwdeM5Tr2P0hDuthr2iut4ggqsz+Cnh20U/LoTzbKIdDS/Nw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=18.0" + "node": ">=20.18.1" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/universalify": { "version": "0.1.2", @@ -11338,10 +12027,11 @@ "dev": true }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.3.0" }, @@ -11367,6 +12057,18 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -11502,7 +12204,10 @@ "scripts/solhint-custom": { "name": "solhint-plugin-openzeppelin", "version": "0.0.0", - "dev": true + "dev": true, + "dependencies": { + "minimatch": "^3.1.2" + } } } } diff --git a/package.json b/package.json index 44c697ac7ef..afdd5e6c10a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "coverage": "scripts/checks/coverage.sh", "docs": "npm run prepare-docs && oz-docs", "docs:watch": "oz-docs watch contracts docs/templates docs/config.js", - "prepare": "scripts/prepare.sh", + "prepare": "husky", "prepare-docs": "scripts/prepare-docs.sh", "lint": "npm run lint:js && npm run lint:sol", "lint:fix": "npm run lint:js:fix && npm run lint:sol:fix", @@ -25,7 +25,7 @@ "prepack": "scripts/prepack.sh", "generate": "scripts/generate/run.js", "version": "scripts/release/version.sh", - "test": "hardhat test", + "test": "scripts/set-max-old-space-size.sh && hardhat test", "test:generation": "scripts/checks/generation.sh", "test:inheritance": "scripts/checks/inheritance-ordering.js artifacts/build-info/*", "test:pragma": "scripts/checks/pragma-consistency.js artifacts/build-info/*", @@ -66,14 +66,16 @@ "chai": "^4.2.0", "eslint": "^9.0.0", "eslint-config-prettier": "^9.0.0", - "ethers": "^6.7.1", - "globals": "^15.3.0", + "ethers": "^6.13.4", "glob": "^11.0.0", + "globals": "^15.3.0", "graphlib": "^2.1.8", "hardhat": "^2.22.2", "hardhat-exposed": "^0.3.15", - "hardhat-gas-reporter": "^2.0.0", + "hardhat-gas-reporter": "^2.1.0", "hardhat-ignore-warnings": "^0.2.11", + "husky": "^9.1.7", + "lint-staged": "^15.2.10", "lodash.startcase": "^4.4.0", "micromatch": "^4.0.2", "p-limit": "^6.0.0", @@ -86,7 +88,17 @@ "solidity-ast": "^0.4.50", "solidity-coverage": "^0.8.5", "solidity-docgen": "^0.6.0-beta.29", - "undici": "^6.11.1", + "undici": "^7.0.0", "yargs": "^17.0.0" + }, + "lint-staged": { + "*.{js,ts}": [ + "prettier --log-level warn --ignore-path .gitignore --check", + "eslint" + ], + "*.sol": [ + "prettier --log-level warn --ignore-path .gitignore --check", + "solhint" + ] } } diff --git a/scripts/checks/coverage.sh b/scripts/checks/coverage.sh index a591069c493..e3b8ffc56b0 100755 --- a/scripts/checks/coverage.sh +++ b/scripts/checks/coverage.sh @@ -5,6 +5,8 @@ set -euo pipefail export COVERAGE=true export FOUNDRY_FUZZ_RUNS=10 +scripts/set-max-old-space-size.sh + # Hardhat coverage hardhat coverage diff --git a/scripts/checks/inheritance-ordering.js b/scripts/checks/inheritance-ordering.js index 4ed2deec454..fbeac9ea7c4 100755 --- a/scripts/checks/inheritance-ordering.js +++ b/scripts/checks/inheritance-ordering.js @@ -31,7 +31,7 @@ for (const artifact of artifacts) { } /// graphlib.alg.findCycles will not find minimal cycles. - /// We are only interested int cycles of lengths 2 (needs proof) + /// We are only interested in cycles of lengths 2 (needs proof) graph.nodes().forEach((x, i, nodes) => nodes .slice(i + 1) diff --git a/scripts/checks/pragma-consistency.js b/scripts/checks/pragma-consistency.js index f2f3c548f59..cf74cd27a72 100755 --- a/scripts/checks/pragma-consistency.js +++ b/scripts/checks/pragma-consistency.js @@ -31,7 +31,7 @@ for (const artifact of artifacts) { const minVersion = semver.minVersion(pragma[source]); // loop over all imports in source for (const { absolutePath } of findAll('ImportDirective', solcOutput.sources[source].ast)) { - // So files that only import without declaring anything cause issues, because they don't shop in in "pragma" + // So files that only import without declaring anything cause issues, because they don't shop in "pragma" if (!pragma[absolutePath]) continue; // Check that the minVersion for source satisfies the requirements of the imported files if (!semver.satisfies(minVersion, pragma[absolutePath])) { diff --git a/scripts/prepare.sh b/scripts/prepare.sh deleted file mode 100755 index a7d74227d26..00000000000 --- a/scripts/prepare.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -if git status &>/dev/null; then git config core.hooksPath .githooks; fi diff --git a/scripts/set-max-old-space-size.sh b/scripts/set-max-old-space-size.sh new file mode 100755 index 00000000000..525c6786418 --- /dev/null +++ b/scripts/set-max-old-space-size.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# This script sets the node `--max-old-space-size` to 8192 if it is not set already. +# All existing `NODE_OPTIONS` are retained as is. + +export NODE_OPTIONS="${NODE_OPTIONS:-}" + +if [[ $NODE_OPTIONS != *"--max-old-space-size"* ]]; then + export NODE_OPTIONS="${NODE_OPTIONS} --max-old-space-size=8192" +fi diff --git a/scripts/solhint-custom/package.json b/scripts/solhint-custom/package.json index 075eb929dea..ce9690d74ef 100644 --- a/scripts/solhint-custom/package.json +++ b/scripts/solhint-custom/package.json @@ -1,5 +1,8 @@ { "name": "solhint-plugin-openzeppelin", "version": "0.0.0", - "private": true + "private": true, + "dependencies": { + "minimatch": "^3.1.2" + } } diff --git a/slither.config.json b/slither.config.json index fa52f4dd1dd..47892af079d 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,5 +1,4 @@ { "detectors_to_run": "arbitrary-send-erc20,array-by-reference,incorrect-shift,name-reused,rtlo,suicidal,uninitialized-state,uninitialized-storage,arbitrary-send-erc20-permit,controlled-array-length,controlled-delegatecall,delegatecall-loop,msg-value-loop,reentrancy-eth,unchecked-transfer,weak-prng,domain-separator-collision,erc20-interface,erc721-interface,locked-ether,mapping-deletion,shadowing-abstract,tautology,write-after-write,boolean-cst,reentrancy-no-eth,reused-constructor,tx-origin,unchecked-lowlevel,unchecked-send,variable-scope,void-cst,events-access,events-maths,incorrect-unary,boolean-equal,cyclomatic-complexity,deprecated-standards,erc20-indexed,function-init-state,pragma,unused-state,reentrancy-unlimited-gas,constable-states,immutable-states,var-read-using-this", - "filter_paths": "contracts/mocks,contracts/vendor,contracts-exposed", - "compile_force_framework": "hardhat" + "filter_paths": "contracts/mocks,contracts/vendor,contracts-exposed" } diff --git a/solhint.config.js b/solhint.config.js index f0bd7994f97..47c1cebfe25 100644 --- a/solhint.config.js +++ b/solhint.config.js @@ -1,4 +1,4 @@ -const customRules = require('./scripts/solhint-custom'); +const customRules = require('solhint-plugin-openzeppelin'); const rules = [ 'avoid-tx-origin', diff --git a/test/TESTING.md b/test/TESTING.md index a5ee9323ff4..321c7e592d6 100644 --- a/test/TESTING.md +++ b/test/TESTING.md @@ -1,3 +1,3 @@ ## Testing -Unit test are critical to OpenZeppelin Contracts. They help ensure code quality and mitigate against security vulnerabilities. The directory structure within the `/test` directory corresponds to the `/contracts` directory. +Unit tests are critical to OpenZeppelin Contracts. They help ensure code quality and mitigate against security vulnerabilities. The directory structure within the `/test` directory corresponds to the `/contracts` directory. diff --git a/test/account/utils/draft-ERC4337Utils.test.js b/test/account/utils/draft-ERC4337Utils.test.js index 96310ed83e3..7c292910dfb 100644 --- a/test/account/utils/draft-ERC4337Utils.test.js +++ b/test/account/utils/draft-ERC4337Utils.test.js @@ -3,12 +3,17 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { packValidationData, UserOperation } = require('../../helpers/erc4337'); +const { deployEntrypoint } = require('../../helpers/erc4337-entrypoint'); const { MAX_UINT48 } = require('../../helpers/constants'); +const ADDRESS_ONE = '0x0000000000000000000000000000000000000001'; const fixture = async () => { - const [authorizer, sender, entrypoint, factory, paymaster] = await ethers.getSigners(); + const { entrypoint } = await deployEntrypoint(); + const [authorizer, sender, factory, paymaster] = await ethers.getSigners(); const utils = await ethers.deployContract('$ERC4337Utils'); - return { utils, authorizer, sender, entrypoint, factory, paymaster }; + const SIG_VALIDATION_SUCCESS = await utils.$SIG_VALIDATION_SUCCESS(); + const SIG_VALIDATION_FAILED = await utils.$SIG_VALIDATION_FAILED(); + return { utils, authorizer, sender, entrypoint, factory, paymaster, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED }; }; describe('ERC4337Utils', function () { @@ -41,6 +46,20 @@ describe('ERC4337Utils', function () { MAX_UINT48, ]); }); + + it('parse canonical values', async function () { + expect(this.utils.$parseValidationData(this.SIG_VALIDATION_SUCCESS)).to.eventually.deep.equal([ + ethers.ZeroAddress, + 0n, + MAX_UINT48, + ]); + + expect(this.utils.$parseValidationData(this.SIG_VALIDATION_FAILED)).to.eventually.deep.equal([ + ADDRESS_ONE, + 0n, + MAX_UINT48, + ]); + }); }); describe('packValidationData', function () { @@ -65,6 +84,21 @@ describe('ERC4337Utils', function () { validationData, ); }); + + it('packing reproduced canonical values', async function () { + expect(this.utils.$packValidationData(ethers.Typed.address(ethers.ZeroAddress), 0n, 0n)).to.eventually.equal( + this.SIG_VALIDATION_SUCCESS, + ); + expect(this.utils.$packValidationData(ethers.Typed.bool(true), 0n, 0n)).to.eventually.equal( + this.SIG_VALIDATION_SUCCESS, + ); + expect(this.utils.$packValidationData(ethers.Typed.address(ADDRESS_ONE), 0n, 0n)).to.eventually.equal( + this.SIG_VALIDATION_FAILED, + ); + expect(this.utils.$packValidationData(ethers.Typed.bool(false), 0n, 0n)).to.eventually.equal( + this.SIG_VALIDATION_FAILED, + ); + }); }); describe('combineValidationData', function () { @@ -135,11 +169,19 @@ describe('ERC4337Utils', function () { describe('hash', function () { it('returns the operation hash with specified entrypoint and chainId', async function () { const userOp = new UserOperation({ sender: this.sender, nonce: 1 }); - const chainId = 0xdeadbeef; + const chainId = await ethers.provider.getNetwork().then(({ chainId }) => chainId); + const otherChainId = 0xdeadbeef; + + // check that helper matches entrypoint logic + expect(this.entrypoint.getUserOpHash(userOp.packed)).to.eventually.equal(userOp.hash(this.entrypoint, chainId)); + // check library against helper expect(this.utils.$hash(userOp.packed, this.entrypoint, chainId)).to.eventually.equal( userOp.hash(this.entrypoint, chainId), ); + expect(this.utils.$hash(userOp.packed, this.entrypoint, otherChainId)).to.eventually.equal( + userOp.hash(this.entrypoint, otherChainId), + ); }); }); diff --git a/test/account/utils/draft-ERC7579Utils.t.sol b/test/account/utils/draft-ERC7579Utils.t.sol new file mode 100644 index 00000000000..fdd4edf5958 --- /dev/null +++ b/test/account/utils/draft-ERC7579Utils.t.sol @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +// Parts of this test file are adapted from Adam Egyed (@adamegyed) proof of concept available at: +// https://github.com/adamegyed/erc7579-execute-vulnerability/tree/4589a30ff139e143d6c57183ac62b5c029217a90 +// +// solhint-disable no-console + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {PackedUserOperation, IAccount, IEntryPoint} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {ERC7579Utils, Mode, CallType, ExecType, ModeSelector, ModePayload, Execution} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol"; +import {Test, Vm, console} from "forge-std/Test.sol"; + +contract SampleAccount is IAccount, Ownable { + using ECDSA for *; + using MessageHashUtils for *; + using ERC4337Utils for *; + using ERC7579Utils for *; + + IEntryPoint internal constant ENTRY_POINT = IEntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); + + event Log(bool duringValidation, Execution[] calls); + + error UnsupportedCallType(CallType callType); + + constructor(address initialOwner) Ownable(initialOwner) {} + + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external override returns (uint256 validationData) { + require(msg.sender == address(ENTRY_POINT), "only from EP"); + // Check signature + if (userOpHash.toEthSignedMessageHash().recover(userOp.signature) != owner()) { + revert OwnableUnauthorizedAccount(_msgSender()); + } + + // If this is an execute call with a batch operation, log the call details from the calldata + if (bytes4(userOp.callData[0x00:0x04]) == this.execute.selector) { + (CallType callType, , , ) = Mode.wrap(bytes32(userOp.callData[0x04:0x24])).decodeMode(); + + if (callType == ERC7579Utils.CALLTYPE_BATCH) { + // Remove the selector + bytes calldata params = userOp.callData[0x04:]; + + // Use the same vulnerable assignment technique here, but assert afterwards that the checks aren't + // broken here by comparing to the result of `abi.decode(...)`. + bytes calldata executionCalldata; + assembly ("memory-safe") { + let dataptr := add(params.offset, calldataload(add(params.offset, 0x20))) + executionCalldata.offset := add(dataptr, 32) + executionCalldata.length := calldataload(dataptr) + } + // Check that this decoding step is done correctly. + (, bytes memory executionCalldataMemory) = abi.decode(params, (bytes32, bytes)); + + require( + keccak256(executionCalldata) == keccak256(executionCalldataMemory), + "decoding during validation failed" + ); + // Now, we know that we have `bytes calldata executionCalldata` as would be decoded by the solidity + // builtin decoder for the `execute` function. + + // This is where the vulnerability from ExecutionLib results in a different result between validation + // andexecution. + + emit Log(true, executionCalldata.decodeBatch()); + } + } + + if (missingAccountFunds > 0) { + (bool success, ) = payable(msg.sender).call{value: missingAccountFunds}(""); + success; // Silence warning. The entrypoint should validate the result. + } + + return ERC4337Utils.SIG_VALIDATION_SUCCESS; + } + + function execute(Mode mode, bytes calldata executionCalldata) external payable { + require(msg.sender == address(this) || msg.sender == address(ENTRY_POINT), "not auth"); + + (CallType callType, ExecType execType, , ) = mode.decodeMode(); + + // check if calltype is batch or single + if (callType == ERC7579Utils.CALLTYPE_SINGLE) { + executionCalldata.execSingle(execType); + } else if (callType == ERC7579Utils.CALLTYPE_BATCH) { + executionCalldata.execBatch(execType); + + emit Log(false, executionCalldata.decodeBatch()); + } else if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) { + executionCalldata.execDelegateCall(execType); + } else { + revert UnsupportedCallType(callType); + } + } +} + +contract ERC7579UtilsTest is Test { + using MessageHashUtils for *; + using ERC4337Utils for *; + using ERC7579Utils for *; + + IEntryPoint private constant ENTRYPOINT = IEntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); + address private _owner; + uint256 private _ownerKey; + address private _account; + address private _beneficiary; + address private _recipient1; + address private _recipient2; + + constructor() { + vm.etch(0x0000000071727De22E5E9d8BAf0edAc6f37da032, vm.readFileBinary("test/bin/EntryPoint070.bytecode")); + vm.etch(0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C, vm.readFileBinary("test/bin/SenderCreator070.bytecode")); + + // signing key + (_owner, _ownerKey) = makeAddrAndKey("owner"); + + // ERC-4337 account + _account = address(new SampleAccount(_owner)); + vm.deal(_account, 1 ether); + + // other + _beneficiary = makeAddr("beneficiary"); + _recipient1 = makeAddr("recipient1"); + _recipient2 = makeAddr("recipient2"); + } + + function testExecuteBatchDecodeCorrectly() public { + Execution[] memory calls = new Execution[](2); + calls[0] = Execution({target: _recipient1, value: 1 wei, callData: ""}); + calls[1] = Execution({target: _recipient2, value: 1 wei, callData: ""}); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = PackedUserOperation({ + sender: _account, + nonce: 0, + initCode: "", + callData: abi.encodeCall( + SampleAccount.execute, + ( + ERC7579Utils.encodeMode( + ERC7579Utils.CALLTYPE_BATCH, + ERC7579Utils.EXECTYPE_DEFAULT, + ModeSelector.wrap(0x00), + ModePayload.wrap(0x00) + ), + ERC7579Utils.encodeBatch(calls) + ) + ), + accountGasLimits: _packGas(500_000, 500_000), + preVerificationGas: 0, + gasFees: _packGas(1, 1), + paymasterAndData: "", + signature: "" + }); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + _ownerKey, + this.hashUserOperation(userOps[0]).toEthSignedMessageHash() + ); + userOps[0].signature = abi.encodePacked(r, s, v); + + vm.recordLogs(); + ENTRYPOINT.handleOps(userOps, payable(_beneficiary)); + + assertEq(_recipient1.balance, 1 wei); + assertEq(_recipient2.balance, 1 wei); + + _collectAndPrintLogs(false); + } + + function testExecuteBatchDecodeEmpty() public { + bytes memory fakeCalls = abi.encodePacked( + uint256(1), // Length of execution[] + uint256(0x20), // offset + uint256(uint160(_recipient1)), // target + uint256(1), // value: 1 wei + uint256(0x60), // offset of data + uint256(0) // length of + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = PackedUserOperation({ + sender: _account, + nonce: 0, + initCode: "", + callData: abi.encodeCall( + SampleAccount.execute, + ( + ERC7579Utils.encodeMode( + ERC7579Utils.CALLTYPE_BATCH, + ERC7579Utils.EXECTYPE_DEFAULT, + ModeSelector.wrap(0x00), + ModePayload.wrap(0x00) + ), + abi.encodePacked( + uint256(0x70) // fake offset pointing to paymasterAndData + ) + ) + ), + accountGasLimits: _packGas(500_000, 500_000), + preVerificationGas: 0, + gasFees: _packGas(1, 1), + paymasterAndData: abi.encodePacked(address(0), fakeCalls), + signature: "" + }); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + _ownerKey, + this.hashUserOperation(userOps[0]).toEthSignedMessageHash() + ); + userOps[0].signature = abi.encodePacked(r, s, v); + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector) + ) + ); + ENTRYPOINT.handleOps(userOps, payable(_beneficiary)); + + _collectAndPrintLogs(false); + } + + function testExecuteBatchDecodeDifferent() public { + bytes memory execCallData = abi.encodePacked( + uint256(0x20), // offset pointing to the next segment + uint256(5), // Length of execution[] + uint256(0), // offset of calls[0], and target (!!) + uint256(0x20), // offset of calls[1], and value (!!) + uint256(0), // offset of calls[2], and rel offset of data (!!) + uint256(0) // offset of calls[3]. + // There is one more to read by the array length, but it's not present here. This will be + // paymasterAndData.length during validation, pointing to an all-zero call. + // During execution, the offset will be 0, pointing to a call with value. + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = PackedUserOperation({ + sender: _account, + nonce: 0, + initCode: "", + callData: abi.encodePacked( + SampleAccount.execute.selector, + ERC7579Utils.encodeMode( + ERC7579Utils.CALLTYPE_BATCH, + ERC7579Utils.EXECTYPE_DEFAULT, + ModeSelector.wrap(0x00), + ModePayload.wrap(0x00) + ), + uint256(0x5c), // offset pointing to the next segment + uint224(type(uint224).max), // Padding to align the `bytes` types + // type(uint256).max, // unknown padding + uint256(execCallData.length), // Length of the data + execCallData + ), + accountGasLimits: _packGas(500_000, 500_000), + preVerificationGas: 0, + gasFees: _packGas(1, 1), + paymasterAndData: abi.encodePacked(uint256(0), uint256(0)), // padding length to create an offset + signature: "" + }); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + _ownerKey, + this.hashUserOperation(userOps[0]).toEthSignedMessageHash() + ); + userOps[0].signature = abi.encodePacked(r, s, v); + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector) + ) + ); + ENTRYPOINT.handleOps(userOps, payable(_beneficiary)); + + _collectAndPrintLogs(true); + } + + function testDecodeBatch() public { + // BAD: buffer empty + vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector); + this.callDecodeBatch(""); + + // BAD: buffer too short + vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector); + this.callDecodeBatch(abi.encodePacked(uint128(0))); + + // GOOD + this.callDecodeBatch(abi.encode(0)); + // Note: Solidity also supports this even though it's odd. Offset 0 means array is at the same location, which + // is interpreted as an array of length 0, which doesn't require any more data + // solhint-disable-next-line var-name-mixedcase + uint256[] memory _1 = abi.decode(abi.encode(0), (uint256[])); + _1; + + // BAD: offset is out of bounds + vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector); + this.callDecodeBatch(abi.encode(1)); + + // GOOD + this.callDecodeBatch(abi.encode(32, 0)); + + // BAD: reported array length extends beyond bounds + vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector); + this.callDecodeBatch(abi.encode(32, 1)); + + // GOOD + this.callDecodeBatch(abi.encode(32, 1, 0)); + + // GOOD + // + // 0000000000000000000000000000000000000000000000000000000000000020 (32) offset + // 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length + // 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset + // 000000000000000000000000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (recipient) target for element #0 + // 000000000000000000000000000000000000000000000000000000000000002a (42) value for element #0 + // 0000000000000000000000000000000000000000000000000000000000000060 (96) offset to calldata for element #0 + // 000000000000000000000000000000000000000000000000000000000000000c (12) length of the calldata for element #0 + // 48656c6c6f20576f726c64210000000000000000000000000000000000000000 (..) buffer for the calldata for element #0 + assertEq( + bytes("Hello World!"), + this.callDecodeBatchAndGetFirstBytes( + abi.encode(32, 1, 32, _recipient1, 42, 96, 12, bytes12("Hello World!")) + ) + ); + + // This is invalid, the first element of the array points is out of bounds + // but we allow it past initial validation, because solidity will validate later when the bytes field is accessed + // + // 0000000000000000000000000000000000000000000000000000000000000020 (32) offset + // 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length + // 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset + //