From 87ecebd6cd98a4f77f9f0f9af2fc893f0ff10081 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Wed, 27 May 2026 11:39:29 +1000 Subject: [PATCH] Re-add GuardedMulticaller --- contracts/multicall/GuardedMulticaller.sol | 297 +++++++++++++++ contracts/staking/StakeHolderBase.sol | 4 +- .../v2/ZoneAccessControl.sol | 4 +- test/multicall/GuardedMulticaller.t.sol | 354 ++++++++++++++++++ test/payment-splitter/PaymentSplitter.t.sol | 32 +- test/staking/StakeHolderTimeDelayBase.t.sol | 5 +- 6 files changed, 684 insertions(+), 12 deletions(-) create mode 100644 contracts/multicall/GuardedMulticaller.sol create mode 100644 test/multicall/GuardedMulticaller.t.sol diff --git a/contracts/multicall/GuardedMulticaller.sol b/contracts/multicall/GuardedMulticaller.sol new file mode 100644 index 00000000..69f68278 --- /dev/null +++ b/contracts/multicall/GuardedMulticaller.sol @@ -0,0 +1,297 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.19 <0.8.29; + +// 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/staking/StakeHolderBase.sol b/contracts/staking/StakeHolderBase.sol index 45bf8ec6..18c439b0 100644 --- a/contracts/staking/StakeHolderBase.sol +++ b/contracts/staking/StakeHolderBase.sol @@ -6,9 +6,7 @@ import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable-4/proxy/utils/ import { AccessControlEnumerableUpgradeable } from "openzeppelin-contracts-upgradeable-4/access/AccessControlEnumerableUpgradeable.sol"; -import { - ReentrancyGuardUpgradeable -} from "openzeppelin-contracts-upgradeable-4/security/ReentrancyGuardUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable-4/security/ReentrancyGuardUpgradeable.sol"; import {IStakeHolder} from "./IStakeHolder.sol"; /** diff --git a/contracts/trading/seaport/zones/immutable-signed-zone/v2/ZoneAccessControl.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v2/ZoneAccessControl.sol index 2748def2..72d600cd 100644 --- a/contracts/trading/seaport/zones/immutable-signed-zone/v2/ZoneAccessControl.sol +++ b/contracts/trading/seaport/zones/immutable-signed-zone/v2/ZoneAccessControl.sol @@ -6,9 +6,7 @@ pragma solidity >=0.8.19 <=0.8.27; import {AccessControl} from "openzeppelin-contracts-5/access/AccessControl.sol"; import {IAccessControl} from "openzeppelin-contracts-5/access/IAccessControl.sol"; import {AccessControlEnumerable} from "openzeppelin-contracts-5/access/extensions/AccessControlEnumerable.sol"; -import { - ZoneAccessControlEventsAndErrors -} from "./interfaces/ZoneAccessControlEventsAndErrors.sol"; +import {ZoneAccessControlEventsAndErrors} from "./interfaces/ZoneAccessControlEventsAndErrors.sol"; /** * @notice ZoneAccessControl encapsulates access control functionality for the zone. diff --git a/test/multicall/GuardedMulticaller.t.sol b/test/multicall/GuardedMulticaller.t.sol new file mode 100644 index 00000000..b5705a2c --- /dev/null +++ b/test/multicall/GuardedMulticaller.t.sol @@ -0,0 +1,354 @@ +// Copyright Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {GuardedMulticaller} from "../../contracts/multicall/GuardedMulticaller.sol"; +import {MockFunctions} from "./MockFunctions.sol"; +import {SigUtils} from "./SigUtils.t.sol"; + +contract GuardedMulticallerTest is Test { + GuardedMulticaller public gmc; + MockFunctions public mock; + SigUtils public sigUtils; + + event Multicalled( + address indexed _multicallSigner, + bytes32 indexed _reference, + address[] _targets, + bytes[] _data, + uint256 _deadline + ); + + event FunctionPermitted(address indexed _target, bytes4 _functionSelector, bool _permitted); + + address public deployer; + address public signer; + uint256 public signerPk; + address public user; + uint256 public userPk; + + string public constant MULTICALLER_NAME = "Multicaller"; + string public constant MULTICALLER_VERSION = "v1"; + + bytes32 public ref; + uint256 public deadline; + + function setUp() public { + deployer = makeAddr("deployer"); + (signer, signerPk) = makeAddrAndKey("signer"); + (user, userPk) = makeAddrAndKey("user"); + + vm.prank(deployer); + gmc = new GuardedMulticaller(deployer, MULTICALLER_NAME, MULTICALLER_VERSION); + vm.prank(deployer); + gmc.grantMulticallSignerRole(signer); + + sigUtils = new SigUtils(MULTICALLER_NAME, MULTICALLER_VERSION, address(gmc)); + + vm.prank(deployer); + mock = new MockFunctions(); + + GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](2); + functionPermits[0] = GuardedMulticaller.FunctionPermit({ + target: address(mock), functionSelector: MockFunctions.succeed.selector, permitted: true + }); + functionPermits[1] = GuardedMulticaller.FunctionPermit({ + target: address(mock), functionSelector: MockFunctions.revertWithNoReason.selector, permitted: true + }); + vm.prank(deployer); + gmc.setFunctionPermits(functionPermits); + + deadline = block.timestamp + 30 minutes; + ref = keccak256(abi.encodePacked("test_ref")); + } + + function test_SuccessfulExecution() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(MockFunctions.succeed.selector); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectEmit(true, true, true, true); + emit Multicalled(signer, ref, targets, data, deadline); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertWithCustomError() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("revertWithNoReason()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.FailedCall.selector, targets[0], data[0])); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfDeadlinePassed() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + uint256 expiredDeadline = block.timestamp; + vm.warp(expiredDeadline + 30 minutes); + bytes memory signature = signTypedData(signerPk, ref, targets, data, expiredDeadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.Expired.selector, expiredDeadline)); + gmc.execute(signer, ref, targets, data, expiredDeadline, signature); + } + + function test_RevertIfReferenceReused() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + gmc.execute(signer, ref, targets, data, deadline, signature); + + vm.prank(user); + vm.expectRevert(abi.encodePacked(GuardedMulticaller.ReusedReference.selector, ref)); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfInvalidReference() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes32 invalidRef = bytes32(0); + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.InvalidReference.selector, invalidRef)); + gmc.execute(signer, invalidRef, targets, data, deadline, signature); + } + + function test_RevertIfUnauthorizedSigner() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.UnauthorizedSigner.selector, user)); + // Note: execute called with user as signer. + gmc.execute(user, ref, targets, data, deadline, signature); + } + + function test_RevertIfSignatureMismatch() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(userPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.UnauthorizedSignature.selector, signature)); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfEmptyTargets() public { + address[] memory targets = new address[](0); + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.EmptyAddressArray.selector)); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfTargetsDataMismatch() public { + address[] memory targets = new address[](2); + targets[0] = address(mock); + targets[1] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + GuardedMulticaller.AddressDataArrayLengthsMismatch.selector, targets.length, data.length + ) + ); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfFunctionNotPermitted() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("notPermitted()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.UnauthorizedFunction.selector, targets[0], data[0])); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfFunctionDisallowed() public { + GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](1); + functionPermits[0] = GuardedMulticaller.FunctionPermit({ + target: address(mock), functionSelector: MockFunctions.succeed.selector, permitted: false + }); + vm.prank(deployer); + gmc.setFunctionPermits(functionPermits); + + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.UnauthorizedFunction.selector, targets[0], data[0])); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_RevertIfInvalidSignature() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes32 maliciousRef = keccak256(abi.encodePacked("malicious_ref")); + bytes memory signature = signTypedData(signerPk, maliciousRef, targets, data, deadline); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.UnauthorizedSignature.selector, signature)); + gmc.execute(signer, ref, targets, data, deadline, signature); + } + + function test_EmitFunctionPermittedEvent() public { + vm.startPrank(deployer); + GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](1); + functionPermits[0] = GuardedMulticaller.FunctionPermit({ + target: address(mock), functionSelector: MockFunctions.succeed.selector, permitted: true + }); + vm.expectEmit(true, true, true, true); + emit FunctionPermitted(address(mock), MockFunctions.succeed.selector, true); + gmc.setFunctionPermits(functionPermits); + + functionPermits[0] = GuardedMulticaller.FunctionPermit({ + target: address(mock), functionSelector: MockFunctions.succeed.selector, permitted: false + }); + vm.expectEmit(true, true, true, true); + emit FunctionPermitted(address(mock), MockFunctions.succeed.selector, false); + gmc.setFunctionPermits(functionPermits); + + vm.stopPrank(); + } + + function test_RevertIfEmptyFunctionPermits() public { + vm.prank(deployer); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.EmptyFunctionPermitArray.selector)); + gmc.setFunctionPermits(new GuardedMulticaller.FunctionPermit[](0)); + } + + function test_RevertIfAccessControlIssueWhileSettingFunctionPermits() public { + GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](1); + functionPermits[0] = GuardedMulticaller.FunctionPermit({ + target: address(mock), functionSelector: MockFunctions.succeed.selector, permitted: false + }); + vm.prank(user); + // Will be an access control error. + vm.expectRevert(); + gmc.setFunctionPermits(functionPermits); + } + + function test_RevertIfSetFunctionPermitsNonContract() public { + GuardedMulticaller.FunctionPermit[] memory functionPermits = new GuardedMulticaller.FunctionPermit[](1); + functionPermits[0] = GuardedMulticaller.FunctionPermit({ + target: deployer, functionSelector: MockFunctions.succeed.selector, permitted: true + }); + vm.prank(deployer); + vm.expectRevert(abi.encodeWithSelector(GuardedMulticaller.NonContractAddress.selector, deployer)); + gmc.setFunctionPermits(functionPermits); + } + + function test_RevertIfGrantRevokeSignerRoleWithInvalidRole() public { + vm.startPrank(user); + + vm.expectRevert(); + gmc.grantMulticallSignerRole(user); + + vm.expectRevert(); + gmc.revokeMulticallSignerRole(user); + + vm.stopPrank(); + } + + function test_HasBeenExecuted() public { + address[] memory targets = new address[](1); + targets[0] = address(mock); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSignature("succeed()"); + + bytes memory signature = signTypedData(signerPk, ref, targets, data, deadline); + + vm.prank(user); + gmc.execute(signer, ref, targets, data, deadline, signature); + + assertTrue(gmc.hasBeenExecuted(ref)); + + bytes32 invalidRef = keccak256(abi.encodePacked("invalid_ref")); + assertFalse(gmc.hasBeenExecuted(invalidRef)); + } + + function testIsFunctionPermitted() public { + assertTrue(gmc.isFunctionPermitted(address(mock), MockFunctions.succeed.selector)); + assertTrue(gmc.isFunctionPermitted(address(mock), MockFunctions.revertWithNoReason.selector)); + assertFalse(gmc.isFunctionPermitted(address(mock), MockFunctions.notPermitted.selector)); + } + + function testHashBytesArray() public { + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSignature("succeed()"); + data[1] = abi.encodeWithSignature("notSucceed()"); + assertEq(sigUtils.hashBytesArray(data), gmc.hashBytesArray(data)); + } + + function signTypedData( + uint256 _signerPk, + bytes32 _reference, + address[] memory _targets, + bytes[] memory _data, + uint256 _deadline + ) public view returns (bytes memory) { + bytes32 digest = sigUtils.hashTypedData(_reference, _targets, _data, _deadline); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPk, digest); + return abi.encodePacked(r, s, v); + } +} diff --git a/test/payment-splitter/PaymentSplitter.t.sol b/test/payment-splitter/PaymentSplitter.t.sol index 2c32ede2..f0e357b5 100644 --- a/test/payment-splitter/PaymentSplitter.t.sol +++ b/test/payment-splitter/PaymentSplitter.t.sol @@ -76,18 +76,42 @@ contract PaymentSplitterTest is Test { } function testInvalidPermissions() public { - vm.expectRevert(abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, defaultAdmin, paymentSplitter.TOKEN_REGISTRAR_ROLE())); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + defaultAdmin, + paymentSplitter.TOKEN_REGISTRAR_ROLE() + ) + ); vm.prank(defaultAdmin); paymentSplitter.addToAllowlist(erc20s); vm.startPrank(registrarAdmin); - vm.expectRevert(abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, registrarAdmin, paymentSplitter.DEFAULT_ADMIN_ROLE())); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + registrarAdmin, + paymentSplitter.DEFAULT_ADMIN_ROLE() + ) + ); paymentSplitter.overridePayees(payees, shares); - vm.expectRevert(abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, registrarAdmin, paymentSplitter.DEFAULT_ADMIN_ROLE())); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + registrarAdmin, + paymentSplitter.DEFAULT_ADMIN_ROLE() + ) + ); paymentSplitter.revokeReleaseFundsRole(fundsAdmin); - vm.expectRevert(abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, registrarAdmin, paymentSplitter.RELEASE_FUNDS_ROLE())); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + registrarAdmin, + paymentSplitter.RELEASE_FUNDS_ROLE() + ) + ); paymentSplitter.releaseAll(); vm.stopPrank(); } diff --git a/test/staking/StakeHolderTimeDelayBase.t.sol b/test/staking/StakeHolderTimeDelayBase.t.sol index 27c7d35a..321567bc 100644 --- a/test/staking/StakeHolderTimeDelayBase.t.sol +++ b/test/staking/StakeHolderTimeDelayBase.t.sol @@ -134,8 +134,9 @@ abstract contract StakeHolderTimeDelayBaseTest is StakeHolderBaseTest { /// forge-lint: disable-next-line(incorrect-shift) bytes32 stateMask = bytes32(1 << uint8(TimelockController.OperationState.Ready)); - vm.expectRevert(abi.encodeWithSelector(TimelockController.TimelockUnexpectedOperationState.selector, operationId, - stateMask)); + vm.expectRevert( + abi.encodeWithSelector(TimelockController.TimelockUnexpectedOperationState.selector, operationId, stateMask) + ); vm.warp(timeNow + delay - 1); // Too early vm.prank(adminExecutor); stakeHolderTimeDelay.execute(target, value, data, predecessor, salt);