diff --git a/audits/multicall/202309-external-audit-multicaller.pdf b/audits/multicall/202309-external-audit-multicaller.pdf new file mode 100644 index 00000000..7ac48956 Binary files /dev/null and b/audits/multicall/202309-external-audit-multicaller.pdf differ diff --git a/audits/multicall/202309-threat-model-multicaller.md b/audits/multicall/202309-threat-model-multicaller.md new file mode 100644 index 00000000..1103df0f --- /dev/null +++ b/audits/multicall/202309-threat-model-multicaller.md @@ -0,0 +1,78 @@ +## Background +--- +Currently the Primary Sales product uses the pattern of Multicall to transfer ERC20 tokens and mint ERC721/ERC1155 tokens in a single transaction. The Multicaller contract is a common pattern in the space and is used by many projects, this has some added security features that will be mentioned below. + +## Architecture +--- +### Contract High-Level Design +--- +![alt text](202309-threat-model-multicaller/high-level.png "High Level") + +The core of the `Guarded Multi-caller` system is `Multi-call`, allowing for the minting and burning of multiple NFTs from various collections in a single transaction. Due to the high level of security needed when working with NFTs, additional safety measures have been implemented to meet security standards. + +- `Function Permits` prevent destructive calls from being made from the Multi-caller contract. +- `Signature Validation` prevents multi-call instructions from random parties and only allows multi-call instructions from trusted parties. +- `Signer Access Control` manages these trusted parties. +- `References` provide anti-replay protection and help sync with any web2 system listening to events. + +### System High-Level Design +--- +![alt text](202309-threat-model-multicaller/architecture.png "Architecture") +#### Components +--- +| Component | Ownership | Description | +|------------------------------ |----------- |------------------------------------------------------------------------------------------------------------------------- | +| Client | Customer | This can be a game client or a mobile client that the players interact with. | +| Central Authority | Customer | It generates a list of function calls to be executed and gets a valid signature for those calls from `Multi-call Signer`. | +| Multi-call Signer | Customer | It takes a list of function calls and generates a valid signature using a `EOA` with `MULTICALL_SIGNER_ROLE`. | +| Guarded Multicaller Contract | Customer | It validates an input signature and executes an authorized list of function calls. | + +#### Flow +--- +Let’s look at the flow for basic crafting, where players burn one NFT from the `ERC721Card` contract and mint a new NFT on the `ERC721Pet` contract: + +1. An `EOA` with `DEFAULT_ADMIN_ROLE` calls the `Guarded Multi-caller` contract to permit mint function `mint(address,uint256)` with the `ERC721Pet` contract. +2. An `EOA` with `DEFAULT_ADMIN_ROLE` calls the `ERC721Pet` contract to grant `MINTER_ROLE` to the `Guarded Multi-caller` contract. +3. A client requests the `Central Authority` to generate a list of function calls to burn and mint and request a signature from the `Multi-call Signer`. +4. The `Multi-call Signer` uses an account with `MULTICALL_SIGNER_ROLE` to sign the function calls and returns the signature back to the `Central Authority`. +5. The `Central Authority` returns the list of function calls and the signature to the client. +6. The `Client` approves the `Guarded Multi-caller` contract as a spender for their `ERC721Card` NFT. +7. The `Client` submits a transaction to the `Guarded Multi-caller` contract to execute the list of function calls. +8. The `GuardedMulticaller` contract calls the `ERC721Card` contract to burn the NFT and calls the `ERC721Pet` contract to mint a new NFT to the player’s wallet. + +## Public Interface +--- +```javascript +function execute( + address _multicallSigner, + bytes32 _ref, + address[] calldata _targets, + bytes[] calldata _data, + uint256 _deadline, + bytes calldata _signature +) + +function setFunctionPermits( + address[] calldata _targets, + bytes4[] calldata _functionSelectors, + bool[] calldata _permitted +) + +function isFunctionPermitted( + address _target, + bytes4 _functionSelector +) + +function grantMulticallSignerRole( + address _user +) + +function revokeMulticallSignerRole( + address _user +) +``` + +## Security Considerations +--- +- If the `Central Authority` is compromised, attackers are able to generate valid signatures to mint new tokens for themselves. As each customer manages their own `Central Authority`, its impact is per customer. +- If the `Multi-call Signer` is compromised, attackers are able to generate valid signatures to mint new tokens for themselves. As each customer manages their own `Multi-call Signer`, its impact is per customer. diff --git a/audits/multicall/202309-threat-model-multicaller/architecture.png b/audits/multicall/202309-threat-model-multicaller/architecture.png new file mode 100644 index 00000000..6f6e6243 Binary files /dev/null and b/audits/multicall/202309-threat-model-multicaller/architecture.png differ diff --git a/audits/multicall/202309-threat-model-multicaller/high-level.png b/audits/multicall/202309-threat-model-multicaller/high-level.png new file mode 100644 index 00000000..3a4ed65b Binary files /dev/null and b/audits/multicall/202309-threat-model-multicaller/high-level.png differ diff --git a/contracts/mocks/MockFunctions.sol b/contracts/mocks/MockFunctions.sol new file mode 100644 index 00000000..1d4eacec --- /dev/null +++ b/contracts/mocks/MockFunctions.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Unlicense +// This file is part of the test code for GuardedMulticaller +pragma solidity ^0.8.19; + +contract MockFunctions { + // solhint-disable-next-line no-empty-blocks + function succeed() public pure { + // This function is intentionally left empty to simulate a successful call + } + + function revertWithNoReason() public pure { + // solhint-disable-next-line custom-errors,reason-string + revert(); + } + + // solhint-disable-next-line no-empty-blocks + function nonPermitted() public pure { + // This function is intentionally left empty to simulate a non-permitted action + } +} diff --git a/contracts/multicall/GuardedMulticaller.sol b/contracts/multicall/GuardedMulticaller.sol new file mode 100644 index 00000000..bcf0ecf4 --- /dev/null +++ b/contracts/multicall/GuardedMulticaller.sol @@ -0,0 +1,305 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Signature Validation +import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; + +// Access Control +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +// Reentrancy Guard +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +// EIP-712 Typed Structs +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +/** + * + * @title GuardedMulticaller contract + * @author Immutable Game Studio + * @notice This contract is used to batch calls to other contracts. + * @dev This contract is not designed to be upgradeable. If an issue is found with this contract, + * a new version will be deployed. All approvals granted to this contract will be revoked before + * a new version is deployed. Approvals will be granted to the new contract. + */ +contract GuardedMulticaller is AccessControl, ReentrancyGuard, EIP712 { + /// @dev Mapping of address to function selector to permitted status + // solhint-disable-next-line named-parameters-mapping + mapping(address => mapping(bytes4 => bool)) private permittedFunctionSelectors; + + /// @dev Mapping of reference to executed status + // solhint-disable-next-line named-parameters-mapping + mapping(bytes32 => bool) private replayProtection; + + /// @dev Only those with MULTICALL_SIGNER_ROLE can generate valid signatures for execute function. + bytes32 public constant MULTICALL_SIGNER_ROLE = bytes32("MULTICALL_SIGNER_ROLE"); + + /// @dev EIP712 typehash for execute function + bytes32 internal constant MULTICALL_TYPEHASH = + keccak256("Multicall(bytes32 ref,address[] targets,bytes[] data,uint256 deadline)"); + + /// @dev Struct for function permit + struct FunctionPermit { + address target; + bytes4 functionSelector; + bool permitted; + } + + /// @dev Event emitted when execute function is called + event Multicalled( + address indexed _multicallSigner, + bytes32 indexed _reference, + address[] _targets, + bytes[] _data, + uint256 _deadline + ); + + /// @dev Event emitted when a function permit is updated + event FunctionPermitted(address indexed _target, bytes4 _functionSelector, bool _permitted); + + /// @dev Error thrown when reference is invalid + error InvalidReference(bytes32 _reference); + + /// @dev Error thrown when reference has already been executed + error ReusedReference(bytes32 _reference); + + /// @dev Error thrown when address array is empty + error EmptyAddressArray(); + + /// @dev Error thrown when address array is empty + error EmptyFunctionPermitArray(); + + /// @dev Error thrown when address array and data array have different lengths + error AddressDataArrayLengthsMismatch(uint256 _addressLength, uint256 _dataLength); + + /// @dev Error thrown when deadline is expired + error Expired(uint256 _deadline); + + /// @dev Error thrown when target address is not a contract + error NonContractAddress(address _target); + + /// @dev Error thrown when signer is not authorized + error UnauthorizedSigner(address _multicallSigner); + + /// @dev Error thrown when signature is invalid + error UnauthorizedSignature(bytes _signature); + + /// @dev Error thrown when call reverts + error FailedCall(address _target, bytes _data); + + /// @dev Error thrown when call data is invalid + error InvalidCallData(address _target, bytes _data); + + /// @dev Error thrown when call data is unauthorized + error UnauthorizedFunction(address _target, bytes _data); + + /** + * + * @notice Grants DEFAULT_ADMIN_ROLE to the contract creator + * @param _owner Owner of the contract + * @param _name Name of the contract + * @param _version Version of the contract + */ + // solhint-disable-next-line no-unused-vars + constructor(address _owner, string memory _name, string memory _version) EIP712(_name, _version) { + _grantRole(DEFAULT_ADMIN_ROLE, _owner); + } + + /** + * @notice Check if a function selector is permitted. + * + * @param _target Contract address + * @param _functionSelector Function selector + */ + function isFunctionPermitted(address _target, bytes4 _functionSelector) public view returns (bool) { + return permittedFunctionSelectors[_target][_functionSelector]; + } + + /** + * + * @dev Returns hash of array of bytes + * + * @param _data Array of bytes + */ + function hashBytesArray(bytes[] memory _data) public pure returns (bytes32) { + bytes32[] memory hashedBytesArr = new bytes32[](_data.length); + for (uint256 i = 0; i < _data.length; i++) { + hashedBytesArr[i] = keccak256(_data[i]); + } + return keccak256(abi.encodePacked(hashedBytesArr)); + } + + /** + * + * @notice Execute a list of calls. Returned data from calls are ignored. + * The signature must be generated by an address with EXECUTION_MULTICALL_SIGNER_ROLE + * The signature must be valid + * The signature must not be expired + * The reference must be unique + * The reference must not be executed before + * The list of calls must not be empty + * The list of calls is executed in order + * + * @param _multicallSigner Address of an approved signer + * @param _reference Reference + * @param _targets List of addresses to call + * @param _data List of call data + * @param _deadline Expiration timestamp + * @param _signature Signature of the multicall signer + */ + // slither-disable-start low-level-calls,cyclomatic-complexity + // solhint-disable-next-line code-complexity + function execute( + address _multicallSigner, + bytes32 _reference, + address[] calldata _targets, + bytes[] calldata _data, + uint256 _deadline, + bytes calldata _signature + ) external nonReentrant { + // solhint-disable-next-line not-rely-on-time + if (_deadline < block.timestamp) { + revert Expired(_deadline); + } + if (_reference == 0) { + revert InvalidReference(_reference); + } + if (replayProtection[_reference]) { + revert ReusedReference(_reference); + } + if (_targets.length == 0) { + revert EmptyAddressArray(); + } + if (_targets.length != _data.length) { + revert AddressDataArrayLengthsMismatch(_targets.length, _data.length); + } + for (uint256 i = 0; i < _targets.length; i++) { + if (_data[i].length < 4) { + revert InvalidCallData(_targets[i], _data[i]); + } + bytes4 functionSelector = bytes4(_data[i][:4]); + if (!permittedFunctionSelectors[_targets[i]][functionSelector]) { + revert UnauthorizedFunction(_targets[i], _data[i]); + } + if (_targets[i].code.length == 0) { + revert NonContractAddress(_targets[i]); + } + } + if (!hasRole(MULTICALL_SIGNER_ROLE, _multicallSigner)) { + revert UnauthorizedSigner(_multicallSigner); + } + + // Signature validation + if ( + !SignatureChecker.isValidSignatureNow( + _multicallSigner, + _hashTypedData(_reference, _targets, _data, _deadline), + _signature + ) + ) { + revert UnauthorizedSignature(_signature); + } + + replayProtection[_reference] = true; + + // Multicall + for (uint256 i = 0; i < _targets.length; i++) { + // solhint-disable avoid-low-level-calls + // slither-disable-next-line calls-loop + (bool success, bytes memory returnData) = _targets[i].call(_data[i]); + if (!success) { + if (returnData.length == 0) { + revert FailedCall(_targets[i], _data[i]); + } + // solhint-disable-next-line no-inline-assembly + assembly { + revert(add(returnData, 32), mload(returnData)) + } + } + } + + emit Multicalled(_multicallSigner, _reference, _targets, _data, _deadline); + } + // slither-disable-end low-level-calls,cyclomatic-complexity + + /** + * @notice Update function permits for a list of function selectors on target contracts. Only DEFAULT_ADMIN_ROLE can call this function. + * + * @param _functionPermits List of function permits + */ + function setFunctionPermits(FunctionPermit[] calldata _functionPermits) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_functionPermits.length == 0) { + revert EmptyFunctionPermitArray(); + } + for (uint256 i = 0; i < _functionPermits.length; i++) { + if (_functionPermits[i].target.code.length == 0) { + revert NonContractAddress(_functionPermits[i].target); + } + permittedFunctionSelectors[_functionPermits[i].target][ + _functionPermits[i].functionSelector + ] = _functionPermits[i].permitted; + emit FunctionPermitted( + _functionPermits[i].target, + _functionPermits[i].functionSelector, + _functionPermits[i].permitted + ); + } + } + + /** + * @notice Grants MULTICALL_SIGNER_ROLE to a user. Only DEFAULT_ADMIN_ROLE can call this function. + * + * @param _user User to grant MULTICALL_SIGNER_ROLE to + */ + function grantMulticallSignerRole(address _user) external onlyRole(DEFAULT_ADMIN_ROLE) { + grantRole(MULTICALL_SIGNER_ROLE, _user); + } + + /** + * @notice Revokes MULTICALL_SIGNER_ROLE for a user. Only DEFAULT_ADMIN_ROLE can call this function. + * + * @param _user User to grant MULTICALL_SIGNER_ROLE to + */ + function revokeMulticallSignerRole(address _user) external onlyRole(DEFAULT_ADMIN_ROLE) { + revokeRole(MULTICALL_SIGNER_ROLE, _user); + } + + /** + * @notice Gets whether the reference has been executed before. + * + * @param _reference Reference to check + */ + function hasBeenExecuted(bytes32 _reference) external view returns (bool) { + return replayProtection[_reference]; + } + + /** + * + * @dev Returns EIP712 message hash for given parameters + * + * @param _reference Reference + * @param _targets List of addresses to call + * @param _data List of call data + * @param _deadline Expiration timestamp + */ + function _hashTypedData( + bytes32 _reference, + address[] calldata _targets, + bytes[] calldata _data, + uint256 _deadline + ) internal view returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + MULTICALL_TYPEHASH, + _reference, + keccak256(abi.encodePacked(_targets)), + hashBytesArray(_data), + _deadline + ) + ) + ); + } +} diff --git a/contracts/multicall/README.md b/contracts/multicall/README.md new file mode 100644 index 00000000..1a53f8f1 --- /dev/null +++ b/contracts/multicall/README.md @@ -0,0 +1,26 @@ +# GuardedMulticaller + +The GuardedMulticaller contract provides functionality to call multiple functions across different target contracts, the function signatures are validated to ensure they are permitted. Currently one of the use cases we have is in the Primary Sales flow, the GuardedMulticaller executes `transferFrom()` and `safeMint()` functions on different target contracts in a single transaction. + +### Features + +- Signature validation: Only approved signers can authorise the `execute` on the multicall contract. +- Function Permits: Security to prevent execution of unauthorised targets and functions. +- Expiry: Ability to set an expiry for the multicall. +- References: Map multicall executions to a reference string to be used by the application. + +# Status + +Contract audits and threat models: + +| Description | Date |Version Audited | Link to Report | +|---------------------------|------------------|-----------------|----------------| +| Threat Model | Sept 26, 2023 | --- | [202309-threat-model-multicaller](../../audits/multicall/202309-threat-model-multicaller.md) | +| External audit | Sept 26, 2023 | [e59b72a](https://github.com/immutable/contracts/blob/e59b72a69294bd6d5857a1e2d019044bbfb14632/contracts/multicall) | [202309-external-audit-multicaller](../../audits/multicall/202309-external-audit-multicaller.pdf) | + + +# Architecture + +The architecture of the GuardedMulticaller system is shown below. + +![GuardedMulticaller Architecture](../../audits/multicall/202309-threat-model-multicaller/architecture.png) diff --git a/package.json b/package.json index 6f348fd9..85382492 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,8 @@ "prettier": "^3.2.4", "prettier-plugin-solidity": "^1.3.1", "rimraf": "^5.0.5", - "solhint": "^3.3.8", - "solhint-community": "^3.7.0", + "solhint": "^5.0.1", + "solhint-community": "^4.0.0", "solhint-plugin-prettier": "^0.1.0", "solidity-coverage": "^0.8.4", "ts-node": "^10.9.1", @@ -74,6 +74,7 @@ "@openzeppelin/contracts-upgradeable": "^4.9.3", "@rari-capital/solmate": "^6.4.0", "eslint-plugin-mocha": "^10.2.0", + "moment": "^2.30.1", "openzeppelin-contracts-5.0.2": "npm:@openzeppelin/contracts@^5.0.2", "openzeppelin-contracts-upgradeable-4.9.3": "npm:@openzeppelin/contracts-upgradeable@^4.9.3", "seaport": "https://github.com/immutable/seaport.git#1.5.0+im.1.3", diff --git a/test/multicall/GuardedMulticaller.test.ts b/test/multicall/GuardedMulticaller.test.ts new file mode 100644 index 00000000..1974e124 --- /dev/null +++ b/test/multicall/GuardedMulticaller.test.ts @@ -0,0 +1,309 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { BigNumberish } from "ethers"; +import moment from "moment"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { randomUUID } from "crypto"; +import { hexlify, keccak256 } from "ethers/lib/utils"; +import { GuardedMulticaller, MockFunctions } from "../../typechain-types"; + +describe("GuardedMulticaller", function () { + let deployerAccount: SignerWithAddress; + let signerAccount: SignerWithAddress; + let userAccount: SignerWithAddress; + + before(async function () { + [deployerAccount, signerAccount, userAccount] = await ethers.getSigners(); + }); + + const multicallerName = "Multicaller"; + const multicallerVersion = "v1"; + + let guardedMulticaller: GuardedMulticaller; + let deadline: number; + let ref: string; + let domain: { name: string; version: string; verifyingContract: string }; + + beforeEach(async function () { + const GuardedMulticallerFactory = await ethers.getContractFactory("GuardedMulticaller"); + guardedMulticaller = (await GuardedMulticallerFactory.deploy( + deployerAccount.address, + multicallerName, + multicallerVersion, + )) as GuardedMulticaller; + await guardedMulticaller.connect(deployerAccount).grantMulticallSignerRole(signerAccount.address); + deadline = moment.utc().add(30, "minute").unix(); + ref = `0x${randomUUID().replace(/-/g, "").padEnd(64, "0")}`; + domain = { + name: multicallerName, + version: multicallerVersion, + verifyingContract: guardedMulticaller.address, + }; + }); + + describe("Mock Functions", function () { + let mock: MockFunctions; + + beforeEach(async function () { + const MockFunctionsFactory = await ethers.getContractFactory("MockFunctions"); + mock = (await MockFunctionsFactory.connect(deployerAccount).deploy()) as MockFunctions; + await guardedMulticaller.setFunctionPermits([ + { + target: mock.address, + functionSelector: funcSignatureToFuncSelector("succeed()"), + permitted: true, + }, + { + target: mock.address, + functionSelector: funcSignatureToFuncSelector("revertWithNoReason()"), + permitted: true, + }, + ]); + }); + + it("Should successfully execute if valid", async function () { + const targets = [mock.address]; + const data = [mock.interface.encodeFunctionData("succeed")]; + const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); + await expect( + guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), + ) + .to.emit(guardedMulticaller, "Multicalled") + .withArgs(signerAccount.address, ref, targets, data, deadline); + }); + + it("Should revert with custom error with empty return data", async function () { + const targets = [mock.address]; + const data = [mock.interface.encodeFunctionData("revertWithNoReason")]; + const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); + await expect( + guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), + ).to.be.revertedWith("FailedCall"); + }); + + it("Should revert if deadline has passed", async function () { + const expiredDeadline = moment.utc().subtract(30, "minute").unix(); + const targets = [mock.address]; + const data = [mock.interface.encodeFunctionData("succeed")]; + const sig = await signMulticallTypedData(signerAccount, ref, targets, data, expiredDeadline, domain); + await expect( + guardedMulticaller + .connect(userAccount) + .execute(signerAccount.address, ref, targets, data, expiredDeadline, sig), + ).to.be.revertedWith("Expired"); + }); + + it("Should revert if reference is reused - anti-replay", async function () { + const targets = [mock.address]; + const data = [mock.interface.encodeFunctionData("succeed")]; + const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); + await guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig); + await expect( + guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), + ).to.be.revertedWith("ReusedReference"); + }); + + it("Should revert if ref is invalid", async function () { + const targets = [mock.address]; + const data = [mock.interface.encodeFunctionData("succeed")]; + const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); + const invalidRef = `0x${"0".repeat(64)}`; + await expect( + guardedMulticaller + .connect(userAccount) + .execute(signerAccount.address, invalidRef, targets, data, deadline, sig), + ).to.be.revertedWith("InvalidReference"); + }); + + it("Should revert if signer does not have MULTICALLER role", async function () { + const targets = [mock.address]; + const data = [mock.interface.encodeFunctionData("succeed")]; + const sig = await signMulticallTypedData(userAccount, ref, targets, data, deadline, domain); + await expect( + guardedMulticaller.connect(userAccount).execute(userAccount.address, ref, targets, data, deadline, sig), + ).to.be.revertedWith("UnauthorizedSigner"); + }); + + it("Should revert if signer and signature do not match", async function () { + const targets = [mock.address]; + const data = [mock.interface.encodeFunctionData("succeed")]; + const sig = await signMulticallTypedData(userAccount, ref, targets, data, deadline, domain); + await expect( + guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), + ).to.be.revertedWith("UnauthorizedSignature"); + }); + + it("Should revert if targets are empty", async function () { + const targets: string[] = []; + const data = [mock.interface.encodeFunctionData("succeed")]; + const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); + await expect( + guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), + ).to.be.revertedWith("EmptyAddressArray"); + }); + + it("Should revert if targets and data sizes do not match", async function () { + const targets = [mock.address, mock.address]; + const data = [mock.interface.encodeFunctionData("succeed")]; + const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); + await expect( + guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), + ).to.be.revertedWith("AddressDataArrayLengthsMismatch"); + }); + + it("Should revert if function not permitted", async function () { + const targets = [mock.address]; + const data = [mock.interface.encodeFunctionData("nonPermitted")]; + const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); + await expect( + guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), + ).to.be.revertedWith("UnauthorizedFunction"); + }); + + it("Should revert if function is disallowed", async function () { + await guardedMulticaller.setFunctionPermits([ + { + target: mock.address, + functionSelector: funcSignatureToFuncSelector("succeed()"), + permitted: false, + }, + ]); + const targets = [mock.address]; + const data = [mock.interface.encodeFunctionData("succeed")]; + const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); + await expect( + guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), + ).to.be.revertedWith("UnauthorizedFunction"); + }); + + it("Should revert if signature is invalid", async function () { + const targets = [mock.address]; + const data = [mock.interface.encodeFunctionData("succeed")]; + const maliciousRef = `0x${randomUUID().replace(/-/g, "").padEnd(64, "0")}`; + const sig = await signMulticallTypedData(signerAccount, maliciousRef, targets, data, deadline, domain); + await expect( + guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), + ).to.be.revertedWith("UnauthorizedSignature"); + }); + + it("Should emit FunctionPermitted event when setting function permits", async function () { + await expect( + guardedMulticaller.connect(deployerAccount).setFunctionPermits([ + { + target: mock.address, + functionSelector: funcSignatureToFuncSelector("succeed()"), + permitted: true, + }, + ]), + ) + .to.emit(guardedMulticaller, "FunctionPermitted") + .withArgs(mock.address, funcSignatureToFuncSelector("succeed()"), true); + await expect( + guardedMulticaller.connect(deployerAccount).setFunctionPermits([ + { + target: mock.address, + functionSelector: funcSignatureToFuncSelector("succeed()"), + permitted: false, + }, + ]), + ) + .to.emit(guardedMulticaller, "FunctionPermitted") + .withArgs(mock.address, funcSignatureToFuncSelector("succeed()"), false); + }); + + it("Should revert if setting function permits with invalid data", async function () { + await expect(guardedMulticaller.connect(deployerAccount).setFunctionPermits([])).to.be.revertedWith( + "EmptyFunctionPermitArray", + ); + await expect( + guardedMulticaller.connect(userAccount).setFunctionPermits([ + { + target: mock.address, + functionSelector: funcSignatureToFuncSelector("succeed()"), + permitted: false, + }, + ]), + ).to.be.revertedWith(/AccessControl/); + await expect( + guardedMulticaller.setFunctionPermits([ + { + target: deployerAccount.address, + functionSelector: funcSignatureToFuncSelector("succeed()"), + permitted: true, + }, + ]), + ).to.be.revertedWith("NonContractAddress"); + }); + + it("Should revert if grant/revoke signer role with invalid role", async function () { + await expect( + guardedMulticaller.connect(userAccount).grantMulticallSignerRole(userAccount.address), + ).to.be.revertedWith(/AccessControl/); + + await expect( + guardedMulticaller.connect(userAccount).revokeMulticallSignerRole(userAccount.address), + ).to.be.revertedWith(/AccessControl/); + }); + + it("Should return hasBeenExecuted = true if the call has been executed", async function () { + const targets = [mock.address]; + const data = [mock.interface.encodeFunctionData("succeed")]; + const sig = await signMulticallTypedData(signerAccount, ref, targets, data, deadline, domain); + await expect( + guardedMulticaller.connect(userAccount).execute(signerAccount.address, ref, targets, data, deadline, sig), + ) + .to.emit(guardedMulticaller, "Multicalled") + .withArgs(signerAccount.address, ref, targets, data, deadline); + + await expect(await guardedMulticaller.hasBeenExecuted(ref)).to.be.true; + }); + + it("Should return hasBeenExecuted = false for an unknown reference", async function () { + const invalidRef = `0x${randomUUID().replace(/-/g, "").padEnd(64, "0")}`; + await expect(await guardedMulticaller.hasBeenExecuted(invalidRef)).to.be.false; + }); + }); +}); + +function funcSignatureToFuncSelector(funcSignature: string): string { + return keccak256(hexlify(ethers.utils.toUtf8Bytes(funcSignature))).substring(0, 10); +} + +async function signMulticallTypedData( + wallet: SignerWithAddress, + ref: string, + targets: string[], + data: string[], + deadline: BigNumberish, + domain: { name: string; version: string; verifyingContract: string }, +): Promise { + return await wallet._signTypedData( + { + name: domain.name, + version: domain.version, + chainId: await wallet.getChainId(), + verifyingContract: domain.verifyingContract, + }, + { + Multicall: [ + { + name: "ref", + type: "bytes32", + }, + { + name: "targets", + type: "address[]", + }, + { + name: "data", + type: "bytes[]", + }, + { + name: "deadline", + type: "uint256", + }, + ], + }, + { ref, targets, data, deadline }, + ); +} diff --git a/yarn.lock b/yarn.lock index 3ffdc1c5..d295a520 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,16 +7,16 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@axelar-network/axelar-gmp-sdk-solidity@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@axelar-network/axelar-gmp-sdk-solidity/-/axelar-gmp-sdk-solidity-5.8.0.tgz#449c6246b9f403af97a030b0a90b4f321c3a8a66" - integrity sha512-ThiCWK7lhwmsipgjKkw8c0z0ubB9toRMV9X0tRVOXHHSknKp5DCFfatbCwjpSC5GZRa+61ciTSqJNtCc7j9YoQ== - "@adraffy/ens-normalize@1.10.0": version "1.10.0" resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== +"@axelar-network/axelar-gmp-sdk-solidity@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@axelar-network/axelar-gmp-sdk-solidity/-/axelar-gmp-sdk-solidity-5.8.0.tgz#449c6246b9f403af97a030b0a90b4f321c3a8a66" + integrity sha512-ThiCWK7lhwmsipgjKkw8c0z0ubB9toRMV9X0tRVOXHHSknKp5DCFfatbCwjpSC5GZRa+61ciTSqJNtCc7j9YoQ== + "@babel/code-frame@^7.0.0": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" @@ -1244,6 +1244,27 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@pnpm/config.env-replace@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" + integrity sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w== + +"@pnpm/network.ca-file@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz#2ab05e09c1af0cdf2fcf5035bea1484e222f7983" + integrity sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA== + dependencies: + graceful-fs "4.2.10" + +"@pnpm/npm-conf@^2.1.0": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz#0058baf1c26cbb63a828f0193795401684ac86f0" + integrity sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA== + dependencies: + "@pnpm/config.env-replace" "^1.1.0" + "@pnpm/network.ca-file" "^1.0.1" + config-chain "^1.1.11" + "@prettier/sync@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@prettier/sync/-/sync-0.3.0.tgz#91f2cfc23490a21586d1cf89c6f72157c000ca1e" @@ -1425,6 +1446,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== +"@sindresorhus/is@^5.2.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668" + integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== + "@solidity-parser/parser@^0.14.0": version "0.14.5" resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.5.tgz#87bc3cc7b068e08195c219c91cd8ddff5ef1a804" @@ -1681,7 +1707,7 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/http-cache-semantics@*": +"@types/http-cache-semantics@*", "@types/http-cache-semantics@^4.0.2": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== @@ -2172,6 +2198,11 @@ antlr4@^4.11.0: resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.13.1.tgz#1e0a1830a08faeb86217cb2e6c34716004e4253d" integrity sha512-kiXTspaRYvnIArgE97z5YVVf/cDVQABr3abFRR6mE7yesLMkgu4ujuyV/sgxafQ8wgve0DJQUJ38Z8tkgA2izA== +antlr4@^4.13.1-patch-1: + version "4.13.1-patch-1" + resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.13.1-patch-1.tgz#946176f863f890964a050c4f18c47fd6f7e57602" + integrity sha512-OjFLWWLzDMV9rdFhpvroCWR4ooktNg9/nvVYSA5z28wuVpU36QUNuioR1XLnQtcjVlf8npjyz593PxnU/f/Cow== + antlr4ts@^0.5.0-alpha.4: version "0.5.0-alpha.4" resolved "https://registry.yarnpkg.com/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz#71702865a87478ed0b40c0709f422cf14d51652a" @@ -2704,6 +2735,24 @@ cacheable-lookup@^6.0.4: resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-6.1.0.tgz#0330a543471c61faa4e9035db583aad753b36385" integrity sha512-KJ/Dmo1lDDhmW2XDPMo+9oiy/CeqosPguPCrgcVzKyZrL6pM1gU2GmPY/xo6OQPTUaA/c0kwHuywB4E6nmT9ww== +cacheable-lookup@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" + integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== + +cacheable-request@^10.2.8: + version "10.2.14" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-10.2.14.tgz#eb915b665fda41b79652782df3f553449c406b9d" + integrity sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ== + 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" + cacheable-request@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817" @@ -3158,6 +3207,14 @@ concat-stream@^1.6.0, concat-stream@^1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" +config-chain@^1.1.11: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + constant-case@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46" @@ -3421,7 +3478,7 @@ deep-eql@^4.1.3: dependencies: type-detect "^4.0.0" -deep-extend@~0.6.0: +deep-extend@^0.6.0, deep-extend@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== @@ -4625,6 +4682,11 @@ form-data-encoder@1.7.1: resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.1.tgz#ac80660e4f87ee0d3d3c3638b7da8278ddb8ec96" integrity sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg== +form-data-encoder@^2.1.2: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" + integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== + form-data@^2.2.0: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" @@ -5071,6 +5133,28 @@ got@^11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" +got@^12.1.0: + version "12.6.1" + resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" + integrity sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ== + 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" + +graceful-fs@4.2.10: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -5316,7 +5400,7 @@ http-basic@^8.1.1: http-response-object "^3.0.1" parse-cache-control "^1.0.1" -http-cache-semantics@^4.0.0: +http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== @@ -5452,7 +5536,7 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, i resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@^1.3.5: +ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -5939,6 +6023,13 @@ klaw@^1.0.0: optionalDependencies: graceful-fs "^4.1.9" +latest-version@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-7.0.0.tgz#843201591ea81a4d404932eeb61240fe04e9e5da" + integrity sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg== + dependencies: + package-json "^8.1.0" + lcid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" @@ -6393,6 +6484,11 @@ mimic-response@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== +mimic-response@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" + integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== + min-document@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" @@ -6531,6 +6627,11 @@ module-error@^1.0.1, module-error@^1.0.2: resolved "https://registry.yarnpkg.com/module-error/-/module-error-1.0.2.tgz#8d1a48897ca883f47a45816d4fb3e3c6ba404d86" integrity sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA== +moment@^2.30.1: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -6722,6 +6823,11 @@ normalize-url@^6.0.1: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== +normalize-url@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.1.tgz#9b7d96af9836577c58f5883e939365fa15623a4a" + integrity sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w== + npm-run-path@^5.1.0: version "5.3.0" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" @@ -6986,6 +7092,16 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-8.1.1.tgz#3e9948e43df40d1e8e78a85485f1070bf8f03dc8" + integrity sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA== + dependencies: + got "^12.1.0" + registry-auth-token "^5.0.1" + registry-url "^6.0.0" + semver "^7.3.7" + param-case@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" @@ -7281,6 +7397,11 @@ promise@^8.0.0: dependencies: asap "~2.0.6" +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -7407,6 +7528,16 @@ raw-body@2.5.2, raw-body@^2.4.1: iconv-lite "0.4.24" unpipe "1.0.0" +rc@1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -7491,6 +7622,20 @@ regexpp@^3.0.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== +registry-auth-token@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.2.tgz#8b026cc507c8552ebbe06724136267e63302f756" + integrity sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ== + dependencies: + "@pnpm/npm-conf" "^2.1.0" + +registry-url@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-6.0.1.tgz#056d9343680f2f64400032b1e199faa692286c58" + integrity sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q== + dependencies: + rc "1.2.8" + req-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/req-cwd/-/req-cwd-2.0.0.tgz#d4082b4d44598036640fb73ddea01ed53db49ebc" @@ -7599,6 +7744,13 @@ responselike@^2.0.0: dependencies: lowercase-keys "^2.0.0" +responselike@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-3.0.0.tgz#20decb6c298aff0dbee1c355ca95461d42823626" + integrity sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg== + dependencies: + lowercase-keys "^3.0.0" + restore-cursor@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" @@ -8079,10 +8231,10 @@ solc@^0.4.20: semver "^5.3.0" yargs "^4.7.1" -solhint-community@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/solhint-community/-/solhint-community-3.7.0.tgz#5d8bd4a2137d44dd636272edce93cb754184c09b" - integrity sha512-8nfdaxVll+IIaEBHFz3CzagIZNNTGp4Mrr+6O4m7c9Bs/L8OcgR/xzZJFwROkGAhV8Nbiv4gqJ42nEXZPYl3Qw== +solhint-community@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/solhint-community/-/solhint-community-4.0.0.tgz#4dba66932ff54ced426a8c035b7ceaa13a224f24" + integrity sha512-BERw3qYzkJE64EwvYrp2+iiTN8yAZOJ74FCiL4bTBp7v0JFUvRYCEGZKAqfHcfi/koKkzM6qThsJUceKm9vvfg== dependencies: "@solidity-parser/parser" "^0.16.0" ajv "^6.12.6" @@ -8112,14 +8264,14 @@ solhint-plugin-prettier@^0.1.0: "@prettier/sync" "^0.3.0" prettier-linter-helpers "^1.0.0" -solhint@^3.3.8: - version "3.6.2" - resolved "https://registry.yarnpkg.com/solhint/-/solhint-3.6.2.tgz#2b2acbec8fdc37b2c68206a71ba89c7f519943fe" - integrity sha512-85EeLbmkcPwD+3JR7aEMKsVC9YrRSxd4qkXuMzrlf7+z2Eqdfm1wHWq1ffTuo5aDhoZxp2I9yF3QkxZOxOL7aQ== +solhint@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-5.0.1.tgz#f0f783bd9d945e5a27b102295a3f28edba241d6c" + integrity sha512-QeQLS9HGCnIiibt+xiOa/+MuP7BWz9N7C5+Mj9pLHshdkNhuo3AzCpWmjfWVZBUuwIUO3YyCRVIcYLR3YOKGfg== dependencies: - "@solidity-parser/parser" "^0.16.0" + "@solidity-parser/parser" "^0.18.0" ajv "^6.12.6" - antlr4 "^4.11.0" + antlr4 "^4.13.1-patch-1" ast-parents "^0.0.1" chalk "^4.1.2" commander "^10.0.0" @@ -8128,6 +8280,7 @@ solhint@^3.3.8: 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" @@ -8414,6 +8567,11 @@ strip-json-comments@3.1.1, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + supports-color@8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"