Skip to content

Commit

Permalink
Multi-caller v2
Browse files Browse the repository at this point in the history
  • Loading branch information
hiep-immutable committed Aug 22, 2024
1 parent 8a86c1d commit e61e6ed
Showing 1 changed file with 271 additions and 0 deletions.
271 changes: 271 additions & 0 deletions contracts/multicall/GuardedMulticallerV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
// 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 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 Event emitted when execute function is called
event Multicalled(
address indexed _multicallSigner,
bytes32 indexed _reference,
address[] _targets,
bytes[] _data,
uint256 _deadline
);

/// @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 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, string functionSignature, 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, string _functionSignature);

/**
*
* @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);
}

/**
*
* @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));
}

/**
*
* @dev Returns hash of array of strings
*
* @param _data Array of strings
*/
function hashStringArray(string[] calldata _data) public pure returns (bytes32) {
bytes32[] memory hashedStringArr = new bytes32[](_data.length);
for (uint256 i = 0; i < _data.length; i++) {
hashedStringArr[i] = keccak256(bytes(_data[i]));
}
return keccak256(abi.encodePacked(hashedStringArr));
}

function stringBytes(string calldata _data) public pure returns (bytes memory) {
return bytes(_data);
}

/**
*
* @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 _functionSignatures List of function signatures
* @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,
string[] calldata _functionSignatures,
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 (_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, _functionSignatures, _data, _deadline),
_signature
)
) {
revert UnauthorizedSignature(_signature);
}

replayProtection[_reference] = true;

// Multicall
for (uint256 i = 0; i < _targets.length; i++) {
bytes4 functionSelector = bytes4(keccak256(bytes(_functionSignatures[i])));
bytes memory callData = abi.encodePacked(functionSelector, _data[i]);
// solhint-disable avoid-low-level-calls
// slither-disable-next-line calls-loop
(bool success, bytes memory returnData) = _targets[i].call(callData);
if (!success) {
if (returnData.length == 0) {
revert FailedCall(_targets[i], _functionSignatures[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 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,
string[] calldata _functionSignatures,
bytes[] calldata _data,
uint256 _deadline
) internal view returns (bytes32) {
return
_hashTypedDataV4(
keccak256(
abi.encode(
MULTICALL_TYPEHASH,
_reference,
keccak256(abi.encodePacked(_targets)),
hashStringArray(_functionSignatures),
hashBytesArray(_data),
_deadline
)
)
);
}
}

0 comments on commit e61e6ed

Please sign in to comment.