diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a05e058..ed83c0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,24 @@ jobs: - run: npm ci + - name: Lint Solidity + run: npm run lint + - run: npx hardhat compile - run: npx hardhat test - run: npx hardhat coverage + + static-analysis: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Run Slither + uses: crytic/slither-action@v0.4.0 + with: + node-version: 20 + fail-on: high + slither-config: slither.config.json diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 0000000..e899b89 --- /dev/null +++ b/.solhint.json @@ -0,0 +1,12 @@ +{ + "extends": "solhint:recommended", + "rules": { + "compiler-version": ["error", "0.8.20"], + "func-visibility": ["error", { "ignoreConstructors": true }], + "not-rely-on-time": "off", + "gas-custom-errors": "error", + "no-empty-blocks": "off", + "no-global-import": "off", + "no-complex-fallback": "off" + } +} diff --git a/.solhintignore b/.solhintignore new file mode 100644 index 0000000..47f2cb0 --- /dev/null +++ b/.solhintignore @@ -0,0 +1,3 @@ +node_modules/ +artifacts/ +cache/ diff --git a/README.md b/README.md index 35bc35e..5262c7d 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ [![Solidity](https://img.shields.io/badge/Solidity-0.8.20-363636?logo=solidity)](https://soliditylang.org/) [![Hardhat](https://img.shields.io/badge/Built%20with-Hardhat-yellow)](https://hardhat.org/) [![OpenZeppelin](https://img.shields.io/badge/OpenZeppelin-v5.0.2-4E5EE4?logo=openzeppelin)](https://www.openzeppelin.com/contracts) -[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC) -[![Tests](https://img.shields.io/badge/Tests-49%20passing-brightgreen)]() +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![Tests](https://img.shields.io/badge/Tests-54%20passing-brightgreen)]() [![Coverage](https://img.shields.io/badge/Coverage-100%25%20lines-brightgreen)]() @@ -50,7 +50,9 @@ Sender ──► [ FundForwarder Contract ] │ | **Two-step Ownership (Ownable2Step)** | Ownership transfers require explicit acceptance, preventing irreversible mistakes | | **Pausable** | Owner can halt all deposits and flushes during emergencies or maintenance | | **Emergency Recovery** | Owner can withdraw any ETH stuck in the contract (works even when paused) | +| **ERC-20 Recovery** | Owner can sweep any ERC-20 tokens accidentally sent to the contract | | **Reentrancy Protection** | Guards against reentrancy on deposit, flush, and recovery paths | +| **Custom Errors** | Reverts use custom errors instead of strings for lower deploy and revert gas | --- @@ -61,7 +63,8 @@ contracts/ ├── FundForwarder.sol # Main contract └── test/ ├── ForceFeeder.sol # Test helper — force-sends ETH - └── Reverter.sol # Test helper — always-reverting receiver + ├── Reverter.sol # Test helper — always-reverting receiver + └── MockERC20.sol # Test helper — minimal ERC-20 token ``` ### Contract Inheritance @@ -86,6 +89,7 @@ Ownable ─► Ownable2Step ─┐ | `updateMinDeposit()` | Owner | Update the minimum deposit threshold | | `pause()` / `unpause()` | Owner | Toggle deposit and flush acceptance | | `recoverFunds()` | Owner | Withdraw stuck ETH to the owner (works when paused) | +| `recoverERC20()` | Owner | Sweep ERC-20 tokens accidentally sent to the contract | | `transferOwnership()` | Owner | Initiate two-step ownership transfer | | `acceptOwnership()` | Pending Owner | Accept ownership transfer | @@ -176,7 +180,7 @@ The deploy script reads `TARGET_WALLET` from `.env` and automatically verifies t ## Test Suite -49 tests across 11 categories with **100% line and function coverage**. +54 tests across 12 categories with **100% line and function coverage**. ``` FundForwarder @@ -185,16 +189,25 @@ The deploy script reads `TARGET_WALLET` from `.env` and automatically verifies t Wallet Change Flow (9 tests) MinDeposit Updates (4 tests) Pause / Unpause (3 tests) - Recovery (3 tests) + Recovery (4 tests) + ERC-20 Recovery (4 tests) Ownable2Step (2 tests) Fallback (1 test) Deposit History (4 tests) Batch Mode (11 tests) Edge Cases (2 tests) - 49 passing + 54 passing ``` +### Linting + +```bash +npm run lint +``` + +Solidity sources are linted with [solhint](https://protofire.github.io/solhint/) (`solhint:recommended`). Both linting and [Slither](https://github.com/crytic/slither) static analysis run automatically in CI. + --- ## Security Considerations @@ -209,7 +222,7 @@ The deploy script reads `TARGET_WALLET` from `.env` and automatically verifies t ### Known Limitations -- The contract does not support ERC-20 token forwarding — ETH only. +- The contract does not *forward* ERC-20 tokens — it handles ETH only. Tokens accidentally sent to the contract can, however, be swept by the owner via `recoverERC20()`. - `selfdestruct`-based force-sends can deposit ETH below `minDeposit` without triggering forwarding. The `recoverFunds()` function handles this case. - In batch mode, if the target wallet is changed before calling `flush()`, accumulated funds will be sent to the **new** target wallet, not the one active when deposits were made. @@ -217,4 +230,4 @@ The deploy script reads `TARGET_WALLET` from `.env` and automatically verifies t ## License -This project is licensed under the [ISC License](https://opensource.org/licenses/ISC). +This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) — see the [LICENSE](LICENSE) file for details. diff --git a/contracts/FundForwarder.sol b/contracts/FundForwarder.sol index b76faae..0e22a11 100644 --- a/contracts/FundForwarder.sol +++ b/contracts/FundForwarder.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.20; import "@openzeppelin/contracts/access/Ownable2Step.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @title FundForwarder @@ -17,8 +19,48 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; * - ReentrancyGuard: Protects the deposit and recovery paths against reentrancy attacks. * - Time-locked wallet changes: A mandatory 24-hour delay on target wallet updates * gives stakeholders time to react to potentially malicious changes. + * - Custom errors: Used throughout for lower deploy and revert gas costs. */ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { + using SafeERC20 for IERC20; + + // ────────────────────────────────────────────── + // Custom Errors + // ────────────────────────────────────────────── + + /// @notice Thrown when a zero address is supplied where a valid address is required. + error ZeroAddress(); + + /// @notice Thrown when a minimum deposit of zero is supplied. + error MinDepositZero(); + + /// @notice Thrown when a minimum deposit above `MAX_MIN_DEPOSIT` is supplied. + error MinDepositExceedsMax(); + + /// @notice Thrown when an incoming deposit is below the configured `minDeposit`. + error DepositBelowMinimum(); + + /// @notice Thrown when the contract is called with non-empty calldata. + error UnexpectedCalldata(); + + /// @notice Thrown when an ETH transfer (forward, flush, or recovery) fails. + error TransferFailed(); + + /// @notice Thrown when a wallet change is requested for the already-active target. + error WalletUnchanged(); + + /// @notice Thrown when finalizing or cancelling with no pending wallet change. + error NoPendingWalletChange(); + + /// @notice Thrown when finalizing a wallet change before the time lock expires. + error TimelockActive(); + + /// @notice Thrown when setting batch mode to its current value. + error BatchModeUnchanged(); + + /// @notice Thrown when a flush or recovery is attempted with no balance available. + error NoBalance(); + // ────────────────────────────────────────────── // State Variables // ────────────────────────────────────────────── @@ -67,7 +109,7 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { event Deposit(address indexed sender, uint256 amount); /// @notice Emitted when ETH is successfully forwarded to the target wallet. - /// @param to The recipient address (target wallet or owner during recovery). + /// @param to The recipient address (the target wallet). /// @param amount The forwarded amount in wei. event Forwarded(address indexed to, uint256 amount); @@ -100,6 +142,17 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { /// @param amount The total amount flushed in wei. event Flushed(address indexed to, uint256 amount); + /// @notice Emitted when the owner recovers stuck ETH to their own address. + /// @param to The owner address that received the recovered ETH. + /// @param amount The recovered amount in wei. + event FundsRecovered(address indexed to, uint256 amount); + + /// @notice Emitted when the owner recovers ERC-20 tokens sent to the contract. + /// @param token The ERC-20 token contract address. + /// @param to The owner address that received the recovered tokens. + /// @param amount The recovered token amount in the token's smallest unit. + event ERC20Recovered(address indexed token, address indexed to, uint256 amount); + // ────────────────────────────────────────────── // Constructor // ────────────────────────────────────────────── @@ -113,9 +166,9 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { * Must be > 0 and <= MAX_MIN_DEPOSIT (10 ETH). */ constructor(address _targetWallet, uint256 _minDeposit) Ownable(msg.sender) { - require(_targetWallet != address(0), "Target wallet cannot be zero address"); - require(_minDeposit > 0, "Minimum deposit must be greater than 0"); - require(_minDeposit <= MAX_MIN_DEPOSIT, "Minimum deposit exceeds maximum"); + if (_targetWallet == address(0)) revert ZeroAddress(); + if (_minDeposit == 0) revert MinDepositZero(); + if (_minDeposit > MAX_MIN_DEPOSIT) revert MinDepositExceedsMax(); targetWallet = _targetWallet; minDeposit = _minDeposit; } @@ -133,7 +186,7 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { * to guard against reentrancy via the external call in `forwardFunds()`. */ receive() external payable whenNotPaused nonReentrant { - require(msg.value >= minDeposit, "Deposit below minimum amount"); + if (msg.value < minDeposit) revert DepositBelowMinimum(); // Track per-depositor and global totals depositHistory[msg.sender] += msg.value; @@ -152,7 +205,7 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { * @dev Only plain ETH transfers (no data) are accepted via `receive()`. */ fallback() external payable { - revert("Use direct ETH transfer"); + revert UnexpectedCalldata(); } // ────────────────────────────────────────────── @@ -168,8 +221,12 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { uint256 amount = address(this).balance; if (amount == 0) return; + // `targetWallet` is an owner-managed, validated, time-lock-protected address + // (see requestTargetWalletChange) — not a user-supplied destination. Forwarding + // the balance to it is the contract's core intended behavior. + // slither-disable-next-line arbitrary-send-eth (bool success, ) = targetWallet.call{value: amount}(""); - require(success, "Forwarding failed"); + if (!success) revert TransferFailed(); emit Forwarded(targetWallet, amount); } @@ -185,8 +242,8 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { * @param _newWallet The proposed new target wallet address. Must be non-zero. */ function requestTargetWalletChange(address _newWallet) external onlyOwner { - require(_newWallet != address(0), "New wallet cannot be zero address"); - require(_newWallet != targetWallet, "New wallet is already the target"); + if (_newWallet == address(0)) revert ZeroAddress(); + if (_newWallet == targetWallet) revert WalletUnchanged(); pendingTargetWallet = _newWallet; walletChangeUnlockTime = block.timestamp + WALLET_CHANGE_DELAY; emit TargetWalletChangeRequested(targetWallet, _newWallet, walletChangeUnlockTime); @@ -198,8 +255,8 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { * Only callable by the owner. */ function finalizeTargetWalletChange() external onlyOwner { - require(pendingTargetWallet != address(0), "No pending wallet change"); - require(block.timestamp >= walletChangeUnlockTime, "Time lock not yet expired"); + if (pendingTargetWallet == address(0)) revert NoPendingWalletChange(); + if (block.timestamp < walletChangeUnlockTime) revert TimelockActive(); address oldWallet = targetWallet; targetWallet = pendingTargetWallet; @@ -217,7 +274,7 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { * or the request was made in error. Only callable by the owner. */ function cancelTargetWalletChange() external onlyOwner { - require(pendingTargetWallet != address(0), "No pending wallet change"); + if (pendingTargetWallet == address(0)) revert NoPendingWalletChange(); address cancelled = pendingTargetWallet; pendingTargetWallet = address(0); walletChangeUnlockTime = 0; @@ -235,8 +292,8 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { * @param _newMinDeposit The new minimum deposit amount in wei. Must be > 0 and <= 10 ETH. */ function updateMinDeposit(uint256 _newMinDeposit) external onlyOwner { - require(_newMinDeposit > 0, "Minimum deposit must be greater than 0"); - require(_newMinDeposit <= MAX_MIN_DEPOSIT, "Minimum deposit exceeds maximum"); + if (_newMinDeposit == 0) revert MinDepositZero(); + if (_newMinDeposit > MAX_MIN_DEPOSIT) revert MinDepositExceedsMax(); uint256 oldMinDeposit = minDeposit; minDeposit = _newMinDeposit; emit MinDepositUpdated(oldMinDeposit, _newMinDeposit); @@ -254,7 +311,7 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { * @param _enabled True to enable batch mode, false to resume instant forwarding. */ function setBatchMode(bool _enabled) external onlyOwner { - require(batchMode != _enabled, "Batch mode already set"); + if (batchMode == _enabled) revert BatchModeUnchanged(); batchMode = _enabled; emit BatchModeUpdated(_enabled); } @@ -269,10 +326,10 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { */ function flush() external onlyOwner whenNotPaused nonReentrant { uint256 amount = address(this).balance; - require(amount > 0, "No funds to flush"); + if (amount == 0) revert NoBalance(); (bool success, ) = targetWallet.call{value: amount}(""); - require(success, "Flush failed"); + if (!success) revert TransferFailed(); emit Flushed(targetWallet, amount); } @@ -312,11 +369,30 @@ contract FundForwarder is Ownable2Step, Pausable, ReentrancyGuard { */ function recoverFunds() external onlyOwner nonReentrant { uint256 amount = address(this).balance; - require(amount > 0, "No funds to recover"); + if (amount == 0) revert NoBalance(); (bool success, ) = msg.sender.call{value: amount}(""); - require(success, "Recovery failed"); + if (!success) revert TransferFailed(); + + emit FundsRecovered(msg.sender, amount); + } + + /** + * @notice Allows the owner to recover ERC-20 tokens accidentally sent to the contract. + * @dev The contract only handles ETH; any ERC-20 tokens transferred to it would + * otherwise be permanently stuck. Tokens are sent to `msg.sender` (the owner). + * Uses `SafeERC20` to support non-standard tokens that do not return a bool. + * Protected by `nonReentrant` to guard against reentrancy via ERC-777-style hooks. + * @param token The ERC-20 token contract address to recover. Must be non-zero. + */ + function recoverERC20(address token) external onlyOwner nonReentrant { + if (token == address(0)) revert ZeroAddress(); + + uint256 amount = IERC20(token).balanceOf(address(this)); + if (amount == 0) revert NoBalance(); + + IERC20(token).safeTransfer(msg.sender, amount); - emit Forwarded(msg.sender, amount); + emit ERC20Recovered(token, msg.sender, amount); } } diff --git a/contracts/test/MockERC20.sol b/contracts/test/MockERC20.sol new file mode 100644 index 0000000..578abdf --- /dev/null +++ b/contracts/test/MockERC20.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @title MockERC20 + * @notice Minimal ERC-20 token used in tests to verify `FundForwarder.recoverERC20()`. + * @dev Mints the full initial supply to the deployer. Not intended for production use. + */ +contract MockERC20 is ERC20 { + /// @param initialSupply The token amount minted to the deployer at construction. + constructor(uint256 initialSupply) ERC20("Mock Token", "MOCK") { + _mint(msg.sender, initialSupply); + } +} diff --git a/contracts/test/Reverter.sol b/contracts/test/Reverter.sol index fad1d23..7c4ce54 100644 --- a/contracts/test/Reverter.sol +++ b/contracts/test/Reverter.sol @@ -8,8 +8,11 @@ pragma solidity 0.8.20; * the contract correctly propagates the revert when `forwardFunds()` fails. */ contract Reverter { + /// @notice Thrown unconditionally on any incoming ETH transfer. + error AlwaysReverts(); + /// @dev Always reverts to simulate an uncooperative recipient. receive() external payable { - revert("I always revert"); + revert AlwaysReverts(); } } diff --git a/hardhat.config.js b/hardhat.config.js index 5c4ddd9..3a5ed95 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -3,7 +3,17 @@ require("dotenv").config(); /** @type {import("hardhat/config").HardhatUserConfig} */ const config = { - solidity: "0.8.20", + solidity: { + version: "0.8.20", + settings: { + // Enable the optimizer for smaller bytecode and lower runtime gas. + // `runs: 200` balances deployment cost against per-call cost. + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, // Etherscan integration for automatic contract verification after deployment etherscan: { diff --git a/package-lock.json b/package-lock.json index 25f5666..855be23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,21 @@ { - "name": "fund-forwarder-project", + "name": "fund-forwarder", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "fund-forwarder-project", + "name": "fund-forwarder", "version": "1.0.0", - "license": "ISC", + "license": "MIT", "dependencies": { "@openzeppelin/contracts": "^5.0.2", "dotenv": "^16.4.5" }, "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^5.0.0", - "hardhat": "^2.25.0" + "hardhat": "^2.25.0", + "solhint": "^5.0.3" } }, "node_modules/@adraffy/ens-normalize": { @@ -24,6 +25,31 @@ "dev": true, "peer": true }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -849,6 +875,16 @@ "node": ">=14" } }, + "node_modules/@humanwhocodes/momoa": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", + "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.10.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1317,6 +1353,51 @@ "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.3.0.tgz", "integrity": "sha512-zj/KGoW7zxWUE8qOI++rUM18v+VeLTTzKs/DJFkSzHpQFPD/jKKF0TrMxBfGLl3kpdELCNccvB3zmofSzm4nlA==" }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", + "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@scure/base": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", @@ -1545,6 +1626,19 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@solidity-parser/parser": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.5.tgz", @@ -1555,6 +1649,19 @@ "antlr4ts": "^0.5.0-alpha.4" } }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1688,6 +1795,13 @@ "@types/node": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", @@ -1834,7 +1948,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1846,6 +1959,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, "node_modules/amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", @@ -1914,6 +2037,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/antlr4": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.2.tgz", + "integrity": "sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16" + } + }, "node_modules/antlr4ts": { "version": "0.5.0-alpha.4", "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", @@ -1994,12 +2127,18 @@ "node": "*" } }, + "node_modules/ast-parents": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/ast-parents/-/ast-parents-0.0.1.tgz", + "integrity": "sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -2079,6 +2218,26 @@ "dev": true, "peer": true }, + "node_modules/better-ajv-errors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-2.0.3.tgz", + "integrity": "sha512-t1vxUP+vYKsaYi/BbKo2K98nEAZmfi4sjwvmRT8aOPDzPJeAtLurfoIDazVkLILxO4K+Sw4YrLYnBQ46l6pePg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@humanwhocodes/momoa": "^2.0.4", + "chalk": "^4.1.2", + "jsonpointer": "^5.0.1", + "leven": "^3.1.0 < 4" + }, + "engines": { + "node": ">= 18.20.6" + }, + "peerDependencies": { + "ajv": "4.11.8 - 8" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2230,6 +2389,35 @@ "node": ">= 0.8" } }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2280,6 +2468,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -2763,6 +2961,17 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "node_modules/cookie": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", @@ -2779,6 +2988,33 @@ "dev": true, "peer": true }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -2861,6 +3097,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -2879,7 +3144,6 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, - "peer": true, "engines": { "node": ">=4.0.0" } @@ -2891,6 +3155,16 @@ "dev": true, "peer": true }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3038,6 +3312,16 @@ "node": ">=6" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3462,8 +3746,14 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true, - "peer": true + "license": "Apache-2.0" }, "node_modules/fast-glob": { "version": "3.3.3", @@ -3482,6 +3772,13 @@ "node": ">=8.6.0" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -3503,8 +3800,7 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ], - "peer": true + ] }, "node_modules/fastq": { "version": "1.19.1", @@ -3619,6 +3915,16 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, "node_modules/fp-ts": { "version": "1.19.3", "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz", @@ -3745,6 +4051,19 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ghost-testrpc": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/ghost-testrpc/-/ghost-testrpc-0.0.2.tgz", @@ -3976,6 +4295,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4344,6 +4689,13 @@ "node": ">=6.0.0" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -4377,6 +4729,20 @@ "dev": true, "peer": true }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -4407,7 +4773,6 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "peer": true, "engines": { "node": ">= 4" } @@ -4429,6 +4794,33 @@ "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", "dev": true }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -4459,8 +4851,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "peer": true + "dev": true }, "node_modules/interpret": { "version": "1.4.0", @@ -4481,6 +4872,13 @@ "fp-ts": "^1.0.0" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -4613,6 +5011,13 @@ "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", "dev": true }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4625,12 +5030,25 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "peer": true + "dev": true }, "node_modules/json-stream-stringify": { "version": "3.1.6", @@ -4666,7 +5084,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -4674,6 +5091,16 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jsonschema": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", @@ -4699,6 +5126,16 @@ "node": ">=10.0.0" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -4719,6 +5156,32 @@ "node": ">=6" } }, + "node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-json": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -4733,6 +5196,13 @@ "node": ">= 0.8.0" } }, + "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, + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4780,8 +5250,7 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/log-symbols": { "version": "4.1.0", @@ -4809,6 +5278,19 @@ "get-func-name": "^2.0.1" } }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru_map": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", @@ -4964,6 +5446,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -4993,7 +5488,6 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5198,6 +5692,19 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "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", @@ -5292,6 +5799,16 @@ "node": ">=0.10.0" } }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5337,6 +5854,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", + "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", + "dev": true, + "license": "MIT", + "dependencies": { + "got": "^12.1.0", + "registry-auth-token": "^5.0.1", + "registry-url": "^6.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-cache-control": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", @@ -5344,6 +5906,25 @@ "dev": true, "peer": true }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5374,7 +5955,6 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -5469,6 +6049,16 @@ "node": ">=6" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5494,7 +6084,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, - "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -5536,6 +6125,13 @@ "node": ">= 6" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5543,6 +6139,16 @@ "dev": true, "peer": true }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -5580,6 +6186,19 @@ ], "peer": true }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -5604,6 +6223,32 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -5691,6 +6336,35 @@ "node": ">=6" } }, + "node_modules/registry-auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^3.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/req-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/req-cwd/-/req-cwd-2.0.0.tgz", @@ -5731,7 +6405,6 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5748,6 +6421,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", @@ -5758,6 +6438,22 @@ "node": ">=4" } }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6255,7 +6951,6 @@ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -6298,6 +6993,111 @@ "semver": "bin/semver" } }, + "node_modules/solhint": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-5.2.0.tgz", + "integrity": "sha512-9NZC1zt+O2K7zEZOhTT9rFeB6GdxC6qTX5pWX70RaQoflR9RejJQUC+/19LNi+e7K9Ptb4k7XAWO9wY5mkprHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@solidity-parser/parser": "^0.20.0", + "ajv": "^6.12.6", + "ajv-errors": "^1.0.1", + "antlr4": "^4.13.1-patch-1", + "ast-parents": "^0.0.1", + "better-ajv-errors": "^2.0.2", + "chalk": "^4.1.2", + "commander": "^10.0.0", + "cosmiconfig": "^8.0.0", + "fast-diff": "^1.2.0", + "fs-extra": "^11.1.0", + "glob": "^8.0.3", + "ignore": "^5.2.4", + "js-yaml": "^4.1.0", + "latest-version": "^7.0.0", + "lodash": "^4.17.21", + "pluralize": "^8.0.0", + "semver": "^7.5.2", + "strip-ansi": "^6.0.1", + "table": "^6.8.1", + "text-table": "^0.2.0" + }, + "bin": { + "solhint": "solhint.js" + }, + "optionalDependencies": { + "prettier": "^2.8.3" + } + }, + "node_modules/solhint/node_modules/@solidity-parser/parser": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.20.2.tgz", + "integrity": "sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/solhint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/solhint/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/solhint/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/solhint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/solhint/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/solidity-coverage": { "version": "0.8.16", "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.16.tgz", @@ -6655,7 +7455,6 @@ "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, - "peer": true, "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", @@ -6703,6 +7502,13 @@ "node": ">=8" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/then-request": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz", @@ -7180,7 +7986,6 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -7194,6 +7999,16 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/utf8": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", diff --git a/package.json b/package.json index cc5337a..574a741 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,19 @@ { - "name": "fund-forwarder-project", + "name": "fund-forwarder", "version": "1.0.0", - "description": "FundForwarder smart contract deployment", + "description": "A non-custodial ETH forwarding smart contract with timelocked wallet changes, batch mode, and emergency recovery.", "scripts": { "test": "npx hardhat test", "coverage": "npx hardhat coverage", "gas": "REPORT_GAS=true npx hardhat test", + "compile": "npx hardhat compile", + "lint": "npx solhint 'contracts/**/*.sol'", "deploy": "npx hardhat run scripts/deploy.js --network sepolia" }, "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^5.0.0", - "hardhat": "^2.25.0" + "hardhat": "^2.25.0", + "solhint": "^5.0.3" }, "dependencies": { "@openzeppelin/contracts": "^5.0.2", @@ -20,7 +23,18 @@ "directories": { "test": "test" }, - "keywords": [], - "author": "", - "license": "ISC" + "keywords": [ + "ethereum", + "solidity", + "smart-contract", + "fund-forwarder", + "hardhat", + "non-custodial" + ], + "repository": { + "type": "git", + "url": "https://github.com/SP1R4/FundsForwarder.git" + }, + "author": "SP1R4", + "license": "MIT" } diff --git a/slither.config.json b/slither.config.json new file mode 100644 index 0000000..cb9e25c --- /dev/null +++ b/slither.config.json @@ -0,0 +1,5 @@ +{ + "filter_paths": "contracts/test/|node_modules/", + "exclude_informational": true, + "exclude_optimization": true +} diff --git a/test/FundForwarder.test.js b/test/FundForwarder.test.js index 8674580..d3fe45e 100644 --- a/test/FundForwarder.test.js +++ b/test/FundForwarder.test.js @@ -7,7 +7,7 @@ const { ethers } = hre; * * Covers all contract functionality including deployment validation, deposit forwarding, * time-locked wallet changes, minimum deposit configuration, pause/unpause controls, - * emergency fund recovery, two-step ownership transfers, and edge cases. + * emergency ETH and ERC-20 recovery, two-step ownership transfers, and edge cases. */ describe("FundForwarder", function () { let fundForwarder, owner, targetWallet, otherAccount, addr3; @@ -44,21 +44,21 @@ describe("FundForwarder", function () { const FundForwarder = await ethers.getContractFactory("FundForwarder"); await expect( FundForwarder.deploy(ethers.ZeroAddress, minDeposit) - ).to.be.revertedWith("Target wallet cannot be zero address"); + ).to.be.revertedWithCustomError(fundForwarder, "ZeroAddress"); }); it("should revert on zero minDeposit", async function () { const FundForwarder = await ethers.getContractFactory("FundForwarder"); await expect( FundForwarder.deploy(targetWallet.address, 0) - ).to.be.revertedWith("Minimum deposit must be greater than 0"); + ).to.be.revertedWithCustomError(fundForwarder, "MinDepositZero"); }); it("should revert when minDeposit exceeds MAX_MIN_DEPOSIT", async function () { const FundForwarder = await ethers.getContractFactory("FundForwarder"); await expect( FundForwarder.deploy(targetWallet.address, ethers.parseEther("11")) - ).to.be.revertedWith("Minimum deposit exceeds maximum"); + ).to.be.revertedWithCustomError(fundForwarder, "MinDepositExceedsMax"); }); }); @@ -81,7 +81,7 @@ describe("FundForwarder", function () { const smallAmount = ethers.parseUnits("0.001", "ether"); await expect( owner.sendTransaction({ to: fundForwarder.target, value: smallAmount }) - ).to.be.revertedWith("Deposit below minimum amount"); + ).to.be.revertedWithCustomError(fundForwarder, "DepositBelowMinimum"); }); it("should revert deposits when paused", async function () { @@ -134,13 +134,13 @@ describe("FundForwarder", function () { await fundForwarder.requestTargetWalletChange(otherAccount.address); await expect( fundForwarder.finalizeTargetWalletChange() - ).to.be.revertedWith("Time lock not yet expired"); + ).to.be.revertedWithCustomError(fundForwarder, "TimelockActive"); }); it("should revert finalize with no pending change", async function () { await expect( fundForwarder.finalizeTargetWalletChange() - ).to.be.revertedWith("No pending wallet change"); + ).to.be.revertedWithCustomError(fundForwarder, "NoPendingWalletChange"); }); it("should cancel a pending wallet change", async function () { @@ -156,7 +156,7 @@ describe("FundForwarder", function () { it("should revert cancel with no pending change", async function () { await expect( fundForwarder.cancelTargetWalletChange() - ).to.be.revertedWith("No pending wallet change"); + ).to.be.revertedWithCustomError(fundForwarder, "NoPendingWalletChange"); }); it("should revert request from non-owner", async function () { @@ -168,13 +168,13 @@ describe("FundForwarder", function () { it("should revert request with zero address", async function () { await expect( fundForwarder.requestTargetWalletChange(ethers.ZeroAddress) - ).to.be.revertedWith("New wallet cannot be zero address"); + ).to.be.revertedWithCustomError(fundForwarder, "ZeroAddress"); }); it("should revert request to change to the current target wallet", async function () { await expect( fundForwarder.requestTargetWalletChange(targetWallet.address) - ).to.be.revertedWith("New wallet is already the target"); + ).to.be.revertedWithCustomError(fundForwarder, "WalletUnchanged"); }); }); @@ -194,13 +194,13 @@ describe("FundForwarder", function () { it("should revert on zero minDeposit", async function () { await expect( fundForwarder.updateMinDeposit(0) - ).to.be.revertedWith("Minimum deposit must be greater than 0"); + ).to.be.revertedWithCustomError(fundForwarder, "MinDepositZero"); }); it("should revert when exceeding MAX_MIN_DEPOSIT", async function () { await expect( fundForwarder.updateMinDeposit(ethers.parseEther("11")) - ).to.be.revertedWith("Minimum deposit exceeds maximum"); + ).to.be.revertedWithCustomError(fundForwarder, "MinDepositExceedsMax"); }); it("should revert from non-owner", async function () { @@ -237,7 +237,7 @@ describe("FundForwarder", function () { }); // ─── Recovery ────────────────────────────────────────────────────── - // Tests the emergency fund recovery mechanism. Uses ForceFeeder to inject + // Tests the emergency ETH recovery mechanism. Uses ForceFeeder to inject // ETH into the contract (bypassing receive()), then verifies the owner can // withdraw it. Also covers the no-funds and non-owner revert cases. describe("Recovery", function () { @@ -263,10 +263,25 @@ describe("FundForwarder", function () { expect(ownerBalAfter).to.equal(ownerBalBefore + amount - gasCost); }); + it("should emit FundsRecovered on recovery", async function () { + await fundForwarder.pause(); + + const ForceFeeder = await ethers.getContractFactory("ForceFeeder"); + const forceFeeder = await ForceFeeder.deploy(); + await forceFeeder.waitForDeployment(); + + const amount = ethers.parseEther("1"); + await forceFeeder.feed(fundForwarder.target, { value: amount }); + + await expect(fundForwarder.recoverFunds()) + .to.emit(fundForwarder, "FundsRecovered") + .withArgs(owner.address, amount); + }); + it("should revert when no funds to recover", async function () { await expect( fundForwarder.recoverFunds() - ).to.be.revertedWith("No funds to recover"); + ).to.be.revertedWithCustomError(fundForwarder, "NoBalance"); }); it("should revert from non-owner", async function () { @@ -276,6 +291,51 @@ describe("FundForwarder", function () { }); }); + // ─── ERC-20 Recovery ─────────────────────────────────────────────── + // Validates that ERC-20 tokens accidentally sent to the contract can be + // swept to the owner, and that invalid callers and inputs are rejected. + describe("ERC-20 Recovery", function () { + let token; + const tokenAmount = ethers.parseEther("100"); + + beforeEach(async function () { + const MockERC20 = await ethers.getContractFactory("MockERC20"); + token = await MockERC20.deploy(tokenAmount); + await token.waitForDeployment(); + + // Simulate tokens accidentally sent to the FundForwarder contract + await token.transfer(fundForwarder.target, tokenAmount); + }); + + it("should let the owner recover ERC-20 tokens", async function () { + await expect(fundForwarder.recoverERC20(token.target)) + .to.emit(fundForwarder, "ERC20Recovered") + .withArgs(token.target, owner.address, tokenAmount); + + expect(await token.balanceOf(owner.address)).to.equal(tokenAmount); + expect(await token.balanceOf(fundForwarder.target)).to.equal(0); + }); + + it("should revert when the contract holds no tokens", async function () { + await fundForwarder.recoverERC20(token.target); + await expect( + fundForwarder.recoverERC20(token.target) + ).to.be.revertedWithCustomError(fundForwarder, "NoBalance"); + }); + + it("should revert recoverERC20 with zero address", async function () { + await expect( + fundForwarder.recoverERC20(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(fundForwarder, "ZeroAddress"); + }); + + it("should revert recoverERC20 from non-owner", async function () { + await expect( + fundForwarder.connect(otherAccount).recoverERC20(token.target) + ).to.be.revertedWithCustomError(fundForwarder, "OwnableUnauthorizedAccount"); + }); + }); + // ─── Ownable2Step ────────────────────────────────────────────────── // Validates the two-step ownership transfer: the new owner must explicitly // accept, preventing accidental transfers to wrong addresses. @@ -309,7 +369,7 @@ describe("FundForwarder", function () { value: ethers.parseEther("0.02"), data: "0x12345678", }) - ).to.be.revertedWith("Use direct ETH transfer"); + ).to.be.revertedWithCustomError(fundForwarder, "UnexpectedCalldata"); }); }); @@ -374,7 +434,7 @@ describe("FundForwarder", function () { it("should revert if batch mode is already set to the same value", async function () { await expect( fundForwarder.setBatchMode(false) - ).to.be.revertedWith("Batch mode already set"); + ).to.be.revertedWithCustomError(fundForwarder, "BatchModeUnchanged"); }); it("should revert setBatchMode from non-owner", async function () { @@ -423,7 +483,7 @@ describe("FundForwarder", function () { it("should revert flush when no funds available", async function () { await expect( fundForwarder.flush() - ).to.be.revertedWith("No funds to flush"); + ).to.be.revertedWithCustomError(fundForwarder, "NoBalance"); }); it("should revert flush from non-owner", async function () { @@ -491,7 +551,7 @@ describe("FundForwarder", function () { // Deposit should fail because the target rejects ETH await expect( owner.sendTransaction({ to: ff.target, value: ethers.parseEther("0.02") }) - ).to.be.revertedWith("Forwarding failed"); + ).to.be.revertedWithCustomError(ff, "TransferFailed"); }); it("should recover batch funds when flush fails due to reverting target", async function () { @@ -510,7 +570,7 @@ describe("FundForwarder", function () { await owner.sendTransaction({ to: ff.target, value: deposit }); // Flush should fail because the target reverts - await expect(ff.flush()).to.be.revertedWith("Flush failed"); + await expect(ff.flush()).to.be.revertedWithCustomError(ff, "TransferFailed"); // Owner can still recover the stuck funds via recoverFunds() const ownerBalBefore = await ethers.provider.getBalance(owner.address);